1<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>Customer Login</title><meta name="viewport" content="width=device-width, initial-scale=1" />
1<script> window.dataLayer = window.dataLayer || []; 2window.zGA4 = function(eventName, params) { 3if (params && params.ecommerce) { 4dataLayer.push({ ecommerce: null }); 5} 6params = params || {}; 7params.event = eventName; 8dataLayer.push(params); 9}; </script>
9
9<script> (function(){ 10try { 11var m = document.cookie.match(/(?:^|; )zephyr_ud=([^;]*)/); 12if (!m) return; 13var ud = JSON.parse(decodeURIComponent(m[1])); 14if (ud && Object.keys(ud).length) { 15window.dataLayer = window.dataLayer || []; 16window.dataLayer.push({ event: 'set_user_data', user_data: ud }); 17} 18} catch (e) {} 19})(); </script>
19 <style> [x-cloak] { display: none !important; } 20/* Sticky header spacing - responsive estimates, refined by JS in header.phtml */ 21:root { 22--header-height: 65px; 23} 24@media (min-width: 1024px) { 25:root { 26--header-height: 97px; 27} 28} </style><meta name="description" content="Order today, get next business day shipping on custom stamps like self-inking, pre-inked rubber stamps, embossers, office and address stamps. We've customized rubber stamps since 1954 and offer stamps for business, professional and craft." /><meta name="robots" content="INDEX,FOLLOW" /><link rel="icon" href="https://cdn.simplystamps.com/media/favicon/websites/5/SS-Icon-Logo.png" type="image/x-icon" /><link rel="shortcut icon" href="https://cdn.simplystamps.com/media/favicon/websites/5/SS-Icon-Logo.png" type="image/x-icon" /><link rel="dns-prefetch" href="//cdn.simplystamps.com"><meta property="og:title" content="Customer Login" /><meta property="og:type" content="article" /><meta property="og:url" content="https://www.simplystamps.com/customer/account/login/" /><meta property="og:description" content="Order today, get next business day shipping on custom stamps like self-inking, pre-inked rubber stamps, embossers, office and address stamps. We've customized rubber stamps since 1954 and offer stamps for business, professional and craft." /><meta property="og:site_name" content="Simply Stamps" /><meta property="og:image" content="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" /><link rel="stylesheet" href="https://cdn.simplystamps.com/skin/frontend/zephyr/default/css/print.css?v1126" media="print" >
28<script type="text/javascript"> //<![CDATA[ 29// mage/cookies.js now loads via a deferred
29<script> tag (see head.phtml), so Mage.Cookies 30// isn't guaranteed to exist yet at this point in parsing. Wait for DOMContentLoaded, which 31// fires only after deferred scripts have executed. 32document.addEventListener('DOMContentLoaded', function () { 33Mage.Cookies.path = '/'; 34Mage.Cookies.domain = '.www.simplystamps.com'; 35}); 36//]]> </script>
36<link rel="stylesheet" href="https://cdn.simplystamps.com/skin/frontend/zephyr/default/css/dist/theme.css?v=1126">
36<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 37new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 38j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 39'https://www.simplystamps.com/d/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 40})(window,document,'script','dataLayer','GTM-TT325PX9');</script>
40 <noscript><iframe src="https://www.simplystamps.com/d/ns.html?id=GTM-TT325PX9" 41height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
41<script type="text/javascript" src="https://chimpstatic.com/mcjs-connected/js/users/de94d9b4a6aa148b052eb1fad/fac024488a3ce12a438eb7e65.js" defer></script>
41<style id="zephyr-brand-colors"> /* Zephyr Brand Colors - Store: sstamps_en */ 42:root { 43--brand-primary: #002E49; 44--brand-primary-hover: #00456E; 45--brand-secondary: #55B5E3; 46--brand-secondary-hover: #22495C; 47--brand-success: #19A745; 48--brand-warning: #C27803; 49--brand-danger: #E02424; 50--brand-info: #3F83F8; 51--blog-bg: #FFFFFF; 52} </style>
52<script>if(typeof Translate==='undefined'){var Translate=function(d){this._data=d||{}};Translate.prototype={translate:function(t){return this._data[t]||t},add:function(){}}}</script>
52
52<script type="text/javascript">//<![CDATA[ 53var Translator = new Translate([]); 54//]]></script>
54<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7201785794839578" 55crossorigin="anonymous"></script>
55
55<script src="https://cdn.simplystamps.com/js/mage/cookies.js" defer></script>
55
55<script type="text/javascript" src="https://onsite.optimonk.com/script.js?account=25642" async> </script>
55</head><body class="customer-account-login"><div class="flex min-h-screen flex-col"> <div class="flex flex-1 flex-col"><div x-data="stickyHeader()" x-init="init()" class="relative"><div 56x-show="mobileMenuOpen" 57@click="mobileMenuOpen = false" 58x-transition:enter="transition-opacity ease-out duration-300" 59x-transition:enter-start="opacity-0" 60x-transition:enter-end="opacity-100" 61x-transition:leave="transition-opacity ease-in duration-200" 62x-transition:leave-start="opacity-100" 63x-transition:leave-end="opacity-0" 64class="fixed inset-0 bg-black bg-opacity-50 z-40" 65style="display: none;" 66></div><div 67x-show="$store.imagePreview.isOpen" 68x-cloak 69@keydown.escape.window="$store.imagePreview.close()" 70class="fixed inset-0 z-[100] flex items-center justify-center p-4" 71style="display: none;" 72><div 73@click.stop="$store.imagePreview.close()" 74x-transition:enter="transition-opacity ease-out duration-300" 75x-transition:enter-start="opacity-0" 76x-transition:enter-end="opacity-100" 77x-transition:leave="transition-opacity ease-in duration-200" 78x-transition:leave-start="opacity-100" 79x-transition:leave-end="opacity-0" 80class="absolute inset-0 bg-black bg-opacity-80" 81></div><div 82x-transition:enter="transition ease-out duration-300" 83x-transition:enter-start="opacity-0 scale-95" 84x-transition:enter-end="opacity-100 scale-100" 85x-transition:leave="transition ease-in duration-200" 86x-transition:leave-start="opacity-100 scale-100" 87x-transition:leave-end="opacity-0 scale-95" 88class="relative w-full max-w-3xl max-h-[90vh]" 89> <button 90@click.stop="$store.imagePreview.close()" 91class="absolute -top-10 right-0 p-2 text-white hover:text-gray-300 transition-colors" 92aria-label="Close preview" 93> <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button> <img 94:src="$store.imagePreview.imageUrl" 95:alt="$store.imagePreview.imageAlt" 96class="max-w-full max-h-[85vh] mx-auto object-contain rounded-lg shadow-2xl bg-white" 97/> <template x-if="$store.imagePreview.imageAlt && $store.imagePreview.linkUrl"> <a 98:href="$store.imagePreview.linkUrl" 99x-text="$store.imagePreview.imageAlt" 100class="block text-center text-white text-sm mt-3 font-medium hover:underline transition-colors" 101></a> </template> <template x-if="$store.imagePreview.imageAlt && !$store.imagePreview.linkUrl"><p 102x-text="$store.imagePreview.imageAlt" 103class="text-center text-white text-sm mt-3 font-medium" 104></p> </template></div></div><div 105x-show="mobileMenuOpen" 106@click.away="mobileMenuOpen = false" 107x-transition:enter="transform transition ease-out duration-300" 108x-transition:enter-start="-translate-x-full" 109x-transition:enter-end="translate-x-0" 110x-transition:leave="transform transition ease-in duration-200" 111x-transition:leave-start="translate-x-0" 112x-transition:leave-end="-translate-x-full" 113class="fixed inset-0 bg-white z-[60] flex flex-col" 114style="display: none;" 115><div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 shrink-0"> <a href="https://www.simplystamps.com/" class="block"> <img 116src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 117alt="Simply Stamps" 118loading="eager" 119class="h-8 w-auto" 120/> </a> <button 121@click="mobileMenuOpen = false" 122class="p-2 hover:bg-gray-100 rounded-lg transition-colors" 123aria-label="Close menu" 124> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M18 6L6 18" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 6L18 18" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button></div><div class="flex-1 overflow-y-auto py-4"> <button 125@click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); $store.search.open()" 126class="flex items-center gap-3 w-[calc(100%-2rem)] mx-4 px-4 py-3 border-2 border-gray-300 rounded-xl text-gray-400 hover:border-brand-primary hover:text-gray-500 transition-colors" 127> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 shrink-0"> <path d="M11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19Z" 128stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="text-base">Search products...</span> </button><div x-init="loadMobileMenu()" x-html="mobileMenuContent"></div><div class="mt-4 pt-4" x-show="$store.auth.isLoggedIn" x-cloak style="display: none;"><h3 class="text-sm font-bold text-gray-700 uppercase tracking-wider px-4 mb-2">Account</h3> <a href="https://www.simplystamps.com/customer/account/" 129class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/> </svg> My Account </a> <a href="https://www.simplystamps.com/sales/order/history/" 130class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/> </svg> My Orders </a> <a x-show="$store.auth.companyStoreUrl" 131:href="$store.auth.companyStoreUrl" 132class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors" 133style="display: none;"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/> </svg> Company Store </a> <button @click="mobileMenuOpen = false;
134if (typeof window.zGA4 === 'function') { window.zGA4('logout', { method: 'site' }); } 135$store.auth.refreshMessage = 'Logging out...'; 136$store.auth.showRefreshOverlay = true; 137window.location.href = 'https://www.simplystamps.com/customer/account/logout/';" 138class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-500 hover:text-red-600 hover:bg-gray-100 transition-colors w-full mt-1"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 139d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/> </svg> Log Out </button></div><div class="mt-4 pt-4"><h3 class="text-sm font-bold text-gray-700 uppercase tracking-wider px-4 mb-2">Help</h3> <button x-show="$store.chat.available" x-cloak id="mobile-live-chat-now" class="flex items-center gap-3 px-4 py-2.5 text-base font-medium text-brand-primary hover:bg-gray-100 transition-colors w-full cursor-pointer" style="display: none;"> <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> <span class="text-left"> Chat with a Live Agent <span class="block text-sm text-brand-success font-medium">Available now</span> <span class="block text-sm text-gray-500" x-text="$store.chat.hoursLabel"></span> </span> </button> <a href="https://www.simplystamps.com/contacts" 140class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/> </svg> Track My Order </a> <a href="https://www.simplystamps.com/contacts" 141class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> Contact Us </a> <a href="https://www.simplystamps.com/faq" 142class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> FAQ </a></div></div><div class="p-4 border-t border-gray-200 shrink-0" x-show="$store.auth.ready && !$store.auth.isLoggedIn" style="display: none;"> <button @click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); mobileMenuOpen = false; setTimeout(() => { $store.auth._loginOpenedAt = Date.now(); $store.auth.showLoginModal = true; }, 100)" 143class="w-full flex items-center justify-center space-x-2 px-4 py-3 bg-brand-primary hover:bg-brand-primary-hover text-white font-medium rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5"> <path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" 144stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" 145stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span>Log In</span> </button></div></div> <header 146:class="{ 147'-translate-y-full': !headerVisible || $store.ui.hideHeader 148}" 149class="fixed top-0 left-0 right-0 bg-white z-50 shadow-lg transition-all duration-300" 150><div class="lg:hidden border-b border-gray-200"><div class="container mx-auto px-4"><div class="flex items-center justify-between py-3"><div class="flex items-center"> <button 151@click="mobileMenuOpen = !mobileMenuOpen" 152class="p-2 hover:bg-gray-100 rounded-lg transition-colors" 153aria-label="Menu" 154> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6"> <path d="M3 12H21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3 6H21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3 18H21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button></div><div class="flex-1 flex justify-center"> <a href="https://www.simplystamps.com/" class="block"> <img 155src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 156alt="Simply Stamps" 157loading="eager" 158class="h-10 w-auto" 159/> </a></div><div class="flex items-center"> <button 160@click="$store.cart.openDrawer()" 161@dblclick.prevent="window.location.href='/checkout/cart/'" 162class="relative p-2 hover:bg-gray-100 rounded-lg transition-colors" 163aria-label="Open cart" 164> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6"> <path d="M9 22C9.55228 22 10 21.5523 10 21C10 20.4477 9.55228 20 9 20C8.44772 20 8 20.4477 8 21C8 21.5523 8.44772 22 9 22Z" fill="#4F4F4F" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20 22C20.5523 22 21 21.5523 21 21C21 20.4477 20.5523 20 20 20C19.4477 20 19 20.4477 19 21C19 21.5523 19.4477 22 20 22Z" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M1 1H5L7.68 14.39C7.77144 14.8504 8.02191 15.264 8.38755 15.5583C8.75318 15.8526 9.2107 16.009 9.68 16H19.4C19.8693 16.009 20.3268 15.8526 20.6925 15.5583C21.0581 15.264 21.3086 14.8504 21.4 14.39L23 6H6" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span x-cloak 165x-show="$store.cart.itemCount > 0" 166x-text="$store.cart.itemCount" 167class="absolute -top-1 -right-1 bg-brand-danger text-white text-xs rounded-full h-5 flex items-center justify-center font-bold" 168:class="$store.cart.itemCount >= 10 ? 'min-w-[1.25rem] px-1' : 'w-5'"></span> </button></div></div><div 169x-show="mobileSearchOpen" 170x-transition:enter="transition ease-out duration-200" 171x-transition:enter-start="opacity-0 -translate-y-2" 172x-transition:enter-end="opacity-100 translate-y-0" 173x-transition:leave="transition ease-in duration-150" 174x-transition:leave-start="opacity-100 translate-y-0" 175x-transition:leave-end="opacity-0 -translate-y-2" 176class="pb-4" 177style="display: none;" 178><form class="searchautocomplete UI-SEARCHAUTOCOMPLETE" action="https://www.simplystamps.com/catalogsearch/result/" method="get" 179data-tip="Search entire store here..." 180data-url="//www.simplystamps.com/searchautocomplete/ajax/get/" 181data-minchars="3" 182data-delay="500"
183data-hide-delay="1000"><label for="search">Search</label><div class="nav"><div class="nav-search-in"> <span class="category-fake UI-CATEGORY-TEXT">All</span> <span class="nav-down-arrow"></span> <select name="cat" class="category UI-CATEGORY"><option value="0">All</option><option value="713" > Address Stamps</option><option value="958" > Custom Business & Office Stamps</option><option value="749" > Craft Stamps</option><option value="1387" > Custom Logo Stamps</option><option value="770" > Embossers</option><option value="802" > Stamp Ink & Pads</option><option value="811" > Teacher Stamps</option><option value="793" > Notary</option><option value="815" > Wedding Stamps</option><option value="1432" > Redeemable Personalized Address Stamp with Gift Box</option><option value="2291" > St. Patrick's Day & Irish Theme Stamps</option><option value="2391" > Stamp of the Week</option><option value="809" > Breast Cancer Awareness</option><option value="3035" > Gift Notes</option><option value="3073" > Real Estate Supplies</option><option value="3085" > Custom Rubber Stamps</option><option value="3094" > Custom Return Address Labels</option><option value="3109" > Here Tomorrow</option><option value="3118" > StampToons</option><option value="3327" > Custom Face Stamps</option><option value="3432" > Salt & Paper Collaboration</option> </select></div><div class="nav-input UI-NAV-INPUT"> <input class="input-text UI-SEARCH" type="text" autocomplete="off" name="q" value="" maxlength="128" /></div><div class="searchautocomplete-loader UI-LOADER"><div id="g01"></div><div id="g02"></div><div id="g03"></div><div id="g04"></div><div id="g05"></div><div id="g06"></div><div id="g07"></div><div id="g08"></div></div></div><div class="nav-submit-button"> <button type="submit" title="Go" class="button">Go</button></div> <style> .store-4-search-autocomplete { 184top: 108px !important; 185right: unset !important; 186}@media (min-width: 768px) { 187.store-4-search-autocomplete { 188top: 74px !important; 189right: unset !important; 190} 191} </style><div style="display:none" class="searchautocomplete-placeholder UI-PLACEHOLDER "></div></form></div></div></div><div class="hidden lg:block border-b border-gray-200"><div class="mx-8"><div class="flex justify-between py-4"><div class="flex items-center justify-center w-1/8"> <a href="https://www.simplystamps.com/"> <img 192src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 193alt="Simply Stamps" 194loading="eager" 195class="h-12 w-auto" 196/> </a></div> <nav class="relative flex-1 px-2 md:px-4 flex items-center justify-around"><div class="hidden md:block"> <nav class="relative"><ul class="flex items-center justify-center space-x-2 list-none p-0 m-0 flex-wrap"><li x-data="{ open: false, hoverTimeout: null }" 197@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 198@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 199@keydown.escape="open = false" 200class="static"> <a href="https://www.simplystamps.com/address-stamps/view-all" 201:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 202class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Address Stamps </a><div x-show="open" style="display: none;" 203x-transition:enter="transition ease-out duration-150" 204x-transition:enter-start="opacity-0 -translate-y-1" 205x-transition:enter-end="opacity-100 translate-y-0" 206x-transition:leave="transition ease-in duration-100" 207x-transition:leave-start="opacity-100 translate-y-0" 208x-transition:leave-end="opacity-0 -translate-y-1" 209class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 210defaultHref: '/address-stamps', 211defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 212defaultLabel: 'Address Stamps Collection', 213activeHref: '/address-stamps', 214activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 215activeLabel: 'Address Stamps Collection', 216set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 217reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 218}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/address-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg" alt="Address Stamps Collection" :src="activeImage" :alt="activeLabel" 219loading="lazy" width="224" height="224" 220class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 221x-text="activeLabel">Address Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-4 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Style</h3><ul class="space-y-2.5"><li><a href="/address-stamps/script-handwritten" 222@mouseenter="set('/address-stamps/script-handwritten', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Script-AS-224x224.jpg', 'Script Address Stamps')" 223class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Script Address Stamps</a></li><li><a href="/address-stamps/custom-line" 224@mouseenter="set('/address-stamps/custom-line', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Multiple-Line-A
224S-224x224.jpg', 'Multiple Line Address Stamps')" 225class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Multiple Line Address Stamps</a></li><li><a href="/address-stamps/round-address-stamps" 226@mouseenter="set('/address-stamps/round-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-AS-224x224.jpg', 'Round Address Stamps')" 227class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Address Stamps</a></li><li><a href="/address-stamps/rectangle-address-stamps" 228@mouseenter="set('/address-stamps/rectangle-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-AS-224x224.jpg', 'Rectangle Address Stamps')" 229class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Address Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 230@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Text-AS-224x224.jpg', 'Line Address Stamps')" 231class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Line Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Design</h3><ul class="space-y-2.5"><li><a href="/address-stamps/floral" 232@mouseenter="set('/address-stamps/floral', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Floral-AS-224x224.jpg', 'Floral Address Stamps')" 233class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Floral Address Stamps</a></li><li><a href="/address-stamps/wedding-couple" 234@mouseenter="set('/address-stamps/wedding-couple', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wedding-AS-224x224.jpg', 'Wedding Address Stamps')" 235class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Address Stamps</a></li><li><a href="/address-stamps/dog-address-stamps" 236@mouseenter="set('/address-stamps/dog-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Dog-AS-224x224.jpg', 'Dog Address Stamps')" 237class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Dog Address Stamps</a></li><li><a href="/address-stamps/animals" 238@mouseenter="set('/address-stamps/animals', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Animal-AS-224x224.jpg', 'Animal Address Stamps')" 239class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Animal Address Stamps</a></li><li><a href="/address-stamps/hearts" 240@mouseenter="set('/address-stamps/hearts', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Whimsical-AS-224x224.jpg', 'Whimsical Address Stamps')" 241class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Whimsical Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Seasonal</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/business-address-stamps" 242@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Business-AS-224x224.jpg', 'Business Address Stamps')" 243class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/address-stamps/patriotic-address-stamps" 244@mouseenter="set('/address-stamps/patriotic-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Patriotic-AS-224x224.jpg', 'Patriotic Address Stamps')" 245class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Patriotic Address Stamps</a></li><li><a href="/address-stamps/holiday" 246@mouseenter="set('/address-stamps/holiday', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Holiday-AS-224x224.jpg', 'Holiday Address Stamps')" 247class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Address Stamps</a></li><li><a href="/address-stamps/christmas-address-stamps" 248@mouseenter="set('/address-stamps/christmas-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Christmas-AS-224x224.jpg', 'Christmas Address Stamps')" 249class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Christmas Address Stamps</a></li><li><a href="/custom-address-labels" 250@mouseenter="set('/custom-address-labels', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/address-stamps/SS-QCS-Address-Labels-AS-400x400.jpg', 'Custom Return Address Labels')" 251class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Return Address Labels</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Collabs</h3><ul class="space-y-2.5"><li><a href="/salt-and-paper" 252@mouseenter="set('/salt-and-paper', 'https://cdn.simplystamps.com/media/catalog/category/8-simply-stamps-spco-2026-AS-Salt-Wine-1-1-lifestyle-1-2000_1.jpg', 'Salt & Paper Collaboration')" 253style='background-image:url(https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/CMSImages/SS_Nav-Salt-and-Paper-Swish.png); background-repeat:no-repeat; background-position:left center; background-size:100% 100%; padding:6px 10px; margin:0 -10px;' 254class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Salt & Paper Collaboration</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/address-stamps/view-all" @mouseenter="reset()" 255class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Address Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
255<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 256@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 257@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 258@keydown.escape="open = false" 259class="static"> <a href="https://www.simplystamps.com/rubber-logo-stamps" 260:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 261class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Logo Stamps </a><div x-show="open" style="display: none;" 262x-transition:enter="transition ease-out duration-150" 263x-transition:enter-start="opacity-0 -translate-y-1" 264x-transition:enter-end="opacity-100 translate-y-0" 265x-transition:leave="transition ease-in duration-100" 266x-transition:leave-start="opacity-100 translate-y-0" 267x-transition:leave-end="opacity-0 -translate-y-1" 268class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 269defaultHref: '/rubber-logo-stamps', 270defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 271defaultLabel: 'Logo Stamps Collection', 272activeHref: '/rubber-logo-stamps', 273activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 274activeLabel: 'Logo Stamps Collection', 275set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 276reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 277}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/rubber-logo-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg" alt="Logo Stamps Collection" :src="activeImage" :alt="activeLabel" 278loading="lazy" width="224" height="224" 279class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 280x-text="activeLabel">Logo Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Shape</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/rectangle" 281@mouseenter="set('/rubber-logo-stamps/rectangle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-Logo.jpg', 'Rectangle Logo Stamp')" 282class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Logo Stamp</a></li><li><a href="/rubber-logo-stamps/round" 283@mouseenter="set('/rubber-logo-stamps/round', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-Logo-2.jpg', 'Round Logo Stamp')" 284class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Logo Stamp</a></li><li><a href="/rubber-logo-stamps/square"
285@mouseenter="set('/rubber-logo-stamps/square', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Square-Logo.jpg', 'Square Logo Stamp')" 286class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Square Logo Stamp</a></li><li><a href="/rubber-logo-stamps/catalog-stamps" 287@mouseenter="set('/rubber-logo-stamps/catalog-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Catalog-Logo.jpg', 'Catalog Stamps')" 288class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Catalog Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Artwork</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/with-text" 289@mouseenter="set('/rubber-logo-stamps/with-text', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Logo-and-Text.jpg', 'Logo & Text')" 290class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo & Text</a></li><li><a href="/rubber-logo-stamps/upload-art" 291@mouseenter="set('/rubber-logo-stamps/upload-art', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Artwork-Logo.jpg', 'Personalized Art')" 292class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Personalized Art</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/pre-inked-logo" 293@mouseenter="set('/rubber-logo-stamps/pre-inked-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Pre-Inked-Logo.jpg', 'Pre-Inked')" 294class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Pre-Inked</a></li><li><a href="/rubber-logo-stamps/self-inking" 295@mouseenter="set('/rubber-logo-stamps/self-inking', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Self-Ink-Logo.jpg', 'Self-Inking')" 296class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Self-Inking</a></li><li><a href="/rubber-logo-stamps/traditional-wooden-handle" 297@mouseenter="set('/rubber-logo-stamps/traditional-wooden-handle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wooden-Logo.jpg', 'Wood Handle')" 298class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wood Handle</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/rubber-logo-stamps" @mouseenter="reset()" 299class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Logo Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 300@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 301@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 302@keydown.escape="open = false" 303class="static"> <a href="https://www.simplystamps.com/business-office-stamps/signature-stamps" 304:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 305class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Signature Stamps </a><div x-show="open" style="display: none;" 306x-transition:enter="transition ease-out duration-150" 307x-transition:enter-start="opacity-0 -translate-y-1" 308x-transition:enter-end="opacity-100 translate-y-0" 309x-transition:leave="transition ease-in duration-100" 310x-transition:leave-start="opacity-100 translate-y-0" 311x-transition:leave-end="opacity-0 -translate-y-1" 312class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 313defaultHref: '/business-office-stamps/signature-stamps', 314defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 315defaultLabel: 'Signature Stamps Collection', 316activeHref: '/business-office-stamps/signature-stamps', 317activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 318activeLabel: 'Signature Stamps Collection', 319set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 320reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 321}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps/signature-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg" alt="Signature Stamps Collection" :src="activeImage" :alt="activeLabel" 322loading="lazy" width="224" height="224" 323class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 324x-text="activeLabel">Signature Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature Stamp Types</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/signature-only" 325@mouseenter="set('/business-office-stamps/signature-stamps/signature-only', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-only.jpg', 'Signature Only')" 326class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature Only</a></li><li><a href="/business-office-stamps/signature-stamps/with-date"
327@mouseenter="set('/business-office-stamps/signature-stamps/with-date', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-date.jpg', 'Signature with Date')" 328class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Date</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature & More</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/with-printed-name" 329@mouseenter="set('/business-office-stamps/signature-stamps/with-printed-name', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-name.jpg', 'Signature with Printed Name')" 330class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Printed Name</a></li><li><a href="/business-office-stamps/signature-stamps/with-logo" 331@mouseenter="set('/business-office-stamps/signature-stamps/with-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-logo.jpg', 'Signature with Logo')" 332class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Logo</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps/signature-stamps" @mouseenter="reset()" 333class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Signature Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 334@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 335@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 336@keydown.escape="open = false" 337class="static"> <a href="https://www.simplystamps.com/custom-rubber-stamps" 338:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 339class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Custom Stamps </a><div x-show="open" style="display: none;" 340x-transition:enter="transition ease-out duration-150" 341x-transition:enter-start="opacity-0 -translate-y-1" 342x-transition:enter-end="opacity-100 translate-y-0" 343x-transition:leave="transition ease-in duration-100" 344x-transition:leave-start="opacity-100 translate-y-0" 345x-transition:leave-end="opacity-0 -translate-y-1" 346class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 347defaultHref: '/custom-rubber-stamps', 348defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 349defaultLabel: 'Custom Stamps Collection', 350activeHref: '/custom-rubber-stamps', 351activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 352activeLabel: 'Custom Stamps Collection', 353set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 354reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 355}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/custom-rubber-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg" alt="Custom Stamps Collection" :src="activeImage" :alt="activeLabel" 356loading="lazy" width="224" height="224" 357class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 358x-text="activeLabel">Custom Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-self-inking-stamps" 359@mouseenter="set('/custom-rubber-stamps/custom-self-inking-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Self-Inking-Stamps-Custom-400x400.jpg', 'Custom Self-Inking Stamps')" 360class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Self-Inking Stamps</a></li><li><a href="/custom-rubber-stamps/custom-pre-inked-stamps" 361@mouseenter="set('/custom-rubber-stamps/custom-pre-inked-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Pre-Inked-Stamps-Custom-400x400.jpg', 'Custom Pre-Inked Stamps')" 362class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Pre-Inked Stamps</a></li><li><a href="/custom-rubber-stamps/custom-wood-handle-stamps" 363@mouseenter="set('/custom-rubber-stamps/custom-wood-handle-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Traditional-Hand-Stamps-Custom-400x400.jpg', 'Custom Wood Handle Stamps')" 364class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Wood Handle Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Specialty</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-european-hand-stamps" 365@mouseenter="set('/custom-rubber-stamps/custom-european-hand-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-European-Hand-Stamps-Custom-400x400.jpg', 'Custom European Hand Stamps')" 366class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom European Hand Stamps</a></li><li><a href="/custom-rubber-stamps/custom-professional-stamps" 367@mouseenter="set('/custom-rubber-stamps/custom-professional-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Professional-Stamps-Custom-400x400.jpg', 'Custom Professional Stamps')" 368class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Professional Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/custom-rubber-stamps" @mouseenter="reset()" 369class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Custom Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
369<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 370@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 371@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 372@keydown.escape="open = false" 373class="static"> <a href="https://www.simplystamps.com/business-office-stamps" 374:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 375class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Office Stamps </a><div x-show="open" style="display: none;" 376x-transition:enter="transition ease-out duration-150" 377x-transition:enter-start="opacity-0 -translate-y-1" 378x-transition:enter-end="opacity-100 translate-y-0" 379x-transition:leave="transition ease-in duration-100" 380x-transition:leave-start="opacity-100 translate-y-0" 381x-transition:leave-end="opacity-0 -translate-y-1" 382class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 383defaultHref: '/business-office-stamps', 384defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 385defaultLabel: 'Office Stamps Collection', 386activeHref: '/business-office-stamps', 387activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 388activeLabel: 'Office Stamps Collection', 389set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 390reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 391}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg" alt="Office Stamps Collection" :src="activeImage" :alt="activeLabel" 392loading="lazy" width="224" height="224" 393class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 394x-text="activeLabel">Office Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Banking</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/accounting-tax-stamps" 395@mouseenter="set('/business-office-stamps/accounting-tax-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Accounting-Stamps-OS-400x400.jpg', 'Accounting Stamps')" 396class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Accounting Stamps</a></li><li><a href="/business-office-stamps/business-address-stamps" 397@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Business-Address-Stamps-OS-400x400.jpg', 'Business Address Stamps')" 398class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement" 399@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Check-Endorsement-Stamps-OS-400x400.jpg', 'Check Endorsement Stamps')" 400class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Check Endorsement Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps" 401@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Deposit-Stamps-OS-400x400.jpg', 'Deposit Stamps')" 402class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Deposit Stamps</a></li><li><a href="/business-office-stamps/accounting-tax-stamps/received" 403@mouseenter="set('/business-office-stamps/accounting-tax-stamps/received', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Received-stamps-OS-400x400.jpg', 'Received Stamps')" 404class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Received Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 405@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Custom-text-Stamps-OS-400x400.jpg', 'Custom Text Stamps')" 406class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Text Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Office Essentials</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/office-stamps" 407@mouseenter="set('/business-office-stamps/office-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Stock-office-stamps-OS-400x400.jpg', 'Stock Office Stamps')" 408class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Stock Office Stamps</a></li><li><a href="/business-office-stamps/daters" 409@mouseenter="set('/business-office-stamps/daters', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Date-Stamps-OS-400x400.jpg', 'Date Stamps')" 410class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Date Stamps</a></li><li><a href="/business-office-stamps/checkbox" 411@mouseenter="set('/business-office-stamps/checkbox', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Checkbox-Stamps-OS-400x400.jpg', 'Checkbox
411Stamps')" 412class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Checkbox Stamps</a></li><li><a href="/business-office-stamps/secure-stamps" 413@mouseenter="set('/business-office-stamps/secure-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Security-Stamps-OS-400x400.jpg', 'Security Stamps')" 414class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Security Stamps</a></li><li><a href="/business-office-stamps/inspection-stamps" 415@mouseenter="set('/business-office-stamps/inspection-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-inspection-Stamps-OS-400x400.jpg', 'Inspection Stamps')" 416class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Inspection Stamps</a></li><li><a href="/business-office-stamps/medical-stamps" 417@mouseenter="set('/business-office-stamps/medical-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Medical-Stamps-OS-400x400.jpg', 'Medical Stamps')" 418class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Medical Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Professional Stamps</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/professional-stamps/llc-stamps" 419@mouseenter="set('/business-office-stamps/professional-stamps/llc-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-LLC-Stamps-OS-400x400.jpg', 'LLC Stamps')" 420class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">LLC Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/architect" 421@mouseenter="set('/business-office-stamps/professional-stamps/architect', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Architect-Stamps-OS-400x400.jpg', 'Architect Stamps')" 422class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Architect Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/engineer" 423@mouseenter="set('/business-office-stamps/professional-stamps/engineer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Engineer-Stamps-OS-400x400.jpg', 'Engineer Stamps')" 424class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Engineer Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/land-surveyor" 425@mouseenter="set('/business-office-stamps/professional-stamps/land-surveyor', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Land-Surveyor-Stamps-OS-400x400.jpg', 'Land Surveyor Stamps')" 426class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Land Surveyor Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/real-estate-appraiser" 427@mouseenter="set('/business-office-stamps/professional-stamps/real-estate-appraiser', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Real-Estate-Appraiser-Stamps-OS-400x400.jpg', 'Real Estate Appraiser Stamps')" 428class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Real Estate Appraiser Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/geologist-stamps-other" 429@mouseenter="set('/business-office-stamps/professional-stamps/geologist-stamps-other', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Geologist-Stamps-OS-400x400.jpg', 'Geologist Stamps')" 430class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Geologist Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps" @mouseenter="reset()" 431class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Office Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
431<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 432@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 433@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 434@keydown.escape="open = false" 435class="static"> <a href="https://www.simplystamps.com/embossers" 436:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 437class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Embossers </a><div x-show="open" style="display: none;" 438x-transition:enter="transition ease-out duration-150" 439x-transition:enter-start="opacity-0 -translate-y-1" 440x-transition:enter-end="opacity-100 translate-y-0" 441x-transition:leave="transition ease-in duration-100" 442x-transition:leave-start="opacity-100 translate-y-0" 443x-transition:leave-end="opacity-0 -translate-y-1" 444class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 445defaultHref: '/embossers', 446defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 447defaultLabel: 'Embossers Collection', 448activeHref: '/embossers', 449activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 450activeLabel: 'Embossers Collection', 451set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 452reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 453}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/embossers" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg" alt="Embossers Collection" :src="activeImage" :alt="activeLabel" 454loading="lazy" width="224" height="224" 455class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 456x-text="activeLabel">Embossers Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Personal & Occasion</h3><ul class="space-y-2.5"><li><a href="/embossers/custom-address" 457@mouseenter="set('/embossers/custom-address', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Address-EMB-400x400.jpg', 'Address Embossers')" 458class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Address Embossers</a></li><li><a href="/embossers/library-book-embossers" 459@mouseenter="set('/embossers/library-book-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Library-Book-EMB-400x400.jpg', 'Library Book Embossers')" 460class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Library Book Embossers</a></li><li><a href="/embossers/wedding-embossers" 461@mouseenter="set('/embossers/wedding-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-wedding-embosser-cat-emboss.jpg', 'Wedding Embossers')" 462class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Embossers</a></li><li><a href="/embossers/luxury-embossers" 463@mouseenter="set('/embossers/luxury-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-Luxury-Embossers-QCS-Nav-Embosser-400.jpg', 'Luxury Embossers')" 464class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Luxury Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Legal</h3><ul class="space-y-2.5"><li><a href="/embossers/corporate-seal" 465@mouseenter="set('/embossers/corporate-seal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Corporate-Seals-EMB-400x400.jpg', 'Corporate Seals')" 466class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Corporate Seals</a></li><li><a href="/embossers/notary-public" 467@mouseenter="set('/embossers/notary-public', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Notary-EMB-400x400.jpg', 'Notary Embossers')" 468class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Notary Embossers</a></li><li><a href="/embossers/custom-logo" 469@mouseenter="set('/embossers/custom-logo', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-LOGO-EMB-400x400.jpg', 'Logo Embossers')" 470class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 471@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/catalog/category/SS_cat-ink_and_accessories_embosser-accesories-265
471x265.jpg', 'Embosser Accessories')" 472class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/embossers" @mouseenter="reset()" 473class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Embossers <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 474@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 475@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 476@keydown.escape="open = false" 477class="relative"> <a href="https://www.simplystamps.com/notary-public" 478:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 479class="block font-medium p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary text-gray-700"> Notary </a></li><li x-data="{ open: false, hoverTimeout: null }" 480@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 481@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 482@keydown.escape="open = false" 483class="static"> <a href="https://www.simplystamps.com/teacher-stamps" 484:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 485class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Teacher Stamps </a><div x-show="open" style="display: none;" 486x-transition:enter="transition ease-out duration-150" 487x-transition:enter-start="opacity-0 -translate-y-1" 488x-transition:enter-end="opacity-100 translate-y-0" 489x-transition:leave="transition ease-in duration-100" 490x-transition:leave-start="opacity-100 translate-y-0" 491x-transition:leave-end="opacity-0 -translate-y-1" 492class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 493defaultHref: '/teacher-stamps', 494defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 495defaultLabel: 'Teacher Stamps Collection', 496activeHref: '/teacher-stamps', 497activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 498activeLabel: 'Teacher Stamps Collection', 499set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 500reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 501}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/teacher-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg" alt="Teacher Stamps Collection" :src="activeImage" :alt="activeLabel" 502loading="lazy" width="224" height="224" 503class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 504x-text="activeLabel">Teacher Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Classroom & Grading</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/grading"
505@mouseenter="set('/teacher-stamps/grading', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Grading-stamps-TS-400x400.jpg', 'Grading Stamps')" 506class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Grading Stamps</a></li><li><a href="/teacher-stamps/feedback" 507@mouseenter="set('/teacher-stamps/feedback', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Feedback-stamps-TS-400x400.jpg', 'Feedback Stamps')" 508class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Feedback Stamps</a></li><li><a href="/teacher-stamps/name-missing" 509@mouseenter="set('/teacher-stamps/name-missing', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Missing-Name-stamps-TS-400x400.jpg', 'Name Missing Stamps')" 510class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Missing Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Fun & Motivation</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/motivational" 511@mouseenter="set('/teacher-stamps/motivational', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Motivational-stamps-TS-400x400.jpg', 'Motivational Stamps')" 512class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Motivational Stamps</a></li><li><a href="/teacher-stamps/fun-designs" 513@mouseenter="set('/teacher-stamps/fun-designs', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Fun-stamps-TS-400x400.jpg', 'Fun Stamps')" 514class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Fun Stamps</a></li><li><a href="/teacher-stamps/from-the-desk-of" 515@mouseenter="set('/teacher-stamps/from-the-desk-of', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Book-stamps-TS-400x400.jpg', 'Book Stamps')" 516class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Book Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/teacher-stamps" @mouseenter="reset()" 517class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Teacher Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 518@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 519@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 520@keydown.escape="open = false" 521class="static"> <a href="https://www.simplystamps.com/craft-stamps" 522:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 523class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Craft Stamps </a><div x-show="open" style="display: none;" 524x-transition:enter="transition ease-out duration-150" 525x-transition:enter-start="opacity-0 -translate-y-1" 526x-transition:enter-end="opacity-100 translate-y-0" 527x-transition:leave="transition ease-in duration-100" 528x-transition:leave-start="opacity-100 translate-y-0" 529x-transition:leave-end="opacity-0 -translate-y-1" 530class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 531defaultHref: '/craft-stamps', 532defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 533defaultLabel: 'Craft Stamps Collection', 534activeHref: '/craft-stamps', 535activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 536activeLabel: 'Craft Stamps Collection', 537set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 538reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 539}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/craft-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg" alt="Craft Stamps Collection" :src="activeImage" :alt="activeLabel" 540loading="lazy" width="224" height="224" 541class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 542x-text="activeLabel">Craft Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Seasonal & Holiday</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/holiday-crafting"
543@mouseenter="set('/craft-stamps/holiday-crafting', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Holiday-Stamps-CS-400x400.jpg', 'Holiday Stamps')" 544class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Stamps</a></li><li><a href="/craft-stamps/halloween" 545@mouseenter="set('/craft-stamps/halloween', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Halloween-Stamps-CS-400x400.jpg', 'Halloween Stamps')" 546class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Halloween Stamps</a></li><li><a href="/craft-stamps/thanksgiving" 547@mouseenter="set('/craft-stamps/thanksgiving', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thanksgiving-Stamps-CS-400x400.jpg', 'Thanksgiving Stamps')" 548class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thanksgiving Stamps</a></li><li><a href="/craft-stamps/autumn-fall" 549@mouseenter="set('/craft-stamps/autumn-fall', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Autumn-Stamps-CS-400x400.jpg', 'Autumn Stamps')" 550class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Autumn Stamps</a></li><li><a href="/craft-stamps/summer" 551@mouseenter="set('/craft-stamps/summer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Summer-Stamps-CS-400x400.jpg', 'Summer Stamps')" 552class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Summer Stamps</a></li><li><a href="/craft-stamps/valentine-s-day-craft-stamp" 553@mouseenter="set('/craft-stamps/valentine-s-day-craft-stamp', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Valentines-Stamps-CS-400x400.jpg', 'Valentine\'s Day Stamps')" 554class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Valentine's Day Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Occasions & Gifts</h3><ul class="space-y-2.5"><li><a href="/wedding-stamps" 555@mouseenter="set('/wedding-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Wedding-Stamps-CS-400x400.jpg', 'Wedding Stamps')" 556class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Stamps</a></li><li><a href="/craft-stamps/we-have-moved" 557@mouseenter="set('/craft-stamps/we-have-moved', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Just-Moved-Stamps-CS-400x400.jpg', 'Just Moved Stamps')" 558class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Just Moved Stamps</a></li><li><a href="/craft-stamps/thank-you-stamps" 559@mouseenter="set('/craft-stamps/thank-you-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thank-You-Stamps-CS-400x400.jpg', 'Thank You Stamps')" 560class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thank You Stamps</a></li><li><a href="/craft-stamps/sentiment" 561@mouseenter="set('/craft-stamps/sentiment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Sentiment-Stamps-CS-400x400.jpg', 'Sentiment Stamps')" 562class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Sentiment Stamps</a></li><li><a href="/teacher-stamps/clothing-stamps" 563@mouseenter="set('/teacher-stamps/clothing-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Name-Stamps-CS-400x400.jpg', 'Name Stamps')" 564class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Stamps</a></li><li><a href="/face-stamps" 565@mouseenter="set('/face-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Face-Stamps-CS-400x400.jpg', 'Face Stamps')" 566class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Face Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Crafting</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/mini-accent-stamps" 567@mouseenter="set('/craft-stamps/mini-accent-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Mini-Accent-Stamps-CS-400x400.jpg', 'Mini Accent Stamps')" 568class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mini Accent Stamps</a></li><li><a href="/craft-stamps/embellishment/bullet-journal" 569@mouseenter="set('/craft-stamps/embellishment/bullet-journal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Bullet-Journal-Stamps-CS-400x400.jpg', 'Bullet Journal Stamps')" 570class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Bullet Journal Stamps</a></li><li><a href="/craft-stamps/embellishment" 571@mouseenter="set('/craft-stamps/embellishment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Decorative-Stamps-CS-400x400.jpg', 'Decorative Stamps')" 572class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Decorative Stamps</a></li><li><a href="/rubber-flower-stamps" 573@mouseenter="set('/rubber-flower-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Flower-Stamps-CS-400x400.jpg', 'Flower Stamps')" 574class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Flower Stamps</a></li><li><a href="/craft-stamps/emoji-stamps" 575@mouseenter="set('/craft-stamps/emoji-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Emoji-Stamps-CS-400x400.jpg', 'Emoji Stamps')" 576class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Emoji Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/craft-stamps" @mouseenter="reset()" 577class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Craft Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
577<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 578@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 579@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 580@keydown.escape="open = false" 581class="static"> <a href="https://www.simplystamps.com/stamp-ink-pads" 582:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 583class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Ink & Accessories </a><div x-show="open" style="display: none;" 584x-transition:enter="transition ease-out duration-150" 585x-transition:enter-start="opacity-0 -translate-y-1" 586x-transition:enter-end="opacity-100 translate-y-0" 587x-transition:leave="transition ease-in duration-100" 588x-transition:leave-start="opacity-100 translate-y-0" 589x-transition:leave-end="opacity-0 -translate-y-1" 590class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 591defaultHref: '/stamp-ink-pads', 592defaultImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 593defaultLabel: 'Ink & Accessories Collection', 594activeHref: '/stamp-ink-pads', 595activeImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 596activeLabel: 'Ink & Accessories Collection', 597set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 598reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 599}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/stamp-ink-pads" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg" alt="Ink & Accessories Collection" :src="activeImage" :alt="activeLabel" 600loading="lazy" width="224" height="224" 601class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 602x-text="activeLabel">Ink & Accessories Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Ink & Refills</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/craft-stamp-ink" 603@mouseenter="set('/stamp-ink-pads/craft-stamp-ink', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Craft-Ink-Pads-Ink-And-Accessories-400.jpg', 'Craft Stamp Ink Pads')" 604class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Craft Stamp Ink Pads</a></li><li><a href="/stamp-ink-pads/ink-pad-replacements" 605@mouseenter="set('/stamp-ink-pads/ink-pad-replacements', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Pad-Refills-Ink-And-Accessories-400.jpg', 'Ink Pad Refills')" 606class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Pad Refills</a></li><li><a href="/stamp-ink-pads/ink-bottle-refills" 607@mouseenter="set('/stamp-ink-pads/ink-bottle-refills', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Bottle-refills-Ink-And-Accessories-400.jpg', 'Ink Bottle Refills')" 608class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Bottle Refills</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 609@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Embosser-Accessories-Ink-And-Accessories-400.jpg', 'Embosser Acce
609ssories')" 610class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li><li><a href="/mail-accessories" 611@mouseenter="set('/mail-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Mail-Accessories-Ink-And-Accessories-400.jpg', 'Mail Accessories')" 612class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mail Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/stamp-ink-pads" @mouseenter="reset()" 613class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Ink & Accessories <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li></ul> </nav></div><div id="mobile-menu-content" class="hidden"> <nav class="py-4"><ul class="space-y-1 list-none p-0 m-0 border-t border-gray-200"><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Address Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 614class="w-5 h-5 transition-transform duration-200" 615fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 616defaultHref: '/address-stamps', 617defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 618defaultLabel: 'Address Stamps Collection', 619activeHref: '/address-stamps', 620activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 621activeLabel: 'Address Stamps Collection', 622set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 623reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 624}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/address-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg" alt="Address Stamps Collection" :src="activeImage" :alt="activeLabel" 625loading="lazy" width="224" height="224" 626class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 627x-text="activeLabel">Address Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-4 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Style</h3><ul class="space-y-2.5"><li><a href="/address-stamps/script-handwritten" 628@mouseenter="set('/address-stamps/script-handwritten', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Script-AS-224x224.jpg', 'Script Address Stamps')" 629class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Script Address Stamps</a></li><li><a href="/address-stamps/custom-line" 630@mouseenter="set('/address-stamps/custom-line', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Multiple-Line-A
630S-224x224.jpg', 'Multiple Line Address Stamps')" 631class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Multiple Line Address Stamps</a></li><li><a href="/address-stamps/round-address-stamps" 632@mouseenter="set('/address-stamps/round-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-AS-224x224.jpg', 'Round Address Stamps')" 633class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Address Stamps</a></li><li><a href="/address-stamps/rectangle-address-stamps" 634@mouseenter="set('/address-stamps/rectangle-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-AS-224x224.jpg', 'Rectangle Address Stamps')" 635class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Address Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 636@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Text-AS-224x224.jpg', 'Line Address Stamps')" 637class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Line Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Design</h3><ul class="space-y-2.5"><li><a href="/address-stamps/floral" 638@mouseenter="set('/address-stamps/floral', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Floral-AS-224x224.jpg', 'Floral Address Stamps')" 639class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Floral Address Stamps</a></li><li><a href="/address-stamps/wedding-couple" 640@mouseenter="set('/address-stamps/wedding-couple', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wedding-AS-224x224.jpg', 'Wedding Address Stamps')" 641class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Address Stamps</a></li><li><a href="/address-stamps/dog-address-stamps" 642@mouseenter="set('/address-stamps/dog-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Dog-AS-224x224.jpg', 'Dog Address Stamps')" 643class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Dog Address Stamps</a></li><li><a href="/address-stamps/animals" 644@mouseenter="set('/address-stamps/animals', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Animal-AS-224x224.jpg', 'Animal Address Stamps')" 645class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Animal Address Stamps</a></li><li><a href="/address-stamps/hearts" 646@mouseenter="set('/address-stamps/hearts', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Whimsical-AS-224x224.jpg', 'Whimsical Address Stamps')" 647class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Whimsical Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Seasonal</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/business-address-stamps" 648@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Business-AS-224x224.jpg', 'Business Address Stamps')" 649class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/address-stamps/patriotic-address-stamps" 650@mouseenter="set('/address-stamps/patriotic-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Patriotic-AS-224x224.jpg', 'Patriotic Address Stamps')" 651class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Patriotic Address Stamps</a></li><li><a href="/address-stamps/holiday" 652@mouseenter="set('/address-stamps/holiday', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Holiday-AS-224x224.jpg', 'Holiday Address Stamps')" 653class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Address Stamps</a></li><li><a href="/address-stamps/christmas-address-stamps" 654@mouseenter="set('/address-stamps/christmas-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Christmas-AS-224x224.jpg', 'Christmas Address Stamps')" 655class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Christmas Address Stamps</a></li><li><a href="/custom-address-labels" 656@mouseenter="set('/custom-address-labels', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/address-stamps/SS-QCS-Address-Labels-AS-400x400.jpg', 'Custom Return Address Labels')" 657class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Return Address Labels</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Collabs</h3><ul class="space-y-2.5"><li><a href="/salt-and-paper" 658@mouseenter="set('/salt-and-paper', 'https://cdn.simplystamps.com/media/catalog/category/8-simply-stamps-spco-2026-AS-Salt-Wine-1-1-lifestyle-1-2000_1.jpg', 'Salt & Paper Collaboration')" 659style='background-image:url(https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/CMSImages/SS_Nav-Salt-and-Paper-Swish.png); background-repeat:no-repeat; background-position:left center; background-size:100% 100%; padding:6px 10px; margin:0 -10px;' 660class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Salt & Paper Collaboration</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/address-stamps/view-all" @mouseenter="reset()" 661class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Address Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
661<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/address-stamps/view-all" 662class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Logo Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 663class="w-5 h-5 transition-transform duration-200" 664fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 665defaultHref: '/rubber-logo-stamps', 666defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 667defaultLabel: 'Logo Stamps Collection', 668activeHref: '/rubber-logo-stamps', 669activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 670activeLabel: 'Logo Stamps Collection', 671set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 672reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 673}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/rubber-logo-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg" alt="Logo Stamps Collection" :src="activeImage" :alt="activeLabel" 674loading="lazy" width="224" height="224" 675class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 676x-text="activeLabel">Logo Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Shape</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/rectangle" 677@mouseenter="set('/rubber-logo-stamps/rectangle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-Logo.jpg', 'Rectangle Logo Stamp')" 678class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Logo Stamp</a></li><li><a href="/rubber-logo-stamps/round" 679@mouseenter="set('/rubber-logo-stamps/round', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-Logo-2.jpg', 'Round Logo Stamp')" 680class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Logo Stamp</a></li><li><a href="/rubber-logo-stamps/square"
681@mouseenter="set('/rubber-logo-stamps/square', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Square-Logo.jpg', 'Square Logo Stamp')" 682class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Square Logo Stamp</a></li><li><a href="/rubber-logo-stamps/catalog-stamps" 683@mouseenter="set('/rubber-logo-stamps/catalog-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Catalog-Logo.jpg', 'Catalog Stamps')" 684class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Catalog Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Artwork</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/with-text" 685@mouseenter="set('/rubber-logo-stamps/with-text', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Logo-and-Text.jpg', 'Logo & Text')" 686class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo & Text</a></li><li><a href="/rubber-logo-stamps/upload-art" 687@mouseenter="set('/rubber-logo-stamps/upload-art', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Artwork-Logo.jpg', 'Personalized Art')" 688class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Personalized Art</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/pre-inked-logo" 689@mouseenter="set('/rubber-logo-stamps/pre-inked-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Pre-Inked-Logo.jpg', 'Pre-Inked')" 690class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Pre-Inked</a></li><li><a href="/rubber-logo-stamps/self-inking" 691@mouseenter="set('/rubber-logo-stamps/self-inking', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Self-Ink-Logo.jpg', 'Self-Inking')" 692class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Self-Inking</a></li><li><a href="/rubber-logo-stamps/traditional-wooden-handle" 693@mouseenter="set('/rubber-logo-stamps/traditional-wooden-handle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wooden-Logo.jpg', 'Wood Handle')" 694class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wood Handle</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/rubber-logo-stamps" @mouseenter="reset()" 695class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Logo Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/rubber-logo-stamps" 696class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Signature Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 697class="w-5 h-5 transition-transform duration-200" 698fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 699defaultHref: '/business-office-stamps/signature-stamps', 700defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 701defaultLabel: 'Signature Stamps Collection', 702activeHref: '/business-office-stamps/signature-stamps', 703activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 704activeLabel: 'Signature Stamps Collection', 705set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 706reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 707}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps/signature-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg" alt="Signature Stamps Collection" :src="activeImage" :alt="activeLabel" 708loading="lazy" width="224" height="224" 709class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 710x-text="activeLabel">Signature Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature Stamp Types</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/signature-only" 711@mouseenter="set('/business-office-stamps/signature-stamps/signature-only', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-only.jpg', 'Signature Only')" 712class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature Only</a></li><li><a href="/business-office-stamps/signature-stamps/with-date"
713@mouseenter="set('/business-office-stamps/signature-stamps/with-date', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-date.jpg', 'Signature with Date')" 714class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Date</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature & More</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/with-printed-name" 715@mouseenter="set('/business-office-stamps/signature-stamps/with-printed-name', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-name.jpg', 'Signature with Printed Name')" 716class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Printed Name</a></li><li><a href="/business-office-stamps/signature-stamps/with-logo" 717@mouseenter="set('/business-office-stamps/signature-stamps/with-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-logo.jpg', 'Signature with Logo')" 718class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Logo</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps/signature-stamps" @mouseenter="reset()" 719class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Signature Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/business-office-stamps/signature-stamps" 720class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Custom Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 721class="w-5 h-5 transition-transform duration-200" 722fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 723defaultHref: '/custom-rubber-stamps', 724defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 725defaultLabel: 'Custom Stamps Collection', 726activeHref: '/custom-rubber-stamps', 727activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 728activeLabel: 'Custom Stamps Collection', 729set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 730reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 731}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/custom-rubber-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg" alt="Custom Stamps Collection" :src="activeImage" :alt="activeLabel" 732loading="lazy" width="224" height="224" 733class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 734x-text="activeLabel">Custom Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-self-inking-stamps" 735@mouseenter="set('/custom-rubber-stamps/custom-self-inking-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Self-Inking-Stamps-Custom-400x400.jpg', 'Custom Self-Inking Stamps')" 736class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Self-Inking Stamps</a></li><li><a href="/custom-rubber-stamps/custom-pre-inked-stamps" 737@mouseenter="set('/custom-rubber-stamps/custom-pre-inked-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Pre-Inked-Stamps-Custom-400x400.jpg', 'Custom Pre-Inked Stamps')" 738class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Pre-Inked Stamps</a></li><li><a href="/custom-rubber-stamps/custom-wood-handle-stamps" 739@mouseenter="set('/custom-rubber-stamps/custom-wood-handle-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Traditional-Hand-Stamps-Custom-400x400.jpg', 'Custom Wood Handle Stamps')" 740class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Wood Handle Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Specialty</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-european-hand-stamps" 741@mouseenter="set('/custom-rubber-stamps/custom-european-hand-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-European-Hand-Stamps-Custom-400x400.jpg', 'Custom European Hand Stamps')" 742class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom European Hand Stamps</a></li><li><a href="/custom-rubber-stamps/custom-professional-stamps" 743@mouseenter="set('/custom-rubber-stamps/custom-professional-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Professional-Stamps-Custom-400x400.jpg', 'Custom Professional Stamps')" 744class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Professional Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/custom-rubber-stamps" @mouseenter="reset()" 745class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Custom Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
745<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/custom-rubber-stamps" 746class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Office Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 747class="w-5 h-5 transition-transform duration-200" 748fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 749defaultHref: '/business-office-stamps', 750defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 751defaultLabel: 'Office Stamps Collection', 752activeHref: '/business-office-stamps', 753activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 754activeLabel: 'Office Stamps Collection', 755set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 756reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 757}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg" alt="Office Stamps Collection" :src="activeImage" :alt="activeLabel" 758loading="lazy" width="224" height="224" 759class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 760x-text="activeLabel">Office Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Banking</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/accounting-tax-stamps" 761@mouseenter="set('/business-office-stamps/accounting-tax-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Accounting-Stamps-OS-400x400.jpg', 'Accounting Stamps')" 762class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Accounting Stamps</a></li><li><a href="/business-office-stamps/business-address-stamps" 763@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Business-Address-Stamps-OS-400x400.jpg', 'Business Address Stamps')" 764class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement" 765@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Check-Endorsement-Stamps-OS-400x400.jpg', 'Check Endorsement Stamps')" 766class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Check Endorsement Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps" 767@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Deposit-Stamps-OS-400x400.jpg', 'Deposit Stamps')" 768class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Deposit Stamps</a></li><li><a href="/business-office-stamps/accounting-tax-stamps/received" 769@mouseenter="set('/business-office-stamps/accounting-tax-stamps/received', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Received-stamps-OS-400x400.jpg', 'Received Stamps')" 770class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Received Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 771@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Custom-text-Stamps-OS-400x400.jpg', 'Custom Text Stamps')" 772class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Text Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Office Essentials</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/office-stamps" 773@mouseenter="set('/business-office-stamps/office-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Stock-office-stamps-OS-400x400.jpg', 'Stock Office Stamps')" 774class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Stock Office Stamps</a></li><li><a href="/business-office-stamps/daters" 775@mouseenter="set('/business-office-stamps/daters', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Date-Stamps-OS-400x400.jpg', 'Date Stamps')" 776class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Date Stamps</a></li><li><a href="/business-office-stamps/checkbox" 777@mouseenter="set('/business-office-stamps/checkbox', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Checkbox-Stamps-OS-400x400.jpg', 'Checkbox
777Stamps')" 778class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Checkbox Stamps</a></li><li><a href="/business-office-stamps/secure-stamps" 779@mouseenter="set('/business-office-stamps/secure-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Security-Stamps-OS-400x400.jpg', 'Security Stamps')" 780class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Security Stamps</a></li><li><a href="/business-office-stamps/inspection-stamps" 781@mouseenter="set('/business-office-stamps/inspection-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-inspection-Stamps-OS-400x400.jpg', 'Inspection Stamps')" 782class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Inspection Stamps</a></li><li><a href="/business-office-stamps/medical-stamps" 783@mouseenter="set('/business-office-stamps/medical-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Medical-Stamps-OS-400x400.jpg', 'Medical Stamps')" 784class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Medical Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Professional Stamps</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/professional-stamps/llc-stamps" 785@mouseenter="set('/business-office-stamps/professional-stamps/llc-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-LLC-Stamps-OS-400x400.jpg', 'LLC Stamps')" 786class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">LLC Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/architect" 787@mouseenter="set('/business-office-stamps/professional-stamps/architect', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Architect-Stamps-OS-400x400.jpg', 'Architect Stamps')" 788class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Architect Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/engineer" 789@mouseenter="set('/business-office-stamps/professional-stamps/engineer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Engineer-Stamps-OS-400x400.jpg', 'Engineer Stamps')" 790class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Engineer Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/land-surveyor" 791@mouseenter="set('/business-office-stamps/professional-stamps/land-surveyor', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Land-Surveyor-Stamps-OS-400x400.jpg', 'Land Surveyor Stamps')" 792class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Land Surveyor Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/real-estate-appraiser" 793@mouseenter="set('/business-office-stamps/professional-stamps/real-estate-appraiser', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Real-Estate-Appraiser-Stamps-OS-400x400.jpg', 'Real Estate Appraiser Stamps')" 794class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Real Estate Appraiser Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/geologist-stamps-other" 795@mouseenter="set('/business-office-stamps/professional-stamps/geologist-stamps-other', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Geologist-Stamps-OS-400x400.jpg', 'Geologist Stamps')" 796class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Geologist Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps" @mouseenter="reset()" 797class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Office Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
797<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/business-office-stamps" 798class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Embossers </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 799class="w-5 h-5 transition-transform duration-200" 800fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 801defaultHref: '/embossers', 802defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 803defaultLabel: 'Embossers Collection', 804activeHref: '/embossers', 805activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 806activeLabel: 'Embossers Collection', 807set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 808reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 809}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/embossers" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg" alt="Embossers Collection" :src="activeImage" :alt="activeLabel" 810loading="lazy" width="224" height="224" 811class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 812x-text="activeLabel">Embossers Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Personal & Occasion</h3><ul class="space-y-2.5"><li><a href="/embossers/custom-address" 813@mouseenter="set('/embossers/custom-address', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Address-EMB-400x400.jpg', 'Address Embossers')" 814class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Address Embossers</a></li><li><a href="/embossers/library-book-embossers" 815@mouseenter="set('/embossers/library-book-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Library-Book-EMB-400x400.jpg', 'Library Book Embossers')" 816class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Library Book Embossers</a></li><li><a href="/embossers/wedding-embossers" 817@mouseenter="set('/embossers/wedding-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-wedding-embosser-cat-emboss.jpg', 'Wedding Embossers')" 818class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Embossers</a></li><li><a href="/embossers/luxury-embossers" 819@mouseenter="set('/embossers/luxury-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-Luxury-Embossers-QCS-Nav-Embosser-400.jpg', 'Luxury Embossers')" 820class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Luxury Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Legal</h3><ul class="space-y-2.5"><li><a href="/embossers/corporate-seal" 821@mouseenter="set('/embossers/corporate-seal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Corporate-Seals-EMB-400x400.jpg', 'Corporate Seals')" 822class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Corporate Seals</a></li><li><a href="/embossers/notary-public" 823@mouseenter="set('/embossers/notary-public', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Notary-EMB-400x400.jpg', 'Notary Embossers')" 824class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Notary Embossers</a></li><li><a href="/embossers/custom-logo" 825@mouseenter="set('/embossers/custom-logo', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-LOGO-EMB-400x400.jpg', 'Logo Embossers')" 826class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 827@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/catalog/category/SS_cat-ink_and_accessories_embosser-accesories-265
827x265.jpg', 'Embosser Accessories')" 828class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/embossers" @mouseenter="reset()" 829class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Embossers <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/embossers" 830class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li > <a href="https://www.simplystamps.com/notary-public" 831class="block px-4 py-3 font-semibold text-lg border-b border-gray-200 transition-colors duration-200 hover:bg-gray-100 hover:text-brand-primary text-gray-700"> Notary </a></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Teacher Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 832class="w-5 h-5 transition-transform duration-200" 833fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 834defaultHref: '/teacher-stamps', 835defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 836defaultLabel: 'Teacher Stamps Collection', 837activeHref: '/teacher-stamps', 838activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 839activeLabel: 'Teacher Stamps Collection', 840set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 841reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 842}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/teacher-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg" alt="Teacher Stamps Collection" :src="activeImage" :alt="activeLabel" 843loading="lazy" width="224" height="224" 844class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 845x-text="activeLabel">Teacher Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Classroom & Grading</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/grading" 846@mouseenter="set('/teacher-stamps/grading', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Grading-stamps-TS-400x400.jpg', 'Grading Stamps')" 847class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Grading Stamps</a></li><li><a href="/teacher-stamps/feedback" 848@mouseenter="set('/teacher-stamps/feedback', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Feedback-stamps-TS-400x400.jpg', 'Feedback Stamps')" 849class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Feedback Stamps</a></li><li><a href="/teacher-stamps/name-missing"
850@mouseenter="set('/teacher-stamps/name-missing', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Missing-Name-stamps-TS-400x400.jpg', 'Name Missing Stamps')" 851class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Missing Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Fun & Motivation</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/motivational" 852@mouseenter="set('/teacher-stamps/motivational', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Motivational-stamps-TS-400x400.jpg', 'Motivational Stamps')" 853class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Motivational Stamps</a></li><li><a href="/teacher-stamps/fun-designs" 854@mouseenter="set('/teacher-stamps/fun-designs', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Fun-stamps-TS-400x400.jpg', 'Fun Stamps')" 855class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Fun Stamps</a></li><li><a href="/teacher-stamps/from-the-desk-of" 856@mouseenter="set('/teacher-stamps/from-the-desk-of', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Book-stamps-TS-400x400.jpg', 'Book Stamps')" 857class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Book Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/teacher-stamps" @mouseenter="reset()" 858class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Teacher Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/teacher-stamps" 859class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Craft Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 860class="w-5 h-5 transition-transform duration-200" 861fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 862defaultHref: '/craft-stamps', 863defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 864defaultLabel: 'Craft Stamps Collection', 865activeHref: '/craft-stamps', 866activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 867activeLabel: 'Craft Stamps Collection', 868set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 869reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 870}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/craft-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg" alt="Craft Stamps Collection" :src="activeImage" :alt="activeLabel" 871loading="lazy" width="224" height="224" 872class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 873x-text="activeLabel">Craft Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Seasonal & Holiday</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/holiday-crafting"
874@mouseenter="set('/craft-stamps/holiday-crafting', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Holiday-Stamps-CS-400x400.jpg', 'Holiday Stamps')" 875class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Stamps</a></li><li><a href="/craft-stamps/halloween" 876@mouseenter="set('/craft-stamps/halloween', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Halloween-Stamps-CS-400x400.jpg', 'Halloween Stamps')" 877class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Halloween Stamps</a></li><li><a href="/craft-stamps/thanksgiving" 878@mouseenter="set('/craft-stamps/thanksgiving', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thanksgiving-Stamps-CS-400x400.jpg', 'Thanksgiving Stamps')" 879class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thanksgiving Stamps</a></li><li><a href="/craft-stamps/autumn-fall" 880@mouseenter="set('/craft-stamps/autumn-fall', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Autumn-Stamps-CS-400x400.jpg', 'Autumn Stamps')" 881class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Autumn Stamps</a></li><li><a href="/craft-stamps/summer" 882@mouseenter="set('/craft-stamps/summer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Summer-Stamps-CS-400x400.jpg', 'Summer Stamps')" 883class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Summer Stamps</a></li><li><a href="/craft-stamps/valentine-s-day-craft-stamp" 884@mouseenter="set('/craft-stamps/valentine-s-day-craft-stamp', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Valentines-Stamps-CS-400x400.jpg', 'Valentine\'s Day Stamps')" 885class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Valentine's Day Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Occasions & Gifts</h3><ul class="space-y-2.5"><li><a href="/wedding-stamps" 886@mouseenter="set('/wedding-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Wedding-Stamps-CS-400x400.jpg', 'Wedding Stamps')" 887class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Stamps</a></li><li><a href="/craft-stamps/we-have-moved" 888@mouseenter="set('/craft-stamps/we-have-moved', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Just-Moved-Stamps-CS-400x400.jpg', 'Just Moved Stamps')" 889class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Just Moved Stamps</a></li><li><a href="/craft-stamps/thank-you-stamps" 890@mouseenter="set('/craft-stamps/thank-you-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thank-You-Stamps-CS-400x400.jpg', 'Thank You Stamps')" 891class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thank You Stamps</a></li><li><a href="/craft-stamps/sentiment" 892@mouseenter="set('/craft-stamps/sentiment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Sentiment-Stamps-CS-400x400.jpg', 'Sentiment Stamps')" 893class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Sentiment Stamps</a></li><li><a href="/teacher-stamps/clothing-stamps" 894@mouseenter="set('/teacher-stamps/clothing-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Name-Stamps-CS-400x400.jpg', 'Name Stamps')" 895class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Stamps</a></li><li><a href="/face-stamps" 896@mouseenter="set('/face-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Face-Stamps-CS-400x400.jpg', 'Face Stamps')" 897class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Face Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Crafting</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/mini-accent-stamps" 898@mouseenter="set('/craft-stamps/mini-accent-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Mini-Accent-Stamps-CS-400x400.jpg', 'Mini Accent Stamps')" 899class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mini Accent Stamps</a></li><li><a href="/craft-stamps/embellishment/bullet-journal" 900@mouseenter="set('/craft-stamps/embellishment/bullet-journal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Bullet-Journal-Stamps-CS-400x400.jpg', 'Bullet Journal Stamps')" 901class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Bullet Journal Stamps</a></li><li><a href="/craft-stamps/embellishment" 902@mouseenter="set('/craft-stamps/embellishment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Decorative-Stamps-CS-400x400.jpg', 'Decorative Stamps')" 903class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Decorative Stamps</a></li><li><a href="/rubber-flower-stamps" 904@mouseenter="set('/rubber-flower-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Flower-Stamps-CS-400x400.jpg', 'Flower Stamps')" 905class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Flower Stamps</a></li><li><a href="/craft-stamps/emoji-stamps" 906@mouseenter="set('/craft-stamps/emoji-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Emoji-Stamps-CS-400x400.jpg', 'Emoji Stamps')" 907class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Emoji Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/craft-stamps" @mouseenter="reset()" 908class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Craft Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
908<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/craft-stamps" 909class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Ink & Accessories </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 910class="w-5 h-5 transition-transform duration-200" 911fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 912defaultHref: '/stamp-ink-pads', 913defaultImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 914defaultLabel: 'Ink & Accessories Collection', 915activeHref: '/stamp-ink-pads', 916activeImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 917activeLabel: 'Ink & Accessories Collection', 918set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 919reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 920}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/stamp-ink-pads" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg" alt="Ink & Accessories Collection" :src="activeImage" :alt="activeLabel" 921loading="lazy" width="224" height="224" 922class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 923x-text="activeLabel">Ink & Accessories Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Ink & Refills</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/craft-stamp-ink" 924@mouseenter="set('/stamp-ink-pads/craft-stamp-ink', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Craft-Ink-Pads-Ink-And-Accessories-400.jpg', 'Craft Stamp Ink Pads')" 925class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Craft Stamp Ink Pads</a></li><li><a href="/stamp-ink-pads/ink-pad-replacements" 926@mouseenter="set('/stamp-ink-pads/ink-pad-replacements', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Pad-Refills-Ink-And-Accessories-400.jpg', 'Ink Pad Refills')" 927class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Pad Refills</a></li><li><a href="/stamp-ink-pads/ink-bottle-refills" 928@mouseenter="set('/stamp-ink-pads/ink-bottle-refills', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Bottle-refills-Ink-And-Accessories-400.jpg', 'Ink Bottle Refills')" 929class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Bottle Refills</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 930@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Embosser-Accessories-Ink-And-Accessories-400.jpg', 'Embosser Acce
930ssories')" 931class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li><li><a href="/mail-accessories" 932@mouseenter="set('/mail-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Mail-Accessories-Ink-And-Accessories-400.jpg', 'Mail Accessories')" 933class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mail Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/stamp-ink-pads" @mouseenter="reset()" 934class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Ink & Accessories <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/stamp-ink-pads" 935class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li></ul> </nav></div> </nav><div class="flex items-center justify-center space-x-1 w-1/8"><div x-data="{ accountOpen: false }" @click.away="accountOpen = false" class="relative"> <button @click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); if ($store.auth.ready && $store.auth.isLoggedIn) { accountOpen = !accountOpen } else if ($store.auth.ready) { $store.auth._loginOpenedAt = Date.now(); $store.auth.showLoginModal = true }" 936class="flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" 937stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" 938stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="flex items-center justify-center gap-0.5"> <span class="text-sm font-medium text-gray-700" 939x-text="$store.auth.ready ? ($store.auth.isLoggedIn ? 'Account' : 'Log In') : 'Account'"></span> <svg x-show="$store.auth.ready && $store.auth.isLoggedIn" 940class="w-2.5 h-2.5 text-gray-400 shrink-0 transition-transform duration-200" 941:class="{ 'rotate-180': accountOpen }" 942fill="none" stroke="currentColor" viewBox="0 0 24 24" 943style="display: none;"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/> </svg> </span> </button><div x-show="accountOpen" 944x-transition:enter="transition ease-out duration-150" 945x-transition:enter-start="opacity-0 scale-95" 946x-transition:enter-end="opacity-100 scale-100" 947x-transition:leave="transition ease-in duration-100" 948x-transition:leave-start="opacity-100 scale-100" 949x-transition:leave-end="opacity-0 scale-95" 950class="absolute left-1/2 -translate-x-1/2 top-full mt-3 w-64 bg-white rounded-lg shadow-lg border border-gray-300 py-1.5 z-50" 951style="display: none;"><div class="absolute -top-2 left-1/2 -translate-x-1/2 w-3 h-3 bg-white border-l border-t border-gray-300 rotate-45"></div><div class="px-5 py-2.5 border-b border-gray-100 mb-1"><p class="text-sm text-gray-400 truncate" x-text="$store.auth.customerEmail"></p></div> <a href="https://www.simplystamps.com/customer/account/" 952class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/> </svg> My Account </a> <a href="https://www.simplystamps.com/sales/order/history/" 953class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/> </svg> My Orders </a> <a x-show="$store.auth.companyStoreUrl" 954:href="$store.auth.companyStoreUrl" 955class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors" 956style="display: none;"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/> </svg> Company Store </a><div class="border-t border-gray-100 my-1.5"></div> <button @click="accountOpen = false;
957if (typeof window.zGA4 === 'function') { window.zGA4('logout', { method: 'site' }); } 958$store.auth.refreshMessage = 'Logging out...'; 959$store.auth.showRefreshOverlay = true; 960window.location.href = 'https://www.simplystamps.com/customer/account/logout/';" 961class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-500 hover:text-red-600 hover:bg-gray-50 transition-colors w-full"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 962d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/> </svg> Log Out </button></div></div><button 963@click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); $store.search.open()" 964class="flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <circle cx="11" cy="11" r="6" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 16L21 21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="text-sm font-medium text-gray-700">Search</span> </button><div x-data="{ helpOpen: false }" @click.away="helpOpen = false" class="relative"> <button @click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); helpOpen = !helpOpen" 965class="flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.08984 8.99959C9.32495 8.33126 9.789 7.7677 10.3998 7.40873C11.0106 7.04975 11.7287 6.91853 12.427 7.0383C13.1253 7.15808 13.7587 7.52112 14.2149 8.06312C14.6712 8.60512 14.9209 9.29112 14.9198 9.99959C14.9198 11.9996 11.9198 12.9996 11.9198 12.9996" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 17H12.01" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="flex items-center justify-center gap-0.5"> <span class="text-sm font-medium text-gray-700">Help</span> <svg class="w-2.5 h-2.5 text-gray-400 shrink-0 transition-transform duration-200" 966:class="{ 'rotate-180': helpOpen }" 967fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/> </svg> </span> </button><div x-show="helpOpen" 968x-transition:enter="transition ease-out duration-150" 969x-transition:enter-start="opacity-0 scale-95" 970x-transition:enter-end="opacity-100 scale-100" 971x-transition:leave="transition ease-in duration-100" 972x-transition:leave-start="opacity-100 scale-100" 973x-transition:leave-end="opacity-0 scale-95" 974class="absolute left-1/2 -translate-x-1/2 top-full mt-3 w-64 bg-white rounded-lg shadow-lg border border-gray-300 py-1.5 z-50" 975style="display: none;"><div class="absolute -top-2 left-1/2 -translate-x-1/2 w-3 h-3 bg-white border-l border-t border-gray-300 rotate-45"></div> <a href="https://www.simplystamps.com/contacts" 976class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/> </svg> Track My Order </a> <a href="https://www.simplystamps.com/contacts" 977class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> Contact Us </a> <a href="https://www.simplystamps.com/faq" 978class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> FAQ </a> <template x-if="$store.chat.available"><div><div class="border-t border-gray-100 my-1.5"></div> <button id="live-chat-now" class="flex items-center gap-3 px-5 py-2.5 pb-1.5 text-base font-medium text-brand-primary hover:bg-gray-50 transition-colors w-full cursor-pointer"> <svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.25
9785-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> <span class="text-left"> Chat with a Live Agent <span class="block text-sm text-brand-success font-medium">Available now</span> <span class="block text-sm text-gray-400 font-normal" x-text="$store.chat.hoursLabel"></span> </span> </button></div> </template></div></div><button 979@click="$store.cart.openDrawer()" 980@dblclick.prevent="window.location.href='/checkout/cart/'" 981class="relative flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors" 982aria-label="Open cart" 983> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M9 22C9.55228 22 10 21.5523 10 21C10 20.4477 9.55228 20 9 20C8.44772 20 8 20.4477 8 21C8 21.5523 8.44772 22 9 22Z" fill="#4F4F4F" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20 22C20.5523 22 21 21.5523 21 21C21 20.4477 20.5523 20 20 20C19.4477 20 19 20.4477 19 21C19 21.5523 19.4477 22 20 22Z" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M1 1H5L7.68 14.39C7.77144 14.8504 8.02191 15.264 8.38755 15.5583C8.75318 15.8526 9.2107 16.009 9.68 16H19.4C19.8693 16.009 20.3268 15.8526 20.6925 15.5583C21.0581 15.264 21.3086 14.8504 21.4 14.39L23 6H6" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="text-sm font-medium text-gray-700">Cart</span> <span x-cloak 984x-show="$store.cart.itemCount > 0" 985x-text="$store.cart.itemCount" 986class="absolute -top-2 right-0 bg-brand-danger text-white text-xs rounded-full h-5 flex items-center justify-center" 987:class="$store.cart.itemCount >= 10 ? 'min-w-[1.25rem] px-1' : 'w-5'"></span> </button></div></div><div 988x-show="searchOpen" 989x-transition:enter="transition ease-out duration-200" 990x-transition:enter-start="opacity-0 -translate-y-2" 991x-transition:enter-end="opacity-100 translate-y-0" 992x-transition:leave="transition ease-in duration-150" 993x-transition:leave-start="opacity-100 translate-y-0" 994x-transition:leave-end="opacity-0 -translate-y-2" 995class="pb-4" 996style="display: none;" 997><form class="searchautocomplete UI-SEARCHAUTOCOMPLETE" action="https://www.simplystamps.com/catalogsearch/result/" method="get" 998data-tip="Search entire store here..." 999data-url="//www.simplystamps.com/searchautocomplete/ajax/get/" 1000data-minchars="3" 1001data-delay="500" 1002data-hide-delay="1000"><label for="search">Search</label><div class="nav"><div class="nav-search-in"> <span class="category-fake UI-CATEGORY-TEXT">All</span> <span class="nav-down-arrow"></span> <select name="cat" class="category UI-CATEGORY"><option value="0">All</option><option value="713" > Address Stamps</option><option value="958" > Custom Business & Office Stamps</option><option value="749" > Craft Stamps</option><option value="1387" > Custom Logo Stamps</option><option value="770" > Embossers</option><option value="802" > Stamp Ink & Pads</option><option value="811" > Teacher Stamps</option><option value="793" > Notary</option><option value="815" > Wedding Stamps</option><option value="1432" > Redeemable Personalized Address Stamp with Gift Box</option><option value="2291" > St. Patrick's Day & Irish Theme Stamps</option><option value="2391" > Stamp of the Week</option><option value="809" > Breast Cancer Awareness</option><option value="3035" > Gift Notes</option><option value="3073" > Real Estate Supplies</option><option value="3085" > Custom Rubber Stamps</option><option value="3094" > Custom Return Address Labels</option><option value="3109" > Here Tomorrow</option><option value="3118" > StampToons</option><option value="3327" > Custom Face Stamps</option><option value="3432" > Salt & Paper Collaboration</option> </select></div><div class="nav-input UI-NAV-INPUT"> <input class="input-text UI-SEARCH" type="text" autocomplete="off" name="q" value="" maxlength="128" /></div><div class="searchautocomplete-loader UI-LOADER"><div id="g01"></div><div id="g02"></div><div id="g03"></div><div id="g04"></div><div id="g05"></div><div id="g06"></div><div id="g07"></div><div id="g08"></div></div></div><div class="nav-submit-button"> <button type="submit" title="Go" class="button">Go</button></div> <style> .store-4-search-autocomplete { 1003top: 108px !important; 1004right: unset !important; 1005}@media (min-width: 768px) { 1006.store-4-search-autocomplete { 1007top: 74px !important; 1008right: unset !important; 1009} 1010} </style><div style="display:none" class="searchautocomplete-placeholder UI-PLACEHOLDER "></div></form></div></div></div><div id="zephyr-promo-banner" 1011role="region" 1012aria-label="Promotion" 1013data-key="19208366" 1014data-start="1790568000" 1015data-end="1793474220" 1016data-code="" 1017data-auto-apply="0" 1018data-countdown="0" 1019class="relative text-sm sm:text-base leading-snug " 1020style="background-color:#60B5E4;color:#012E4B;" x-data="{
1021copied: false, 1022applied: false, 1023applying: false, 1024hovering: false, 1025countdown: '', 1026timer: null,init() { 1027if (this.$root.dataset.countdown === '1') { 1028this.tick(); 1029this.timer = setInterval(() => this.tick(), 1000); 1030} 1031},// Mirrors HSC_Zephyr_Model_PromoBanner::formatCountdown() exactly. 1032formatCountdown(s) { 1033const pad = n => String(n).padStart(2, '0'); 1034const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; 1035return (d > 0 ? pad(d) + 'd ' : '') + pad(h) + 'h ' + pad(m) + 'm ' + pad(sec) + 's'; 1036},tick() { 1037const end = parseInt(this.$root.dataset.end, 10) || 0; 1038const left = Math.max(0, end - Math.floor(Date.now() / 1000)); 1039this.countdown = this.formatCountdown(left); 1040if (left <= 0) { 1041// Promo is over: take the band down the same way dismiss does. 1042clearInterval(this.timer); 1043this.$root.hidden = true; 1044window.dispatchEvent(new CustomEvent('zephyr:header-resize')); 1045} 1046},copy() { 1047// A tap fires mouseenter/focus with no matching leave, so clear the hover state here. 1048this.hovering = false; 1049const code = this.$root.dataset.code; 1050const write = navigator.clipboard 1051? navigator.clipboard.writeText(code) 1052: Promise.reject(); 1053write.then(() => { 1054this.copied = true; 1055setTimeout(() => { this.copied = false; }, 2000); 1056}).catch(() => {});if (this.$root.dataset.autoApply === '1') { 1057this.apply(code); 1058} 1059},// Mirrors Alpine.store('promo').apply() in promo-code.js, minus the modal 1060// state. Only when the cart has items; a coupon on an empty quote does 1061// not validate reliably. Rejections stay silent: the shopper still has 1062// the code on the clipboard and the cart's own promo UI reports errors. 1063apply(code) { 1064const cart = Alpine.store('cart'); 1065if (this.applying || this.applied || !cart || !cart.itemCount) return; 1066if (typeof promoCodeConfig === 'undefined' || !promoCodeConfig.applyCouponUrl) return;this.applying = true; 1067fetch(promoCodeConfig.applyCouponUrl, { 1068method: 'POST', 1069credentials: 'same-origin', 1070headers: { 1071'Content-Type': 'application/x-www-form-urlencoded', 1072'X-Requested-With': 'XMLHttpRequest' 1073}, 1074body: 'coupon_code=' + encodeURIComponent(code) + '&form_key=' + encodeURIComponent(promoCodeConfig.formKey || '') 1075}) 1076.then(r => r.json()) 1077.then(data => { 1078if (!data.success) return; 1079this.applied = true; 1080cart.refresh(); 1081if (Alpine.store('promo')) { 1082Alpine.store('promo').appliedCode = code.toUpperCase(); 1083} 1084window.dispatchEvent(new CustomEvent('cart-updated')); 1085}) 1086.catch(() => {}) 1087.finally(() => { this.applying = false; }); 1088},dismiss() { 1089try { sessionStorage.setItem('zephyr_promo_dismissed', this.$root.dataset.key); } catch (e) {} 1090this.$root.hidden = true; 1091window.dispatchEvent(new CustomEvent('zephyr:header-resize')); 1092} 1093}"><div class="container mx-auto px-9 sm:px-10 min-h-[36px] sm:min-h-[40px] py-1.5 sm:py-2 flex flex-col items-center justify-center gap-y-1.5 text-center sm:flex-row sm:flex-wrap sm:gap-x-3 sm:gap-y-1"> <span class="leading-tight sm:contents"> <span class="[&_a]:underline [&_a]:font-semibold [&_a]:text-inherit [&_img]:inline-block [&_img]:h-[1.3em] [&_img]:w-auto [&_img]:align-middle [&_img]:mx-0.5">Free shipping on orders $79+ | <b>FREE GIFT</b> with every logo stamp order</span> <a href="https://www.simplystamps.com/rubber-logo-stamps" class="ml-1 sm:ml-0 sm:order-last font-semibold underline underline-offset-2 whitespace-nowrap text-inherit hover:text-inherit hover:opacity-80 transition-opacity"> Shop logo stamps </a> </span></div><button type="button" 1094@click="dismiss()" 1095aria-label="Dismiss" 1096class="absolute right-1.5 sm:right-2 top-1/2 -translate-y-1/2 p-1.5 rounded transition-colors hover:bg-black/10"> <svg class="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/> </svg> </button> <a x-show="$store.auth.isStaff" x-cloak 1097:href="$store.auth.adminEditBase ? $store.auth.adminEditBase + 'type/config/id/hsc_zephyr/store/sstamps_en/' : '#'" 1098target="_blank" rel="noopener" 1099title="Edit Promo Banner" 1100class="absolute left-2 top-1/2 -translate-y-1/2 flex w-6 h-6 bg-blue-600 hover:bg-blue-700 text-white rounded-full items-center justify-center shadow transition-colors staff-edit-icon"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/> </svg> </a></div>
1100<script> // Pre-paint gate: runs before header.phtml measures --header-height. 1101(function () { 1102var el = document.getElementById('zephyr-promo-banner'); 1103if (!el) return; 1104var now = Date.now() / 1000; 1105var start = parseInt(el.dataset.start, 10) || 0; 1106var end = parseInt(el.dataset.end, 10) || 0; 1107var hide = (start && now < start) || (end && now > end); 1108if (!hide) { 1109try { hide = sessionStorage.getItem('zephyr_promo_dismissed') === el.dataset.key; } catch (e) {} 1110} 1111if (hide) el.hidden = true; 1112})(); </script>
1112 </header></div>
1112<script> // Alpine.js global stores 1113document.addEventListener('alpine:init', () => { 1114// Chat Availability Store (client-side check to bypass Fastly cache) 1115Alpine.store('chat', { 1116available: false, 1117hoursLabel: '', 1118schedule: {"1":{"from":"09:00:00","to":"20:00:00"},"2":{"from":"09:00:00","to":"20:00:00"},"3":{"from":"09:00:00","to":"20:00:00"},"4":{"from":"09:00:00","to":"20:00:00"},"5":{"from":"09:00:00","to":"20:00:00"},"6":{"from":"00:00:00","to":"00:00:00"},"7":{"from":"00:00:00","to":"00:00:00"}},init() { 1119this.check(); 1120setInterval(() => this.check(), 60000); 1121},check() { 1122if (!Object.keys(this.schedule).length) return; 1123try { 1124var etDate = new Date(new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })); 1125var dow = etDate.getDay(); 1126var isoDay = dow === 0 ? 7 : dow; 1127var daySchedule = this.schedule[isoDay]; 1128if (!daySchedule) { this.available = false; this.hoursLabel = ''; return; }var h = etDate.getHours(), m = etDate.getMinutes(), s = etDate.getSeconds(); 1129var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; 1130var currentTime = pad(h) + ':' + pad(m) + ':' + pad(s); 1131this.available = currentTime >= daySchedule.from && currentTime < daySchedule.to; 1132this.hoursLabel = this.formatTime(daySchedule.from) + '-' + this.formatTime(daySchedule.to) + ' ET'; 1133} catch (e) { 1134this.available = false; 1135} 1136},// "09:00:00" â "9AM", "13:30:00" â "1:30PM" 1137formatTime(timeStr) { 1138var p = timeStr.split(':'); 1139var h = parseInt(p[0], 10), m = parseInt(p[1], 10); 1140var suffix = h >= 12 ? 'PM' : 'AM'; 1141var h12 = h % 12 || 12; 1142return m > 0 ? h12 + ':' + (m < 10 ? '0' + m : m) + suffix : h12 + suffix; 1143} 1144});// Image Preview Store 1145Alpine.store('imagePreview', { 1146isOpen: false, 1147imageUrl: '', 1148imageAlt: '', 1149linkUrl: '',open(url, alt = '', link = '') { 1150this.imageUrl = url; 1151this.imageAlt = alt; 1152this.linkUrl = link; 1153this.isOpen = true; 1154document.body.style.overflow = 'hidden'; 1155},close() { 1156this.isOpen = false; 1157document.body.style.overflow = ''; 1158} 1159}); 1160});// Alpine.js component for sticky header and mobile menu 1161function stickyHeader() { 1162return { 1163headerVisible: true, 1164searchOpen: false, 1165mobileSearchOpen: false, 1166mobileMenuOpen: false, 1167mobileMenuContent: '', 1168headerHeight: 0, 1169scrollThreshold: 100, 1170lastScrollY: 0, 1171scrollTimeout: null, 1172// Minimum pixels to scroll before detecting direction change 1173scrollDelta: 5,init() { 1174this.lastScrollY = this.getScrollY(); 1175this.updateHeaderHeight(); 1176window.addEventListener('scroll', () => this.handleScroll()); 1177window.addEventListener('resize', () => this.updateHeaderHeight()); 1178// Fired by children (promo banner dismiss) whose size change moves the page top 1179window.addEventListener('zephyr:header-resize', () => this.updateHeaderHeight()); 1180this.$watch('mobileMenuOpen', value => { 1181document.body.style.overflow = value ? 'hidden' : ''; 1182}); 1183},getScrollY() { 1184// Use multiple methods for compatibility (window.scrollY may be overridden by legacy JS) 1185return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 1186},handleScroll() { 1187const currentScrollY = this.getScrollY(); 1188const scrollDiff = currentScrollY - this.lastScrollY;// Clear existing timeout 1189if (this.scrollTimeout) { 1190clearTimeout(this.scrollTimeout); 1191}// Determine scroll direction and header visibility 1192if (currentScrollY <= this.scrollThreshold) { 1193// At top of page - always show header 1194this.setHeaderVisible(true); 1195this.lastScrollY = currentScrollY; 1196} else if (scrollDiff < -this.scrollDelta) { 1197// Scrolling up - show header immediately 1198this.setHeaderVisible(true); 1199this.lastScrollY = currentScrollY; 1200} else if (scrollDiff > this.scrollDelta) { 1201// Scrolling down - hide header immediately 1202this.setHeaderVisible(false); 1203this.lastScrollY = currentScrollY;// Show header again after user stops scrolling for 200ms 1204this.scrollTimeout = setTimeout(() => { 1205this.setHeaderVisible(true); 1206}, 200); 1207} 1208// If scrollDiff is between -scrollDelta and +scrollDelta, ignore (too small) 1209},setHeaderVisible(visible) { 1210this.headerVisible = visible; 1211// Sync with global store for other components (cart drawer, etc.) 1212Alpine.store('ui').headerVisible = visible; 1213},updateHeaderHeight() { 1214this.$nextTick(() => { 1215const header = this.$el.querySelector('header'); 1216if (header) { 1217this.headerHeight = header.offsetHeight; 1218// Update CSS variable for main content spacing 1219document.documentElement.style.setProperty('--header-height', this.headerHeight + 'px'); 1220// Sync with global store for other components 1221Alpine.store('ui').headerHeight = this.headerHeight; 1222} 1223}); 1224}
1224,loadMobileMenu() { 1225// Load mobile menu content from the navigation template 1226const mobileMenuElement = document.getElementById('mobile-menu-content'); 1227if (mobileMenuElement) { 1228this.mobileMenuContent = mobileMenuElement.innerHTML; 1229} 1230} 1231} 1232}</script>
1232
1232<script> (function() { 1233var header = document.querySelector('header.fixed'); 1234if (header) { 1235document.documentElement.style.setProperty('--header-height', header.offsetHeight + 'px'); 1236} 1237})(); </script>
1237 <main class="flex-1" style="padding-top: var(--header-height);"><div class="container mx-auto px-4"></div><div class="fixed left-0 right-0 z-40 pointer-events-none flex justify-center" style="top: var(--header-height, 80px);"><div class="w-full max-w-lg px-4 pt-4"><div class="pointer-events-auto"><div x-data x-show="$store.flash.messages.length > 0" x-cloak class="space-y-3"> <template x-for="msg in $store.flash.messages" :key="msg.id"><div x-show="msg.visible" 1238x-transition:enter="transition ease-out duration-300" 1239x-transition:enter-start="opacity-0 -translate-y-2" 1240x-transition:enter-end="opacity-100 translate-y-0" 1241x-transition:leave="transition ease-in duration-200" 1242x-transition:leave-start="opacity-100" 1243x-transition:leave-end="opacity-0" 1244role="alert" 1245:class="{ 1246'bg-green-50 border-green-400 text-green-800': msg.type === 'success', 1247'bg-red-50 border-red-400 text-red-800': msg.type === 'error', 1248'bg-yellow-50 border-yellow-400 text-yellow-800': msg.type === 'warning' || msg.type === 'notice', 1249'bg-blue-50 border-blue-400 text-blue-800': msg.type !== 'success' && msg.type !== 'error' && msg.type !== 'warning' && msg.type !== 'notice' 1250}" 1251class="border-l-4 rounded-lg p-4 shadow-lg flex items-start gap-3"><div class="shrink-0 mt-0.5"> <template x-if="msg.type === 'success'"> <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg> </template> <template x-if="msg.type === 'error'"> <svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg> </template> <template x-if="msg.type === 'warning' || msg.type === 'notice'"> <svg class="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg> </template></div><div class="flex-1 text-sm" x-html="msg.text"></div> <button @click="$store.flash.dismiss(msg.id)" class="shrink-0 ml-auto -mr-1 -mt-1 p-1 rounded hover:bg-black/5 transition-colors" aria-label="Dismiss"> <svg class="w-4 h-4 opacity-50" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg> </button></div> </template></div></div></div></div><div class="container mx-auto px-4"><div class="w-full"><div x-data="loginPage()" class="min-h-[60vh] flex flex-col items-center justify-center py-12 px-4"><h1 class="text-3xl font-bold text-gray-900 mb-8">Login or Create an Account</h1><div x-show="success" 1252x-cloak 1253x-transition:enter="transition ease-out duration-300" 1254x-transition:enter-start="opacity-0 transform scale-95" 1255x-transition:enter-end="opacity-100 transform scale-100" 1256class="bg-white rounded-lg shadow-lg w-full max-w-md p-8 flex flex-col items-center justify-center min-h-[400px]"><div class="flex justify-center mb-6"><div class="w-20 h-20 bg-brand-success/20 rounded-full flex items-center justify-center"> <svg class="w-12 h-12 text-brand-success" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path> </svg></div></div><h2 class="text-2xl font-bold text-gray-900 mb-2 text-center"> Success!</h2><p class="text-gray-600 text-center" x-text="successMessage"></p><p class="text-sm text-gray-500 mt-4">Redirecting...</p></div><div x-show="!success" class="w-full max-w-5xl grid md:grid-cols-2 gap-0 bg-white rounded-lg shadow-lg overflow-hidden"><div class="bg-gray-50 p-8 md:p-10 flex flex-col border-b md:border-b-0 md:border-r border-gray-200"><h2 class="text-2xl font-bold text-gray-900 mb-4"> New Customers</h2><p class="text-gray-600 mb-6 leading-relaxed"> By creating an account with our store, you will be able to move through the checkout process faster, store multiple shipping addresses, view and track y
1256our orders in your account and more.</p> <a href="https://www.simplystamps.com/customer/account/create/" 1257class="inline-flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover text-white font-medium py-2.5 px-6 rounded-md transition-colors self-start"> Create an Account </a></div><div class="p-8 md:p-10"><div x-show="currentForm === 'login'"><h2 class="text-2xl font-bold text-gray-900 mb-2"> Registered Customers</h2><p class="text-sm text-gray-600 mb-4"> If you have an account with us, please log in.</p><div x-show="message" x-cloak class="mb-4"><div x-text="message" 1258:class="messageType === 'error' ? 'bg-brand-danger/10 text-brand-danger border-brand-danger/30' : 'bg-brand-success/10 text-brand-success border-brand-success/30'" 1259class="p-4 rounded-md border text-sm"></div></div><form @submit.prevent="submitLogin" id="login-form"> <input type="hidden" name="form_key" :value="formKey" /><div class="space-y-4"><div> <label for="login-email" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-brand-danger">*</span> </label> <input type="email" 1260id="login-email" 1261name="login[username]" 1262x-model="loginForm.username" 1263required 1264class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="login-password" class="block text-sm font-medium text-gray-700 mb-1"> Password <span class="text-brand-danger">*</span> </label> <input type="password" 1265id="login-password" 1266name="login[password]" 1267x-model="loginForm.password" 1268required 1269class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div class="flex items-center"> <input type="checkbox" 1270id="login-remember" 1271name="persistent_remember_me" 1272x-model="loginForm.rememberMe" 1273class="w-4 h-4 text-brand-primary border-gray-300 rounded focus:ring-brand-primary"> <label for="login-remember" class="ml-2 text-sm text-gray-700"> Remember Me </label></div></div><div class="mt-6 space-y-3"> <button type="submit" 1274:disabled="loading" 1275class="w-full h-[42px] flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover disabled:bg-brand-primary/70 text-white font-medium py-2 px-4 rounded-md transition-colors"> <span x-show="!loading">Log In</span> <span x-show="loading" x-cloak class="flex items-center justify-center"> <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> </span> </button></div><div class="mt-4 text-center"> <button type="button" 1276@click="switchForm('forgot')" 1277class="text-sm text-brand-primary hover:text-brand-primary-hover transition-colors"> Forgot Your Password? </button></div></form></div><div x-show="currentForm === 'forgot'" x-cloak><h2 class="text-2xl font-bold text-gray-900 mb-2"> Forgot Your Password?</h2><p class="text-sm text-gray-600 mb-4"> Enter your email address below and we will send you a link to reset your password.</p><div x-show="message" x-cloak class="mb-4"><div x-text="message" 1278:class="messageType === 'error' ? 'bg-brand-danger/10 text-brand-danger border-brand-danger/30' : 'bg-brand-success/10 text-brand-success border-brand-success/30'" 1279class="p-4 rounded-md border text-sm"></div></div><form @submit.prevent="submitForgot" id="forgot-form"> <input type="hidden" name="form_key" :value="formKey" /><div class="space-y-4"><div> <label for="forgot-email" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-brand-danger">*</span> </label> <input type="email" 1280id="forgot-email" 1281name="email" 1282x-model="forgotForm.email" 1283required 1284class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div></div><div class="mt-6 space-y-3"> <button type="submit" 1285:disabled="loading" 1286class="w-full h-[42px] flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover disabled:bg-brand-primary/70 text-white font-medium py-2 px-4 roun
1286ded-md transition-colors"> <span x-show="!loading">Submit</span> <span x-show="loading" x-cloak class="flex items-center justify-center"> <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> </span> </button><button type="button" 1287@click="switchForm('login')" 1288class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 rounded-md transition-colors"> Back to Log In </button></div></form></div></div></div></div>
1288<script> function loginPage() { 1289return { 1290currentForm: 'login', 1291isFullPage: true, 1292redirectUrl: 'https://www.simplystamps.com/customer/account/', 1293loading: false, 1294success: false, 1295successMessage: '', 1296message: '', 1297messageType: 'error', 1298formKey: 'e3374479ab86b5888526972805eb3d8f',loginForm: { 1299username: '', 1300password: '', 1301rememberMe: false 1302},forgotForm: { 1303email: '' 1304},switchForm(formName) { 1305this.currentForm = formName; 1306this.message = ''; 1307this.loading = false; 1308this.success = false; 1309},handleSuccess() { 1310window.location.href = this.redirectUrl; 1311},async submitLogin() { 1312this.loading = true; 1313this.message = '';const formData = new FormData(); 1314formData.append('form_key', this.formKey); 1315formData.append('login[username]', this.loginForm.username); 1316formData.append('login[password]', this.loginForm.password); 1317if (this.loginForm.rememberMe) { 1318formData.append('persistent_remember_me', 'on'); 1319}try { 1320const response = await fetch('https://www.simplystamps.com/zephyr/auth/login/', { 1321method: 'POST', 1322body: formData, 1323credentials: 'same-origin' 1324});const data = await response.json();if (data.success) { 1325if (typeof zGA4 === 'function') { 1326zGA4('login', { method: 'site' }); 1327if (data.hashedUserData) { 1328zGA4('set_user_data', { user_data: data.hashedUserData }); 1329} 1330}this.loading = false; 1331this.success = true; 1332this.successMessage = 'You are now logged in.';if (typeof Alpine !== 'undefined' && Alpine.store('auth')) { 1333Alpine.store('auth').setLoggedIn(data.customer || {}); 1334}if (typeof TheDesigner !== 'undefined' && TheDesigner.ProductDesigner) { 1335TheDesigner.ProductDesigner.config.isCustomerLoggedIn = true; 1336if (typeof TheDesigner.ImageUploader !== 'undefined') { 1337TheDesigner.ImageUploader.getUserImages(); 1338} 1339}await new Promise(resolve => setTimeout(resolve, 1500)); 1340this.handleSuccess(); 1341} else { 1342await new Promise(resolve => setTimeout(resolve, 800)); 1343this.message = data.error || 'Invalid login or password.'; 1344this.messageType = 'error'; 1345this.loading = false; 1346} 1347} catch (error) { 1348await new Promise(resolve => setTimeout(resolve, 800)); 1349this.message = 'An error occurred. Please try again.'; 1350this.messageType = 'error'; 1351this.loading = false; 1352} 1353},async submitForgot() { 1354this.loading = true; 1355this.message = '';const formData = new FormData(); 1356formData.append('form_key', this.formKey); 1357formData.append('email', this.forgotForm.email);// reCAPTCHA v3. The forgotpasswordpost endpoint rejects a token-less post 1358// outright, so no token means no reset email. 1359const recaptchaToken = await getZephyrRecaptchaToken('forgot'); 1360if (recaptchaToken) { 1361formData.append('recaptcha_response', recaptchaToken); 1362}try { 1363const response = await fetch('https://www.simplystamps.com/customer/account/forgotpasswordpost/', { 1364method: 'POST', 1365body: formData, 1366credentials: 'same-origin' 1367});const text = await response.text();if (text.includes('success-msg') || text.includes('password-reset')) { 1368this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 1369this.messageType = 'success'; 1370this.forgotForm.email = ''; 1371} else if (text.includes('error-msg')) { 1372const parser = new DOMParser(); 1373const doc = parser.parseFromString(text, 'text/html'); 1374const errorMsg = doc.querySelector('.error-msg'); 1375this.message = errorMsg ? errorMsg.textContent.trim() : 'Unable to send password reset email.'; 1376this.messageType = 'error'; 1377} else { 1378this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 1379this.messageType = 'success'; 1380} 1381} catch (error) { 1382this.message = 'An error occurred. Please try again.'; 1383this.messageType = 'error'; 1384} finally { 1385this.loading = false; 1386} 1387} 1388} 1389} </script>
1389</div></div> </main> <footer id="site-footer" class="w-full"><div class="relative"><div class="widget widget-static-block"><style> /* ââ SimplyStamps Shipping ETA Timer ââ */ 1390/* v4.0 â Unified copy, color does the talking */ 1391.ss-ship-eta { 1392display: none; 1393flex-direction: column; 1394gap: 5px; 1395padding: 12px 16px; 1396border-radius: 8px 8px 0px 0px; 1397background: #eef7ff; 1398width: 100%; 1399box-sizing: border-box; 1400margin-top: 0; 1401margin-bottom: 0px; 1402border: 1px solid #d1e9ff; 1403transition: background 0.35s ease, border-color 0.35s ease; 1404font-family: inherit; 1405} 1406.ss-ship-eta__topline { 1407font-size: 14px; 1408font-weight: 600; 1409color: #374151; 1410line-height: 1.4; 1411margin: 0; 1412} 1413.ss-ship-eta__hero { 1414font-size: 14px; 1415color: #374151; 1416line-height: 1.5; 1417} 1418.ss-ship-eta__hero-date { 1419font-weight: 700; 1420} 1421.ss-ship-eta__hero-tier { 1422font-size: 12px; 1423color: #9ca3af; 1424margin-left: 6px; 1425} 1426.ss-ship-eta__toggle { 1427background: none; 1428border: none; 1429padding: 0; 1430margin-top: 3px; 1431font-size: 12px; 1432font-weight: 600; 1433color: #1d4ed8; 1434cursor: pointer; 1435display: inline-flex; 1436align-items: center; 1437gap: 3px; 1438font-family: inherit; 1439} 1440.ss-ship-eta__toggle:hover { text-decoration: underline; } 1441.ss-ship-eta__toggle-arrow { 1442font-size: 10px; 1443transition: transform 0.2s ease; 1444display: inline-block; 1445} 1446.ss-ship-eta__toggle-arrow.ss-open { 1447transform: rotate(180deg); 1448} 1449.ss-ship-eta__more { 1450display: none; 1451flex-direction: column; 1452gap: 1px; 1453margin-top: 4px; 1454font-size: 14px; 1455color: #374151; 1456line-height: 1.5; 1457} 1458.ss-ship-eta__more.ss-visible { 1459display: flex; 1460} 1461.ss-ship-eta__more-row { 1462display: flex; 1463align-items: baseline; 1464gap: 6px; 1465} 1466.ss-ship-eta__more-label { 1467font-size: 12px; 1468font-weight: 500; 1469color: #9ca3af; 1470min-width: 52px; 1471} 1472.ss-ship-eta__more-date { 1473font-weight: 500; 1474} 1475.ss-eta-cd { 1476font-weight: 800; 1477color: #1d4ed8; 1478} 1479/* ââ Warn state ââ */ 1480.ss-ship-eta.ss-warn { 1481background: #fffbeb; 1482border-color: #fde68a; 1483} 1484.ss-ship-eta.ss-warn .ss-ship-eta__topline, 1485.ss-ship-eta.ss-warn .ss-ship-eta__hero { color: #78350f; } 1486.ss-ship-eta.ss-warn .ss-ship-eta__more { color: #78350f; } 1487.ss-ship-eta.ss-warn .ss-eta-cd { color: #d97706; } 1488.ss-ship-eta.ss-warn .ss-ship-eta__toggle { color: #b45309; } 1489/* ââ Urgent state ââ */ 1490.ss-ship-eta.ss-urgent { 1491background: #fff1f2; 1492border-color: #fecdd3; 1493} 1494.ss-ship-eta.ss-urgent .ss-ship-eta__topline, 1495.ss-ship-eta.ss-urgent .ss-ship-eta__hero { color: #7f1d1d; } 1496.ss-ship-eta.ss-urgent .ss-ship-eta__more { color: #7f1d1d; } 1497.ss-ship-eta.ss-urgent .ss-eta-cd { 1498color: #dc2626; 1499animation: ss-pulse 1.1s ease-in-out 5; 1500} 1501.ss-ship-eta.ss-urgent .ss-ship-eta__toggle { color: #dc2626; } 1502@keyframes ss-pulse { 15030%, 100% { opacity: 1; } 150450% { opacity: 0.5; } 1505} 1506/* ââ After-cutoff state ââ */ 1507.ss-ship-eta.ss-after { 1508background: #f9fafb; 1509border-color: #e5e7eb; 1510} 1511.ss-ship-eta.ss-after .ss-ship-eta__topline, 1512.ss-ship-eta.ss-after .ss-ship-eta__hero { color: #6b7280; } 1513.ss-ship-eta.ss-after .ss-ship-eta__more { color: #6b7280; } 1514.ss-ship-eta.ss-after .ss-ship-eta__toggle { color: #6b7280; } </style>
1514<script> (() => { 1515var TIMEZONE = "America/New_York"; 1516var CUTOFF_HOUR_ET = 17; 1517var CUTOFF_MINUTE_ET = 0; 1518var HOLIDAYS_MMDD = new Set([ 1519"01/01","01/19","02/16","05/25","07/03","09/07", 1520"11/26","11/27","12/24","12/25","12/31" 1521]); 1522var OFFSETS = { rush: 2, express: 3, standard: 5, economy: 6 }; 1523var STD_LABEL = "Preferred"; 1524var SHIPPING_CONTAINER_SELECTOR = '.ships-and-made-in-usa-container'; 1525var FALLBACK_PARENT_SELECTOR = '.vpt-product'; 1526var PFX = "ss"; 1527function createTimerEl() { 1528var div = document.createElement("div"); 1529div.className = PFX + "-ship-eta"; 1530div.setAttribute("aria-label", "Delivery estimates"); 1531div.innerHTML = 1532'<div class="' + PFX + '-ship-eta__topline"></div>' + 1533'<div class="' + PFX + '-ship-eta__hero"></div>' + 1534'<button class="' + PFX + '-ship-eta__toggle" type="button" aria-expanded="false">' + 1535'All shipping options <span class="' + PFX + '-ship-eta__toggle-arrow">\u25BC</span>' + 1536'</button>' + 1537'<div class="' + PFX + '-ship-eta__more">' + 1538'<div class="' + PFX + '-ship-eta__more-row" data-tier="express"></div>' + 1539'<div class="' + PFX + '-ship-eta__more-row" data-tier="standard"></div>' + 1540'<div class="' + PFX + '-ship-eta__more-row" data-tier="economy"></div>' + 1541'</div>'; 1542var toggleEl = div.querySelector('.' + PFX + '-ship-eta__toggle'); 1543var arrowEl = div.querySelector('.' + PFX + '-ship-eta__toggle-arrow'); 1544var moreEl = div.querySelector('.' + PFX + '-ship-eta__more'); 1545var expanded = false; 1546toggleEl.addEventListener("click", function() { 1547expanded = !expanded; 1548moreEl.classList.toggle(PFX + "-visible", expanded); 1549arrowEl.classList.toggle(PFX + "-open", expanded); 1550toggleEl.setAttribute("aria-expanded", expanded); 1551toggleEl.firstChild.textContent = expanded ? "Hide options " : "All shipping options "; 1552}); 1553return div; 1554} 1555function getEasternParts(date) { 1556date = date || new Date(); 1557var fmt = new Intl.DateTimeFormat("en-US", { 1558timeZone: TIMEZONE, 1559year: "numeric", month: "2-digit", day: "2-digit", 1560hour: "2-digit", minute: "2-digit", second: "2-digit", 1561hour12: false 1562}); 1563var out = {}; 1564var parts = fmt.formatToParts(date); 1565for (var i = 0; i < parts.length; i++) out[parts[i].type] = parts[i].value; 1566return out; 1567} 1568function getNowET() { 1569var p = getEasternParts(); 1570return new Date(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second); 1571} 1572function mmdd(d) { 1573return String(d.getMonth()+1).padStart(2,"0") + "/" + String(d.getDate()).padStart(2,"0"); 1574} 1575function isWeekend(d) { var w = d.getDay(); return w === 0 || w === 6; } 1576function isHoliday(d) { return HOLIDAYS_MMDD.has(mmdd(d)); } 1577function isBusinessDay(d) { return !isWeekend(d) && !isHoliday(d); } 1578function startOfDay(d) { var x = new Date(d); x.setHours(0,0,0,0); return x; } 1579function nextDay(d) { var x = new Date(d); x.setDate(x.getDate()+1); return x; } 1580function nextBusinessDay(from) { 1581var x = nextDay(from); 1582while (!isBusinessDay(x)) x = nextDay(x); 1583return startOfDay(x); 1584} 1585function addBusinessDays(from, n) { 1586var x = startOfDay(from); 1587var added = 0; 1588while (added < n) { x = nextDay(x); if (isBusinessDay(x)) added++; } 1589return x; 1590} 1591function formatDate(d) { 1592return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "
1592numeric" }); 1593} 1594function countdownStr(diffMs) { 1595var totalSecs = Math.max(0, Math.floor(diffMs / 1000)); 1596var totalMins = Math.floor(totalSecs / 60); 1597var hrs = Math.floor(totalSecs / 3600); 1598var mins = Math.floor((totalSecs % 3600) / 60); 1599var secs = totalSecs % 60; 1600if (totalMins < 5) { 1601return hrs + "h " + String(mins).padStart(2,"0") + "m " + String(secs).padStart(2,"0") + "s"; 1602} 1603return hrs + " hr" + (hrs !== 1 ? "s" : "") + " " + String(mins).padStart(2,"0") + " min"; 1604} 1605var instances = []; 1606function ensureInstances() { 1607var containers = document.querySelectorAll(SHIPPING_CONTAINER_SELECTOR); 1608var placed = false; 1609for (var i = 0; i < containers.length; i++) { 1610var container = containers[i]; 1611var prev = container.previousElementSibling; 1612if (prev && prev.classList.contains(PFX + "-ship-eta")) { 1613placed = true; 1614continue; 1615} 1616var timer = createTimerEl(); 1617container.insertAdjacentElement("beforebegin", timer); 1618instances.push(timer); 1619placed = true; 1620} 1621if (!placed) { 1622var fallback = document.querySelector(FALLBACK_PARENT_SELECTOR); 1623if (fallback) { 1624var first = fallback.firstElementChild; 1625if (!first || !first.classList.contains(PFX + "-ship-eta")) { 1626var timer = createTimerEl(); 1627fallback.insertAdjacentElement("afterbegin", timer); 1628instances.push(timer); 1629placed = true; 1630} 1631} 1632} 1633return placed; 1634} 1635function renderAll() { 1636var nowET = getNowET(); 1637var cutoff = new Date(nowET); 1638cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0); 1639var diffMs = cutoff - nowET; 1640var totalMins = Math.floor(diffMs / 60000); 1641var isPastCutoff = nowET >= cutoff; 1642var isOffDay = !isBusinessDay(nowET); 1643var urgencyClass = null; 1644var toplineHTML = ""; 1645var base; 1646if (isPastCutoff || isOffDay) { 1647urgencyClass = PFX + "-after"; 1648base = nextBusinessDay(nowET); 1649toplineHTML = 'Get as early as:'; 1650} else { 1651var cdStr = countdownStr(diffMs); 1652base = startOfDay(nowET); 1653toplineHTML = 'Order in <span class="' + PFX + '-eta-cd">' + cdStr + '</span> to get as early as:'; 1654if (totalMins <= 120) { 1655urgencyClass = PFX + "-urgent"; 1656} else if (totalMins <= 240) { 1657urgencyClass = PFX + "-warn"; 1658} 1659} 1660var rushDate = addBusinessDays(base, OFFSETS.rush); 1661var expressDate = addBusinessDays(base, OFFSETS.express); 1662var stdDate = addBusinessDays(base, OFFSETS.standard); 1663var ecoDate = addBusinessDays(base, OFFSETS.economy); 1664var heroHTML = '<span class="' + PFX + '-ship-eta__hero-date">' + formatDate(rushDate) + '</span><span class="' + PFX + '-ship-eta__hero-tier">via Rush</span>'; 1665var expressHTML = '<span class="' + PFX + '-ship-eta__more-label">Express</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(expressDate) + '</span>'; 1666var stdHTML = '<span class="' + PFX + '-ship-eta__more-label">' + STD_LABEL + '</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(stdDate) + '</span>'; 1667var ecoHTML = '<span class="' + PFX + '-ship-eta__more-label">Economy</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(ecoDate) + '</span>'; 1668for (var i = 0; i < instances.length; i++) { 1669var el = instances[i]; 1670el.classList.remove(PFX + "-warn", PFX + "-urgent", PFX + "-after"); 1671if (urgencyClass) el.classList.add(urgencyClass); 1672el.querySelector('.' + PFX + '-ship-eta__topline').innerHTML = toplineHTML; 1673el.querySelector('.' + PFX + '-ship-eta__hero').innerHTML = heroHTML; 1674el.querySelector('[data-tier="express"]').innerHTML = expressHTML; 1675el.querySelector('[data-tier="standard"]').innerHTML = stdHTML; 1676el.querySelector('[data-tier="economy"]').innerHTML = ecoHTML; 1677el.style.display = "flex"; 1678} 1679} 1680function tick() { 1681var placed = ensureInstances(); 1682if (placed && instances.length > 0) renderAll(); 1683var nowET = getNowET(); 1684var cutoff = new Date(nowET); 1685cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0); 1686var minsLeft = Math.floor((cutoff - nowET) / 60000); 1687var interval = !placed ? 250 1688: (minsLeft >= 0 && minsLeft < 5) ? 1000 1689: 60000; 1690setTimeout(tick, interval); 1691} 1692tick(); 1693if (window.MutationObserver) { 1694var mo = new MutationObserver(function() { ensureInstances(); }); 1695mo.observe(document.body, { childList: true, subtree: true }); 1696} 1697})(); 1698</script>
1698</div> <style> .full-size-container.footer-marketing { 1699background-color: #60b5e5; 1700width: 100vw; 1701margin-top: 30px; 1702} </style> <style> .CustomerServiceNotice img { 1703width: 100%; 1704margin-bottom: 2%; 1705} 1706.CustomerServiceNotice .modal-footer { 1707display: none; 1708} </style></div> <section id="zephyr-signup-band" 1709class="relative w-full py-10 sm:py-12 bg-brand-primary text-white" 1710x-data="zephyrSignupBand()" 1711aria-labelledby="zephyr-signup-headline"><div class="relative max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><div x-ref="stage" 1712class="relative flex flex-col justify-center"><div x-ref="panelOffers" 1713class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1714:class="view === 'offers' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'" 1715:aria-hidden="view !== 'offers'"><p id="zephyr-signup-headline" class="text-3xl sm:text-4xl font-bold text-center"> Save On Your Order Today & On Future Deals!</p><div class="mt-8 sm:mt-10 grid gap-4 sm:gap-8 sm:grid-cols-2 max-w-5xl mx-auto items-stretch"><div class="group relative bg-white rounded-2xl shadow-xl p-6 pt-10 sm:p-8 sm:pt-12 flex flex-col text-center transition-transform duration-200 hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:translate-y-0 ring-4 ring-brand-success lg:scale-[1.03]"> <span class="absolute -top-4 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-brand-success px-5 py-1.5 text-sm font-bold uppercase tracking-widest text-white shadow-md"> Most Popular! </span><div class="flex flex-col-reverse items-center gap-4 text-center sm:block sm:gap-0"><p class="text-sm font-semibold uppercase tracking-wider text-gray-500"> Subscribe To Email + Text and get</p><p class="text-4xl sm:text-6xl font-extrabold leading-none sm:mt-4 text-brand-success"> 15% Off</p></div><ul class="mt-5 sm:mt-6 space-y-2 w-fit max-w-full mx-auto text-left text-sm text-gray-700"><li class="flex items-start gap-2"> <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Everything in the email list</span></li><li class="flex items-start gap-2"> <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Early access to promotions</span></li><li class="flex items-start gap-2"> <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Text-only flash sales</span></li></ul><div class="mt-auto pt-5 sm:pt-6"> <button type="button" 1716@click="choose('email_sms')" 1717class="w-full min-h-[56px] bg-brand-primary hover:bg-brand-primary-hover text-white text-base font-semibold px-4 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-primary"> Subscribe To Email + Text </button></div></div><div class="group bg-white rounded-2xl border border-gray-200 shadow-sm p-6 sm:p-8 flex flex-col text-center transition-transform duration-200 hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:translate-y-0"><div class="flex flex-col-reverse items-center gap-4 text-center sm:block sm:gap-0"><p class="text-sm font-semibold uppercase tracking-wider text-gray-500"> Subscribe to Email Only and get</p><p class="text-4xl sm:text-6xl font-extrabold leading-none text-brand-primary sm:mt-4"> 10% Off</p></div><ul class="mt-5 sm:mt-6 space-y-2 w-fit max-w-full mx-auto text-left text-sm text-gray-700"><li class="flex items-start gap-2"> <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Exclusive subscriber-only deals</span></li><li class="flex items-start gap-2">
1717 <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Early access to promotions</span></li></ul><div class="mt-auto pt-5 sm:pt-6"> <button type="button" 1718@click="choose('email')" 1719class="w-full min-h-[56px] border-2 border-brand-primary text-brand-primary hover:bg-brand-primary hover:text-white text-base font-semibold px-4 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-primary"> Subscribe To Email List </button></div></div></div><p class="mt-10 text-center text-xs leading-relaxed opacity-90 max-w-2xl mx-auto"> One use per email address/cell-phone number. By signing up, you agree to our <a href="/terms-and-conditions" class="text-inherit underline hover:no-underline">Terms and Conditions</a> & <a href="/privacy-policy" class="text-inherit underline hover:no-underline">Privacy Policy</a></p></div><div x-ref="panelForm" 1720x-cloak 1721class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1722:class="view === 'email' || view === 'email_sms' 1723? 'relative opacity-100 visible' 1724: 'absolute inset-x-0 top-0 opacity-0 invisible'" 1725:aria-hidden="!(view === 'email' || view === 'email_sms')"><div class="max-w-2xl mx-auto"><div class="bg-white rounded-xl shadow-sm p-6 sm:p-8 text-gray-900"><p class="text-xl sm:text-2xl font-bold text-center" x-text="headingText()"></p><p class="mt-2 text-sm text-gray-600 text-center" x-text="subheadingText()"></p><div x-show="errorMessage" 1726x-transition 1727role="alert" 1728aria-live="polite" 1729class="mt-5 p-4 bg-red-50 border border-red-200 rounded-lg"><p class="text-sm text-brand-danger" x-text="errorMessage"></p></div><form @submit.prevent="submit()" class="mt-6 space-y-5" novalidate><div> <label for="zephyr-signup-email" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-brand-danger">*</span> </label> <input type="email" 1730id="zephyr-signup-email" 1731x-model="email" 1732required 1733autocomplete="email" 1734placeholder="[email protected]" 1735class="w-full px-4 py-2 min-h-[44px] border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div x-show="view === 'email_sms' || measuring" x-cloak> <label for="zephyr-signup-phone" class="block text-sm font-medium text-gray-700 mb-1"> Mobile Number <span class="text-brand-danger">*</span> </label><div class="flex"> <span class="inline-flex items-center px-3 rounded-l-lg border border-r-0 border-gray-300 bg-gray-50 text-sm text-gray-600 select-none whitespace-nowrap" 1736aria-hidden="true">US +1</span> <input type="tel" 1737id="zephyr-signup-phone" 1738x-model="phone" 1739autocomplete="tel-national" 1740inputmode="tel" 1741placeholder="(555) 555-5555" 1742aria-describedby="zephyr-signup-phone-hint" 1743class="w-full px-4 py-2 min-h-[44px] border border-gray-300 rounded-r-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><p id="zephyr-signup-phone-hint" class="mt-1 text-xs text-gray-500"> US mobile numbers only. Landlines cannot receive texts.</p></div><div x-show="view === 'email_sms' || measuring" x-cloak class="flex gap-3"> <input type="checkbox" 1744id="zephyr-signup-consent" 1745x-model="consent" 1746class="mt-0.5 h-5 w-5 shrink-0 rounded border-gray-300 text-brand-primary focus:ring-brand-primary"> <label for="zephyr-signup-consent" class="text-xs leading-relaxed text-gray-600"> By checking this box, you agree to receive recurring automated promotional and personalized marketing text messages (e.g., cart reminders) from Simply Stamps at the mobile number provided at signup. Consent is not a condition of purchase. Reply HELP for help and STOP to cancel. Message frequency may vary. Message and data rates may apply. <a href="/terms-and-conditions" class="underline hover:no-underline">Terms and Conditions</a> & <a href="/privacy-policy" class="underline hover:no-underline">Privacy Policy</a> </label></div><div class="flex flex-col sm:flex-row sm:items-center gap-4 pt-1"> <button type="submit" 1747:disabled="loading" 1748class="w-full sm:w-auto min-h-[48px] px-6 bg-brand-primary hover:bg-brand-primary-hover disabled:opacity-60 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-primary inline-flex items-center justify-center"> <span x-show="!loading" x-text="submitText()"></span> <span x-show="loading" x-cloak class="inline-flex items-center gap-2"> <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path> </svg> Signing up... </span> </button><button type="button" 1749@click="back()"
1750class="text-sm text-gray-600 underline hover:no-underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-brand-primary rounded"> Back to both offers </button></div></form></div></div></div><div x-ref="panelDone" 1751x-cloak 1752class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1753:class="view === 'done' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'" 1754:aria-hidden="view !== 'done'"><div class="bg-white rounded-xl shadow-sm p-8 text-center text-gray-900 max-w-2xl mx-auto" role="status" aria-live="polite"> <svg class="mx-auto h-12 w-12 text-brand-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path> </svg><p class="mt-4 text-xl font-bold">You're in!</p><p class="mt-2 text-sm text-gray-600" x-text="successMessage"></p></div></div></div></div> </section>
1754<script> /* 1755* Hide the band synchronously, before paint, for anyone who has already signed 1756* up. This runs inline rather than through Alpine on purpose: the footer is 1757* served from the Fastly cache, so the cookie is the only signal available, and 1758* Alpine boots after first paint â gating on x-show alone would render the band 1759* and then yank it, shifting the footer. 1760*/ 1761(function () { 1762if (document.cookie.indexOf('zephyr_signup=') !== -1) { 1763var band = document.getElementById('zephyr-signup-band'); 1764if (band) { 1765band.parentNode.removeChild(band); 1766} 1767} 1768})();function zephyrSignupBand() { 1769return { 1770view: 'offers', 1771email: '', 1772phone: '', 1773consent: false, 1774loading: false, 1775errorMessage: '', 1776successMessage: '', 1777postUrl: 'https://www.simplystamps.com/zephyr/signup/post/', 1778hideDays: 365,/* Per-offer copy, so the two form states share one set of markup. */ 1779copy: { 1780email: { 1781heading: 'Save 10% - Subscribe to Email Only', 1782subheading: 'Please enter your email below to receive your discount code!', 1783submit: 'Get My Discount Code' 1784}, 1785email_sms: { 1786heading: 'Save 15% - Subscribe to Email + Text', 1787subheading: 'Please enter your email address and your phone number to receive the 20% off discount code.', 1788submit: 'Get my 15% Off Now!' 1789} 1790},/* 1791* While `measuring` is on, the form panel renders in its tallest 1792* configuration so it can be sized. See measureTallest(). 1793*/ 1794formCopy: function () { 1795return this.copy[this.view] 1796|| (this.measuring ? this.copy.email_sms : null); 1797},headingText: function () { 1798var copy = this.formCopy(); 1799return copy ? copy.heading : ''; 1800},subheadingText: function () { 1801var copy = this.formCopy(); 1802return copy ? copy.subheading : ''; 1803},submitText: function () { 1804var copy = this.formCopy(); 1805return copy ? copy.submit : ''; 1806},measuring: false,init: function () { 1807this.measureTallest();// Panel heights change with the breakpoint, so re-pin on resize. 1808var self = this; 1809var timer = null; 1810window.addEventListener('resize', function () { 1811clearTimeout(timer); 1812timer = setTimeout(function () { self.measureTallest(); }, 150); 1813}); 1814},/* 1815* Pin the stage to the tallest panel so switching states moves nothing 1816* on the page at all. 1817* 1818* The form panel has to be measured in its email+SMS configuration â 1819* its phone and consent blocks are x-show'd off in the email-only view, 1820* and its heading is bound to the current view, so it otherwise reports 1821* a short height. `measuring` forces that configuration on, and because 1822* an inactive panel is `absolute` + `invisible` (never display:none) it 1823* is measurable without ever being seen. 1824* 1825* Skipped while a form is on screen: `measuring` would briefly reveal 1826* the phone and consent fields in the email-only view. The pin just 1827* stays as-is until the visitor returns to the offers. 1828*/ 1829measureTallest: function () { 1830var stage = this.$refs.stage; 1831if (!stage || this.view === 'email' || this.view === 'email_sms') { 1832return; 1833}var self = this; 1834this.measuring = true;this.$nextTick(function () { 1835var tallest = 0; 1836['panelOffers', 'panelForm', 'panelDone'].forEach(function (ref) { 1837var panel = self.$refs[ref]; 1838if (panel) { 1839tallest = Math.max(tallest, panel.offsetHeight); 1840} 1841});self.measuring = false; 1842if (tallest) { 1843stage.style.minHeight = tallest + 'px'; 1844} 1845}); 1846},choose: function (which) { 1847this.errorMessage = ''; 1848this.view = which;// After the DOM updates, not before: the field sits in a panel that 1849// is still `invisible` at this point, and focus() is a silent no-op 1850// on a hidden element. 1851var self = this; 1852this.$nextTick(function () { 1853// Both offers open the same panel, so email is always first. 1854self.focusLater('zephyr-signup-email'); 1855}); 1856},back: function () { 1857this.errorMessage = ''; 1858this.view = 'offers'; 1859},/* Move focus to the first field without yanking the page around. */ 1860focusLater: function (focusId) { 1861if (!focusId) { 1862return; 1863} 1864var el = document.getElementById(focusId); 1865if (!el) { 1866return; 1867} 1868try { 1869el.focus({ preventScroll: true }); 1870} catch (e) { 1871el.focus(); 1872} 1873},submit: function () { 1874var self = this; 1875if (this.loading) { 1876return; 1877}// Client-side checks mirror the server's; the server is still the 1878// authority, these just save a round trip. 1879if (!this.email || this.email.indexOf('@') === -1) { 1880this.errorMessage = 'Enter a valid email address.'; 1881return; 1882} 1883if (this.view === 'email_sms') { 1884if (this.phone.replace(/[^0-9]/g, '').length < 10) { 1885this.errorMessage = 'Enter a valid 10-digit US mobile number.'; 1886return; 1887} 1888if (!this.consent) { 1889this.errorMessage = 'Check the consent box to receive text messages.'; 1890return; 1891} 1892}this.loading = true; 1893this.errorMessage = '';var body = new FormData(); 1894body.append('email', this.email); 1895body.append('signup_type', this.view); 1896if (this.view === 'email_sms') { 1897body.append('phone', this.phone); 1898body.append('sms_consent', this.consent ? '1' : '0'); 1899}var send = function (token) { 1900if (token) { 1901body.append('recaptcha_response', token); 1902}fetch(self.postUrl, { 1903method: 'POST', 1904body: body, 1905credentials: 'same-origin' 1906}).then(function (response) { 1907return response.json(); 1908}).then(function (data) { 1909if (data && data.success) { 1910self.successMessage = data.message || ''; 1911self.view = 'done'; 1912self.setHideCookie(data.hide_days); 1913if (typeof zGA4 === 'function') { 1914zGA4('generate_lead', {}); 1915} 1916} else { 1917self.errorMessage = (data && data.error) 1918? data.error 1919: 'We could not complete your signup. Please try again in a moment.'; 1920} 1921}).catch(function () { 1922self.errorMessage = 'We could not complete your signup. Please try again in a moment.'; 1923}).then(function () { 1924self.loading = false;
1925}); 1926};if (typeof window.getZephyrRecaptchaToken === 'function') { 1927window.getZephyrRecaptchaToken('signup').then(send, function () { send(null); }); 1928} else { 1929send(null); 1930} 1931},/* Keeps the band hidden on later visits. 0 days means always show it. */ 1932setHideCookie: function (days) { 1933var lifetime = typeof days === 'number' ? days : this.hideDays; 1934if (!lifetime || lifetime < 1) { 1935return; 1936} 1937var expires = new Date(); 1938expires.setTime(expires.getTime() + (lifetime * 24 * 60 * 60 * 1000)); 1939document.cookie = 'zephyr_signup=1; expires=' + expires.toUTCString() 1940+ '; path=/; SameSite=Lax' 1941+ (location.protocol === 'https:' ? '; Secure' : ''); 1942} 1943}; 1944} 1945</script>
1945<div class="relative"><div class="widget widget-static-block"></div> <!-On PDP FAQ Accordion Compatibility Shim--><div class="widget widget-static-block"><style> /* --------------------------------------------------------------------------- 1946FAQ blocks â standalone styles for the legacy Bootstrap panel markup. 1947Pairs with faq-collapse.js. No Bootstrap, no dependencies. 1948Scoped to .faq_block / .faq_table so nothing else on the page inherits 1949these rules. Retune the custom properties below to match the theme. 1950--------------------------------------------------------------------------- */ 1951.faq_block { 1952--faq-border: #dcdcdc; 1953--faq-radius: 6px; 1954--faq-gap: 10px; 1955--faq-surface: #fff; 1956--faq-heading: #f7f7f7; 1957--faq-heading-hover: #efefef; 1958--faq-focus: #2563eb; 1959--faq-mark: #ffe9a8; 1960--faq-muted: #616161; 1961line-height: 1.55; 1962} 1963/* --- neutralize the layout table ---------------------------------------- */ 1964.faq_block .faq_table, 1965.faq_block .faq_table > tbody, 1966.faq_block .faq_table > tbody > tr, 1967.faq_block .faq_table > tbody > tr > td { 1968display: block; 1969width: 100%; 1970max-width: 100%; 1971margin: 0; 1972padding: 0; 1973border: 0; 1974background: none; 1975} 1976.faq_block .faq_table > tbody > tr > td + td { 1977margin-top: var(--faq-gap); 1978} 1979/* the intro row holds the section heading, not a panel */ 1980.faq_block .faq_table h3 { 1981margin: 0 0 calc(var(--faq-gap) * 1.5); 1982font-size: 1.5em; 1983line-height: 1.25; 1984} 1985/* --- panel ---------------------------------------------------------------- */ 1986.faq_block .panel { 1987margin: 0 0 var(--faq-gap); 1988background: var(--faq-surface); 1989border: 1px solid var(--faq-border); 1990border-radius: var(--faq-radius); 1991overflow: hidden; 1992} 1993.faq_block .panel:last-child { 1994margin-bottom: 0; 1995} 1996.faq_block .panel-heading { 1997padding: 0; 1998background: var(--faq-heading); 1999border-bottom: 1px solid transparent; 2000transition: background-color .15s ease; 2001} 2002.faq_block .panel-heading:hover { 2003background: var(--faq-heading-hover); 2004} 2005.faq_block .panel-title { 2006margin: 0; 2007font-size: 1em; 2008font-weight: 600; 2009line-height: inherit; 2010} 2011/* the trigger fills the whole heading so the entire bar is clickable */ 2012.faq_block .panel-title a { 2013display: flex; 2014align-items: center; 2015justify-content: space-between; 2016gap: 1em; 2017padding: 14px 16px; 2018color: inherit; 2019text-decoration: none; 2020cursor: pointer; 2021} 2022.faq_block .panel-title a:hover, 2023.faq_block .panel-title a:focus { 2024color: inherit; 2025text-decoration: none; 2026} 2027.faq_block .panel-title a:focus-visible { 2028outline: 2px solid var(--faq-focus); 2029outline-offset: -2px; 2030} 2031/* chevron â driven by aria-expanded, which the shim keeps in sync */ 2032.faq_block .panel-title a::after { 2033content: ''; 2034flex: 0 0 auto; 2035width: .45em; 2036height: .45em; 2037margin-top: -.22em; 2038border-right: 2px solid currentColor; 2039border-bottom: 2px solid currentColor; 2040transform: rotate(45deg); 2041transition: transform .2s ease, margin-top .2s ease; 2042opacity: .65; 2043} 2044.faq_block .panel-title a[aria-expanded="true"]::after { 2045margin-top: .18em; 2046transform: rotate(-135deg); 2047} 2048/* --- answer body ---------------------------------------------------------- */ 2049/* Fallback for a JS failure: the browser still reveals a panel when its id is 2050in the URL, so the answers stay reachable. */ 2051.faq_block .panel-collapse:target { 2052display: block !important; 2053} 2054/* divider only appears once a panel is open (:has() â no-op on old browsers) */ 2055.faq_block .panel:has(.panel-collapse.in) .panel-heading { 2056border-bottom-color: var(--faq-border); 2057} 2058/* Tailwind ships a `.collapse` utility (visibility: collapse) that collides with 2059the legacy Bootstrap class of the same name in this markup. Two class-level 2060selectors here so this wins on specificity regardless of stylesheet order. */ 2061.faq_block .panel-collapse, 2062.faq_table .panel-collapse { 2063visibility: visible; 2064} 2065/* padding lives on .panel-body, never margins â the height animation measures 2066scrollHeight on .panel-collapse and collapsing margins would break it */ 2067.faq_block .panel-body { 2068padding: 14px 16px 16px; 2069} 2070.faq_block .panel-body > :first-child { 2071margin-top: 0; 2072} 2073.faq_block .panel-body > :last-child { 2074margin-bottom: 0; 2075} 2076.faq_block .panel-body ol, 2077.faq_block .panel-body ul { 2078margin: .75em 0; 2079padding-left: 1.4em; 2080} 2081.faq_block .panel-body li { 2082margin-bottom: .35em; 2083} 2084.faq_block .panel-body em { 2085display: block; 2086margin-top: .9em; 2087color: var(--faq-muted); 2088} 2089/* legacy inline font sizes inside answers (span style="font-size: medium") 2090fight the 14px wrapper â normalize them back to the block's scale */ 2091.faq_block .panel-body [style*="font-size"] { 2092font-size: inherit !important; 2093} 2094/* --- search --------------------------------------------------------------- */ 2095.faq_block mark, 2096.faq_table mark { 2097padding: 0 1px; 2098background: var(--faq-mark); 2099color: inherit; 2100border-radius: 2px; 2101} 2102#search_faq, 2103[data-faq-search] { 2104width: 100%; 2105max-width: 420px; 2106margin-bottom: calc(var(--faq-gap) * 1.5); 2107padding: 10px 12px; 2108font: inherit; 2109color: inherit; 2110background: #fff; 2111border: 1px solid #dcdcdc; 2112border-radius: 6px; 2113} 2114#search_faq:focus, 2115[data-faq-search]:focus { 2116border-color: #2563eb; 2117outline: 2px solid rgba(37, 99, 235, .25); 2118outline-offset: 0; 2119} 2120/* --- small screens -------------------------------------------------------- */ 2121@media (max-width: 480px) { 2122.faq_block .panel-title a { 2123padding: 12px 13px; 2124} 2125.faq_block .panel-body { 2126padding: 12px 13px 14px; 2127} 2128} </style>
2128<script> /** 2129* FAQ collapse + search shim (no jQuery, no Bootstrap JS, no mark.js) 2130* 2131* Restores the behavior of the legacy CMS FAQ blocks: 2132* - a[data-toggle="collapse"][href="#id"] -> toggles .panel-collapse 2133* - #search_faq -> filters panels, highlights matches 2134* - ?search=term in the URL -> pre-fills and runs the filter 2135* 2136* Deliberately does NOT define window.jQuery / window.$ â a global jQuery stub 2137* makes third-party widgets take the jQuery code path and break. 2138* 2139* Public API: window.FaqShim.refresh() â re-scan after injecting markup (ESI, 2140* Alpine, AJAX). Not needed for clicks; those are delegated on document. 2141*/ 2142(function (window, document) { 2143'use strict'; 2144var TRIGGER_SEL = '[data-toggle="collapse"]'; 2145var PANEL_SEL = '.panel'; 2146var TITLE_SEL = '.panel-title a'; 2147var SEARCH_SEL = '#search_faq, [data-faq-search]'; 2148var SCOPE_SEL = '.faq_block, .faq_table, .panel-group'; 2149var DURATION = 250; 2150var originalTitles = new WeakMap(); 2151var ready = false; 2152/* ---------------------------------------------------------- styles ---- */ 2153function injectStyles() { 2154if (document.getElementById('faq-shim-css')) return; 2155var style = document.createElement('style'); 2156style.id = 'faq-shim-css'; 2157style.textContent = [ 2158/* structural fallback so panels are closed even if Bootstrap CSS is gone */ 2159'.faq_block .panel-collapse:not(.in),', 2160'.faq_table .panel-collapse:not(.in),', 2161'[data-faq-collapse]:not(.in){display:none}', 2162'[data-faq-collapse].in{display:block}', 2163'[data-faq-collapse][data-faq-collapse]{overflow:hidden;visibility:visible}', 2164'[data-faq-collapse].faq-animating{transition:height ' + DURATION + 'ms ease}', 2165'@media (prefers-reduced-motion:reduce){', 2166'[data-faq-collapse].faq-animating{transition:none}}' 2167].join(''); 2168(document.head || document.documentElement).appendChild(style); 2169} 2170/* ------------------------------------------------------- targeting ---- */ 2171function targetOf(trigger) { 2172var sel = trigger.getAttribute('data-target') || trigger.getAttribute('href') || ''; 2173if (!sel || sel === '#') return null; 2174// Duplicate ids across repeated CMS blocks are likely, so look inside the 2175// trigger's own FAQ scope first and only then fall back to the document. 2176var scope = trigger.closest(SCOPE_SEL); 2177var el = null; 2178try { 2179if (scope) el = scope.querySelector(sel); 2180if (!el) el = document.querySelector(sel); 2181} catch (e) { 2182return null; 2183} 2184return el; 2185} 2186function claim(el) { 2187if (!el || el.hasAttribute('data-faq-collapse')) return el; 2188el.setAttribute('data-faq-collapse', ''); 2189// `collapse` is a Tailwind utility (visibility: collapse) as well as a 2190// legacy Bootstrap hook. Nothing needs it now â .in and the data attribute 2191// carry the state â so drop it rather than fight it on specificity. 2192el.classList.remove('collapse'); 2193if (!el.id) el.id = 'faq-c-' + Math.random().toString(36).slice(2, 9); 2194return el; 2195} 2196function syncAria(target) { 2197var open = target.classList.contains('in'); 2198var triggers = document.querySelectorAll( 2199TRIGGER_SEL + '[href="#' + target.id + '"], ' + 2200TRIGGER_SEL + '[data-target="#' + target.id + '"]' 2201); 2202for (var i = 0; i < triggers.length; i++) { 2203triggers[i].setAttribute('aria-expanded', open ? 'true' : 'false'); 2204triggers[i].setAttribute('aria-controls', target.id); 2205if (triggers[i].tagName === 'A') triggers[i].setAttribute('role', 'button'); 2206} 2207} 2208/* ------------------------------------------------------ open/close ---- */ 2209function settle(el) { 2210if (el._faqTimer) { clearTimeout(el._faqTimer); el._faqTimer = null; } 2211if (el._faqEnd) { el.removeEventListener('transitionend', el._faqEnd); el._faqEnd = null; } 2212el.classList.remove('faq-animating'); 2213el.style.height = ''; 2214} 2215function finish(el, cb) { 2216var done = function (e) { 2217if (e && e.target !== el) return; 2218settle(el); 2219if (cb) cb(); 2220}; 2221el._faqEnd = done; 2222el.addEventListener('transitionend', done); 2223el._faqTimer = setTimeout(done, DURATION + 60); 2224} 2225function open(el, animate) { 2226claim(el); 2227settle(el); 2228el.classList.add('in'); 2229if (animate === false) { syncAria(el); return; } 2230el.style.height = '0px'; 2231el.classList.add('faq-animating'); 2232void el.offsetHeight; // force reflow 2233el.style.height = el.scrollHeight + 'px'; 2234finish(el); 2235syncAria(el); 2236} 2237function close(el, animate) { 2238claim(el); 2239settle(el); 2240if (animate === false) { 2241el.classList.remove('in'); 2242syncAria(el); 2243return; 2244} 2245el.style.height = el.scrollHeight + 'px'; 2246el.classList.add('faq-animating'); 2247void el.offsetHeight; 2248el.style.height = '0px'; 2249finish(el, function () { el.classList.remove('in'); syncAria(el); }); 2250} 2251function toggle(el) { 2252if (el.classList.contains('in')) close(el); else open(el); 2253} 2254/* --------------------------------------------------------- marking ---- */ 2255function unmark(link) { 2256var original = originalTitles.get(link); 2257if (original !== undefined && link.innerHTML !== original) link.innerHTML = original; 2258} 2259function mark(link, term) { 2260if (!originalTitles.has(link)) originalTitles.set(link, link.innerHTML); 2261unmark(link); 2262if (!term) return false;
2263var needle = term.toLowerCase(); 2264var walker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT, null, false); 2265var nodes = []; 2266var node; 2267while ((node = walker.nextNode())) nodes.push(node); 2268var found = false; 2269for (var i = 0; i < nodes.length; i++) { 2270var text = nodes[i].nodeValue; 2271var hay = text.toLowerCase(); 2272var at = hay.indexOf(needle); 2273if (at === -1) continue; 2274var frag = document.createDocumentFragment(); 2275var pos = 0; 2276while (at !== -1) { 2277if (at > pos) frag.appendChild(document.createTextNode(text.slice(pos, at))); 2278var m = document.createElement('mark'); 2279m.textContent = text.slice(at, at + needle.length); 2280frag.appendChild(m); 2281pos = at + needle.length; 2282at = hay.indexOf(needle, pos); 2283found = true; 2284} 2285if (pos < text.length) frag.appendChild(document.createTextNode(text.slice(pos))); 2286nodes[i].parentNode.replaceChild(frag, nodes[i]); 2287} 2288return found; 2289} 2290/* ---------------------------------------------------------- search ---- */ 2291function panels() { 2292var scoped = document.querySelectorAll( 2293'.faq_block ' + PANEL_SEL + ', .faq_table ' + PANEL_SEL + ', .panel-group ' + PANEL_SEL 2294); 2295return scoped.length ? scoped : document.querySelectorAll(PANEL_SEL); 2296} 2297function search(term) { 2298term = (term || '').trim(); 2299var list = panels(); 2300for (var i = 0; i < list.length; i++) { 2301var panel = list[i]; 2302var link = panel.querySelector(TITLE_SEL); 2303var body = panel.querySelector('.panel-collapse'); 2304panel.style.display = ''; 2305if (link) mark(link, ''); 2306if (body) close(claim(body), false); 2307if (!term) continue; 2308var hit = link ? mark(link, term) : false; 2309if (hit) { 2310if (body) open(body, false); 2311} else { 2312panel.style.display = 'none'; 2313} 2314} 2315} 2316function paramValue(name) { 2317var match = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.search); 2318if (!match) return ''; 2319try { 2320return decodeURIComponent(match[1].replace(/\+/g, ' ')); 2321} catch (e) { 2322return match[1]; 2323} 2324} 2325/* ------------------------------------------------------------ init ---- */ 2326function refresh() { 2327injectStyles(); 2328var triggers = document.querySelectorAll(TRIGGER_SEL); 2329for (var i = 0; i < triggers.length; i++) { 2330var target = targetOf(triggers[i]); 2331if (!target) continue; 2332claim(target); 2333syncAria(target); 2334} 2335// deep link: /page?...#collapse84 2336if (window.location.hash.length > 1) { 2337var hashed = null; 2338try { hashed = document.querySelector(window.location.hash); } catch (e) { hashed = null; } 2339if (hashed && hashed.hasAttribute('data-faq-collapse')) open(hashed, false); 2340} 2341} 2342function init() { 2343if (ready) return; 2344ready = true; 2345refresh(); 2346var seeded = paramValue('search'); 2347var input = document.querySelector(SEARCH_SEL); 2348if (input && seeded) { 2349input.value = seeded; 2350search(seeded); 2351} 2352} 2353// Delegated â survives ESI fragments and anything injected after load. 2354document.addEventListener('click', function (e) { 2355var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null; 2356if (!trigger) return; 2357var target = targetOf(trigger); 2358if (!target) return; 2359e.preventDefault(); 2360toggle(claim(target)); 2361}); 2362document.addEventListener('keydown', function (e) { 2363if (e.key !== 'Enter' && e.key !== ' ') return; 2364var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null; 2365if (!trigger || trigger.tagName === 'A') return; // anchors fire click natively 2366var target = targetOf(trigger); 2367if (!target) return; 2368e.preventDefault(); 2369toggle(claim(target)); 2370}); 2371document.addEventListener('input', function (e) { 2372if (!e.target.matches || !e.target.matches(SEARCH_SEL)) return; 2373search(e.target.value); 2374}); 2375if (document.readyState === 'loading') { 2376document.addEventListener('DOMContentLoaded', init); 2377} else { 2378init(); 2379} 2380window.addEventListener('load', refresh); 2381window.FaqShim = { refresh: refresh, open: open, close: close, search: search }; 2382})(window, document); </script>
2382</div> <!-promo_free_logo_gift_badge--><div class="widget widget-static-block"><style> /* SimplyStamps | Free Logo Stamp Gift promo badge. 2383Renders in 4 places: homepage Logo Stamps tile, the /rubber-logo-stamps PLP + its 10 sibling logo-stamp sub-category PLPs 2384(banner right of the intro heading, wraps under on mobile), and the title of every PDP carrying the promo option (select_7643). Nowhere else. 2385To end the promo: disable this static block. */ 2386.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child::after,.ss-fg-badge::before{background:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 120 120%22%3E%3Cpath d=%22M60.00 8.00 Q70.51 0.42 77.79 11.14 Q90.25 7.61 93.42 20.17 Q106.35 21.11 105.03 34.00 Q116.85 39.31 111.21 50.97 Q120.50 60.00 111.21 69.03 Q116.85 80.69 105.03 86.00 Q106.35 98.89 93.42 99.83 Q90.25 112.39 77.79 108.86 Q70.51 119.58 60.00 112.00 Q49.49 119.58 42.21 108.86 Q29.75 112.39 26.58 99.83 Q13.65 98.89 14.97 86.00 Q3.15 80.69 8.79 69.03 Q-0.50 60.00 8.79 50.97 Q3.15 39.31 14.97 34.00 Q13.65 21.11 26.58 20.17 Q29.75 7.61 42.21 11.14 Q49.49 0.42 60.00 8.00 Z%22 fill=%22%23002E49%22/%3E%3Ccircle cx=%2260%22 cy=%2260%22 r=%2245.5%22 fill=%22none%22 stroke=%22%23FFFFFF%22 stroke-opacity=%22.55%22 stroke-width=%221.6%22 stroke-dasharray=%223.2 3.2%22/%3E%3Cg transform=%22translate(50.4 23) scale(.8)%22 fill=%22%2355B5E3%22%3E%3Cpath d=%22M12 8C9 8 7.4 6.6 8.3 5.1 9.3 3.4 11.4 4.8 12 8Z%22/%3E%3Cpath d=%22M12 8C15 8 16.6 6.6 15.7 5.1 14.7 3.4 12.6 4.8 12 8Z%22/%3E%3Crect x=%223%22 y=%228%22 width=%2218%22 height=%224.2%22 rx=%221%22/%3E%3Crect x=%224.6%22 y=%2212.2%22 width=%2214.8%22 height=%229.3%22 rx=%221%22/%3E%3Crect x=%2211%22 y=%228%22 width=%222%22 height=%2213.5%22 fill=%22%23002E49%22/%3E%3C/g%3E%3Ctext x=%2260%22 y=%2270%22 text-anchor=%22middle%22 font-family=%22ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Arial,sans-serif%22 font-size=%2228%22 font-weight=%22800%22 fill=%22%23FFFFFF%22 letter-spacing=%22.5%22%3EFREE%3C/text%3E%3Ctext x=%2260%22 y=%2290%22 text-anchor=%22middle%22 font-family=%22ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Arial,sans-serif%22 font-size=%2217%22 font-weight=%22800%22 fill=%22%2355B5E3%22 letter-spacing=%222.5%22%3EGIFT%3C/text%3E%3C/svg%3E") center/contain no-repeat} 2387.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child{position:relative;width:fit-content;margin-left:auto;margin-right:auto} 2388.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child::after{content:"";position:absolute;top:-7%;right:-7%;width:38%;height:38%;transform:rotate(-12deg);filter:drop-shadow(0 2px 3px rgba(0,0,0,.28));pointer-events:none;z-index:2} 2389@media (max-width:639px){.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child::after{top:-8%;right:-8%;width:42%;height:42%}} 2390.ss-fg-badge{position:relative;display:block;cursor:help;outline:none}.ss-fg-badge::before{content:"";position:absolute;inset:0;transform:rotate(-8deg);filter:drop-shadow(0 2px 3px rgba(0,0,0,.25))} 2391.ss-fg-badge:focus-visible{outline:2px solid #55B5E3;outline-offset:3px;border-radius:50%} 2392.ss-fg-h2row{display:flex;flex-wrap:wrap;align-items:center;gap:10px 20px;margin:0 0 12px} 2393.ss-fg-h2row>h2,.ss-fg-h2row>h1{margin:0!important;flex:0 1 auto} 2394.ss-fg-banner{position:relative;display:inline-flex;align-items:center;gap:10px;margin-left:auto;padding:4px 16px 4px 4px;border:1px solid rgba(85,181,227,.6);border-radius:9999px;background:#EAF6FC;color:#002E49;font-size:14px;line-height:20px;cursor:help;outline:none} 2395.ss-fg-banner strong{font-weight:700;text-transform:uppercase;letter-spacing:.04em} 2396.ss-fg-banner:focus-visible{outline:2px solid #55B5E3;outline-offset:2px} 2397.ss-fg-bicon{flex:0 0 30px;width:30px;height:30px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='16' fill='%23002E49'/%3E%3Cg transform='translate(3.4 2.9) scale(1.05)' fill='%2355B5E3'%3E%3Cpath d='M12 8C9 8 7.4 6.6 8.3 5.1 9.3 3.4 11.4 4.8 12 8Z'/%3E%3Cpath d='M12 8C15 8 16.6 6.6 15.7 5.1 14.7 3.4 12.6 4.8 12 8Z'/%3E%3Crect x='3' y='8' width='18' height='4.2' rx='1'/%3E%3Crect x='4.6' y='12.2' width='14.8' height='9.3' rx='1'/%3E%3Crect x='11' y='8' width='2' height='13.5' fill='%23002E49'/%3E%3C/g%3E%3C/svg%3E") center/contain no-repeat} 2398.ss-fg-banner .ss-fg-tip{right:0} 2399.ss-fg-banner.is-wrapped{margin-left:0;width:100%;border-radius:12px} 2400.ss-fg-title-row{position:relative} 2401.ss-fg-title-row>h1{padding-right:68px} 2402.ss-fg-pdp{position:absolute;top:-10px;right:0;width:58px;height:58px} 2403.ss-fg-tip{position:absolute;top:calc(100% + 10px);z-index:50;width:max-content;max-width:min(320px,calc(100vw - 32px));padding:8px 12px;border-radius:8px;background:#111827;color:#fff;font-size:12px;font-weight:500;line-height:16px;text-align:left;white-space:normal;box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);opacity:0;visibility:hidden;transition:opacity .15s;pointer-events:none} 2404.ss-fg-pdp .ss-fg-tip{right:0}
2405@media (hover:hover){.ss-fg-badge:hover .ss-fg-tip,.ss-fg-banner:hover .ss-fg-tip{opacity:1;visibility:visible}} 2406.ss-fg-badge:focus-visible .ss-fg-tip,.ss-fg-badge.is-open .ss-fg-tip,.ss-fg-banner:focus-visible .ss-fg-tip,.ss-fg-banner.is-open .ss-fg-tip{opacity:1;visibility:visible} 2407@media (max-width:639px){.ss-fg-banner{margin-left:0;width:100%;border-radius:12px}.ss-fg-pdp{width:48px;height:48px;top:-6px}.ss-fg-title-row>h1{padding-right:56px}} 2408@media (prefers-reduced-motion:reduce){.ss-fg-tip{transition:none}} </style>
2408<script> (function(){ 2409var TIP="Free with your logo stamp order. Pick any design and weâll make it and ship it alongside your custom logo stamp at no extra charge."; 2410function make(extra,banner){ 2411var b=document.createElement('span'); 2412b.tabIndex=0; 2413if(banner){b.className='ss-fg-banner';b.innerHTML='<span class="ss-fg-bicon" aria-hidden="true"></span><span><strong>Free gift</strong> with every logo stamp order: choose your bonus stamp design <span style="font-weight:700;font-style:italic">for a limited time</span></span>';} 2414else{b.className='ss-fg-badge '+extra;b.setAttribute('role','img');b.setAttribute('aria-label','Free gift with any logo stamp. '+TIP);} 2415var t=document.createElement('span'); 2416t.className='ss-fg-tip'; 2417t.setAttribute('aria-hidden','true'); 2418t.textContent=TIP; 2419b.appendChild(t); 2420b.addEventListener('click',function(e){e.stopPropagation();b.classList.toggle('is-open');}); 2421b.addEventListener('keydown',function(e){if(e.key==='Escape'){b.classList.remove('is-open');b.blur();}}); 2422document.addEventListener('click',function(){b.classList.remove('is-open');}); 2423return b; 2424} 2425function run(){ 2426var c=document.body.classList; 2427if(c.contains('catalog-category-view')&&c.contains('category-rubber-logo-stamps')){ 2428var h2=document.querySelector('.ss-logo-top > h2'); 2429if(h2&&!document.querySelector('.ss-fg-banner')){var row=document.createElement('div');row.className='ss-fg-h2row';h2.parentNode.insertBefore(row,h2);row.appendChild(h2);var bn=make('',true);row.appendChild(bn); 2430var fit=function(){bn.classList.remove('is-wrapped');if(bn.offsetTop>h2.offsetTop+h2.offsetHeight/2)bn.classList.add('is-wrapped');}; 2431fit();window.addEventListener('resize',fit);if(window.ResizeObserver)new ResizeObserver(fit).observe(row);} 2432} 2433var SIB=['category-rectangle','category-round','category-square','category-with-text','category-upload-art','category-self-inking','category-traditional-wooden-handle','category-pre-inked-logo','category-loyalty-card-stamps','category-catalog-stamps']; 2434if(c.contains('catalog-category-view')&&SIB.some(function(k){return c.contains(k);})){ 2435var h1c=document.querySelector('.flex-1 > h1'); 2436if(h1c&&!document.querySelector('.ss-fg-banner')){ 2437var rowc=document.createElement('div');rowc.className='ss-fg-h2row';h1c.parentNode.insertBefore(rowc,h1c);rowc.appendChild(h1c); 2438var bnc=make('',true);rowc.appendChild(bnc); 2439var fitc=function(){bnc.classList.remove('is-wrapped');if(bnc.offsetTop>h1c.offsetTop+h1c.offsetHeight/2)bnc.classList.add('is-wrapped');}; 2440fitc();window.addEventListener('resize',fitc);if(window.ResizeObserver)new ResizeObserver(fitc).observe(rowc); 2441} 2442} 2443if(c.contains('catalog-product-view')&&document.getElementById('select_7643')){ 2444var h1=document.querySelector('.product-header h1'); 2445if(h1&&!document.querySelector('.ss-fg-pdp')){h1.parentNode.classList.add('ss-fg-title-row');h1.parentNode.appendChild(make('ss-fg-pdp'));} 2446} 2447} 2448if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',run);else run(); 2449})(); </script>
2449</div><div class="widget widget-static-block"><div id="hcb-promo-root" hidden></div>
2449<script> (function () { 2450/* ====================================================================== 2451>>>>>>>>>>>>>>>>>>>>>> EDIT ZONE START <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 2452One list of promo phases. Each runs FROM its start UNTIL its end (Eastern). 2453MESSAGE = banner text (HTML ok â <strong> etc.) CODE = coupon 2454THRESHOLD = free-ship order minimum REWARD = what they unlock 2455Dates are Eastern: -04:00 summer, -05:00 winter. Day-end = 23:59:59. 2456====================================================================== */ 2457var PROMO = { 2458PHASES: [ 2459{ FROM: "2026-08-10T00:00:00-04:00", UNTIL: "2026-08-16T23:59:59-04:00", MESSAGE: "<strong>Free Shipping</strong> on orders $99+", CODE: "FreeShip99", THRESHOLD: 99, REWARD: "free shipping" }, 2460{ FROM: "2026-08-17T00:00:00-04:00", UNTIL: "2026-08-23T23:59:59-04:00", MESSAGE: "<strong>Free Shipping</strong> on orders $89+", CODE: "FreeShip89", THRESHOLD: 89, REWARD: "free shipping" }, 2461{ FROM: "2026-08-24T00:00:00-04:00", UNTIL: "2026-08-30T23:59:59-04:00", MESSAGE: "<strong>Free Shipping</strong> on orders $75+", CODE: "FreeShip75", THRESHOLD: 75, REWARD: "free shipping" }, 2462{ FROM: "2026-08-31T00:00:00-04:00", UNTIL: "2026-09-07T23:59:59-04:00", MESSAGE: "<strong>Free Shipping</strong> on orders $69+", CODE: "FreeShip69", THRESHOLD: 69, REWARD: "free shipping" } 2463], 2464COUNTDOWN_DAYS_BEFORE: 0, // 0 = NO timer (banner shows message only) 2465LINK_URL: "", // no link â banner is not clickable 2466SHOW_CODE: false, // native free shipping = no coupon; hide the code chip 2467// Small â bubble after the banner message. Leave "" to hide it. (Was used to explain 2468// the old coupon deducting $9.99; native free shipping needs no explainer.) 2469INFO_TIP: "", 2470LOAD_GOOGLE_FONT: true, 2471// --- ZEPHYR header banner mount --- 2472HEADER_SELECTOR: "header", // the fixed Zephyr <header> MOUNT_POSITION: "bottom", // "bottom" = below the nav (default) · "top" = above the nav rows 2473HEADER_HEIGHT_VAR: "--header-height", // Zephyr offsets <main> by padding-top:var(--header-height) 2474// --- PDP progress bar mount + subtotal source --- 2475GOAL_ENABLED: true, 2476GOAL_MOUNTS: [ 2477{ selector: ".price-pipe-container", position: "after" } // product pages (under the price block) 2478], 2479SUBTOTAL_URL: "/zephyr/cart/getItems", // cart subtotal endpoint; fetched ONCE per load (heavy) 2480POLL_MS: 1500, 2481COLORS: { 2482banner: { background: "#60B5E5", text: "#0A3550", chipBg: "#FFFFFF", chipText: "#0A3550" }, 2483goal: { text: "#0A3550", track: "#E6EEF3", fill: "#60B5E5", done: "#1A7F37" } 2484} 2485}; 2486/* ====================================================================== 2487>>>>>>>>>>>>>>>>>>>>>>> EDIT ZONE END <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 2488Do not edit below this line. 2489====================================================================== */ 2490if (window.__hcbPromoLoaded) return; 2491window.__hcbPromoLoaded = true; 2492var DAY = 86400000; 2493var P = PROMO.PHASES.map(function (p){ return { from:new Date(p.FROM).getTime(), ts:new Date(p.UNTIL).getTime(), MESSAGE:p.MESSAGE, CODE:p.CODE, THRESHOLD:+p.THRESHOLD, REWARD:p.REWARD }; }) 2494.filter(function (p, i){ var ok = !isNaN(p.from) && !isNaN(p.ts) && p.ts > p.from; if (!ok) console.warn("[HCB Promo] skipping phase " + (i+1) + " â bad FROM/UNTIL:", PROMO.PHASES[i]); return ok; }) 2495.sort(function (a, b){ return a.from - b.from; }); 2496if (!P.length){ console.warn("[HCB Promo] no valid phases â disabled."); return; } 2497var endTs = P.reduce(function (m, p){ return Math.max(m, p.ts); }, 0); 2498var dismissKey = "hcbPromoDismissed:" + endTs; 2499try { if (sessionStorage.getItem(dismissKey)) window.__hcbDismissed = true; } catch (e) {} 2500if (Date.now() >= endTs) return; 2501function esc(s){ return String(s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } 2502function phaseIndexAt(now){ for (var i=0;i<P.length;i++) if (now >= P[i].from && now <= P[i].ts) return i; return -1; } 2503function pad(n){ return (n < 10 ? '0' : '') + n; } 2504function money(n){ return "$" + (Math.round(n*100)/100).toFixed(2).replace(/\.00$/, ""); } 2505var currentCode = P[Math.max(0, phaseIndexAt(Date.now()))].CODE; 2506var bannerInstances = [], goals = [], cachedUrlVal = null, interval = null; 2507function readSubtotal(){ return cachedUrlVal; } // populated by pollUrl() 2508function pollUrl(){ 2509if (!PROMO.SUBTOTAL_URL) return; 2510fetch(PROMO.SUBTOTAL_URL, {credentials:"include", headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(d){ 2511if (!d) return; 2512var v = (typeof d.subtotal_numeric !== "undefined") ? parseFloat(d.subtotal_numeric) 2513: (typeof d.subtotal !== "undefined") ? parseFloat(String(d.subtotal).replace(/[^0-9.]/g,"")) : NaN; 2514if (!isNaN(v)){ cachedUrlVal = v; tick(); } 2515}).catch(function(){}); 2516} 2517if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); 2518else start(); 2519function start(){ 2520if (PROMO.LOAD_GOOGLE_FONT && !document.getElementById('hcb-roboto-font')) { 2521var lk = document.createElement('link'); lk.id='hcb-roboto-font'; lk.rel='stylesheet'; 2522lk.href='https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'; 2523document.head.appendChild(lk); 2524} 2525var Cb = PROMO.COLORS.banner, Cg = PROMO.COLORS.goal; 2526var css = '' 2527/* banner â position:relative because it lives INSIDE the fixed Zephyr <header> */ 2528+ '.hcb-promo{position:relative;display:flex;align-items:center;justify-content:center;' 2529+ 'gap:.6em;padding:9px 40px;font:600 14px/1.2 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;' 2530+ 'white-space:nowrap;overflow:hidden;box-sizing:border-box;width:100%}' 2531+ '.hcb-promo *{box-sizing:border-box}' 2532+ '.hcb-promo__main{display:flex;align-items:center;justify-content:center;gap:.6em;min-width:0;color:inherit;text-decoration:none}' 2533+ '.hcb-promo__msg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;font-weight:400}' 2534+ '.hcb-promo__code{flex:none;cursor:pointer;border:0;border-radius:4px;padding:3px 9px;font:inherit;font-weight:700;letter-spacing:.03em}' 2535+ '.hcb-promo__timer{flex:none;font-variant-numeric:tabular-nums;font-weight:400}' 2536+ '.hcb-promo__timer b{font-weight:400}' 2537+ '.hcb-promo__timer--min{display:none}' 2538/* info â bubble â inline, height-safe; tooltip is portaled to<body> + fixed so nothing clips/covers it */ 2539+ '.hcb-promo__info{position:relative;flex:none;display:inline-flex;align-items:center}' 2540+ '.hcb-promo__ibtn{width:16px;height:16px;border-radius:50%;border:1.5px solid currentColor;color:inherit;background:transparent;font:700 11px/1 "Roboto",Arial,sans-serif;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}' 2541+ '.hcb-promo__itip{position:fixed;transform:translateX(-50%);width:240px;max-width:72vw;background:' + Cb.text + ';color:' + Cb.background + ';font:400 12px/1.4 "Roboto",Arial,sans-serif;padding:8px 10px;border-radius:6px;box-shadow:0 2px 10px rgba(0,0,0,.25);opacity:0;visibility:hidden;transition:opacity .12s;z-index:2147483000;text-align:center;white-space:normal;pointer-events:none}' 2542+ '.hcb-promo__itip::after{content:"";position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:' + Cb.text + '}' 2543+ '.hcb-promo__itip.is-open{opacity:1;visibility:visible}' 2544+ '.hcb-promo__x{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:0;color:inherit;opacity:.75;cursor:pointer;font-size:20px;line-height:1;padding:4px 6px}' 2545+ '.hcb-promo__x:hover{opacity:1}' 2546+ '@media (max-width:640px){.hcb-promo{font-size:13px;padding:9px 24px;gap:.35em}.hcb-promo__timer--full{display:none}.hcb-promo__timer--min{display:inline}}' 2547/* goal bar */ 2548+ '.hcb-goal{font:600 14px/1.35 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;margin:10px 0;max-width:420px}' 2549+ '.hcb-goal__msg{color:' + Cg.text + ';margin:0 0 6px}.hcb-goal__msg b{font-weight:800}' 2550+ '.hcb-goal__track{height:10px;background:' + Cg.track + ';border-radius:999px;overflow:hidden}' 2551+ '.hcb-goal__fill{height:100%;width:0;background:' + Cg.fill + ';border-radius:999px;transition:width .4s ease}' 2552+ '.hcb-goal.is-done .hcb-goal__msg{color:' + Cg.done + '}.hcb-goal.is-done .hcb-goal__fill{background:' + Cg.done + '}'; 2553var style=document.createElement('style'); style.textContent=css; document.head.appendChild(style); 2554/* ---------- build header banner ---------- */ 2555var seed = P[Math.max(0, phaseIndexAt(Date.now()))]; 2556var linkOpen = PROMO.LINK_URL ? 'a href="' + esc(PROMO.LINK_URL) + '"' : 'span'; 2557var linkClose = PROMO.LINK_URL ? 'a' : 'span'; 2558var INFO_TIP = PROMO.INFO_TIP || ""; 2559function bannerMarkup(ph){ 2560return '<div class="hcb-promo" role="region" aria-label="Promotion" style="background:' + Cb.background + ';color:' + Cb.text + '">' 2561+ '<' + linkOpen + ' class="hcb-promo__main">' 2562+ '<span class="hcb-promo__msg">' + ph.MESSAGE + '</span>' 2563+ '<span class="hcb-promo__timer hcb-promo__timer--full" aria-hidden="true" style="display:none">Ends in <b data-u="d">00</b>d : <b data-u="h">00</b>h : <b data-u="m">00</b>m : <b data-u="s">00</b>s</span>' 2564+ '<span class="hcb-promo__timer hcb-promo__timer--min" aria-hidden="true" style="display:none"><b data-u="dm">0</b>d <b data-u="h">00</b>:<b data-u="m">00</b>:<b data-u="s">00</b></span>' 2565+ '</' + linkClose + '>' 2566+ (INFO_TIP ? '<span class="hcb-promo__info"><button type="button" class="hcb-promo__ibtn" aria-label="More info">i</button><span class="hcb-promo__itip" role="tooltip">' + esc(INFO_TIP) + '</span></span>' : '') 2567+ (PROMO.SHOW_CODE ? '<button type="button" class="hcb-promo__code" style="background:' + Cb.chipBg + ';color:' + Cb.chipText + '" aria-label="Copy promo code ' + esc(ph.CODE) + '">' + esc(ph.CODE) + '</button>' : '') 2568+ '<button type="button" class="hcb-promo__x" aria-label="Dismiss promotion">×</button>' 2569+ '</div>'; 2570} 2571var header = document.querySelector(PROMO.HEADER_SELECTOR);
2572var mountHost = header || document.querySelector('main') || document.body; 2573var root = document.createElement('div'); root.className='hcb-promo-root'; root.innerHTML = bannerMarkup(seed); 2574if (PROMO.MOUNT_POSITION === 'top') mountHost.insertBefore(root, mountHost.firstChild); // above the nav rows 2575else mountHost.appendChild(root); // below the nav (default) 2576syncHeaderHeight(); // set --header-height now (anti-jump); Zephyr's own observer also maintains it 2577var inst={ root:root, msg:root.querySelector('.hcb-promo__msg'), units:root.querySelectorAll('[data-u]'), 2578fulls:root.querySelectorAll('.hcb-promo__timer--full'), mins:root.querySelectorAll('.hcb-promo__timer--min'), 2579code:root.querySelector('.hcb-promo__code'), x:root.querySelector('.hcb-promo__x') }; 2580if (inst.code) inst.code.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); 2581var b=inst.code, done=function(){ b.textContent='Copied!'; setTimeout(function(){ b.textContent=currentCode; },1500); }; 2582if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(currentCode).then(done,fb);}else{fb();} 2583function fb(){var ta=document.createElement('textarea');ta.value=currentCode;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');done();}catch(err){}document.body.removeChild(ta);} }); 2584inst.x.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); 2585try{sessionStorage.setItem(dismissKey,'1');}catch(err){} 2586window.__hcbDismissed=true; root.style.display='none'; syncHeaderHeight(); }); 2587// info â tooltip â portal to<body>, position:fixed under the icon 2588var info=root.querySelector('.hcb-promo__info'); 2589if(info){ 2590var ib=info.querySelector('.hcb-promo__ibtn'); 2591var tip=info.querySelector('.hcb-promo__itip'); 2592document.body.appendChild(tip); 2593var place=function(){ var r=ib.getBoundingClientRect(); tip.style.left=(r.left+r.width/2)+'px'; tip.style.top=(r.bottom+9)+'px'; }; 2594var openTip=function(){ place(); tip.classList.add('is-open'); }; 2595var closeTip=function(){ tip.classList.remove('is-open'); }; 2596ib.addEventListener('mouseenter', openTip); 2597ib.addEventListener('mouseleave', closeTip); 2598ib.addEventListener('focus', openTip); 2599ib.addEventListener('blur', closeTip); 2600ib.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); if(tip.classList.contains('is-open')) closeTip(); else openTip(); }); 2601document.addEventListener('click', function(e){ if(e.target!==ib) closeTip(); }); 2602window.addEventListener('scroll', function(){ if(tip.classList.contains('is-open')) place(); }, {passive:true}); 2603window.addEventListener('resize', function(){ if(tip.classList.contains('is-open')) place(); }); 2604} 2605bannerInstances.push(inst); 2606if (window.__hcbDismissed) root.style.display='none'; 2607/* ---------- keep --header-height in sync (includes this banner) ---------- */ 2608function syncHeaderHeight(){ 2609if (!header) return; 2610var h = Math.round(header.getBoundingClientRect().height); 2611if (h) document.documentElement.style.setProperty(PROMO.HEADER_HEIGHT_VAR, h + 'px'); 2612} 2613if (header){ 2614if (window.ResizeObserver){ try { new ResizeObserver(syncHeaderHeight).observe(header); } catch(e){} } 2615window.addEventListener('resize', syncHeaderHeight); 2616setTimeout(syncHeaderHeight,300); setTimeout(syncHeaderHeight,1200); 2617} 2618/* ---------- build PDP goal bar (only where the price container exists) ---------- */ 2619if (PROMO.GOAL_ENABLED){ 2620(PROMO.GOAL_MOUNTS||[]).forEach(function(mnt){ 2621var ghost=document.querySelector(mnt.selector); if(!ghost) return; 2622var groot=document.createElement('div'); groot.className='hcb-goal'; 2623groot.innerHTML='<p class="hcb-goal__msg"></p><div class="hcb-goal__track"><div class="hcb-goal__fill"></div></div>'; 2624var gp=mnt.position; 2625if(gp==='before') ghost.parentNode.insertBefore(groot, ghost); 2626else if(gp==='prepend') ghost.insertBefore(groot, ghost.firstChild); 2627else if(gp==='append') ghost.appendChild(groot); 2628else ghost.parentNode.insertBefore(groot, ghost.nextSibling); 2629goals.push({ root:groot, msg:groot.querySelector('.hcb-goal__msg'), fill:groot.querySelector('.hcb-goal__fill') }); 2630}); 2631} 2632if (goals.length) pollUrl(); // fetch cart subtotal ONCE (heavy endpoint) â only where a goal bar renders 2633interval=setInterval(tick, 1000); // 1s clock only (no re-fetch) 2634tick(); 2635} 2636/* ---------- single clock drives banner + goal ---------- */ 2637var lastIdx = -2; 2638function setUnit(inst,u,val){ for(var i=0;i<inst.units.length;i++) if(inst.units[i].getAttribute('data-u')===u) inst.units[i].textContent=val; } 2639function showTimer(inst,on){ for(var i=0;i<inst.fulls.length;i++) inst.fulls[i].style.display=on?'':'none'; for(var j=0;j<inst.mins.length;j++) inst.mins[j].style.display=on?'':'none'; } 2640function tick(){ 2641var now = Date.now(); 2642var idx = phaseIndexAt(now); 2643if (now >= endTs || idx === -1){ 2644if (interval) clearInterval(interval);
2645bannerInstances.forEach(function(o){ o.root.style.display='none'; }); 2646goals.forEach(function(g){ g.root.style.display='none'; }); 2647return; 2648} 2649var ph = P[idx]; 2650if (idx !== lastIdx){ 2651lastIdx = idx; currentCode = ph.CODE; 2652bannerInstances.forEach(function(o){ o.msg.innerHTML=ph.MESSAGE; if(o.code){ o.code.textContent=currentCode; o.code.setAttribute('aria-label','Copy promo code '+currentCode); } }); 2653} 2654var ms = ph.ts - now, showT = ms <= PROMO.COUNTDOWN_DAYS_BEFORE*DAY; 2655var d=Math.floor(ms/DAY), h=Math.floor((ms%DAY)/3600000), m=Math.floor((ms%3600000)/60000), s=Math.floor((ms%60000)/1000); 2656bannerInstances.forEach(function(o){ 2657if (window.__hcbDismissed) return; 2658if (showT){ setUnit(o,'d',pad(d)); setUnit(o,'dm',d); setUnit(o,'h',pad(h)); setUnit(o,'m',pad(m)); setUnit(o,'s',pad(s)); showTimer(o,true); } 2659else showTimer(o,false); 2660}); 2661if (goals.length){ 2662var sub=readSubtotal(), pct, html, done=false; 2663var useCode=PROMO.SHOW_CODE; 2664if (sub===null){ pct=0; html='Spend '+money(ph.THRESHOLD)+'+ for <b>'+ph.REWARD+'</b>'+(useCode?' â use code <b>'+ph.CODE+'</b>':''); } 2665else if (sub>=ph.THRESHOLD){ pct=100; done=true; html="You've unlocked <b>"+ph.REWARD+"</b>!"+(useCode?" Use code <b>"+ph.CODE+"</b> at checkout.":""); } 2666else { pct=Math.max(0,Math.min(100, sub/ph.THRESHOLD*100)); html="You're <b>"+money(ph.THRESHOLD-sub)+" away</b> from <b>"+ph.REWARD+"</b>"+(useCode?" (code "+ph.CODE+")":""); } 2667goals.forEach(function(gEl){ gEl.msg.innerHTML=html; gEl.fill.style.width=pct+'%'; gEl.root.classList.toggle('is-done',done); gEl.root.style.display=''; }); 2668} 2669} 2670})(); </script>
2670</div><div class="widget widget-static-block"> <style> /* v1.31 -- Roboto promoted to first in the stack to match OpenMage brand. 2671All child elements inherit this via font-family: inherit or unspecified. */ 2672.ss-upsell-overlay { position: fixed; inset: 0; background: rgba(15,23,42,0.55); display: flex; align-items: center; justify-content: center; z-index: 99999; padding: 16px; animation: ss-fade-in 180ms ease-out; font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; } 2673@keyframes ss-fade-in { from { opacity: 0; } to { opacity: 1; } } 2674.ss-upsell-modal { background: #fff; border-radius: 12px; max-width: 480px; width: 100%; padding: 32px 28px 24px; position: relative; box-shadow: 0 20px 60px rgba(0,0,0,0.25); animation: ss-slide-up 220ms ease-out; box-sizing: border-box; } 2675@keyframes ss-slide-up { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } 2676.ss-upsell-close { position: absolute; top: 12px; right: 14px; background: transparent; border: 0; font-size: 28px; line-height: 1; color: #94a3b8; cursor: pointer; padding: 4px 8px; border-radius: 4px; } 2677.ss-upsell-close:hover { color: #475569; background: #f1f5f9; } 2678.ss-upsell-badge { display: inline-block; background: #fef3c7; color: #92400e; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; padding: 4px 10px; border-radius: 999px; margin-bottom: 12px; } 2679/* v1.31 -- title case + Roboto Bold. Letter-spacing removed (was for all-caps). */ 2680.ss-upsell-modal h2 { font-family: inherit; font-size: 22px; line-height: 1.25; margin: 0 0 8px; color: #0f172a; font-weight: 700; text-align: center; } 2681/* v1.31 -- subhead sits directly under the headline; smaller, softer color */ 2682.ss-upsell-subhead { font-size: 14px; line-height: 1.5; margin: 0 0 16px; color: #475569; font-weight: 400; text-align: center; } 2683/* v1.31 -- 300px image slot, padding removed so GIF fills edge-to-edge. 2684overflow:hidden clips anything spilling outside the rounded corners. */ 2685.ss-upsell-image { margin: 0 0 18px; background: #f8fafc; border-radius: 8px; padding: 0; display: flex; align-items: center; justify-content: center; height: 300px; box-sizing: border-box; overflow: hidden; } 2686.ss-upsell-image img { width: 100%; height: 100%; object-fit: cover; display: block; } 2687.ss-upsell-bullets { list-style: none; margin: 0 0 20px; padding: 0; } 2688.ss-upsell-bullets li { position: relative; padding-left: 24px; margin-bottom: 10px; color: #334155; line-height: 1.5; font-size: 14px; } 2689.ss-upsell-bullets li:before { content: "\2713"; position: absolute; left: 0; color: #16a34a; font-weight: 700; } 2690.ss-upsell-price { background: #f8fafc; border-left: 3px solid #16a34a; padding: 12px 14px; margin: 0 0 20px; font-size: 15px; color: #0f172a; border-radius: 4px; } 2691.ss-upsell-price strong { color: #16a34a; font-size: 17px; } 2692.ss-upsell-actions { display: flex; flex-direction: column; gap: 4px; align-items: center; } 2693.ss-upsell-accept { width: 100%; padding: 13px 16px; border-radius: 6px; border: 0; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 120ms ease; font-family: inherit; background: #16a34a; color: #fff; } 2694.ss-upsell-accept:hover { background: #15803d; } 2695.ss-upsell-accept:focus { outline: 2px solid #15803d; outline-offset: 2px; } 2696/* v1.31 -- "No thanks" is now a small centered text link, not a ghost button. 2697Reduces visual competition with the primary Upgrade CTA. */ 2698.ss-upsell-decline { background: transparent; color: #64748b; border: 0; padding: 8px 4px; font-size: 13px; font-weight: 500; text-decoration: underline; cursor: pointer; font-family: inherit; } 2699.ss-upsell-decline:hover { color: #334155; text-decoration: underline; } 2700.ss-upsell-decline:focus { outline: 2px solid #64748b; outline-offset: 2px; } 2701/* v1.31 -- config-step Cancel now inherits the small-link style from 2702.ss-upsell-decline to match Step 1's philosophy. Ghost-button CSS 2703override deliberately removed. .ss-config-cancel class kept purely 2704as a JS querySelector hook. */ 2705@media (max-width: 480px) { .ss-upsell-modal { padding: 24px 20px 20px; border-radius: 10px; } .ss-upsell-modal h2 { font-size: 18px; } } 2706/* Success state */ 2707.ss-upsell-success { text-align: center; padding-top: 40px; } 2708.ss-upsell-success-icon { width: 64px; height: 64px; border-radius: 50%; background: #16a34a; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 36px; line-height: 1; margin: 0 auto 16px; animation: ss-pop 320ms cubic-bezier(0.34, 1.56, 0.64, 1); } 2709@keyframes ss-pop { 0% { transform: scale(0.3); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } 2710.ss-upsell-success h2 { color: #15803d; margin-bottom: 8px; } 2711.ss-upsell-success-tagline { color: #334155; font-size: 15px; line-height: 1.5; margin: 0 0 18px; } 2712.ss-upsell-success-cta { color: #94a3b8; font-size: 13px; margin: 0; display: flex; align-items: center; justify-content: center; gap: 8px; } 2713.ss-upsell-spinner { width: 14px; height: 14px; border: 2px solid #cbd5e1; border-top-color: #16a34a; border-radius: 50%;
2713 animation: ss-spin 0.7s linear infinite; } 2714@keyframes ss-spin { to { transform: rotate(360deg); } } 2715.ss-upsell-success-skip { background: transparent; border: 0; color: #16a34a; font-size: 13px; cursor: pointer; padding: 6px 12px; margin-top: 8px; font-weight: 600; text-decoration: underline; } 2716/* Config step (step 2) */ 2717.ss-config-intro { color: #475569; font-size: 14px; line-height: 1.5; margin: 0 0 18px; } 2718.ss-config-fields { max-height: 50vh; overflow-y: auto; margin: 0 0 20px; padding-right: 4px; } 2719.ss-opt-field { margin-bottom: 14px; } 2720.ss-opt-field label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 6px; } 2721.ss-opt-field select { width: 100%; padding: 10px 12px; font-size: 14px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; font-family: inherit; color: #0f172a; box-sizing: border-box; } 2722.ss-opt-field select:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; } 2723.ss-opt-field input[type="text"], .ss-opt-field textarea { width: 100%; padding: 10px 12px; font-size: 14px; border: 1px solid #cbd5e1; border-radius: 6px; font-family: inherit; box-sizing: border-box; color: #0f172a; } 2724.ss-opt-field input[type="text"]:focus, .ss-opt-field textarea:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; } 2725.ss-opt-field textarea { resize: vertical; min-height: 60px; } 2726.ss-opt-group-label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 8px; } 2727.ss-opt-radio { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 14px; color: #334155; cursor: pointer; font-weight: 400; } 2728.ss-opt-radio input { margin: 0; } 2729.ss-config-required { color: #dc2626; margin-left: 3px; } 2730.ss-opt-field-error select, 2731.ss-opt-field-error input[type="text"], 2732.ss-opt-field-error textarea { border-color: #dc2626; box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12); } 2733.ss-opt-field-error > label, 2734.ss-opt-field-error > .ss-opt-group-label { color: #dc2626; } 2735.ss-opt-error { color: #dc2626; font-size: 12px; margin-top: 6px; font-weight: 500; } 2736/* Variant B / C -- value messaging enhancements */ 2737.ss-upsell-badge.ss-upsell-badge-popular { background: #dcfce7; color: #15803d; } 2738.ss-upsell-badge.ss-upsell-badge-discount { background: #fef3c7; color: #92400e; } 2739.ss-upsell-cost-per-use { font-size: 12px; color: #64748b; margin-top: 4px; } 2740.ss-upsell-trust { font-size: 12px; color: #64748b; margin: 0 0 16px; text-align: center; line-height: 1.4; } 2741.ss-upsell-trust .ss-stars { color: #f59e0b; letter-spacing: 1px; } 2742.ss-upsell-discount-banner { background: #fff7ed; border: 1px dashed #f59e0b; color: #92400e; padding: 10px 12px; border-radius: 6px; margin: 0 0 16px; font-size: 13px; font-weight: 600; text-align: center; } 2743.ss-upsell-price-strike { text-decoration: line-through; color: #94a3b8; font-weight: 400; margin-right: 6px; } </style>
2743<script> (function () { 2744'use strict'; 2745var TIER_MAP = [ 2746{ tier: 1, name: 'Wood Handle', match: ['wood handle', 'wood'] }, 2747{ tier: 2, name: 'Self-Inking', match: ['self-inking', 'self inking', 'selfinking'] }, 2748{ tier: 3, name: 'Pre-Inked', match: ['pre-inked', 'pre inked', 'preinked'], subtype: 'pre_inked' }, 2749{ tier: 3, name: 'Professional', match: ['professional'], subtype: 'professional' } 2750]; 2751var STAMP_TYPE_LABEL_RE = /stamp\s*(type|style)/i; 2752var SESSION_ACCEPTED_KEY = 'ss_upsell_accepted_session'; 2753var SESSION_DECLINE_KEY = 'ss_upsell_decline_count'; 2754var SUCCESS_DWELL_MS = 1500; // how long the success state stays before submit 2755// After this many declines in a session, the modal stops firing -- respects 2756// a clear "no thanks" signal so we don't pester. Cancels at the config 2757// step count as declines too. Set to 0 for unlimited (no decline cap). 2758var MAX_DECLINES_PER_SESSION = 3; 2759// ============================================================ 2760// A/B VARIANT FRAMEWORK -- discount test 2761// ------------------------------------------------------------ 2762// Visitors are randomly assigned to A or B (50/50) on first PDP 2763// visit and persisted in localStorage. Same visitor always sees 2764// the same variant. URL override: ?ss_variant=B for QA. 2765// 2766// A = control: current production modal, no discount. 2767// B = treatment: same modal + 10% off coupon (UPSELL10) auto-applied. 2768// 2769// Variant content lives in VARIANT_CONFIG -- edit copy/discount here 2770// without touching the rendering logic below. 2771// ============================================================ 2772var VARIANT_STORAGE_KEY = 'ss_upsell_variant'; 2773var VARIANT_OVERRIDE_PARAM = 'ss_variant'; 2774var VARIANTS = ['A', 'B']; 2775var VARIANT_CONFIG = { 2776// A = CONTROL -- current production modal exactly as-is, no overrides. 2777A: { 2778// Empty = use defaults. Same as live production behavior. 2779}, 2780// B = DISCOUNT -- current modal + 10% discount on accept. The ONLY 2781// variable changing vs A is the discount; everything else (badge, 2782// copy, image, trust elements) stays identical so the lift can be 2783// attributed cleanly to the discount. 2784// 2785// Coupon codes are per-transition (keyed by upgrade target tier) so 2786// each transition can have its own Magento Cart Price Rule with its 2787// own conditions/scope. If a code leaks, only that specific upgrade 2788// path is exposed. 2789B: { 2790discountBanner: 'Save 10% on this upgrade -- today only', 2791couponCodes: { 2792'self-inking': 'UPSELL-SI10', // Wood Handle -> Self-Inking 2793'pre_inked': 'UPSELL-PI10', // Self-Inking -> Pre-Inked 2794'professional': 'UPSELL-PRO10' // Self-Inking -> Professional 2795}, 2796discountPct: 10 2797} 2798}; 2799// If false, the config step skips text inputs and textareas (stamp text 2800// lines, etc). Their values still carry over to the cart -- they just 2801// aren't re-shown for editing in the modal. 2802var INCLUDE_TEXT_FIELDS_IN_CONFIG = false; 2803// DEV TOGGLE -- when true, the modal fires on EVERY eligible ATC, ignoring 2804// the no-badger session lock. Flip to false before production launch so 2805// the lock kicks in after a user accepts an upgrade. 2806var DEV_MODE_ALWAYS_SHOW = false;
2807// ============================================================ 2808// CATEGORY TARGETING -- controlled rollout 2809// ------------------------------------------------------------ 2810// Use this to limit which product categories show the upsell modal. 2811// Substring match (case-insensitive) against the breadcrumb path. 2812// 2813// TARGET_CATEGORIES -- if non-empty, modal ONLY fires when the 2814// product's category breadcrumb contains one of these strings. 2815// Empty array = fire on all categories (default). 2816// 2817// EXCLUDED_CATEGORIES -- modal NEVER fires when the breadcrumb 2818// contains one of these strings, even if it also matches TARGET. 2819// Exclusions always win over targets. 2820// 2821// Examples: 2822// Target Address Stamps only: 2823// TARGET_CATEGORIES = ['Address Stamps']; 2824// Target everything EXCEPT custom/specialty stamps: 2825// EXCLUDED_CATEGORIES = ['Custom', 'Specialty']; 2826// Target Logo + Notary, exclude any "Bulk" sub-category: 2827// TARGET_CATEGORIES = ['Logo Stamps', 'Notary Stamps']; 2828// EXCLUDED_CATEGORIES = ['Bulk']; 2829// 2830// v1.32 ACTION NEEDED: these names came from the pre-Zephyr category 2831// landing page. Zephyr breadcrumbs read differently (a Duncan address 2832// stamp now shows "Address Stamps", not "Business Address Stamps"), so 2833// this whitelist must be re-mapped to the LIVE Zephyr breadcrumb strings 2834// or the modal won't fire on the categories you intend. Verify each on 2835// the live site and update below. 2836// ============================================================ 2837var TARGET_CATEGORIES = [ 2838'Office Stamps', 2839'Business Address Stamps', 2840'Signature Stamps', 2841'Date Stamps', 2842'Banking Stamps', 2843'Accounting & Tax', 2844'Postage Paid', 2845'Medical', 2846'Inspection', 2847'QR Code', 2848'Secure', 2849'Eco-Friendly', 2850'Checkbox', 2851'Custom Text' 2852]; 2853var EXCLUDED_CATEGORIES = [ 2854'Architects', 2855'Engineer', 2856'Land Surveyor', 2857'licensed Professional Stamps', 2858'Interior Design', 2859'LLC Stamps', 2860'Real Estate Appraisers' 2861]; // e.g. ['Custom', 'Specialty'] 2862// ============================================================ 2863// TIER IMAGES -- generic product image shown in the upsell modal 2864// ------------------------------------------------------------ 2865// Drop in a URL per upgrade tier. Image appears above the value 2866// checkmarks in step 1 of the modal. Leave a key empty to skip 2867// the image section for that tier (graceful fallback). 2868// 2869// Recommended: ~600x300px, transparent or white background, the 2870// bare stamp photographed straight-on. Hosted anywhere reachable 2871// by the site (Magento media folder, CDN, etc). 2872// ============================================================ 2873// v1.31 -- GIF assets, TEMPORARY LOCAL PATHS. 2874// Currently uploaded flat to wysiwyg/SimplyStamps/ with the "upsell-" prefix. 2875// TODO before prod push: move files to wysiwyg/SimplyStamps/upsell/ and rename 2876// to {tier}.gif (dropping the "upsell-" prefix), then update these paths to: 2877// 'self-inking': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/self-inking.gif', 2878// 'pre_inked': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/pre-inked.gif', 2879// 'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/professional.gif' 2880var TIER_IMAGES = { 2881'self-inking': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-self-inking.gif', 2882'pre_inked': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-pre-inked-v2.gif', 2883'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-professional-v2.gif' 2884}; 2885// ============================================================ 2886// CONFIG STEP -- tier-relevant option filtering 2887// ------------------------------------------------------------ 2888// PRIMARY MECHANISM: auto-detect tier-tagged labels. 2889// For each option label on the form, scan it for any tier name 2890// ('Pre-Inked', 'Self-Inking', 'Wood Handle', 'Professional'). 2891// - Mentions UPGRADE TARGET tier name -> SHOW 2892// - Mentions a DIFFERENT tier name -> HIDE 2893// - Mentions no tier name -> SHOW (generic option) 2894// 2895// This handles SimplyStamps's convention of naming options like 2896// "Pre-Inked Sizes", "Self-Inking Sizes", "Ink Color - Pre-Inked" 2897// with no manual config required. 2898// 2899// ESCAPE HATCH: TIER_OPTION_FILTERS below lets you override per tier 2900// for edge cases (labels that don't follow the convention). 2901// show: [] -- if non-empty, ONLY GENERIC options matching these 2902// keywords are kept. Tier-tagged auto-detection still runs. 2903// hide: [] -- additional labels to drop (substring match). 2904// 2905// Inspect what survives for a given product: 2906// window.SS_UPSELL_DEBUG = true; window.SSUpsell.inspect(); 2907// ============================================================ 2908var TIER_OPTION_FILTERS = { 2909// All upgrade tiers (Self-Inking, Pre-Inked, Professional) have 2910// built-in pads -- separate ink pad option is Wood-Handle-only. 2911'self_inking': { hide: ['ink pad', 'pad color'] }, 2912// Pre-Inked also has pre-loaded ink, so the extra-bottle option doesn't apply. 2913'pre_inked': { hide: ['ink pad', 'pad color', 'extra bottle', 'bottle of ink'] }, 2914'professional': { hide: ['ink pad', 'pad color'] } 2915}; 2916// v1.31 -- Routing flipped: default to Professional, override to Pre-Inked 2917// only if the product name/breadcrumb signals a logo/brand focus. 2918// Rationale: nearly all TARGET_CATEGORIES are professional-use stamps 2919// (office, notary, banking, medical, etc), so Professional is the norm. 2920// Pre-Inked's sweet spot is high-fidelity logo/brand reproduction -- 2921// that's the exception, so we key off those signals specifically. 2922var PRE_INKED_KEYWORDS = [ 2923'logo', 'brand', 'branding', 'monogram', 'crest', 'emblem' 2924]; 2925// v1.31 -- copy refresh per transition. New pattern: 2926// headline: short, all-caps, question-style attention grabber
2927// subhead: one-sentence value prop that reinforces the tier 2928// bullets: detail points (kept from v1.30, still useful proof) 2929// If headline is empty string, the<h2> is skipped and subhead renders alone. 2930var COPY = { 2931'1->self-inking': { 2932// Wood Handle -> Self-Inking (teacher buyup). Heading pairs with the 2933// subhead's "ink is already inside" promise -- calls out the ink pad 2934// as the pain point being solved. 2935headline: 'Skip The Ink Pad?', 2936subhead: 'The ink is already inside. Just press down and you get a clean, perfect mark.', 2937bullets: [ 2938'No separate ink pad needed -- built right in', 2939'10,000+ impressions before re-inking' 2940] 2941}, 2942'2->pre_inked': { 2943headline: 'Serious About Your Brand?', 2944subhead: 'Pre-inked stamps print sharper detail and cleaner lines. Your logo, exactly right every time.', 2945bullets: [ 2946'Sharper, crisper detail -- ideal for logos & branding', 2947'Up to 50,000 impressions per fill' 2948] 2949}, 2950'2->professional': { 2951headline: 'Are You A Professional?', 2952subhead: 'For desks that stamp all day. Heavier build, thousands of crisp impressions.', 2953bullets: [ 2954'Heavy-duty frame engineered for high-volume stamping', 2955'Durable mechanism backed by a long-term warranty' 2956] 2957} 2958}; 2959// Success state copy -- keyed the same way as COPY 2960var SUCCESS_COPY = { 2961'1->self-inking': { 2962headline: 'Upgraded to Self-Inking', 2963tagline: 'You\'ll get 10,000+ cleaner impressions without juggling a separate ink pad.' 2964}, 2965'2->pre_inked': { 2966headline: 'Upgraded to Pre-Inked', 2967tagline: 'Premium, sharper impressions -- up to 50,000 per fill. A smart pick for branding.' 2968}, 2969'2->professional': { 2970headline: 'Upgraded to Professional', 2971tagline: 'Built for daily volume -- durable mechanism backed by a long-term warranty.' 2972} 2973}; 2974// Session helpers -- once a user accepts an upgrade, don't badger them again. 2975// DEV_MODE_ALWAYS_SHOW bypasses the lock so the modal fires every ATC. 2976function hasUpgradedThisSession() { 2977if (DEV_MODE_ALWAYS_SHOW) return false; 2978try { return sessionStorage.getItem(SESSION_ACCEPTED_KEY) === '1'; } catch (e) { return false; } 2979} 2980function markUpgradedThisSession() { 2981try { sessionStorage.setItem(SESSION_ACCEPTED_KEY, '1'); } catch (e) {} 2982} 2983// Decline-cap helpers -- count declines (modal dismiss OR cancel at config) 2984// so the modal stops firing once the user has clearly opted out. 2985function getDeclineCount() { 2986try { return parseInt(sessionStorage.getItem(SESSION_DECLINE_KEY) || '0', 10) || 0; } 2987catch (e) { return 0; } 2988} 2989function incrementDeclineCount() {
2990try { sessionStorage.setItem(SESSION_DECLINE_KEY, String(getDeclineCount() + 1)); } catch (e) {} 2991} 2992function hasDeclinedTooMany() { 2993if (DEV_MODE_ALWAYS_SHOW) return false; 2994if (!MAX_DECLINES_PER_SESSION) return false; // 0 = unlimited 2995return getDeclineCount() >= MAX_DECLINES_PER_SESSION; 2996} 2997// ---------------------------------------------------------- 2998// Variant helpers (A/B/C framework) 2999// ---------------------------------------------------------- 3000function getAssignedVariant() { 3001// URL override wins (for QA) 3002try { 3003var url = new URL(window.location.href); 3004var override = (url.searchParams.get(VARIANT_OVERRIDE_PARAM) || '').toUpperCase(); 3005if (VARIANTS.indexOf(override) !== -1) return override; 3006} catch (e) {} 3007// Persisted in localStorage 3008try { 3009var stored = localStorage.getItem(VARIANT_STORAGE_KEY); 3010if (stored && VARIANTS.indexOf(stored) !== -1) return stored; 3011} catch (e) {} 3012// First visit -- random assignment 3013var idx = Math.floor(Math.random() * VARIANTS.length); 3014var variant = VARIANTS[idx]; 3015try { localStorage.setItem(VARIANT_STORAGE_KEY, variant); } catch (e) {} 3016log('Assigned new variant:', variant); 3017return variant; 3018} 3019function getVariantConfig(variant) { 3020return VARIANT_CONFIG[variant] || VARIANT_CONFIG.A || {}; 3021} 3022// Pick the right coupon code for this upgrade transition. Variant B uses 3023// a couponCodes map keyed by upgrade target ("self-inking", "pre_inked", 3024// "professional"). Legacy single couponCode also supported as fallback. 3025function resolveCouponCode(variantCfg, targetTier) { 3026if (!variantCfg) return null; 3027if (variantCfg.couponCodes && targetTier) { 3028var key = targetTier.subtype ? targetTier.subtype 3029: (targetTier.tier === 2 ? 'self-inking' : null); 3030if (key && variantCfg.couponCodes[key]) return variantCfg.couponCodes[key]; 3031} 3032return variantCfg.couponCode || null; 3033} 3034// Small local merge helper -- inline to avoid scope issues with the 3035// init()-scoped mergeParams. Used by both applyVariantCDiscount 3036// (stash phase) and applyDeferredCoupon (apply phase).
3037function _mergeParams(a, b) { 3038var out = {}; 3039for (var k in a) if (Object.prototype.hasOwnProperty.call(a, k)) out[k] = a[k]; 3040for (var k2 in b) if (Object.prototype.hasOwnProperty.call(b, k2)) out[k2] = b[k2]; 3041return out; 3042} 3043// v1.30 -- STASH the coupon code (do NOT POST from PDP). 3044// Previously we POSTed to /checkout/cart/couponPost/ from the PDP right 3045// before form.submit. That raced with the concurrent add-to-cart POST 3046// and hit an empty cart in Magento's session, so the coupon never stuck. 3047// Now we save the code + tracking context to sessionStorage; the actual 3048// apply happens on the cart page once the item is in cart (see 3049// applyDeferredCoupon below). 3050// 3051// v1.32 -- On Zephyr the AJAX add opens the cart DRAWER instead of 3052// navigating to /checkout/cart, so the stashed coupon applies the next 3053// time the shopper opens the full cart / checkout (still before purchase). 3054var PENDING_COUPON_KEY = 'ss_upsell_pending_coupon'; 3055function applyVariantCDiscount(couponCode, trackingParams) { 3056if (!couponCode) return; 3057try { 3058var payload = { 3059code: couponCode, 3060tracking: trackingParams || {}, 3061ts: Date.now() 3062}; 3063sessionStorage.setItem(PENDING_COUPON_KEY, JSON.stringify(payload)); 3064log('Coupon deferred for cart-page application:', couponCode); 3065} catch (e) { 3066log('Failed to defer coupon:', e); 3067} 3068} 3069// v1.30 -- Apply any pending upsell coupon on the cart page. 3070// Runs at module scope so it fires on every page load, cart or not. 3071// Bails immediately if not on the cart page or no pending coupon. 3072// Clears sessionStorage BEFORE posting to prevent reload loops if 3073// the POST fails or reload happens for an unrelated reason. 3074function applyDeferredCoupon() { 3075// Only run on the cart page 3076if (!/\/checkout\/cart(\/|\?|$)/.test(window.location.pathname + window.location.search)) return; 3077var raw; 3078try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; } 3079if (!raw) return; 3080var data; 3081try { data = JSON.parse(raw); } catch (e) { data = null; } 3082if (!data || !data.code) { 3083try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3084return; 3085} 3086// Clear FIRST so a failed apply doesn't cause a reload loop 3087try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3088log('Applying deferred coupon on cart page:', data.code);
3089var params = _mergeParams(data.tracking || {}, { coupon_code: data.code }); 3090try { 3091var fd = new FormData(); 3092fd.append('form_key', getFormKey()); 3093fd.append('coupon_code', data.code); 3094fd.append('remove', '0'); 3095fetch('/checkout/cart/couponPost/', { 3096method: 'POST', 3097body: fd, 3098credentials: 'same-origin', 3099headers: { 'X-Requested-With': 'XMLHttpRequest' } 3100}).then(function (r) { 3101log('Deferred coupon response:', r.status); 3102if (r.status >= 200 && r.status < 400) { 3103track('upsell_discount_applied', _mergeParams(params, { http_status: r.status })); 3104// Reload cart so the discount appears in the totals. 3105// Small delay so the event has time to fire before navigation. 3106setTimeout(function () { 3107// Guard against loop -- only reload if sessionStorage is now empty 3108var still; 3109try { still = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { still = null; } 3110if (!still) window.location.reload(); 3111}, 150); 3112} else { 3113track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response' })); 3114} 3115}).catch(function (e) { 3116log('Deferred coupon POST failed:', e); 3117track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) })); 3118}); 3119} catch (e) { 3120log('Deferred coupon apply threw:', e); 3121track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) })); 3122} 3123} 3124// v1.32b -- Apply a stashed coupon IMMEDIATELY (no page gate, no reload). 3125// On Zephyr the AJAX add opens the cart drawer and never loads /checkout/cart, 3126// so applyDeferredCoupon (cart-page only) might never run -- the shopper could 3127// reach checkout via the drawer and be charged full price on a promised 3128// discount. We call this right after the add's promise resolves, so the cart 3129// already has the item (no v1.30 race) and the coupon lands on the quote. 3130// On success we clear the stash; on failure we LEAVE it so the cart-page 3131// applyDeferredCoupon can still retry as a fallback. 3132function applyPendingCouponNow() { 3133var raw; 3134try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; } 3135if (!raw) return; 3136var data; 3137try { data = JSON.parse(raw); } catch (e) { data = null; } 3138if (!data || !data.code) { try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} return; } 3139log('Applying pending coupon immediately (post-add):', data.code);
3140var params = _mergeParams(data.tracking || {}, { coupon_code: data.code }); 3141try { 3142var fd = new FormData(); 3143fd.append('form_key', getFormKey()); 3144fd.append('coupon_code', data.code); 3145fd.append('remove', '0'); 3146fetch('/checkout/cart/couponPost/', { 3147method: 'POST', 3148body: fd, 3149credentials: 'same-origin', 3150headers: { 'X-Requested-With': 'XMLHttpRequest' } 3151}).then(function (r) { 3152log('Immediate coupon response:', r.status); 3153if (r.status >= 200 && r.status < 400) { 3154// Applied -- clear so the cart page doesn't reload to re-apply. 3155try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3156track('upsell_discount_applied', _mergeParams(params, { http_status: r.status, applied_from: 'pdp_post_add' })); 3157} else { 3158// Leave stashed -- cart-page applyDeferredCoupon will retry. 3159track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response', applied_from: 'pdp_post_add' })); 3160} 3161}).catch(function (e) { 3162log('Immediate coupon POST failed (will retry on cart page):', e); 3163track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e), applied_from: 'pdp_post_add' })); 3164}); 3165} catch (e) { 3166log('Immediate coupon apply threw:', e); 3167} 3168} 3169// Run deferred coupon apply at module scope (fires on every page load, 3170// including cart page -- bails if not cart / no pending coupon). This is the 3171// fallback for when the immediate post-add apply (applyPendingCouponNow) 3172// didn't run or didn't succeed. 3173try { applyDeferredCoupon(); } catch (e) { /* never break page load */ } 3174// Record each upgrade so the order-success page can fire upsell_purchased 3175// with order_id + revenue_delta. Stored as an array in sessionStorage; 3176// the success-page snippet reads, dispatches, and clears it. 3177function recordUpgradeForPurchaseTracking(params, qty) { 3178try { 3179var key = 'ss_upsell_pending_purchases'; 3180var existing = sessionStorage.getItem(key); 3181var arr = existing ? JSON.parse(existing) : []; 3182var rec = { qty: parseInt(qty, 10) || 1, ts: Date.now() }; 3183for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) rec[k] = params[k]; 3184arr.push(rec); 3185sessionStorage.setItem(key, JSON.stringify(arr)); 3186log('Recorded pending upgrade for purchase tracking:', rec); 3187} catch (e) {} 3188} 3189function log() { 3190if (window.SS_UPSELL_DEBUG && window.console) { 3191var args = ['[TierUpsell]'].concat([].slice.call(arguments)); 3192console.log.apply(console, args); 3193} 3194} 3195function norm(s) { return (s || '').toLowerCase().trim(); } 3196function findTierForText(text) { 3197var t = norm(text); 3198for (var i = 0; i < TIER_MAP.length; i++) { 3199var tier = TIER_MAP[i]; 3200for (var j = 0; j < tier.match.length; j++) { 3201if (t.indexOf(tier.match[j]) !== -1) return tier; 3202} 3203} 3204return null; 3205} 3206// Like findTierForText but returns ALL tiers whose name appears in the text. 3207// Used to detect which tier an option label is tagged for. 3208function findAllTiersInText(text) { 3209var t = norm(text); 3210var matches = []; 3211for (var i = 0; i < TIER_MAP.length; i++) { 3212var tier = TIER_MAP[i]; 3213for (var j = 0; j < tier.match.length; j++) { 3214if (t.indexOf(tier.match[j]) !== -1) { matches.push(tier); break; } 3215} 3216} 3217return matches; 3218} 3219function findStampTypeSelect() { 3220var labels = document.querySelectorAll('.product-options label, .product-options-wrapper label, dt label, .field label, label'); 3221for (var i = 0; i < labels.length; i++) { 3222var lbl = labels[i]; 3223if (!STAMP_TYPE_LABEL_RE.test(lbl.textContent)) continue; 3224var forId = lbl.getAttribute('for'); 3225if (forId) { 3226var byId = document.getElementById(forId); 3227if (byId && byId.tagName === 'SELECT') return byId; 3228} 3229var container = lbl.closest && (lbl.closest('.field, dd, dt, li, .input-box, .product-options li')) || lbl.parentElement; 3230if (container) { 3231var sel = container.querySelector('select'); 3232if (sel) return sel; 3233} 3234} 3235var selects = document.querySelectorAll('.product-options select, .product-options-wrapper select'); 3236for (var k = 0; k < selects.length; k++) { 3237for (var m = 0; m < selects[k].options.length; m++) { 3238if (findTierForText(selects[k].options[m].text)) return selects[k]; 3239} 3240} 3241return null; 3242} 3243function getCurrentTier(selectEl) { 3244if (!selectEl || selectEl.selectedIndex < 0) return null; 3245var opt = selectEl.options[selectEl.selectedIndex]; 3246var tier = findTierForText(opt.text); 3247return tier ? { tier: tier, optionEl: opt } : null; 3248} 3249// Scan the dropdown -- return which tiers are actually on THIS product. 3250// Keys: tier1, tier2, pre_inked, professional 3251function getAvailableTiers(selectEl) { 3252var available = {}; 3253for (var i = 0; i < selectEl.options.length; i++) { 3254var t = findTierForText(selectEl.options[i].text); 3255if (!t) continue; 3256if (t.subtype) available[t.subtype] = t; 3257else available['tier' + t.tier] = t; 3258} 3259return available; 3260} 3261// Available options trump path preference. Preferred T3 first; fall back 3262// to the other T3 before bailing. 3263function getNextTier(currentTier, upsellPath, availableTiers) { 3264if (!currentTier) return null; 3265if (currentTier.tier >= 3) return null; 3266if (currentTier.tier === 1) { 3267return availableTiers.tier2 || null; 3268} 3269if (currentTier.tier === 2) {
3270var preferred = upsellPath === 'professional' ? 'professional' : 'pre_inked'; 3271var fallback = upsellPath === 'professional' ? 'pre_inked' : 'professional'; 3272return availableTiers[preferred] || availableTiers[fallback] || null; 3273} 3274return null; 3275} 3276function findOptionInSelect(selectEl, tierDef) { 3277for (var i = 0; i < selectEl.options.length; i++) { 3278if (findTierForText(selectEl.options[i].text) === tierDef) return selectEl.options[i]; 3279} 3280return null; 3281} 3282function parsePriceModifier(text) { 3283if (!text) return 0; 3284var m = text.match(/[+\-]?\s*\$\s*([\d,]+\.\d{2})/); 3285return m ? parseFloat(m[1].replace(/,/g, '')) : 0; 3286} 3287function calcPriceDelta(currentOpt, upgradeOpt) { 3288var cur = parsePriceModifier(currentOpt.text); 3289var up = parsePriceModifier(upgradeOpt.text); 3290return Math.max(0, up - cur); 3291} 3292function getFormKey() { 3293if (window.FORM_KEY) return window.FORM_KEY; 3294var input = document.querySelector('input[name="form_key"]'); 3295return input ? input.value : ''; 3296} 3297function getOptionIdFromSelectName(selectName) { 3298var m = (selectName || '').match(/options\[(\d+)\]/); 3299return m ? m[1] : null; 3300} 3301function detectUpsellPath() { 3302var explicit = document.querySelector('[data-upsell-path]'); 3303if (explicit) { 3304var val = explicit.getAttribute('data-upsell-path'); 3305log('upsell_path from data attribute:', val); 3306return val; 3307} 3308var hay = ''; 3309hay += ' ' + (document.body.className || ''); 3310var crumbs = document.querySelectorAll('.breadcrumbs a, .breadcrumbs li, nav[aria-label*="readcrumb"] a, nav[aria-label*="readcrumb"] li'); 3311for (var i = 0; i < crumbs.length; i++) hay += ' ' + (crumbs[i].textContent || ''); 3312var h1 = document.querySelector('.product-name h1, h1.product-name, h1[itemprop="name"], h1'); 3313if (h1) hay += ' ' + (h1.textContent || ''); 3314hay = hay.toLowerCase(); 3315// v1.31 -- flipped: check for pre-inked signals (logo/brand focus). 3316// If any pre-inked keyword matches, route to pre_inked. Otherwise 3317// default to professional (aligns with our target category mix). 3318for (var k = 0; k < PRE_INKED_KEYWORDS.length; k++) { 3319if (hay.indexOf(PRE_INKED_KEYWORDS[k]) !== -1) { 3320log('upsell_path = pre_inked (matched keyword:', PRE_INKED_KEYWORDS[k], ')'); 3321return 'pre_inked'; 3322} 3323} 3324log('upsell_path = professional (default)'); 3325return 'professional'; 3326} 3327// Check if the current product's category breadcrumb passes the 3328// TARGET_CATEGORIES / EXCLUDED_CATEGORIES filters. Returns true if the 3329// upsell modal should fire for this product. 3330function isCategoryAllowed(categoryBreadcrumb) { 3331var crumb = String(categoryBreadcrumb || '').toLowerCase(); 3332// Exclusions always win 3333for (var i = 0; i < EXCLUDED_CATEGORIES.length; i++) { 3334var ex = String(EXCLUDED_CATEGORIES[i]).toLowerCase(); 3335if (ex && crumb.indexOf(ex) !== -1) { 3336log('Category excluded by rule:', EXCLUDED_CATEGORIES[i]); 3337return false; 3338} 3339} 3340// If no targets defined, allow all 3341if (TARGET_CATEGORIES.length === 0) return true; 3342// Targets defined -- must match at least one 3343for (var j = 0; j < TARGET_CATEGORIES.length; j++) { 3344var tg = String(TARGET_CATEGORIES[j]).toLowerCase(); 3345if (tg && crumb.indexOf(tg) !== -1) return true; 3346} 3347log('Category not in TARGET_CATEGORIES list -- skipping'); 3348return false; 3349} 3350function detectCategory() { 3351// v1.32 -- Zephyr renders breadcrumbs as<li><a>...</a></li>. The old 3352// query (".breadcrumbs a, .breadcrumbs li") double-counted every crumb 3353// (e.g. "Address Stamps > Address Stamps > View All > View All"). Collect 3354// ONE entry per crumb (prefer the<li> text, which contains its anchor), 3355// drop Home, and collapse consecutive duplicates. 3356var items = document.querySelectorAll('.breadcrumbs li'); 3357if (!items.length) items = document.querySelectorAll('.breadcrumbs a'); 3358var trail = []; 3359for (var i = 0; i < items.length; i++) { 3360var t = (items[i].textContent || '').replace(/\s+/g, ' ').trim(); 3361if (!t || t.toLowerCase() === 'home') continue; 3362if (trail.length && trail[trail.length - 1].toLowerCase() === t.toLowerCase()) continue; 3363trail.push(t); 3364} 3365return trail.join(' > '); 3366} 3367function track(eventName, payload) { 3368window.dataLayer = window.dataLayer || []; 3369var data = { event: eventName }; 3370for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) data[k] = payload[k]; 3371window.dataLayer.push(data); 3372log('Event:', eventName, payload); 3373} 3374// Resolve the image URL for an upgrade tier. Uses subtype for tier 3 3375// (pre_inked / professional) and 'self-inking' for tier 2. 3376function getTierImage(tierDef) { 3377if (!tierDef) return ''; 3378var key = tierDef.subtype ? tierDef.subtype 3379: (tierDef.tier === 2 ? 'self-inking' : null); 3380if (!key) return ''; 3381return TIER_IMAGES[key] || ''; 3382} 3383function buildModal(ctx) { 3384var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype; 3385var copy = COPY[key]; 3386if (!copy) return null; 3387// Resolve variant config (passed via ctx so buildModal stays pure) 3388var vCfg = ctx.variantCfg || {};
3389var overlay = document.createElement('div'); 3390overlay.className = 'ss-upsell-overlay'; 3391overlay.setAttribute('role', 'presentation'); 3392var bulletHtml = copy.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join(''); 3393var imgUrl = getTierImage(ctx.next); 3394var imgHtml = imgUrl 3395? '<div class="ss-upsell-image"><img src="' + imgUrl + '" alt="' + escHtml(ctx.next.name) + ' stamp"></div>' 3396: ''; 3397// === Variant-aware elements === 3398// v1.31 -- "Recommended upgrade" chip/badge removed from Step 1 render. 3399// The badge variables + CSS classes remain in the file (dead code) in 3400// case a future variant wants to bring back a chip. To re-enable, add 3401// badgeHtml back into the innerHTML template below. 3402var badgeText = vCfg.badgeText || 'Recommended upgrade'; 3403var badgeClass = vCfg.badgeClass || ''; 3404var badgeHtml = '<div class="ss-upsell-badge ' + badgeClass + '">' + escHtml(badgeText) + '</div>'; 3405void badgeHtml; // suppress unused warning; kept for potential future re-enable 3406// Discount banner (variant B only) 3407var discountBannerHtml = vCfg.discountBanner 3408? '<div class="ss-upsell-discount-banner">' + vCfg.discountBanner + '</div>' 3409: ''; 3410// Price line -- strikethrough + discounted price for variant C 3411var priceLineHtml; 3412if (vCfg.discountPct && ctx.priceDelta > 0) { 3413var discounted = (ctx.priceDelta * (1 - vCfg.discountPct / 100)).toFixed(2); 3414priceLineHtml = 3415'<p class="ss-upsell-price">' + 3416'Upgrade for ' + 3417'<span class="ss-upsell-price-strike">+$' + ctx.priceDelta.toFixed(2) + '</span>' + 3418'<strong>+$' + discounted + '</strong>' + 3419'</p>'; 3420} else { 3421priceLineHtml = '<p class="ss-upsell-price">Upgrade for <strong>+$' + ctx.priceDelta.toFixed(2) + '</strong></p>'; 3422} 3423// Cost-per-use line (variant B + C) 3424var costPerUseHtml = ''; 3425if (vCfg.showCostPerUse && ctx.priceDelta > 0) { 3426var impressions = ctx.next.subtype === 'pre_inked' ? 50000 : (ctx.next.tier === 2 ? 10000 : 100000); 3427var costPerUse = (ctx.priceDelta / impressions).toFixed(4); 3428costPerUseHtml = '<div class="ss-upsell-cost-per-use">~ $' + costPerUse + ' per impression over the life of this stamp</div>'; 3429} 3430// Trust signal (variant B + C) 3431var trustHtml = ''; 3432if (vCfg.trustSignal) { 3433var t = vCfg.trustSignal; 3434var parts = []; 3435if (t.ratingValue && t.ratingCount) { 3436parts.push('<span class="ss-stars">★★★★★</span> ' + escHtml(t.ratingValue) + ' from ' + escHtml(t.ratingCount) + ' reviews'); 3437} 3438if (t.returnPolicy) parts.push(escHtml(t.returnPolicy)); 3439if (parts.length) trustHtml = '<div class="ss-upsell-trust">' + parts.join(' | ') + '</div>'; 3440} 3441// v1.31 -- optional headline + subhead. If headline is empty (e.g., the 3442// Wood Handle -> Self-Inking transition until copy is finalized), 3443// skip the<h2> so the subhead reads clean on its own. 3444var headlineHtml = copy.headline 3445? '<h2 id="ss-upsell-h">' + escHtml(copy.headline) + '</h2>' 3446: ''; 3447var subheadHtml = copy.subhead 3448? '<p class="ss-upsell-subhead">' + escHtml(copy.subhead) + '</p>' 3449: ''; 3450overlay.innerHTML = 3451'<div class="ss-upsell-modal" role="dialog" aria-modal="true"' + 3452(copy.headline ? ' aria-labelledby="ss-upsell-h"' : '') + '>' + 3453'<button class="ss-upsell-close" aria-label="Close">×</button>' + 3454// v1.31 -- badge removed from render 3455headlineHtml + 3456subheadHtml + 3457imgHtml + 3458discountBannerHtml + 3459'<ul class="ss-upsell-bullets">' + bulletHtml + '</ul>' + 3460priceLineHtml + 3461costPerUseHtml + 3462trustHtml + 3463'<div class="ss-upsell-actions">' + 3464'<button class="ss-upsell-accept" type="button">Upgrade my stamp</button>' + 3465'<button class="ss-upsell-decline" type="button">No thanks</button>' + 3466'</div>' + 3467'</div>'; 3468return overlay; 3469} 3470function showModal(ctx) { 3471var overlay = buildModal(ctx); 3472if (!overlay) { log('No copy template for transition -- skipping'); return; } 3473document.body.appendChild(overlay); 3474var accept = overlay.querySelector('.ss-upsell-accept'); 3475var decline = overlay.querySelector('.ss-upsell-decline'); 3476var close = overlay.querySelector('.ss-upsell-close'); 3477// v1.32b -- once the user has Accepted, step-1 dismissal must be dead. 3478// Otherwise (Zephyr AJAX path, no navigation) an outside/backdrop click 3479// during the success-state dwell would fire onDecline -> doSubmit and add 3480// the item a SECOND time alongside the success timer's submit. 3481var accepted = false;
3482function dismiss(method) { 3483if (accepted) return; 3484if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 3485document.removeEventListener('keydown', escHandler); 3486ctx.onDecline(method); 3487} 3488function escHandler(e) { if (e.key === 'Escape') dismiss('escape'); } 3489document.addEventListener('keydown', escHandler); 3490accept.addEventListener('click', function () { 3491// Hand the overlay to onAccept -- it decides whether to chain into the 3492// config step, the success state, or simply close. 3493if (accepted) return; // v1.32b -- ignore double-click on Upgrade 3494accepted = true; 3495document.removeEventListener('keydown', escHandler); 3496ctx.onAccept(overlay); 3497}); 3498decline.addEventListener('click', function () { dismiss('decline_button'); }); 3499close.addEventListener('click', function () { dismiss('close_button'); }); 3500overlay.addEventListener('click', function (e) { if (e.target === overlay) dismiss('outside_click'); }); 3501setTimeout(function () { accept.focus(); }, 50); 3502} 3503// Pick a sane default value for a required field that has no current 3504// value selected. Prefers options labeled "No" (avoids accidentally 3505// adding paid extras like "Yes - +$5.00") over the first non-empty. 3506function autoDefaultFor(field) { 3507if (!field || !field.choices) return ''; 3508if (field.currentValue && String(field.currentValue).trim() !== '') return field.currentValue; 3509if (!field.required) return field.currentValue || ''; 3510// Pass 1: prefer an option whose text starts with "no" 3511for (var i = 0; i < field.choices.length; i++) { 3512var c = field.choices[i]; 3513if (!c.value || c.disabled) continue; 3514if (/^\s*no\b/i.test(c.text)) return c.value; 3515} 3516// Pass 2: first non-empty, non-disabled option 3517for (var j = 0; j < field.choices.length; j++) { 3518var c2 = field.choices[j]; 3519if (c2.value && !c2.disabled) return c2.value; 3520} 3521return ''; 3522} 3523// Step 2: render the config step inside the existing overlay so the 3524// user can review/adjust their other custom options for the upgrade. 3525function showConfigStep(overlay, ctx, fields, onConfirm, onCancel) { 3526var fieldsHtml = fields.map(function (field, i) { 3527var req = field.required ? ' <span class="ss-config-required">*</span>' : ''; 3528var effectiveValue = autoDefaultFor(field); 3529if (field.type === 'select') { 3530var optHtml = field.choices.map(function (c) { 3531var sel = c.value === effectiveValue ? ' selected' : ''; 3532var dis = c.disabled ? ' disabled' : ''; 3533return '<option value="' + escHtml(c.value) + '"' + sel + dis + '>' + escHtml(c.text) + '</option>'; 3534}).join(''); 3535return '<div class="ss-opt-field">' + 3536'<label>' + escHtml(field.label) + req + '</label>' + 3537'<select data-field-idx="' + i + '">' + optHtml + '</select>' + 3538'</div>'; 3539} 3540if (field.type === 'radio') { 3541var radioHtml = field.choices.map(function (c) { 3542var ch = c.value === effectiveValue ? ' checked' : ''; 3543return '<label class="ss-opt-radio">' + 3544'<input type="radio" name="ss-radio-' + i + '" value="' + escHtml(c.value) + '"' + ch + '> ' +
3545escHtml(c.text) + 3546'</label>'; 3547}).join(''); 3548return '<div class="ss-opt-field" data-field-idx="' + i + '">' + 3549'<div class="ss-opt-group-label">' + escHtml(field.label) + req + '</div>' + 3550radioHtml + 3551'</div>'; 3552} 3553if (field.type === 'text') { 3554var maxAttr = field.maxlength ? ' maxlength="' + field.maxlength + '"' : ''; 3555return '<div class="ss-opt-field">' + 3556'<label>' + escHtml(field.label) + req + '</label>' + 3557'<input type="text" data-field-idx="' + i + '" value="' + escHtml(field.currentValue) + '"' + maxAttr + '>' + 3558'</div>'; 3559} 3560if (field.type === 'textarea') { 3561return '<div class="ss-opt-field">' + 3562'<label>' + escHtml(field.label) + req + '</label>' + 3563'<textarea data-field-idx="' + i + '" rows="3">' + escHtml(field.currentValue) + '</textarea>' + 3564'</div>'; 3565} 3566return ''; 3567}).join(''); 3568// v1.31 -- config-step badge removed to match Step 1 chip removal. 3569// Cancel button now inherits the small-link style (ss-config-cancel 3570// modifier retained for JS querySelector but no longer overrides CSS). 3571overlay.innerHTML = 3572'<div class="ss-upsell-modal" role="dialog" aria-modal="true" aria-labelledby="ss-config-h">' + 3573'<button class="ss-upsell-close" aria-label="Cancel">×</button>' + 3574'<h2 id="ss-config-h">Review your options</h2>' + 3575'<p class="ss-config-intro">Confirm the selections for your upgraded stamp.</p>' + 3576'<div class="ss-config-fields">' + fieldsHtml + '</div>' + 3577'<div class="ss-upsell-actions">' + 3578'<button class="ss-upsell-accept" type="button">Confirm upgrade</button>' + 3579'<button class="ss-upsell-decline ss-config-cancel" type="button">Cancel upgrade</button>' + 3580'</div>' + 3581'</div>'; 3582var confirmBtn = overlay.querySelector('.ss-upsell-accept'); 3583var cancelBtn = overlay.querySelector('.ss-config-cancel'); 3584var closeBtn = overlay.querySelector('.ss-upsell-close'); 3585// v1.32b -- guard against a double-click on Confirm firing two submits 3586// (the Zephyr AJAX add doesn't navigate, so a second one really adds twice). 3587var confirmed = false; 3588confirmBtn.addEventListener('click', function () { 3589if (confirmed) return; 3590var values = {}; 3591var missing = []; 3592fields.forEach(function (field, i) { 3593var val = null; 3594if (field.type === 'select') { 3595var s = overlay.querySelector('select[data-field-idx="' + i + '"]'); 3596if (s) val = s.value; 3597} else if (field.type === 'radio') { 3598var r = overlay.querySelector('[data-field-idx="' + i + '"] input[type="radio"]:checked'); 3599if (r) val = r.value; 3600} else if (field.type === 'text' || field.type === 'textarea') { 3601var ti = overlay.querySelector('[data-field-idx="' + i + '"]'); 3602if (ti) val = ti.value; 3603} 3604if (val != null) values[field.name] = val; 3605if (field.required && (val == null || String(val).trim() === '')) { 3606missing.push(i); 3607} 3608}); 3609// Clear prior error styling 3610var oldErrs = overlay.querySelectorAll('.ss-opt-field-error'); 3611for (var e = 0; e < oldErrs.length; e++) oldErrs[e].classList.remove('ss-opt-field-error'); 3612var oldMsgs = overlay.querySelectorAll('.ss-opt-error'); 3613for (var m = 0; m < oldMsgs.length; m++) oldMsgs[m].parentNode && oldMsgs[m].parentNode.removeChild(oldMsgs[m]); 3614// Mark missing fields and bail if any 3615if (missing.length > 0) { 3616missing.forEach(function (idx, n) { 3617var el = overlay.querySelector('[data-field-idx="' + idx + '"]'); 3618if (!el) return; 3619var fieldEl = el.closest('.ss-opt-field') || el.parentElement; 3620if (!fieldEl) return; 3621fieldEl.classList.add('ss-opt-field-error'); 3622var msg = document.createElement('div'); 3623msg.className = 'ss-opt-error'; 3624msg.textContent = 'Please make a selection'; 3625fieldEl.appendChild(msg); 3626if (n === 0) { 3627try { fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) { fieldEl.scrollIntoView(); } 3628} 3629}); 3630track('upsell_config_validation_failed', { missing_count: missing.length }); 3631return; 3632} 3633confirmed = true; // v1.32b -- lock after a valid confirm 3634onConfirm(values); 3635}); 3636cancelBtn.addEventListener('click', function () { if (confirmed) return; onCancel('cancel_button'); }); 3637closeBtn.addEventListener('click', function () { if (confirmed) return; onCancel('close_button'); }); 3638setTimeout(function () { confirmBtn.focus(); }, 50); 3639} 3640// Replace the modal contents with a celebratory success state. After 3641// SUCCESS_DWELL_MS, run ctx.onSuccessComplete (which triggers the submit). 3642function showSuccessState(overlay, ctx) { 3643var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype; 3644var copy = SUCCESS_COPY[key] || { headline: 'Upgrade applied', tagline: 'Your stamp has been upgraded.' }; 3645overlay.innerHTML = 3646// v1.31 -- "Premium upgrade applied" chip removed to match Step 1 + Step 2. 3647// Green checkmark icon carries the celebration signal on its own now. 3648'<div class="ss-upsell-modal ss-upsell-success" role="dialog" aria-modal="true" aria-labelledby="ss-upsell-success-h">' + 3649'<div class="ss-upsell-success-icon" aria-hidden="true">✓</div>' + 3650'<h2 id="ss-upsell-success-h">' + copy.headline + '</h2>' + 3651'<p class="ss-upsell-success-tagline">
3651' + copy.tagline + '</p>' + 3652'<p class="ss-upsell-success-cta"><span class="ss-upsell-spinner"></span> Continuing to your cart...</p>' + 3653'<button class="ss-upsell-success-skip" type="button">Continue now</button>' + 3654'</div>'; 3655var skipBtn = overlay.querySelector('.ss-upsell-success-skip'); 3656var done = false; 3657function finalize() { 3658if (done) return; 3659done = true; 3660// Intentionally DO NOT remove the overlay here. We want the success 3661// state to remain visible while the browser navigates to the cart. 3662// The page transition will replace the modal naturally -- no flicker 3663// of bare PDP between success state and cart load. 3664// 3665// v1.32 note: on Zephyr the AJAX add opens the cart drawer rather than 3666// navigating away, so the success state is removed by handleAjaxAddToCart 3667// /the drawer taking over. It still dwells for SUCCESS_DWELL_MS first. 3668if (typeof ctx.onSuccessComplete === 'function') ctx.onSuccessComplete(); 3669} 3670skipBtn.addEventListener('click', finalize); 3671setTimeout(finalize, SUCCESS_DWELL_MS); 3672} 3673// ============================================================ 3674// CONFIG STEP -- extract / sync custom options 3675// ============================================================ 3676function escHtml(s) { 3677return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 3678} 3679function isElementHidden(el) { 3680if (!el) return true; 3681if (el.type === 'hidden') return true; 3682// Walk up the ancestor chain checking each computed style. 3683// Catches display:none, visibility:hidden, opacity:0 on the element 3684// OR any of its ancestors up to the form. 3685var node = el; 3686while (node && node !== document && node !== document.body) { 3687var s; 3688try { s = window.getComputedStyle(node); } catch (e) { s = null; } 3689if (s) { 3690if (s.display === 'none') return true; 3691if (s.visibility === 'hidden') return true; 3692if (parseFloat(s.opacity) === 0) return true; 3693} 3694node = node.parentElement; 3695} 3696// Final fallback -- offsetParent null catches some edge cases 3697if (el.offsetParent === null && el.tagName !== 'BODY') { 3698var rect = el.getBoundingClientRect(); 3699if (rect.width === 0 && rect.height === 0) return true; 3700} 3701return false; 3702} 3703function cleanLabel(s) { 3704return String(s || '').replace(/\*/g, '').replace(/:\s*$/, '').replace(/\s+/g, ' ').trim(); 3705} 3706function findLabelForFormField(el) { 3707if (!el) return 'Option'; 3708if (el.id) { 3709var byFor = document.querySelector('label[for="' + el.id + '"]'); 3710if (byFor) return cleanLabel(byFor.textContent); 3711} 3712// Magento default: previous DT contains the label 3713var dd = el.closest && el.closest('dd'); 3714if (dd) { 3715var prevDt = dd.previousElementSibling; 3716if (prevDt && prevDt.tagName === 'DT') { 3717var lblInDt = prevDt.querySelector('label') || prevDt; 3718return cleanLabel(lblInDt.textContent); 3719} 3720} 3721var field = el.closest && el.closest('.field, .input-box, li'); 3722if (field) { 3723var inLbl = field.querySelector('label.label, label'); 3724if (inLbl) return cleanLabel(inLbl.textContent); 3725} 3726return 'Option'; 3727} 3728// Pull the form's other custom option fields so we can render them in 3729// the config step. Skips the Stamp Type dropdown and any hidden fields. 3730function extractCustomOptions(form, excludeSelectEl) { 3731var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 3732var fields = []; 3733// Selects 3734var selects = container.querySelectorAll('select[name^="options"]'); 3735for (var i = 0; i < selects.length; i++) { 3736var sel = selects[i]; 3737if (sel === excludeSelectEl) continue; 3738if (isElementHidden(sel)) continue; 3739var choices = []; 3740for (var j = 0; j < sel.options.length; j++) { 3741var opt = sel.options[j]; 3742choices.push({ value: opt.value, text: opt.text, disabled: opt.disabled }); 3743} 3744fields.push({ 3745type: 'select', 3746name: sel.name, 3747label: findLabelForFormField(sel), 3748currentValue: sel.value, 3749required: sel.hasAttribute('required') || sel.classList.contains('required-entry') || sel.classList.contains('validate-select'), 3750choices: choices 3751}); 3752} 3753// Text inputs + textareas (stamp text, line 1, line 2, etc.) 3754// Gated by INCLUDE_TEXT_FIELDS_IN_CONFIG so the modal stays focused on 3755// tier-relevant choices. Text values still carry to the cart untouched. 3756if (INCLUDE_TEXT_FIELDS_IN_CONFIG) { 3757var texts = container.querySelectorAll('input[type="text"][name^="options"], textarea[name^="options"]'); 3758for (var t = 0; t < texts.length; t++) { 3759var inp = texts[t]; 3760if (isElementHidden(inp)) continue; 3761fields.push({ 3762type: inp.tagName === 'TEXTAREA' ? 'textarea' : 'text', 3763name: inp.name, 3764label: findLabelForFormField(inp), 3765currentValue: inp.value, 3766required: inp.hasAttribute('required') || inp.classList.contains('required-entry'), 3767maxlength: inp.maxLength > 0 ? inp.maxLength : null 3768}); 3769} 3770} 3771// Radio groups 3772var radios = container.querySelectorAll('input[type="radio"][name^="options"]'); 3773var groups = {}; 3774for (var k = 0; k < radios.length; k++) { 3775var r = radios[k]; 3776if (isElementHidden(r)) continue; 3777if (!groups[r.name]) groups[r.name] = []; 3778groups[r.name].push(r); 3779} 3780for (var name in groups) { 3781if (!Object.prototype.hasOwnProperty.call(groups, name)) continue; 3782var grp = groups[name]; 3783var checked = null; 3784var rChoices = []; 3785for (var m = 0; m < grp.length; m++) { 3786if (grp[m].checked) checked = grp[m]; 3787rChoices.push({ value: grp[m].value, text: findLabelForFormField(grp[m]) || grp[m].value }); 3788} 3789fields.push({ 3790type: 'radio', 3791name: name, 3792label: findLabelForFormField(grp[0]), 3793currentValue: checked ? checked.value : '', 3794required: grp[0].classList.contains('validate-one-required-by-name'), 3795choices: rChoices 3796}); 3797} 3798return fields; 3799} 3800// Normalize a tier definition into a key for TIER_OPTION_FILTERS. 3801// Self-Inking -> 'self_inking' 3802// Pre-Inked -> 'pre_inked' (from subtype) 3803// Professional-> 'professional' 3804function tierFilterKey(tierDef) { 3805if (!tierDef) return null; 3806if (tierDef.subtype) return tierDef.subtype; 3807return String(tierDef.name || '').toLowerCase().replace(/[\s\-]+/g, '_'); 3808} 3809// Filter custom option fields to those that apply to the upgrade target. 3810// Logic: 3811// 1. Override `hide` list (if any) drops matches first. 3812// 2. Auto-detect tier mentions in label: 3813// - mentions target tier name -> SHOW 3814// - mentions a different tier's name -> HIDE 3815// - mentions no tier -> fall through to step 3 3816// 3. Generic (no tier in label) options: 3817// - if override `show` is set, only labels matching survive 3818// - otherwise, show 3819function filterFieldsForTier(fields, tierDef) { 3820if (!tierDef) return fields; 3821var override = TIER_OPTION_FILTERS[tierFilterKey(tierDef)] || {}; 3822var overrideShow = (override.show || []).map(function (s) { return String(s).toLowerCase(); }); 3823var overrideHide = (override.hide || []).map(function (s) { return String(s).toLowerCase(); }); 3824return fields.filter(function (field) { 3825var label = String(field.label || '').toLowerCase(); 3826// Manual hide override wins 3827if (overrideHide.length && overrideHide.some(function (kw) { return label.indexOf(kw) !== -1; })) { 3828return false;
3829} 3830// Auto-detect tier tags in the label 3831var tiersInLabel = findAllTiersInText(label); 3832var mentionsTarget = tiersInLabel.indexOf(tierDef) !== -1; 3833var mentionsOther = tiersInLabel.some(function (t) { return t !== tierDef; }); 3834if (mentionsTarget) return true; // explicitly tagged for our tier 3835if (mentionsOther) return false; // explicitly tagged for a different tier 3836// Generic (no tier tag): apply manual show list if present 3837if (overrideShow.length > 0) { 3838return overrideShow.some(function (kw) { return label.indexOf(kw) !== -1; }); 3839} 3840return true; 3841}); 3842} 3843// Set the value of a single select-or-multiselect element. For multi- 3844// selects (name ends with "[]"), we must iterate options and set 3845// .selected explicitly -- assigning .value alone is unreliable across 3846// browsers and may leave stale selections active. 3847function setSelectValue(selectEl, targetValue) { 3848if (!selectEl) return; 3849if (selectEl.multiple) { 3850for (var i = 0; i < selectEl.options.length; i++) { 3851selectEl.options[i].selected = (selectEl.options[i].value === targetValue); 3852} 3853} else { 3854selectEl.value = targetValue; 3855} 3856} 3857// Cascade auto-fill: themes that use progressive reveal only show the 3858// next dropdown after the previous one has a value. After we swap 3859// Stamp Type, we may need to fill Size before Ink Color appears, fill 3860// Ink Color before Extra Bottle appears, etc. 3861// 3862// Strategy: find any required dropdown that is visible AND empty, 3863// pick a sane default ("No" if present, else first non-empty option), 3864// dispatch change, wait, repeat until DOM stops revealing new fields. 3865function cascadeAutoFill(form, stampSelect, callback, iter, maxIter) { 3866iter = iter || 0; 3867maxIter = maxIter || 10; 3868if (iter >= maxIter) { log('Cascade hit max iterations (' + maxIter + ')'); callback(); return; } 3869var container = form.querySelector('.product-options, .product-options-wrapper') || form; 3870var selects = container.querySelectorAll('select[name^="options"]'); 3871var changedAny = false; 3872for (var i = 0; i < selects.length; i++) { 3873var sel = selects[i]; 3874if (sel === stampSelect) continue; 3875if (sel.offsetParent === null) continue; // skip currently-hidden 3876if (sel.value && String(sel.value).trim() !== '') continue; // already filled 3877var isRequired = sel.hasAttribute('required') || 3878sel.classList.contains('required-entry') || 3879sel.classList.contains('validate-select'); 3880if (!isRequired) continue; 3881// Pick a sane default -- "No" preferred to avoid paid extras 3882var defaultVal = ''; 3883for (var j = 0; j < sel.options.length; j++) { 3884var opt = sel.options[j]; 3885if (!opt.value || opt.disabled) continue; 3886if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; } 3887if (!defaultVal) defaultVal = opt.value; 3888} 3889if (defaultVal) { 3890log('Cascade fill: ' + sel.name + ' = ' + defaultVal); 3891sel.value = defaultVal; 3892try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 3893changedAny = true; 3894} 3895} 3896if (!changedAny) { log('Cascade stable at iter ' + iter); callback(); return; } 3897setTimeout(function () { cascadeAutoFill(form, stampSelect, callback, iter + 1, maxIter); }, 300); 3898} 3899// Safety-net pass -- runs RIGHT BEFORE form submit. Iterates one final time 3900// through every visible required empty select/radio and fills with defaults. 3901// Catches any field that cascade missed because the theme revealed it after 3902// cascade exited (chained progressive reveals with long settle times). 3903// Returns the count of fields it filled (for logging). 3904function safetyNetFill(form, stampSelect) { 3905var filled = 0; 3906var container = form.querySelector('.product-options, .product-options-wrapper') || form; 3907// Selects 3908var selects = container.querySelectorAll('select[name^="options"]'); 3909for (var i = 0; i < selects.length; i++) { 3910var sel = selects[i]; 3911if (sel === stampSelect) continue; 3912if (sel.offsetParent === null) continue; 3913if (sel.value && String(sel.value).trim() !== '') continue; 3914var isRequired = sel.hasAttribute('required') || 3915sel.classList.contains('required-entry') || 3916sel.classList.contains('validate-select'); 3917if (!isRequired) continue; 3918var defaultVal = ''; 3919for (var j = 0; j < sel.options.length; j++) { 3920var opt = sel.options[j]; 3921if (!opt.value || opt.disabled) continue; 3922if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; } 3923if (!defaultVal) defaultVal = opt.value; 3924} 3925if (defaultVal) { 3926log('Safety-net fill: ' + sel.name + ' = ' + defaultVal); 3927sel.value = defaultVal; 3928try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 3929filled++; 3930} 3931} 3932// Radio groups 3933var radios = container.querySelectorAll('input[type="radio"][name^="options"]'); 3934var groups = {}; 3935for (var k = 0; k < radios.length; k++) { 3936var r = radios[k]; 3937if (r.offsetParent === null) continue; 3938if (!groups[r.name]) groups[r.name] = []; 3939groups[r.name].push(r); 3940} 3941for (var name in groups) { 3942if (!Object.prototype.hasOwnProperty.call(groups, name)) continue; 3943var grp = groups[name]; 3944var hasChecked = false;
3945for (var m = 0; m < grp.length; m++) { if (grp[m].checked) { hasChecked = true; break; } } 3946if (hasChecked) continue; 3947var isReq = grp[0].classList.contains('validate-one-required-by-name'); 3948if (!isReq) continue; 3949// Pick first non-disabled radio (or one with "no" label) 3950var pick = null; 3951for (var p = 0; p < grp.length; p++) { 3952if (grp[p].disabled) continue; 3953var lbl = findLabelForFormField(grp[p]) || ''; 3954if (/^\s*no\b/i.test(lbl)) { pick = grp[p]; break; } 3955if (!pick) pick = grp[p]; 3956} 3957if (pick) { 3958log('Safety-net fill (radio): ' + name + ' = ' + pick.value); 3959pick.checked = true; 3960try { pick.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 3961filled++; 3962} 3963} 3964return filled; 3965} 3966// v1.26 -- Carry-forward: capture user's pre-upgrade visible dropdown 3967// selections (Ink Color, Size, Ink Pad, Extra Ink, etc.) by category. 3968// These are re-applied after cascade so the user's original choice 3969// survives the tier swap, even when the theme reveals a different 3970// dropdown for the new tier (e.g., product 4378: Self-Inking exposes 3971// options[7505] "Ink Color" but Professional exposes options[7504] 3972// "Ink Colors" -- same colors, different option IDs). 3973function categorizeLabel(label) { 3974var s = String(label || '').toLowerCase(); 3975if (/ink\s*colou?rs?/.test(s)) return 'ink_color'; 3976if (/replacement\s*pad|ink\s*pad|extra.*pad/.test(s)) return 'ink_pad'; 3977if (/extra\s*ink|extra.*bottle|bottle.*ink/.test(s)) return 'extra_ink'; 3978if (/size/.test(s)) return 'size'; 3979// Fallback -- use full lowercased label as its own category 3980return 'other::' + s.trim(); 3981} 3982function captureUserSelections(form, stampSelect) { 3983var captured = {}; 3984var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 3985var selects = container.querySelectorAll('select[name^="options"]'); 3986for (var i = 0; i < selects.length; i++) { 3987var sel = selects[i]; 3988if (sel === stampSelect) continue; 3989if (isElementHidden(sel)) continue; 3990if (!sel.value || String(sel.value).trim() === '') continue; 3991var selectedOpt = sel.options[sel.selectedIndex]; 3992if (!selectedOpt) continue; 3993var selectedText = String(selectedOpt.text || '').trim(); 3994if (!selectedText) continue; 3995var label = findLabelForFormField(sel) || ''; 3996var category = categorizeLabel(label); 3997// First-wins -- if two dropdowns share a category (rare), keep the first 3998if (!captured[category]) { 3999captured[category] = { text: selectedText, sourceLabel: label, sourceName: sel.name }; 4000} 4001} 4002return captured; 4003} 4004function applyCarriedForward(form, stampSelect, captured) { 4005var applied = 0; 4006if (!captured) return 0; 4007var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 4008var selects = container.querySelectorAll('select[name^="options"]'); 4009for (var i = 0; i < selects.length; i++) { 4010var sel = selects[i]; 4011if (sel === stampSelect) continue; 4012if (isElementHidden(sel)) continue; 4013var label = findLabelForFormField(sel) || ''; 4014var category = categorizeLabel(label); 4015var wanted = captured[category]; 4016if (!wanted) continue; 4017var currentText = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : ''; 4018// If already matches (e.g., cascade happened to pick right), skip 4019if (String(currentText).trim().toLowerCase() === wanted.text.toLowerCase()) continue; 4020// Find option whose text matches (case-insensitive, trimmed) 4021var matchOpt = null; 4022for (var j = 0; j < sel.options.length; j++) { 4023var opt = sel.options[j]; 4024if (!opt.value || opt.disabled) continue; 4025// Strip price suffix like " +$5.00" for match tolerance 4026var optText = String(opt.text || '').replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim(); 4027if (optText.toLowerCase() === wanted.text.replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim().toLowerCase()) { 4028matchOpt = opt; 4029break; 4030} 4031} 4032if (matchOpt) { 4033log('Carry-forward: ' + sel.name + ' (' + category + ') <- "' + wanted.text + '" from ' + wanted.sourceName); 4034sel.value = matchOpt.value; 4035try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4036applied++; 4037} else { 4038log('Carry-forward: no match for "' + wanted.text + '" in ' + sel.name + ' (' + category + ') -- cascade default stays'); 4039} 4040} 4041return applied; 4042} 4043function syncValuesToForm(form, values, fields) { 4044fields.forEach(function (field) { 4045var v = values[field.name]; 4046if (v == null) return; 4047if (field.type === 'select') { 4048var sel = form.querySelector('select[name="' + field.name + '"]'); 4049if (sel) { 4050sel.value = v; 4051try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4052} 4053} else if (field.type === 'radio') { 4054var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]'); 4055for (var i = 0; i < rs.length; i++) { 4056rs[i].checked = (rs[i].value === v); 4057if (rs[i].checked) { 4058try { rs[i].dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4059} 4060} 4061} else if (field.type === 'text' || field.type === 'textarea') { 4062var ti = form.querySelector('[name="' + field.name + '"]'); 4063if (ti) { 4064ti.value = v; 4065try { 4066ti.dispatchEvent(new Event('input', { bubbles: true })); 4067ti.dispatchEvent(new Event('change', { bubbles: true })); 4068} catch (e) {} 4069} 4070} 4071}); 4072}
4073function init() { 4074var form = document.getElementById('product_addtocart_form'); 4075if (!form) { log('Not a PDP (no ATC form) -- skipping init'); return; } 4076var stampSelect = findStampTypeSelect(); 4077if (!stampSelect) { log('No Stamp Type dropdown -- skipping init'); return; } 4078var upsellPath = detectUpsellPath(); 4079var productInput = form.querySelector('input[name="product"]'); 4080var productId = productInput ? productInput.value : ''; 4081var skuMeta = document.querySelector('meta[itemprop="sku"], [itemprop="sku"]'); 4082var productSku = skuMeta ? (skuMeta.getAttribute('content') || skuMeta.textContent || '').trim() : ''; 4083var category = detectCategory(); 4084var variant = getAssignedVariant(); 4085var variantCfg = getVariantConfig(variant); 4086log('Init', { upsellPath: upsellPath, productId: productId, category: category, variant: variant }); 4087function buildContext(cartItemId) { 4088// No-badger: if user already accepted an upgrade this session, skip. 4089if (hasUpgradedThisSession()) { log('buildContext: user already upgraded this session -- skipping'); return null; } 4090// Decline cap: if user has declined too many times this session, respect that and skip. 4091if (hasDeclinedTooMany()) { log('buildContext: decline cap reached (' + getDeclineCount() + '/' + MAX_DECLINES_PER_SESSION + ') -- skipping'); return null; } 4092// Category targeting: skip if this product's category isn't allowed. 4093if (!isCategoryAllowed(category)) { log('buildContext: category not in target/excluded by rule'); return null; } 4094var current = getCurrentTier(stampSelect); 4095if (!current) { log('buildContext: no current tier detected'); return null; } 4096var availableTiers = getAvailableTiers(stampSelect); 4097log('buildContext: available tiers on this product:', Object.keys(availableTiers)); 4098var next = getNextTier(current.tier, upsellPath, availableTiers); 4099if (!next) { log('buildContext: no upgrade tier (top tier or no upgrade options on product)'); return null; } 4100var upgradeOpt = findOptionInSelect(stampSelect, next); 4101if (!upgradeOpt) { log('buildContext: upgrade option vanished from dropdown'); return null; } 4102log('buildContext: ' + current.tier.name + ' -> ' + next.name); 4103return { 4104current: current, next: next, upgradeOpt: upgradeOpt, 4105priceDelta: calcPriceDelta(current.optionEl, upgradeOpt), 4106cartItemId: cartItemId, 4107productId: productId, productSku: productSku, category: category, upsellPath: upsellPath 4108}; 4109} 4110function mergeParams(a, b) { 4111var out = {}; for (var k in a) out[k] = a[k]; for (var k2 in b) out[k2] = b[k2]; return out; 4112} 4113// ---------------------------------------------------------- 4114// ATC interception -- v1.2 PRE-ADD flow: 4115// Show modal IMMEDIATELY on submit (no AJAX wait). 4116// On accept: change the Stamp Type dropdown to the upgrade value, 4117// then call the original submit. One cart write, right SKU. 4118// On decline: just call the original submit unchanged. 4119// 4120// v1.32 -- Zephyr's primary path is window.handleAjaxAddToCart (see 4121// patchAjaxHandler below). The Varien patch + form 'submit' listener 4122// are kept as fallbacks for any legacy/native path. 4123// 1. Patch window.handleAjaxAddToCart (Zephyr AJAX ATC) -- PRIMARY 4124// 2. Patch window.productAddToCartForm.submit (Varien) -- fallback 4125// 3. Fallback: form 'submit' event listener (non-Varien themes) 4126// ---------------------------------------------------------- 4127function buildBaseParams(ctx) { 4128return { 4129from_tier: ctx.current.tier.name, 4130to_tier: ctx.next.name, 4131upsell_path: ctx.upsellPath, 4132product_id: ctx.productId, 4133product_sku: ctx.productSku, 4134category: ctx.category, 4135price_delta: ctx.priceDelta, 4136variant: variant 4137}; 4138} 4139function showUpsellAndSubmit(ctx, doSubmit) { 4140// v1.32b -- never stack two modals if the ATC button is double-clicked. 4141if (document.querySelector('.ss-upsell-overlay')) { log('Modal already open -- ignoring duplicate trigger'); return; } 4142// v1.32b -- the underlying submit (native AJAX add on Zephyr) must run at 4143// most ONCE per flow. The old code relied on a full-page navigation to 4144// make a stray second call harmless; the AJAX add has no navigation, so 4145// we guard it explicitly. onDecline / onCancel / onSuccessComplete all 4146// route through this wrapper. 4147var _rawSubmit = doSubmit, _submitted = false;
4148doSubmit = function () { if (_submitted) return; _submitted = true; _rawSubmit(); }; 4149var baseParams = buildBaseParams(ctx); 4150var originalStampValue = ctx.current.optionEl.value; 4151track('upsell_modal_shown', baseParams); 4152// v1.28 -- Fire discount-side telemetry when variant B modal renders 4153// WITH a coupon code available for the target transition. Signals 4154// "user was exposed to the discount incentive" and lets B-only 4155// reports build without filtering upsell_modal_shown by variant. 4156if (variant === 'B') { 4157var shownCode = resolveCouponCode(variantCfg, ctx.next); 4158if (shownCode) { 4159track('upsell_discount_shown', mergeParams(baseParams, { coupon_code: shownCode })); 4160} 4161} 4162function chainToSuccessAndSubmit(overlay, userPicks, userFields, carriedForward) { 4163showSuccessState(overlay, { 4164current: ctx.current, 4165next: ctx.next, 4166onSuccessComplete: function () { 4167// Final safety net: catch any required field the cascade missed 4168// (e.g., chained progressive reveals where the theme exposes 4169// fields after our cascade exited). Without this, Magento 4170// backend validation can reject the submit and leave the user 4171// stuck on the PDP after the success state closes. 4172var filled = safetyNetFill(form, stampSelect); 4173if (filled > 0) log('Safety net filled ' + filled + ' field(s) before submit'); 4174// v1.27 -- Re-apply carry-forward AFTER safety net. When the 4175// user changes Size in the modal, the theme cascade can reveal 4176// a NEW Ink Color dropdown that our initial carry-forward 4177// (which ran pre-modal) never touched. Safety net just filled 4178// it with the default (Black). Now we override that default 4179// with the user's original pre-upgrade pick (Red) if we find 4180// a match by category + text. 4181if (carriedForward) { 4182var reCarried = applyCarriedForward(form, stampSelect, carriedForward); 4183if (reCarried > 0) log('Late carry-forward: ' + reCarried + ' field(s) restored to original pick'); 4184} 4185// CRITICAL: Re-apply user's explicit picks LAST. The theme's 4186// progressive reveal cascade can reset dependent options (e.g., 4187// Ink Color resets when Stamp Type/Size change). Without this 4188// final re-sync, the safety net's defaults overwrite the user's 4189// intended color/option picks, leading to "I picked Red but cart 4190// shows Black" bugs. Direct value writes only -- no change event 4191// dispatch -- to avoid re-triggering the theme's cascade. 4192if (userPicks && userFields) { 4193var reapplied = 0; 4194userFields.forEach(function (field) { 4195var v = userPicks[field.name]; 4196if (v == null || v === '') return; 4197if (field.type === 'select') { 4198var sel = form.querySelector('select[name="' + field.name + '"]'); 4199if (sel && String(sel.value) !== String(v)) { 4200log('Re-applying user pick: ' + field.name + ' ' + sel.value + ' -> ' + v); 4201sel.value = v; 4202reapplied++; 4203} 4204} else if (field.type === 'radio') { 4205var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]'); 4206for (var i = 0; i < rs.length; i++) { 4207var shouldCheck = (rs[i].value === v); 4208if (rs[i].checked !== shouldCheck) { 4209rs[i].checked = shouldCheck; 4210if (shouldCheck) reapplied++; 4211} 4212} 4213} else if (field.type === 'text' || field.type === 'textarea') { 4214var ti = form.querySelector('[name="' + field.name + '"]'); 4215if (ti && ti.value !== v) { 4216log('Re-applying user pick: ' + field.name + ' = ' + v); 4217ti.value = v; 4218reapplied++; 4219} 4220} 4221}); 4222if (reapplied > 0) log('Re-applied ' + reapplied + ' user pick(s) before submit'); 4223} 4224log('Success state complete -- submitting'); 4225doSubmit(); 4226} 4227}); 4228} 4229showModal({ 4230current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4231variantCfg: variantCfg, 4232onAccept: function (overlay) { 4233track('upsell_accepted', baseParams); 4234// v1.26 -- Capture user's pre-upgrade visible selections BEFORE 4235// Stamp Type swap wipes visibility (Ink Color, Size, etc.). 4236// These get carried forward to whichever dropdowns the new tier 4237// reveals, matched by category + option text (e.g., "Red"). 4238var carriedForward = captureUserSelections(form, stampSelect); 4239log('Captured for carry-forward:', carriedForward); 4240// Multiselect-safe Stamp Type swap (handles options[1508][] style) 4241setSelectValue(stampSelect, ctx.upgradeOpt.value); 4242try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4243log('Stamp Type swapped to:', ctx.upgradeOpt.text); 4244// Cascade auto-fill (themes with progressive reveal only show the 4245// next dropdown after the previous has a value). Then extract. 4246setTimeout(function () { 4247cascadeAutoFill(form, stampSelect, function () { 4248// v1.26 -- Apply carry-forward BEFORE extraction so the modal 4249// shows user's original picks as pre-selected values. User 4250// can still override in the modal if they want to change. 4251var carriedCount = applyCarriedForward(form, stampSelect, carriedForward); 4252if (carriedCount > 0) log('Carried forward ' + carriedCount + ' user selection(s) to new tier'); 4253var allFields = extractCustomOptions(form, stampSelect); 4254var filtered = filterFieldsForTier(allFields, ctx.next); 4255// Dedupe by label -- products sometimes have multiple custom 4256// options with identical labels (one per tier variant). Without 4257// tier hints in the label we can't distinguish them, so keep 4258// the first and drop the rest. 4259var seenLabels = {}; 4260var fields = filtered.filter(function (f) { 4261var key = String(f.label || '').toLowerCase().trim(); 4262if (!key) return true; 4263if (seenLabels[key]) return false;
4264seenLabels[key] = true; 4265return true; 4266}); 4267log('Extracted ' + allFields.length + ' option(s), ' + 4268filtered.length + ' apply to ' + ctx.next.name + ', ' + 4269fields.length + ' after dedupe'); 4270// Read qty from the form (used in revenue_delta calc later) 4271var qtyInput = form.querySelector('input[name="qty"]'); 4272var qty = qtyInput ? qtyInput.value : 1; 4273if (fields.length === 0) { 4274// No other options to configure -- straight to success 4275markUpgradedThisSession(); 4276track('upsell_confirmed', mergeParams(baseParams, { config_skipped: true })); 4277recordUpgradeForPurchaseTracking(baseParams, qty); 4278var skipCode = resolveCouponCode(variantCfg, ctx.next); 4279if (skipCode) { 4280log('Applying coupon for transition -> ' + ctx.next.name + ': ' + skipCode); 4281applyVariantCDiscount(skipCode, baseParams); 4282} 4283chainToSuccessAndSubmit(overlay, null, null, carriedForward); 4284return; 4285} 4286showConfigStep(overlay, ctx, fields, 4287function onConfirm(values) { 4288track('upsell_confirmed', mergeParams(baseParams, { config_field_count: fields.length })); 4289markUpgradedThisSession(); 4290syncValuesToForm(form, values, fields); 4291recordUpgradeForPurchaseTracking(baseParams, qty); 4292var confirmCode = resolveCouponCode(variantCfg, ctx.next); 4293if (confirmCode) { 4294log('Applying coupon for transition -> ' + ctx.next.name + ': ' + confirmCode); 4295applyVariantCDiscount(confirmCode, baseParams); 4296} 4297// Pass user picks + fields so they get re-applied AFTER 4298// safetyNetFill, defeating theme cascade resets. Also pass 4299// carriedForward for v1.27's late-cascade rescue: if user 4300// changed Size in modal and theme revealed a NEW Ink Color 4301// dropdown, restore original pre-upgrade pick. 4302chainToSuccessAndSubmit(overlay, values, fields, carriedForward); 4303}, 4304function onCancel(method) { 4305incrementDeclineCount();
4306track('upsell_canceled_at_config', mergeParams(baseParams, { cancel_method: method, decline_count: getDeclineCount() })); 4307// Revert Stamp Type to original -- user bailed at config 4308setSelectValue(stampSelect, originalStampValue); 4309try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4310if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 4311log('Canceled at config -- count:', getDeclineCount(), '-- submitting original selection'); 4312doSubmit(); 4313} 4314); 4315}); // end cascadeAutoFill callback 4316}, 100); 4317}, 4318onDecline: function (method) { 4319incrementDeclineCount(); 4320track('upsell_declined', mergeParams(baseParams, { dismiss_method: method, decline_count: getDeclineCount() })); 4321log('Declined (' + method + ') -- count:', getDeclineCount(), '-- submitting original selection'); 4322doSubmit(); 4323} 4324}); 4325} 4326// v1.32 -- PRIMARY Zephyr interception. The ATC button on Zephyr is 4327// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // so it never reaches Varien's submit or a native form submit event. 4328// We wrap the global handler: validate (Varien validator still exists), 4329// build the upsell context, and if an upgrade is available show the modal 4330// first. The original AJAX add runs only after accept/decline, by which 4331// point the Stamp Type has been swapped (on accept). handleAjaxAddToCart 4332// re-serializes the form when it runs, so the upgraded selection is added. 4333function patchAjaxHandler() { 4334var fn = window.handleAjaxAddToCart; 4335if (typeof fn !== 'function') return false; 4336if (fn._ssTierPatched) return true; 4337var orig = fn; 4338window.handleAjaxAddToCart = function (button) { 4339var args = arguments; 4340function proceed() { return orig.apply(window, args); } 4341// Respect Magento/Varien validation first. handleAjaxAddToCart runs 4342// the same validator internally; if the form isn't valid, hand off to 4343// the original so it surfaces the field errors (and we don't pop the 4344// modal on an incomplete form). 4345try { 4346var paf = window.productAddToCartForm; 4347if (paf && paf.validator && typeof paf.validator.validate === 'function' && !paf.validator.validate()) { 4348log('Validator failed -- passing through to native AJAX add'); 4349return proceed(); 4350} 4351} catch (e) {} 4352var ctx = buildContext(null); 4353if (!ctx) { log('No upsell context -- native AJAX add'); return proceed(); } 4354log('Upsell available -- showing modal before AJAX add (Zephyr path)'); 4355showUpsellAndSubmit(ctx, function () { 4356// Run the native AJAX add, then clean up. Unlike the old Varien 4357// path this does NOT navigate, so WE must remove the success 4358// overlay (otherwise its full-screen fixed layer covers the page 4359// forever) and apply any stashed variant-B coupon now that the 4360// item is in the cart (the drawer never loads /checkout/cart). 4361var r; 4362try { r = proceed(); } catch (e) { r = null; } 4363var _done = false; 4364var done = function () { 4365if (_done) return; _done = true; // idempotent -- promise OR timeout, whichever first 4366var o = document.querySelector('.ss-upsell-overlay'); 4367if (o && o.parentNode) o.parentNode.removeChild(o); 4368try { applyPendingCouponNow(); } catch (e) {} 4369}; 4370var thenable = r && typeof r.then === 'function'; 4371if (thenable) { r.then(done, done); } 4372// Always arm a max-timeout too: if the add promise is non-thenable 4373// (short delay) OR never settles (6s hang guard), we still remove the 4374// overlay so it can't cover the page or suppress future modals. 4375setTimeout(done, thenable ? 6000 : 1200); 4376}); 4377return false; // block the immediate add; proceed() runs after the modal resolves 4378}; 4379window.handleAjaxAddToCart._ssTierPatched = true; 4380log('Patched window.handleAjaxAddToCart (Zephyr AJAX ATC)'); 4381return true; 4382} 4383(function pollForAjaxHandler(attempts) { 4384attempts = attempts || 0; 4385if (patchAjaxHandler()) return; 4386if (attempts >= 30) { log('Gave up polling for handleAjaxAddToCart after 3s'); return; } 4387setTimeout(function () { pollForAjaxHandler(attempts + 1); }, 100); 4388})(); 4389function patchVarienForm() { 4390var paf = window.productAddToCartForm; 4391if (!paf || typeof paf.submit !== 'function' || paf._ssTierPatched) return false; 4392var origSubmit = paf.submit.bind(paf); 4393paf.submit = function (button, url) { 4394if (this.validator && typeof this.validator.validate === 'function' && !this.validator.validate()) { 4395log('Magento validator failed -- letting native submit handle the error UI'); 4396return origSubmit(button, url); 4397} 4398var ctx = buildContext(null); 4399if (!ctx) { 4400log('No upsell context -- proceeding with native ATC'); 4401return origSubmit(button, url); 4402} 4403log('Upsell available -- showing modal pre-add'); 4404showUpsellAndSubmit(ctx, function () { origSubmit(button, url); }); 4405}; 4406paf._ssTierPatched = true; 4407log('Patched window.productAddToCartForm.submit'); 4408return true; 4409} 4410(function pollForVarien(attempts) { 4411attempts = attempts || 0; 4412if (patchVarienForm()) return; 4413if (attempts >= 30) { log('Gave up polling for productAddToCartForm after 3s'); return; } 4414setTimeout(function () { pollForVarien(attempts + 1); }, 100); 4415})(); 4416// Fallback for non-Varien / Enter-key submission 4417form.addEventListener('submit', function (e) { 4418if (form._ssBypass) { form._ssBypass = false; return; } // we already showed modal 4419if (window.productAddToCartForm && window.productAddToCartForm._ssTierPatched) return; 4420// If the Zephyr AJAX handler is patched, it owns the flow -- don't
4421// double-fire the modal from a native submit that may also occur. 4422if (window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched) return; 4423var ctx = buildContext(null); 4424if (!ctx) return; 4425e.preventDefault(); 4426log('Submit event intercepted (fallback path) -- showing modal pre-add'); 4427showUpsellAndSubmit(ctx, function () { 4428form._ssBypass = true; 4429form.submit(); 4430}); 4431}); 4432window.SSUpsell = { 4433showTestModal: function () { 4434var ctx = buildContext('TEST-CART-ITEM'); 4435if (!ctx) { alert('No upsell available. Current: ' + (stampSelect.options[stampSelect.selectedIndex] || {}).text); return; } 4436var baseParams = { from_tier: ctx.current.tier.name, to_tier: ctx.next.name, upsell_path: ctx.upsellPath, product_id: ctx.productId, price_delta: ctx.priceDelta }; 4437track('upsell_modal_shown', baseParams); 4438showModal({ 4439current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4440variantCfg: variantCfg, 4441onAccept: function (overlay) { 4442track('upsell_accepted', baseParams); 4443if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); 4444alert('Accept (test mode -- no cart write)'); 4445},
4446onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); } 4447}); 4448}, 4449inspect: function () { 4450var ctx = buildContext(null); 4451var available = getAvailableTiers(stampSelect); 4452var currentTierLive = getCurrentTier(stampSelect); 4453// Dry-run the config-step extraction + filter so you can see what 4454// the user WOULD see if they accepted right now. 4455var optsPreview = null; 4456if (ctx) { 4457var all = extractCustomOptions(form, stampSelect); 4458var kept = filterFieldsForTier(all, ctx.next); 4459var seenL = {}; 4460var deduped = kept.filter(function (f) { 4461var k = String(f.label || '').toLowerCase().trim(); 4462if (!k) return true; 4463if (seenL[k]) return false; 4464seenL[k] = true; 4465return true; 4466}); 4467optsPreview = { 4468extracted: all.map(function (f) { return f.label; }), 4469keptAfterFilter: kept.map(function (f) { return f.label; }), 4470shownToUser: deduped.map(function (f) { return f.label; }), 4471filterRule: TIER_OPTION_FILTERS[tierFilterKey(ctx.next)] || '(none -- show all)' 4472}; 4473} 4474return { 4475upsellPath: upsellPath, 4476productId: productId, 4477productSku: productSku, 4478category: category, 4479currentTier: currentTierLive ? currentTierLive.tier.name : null, 4480nextTier: ctx ? ctx.next.name : null, 4481priceDelta: ctx ? ctx.priceDelta : null, 4482availableOnProduct: Object.keys(available), 4483ajaxHandlerPatched: !!(window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched), 4484ajaxHandlerPresent: typeof window.handleAjaxAddToCart === 'function', 4485varienFormPatched: !!(window.productAddToCartForm && window.productAddToCartForm._ssTierPatched), 4486varienFormPresent: !!window.productAddToCartForm, 4487alreadyUpgradedThisSession: hasUpgradedThisSession(), 4488declineCount: getDeclineCount(), 4489maxDeclinesPerSession: MAX_DECLINES_PER_SESSION, 4490declineCapReached: hasDeclinedTooMany(), 4491variant: variant, 4492variantConfig: variantCfg, 4493devModeAlwaysShow: DEV_MODE_ALWAYS_SHOW, 4494categoryAllowed: isCategoryAllowed(category), 4495targetCategories: TARGET_CATEGORIES, 4496excludedCategories: EXCLUDED_CATEGORIES, 4497configStepPreview: optsPreview 4498}; 4499}, 4500// QA helper -- clear the session flags (upgrade lock + decline count) so the modal fires again 4501resetSession: function () { 4502try { 4503sessionStorage.removeItem(SESSION_ACCEPTED_KEY); 4504sessionStorage.removeItem(SESSION_DECLINE_KEY); 4505} catch (e) {} 4506console.log('[TierUpsell] Session reset -- modal will fire on next ATC'); 4507}, 4508// QA helpers for variant testing 4509setVariant: function (v) { 4510v = String(v || '').toUpperCase(); 4511if (VARIANTS.indexOf(v) === -1) { 4512console.warn('Invalid variant. Use one of:', VARIANTS); 4513return; 4514} 4515try { localStorage.setItem(VARIANT_STORAGE_KEY, v); } catch (e) {} 4516console.log('[TierUpsell] Variant set to:', v, '-- refresh the page to see changes'); 4517}, 4518clearVariant: function () { 4519try { localStorage.removeItem(VARIANT_STORAGE_KEY); } catch (e) {} 4520console.log('[TierUpsell] Variant cleared -- next page load will assign a new one'); 4521}, 4522simulateATC: function () { 4523var ctx = buildContext('TEST-CART-ITEM'); 4524if (!ctx) { alert('No upsell available right now.'); return; } 4525var baseParams = { 4526from_tier: ctx.current.tier.name, to_tier: ctx.next.name, 4527upsell_path: ctx.upsellPath, product_id: ctx.productId, 4528product_sku: ctx.productSku, category: ctx.category, price_delta: ctx.priceDelta 4529}; 4530track('upsell_modal_shown', baseParams); 4531showModal({ 4532current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4533variantCfg: variantCfg, 4534onAccept: function (overlay) { 4535track('upsell_accepted', baseParams); 4536if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); 4537console.log('[TierUpsell] Would update cart now (simulate mode)'); 4538},
4539onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); console.log('[TierUpsell] Declined (simulate mode):', m); } 4540}); 4541} 4542}; 4543} 4544if (document.readyState === 'loading') { 4545document.addEventListener('DOMContentLoaded', init); 4546} else { 4547init(); 4548} 4549})(); </script>
4549</div><div class="widget widget-static-block">
4549<script type="application/json" id="cart-kit-personas"> {"version":"2026-09-15","personas":{"brand_champions":{"persona_name":"business owners","match_keywords":["branding","brand identity","logo","custom logo","packaging","small business","business owner","ecommerce","online store","handmade business","promotional","qr code","thank you","bulk orders"],"header_title":"Brand Essentials: finish your packaging strong.","stamp_label":"Business add-ons (popular picks)","ink_label":"Choose an ink color for consistent branding","tooltip_text":"These add-ons help your brand look polished across packaging, mailers, and customer communications.","tile_subtext":"Popular with small businesses","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"2577","product_url":"https://www.simplystamps.com/qr-code-stamp-1-5-8-x-1-5-8","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["qr code stamp","qr stamp","scan me stamp","website link stamp","review qr stamp","google review qr","instagram qr","menu qr code","payment qr","contactless qr","marketing qr code"]},{"product_id":"4419","product_url":"https://www.simplystamps.com/medium-custom-logo-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["custom logo stamp","business logo stamp","company logo stamp","self inking logo stamp","branding stamp","packaging logo stamp","small business stamp","shop logo stamp","return address + logo","rubber logo stamp"]},{"product_id":"15261","product_url":"https://www.simplystamps.com/loyalty-stamp-logo-round","sort_order":0,"role":"anchor","friction":"medium","experimental":false,"match_keywords":["loyalty stamp","rewards stamp","punch card stamp","loyalty card stamp","round logo stamp","cafe loyalty stamp","retail loyalty stamp","frequent buyer stamp","stamp for loyalty program"]},{"product_id":"2578","product_url":"https://www.simplystamps.com/qr-code-stamp-3-x-1-5","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["qr code stamp","large qr stamp","scan code stamp","website qr stamp","review qr stamp","menu qr stamp","marketing qr stamp","contactless qr stamp"]},{"product_id":"5227","product_url":"https://www.simplystamps.com/custom-logo-stamp-style-12-w-catalog-ink","sort_order":0,"role":"backup","friction":"medium","experimental":false,"match_keywords":["custom logo stamp","self inking logo stamp","logo stamp with ink","replacement ink included","business branding stamp","packaging logo stamp","company logo stamp","rubber logo stamp"]},{"product_id":"8939","product_url":"https://www.simplystamps.com/medium-round-logo-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["round logo stamp","circular logo stamp","business round stamp","self inking round stamp","company seal-style stamp","packaging round stamp","branding round stamp"]},{"product_id":"10796","product_url":"https://www.simplystamps.com/mini-square-logo-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["small logo stamp","mini logo stamp","square logo stamp","tiny logo stamp","branding tag stamp","packaging logo stamp","craft fair stamp","etsy logo stamp","label logo stamp"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","identity theft roller","hide personal info","cover up info roller","id protection roller"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","desk envelope opener","office mail tool","open envelopes quickly"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope glue","envelope adhesive","seal envelopes","mailing supplies","envelope sealing bottle","stationery adhesive"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle","replacement stamp ink","2 oz stamp ink","re-ink stamp pad"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","gold foil seals","silver foil seals","raised seal foil","foil sticker seals","certificate seal stickers"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]},{"product_id":"7894","product_url":"https://www.simplystamps.com/black-medium-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","medium ink pad","stamp pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad"]},{"product_id":"7899","product_url":"https://www.simplystamps.com/black-large-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","large ink pad","stamp pad","hand stamp","wood handle stamp","extra large stamp","traditional stamp"]}]},"home_sweet_stampers":{"persona_name":"mail senders","match_keywords":["home organization","return address","home address","address stamp","household","family mail","personal address","home office","family paperwork","mailing at home","personal documents","holiday mail","wedding mail"],"header_title":"Mail Essentials: everything you need for clean, easy stamping.","stamp_label":"Helpful add-ons for everyday mail","ink_label":"Choose an ink color","tooltip_text":"Perfect for cards, envelopes, and seasonal mail - these extras make stamping easier and mess-free.","tile_subtext":"Great for everyday mail","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"23105","product_url":"https://www.simplystamps.com/delicate-flower-envelope-accent-craft-stamp","
4549sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["holiday stamp","merry christmas stamp","christmas stamp","holiday card stamp","envelope holiday stamp","seasonal greeting stamp","xmas stamp","christmas mail stamp"]},{"product_id":"6126","product_url":"https://www.simplystamps.com/happy-birthday-smiley-craft-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["happy birthday stamp","birthday card stamp","birthday mail stamp","party invitation stamp","envelope birthday stamp","celebration stamp","kids birthday stamp"]},{"product_id":"8557","product_url":"https://www.simplystamps.com/duncan-script-address-stamp","sort_order":0,"role":"anchor","friction":"medium","experimental":false,"match_keywords":["return address stamp","address stamp","script address stamp","cursive address stamp","custom address stamp","home address stamp","mail return address stamp"]},{"product_id":"24186","product_url":"https://www.simplystamps.com/gold-wax-seal-sticker-rosemary-design","sort_order":0,"role":"accessory","friction":"none","experimental":false,"match_keywords":["wax seal stickers","gold wax seal stickers","rosemary wax seal","envelope seal stickers","stationery seal stickers","invitation seal stickers","wax seal sticker set"]},{"product_id":"24188","product_url":"https://www.simplystamps.com/gold-wax-seal-sticker-eucalyptus-design","sort_order":0,"role":"accessory","friction":"none","experimental":false,"match_keywords":["wax seal stickers","gold wax seal stickers","eucalyptus wax seal","greenery wax seal","envelope seal stickers","invitation seal stickers","wax seal sticker set"]},{"product_id":"24189","product_url":"https://www.simplystamps.com/gold-wax-seal-sticker-heart-design","sort_order":0,"role":"accessory","friction":"none","experimental":false,"match_keywords":["wax seal stickers","gold wax seal stickers","heart wax seal","envelope seal stickers","love wax seal sticker","invitation seal stickers","wax seal sticker set"]},{"product_id":"5529","product_url":"https://www.simplystamps.com/live-well-rubber-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["inspirational stamp","motivational stamp","live well stamp","positivity stamp","journaling stamp","planner stamp","sentiment stamp","encouragement stamp"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","hide personal info","identity theft roller"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","desk envelope opener","open envelopes quickly"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope glue","envelope adhesive","seal envelopes","mailing supplies","stationery adhesive"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":17,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle","replacement stamp ink","2 oz stamp ink"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":18,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","gold foil seals","silver foil seals","raised seal foil","certificate seal stickers"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]}]},"office_optimizers":{"persona_name":"office admin","match_keywords":["office admin","professional","past due","void","scanned","custom date","signature","office workflow","paperwork","document control","internal use","filing","paid","processing","received","approved","office documents"],"header_title":"Office Essentials: stay stocked and avoid reorders.","stamp_label":"Office add-ons (frequently reordered)","ink_label":"Ink selection for daily office use","tooltip_text":"These supplies help keep workflows moving without interruptions or last-minute supply runs.","tile_subtext":"Frequently reordered","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"none","experimental":false,"match_keywords":["envelope sealer","envelope adhesive","seal envelopes","office mailing supplies","envelope glue bottle","mailroom supplies"]},{"product_id":"15209","product_url":"https://www.simplystamps.com/4810-date-only-dater-stamp","
4549sort_order":0,"role":"anchor","friction":"low","experimental":false,"match_keywords":["date stamp","dater stamp","date only stamp","office date stamp","received date stamp","processed date stamp","document date stamp"]},{"product_id":"8926","product_url":"https://www.simplystamps.com/small-signature-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["signature stamp","small signature stamp","name signature stamp","authorized signature stamp","stamp signature","office signature stamp","sign forms stamp"]},{"product_id":"6413","product_url":"https://www.simplystamps.com/rejected-stamp","sort_order":0,"role":"backup","friction":"none","experimental":false,"match_keywords":["rejected stamp","rejected rubber stamp","rejected stock stamp","office rejected stamp","document rejected stamp","return/reject stamp","rejection stamp"]},{"product_id":"4810","product_url":"https://www.simplystamps.com/3-line-deposit-only-stamp","sort_order":0,"role":"core","friction":"high","experimental":false,"match_keywords":["deposit only stamp","for deposit only stamp","bank deposit stamp","endorsement stamp","check deposit stamp","business deposit stamp","deposit endorsement stamp"]},{"product_id":"2350","product_url":"https://www.simplystamps.com/received-outline-stock-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["received stamp","received stock stamp","office received stamp","document received stamp","mail received stamp","received rubber stamp"]},{"product_id":"2334","product_url":"https://www.simplystamps.com/paid-outlined-stock-stamp","sort_order":0,"role":"core","friction":"low","experimental":false,"match_keywords":["paid stamp","paid stock stamp","invoice paid stamp","office paid stamp","accounts payable stamp","payment received stamp","paid rubber stamp"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","identity theft roller","hide personal info"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"none","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","desk envelope opener","mailroom tool"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle","replacement stamp ink"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil","certificate seal stickers"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]},{"product_id":"7894","product_url":"https://www.simplystamps.com/black-medium-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","medium ink pad","stamp pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad"]},{"product_id":"7899","product_url":"https://www.simplystamps.com/black-large-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","large ink pad","stamp pad","hand stamp","wood handle stamp","extra large stamp","traditional stamp"]}]},"notary_ninjas":{"persona_name":"notary","match_keywords":["notary","state of","notarization","signing agent","loan signing","jurat","acknowledgment","witness","official document","seal","embosser","certified"],"header_title":"Notary Essentials: add backup ink & supplies before checkout.","stamp_label":"Notary add-ons (quick picks)","ink_label":"Ink choice (recommended for notary work)","tooltip_text":"These items help ensure clean, compliant notarizations and prevent downtime if ink runs out.","tile_subtext":"Recommended for notary work","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"11712","product_url":"https://www.simplystamps.com/the-complete-notary-records-book","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["notary records book","
4549notary journal","notary logbook","notary ledger","notarization record book","notary signing log","notary supplies book"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","gold foil seals","raised seal foil","notary seal foil","certificate seal stickers"]},{"product_id":"7986","product_url":"https://www.simplystamps.com/seal-impression-inker","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embosser inker","seal impression inker","raised seal inker","embosser ink pad","make embossing visible","notary embosser accessory","seal impression enhancer"]},{"product_id":"13384","product_url":"https://www.simplystamps.com/legal-terms-dictionary","sort_order":0,"role":"anchor","friction":"none","experimental":false,"match_keywords":["legal terms dictionary","law dictionary","notary reference book","legal reference","terms dictionary"]},{"product_id":"8927","product_url":"https://www.simplystamps.com/medium-signature-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["signature stamp","medium signature stamp","authorized signature stamp","stamp signature","sign documents stamp","office signature stamp"]},{"product_id":"13387","product_url":"https://www.simplystamps.com/embosser-vinyl-black-pouch","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embosser pouch","embosser case","notary embosser case","embosser storage","carry embosser","protect embosser","vinyl embosser pouch"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","identity theft roller"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","desk envelope opener"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope adhesive","seal envelopes","mailing supplies","envelope glue bottle"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle","replacement stamp ink"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil","certificate seal stickers"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]},{"product_id":"7894","product_url":"https://www.simplystamps.com/black-medium-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","medium ink pad","stamp pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad"]},{"product_id":"7899","product_url":"https://www.simplystamps.com/black-large-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","large ink pad","stamp pad","hand stamp","wood handle stamp","extra large stamp","traditional stamp"]}]},"classroom_heroes":{"persona_name":"teachers","match_keywords":["teacher","classroom","education","grading","student work","homework","lesson planning","classroom management","school"],"header_title":"Classroom Essentials: be ready for every lesson.","stamp_label":"Teacher add-ons (classroom favorites)","ink_label":"Choose an ink color students will love","tooltip_text":"Extra ink and supplies help keep grading, rewards, and feedback flowing all year.","tile_subtext":"Classroom favorites","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"2632","product_url":"https://www.simplystamps.com/a-good-work-teacher-stamp","
4549sort_order":0,"role":"anchor","friction":"none","experimental":false,"match_keywords":["good work stamp","teacher good work stamp","student praise stamp","grading feedback stamp","classroom reward stamp","teacher feedback stamp"]},{"product_id":"18180","product_url":"https://www.simplystamps.com/instagram-like-grading-teacher-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["grading stamp","teacher grading stamp","student feedback stamp","like stamp teacher","classroom reward stamp","fun grading stamp"]},{"product_id":"24369","product_url":"https://www.simplystamps.com/the-cubie-teacher-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["teacher stamp","personalized teacher stamp","classroom stamp","teacher name stamp","grading stamp","teacher reward stamp"]},{"product_id":"10964","product_url":"https://www.simplystamps.com/missing-check-box-teacher-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["missing stamp","missing work stamp","check box stamp","teacher grading stamp","homework missing stamp","classroom feedback stamp"]},{"product_id":"23518","product_url":"https://www.simplystamps.com/modern-sun-custom-book-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["custom book stamp","teacher book stamp","classroom library stamp","book ownership stamp","from the library stamp","personalized book stamp"]},{"product_id":"9810","product_url":"https://www.simplystamps.com/teacher-library-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["teacher library stamp","classroom library stamp","from the library teacher","book stamp teacher","book ownership stamp","student book return stamp"]},{"product_id":"19215","product_url":"https://www.simplystamps.com/stacked-teacher-stamps-set-of-6","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["teacher stamp set","grading stamps set","reward stamp set","classroom feedback stamps","teacher supplies stamps","student praise stamps"]},{"product_id":"10965","product_url":"https://www.simplystamps.com/no-name-no-fame-owl-teacher-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["personalized teacher stamp","owl teacher stamp","teacher name stamp","classroom stamp","cute grading stamp","teacher gift stamp"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","stamp pad set","teacher stamp ink pads","ink pads for rubber stamps"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]}]},"craft_crowd":{"persona_name":"crafters","match_keywords":["crafting","craft stamp","Create","hand stamp","european","handmade","arts and crafts","creative projects","diy","small batch","maker","personalization","packaging"],"header_title":"Creative Essentials: finish your projects with ease.","stamp_label":"Craft add-ons (popular with makers)","ink_label":"Choose an ink color to match your style","tooltip_text":"These supplies are perfect for handmade goods, packaging, and creative projects.","tile_subtext":"Popular with makers","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"10464","product_url":"https://www.simplystamps.com/gem-cluster-stamp","
4549sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","colored ink pads","stamping ink","stamp pad set","acid free ink pad","cardmaking ink","scrapbook ink pads"]},{"product_id":"9768","product_url":"https://www.simplystamps.com/seamless-eucalyptus-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["gem stamp","diamond stamp","sparkle stamp","crystal stamp","decorative craft stamp","embellishment stamp","scrapbook stamp"]},{"product_id":"6152","product_url":"https://www.simplystamps.com/mr-mrs-save-the-date-rubber-stamp","sort_order":0,"role":"anchor","friction":"none","experimental":false,"match_keywords":["eucalyptus stamp","botanical stamp","greenery stamp","floral craft stamp","envelope greenery stamp","wedding greenery stamp","leaf stamp"]},{"product_id":"23532","product_url":"https://www.simplystamps.com/book-with-skull-custom-book-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["save the date stamp","mr & mrs stamp","wedding save the date","engagement announcement stamp","wedding mail stamp","envelope save date stamp"]},{"product_id":"23534","product_url":"https://www.simplystamps.com/mushrooms-custom-book-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["custom book stamp","skull book stamp","gothic book stamp","book ownership stamp","personal library stamp","bookplate stamp"]},{"product_id":"23113","product_url":"https://www.simplystamps.com/outlined-heart-envelope-accent-craft-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["custom book stamp","mushroom book stamp","whimsical book stamp","book ownership stamp","personal library stamp","bookplate stamp"]},{"product_id":"23110","product_url":"https://www.simplystamps.com/hearts-through-arrow-envelope-accent-craft-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["heart envelope stamp","envelope accent stamp","love stamp","cardmaking heart stamp","wedding envelope stamp","valentine stamp"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["heart arrow stamp","love envelope stamp","envelope accent stamp","wedding envelope stamp","valentine stamp","cardmaking stamp"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]}]},"banking_bookkeepers":{"persona_name":"banking & bookkeeping","match_keywords":["banking","bookkeeping","accounting","deposit","endorsement","accounts payable","accounts receivable","invoicing","payment processing","check processing"],"header_title":"Banking Essentials: keep deposits fast, clean, and compliant.","stamp_label":"Banking add-ons (high-utility picks)","ink_label":"Choose an ink color for daily office use","tooltip_text":"These add-ons help reduce errors, speed up deposits, and protect sensitive info at the desk.","tile_subtext":"Popular with finance teams","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"4810","product_url":"https://www.simplystamps.com/3-line-deposit-only-stamp","
4549sort_order":0,"role":"anchor","friction":"medium","experimental":false,"match_keywords":["deposit only stamp","for deposit only stamp","endorsement stamp","check deposit stamp","bank deposit stamp","business deposit stamp","deposit endorsement stamp"]},{"product_id":"2487","product_url":"https://www.simplystamps.com/6-line-self-inking-for-deposit-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["deposit stamp","self inking deposit stamp","endorsement stamp","deposit only endorsement","bank deposit stamp","deposit stamp with account","business deposit stamp"]},{"product_id":"5924","product_url":"https://www.simplystamps.com/mobile-deposit-mobile-check-deposit-rubber-stamp","sort_order":0,"role":"core","friction":"low","experimental":false,"match_keywords":["mobile deposit stamp","remote deposit stamp","mobile check deposit stamp","bank app deposit stamp","mobile endorsement stamp","deposit only mobile stamp"]},{"product_id":"9303","product_url":"https://www.simplystamps.com/remote-deposit-stamp","sort_order":0,"role":"core","friction":"low","experimental":false,"match_keywords":["remote deposit stamp","remote check deposit stamp","deposit endorsement stamp","for deposit only remote","business remote deposit stamp","endorsement stamp remote deposit"]},{"product_id":"6026","product_url":"https://www.simplystamps.com/pay-to-the-order-of-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["pay to the order of stamp","payable to stamp","check endorsement stamp","accounts payable stamp","payment processing stamp","check handling stamp"]},{"product_id":"4349","product_url":"https://www.simplystamps.com/for-deposit-only-with-underline","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["for deposit only stamp","deposit stamp underline","endorsement stamp","bank deposit stamp","check deposit stamp","office deposit stamp"]},{"product_id":"5915","product_url":"https://www.simplystamps.com/deposited-dater-mobile-deposit-rubber-stamp","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["deposited dater stamp","deposit date stamp","deposit received date stamp","bank deposit date stamp","deposited stamp with date","deposit tracking stamp"]},{"product_id":"2429","product_url":"https://www.simplystamps.com/paid-round-stock-stamp","sort_order":0,"role":"backup","friction":"none","experimental":false,"match_keywords":["paid stamp","paid round stamp","invoice paid stamp","accounts payable stamp","office paid stamp","paid rubber stamp","paid self inking stamp"]},{"product_id":"2420","product_url":"https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip","sort_order":0,"role":"protection","friction":"none","experimental":false,"match_keywords":["secure marker","security marker","check fraud marker","check washing protection","document security marker","tamper resistant marker","fraud prevention marker"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","identity theft roller"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope adhesive","seal envelopes","mailing supplies"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]},{"product_id":"7894","product_url":"https://www.simplystamps.com/black-medium-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","medium ink pad","stamp pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad"]},{"product_id":"7899","product_url":"https://www.simplystamps.com/black-large-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","large ink pad","stamp pad","hand stamp","wood handle stamp","extra large stamp","traditional stamp"]}]},"shipping_station":{"persona_name":"shipping & fulfillment","match_keywords":["shipping","shipped","packing","packed","fulfillment","mailing","fragile","handle with care","return to
4549sender","warehouse","usps","ups","fedex"],"header_title":"Shipping Essentials: keep packages moving with clear handling marks.","stamp_label":"Shipping add-ons (warehouse favorites)","ink_label":"Choose an ink color for shipping workflows","tooltip_text":"Mark packages clearly to reduce damage, speed sorting, and keep fulfillment consistent.","tile_subtext":"Warehouse favorites","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"20191","product_url":"https://www.simplystamps.com/fragile-handle-with-care-business-stamp","sort_order":0,"role":"anchor","friction":"none","experimental":false,"match_keywords":["fragile stamp","handle with care stamp","shipping fragile stamp","package handling stamp","packing station stamp","warehouse handling stamp","fragile rubber stamp"]},{"product_id":"20190","product_url":"https://www.simplystamps.com/fragile-do-not-crush-business-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["do not crush stamp","fragile stamp","shipping handling stamp","package protection stamp","warehouse handling stamp","packing station stamp","do not crush rubber stamp"]},{"product_id":"20260","product_url":"https://www.simplystamps.com/fragile-this-side-up-business-stamp","
4549sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["this side up stamp","fragile stamp","keep upright stamp","shipping orientation stamp","package direction stamp","warehouse handling stamp","this side up rubber stamp"]},{"product_id":"2702","product_url":"https://www.simplystamps.com/stock-fragile-1010-stamp","sort_order":0,"role":"backup","friction":"none","experimental":false,"match_keywords":["fragile stock stamp","fragile stamp","shipping fragile stamp","package handling stamp","warehouse stamp","packing stamp","fragile rubber stamp"]},{"product_id":"9294","product_url":"https://www.simplystamps.com/return-to-sender-stamp-simply","sort_order":0,"role":"anchor","friction":"none","experimental":false,"match_keywords":["return to sender stamp","undeliverable stamp","address correction stamp","mail return stamp","package return stamp","shipping return stamp","return mail stamp"]},{"product_id":"9295","product_url":"https://www.simplystamps.com/return-to-sender-block-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["return to sender stamp","return to sender block","undeliverable stamp","mail return stamp","return label stamp","shipping return stamp","return package stamp"]},{"product_id":"6442","product_url":"https://www.simplystamps.com/return-to-sender-stamp-blue","sort_order":0,"role":"backup","friction":"none","experimental":false,"match_keywords":["return to sender stamp","blue ink return stamp","undeliverable stamp","mail return stamp","return package stamp","shipping return stamp"]},{"product_id":"2365","product_url":"https://www.simplystamps.com/shipped-via-usps-stock-stamp","sort_order":0,"role":"accessory","friction":"none","experimental":false,"match_keywords":["usps shipped stamp","shipped via usps","usps stamp","shipped stamp","carrier stamp","fulfillment stamp","shipping department stamp"]},{"product_id":"5301","product_url":"https://www.simplystamps.com/shipped-ship-dater-stamp","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["shipped dater stamp","ship date stamp","shipping date stamp","processed date stamp","fulfillment date stamp","warehouse date stamp","shipping workflow stamp"]},{"product_id":"6006","product_url":"https://www.simplystamps.com/packaged-by-cursive-packaging-stamp","sort_order":0,"role":"accessory","friction":"medium","experimental":false,"match_keywords":["packaged by stamp","packed by stamp","packaging stamp","fulfillment stamp","quality control stamp","packing station stamp","packaged by name stamp"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","mailroom tool"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope adhesive","seal envelopes","mailing supplies","envelope glue bottle"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]}]},"wedding_bells":{"persona_name":"weddings & events","match_keywords":["wedding","mr & mrs","engagement","save the date","invitations","rsvp","bridal","groom","wedding planning","event stationery","celebration"],"header_title":"Wedding Essentials: elevate invites, save time, and keep mail looking perfect.","stamp_label":"Wedding add-ons (stationery must-haves)","ink_label":"Choose an ink color for invites & envelopes","tooltip_text":"These extras help your invites feel polished â and make mailing a whole lot easier.","tile_subtext":"Stationery must-haves","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"6321","product_url":"https://www.simplystamps.com/simple-written-save-the-date-stamp","
4549sort_order":1,"role":"anchor","friction":"medium","experimental":false,"match_keywords":["save the date stamp","wedding save the date","engagement announcement stamp","wedding mail stamp","envelope save date stamp","custom save the date stamp"]},{"product_id":"11408","product_url":"https://www.simplystamps.com/formal-ornate-wedding-save-the-date-stamp","sort_order":2,"role":"core","friction":"medium","experimental":false,"match_keywords":["save the date stamp","formal save the date","ornate save the date","elegant save date stamp","wedding stationery stamp","envelope save date stamp"]},{"product_id":"11405","product_url":"https://www.simplystamps.com/formal-ornate-wedding-invitation-stamp","sort_order":3,"role":"core","friction":"medium","experimental":false,"match_keywords":["wedding invitation stamp","invitation stamp","formal wedding stamp","ornate wedding stamp","envelope invitation stamp","custom invitation stamp"]},{"product_id":"11406","product_url":"https://www.simplystamps.com/whimsical-wedding-invitation-stamp","
4549sort_order":4,"role":"backup","friction":"medium","experimental":false,"match_keywords":["wedding invitation stamp","whimsical wedding stamp","modern invitation stamp","fun wedding stamp","envelope invitation stamp","custom wedding invitation stamp"]},{"product_id":"22261","product_url":"https://www.simplystamps.com/couples-initial-wedding-address-stamp","sort_order":5,"role":"anchor","friction":"medium","experimental":false,"match_keywords":["wedding address stamp","wedding return address stamp","couple address stamp","newlywed address stamp","initials address stamp","wedding envelope stamp"]},{"product_id":"22259","product_url":"https://www.simplystamps.com/traditional-serif-wedding-address-stamp","sort_order":6,"role":"core","friction":"medium","experimental":false,"match_keywords":["wedding address stamp","wedding return address stamp","serif address stamp","classic wedding stamp","formal address stamp","wedding envelope address stamp"]},{"product_id":"5776","product_url":"https://www.simplystamps.com/nash-return-address-stamp","sort_order":7,"role":"backup","friction":"medium","experimental":false,"match_keywords":["return address stamp","address stamp","custom address stamp","family address stamp","wedding return address stamp","envelope return address stamp"]},{"product_id":"24186","product_url":"https://www.simplystamps.com/gold-wax-seal-sticker-rosemary-design","sort_order":8,"role":"accessory","friction":"none","experimental":false,"match_keywords":["wax seal stickers","gold wax seal stickers","rosemary wax seal","wedding wax seal sticker","invitation seal stickers","envelope seal stickers"]},{"product_id":"24188","product_url":"https://www.simplystamps.com/gold-wax-seal-sticker-eucalyptus-design","sort_order":9,"role":"accessory","friction":"none","experimental":false,"match_keywords":["wax seal stickers","gold wax seal stickers","eucalyptus wax seal","greenery wax seal","invitation seal stickers","envelope seal stickers"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":10,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","identity protection roller","security roller"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":11,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","package opener","paper opener"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":12,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope glue","envelope adhesive","seal envelopes","wedding invitation sealer","stationery supplies"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":13,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","colored ink pads","stamping ink","stamp pad set","cardmaking ink","scrapbook ink pads"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":14,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle","refill ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":15,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil","embossing foil stickers"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]}]},"medical_messengers":{"persona_name":"medical offices","match_keywords":["medical office","healthcare","patient paperwork","hipaa","confidential","privacy","medical records","office workflow","provider","clinic"],"header_title":"Medical Essentials: protect privacy and speed up paperwork.","stamp_label":"Medical add-ons (front-desk essentials)","ink_label":"Choose an ink color for daily use","tooltip_text":"These tools help reduce mistakes, protect patient info, and keep paperwork moving.","tile_subtext":"Front-desk essentials","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"5888","product_url":"https://www.simplystamps.com/read-all-instructions-rubber-stamp","
4549sort_order":0,"role":"anchor","friction":"low","experimental":false,"match_keywords":["read all instructions stamp","read instructions stamp","patient instructions stamp","medical instructions stamp","rx instructions stamp","patient paperwork stamp"]},{"product_id":"2736","product_url":"https://www.simplystamps.com/stock-confidential-1130-stamp","sort_order":0,"role":"core","friction":"none","experimental":false,"match_keywords":["confidential stamp","hipaa confidential stamp","medical confidential stamp","privacy stamp","patient record confidential","confidential paperwork stamp"]},{"product_id":"2420","product_url":"https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip","sort_order":0,"role":"protection","friction":"none","experimental":false,"match_keywords":["id protector roller","hipaa privacy roller","patient info blocker","data masking roller","address blocker roller","confidential info roller"]},{"product_id":"8927","product_url":"https://www.simplystamps.com/medium-signature-stamp","sort_order":0,"role":"backup","friction":"medium","experimental":false,"match_keywords":["secure marker","document security marker","confidential marker","tamper resistant marker","fraud prevention marker","security marker pen"]},{"product_id":"4378","product_url":"https://www.simplystamps.com/7-line-custom-rubber-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","front desk mail tool","open envelopes quickly"]},{"product_id":"2429","product_url":"https://www.simplystamps.com/paid-round-stock-stamp","sort_order":0,"role":"backup","friction":"none","experimental":false,"match_keywords":["envelope sealer","envelope adhesive","seal envelopes","office envelope sealer","mailing supplies","envelope glue bottle"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["signature stamp","doctor signature stamp","provider signature stamp","office signature stamp","authorized signature stamp","sign forms stamp"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"sLC","friction":"low","experimental":false,"match_keywords":["custom office stamp","medical address stamp","clinic info stamp","practice contact stamp","return address stamp business","patient paperwork stamp"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["paid stamp","billing paid stamp","invoice paid stamp","payment received stamp","accounts paid stamp","office paid stamp"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]}]},"legal_eagles":{"persona_name":"legal offices","match_keywords":["legal office","attorney","law firm","confidential","legal documents","signature authority","privacy","document security","client records"],"header_title":"Legal Essentials: protect sensitive docs and speed up processing.","stamp_label":"Legal add-ons (desk essentials)","ink_label":"Choose an ink color for daily legal use","tooltip_text":"These add-ons help reduce admin friction and protect client information across documents and mail.","tile_subtext":"Desk essentials","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"2736","product_url":"https://www.simplystamps.com/stock-confidential-1130-stamp","
4549sort_order":0,"role":"anchor","friction":"none","experimental":false,"match_keywords":["confidential stamp","legal confidential stamp","attorney confidential stamp","client info confidential","confidential document stamp","confidential file stamp"]},{"product_id":"13384","product_url":"https://www.simplystamps.com/legal-terms-dictionary","sort_order":0,"role":"accessory","friction":"none","experimental":false,"match_keywords":["legal terms dictionary","law dictionary","legal reference book","paralegal reference","attorney desk reference"]},{"product_id":"8927","product_url":"https://www.simplystamps.com/medium-signature-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["signature stamp","attorney signature stamp","authorized signature stamp","office signature stamp","stamp signature","sign documents stamp"]},{"product_id":"11712","product_url":"https://www.simplystamps.com/the-complete-notary-records-book","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["notary records book","notary journal","notarization record book","legal notary logbook","notary ledger","recordkeeping book"]},{"product_id":"2420","product_url":"https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip","sort_order":0,"role":"protection","friction":"none","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","identity theft roller"]},{"product_id":"4378","product_url":"https://www.simplystamps.com/7-line-custom-rubber-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["secure marker","document security marker","confidential marker","tamper resistant marker","fraud prevention marker","security marker pen"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","mailroom tool","open envelopes quickly"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope adhesive","seal envelopes","legal mailing supplies","envelope glue bottle","secure mail sealing"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["custom office stamp","law office stamp","firm info stamp","attorney address stamp","return address stamp business","legal paperwork stamp"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]},{"product_id":"7894","product_url":"https://www.simplystamps.com/black-medium-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","medium ink pad","stamp pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad"]},{"product_id":"7899","product_url":"https://www.simplystamps.com/black-large-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","large ink pad","stamp pad","hand stamp","wood handle stamp","extra large stamp","traditional stamp"]}]},"licensed_professionals":{"persona_name":"Licensed Professionals","header_title":"Professional & Licensed Stamps","tooltip_text":"Designed for licensed professionals who need official, compliant stamps for their trade or certification.","allowed_ink_colors":[],"default_primary_index":0,"tile_subtext":"Built for regulated professions and official use.","uses_product_tiles":true,"match_keywords":["license","architect","state of","tax","engineer","mechanical","auditor","land surveyor","geologist","inspection","licensed","professional stamp","official stamp","certification","corporate","registered","registration number","compliance","legal requirement","authorized","state issued","seal","embosser","jurisdiction","regulated","professional use"],"products":[{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":2,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller","identity theft roller","hide personal info","cover up info roller","id protection roller"]},{"product_id":"2420","product_url":"https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip","sort_order":3,"role":"protection","friction":"none","experimental":false,"match_keywords":["secure marker","security marker","check fraud marker","check washing protection","document security marker","tamper resistant marker","fraud prevention marker"]},{"product_id":"2736","product_url":"https://www.simplystamps.com/stock-confidential-1130-stamp","
4549sort_order":4,"role":"anchor","friction":"none","experimental":false,"match_keywords":["confidential stamp","legal confidential stamp","attorney confidential stamp","client info confidential","confidential document stamp","confidential file stamp"]},{"product_id":"8927","product_url":"https://www.simplystamps.com/medium-signature-stamp","sort_order":5,"role":"core","friction":"medium","experimental":false,"match_keywords":["signature stamp","medium signature stamp","authorized signature stamp","stamp signature","sign documents stamp","office signature stamp"]},{"product_id":"15209","product_url":"https://www.simplystamps.com/4810-date-only-dater-stamp","sort_order":6,"role":"anchor","friction":"none","experimental":false,"match_keywords":["date stamp","dater stamp","date only stamp","office date stamp","received date stamp","processed date stamp","document date stamp"]},{"product_id":"4378","product_url":"https://www.simplystamps.com/7-line-custom-rubber-stamp","sort_order":7,"role":"core","friction":"medium","experimental":false,"match_keywords":["custom office stamp","law office stamp","firm info stamp","attorney address stamp","return address stamp business","legal paperwork stamp"]},{"product_id":"2350","product_url":"https://www.simplystamps.com/received-outline-stock-stamp","sort_order":8,"role":"core","friction":"none","experimental":false,"match_keywords":["received stamp","received stock stamp","office received stamp","document received stamp","mail received stamp","received rubber stamp"]},{"product_id":"2334","product_url":"https://www.simplystamps.com/paid-outlined-stock-stamp","sort_order":9,"role":"core","friction":"none","experimental":false,"match_keywords":["paid stamp","paid stock stamp","invoice paid stamp","office paid stamp","accounts payable stamp","payment received stamp","paid rubber stamp"]},{"product_id":"4810","product_url":"https://www.simplystamps.com/3-line-deposit-only-stamp","sort_order":10,"role":"core","friction":"none","experimental":false,"match_keywords":["deposit only stamp","for deposit only stamp","bank deposit stamp","endorsement stamp","check deposit stamp","business deposit stamp","deposit endorsement stamp"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":11,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener","desk envelope opener","office mail tool","open envelopes quickly"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":12,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope glue","envelope adhesive","seal envelopes","mailing supplies","envelope sealing bottle","stationery adhesive"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":13,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","colored ink pads","stamping ink","stamp pad set","acid free ink pad","cardmaking ink","scrapbook ink pads","ink pads for rubber stamps"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":14,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle","replacement stamp ink","2 oz stamp ink","re-ink stamp pad"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":15,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","gold foil seals","silver foil seals","raised seal foil","foil sticker seals","certificate seal stickers"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]},{"product_id":"7894","product_url":"https://www.simplystamps.com/black-medium-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","medium ink pad","stamp pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad"]},{"product_id":"7899","product_url":"https://www.simplystamps.com/black-large-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","large ink pad","stamp pad","hand stamp","wood handle stamp","extra large stamp","traditional stamp"]}]},"bookish_collectors":{"persona_name":"book lovers & collectors","match_keywords":["book collection","book embosser","from the library","custom book","book stamp","personal library","book ownership","collector","collection management","bookplates","archival","name marking"],"header_title":"Library Essentials: mark your books with style (and keep them coming home).","stamp_label":"Book add-ons (collector favorites)","ink_label":"Choose an ink color for book pages","tooltip_text":"Perfect for personal libraries, classrooms, and gifting â these add-ons help your books stay yours.","tile_subtext":"Collector favorites","uses_product_tiles":true,"allowed_ink_colors":[],"default_primary_index":0,"products":[{"product_id":"23518","product_url":"https://www.simplystamps.com/modern-sun-custom-book-stamp","
4549sort_order":0,"role":"anchor","friction":"medium","experimental":false,"match_keywords":["custom book stamp","book ownership stamp","from the library of stamp","personal library stamp","teacher book stamp","bookplate stamp","stamp for books"]},{"product_id":"22204","product_url":"https://www.simplystamps.com/from-the-library-of-teacher-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["from the library of stamp","teacher library stamp","classroom library stamp","book stamp teacher","stamp for classroom books","book ownership stamp"]},{"product_id":"9810","product_url":"https://www.simplystamps.com/teacher-library-stamp","sort_order":0,"role":"backup","friction":"medium","experimental":false,"match_keywords":["teacher library stamp","classroom library stamp","from the library teacher","book stamp teacher","student book return stamp","book ownership stamp"]},{"product_id":"5932","product_url":"https://www.simplystamps.com/from-the-library-book-embosser","sort_order":0,"role":"anchor","friction":"medium","experimental":false,"match_keywords":["book embosser","library embosser","from the library embosser","raised seal book embosser","book ownership embosser","embosser for books"]},{"product_id":"22716","product_url":"https://www.simplystamps.com/round-floral-script-property-of-book-embosser","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["property of book embosser","floral book embosser","round book embosser","custom book embosser","raised seal embosser books","name embosser for books"]},{"product_id":"23528","product_url":"https://www.simplystamps.com/sword-crest-custom-book-stamp","sort_order":0,"role":"backup","friction":"medium","experimental":false,"match_keywords":["custom book stamp","crest book stamp","fantasy book stamp","gothic book stamp","book ownership stamp","personal library stamp","bookplate stamp"]},{"product_id":"23537","product_url":"https://www.simplystamps.com/enchanting-custom-book-stamp","sort_order":0,"role":"backup","friction":"medium","experimental":false,"match_keywords":["custom book stamp","whimsical book stamp","enchanting book stamp","book ownership stamp","personal library stamp","bookplate stamp","gift book stamp"]},{"product_id":"23536","product_url":"https://www.simplystamps.com/snake-seal-custom-book-stamp","sort_order":0,"role":"backup","friction":"medium","experimental":false,"match_keywords":["custom book stamp","snake book stamp","gothic book stamp","book ownership stamp","personal library stamp","bookplate stamp"]},{"product_id":"23509","product_url":"https://www.simplystamps.com/growth-custom-book-stamp","sort_order":0,"role":"core","friction":"medium","experimental":false,"match_keywords":["custom book stamp","botanical book stamp","modern book stamp","book ownership stamp","personal library stamp","from the library stamp","bookplate stamp"]},{"product_id":"24221","product_url":"https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller","sort_order":0,"role":"protection","friction":"low","experimental":false,"match_keywords":["id protector roller","privacy roller","address blocker roller","data masking roller"]},{"product_id":"23810","product_url":"https://www.simplystamps.com/envelope-opener","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope opener","letter opener","mail opener"]},{"product_id":"22023","product_url":"https://www.simplystamps.com/envelope-sealer-2oz-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["envelope sealer","envelope adhesive","seal envelopes","mailing supplies"]},{"product_id":"18507","product_url":"https://www.simplystamps.com/craft-ink-pads","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["craft ink pads","stamping ink","colored ink pads","craft stamp","envelope craft","wood handle","accent stamp","hand stamps","stamp pad set"]},{"product_id":"2517","product_url":"https://www.simplystamps.com/trodat-ideal-ink-2-oz-bottle","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["trodat ideal ink","stamp refill ink","self inking refill ink","stamp ink bottle"]},{"product_id":"6524","product_url":"https://www.simplystamps.com/embossing-adhesive-foil","sort_order":0,"role":"backup","friction":"low","experimental":false,"match_keywords":["embossing foil seals","embosser foil stickers","raised seal foil"]},{"product_id":"2518","product_url":"https://www.simplystamps.com/trodat-ideal-ink-1-oz-6cc-bottle","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink refill","refill ink","stamp ink","re-ink","reink","dried out stamp","replacement ink","self inking","self-inking","ink bottle","more ink","out of ink"]},{"product_id":"7889","product_url":"https://www.simplystamps.com/black-small-ink-pad","sort_order":0,"role":"accessory","friction":"low","experimental":false,"match_keywords":["ink pad","stamp pad","small ink pad","hand stamp","wood handle stamp","traditional stamp","rubber stamp pad","stamping pad"]}]}}} </script>
4549</div><div class="widget widget-static-block"> <style> /* ========================================================= 4550Scoped to #cart-kit-upsell-mount to avoid site collisions 4551========================================================= */ 4552#cart-kit-upsell-mount { 4553--ck-accent: var(--brand-primary, #1976d2); 4554--ck-accent-hover: var(--brand-primary-hover, #60B5E5); 4555--ck-success: var(--brand-success, #2e7d32); 4556--ck-success-hover: #256628; 4557} 4558#cart-kit-upsell-mount .cart-kit-upsell { 4559margin: 24px 0; 4560border-radius: 8px; 4561box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); 4562background: #ffffff; 4563border: 1px solid #e3e3e3; 4564overflow: visible; 4565font-family: inherit; 4566position: relative; 4567} 4568#cart-kit-upsell-mount .cart-kit-upsell__header { 4569width: 100%; 4570text-align: left; 4571border: 0; 4572padding: 12px 16px; 4573background: #f5fafc; 4574display: flex; 4575align-items: center; 4576justify-content: flex-start; 4577gap: 12px; 4578cursor: pointer; 4579border-radius: 8px; 4580} 4581#cart-kit-upsell-mount .cart-kit-upsell__header:focus { 4582outline: 2px solid var(--ck-accent); 4583outline-offset: -2px; 4584} 4585#cart-kit-upsell-mount .cart-kit-upsell__header-text { 4586display: flex; 4587flex-direction: column; 4588gap: 4px; 4589flex: 1 1 auto; 4590} 4591#cart-kit-upsell-mount .cart-kit-upsell__title { 4592font-size: 20px; 4593font-weight: 600; 4594color: #222; 4595} 4596#cart-kit-upsell-mount .cart-kit-upsell__chevron { 4597width: 10px; 4598height: 10px; 4599border-right: 2px solid #555; 4600border-bottom: 2px solid #555; 4601transform: rotate(45deg); 4602transition: transform 0.2s ease; 4603} 4604#cart-kit-upsell-mount .cart-kit-upsell--open .cart-kit-upsell__chevron { 4605transform: rotate(-135deg); 4606} 4607#cart-kit-upsell-mount .cart-kit-upsell__body { 4608padding: 12px 16px 14px; 4609border-top: 1px solid #e3e3e3; 4610animation: cartKitUpsellFadeIn 0.18s ease-out; 4611} 4612@keyframes cartKitUpsellFadeIn { 4613from { 4614opacity: 0; 4615transform: translateY(-2px); 4616} 4617to { 4618opacity: 1; 4619transform: translateY(0); 4620} 4621} 4622/* Icon zone */ 4623#cart-kit-upsell-mount .cart-kit-upsell__icon-wrap { 4624flex: 0 0 auto; 4625width: auto; 4626height: 36px; 4627display: flex; 4628align-items: center; 4629justify-content: center; 4630overflow: visible; 4631} 4632#cart-kit-upsell-mount .cart-kit-upsell__icon-img { 4633width: 70px; 4634height: auto; 4635display: block; 4636transform: none; 4637opacity: 1; 4638transition: none; 4639} 4640/* Options */ 4641#cart-kit-upsell-mount .cart-kit-upsell__options-group { 4642display: flex; 4643flex-direction: column; 4644gap: 10px; 4645} 4646#cart-kit-upsell-mount .cart-kit-upsell__options-label { 4647font-size: 16px; 4648font-weight: 600; 4649color: #333; 4650} 4651#cart-kit-upsell-mount .cart-kit-upsell__options-header { 4652display: flex; 4653align-items: center; 4654gap: 6px; 4655margin-bottom: 4px; 4656overflow: visible !important; 4657} 4658/* Tooltip */ 4659#cart-kit-upsell-mount .cart-kit-upsell__info { 4660position: relative; 4661border: 0; 4662padding: 0; 4663background: none; 4664cursor: pointer; 4665display: inline-flex; 4666z-index: 5; 4667} 4668#cart-kit-upsell-mount .cart-kit-upsell__info-icon { 4669width: 18px; 4670height: 18px; 4671border-radius: 999px; 4672border: 1px solid #888; 4673font-size: 11px; 4674font-weight: 600; 4675display: flex; 4676align-items: center; 4677justify-content: center; 4678color: #555; 4679background: #fff; 4680line-height: 1; 4681} 4682#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip { 4683position: absolute; 4684left: 50%; 4685bottom: calc(100% + 8px); 4686transform: translateX(-50%); 4687background: #222; 4688color: #fff; 4689font-size: 13px; 4690padding: 6px 8px; 4691border-radius: 4px; 4692box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); 4693opacity: 0; 4694pointer-events: none; 4695transition: opacity .15s ease, transform .15s ease; 4696white-space: normal; 4697max-width: min(260px, 80vw); 4698width: 180px; 4699line-height: 1.2; 4700z-index: 9999; 4701} 4702#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip::after { 4703content: ''; 4704position: absolute; 4705top: 100%; 4706left: 50%; 4707transform: translateX(-50%); 4708border-width: 6px; 4709border-style: solid; 4710border-color: #222 transparent transparent transparent; 4711} 4712#cart-kit-upsell-mount .cart-kit-upsell__info:hover .cart-kit-upsell__info-tooltip, 4713#cart-kit-upsell-mount .cart-kit-upsell__info:focus-visible .cart-kit-upsell__info-tooltip { 4714opacity: 1; 4715transform: translateX(-50%) translateY(-2px); 4716pointer-events: auto; 4717} 4718/* ========================================= 4719Grid (Accent stamps OR Notary product tiles) 4720========================================= */ 4721/* Default (Accent chips) */ 4722#cart-kit-upsell-mount .kit-stamp-grid { 4723display: flex; 4724flex-wrap: wrap; 4725gap: 16px; 4726align-items: stretch; 4727} 4728#cart-kit-upsell-mount .kit-stamp-card { 4729border: none; 4730background: transparent; 4731padding: 0 4px 4px; 4732margin: 0; 4733cursor: pointer; 4734outline: none; 4735flex: 0 0 auto; 4736} 4737/* Accent chip circle */ 4738#cart-kit-upsell-mount .kit-stamp-chip { 4739display: flex; 4740flex-direction: column; 4741align-items: center; 4742text-align: center; 4743max-width: 200px; 4744} 4745#cart-kit-upsell-mount .kit-stamp-chip__circle { 4746width: 200px; 4747height: auto; 4748border-radius: 16px; 4749background: #f3fbff; 4750box-shadow: 0 0 0 1px #e1f0fb; 4751display: flex; 4752align-items: center; 4753justify-content: center; 4754margin-bottom: 6px; 4755transition: box-shadow .15s ease, transform .1s ease, background .15s ease; 4756overflow: clip; 4757} 4758#cart-kit-upsell-mount .kit-stamp-chip__image { 4759width: 100%; 4760height: auto; 4761display: block; 4762} 4763#cart-kit-upsell-mount .kit-stamp-card:hover .kit-stamp-chip__circle { 4764box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.25); 4765transform: translateY(-1px); 4766} 4767#cart-kit-upsell-mount .kit-stamp-card.is-selected .kit-stamp-chip__circle { 4768background: #e6f2ff; 4769box-shadow: 0 0 0 2px var(--ck-accent); 4770} 4771/* ========================================= 4772Notary tile layout (only when .is-notary-grid is applied) 4773========================================= */ 4774#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4775display: inline-grid; 4776grid-template-columns: repeat(6, minmax(0, 1fr)); 4777gap: 16px; 4778align-items: stretch; 4779} 4780@media (max-width: 900px) { 4781#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4782grid-template-columns: repeat(4, minmax(0, 1fr)); 4783} 4784} 4785@media (max-width: 768px) { 4786#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4787grid-template-columns: repeat(3, minmax(0, 1fr)); 4788} 4789} 4790@media (max-width: 520px) {
4791#cart-kit-upsell-mount .cart-kit-upsell__header-text { 4792width: 50%; 4793} 4794/* Swipeable tiles on mobile */ 4795#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4796display: flex; 4797overflow-x: auto; 4798gap: 12px; 4799scroll-snap-type: x mandatory; 4800-webkit-overflow-scrolling: touch; 4801padding-bottom: 8px; 4802padding-top: 4px; 4803padding-left: 4px; 4804padding-right: 4px; 4805} 4806/* Each tile becomes a "slide" */ 4807#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid .kit-stamp-card.kit-product-card { 4808flex: 0 0 78%; 4809scroll-snap-align: start; 4810} 4811/* Hide scrollbar (nice-to-have) */ 4812#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid::-webkit-scrollbar { 4813display: none; 4814} 4815#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4816scrollbar-width: none; 4817} 4818/* Target the actual tile grid container */ 4819#cart-kit-upsell-mount .kit-stamp-grid { 4820display: flex !important; 4821flex-wrap: nowrap !important; 4822overflow-x: auto !important; 4823overflow-y: hidden !important; 4824gap: 12px !important; 4825scroll-snap-type: x mandatory; 4826-webkit-overflow-scrolling: touch; 4827padding-bottom: 10px; 4828} 4829/* Make each tile act like a slide */ 4830#cart-kit-upsell-mount .kit-stamp-grid>* { 4831flex: 0 0 78% !important; 4832/* change to 60% to show ~2 at a time */ 4833scroll-snap-align: start; 4834} 4835/* Optional: hide scrollbar */ 4836#cart-kit-upsell-mount .kit-stamp-grid::-webkit-scrollbar {
4837display: none; 4838} 4839#cart-kit-upsell-mount .kit-stamp-grid { 4840scrollbar-width: none; 4841} 4842#cart-kit-upsell-mount, 4843#cart-kit-upsell-mount .kit-upsell-inner, 4844#cart-kit-upsell-mount .kit-upsell-body { 4845overflow: visible !important; 4846} 4847} 4848/* Notary tile card */ 4849#cart-kit-upsell-mount .kit-stamp-card.kit-product-card { 4850background: #fff; 4851border: 1px solid #e1f0fb; 4852border-radius: 12px; 4853padding: 12px; 4854text-align: left; 4855flex: 1 1 auto; 4856transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, background .12s ease; 4857} 4858#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:hover { 4859transform: translateY(-1px); 4860border-color: rgba(25, 118, 210, 0.35); 4861box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); 4862} 4863#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.ck-swapping { 4864opacity: 0; 4865transition: opacity .18s ease; 4866} 4867#cart-kit-upsell-mount .kit-empty-tiles { 4868padding: 18px; 4869border: 1px dashed #d9d9d9; 4870border-radius: 10px; 4871margin-top: 10px; 4872font-size: 14px; 4873color: #444; 4874background: #fafafa; 4875} 4876/* Pool exhaustion: completion tile */ 4877#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete { 4878background: #f6faf7; 4879border: 1px dashed #b6dfc7; 4880cursor: default; 4881} 4882#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete:hover { 4883transform: none; 4884border-color: #b6dfc7; 4885box-shadow: none; 4886} 4887#cart-kit-upsell-mount .ck-tile-complete { 4888text-align: center; 4889padding: 16px 10px; 4890} 4891#cart-kit-upsell-mount .ck-tile-complete__icon { 4892font-size: 22px; 4893margin-bottom: 6px; 4894line-height: 1; 4895} 4896#cart-kit-upsell-mount .ck-tile-complete__title { 4897font-weight: 700; 4898margin-bottom: 4px; 4899} 4900#cart-kit-upsell-mount .ck-tile-complete__subtext { 4901font-size: 13px; 4902opacity: 0.85; 4903} 4904#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:focus-visible { 4905outline: 3px solid rgba(25, 118, 210, 0.35); 4906outline-offset: 2px; 4907} 4908#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected { 4909background: #f5f9ff; 4910border-color: var(--ck-accent); 4911box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.14); 4912} 4913/* Notary tile thumb */ 4914#cart-kit-upsell-mount .kit-product-thumb-tile { 4915width: 100%; 4916aspect-ratio: 1 / 1; 4917border-radius: 10px; 4918background: #f3f6fb; 4919border: 1px solid rgba(0, 0, 0, 0.06); 4920overflow: hidden; 4921position: relative; 4922display: flex; 4923align-items: center; 4924justify-content: center; 4925} 4926#cart-kit-upsell-mount .kit-product-thumb-tile img { 4927width: 100%; 4928height: 100%; 4929object-fit: contain; 4930display: block; 4931background-color: #fff; 4932} 4933#cart-kit-upsell-mount .kit-product-meta { 4934margin-top: 10px; 4935} 4936#cart-kit-upsell-mount .kit-product-name { 4937font-weight: 700; 4938font-size: 13px; 4939color: #222; 4940line-height: 1.2; 4941margin: 0; 4942} 4943#cart-kit-upsell-mount .kit-product-price { 4944margin-top: 6px; 4945font-size: 12px; 4946color: #4a4a4a; 4947} 4948#cart-kit-upsell-mount .kit-product-sub { 4949margin-top: 4px; 4950font-size: 11px; 4951color: #6b6b6b; 4952} 4953/* Notary tile selected checkmark */ 4954#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected .kit-product-thumb-tile::after { 4955content: "â"; 4956position: absolute; 4957right: 8px; 4958bottom: 8px; 4959width: 22px; 4960height: 22px; 4961border-radius: 999px; 4962background: var(--ck-success); 4963color: #fff; 4964font-size: 13px; 4965font-weight: 900; 4966display: flex; 4967align-items: center; 4968justify-content: center; 4969box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); 4970} 4971/* ========================================= 4972Micro PDP "Added" state (tile grey-out + overlay) 4973========================================= */ 4974#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added { 4975opacity: 0.55; 4976filter: grayscale(0.25); 4977pointer-events: none; 4978/* prevent clicking again */ 4979position: relative; 4980} 4981/* Stronger "Added" overlay */ 4982#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added::before { 4983content: "â Added to cart"; 4984position: absolute; 4985left: 10px; 4986right: 10px; 4987bottom: 10px; 4988padding: 10px 12px; 4989border-radius: 999px; 4990background: rgba(20, 20, 20, .90); 4991color: #fff; 4992font-weight: 800; 4993font-size: 13px; 4994text-align: center; 4995z-index: 6; 4996} 4997#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added img { 4998opacity: 0.75; 4999} 5000/* Module success toast */ 5001#cart-kit-upsell-mount .kit-success-toast { 5002position: absolute; 5003right: 16px; 5004bottom: 16px; 5005background: var(--ck-success-hover); 5006color: #fff; 5007padding: 10px 12px; 5008border-radius: 10px;
5009font-weight: 800; 5010font-size: 13px; 5011max-width: 340px; 5012box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); 5013opacity: 0; 5014transform: translateY(6px); 5015transition: opacity .18s ease, transform .18s ease; 5016pointer-events: none; 5017z-index: 50; 5018} 5019#cart-kit-upsell-mount .kit-success-toast.is-visible { 5020opacity: 1; 5021transform: translateY(0); 5022} 5023/* ========================================= 5024Cart row highlight after Micro PDP add 5025========================================= */ 5026#cart-kit-upsell-mount .ck-cart-highlight { 5027outline: 3px solid rgba(46, 125, 50, 0.35); 5028outline-offset: 4px; 5029border-radius: 8px; 5030animation: ckPulse 1.6s ease-in-out 0s 2; 5031} 5032@keyframes ckPulse { 50330% { 5034box-shadow: 0 0 0 rgba(46, 125, 50, 0.00); 5035} 503650% { 5037box-shadow: 0 0 0 10px rgba(46, 125, 50, 0.10); 5038} 5039100% { 5040box-shadow: 0 0 0 rgba(46, 125, 50, 0.00); 5041} 5042} 5043/* Ink swatches */ 5044#cart-kit-upsell-mount .kit-swatch-list { 5045display: flex; 5046flex-wrap: wrap; 5047gap: 8px; 5048} 5049#cart-kit-upsell-mount .kit-swatch { 5050display: inline-flex; 5051align-items: center; 5052gap: 6px; 5053padding: 4px 8px; 5054border-radius: 999px; 5055border: 1px solid #d8e0ea; 5056background: #fff; 5057cursor: pointer; 5058font-size: 14px; 5059color: #333; 5060transition: border-color .15s ease, box-shadow .15s ease, background .15s ease; 5061} 5062#cart-kit-upsell-mount .kit-swatch:hover { 5063border-color: var(--ck-accent); 5064box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); 5065} 5066#cart-kit-upsell-mount .kit-swatch.is-selected { 5067border-color: var(--ck-accent); 5068background: #f5f9ff; 5069box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.18); 5070} 5071#cart-kit-upsell-mount .kit-swatch__dot { 5072width: 14px; 5073height: 14px; 5074border-radius: 999px; 5075box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15); 5076} 5077#cart-kit-upsell-mount .kit-swatch__dot--black { 5078background: #111; 5079} 5080#cart-kit-upsell-mount .kit-swatch__dot--blue { 5081background: #1565c0; 5082} 5083#cart-kit-upsell-mount .kit-swatch__dot--red { 5084background: #c62828; 5085} 5086#cart-kit-upsell-mount .kit-swatch__dot--green { 5087background: var(--ck-success); 5088} 5089#cart-kit-upsell-mount .kit-swatch__dot--purple { 5090background: #6a1b9a; 5091} 5092#cart-kit-upsell-mount .kit-swatch__dot--none { 5093background: repeating-linear-gradient(45deg, #ccc, #ccc 3px, #eee 3px, #eee 6px); 5094} 5095/* Footer / CTA */ 5096#cart-kit-upsell-mount .cart-kit-upsell__footer { 5097display: flex; 5098flex-wrap: wrap; 5099align-items: center; 5100justify-content: space-between; 5101gap: 8px; 5102margin-top: 18px; 5103padding: 8px; 5104} 5105#cart-kit-upsell-mount .cart-kit-upsell__price { 5106font-size: 16px; 5107color: #222; 5108display: flex; 5109gap: 14px; 5110align-items: center; 5111flex-wrap: wrap; 5112} 5113#cart-kit-upsell-mount .cart-kit-upsell__cta { 5114padding: 8px 16px; 5115border-radius: 4px; 5116border: 0; 5117background: var(--ck-success); 5118color: #fff; 5119font-size: 13px; 5120font-weight: 600; 5121cursor: pointer; 5122white-space: nowrap; 5123} 5124#cart-kit-upsell-mount .cart-kit-upsell__cta:hover { 5125background: var(--ck-success-hover); 5126} 5127#cart-kit-upsell-mount .cart-kit-upsell__cta:disabled { 5128opacity: .6; 5129cursor: default; 5130} 5131#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary { 5132padding: 8px 14px; 5133border-radius: 4px; 5134border: 1px solid #ccc; 5135background: #f7f7f7; 5136color: #333; 5137font-size: 13px; 5138font-weight: 600; 5139cursor: pointer; 5140white-space: nowrap; 5141} 5142#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary:hover { 5143background: #eee; 5144} 5145/* Spinner on Add button */ 5146#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading { 5147position: relative; 5148padding-right: 30px; 5149} 5150#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading::after { 5151content: ''; 5152position: absolute; 5153right: 10px; 5154top: 50%; 5155width: 14px; 5156height: 14px; 5157margin-top: -7px; 5158border-radius: 50%; 5159border: 2px solid rgba(255, 255, 255, 0.7); 5160border-top-color: transparent; 5161animation: cartKitSpinner .6s linear infinite; 5162} 5163@keyframes cartKitSpinner { 5164to { 5165transform: rotate(360deg); 5166} 5167} 5168/* Add-to-cart progress bar (the ONLY progress bar in this script) */ 5169#cart-kit-upsell-mount .cart-kit-upsell__progresswrap { 5170margin-top: 10px; 5171} 5172#cart-kit-upsell-mount .cart-kit-upsell__progresstop { 5173display: flex; 5174justify-content: space-between; 5175gap: 12px; 5176margin-bottom: 6px; 5177} 5178#cart-kit-upsell-mount .cart-kit-upsell__progresstext, 5179#cart-kit-upsell-mount .cart-kit-upsell__progresspct { 5180font-size: 12px; 5181color: #555; 5182}
5183#cart-kit-upsell-mount .cart-kit-upsell__progressbar { 5184height: 10px; 5185border-radius: 999px; 5186background: #eef3f6; 5187overflow: hidden; 5188} 5189#cart-kit-upsell-mount .cart-kit-upsell__progressfill { 5190height: 100%; 5191background: var(--ck-success); 5192transition: width .18s ease; 5193width: 0%; 5194} 5195/* Success header state */ 5196#cart-kit-upsell-mount .cart-kit-upsell__header--success { 5197background: #f1faf3; 5198} 5199#cart-kit-upsell-mount .cart-kit-upsell__success-inline { 5200font-size: 14px; 5201color: var(--ck-success); 5202display: flex; 5203align-items: center; 5204gap: 8px; 5205font-weight: 600; 5206} 5207/* ========================================================= 5208Micro PDP Modal (iframe-based) 5209========================================================= */ 5210#cart-kit-upsell-mount .ck-micropdp-overlay { 5211position: fixed; 5212inset: 0; 5213background: rgba(0, 0, 0, .55); 5214display: none; 5215align-items: center; 5216justify-content: center; 5217z-index: 999999; 5218/* above cart + header */ 5219} 5220#cart-kit-upsell-mount .ck-micropdp-overlay.is-open { 5221display: flex; 5222} 5223#cart-kit-upsell-mount .ck-micropdp-modal { 5224width: min(960px, 92vw); 5225height: 96vh; 5226background: #fff; 5227border-radius: 12px; 5228overflow: hidden; 5229box-shadow: 0 12px 40px rgba(0, 0, 0, .35); 5230display: flex; 5231flex-direction: column; 5232} 5233@media (max-width: 600px) { 5234#cart-kit-upsell-mount .ck-micropdp-modal { 5235width: 96vw; 5236height: 98vh; 5237border-radius: 10px; 5238} 5239} 5240#cart-kit-upsell-mount .ck-micropdp-head { 5241display: flex; 5242flex-direction: column; 5243align-items: stretch; 5244justify-content: flex-start; 5245gap: 10px; 5246padding: 12px 14px; 5247border-bottom: 1px solid #e6e6e6; 5248background: #f7fbff; 5249} 5250#cart-kit-upsell-mount .ck-micropdp-head-top { 5251display: flex; 5252align-items: center; 5253justify-content: space-between; 5254gap: 12px; 5255width: 100%; 5256} 5257#cart-kit-upsell-mount .ck-micropdp-progress { 5258width: 100%; 5259display: flex; 5260flex-direction: column; 5261gap: 6px; 5262} 5263#cart-kit-upsell-mount .ck-micropdp-progressbar { 5264width: 100%; 5265height: 8px; 5266background: rgba(25, 118, 210, .14); 5267border-radius: 999px; 5268overflow: hidden; 5269} 5270#cart-kit-upsell-mount .ck-micropdp-progressfill { 5271height: 100%; 5272width: 0%; 5273background: var(--ck-accent); 5274border-radius: 999px; 5275transition: width .25s ease; 5276} 5277#cart-kit-upsell-mount .ck-micropdp-progresstext { 5278font-size: 12px; 5279color: #1b1b1b; 5280opacity: .85; 5281line-height: 1.2; 5282} 5283/* Micro PDP header progress â completion delight */ 5284#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progressfill { 5285background: var(--ck-success); 5286animation: ckMicroFillPulse .8s ease-in-out 0s 2; 5287} 5288#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progresstext { 5289font-weight: 700; 5290opacity: 1; 5291} 5292@keyframes ckMicroFillPulse { 52930% { 5294filter: brightness(1); 5295} 529650% { 5297filter: brightness(1.25); 5298} 5299100% { 5300filter: brightness(1); 5301} 5302} 5303#cart-kit-upsell-mount .ck-micropdp-title { 5304font-size: 16px; 5305font-weight: 700; 5306color: #1b1b1b; 5307margin: 0; 5308} 5309#cart-kit-upsell-mount .ck-micropdp-sub { 5310font-size: 12px; 5311color: #666; 5312margin-top: 2px; 5313} 5314#cart-kit-upsell-mount .ck-micropdp-actions { 5315display: flex; 5316gap: 8px; 5317align-items: center; 5318} 5319#cart-kit-upsell-mount .ck-micropdp-btn { 5320border: 1px solid #cfd8e3; 5321background: #fff; 5322color: #1b1b1b; 5323border-radius: 8px; 5324padding: 8px 10px; 5325cursor: pointer; 5326font-weight: 600; 5327font-size: 13px; 5328} 5329/* Loading cursor while micro PDP is submitting */ 5330body.ck-loading, 5331body.ck-loading * { 5332cursor: progress !important; 5333} 5334#cart-kit-upsell-mount .ck-micropdp-btn:hover { 5335background: #f2f5f9; 5336} 5337#cart-kit-upsell-mount .ck-micropdp-body { 5338flex: 1 1 auto; 5339position: relative; 5340} 5341#cart-kit-upsell-mount .ck-micropdp-iframe { 5342width: 100%; 5343height: 100%; 5344border: 0; 5345display: block; 5346} 5347#cart-kit-upsell-mount .ck-micropdp-loading { 5348position: absolute; 5349inset: 0; 5350display: flex; 5351align-items: center; 5352justify-content: center; 5353background: rgba(255, 255, 255, .85); 5354font-weight: 700; 5355color: #444; 5356} 5357.ck-debug-keywords { 5358margin-top: 4px; 5359font-size: 11px; 5360line-height: 1.4; 5361} 5362.ck-debug-keyword { 5363display: inline-block; 5364background: rgba(255, 255, 255, 0.08); 5365border: 1px solid rgba(255, 255, 255, 0.12); 5366border-radius: 999px; 5367padding: 2px 8px; 5368margin: 2px 6px 2px 0; 5369opacity: .95; 5370} 5371.ck-debug-keyword.muted, 5372.ck-debug-keywords.muted { 5373opacity: 0.55; 5374font-style: italic; 5375} 5376@media (max-width: 640px) {
5377#cart-kit-upsell-mount .cart-kit-upsell__title { 5378font-size: 16px; 5379} 5380} 5381@media (max-width: 520px) { 5382/* Accent chip mobile layout */ 5383#cart-kit-upsell-mount .kit-stamp-grid:not(.is-notary-grid) { 5384display: grid !important; 5385grid-template-columns: repeat(2, 1fr); 5386gap: 20px 12px; 5387justify-items: center; 5388} 5389#cart-kit-upsell-mount .kit-stamp-card { 5390width: 100%; 5391max-width: 160px; 5392} 5393#cart-kit-upsell-mount .kit-stamp-chip__circle { 5394width: 80px; 5395height: 80px; 5396} 5397#cart-kit-upsell-mount .kit-stamp-chip__image { 5398max-width: 62px; 5399max-height: 62px; 5400} 5401} </style>
5401<script> // ---- GA safety shim (prevents "gtag is not defined") ---- 5402window.dataLayer = window.dataLayer || []; 5403window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; 5404/** 5405* Cart Kit Upsell Module â Contract (Do Not Break) 5406* - Runs only on Cart page 5407* - Never blocks checkout interactions 5408* - Never modifies existing cart items (only adds new items) 5409* - Fails silently (no uncaught JS errors if PDP fetch/hydration fails) 5410* - Always has a fallback persona (generic) 5411* - Micro PDP is only used for SKUs that require custom options/uploads 5412*/ 5413document.addEventListener('DOMContentLoaded', function () { 5414// ---------------------------------------------------- 5415// 0) Render on Cart? 5416// ---------------------------------------------------- 5417// Only run SmartCart on the actual cart page. 5418// IMPORTANT: product pages and microPDP iframe pages often contain ".cart" elements (header/minicart), 5419// so the old detection was firing in the iframe and breaking. 5420var isCart = 5421document.body.classList.contains('checkout-cart-index') || 5422/\/checkout\/cart(\/|$)/.test(window.location.pathname); 5423if (!isCart) return; 5424// ---------------------------------------------------- 5425// 1) Mount BELOW .shoppingCartHeader-container 5426// ---------------------------------------------------- 5427// Zephyr cart is Tailwind markup (no #shopping-cart-table / .shoppingCartHeader-container). 5428// Mount after the "Shopping Cart" H1 header block; keep legacy anchors as fallback. 5429function ckFindZephyrHeader() { 5430var hs = document.querySelectorAll('h1'); 5431for (var i = 0; i < hs.length; i++) { 5432if (/cart/i.test(hs[i].textContent || '')) return hs[i].closest('.mb-6') || hs[i].parentElement; 5433} 5434return null; 5435} 5436var zHeader = ckFindZephyrHeader(); 5437var anchor = 5438document.querySelector('.shoppingCartHeader-container') || 5439zHeader; 5440var fallback = 5441(document.querySelector('#shopping-cart-table') && document.querySelector('#shopping-cart-table').parentElement) || 5442(zHeader && zHeader.parentElement) || 5443(document.querySelector('[data-cart-item-id]') && document.querySelector('[data-cart-item-id]').closest('.pb-32, main, #maincontent')) || 5444document.querySelector('.cart'); 5445if (!anchor && !fallback) return; 5446if (document.getElementById('cart-kit-upsell-mount')) return; 5447var mount = document.createElement('div'); 5448mount.id = 'cart-kit-upsell-mount'; 5449if (anchor && anchor.parentNode) anchor.insertAdjacentElement('afterend', mount); 5450else fallback.appendChild(mount); 5451// ---------------------------------------------------- 5452// 2) Query params 5453// ---------------------------------------------------- 5454var urlParams = new URLSearchParams(window.location.search); 5455var accentAddedQS = urlParams.get('accentAdded') === '1'; 5456var accentInCart = false; 5457var IS_ACCENT_EXPERIENCE = !!(accentAddedQS || accentInCart); 5458var accentAddMoreQS = urlParams.get('accentAddMore') === '1'; 5459var microAddedQS = urlParams.get('microAdded'); // productId 5460var microAddedNameQS = urlParams.get('microAddedName'); // product name (best effort) 5461// ===== SmartCart Persona Memory (Device-based) ===== 5462const SMARTCART_PERSONA_STORAGE_KEY = "smartcart_persona_v1"; 5463// --- Helpers --- 5464function nowMs() { return Date.now(); } 5465function safeJsonParse(str) { 5466try { return JSON.parse(str); } catch { return null; } 5467} 5468// --- Read / Write memory --- 5469function savePersonaToDevice(personaKey) { 5470if (!personaKey) return; 5471const payload = { 5472key: personaKey, 5473setAt: Date.now(), 5474version: 2 // bump version since behavior changed 5475}; 5476try { 5477localStorage.setItem( 5478SMARTCART_PERSONA_STORAGE_KEY, 5479JSON.stringify(payload) 5480); 5481} catch (e) { 5482// storage may be blocked; fail silently 5483} 5484} 5485function getPersonaFromDevice() { 5486let raw; 5487try { 5488raw = localStorage.getItem(SMARTCART_PERSONA_STORAGE_KEY); 5489} catch { 5490return null; 5491} 5492if (!raw) return null; 5493const payload = safeJsonParse(raw); 5494if (!payload?.key) return null; 5495return payload; 5496} 5497function clearPersonaFromDevice() { 5498try { localStorage.removeItem(SMARTCART_PERSONA_STORAGE_KEY); } catch { } 5499}
5500// --- GA user_property (set once per session/page lifecycle) --- 5501function setSmartCartUserPropertyOnce(personaKey) { 5502if (!personaKey) return; 5503if (window.__SMARTCART_USER_PROP_SET__) return; 5504window.__SMARTCART_USER_PROP_SET__ = true; 5505// If consent gating exists, you can wrap this (see section 4) 5506if (typeof gtag === "function") { 5507gtag("set", "user_properties", { smartcart_persona: personaKey }); 5508} 5509} 5510function onSmartCartMeaningfulEngagement(personaKey) { 5511if (!personaKey) return; 5512// Prevent repeated writes in one session/page lifecycle 5513if (window.__SMARTCART_ENGAGED__) return; 5514window.__SMARTCART_ENGAGED__ = true; 5515// Remember persona on device (UX) 5516savePersonaToDevice(personaKey); 5517// Attribute session in GA (analytics) 5518setSmartCartUserPropertyOnce(personaKey); 5519} 5520// ---------------------------------------------------- 5521// Debug exports (safe): expose helpers in console 5522// ---------------------------------------------------- 5523if (new URLSearchParams(location.search).get("sc_debug") === "1") { 5524window.__SMARTCART_DEBUG__ = window.__SMARTCART_DEBUG__ || {}; 5525window.__SMARTCART_DEBUG__.getPersonaFromDevice = getPersonaFromDevice; 5526window.__SMARTCART_DEBUG__.savePersonaToDevice = savePersonaToDevice; 5527window.__SMARTCART_DEBUG__.setSmartCartUserPropertyOnce = setSmartCartUserPropertyOnce; 5528window.__SMARTCART_DEBUG__.onSmartCartMeaningfulEngagement = onSmartCartMeaningfulEngagement; 5529} 5530// ---------------------------------------------------- 5531// 2.5) Hard SKU overrides (micro PDP required) 5532// ---------------------------------------------------- 5533var HARD_SKU_OVERRIDES = { 5534"2577": { addMode: "microPdp" }, 5535"4419": { addMode: "microPdp" }, 5536"10796": { addMode: "microPdp" } 5537}; 5538// ---------------------------------------------------- 5539// 3) PERSONA DETECTION (v2: Config-driven cart text scan) 5540// ---------------------------------------------------- 5541function loadPersonaConfigFromBlock() { 5542var el = document.getElementById('cart-kit-personas'); 5543if (!el) return null; 5544try { 5545var raw = (el.textContent || el.innerText || '').trim(); 5546if (!raw) return null; 5547return JSON.parse(raw); 5548} catch (e) { 5549console.warn('Cart Kit: Failed to parse persona JSON', e); 5550return null; 5551} 5552} 5553function buildCartSignals() { 5554// Adjust selectors to your cart markup if needed 5555var nameNodes = document.querySelectorAll('.product-name a, .product-name, .cart.item .product-item-name a, .cart.item .product-item-name'); 5556var optNodes = document.querySelectorAll('.item-options, dl.item-options, .product.options, .cart.item .item-options'); 5557var names = []; 5558for (var i = 0; i < nameNodes.length; i++) { 5559var t = (nameNodes[i].textContent || '').trim(); 5560if (t) names.push(t); 5561} 5562var opts = []; 5563for (var j = 0; j < optNodes.length; j++) { 5564var o = (optNodes[j].textContent || '').trim(); 5565if (o) opts.push(o); 5566} 5567var titleText = names.join(' | '); 5568var fullText = (names.concat(opts)).join(' | '); 5569return { titleText: titleText, fullText: fullText }; 5570} 5571// Product titles only (recommended for persona scoring) 5572function getCartTitleTextLower() { 5573try { 5574// Magento cart on SimplyStamps: h2.cartProductName a 5575var nameNodes = document.querySelectorAll( 5576'[data-cart-item-id] h3 a,' + 5577'[data-cart-item-id] h3,' + 5578'#shopping-cart-table h2.cartProductName a,' + 5579'#shopping-cart-table h2.cartProductName,' + 5580'#shopping-cart-table .product-name a,' + 5581'#shopping-cart-table .product-name,' + 5582'.product-item-name a,' + 5583'.product-item-name' 5584); 5585var names = []; 5586for (var i = 0; i < nameNodes.length; i++) { 5587var t = (nameNodes[i].textContent || '').trim(); 5588if (t) names.push(t); 5589} 5590return names.join(' | ').toLowerCase(); 5591} catch (e) { 5592return ''; 5593} 5594} 5595// Full cart text (titles + options). Keep for product scoring/debug if desired. 5596function getCartFullTextLower() { 5597try { 5598var titleLower = getCartTitleTextLower(); 5599var optNodes = document.querySelectorAll( 5600'[data-cart-item-id] .flex-wrap,' + 5601'#shopping-cart-table .item-options,' + 5602'#shopping-cart-table dl.item-options,' + 5603'.item-options,' + 5604'dl.item-options' 5605); 5606var opts = []; 5607for (var j = 0; j < optNodes.length; j++) { 5608var o = (optNodes[j].textContent || '').trim(); 5609if (o) opts.push(o); 5610} 5611// titleLower is already lowercase
5612var full = (titleLower ? [titleLower] : []).concat(opts.join(' | ').toLowerCase()).join(' | '); 5613return full.toLowerCase(); 5614} catch (e) { 5615return ''; 5616} 5617} 5618// ---------------------------------------------------- 5619// Cart product path signals (reliable for "already in cart" suppression) 5620// ---------------------------------------------------- 5621function normalizeUrlPathForCompare(url) { 5622try { 5623var u = new URL(url, window.location.origin); 5624var p = (u.pathname || ''); 5625// normalize: drop .html and trailing slashes 5626p = p.replace(/\.html$/i, '').replace(/\/+$/, ''); 5627return String(p || '').toLowerCase(); 5628} catch (e) { 5629try { 5630var s = String(url || '').split('#')[0].split('?')[0]; 5631s = s.replace(/\.html$/i, '').replace(/\/+$/, ''); 5632return String(s || '').toLowerCase(); 5633} catch (e2) { return ''; } 5634} 5635} 5636function getCartProductPathSet() { 5637var set = Object.create(null); 5638try { 5639var nodes = document.querySelectorAll( 5640'[data-cart-item-id] h3 a,' + 5641'#shopping-cart-table .product-item-name a,' + 5642'#shopping-cart-table a.product-item-link,' + 5643'.cart.table-wrapper .product-item-name a,' + 5644'.cart.table-wrapper a.product-item-link,' + 5645'.cart-container .product-item-name a,' + 5646'.cart-container a.product-item-link' 5647); 5648for (var i = 0; i < nodes.length; i++) { 5649var href = nodes[i] && (nodes[i].getAttribute('href') || nodes[i].href); 5650if (!href) continue; 5651var pth = normalizeUrlPathForCompare(href); 5652if (!pth) continue; 5653// Exclude cart actions/downloads that appear in the cart table 5654if (/^\/checkout\//i.test(pth)) continue; 5655if (/^\/sales\//i.test(pth)) continue; 5656if (/^\/download\//i.test(pth)) continue; 5657set[pth] = true; 5658} 5659} catch (e) { } 5660return set; 5661} 5662// Zephyr: exact product-id match from cart cards (data-product-id = Magento entity id) 5663function getCartProductIdSet() { 5664var set = Object.create(null); 5665try { 5666var nodes = document.querySelectorAll('[data-product-id]'); 5667for (var i = 0; i < nodes.length; i++) { 5668var pid = String(nodes[i].getAttribute('data-product-id') || '').trim(); 5669if (pid) set[pid] = true; 5670} 5671} catch (e) { } 5672return set; 5673} 5674// Compatibility alias used by existing scoring code 5675function getCartTextLower() { 5676// Persona scoring should use titles (cleanest signal) 5677return getCartTitleTextLower(); 5678// If you ever want âtitles + optionsâ instead, swap to: 5679// return getCartFullTextLower(); 5680} 5681// Spreadsheet-driven personas (local environment) 5682var PERSONAS_FALLBACK = [ 5683{ 5684key: 'brand_champions', 5685priority: 80, 5686match: [ 5687'business stamp', 'logo stamp', 'logo address stamp', 'custom logo stamp', 'company stamp', 'business address stamp', 5688'return address stamp business', 'self inking logo stamp', 'office branding', 'small business supplies', 5689'custom business stamps', 'bulk stamps' 5690], 5691headerTitle: 'Brand Essentials: finish your packaging strong.', 5692stampLabel: 'Business add-ons (popular picks)', 5693inkLabel: 'Choose an ink color for consistent branding', 5694tooltip: 'These add-ons help your brand look polished across packaging, mailers, and customer communications.', 5695defaultPrimaryIndex: 0, 5696tileSubtext: 'Popular with small businesses', 5697items: [ 5698{ productId: '2577', productUrl: 'https://www.simplystamps.com/qr-code-stamp-1-5-8-x-1-5-8', sortOrder: 2, addMode: 'microPdp' }, 5699{ productId: '4419', productUrl: 'https://www.simplystamps.com/medium-custom-logo-stamp', sortOrder: 1, addMode: 'microPdp' }, 5700{ productId: '10796', productUrl: 'https://www.simplystamps.com/mini-square-logo-stamp', sortOrder: 3, addMode: 'microPdp' } 5701] 5702}, 5703{ 5704key: 'home_sweet_stampers', 5705priority: 70, 5706match: [ 5707'return address stamp', 'address stamp', 'custom address stamp', 'family address stamp', 'wedding address stamp', 5708'holiday address stamp', 'mail stamp', 'envelope stamp', 'home address stamp', 'personalized address stamp' 5709], 5710headerTitle: 'Mail Essentials: everything you need for clean, easy stamping.', 5711stampLabel: 'Helpful add-ons for everyday mail', 5712inkLabel: 'Choose an ink color', 5713tooltip: 'Perfect for cards, envelopes, and seasonal mail â these extras make stamping easier and mess-free.', 5714defaultPrimaryIndex: 0, 5715tileSubtext: 'Great for everyday mail', 5716items: [ 5717{ productId: '5654', productUrl: 'https://www.simplystamps.com/merry-christmas-craft-rubber-stamp', sortOrder: 1 }, 5718{ productId: '6126', productUrl: 'https://www.simplystamps.com/happy-birthday-smiley-craft-stamp', sortOrder: 2 }, 5719{ productId: '5529', productUrl: 'https://www.simplystamps.com/live-well-rubber-stamp', sortOrder: 3 } 5720] 5721}, 5722{ 5723key: 'office_optimizers', 5724priority: 85, 5725match: [ 5726'signature', 'received stamp', 'approved stamp', 'paid stamp', 'office stamp', 'date stamp', 'custom office stamp', 5727'self inking stamp', 'pre inked stamp', 'inspection stamp', 'file stamp', 'reorder stamp', 'replacement ink' 5728], 5729headerTitle: 'Office Essentials: stay stocked and avoid reorders.', 5730stampLabel: 'Office add-ons (frequently reordered)', 5731inkLabel: 'Ink selection for daily office use', 5732tooltip: 'These supplies help keep workflows moving without interruptions or last-minute supply runs.', 5733defaultPrimaryIndex: 0, 5734tileSubtext: 'Frequently reordered', 5735items: [ 5736{ productId: '6560', productUrl: 'https://www.simplystamps.com/trodat-id-protection-stamp', sortOrder: 1 }, 5737{ productId: '2420', productUrl: 'https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip', sortOrder: 2 } 5738] 5739}, 5740{ 5741key: 'notary_ninjas', 5742priority: 100, 5743match: [ 5744'notary public', 'notary', 'notary stamp', 'notary seal', 'notary embosser', 'state notary stamp', 'notary supplies', 5745'notary kit', 'notary replacement stamp', 'official notary seal', 'notary expiration stamp', 5746'jurat stamp', 'acknowledgment stamp' 5747], 5748headerTitle: 'Notary Essentials: add backup ink & supplies before checkout.', 5749stampLabel: 'Notary add-ons (quick picks)', 5750inkLabel: 'Ink choice (recommended for notary work)', 5751tooltip: 'These items help ensure clean, compliant notarizations and prevent downtime if ink runs out.', 5752defaultPrimaryIndex: 0, 5753tileSubtext: 'Recommended for notary work', 5754items: [ 5755{ productId: '11712', productUrl: 'https://www.simplystamps.com/the-complete-notary-records-book', sortOrder: 1 }, 5756{ productId: '6524', productUrl: 'https://www.simplystamps.com/embossing-adhesive-foil', sortOrder: 2 }, 5757{ productId: '7986', productUrl: 'https://www.simplystamps.com/seal-impression-inker', sortOrder: 3 }, 5758{ productId: '13387', productUrl: 'https://www.simplystamps.com/embosser-vinyl-black-pouch', sortOrder: 4 } 5759] 5760}, 5761{ 5762key: 'classroom_heroes', 5763priority: 60, 5764match: [ 5765'teacher stamp', 'teacher ', 'grading stamp', 'school stamp', 'custom teacher stamp', 'classroom stamp', 5766'reward stamp', 'student feedback stamp', 'motivational stamp', 'education stamp', 'teacher name stamp' 5767], 5768headerTitle: 'Classroom Essentials: be ready for every lesson.', 5769stampLabel: 'Teacher add-ons (classroom favorites)', 5770inkLabel: 'Choose an ink color students will love', 5771tooltip: 'Extra ink and supplies help keep grading, rewards, and feedback flowing all year.', 5772defaultPrimaryIndex: 0, 5773tileSubtext: 'Classroom favorites', 5774items: [ 5775{ productId: '19215', productUrl: 'https://www.simplystamps.com/stacked-teacher-stamps-set-of-6', sortOrder: 1 }, 5776{ productId: '10965', productUrl: 'https://www.simplystamps.com/no-name-no-fame-owl-teacher-stamp', sortOrder: 2 }, 5777{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 3 } 5778] 5779}, 5780{
5781key: 'craft_crowd', 5782priority: 50, 5783match: [ 5784'craft stamp', 'handmade stamp', 'custom rubber stamp', 'craft rubber stamp', 'logo stamp small business', 'etsy stamp', 5785'thank you stamp', 'packaging stamp', 'product label stamp', 'arts and crafts stamp', 'creative stamp' 5786], 5787headerTitle: 'Creative Essentials: finish your projects with ease.', 5788stampLabel: 'Craft add-ons (popular with makers)', 5789inkLabel: 'Choose an ink color to match your style', 5790tooltip: 'These supplies are perfect for handmade goods, packaging, and creative projects.', 5791allowedInk: ['black', 'blue', 'red', 'green', 'purple'], 5792defaultPrimaryIndex: 0, 5793tileSubtext: 'Popular with makers', 5794items: [ 5795{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 1 }, 5796{ productId: '10464', productUrl: 'https://www.simplystamps.com/gem-cluster-stamp', sortOrder: 2 }, 5797{ productId: '9768', productUrl: 'https://www.simplystamps.com/seamless-eucalyptus-stamp', sortOrder: 3 } 5798] 5799}, 5800// Fallback (Accent Stamp grid) 5801{ 5802key: 'generic', 5803priority: 0, 5804match: [], 5805headerTitle: 'Choose your favorite accent stamp below and add to cart.', 5806stampLabel: 'Choose one or many Accent Stamp Designs:', 5807inkLabel: 'Would you like an Ink Pad? (Needed to make impression)', 5808tooltip: 'Stamp impression size: approx 1.75" Ã 0.67".', 5809allowedInk: ['black', 'blue', 'red', 'green', 'purple', 'none'], 5810defaultPrimaryIndex: 0, 5811tileSubtext: '', 5812items: [] 5813} 5814]; 5815function normalizePersonasConfigToArray(cfg) { 5816// Expect cfg like: { version: "...", personas: { key: { ... }, ... } } 5817if (!cfg || !cfg.personas || typeof cfg.personas !== 'object') return null; 5818var out = []; 5819Object.keys(cfg.personas).forEach(function (key) { 5820var p = cfg.personas[key] || {}; 5821// Map JSON schema â existing runtime schema 5822// Your runtime expects: key, priority, match, headerTitle, stampLabel, inkLabel, tooltip, allowedInk, defaultPrimaryIndex, tileSubtext, items[] 5823var persona = { 5824key: key, 5825priority: (typeof p.priority === 'number' ? p.priority : 0), 5826match: Array.isArray(p.match_keywords) ? p.match_keywords : [], 5827headerTitle: p.header_title || '', 5828stampLabel: p.stamp_label || '', 5829inkLabel: p.ink_label || '', 5830tooltip: p.tooltip_text || '', 5831allowedInk: Array.isArray(p.allowed_ink_colors) ? p.allowed_ink_colors : [], 5832defaultPrimaryIndex: (typeof p.default_primary_index === 'number' ? p.default_primary_index : 0), 5833tileSubtext: p.tile_subtext || '', 5834items: [] 5835}; 5836// Products â items (what buildGrid uses) 5837if (Array.isArray(p.products)) { 5838persona.items = p.products.map(function (prod) { 5839return { 5840productId: String(prod.product_id || '').trim(), 5841productUrl: prod.product_url || '', 5842sortOrder: (typeof prod.sort_order === 'number' ? prod.sort_order : 9999), 5843tileSubtext: prod.tile_subtext || '', 5844addMode: prod.addMode || prod.add_mode || undefined, 5845// NEW (traits) 5846role: prod.role || 'helper', 5847friction: prod.friction || 'low', 5848experimental: (typeof prod.experimental === 'boolean' ? prod.experimental : false), 5849// NEW (product-level keywords for scoring) 5850matchKeywords: Array.isArray(prod.match_keywords) ? prod.match_keywords : [] 5851}; 5852}).filter(function (it) { return !!it.productId || !!it.productUrl; }); 5853} 5854out.push(persona); 5855}); 5856return out; 5857} 5858var PERSONA_CONFIG = loadPersonaConfigFromBlock(); 5859// ---------------------------------------------------- 5860// 4) Now set PERSONAS like this (config â normalized array â fallback) 5861// ---------------------------------------------------- 5862var PERSONAS = (function () { 5863// Try config block first 5864var fromConfig = normalizePersonasConfigToArray(PERSONA_CONFIG); 5865// Use config if it produced at least 1 persona, otherwise fallback 5866var arr = (Array.isArray(fromConfig) && fromConfig.length) ? fromConfig : PERSONAS_FALLBACK.slice(); 5867// Ensure we ALWAYS have a generic fallback persona 5868var hasGeneric = arr.some(function (p) { return p && p.key === 'generic'; }); 5869if (!hasGeneric) { 5870var genericFallback = PERSONAS_FALLBACK.find(function (p) { return p.key === 'generic'; }); 5871if (genericFallback) arr.push(genericFallback); 5872} 5873// Normalize a few expected fields defensively 5874arr.forEach(function (p) { 5875if (!p) return; 5876if (!Array.isArray(p.match)) p.match = []; 5877if (!Array.isArray(p.items)) p.items = []; 5878if (!Array.isArray(p.allowedInk) || !p.allowedInk.length) { 5879// keep your existing default behavior (includes 'none') 5880p.allowedInk = ['black', 'blue', 'red', 'green', 'purple', 'none']; 5881} 5882}); 5883return arr; 5884})();
5885PERSONAS.forEach(function (p) { 5886if (Array.isArray(p.items)) p.items.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); }); 5887}); 5888PERSONAS.sort(function (a, b) { 5889if (a.key === 'generic') return 1; 5890if (b.key === 'generic') return -1; 5891return (b.priority || 0) - (a.priority || 0); 5892}); 5893function setLoadingCursor(on) { 5894document.body.classList.toggle('ck-loading', !!on); 5895} 5896// ---------------------------------------------------- 5897// 3.9) SCORING (persona + product) 5898// ---------------------------------------------------- 5899function keywordMatchScore(keywords, haystackLower) { 5900if (!Array.isArray(keywords) || !keywords.length) return { score: 0, hits: 0, matched: [] }; 5901var score = 0, hits = 0, matched = []; 5902for (var i = 0; i < keywords.length; i++) { 5903var k = String(keywords[i] || '').toLowerCase().trim(); 5904if (!k) continue; 5905// Exact substring match 5906if (haystackLower.indexOf(k) > -1) { 5907score += (k.indexOf(' ') > -1 ? 10 : 4); 5908hits += 1; 5909matched.push(k); 5910continue; 5911} 5912// Phrase fallback: all tokens appear anywhere 5913if (k.indexOf(' ') > -1) { 5914var parts = k.split(/\s+/).filter(Boolean); 5915if (parts.length >= 2) { 5916var all = true; 5917for (var p = 0; p < parts.length; p++) { 5918if (haystackLower.indexOf(parts[p]) === -1) { all = false; break; } 5919} 5920if (all) { 5921score += 6; 5922hits += 1; 5923matched.push(k + ' (tokens)'); 5924} 5925} 5926} 5927} 5928return { score: score, hits: hits, matched: matched }; 5929} 5930function scoreKeywordHits(keywords, haystackLower) { 5931return keywordMatchScore(keywords, haystackLower).score; 5932} 5933function scorePersonaFromCart(personaObj, cartTextLower) { 5934// Persona match keywords are in personaObj.match[] 5935var base = scoreKeywordHits(personaObj.match, cartTextLower); 5936// Priority is a tie-breaker, not a primary driver. 5937// Still, give a tiny nudge so higher-priority personas win ties. 5938var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0; 5939return base + priorityNudge; 5940} 5941function detectPersonaKey() { 5942var cartTextLower = getCartTitleTextLower(); 5943var bestKey = 'generic'; 5944var bestScore = -1; 5945for (var i = 0; i < PERSONAS.length; i++) { 5946var p = PERSONAS[i]; 5947if (!p || p.key === 'generic') continue;
5948var s = scorePersonaFromCart(p, cartTextLower); 5949if (s > bestScore) { 5950bestScore = s; 5951bestKey = p.key; 5952} 5953} 5954// If nobody matched anything, fall back 5955if (bestScore <= 0) return 'generic'; 5956return bestKey; 5957} 5958function getPersonaByKey(key) { 5959for (var i = 0; i < PERSONAS.length; i++) { 5960if (PERSONAS[i].key === key) return PERSONAS[i]; 5961} 5962return PERSONAS[PERSONAS.length - 1]; 5963} 5964var activePersonaKey = detectPersonaKey(); 5965var persona = getPersonaByKey(activePersonaKey); 5966window.__CK = { 5967personas: PERSONAS, 5968activePersonaKey: activePersonaKey, 5969persona: persona 5970}; 5971var useTiles = Array.isArray(persona.items) && persona.items.length > 0; 5972// Decide which persona to use for this render 5973function choosePersona({ detectedKey, detectedConfidence = 0, confidenceThreshold = 0.75 }) { 5974const stored = getPersonaFromDevice(); // { key, setAt... } or null 5975// Strong real-time signal wins 5976if (detectedKey && detectedConfidence >= confidenceThreshold) { 5977return { key: detectedKey, source: "detected_strong" }; 5978} 5979// Otherwise fall back to remembered persona if available 5980if (stored?.key) { 5981return { key: stored.key, source: "stored" }; 5982} 5983// Otherwise use detected (weak) or default 5984if (detectedKey) return { key: detectedKey, source: "detected_weak" }; 5985return { key: "default", source: "default" }; 5986} 5987// ---------------------------------------------------- 5988// 3b) Analytics helper (optional) 5989// ---------------------------------------------------- 5990function pushUpsellEvent(eventName, payload) { 5991payload = payload || {}; 5992// ---------------------------------------------------- 5993// Enrich payload persona context so GA4 can segment 5994// ---------------------------------------------------- 5995try { 5996var __ckPersonaKey = (typeof activePersonaKey !== 'undefined' && activePersonaKey) ? String(activePersonaKey) : 'generic'; 5997// Prefer persona.headerTitle as a human label, fall back to key 5998var __ckPersonaLabel = (typeof persona !== 'undefined' && persona && (persona.headerTitle || persona.key)) 5999? String(persona.headerTitle || persona.key) 6000: __ckPersonaKey; 6001// Only set defaults if caller didn't provide them 6002if (typeof payload.persona_key === 'undefined') payload.persona_key = __ckPersonaKey; 6003if (typeof payload.persona_label === 'undefined') payload.persona_label = __ckPersonaLabel; 6004if (typeof payload.upsell_variant === 'undefined') payload.upsell_variant = (typeof useTiles !== 'undefined' && useTiles) ? 'tiles' : 'stamps'; 6005if (typeof payload.page_path === 'undefined') payload.page_path = (window.location && window.location.pathname) ? window.location.pathname : ''; 6006} catch (eCtx) { } 6007// ---------------------------------------------------- 6008// Option 4: Meaningful engagement â remember persona + set GA user_property 6009// Trigger only on specific SmartCart events (no impressions) 6010// ---------------------------------------------------- 6011try { 6012var isMeaningful = 6013eventName === 'SmartCart_upsell_tile_selected' || 6014eventName === 'SmartCart_upsell_tile_click' || 6015eventName === 'SmartCart_upsell_tile_add' || 6016eventName === 'SmartCart_upsell_tile_added' || 6017eventName === 'SmartCart_upsell_added'; 6018if (isMeaningful) { 6019var pk = 6020(payload && (payload.persona_key || payload.personaKey)) || 6021(typeof activePersonaKey !== 'undefined' && activePersonaKey) || 6022(window.__CK && window.__CK.activePersonaKey); 6023if (pk) onSmartCartMeaningfulEngagement(String(pk)); 6024} 6025} catch (e) { } 6026// Always push to dataLayer (never depend on gtag existing) 6027try { 6028window.dataLayer = window.dataLayer || []; 6029window.dataLayer.push(Object.assign({ event: eventName }, payload)); 6030} catch (e) { } 6031// Optional: also send to gtag if it exists (but NEVER crash if missing) 6032try { 6033if (typeof window.gtag === 'function') { 6034window.gtag('event', eventName, payload); 6035} 6036} catch (e) { } 6037// Mirror to GA4 standard ecommerce (see CK_GA4 block below). Never 6038// let an analytics mirror break the module. 6039try { ckGaMirror(eventName, payload); } catch (eGa) { } 6040}
6041// ---------------------------------------------------- 6042// 3b-ii) GA4 standard ecommerce mirror [added 2026-09-15] 6043// ---------------------------------------------------- 6044// WHY: the SmartCart_* custom events carry product ids only inside a 6045// nested array (tiles_shown), and GA4 cannot report on nested arrays. 6046// The property has no custom dimension for productId, so per-product 6047// performance has been invisible since launch. GA4's `item_id` is a 6048// BUILT-IN dimension, so emitting standard ecommerce events gives us a 6049// per-product view -> select -> add funnel with zero custom-dimension 6050// setup. These are emitted ALONGSIDE the custom events, never instead. 6051// 6052// dataLayer only -- GTM is confirmed live on this page and already 6053// processing SmartCart pushes. We do not call gtag() here because line 6054// ~1015 stubs window.gtag when it is absent, so a gtag() call would be 6055// an unconditional duplicate push. 6056var CK_GA4 = { 6057EMIT: true, // master kill switch 6058EMIT_MICRO_ADD: true, // set false if the micro-PDP iframe fires its own add_to_cart 6059LIST_PREFIX: 'SmartCart', 6060HYDRATE_POLLS: 8, // 8 x 300ms = up to ~2.4s waiting for tile names 6061HYDRATE_POLL_MS: 300, 6062// SKUs for products that never go through parsePdpDoc, so nothing can 6063// hydrate a SKU for them: the ink parent (added by id + options) and 6064// the six accent stamps used by the non-tile 'generic' persona, where 6065// buildGrid returns before any PDP fetch. Without these, every add in 6066// the generic variant -- the bucket every unmatched cart falls into -- 6067// would ship an entity id and be unjoinable to purchase. 6068// All verified against prod 2026-09-16. Re-check if SKUs change. 6069STATIC_SKUS: { 6070'3631': '1009260', // Ideal Ink Pad for Traditional Wood Handle 6071'23105': '1027259', // Delicate Flower envelope accent 6072'23106': '1027260', // Simple botanical branches 6073'23107': '1027261', // (accent) 6074'23108': '1027262', // Elegant botanical branch 6075'23110': '1027264', // Hearts through arrow 6076'23113': '1027267' // Outlined heart 6077} 6078}; 6079// A SKU we are willing to put in item_id. Magento SKUs are short and 6080// token-like; a scrape that picked up a wrapper element's textContent 6081// ('SKU:\n 1012837') or a sentinel ('N/A') must not ship -- and must not 6082// be labelled id_source:'sku', which would hide it from the very 6083// diagnostic added to catch it. 6084function ckGaSkuOk(v) { 6085var s = ckGaValidId(v); 6086if (!s) return ''; 6087if (s.length > 40) return ''; 6088return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(s) ? s : ''; 6089} 6090// ckGaItem records, per built item, whether item_id is a real SKU or the 6091// entity-id fallback. ckGaPush reads and clears it -- the whole mirror runs 6092// synchronously inside one ckGaMirror call, so there is no interleaving. 6093var CK_GA4_IDSRC = []; 6094// Analytics-only mirror of tile metadata for products that entered a slot 6095// after buildGrid (swaps). Read by ckGaItem, by nothing else -- writing 6096// these into TILE_ITEM_BY_ID would change add-flow routing. 6097var CK_GA_TILE_META = Object.create(null); 6098// Reject ids that are not real product ids. TILE_ITEMS tolerates entries 6099// with no productId, and String(undefined) === 'undefined' would ship a 6100// junk item_id that also never hydrates (stalling the impression poll). 6101function ckGaValidId(v) { 6102if (v === null || typeof v === 'undefined') return ''; 6103var s = String(v).trim(); 6104if (!s || s === 'undefined' || s === 'null' || s === 'NaN') return ''; 6105return s; 6106} 6107function ckGaListId() { 6108try { 6109return String((typeof activePersonaKey !== 'undefined' && activePersonaKey) || 'generic'); 6110} catch (e) { return 'generic'; } 6111} 6112function ckGaListName() { 6113var label = ckGaListId(); 6114try { 6115if (typeof persona !== 'undefined' && persona && persona.headerTitle) { 6116label = String(persona.headerTitle); 6117} 6118} catch (e) { } 6119// GA4 truncates item_list_name at 100 chars; header_title is 6120// CMS-editable, so cap it here rather than letting GA4 split rows. 6121return (CK_GA4.LIST_PREFIX + ' - ' + label).slice(0, 100); 6122} 6123function ckGaPrice(v) { 6124if (v === null || typeof v === 'undefined') return undefined; 6125var n = parseFloat(String(v).replace(/[^0-9.\-]/g, '')); 6126return isFinite(n) ? Math.round(n * 100) / 100 : undefined; 6127} 6128// Build one GA4 item object for a SmartCart product id. 6129function ckGaItem(pid, index, listNameOverride) { 6130var id = String(pid);
6131var raw = null, hyd = null; 6132try { 6133if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null; 6134if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null; 6135} catch (e1) { } 6136try { 6137if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd = NOTARY_HYDRATED.get(id) || null; 6138} catch (e2) { } 6139// SKU is the id GA4 must see (see parsePdpDoc). Fall back to the 6140// entity id only when hydration has not produced a SKU yet -- that is 6141// recoverable and countable via the id_source param on the event, 6142// whereas dropping the item outright would lose the conversion. 6143var gaSku = ckGaSkuOk(hyd && hyd.sku) || 6144ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id]); 6145var gaId = gaSku || id; 6146try { CK_GA4_IDSRC.push(gaSku ? 'sku' : 'entity'); } catch (eSrc0) { } 6147var item = { 6148item_id: gaId, 6149item_brand: 'SimplyStamps', 6150item_category: 'SmartCart Upsell', 6151item_list_id: ckGaListId(), 6152// Item-scoped item_list_name is what GA4 item reports actually 6153// use, so an event-level override has to reach the items too. 6154item_list_name: listNameOverride || ckGaListName(), 6155quantity: 1 6156}; 6157// NEVER send a placeholder name. Tiles hydrate asynchronously, so a 6158// 'Product 12345' filler would permanently split the item_name 6159// dimension against the real name sent by later events. An absent 6160// dimension is recoverable; a wrong one is not. 6161var name = (hyd && hyd.name) || (raw && (raw.title || raw.name)) || ''; 6162if (name) item.item_name = String(name); 6163if (typeof index === 'number' && index > 0) item.index = index; 6164// Price: hydrated PDP price for tiles; the flat accent-stamp price 6165// in the non-tile variant, where nothing hydrates at all. 6166var price = ckGaPrice(hyd ? hyd.price : null); 6167if (typeof price !== 'number') { 6168try { 6169if (typeof useTiles !== 'undefined' && !useTiles && typeof STAMP_UNIT_PRICE === 'number') { 6170price = STAMP_UNIT_PRICE; 6171} 6172} catch (eP) { } 6173} 6174if (typeof price === 'number') item.price = price; 6175// Slice-able strategy attributes, so GA4 can answer 'do experimental 6176// tiles convert?' and 'does friction cost us adds?' without new dims. 6177// Only set what the config actually declares -- defaulting friction to 6178// 'low' would read back as 'low friction converts fine' off data that 6179// really means 'we did not know'. 6180if (raw) { 6181if (raw.role) item.item_variant = String(raw.role); 6182if (raw.friction) item.item_category2 = String(raw.friction); 6183if (typeof raw.experimental !== 'undefined') { 6184item.item_category3 = raw.experimental ? 'experimental' : 'core'; 6185} 6186} 6187return item; 6188} 6189// startIndex === null means 'do not emit index'. Only view_item_list 6190// knows a real display position; on select_item / add_to_cart an index 6191// would be fabricated (always 1, or add-order), and a fabricated 6192// position silently answers 'which slot converts?' wrong. 6193function ckGaItemsFor(ids, startIndex, listNameOverride) { 6194var out = []; 6195var useIdx = (startIndex !== null && typeof startIndex !== 'undefined'); 6196try { 6197(ids || []).forEach(function (pid) { 6198var id = ckGaValidId(pid); 6199if (!id) return; 6200out.push(ckGaItem(id, useIdx ? (startIndex + out.length + 1) : null, listNameOverride)); 6201}); 6202} catch (e) { } 6203return out; 6204} 6205// True once we can give this product the id GA4 needs. Gating the deferred 6206// impression on the NAME alone was not enough: ckRepaintCardAs writes a 6207// CK_TILE_FALLBACKS title into TILE_ITEM_BY_ID synchronously, before it even 6208// starts the PDP fetch, so a swapped-in fallback tile looked 'resolved' with 6209// no SKU yet -- view_item_list would ship its entity id while the shopper's 6210// later select_item shipped the real SKU. Same product, two item_ids, one 6211// session: exactly the split this is all meant to remove. 6212function ckGaHasSku(pid) { 6213var id = String(pid); 6214if (ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id])) return true; 6215try { 6216if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) { 6217var h = NOTARY_HYDRATED.get(id); 6218if (h && ckGaSkuOk(h.sku)) return true; 6219} 6220} catch (e) { } 6221return false; 6222} 6223// True once a product has a real, non-placeholder name available. 6224function ckGaHasName(pid) { 6225var id = String(pid), hyd = null, raw = null; 6226try { if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd =
6226NOTARY_HYDRATED.get(id) || null; } catch (e1) { } 6227try { 6228if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null; 6229if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null; 6230} catch (e2) { } 6231return !!((hyd && hyd.name) || (raw && (raw.title || raw.name))); 6232} 6233// Ink pads are options on a single parent SKU, not tiles -- model them as 6234// variants of that parent so ink attach is measurable too. 6235function ckGaInkItems(colorKeys, startIndex) { 6236var out = []; 6237try { 6238// The ink parent never hydrates through parsePdpDoc (it is added 6239// by id + options, not from a tile), so its SKU is pinned here. 6240var inkEnt = (typeof INK_PARENT_ID !== 'undefined' && INK_PARENT_ID) ? String(INK_PARENT_ID) : ''; 6241var inkSku = ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[inkEnt]); 6242var pid = inkSku || inkEnt || 'ink-pad'; 6243(colorKeys || []).forEach(function (ck, i) { 6244var row = { 6245item_id: pid, 6246item_name: 'Ink Pad', 6247item_brand: 'SimplyStamps', 6248item_category: 'SmartCart Upsell', 6249item_category3: 'core', 6250item_variant: String(ck), 6251item_list_id: ckGaListId(), 6252item_list_name: ckGaListName(), 6253quantity: 1 6254}; 6255// Ink is a flat-price option the module already prices itself. 6256// Without this, an ink-only add -- the module's core ask -- 6257// reports add_to_cart with no value at all. 6258if (typeof INK_PRICE === 'number') row.price = INK_PRICE; 6259if (startIndex !== null && typeof startIndex !== 'undefined') row.index = startIndex + i + 1; 6260// Report ink's id source too, or a mixed add reads as 'entity' 6261// and an analyst quarantining that label throws away good rows. 6262try { CK_GA4_IDSRC.push(inkSku ? 'sku' : 'entity'); } catch (eSrc1) { } 6263out.push(row); 6264}); 6265} catch (e) { } 6266return out; 6267} 6268function ckGaValue(items) { 6269var v = 0, any = false; 6270try { 6271(items || []).forEach(function (it) { 6272if (typeof it.price === 'number') { v += it.price * (it.quantity || 1); any = true; } 6273}); 6274} catch (e) { } 6275return any ? Math.round(v * 100) / 100 : undefined; 6276} 6277function ckGaPush(eventName, ecommerce) { 6278// Drain the id-source buffer FIRST, above every early return -- a 6279// stale entry would mislabel the next event. 6280var idsrc = CK_GA4_IDSRC; 6281CK_GA4_IDSRC = []; 6282if (!CK_GA4.EMIT) return; 6283if (!ecommerce || !Array.isArray(ecommerce.items) || !ecommerce.items.length) return; 6284try { 6285window.dataLayer = window.dataLayer || []; 6286// Clear the previous ecommerce object first so params never bleed 6287// between events (standard GTM ecommerce practice). 6288window.dataLayer.push({ ecommerce: null }); 6289// id_source tells the analyst whether these item_ids are real SKUs 6290// (joinable to purchase) or entity-id fallbacks from a tile that 6291// had not hydrated yet. Ink items are always pinned to a real SKU. 6292var srcTag = 'sku'; 6293try { 6294if (idsrc.length) { 6295var nEnt = idsrc.filter(function (x) { return x === 'entity'; }).length; 6296srcTag = (nEnt === 0) ? 'sku' : (nEnt === idsrc.length ? 'entity' : 'mixed'); 6297} 6298} catch (eSrc) { } 6299window.dataLayer.push({ 6300event: eventName, 6301smartcart: true, 6302persona_key: ckGaListId(), 6303id_source: srcTag, 6304ecommerce: ecommerce 6305}); 6306} catch (e) { } 6307} 6308// Single mirror point: called for every pushUpsellEvent. Adding a new 6309// GA4 event means adding a case here, not touching call sites. 6310function ckGaMirror(eventName, payload) { 6311if (!CK_GA4.EMIT) return; 6312payload = payload || {}; 6313var items, val, ec; 6314if (eventName === 'SmartCart_upsell_impression') { 6315// null = read the live grid at fire time. See 6316// ckGaFireListWhenHydrated for why that beats the build-time list. 6317ckGaFireListWhenHydrated(null); 6318return; 6319} 6320// Three distinct code paths end in a real selection. _select_fallback 6321// fires when the PDP prefetch failed but the tile WAS still selected, 6322// so omitting it would under-report select_item in proportion to our 6323// own fetch error rate. accent_selected is the non-tile variant. 6324if (eventName === 'SmartCart_upsell_tile_select_fallback' && payload.selected === false) { 6325return; // that path fires on deselect too 6326} 6327if (eventName === 'SmartCart_upsell_tile_selected' || 6328eventName === 'SmartCart_upsell_tile_select_fallback' || 6329eventName === 'SmartCart_upsell_accent_selected') { 6330var selId = payload.productId || payload.product_id || payload.stamp_id; 6331items = ckGaItemsFor([selId], null); 6332ckGaPush('select_item', { 6333item_list_id: ckGaListId(), 6334item_list_name: ckGaListName(), 6335items: items 6336}); 6337return; 6338} 6339if (eventName === 'SmartCart_upsell_added') { 6340items = ckGaItemsFor(payload.product_ids, null) 6341.concat(ckGaInkItems(payload.ink_colors, null)); 6342val = ckGaValue(items); 6343ec = { currency: 'USD', items: items }; 6344if (typeof val === 'number') ec.value = val; 6345ckGaPush('add_to_cart', ec); 6346return; 6347} 6348if (eventName === 'SmartCart_upsell_micro_added') { 6349if (!CK_GA4.EMIT_MICRO_ADD) return; 6350items = ckGaItemsFor(payload.product_ids, null); 6351val = ckGaValue(items); 6352ec = { currency: 'USD', items: items }; 6353if (typeof val === 'number') ec.value = val; 6354ckGaPush('add_to_cart', ec); 6355return; 6356} 6357} 6358// Read the product ids actually rendered in the grid right now. 6359function ckGaVisibleTileIds() { 6360var out = []; 6361try { 6362if (!stampGrid) return out; 6363var cards = stampGrid.querySelectorAll('[data-kit-primary]'); 6364for (var i = 0; i < cards.length; i++) { 6365var id = ckGaValidId(cards[i].getAttribute('data-kit-primary')); 6366if (id && out.indexOf(id) === -1) out.push(id); 6367} 6368} catch (e) { } 6369return out; 6370} 6371// view_item_list is deliberately NOT fired inline with the impression. 6372// 6373// Two reasons. (1) fireTilesImpressionOnce() runs synchronously right 6374// after the hydration fetches are *started*, so at that instant no tile
6375// has a real name or price -- firing then sends an id-only list whose 6376// item_name never matches the select_item / add_to_cart that follow, 6377// splitting every per-product report in two. (2) the tile quality gate 6378// drops or repaints tiles during those same milliseconds, so the 6379// build-time id list is not what the shopper ends up seeing. 6380// 6381// So: pass ids = null to re-read the LIVE grid on every poll, wait for 6382// names, then fire once. On timeout we still fire -- an id-only 6383// impression beats no impression at all, and a stalled poll would let 6384// select_item land before view_item_list and break funnel step order. 6385function ckGaFireListWhenHydrated(ids, attempts, listNameOverride) { 6386if (!CK_GA4.EMIT) return; 6387try { 6388var live = (ids === null || typeof ids === 'undefined'); 6389var list = live ? ckGaVisibleTileIds() 6390: (ids || []).map(ckGaValidId).filter(function (x) { return !!x; }); 6391if (!list.length) return; 6392attempts = (typeof attempts === 'number') ? attempts : CK_GA4.HYDRATE_POLLS; 6393var resolved = 0; 6394list.forEach(function (id) { if (ckGaHasName(id) && ckGaHasSku(id)) resolved++; }); 6395if (resolved < list.length && attempts > 0) { 6396setTimeout(function () { 6397ckGaFireListWhenHydrated(ids, attempts - 1, listNameOverride); 6398}, CK_GA4.HYDRATE_POLL_MS); 6399return; 6400} 6401ckGaPush('view_item_list', { 6402item_list_id: ckGaListId(), 6403item_list_name: listNameOverride || ckGaListName(), 6404items: ckGaItemsFor(list, 0, listNameOverride) 6405}); 6406} catch (e) { } 6407} 6408function fireTilesImpressionOnce() { 6409try { 6410if (!useTiles) return; 6411if (!Array.isArray(TILE_ITEMS) || !TILE_ITEMS.length) return; 6412// fire once per page load per persona 6413window.__ckImpressionFired = window.__ckImpressionFired || {}; 6414var key = String(activePersonaKey || 'generic'); 6415if (window.__ckImpressionFired[key]) return; 6416window.__ckImpressionFired[key] = true; 6417pushUpsellEvent('SmartCart_upsell_impression', { 6418// Flat id list -- GA4 cannot report on the nested tiles_shown 6419// array; ckGaMirror() turns this into view_item_list items[]. 6420product_ids: TILE_ITEMS.map(function (t) { return String(t.productId); }), 6421tiles_shown: TILE_ITEMS.map(function (t, idx) { 6422return { 6423productId: t.productId, 6424position: idx + 1, 6425experimental: !!t.experimental, 6426role: t.role || 'helper', 6427friction: t.friction || 'low' 6428}; 6429}) 6430}); 6431} catch (e) { } 6432} 6433function pushTileEvent(action, payload) { 6434payload = payload || {}; 6435pushUpsellEvent('SmartCart_upsell_tile_' + action, payload); 6436const personaKey = 6437payload.persona_key || 6438payload.personaKey || 6439(window.__CK && window.__CK.activePersonaKey) || 6440window.activePersonaKey; 6441if ( 6442personaKey && 6443(action === 'view' || action === 'in_view' || action === 'click' || action === 'add' || action === 'added' || action === 'selected') 6444) { 6445onSmartCartMeaningfulEngagement(personaKey); 6446} 6447} 6448// ---------------------------------------------------- 6449// 4) CONFIG 6450// ---------------------------------------------------- 6451// Ink pad parent product + options 6452var INK_PARENT_ID = '3631'; 6453var INK_SIZE_OPTION_ID = '1296'; // Size 6454var INK_COLOR_OPTION_ID = '1295'; // Ink Pad Color 6455var INK_SIZE_VALUE_ID = '6265'; // Small 6456var INK_COLOR_VALUES = { 6457black: '6257', 6458blue: '6258', 6459red: '6259', 6460green: '6260', 6461purple: '6262' 6462}; 6463// Accent stamp options 6464var ACCENT_SIZE_OPTION_ID = '1453'; 6465var ACCENT_SIZE_VALUE_ID = '6921'; 6466var ACCENT_INK_OPTION_ID = '2403'; 6467var ACCENT_INK_NO_THANKS_VALUE_ID = '9532'; 6468// Pricing model for Accent stamps (existing behavior) 6469var STAMP_UNIT_PRICE = 5.49; 6470var INK_PRICE = 4.99; 6471var STAMP_OPTIONS = [ 6472{ 6473id: '23105', 6474imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-1_800.jpg', 6475imgAlt: 'Delicate flower envelope accent craft stamp design' 6476}, 6477{ 6478id: '23106', 6479imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-2_800.jpg', 6480imgAlt: 'Simple botanical branches envelope accent craft stamp design' 6481}, 6482{ 6483id: '23108', 6484imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-4_800.jpg', 6485imgAlt: 'Elegant botanical branch envelope accent craft stamp design' 6486}, 6487{ 6488id: '23107', 6489imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-3_800.jpg', 6490imgAlt: 'Daisy flower envelope accent craft stamp design' 6491}, 6492{ 6493id: '23110', 6494imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-arrow_800.jpg', 6495imgAlt: 'Hearts through arrow envelope accent craft stamp design' 6496}, 6497{ 6498id: '23113', 6499imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-2_800.jpg', 6500imgAlt: 'Outlined heart envelope accent craft stamp design' 6501} 6502]; 6503var MAX_TILES_TO_SHOW = 6; // guardrail 6504var ALLOW_EXPERIMENTAL_TILES = true; // easy toggle 6505// Persona tile items (simple products). If none, we show Accent Stamp grid. 6506// Uses product-level matchKeywords + role/friction modifiers to pick the best N (MAX_TILES_TO_SHOW). 6507function scoreProductForCart(item, cartTextLower, allowExperimental) { 6508var score = 0; 6509// Keyword hits (product-level) 6510score += scoreKeywordHits(item.matchKeywords, cartTextLower); 6511// Role modifiers 6512switch (item.role) {
6513case 'anchor': score += 6; break; 6514case 'core': score += 4; break; 6515case 'compliance': score += 8; break; 6516case 'backup': score += 2; break; 6517case 'accessory': score += 1; break; 6518default: break; 6519} 6520// Friction modifiers 6521switch (item.friction) { 6522case 'none': score += 3; break; 6523case 'low': score += 1; break; 6524case 'medium': score -= 2; break; 6525case 'high': score -= 5; break; 6526default: break; 6527} 6528// Experimental gating 6529if (item.experimental && !allowExperimental) score -= 100; 6530return score; 6531} 6532function pickTopTilesForDisplay(items, cartTextLower, maxTiles, allowExperimental) { 6533var pool = Array.isArray(items) ? items.slice() : []; 6534// Ensure fields exist 6535pool.forEach(function (it) { 6536if (!it) return; 6537if (!Array.isArray(it.matchKeywords)) it.matchKeywords = []; 6538if (!it.role) it.role = 'helper'; 6539if (!it.friction) it.friction = 'low'; 6540}); 6541// Score + stable tiebreakers 6542var scored = pool.map(function (it, idx) { 6543return { 6544it: it, 6545idx: idx, 6546score: scoreProductForCart(it, cartTextLower, allowExperimental) 6547}; 6548}); 6549scored.sort(function (a, b) { 6550if (b.score !== a.score) return b.score - a.score; 6551// If tie: prefer anchors 6552var ar = (a.it.role === 'anchor') ? 1 : 0; 6553var br = (b.it.role === 'anchor') ? 1 : 0; 6554if (br !== ar) return br - ar; 6555// Then lower friction 6556var fr = { none: 0, low: 1, medium: 2, high: 3 }; 6557var af = fr[a.it.friction] != null ? fr[a.it.friction] : 9; 6558var bf = fr[b.it.friction] != null ? fr[b.it.friction] : 9; 6559if (af !== bf) return af - bf; 6560// Then sortOrder 6561return (a.it.sortOrder || 0) - (b.it.sortOrder || 0); 6562}); 6563// Dedupe by product id: the pool can carry the same product under two 6564// entries, and rendering it twice reads as a bug. 6565var seenPid = Object.create(null); 6566var selected = []; 6567for (var si = 0; si < scored.length && selected.length < maxTiles; si++) { 6568var cand = scored[si].it; 6569var cpid = String((cand && (cand.productId || cand.product_id)) || ''); 6570if (cpid) { 6571if (seenPid[cpid]) continue; 6572seenPid[cpid] = true; 6573} 6574selected.push(cand); 6575} 6576// Default index: highest score, but if an anchor is within 2 points of top, prefer it. 6577var defaultIndex = 0; 6578if (selected.length) { 6579var topScore = scored.length ? scored[0].score : 0; 6580for (var i = 0; i < selected.length; i++) { 6581var it = selected[i]; 6582var itScore = scoreProductForCart(it, cartTextLower, allowExperimental); 6583if (it.role === 'anchor' && itScore >= (topScore - 2)) { 6584defaultIndex = i; 6585break; 6586} 6587} 6588} 6589return { tiles: selected, defaultIndex: defaultIndex }; 6590} 6591// Respect ?upsellDebug=1 to include experimental tiles 6592var allowExperimentalNow = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1'); 6593var cartTextForScoring = getCartTitleTextLower(); 6594var tilePick = (useTiles ? pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow) : { tiles: [], defaultIndex: 0 }); 6595// Best-effort: suppress tiles that are already in the cart using cart link paths (most reliable signal we have on cart) 6596var __cartPaths0 = Object.create(null); 6597try { __cartPaths0 = getCartProductPathSet(); } catch (eCP0) { __cartPaths0 = Object.create(null); } 6598var __cartIds0 = Object.create(null); 6599try { __cartIds0 = getCartProductIdSet(); } catch (eCI0) { __cartIds0 = Object.create(null); } 6600function __isInCartByPath(it) { 6601try { 6602// Zephyr: exact product-id match first (data-product-id on cart cards) 6603var pid0 = it && (it.productId || it.product_id);
6604if (pid0 && __cartIds0[String(pid0)]) return true; 6605// Be tolerant: different shapes may use productUrl vs product_url 6606var url = ''; 6607if (it) { 6608url = it.productUrl || it.product_url || it.url || ''; 6609} 6610// If url missing but we have an id, try the lookup map (populated during tile build) 6611if (!url && it && it.productId && TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[it.productId]) { 6612url = TILE_ITEM_BY_ID[it.productId].productUrl || TILE_ITEM_BY_ID[it.productId].product_url || ''; 6613} 6614var pth = normalizeUrlPathForCompare(url); 6615return !!(pth && __cartPaths0[pth]); 6616} catch (e) { return false; } 6617} 6618// Build initial visible tiles from the ordered pool, skipping in-cart items. 6619// Keep tile count stable up to MAX_TILES_TO_SHOW. 6620var TILE_ITEMS = []; 6621var TILE_ITEM_BY_ID = {}; 6622if (useTiles && Array.isArray(tilePick.tiles)) { 6623for (var __i = 0; __i < tilePick.tiles.length; __i++) { 6624if (TILE_ITEMS.length >= MAX_TILES_TO_SHOW) break; 6625var it = tilePick.tiles[__i]; 6626if (!it) continue; 6627// Allow tiles even if productId is missing (null IDs can still be previewed). We'll block add-to-cart for them later. 6628// Skip anything already in cart (path match) 6629if (__isInCartByPath(it)) continue; 6630TILE_ITEMS.push(it); 6631try { if (it.productId) TILE_ITEM_BY_ID[it.productId] = it; } catch (eMap) { } 6632} 6633} 6634// Default index: keep "best" (0) after exclusions 6635if (useTiles) persona.defaultPrimaryIndex = 0; 6636// ---------------------------------------------------- 6637// 5) Build UI 6638// ---------------------------------------------------- 6639var inkPadHtml = 6640'<div class="cart-kit-upsell__options-group" style="margin-top:10px;">' 6641+ '<div class="cart-kit-upsell__options-label" id="kit-ink-label">Would you like an Ink Pad? (Needed to make impression)</div>' 6642+ '<div class="kit-swatch-list" id="kit-swatch-list">' 6643+ '<button type="button" class="kit-swatch" data-kit-color="black"><span class="kit-swatch__dot kit-swatch__dot--black"></span><span class="kit-swatch__label">Black</span></button>' 6644+ '<button type="button" class="kit-swatch" data-kit-color="blue"><span class="kit-swatch__dot kit-swatch__dot--blue"></span><span class="kit-swatch__label">Blue</span></button>' 6645+ '<button type="button" class="kit-swatch" data-kit-color="red"><span class="kit-swatch__dot kit-swatch__dot--red"></span><span class="kit-swatch__label">Red</span></button>' 6646+ '<button type="button" class="kit-swatch" data-kit-color="green"><span class="kit-swatch__dot kit-swatch__dot--green"></span><span class="kit-swatch__label">Green</span></button>' 6647+ '<button type="button" class="kit-swatch" data-kit-color="purple"><span class="kit-swatch__dot kit-swatch__dot--purple"></span><span class="kit-swatch__label">Purple</span></button>' 6648+ '<button type="button" class="kit-swatch" data-kit-color="none"><span class="kit-swatch__dot kit-swatch__dot--none"></span><span class="kit-swatch__label">I have an ink pad</span></button>' 6649+ '</div>' 6650+ '</div>'; 6651var inkPadBlock = (!useTiles ? inkPadHtml : ''); 6652var upsellMarkup = '' 6653+ '<div class="cart-kit-upsell cart-kit-upsell--open" id="cart-kit-upsell">' 6654+ '<button class="cart-kit-upsell__header" type="button">' 6655+ '<div class="cart-kit-upsell__icon-wrap" aria-hidden="true">' 6656+ '<img class="cart-kit-upsell__icon-img cart-kit-upsell__icon-img--static"' 6657+ ' src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/SS_Homepage-Icon_Stamps_120x120.png"' 6658+ ' alt="" />' 6659+ '</div>' 6660+ '<div class="cart-kit-upsell__header-text">' 6661+ '<span class="cart-kit-upsell__title" id="cart-kit-header-text">$1 Accent Stamp Unlocked! Choose your favorite below and add to cart.</span>' 6662+ '</div>' 6663+ '<span class="cart-kit-upsell__chevron" aria-hidden="true"></span>' 6664+ '</button>' 6665+ '<div class="cart-kit-upsell__body">' 6666+ '<div class="cart-kit-upsell__options-group">' 6667+ '<div class="cart-kit-upsell__options-header">' 6668+ '<div class="cart-kit-upsell__options-label" id="kit-stamp-label">Choose one or many Accent Stamp Designs:</div>' 6669+ '<button type="button" class="cart-kit-upsell__info" aria-label="Stamp impression size information">' 6670+ '<span class="cart-kit-upsell__info-icon">i</span>' 6671+ '<span class="cart-kit-upsell__info-tooltip" id="kit-tooltip">Stamp impression size: approx 1.75" Ã 0.67".</span>' 6672+ '</button>' 6673+ '</div>' 6674+ '<div class="kit-stamp-grid" id="kit-stamp-grid"></div>' 6675+ '</div>' 6676+ inkPadBlock 6677+ '<div class="cart-kit-upsell__footer">' 6678+ '<div class="cart-kit-upsell__price">' 6679+ '<span class="cart-kit-upsell__price-label" id="kit-price-label">Total:</span>' 6680+ '<span class="kit-value-line__new" id="kit-price-new">$1.00</span>' 6681+ '<button type="button" class="cart-kit-upsell__cta" id="cart-kit-upsell-add">Add to Cart</button>' 6682+ '<button type="button" class="cart-kit-upsell__cta-secondary" id="cart-kit-upsell-hide">Hide Offer</button>' 6683+ '</div>' 6684+ '<div class="kit-success-toast" id="kit-success-toast"></div>' 6685+ '<div class="cart-kit-upsell__status" id="cart-kit-status" aria-l
6685ive="polite"></div>' 6686+ '<div class="cart-kit-upsell__progresswrap" id="cart-kit-progresswrap" style="display:none;">' 6687+ '<div class="cart-kit-upsell__progresstop">' 6688+ '<div class="cart-kit-upsell__progresstext" id="cart-kit-progresstext">Adding itemsâ¦</div>' 6689+ '<div class="cart-kit-upsell__progresspct" id="cart-kit-progresspct">0%</div>' 6690+ '</div>' 6691+ '<div class="cart-kit-upsell__progressbar">' 6692+ '<div class="cart-kit-upsell__progressfill" id="cart-kit-progressfill"></div>' 6693+ '</div>' 6694+ '</div>' 6695+ '</div>' 6696+ '</div>' 6697+ '</div>' 6698+ '</div>'; 6699mount.insertAdjacentHTML('beforeend', upsellMarkup); 6700var upsell = mount.querySelector('#cart-kit-upsell'); 6701var header = upsell.querySelector('.cart-kit-upsell__header'); 6702var body = upsell.querySelector('.cart-kit-upsell__body'); 6703var addBtn = upsell.querySelector('#cart-kit-upsell-add'); 6704if (!upsell || !addBtn) { 6705console.warn('[SmartCart] Required module elements missing. Aborting init.'); 6706return; 6707} 6708var hideBtn = upsell.querySelector('#cart-kit-upsell-hide'); 6709var stampGrid = upsell.querySelector('#kit-stamp-grid'); 6710var swatchList = upsell.querySelector('#kit-swatch-list'); 6711var statusEl = upsell.querySelector('#cart-kit-status'); 6712if (statusEl) { 6713statusEl.setAttribute('role', 'status'); 6714statusEl.setAttribute('aria-live', 'polite'); 6715statusEl.setAttribute('aria-atomic', 'true'); 6716} 6717var progressWrap = upsell.querySelector('#cart-kit-progresswrap'); 6718var progressFill = upsell.querySelector('#cart-kit-progressfill'); 6719var progressText = upsell.querySelector('#cart-kit-progresstext'); 6720var progressPct = upsell.querySelector('#cart-kit-progresspct'); 6721var stampLabelEl = upsell.querySelector('#kit-stamp-label'); 6722var inkLabelEl = upsell.querySelector('#kit-ink-label'); 6723var tooltipEl = upsell.querySelector('#kit-tooltip'); 6724var headerTextEl = upsell.querySelector('#cart-kit-header-text'); 6725// Animate icon 6726setTimeout(function () { 6727var wrap = upsell.querySelector('.cart-kit-upsell__icon-wrap'); 6728if (wrap) wrap.classList.add('is-entered'); 6729}, 40); 6730// ---------------------------------------------------- 6731// 6) Apply persona UI (Notary) 6732// ---------------------------------------------------- 6733function applyPersonaUI() { 6734// Header + labels + tooltip 6735if (headerTextEl) headerTextEl.textContent = persona.headerTitle || ''; 6736if (stampLabelEl) stampLabelEl.textContent = persona.stampLabel || ''; 6737if (inkLabelEl) inkLabelEl.textContent = persona.inkLabel || ''; 6738if (tooltipEl) tooltipEl.textContent = persona.tooltip || ''; 6739// Toggle tile layout when persona has items 6740if (stampGrid) stampGrid.classList.toggle('is-notary-grid', !!useTiles); 6741// Filter ink swatches based on allowedInk 6742var allowed = (persona.allowedInk && persona.allowedInk.length) ? persona.allowedInk : ['black', 'blue', 'red', 'green', 'purple', 'none']; 6743if (swatchList) { 6744var swatches = swatchList.querySelectorAll('.kit-swatch'); 6745for (var i = 0; i < swatches.length; i++) { 6746var key = swatches[i].getAttribute('data-kit-color'); 6747swatches[i].style.display = (allowed.indexOf(key) > -1) ? '' : 'none'; 6748} 6749} 6750} 6751applyPersonaUI(); 6752// ---------------------------------------------------- 6753// 7) PDP Fetch Helpers (image + real price + real name) 6754// ---------------------------------------------------- 6755var PDP_CACHE = new Map(); // url -> { name, price, img } 6756function toNumberPrice(val) { 6757if (val == null) return null; 6758var n = Number(String(val).replace(/[^0-9.]/g, '')); 6759return isFinite(n) ? n : null; 6760} 6761function absUrlMaybe(src) { 6762if (!src) return null; 6763if (/^https?:\/\//i.test(src)) return src; 6764if (src.indexOf('/') === 0) return window.location.origin + src; 6765return src; 6766} 6767function detectRequiresMicro(doc) { 6768try { 6769var form = 6770doc.querySelector('#product_addtocart_form') || 6771doc.querySelector('form[action*="checkout/cart/add"]') || 6772doc.querySelector('form'); 6773if (!form) return false; 6774// Strong signal: any file upload = micro PDP 6775if (form.querySelector('input[type="file"]')) return true; 6776// Strong signal: Magento custom options payload (options[...]) 6777if (form.querySelector('[name^="options["]')) return true; 6778// Strong signal: configurables often use super_attribute 6779if (form.querySelector('[name^="super_attribute"]')) return true; 6780// Strong signal: required fields inside options wrapper 6781var optionsWrap = 6782form.querySelector('.product-options-wrapper') || 6783form.querySelector('#product-options-wrapper') || 6784form.querySelector('.fieldset') || 6785form; 6786// Look for explicit required markers/validators within options area 6787if ( 6788optionsWrap.querySelector('[required]') || 6789optionsWrap.querySelector('[aria-required="true"]') || 6790optionsWrap.querySelector('[data-validate*="required"]') || 6791optionsWrap.querySelector('.required-entry') || 6792optionsWrap.querySelector('.required') 6793) { 6794return true; 6795} 6796// If there are selectable option controls (select/radio/checkbox/textarea) 6797// inside a known options wrapper, treat as "likely micro" 6798// (kept conservative: only if wrapper exists) 6799var hasOptionControls = 6800!!optionsWrap.querySelector('select, input[type="radio"], input[type="checkbox"], textarea'); 6801if (hasOptionControls && (optionsWrap !== form)) return true; 6802return false;
6803} catch (e) { 6804return false; 6805} 6806} 6807function requiresMicroPdp(productId, rawTile) { 6808var pid = String(productId || '').trim(); 6809if (!pid) return false; 6810// 1) Explicit tile intent 6811if (rawTile && rawTile.addMode === 'microPdp') return true; 6812// 2) Hard override (authoritative) 6813if ( 6814typeof HARD_SKU_OVERRIDES !== 'undefined' && 6815HARD_SKU_OVERRIDES && 6816HARD_SKU_OVERRIDES[pid] && 6817HARD_SKU_OVERRIDES[pid].addMode === 'microPdp' 6818) { 6819return true; 6820} 6821// 3) Autodetect fallback from cached PDP parse 6822if ( 6823rawTile && 6824rawTile.productUrl && 6825typeof PDP_CACHE !== 'undefined' && 6826PDP_CACHE && 6827PDP_CACHE.has(rawTile.productUrl) 6828) { 6829var cached = PDP_CACHE.get(rawTile.productUrl); 6830if (cached && cached.requiresMicro === true) return true; 6831} 6832return false; 6833} 6834function isAccentExperience() { 6835try { 6836var urlParams = new URLSearchParams(window.location.search); 6837var accentAddedQS = (urlParams.get('accentAdded') === '1' || urlParams.get('accentAdded') === 'true'); 6838return !!accentAddedQS; // keep it strict for now 6839} catch (e) { 6840return false; 6841} 6842} 6843function parsePdpDoc(doc) { 6844var name = 6845(doc.querySelector('meta[property="og:title"]') && 6846doc.querySelector('meta[property="og:title"]').getAttribute('content')) || 6847(doc.querySelector('h1') && (doc.querySelector('h1').textContent || '').trim()) || 6848null; 6849var img = 6850(doc.querySelector('meta[property="og:image"]') && 6851doc.querySelector('meta[property="og:image"]').getAttribute('content')) || 6852(doc.querySelector('meta[name="twitter:image"]') && 6853doc.querySelector('meta[name="twitter:image"]').getAttribute('content')) || 6854null; 6855img = absUrlMaybe(img); 6856var price = 6857(doc.querySelector('meta[property="product:price:amount"]') && 6858doc.querySelector('meta[property="product:price:amount"]').getAttribute('content')) || 6859(doc.querySelector('meta[itemprop="price"]') && 6860doc.querySelector('meta[itemprop="price"]').getAttribute('content')) || 6861(doc.querySelector('[itemprop="price"]') && 6862doc.querySelector('[itemprop="price"]').textContent) || 6863(doc.querySelector('.price') && 6864doc.querySelector('.price').textContent) || 6865null; 6866price = toNumberPrice(price); 6867// Magento SKU. GA4 item_id MUST be the SKU, not the entity id: every 6868// other ecommerce event on this site (view_cart, purchase) keys items 6869// by SKU, so sending entity ids here would make the same product two 6870// different rows in GA4 and break any SmartCart-add -> purchase join. 6871var sku = null; 6872try { 6873var skuEl = doc.querySelector('meta[itemprop="sku"]') || doc.querySelector('[itemprop="sku"]'); 6874if (skuEl) { 6875sku = (skuEl.getAttribute('content') || skuEl.textContent || '').trim() || null; 6876} 6877} catch (eSku) { } 6878var requiresMicro = detectRequiresMicro(doc); 6879// The single reliable "is this a product?" signal. An account, login, 6880// 404 or category page still yields an og:title and an og:image, so 6881// title/image alone can't tell you the scrape succeeded. 6882var isProduct = !!( 6883doc.querySelector('#product_addtocart_form') || 6884doc.querySelector('form[action*="checkout/cart/add"]') 6885); 6886return { 6887name: name, 6888img: img, 6889price: price, 6890sku: sku, 6891isProduct: isProduct, 6892requiresMicro: requiresMicro 6893}; 6894} 6895// Same-origin PDP fetch: config product_urls are absolute www.simplystamps.com; 6896// rewrite to the current origin so hydration works on local/staging AND prod (no-op on www). 6897function ckSameOrigin(u) { 6898try { 6899var p = new URL(u, window.location.origin); 6900if (/(^|\.)simplystamps\.com$/i.test(p.hostname) && p.origin !== window.location.origin) { 6901return window.location.origin + p.pathname + p.search + p.hash; 6902} 6903return p.href; 6904} catch (e) { return u; } 6905} 6906// ---------------------------------------------------- 6907// ZEPHYR SIDE-CART BRIDGE (added 2026-09-11) 6908// Zephyr ships a slide-out cart drawer driven by an Alpine store, and the 6909// cart PAGE is a SECOND, independent Alpine component (cartPage()) holding 6910// its own copy of the same /zephyr/cart/getItems/ JSON. They do not talk: 6911// cart-drawer.js store.refresh() -> fires 'cart-refreshed' 6912// cart-page.js init() -> listens 'cart-updated' 6913// Nothing bridges those two names, so ANY out-of-band cart mutation must do
6914// BOTH: refresh the store (drawer + header badge) AND dispatch 'cart-updated' 6915// (main cart list + Order Summary). Doing only one leaves half the page 6916// stale -- which is the reason this module used to hard-reload. 6917// ---------------------------------------------------- 6918function ckCartStore(win) { 6919try { 6920var w = win || window; 6921if (!w.Alpine || typeof w.Alpine.store !== 'function') return null; 6922return w.Alpine.store('cart') || null; 6923} catch (e) { 6924return null; 6925} 6926} 6927// Push a cart mutation into BOTH Zephyr cart trees, optionally revealing the 6928// side cart. Returns false when the Zephyr store is absent (legacy theme) so 6929// callers can fall back to the old page-reload path. 6930function ckAfterCartMutation(openDrawer) { 6931var st = ckCartStore(window); 6932if (!st) return false; 6933if (openDrawer && typeof st.openDrawer === 'function') { 6934try { st._scrollToNew = true; } catch (e) { } 6935try { st.openDrawer(); } catch (e) { } 6936} 6937// Mirror Zephyr's own post-add timing (handleAjaxAddToCart waits 100ms). 6938setTimeout(function () { 6939try { if (typeof st.refresh === 'function') st.refresh(); } catch (e) { } 6940try { window.dispatchEvent(new CustomEvent('cart-updated')); } catch (e) { } 6941}, 100); 6942return true; 6943} 6944function fetchPdpData(productUrl) { 6945if (!productUrl) return Promise.resolve({ name: null, img: null, price: null }); 6946if (PDP_CACHE.has(productUrl)) return Promise.resolve(PDP_CACHE.get(productUrl)); 6947var __fetchUrl = ckSameOrigin(productUrl); 6948return fetch(__fetchUrl, { credentials: 'same-origin' }) 6949.then(function (res) { return res.text(); }) 6950.then(function (html) { 6951var doc = new DOMParser().parseFromString(html, 'text/html'); 6952var data = parsePdpDoc(doc); 6953PDP_CACHE.set(productUrl, data); 6954return data; 6955}) 6956.catch(function () { 6957var data = { name: null, img: null, price: null }; 6958PDP_CACHE.set(productUrl, data); 6959return data; 6960}); 6961} 6962// ---------------------------------------------------- 6963// Tile State Model (no UX changes yet) 6964// ---------------------------------------------------- 6965var TILE_STATE = { 6966AVAILABLE: 'available', 6967SELECTED: 'selected', 6968ADDED: 'added' 6969}; 6970// Central store keyed by SKU 6971// ---------------------------------------------------- 6972// TILE QUALITY GATE + APPROVED FALLBACKS (added 2026-09-15) 6973// Some persona entries point at URLs that aren't product pages on every store 6974// view (a prod-only SKU, a retired URL). The scrape then returns whatever that 6975// page happens to be -- "Create New Customer Account", no price, the site logo 6976// -- and we rendered it as a product. Never show that. Substitute an approved 6977// alternative the shopper doesn't already have, or drop the slot entirely. 6978// ---------------------------------------------------- 6979var CK_TILE_FALLBACKS = [ 6980{ productId: '22023', productUrl: 'https://www.simplystamps.com/envelope-sealer-2oz-bottle', title: 'Envelope Sealer - 2oz Bottle' }, 6981{ productId: '25340', productUrl: 'https://www.simplystamps.com/stamp-cleaner-kit', title: 'Stamp Cleaner Kit' }, 6982{ productId: '24186', productUrl: 'https://www.simplystamps.com/gold-wax-seal-sticker-rosemary-design', title: 'Gold Wax Seal Sticker, Rosemary Design' }, 6983{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', title: 'Craft Ink Pads' }, 6984{ productId: '24221', productUrl: 'https://www.simplystamps.com/guard-your-id-with-the-trodat-id-protector-roller', title: 'Guard Your ID with the Trodat ID Protector Roller' }, 6985{ productId: '23105', productUrl: 'https://www.simplystamps.com/delicate-flower-envelope-accent-craft-stamp', title: 'Delicate Flower Envelope Accent Craft Stamp' } 6986]; 6987var CK_FALLBACKS_USED = new Set(); 6988function ckIsUsableTileData(data) { 6989if (!data) return false; 6990if (data.isProduct === false) return false; // definitely not a PDP 6991if (data.price == null && !data.img) return false; // nothing worth showing 6992var n = String(data.name || '').trim().toLowerCase(); 6993if (!n) return false; 6994if (/create new customer|customer login|^log ?in|^sign ?in|page not found|404|access denied|shopping cart|my account|dashboard/.test(n)) return false; 6995return true; 6996} 6997function ckCartIdsNow() { 6998try { return getCartProductIdSet() || Object.create(null); } 6999catch (e) { return Object.create(null); } 7000} 7001// First approved alternative that is NOT already in the cart and NOT already on 7002// screen. Null when there is nothing safe left to show. 7003function ckPickFallbackTile() { 7004var inCart = ckCartIdsNow(); 7005var onScreen = Object.create(null); 7006try { 7007var rendered = document.querySelectorAll('[data-kit-primary]'); 7008for (var r = 0; r < rendered.length; r++) { 7009onScreen[String(rendered[r].getAttribute('data-kit-primary'))] = true; 7010} 7011} catch (e) { } 7012for (var i = 0; i < CK_TILE_FALLBACKS.length; i++) { 7013var f = CK_TILE_FALLBACKS[i]; 7014var id = String(f.productId); 7015if (CK_FALLBACKS_USED.has(id)) continue; 7016if (inCart[id]) continue; 7017if (onScreen[id]) continue; 7018CK_FALLBACKS_USED.add(id); 7019return f; 7020} 7021return null; 7022} 7023// Nothing safe for this slot -- remove it rather than render junk. 7024function ckDropTile(card) { 7025try { 7026var pid = String(card.getAttribute('data-kit-primary') || ''); 7027if (pid) { try { selectedPrimaryIds.delete(pid); } catch (eS) { } } 7028if (card.parentNode) card.parentNode.removeChild(card); 7029try { updatePriceUI(); } catch (eU) { } 7030// Tiles are dropped asynchronously, so this is the moment the module 7031// can become empty -- re-check every time. 7032try { ckSyncModuleEmptyState(); } catch (eE) { } 7033} catch (e) { } 7034} 7035// Repaint an already-rendered card onto a different product, then hydrate it. 7036function ckRepaintCardAs(card, item) { 7037if (!card || !item) return; 7038var id = String(item.productId); 7039try { TILE_ITEM_BY_ID[id] = item; } catch (e) { } 7040try { if (ckSeenTileIds) ckSeenTileIds.add(id); } catch (e) { } 7041card.setAttribute('data-kit-primary', id); 7042card.setAttribute('data-sku', id); 7043card.setAttribute('data-kit-url', item.productUrl || ''); 7044try { card.classList.remove('is-added', 'is-selected', 'is-complete'); } catch (e) { } 7045try { card.removeAttribute('aria-disabled'); } catch (e) { } 7046var nameEl = card.querySelector('.kit-product-name'); 7047var priceEl = card.querySelector('.kit-product-price'); 7048var thumbEl = card.querySelector('.kit-product-thumb-tile'); 7049if (nameEl) nameEl.textContent = item.title || 'Loading\u2026'; 7050if (priceEl) priceEl.textContent = '\u2014'; 7051if (thumbEl) thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">Loading\u2026</span>'; 7052fetchPdpData(item.productUrl).then(function (d2) { 7053if (String(card.getAttribute('data-kit-primary') || '') !== id) return; // swapped again 7054if (!ckIsUsableTileData(d2)) { 7055// This alternative isn't available in this store view either. Try the 7056// next approved one; CK_FALLBACKS_USED makes the recursion terminate. 7057var nextAlt = ckPickFallbackTile(); 7058if (nextAlt) ckRepaintCardAs(card, nextAlt); 7059else ckDropTile(card); 7060return; 7061} 7062try { 7063NOTARY_HYDRATED.set(id, { 7064sku: d2.sku, 7065productId: id, productUrl: item.productUrl, 7066name: d2.name || item.title, img: d2.img || null, 7067price: (d2.price != null ? Number(d2.price) : null) 7068}); 7069} catch (e) { } 7070if (nameEl) nameEl.textContent = d2.name || item.title; 7071if (priceEl) priceEl.textContent = (d2.price != null ? ('$' + Number(d2.price).toFixed(2)) : 'See price in cart'); 7072if (thumbEl) { 7073if (d2.img) thumbEl.innerHTML = '<img src="' + d2.img + '" alt="" />'; 7074else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 7075} 7076}); 7077} 7078// ---------------------------------------------------- 7079// MODULE EMPTY STATE (added 2026-09-15) 7080// Tiles can vanish AFTER render: the quality gate drops non-products and 7081// in-cart suppression removes what the shopper already owns. The original 7082// empty-state branch only ran when the pool was empty at BUILD time, and it 7083// targeted #kit-add-btn -- which does not exist, the CTA is 7084// #cart-kit-upsell-add -- so an emptied module was left on the page as a
7085// header + "Total: $0.00" + a live Add to Cart button with nothing to add. 7086// ---------------------------------------------------- 7087function ckSyncModuleEmptyState() { 7088try { 7089var tileCount = stampGrid ? stampGrid.querySelectorAll('[data-kit-primary]').length : 0; 7090var swatchCount = swatchList ? swatchList.querySelectorAll('.kit-swatch').length : 0; 7091if (tileCount > 0 || swatchCount > 0) { 7092if (upsell) upsell.style.display = ''; 7093return false; 7094} 7095// Nothing left to offer. An upsell with no offer is noise on a cart 7096// page, so take the whole module off the page rather than leave a 7097// header and a dead CTA behind. 7098try { console.warn('[SmartCart] nothing offerable remains (all in cart or filtered); hiding the module.'); } catch (e1) { } 7099try { 7100if (addBtn) { 7101addBtn.disabled = true; 7102addBtn.setAttribute('aria-disabled', 'true'); 7103} 7104} catch (e2) { } 7105if (upsell) upsell.style.display = 'none'; 7106return true; 7107} catch (e) { return false; } 7108} 7109var TileStore = (function () { 7110var map = Object.create(null); 7111function getSkuFromTile(tileEl) { 7112return ( 7113tileEl.getAttribute('data-sku') || 7114tileEl.getAttribute('data-product-sku') || 7115tileEl.getAttribute('data-kit-sku') || 7116'' 7117).trim(); 7118} 7119function inferIsHard(tileEl) { 7120// Preferred: explicit attribute you set when building tiles 7121// data-hard="1" or data-is-hard="true" 7122var hardAttr = 7123tileEl.getAttribute('data-hard') || 7124tileEl.getAttribute('data-is-hard') || 7125''; 7126if (hardAttr) return hardAttr === '1' || hardAttr === 'true'; 7127// Fallback: if you already tag these somewhere else 7128// (you can tighten this later when we implement Step 2) 7129return tileEl.classList.contains('is-hard') || false; 7130} 7131function initFromDom(rootEl) { 7132if (!rootEl) return; 7133var tiles = Array.from(rootEl.querySelectorAll('.kit-stamp-card[data-kit-primary]')); 7134tiles.forEach(function (tileEl) { 7135var sku = getSkuFromTile(tileEl); 7136if (!sku) return; 7137try { ckSeenTileIds.add(String(sku)); } catch (e) { } 7138if (!map[sku]) { 7139map[sku] = { 7140sku: sku, 7141el: tileEl, 7142isHard: inferIsHard(tileEl), 7143state: TILE_STATE.AVAILABLE 7144}; 7145} else { 7146// re-rendered tile, keep state but update DOM ref 7147map[sku].el = tileEl; 7148// do not overwrite isHard unless you want to re-infer 7149} 7150// Bootstrap state from current UI (so no visible changes) 7151// You can adjust these classnames to match your actual markup. 7152if (tileEl.classList.contains('is-added') || tileEl.querySelector('.ck-added, .cart-kit-added, .cart-kit-check')) { 7153map[sku].state = TILE_STATE.ADDED; 7154} else if (tileEl.classList.contains('is-selected') || tileEl.getAttribute('aria-pressed') === 'true') { 7155map[sku].state = TILE_STATE.SELECTED; 7156} else { 7157map[sku].state = TILE_STATE.AVAILABLE; 7158} 7159syncDom(sku); 7160}); 7161} 7162function get(sku) { 7163return map[String(sku || '')] || null; 7164} 7165function setState(sku, nextState) { 7166sku = String(sku || ''); 7167if (!map[sku]) return; 7168var prev = map[sku].state; 7169map[sku].state = nextState; 7170// Keep DOM aligned (but we preserve current look) 7171syncDom(sku); 7172return { sku: sku, prev: prev, next: nextState }; 7173} 7174function syncDom(sku) { 7175var t = map[String(sku || '')]; 7176if (!t || !t.el) return; 7177var el = t.el; 7178// Normalize classes (keep current behavior intact) 7179el.classList.toggle('is-selected', t.state === TILE_STATE.SELECTED); 7180el.classList.toggle('is-added', t.state === TILE_STATE.ADDED); 7181// Optional aria alignment if you already use it 7182// (doesn't change visuals but improves correctness) 7183if (t.state === TILE_STATE.SELECTED) el.setAttribute('aria-pressed', 'true'); 7184else el.setAttribute('aria-pressed', 'false'); 7185} 7186function all() { 7187return map; 7188} 7189return { 7190initFromDom: initFromDom, 7191get: get, 7192setState: setState, 7193syncDom: syncDom, 7194all: all 7195}; 7196})(); 7197// ---------------------------------------------------- 7198// Micro PDP (iframe) â for hard SKUs w/ uploads + dependent options 7199// ---------------------------------------------------- 7200var MICRO_PDP = { 7201overlay: null, 7202iframe: null, 7203loading: null, 7204lastUrl: null, 7205lastProductId: null, 7206returnUrl: null, 7207progressDone: 0, 7208progressTotal: 0 7209}; 7210// ---------------------------------------------------- 7211// Micro PDP progress (header bar) 7212// ---------------------------------------------------- 7213function getMicroProgressFromUrl() { 7214try { 7215var u = new URL(window.location.href); 7216var done = parseInt(u.searchParams.get('microDone') || '0', 10); 7217var total = parseInt(u.searchParams.get('microTotal') || '0', 10); 7218if (!isFinite(done) || done < 0) done = 0; 7219if (!isFinite(total) || total < 0) total = 0; 7220return { done: done, total: total }; 7221} catch (e) { 7222return { done: 0, total: 0 }; 7223} 7224} 7225function bumpMicroDoneInUrl() { 7226try { 7227var u = new URL(window.location.href); 7228var done = parseInt(u.searchParams.get('microDone') || '0', 10); 7229var total = parseInt(u.searchParams.get('microTotal') || '0', 10); 7230if (!isFinite(done) || done < 0) done = 0; 7231if (!isFinite(total) || total < 0) total = 0; 7232if (total > 0) { 7233done = Math.min(total, done + 1); 7234u.searchParams.set('microDone', String(done)); 7235window.history.replaceState({}, document.title, u.toString()); 7236} 7237return { done: done, total: total }; 7238} catch (e) { 7239return { done: 0, total: 0 }; 7240} 7241} 7242function renderMicroProgress(done, total) { 7243var wrap = mount.querySelector('#ck-micropdp-progress'); 7244var fill = mount.querySelector('#ck-micropdp-progressfill'); 7245var txt = mount.querySelector('#ck-micropdp-progresstext'); 7246// Only show when the user is doing a multi-item add (2+ total) 7247if (!wrap || !fill || !txt || !total || total <= 1) { 7248if (wrap) wrap.hidden = true; 7249return; 7250} 7251if (!isFinite(done) || done < 0) done = 0; 7252if (!isFinite(total) || total < 1) total = 1; 7253if (done > total) done = total; 7254var pct = Math.round((done / total) * 100); 7255wrap.hidden = false;
7256fill.style.width = pct + '%'; 7257// Completion state (for âAll setâ moment) 7258if (done >= total) { 7259wrap.classList.add('is-complete'); 7260txt.textContent = 'All set â ' + total + ' of ' + total + ' items added'; 7261} else { 7262wrap.classList.remove('is-complete'); 7263txt.textContent = done + ' of ' + total + ' items added'; 7264} 7265} 7266ensureMicroPdpModal(); 7267function ensureMicroPdpModal() { 7268if (MICRO_PDP.overlay) return; 7269var modalMarkup = `<div class="ck-micropdp-overlay" id="ck-micropdp-overlay" role="dialog" aria-modal="true" aria-label="Customize and add item"><div class="ck-micropdp-modal"><div class="ck-micropdp-head"><div class="ck-micropdp-head-top"><div><div class="ck-micropdp-title" id="ck-micropdp-title">Customize item</div><div class="ck-micropdp-sub" id="ck-micropdp-sub">Complete the required fields, then click âAdd To Cartâ.</div></div><div class="ck-micropdp-actions"> <button type="button" class="ck-micropdp-btn" id="ck-micropdp-full">View full details</button> <button type="button" class="ck-micropdp-btn" id="ck-micropdp-close">Close</button></div></div><div class="ck-micropdp-progress" id="ck-micropdp-progress" hidden><div class="ck-micropdp-progressbar"><div class="ck-micropdp-progressfill" id="ck-micropdp-progressfill"></div></div><div class="ck-micropdp-progresstext" id="ck-micropdp-progresstext">0 of 0 items added</div></div></div><div class="ck-micropdp-body"><div class="ck-micropdp-loading" id="ck-micropdp-loading">Loadingâ¦</div> <iframe class="ck-micropdp-iframe" id="ck-micropdp-iframe" title="Micro product details"></iframe></div></div></div> `; 7270// Append inside mount so scoped CSS applies 7271mount.insertAdjacentHTML('beforeend', modalMarkup); 7272MICRO_PDP.overlay = mount.querySelector('#ck-micropdp-overlay'); 7273MICRO_PDP.iframe = mount.querySelector('#ck-micropdp-iframe'); 7274MICRO_PDP.loading = mount.querySelector('#ck-micropdp-loading'); 7275var closeBtn = mount.querySelector('#ck-micropdp-close'); 7276var fullBtn = mount.querySelector('#ck-micropdp-full'); 7277// Safety guards 7278if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-open'); 7279if (closeBtn) closeBtn.addEventListener('click', function () { closeMicroPdp('button'); }); 7280if (fullBtn) fullBtn.addEventListener('click', function () { 7281if (MICRO_PDP.lastUrl) window.open(MICRO_PDP.lastUrl, '_blank', 'noopener'); 7282}); 7283// Optional overlay click closes 7284if (MICRO_PDP.overlay) { 7285MICRO_PDP.overlay.addEventListener('click', function (e) { 7286if (e.target === MICRO_PDP.overlay) closeMicroPdp('overlay'); 7287}); 7288} 7289} 7290// ESC closes 7291document.addEventListener('keydown', function (e) { 7292if (e.key === 'Escape' && MICRO_PDP.overlay && MICRO_PDP.overlay.classList.contains('is-open')) { 7293closeMicroPdp(); 7294} 7295}); 7296// On iframe load, inject âmicroâ CSS + force form submit to top window 7297MICRO_PDP.iframe.addEventListener('load', function () { 7298try { 7299var doc = MICRO_PDP.iframe.contentDocument; 7300if (!doc) return; 7301// Ensure scope class exists inside iframe (so our selectors always match) 7302doc.documentElement.classList.add('cart-kit-micro-pdp'); 7303doc.body.classList.add('cart-kit-micro-pdp'); 7304// Hide big site chrome + tighten layout (best-effort; selectors may vary) 7305var style = doc.createElement('style'); 7306style.id = 'ck-micro-css'; 7307style.textContent = ` 7308/* ========================================================= 7309Micro PDP trims â chrome off, form focus, thumbnail preview 7310========================================================= */ 7311/* 1) Remove site chrome */ 7312header, .header, 7313.nav-container, .navigation, 7314.breadcrumbs, .breadcrumb, 7315.page-header, .header.content, 7316.sections.nav-sections, 7317.message.global, .message.notice, 7318.cookie-status-message, 7319footer, .footer, .page-footer { 7320display: none !important; 7321} 7322/* 1b) Zephyr side cart must never render inside the configurator. 7323Neither the drawer nor its trigger sits inside <header>, so the 7324chrome rules above miss both. */ 7325button[aria-label="Open cart"], 7326[x-show*="drawerOpen"] { 7327display: none !important; 7328} 7329/* 2) Normalize layout */ 7330html, body { height: auto !important; } 7331body { 7332margin: 0 !important; 7333padding: 12px !important; 7334background: #fff !important; 7335overflow-x: hidden !important; 7336} 7337.page-wrapper, .page-main, main, .columns, .column.main { 7338width: 100% !important; 7339max-width: none !important; 7340margin: 0 !important; 7341padding: 0 !important; 7342} 7343/* Hide the INLINE form add-to-cart row (but keep the sticky bottom bar) */ 7344/* Safety: ensure the STICKY/mobile bar still shows */
7345.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container, 7346.cart-kit-micro-pdp .add-to-box-mobile .add-to-cart-button-container-non-grouped, 7347.cart-kit-micro-pdp .add-to-box-mobile .input-group-mobile-container { 7348display: flex !important; 7349} 7350/* 2) Re-show the sticky/mobile bar at the bottom (itâs outside the form) */ 7351.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container { 7352display: flex !important; 7353} 7354/* Hide qty+button ONLY when it's NOT inside the sticky bottom bar */ 7355.cart-kit-micro-pdp .qty-and-add-to-cart-container:not(.add-to-box-mobile .qty-and-add-to-cart-container) { 7356display: none !important; 7357} 7358/* Kill common "extra" sections we don't need */ 7359#ss-main-content { margin-top: unset !important; display:none !important; } 7360#sticky-mobile-header{ display: none !important; } 7361.gqcs-swiper-container{ display: none !important; } 7362.product-wapper-tab { display: none !important; } 7363/* 3) Preview: make it a small thumbnail WITHOUT reflowing layouts 7364(works across templates: big hero, centered preview, swiper, etc.) */ 7365.cart-kit-micro-pdp .product.media, 7366.cart-kit-micro-pdp .product-image-carousel-container, 7367.cart-kit-micro-pdp .product-image-carousel-select, 7368.cart-kit-micro-pdp #ss-image-sheet, 7369.cart-kit-micro-pdp .gallery-placeholder, 7370.cart-kit-micro-pdp .product-img-content { 7371width: 100% !important; 7372max-width: 100% !important; 7373} 7374/* Center the preview area */ 7375.cart-kit-micro-pdp .product.media, 7376.cart-kit-micro-pdp .product-image-carousel-container, 7377.cart-kit-micro-pdp .product-image-carousel-select, 7378.cart-kit-micro-pdp .gallery-placeholder { 7379display: flex !important; 7380justify-content: center !important; 7381} 7382/* Image itself: cap size (no cropping) */ 7383.cart-kit-micro-pdp img.img-responsive, 7384.cart-kit-micro-pdp .product.media img, 7385.cart-kit-micro-pdp .product-image-photo, 7386.cart-kit-micro-pdp .fotorama__img, 7387.cart-kit-micro-pdp .swiper-slide img { 7388max-height: 300px !important; 7389max-width: 300px !important; 7390width: auto !important; 7391height: auto !important; 7392object-fit: contain !important; 7393margin: 0 !important; 7394} 7395/* Prevent swiper/nav chrome from adding vertical junk */ 7396/* Swiper UI: show dots when multiple images exist (arrows stay hidden) */ 7397.cart-kit-micro-pdp .swiper-button-prev, 7398.cart-kit-micro-pdp .swiper-button-next, 7399.cart-kit-micro-pdp .swiper-scrollbar {
7400display: none !important; 7401} 7402/* Dots ON (we'll hide them via JS when only 1 image) */ 7403.cart-kit-micro-pdp .swiper-pagination { 7404display: flex !important; 7405justify-content: center !important; 7406gap: 6px !important; 7407margin-top: 6px !important; 7408position: relative !important; 7409} 7410/* Make dots smaller + subtle so they don't add height */ 7411.cart-kit-micro-pdp .swiper-pagination-bullet { 7412width: 6px !important; 7413height: 6px !important; 7414opacity: 0.35 !important; 7415} 7416.cart-kit-micro-pdp .swiper-pagination-bullet-active { 7417opacity: 0.9 !important; 7418} 7419/* Tighten spacing under preview */ 7420.cart-kit-micro-pdp .product.media, 7421.cart-kit-micro-pdp .product-image-carousel-container, 7422.cart-kit-micro-pdp .product-image-carousel-select, 7423.cart-kit-micro-pdp .gallery-placeholder { 7424margin-bottom: 64px !important; 7425} 7426/* 4) Form focus */ 7427#product_addtocart_form, .product-add-form { 7428margin-top: 10px !important; 7429} 7430/* 5) Hide âExtra bottle of ink?â field (best effort selectors) 7431Note: :has() may not be supported everywhere; we also keep the JS fallback below */ 7432.field.extra_ink, .field.extra-ink, .field.extra_bottle_ink, 7433.product-options-wrapper .field:has(select[name*="extra"]) { 7434display: none !important; 7435} 7436.full-size-container{ 7437display:none; 7438} 7439`; 7440// Prevent double-inject if iframe reloads quickly 7441var existing = doc.getElementById('ck-micro-css'); 7442if (existing && existing.parentNode) existing.parentNode.removeChild(existing); 7443doc.head.appendChild(style); 7444// Force add-to-cart form to submit in TOP window so we return to cart normally 7445var form = 7446doc.querySelector('#product_addtocart_form') || 7447doc.querySelector('form[action*="checkout/cart/add"]'); 7448if (form) { 7449form.setAttribute('target', '_top'); 7450// Ensure return_url points back to cart (best effort) 7451var existingReturn = form.querySelector('input[name="return_url"]'); 7452if (!existingReturn) { 7453existingReturn = doc.createElement('input'); 7454existingReturn.type = 'hidden'; 7455existingReturn.name = 'return_url'; 7456form.appendChild(existingReturn); 7457} 7458existingReturn.value = (MICRO_PDP.returnUrl || window.location.href); 7459// Some Magento setups use return_url_encoded; set it if present 7460var encodedReturn = form.querySelector('input[name="return_url_encoded"]'); 7461if (encodedReturn) { 7462try { 7463encodedReturn.value = btoa(unescape(encodeURIComponent(MICRO_PDP.returnUrl || window.location.href))); 7464} catch (e) { } 7465} 7466// If there's a quantity input, default to 1 7467var qty = form.querySelector('input[name="qty"]'); 7468if (qty && !qty.value) qty.value = '1'; 7469// ---- LOADING CURSOR HOOK (reliable) ---- 7470try { 7471// Zephyr's ATC is id="product-addtocart-button" and type="button", 7472// so look for it FIRST -- a bare button[type="submit"] lookup can 7473// match an unrelated submit (search / newsletter) earlier in the DOM. 7474var btn = 7475doc.querySelector('button#product-addtocart-button') || 7476form.querySelector('button[type="submit"], input[type="submit"]') || 7477doc.querySelector('button.addtocart') || 7478doc.querySelector('button.action.tocart') || 7479doc.querySelector('button[type="submit"]') || 7480doc.querySelector('input[type="submit"]'); 7481if (btn && !btn.__ckBound) { 7482btn.__ckBound = true; 7483btn.addEventListener('click', function () { 7484// NOTE: arming happens in the handleAjaxAddToCart 7485// wrapper, not here -- the button the shopper actually 7486// clicks is the sticky bar's, which lives outside 7487// #product_addtocart_form and may not be this element. 7488setLoadingCursor(true); 7489if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); 7490}, { capture: true }); 7491} 7492if (!form.__ckBound) { 7493form.__ckBound = true; 7494form.addEventListener('submit', function () { 7495setLoadingCursor(true); 7496if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); 7497}, { capture: true }); 7498} 7499} catch (e) { 7500// If iframe isn't accessible due to cross-domain, this will throw. 7501} 7502} 7503// Hook the iframe's own cart store so the side cart cannot open in 7504// here, and so the AJAX add gives us a reliable success signal. 7505try { 7506var ckSrc = String(MICRO_PDP.iframe.getAttribute('src') || ''); 7507if (ckSrc && ckSrc.indexOf('about:blank') !== 0) bindMicroPdpAddBridge(); 7508} catch (e) { } 7509// ---------------------------------------------------- 7510// Cut everything below the shipping block (keep shipping visible) 7511// Anchor: .ships-and-made-in-usa-container 7512// ---------------------------------------------------- 7513(function () { 7514var d = MICRO_PDP.iframe.contentDocument; 7515if (!d) return; 7516var shipContainer = d.querySelector('.ships-and-made-in-usa-container'); 7517if (!shipContainer) return;
7518// Find a reasonable scope that contains the PDP âcontentâ sections 7519var scope = 7520shipContainer.closest('.vpt-product, .product-shop-content, .product-info-main, .column.main, main') 7521|| d.body; 7522// We want to hide everything AFTER the shipping container's row 7523var row = shipContainer.closest('.row') || shipContainer.parentElement; 7524if (!row) return; 7525var sib = row.nextElementSibling; 7526while (sib) { 7527sib.style.display = 'none'; 7528sib = sib.nextElementSibling; 7529} 7530})(); 7531// ---------------------------------------------------- 7532// Hide âExtra bottle of ink?â by label text (fallback) 7533// ---------------------------------------------------- 7534(function () { 7535var d = MICRO_PDP.iframe.contentDocument; 7536if (!d) return; 7537var labels = Array.from(d.querySelectorAll('label')); 7538var inkLabel = labels.find(function (l) { 7539return (l.textContent || '').toLowerCase().includes('extra bottle of ink'); 7540}); 7541if (!inkLabel) return; 7542var field = inkLabel.closest('.field, .control, .admin__field, .product-options-wrapper') || inkLabel.parentElement; 7543if (field) field.style.display = 'none'; 7544})(); 7545// Done loading 7546if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 7547} catch (err) { 7548// If cross-domain or blocked, just hide loader after a beat 7549if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 7550} 7551}); 7552// ---------------------------------------------------- 7553// MICRO-PDP ADD BRIDGE (added 2026-09-11) 7554// Zephyr's PDP add-to-cart control is 7555// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // It never submits the form, so the form.target='_top' hook further down -- 7556// which used to be the ONLY way the parent learned an add had happened -- is 7557// inert on a Zephyr PDP. The add is AJAX, and on success handleAjaxAddToCart 7558// calls Alpine.store('cart').openDrawer() INSIDE this iframe: the side cart 7559// slides out within the 960px modal and the parent cart page never updates. 7560// 7561// Fix: the iframe is same-origin, so we replace openDrawer() on the iframe's 7562// own cart store. That call is our exact "add succeeded" signal (it runs only 7563// in handleAjaxAddToCart's success branch) and the drawer never renders in the 7564// frame. We then reproduce the receipt state the old reload used to deliver in 7565// the URL -- via history.replaceState instead of navigating -- so the whole 7566// existing downstream pipeline (tile -> "added", tile swap, multi-SKU chaining) 7567// keeps working untouched. form.target='_top' stays as a fallback for any PDP 7568// that still submits natively. 7569// ---------------------------------------------------- 7570var MICRO_BRIDGE = { 7571timer: null, loadTimer: null, tries: 0, 7572armed: false, handled: false, 7573addPatched: false, storePatched: false, 7574countBefore: null 7575}; 7576// Zephyr exposes a running cart count at /zephyr/cart/count/ (verified live: 7577// returns {"count": N} where N is total UNITS, not lines -- so a qty bump on an 7578// existing line still moves it). Authoritative fallback for when the in-frame 7579// success signal never arrives, so a miss degrades to a late receipt, not silence. 7580function ckCartCount() { 7581try { 7582return fetch('/zephyr/cart/count/', { 7583credentials: 'same-origin', 7584headers: { 'X-Requested-With': 'XMLHttpRequest' } 7585}) 7586.then(function (r) { 7587if (!r.ok) throw new Error('cart count HTTP ' + r.status); 7588return r.json(); 7589}) 7590.then(function (j) { return (j && typeof j.count === 'number') ? j.count : null; }) 7591.catch(function (err) { 7592try { console.warn('[SmartCart] /zephyr/cart/count/ unavailable:', err && err.message); } catch (e) { } 7593return null; 7594}); 7595} catch (e) { 7596return Promise.resolve(null); 7597} 7598} 7599// A 200 from /checkout/cart/add is NOT proof the cart changed: OpenMage 7600// answers a bare add for a product it can't sell (missing from this store 7601// view, disabled, required options) with a 200 and an empty result, and 7602// postToCart() only throws on !res.ok. Without this the module shows a full 7603// success state -- drawer, toast, tile swap -- over an unchanged cart. 7604function ckVerifySimpleAdd(baseline) { 7605if (typeof baseline !== 'number') return; // no baseline: stay optimistic 7606setTimeout(function () { 7607ckCartCount().then(function (after) { 7608if (after === null || after > baseline) return; // added, or can't tell 7609try { console.warn('[SmartCart] add returned 200 but the cart did not change (baseline ' + baseline + ').'); } catch (e) { } 7610var msg = 'That item could not be added \u2014 it may be unavailable. Please try another.'; 7611try { if (statusEl) statusEl.textContent = msg; } catch (e) { } 7612try { 7613var fl = (window.Alpine && typeof window.Alpine.store === 'function') ? window.Alpine.store('flash') : null; 7614if (fl && typeof fl.show === 'function') fl.show('error', msg); 7615} catch (e) { } 7616// Don't leave a side cart open over an unchanged cart. 7617try { 7618var st = ckCartStore(window); 7619if (st && typeof st.closeDrawer === 'function') st.closeDrawer(); 7620} catch (e) { } 7621}); 7622}, 600); 7623} 7624// Clears the visual loading state ONLY. It must never disarm: the add can 7625// still be in flight, and disarming would make the openDrawer success signal
7626// get swallowed when it finally arrives. 7627function clearMicroLoadingState() { 7628try { setLoadingCursor(false); } catch (e) { } 7629try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { } 7630} 7631// Ask the server whether the add actually landed. Callers decide when it's 7632// worth asking; this only refuses to act if a receipt already fired. 7633// Polls rather than asking once. handleAjaxAddToCart returns a BOOLEAN, not a 7634// promise (measured on local 2026-09-14), so there is no completion callback -- 7635// a real add on this catalog takes ~3.5s. A single check right after the click 7636// always misses, and treating that miss as failure is what left the modal open. 7637function confirmMicroAddByCount(reason, attemptsLeft) { 7638if (MICRO_BRIDGE.handled) return; 7639if (typeof attemptsLeft !== 'number') attemptsLeft = 0; 7640if (MICRO_BRIDGE.countBefore === null) { 7641try { console.warn('[SmartCart] cannot confirm micro-PDP add, no cart-count baseline (' + reason + ').'); } catch (e) { } 7642clearMicroLoadingState(); 7643return; 7644} 7645var baseline = MICRO_BRIDGE.countBefore; 7646ckCartCount().then(function (after) { 7647if (MICRO_BRIDGE.handled) return; 7648if (after !== null && after > baseline) { 7649try { console.warn('[SmartCart] micro-PDP add confirmed by cart count (' + reason + ').'); } catch (e) { } 7650try { onMicroPdpAddSucceeded(); } catch (e) { } 7651return; 7652} 7653if (attemptsLeft > 0) { 7654setTimeout(function () { 7655confirmMicroAddByCount(reason, attemptsLeft - 1); 7656}, 1200); 7657return; 7658} 7659// Out of attempts. Drop the spinner but STAY ARMED -- if the add is 7660// simply slow, openDrawer will still arrive and fire the receipt. 7661clearMicroLoadingState(); 7662}); 7663} 7664function resetMicroBridge() { 7665try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7666try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7667MICRO_BRIDGE.timer = null; 7668MICRO_BRIDGE.loadTimer = null; 7669MICRO_BRIDGE.tries = 0; 7670MICRO_BRIDGE.armed = false; 7671MICRO_BRIDGE.addPatched = false; 7672MICRO_BRIDGE.storePatched = false; 7673// `handled` is deliberately NOT cleared -- closeMicroPdp() runs as part of a 7674// successful receipt. openMicroPdp() clears it, so the guard really is one 7675// receipt per configurator open. 7676} 7677// Arm on the ACTUAL add call, not a button click. Validation runs inside 7678// handleAjaxAddToCart, so armed only means "an add was attempted". 7679function armMicroBridge() { 7680MICRO_BRIDGE.armed = true; 7681try { setLoadingCursor(true); } catch (e) { } 7682try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); } catch (e) { } 7683// Long backstop only. The real resolution is onMicroAddSettled(), which 7684// fires the moment handleAjaxAddToCart's own promise settles -- so a slow 7685// upload resolves when it lands rather than being cut off by a fixed timer. 7686try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7687MICRO_BRIDGE.loadTimer = setTimeout(function () { 7688if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return; 7689confirmMicroAddByCount('60s backstop', 0); 7690}, 60000); 7691} 7692// handleAjaxAddToCart returns its fetch promise. When it settles the add is 7693// decided: either our openDrawer signal already fired (handled), or it did not 7694// and we resolve it against the server instead of waiting out the backstop. 7695function onMicroAddSettled() { 7696// ~10s of polling (500ms + 8 x 1200ms), which comfortably covers a real 7697// add. The openDrawer signal normally wins long before this; this only 7698// catches an add that completes without ever reaching the Alpine store. 7699setTimeout(function () { 7700if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return; 7701confirmMicroAddByCount('no in-frame signal yet', 8); 7702}, 500); 7703} 7704function bindMicroPdpAddBridge() { 7705try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7706MICRO_BRIDGE.timer = null; 7707MICRO_BRIDGE.tries = 0; 7708MICRO_BRIDGE.addPatched = false; 7709MICRO_BRIDGE.storePatched = false; 7710// Two hooks, both inside the iframe, both same-origin: 7711// 1. window.handleAjaxAddToCart -- wrapped to ARM on a real add call. 7712// This is the pattern ss-tier-upsell-zephyr.html v1.32 proved out; 7713// binding a button click is what it proved unreliable. 7714// 2. Alpine store cart.openDrawer -- the SUCCESS signal. Zephyr calls 7715// it only in handleAjaxAddToCart's success branch, and overriding it 7716// also keeps the side cart from rendering inside the 960px modal. 7717// handleAjaxAddToCart is an inline PDP script so it lands well before 7718// Alpine boots; poll for each independently. 7719MICRO_BRIDGE.timer = setInterval(function () { 7720MICRO_BRIDGE.tries++; 7721var w = null; 7722try { w = MICRO_PDP.iframe && MICRO_PDP.iframe.contentWindow; } catch (e) { w = null; } 7723if (!w) return; 7724if (!MICRO_BRIDGE.addPatched) { 7725try { 7726if (typeof w.handleAjaxAddToCart === 'function') { 7727if (w.handleAjaxAddToCart.__ckWrapped) { 7728MICRO_BRIDGE.addPatched = true; 7729} else { 7730var origAdd = w.handleAjaxAddToCart; 7731var wrapped = function () { 7732try { armMicroBridge(); } catch (eA) { } 7733var r; 7734try { 7735r = origAdd.apply(this, arguments); 7736} catch (eR) { 7737// Do NOT disarm here: origAdd may have dispatched 7738// the fetch before throwing, so the add can still 7739// land. Resolve it against the server like any other. 7740try { console.warn('[SmartCart] handleAjaxAddToCart threw:', eR && eR.message); } catch (eW) { } 7741try { onMicroAddSettled(); } catch (eS) { } 7742throw eR; 7743} 7744// The precise completion signal -- see onMicroAddSettled. 7745try { 7746// NOTE: on this catalog handleAjaxAddToCart returns a 7747// BOOLEAN, so the thenable branch is the exception, not 7748// the rule -- onMicroAddSettled() must therefore poll. 7749if (r && typeof r.then === 'function') r.then(onMicroAddSettled, onMicroAddSettled); 7750else onMicroAddSettled(); 7751} catch (eT) { } 7752return r; 7753}; 7754wrapped.__ckWrapped = true; 7755// The ATC uses an inline onclick, which resolves the 7756// global at call time -- so replacing it is enough. 7757w.handleAjaxAddToCart = wrapped; 7758MICRO_BRIDGE.addPatched = true; 7759} 7760} 7761} catch (eP1) { } 7762} 7763if (!MICRO_BRIDGE.storePatched) { 7764var st = ckCartStore(w); 7765if (st) { 7766try { 7767if (!st.__ckMicroPatched) { 7768st.__ckMicroPatched = true; 7769var suppress = function () { 7770// The side cart must never render in the configurator. 7771try { st.drawerOpen = false; } catch (e1) { } 7772try { 7773if (w && w.document && w.document.body) w.document.body.style.overflow = ''; 7774} catch (e2) { } 7775if (!MICRO_BRIDGE.armed) return; 7776try { onMicroPdpAddSucceeded(); } catch (e3) { } 7777}; 7778st.openDrawer = suppress; 7779st.toggleDrawer = suppress; 7780} 7781MICRO_BRIDGE.storePatched = true; 7782} catch (eP2) { } 7783} 7784} 7785if (MICRO_BRIDGE.addPatched && MICRO_BRIDGE.storePatched) { 7786try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7787MICRO_BRIDGE.timer = null; 7788return; 7789} 7790if (MICRO_BRIDGE.tries > 120) { // ~6s 7791try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7792MICRO_BRIDGE.timer = null;
7793// Never give up silently -- that is how this class of bug hides. 7794try { 7795console.warn('[SmartCart] micro-PDP bridge gave up after 6s.', 7796'handleAjaxAddToCart patched:', MICRO_BRIDGE.addPatched, 7797'| cart store patched:', MICRO_BRIDGE.storePatched, 7798'| the count fallback still covers the add.'); 7799} catch (eW) { } 7800} 7801}, 50); 7802} 7803function onMicroPdpAddSucceeded() { 7804if (MICRO_BRIDGE.handled) return; // one receipt per configurator open 7805MICRO_BRIDGE.handled = true; 7806MICRO_BRIDGE.armed = false; 7807try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7808// Recreate, in the parent's own URL, the receipt state the old 7809// target="_top" reload used to deliver -- without navigating. 7810try { 7811if (MICRO_PDP.returnUrl) { 7812var ru = new URL(MICRO_PDP.returnUrl, window.location.origin); 7813window.history.replaceState({}, document.title, ru.pathname + ru.search + ru.hash); 7814} 7815} catch (e) { } 7816// If more hard SKUs are queued the next configurator opens in ~350ms -- 7817// don't pop the side cart underneath it. Reveal it only on the last one. 7818var moreQueued = false; 7819try { moreQueued = parseRemainingHardFromUrl().length > 0; } catch (e) { } 7820try { setLoadingCursor(false); } catch (e) { } 7821closeMicroPdp('added'); 7822ckAfterCartMutation(!moreQueued); 7823// Existing receipt pipeline: added-state on the tile, tile swap, strip the 7824// params, and chain the next hard SKU if one is queued. 7825try { handleMicroAddedFromQuery(); } catch (e) { } 7826} 7827function openMicroPdp(productUrl, titleText, productId, remainingHardIds, progressDone, progressTotal) { 7828ensureMicroPdpModal(); 7829MICRO_PDP.lastUrl = productUrl; 7830MICRO_PDP.lastProductId = String(productId || ''); 7831// Progress defaults (if not passed) 7832var progFromUrl = getMicroProgressFromUrl(); 7833if (typeof progressDone !== 'number') progressDone = progFromUrl.done; 7834if (typeof progressTotal !== 'number') progressTotal = progFromUrl.total; 7835MICRO_PDP.progressDone = progressDone || 0; 7836MICRO_PDP.progressTotal = progressTotal || 0; 7837// Paint header progress bar (only shows when total > 1) 7838renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal); 7839// Build a "receipt" return URL so the parent can show success + grey out tile + scroll 7840try { 7841var u = new URL(window.location.href); 7842// Set receipt params (overwrite if they already exist) 7843u.searchParams.set('microAdded', MICRO_PDP.lastProductId); 7844u.searchParams.set('microAddedName', titleText || ''); 7845// Carry micro progress counts 7846if (progressTotal && progressTotal > 0) { 7847u.searchParams.set('microTotal', String(progressTotal)); 7848u.searchParams.set('microDone', String(progressDone || 0)); 7849} else { 7850u.searchParams.delete('microTotal'); 7851u.searchParams.delete('microDone'); 7852} 7853// If we are chaining multiple HARD SKUs, persist the remaining list 7854if (Array.isArray(remainingHardIds) && remainingHardIds.length) { 7855u.searchParams.set('remainingHard', remainingHardIds.join(',')); 7856} else { 7857u.searchParams.delete('remainingHard'); 7858} 7859// Keep anchor consistent so user lands near module area 7860u.hash = 'cart-kit-upsell'; 7861MICRO_PDP.returnUrl = u.toString(); 7862} catch (e) { 7863// Fallback if URL API fails 7864MICRO_PDP.returnUrl = window.location.href + (window.location.search ? '&' : '?') 7865+ 'microAdded=' + encodeURIComponent(MICRO_PDP.lastProductId) 7866+ 'µAddedName=' + encodeURIComponent(titleText || '') 7867+ '#cart-kit-upsell'; 7868} 7869var titleEl = MICRO_PDP.overlay.querySelector('#ck-micropdp-title'); 7870if (titleEl) titleEl.textContent = titleText || 'Customize item'; 7871if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'flex'; 7872// Add a cache-buster so iframe refreshes if same SKU reopened 7873var url = ckSameOrigin(productUrl); 7874url += (url.indexOf('?') > -1 ? '&' : '?') + 'cartUpsellMicro=1&_=' + Date.now(); 7875// Fresh receipt state + a count snapshot for the fallback in armMicroBridge. 7876MICRO_BRIDGE.handled = false; 7877MICRO_BRIDGE.countBefore = null; 7878MICRO_BRIDGE.countGen = (MICRO_BRIDGE.countGen || 0) + 1; 7879try { 7880// Tagged so a slow response from the PREVIOUS chain item (opens ~350ms 7881// apart) can't land here and produce a false-positive receipt. 7882(function (gen) { 7883ckCartCount().then(function (n) { 7884if (gen === MICRO_BRIDGE.countGen) MICRO_BRIDGE.countBefore = n; 7885}); 7886})(MICRO_BRIDGE.countGen); 7887} catch (e) { }
7888MICRO_PDP.iframe.src = url; 7889MICRO_PDP.overlay.classList.add('is-open'); 7890pushUpsellEvent('SmartCart_upsell_micro_pdp_open', { url: productUrl }); 7891} 7892function closeMicroPdp(reason) { 7893if (!MICRO_PDP.overlay) return; 7894var added = (reason === 'added'); 7895var ckWasArmed = MICRO_BRIDGE.armed; 7896try { resetMicroBridge(); } catch (e) { } 7897// Shopper closed while an add was in flight. Don't drop it on the floor -- 7898// check the server before discarding the tile state and the chain. 7899if (!added && ckWasArmed) { 7900try { confirmMicroAddByCount('closed mid-add', 3); } catch (e) { } 7901} 7902try { setLoadingCursor(false); } catch (e) { } 7903try { MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { } 7904MICRO_PDP.overlay.classList.remove('is-open'); 7905// Stop network / reset iframe 7906if (MICRO_PDP.iframe) MICRO_PDP.iframe.src = 'about:blank'; 7907if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 7908// Tell anything listening that the micro PDP closed 7909try { 7910document.dispatchEvent(new CustomEvent('smartcart:microPdpClosed', { detail: { reason: reason || 'close' } })); 7911} catch (e) { } 7912// CRITICAL: refresh cart UI and un-stick the SmartCart state 7913Promise.resolve() 7914.then(function () { 7915// Force the same refresh path we use after adds 7916// 1) Reload Magento customer-data cart section (minicart + cart counters) 7917// 2) Refresh cart DOM fragments (table/totals) if weâre on the cart page 7918// Legacy (pre-Zephyr) refresh path -- a no-op on Zephyr, kept for safety. 7919return reloadCustomerDataCart() 7920.then(refreshCartDomIfOnCartPage) 7921.then(function () { 7922// After an add the caller syncs; on a plain close re-sync 7923// anyway in case the shopper added inside the configurator 7924// by some other route. 7925if (!added) ckAfterCartMutation(false); 7926}); 7927}) 7928.then(function () { 7929// reset CTA back to normal (user cancelled hard sku) 7930try { 7931addBtn.classList.remove('is-loading'); 7932// This tail resolves after handleMicroAddedFromQuery() has run. 7933// On a SUCCESSFUL add the receipt pipeline owns the status line, 7934// the progress bar and the tile grid -- buildGrid() in particular 7935// would re-render over replaceAddedTileWithNext()'s swap. Touch 7936// none of it unless the shopper cancelled. 7937if (!added) { 7938addBtn.disabled = false; 7939addBtn.textContent = 'Add to Cart'; 7940if (statusEl) statusEl.textContent = 'No worries â you can keep shopping or add something else.'; 7941resetAddProgressUI(); 7942} 7943// Safe on BOTH paths and required on the added one: derives the 7944// CTA's disabled state and label from the live selection, so a 7945// completed add leaves it correctly disabled rather than enabled 7946// with nothing selected. 7947updatePriceUI(); 7948} catch (e) { } 7949// Rebuild tiles so "already in cart" suppression is correct 7950if (!added) { try { buildGrid(); } catch (e) { } } 7951}) 7952.catch(function () { 7953// Even if refresh fails, never leave the UI stuck 7954try { 7955addBtn.disabled = false; 7956addBtn.classList.remove('is-loading'); 7957addBtn.textContent = 'Add to Cart'; 7958} catch (e) { } 7959}); 7960} 7961// ---------------------------------------------------- 7962// 8) Selection state 7963// ---------------------------------------------------- 7964var selectedPrimaryIds = new Set(); // stamp IDs OR notary product IDs (strings) 7965var selectedInkColors = new Set(); 7966var inkChoiceMade = false; 7967// Track tiles that have been added via Micro PDP (so we can grey them out) 7968var microAddedIds = new Set(); 7969var ckSeenTileIds = new Set(); 7970// Defaults 7971var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0); 7972if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId)); 7973else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id); 7974// ---------------------------------------------------- 7975// 9) Build grid (Accent or Notary) 7976// ---------------------------------------------------- 7977var NOTARY_HYDRATED = new Map(); // productId -> {productId, productUrl, name, img, price} 7978function buildGrid() { 7979// Build cart signals for duplicate suppression (4C) 7980var __cartPaths = Object.create(null); 7981try { __cartPaths = getCartProductPathSet(); } catch (eCP0) { } 7982stampGrid.innerHTML = ''; 7983// Accent stamps (unchanged) 7984if (!useTiles) { 7985for (var i = 0; i < STAMP_OPTIONS.length; i++) { 7986var opt = STAMP_OPTIONS[i]; 7987var card = document.createElement('button'); 7988card.type = 'button'; 7989card.className = 'kit-stamp-card'; 7990card.setAttribute('data-kit-primary', opt.id); 7991card.innerHTML = 7992'<div class="kit-stamp-chip">' 7993+ '<div class="kit-stamp-chip__circle">' 7994+ '<img class="kit-stamp-chip__image"' 7995+ ' src="' + opt.imgSrc + '"' 7996+ ' alt="' + (opt.imgAlt || '') + '"' 7997+ (opt.imgStyle ? ' style="' + opt.imgStyle + '"' : '') 7998+ '/>' 7999+ '</div>' 8000+ '</div>'; 8001if (selectedPrimaryIds.has(opt.id)) card.classList.add('is-selected'); 8002stampGrid.appendChild(card); 8003} 8004// Step 1: sync tile state store after grid render 8005try { TileStore.initFromDom(mount); } catch (e) { } 8006return; 8007} 8008// Notary product tiles (PDP fetched) 8009// Empty-state: if everything is already in cart (or filtered), show "All set" instead of a blank module. 8010if (!TILE_ITEMS || TILE_ITEMS.length === 0) { 8011// Nothing to build. (The old code here appended an "all set" note next 8012// to an undefined `tilesGrid` and tried to disable a #kit-add-btn that 8013// has never existed in this markup -- both silent no-ops.) 8014ckSyncModuleEmptyState(); 8015return; 8016} 8017for (var j = 0; j < TILE_ITEMS.length; j++) { 8018(function (raw) { 8019var pid = String(raw.productId || ''); 8020// Keep a reference so we can apply persona rules later (micro PDP gating, etc.) 8021TILE_ITEM_BY_ID[pid] = raw; 8022var card = document.createElement('button'); 8023card.type = 'button'; 8024card.className = 'kit-stamp-card kit-product-card'; 8025card.setAttribute('data-kit-primary', pid); 8026card.setAttribute('data-sku', pid); 8027card.setAttribute('data-kit-url', raw.productUrl || ''); 8028if (microAddedIds.has(pid)) { 8029card.classList.add('is-added'); 8030} 8031card.innerHTML = 8032'<div class="kit-product-thumb-tile"><span style="font-size:12px;color:#888;">Loadingâ¦</span></div>' 8033+ '<div class="kit-product-meta">' 8034+ '<p class="kit-product-name">Loadingâ¦</p>' 8035+ '<div class="kit-product-price">â</div>' 8036+ '<div class="kit-product-sub">' + (persona.tileSubtext || '') + '</div>' 8037+ '</div>'; 8038if (selectedPrimaryIds.has(pid)) card.classList.add('is-selected'); 8039stampGrid.appendChild(card); 8040fetchPdpData(raw.productUrl).then(function (data) { 8041// Guard: this tile may have been swapped; ignore stale hydration 8042if (String(card.getAttribute('data-kit-primary') || '') !== String(pid)) return; 8043// Quality gate: this URL did not resolve to a real product page, 8044// so do NOT render whatever came back. Substitute an approved 8045// alternative the shopper doesn't already have, else drop the slot. 8046if (!ckIsUsableTileData(data)) { 8047try { console.warn('[SmartCart] tile ' + pid + ' did not resolve to a product page; substituting.'); } catch (eW) { } 8048var alt = ckPickFallbackTile(); 8049if (alt) ckRepaintCardAs(card, alt); 8050else ckDropTile(card); 8051return; 8052} 8053var hydrated = { 8054productId: pid, 8055productUrl: raw.productUrl, 8056name: data.name || 'Notary add-on', 8057img: data.img || null, 8058price: (data.price != null ? Number(data.price) : null) 8059}; 8060try { hydrated.sku = data.sku; } catch (eSk1) { } 8061NOTARY_HYDRATED.set(pid, hydrated); 8062var nameEl = card.querySelector('.kit-product-name'); 8063var priceEl = card.querySelector('.kit-product-price'); 8064var thumbEl = card.querySelector('.kit-product-thumb-tile'); 8065if (nameEl) nameEl.textContent = hydrated.name; 8066if (priceEl) priceEl.textContent = (hydrated.price != null ? ('$' + hydrated.price.toFixed(2)) : 'See price in cart'); 8067if (thumbEl) { 8068if (hydrated.img) thumbEl.innerHTML = '<img src="' + hydrated.img + '" alt="" />'; 8069else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 8070} 8071if (selectedPrimaryIds.has(pid)) updatePriceUI(); 8072}); 8073})(TILE_ITEMS[j]); 8074} 8075fireTilesImpressionOnce(); 8076// Step 1: sync tile state store after grid render 8077try { TileStore.initFromDom(mount); } catch (e) { } 8078ckSyncModuleEmptyState(); 8079} 8080// ---------------------------------------------------- 8081// 10) Pricing 8082// ---------------------------------------------------- 8083var priceLabelEl = upsell.querySelector('#kit-price-label'); 8084var priceNewEl = upsell.querySelector('#kit-price-new'); 8085function computeTotals() { 8086var inkCount = selectedInkColors.size; 8087if (!useTiles) { 8088var stampCount = selectedPrimaryIds.size; 8089return { 8090primaryCount: stampCount, 8091inkCount: inkCount, 8092total: (stampCount * STAMP_UNIT_PRICE) + (inkCount * INK_PRICE) 8093}; 8094} 8095// Notary: sum selected notary items (real PDP price if fetched; otherwise 0 until loaded) 8096var total = 0; 8097var count = 0; 8098selectedPrimaryIds.forEach(function (pid) { 8099var it = NOTARY_HYDRATED.get(String(pid)); 8100count += 1; 8101total += (it && it.price != null) ? Number(it.price) : 0; 8102}); 8103total += inkCount * INK_PRICE; 8104return { primaryCount: count, inkCount: inkCount, total: total }; 8105} 8106function updatePriceUI() { 8107var totals = computeTotals(); 8108if (!totals.primaryCount) { 8109priceLabelEl.textContent = 'Total:'; 8110priceNewEl.textContent = '$0.00'; 8111addBtn.disabled = true; 8112addBtn.textContent = 'Add to Cart'; 8113return; 8114} 8115priceLabelEl.textContent = 'Total:'; 8116priceNewEl.textContent = '$' + totals.total.toFixed(2); 8117// CTA label: if any selected item requires configuration, guide user into Micro PDP. 8118var hasHard = false;
8119try { 8120Array.from(selectedPrimaryIds).some(function (pid) { 8121var raw = (TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[String(pid)]) ? TILE_ITEM_BY_ID[String(pid)] : null; 8122if (requiresMicroPdp(pid, raw)) { hasHard = true; return true; } 8123return false; 8124}); 8125} catch (e) { } 8126addBtn.textContent = hasHard ? 'Customize & Add' : 'Add to Cart'; 8127addBtn.disabled = false; 8128} 8129// ---------------------------------------------------- 8130// 11) Build + init 8131// ---------------------------------------------------- 8132buildGrid(); 8133updatePriceUI(); 8134pushUpsellEvent('SmartCart_upsell_shown', { matched: activePersonaKey !== 'generic' }); 8135// ---------------------------------------------------- 8136// 12) Interactions 8137// ---------------------------------------------------- 8138header.addEventListener('click', function () { 8139var isOpen = upsell.classList.toggle('cart-kit-upsell--open'); 8140body.hidden = !isOpen; 8141}); 8142// Primary selection (stamps or notary items) 8143stampGrid.addEventListener('click', function (evt) { 8144var card = evt.target.closest('.kit-stamp-card'); 8145if (!card) return; if (card.classList && card.classList.contains('is-complete')) return; 8146var id = String(card.getAttribute('data-kit-primary') || ''); 8147if (!id) return; 8148// ---- TRACK: tile click ---- 8149try { 8150if (useTiles) { 8151// find tile position in TILE_ITEMS 8152var pos = -1; 8153var rawTile = null; 8154for (var i = 0; i < TILE_ITEMS.length; i++) { 8155if (String(TILE_ITEMS[i].productId) === String(id)) { 8156pos = i + 1; 8157rawTile = TILE_ITEMS[i]; 8158break; 8159} 8160} 8161var hydrated = NOTARY_HYDRATED.get(String(id)); 8162pushUpsellEvent('SmartCart_upsell_tile_click', { 8163persona: activePersonaKey || 'generic', 8164upsell_mode: 'tiles', 8165product_id: id, 8166position: pos, 8167product_name: hydrated && hydrated.name ? hydrated.name : undefined, 8168product_price: hydrated && hydrated.price != null ? hydrated.price : undefined, 8169product_url: rawTile && rawTile.productUrl ? rawTile.productUrl : undefined, 8170requires_micro: rawTile ? requiresMicroPdp(id, rawTile) : undefined, 8171already_selected: selectedPrimaryIds.has(id) 8172}); 8173} else { 8174// Accent stamp click 8175pushUpsellEvent('SmartCart_upsell_accent_click', { 8176stamp_id: id 8177}); 8178} 8179} catch (e) { } 8180// ---------------------------------------------------- 8181// Tile tracking + micro PDP routing (tiles only) 8182// ---------------------------------------------------- 8183var raw = null; 8184if (useTiles) { 8185for (var k = 0; k < TILE_ITEMS.length; k++) { 8186if (String(TILE_ITEMS[k].productId) === String(id)) { raw = TILE_ITEMS[k]; break; } 8187} 8188if (raw && raw.productUrl) { 8189// If we already know it's micro (tile config, hard override, or cached autodetect) 8190if (requiresMicroPdp(id, raw)) { 8191// Hard SKU: tile click should ONLY toggle selection. 8192// Do not open Micro PDP here; Micro PDP opens only from the main CTA. 8193} 8194// If we don't have PDP cached yet, fetch once and decide (airbag) 8195if (!PDP_CACHE.has(raw.productUrl)) { 8196evt.preventDefault(); 8197evt.stopPropagation(); 8198setLoadingCursor(true); 8199fetchPdpData(raw.productUrl).then(function (data) { 8200setLoadingCursor(false); 8201// If autodetect says "micro", open modal 8202if (data && data.requiresMicro) { 8203// NOTE: Do NOT auto-open Micro PDP on tile click. 8204// We only open Micro PDP when the user explicitly clicks the CTA to add. 8205pushUpsellEvent('SmartCart_upsell_tile_micro_detected', { productId: id, productUrl: raw.productUrl }); 8206} 8207// Otherwise proceed with normal select toggle now 8208var willSelect = !selectedPrimaryIds.has(id); 8209if (willSelect) { 8210selectedPrimaryIds.add(id); 8211card.classList.add('is-selected'); 8212} else { 8213selectedPrimaryIds.delete(id); 8214card.classList.remove('is-selected'); 8215} 8216pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), { 8217productId: id, 8218selected_count: selectedPrimaryIds.size 8219}); 8220updatePriceUI(); 8221}).catch(function () { 8222setLoadingCursor(false); 8223// On failure, fall back to normal select so UX doesn't dead-end 8224var willSelect2 = !selectedPrimaryIds.has(id); 8225if (willSelect2) { 8226selectedPrimaryIds.add(id); 8227card.classList.add('is-selected'); 8228} else { 8229selectedPrimaryIds.delete(id); 8230card.classList.remove('is-selected'); 8231} 8232pushUpsellEvent('SmartCart_upsell_tile_select_fallback', { 8233productId: id, 8234// This path fires on DESELECT as well as select. 8235// Without this flag the GA4 mirror would count a 8236// deselect as a select_item. 8237selected: willSelect2, 8238selected_count: selectedPrimaryIds.size 8239}); 8240updatePriceUI(); 8241}); 8242return; 8243} 8244} 8245} 8246var willSelect = !selectedPrimaryIds.has(id); 8247if (willSelect) { 8248selectedPrimaryIds.add(id); 8249card.classList.add('is-selected'); 8250} else { 8251selectedPrimaryIds.delete(id); 8252card.classList.remove('is-selected'); 8253} 8254if (useTiles) { 8255pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), { 8256productId: id, 8257selected_count: selectedPrimaryIds.size 8258}); 8259} else { 8260pushUpsellEvent('SmartCart_upsell_accent_' + (willSelect ? 'selected' : 'deselected'), { 8261stamp_id: id, 8262selected_count: selectedPrimaryIds.size 8263}); 8264} 8265updatePriceUI(); 8266}); 8267// Ink multi-select with "none" 8268if (swatchList) { 8269swatchList.addEventListener('click', function (evt) { 8270var swatch = evt.target.closest('.kit-swatch'); 8271if (!swatch) return; 8272if (swatch.style.display === 'none') return; 8273var colorKey = swatch.getAttribute('data-kit-color'); 8274inkChoiceMade = true;
8275if (colorKey === 'none') { 8276selectedInkColors.clear(); 8277var all = swatchList.querySelectorAll('.kit-swatch'); 8278for (var i = 0; i < all.length; i++) { 8279var s = all[i]; 8280if (s.style.display === 'none') continue; 8281s.classList.toggle('is-selected', s.getAttribute('data-kit-color') === 'none'); 8282} 8283} else { 8284if (selectedInkColors.has(colorKey)) selectedInkColors.delete(colorKey); 8285else selectedInkColors.add(colorKey); 8286var hasAnyInk = selectedInkColors.size > 0; 8287var all2 = swatchList.querySelectorAll('.kit-swatch'); 8288for (var j = 0; j < all2.length; j++) { 8289var b = all2[j]; 8290if (b.style.display === 'none') continue; 8291var key = b.getAttribute('data-kit-color'); 8292if (key === 'none') b.classList.toggle('is-selected', !hasAnyInk); 8293else b.classList.toggle('is-selected', selectedInkColors.has(key)); 8294} 8295} 8296updatePriceUI(); 8297}); 8298} 8299if (hideBtn) { 8300hideBtn.addEventListener('click', function (evt) { 8301evt.preventDefault(); 8302upsell.classList.remove('cart-kit-upsell--open'); 8303body.hidden = true; 8304upsell.scrollIntoView({ behavior: 'smooth', block: 'start' }); 8305}); 8306} 8307// ---------------------------------------------------- 8308// 13) Add-to-cart progress bar helpers (ONLY progress bar) 8309// ---------------------------------------------------- 8310function resetAddProgressUI() { 8311if (progressWrap) progressWrap.style.display = 'none'; 8312if (progressFill) progressFill.style.width = '0%'; 8313if (progressPct) progressPct.textContent = '0%'; 8314if (progressText) progressText.textContent = ''; 8315} 8316function setAddProgress(done, total) { 8317if (!progressWrap || !progressFill) return; 8318var pct = total ? Math.round((done / total) * 100) : 0; 8319progressWrap.style.display = ''; 8320progressFill.style.width = pct + '%'; 8321if (progressPct) progressPct.textContent = pct + '%'; 8322if (progressText) progressText.textContent = 'Adding ' + done + ' of ' + total + ' itemsâ¦'; 8323if (statusEl) statusEl.textContent = 'Added ' + done + ' of ' + total + ' items'; 8324} 8325// ---------------------------------------------------- 8326// 14) Add to Cart 8327// ---------------------------------------------------- 8328function postToCart(params) { 8329return fetch('/checkout/cart/add', { 8330method: 'POST', 8331headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 8332body: params.toString(), 8333credentials: 'same-origin' 8334}).then(function (res) { 8335if (!res.ok) throw new Error('Add to cart failed: ' + res.status); 8336return res; 8337}); 8338} 8339// ---------------------------------------------------- 8340// Cart sync helpers (run after async adds so UI + tile logic are up-to-date) 8341// ---------------------------------------------------- 8342function reloadCustomerDataCart() { 8343// Zephyr: /customer/section/load/ 404s on this build (no M2 customer-data) 8344// and the Alpine store is the real cart state. Skip the hop entirely. 8345if (ckCartStore(window)) return Promise.resolve(); 8346return new Promise(function (resolve) { 8347try { 8348if (window.require) { 8349window.require(['Magento_Customer/js/customer-data'], function (customerData) { 8350try { 8351var r = customerData.reload(['cart'], true); 8352if (r && r.always) return r.always(function () { resolve(); }); 8353resolve(); 8354} catch (e) { resolve(); } 8355}, function () { resolve(); }); 8356return; 8357} 8358fetch('/customer/section/load/?sections=cart&force_new_section_timestamp=true', { 8359credentials: 'same-origin' 8360}).then(function () { resolve(); }).catch(function () { resolve(); }); 8361} catch (e) { resolve(); } 8362}); 8363} 8364function refreshCartDomIfOnCartPage() { 8365// Zephyr: the cart page is Alpine-rendered. This swaps innerHTML on 8366// .cart-summary / [data-block="cart-summary"] if either matches, which 8367// would destroy live Alpine bindings in the Order Summary -- and it costs 8368// a full-page HTML fetch every time. ckAfterCartMutation() is the Zephyr 8369// path; this one is for the legacy theme only. 8370if (ckCartStore(window)) return Promise.resolve(); 8371return new Promise(function (resolve) { 8372try { 8373// Cart page fragments we attempt to refresh. Selectors vary by theme. 8374var table = document.querySelector('#shopping-cart-table'); 8375var totals = document.querySelector('#cart-totals') 8376|| document.querySelector('.cart-summary') 8377|| document.querySelector('.cart-totals') 8378|| document.querySelector('[data-block="cart-summary"]') 8379|| document.querySelector('[data-role="cart-totals"]'); 8380if (!table && !totals) return resolve(); 8381// Bust caches (some themes/CDNs will otherwise hand back stale totals) 8382var url = String(window.location.href || '').split('#')[0]; 8383url += (url.indexOf('?') > -1 ? '&' : '?') + 'ckts=' + Date.now(); 8384fetch(url, { credentials: 'same-origin', cache: 'no-store' }) 8385.then(function (r) { return r.text(); }) 8386.then(function (html) { 8387try { 8388var doc = new DOMParser().parseFromString(html, 'text/html'); 8389var newTable = doc.querySelector('#shopping-cart-table'); 8390if (table && newTable) table.innerHTML = newTable.innerHTML; 8391// Totals / summary fragment (try multiple fallbacks)
8392var newTotals = doc.querySelector('#cart-totals') 8393|| doc.querySelector('.cart-summary') 8394|| doc.querySelector('.cart-totals') 8395|| doc.querySelector('[data-block="cart-summary"]') 8396|| doc.querySelector('[data-role="cart-totals"]'); 8397if (totals && newTotals) totals.innerHTML = newTotals.innerHTML; 8398try { document.body.dispatchEvent(new Event('contentUpdated')); } catch (e) { } 8399} catch (e2) { } 8400resolve(); 8401}) 8402.catch(function () { resolve(); }); 8403} catch (e) { resolve(); } 8404}); 8405} 8406function syncCartAfterAdds() { 8407return reloadCustomerDataCart() 8408.then(refreshCartDomIfOnCartPage) 8409.catch(function () { /* silent */ }); 8410} 8411addBtn.addEventListener('click', function () { 8412if (!selectedPrimaryIds.size) { 8413alert(useTiles ? 'Please choose at least one add-on.' : 'Please choose at least one stamp design.'); 8414return; 8415} 8416if (swatchList && !inkChoiceMade) { 8417alert('Please select an ink pad color or select you already have an ink pad'); 8418return; 8419} 8420var formKeyInput = document.querySelector('input[name="form_key"]'); 8421var formKey = formKeyInput ? formKeyInput.value : null; 8422if (!formKey) { 8423console.error('Cart kit upsell: form_key not found.'); 8424alert('Sorry, we could not add the kit to your cart. Please refresh and try again.'); 8425return; 8426} 8427// Baseline for the post-add reality check (see ckVerifySimpleAdd). 8428var ckSimpleCountBefore = null; 8429try { ckCartCount().then(function (n) { ckSimpleCountBefore = n; }); } catch (eCB) { } 8430var primaryArr = Array.from(selectedPrimaryIds); 8431var inkArr = Array.from(selectedInkColors); 8432// ---------------------------------------------------- 8433// Hard SKU gating: 8434// - NEVER add "hard" products directly (they need options/uploads) 8435// - Only open Micro PDP when user clicks the CTA 8436// ---------------------------------------------------- 8437var hardPrimaryArr = []; 8438var simplePrimaryArr = []; 8439primaryArr.forEach(function (pid) { 8440var rawTile = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? TILE_ITEM_BY_ID[String(pid)] : null; 8441if (requiresMicroPdp(pid, rawTile)) { 8442hardPrimaryArr.push(pid); 8443} else { 8444simplePrimaryArr.push(pid); 8445} 8446}); 8447// Only count items we can actually add via POST chain (hard items are handled via Micro PDP) 8448var totalItemsCount = simplePrimaryArr.length + inkArr.length; 8449// If the user only selected hard items, we skip the POST chain and jump straight to Micro PDP. 8450var willOpenMicroAfterChain = hardPrimaryArr.length > 0; 8451pushUpsellEvent('SmartCart_upsell_add_clicked', { 8452primary_count: simplePrimaryArr.length, 8453hard_primary_count: hardPrimaryArr.length, 8454ink_count: inkArr.length, 8455total_count: totalItemsCount 8456}); 8457var completedCount = 0; 8458// If there's nothing to POST (only hard items), open Micro PDP immediately. 8459if (totalItemsCount === 0 && willOpenMicroAfterChain) { 8460var firstHard = hardPrimaryArr[0]; 8461var rawFirst = TILE_ITEM_BY_ID[String(firstHard)] || {}; 8462var hydratedFirst = NOTARY_HYDRATED.get(String(firstHard)); 8463var titleFirst = (hydratedFirst && hydratedFirst.name) ? hydratedFirst.name : (rawFirst.title || rawFirst.name || 'Customize item'); 8464// Seed Micro PDP progress for HARD-only flows 8465// done = 0 (nothing added yet), total = number of hard items selected 8466var progDone0 = 0; 8467var progTotal0 = hardPrimaryArr.length; 8468openMicroPdp(rawFirst.productUrl, titleFirst, firstHard, hardPrimaryArr.slice(1), progDone0, progTotal0); 8469return; 8470} 8471addBtn.disabled = true; 8472addBtn.classList.add('is-loading'); 8473addBtn.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' itemsâ¦') : 'Adding itemâ¦'; 8474if (statusEl) statusEl.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' items to your cartâ¦') : 'Adding item to your cartâ¦'; 8475// show + init progress bar 8476setAddProgress(0, totalItemsCount); 8477var chain = Promise.resolve(); 8478// Add primary selections 8479simplePrimaryArr.forEach(function (id) { 8480chain = chain.then(function () { 8481var params = new URLSearchParams(); 8482params.append('form_key', formKey); 8483if (!useTiles) { 8484params.append('product', id); 8485params.append('qty', '1'); 8486params.append('options[' + ACCENT_SIZE_OPTION_ID + '][]', ACCENT_SIZE_VALUE_ID); 8487params.append('options[' + ACCENT_INK_OPTION_ID + ']', ACCENT_INK_NO_THANKS_VALUE_ID); 8488} else { 8489params.append('product', id); 8490params.append('qty', '1'); 8491} 8492return postToCart(params).then(function () { 8493completedCount += 1; 8494setAddProgress(completedCount, totalItemsCount); 8495}); 8496}); 8497}); 8498// Add ink pads
8499inkArr.forEach(function (colorKey) { 8500chain = chain.then(function () { 8501var colorValueId = INK_COLOR_VALUES[colorKey]; 8502if (!colorValueId) throw new Error('Unknown ink color key: ' + colorKey); 8503var inkParams = new URLSearchParams(); 8504inkParams.append('form_key', formKey); 8505inkParams.append('product', INK_PARENT_ID); 8506inkParams.append('qty', '1'); 8507inkParams.append('options[' + INK_SIZE_OPTION_ID + ']', INK_SIZE_VALUE_ID); 8508inkParams.append('options[' + INK_COLOR_OPTION_ID + ']', colorValueId); 8509return postToCart(inkParams).then(function () { 8510completedCount += 1; 8511setAddProgress(completedCount, totalItemsCount); 8512}); 8513}); 8514}); 8515chain 8516.then(function () { 8517pushUpsellEvent('SmartCart_upsell_added', { 8518// Per-product ids drive the GA4 add_to_cart mirror. 8519product_ids: simplePrimaryArr.map(function (p) { return String(p); }), 8520ink_colors: inkArr.slice(), 8521primary_count: simplePrimaryArr.length, 8522hard_primary_count: hardPrimaryArr.length, 8523ink_count: inkArr.length, 8524total_count: totalItemsCount 8525}); 8526// CRITICAL: sync cart state + DOM BEFORE swap/dedupe logic runs 8527return syncCartAfterAdds().then(function () { 8528if (willOpenMicroAfterChain) { 8529// The simple items are already in the cart but the configurator 8530// is about to take over the screen -- push them into both cart 8531// trees now, or the drawer, badge and cart column sit stale for 8532// as long as the shopper spends configuring. 8533try { ckAfterCartMutation(false); } catch (eZ) { } 8534// Mark the items we successfully added (simple + ink) without reloading. 8535try { 8536simplePrimaryArr.forEach(function (pid) { 8537var el = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]'); 8538if (el) { 8539el.classList.remove('is-selected'); 8540el.classList.add('is-added'); 8541} 8542try { microAddedIds.add(String(pid)); } catch (eM0) { } 8543try { selectedPrimaryIds.delete(pid); } catch (eD0) { } 8544// Swap AFTER cart sync so in-cart suppression works 8545setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR0) { } }, 800); 8546}); 8547} catch (e) { } 8548try { 8549// Clear ink selections after adding ink pads 8550selectedInkColors.clear(); 8551inkChoiceMade = false; 8552if (swatchList) { 8553var allSw = swatchList.querySelectorAll('.kit-swatch'); 8554for (var s = 0; s < allSw.length; s++) allSw[s].classList.remove('is-selected'); 8555} 8556} catch (e2) { } 8557addBtn.disabled = false; 8558addBtn.classList.remove('is-loading'); 8559addBtn.textContent = 'Customize & Add'; 8560if (statusEl) statusEl.textContent = 'Added to cart. Next, customize your selected item.'; 8561// Open Micro PDP for the first hard item selected 8562var firstHard2 = hardPrimaryArr[0]; 8563var rawFirst2 = TILE_ITEM_BY_ID[String(firstHard2)] || {}; 8564var hydratedFirst2 = NOTARY_HYDRATED.get(String(firstHard2)); 8565var titleFirst2 = (hydratedFirst2 && hydratedFirst2.name) ? hydratedFirst2.name : (rawFirst2.title || rawFirst2.name || 'Customize item'); 8566// Seed Micro PDP progress for mixed flows: 8567// done = count of items already added via POST chain (simple + ink) 8568// total = done + remaining hard items to configure 8569var progDone1 = totalItemsCount; 8570var progTotal1 = totalItemsCount + hardPrimaryArr.length; 8571openMicroPdp(rawFirst2.productUrl, titleFirst2, firstHard2, hardPrimaryArr.slice(1), progDone1, progTotal1); 8572pushUpsellEvent('SmartCart_upsell_micro_pdp_open_from_cta', { 8573productId: firstHard2, 8574remaining_hard_count: hardPrimaryArr.length 8575}); 8576// Keep progress visible but "complete" the chain we ran 8577setAddProgress(totalItemsCount, totalItemsCount); 8578return; // prevent redirect 8579} 8580// If we are on the full cart page, do a hard refresh so Magento totals/summary 8581// update correctly (these often won't update reliably via partial DOM swaps). 8582// We preserve the success-state UX via ?accentAdded=1. 8583// ---------------------------------------------------- 8584// Zephyr: the rest of this site mutates the cart in place, so do 8585// the same here instead of reloading. ckAfterCartMutation refreshes 8586// BOTH cart trees (drawer store + cart-page component) and pops the 8587// side cart. It returns false only when the Zephyr store is absent 8588// (legacy theme) -- then, and only then, fall back to the old 8589// ?accentAdded=1 reload. Returning true lets the in-place success UX 8590// below finally run; that block was unreachable for as long as this 8591// branch always reloaded. 8592// ---------------------------------------------------- 8593try { 8594var onCartPage = document.body.classList.contains('checkout-cart-index') 8595|| /\/checkout\/cart(\/|$)/.test(window.location.pathname) 8596|| !!document.querySelector('[data-cart-item-id]') 8597|| !!document.querySelector('#shopping-cart-table') 8598|| !!document.querySelector('form#form-validate') 8599|| !!document.querySelector('.cart-container') 8600|| !!document.querySelector('[data-block="cart-summary"]') 8601|| !!document.querySelector('.cart-summary'); 8602if (onCartPage && !ckAfterCartMutation(true)) { 8603try { 8604var u2 = new URL(window.location.href); 8605u2.searchParams.set('accentAdded', '1'); 8606window.location.href = u2.toString(); 8607} catch (eU2) { 8608window.location.reload(); 8609} 8610return; 8611} 8612} catch (eCPR) { } 8613// Reality check: confirm the cart actually changed before the 8614// success UX below is allowed to stand. 8615try { ckVerifySimpleAdd(ckSimpleCountBefore); } catch (eVS) { } 8616// ---------------------------------------------------- 8617// No hard items left â keep the module live and refresh tiles in-place (no page reload) 8618// ---------------------------------------------------- 8619try {
8620// Mark added + swap tiles (stable slots) 8621simplePrimaryArr.forEach(function (pid) { 8622try { microAddedIds.add(String(pid)); } catch (eM) { } 8623try { 8624var el2 = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]'); 8625if (el2) { 8626el2.classList.remove('is-selected'); 8627el2.classList.add('is-added'); 8628} 8629} catch (eC) { } 8630try { selectedPrimaryIds.delete(pid); } catch (eD) { } 8631// Swap AFTER cart sync so in-cart suppression works 8632setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR) { } }, 800); 8633}); 8634} catch (eS3) { } 8635try { 8636// Clear ink selections after adding ink pads 8637selectedInkColors.clear(); 8638inkChoiceMade = false; 8639if (swatchList) { 8640var allSw3 = swatchList.querySelectorAll('.kit-swatch'); 8641for (var s3 = 0; s3 < allSw3.length; s3++) allSw3[s3].classList.remove('is-selected'); 8642// restore "none" default if visible 8643var noneBtn3 = swatchList.querySelector('.kit-swatch[data-kit-color="none"]'); 8644if (noneBtn3 && noneBtn3.style.display !== 'none') { 8645noneBtn3.classList.add('is-selected'); 8646inkChoiceMade = true; 8647} 8648} 8649} catch (eS4) { } 8650// Success toast 8651try { 8652var toast2 = document.getElementById('kit-success-toast'); 8653if (toast2) { 8654var msg = (totalItemsCount > 1) ? ('â Added ' + totalItemsCount + ' items to cart') : 'â Added to cart'; 8655toast2.textContent = msg; 8656toast2.classList.add('is-visible'); 8657setTimeout(function () { toast2.classList.remove('is-visible'); }, 3000); 8658} 8659} catch (eT2) { } 8660// Reset CTA + progress 8661addBtn.disabled = false; 8662addBtn.classList.remove('is-loading'); 8663addBtn.textContent = 'Add to Cart'; 8664if (statusEl) statusEl.textContent = 'Added to cart. Want to add another?'; 8665resetAddProgressUI(); 8666updatePriceUI(); 8667// Keep module open to encourage additional add-ons 8668upsell.classList.add('cart-kit-upsell--open'); 8669body.hidden = false; 8670}); 8671}) 8672.catch(function (err) { 8673console.error('Cart kit upsell error:', err); 8674var ckErrMsg = 'Sorry, we could not add the kit to your cart. Please try again.'; 8675try { 8676var ckFlash = (window.Alpine && typeof window.Alpine.store === 'function') 8677? window.Alpine.store('flash') : null; 8678if (ckFlash && typeof ckFlash.show === 'function') ckFlash.show('error', ckErrMsg); 8679else alert(ckErrMsg); 8680} catch (eFl) { alert(ckErrMsg); } 8681addBtn.disabled = false; 8682addBtn.classList.remove('is-loading'); 8683addBtn.textContent = 'Add to Cart'; 8684if (statusEl) statusEl.textContent = ''; 8685// hide progress bar on error 8686resetAddProgressUI(); 8687}); 8688}); 8689// ---------------------------------------------------- 8690// Cart sync helpers (run after async adds so UI + tile logic are up-to-date) 8691// ---------------------------------------------------- 8692// 15) Success + Add More 8693// ---------------------------------------------------- 8694function resetSelections() { 8695selectedPrimaryIds.clear(); 8696selectedInkColors.clear(); 8697inkChoiceMade = false; 8698var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0); 8699if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId)); 8700else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id); 8701buildGrid(); 8702if (swatchList) { 8703var sw = swatchList.querySelectorAll('.kit-swatch'); 8704for (var i = 0; i < sw.length; i++) sw[i].classList.remove('is-selected'); 8705// Default to "none" (I have an ink pad) so user isn't forced into an alert loop 8706var noneBtn = swatchList.querySelector('.kit-swatch[data-kit-color="none"]'); 8707if (noneBtn && noneBtn.style.display !== 'none') { 8708noneBtn.classList.add('is-selected'); 8709// keep selectedInkColors empty, but mark a choice as made 8710inkChoiceMade = true; 8711} 8712} 8713addBtn.disabled = false; 8714addBtn.classList.remove('is-loading'); 8715addBtn.textContent = 'Add to Cart'; 8716if (statusEl) statusEl.textContent = ''; 8717resetAddProgressUI(); 8718updatePriceUI(); 8719} 8720function handleAddMoreFromQuery() { 8721if (!accentAddMoreQS) return; 8722upsell.classList.add('cart-kit-upsell--open'); 8723body.hidden = false; 8724resetSelections(); 8725try { 8726var href = window.location.href; 8727var clean = href 8728.replace(/([?&])accentAdded=1(?=(&|#|$))/, '$1') 8729.replace(/[?&]#/, '#') 8730.replace(/[?&]$/, ''); 8731window.history.replaceState({}, document.title, clean); 8732} catch (e) { } 8733} 8734function applySuccessStateFromQuery() { 8735if (!accentAddedQS) return;
8736var headerText = upsell.querySelector('#cart-kit-header-text'); 8737if (headerText) { 8738headerText.innerHTML = 8739'<span class="cart-kit-upsell__success-inline">' 8740+ 'â Items added to cart!' 8741+ ' <span style="color:#000; text-decoration: underline;">Add more?</span>' 8742+ '</span>'; 8743} 8744upsell.classList.remove('cart-kit-upsell--open'); 8745body.hidden = true; 8746try { 8747var clean2 = window.location.href.replace(/([?&])accentAdded=1(&|$)/, '$1').replace(/[?&]$/, ''); 8748window.history.replaceState({}, document.title, clean2); 8749} catch (e) { } 8750} 8751// ---------------------------------------------------- 8752// Micro PDP queue helpers (supports 2+ hard SKUs) 8753// ---------------------------------------------------- 8754function getTileInfoByProductId(pid) { 8755pid = String(pid || '').trim(); 8756if (!pid) return null; 8757var raw = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? (TILE_ITEM_BY_ID[pid] || TILE_ITEM_BY_ID[String(pid)]) : null; 8758if (!raw) return null; 8759var hydrated = (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) ? NOTARY_HYDRATED.get(pid) : null; 8760var title = (hydrated && hydrated.name) ? hydrated.name : (raw.title || raw.name || 'Customize item'); 8761var url = raw.productUrl || raw.url || raw.href || null; 8762if (!url) return null; 8763return { productId: pid, title: title, url: url }; 8764} 8765function parseRemainingHardFromUrl() { 8766try { 8767var u = new URL(window.location.href); 8768var val = (u.searchParams.get('remainingHard') || '').trim(); 8769if (!val) return []; 8770return val.split(',').map(function (s) { return String(s || '').trim(); }).filter(Boolean); 8771} catch (e) { 8772return []; 8773} 8774} 8775function stripRemainingHardParam() { 8776try { 8777var u = new URL(window.location.href); 8778u.searchParams.delete('remainingHard'); 8779u.searchParams.delete('microTotal'); 8780u.searchParams.delete('microDone'); 8781window.history.replaceState({}, document.title, u.toString()); 8782} catch (e) { } 8783} 8784function openNextHardFromRemaining() { 8785var remaining = parseRemainingHardFromUrl(); 8786if (!remaining.length) { 8787stripRemainingHardParam(); 8788return false; 8789} 8790var nextId = remaining.shift(); 8791var info = getTileInfoByProductId(nextId); 8792if (!info) { 8793// If we can't resolve the next item, stop chaining to avoid a loop 8794stripRemainingHardParam(); 8795return false; 8796} 8797// Rebuild the remainingHard param (after removing the one we're about to open) 8798try { 8799var u = new URL(window.location.href); 8800if (remaining.length) u.searchParams.set('remainingHard', remaining.join(',')); 8801else u.searchParams.delete('remainingHard'); 8802window.history.replaceState({}, document.title, u.toString()); 8803} catch (e) { } 8804// Small status hint before opening the next configurator 8805if (statusEl) statusEl.textContent = 'Next: customize ' + info.title + '.'; 8806// Open next Micro PDP 8807var prog = getMicroProgressFromUrl(); 8808openMicroPdp(info.url, info.title, info.productId, remaining, prog.done, prog.total); 8809return true; 8810} 8811// --- SmartCart: ensure cart + tiles refresh when micro-PDP closes --- 8812(function bindMicroPdpCloseRefresh() { 8813var MODAL_SEL = '#smartcartMicroPdp, .smartcart-micro-pdp, [data-smartcart-micro-pdp]'; 8814var CLOSE_SEL = '[data-smartcart-close], .smartcart-close, .close, [aria-label="Close"]'; 8815var OVERLAY_SEL = '.smartcart-overlay, .modal-backdrop, [data-smartcart-overlay]'; 8816// You likely already have these functions; map to your actual names: 8817var refresh = window.smartcartRefresh || window.SmartCartRefresh || window.renderSmartCart || null; 8818// If you *donât* have a single refresh entrypoint, use whatever you normally call after add-to-cart: 8819function hardRefreshFallback() { 8820try { 8821if (typeof refresh === 'function') return refresh(); 8822// Fallback: call the same method you use after cart update 8823if (window.SmartCart && typeof window.SmartCart.render === 'function') return window.SmartCart.render(); 8824if (window.smartcart && typeof window.smartcart.render === 'function') return window.smartcart.render(); 8825} catch (e) { 8826console.warn('[SmartCart] refresh on modal close failed:', e); 8827} 8828} 8829// Debounce so multiple close signals (button + overlay) donât double-trigger 8830var t; 8831function refreshSoon() { 8832clearTimeout(t); 8833t = setTimeout(function () { 8834hardRefreshFallback(); 8835}, 150); 8836} 8837function onDocClick(e) { 8838var modal = document.querySelector(MODAL_SEL); 8839if (!modal) return; 8840var isClose = e.target.closest && e.target.closest(CLOSE_SEL); 8841var isOverlay = e.target.closest && e.target.closest(OVERLAY_SEL); 8842if (isClose || isOverlay) { 8843// Let the DOM close animation finish, then refresh 8844refreshSoon(); 8845} 8846} 8847function onEsc(e) { 8848if (e.key === 'Escape' || e.keyCode === 27) refreshSoon(); 8849} 8850document.addEventListener('click', onDocClick, true); 8851document.addEventListener('keydown', onEsc, true); 8852// Optional: if you dispatch a custom "microPdpClosed" event already, hook that too: 8853document.addEventListener('smartcart:microPdpClosed', refreshSoon); 8854})(); 8855// ---------------------------------------------------- 8856// 4A) Replace an "Added" product tile with the next best recommendation 8857// (keeps the grid fresh without requiring a full page reload) 8858// ---------------------------------------------------- 8859function convertTileToCompleteState(card, totalCount) { 8860if (!card) return; 8861// Stable-slot swap: fade this card out before repopulating 8862try { card.classList.add('ck-swapping'); } catch (eFade0) { } 8863try { 8864card.classList.remove('is-selected'); 8865card.classList.remove('is-added'); 8866card.classList.add('is-complete'); 8867// prevent future selection toggles 8868card.setAttribute('aria-disabled', 'true'); 8869card.innerHTML = 8870'<div class="ck-tile-complete">' 8871+ '<div class="ck-tile-complete__icon">â</div>' 8872+ '<div class="ck-tile-complete__title">All set</div>' 8873+ '<div class="ck-tile-complete__subtext">You\'ve added all recommended items for this order.</div>' 8874+ '</div>'; 8875} catch (e) { } 8876}
8877function replaceAddedTileWithNext(addedPid) { 8878try { 8879if (!useTiles || !stampGrid) return; 8880addedPid = String(addedPid || '').trim(); 8881if (!addedPid) return; 8882var card = stampGrid.querySelector('.kit-product-card[data-kit-primary="' + addedPid + '"]'); 8883if (!card) return; 8884// Build an ordered pool (highest relevance first) 8885var ordered = pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow).tiles; 8886// Build exclusion map 8887var exclude = Object.create(null); 8888// Exclude currently visible tiles 8889var visible = stampGrid.querySelectorAll('.kit-product-card[data-kit-primary]'); 8890for (var i = 0; i < visible.length; i++) { 8891var v = String(visible[i].getAttribute('data-kit-primary') || '').trim(); 8892if (v) exclude[v] = true; 8893} 8894// Exclude anything already added (session + micro returns) 8895try { 8896if (microAddedIds && microAddedIds.forEach) { 8897microAddedIds.forEach(function (id) { exclude[String(id)] = true; }); 8898} 8899} catch (eAdded) { } 8900// Exclude anything already shown during this session (prevents repeats when pool is small) 8901try { 8902if (ckSeenTileIds && ckSeenTileIds.forEach) { 8903ckSeenTileIds.forEach(function (id) { exclude[String(id)] = true; }); 8904} 8905} catch (eSeen) { } 8906// Exclude queued HARD SKUs still pending configuration 8907try { 8908var queued = parseRemainingHardFromUrl(); 8909for (var q = 0; q < queued.length; q++) { 8910var qid = String(queued[q] || '').trim(); 8911if (qid) exclude[qid] = true; 8912} 8913} catch (eQ) { } 8914// Exclude anything currently selected (so swaps don't "steal" into a tile the user already picked) 8915try { 8916if (selectedPrimaryIds && selectedPrimaryIds.forEach) { 8917selectedPrimaryIds.forEach(function (sid) { 8918var s = String(sid || '').trim(); 8919if (s) exclude[s] = true; 8920}); 8921} 8922} catch (eSel) { } 8923// Exclude anything already in the cart (prefer URL-path match; fallback to title match) 8924var cartPaths = Object.create(null); 8925var cartFullLower = ''; 8926try { cartPaths = getCartProductPathSet(); } catch (eCP) { } 8927try { cartFullLower = getCartFullTextLower(); } catch (eCT) { } 8928// Find next eligible item 8929var next = null; 8930for (var k = 0; k < ordered.length; k++) { 8931var it = ordered[k]; 8932var pid = String(it && (it.productId || it.id || it.product || it.primaryId) || '').trim(); 8933if (!pid || exclude[pid]) continue; 8934// URL-path in-cart exclusion 8935var pth = ''; 8936try { pth = normalizeUrlPathForCompare(it.productUrl || it.url || ''); } catch (eNP) { } 8937if (pth && cartPaths[pth]) continue; 8938// Title fallback exclusion 8939var nm = String(it.name || it.title || '').toLowerCase(); 8940if (nm && cartFullLower && cartFullLower.indexOf(nm) !== -1) continue; 8941next = it; 8942break; 8943} 8944if (!next) { 8945// Pool exhausted -- offer an approved alternative before giving up. 8946var altNext = ckPickFallbackTile(); 8947if (altNext) { 8948try { card.classList.remove('ck-swapping'); } catch (eFA) { } 8949ckRepaintCardAs(card, altNext); 8950return; 8951} 8952// Nothing left â convert this tile to completion tile 8953try { convertTileToCompleteState(card); } catch (eDone) { } 8954setTimeout(function () { try { card.classList.remove('ck-swapping'); } catch (eFadeX) { } }, 160); 8955return; 8956} 8957// Swap this card to the new item 8958var nextId = String(next.productId || next.id || next.product || next.primaryId || '').trim(); 8959var nextUrl = next.productUrl || next.url || ''; 8960var nextHard = !!requiresMicroPdp(nextId, next); 8961// Mark seen immediately so async hydration can't cause repeats 8962try { if (ckSeenTileIds) ckSeenTileIds.add(String(nextId)); } catch (eS) { } 8963// Fade out before repopulating (stable slot) 8964try { card.classList.add('ck-swapping'); } catch (eFade0) { } 8965// Reset card state + dataset 8966try { 8967card.classList.remove('is-added', 'is-selected', 'is-complete'); 8968card.removeAttribute('aria-disabled'); 8969} catch (eState) { } 8970card.setAttribute('data-kit-primary', nextId); 8971card.setAttribute('data-kit-url', nextUrl); 8972card.setAttribute('data-kit-hard', nextHard ? '1' : '0'); 8973// Paint skeleton while hydration finishes (DOM structure used by buildGrid) 8974var titleEl = card.querySelector('.kit-product-name'); 8975var priceEl = card.querySelector('.kit-product-price'); 8976var thumbTile = card.querySelector('.kit-product-thumb-tile'); 8977if (titleEl) titleEl.textContent = (next.title || next.name || 'Loadingâ¦'); 8978if (priceEl) priceEl.textContent = ''; 8979// Reset the thumb; we'll hydrate with a real image after fetchPdpData 8980if (thumbTile) { 8981var imgSrc = (next.image || next.img || ''); 8982if (imgSrc) { 8983var altText = String(next.title || next.name || '').replace(/"/g, '"'); 8984thumbTile.innerHTML = '<img src="' + imgSrc + '" alt="' + altText + '">'; 8985} else { 8986thumbTile.innerHTML = '<span class="kit-thumb-loading">Loading...</span>'; 8987} 8988} 8989// Fade back in once the new tile has been painted 8990setTimeout(function () { 8991try { card.classList.remove('ck-swapping'); } catch (eFade1) { } 8992}, 160); 8993// Record the swapped-in product's strategy attributes (role, 8994// friction, experimental) in an ANALYTICS-ONLY map. 8995// 8996// Deliberately NOT written to TILE_ITEM_BY_ID: that map feeds 8997// requiresMicroPdp(), so registering swapped tiles there would 8998// flip the CTA to 'Customize & Add' and reroute the add through 8999// the micro-PDP. That may be the correct behaviour -- a bare 9000// product=id&qty=1 POST of a required-options SKU is wrong -- but 9001// it is an add-flow change and does not belong in an analytics 9002// patch. Tracked separately. 9003try { CK_GA_TILE_META[String(nextId)] = next; } catch (eReg) { } 9004// Hydrate: fetch canonical name/price/image (guard against stale async writes) 9005(function (el, expectedId, raw) { 9006try { 9007var url = raw.productUrl || raw.url || ''; 9008var p = fetchPdpData(url); 9009if (!p || !p.then) return; 9010p.then(function (data) { 9011try { 9012// Guard a stale async write: the slot may have been 9013// swapped again while this fetch was in flight. 9014if (!el || el.getAttribute('data-kit-primary') !== String(expectedId)) return; 9015var tEl = el.querySelector('.kit-product-name'); 9016var prEl = el.querySelector('.kit-product-price'); 9017var thEl = el.querySelector('.kit-product-thumb-tile'); 9018if (tEl && data.name) tEl.textContent = data.name;
9019// Cache the canonical name/price so analytics and 9020// the price UI see the same product the shopper does. 9021// Cache the canonical name/price. Gated by the same 9022// quality check buildGrid and ckRepaintCardAs use -- 9023// a URL that scrapes to 'Create New Customer Account' 9024// must not reach the cache, because it feeds the 9025// micro-PDP modal title and the displayed total. 9026// Also normalises price the way the other two sites do. 9027try { 9028if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED 9029&& ckIsUsableTileData(data)) { 9030NOTARY_HYDRATED.set(String(expectedId), { 9031productId: String(expectedId), 9032sku: data.sku, 9033productUrl: (raw && (raw.productUrl || raw.url)) || '', 9034name: data.name || '', 9035img: data.img || '', 9036price: (data.price != null ? Number(data.price) : null) 9037}); 9038} 9039} catch (eHyd) { } 9040// A swapped tile is newly visible and was in no 9041// view_item_list, so its funnel would otherwise start 9042// at select_item. Give it its own one-item impression. 9043// attempts = 0 -> fire now; the name is already cached. 9044try { 9045ckGaFireListWhenHydrated( 9046[String(expectedId)], 0, 9047(CK_GA4.LIST_PREFIX + ' - ' + ckGaListId() + ' (swap)').slice(0, 100) 9048); 9049} catch (eSwapGa) { } 9050if (prEl) { 9051prEl.textContent = (data.price != null ? ('$' + Number(data.price).toFixed(2)) : 'See price in cart'); 9052} 9053if (thEl) { 9054if (data.img) { 9055var alt2 = String(data.name || '').replace(/"/g, '"'); 9056thEl.innerHTML = '<img src="' + data.img + '" alt="' + alt2 + '">'; 9057} else { 9058thEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 9059} 9060} 9061} catch (eH) { } 9062}).catch(function () { }); 9063} catch (eF) { } 9064})(card, nextId, next); 9065} catch (e) { 9066// fail silently 9067} 9068} 9069function handleMicroAddedFromQuery() { 9070// The micro-PDP no longer reloads the page, so microAddedQS / 9071// microAddedNameQS -- captured from window.location.search back at 9072// DOMContentLoaded -- are stale by definition. onMicroPdpAddSucceeded() 9073// writes the receipt into the URL via history.replaceState, so re-read 9074// it live here. Still correct for the legacy reload path. 9075try { 9076var ckLive = new URL(window.location.href).searchParams; 9077microAddedQS = ckLive.get('microAdded'); 9078microAddedNameQS = ckLive.get('microAddedName'); 9079} catch (eLive) { } 9080if (!microAddedQS) return; 9081var pid = String(microAddedQS || '').trim(); 9082if (!pid) return; 9083// Mark as added so tile greys out 9084microAddedIds.add(pid); 9085// Remove from current selections (prevents stale totals when tile is swapped) 9086try { selectedPrimaryIds.delete(pid); } catch (eSel0) { } 9087// Micro progress: user just completed one configurator 9088var progNow = bumpMicroDoneInUrl(); 9089MICRO_PDP.progressDone = progNow.done; 9090MICRO_PDP.progressTotal = progNow.total; 9091renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal); 9092// Hard SKUs add inside the micro-PDP iframe, so they never reach the 9093// POST chain above and were invisible to analytics. Fire the 9094// per-product event here. Dedupe on pid + completed-count: the 9095// receipt params can survive a re-entry of this handler. 9096try { 9097// Dedupe on a short time window, NOT on progNow.done: 9098// bumpMicroDoneInUrl() increments on every call, so a re-entry 9099// would produce a different key and defeat the guard entirely. 9100// A configurator takes seconds, so two legitimate adds of the 9101// same SKU inside 5s are impossible. 9102window.__ckMicroAddReported = window.__ckMicroAddReported || {}; 9103var ckNowT = Date.now(); 9104if (ckNowT - (window.__ckMicroAddReported[pid] || 0) > 5000) { 9105window.__ckMicroAddReported[pid] = ckNowT; 9106// On the legacy reload path this handler runs in the same tick as 9107// buildGrid(), which only STARTS hydration -- so firing now would 9108// send the entity id for the highest-AOV items in the module. 9109// Wait for the SKU, then fire; fire anyway on timeout. 9110(function emitMicroAdd(tries) { 9111try { 9112if (!ckGaHasSku(pid) && tries > 0) { 9113setTimeout(function () { emitMicroAdd(tries - 1); }, CK_GA4.HYDRATE_POLL_MS); 9114return; 9115} 9116} catch (eW) { } 9117pushUpsellEvent('SmartCart_upsell_micro_added', { 9118product_ids: [pid], 9119productId: pid, 9120micro_done: progNow.done, 9121micro_total: progNow.total 9122}); 9123})(CK_GA4.HYDRATE_POLLS); 9124} 9125} catch (eGaM) { } 9126// Show a simple success message in the module status area 9127var niceName = (microAddedNameQS || '').trim(); 9128if (!niceName) { 9129// If name not provided, try hydrated cache 9130var hydrated = NOTARY_HYDRATED.get(pid);
9131if (hydrated && hydrated.name) niceName = hydrated.name; 9132} 9133if (!niceName) niceName = 'Product'; 9134var toast = document.getElementById('kit-success-toast'); 9135if (toast) { 9136toast.textContent = 'â Added to cart: ' + niceName; 9137// If this was the LAST remaining hard item, celebrate completion 9138if (progNow.total > 1 && progNow.done >= progNow.total) { 9139toast.textContent = '???? All set â ' + progNow.done + ' of ' + progNow.total + ' items added'; 9140} 9141toast.classList.add('is-visible'); 9142setTimeout(function () { 9143toast.classList.remove('is-visible'); 9144}, 3500); 9145} else if (statusEl) { 9146// fallback 9147statusEl.textContent = 'â Added to cart: ' + niceName; 9148if (progNow.total > 1 && progNow.done >= progNow.total) { 9149statusEl.textContent = '???? All set â ' + progNow.done + ' of ' + progNow.total + ' items added'; 9150} 9151} 9152// Rebuild grid so the "is-added" state is applied 9153if (useTiles) buildGrid(); 9154try { updatePriceUI(); } catch (ePU0) { } 9155// 4B) After a HARD SKU is completed, swap the added tile for the next recommendation 9156// so the customer can keep adding without stale/greyed tiles. 9157// (Run after buildGrid() so we target the freshly-rendered card.) 9158setTimeout(function () { 9159try { replaceAddedTileWithNext(pid); } catch (e) { } 9160}, 800); 9161// If there are more HARD SKUs waiting, open the next Micro PDP automatically 9162// (keeps the user in a guided configuration flow) 9163setTimeout(function () { 9164try { 9165openNextHardFromRemaining(); 9166} catch (e) { } 9167}, 350); 9168// Scroll to the newly added item in cart + highlight 9169// Strategy: find a cart row that contains the product name 9170setTimeout(function () { 9171try { 9172var table = document.querySelector('#shopping-cart-table'); 9173if (!table) return; 9174var rows = Array.from(table.querySelectorAll('tr')); 9175var targetRow = null; 9176if (niceName && niceName !== 'Product') { 9177var nameLower = niceName.toLowerCase(); 9178targetRow = rows.find(function (r) { 9179return (r.innerText || '').toLowerCase().indexOf(nameLower) > -1; 9180}) || null; 9181} 9182// Fallback: if we can't find the row by name, just scroll to cart table 9183var scrollEl = targetRow || table; 9184scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); 9185if (targetRow) { 9186targetRow.classList.add('ck-cart-highlight'); 9187setTimeout(function () { 9188targetRow.classList.remove('ck-cart-highlight'); 9189}, 3500); 9190} 9191} catch (e) { } 9192}, 250); 9193// Remove receipt params so refresh doesn't repeat the success behavior 9194try { 9195var u = new URL(window.location.href); 9196u.searchParams.delete('microAdded'); 9197u.searchParams.delete('microAddedName'); 9198window.history.replaceState({}, document.title, u.toString()); 9199} catch (e) { } 9200} 9201// Query-param handlers MUST run in this order (priority: micro receipt > add more > success) 9202handleMicroAddedFromQuery(); // receipt event â toast/greyout/highlight, then strip params 9203handleAddMoreFromQuery(); // user intent to reopen + reset 9204applySuccessStateFromQuery(); // generic success collapse/header state 9205// ---------------------------------------------------- 9206// 9) DEBUG OVERLAY (persona + product scores) 9207// Enable with ?upsellDebug=1 9208// ---------------------------------------------------- 9209(function initDebugOverlay() { 9210var debugOn = (urlParams.get('upsellDebug') === '1'); 9211if (!debugOn) return; 9212// ---- styles (scoped) ---- 9213var style = document.createElement('style'); 9214style.textContent = 9215'#ck-debug-overlay{position:fixed;right:12px;bottom:12px;z-index:999999;' + 9216'width:360px;max-width:calc(100vw - 24px);' + 9217'background:#0b1220;color:#e8eefc;border:1px solid rgba(255,255,255,.15);' + 9218'border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35);font-family:inherit;}' + 9219'#ck-debug-overlay *{box-sizing:border-box;}' + 9220'#ck-debug-head{display:flex;align-items:center;justify-content:space-between;' + 9221'padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.12);}' + 9222'#ck-debug-title{font-weight:700;font-size:13px;letter-spacing:.2px;}' + 9223'#ck-debug-actions{display:flex;gap:8px;align-items:center;}' + 9224'#ck-debug-actions button{background:rgba(255,255,255,.12);color:#e8eefc;border:1px solid rgba(255,255,255,.15);' + 9225'border-radius:8px;padding:6px 8px;font-size:12px;cursor:pointer;}' + 9226'#ck-debug-actions button:hover{background:rgba(255,255,255,.18);}' + 9227'#ck-debug-body{padding:10px 12px;max-height:55vh;overflow:auto;}' + 9228'#ck-debug-body h4{margin:10px 0 6px 0;font-size:12px;opacity:.9;}' + 9229'#ck-debug-kv{display:grid;grid-template-columns:120px 1fr;gap:6px 10px;font-size:12px;}' + 9230'#ck-debug-kv .k{opacity:.75;}' + 9231'#ck-debug-table{width:100%;border-collapse:collapse;font-size:12px;}' + 9232'#ck-debug-table th,#ck-debug-table td{padding:6px 6px;border-bottom:1px solid rgba(255,255,255,.10);vertical-align:top;}' + 9233'#ck-debug-table th{opacity:.75;text-align:left;font-weight:600;}' + 9234'#ck-debug-pill{display:inline-block;padding:2px 6px;border-radius:999px;' + 9235'background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.14);font-size:11px;}' + 9236'#ck-debug-muted{opacity:.7;font-size:11px;line-height:1.35;}' +
9237'#ck-debug-overlay.collapsed #ck-debug-body{display:none;}'; 9238document.head.appendChild(style); 9239// ---- UI ---- 9240var box = document.createElement('div'); 9241box.id = 'ck-debug-overlay'; 9242box.innerHTML = 9243'<div id="ck-debug-head">' + 9244'<div id="ck-debug-title">Cart Kit Debug</div>' + 9245'<div id="ck-debug-actions">' + 9246'<button type="button" id="ck-debug-copy">Copy</button>' + 9247'<button type="button" id="ck-debug-toggle">Collapse</button>' + 9248'</div>' + 9249'</div>' + 9250'<div id="ck-debug-body">' + 9251'<div id="ck-debug-kv"></div>' + 9252'<h4>Persona scores</h4>' + 9253'<table id="ck-debug-table"><thead><tr><th>Persona</th><th>Score</th><th>Hits</th></tr></thead><tbody id="ck-debug-personas"></tbody></table>' + 9254'<h4>Top products (selected persona)</h4>' + 9255'<table id="ck-debug-table"><thead><tr><th>Product</th><th>Score</th><th>Role</th></tr></thead><tbody id="ck-debug-products"></tbody></table>' + 9256'<div id="ck-debug-muted" style="margin-top:10px;">Tip: add/remove items and watch scores update live.</div>' + 9257'</div>'; 9258document.body.appendChild(box); 9259var btnToggle = document.getElementById('ck-debug-toggle'); 9260var btnCopy = document.getElementById('ck-debug-copy'); 9261btnToggle.addEventListener('click', function () { 9262box.classList.toggle('collapsed'); 9263btnToggle.textContent = box.classList.contains('collapsed') ? 'Expand' : 'Collapse'; 9264}); 9265function safeJson(obj) { 9266try { return JSON.stringify(obj, null, 2); } catch (e) { return String(obj); } 9267} 9268btnCopy.addEventListener('click', function () { 9269var payload = window.__CK_DEBUG_LAST__ || {}; 9270var textToCopy = safeJson(payload); 9271if (navigator.clipboard && navigator.clipboard.writeText) { 9272navigator.clipboard.writeText(textToCopy).catch(function () { }); 9273} else { 9274// fallback 9275var ta = document.createElement('textarea'); 9276ta.value = textToCopy; 9277ta.style.position = 'fixed'; 9278ta.style.left = '-9999px'; 9279document.body.appendChild(ta); 9280ta.select(); 9281try { document.execCommand('copy'); } catch (e) { } 9282document.body.removeChild(ta); 9283} 9284}); 9285// ---- scoring helpers for overlay ---- 9286function personaScoreBreakdown(personaObj, cartTextLower) { 9287var kw = keywordMatchScore((personaObj.match || personaObj.matchKeywords || []), cartTextLower); 9288var base = kw.score; 9289var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0; 9290return { 9291base: base, 9292hits: kw.hits, 9293matched: kw.matched, 9294priorityNudge: priorityNudge, 9295total: base + priorityNudge 9296}; 9297} 9298function productScoreForOverlay(item, cartTextLower, allowExperimental) { 9299return scoreProductForCart(item, cartTextLower, allowExperimental); 9300} 9301function esc(s) { 9302return String(s || '').replace(/[&<>"']/g, function (c) { 9303return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]); 9304}); 9305} 9306// Debug helpers: get cart titles/options as arrays 9307function getCartTitlesList() { 9308try { 9309var nameNodes = document.querySelectorAll( 9310'[data-cart-item-id] h3 a,' + 9311'[data-cart-item-id] h3,' + 9312'#shopping-cart-table h2.cartProductName a,' + 9313'#shopping-cart-table h2.cartProductName,' + 9314'#shopping-cart-table .product-name a,' + 9315'#shopping-cart-table .product-name,' + 9316'.product-item-name a,' + 9317'.product-item-name' 9318); 9319var out = []; 9320var seen = {}; 9321for (var i = 0; i < nameNodes.length; i++) { 9322var t = (nameNodes[i].textContent || '').trim(); 9323if (!t) continue; 9324var k = t.toLowerCase(); 9325if (seen[k]) continue; 9326seen[k] = true; 9327out.push(t); 9328} 9329return out; 9330} catch (e) { return []; } 9331} 9332function getCartOptionsList() { 9333try { 9334var optNodes = document.querySelectorAll( 9335'[data-cart-item-id] .flex-wrap,' + 9336'#shopping-cart-table .item-options,' + 9337'#shopping-cart-table dl.item-options,' + 9338'.item-options,' + 9339'dl.item-options' 9340); 9341var out = []; 9342for (var j = 0; j < optNodes.length; j++) { 9343var o = (optNodes[j].textContent || '').trim(); 9344if (o) out.push(o); 9345} 9346return out; 9347} catch (e) { return []; } 9348} 9349function renderOverlay() { 9350var cartTitleLower = getCartTitleTextLower(); 9351var cartFullLower = getCartFullTextLower(); 9352var allowExperimental = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1'); 9353// Persona scoring 9354var personaRows = []; 9355var best = { key: 'generic', score: -1 }; 9356for (var i = 0; i < PERSONAS.length; i++) { 9357var p = PERSONAS[i]; 9358if (!p || p.key === 'generic') continue; 9359var br = personaScoreBreakdown(p, cartTitleLower); 9360personaRows.push({ 9361key: p.key, 9362name: p.name, 9363score: br.total, 9364hits: br.hits, 9365matched: br.matched || [], 9366priority: br.priorityNudge 9367}); 9368if (br.hits > 0 && br.total > best.score) { 9369best = { key: p.key, score: br.total }; 9370} 9371} 9372personaRows.sort(function (a, b) { return b.score - a.score; }); 9373// If nobody matched, generic 9374var chosenKey = (best.score <= 0) ? 'generic' : best.key; 9375var chosenPersona = getPersonaByKey(chosenKey); 9376// Product scoring for chosen persona 9377var pool = (chosenPersona && chosenPersona.items) ? chosenPersona.items.slice() : []; 9378var prodRows = pool.map(function (it) { 9379return { 9380id: it.productId, 9381url: it.productUrl, 9382name: it.productName || it.name || it.title || it.label || '', 9383role: it.role || '', 9384friction: it.friction || '', 9385experimental: (it.experimental === true), 9386score: productScoreForOverlay(it, cartFullLower, allowExperimental) 9387}; 9388}); 9389prodRows.sort(function (a, b) { return b.score - a.score; }); 9390// Top N shown for overlay (not just 3) 9391var topN = prodRows.slice(0, 8); 9392// Update KV 9393var kv = document.getElementById('ck-debug-kv'); 9394var titleList = getCartTitlesList(); 9395var optList = getCartOptionsList(); 9396var cartTitlesText = (titleList || []).join(' | '); 9397var cartOptionsText = (optList || []).join(' | '); 9398kv.innerHTML = 9399'<div class="k">Chosen persona</div><div><span id="ck-debug-pill" class="ck-pill"></span></div>' + 9400'<div class="k">Experimental</div><div>' + (allowExperimental ? 'ON (debug)' : 'OFF') + '</div>' + 9401'<div class="k">Cart titles (used for persona)</div><div id="ck-debug-muted">' + esc(cartTitlesText.slice(0, 220)) + (cartTitlesText.length > 220 ? 'â¦' : '') + '</div>' + 9402'<div class="k">Cart options (additional signals)</div><div id="ck-debug-muted">' + (cartOptionsText ? (esc(cartOptionsText.slice(0, 220)) + (cartOptionsText.length > 220 ? 'â¦' : '')) : '<span style="opacity:.7;">none detected</span>') + '</div>'; 9403var pill = document.getElementById('ck-debug-pill'); 9404if (pill) { 9405pill.textContent = chosenKey; 9406pill.className = ''; 9407pill.id = 'ck-debug-pill'; 9408pill.style.display = 'inline-block'; 9409pill.style.padding = '2px 6px'; 9410pill.style.borderRadius = '999px'; 9411pill.style.background = 'rgba(255,255,255,.12)'; 9412pill.style.border = '1px solid rgba(255,255,255,.14)'; 9413pill.style.fontSize = '11px'; 9414} 9415// Persona tables 9416var tbody = document.getElementById('ck-debug-personas'); 9417tbody.innerHTML = personaRows.map(function (r) { 9418var isChosen = (r.key === chosenKey); 9419// Build keyword pills (or fallback) 9420var kwHtml = (r.matched && r.matched.length) 9421? '<div class="ck-debug-keywords">' + 9422r.matched.slice(0, 10).map(function (k) { 9423return '<span class="ck-debug-keyword">' + esc(k) + '</span>'; 9424}).join('') + 9425(r.matched.length > 10 ? '<span class="ck-debug-keyword muted">+' + (r.matched.length - 10) + ' more</span>' : '') + 9426'</div>' 9427: '<div class="ck-debug-keywords muted">no keyword matches</div>'; 9428return '' 9429+ '<tr>' 9430+ '<td>' + esc(r.key) + (isChosen ? ' <span class="ck-badge" style="margin-left:6px;opacity:.9;">â </span>' : '') + '</td>' 9431+ '<td>
9431' + (Math.round(r.score * 100) / 100) + '</td>' 9432+ '<td>' + r.hits + '</td>' 9433+ '</tr>' 9434+ '<tr class="ck-debug-keyword-row">' 9435+ '<td colspan="3">' + kwHtml + '</td>' 9436+ '</tr>'; 9437}).join(''); 9438// Products table 9439var prodTbody = document.getElementById('ck-debug-products'); 9440if (prodTbody) { 9441if (!topN || !topN.length) { 9442prodTbody.innerHTML = '<tr><td colspan="3" style="opacity:.7;">No products found for this persona.</td></tr>'; 9443} else { 9444prodTbody.innerHTML = topN.map(function (r) { 9445var label = r.name || r.url || r.id || ''; 9446var roleBits = []; 9447if (r.role) roleBits.push(r.role); 9448if (r.friction) roleBits.push('friction: ' + r.friction); 9449if (r.experimental) roleBits.push('exp'); 9450return '' 9451+ '<tr>' 9452+ '<td>' + esc(label) + '</td>' 9453+ '<td>' + (Math.round(r.score * 100) / 100) + '</td>' 9454+ '<td>' + esc(roleBits.join(' ⢠')) + '</td>' 9455+ '</tr>'; 9456}).join(''); 9457} 9458} 9459// Store last payload for easy copy/paste to stakeholders 9460window.__CK_DEBUG_LAST__ = { 9461chosen_persona: chosenKey, 9462allow_experimental: allowExperimental, 9463persona_scores: personaRows, 9464product_scores_top: topN, 9465cart_titles: titleList, 9466cart_options: optList, 9467cart_titles_used_for_persona: cartTitleLower, 9468cart_options_text: (cartOptionsText || '').toLowerCase(), 9469cart_full_text: cartFullLower 9470}; 9471} 9472// Update live (cart can change after ajax) 9473renderOverlay(); 9474setInterval(renderOverlay, 800); 9475})(); 9476}); </script>
9476</div><div class="widget widget-static-block"></div><div class="widget widget-static-block"></div>
9476<script> /* Ported from the pre-Zephyr footer's jQuery block. Zephyr ships a jQuery 9477no-op shim, so the original jQuery(document).ready() never fired â see 9478note 1 in the header comment. Only the email-CTA rewrite was live; the 9479#blog and #corporate-accounts branches were dead and are not carried over. */ 9480(function () { 9481"use strict"; 9482var done = false; 9483function apply() { 9484if (done) return true; 9485var s = document.querySelectorAll('.email-cta > strong'); 9486if (s.length < 2) return false; 9487s[1].textContent = 'and SAVE 15%'; 9488done = true; 9489return true; 9490} 9491if (!apply()) { 9492document.addEventListener('DOMContentLoaded', apply); 9493/* the email CTA is injected by a widget block that can render late */ 9494if (window.MutationObserver) { 9495var obs = new MutationObserver(function () { if (apply()) obs.disconnect(); }); 9496obs.observe(document.documentElement, { childList: true, subtree: true }); 9497setTimeout(function () { obs.disconnect(); }, 10000); 9498} 9499} 9500})(); 9501</script>
9501<div class="widget widget-static-block"> <style> /* Hairline light, HARDENED against theme globals. ID-scoped (#fu-foot) for 9502specificity + a child reset so theme font / letter-spacing / text-transform / 9503line-height / margins / list & link styling can't bleed in. Targeted 9504!important only on the props themes tend to override. */ 9505#fu-foot.fu-foot{--fu-accent:var(--brand-primary,#012e4a);--fu-border:#e7e9ec;--fu-sep:#e3e6ea; 9506display:flex!important;align-items:center!important;justify-content:center!important; 9507gap:8px!important;width:100%!important;min-height:46px!important; 9508margin:0!important;padding:9px 12px!important;margin-top:2rem; 9509background:#fff!important;border-top:1px solid var(--fu-border)!important;border-bottom:1px solid var(--fu-border)!important; 9510box-sizing:border-box!important;font-family:inherit!important;list-style:none!important} 9511#fu-foot.fu-foot *{margin:0!important;padding:0!important;box-sizing:border-box!important; 9512font-family:inherit!important;letter-spacing:normal!important;text-transform:none!important; 9513line-height:normal!important;list-style:none!important;text-decoration:none!important;float:none!important} 9514#fu-foot .fu-item{display:flex!important;align-items:center!important;gap:9px!important;flex:1 1 0!important;justify-content:center!important;min-width:0!important} 9515#fu-foot .fu-icon{width:22px!important;height:22px!important;flex:0 0 22px!important;color:var(--fu-accent)!important;display:inline-block!important;vertical-align:middle!important} 9516#fu-foot .fu-head{font-size:13px!important;font-weight:700!important;color:var(--fu-accent)!important;white-space:nowrap!important;line-height:1.2!important} 9517#fu-foot .fu-sep{width:1px!important;height:20px!important;background:var(--fu-sep)!important;flex:0 0 1px!important} 9518/* Copy is approved marketing copy and is never trimmed to fit. nowrap keeps the 9519crisp single line on wide desktop; below 1280px the longest lines wrap instead of 9520overflowing their equal-width column. Measured on live NTW: its slot 1 ("Badges 9521Your Team Will Be Proud to Wear", 38 chars) renders 251px wide. Text space per 9522column is ~430px at 1920, ~250px at 1200, ~270px at 1280 - so 1200 was 1px short 9523and 1280 is the correct boundary. Re-measure if any line gets longer. */ 9524@media(max-width:1280px){ 9525#fu-foot .fu-head{white-space:normal!important} 9526#fu-foot.fu-foot{min-height:56px!important} 9527} 9528/* symbol library â out of the flex container, never rendered */ 9529#fu-defs{position:absolute!important;width:0!important;height:0!important;overflow:hidden!important} 9530@media(max-width:768px){ 9531#fu-foot.fu-foot{flex-wrap:wrap!important;min-height:82px!important} 9532#fu-foot .fu-item{flex:1 1 46%!important;justify-content:flex-start!important} 9533#fu-foot .fu-head{font-size:12px!important;white-space:normal!important} 9534#fu-foot .fu-sep{display:none!important} 9535} </style> <svg id="fu-defs" width="0" height="0" aria-hidden="true" focusable="false"><defs> <symbol id="fu-quality" viewBox="0 0 24 24"><path d="M12 2 4 5v6c0 5 3.4 8.5 8 11 4.6-2.5 8-6 8-11V5l-8-3z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M9 12l2 2 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></symbol> <symbol id="fu-design" viewBox="0 0 24 24"><path d="M4 20l1.2-4L16 5.2 18.8 8 8 18.8 4 20z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M14.5 6.7 17.3 9.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M15.5 4.5 18 2l4 4-2.5 2.5z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol> <symbol id="fu-customization" viewBox="0 0 24 24"><line x1="4" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><line x1="4" y1="12" x2="20" y2="12" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><line x1="4" y1="17" x2="20" y2="17" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="9" cy="7" r="2.4" fill="#fff" stroke="currentColor" stroke-width="1.6"/><circle cx="15" cy="12" r="2.4" fill="#fff" stroke="currentColor" stroke-width="1.6"/><circle cx="8" cy="17" r="2.4" fill="#fff" stroke="currentColor" stroke-width="1.6"/></symbol>
9535 <symbol id="fu-speed" viewBox="0 0 24 24"><path d="M13 2 4 14h6l-1 8 9-12h-6l1-8z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol> <symbol id="fu-compliance" viewBox="0 0 24 24"><rect x="5" y="4.5" width="14" height="16.5" rx="2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M9 3.5h6v3H9z" fill="#fff" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M8.5 13l2 2 4.5-4.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></symbol> <symbol id="fu-shipping" viewBox="0 0 24 24"><path d="M2 6h11v9H2z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M13 9h4l3.2 3.2V15H13z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><circle cx="6.5" cy="17" r="1.8" fill="#fff" stroke="currentColor" stroke-width="1.6"/><circle cx="16.5" cy="17" r="1.8" fill="#fff" stroke="currentColor" stroke-width="1.6"/></symbol> <symbol id="fu-guarantee" viewBox="0 0 24 24"><circle cx="12" cy="9" r="5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M10 9l1.5 1.5L15 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 13l-2 8 5-3 5 3-2-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol> <symbol id="fu-usa" viewBox="0 0 24 24"><line x1="5" y1="3" x2="5" y2="21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M5 4h14l-3 3.5 3 3.5H5z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol> <symbol id="fu-gift" viewBox="0 0 24 24"><rect x="4" y="9.5" width="16" height="10.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M3 9.5h18V13H3z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><line x1="12" y1="6" x2="12" y2="20" stroke="currentColor" stroke-width="1.6"/><path d="M12 6S10.5 3 8.8 4.2 9.5 6.2 12 6z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M12 6s1.5-3 3.2-1.8S14.5 6.2 12 6z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol> <symbol id="fu-star" viewBox="0 0 24 24"><path d="M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 17.8 6.8 19.1l1-5.8L3.5 9.2l5.9-.9z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol> <symbol id="fu-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 7v5l3.5 2" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></symbol> </defs></svg><div class="fu-foot" id="fu-foot" role="list"> <span class="fu-item" role="listitem"><svg class="fu-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><use href="#fu-quality" xlink:href="#fu-quality"></use></svg><span class="fu-head">Passionate About Quality</span></span> <span class="fu-sep" aria-hidden="true"></span> <span class="fu-item" role="listitem"><svg class="fu-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><use href="#fu-design" xlink:href="#fu-design"></use></svg><span class="fu-head">Design Experts</span></span> <span class="fu-sep" aria-hidden="true"></span> <span class="fu-item" role="listitem"><svg class="fu-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><use href="#fu-customization" xlink:href="#fu-customization"></use></svg><span class="fu-head">Easy Customization</span></span> <span class="fu-sep" aria-hidden="true"></span> <span class="fu-item" role="listitem"><svg class="fu-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><use href="#fu-speed" xlink:href="#fu-speed"></use></svg><span class="fu-head">Personalization Fast</span></span></div>
9535<script type="text/javascript"> // <![CDATA[ 9536(function () { 9537"use strict"; 9538var SITE_DATA = { 9539"simplystamps": [["fu-quality","Crafted with Precision"],["fu-guarantee","70+ Years of Stamp Expertise"],["fu-customization","Personalize in a Few Clicks"],["fu-shipping","Ships Next Business Day"]], 9540"customsigns": [["fu-quality","Built to Last"],["fu-customization","Customized Your Way"],["fu-guarantee","Experts Since 1954"],["fu-star","Reliable Business Signs"]], 9541"allstatenotarysupplies": [["fu-guarantee","Trusted Since 2008"],["fu-compliance","Compliance Experts"],["fu-customization","Easy to Customize"],["fu-clock","Ships in 24 Hours"]], 9542"nametagwizard": [["fu-quality","Badges Your Team Will Be Proud to Wear"],["fu-design","Logo Color Matching Included"],["fu-customization","Outfit Your Whole Team in Minutes"],["fu-shipping","Ready in 1-2 Business Days"]], 9543"corpconnect": [["fu-quality","Professional Grade"],["fu-compliance","State Approved"],["fu-customization","Order Your Way"],["fu-shipping","Ships Next Business Day"]], 9544"mykoozie": [["fu-star","100% Official Koozie\u00ae"],["fu-design","Full-Color Printing"],["fu-gift","No Minimums or Fees"],["fu-shipping","Ships in 2 Business Days"]], 9545"904custom": [["fu-quality","Heirloom Quality"],["fu-design","Real Craftspeople"],["fu-customization","Personalize in Minutes"],["fu-shipping","Ships Next Day"]], 9546"hcbrands": [["fu-guarantee","70 Years of Making It Right"],["fu-design","Expert Craftspeople"],["fu-customization","Easy to Personalize"],["fu-shipping","Ships Next Business Day"]] 9547}; 9548// Per-brand icon/text color. Omit a site to inherit var(--brand-primary) with 9549// a navy fallback. MyKoozie is pinned to its theme blue so the strip matches 9550// the footer headers directly beneath it on legacy AND Zephyr. 9551var SITE_ACCENT = { "mykoozie": "#456FB2" }; 9552// Matching below is a SUBSTRING test against the hostname, so a shared staging 9553// host resolves to whichever key it happens to contain â mk.staging.hcbrands.com 9554// would render HCBrands copy, not MyKoozie's. Force any such host here; this map 9555// is consulted first and wins outright. Key = exact lowercase hostname. 9556var HOST_OVERRIDE = { 9557// "mk.staging.hcbrands.com": "mykoozie", 9558// "cs.staging.hcbrands.com": "customsigns" 9559}; 9560try { 9561var root = document.getElementById("fu-foot"); 9562if (!root) return; 9563var host = (location.hostname || "").toLowerCase(); 9564var key = null; 9565if (HOST_OVERRIDE[host] && SITE_DATA[HOST_OVERRIDE[host]]) { 9566key = HOST_OVERRIDE[host]; 9567} else { 9568for (var k in SITE_DATA) { 9569if (Object.prototype.hasOwnProperty.call(SITE_DATA, k) && host.indexOf(k) > -1) { key = k; break; } 9570} 9571} 9572if (!key) return; // unknown host -> keep the generic copy already in the markup 9573var cfg = SITE_DATA[key]; 9574if (SITE_ACCENT[key]) root.style.setProperty("--fu-accent", SITE_ACCENT[key]); 9575var items = root.getElementsByClassName("fu-item"); 9576var seps = root.getElementsByClassName("fu-sep"); 9577for (var i = 0; i < items.length; i++) { 9578if (!cfg[i]) { 9579items[i].style.display = "none"; 9580if (seps[i - 1]) seps[i - 1].style.display = "none"; // no orphan divider 9581continue; 9582} 9583var use = items[i].getElementsByTagName("use")[0]; 9584if (use) { 9585use.setAttribute("href", "#" + cfg[i][0]); 9586use.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#" + cfg[i][0]); 9587} 9588var head = items[i].getElementsByClassName("fu-head")[0]; 9589if (head) head.textContent = cfg[i][1]; 9590} 9591} catch (e) { /* strip keeps its default copy rather than breaking the footer */ } 9592})(); 9593// ]]> </script>
9593</div><div class="bg-white p-12"><div class="max-w-7xl mx-auto"><div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-10"> <nav aria-labelledby="footer-products" class="mb-6 sm:mb-0"><h3 id="footer-products" class="text-base font-bold text-gray-900 mb-5">Products</h3><ul class="space-y-2.5"><li><a href="/address-stamps/view-all" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Address Stamps</a></li><li><a href="/custom-rubber-stamps" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Custom Stamps</a></li><li><a href="/rubber-logo-stamps" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Logo Stamps</a></li><li><a href="/business-office-stamps/signature-stamps" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Signature Stamps</a></li></ul> </nav> <nav aria-labelledby="footer-company" class="mb-6 sm:mb-0"><h3 id="footer-company" class="text-base font-bold text-gray-900 mb-5">Company</h3><ul class="space-y-2.5"><li><a href="/about-us" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">About Us</a></li><li><a href="/buy-in-america" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">American Made</a></li><li><a href="/rubber-stamp-videos" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Videos</a></li><li><a href="/blog" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Blog</a></li></ul> </nav> <nav aria-labelledby="footer-account" class="mb-6 sm:mb-0"><h3 id="footer-account" class="text-base font-bold text-gray-900 mb-5">My Account</h3><ul class="space-y-2.5"><li><a href="/customer/account/login/" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Account</a></li><li><a href="/sales/order/history/" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Reorders</a></li><li><a href="/customer/account/forgotpassword/" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Forgot Password</a></li><li><a href="/contacts" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Track My Order</a></li></ul> </nav> <nav aria-labelledby="footer-support" class="mb-6 sm:mb-0"><h3 id="footer-support" class="text-base font-bold text-gray-900 mb-5">Support</h3><ul class="space-y-2.5"><li><a href="/contacts" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Contact Us</a></li><li><a href="/shipping" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Shipping</a></li><li><a href="/returns" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Returns</a></li><li><a href="/faq" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">FAQ</a></li><li><a href="/accessibility" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Accessibility</a></li></ul> </nav><div class="mb-6 sm:mb-0"><h3 class="text-base font-bold text-gray-900 mb-5">Customer Service</h3> <address class="not-italic"><ul class="space-y-2.5"><li><a href="tel:+18886889998" aria-label="Call customer service at (888) 688-9998" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">(888) 688-9998</a></li><li><span class="text-sm text-gray-700">M-Th: 10am - 4pm ET</span></li><li><a href="mailto:[email protected]" aria-label="Email sales at [email protected]" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">[email protected]</a></li><li><a href="https://www.google.com/maps/search/?api=1&query=2021+St+Augustine+Rd+E+Suite+2+Jacksonville+FL+32207" target="_blank" rel="noopener noreferrer" aria-label="Open address in Google Maps (opens in new tab)" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">
95932021 St Augustine Rd E, Suite 2<br>Jacksonville, FL 32207</a></li></ul> </address></div> <nav aria-labelledby="footer-social" class="mb-6 sm:mb-0"><h3 id="footer-social" class="text-base font-bold text-gray-900 mb-5">Follow Us</h3><ul class="flex flex-wrap gap-3"><li> <a href="https://www.facebook.com/SimplyStamps/" target="_blank" rel="noopener noreferrer" aria-label="Follow Simply Stamps on Facebook (opens in new tab)" class="flex items-center justify-center w-10 h-10 bg-brand-primary rounded-full text-white hover:bg-brand-primary-hover hover:scale-110 focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 transition-all"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M22.675 0h-21.35C.595 0 0 .593 0 1.325v21.351C0 23.407.595 24 1.325 24H12.82v-9.294H9.692v-3.622h3.128V8.413c0-3.1 1.893-4.788 4.659-4.788 1.325 0 2.463.099 2.795.143v3.24l-1.918.001c-1.504 0-1.795.715-1.795 1.763v2.313h3.587l-.467 3.622h-3.12V24h6.116c.73 0 1.323-.593 1.323-1.325V1.325C24 .593 23.405 0 22.675 0z"/></svg> </a></li><li> <a href="https://www.pinterest.com/simplystamps/" target="_blank" rel="noopener noreferrer" aria-label="Follow Simply Stamps on Pinterest (opens in new tab)" class="flex items-center justify-center w-10 h-10 bg-brand-primary rounded-full text-white hover:bg-brand-primary-hover hover:scale-110 focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 transition-all"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z"/></svg> </a></li><li> <a href="https://www.instagram.com/simplystamps/" target="_blank" rel="noopener noreferrer" aria-label="Follow Simply Stamps on Instagram (opens in new tab)" class="flex items-center justify-center w-10 h-10 bg-brand-primary rounded-full text-white hover:bg-brand-primary-hover hover:scale-110 focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 transition-all"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg> </a></li></ul> </nav></div></div></div>
9593<script type="application/ld+json"> { 9594"@context": "http://schema.org", 9595"@type": "LocalBusiness", 9596"name": "SimplyStamps.com", 9597"address": { 9598"@type": "PostalAddress", 9599"streetAddress": "2021 St Augustine Rd E, Suite 2", 9600"addressLocality": "Jacksonville", 9601"addressRegion": "FL", 9602"postalCode": "32207", 9603"addressCountry": "US" 9604}, 9605"geo": { 9606"@type": "GeoCoordinates", 9607"latitude": "30.3220", 9608"longitude": "-81.6690" 9609}, 9610"telephone": "+1-888-688-9998", 9611"email": "[email protected]", 9612"url": "https://www.simplystamps.com", 9613"sameAs": [ 9614"https://www.facebook.com/SimplyStamps", 9615"https://twitter.com/SimplyStamps", 9616"https://www.instagram.com/SimplyStamps", 9617"https://www.pinterest.com/SimplyStamps", 9618"https://www.linkedin.com/company/SimplyStamps" 9619], 9620"openingHoursSpecification": [ 9621{ 9622"@type": "OpeningHoursSpecification", 9623"dayOfWeek": [ 9624"Monday", 9625"Tuesday", 9626"Wednesday", 9627"Thursday" 9628], 9629"opens": "10:00", 9630"closes": "16:00" 9631} 9632], 9633"hasMerchantReturnPolicy": { 9634"@type": "MerchantReturnPolicy", 9635"name": "Return Policy", 9636"url": "https://www.simplystamps.com/returns", 9637"merchantReturnDays": 60, 9638"returnFees": "FreeReturn", 9639"returnMethod": "ReturnByMail", 9640"returnPolicyCategory": "Refundable", 9641"inStoreReturnsOffered": false, 9642"itemCondition": "New", 9643"description": "We offer a straightforward 60-day return policy. Custom-made products are generally non-refundable, but exceptions can be made in specific cases. If a mistake occurs on our part, we will remake the order within 60 days or offer a refund if necessary. Customers may choose to exchange items instead of requesting a refund." 9644} 9645} </script>
9645</div><div itemscope itemtype="https://schema.org/WebSite" class="hidden"><meta itemprop="name" content="Simply Stamps"><link itemprop="url" href="https://www.simplystamps.com/"><div itemprop="potentialAction" itemscope itemtype="https://schema.org/SearchAction"><meta itemprop="target" content="https://www.simplystamps.com/catalogsearch/result/?q={search_term_string}"><meta itemprop="query-input" content="required name=search_term_string"></div></div><div class="bg-gray-800 text-white" itemscope itemtype="https://schema.org/Organization"><meta itemprop="name" content="Simply Stamps"><link itemprop="url" href="https://www.simplystamps.com/"><meta itemprop="logo" content="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <address class="text-center text-sm text-gray-400 not-italic"> © 2026 Simply Stamps. All Rights Reserved.<p><a href="/terms-and-conditions">Terms & Conditions</a> | <a href="/privacy-policy">Privacy</a></p> </address></div></div> </footer> <style> @keyframes shake { 96460%, 100% { transform: translateX(0); } 964710%, 30%, 50%, 70%, 90% { transform: translateX(-4px); } 964820%, 40%, 60%, 80% { transform: translateX(4px); } 9649} 9650.shake { 9651animation: shake 0.5s ease-in-out; 9652} </style><div 9653x-data 9654x-show="$store.cart.drawerOpen" 9655@click="$store.cart.closeDrawer()" 9656x-transition:enter="transition-opacity ease-out duration-300" 9657x-transition:enter-start="opacity-0" 9658x-transition:enter-end="opacity-100" 9659x-transition:leave="transition-opacity ease-in duration-200" 9660x-transition:leave-start="opacity-100" 9661x-transition:leave-end="opacity-0" 9662class="fixed inset-0 z-40 transition-all duration-500" 9663:class="$store.cart.drawerFocused ? 'bg-black/15' : 'bg-transparent'" 9664style="display: none;" 9665></div><div 9666x-data 9667x-show="$store.cart.drawerOpen" 9668@click.away="if (!$store.imagePreview?.isOpen && !$store.promo?.isOpen && !$store.confirm?.isOpen && !document.getElementById('wizardModal')?._x_dataStack?.[0]?.isOpen) $store.cart.closeDrawer()" 9669@mouseenter="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = true }, 215)" 9670@mouseleave="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = false }, 100)" 9671x-transition:enter="transition ease-out duration-300" 9672x-transition:enter-start="translate-x-full" 9673x-transition:enter-end="translate-x-0" 9674x-transition:leave="transition ease-in duration-200" 9675x-transition:leave-start="translate-x-0" 9676x-transition:leave-end="translate-x-full" 9677:style="{ top: $store.ui.getHeaderOffset() + 'px' }" 9678class="fixed right-0 bottom-0 w-full md:w-[26rem] bg-white shadow-2xl z-50 flex flex-col transition-all duration-300" 9679style="display: none;" 9680><div class="flex flex-col h-full relative"><div class="flex items-center justify-between px-4 py-2 border-b border-gray-200"> <span class="text-lg font-bold text-gray-900">Shopping Cart</span> <button 9681@click="$store.cart.closeDrawer()" 9682class="p-2 hover:bg-gray-100 rounded-lg transition-colors" 9683aria-label="Close cart" 9684> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6"> <path d="M18 6L6 18" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 6L18 18" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button></div><div 9685x-show="$store.cart.isRefreshing" 9686x-cloak 9687x-transition:enter="transition ease-out duration-200" 9688x-transition:enter-start="opacity-0" 9689x-transition:enter-end="opacity-100" 9690x-transition:leave="transition ease-in duration-150" 9691x-transition:leave-start="opacity-100" 9692x-transition:leave-end="opacity-0" 9693class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center z-10" 9694><div class="flex flex-col items-center gap-3"> <svg class="animate-spin h-10 w-10 text-brand-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> <span class="text-sm font-medium text-gray-700">
9694Updating cart...</span></div></div><div class="flex-1 min-h-0 overflow-hidden relative" 9695x-data="{ 9696canScroll: false, 9697atBottom: false, 9698checkScroll() { 9699const el = this.$refs.scrollContainer; 9700if (!el) return; 9701this.canScroll = el.scrollHeight > el.clientHeight + 10; 9702this.atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 20; 9703}, 9704scrollToLastItem() { 9705const container = this.$refs.scrollContainer; 9706if (!container) return; 9707const items = container.querySelectorAll('[data-cart-item-id]'); 9708if (!items.length) return; 9709const lastItem = items[items.length - 1]; 9710const cRect = container.getBoundingClientRect(); 9711const iRect = lastItem.getBoundingClientRect(); 9712if (iRect.bottom > cRect.bottom || iRect.top < cRect.top) { 9713lastItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 9714} 9715this.checkScroll(); 9716} 9717}" 9718x-init="$nextTick(() => checkScroll()); $watch(() => $store.cart.drawerOpen, (open) => { if (open) $nextTick(() => checkScroll()) })" 9719@cart-refreshed.window="$nextTick(() => { checkScroll(); if ($store.cart.drawerOpen && $store.cart._scrollToNew) { scrollToLastItem(); $store.cart._scrollToNew = false; } })"><div class="absolute inset-0 overflow-y-auto" 9720id="cart-items-container" 9721x-ref="scrollContainer" 9722@scroll.throttle.100ms="checkScroll()" 9723@resize.window.throttle.200ms="checkScroll()"><div class="text-center py-12"> <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /> </svg><p class="mt-4 text-gray-500">Your cart is empty</p></div></div><div x-show="canScroll && !atBottom" 9724x-transition:enter="transition ease-out duration-200" 9725x-transition:enter-start="opacity-0" 9726x-transition:enter-end="opacity-100" 9727x-transition:leave="transition ease-in duration-150" 9728x-transition:leave-start="opacity-100" 9729x-transition:leave-end="opacity-0" 9730class="absolute bottom-0 left-0 right-0"><div class="h-12 bg-gradient-to-t from-white via-white/80 to-transparent pointer-events-none"></div> <button type="button" 9731@click.prevent.stop=" 9732const container = document.getElementById('cart-items-container'); 9733if (!container) return; 9734const items = container.querySelectorAll('[data-cart-item-id]'); 9735if (!items.length) return; 9736const containerTop = container.getBoundingClientRect().top; 9737let target = null; 9738const containerBottom = container.getBoundingClientRect().bottom; 9739for (let i = 0; i < items.length; i++) { 9740if (items[i].getBoundingClientRect().bottom > containerBottom) { 9741target = items[i]; 9742break; 9743} 9744} 9745if (target) { 9746const scrollAmount = target.getBoundingClientRect().top - containerTop; 9747container.scrollBy({ top: scrollAmount, behavior: 'smooth' }); 9748} 9749" 9750class="absolute bottom-2 left-1/2 -translate-x-1/2 animate-bounce p-1 rounded-full hover:bg-gray-100 transition-colors cursor-pointer" 9751aria-label="Scroll to next item"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> </svg> </button></div></div><div class="p-4" id="cart-footer" 9752x-data 9753x-init="$store.cart.hasError = false; $store.cart.subtotal = "$0.00"; $store.cart.originalSubtotal = null; $store.cart.grandTotal = "$0.00"; $store.cart.totals = []; $store.cart.couponCode = ""; $store.cart.expeditedProduct = {"id":"12060","price":"$7.99","add_url":"https:\/\/www.simplystamps.com\/checkout\/cart\/add\/uenc\/aHR0cHM6Ly93d3cuc2ltcGx5c3RhbXBzLmNvbS9jdXN0b21lci9hY2NvdW50L2xvZ2luLw,,\/product\/12060\/form_key\/e3374479ab86b5888526972805eb3d8f\/qty\/1\/"}; $store.cart.expeditedInCart = false; $store.cart.totalSavings = null; $store.cart.savings = {"label":null,"has_savings":false,"sale_savings":null,"price_adjustments":[],"your_price":null}"> <template x-if="!$store.cart.isEmpty"><div><div class="mb-3 text-right"> <button 9754onclick="clearCart(); return false;" 9755class="text-xs text-red-600 hover:text-red-700 hover:underline transition-colors">
9755 Clear Cart </button></div><div x-data="{ threshold: 79 }" 9756x-show="$store.cart.subtotalNumeric > 0 && $store.cart.subtotalNumeric >= threshold" 9757x-cloak 9758class="mb-3"><div class="flex items-center justify-center gap-1.5"> <span class="text-sm font-semibold text-green-700">You qualify for FREE economy shipping!</span> <span class="text-lg">ð</span></div></div><div class="space-y-2 mb-2"><div class="flex items-center justify-between"> <span class="text-sm text-gray-600">Subtotal:</span> <span class="text-sm font-semibold text-gray-900" x-text="$store.cart.originalSubtotal ? $store.cart.originalSubtotal : $store.cart.subtotal"></span></div> <template x-if="$store.cart.originalSubtotal && $store.cart.savings && $store.cart.savings.sale_savings"><div class="flex items-center justify-between"> <span class="text-sm text-green-600">Savings:</span> <span class="text-sm font-semibold text-green-600" x-text="$store.cart.savings.sale_savings"></span></div> </template> <template x-for="adj in ($store.cart.savings && $store.cart.savings.price_adjustments ? $store.cart.savings.price_adjustments : [])" :key="adj.code"><div class="flex items-center justify-between"> <span class="text-sm" :class="adj.is_discount ? 'text-green-600' : 'text-gray-600'" x-text="adj.title + ':'"></span> <span class="text-sm font-semibold" :class="adj.is_discount ? 'text-green-600' : 'text-gray-900'" x-text="adj.value"></span></div> </template> <template x-if="$store.cart.savings && $store.cart.savings.your_price"><div class="flex items-center justify-between"> <span class="text-sm text-gray-600">Your Price:</span> <span class="text-sm font-semibold text-green-900" x-text="$store.cart.savings.your_price"></span></div> </template> <template x-for="total in $store.cart.totals" :key="total.code"><div class="flex items-center justify-between"> <span class="text-sm text-gray-600" x-text="total.title + ':'"></span> <span class="text-sm font-semibold text-gray-900" x-text="total.value"></span></div> </template><div class="flex items-center justify-between border-t pt-2"> <span class="text-lg font-semibold text-gray-900">Total:</span> <span class="text-2xl font-bold text-gray-900" x-text="$store.cart.grandTotal"></span></div><div class="pt-1 flex gap-2"><div class="flex-1"> <template x-if="$store.cart.couponCode"><div class="h-full flex items-center justify-between p-2 bg-green-50 rounded-lg border border-green-200"><div class="flex items-center gap-2"> <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> </svg> <span class="text-sm font-medium text-green-800" x-text="$store.cart.couponCode.toUpperCase()"></span></div> <button 9759type="button" 9760@click="$store.promo.remove()" 9761class="text-xs text-green-700 hover:text-green-900 underline" 9762>Remove</button></div> </template> <template x-if="!$store.cart.couponCode"> <button 9763type="button" 9764@click="$store.promo.open()" 9765class="h-full w-full flex items-center justify-center gap-2 py-2.5 text-sm text-gray-600 hover:text-gray-900 border border-dashed border-gray-300 rounded-lg hover:border-gray-400 transition-colors" 9766> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> </svg> <span>Add Promo Code</span> </button> </template></div> <template x-if="$store.cart.expeditedProduct && !$store.cart.expeditedInCart"><div class="flex-1 relative" x-data="{ showTip: false }"><div x-show="showTip" x-cloak 9767x-transition:enter="transition ease-out duration-200" 9768x-transition:enter-start="opacity-0 translate-y-1" 9769x-transition:enter-end="opacity-100 translate-y-0" 9770x-transition:leave="transition ease-in duration-150" 9771x-transition:leave-start="opacity-100 translate-y-0" 9772x-transition:leave-end="opacity-0 translate-y-1" 9773class="absolute bottom-full left-1/2 -translate-x-1/2 -ml-5 mb-3 z-20 pointer-events-none w-56"><div class="bg-brand-primary text-white text-sm rounded-lg px-3 py-2 shadow-lg text-center leading-snug relative"> <span x-text="'â¡ Send your order to the front of the production line for just ' + $store.cart.expeditedProduct.price + '!'"></span><div class="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent" style="border-top-color: var(--brand-primary)"></div></div></div> <button 9774type="button" 9775onclick="addExpeditedToCart()" 9776@mouseenter="showTip = true"
9777@mouseleave="showTip = false" 9778class="w-full flex items-center justify-center gap-2 py-2.5 text-sm font-medium border border-brand-primary text-brand-primary bg-white rounded-lg hover:bg-brand-primary/10 transition-colors"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> </svg> <span x-text="'Add Expedite ' + $store.cart.expeditedProduct.price"></span> </button></div> </template></div></div><p x-show="$store.cart.hasError" x-cloak class="mb-2 text-xs text-amber-800 text-center" :class="$store.cart.nudged ? 'font-bold' : ''">Edit the marked items to make your selection(s) before continuing to checkout.</p> <a href="https://www.simplystamps.com/checkout/onepage/" 9779@click.prevent="$store.cart.hasError ? $store.cart.shake($event.currentTarget) : $store.upsell.maybeProceed('https://www.simplystamps.com/checkout/onepage/')" 9780class="btn-crt flex items-center justify-center gap-2 w-full text-white hover:text-white font-semibold py-3 rounded-lg transition-colors mb-2"> <span>Proceed to Checkout</span> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" /> </svg> </a> <a href="https://www.simplystamps.com/checkout/cart/" 9781class="block w-full text-center text-sm border border-brand-success text-brand-success hover:bg-brand-success hover:text-white font-medium py-1.5 rounded-lg transition-colors"> View Full Cart </a></div> </template> <template x-if="$store.cart.isEmpty"> <button 9782@click="$store.cart.closeDrawer()" 9783class="w-full bg-gray-600 hover:bg-gray-700 text-white font-semibold py-3 rounded-lg transition-colors"> Continue Shopping </button> </template></div></div></div><div 9784x-data 9785x-show="$store.promo.isOpen" 9786x-cloak 9787class="fixed inset-0 z-[100] overflow-y-auto" 9788aria-labelledby="promo-modal-title" 9789role="dialog" 9790aria-modal="true" 9791><div 9792x-show="$store.promo.isOpen" 9793x-transition:enter="transition ease-out duration-200" 9794x-transition:enter-start="opacity-0" 9795x-transition:enter-end="opacity-100" 9796x-transition:leave="transition ease-in duration-150" 9797x-transition:leave-start="opacity-100" 9798x-transition:leave-end="opacity-0" 9799@click="$store.promo.close()" 9800class="fixed inset-0 bg-black/50 backdrop-blur-sm" 9801></div><div @click.self="$store.promo.close()" class="fixed inset-0 flex items-center justify-center p-4"><div 9802x-show="$store.promo.isOpen" 9803x-transition:enter="transition ease-out duration-200" 9804x-transition:enter-start="opacity-0 scale-95" 9805x-transition:enter-end="opacity-100 scale-100" 9806x-transition:leave="transition ease-in duration-150" 9807x-transition:leave-start="opacity-100 scale-100" 9808x-transition:leave-end="opacity-0 scale-95" 9809@click.stop 9810class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6" 9811> <button 9812@click="$store.promo.close()" 9813type="button" 9814class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 9815> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button><div 9816x-show="$store.promo.success" 9817x-transition:enter="transition ease-out duration-300" 9818x-transition:enter-start="opacity-0 scale-95" 9819x-transition:enter-end="opacity-100 scale-100" 9820class="flex flex-col items-center justify-center py-4" 9821><div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-4"> <svg class="h-10 w-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" /> </svg></div><h3 class="text-xl font-semibold text-gray-900 text-center mb-2" x-text="$store.promo.successMessage"></h3><div class="flex items-center gap-2 px-4 py-2 bg-green-50 rounded-lg border border-green-200 mb-2"> <svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> </svg> <span class="text-lg font-bold text-green-800 uppercase" x-text="$store.promo.appliedCode"></span></div><p x-show="$store.promo.discount" class="text-sm text-gray-600"> You save <span class="font-semibold text-green-700" x-text="$store.promo.discount"></span></p></div><div x-show="!$store.promo.success"><div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4"> <svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> </svg></div><h3 id="promo-modal-title" class="text-lg font-semibold text-gray-900 text-center mb-2"> Enter Promo Code</h3><p class="text-sm text-gray-600 text-center mb-4"> Have a promo code? Enter it below to apply your discount.</p><form @submit.prevent="$store.promo.apply()" class="space-y-4"><div class="relative"> <label for="promo-code-input" class="sr-only">Promo Code</label> <input 9822type="text" 9823id="promo-code-input" 9824x-model="$store.promo.code" 9825placeholder="Enter promo code" 9826class="w-full pl-12 pr-12 py-3 text-center text-lg font-medium uppercase tracking-wider border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary focus:border-transparent placeholder:normal-case placeholder:tracking-normal placeholder:font-normal placeholder:text-base" 9827:disabled="$store.promo.isLoading" 9828@keydown.escape="$store.promo.close()" 9829/> <button 9830type="button" 9831@click="navigator.clipboard.readText().then(text => { $store.promo.code = text.trim(); })" 9832class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-gray-600 transition-colors rounded-md hover:bg-gray-100" 9833title="Paste from clipboard" 9834:disabled="$store.promo.isLoading" 9835> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> </svg> </button></div><div 9836x-show="$store.promo.error" 9837x-transition 9838class="text-sm text-red-600 text-center" 9839x-text="$store.promo.error" 9840></div><div class="flex gap-3"> <button 9841@click="$store.promo.close()" 9842type="button" 9843class="flex-1 px-4 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors" 9844:disabled="$store.promo.isLoading" 9845> Cancel </button> <button 9846type="submit" 9847class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-brand-primary rounded-lg hover:bg-brand-primary-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed" 9848:disabled="$store.promo.isLoading || !$store.promo.code.trim()" 9849> <span x-show="!$store.promo.isLoading">
9849Apply Code</span> <span x-show="$store.promo.isLoading" class="flex items-center justify-center gap-2"> <svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Applying... </span> </button></div></form></div></div></div></div><div 9850x-data="{ open: false }" 9851@expedited-added.window="open = true" 9852x-show="open" 9853x-cloak 9854class="fixed inset-0 z-[100] overflow-y-auto" 9855aria-labelledby="expedited-modal-title" 9856role="dialog" 9857aria-modal="true" 9858><div 9859x-show="open" 9860x-transition:enter="transition ease-out duration-200" 9861x-transition:enter-start="opacity-0" 9862x-transition:enter-end="opacity-100" 9863x-transition:leave="transition ease-in duration-150" 9864x-transition:leave-start="opacity-100" 9865x-transition:leave-end="opacity-0" 9866@click="open = false" 9867class="fixed inset-0 bg-black/50 backdrop-blur-sm" 9868></div><div @click.self="open = false" class="fixed inset-0 flex items-center justify-center p-4"><div 9869x-show="open" 9870x-transition:enter="transition ease-out duration-200" 9871x-transition:enter-start="opacity-0 scale-95" 9872x-transition:enter-end="opacity-100 scale-100" 9873x-transition:leave="transition ease-in duration-150" 9874x-transition:leave-start="opacity-100 scale-100" 9875x-transition:leave-end="opacity-0 scale-95" 9876@click.stop 9877class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6" 9878> <button 9879@click="open = false" 9880type="button" 9881class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 9882> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button><div class="flex flex-col items-center justify-center py-4"><div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-4"> <svg class="h-10 w-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" /> </svg></div><h3 id="expedited-modal-title" class="text-xl font-semibold text-gray-900 text-center mb-2"> Added to your cart!</h3><div class="flex items-center gap-2 px-4 py-2 bg-green-50 rounded-lg border border-green-200 mb-2"> <svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> </svg> <span class="text-lg font-bold text-green-800">Expedited Processing</span></div><p class="text-sm text-gray-600 text-center"> Your order will now be sent to the front of the production queue.</p> <button 9883@click="open = false" 9884type="button" 9885class="mt-4 px-6 py-2 text-sm font-medium text-white bg-brand-primary rounded-lg hover:bg-brand-primary-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-primary transition-colors" 9886> Close </button></div></div></div></div><div 9887x-data 9888x-show="$store.upsell.isOpen" 9889x-cloak 9890@keydown.escape.window="$store.upsell.isOpen && $store.upsell.skip()" 9891class="fixed inset-0 z-[120] overflow-y-auto" 9892role="dialog" 9893aria-modal="true" 9894aria-labelledby="upsell-modal-title" 9895style="display: none;" 9896><div 9897x-show="$store.upsell.isOpen" 9898x-transition:enter="transition ease-out duration-200" 9899x-transition:enter-start="opacity-0" 9900x-transition:enter-end="opacity-100" 9901x-transition:leave="transition ease-in duration-150" 9902x-transition:leave-start="opacity-100" 9903x-transition:leave-end="opacity-0" 9904@click="$store.upsell.skip()" 9905class="fixed inset-0 bg-black/50 backdrop-blur-sm" 9906></div><div class="fixed inset-0 flex items-center justify-center p-4"><div 9907x-show="$store.upsell.isOpen" 9908x-transition:enter="transition ease-out duration-200" 9909x-transition:enter-start="opacity-0 scale-95" 9910x-transition:enter-end="opacity-100 scale-100" 9911x-transition:leave="transition ease-in duration-150" 9912x-transition:leave-start="opacity-100 scale-100" 9913x-transition:leave-end="opacity-0 scale-95" 9914@click.stop 9915class="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col" 9916><div class="flex items-start justify-between gap-4 p-5 border-b border-gray-200"><div><h3 id="upsell-modal-title" class="text-[1.52rem] font-bold text-gray-900" x-text="$store.upsell.header || 'You might also like'"></h3><p x-show="$store.upsell.subheader" class="text-[1.18rem] text-gray-500 mt-1" x-text="$store.upsell.subheader"></p></div> <button @click="$store.upsell.skip()" type="button" class="text-gray-400 hover:text-gray-600 transition-colors p-1 -m-1" aria-label="Close"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button></div><div class="p-5 overflow-y-auto grid grid-cols-2 sm:grid-cols-3 gap-4"> <template x-for="product in $store.upsell.products" :key="product.id"><div class="relative border rounded-lg p-3 flex flex-col bg-white transition-shadow hover:shadow-md" 9917:class="product.addable && $store.upsell.isSelected(product.id) ? 'ring-2 ring-brand-primary border-brand-primary' : 'border-gray-200'"> <a :href="product.url" target="_blank" class="block aspect-square mb-2 rounded overflow-hidden bg-gray-50"> <img :src="product.image" :alt="product.name" class="w-full h-full object-contain" /> </a> <a :href="product.url" target="_blank" class="block text-sm font-medium text-gray-900 hover:text-brand-primary line-clamp-2 mb-1 no-underline" x-text="product.name"></a><div class="mb-2"> <template x-if="product.has_discount"><div class="flex items-baseline gap-1.5"> <span class="text-gray-400 line-through text-xs font-normal" x-html="product.original_price"></span>
9917 <span class="text-sm font-semibold text-gray-900" x-html="product.price_html"></span></div> </template> <template x-if="!product.has_discount"><div class="text-sm font-semibold text-gray-900" x-html="product.price_html"></div> </template></div><div class="mt-auto"> <template x-if="product.addable"> <button type="button" 9918@click="$store.upsell.toggle(product.id)" 9919class="w-full flex items-center justify-center gap-1.5 py-2 text-sm font-medium rounded-lg border transition-colors" 9920:class="$store.upsell.isSelected(product.id) ? 'bg-brand-primary text-white border-brand-primary hover:bg-brand-primary-hover' : 'bg-white text-brand-primary border-brand-primary hover:bg-brand-primary/10'"> <svg x-show="$store.upsell.isSelected(product.id)" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> </svg> <svg x-show="!$store.upsell.isSelected(product.id)" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> </svg> <span x-text="$store.upsell.isSelected(product.id) ? 'Added' : 'Add'"></span> </button> </template> <template x-if="!product.addable"> <a :href="product.customize_url" 9921class="w-full flex items-center justify-center gap-1.5 py-2 text-sm font-medium rounded-lg border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 transition-colors no-underline"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> </svg> Customize </a> </template></div></div> </template></div><div class="p-5 border-t border-gray-200 flex flex-col sm:flex-row gap-3 sm:justify-between sm:items-center"> <button type="button" @click="$store.upsell.skip()" 9922class="order-2 sm:order-1 text-sm text-gray-500 hover:text-gray-700 hover:underline transition-colors"> No thanks, continue to checkout </button> <button type="button" @click="$store.upsell.addSelectedAndCheckout()" 9923:disabled="$store.upsell.isLoading" 9924class="order-1 sm:order-2 btn-crt flex items-center justify-center gap-2 text-white hover:text-white font-semibold py-3 px-6 rounded-lg transition-colors disabled:opacity-60"> <span x-show="!$store.upsell.selected.length">Continue to Checkout</span> <span x-show="$store.upsell.selected.length" x-text="'Add ' + $store.upsell.selected.length + ' & Continue'"></span> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" /> </svg> </button></div></div></div></div><div x-data="{ open: false }" 9925x-show="$store.auth.isStaff" 9926x-cloak 9927@mouseenter="open = true" 9928@mouseleave="open = false" 9929class="fixed left-0 top-3/4 -translate-y-1/2 z-[9999] hidden lg:block" 9930style="display: none;"><div x-show="!open" 9931x-transition:enter="transition ease-out duration-200" 9932x-transition:enter-start="opacity-0 -translate-x-2" 9933x-transition:enter-end="opacity-100 translate-x-0" 9934x-transition:leave="transition ease-in duration-150" 9935x-transition:leave-start="opacity-100" 9936x-transition:leave-end="opacity-0" 9937class="w-8 bg-blue-600 rounded-r-lg flex flex-col items-center justify-center cursor-pointer shadow-lg py-4 gap-3"> <svg class="w-5 h-5 text-white flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/> </svg> <span class="text-white text-sm font-medium tracking-wide" style="writing-mode: vertical-rl; text-orientation: mixed;">Mage Admin Quick Links</span></div><div x-show="open" 9938x-transition:enter="transition ease-out duration-200" 9939x-transition:enter-start="opacity-0 -translate-x-4" 9940x-transition:enter-end="opacity-100 translate-x-0" 9941x-transition:leave="transition ease-in duration-150" 9942x-transition:leave-start="opacity-100 translate-x-0" 9943x-transition:leave-end="opacity-0 -translate-x-4" 9944class="absolute left-0 top-1/2 -translate-y-1/2 bg-white rounded-r-lg shadow-xl p-4 min-w-[220px]"><div class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-3 pb-2 border-b border-gray-200"> Mage Admin Quick Links</div><ul class="space-y-1"><li> <a :href="$store.auth.adminEditBase ? $store.auth.adminEditBase + 'type/config/id/hsc_zephyr/store/sstamps_en/' : '#'" 9945target="_blank" 9946class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:text-brand-primary hover:bg-gray-100 rounded-md transition-colors">
9946 <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> </svg> Zephyr Theme Settings </a></li><li class="border-t border-gray-200 pt-2 mt-2"> <button @click="$store.navEdit.open()" 9947class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:text-brand-primary hover:bg-gray-100 rounded-md transition-colors w-full text-left"> <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/> </svg> Edit Site Navigation </button></li></ul></div></div>
9947<script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() { 9948Mage.Cookies.set( 9949'FASTLY_CDN_ENV', 9950'250e7c153723fdf375c9db11b6b2b305', 9951new Date(new Date().getTime() + 86400000) 9952); 9953}); </script>
9953
9953<script type="text/javascript"> (function () { 9954var name = 'FASTLY_CDN_FORMKEY'; 9955if (document.cookie.indexOf(name + '=') !== -1) { 9956return; 9957} 9958var expires = new Date(new Date().getTime() + 86400000); 9959document.cookie = name + '=' + 'e3374479ab86b5888526972805eb3d8f' 9960+ '; expires=' + expires.toUTCString() 9961+ '; path=/' + '; domain=.www.simplystamps.com'; 9962})(); </script>
9962<div id="wizardModal" 9963x-data="{ isOpen: false, wizardUrl: '', showConfirm: false, showResume: false, isLoading: true, hasSession: false, pendingUrl: '' }" 9964x-show="isOpen || showResume" 9965x-cloak 9966@keydown.escape.window="showConfirm ? showConfirm = false : (showResume ? showResume = false : null)" 9967class="fixed inset-0 z-50 overflow-hidden" 9968style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 transition-opacity" 9969@click="if (showResume) { showResume = false; } else { isOpen = false; }"></div><div class="fixed inset-0" x-show="isOpen" style="display: none;"><div class="w-full h-full bg-white overflow-hidden" id="wizardIframeContainer"><div x-show="isLoading" 9970x-transition:leave="transition ease-in duration-300" 9971x-transition:leave-start="opacity-100" 9972x-transition:leave-end="opacity-0" 9973class="absolute inset-0 flex items-center justify-center bg-white z-10"><div class="flex flex-col items-center gap-4"> <svg class="animate-spin h-12 w-12 text-brand-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> <span class="text-gray-600 font-medium">Loading Designer...</span></div></div> <template x-if="wizardUrl"> <iframe id="wizard_wrapper" 9974:src="wizardUrl" 9975@load="isLoading = false; clearTimeout(window._wizardLoadTimeout);" 9976class="w-full h-full" 9977frameborder="0" 9978allow="clipboard-read; clipboard-write" 9979title="Product Customization Wizard"> </iframe> </template></div></div><div x-show="showResume" 9980x-transition:enter="transition ease-out duration-200" 9981x-transition:enter-start="opacity-0" 9982x-transition:enter-end="opacity-100" 9983x-transition:leave="transition ease-in duration-150" 9984x-transition:leave-start="opacity-100" 9985x-transition:leave-end="opacity-0" 9986class="fixed inset-0 z-[60] flex items-center justify-center p-4" 9987style="display: none;"><div class="absolute inset-0 bg-black bg-opacity-50" @click="showResume = false"></div><div class="relative bg-white rounded-lg shadow-xl max-w-md w-full p-6" 9988x-transition:enter="transition ease-out duration-200" 9989x-transition:enter-start="opacity-0 scale-95" 9990x-transition:enter-end="opacity-100 scale-100" 9991x-transition:leave="transition ease-in duration-150" 9992x-transition:leave-start="opacity-100 scale-100" 9993x-transition:leave-end="opacity-0 scale-95"><div class="flex items-center mb-4"><div class="flex-shrink-0 w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center"> <svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path> </svg></div><h3 class="ml-3 text-lg font-semibold text-gray-900">Resume Design?</h3></div><p class="text-gray-600 mb-6">You have an in-progress design. Would you like to pick up where you left off, or start fresh?</p><div class="flex justify-end gap-3"> <button @click="showResume = false; hasSession = false; wizardUrl = ''; isLoading = true; isOpen = true; $nextTick(() => { window._wizardStartNew(pendingUrl); })" 9994class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"> 9995Start New Design </button> <button @click="showResume = false; isOpen = true;" 9996class="px-4 py-2 text-sm font-medium text-white bg-brand-primary rounded-lg hover:bg-brand-primary/90 transition-colors"> Continue Design </button></div></div></div><div x-show="showConfirm" 9997x-transition:enter="transition ease-out duration-200" 9998x-transition:enter-start="opacity-0" 9999x-transition:enter-end="opacity-100" 10000x-transition:leave="transition ease-in duration-150" 10001x-transition:leave-start="opacity-100" 10002x-transition:leave-end="opacity-0" 10003class="fixed inset-0 z-[60] flex items-center justify-center p-4" 10004style="display: none;"><div class="absolute inset-0 bg-black bg-opacity-50" @click="showConfirm = false"></div><div class="relative bg-white rounded-lg shadow-xl max-w-md w-full p-6" 10005x-transition:enter="transition ease-out duration-200" 10006x-transition:enter-start="opacity-0 scale-95" 10007x-transition:enter-end="opacity-100 scale-100" 10008x-transition:leave="transition ease-in duration-150" 10009x-transition:leave-start="opacity-100 scale-100" 10010x-transition:leave-end="opacity-0 scale-95"><div class="flex items-center mb-4"><div class="flex-shrink-0 w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center"> <svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path> </svg></div><h3 class="ml-3 text-lg font-semibold text-gray-900">Close Designer</h3></div><p class="text-gray-600 mb-6">Are you sure you want to close the designer? Your design will be saved for this session â you can resume it if you reopen the designer.</p><div class="flex justify-end gap-3"> <button @click="showConfirm = false" 10011class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"> Cancel </button> <button @click="showConfirm = false; $nextTick(() => { isOpen = false; hasSession = true; window._wizardOnClose(false); })" 10012class="px-4 py-2 text-sm font-medium text-white bg-brand-primary rounded-lg hover:bg-brand-primary/90 transition-colors"> 10013Close Designer </button></div></div></div></div>
10013<script> window._wizardLoadTimeout = null;// Internal: load a fresh wizard URL into the iframe 10014window._wizardStartNew = function(url) { 10015var modal = document.getElementById('wizardModal'); 10016if (!modal || !modal._x_dataStack) return; 10017var data = modal._x_dataStack[0]; 10018data.wizardUrl = ''; 10019setTimeout(function() { 10020data.wizardUrl = url; 10021}, 0); 10022// Fallback: if iframe hasn't loaded after 30s, show error state 10023clearTimeout(window._wizardLoadTimeout); 10024window._wizardLoadTimeout = setTimeout(function() { 10025if (data.isLoading && data.isOpen) { 10026console.error('[Wizard] Load timeout â iframe did not respond'); 10027data.isLoading = false; 10028data.hasSession = false; 10029var iframe = document.getElementById('wizard_wrapper'); 10030if (iframe) { 10031iframe.srcdoc = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;flex-direction:column;gap:16px;color:#374151;">' 10032+ '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>' 10033+ '<p style="font-size:18px;font-weight:600;margin:0;">Something went wrong</p>' 10034+ '<p style="font-size:14px;color:#6b7280;margin:0;">The designer took too long to load.</p>' 10035+ '<button onclick="window.parent.closeWizardModal(false)" style="margin-top:8px;padding:8px 20px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer;">Close & Try Again</button>' 10036+ '</div>'; 10037} 10038} 10039}, 30000); 10040};// Set when a customer signs in or registers inside the wizard iframe. 10041// 10042// The ajax-login module calls parent.LogInParent() across the frame boundary after a 10043// successful login or registration. Defining that name here is what makes the call land 10044// on Zephyr â without it the notification is silently dropped, since this theme has its 10045// own wizard modal and never loads the PCE one that defines LogInParent. 10046window._wizardAuthChanged = false; 10047window.LogInParent = function() { 10048console.log('[Wizard] Customer authenticated inside the iframe'); 10049window._wizardAuthChanged = true; 10050};// Internal: handle post-close actions (cart refresh etc) 10051window._wizardOnClose = function(refreshCart) { 10052clearTimeout(window._wizardLoadTimeout);// The page behind the wizard was rendered for a logged-out visitor, so everything 10053// keyed off login state is stale â most visibly the My Account entry in the nav. 10054// A reload also picks up the cart, so it supersedes the refresh below. 10055if (window._wizardAuthChanged) { 10056window._wizardAuthChanged = false; 10057console.log('[Wizard] Auth changed inside wizard, reloading host page'); 10058window.location.reload(); 10059return; 10060}if (refreshCart && window.Alpine && Alpine.store('cart')) { 10061setTimeout(function() { 10062console.log('[Wizard] Refreshing cart data...'); 10063Alpine.store('cart').refresh(); 10064}, 300); 10065} 10066};// Global function to open wizard modal 10067window.openWizardModal = function(url) { 10068console.log('[Wizard] Opening modal with URL:', url); 10069var modal = document.getElementById('wizardModal'); 10070if (modal && modal._x_dataStack) { 10071var data = modal._x_dataStack[0]; 10072// If there's an existing session, ask user what to do 10073if (data.hasSession && data.wizardUrl) { 10074data.pendingUrl = url; 10075data.showResume = true; 10076return; 10077} 10078// Fresh open â load the wizard 10079data.isLoading = true; 10080data.isOpen = true; 10081data.hasSession = false; 10082window._wizardStartNew(url); 10083} else { 10084console.error('[Wizard] Modal not found or Alpine.js not initialized'); 10085} 10086};// Allow wizard iframe to close the modal and optionally refresh cart 10087window.closeWizardModal = function(refreshCart) { 10088if (typeof refreshCart === 'undefined') refreshCart = false; 10089console.log('[Wizard] Closing modal, refresh cart:', refreshCart); 10090var modal = document.getElementById('wizardModal'); 10091if (modal && modal._x_dataStack) { 10092var data = modal._x_dataStack[0]; 10093data.isOpen = false; 10094// If the wizard completed (added to cart), destroy the session 10095if (refreshCart) { 10096data.hasSession = false; 10097data.wizardUrl = ''; 10098} else { 10099data.hasSession = true; 10100} 10101window._wizardOnClose(refreshCart); 10102} 10103};// Legacy compatibility: alias for old PCE modal functions 10104window.closeModalWindow = function() { 10105window.closeWizardModal(true); 10106};// Minimal jQuery shim for PCE wizard iframe compatibility 10107// newpce.js calls window.parent.jQuery("iframe#wizard_wrapper").height() 10108if (typeof window.jQuery === 'undefined') { 10109window.jQuery = function(selector) { 10110var el = document.querySelector(selector); 10111return { 10112height: function() { return el ? el.clientHeight : window.innerHeight; } 10113}; 10114}; 10115}// Called by wizard when user clicks close button - shows confirmation dialog 10116window.requestCloseIframe = function() {
10117console.log('[Wizard] Close requested, showing confirmation'); 10118var modal = document.getElementById('wizardModal'); 10119if (modal && modal._x_dataStack) { 10120modal._x_dataStack[0].showConfirm = true; 10121} 10122};window.redirectToCart = function() { 10123window.location.href = '/checkout/cart'; 10124}; </script>
10124<div x-data="loginModal()" 10125x-show="$store.auth.showLoginModal" 10126x-cloak 10127@keydown.escape.window="$store.auth.showLoginModal = false" 10128class="fixed inset-0 z-50 overflow-y-auto" 10129style="display: none;"><div x-show="$store.auth.showLoginModal" 10130x-transition:enter="transition ease-out duration-300" 10131x-transition:enter-start="opacity-0" 10132x-transition:enter-end="opacity-100" 10133x-transition:leave="transition ease-in duration-200" 10134x-transition:leave-start="opacity-100" 10135x-transition:leave-end="opacity-0" 10136@click="if (Date.now() - $store.auth._loginOpenedAt > 300) $store.auth.showLoginModal = false" 10137class="fixed inset-0 bg-black bg-opacity-50"></div><div class="flex min-h-full items-center justify-center p-4"><div x-show="$store.auth.showLoginModal" 10138x-transition:enter="transition ease-out duration-300" 10139x-transition:enter-start="opacity-0 transform scale-95" 10140x-transition:enter-end="opacity-100 transform scale-100" 10141x-transition:leave="transition ease-in duration-200" 10142x-transition:leave-start="opacity-100 transform scale-100" 10143x-transition:leave-end="opacity-0 transform scale-95" 10144@click.stop 10145class="relative bg-white rounded-lg shadow-xl w-full max-w-md"> <button @click="$store.auth.showLoginModal = false" 10146class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> </svg> </button><div x-show="currentForm === 'login'" class="p-8"><div class="mb-4 mt-2 min-h-[56px]"><div x-show="message" x-text="message" 10147:class="messageType === 'error' ? 'bg-brand-danger/10 text-brand-danger border-brand-danger/30' : 'bg-brand-success/10 text-brand-success border-brand-success/30'" 10148class="p-4 rounded-md border text-sm"></div></div><div class="flex justify-center mb-6"> <img src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 10149alt="Simply Stamps" 10150class="h-16 w-auto"></div><form @submit.prevent="submitLogin" id="login-form"> <input type="hidden" name="form_key" :value="formKey" /><div class="space-y-4"><div> <label for="login-email" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-brand-danger">*</span> </label> <input type="email" 10151id="login-email" 10152name="login[username]" 10153x-model="loginForm.username" 10154required 10155class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="login-password" class="block text-sm font-medium text-gray-700 mb-1"> Password <span class="text-brand-danger">*</span> </label> <input type="password" 10156id="login-password" 10157name="login[password]" 10158x-model="loginForm.password" 10159required 10160class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div class="flex items-center"> <input type="checkbox" 10161id="login-remember" 10162name="persistent_remember_me" 10163x-model="loginForm.rememberMe" 10164class="w-4 h-4 text-brand-primary border-gray-300 rounded focus:ring-brand-primary"> <label for="login-remember" class="ml-2 text-sm text-gray-700"> Remember Me </label></div></div><div class="mt-6 space-y-3"> <button type="submit" 10165:disabled="loading" 10166class="w-full h-[42px] flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover disabled:bg-brand-primary/70 text-white font-medium py-2 px-4 rounded-md transition-colors"> <span x-show="!loading">Log In</span> <span x-show="loading" class="flex items-center justify-center"> <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> </span> </button><button type="button" 10167@click="switchForm('register')" 10168class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10168ded-md transition-colors"> Create an Account </button></div><div class="mt-4 text-center"> <button type="button" 10169@click="switchForm('forgot')" 10170class="text-sm text-brand-primary hover:text-brand-primary-hover transition-colors"> Forgot Your Password? </button></div></form></div><div x-show="currentForm === 'register'" class="p-8"><div class="mb-4 mt-2 min-h-[56px]"><div x-show="message" x-text="message" 10171:class="messageType === 'error' ? 'bg-brand-danger/10 text-brand-danger border-brand-danger/30' : 'bg-brand-success/10 text-brand-success border-brand-success/30'" 10172class="p-4 rounded-md border text-sm"></div></div><div class="flex justify-center mb-6"> <img src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 10173alt="Simply Stamps" 10174class="h-16 w-auto"></div><form @submit.prevent="submitRegister" id="register-form"> <input type="hidden" name="form_key" :value="formKey" /><div class="space-y-4"><div> <label for="register-firstname" class="block text-sm font-medium text-gray-700 mb-1"> First Name <span class="text-brand-danger">*</span> </label> <input type="text" 10175id="register-firstname" 10176name="firstname" 10177x-model="registerForm.firstname" 10178required 10179class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="register-lastname" class="block text-sm font-medium text-gray-700 mb-1"> Last Name <span class="text-brand-danger">*</span> </label> <input type="text" 10180id="register-lastname" 10181name="lastname" 10182x-model="registerForm.lastname" 10183required 10184class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="register-email" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-brand-danger">*</span> </label> <input type="email" 10185id="register-email" 10186name="email" 10187x-model="registerForm.email" 10188required 10189class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="register-password" class="block text-sm font-medium text-gray-700 mb-1"> Password <span class="text-brand-danger">*</span> </label> <input type="password" 10190id="register-password" 10191name="password" 10192x-model="registerForm.password" 10193required 10194class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="register-confirmation" class="block text-sm font-medium text-gray-700 mb-1"> Confirm Password <span class="text-brand-danger">*</span> </label> <input type="password" 10195id="register-confirmation" 10196name="confirmation" 10197x-model="registerForm.confirmation" 10198required 10199class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div class="flex items-center"> <input type="checkbox" 10200id="register-newsletter" 10201name="is_subscribed" 10202x-model="registerForm.newsletter" 10203value="1" 10204class="w-4 h-4 text-brand-primary border-gray-300 rounded focus:ring-brand-primary"> <label for="register-newsletter" class="ml-2 text-sm text-gray-700"> Sign Up for Newsletter </label></div></div><div class="mt-6 space-y-3"> <button type="submit" 10205:disabled="loading" 10206class="w-full h-[42px] flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover disabled:bg-brand-primary/70 text-white font-medium py-2 px-4 rounded-md transition-colors"> <span x-show="!loading">Create Account</span> <span x-show="loading" class="flex items-center justify-center"> <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> </span> </button><button type="button" 10207@click="switchForm('login')" 10208class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10208ded-md transition-colors"> Back to Log In </button></div></form></div><div x-show="currentForm === 'forgot'" class="p-8"><div class="mb-4 mt-2 min-h-[56px]"><div x-show="message" x-text="message" 10209:class="messageType === 'error' ? 'bg-brand-danger/10 text-brand-danger border-brand-danger/30' : 'bg-brand-success/10 text-brand-success border-brand-success/30'" 10210class="p-4 rounded-md border text-sm"></div></div><div class="flex justify-center mb-6"> <img src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 10211alt="Simply Stamps" 10212class="h-16 w-auto"></div><h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">Forgot Your Password?</h2><p class="text-sm text-gray-600 mb-6"> Enter your email address below and we will send you a link to reset your password.</p><form @submit.prevent="submitForgot" id="forgot-form"> <input type="hidden" name="form_key" :value="formKey" /><div class="space-y-4"><div> <label for="forgot-email" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-brand-danger">*</span> </label> <input type="email" 10213id="forgot-email" 10214name="email" 10215x-model="forgotForm.email" 10216required 10217class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div></div><div class="mt-6 space-y-3"> <button type="submit" 10218:disabled="loading" 10219class="w-full h-[42px] flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover disabled:bg-brand-primary/70 text-white font-medium py-2 px-4 rounded-md transition-colors"> <span x-show="!loading">Submit</span> <span x-show="loading" class="flex items-center justify-center"> <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> </span> </button><button type="button" 10220@click="switchForm('login')" 10221class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 rounded-md transition-colors"> Back to Log In </button></div></form></div></div></div></div>
10221<script> function loginModal() { 10222return { 10223currentForm: 'login', 10224loading: false, 10225message: '', 10226messageType: 'error', 10227formKey: 'e3374479ab86b5888526972805eb3d8f',loginForm: { 10228username: '', 10229password: '', 10230rememberMe: false 10231},registerForm: { 10232firstname: '', 10233lastname: '', 10234email: '', 10235password: '', 10236confirmation: '', 10237newsletter: false 10238},forgotForm: { 10239email: '' 10240},switchForm(formName) { 10241this.currentForm = formName; 10242this.message = ''; 10243this.loading = false; 10244},async submitLogin() { 10245this.loading = true; 10246this.message = '';const formData = new FormData(); 10247formData.append('form_key', this.formKey); 10248formData.append('login[username]', this.loginForm.username); 10249formData.append('login[password]', this.loginForm.password); 10250if (this.loginForm.rememberMe) { 10251formData.append('persistent_remember_me', 'on'); 10252}try { 10253const response = await fetch('https://www.simplystamps.com/zephyr/auth/login/', { 10254method: 'POST', 10255body: formData, 10256credentials: 'same-origin' 10257});const data = await response.json();if (data.success) { 10258if (typeof zGA4 === 'function') { 10259zGA4('login', { method: 'site' }); 10260if (data.hashedUserData) { 10261zGA4('set_user_data', { user_data: data.hashedUserData }); 10262} 10263} 10264window.location.reload(); 10265} else { 10266await new Promise(resolve => setTimeout(resolve, 800)); 10267this.message = data.error || 'Invalid login or password.'; 10268this.messageType = 'error'; 10269this.loading = false; 10270} 10271} catch (error) { 10272await new Promise(resolve => setTimeout(resolve, 800)); 10273this.message = 'An error occurred. Please try again.'; 10274this.messageType = 'error'; 10275this.loading = false; 10276} 10277},async submitRegister() { 10278this.loading = true; 10279this.message = '';if (this.registerForm.password !== this.registerForm.confirmation) { 10280await new Promise(resolve => setTimeout(resolve, 800)); 10281this.message = 'Passwords do not match.'; 10282this.messageType = 'error'; 10283this.loading = false; 10284return; 10285}const formData = new FormData(); 10286formData.append('form_key', this.formKey); 10287formData.append('firstname', this.registerForm.firstname); 10288formData.append('lastname', this.registerForm.lastname); 10289formData.append('email', this.registerForm.email); 10290formData.append('password', this.registerForm.password); 10291formData.append('confirmation', this.registerForm.confirmation); 10292if (this.registerForm.newsletter) { 10293formData.append('is_subscribed', '1'); 10294}const recaptchaToken = await getZephyrRecaptchaToken('register'); 10295if (recaptchaToken) { 10296formData.append('recaptcha_response', recaptchaToken); 10297}try { 10298const response = await fetch('https://www.simplystamps.com/zephyr/auth/register/', { 10299method: 'POST', 10300body: formData, 10301credentials: 'same-origin' 10302});const data = await response.json();if (data.success) { 10303if (typeof zGA4 === 'function') { 10304zGA4('sign_up', { method: 'site' }); 10305if (data.hashedUserData) { 10306zGA4('set_user_data', { user_data: data.hashedUserData }); 10307} 10308} 10309window.location.reload(); 10310} else { 10311await new Promise(resolve => setTimeout(resolve, 800)); 10312this.message = data.error || 'There was a problem with your registration.'; 10313this.messageType = 'error'; 10314this.loading = false; 10315} 10316} catch (error) { 10317await new Promise(resolve => setTimeout(resolve, 800)); 10318this.message = 'An error occurred. Please try again.'; 10319this.messageType = 'error'; 10320this.loading = false; 10321} 10322},async submitForgot() { 10323this.loading = true; 10324this.message = '';const formData = new FormData(); 10325formData.append('form_key', this.formKey); 10326formData.append('email', this.forgotForm.email);// reCAPTCHA v3. The forgotpasswordpost endpoint rejects a token-less post 10327// outright, so no token means no reset email. 10328const recaptchaToken = await getZephyrRecaptchaToken('forgot'); 10329if (recaptchaToken) { 10330formData.append('recaptcha_response', recaptchaToken); 10331}try { 10332const response = await fetch('https://www.simplystamps.com/customer/account/forgotpasswordpost/', { 10333method: 'POST', 10334body: formData, 10335credentials: 'same-origin' 10336});const text = await response.text();if (text.includes('success-msg') || text.includes('password-reset')) { 10337this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 10338this.messageType = 'success'; 10339this.forgotForm.email = ''; 10340} else if (text.includes('error-msg')) { 10341const parser = new DOMParser(); 10342const doc = parser.parseFromString(text, 'text/html'); 10343const errorMsg = doc.querySelector('.error-msg'); 10344this.message = errorMsg ? errorMsg.textContent.trim() : 'Unable to send password reset email.'; 10345this.messageType = 'error'; 10346} else { 10347this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 10348this.messageType = 'success'; 10349} 10350} catch (error) { 10351this.message = 'An error occurred. Please try again.'; 10352this.messageType = 'error'; 10353} finally { 10354this.loading = false;
10355} 10356} 10357} 10358} </script>
10358 <input id="searchFocusProxy" type="text" aria-hidden="true" 10359style="position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;" /><div id="searchModal" 10360x-data="searchModal()" 10361x-show="$store.search.isOpen" 10362x-cloak 10363@keydown.escape.window="$store.search.close()" 10364class="fixed inset-0 z-[70] overflow-hidden" 10365style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm transition-opacity hidden md:block" 10366x-show="$store.search.isOpen" 10367x-transition:enter="transition ease-out duration-300" 10368x-transition:enter-start="opacity-0" 10369x-transition:enter-end="opacity-100" 10370x-transition:leave="transition ease-in duration-200" 10371x-transition:leave-start="opacity-100" 10372x-transition:leave-end="opacity-0" 10373@click="$store.search.close()"></div><div class="fixed inset-0 flex items-start justify-center md:pt-12 md:px-4" 10374@mousedown.self="$store.search.close()"><div x-show="$store.search.isOpen" 10375class="relative bg-white w-full h-full flex flex-col md:h-auto md:max-w-2xl md:rounded-xl md:shadow-2xl" 10376x-ref="modalContent"><div class="flex items-center justify-between p-4 md:px-6 border-b border-gray-200 shrink-0"> <a href="https://www.simplystamps.com/" class="block"> <img src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 10377alt="Simply Stamps" 10378class="h-8 w-auto" /> </a> <button @click="$store.search.close()" 10379class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"> <svg class="w-7 h-7 md:w-6 md:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> </svg> </button></div><div class="px-4 pt-4 md:px-6 md:pt-6 shrink-0"><form @submit.prevent="submitSearch" class="relative"><div class="relative flex items-center"><div class="absolute left-4 flex items-center pointer-events-none"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10380d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg></div> <input type="text" 10381x-ref="searchInput" 10382x-model="query" 10383@input="onInput()" 10384@keydown.down.prevent="selectNext" 10385@keydown.tab.prevent="selectNext" 10386@keydown.up.prevent="selectPrev" 10387@keydown.enter.prevent="selectCurrent" 10388placeholder="Search products, articles..." 10389class="w-full pl-12 pr-24 py-4 text-lg border-2 border-gray-200 rounded-xl 10390focus:border-brand-primary focus:outline-none focus:ring-0 10391transition-colors" 10392autocomplete="off" /><div x-show="$store.search.isLoading" 10393class="absolute right-[4.5rem] md:right-[5.5rem] flex items-center"> <svg class="animate-spin h-5 w-5 text-brand-primary" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg></div> <button x-show="query.length > 0 && !$store.search.isLoading" 10394@click="clearSearch()" 10395type="button" 10396class="absolute right-[4.5rem] md:right-[5.5rem] flex items-center text-gray-400 hover:text-gray-600"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> </svg> </button> <button @click="submitSearch()" 10397type="button" 10398:disabled="query.length < minChars" 10399class="absolute right-2 flex items-center justify-center h-10 px-3 bg-brand-primary text-white 10400rounded-lg hover:bg-brand-primary-hover transition-colors 10401disabled:opacity-40 disabled:cursor-not-allowed"> <svg class="w-5 h-5 md:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10402d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> <span class="hidden md:inline text-sm font-medium">Search</span> </button></div></form></div><div x-show="!$store.search.hasSearched" class="px-4 pt-4 pb-2 md:px-6"><h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3"> Popular Searches</h3><div class="flex flex-wrap gap-2"> <button type="button" 10403@click.prevent="selectPopular('self-inking stamp')" 10404class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10405rounded-lg text-sm font-medium text-gray-700 transition-colors cursor-pointer"> <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10406d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> self-inking stamp </button> <button type="button" 10407@click.prevent="selectPopular('wood handle stamp')" 10408class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10409rounded-lg text-sm font-medium text-gray-700 transition-colors cursor-pointer"> <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10410d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> wood handle stamp </button> <button type="button" 10411@click.prevent="selectPopular('library embosser')" 10412class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10413rounded-lg text-sm font-medium text-gray-700 transition-colors cursor-pointer"> <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10414d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> library embosser </button> <button type="button" 10415@click.prevent="selectPopular('wedding address stamps')" 10416class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10417rounded-lg text-sm font-medium text-gray-700 transition-colors cursor-pointer"> <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10418d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> wedding address stamps </button></div></div><div x-show="$store.search.hasSearched" 10419x-ref="resultsPane" 10420class="flex-1 flex flex-col overflow-y-auto md:max-h-[60vh]"> <template x-if="results.suggestions && results.suggestions.length > 0"><div class="order-1 px-4 pt-2 pb-0.5"> <template x-for="(suggestion, index) in visibleSuggestions" :key="suggestion.text"> <a :href="'https://www.simplystamps.com/catalogsearch/result/?q=' + encodeURIComponent(suggestion.text)" 10421:class="{ 'bg-blue-50 text-blue-900': selectedIndex === index }" 10422class="flex items-center gap-3 px-3 py-1.5 rounded-lg 10423hover:bg-gray-50 transition-colors cursor-pointer group" 10424@mousemove="selectedIndex = index" 10425@click.prevent="selectSuggestion(suggestion)"> <svg class="w-4 h-4 text-gray-300 shrink-0" :class="{ 'text-blue-400': selectedIndex === index }" 10426fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10427d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> <span class="flex-1 text-sm text-gray-700"> <span x-show="suggestion.fuzzy" class="text-xs text-gray-400 italic mr-1">did you mean:</span> <span x-html="highlightMatch(suggestion.text, typedQuery)"></span> </span> </a> </template></div> </template><div x-show="showSkeleton" 10428x-transition:enter="transition ease-out duration-200" 10429x-transition:enter-start="opacity-0" 10430x-transition:enter-end="opacity-100" 10431x-transition:leave="transition ease-in duration-150" 10432x-transition:leave-start="opacity-100" 10433x-transition:leave-end="opacity-0" 10434class="order-2 animate-pulse p-4 border-t border-gray-300"><div class="space-y-1"><div class="flex items-center gap-4 p-3"><div class="w-14 h-14 bg-gray-200 rounded-lg shrink-0"></div><div class="flex-1 space-y-2"><div class="h-4 bg-gray-200 rounded w-3/4"></div><div class="h-3 bg-gray-200 rounded w-1/2"></div></div></div><div class="flex items-center gap-4 p-3"><div class="w-14 h-14 bg-gray-200 rounded-lg shrink-0"></div><div class="flex-1 space-y-2"><div class="h-4 bg-gray-200 rounded w-2/3"></div><div class="h-3 bg-gray-200 rounded w-2/5"></div></div></div><div class="flex items-center gap-4 p-3"><div class="w-14 h-14 bg-gray-200 rounded-lg shrink-0"></div><div class="flex-1 space-y-2"><div class="h-4 bg-gray-200 rounded w-4/5"></div><div class="h-3 bg-gray-200 rounded w-1/3"></div></div></div></div></div> <template x-if="results.products && results.products.length > 0"><div class="order-4 p-4 transition-opacity duration-150" 10435:class="{ 10436'border-t border-gray-300': suggestionsCount > 0 || (blogFirst && blogCount > 0), 10437'opacity-40 pointer-events-none': $store.search.isLoading 10438}"><h3 class="text-xs font-semibold text-gray-600 uppercase tracking-wider mb-2 px-1"> Products</h3><div class="space-y-1"> <template x-for="(product, index) in results.products" :key="product.url"> <a :href="product.url" 10439:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (productsOffset + index) }" 10440class="flex items-center gap-4 p-3 rounded-lg border border-transparent 10441hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer" 10442@mousemove="selectedIndex = productsOffset + index"><div class="flex-shrink-0 w-14 h-14 bg-gray-100 rounded-lg overflow-hidden"> <img :src="product.image" :alt="product.name" 10443class="w-full h-full object-cover" 10444onerror="this.src='https://cdn.simplystamps.com/skin/frontend/zephyr/default/images/catalog/product/placeholder/small_image.jpg'" /></div><div class="flex-1 min-w-0"><h4 class="font-medium text-gray-900 text-sm truncate" x-html="highlightMatch(product.name, typedQuery)"></h4><p x-show="product.description" 10445class="text-xs text-gray-500 line-clamp-1 mt-0.5" 10446x-text="product.description"></p></div> </a> </template></div></div> </template> <template x-if="results.blog && results.blog.length > 0"><div class="p-4 transition-opacity duration-150" 10447:class="{ 10448'order-3': blogFirst, 10449'order-5': !blogFirst, 10450'border-t border-gray-300': !blogFirst || suggestionsCount > 0, 10451'opacity-40 pointer-events-none': $store.search.isLoading 10452}"><h3 class="text-xs font-semibold text-gray-600 uppercase tracking-wider mb-2 px-1"> From the blog</h3><div class="space-y-1"> <template x-for="(post, index) in results.blog" :key="post.url"> <a :href="post.url" 10453:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (blogOffset + index) }" 10454class="flex items-center gap-4 p-3 rounded-lg border border-transparent 10455hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer" 10456@mousemove="selectedIndex = blogOffset + index"><div class="flex-shrink-0 w-14 h-14 bg-gray-100 rounded-lg overflow-hidden flex items-center justify-center"> <template x-if="post.image"> <img :src="post.image" :alt="post.name" class="w-full h-full object-cover" /> </template> <template x-if="!post.image"> <svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10457d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path> </svg> </template></div><div class="flex-1 min-w-0"><h4 class="font-medium text-gray-900 text-sm truncate" x-html="highlightMatch(post.name, typedQuery)"></h4><p x-show="post.description" 10458class="text-xs text-gray-500 line-clamp-1 mt-0.5" 10459x-text="post.description"></p></div> <span class="hidden sm:inline-flex shrink-0 items-center rounded-md bg-gray-100 px-2 py-0.5 text-[11px] font-medium uppercase tracking-wide text-gray-500"> Article </span> </a> </template></div></div> </template> <template x-if="results.categories && results.categories.length > 0"><div class="order-6 p-4 border-t border-gray-300"><h3 class="text-xs font-semibold text-gray-600 uppercase tracking-wider mb-2 px-1"> Categories</h3><div class="flex flex-wrap gap-2"> <template x-for="category in results.categories" :key="category.url"> <a :href="category.url" 10460class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10461rounded-lg text-sm font-medium text-gray-700 transition-colors"> <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10462d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path> </svg> <span x-text="category.name"></span> </a> </template></div></div> </template></div><div class="p-4 border-t border-gray-300 bg-gray-50 md:rounded-b-xl shrink-0"><div class="flex items-center justify-between"><div class="hidden md:flex items-center gap-6 text-xs text-gray-400"> <span class="flex items-center gap-1"> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">↑</kbd> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">↓</kbd> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">Tab</kbd> to navigate </span> <span class="flex items-center gap-1"> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">Enter</kbd> to select </span> <span class="flex items-center gap-1"> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">Esc</kbd> to close </span></div> <a x-show="query.length >= minChars" 10463:href="'https://www.simplystamps.com/catalogsearch/result/?q=' + encodeURIComponent(query)" 10464class="flex items-center gap-1 text-sm font-medium text-brand-primary hover:text-brand-primary-hover 10465transition-colors ml-auto"> 10466View All Results <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> </svg> </a></div></div></div></div></div>
10466<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/search-modal.js?v=1126"></script>
10466
10466<script> searchModalConfig.minChars = 3; 10467searchModalConfig.searchUrl = 'https://www.simplystamps.com/searchautocomplete/ajax/get/'; 10468searchModalConfig.suggestUrl = 'https://www.simplystamps.com/zephyr/search/suggest/'; 10469searchModalConfig.resultUrl = 'https://www.simplystamps.com/catalogsearch/result/'; 10470searchModalConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10471searchModalConfig.suggestedQueries = ["self-inking stamp","wood handle stamp","library embosser","wedding address stamps"]; 10472searchModalConfig.blogFirst = false; </script>
10472<style> .line-clamp-1 { 10473overflow: hidden; 10474display: -webkit-box; 10475-webkit-box-orient: vertical; 10476-webkit-line-clamp: 1; 10477} </style></div></div><link rel="preload" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"></noscript>
10477<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>
10477
10477<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/rileyprint.js?v=1126" defer></script>
10477
10477<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/product-media.js?v=1126" defer></script>
10477
10477<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/file-upload.js?v=1126" defer></script>
10477
10477<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-drawer.js?v=1126" defer></script>
10477
10477<script> // Configure cart drawer URLs. cart-drawer.js is now deferred (its var cartDrawerConfig 10478// default object still runs before DOMContentLoaded per spec), and every read of 10479// cartDrawerConfig.* inside cart-drawer.js happens lazily inside user-triggered 10480// handlers (open drawer, add/remove item, checkout) â none of it runs at 10481// alpine:init â so it's safe to set these values on DOMContentLoaded too. 10482document.addEventListener('DOMContentLoaded', function() { 10483cartDrawerConfig.getItemsUrl = 'https://www.simplystamps.com/zephyr/cart/getItems/'; 10484cartDrawerConfig.deleteUrl = 'https://www.simplystamps.com/checkout/cart/delete/'; 10485cartDrawerConfig.removeItemsUrl = 'https://www.simplystamps.com/zephyr/cart/removeItems/'; 10486cartDrawerConfig.updatePostUrl = 'https://www.simplystamps.com/checkout/cart/updatePost/'; 10487cartDrawerConfig.clearUrl = 'https://www.simplystamps.com/zephyr/cart/clear/'; 10488cartDrawerConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/'; 10489cartDrawerConfig.cartUrl = 'https://www.simplystamps.com/checkout/cart/'; 10490cartDrawerConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10491// Cart drawer feature settings (from admin config) 10492cartDrawerConfig.enableCheckboxDelete = true; 10493cartDrawerConfig.enableIndividualDelete = true; 10494cartDrawerConfig.showClearCart = true; 10495cartDrawerConfig.checkoutButtonText = 'Proceed to Checkout'; 10496}); </script>
10496
10496<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-upsell.js?v=1126"></script>
10496
10496<script> // Configure cart upsell popup 10497cartUpsellConfig.getUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/getUpsells/'; 10498cartUpsellConfig.addUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/addUpsells/'; 10499cartUpsellConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/'; 10500cartUpsellConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10501cartUpsellConfig.enabled = true; 10502cartUpsellConfig.storeId = '5'; 10503cartUpsellConfig.devMode = false; </script>
10503<script> // Fetch a reCAPTCHA v3 token, lazy-loading Google's api.js on first use so it 10504// adds no page weight until a protected form is actually submitted. Shared by 10505// every Zephyr form protected by v3 (registration, contact, quote), which is 10506// also why it must be the ONLY thing loading recaptcha on modern pages â a 10507// v3 key can't execute against a grecaptcha client created by a bare v2 10508// api.js load, so don't reintroduce v2 script tags into modern templates. 10509// Resolves null when reCAPTCHA is disabled or the script/execution fails â 10510// the server decides what to do with a missing token. Never rejects or hangs. 10511window.zephyrRecaptchaKey = '6Lem-q0gAAAAAMhOqUz2tke2IN3t0Pt37x5oqRq3'; 10512window.getZephyrRecaptchaToken = function(action) { 10513var key = window.zephyrRecaptchaKey; 10514if (!key) return Promise.resolve(null); 10515return new Promise(function(resolve) { 10516var settled = false; 10517var done = function(token) { if (!settled) { settled = true; resolve(token || null); } }; 10518// Whatever happens, never leave the submit hanging 10519setTimeout(function() { done(null); }, 15000);function execute() { 10520try { 10521grecaptcha.ready(function() { 10522try { 10523grecaptcha.execute(key, { action: action }) 10524.then(done, function() { done(null); }); 10525} catch (e) { done(null); } 10526}); 10527} catch (e) { done(null); } 10528}if (window.grecaptcha) { execute(); return; } 10529var s = document.createElement('script'); 10530s.src = 'https://www.google.com/recaptcha/api.js?render=' + key; 10531s.onload = execute; 10532s.onerror = function() { done(null); }; 10533document.head.appendChild(s); 10534}); 10535}; </script>
10535
10535<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/promo-code.js?v=1126"></script>
10535
10535<script> // Configure promo code URLs 10536promoCodeConfig.applyCouponUrl = 'https://www.simplystamps.com/zephyr/cart/applyCoupon/'; 10537promoCodeConfig.removeCouponUrl = 'https://www.simplystamps.com/zephyr/cart/removeCoupon/'; 10538promoCodeConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; </script>
10538
10538<script>function setLocation(url){window.location.href=url;}</script>
10538
10538<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/alpine.min.js?v=1126" defer></script>
10538
10538<script> document.addEventListener('alpine:init', () => { 10539// Global UI state - controls visibility of shared UI elements 10540Alpine.store('ui', { 10541hideHeader: false, 10542// Header scroll state (updated by stickyHeader component) 10543headerVisible: true, 10544headerHeight: 0,// Call this when opening fullscreen overlays (lightbox, modals, etc.) 10545enterFullscreen() { 10546this.hideHeader = true; 10547},// Call this when closing fullscreen overlays 10548exitFullscreen() { 10549this.hideHeader = false; 10550},// Get the effective top position for fixed elements below the header 10551getHeaderOffset() { 10552// If header is hidden (scroll or fullscreen), no offset needed 10553if (this.hideHeader || !this.headerVisible) { 10554return 0; 10555} 10556return this.headerHeight; 10557} 10558});Alpine.store('auth', { 10559showLoginModal: false, 10560showRefreshOverlay: false, 10561refreshMessage: '', 10562isLoggedIn: false, 10563isStaff: false, 10564adminEditBase: '', 10565customerName: '', 10566customerEmail: '', 10567companyStoreUrl: '', 10568cartCount: 0, 10569cartSubtotal: 0, 10570formKey: '', 10571ready: false, 10572_loginOpenedAt: 0,init() { 10573// State is hydrated via AJAX so the page HTML stays static/compressible. 10574this.fetchStatus(); 10575},/** 10576* Fetch auth status from session via AJAX. 10577* Primary hydration path (called from init()), and also used 10578* after login/logout to refresh state reactively. 10579*/ 10580fetchStatus() { 10581var self = this; 10582fetch('https://www.simplystamps.com/zephyr/auth/status/', { 10583credentials: 'same-origin', 10584headers: { 'X-Requested-With': 'XMLHttpRequest' } 10585}) 10586.then(function(r) { return r.json(); }) 10587.then(function(data) { 10588self.isLoggedIn = !!data.isLoggedIn; 10589self.isStaff = !!data.isStaff; 10590self.adminEditBase = data.adminEditBase || ''; 10591self.customerName = data.customerName || ''; 10592self.customerEmail = data.customerEmail || ''; 10593self.companyStoreUrl = data.companyStoreUrl || ''; 10594self.cartCount = data.cartCount || 0; 10595self.cartSubtotal = data.cartSubtotal || 0; 10596self.formKey = data.formKey || '';// Hydrate the cart store's badge count / free-shipping subtotal from 10597// this same response â the async equivalent of the inline global 10598// state object the ESI block used to provide synchronously. 10599if (window.Alpine && Alpine.store('cart')) { 10600var cartStore = Alpine.store('cart'); 10601cartStore.itemCount = data.cartCount || 0; 10602cartStore.isEmpty = !(data.cartCount > 0); 10603cartStore.subtotalNumeric = data.cartSubtotal || 0; 10604cartStore._esiReady = true; 10605} 10606if (data.messages && data.messages.length && window.Alpine) { 10607var flash = Alpine.store('flash'); 10608for (var i = 0; i < data.messages.length; i++) { 10609if (flash && flash.addMessage) flash.addMessage(data.messages[i].type, data.messages[i].text); 10610} 10611} 10612self.ready = true; 10613}) 10614.catch(function() { 10615self.ready = true; 10616}); 10617},setLoggedIn(customerData) { 10618this.isLoggedIn = true; 10619this.customerName = customerData.name || '';window.dispatchEvent(new CustomEvent('customer-logged-in', { 10620detail: customerData 10621})); 10622},setLoggedOut() { 10623this.isLoggedIn = false; 10624this.isStaff = false;
10625this.adminEditBase = ''; 10626this.customerName = ''; 10627this.customerEmail = ''; 10628this.companyStoreUrl = '';window.dispatchEvent(new CustomEvent('customer-logged-out')); 10629} 10630});Alpine.store('search', { 10631isOpen: false, 10632isLoading: false, 10633hasSearched: false, 10634_openedAt: 0,open() { 10635// Focus proxy input immediately to preserve iOS user-gesture context 10636// so the virtual keyboard opens. Focus transfers to real input in $nextTick. 10637var proxy = document.getElementById('searchFocusProxy'); 10638if (proxy) proxy.focus(); 10639this.isOpen = true; 10640this._openedAt = Date.now(); 10641},close() { 10642if (Date.now() - this._openedAt < 300) return; 10643this.isOpen = false; 10644} 10645});// Confirmation modal store 10646Alpine.store('confirm', { 10647isOpen: false, 10648title: '', 10649message: '', 10650html: null, 10651confirmText: 'Confirm', 10652cancelText: 'Cancel', 10653confirmStyle: 'danger', // 'danger' or 'primary' 10654onConfirm: null, 10655onCancel: null,show(options) { 10656this.title = options.title || 'Confirm'; 10657this.message = options.message || 'Are you sure?'; 10658this.html = options.html || null; 10659this.confirmText = options.confirmText || 'Confirm'; 10660this.cancelText = options.cancelText || 'Cancel'; 10661this.confirmStyle = options.confirmStyle || 'danger'; 10662this.onConfirm = options.onConfirm || null; 10663this.onCancel = options.onCancel || null; 10664this.isOpen = true; 10665},confirm() { 10666if (this.onConfirm) { 10667this.onConfirm(); 10668} 10669this.close(); 10670},cancel() { 10671if (this.onCancel) { 10672this.onCancel(); 10673} 10674this.close(); 10675},close() { 10676this.isOpen = false; 10677this.onConfirm = null; 10678this.onCancel = null; 10679// Defer clearing html until after the 150ms leave transition 10680// so the image doesn't vanish mid-animation 10681var self = this; 10682setTimeout(function() { 10683if (!self.isOpen) self.html = null; 10684}, 200); 10685} 10686});// Flash messages store â messages arrive via auth store's fetchStatus() (AJAX), 10687// or as inline JSON on pages that embed them directly (fallback below). 10688Alpine.store('flash', { 10689messages: [], 10690_nextId: 1, 10691_seen: {},init() { 10692var self = this;// Fallback: read messages embedded as JSON in page HTML (for pages without ESI block) 10693var scriptEl = document.querySelector('script[data-flash-messages]'); 10694if (scriptEl) { 10695try { 10696var inline = JSON.parse(scriptEl.textContent); 10697for (var i = 0; i < inline.length; i++) { 10698self.addMessage(inline[i].type, inline[i].text); 10699} 10700} catch (e) {} 10701scriptEl.remove(); 10702} 10703},addMessage(type, text) { 10704// Deduplication 10705var key = type + ':' + text; 10706if (this._seen[key]) { 10707return; 10708} 10709this._seen[key] = true;var id = this._nextId++; 10710this.messages.push({ id: id, type: type, text: text, visible: true });// Auto-dismiss non-error messages after 8 seconds 10711if (type !== 'error') { 10712var self = this; 10713setTimeout(function() { 10714self.dismiss(id); 10715}, 8000); 10716} 10717},dismiss(id) { 10718var idx = this.messages.findIndex(function(m) { return m.id === id; }); 10719if (idx !== -1) { 10720this.messages[idx].visible = false; 10721var self = this; 10722setTimeout(function() { 10723self.messages = self.messages.filter(function(m) { return m.id !== id; }); 10724}, 300); 10725} 10726},show(type, text) { 10727this.addMessage(type, text); 10728} 10729});Alpine.store('flash').init(); 10730});// BFCache restoration handler â when iOS Safari restores a page from BFCache, 10731// Alpine stores are frozen with stale data. Refresh critical stores. 10732window.addEventListener('pageshow', function(event) { 10733if (event.persisted) { 10734// Page was restored from BFCache â refresh cart data and auth status 10735if (typeof Alpine !== 'undefined' && Alpine.store) { 10736var cart = Alpine.store('cart'); 10737if (cart) { 10738cart.lastFetch = null; 10739cart.refresh(); 10740} 10741var auth = Alpine.store('auth'); 10742if (auth && typeof auth.fetchStatus === 'function') { 10743auth.fetchStatus(); 10744} 10745} 10746} 10747}); 10748</script>
10748<div x-data 10749x-show="$store.auth.showRefreshOverlay" 10750x-cloak 10751x-transition:enter="transition ease-out duration-200" 10752x-transition:enter-start="opacity-0" 10753x-transition:enter-end="opacity-100" 10754class="fixed inset-0 z-[200] flex items-center justify-center bg-white/80 backdrop-blur-sm" 10755style="display: none;"><div class="flex flex-col items-center gap-3"> <svg class="animate-spin h-8 w-8 text-brand-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg><p class="text-gray-700 font-medium text-lg" x-text="$store.auth.refreshMessage"></p></div></div><div 10756x-data 10757x-show="$store.confirm.isOpen" 10758x-cloak 10759@keydown.escape.window="$store.confirm.cancel()" 10760class="fixed inset-0 z-[100] overflow-y-auto" 10761aria-labelledby="confirm-modal-title" 10762role="dialog" 10763aria-modal="true" 10764><div 10765x-show="$store.confirm.isOpen" 10766x-transition:enter="transition ease-out duration-200" 10767x-transition:enter-start="opacity-0" 10768x-transition:enter-end="opacity-100" 10769x-transition:leave="transition ease-in duration-150" 10770x-transition:leave-start="opacity-100" 10771x-transition:leave-end="opacity-0" 10772@click="$store.confirm.cancel()" 10773class="fixed inset-0 bg-black/50 backdrop-blur-sm" 10774></div><div class="fixed inset-0 flex items-center justify-center p-4"><div 10775x-show="$store.confirm.isOpen" 10776x-transition:enter="transition ease-out duration-200" 10777x-transition:enter-start="opacity-0 scale-95" 10778x-transition:enter-end="opacity-100 scale-100" 10779x-transition:leave="transition ease-in duration-150" 10780x-transition:leave-start="opacity-100 scale-100" 10781x-transition:leave-end="opacity-0 scale-95" 10782@click.stop 10783class="relative bg-white rounded-xl shadow-2xl max-w-md w-full p-6" 10784> <button 10785@click="$store.confirm.cancel()" 10786type="button" 10787class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 10788> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button><div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full mb-4" 10789:class="$store.confirm.confirmStyle === 'danger' ? 'bg-red-100' : 'bg-blue-100'"> <svg x-show="$store.confirm.confirmStyle === 'danger'" 10790class="h-6 w-6 text-red-600" 10791fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> </svg> <svg x-show="$store.confirm.confirmStyle !== 'danger'" 10792class="h-6 w-6 text-blue-600" 10793fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg></div><h3 id="confirm-modal-title" 10794class="text-lg font-semibold text-gray-900 text-center mb-2" 10795x-text="$store.confirm.title"></h3><div x-show="$store.confirm.html" class="text-sm text-gray-600 text-center mb-6" 10796x-html="$store.confirm.html"></div><p x-show="!$store.confirm.html" class="text-sm text-gray-600 text-center mb-6" 10797x-text="$store.confirm.message"></p><div class="flex gap-3"> <button 10798@click="$store.confirm.cancel()" 10799type="button" 10800class="flex-1 px-4 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 transition-colors" 10801x-text="$store.confirm.cancelText"> </button> <button 10802@click="$store.confirm.confirm()" 10803type="button" 10804class="flex-1 px-4 py-2.5 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors" 10805:class="$store.confirm.confirmStyle === 'danger' 10806? 'bg-red-600 hover:bg-red-700 focus:ring-red-500' 10807: 'bg-brand-primary hover:bg-brand-primary-hover focus:ring-brand-primary'" 10808x-text="$store.confirm.confirmText"> </button></div></div></div></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.