1<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>About Us - Simply Stamps</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="About Us - Simply Stamps" /><meta property="og:type" content="article" /><meta property="og:url" content="https://www.simplystamps.com/about-us" /><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" ><link rel="stylesheet" href="https://cdn.simplystamps.com/skin/frontend/base/default/css/dist/builder.css?v1126" media="all" ><link rel="canonical" href="https://www.simplystamps.com/about-us">
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><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>
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="cms-page-view cms-about-us"><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="w-full"><div class="w-full"><div x-data x-show="$store.flash.messages.length > 0" x-cloak class="space-y-3 mb-4 px-4"> <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-r-lg p-4 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 class="std"><div class="zephyr-builder-layout"><div class="zephyr-builder-section" 1252data-section-index="0" 1253data-section-id="sec_11ac6ec5" 1254data-layout-id="33" 1255data-bg-color="" 1256data-bg-image="" 1257data-overlay-color="" 1258data-overlay-opacity="0" 1259data-full-width="xl" 1260><div class="mx-auto max-w-screen-xl"><div class="zephyr-builder-row" 1261data-section-index="0" 1262data-row-index="0"><div class="grid grid-cols-12 gap-6"><div class="zephyr-builder-zone col-span-12 sm:col-span-4"><div class="zephyr-builder-block" 1263data-placement-id="2858" 1264data-layout-id="33" 1265data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 80%"><div class="flex justify-center"> <img 1266src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS-Tagline-Logo.png" 1267alt="Simply Stamps logo with tagline Your Mark Matters" 1268class="max-w-full h-auto rounded-lg shadow-sm" 1269/></div></div> </section></div></div><div class="zephyr-builder-zone col-span-12 sm:col-span-8"><div class="zephyr-builder-block" 1270data-placement-id="2859" 1271data-layout-id="33" 1272data-library-block="content_seo_header"> <section class="pt-12 pb-4 border-0"><div class="w-full px-4 sm:px-0 justify-center text-left"><div class="text-left"><h1 class=" text-2xl sm:text-3xl md:text-4xl font-bold mb-4" style="color: #012e4b;"> About Us</h1> <span class="block text-lg sm:text-xl font-normal mt-2" style="color: #012e4b; opacity: 0.8;"> Trusted custom stamp makers for 70+ years. </span></div></div> </section></div>
1272<div class="zephyr-builder-block" 1273data-placement-id="2860" 1274data-layout-id="33" 1275data-library-block="content_markdown"> <section class="pt-2 pb-2 border-0"><div class="prose prose-lg max-w-none px-4 sm:px-0 1276prose-headings:text-gray-900 prose-headings:font-bold 1277prose-h2:text-2xl prose-h2:md:text-3xl prose-h2:mt-8 prose-h2:mb-4 1278prose-h3:text-xl prose-h3:md:text-2xl prose-h3:mt-6 prose-h3:mb-3 1279prose-p:text-gray-600 prose-p:leading-relaxed prose-p:mb-4 1280prose-a:text-brand-primary hover:prose-a:text-brand-primary-hover prose-a:underline 1281prose-strong:text-gray-900 prose-strong:font-semibold 1282prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-4 1283prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-4 1284prose-li:text-gray-600 prose-li:mb-2 1285prose-blockquote:border-l-4 prose-blockquote:border-brand-primary prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-gray-500 1286prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono 1287prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:p-4 prose-pre:rounded-lg prose-pre:overflow-x-auto" 1288><p style="line-height: 1.25"><span style="font-size: 18px">Our name, Simply Stamps, reflects our core belief that even the simplest tools can make a profound impact. We're committed to delivering exceptional, high-quality rubber stamps that leave a lasting impression and elevate everyday interactions within the community.</span></p></div> </section></div></div></div></div></div></div><div class="zephyr-builder-section" 1289data-section-index="1" 1290data-section-id="sec_6e1a5418" 1291data-layout-id="33" 1292data-bg-color="" 1293data-bg-image="" 1294data-overlay-color="" 1295data-overlay-opacity="0" 1296data-full-width="2xl" 1297><div class="mx-auto max-w-screen-2xl"><div class="zephyr-builder-row" 1298data-section-index="1" 1299data-row-index="0"><div class="grid grid-cols-12 gap-6"><div class="zephyr-builder-zone col-span-12 sm:col-span-6"><div class="zephyr-builder-block" 1300data-placement-id="2861" 1301data-layout-id="33" 1302data-library-block="content_seo_header"> <section class="pt-8 pb-0 border-0"><div class="w-full px-4 sm:px-0 justify-center text-left"><div class="text-left"><h2 class=" text-xl sm:text-2xl md:text-3xl font-bold mb-4" style="color: #012e4b;"> The Foundation of Simply Stamps</h2></div></div> </section></div><div class="zephyr-builder-block" 1303data-placement-id="2862" 1304data-layout-id="33" 1305data-library-block="content_seo_header"> <section class="pt-0 pb-0 border-0"><div class="w-full px-4 sm:px-0 justify-center text-left"><div class="text-left"><h3 class=" text-lg sm:text-xl font-normal mt-2" style="color: #012e4b; opacity: 0.8;"> Handmade History</h3></div></div> </section></div><div class="zephyr-builder-block" 1306data-placement-id="2863" 1307data-layout-id="33" 1308data-library-block="content_markdown"> <section class="pt-0 pb-0 border-0"><div class="prose prose-lg max-w-none px-4 sm:px-0 1309prose-headings:text-gray-900 prose-headings:font-bold 1310prose-h2:text-2xl prose-h2:md:text-3xl prose-h2:mt-8 prose-h2:mb-4 1311prose-h3:text-xl prose-h3:md:text-2xl prose-h3:mt-6 prose-h3:mb-3 1312prose-p:text-gray-600 prose-p:leading-relaxed prose-p:mb-4 1313prose-a:text-brand-primary hover:prose-a:text-brand-primary-hover prose-a:underline 1314prose-strong:text-gray-900 prose-strong:font-semibold 1315prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-4 1316prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-4 1317prose-li:text-gray-600 prose-li:mb-2 1318prose-blockquote:border-l-4 prose-blockquote:border-brand-primary prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-gray-500 1319prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono 1320prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:p-4 prose-pre:rounded-lg prose-pre:overflow-x-auto" 1321><p style="line-height: 1"><span style="font-size: 16px">Before instant messages and social media, rubber stamps were the tools we relied on to spread information and streamline tasks. In Jacksonville, Florida, entrepreneur Owen Holmes saw beyond the practical. He envisioned a way to elevate everyday interactions within his beloved community. Motivated by a de
1321sire to make ordinary moments special, Mr. Holmes launched Holmes Stamp Co. right out of his garage in 1954. The small business strived to make a meaningful impression with every stamp, marking the beginning of a lasting legacy.</span></p></div> </section></div><div class="zephyr-builder-block" 1322data-placement-id="2864" 1323data-layout-id="33" 1324data-library-block="content_seo_header"> <section class="pt-2 pb-0 border-0"><div class="w-full px-4 sm:px-0 justify-center text-left"><div class="text-left"><h3 class=" text-lg sm:text-xl font-normal mt-2" style="color: #012e4b; opacity: 0.8;"> Brand Innovation</h3></div></div> </section></div><div class="zephyr-builder-block" 1325data-placement-id="2865" 1326data-layout-id="33" 1327data-library-block="content_markdown"> <section class="pt-0 pb-0 border-0"><div class="prose prose-lg max-w-none px-4 sm:px-0 1328prose-headings:text-gray-900 prose-headings:font-bold 1329prose-h2:text-2xl prose-h2:md:text-3xl prose-h2:mt-8 prose-h2:mb-4 1330prose-h3:text-xl prose-h3:md:text-2xl prose-h3:mt-6 prose-h3:mb-3 1331prose-p:text-gray-600 prose-p:leading-relaxed prose-p:mb-4 1332prose-a:text-brand-primary hover:prose-a:text-brand-primary-hover prose-a:underline 1333prose-strong:text-gray-900 prose-strong:font-semibold 1334prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-4 1335prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-4 1336prose-li:text-gray-600 prose-li:mb-2 1337prose-blockquote:border-l-4 prose-blockquote:border-brand-primary prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-gray-500 1338prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono 1339prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:p-4 prose-pre:rounded-lg prose-pre:overflow-x-auto" 1340><p style="line-height: 1"><span style="font-size: 16px">After a lifetime dedicated to crafting meaningful connections through stamps, Mr. Holmes retired in 1998 and handed the reigns over to Bob Croft, a long-time delivery driver. Bob had a deep understanding of the products and the pulse of our community. He introduced a wider variety of customizable goods that empowered people to express themselves in unique ways. But Bob never lost sight of the company's roots. To ensure stamps remained the heart and soul of the business, Holmes Stamp Co. was proudly transformed into Simply Stamps in 2002.</span></p></div> </section></div><div class="zephyr-builder-block" 1341data-placement-id="2866" 1342data-layout-id="33" 1343data-library-block="content_seo_header"> <section class="pt-2 pb-0 border-0"><div class="w-full px-4 sm:px-0 justify-center text-left"><div class="text-left"><h3 class=" text-lg sm:text-xl font-normal mt-2" style="color: #012e4b; opacity: 0.8;"> Digital Evolution</h3></div></div> </section></div><div class="zephyr-builder-block" 1344data-placement-id="2867" 1345data-layout-id="33" 1346data-library-block="content_markdown"> <section class="pt-0 pb-0 border-0"><div class="prose prose-lg max-w-none px-4 sm:px-0 1347prose-headings:text-gray-900 prose-headings:font-bold 1348prose-h2:text-2xl prose-h2:md:text-3xl prose-h2:mt-8 prose-h2:mb-4 1349prose-h3:text-xl prose-h3:md:text-2xl prose-h3:mt-6 prose-h3:mb-3 1350prose-p:text-gray-600 prose-p:leading-relaxed prose-p:mb-4 1351prose-a:text-brand-primary hover:prose-a:text-brand-primary-hover prose-a:underline 1352prose-strong:text-gray-900 prose-strong:font-semibold 1353prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-4 1354prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-4 1355prose-li:text-gray-600 prose-li:mb-2 1356prose-blockquote:border-l-4 prose-blockquote:border-brand-primary prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-gray-500 1357prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono 1358prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:p-4 prose-pre:rounded-lg prose-pre:overflow-x-auto" 1359><p style="line-height: 1">Following in his father's footsteps, Bryan Croft joined the Simply Stamps family in 1998 as a screen printer. Inspired by the joy that Mr. Holmes and Bob brought with each stamp they created, Bryan was driven to expand the business even further. As the internet gained prominence, Bryan saw a chance to introduce stamps to a much broader audience. This vision led to the launch of Simply Stamps' online platform in 2009, allowing us to reach the digital community and continue celebrating lifeâs special moments with the world.</p></div> </section></div></div>
1359<div class="zephyr-builder-zone col-span-12 sm:col-span-6"><div class="zephyr-builder-block" 1360data-placement-id="2868" 1361data-layout-id="33" 1362data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"><div class="flex justify-center"> <img 1363src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_about-us_mr-holmes-retro-pic.png" 1364alt="image from 1950s of Ownen Holmes, founder of Holmes Stamp Co." 1365class="max-w-full h-auto rounded-lg shadow-sm" 1366/></div></div> </section></div></div></div></div></div></div><div class="zephyr-builder-section" 1367data-section-index="2" 1368data-section-id="sec_8a705068" 1369data-layout-id="33" 1370data-bg-color="" 1371data-bg-image="" 1372data-overlay-color="" 1373data-overlay-opacity="0" 1374data-full-width="xl" 1375><div class="mx-auto max-w-screen-xl"><div class="zephyr-builder-row" 1376data-section-index="2" 1377data-row-index="0"><div class="zephyr-builder-block" 1378data-placement-id="2869" 1379data-layout-id="33" 1380data-library-block="content_seo_header"> <section class="pt-8 pb-2 border-0"><div class="w-full px-4 sm:px-0 justify-center text-center"><div class="text-center"><h2 class=" text-xl sm:text-2xl md:text-3xl font-bold mb-4" style="color: #012e4b;"> The Stamp Experts Personalizing Your Products for Over 30 Years</h2></div></div> </section></div></div><div class="zephyr-builder-row" 1381data-section-index="2" 1382data-row-index="1"><div class="grid grid-cols-12 gap-6"><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1383data-placement-id="2870" 1384data-layout-id="33" 1385data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"> <a href="https://www.simplystamps.com/blog/bryan-croft" class="group block"><div class="flex justify-center"> <img 1386src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_About-us_bryan_author-bio.jpg" 1387alt="Mr.Holmes on the left and Bryan Croft on the right sitting at a dinner table" 1388class="max-w-full h-auto rounded-lg shadow-sm transition-all duration-300 group-hover:scale-105 group-hover:shadow-xl" 1389/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900 transition-all duration-300 group-hover:scale-105"> Bryan Croft</h3> <span class="block mt-2 text-base sm:text-lg text-gray-600 transition-all duration-300 group-hover:scale-105"> Bryan inherited a love for stamps that has spanned generations. Inspired by his dad's decade-long ownership of the business, Bryan carries the torch with the same passion. </span></div> </a></div> </section></div></div><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1390data-placement-id="2871" 1391data-layout-id="33" 1392data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"> <a href="https://www.simplystamps.com/blog/dottie-burleigh" class="group block"><div class="flex justify-center"> <img 1393src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_About_us_Dottie_300x325.jpg" 1394alt="ottie leaning over a work bench looking at the rubber impressions that are ready to be separated" 1395class="max-w-full h-auto rounded-lg shadow-sm transition-all duration-300 group-hover:scale-105 group-hover:shadow-xl" 1396/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900 transition-all duration-300 group-hover:scale-105"> Dottie Burleigh</h3> <span class="block mt-2 text-base sm:text-lg text-gray-600 transition-all duration-300 group-hover:scale-105"> Dottie has been a cherished member of our team since 1986. With her extensive knowledge of our stamp products, she now operates our laser-engraving machines. </span></div> </a></div> </section></div></div><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1397data-placement-id="2872" 1398data-layout-id="33" 1399data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"> <a href="https://www.simplystamps.com/blog/mortensens-interview" class="group block"><div class="flex justify-center"> <img 1400src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_About_us_Mortensons_300x325.jpg" 1401alt="Mick and Kelly dressed nicely smiling for the camera" 1402class="max-w-full h-auto rounded-lg shadow-sm transition-all duration-300 group-hover:scale-105 group-hover:shadow-xl" 1403/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900 transition-all duration-300 group-hover:scale-105"> The Mortensens</h3> <span class="block mt-2 text-base sm:text-lg text-gray-600 transition-all duration-300 group-hover:scale-105"> The Mortensens have been working in the stamp industry together for over 40 years. With their shared expertise, they ensure the smooth operation of the business alongside their son, Zac. </span></div> </a></div> </section></div></div>
1403<div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1404data-placement-id="2873" 1405data-layout-id="33" 1406data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"> <a href="https://www.simplystamps.com/blog/jesse-devaughn-interview" class="group block"><div class="flex justify-center"> <img 1407src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_About_us_Jesse_300x325.jpg" 1408alt="Jesse as a little kid sitting on the stamp assembly work bench" 1409class="max-w-full h-auto rounded-lg shadow-sm transition-all duration-300 group-hover:scale-105 group-hover:shadow-xl" 1410/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900 transition-all duration-300 group-hover:scale-105"> Jesse DeVaughn</h3> <span class="block mt-2 text-base sm:text-lg text-gray-600 transition-all duration-300 group-hover:scale-105"> Jesse practically grew up in the rubber stamp business. He learned the tricks of the trade from his dad over 30 years ago, and now he leads production at our northern factory. </span></div> </a></div> </section></div></div></div></div></div></div><div class="zephyr-builder-section" 1411data-section-index="3" 1412data-section-id="sec_03ed50b3" 1413data-layout-id="33" 1414data-bg-color="" 1415data-bg-image="" 1416data-overlay-color="" 1417data-overlay-opacity="0" 1418data-full-width="xl" 1419><div class="mx-auto max-w-screen-xl"><div class="zephyr-builder-row" 1420data-section-index="3" 1421data-row-index="0"><div class="zephyr-builder-block" 1422data-placement-id="2874" 1423data-layout-id="33" 1424data-library-block="content_seo_header"> <section class="pt-8 pb-2 border-0"><div class="w-full px-4 sm:px-0 justify-center text-center"><div class="text-center"><h2 class=" text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 mb-4"> Partnerships and Brands We Trust</h2></div></div> </section></div><div class="zephyr-builder-block" 1425data-placement-id="2875" 1426data-layout-id="33" 1427data-library-block="content_markdown"> <section class="pt-2 pb-2 border-0"><div class="prose prose-lg max-w-none px-4 sm:px-0 1428prose-headings:text-gray-900 prose-headings:font-bold 1429prose-h2:text-2xl prose-h2:md:text-3xl prose-h2:mt-8 prose-h2:mb-4 1430prose-h3:text-xl prose-h3:md:text-2xl prose-h3:mt-6 prose-h3:mb-3 1431prose-p:text-gray-600 prose-p:leading-relaxed prose-p:mb-4 1432prose-a:text-brand-primary hover:prose-a:text-brand-primary-hover prose-a:underline 1433prose-strong:text-gray-900 prose-strong:font-semibold 1434prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-4 1435prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-4 1436prose-li:text-gray-600 prose-li:mb-2 1437prose-blockquote:border-l-4 prose-blockquote:border-brand-primary prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-gray-500 1438prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono 1439prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:p-4 prose-pre:rounded-lg prose-pre:overflow-x-auto" 1440><p>In partnership with leading manufacturers such as Trodat, Shiny, and Colop, we offer unparalleled quality and innovation in every stamp we create. Discover the ideal stamp for your home, business, or creative projects. Choose from our unique address stamp designs, enhance your branding with custom logo stamps, streamline your work with signature stamps, or our wide assortment of business stamps. Design your own custom stamp today!</p></div> </section></div></div><div class="zephyr-builder-row" 1441data-section-index="3" 1442data-row-index="1"><div class="grid grid-cols-12 gap-6"><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1443data-placement-id="2876" 1444data-layout-id="33" 1445data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 90%"><div class="flex justify-center"> <img 1446src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/trodat-logo.jpg" 1447alt="Trodat Creating Originals Brand" 1448class="max-w-full h-auto rounded-lg shadow-sm" 1449/></div></div> </section></div></div>
1449<div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1450data-placement-id="2877" 1451data-layout-id="33" 1452data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 80%"><div class="flex justify-center"> <img 1453src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/trotec-logo.png" 1454alt="Trodec laser. marking cutting engraving" 1455class="max-w-full h-auto rounded-lg shadow-sm" 1456/></div></div> </section></div></div><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1457data-placement-id="2878" 1458data-layout-id="33" 1459data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 90%"><div class="flex justify-center"> <img 1460src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/colop_logo.png" 1461alt="Colop Marking Solutions" 1462class="max-w-full h-auto rounded-lg shadow-sm" 1463/></div></div> </section></div></div><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1464data-placement-id="2879" 1465data-layout-id="33" 1466data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 80%"><div class="flex justify-center"> <img 1467src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/shiny-logo.png" 1468alt="Shiny America" 1469class="max-w-full h-auto rounded-lg shadow-sm" 1470/></div></div> </section></div></div></div></div></div></div><div class="zephyr-builder-section" 1471data-section-index="4" 1472data-section-id="sec_89f2d726" 1473data-layout-id="33" 1474data-bg-color="" 1475data-bg-image="" 1476data-overlay-color="" 1477data-overlay-opacity="0" 1478data-full-width="2xl" 1479><div class="mx-auto max-w-screen-2xl"><div class="zephyr-builder-row" 1480data-section-index="4" 1481data-row-index="0"><div class="zephyr-builder-block" 1482data-placement-id="2880" 1483data-layout-id="33" 1484data-library-block="content_seo_header"> <section class="pt-8 pb-2 border-0"><div class="w-full px-4 sm:px-0 justify-center text-center"><div class="text-center"><h2 class=" text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 mb-4"> Simply Stamps Core Values</h2></div></div> </section></div></div></div></div><div class="zephyr-builder-section" 1485data-section-index="5" 1486data-section-id="sec_7a7335a0" 1487data-layout-id="33" 1488data-bg-color="" 1489data-bg-image="" 1490data-overlay-color="" 1491data-overlay-opacity="0" 1492data-full-width="lg" 1493><div class="mx-auto max-w-screen-lg"><div class="zephyr-builder-row" 1494data-section-index="5" 1495data-row-index="0"><div class="grid grid-cols-12 gap-6"><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1496data-placement-id="2881" 1497data-layout-id="33" 1498data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"><div class="flex justify-center"> <img 1499src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_core-values_Passion.png" 1500alt="Passion" 1501class="max-w-full h-auto rounded-lg shadow-sm" 1502/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900"> Passion</h3> <span class="block mt-2 text-sm sm:text-base text-gray-600"> Grow with contagious energy! </span></div></div> </section></div></div><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1503data-placement-id="2882" 1504data-layout-id="33" 1505data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"><div class="flex justify-center"> <img 1506src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_core-values_Pride.png" 1507alt="Pride" 1508class="max-w-full h-auto rounded-lg shadow-sm" 1509/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900"> Pride</h3> <span class="block mt-2 text-sm sm:text-base text-gray-600"> Embrace a winning attitude. Be proud to deliver excellence every day! </span></div></div> </section></div></div><div class="zephyr-builder-zone col-span-6 md:col-span-3">
1509<div class="zephyr-builder-block" 1510data-placement-id="2883" 1511data-layout-id="33" 1512data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"><div class="flex justify-center"> <img 1513src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_core-values_Curiosity.png" 1514alt="Curiosity " 1515class="max-w-full h-auto rounded-lg shadow-sm" 1516/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900"> Curiosity</h3> <span class="block mt-2 text-sm sm:text-base text-gray-600"> Be opportunistic and look at our business differently. </span></div></div> </section></div></div><div class="zephyr-builder-zone col-span-6 md:col-span-3"><div class="zephyr-builder-block" 1517data-placement-id="2884" 1518data-layout-id="33" 1519data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"><div class="flex justify-center"> <img 1520src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_core-values_Collaboration.png" 1521alt="Collaboration" 1522class="max-w-full h-auto rounded-lg shadow-sm" 1523/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900"> Collaboration</h3> <span class="block mt-2 text-sm sm:text-base text-gray-600"> Think BIG and make thoughtful decisions as a team. </span></div></div> </section></div></div></div></div></div></div><div class="zephyr-builder-section" 1524data-section-index="6" 1525data-section-id="sec_8db6d3d1" 1526data-layout-id="33" 1527data-bg-color="" 1528data-bg-image="" 1529data-overlay-color="" 1530data-overlay-opacity="0" 1531data-full-width="md" 1532><div class="mx-auto max-w-screen-md"><div class="zephyr-builder-row" 1533data-section-index="6" 1534data-row-index="0"><div class="grid grid-cols-12 gap-6"><div class="zephyr-builder-zone col-span-12 sm:col-span-6 md:col-span-4"><div class="zephyr-builder-block" 1535data-placement-id="2885" 1536data-layout-id="33" 1537data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 90%"><div class="flex justify-center"> <img 1538src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_core-values_Accountability.png" 1539alt="Accountability" 1540class="max-w-full h-auto rounded-lg shadow-sm" 1541/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900"> Accountability</h3> <span class="block mt-2 text-sm sm:text-base text-gray-600"> NO EXCUSES. Plan it and own it. </span></div></div> </section></div></div><div class="zephyr-builder-zone col-span-12 sm:col-span-6 md:col-span-4"><div class="zephyr-builder-block" 1542data-placement-id="2886" 1543data-layout-id="33" 1544data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 90%"><div class="flex justify-center"> <img 1545src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_core-values_Grit.png" 1546alt="Grit" 1547class="max-w-full h-auto rounded-lg shadow-sm" 1548/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900"> Grit</h3> <span class="block mt-2 text-sm sm:text-base text-gray-600"> Roll up your sleeves and get your hands inky. </span></div></div> </section></div></div><div class="zephyr-builder-zone col-span-12 sm:col-span-6 md:col-span-4"><div class="zephyr-builder-block" 1549data-placement-id="2887" 1550data-layout-id="33" 1551data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 90%"><div class="flex justify-center"> <img 1552src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_core-values_Winning.png" 1553alt="Winning" 1554class="max-w-full h-auto rounded-lg shadow-sm" 1555/></div><div class="text-center mt-3 md:mt-4"><h3 class=" text-xl sm:text-2xl font-bold text-gray-900"> Winning</h3> <span class="block mt-2 text-sm sm:text-base text-gray-600"> Give our customers 1% more every day. </span></div></div> </section></div></div></div></div></div></div><div class="zephyr-builder-section" 1556data-section-index="7" 1557data-section-id="sec_a949e02d" 1558data-layout-id="33" 1559data-bg-color="" 1560data-bg-image="" 1561data-overlay-color="" 1562data-overlay-opacity="0" 1563data-full-width="yes" 1564>
1564<div class="zephyr-builder-row" 1565data-section-index="7" 1566data-row-index="0"><div class="zephyr-builder-block" 1567data-placement-id="2888" 1568data-layout-id="33" 1569data-library-block="media_image"> <section class="py-8 md:py-12"><div class="mx-auto px-1 sm:px-2" style="max-width: 100%"><div class="flex justify-center"> <img 1570src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/about-us/SS_About-Us_Full-Opacity_1920x615-50.jpg" 1571alt="Team Picture with all HC Brands Employees" 1572class="max-w-full h-auto rounded-none shadow-sm" 1573/></div></div> </section></div></div></div></div></div></div></div> </main> <footer id="site-footer" class="w-full"><div class="relative"><div class="widget widget-static-block"><style> /* ââ SimplyStamps Shipping ETA Timer ââ */ 1574/* v4.0 â Unified copy, color does the talking */ 1575.ss-ship-eta { 1576display: none; 1577flex-direction: column; 1578gap: 5px; 1579padding: 12px 16px; 1580border-radius: 8px 8px 0px 0px; 1581background: #eef7ff; 1582width: 100%; 1583box-sizing: border-box; 1584margin-top: 0; 1585margin-bottom: 0px; 1586border: 1px solid #d1e9ff; 1587transition: background 0.35s ease, border-color 0.35s ease; 1588font-family: inherit; 1589} 1590.ss-ship-eta__topline { 1591font-size: 14px; 1592font-weight: 600; 1593color: #374151; 1594line-height: 1.4; 1595margin: 0; 1596} 1597.ss-ship-eta__hero { 1598font-size: 14px; 1599color: #374151; 1600line-height: 1.5; 1601} 1602.ss-ship-eta__hero-date { 1603font-weight: 700; 1604} 1605.ss-ship-eta__hero-tier { 1606font-size: 12px; 1607color: #9ca3af; 1608margin-left: 6px; 1609} 1610.ss-ship-eta__toggle { 1611background: none; 1612border: none; 1613padding: 0; 1614margin-top: 3px; 1615font-size: 12px; 1616font-weight: 600; 1617color: #1d4ed8; 1618cursor: pointer; 1619display: inline-flex; 1620align-items: center; 1621gap: 3px; 1622font-family: inherit; 1623} 1624.ss-ship-eta__toggle:hover { text-decoration: underline; } 1625.ss-ship-eta__toggle-arrow { 1626font-size: 10px; 1627transition: transform 0.2s ease; 1628display: inline-block; 1629} 1630.ss-ship-eta__toggle-arrow.ss-open { 1631transform: rotate(180deg); 1632} 1633.ss-ship-eta__more { 1634display: none; 1635flex-direction: column; 1636gap: 1px; 1637margin-top: 4px; 1638font-size: 14px; 1639color: #374151; 1640line-height: 1.5; 1641} 1642.ss-ship-eta__more.ss-visible { 1643display: flex; 1644} 1645.ss-ship-eta__more-row { 1646display: flex; 1647align-items: baseline; 1648gap: 6px; 1649} 1650.ss-ship-eta__more-label { 1651font-size: 12px; 1652font-weight: 500; 1653color: #9ca3af; 1654min-width: 52px; 1655} 1656.ss-ship-eta__more-date { 1657font-weight: 500; 1658} 1659.ss-eta-cd { 1660font-weight: 800; 1661color: #1d4ed8; 1662} 1663/* ââ Warn state ââ */ 1664.ss-ship-eta.ss-warn { 1665background: #fffbeb; 1666border-color: #fde68a; 1667} 1668.ss-ship-eta.ss-warn .ss-ship-eta__topline, 1669.ss-ship-eta.ss-warn .ss-ship-eta__hero { color: #78350f; } 1670.ss-ship-eta.ss-warn .ss-ship-eta__more { color: #78350f; } 1671.ss-ship-eta.ss-warn .ss-eta-cd { color: #d97706; } 1672.ss-ship-eta.ss-warn .ss-ship-eta__toggle { color: #b45309; } 1673/* ââ Urgent state ââ */ 1674.ss-ship-eta.ss-urgent { 1675background: #fff1f2; 1676border-color: #fecdd3; 1677} 1678.ss-ship-eta.ss-urgent .ss-ship-eta__topline, 1679.ss-ship-eta.ss-urgent .ss-ship-eta__hero { color: #7f1d1d; } 1680.ss-ship-eta.ss-urgent .ss-ship-eta__more { color: #7f1d1d; } 1681.ss-ship-eta.ss-urgent .ss-eta-cd { 1682color: #dc2626; 1683animation: ss-pulse 1.1s ease-in-out 5; 1684} 1685.ss-ship-eta.ss-urgent .ss-ship-eta__toggle { color: #dc2626; } 1686@keyframes ss-pulse { 16870%, 100% { opacity: 1; } 168850% { opacity: 0.5; } 1689} 1690/* ââ After-cutoff state ââ */ 1691.ss-ship-eta.ss-after { 1692background: #f9fafb; 1693border-color: #e5e7eb; 1694} 1695.ss-ship-eta.ss-after .ss-ship-eta__topline, 1696.ss-ship-eta.ss-after .ss-ship-eta__hero { color: #6b7280; } 1697.ss-ship-eta.ss-after .ss-ship-eta__more { color: #6b7280; } 1698.ss-ship-eta.ss-after .ss-ship-eta__toggle { color: #6b7280; } </style>
1698<script> (() => { 1699var TIMEZONE = "America/New_York"; 1700var CUTOFF_HOUR_ET = 17; 1701var CUTOFF_MINUTE_ET = 0; 1702var HOLIDAYS_MMDD = new Set([ 1703"01/01","01/19","02/16","05/25","07/03","09/07", 1704"11/26","11/27","12/24","12/25","12/31" 1705]); 1706var OFFSETS = { rush: 2, express: 3, standard: 5, economy: 6 }; 1707var STD_LABEL = "Preferred"; 1708var SHIPPING_CONTAINER_SELECTOR = '.ships-and-made-in-usa-container'; 1709var FALLBACK_PARENT_SELECTOR = '.vpt-product'; 1710var PFX = "ss"; 1711function createTimerEl() { 1712var div = document.createElement("div"); 1713div.className = PFX + "-ship-eta"; 1714div.setAttribute("aria-label", "Delivery estimates"); 1715div.innerHTML = 1716'<div class="' + PFX + '-ship-eta__topline"></div>' + 1717'<div class="' + PFX + '-ship-eta__hero"></div>' + 1718'<button class="' + PFX + '-ship-eta__toggle" type="button" aria-expanded="false">' + 1719'All shipping options <span class="' + PFX + '-ship-eta__toggle-arrow">\u25BC</span>' + 1720'</button>' + 1721'<div class="' + PFX + '-ship-eta__more">' + 1722'<div class="' + PFX + '-ship-eta__more-row" data-tier="express"></div>' + 1723'<div class="' + PFX + '-ship-eta__more-row" data-tier="standard"></div>' + 1724'<div class="' + PFX + '-ship-eta__more-row" data-tier="economy"></div>' + 1725'</div>'; 1726var toggleEl = div.querySelector('.' + PFX + '-ship-eta__toggle'); 1727var arrowEl = div.querySelector('.' + PFX + '-ship-eta__toggle-arrow'); 1728var moreEl = div.querySelector('.' + PFX + '-ship-eta__more'); 1729var expanded = false; 1730toggleEl.addEventListener("click", function() { 1731expanded = !expanded; 1732moreEl.classList.toggle(PFX + "-visible", expanded); 1733arrowEl.classList.toggle(PFX + "-open", expanded); 1734toggleEl.setAttribute("aria-expanded", expanded); 1735toggleEl.firstChild.textContent = expanded ? "Hide options " : "All shipping options "; 1736}); 1737return div; 1738} 1739function getEasternParts(date) { 1740date = date || new Date(); 1741var fmt = new Intl.DateTimeFormat("en-US", { 1742timeZone: TIMEZONE, 1743year: "numeric", month: "2-digit", day: "2-digit", 1744hour: "2-digit", minute: "2-digit", second: "2-digit", 1745hour12: false 1746}); 1747var out = {}; 1748var parts = fmt.formatToParts(date); 1749for (var i = 0; i < parts.length; i++) out[parts[i].type] = parts[i].value; 1750return out; 1751} 1752function getNowET() { 1753var p = getEasternParts(); 1754return new Date(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second); 1755} 1756function mmdd(d) { 1757return String(d.getMonth()+1).padStart(2,"0") + "/" + String(d.getDate()).padStart(2,"0"); 1758} 1759function isWeekend(d) { var w = d.getDay(); return w === 0 || w === 6; } 1760function isHoliday(d) { return HOLIDAYS_MMDD.has(mmdd(d)); } 1761function isBusinessDay(d) { return !isWeekend(d) && !isHoliday(d); } 1762function startOfDay(d) { var x = new Date(d); x.setHours(0,0,0,0); return x; } 1763function nextDay(d) { var x = new Date(d); x.setDate(x.getDate()+1); return x; } 1764function nextBusinessDay(from) { 1765var x = nextDay(from); 1766while (!isBusinessDay(x)) x = nextDay(x); 1767return startOfDay(x); 1768} 1769function addBusinessDays(from, n) { 1770var x = startOfDay(from); 1771var added = 0; 1772while (added < n) { x = nextDay(x); if (isBusinessDay(x)) added++; } 1773return x; 1774} 1775function formatDate(d) { 1776return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "
1776numeric" }); 1777} 1778function countdownStr(diffMs) { 1779var totalSecs = Math.max(0, Math.floor(diffMs / 1000)); 1780var totalMins = Math.floor(totalSecs / 60); 1781var hrs = Math.floor(totalSecs / 3600); 1782var mins = Math.floor((totalSecs % 3600) / 60); 1783var secs = totalSecs % 60; 1784if (totalMins < 5) { 1785return hrs + "h " + String(mins).padStart(2,"0") + "m " + String(secs).padStart(2,"0") + "s"; 1786} 1787return hrs + " hr" + (hrs !== 1 ? "s" : "") + " " + String(mins).padStart(2,"0") + " min"; 1788} 1789var instances = []; 1790function ensureInstances() { 1791var containers = document.querySelectorAll(SHIPPING_CONTAINER_SELECTOR); 1792var placed = false; 1793for (var i = 0; i < containers.length; i++) { 1794var container = containers[i]; 1795var prev = container.previousElementSibling; 1796if (prev && prev.classList.contains(PFX + "-ship-eta")) { 1797placed = true; 1798continue; 1799} 1800var timer = createTimerEl(); 1801container.insertAdjacentElement("beforebegin", timer); 1802instances.push(timer); 1803placed = true; 1804} 1805if (!placed) { 1806var fallback = document.querySelector(FALLBACK_PARENT_SELECTOR); 1807if (fallback) { 1808var first = fallback.firstElementChild; 1809if (!first || !first.classList.contains(PFX + "-ship-eta")) { 1810var timer = createTimerEl(); 1811fallback.insertAdjacentElement("afterbegin", timer); 1812instances.push(timer); 1813placed = true; 1814} 1815} 1816} 1817return placed; 1818} 1819function renderAll() { 1820var nowET = getNowET(); 1821var cutoff = new Date(nowET); 1822cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0); 1823var diffMs = cutoff - nowET; 1824var totalMins = Math.floor(diffMs / 60000); 1825var isPastCutoff = nowET >= cutoff; 1826var isOffDay = !isBusinessDay(nowET); 1827var urgencyClass = null; 1828var toplineHTML = ""; 1829var base; 1830if (isPastCutoff || isOffDay) { 1831urgencyClass = PFX + "-after"; 1832base = nextBusinessDay(nowET); 1833toplineHTML = 'Get as early as:'; 1834} else { 1835var cdStr = countdownStr(diffMs); 1836base = startOfDay(nowET); 1837toplineHTML = 'Order in <span class="' + PFX + '-eta-cd">' + cdStr + '</span> to get as early as:'; 1838if (totalMins <= 120) { 1839urgencyClass = PFX + "-urgent"; 1840} else if (totalMins <= 240) { 1841urgencyClass = PFX + "-warn"; 1842} 1843} 1844var rushDate = addBusinessDays(base, OFFSETS.rush); 1845var expressDate = addBusinessDays(base, OFFSETS.express); 1846var stdDate = addBusinessDays(base, OFFSETS.standard); 1847var ecoDate = addBusinessDays(base, OFFSETS.economy); 1848var heroHTML = '<span class="' + PFX + '-ship-eta__hero-date">' + formatDate(rushDate) + '</span><span class="' + PFX + '-ship-eta__hero-tier">via Rush</span>'; 1849var expressHTML = '<span class="' + PFX + '-ship-eta__more-label">Express</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(expressDate) + '</span>'; 1850var stdHTML = '<span class="' + PFX + '-ship-eta__more-label">' + STD_LABEL + '</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(stdDate) + '</span>'; 1851var ecoHTML = '<span class="' + PFX + '-ship-eta__more-label">Economy</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(ecoDate) + '</span>'; 1852for (var i = 0; i < instances.length; i++) { 1853var el = instances[i]; 1854el.classList.remove(PFX + "-warn", PFX + "-urgent", PFX + "-after"); 1855if (urgencyClass) el.classList.add(urgencyClass); 1856el.querySelector('.' + PFX + '-ship-eta__topline').innerHTML = toplineHTML; 1857el.querySelector('.' + PFX + '-ship-eta__hero').innerHTML = heroHTML; 1858el.querySelector('[data-tier="express"]').innerHTML = expressHTML; 1859el.querySelector('[data-tier="standard"]').innerHTML = stdHTML; 1860el.querySelector('[data-tier="economy"]').innerHTML = ecoHTML; 1861el.style.display = "flex"; 1862} 1863} 1864function tick() { 1865var placed = ensureInstances(); 1866if (placed && instances.length > 0) renderAll(); 1867var nowET = getNowET(); 1868var cutoff = new Date(nowET); 1869cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0); 1870var minsLeft = Math.floor((cutoff - nowET) / 60000); 1871var interval = !placed ? 250 1872: (minsLeft >= 0 && minsLeft < 5) ? 1000 1873: 60000; 1874setTimeout(tick, interval); 1875} 1876tick(); 1877if (window.MutationObserver) { 1878var mo = new MutationObserver(function() { ensureInstances(); }); 1879mo.observe(document.body, { childList: true, subtree: true }); 1880} 1881})(); 1882</script>
1882</div> <style> .full-size-container.footer-marketing { 1883background-color: #60b5e5; 1884width: 100vw; 1885margin-top: 30px; 1886} </style> <style> .CustomerServiceNotice img { 1887width: 100%; 1888margin-bottom: 2%; 1889} 1890.CustomerServiceNotice .modal-footer { 1891display: none; 1892} </style></div> <section id="zephyr-signup-band" 1893class="relative w-full py-10 sm:py-12 bg-brand-primary text-white" 1894x-data="zephyrSignupBand()" 1895aria-labelledby="zephyr-signup-headline"><div class="relative max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><div x-ref="stage" 1896class="relative flex flex-col justify-center"><div x-ref="panelOffers" 1897class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1898:class="view === 'offers' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'" 1899: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" 1900@click="choose('email_sms')" 1901class="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">
1901 <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" 1902@click="choose('email')" 1903class="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" 1904x-cloak 1905class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1906:class="view === 'email' || view === 'email_sms' 1907? 'relative opacity-100 visible' 1908: 'absolute inset-x-0 top-0 opacity-0 invisible'" 1909: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" 1910x-transition 1911role="alert" 1912aria-live="polite" 1913class="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" 1914id="zephyr-signup-email" 1915x-model="email" 1916required 1917autocomplete="email" 1918placeholder="[email protected]" 1919class="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" 1920aria-hidden="true">US +1</span> <input type="tel" 1921id="zephyr-signup-phone" 1922x-model="phone" 1923autocomplete="tel-national" 1924inputmode="tel" 1925placeholder="(555) 555-5555" 1926aria-describedby="zephyr-signup-phone-hint" 1927class="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" 1928id="zephyr-signup-consent" 1929x-model="consent" 1930class="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" 1931:disabled="loading" 1932class="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" 1933@click="back()"
1934class="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" 1935x-cloak 1936class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1937:class="view === 'done' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'" 1938: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>
1938<script> /* 1939* Hide the band synchronously, before paint, for anyone who has already signed 1940* up. This runs inline rather than through Alpine on purpose: the footer is 1941* served from the Fastly cache, so the cookie is the only signal available, and 1942* Alpine boots after first paint â gating on x-show alone would render the band 1943* and then yank it, shifting the footer. 1944*/ 1945(function () { 1946if (document.cookie.indexOf('zephyr_signup=') !== -1) { 1947var band = document.getElementById('zephyr-signup-band'); 1948if (band) { 1949band.parentNode.removeChild(band); 1950} 1951} 1952})();function zephyrSignupBand() { 1953return { 1954view: 'offers', 1955email: '', 1956phone: '', 1957consent: false, 1958loading: false, 1959errorMessage: '', 1960successMessage: '', 1961postUrl: 'https://www.simplystamps.com/zephyr/signup/post/', 1962hideDays: 365,/* Per-offer copy, so the two form states share one set of markup. */ 1963copy: { 1964email: { 1965heading: 'Save 10% - Subscribe to Email Only', 1966subheading: 'Please enter your email below to receive your discount code!', 1967submit: 'Get My Discount Code' 1968}, 1969email_sms: { 1970heading: 'Save 15% - Subscribe to Email + Text', 1971subheading: 'Please enter your email address and your phone number to receive the 20% off discount code.', 1972submit: 'Get my 15% Off Now!' 1973} 1974},/* 1975* While `measuring` is on, the form panel renders in its tallest 1976* configuration so it can be sized. See measureTallest(). 1977*/ 1978formCopy: function () { 1979return this.copy[this.view] 1980|| (this.measuring ? this.copy.email_sms : null); 1981},headingText: function () { 1982var copy = this.formCopy(); 1983return copy ? copy.heading : ''; 1984},subheadingText: function () { 1985var copy = this.formCopy(); 1986return copy ? copy.subheading : ''; 1987},submitText: function () { 1988var copy = this.formCopy(); 1989return copy ? copy.submit : ''; 1990},measuring: false,init: function () { 1991this.measureTallest();// Panel heights change with the breakpoint, so re-pin on resize. 1992var self = this; 1993var timer = null; 1994window.addEventListener('resize', function () { 1995clearTimeout(timer); 1996timer = setTimeout(function () { self.measureTallest(); }, 150); 1997}); 1998},/* 1999* Pin the stage to the tallest panel so switching states moves nothing 2000* on the page at all. 2001* 2002* The form panel has to be measured in its email+SMS configuration â 2003* its phone and consent blocks are x-show'd off in the email-only view, 2004* and its heading is bound to the current view, so it otherwise reports 2005* a short height. `measuring` forces that configuration on, and because 2006* an inactive panel is `absolute` + `invisible` (never display:none) it 2007* is measurable without ever being seen. 2008* 2009* Skipped while a form is on screen: `measuring` would briefly reveal 2010* the phone and consent fields in the email-only view. The pin just 2011* stays as-is until the visitor returns to the offers. 2012*/ 2013measureTallest: function () { 2014var stage = this.$refs.stage; 2015if (!stage || this.view === 'email' || this.view === 'email_sms') { 2016return; 2017}var self = this; 2018this.measuring = true;this.$nextTick(function () { 2019var tallest = 0; 2020['panelOffers', 'panelForm', 'panelDone'].forEach(function (ref) { 2021var panel = self.$refs[ref]; 2022if (panel) { 2023tallest = Math.max(tallest, panel.offsetHeight); 2024} 2025});self.measuring = false; 2026if (tallest) { 2027stage.style.minHeight = tallest + 'px'; 2028} 2029}); 2030},choose: function (which) { 2031this.errorMessage = ''; 2032this.view = which;// After the DOM updates, not before: the field sits in a panel that 2033// is still `invisible` at this point, and focus() is a silent no-op 2034// on a hidden element. 2035var self = this; 2036this.$nextTick(function () { 2037// Both offers open the same panel, so email is always first. 2038self.focusLater('zephyr-signup-email'); 2039}); 2040},back: function () { 2041this.errorMessage = ''; 2042this.view = 'offers'; 2043},/* Move focus to the first field without yanking the page around. */ 2044focusLater: function (focusId) { 2045if (!focusId) { 2046return; 2047} 2048var el = document.getElementById(focusId); 2049if (!el) { 2050return; 2051} 2052try { 2053el.focus({ preventScroll: true }); 2054} catch (e) { 2055el.focus(); 2056} 2057},submit: function () { 2058var self = this; 2059if (this.loading) { 2060return; 2061}// Client-side checks mirror the server's; the server is still the 2062// authority, these just save a round trip. 2063if (!this.email || this.email.indexOf('@') === -1) { 2064this.errorMessage = 'Enter a valid email address.'; 2065return; 2066} 2067if (this.view === 'email_sms') { 2068if (this.phone.replace(/[^0-9]/g, '').length < 10) { 2069this.errorMessage = 'Enter a valid 10-digit US mobile number.'; 2070return; 2071} 2072if (!this.consent) { 2073this.errorMessage = 'Check the consent box to receive text messages.'; 2074return; 2075} 2076}this.loading = true; 2077this.errorMessage = '';var body = new FormData(); 2078body.append('email', this.email); 2079body.append('signup_type', this.view); 2080if (this.view === 'email_sms') { 2081body.append('phone', this.phone); 2082body.append('sms_consent', this.consent ? '1' : '0'); 2083}var send = function (token) { 2084if (token) { 2085body.append('recaptcha_response', token); 2086}fetch(self.postUrl, { 2087method: 'POST', 2088body: body, 2089credentials: 'same-origin' 2090}).then(function (response) { 2091return response.json(); 2092}).then(function (data) { 2093if (data && data.success) { 2094self.successMessage = data.message || ''; 2095self.view = 'done'; 2096self.setHideCookie(data.hide_days); 2097if (typeof zGA4 === 'function') { 2098zGA4('generate_lead', {}); 2099} 2100} else { 2101self.errorMessage = (data && data.error) 2102? data.error 2103: 'We could not complete your signup. Please try again in a moment.'; 2104} 2105}).catch(function () { 2106self.errorMessage = 'We could not complete your signup. Please try again in a moment.'; 2107}).then(function () { 2108self.loading = false;
2109}); 2110};if (typeof window.getZephyrRecaptchaToken === 'function') { 2111window.getZephyrRecaptchaToken('signup').then(send, function () { send(null); }); 2112} else { 2113send(null); 2114} 2115},/* Keeps the band hidden on later visits. 0 days means always show it. */ 2116setHideCookie: function (days) { 2117var lifetime = typeof days === 'number' ? days : this.hideDays; 2118if (!lifetime || lifetime < 1) { 2119return; 2120} 2121var expires = new Date(); 2122expires.setTime(expires.getTime() + (lifetime * 24 * 60 * 60 * 1000)); 2123document.cookie = 'zephyr_signup=1; expires=' + expires.toUTCString() 2124+ '; path=/; SameSite=Lax' 2125+ (location.protocol === 'https:' ? '; Secure' : ''); 2126} 2127}; 2128} 2129</script>
2129<div class="relative"><div class="widget widget-static-block"></div> <!-On PDP FAQ Accordion Compatibility Shim--><div class="widget widget-static-block"><style> /* --------------------------------------------------------------------------- 2130FAQ blocks â standalone styles for the legacy Bootstrap panel markup. 2131Pairs with faq-collapse.js. No Bootstrap, no dependencies. 2132Scoped to .faq_block / .faq_table so nothing else on the page inherits 2133these rules. Retune the custom properties below to match the theme. 2134--------------------------------------------------------------------------- */ 2135.faq_block { 2136--faq-border: #dcdcdc; 2137--faq-radius: 6px; 2138--faq-gap: 10px; 2139--faq-surface: #fff; 2140--faq-heading: #f7f7f7; 2141--faq-heading-hover: #efefef; 2142--faq-focus: #2563eb; 2143--faq-mark: #ffe9a8; 2144--faq-muted: #616161; 2145line-height: 1.55; 2146} 2147/* --- neutralize the layout table ---------------------------------------- */ 2148.faq_block .faq_table, 2149.faq_block .faq_table > tbody, 2150.faq_block .faq_table > tbody > tr, 2151.faq_block .faq_table > tbody > tr > td { 2152display: block; 2153width: 100%; 2154max-width: 100%; 2155margin: 0; 2156padding: 0; 2157border: 0; 2158background: none; 2159} 2160.faq_block .faq_table > tbody > tr > td + td { 2161margin-top: var(--faq-gap); 2162} 2163/* the intro row holds the section heading, not a panel */ 2164.faq_block .faq_table h3 { 2165margin: 0 0 calc(var(--faq-gap) * 1.5); 2166font-size: 1.5em; 2167line-height: 1.25; 2168} 2169/* --- panel ---------------------------------------------------------------- */ 2170.faq_block .panel { 2171margin: 0 0 var(--faq-gap); 2172background: var(--faq-surface); 2173border: 1px solid var(--faq-border); 2174border-radius: var(--faq-radius); 2175overflow: hidden; 2176} 2177.faq_block .panel:last-child { 2178margin-bottom: 0; 2179} 2180.faq_block .panel-heading { 2181padding: 0; 2182background: var(--faq-heading); 2183border-bottom: 1px solid transparent; 2184transition: background-color .15s ease; 2185} 2186.faq_block .panel-heading:hover { 2187background: var(--faq-heading-hover); 2188} 2189.faq_block .panel-title { 2190margin: 0; 2191font-size: 1em; 2192font-weight: 600; 2193line-height: inherit; 2194} 2195/* the trigger fills the whole heading so the entire bar is clickable */ 2196.faq_block .panel-title a { 2197display: flex; 2198align-items: center; 2199justify-content: space-between; 2200gap: 1em; 2201padding: 14px 16px; 2202color: inherit; 2203text-decoration: none; 2204cursor: pointer; 2205} 2206.faq_block .panel-title a:hover, 2207.faq_block .panel-title a:focus { 2208color: inherit; 2209text-decoration: none; 2210} 2211.faq_block .panel-title a:focus-visible { 2212outline: 2px solid var(--faq-focus); 2213outline-offset: -2px; 2214} 2215/* chevron â driven by aria-expanded, which the shim keeps in sync */ 2216.faq_block .panel-title a::after { 2217content: ''; 2218flex: 0 0 auto; 2219width: .45em; 2220height: .45em; 2221margin-top: -.22em; 2222border-right: 2px solid currentColor; 2223border-bottom: 2px solid currentColor; 2224transform: rotate(45deg); 2225transition: transform .2s ease, margin-top .2s ease; 2226opacity: .65; 2227} 2228.faq_block .panel-title a[aria-expanded="true"]::after { 2229margin-top: .18em; 2230transform: rotate(-135deg); 2231} 2232/* --- answer body ---------------------------------------------------------- */ 2233/* Fallback for a JS failure: the browser still reveals a panel when its id is 2234in the URL, so the answers stay reachable. */ 2235.faq_block .panel-collapse:target { 2236display: block !important; 2237} 2238/* divider only appears once a panel is open (:has() â no-op on old browsers) */ 2239.faq_block .panel:has(.panel-collapse.in) .panel-heading { 2240border-bottom-color: var(--faq-border); 2241} 2242/* Tailwind ships a `.collapse` utility (visibility: collapse) that collides with 2243the legacy Bootstrap class of the same name in this markup. Two class-level 2244selectors here so this wins on specificity regardless of stylesheet order. */ 2245.faq_block .panel-collapse, 2246.faq_table .panel-collapse { 2247visibility: visible; 2248} 2249/* padding lives on .panel-body, never margins â the height animation measures 2250scrollHeight on .panel-collapse and collapsing margins would break it */ 2251.faq_block .panel-body { 2252padding: 14px 16px 16px; 2253} 2254.faq_block .panel-body > :first-child { 2255margin-top: 0; 2256} 2257.faq_block .panel-body > :last-child { 2258margin-bottom: 0; 2259} 2260.faq_block .panel-body ol, 2261.faq_block .panel-body ul { 2262margin: .75em 0; 2263padding-left: 1.4em; 2264} 2265.faq_block .panel-body li { 2266margin-bottom: .35em; 2267} 2268.faq_block .panel-body em { 2269display: block; 2270margin-top: .9em; 2271color: var(--faq-muted); 2272} 2273/* legacy inline font sizes inside answers (span style="font-size: medium") 2274fight the 14px wrapper â normalize them back to the block's scale */ 2275.faq_block .panel-body [style*="font-size"] { 2276font-size: inherit !important; 2277} 2278/* --- search --------------------------------------------------------------- */ 2279.faq_block mark, 2280.faq_table mark { 2281padding: 0 1px; 2282background: var(--faq-mark); 2283color: inherit; 2284border-radius: 2px; 2285} 2286#search_faq, 2287[data-faq-search] { 2288width: 100%; 2289max-width: 420px; 2290margin-bottom: calc(var(--faq-gap) * 1.5); 2291padding: 10px 12px; 2292font: inherit; 2293color: inherit; 2294background: #fff; 2295border: 1px solid #dcdcdc; 2296border-radius: 6px; 2297} 2298#search_faq:focus, 2299[data-faq-search]:focus { 2300border-color: #2563eb; 2301outline: 2px solid rgba(37, 99, 235, .25); 2302outline-offset: 0; 2303} 2304/* --- small screens -------------------------------------------------------- */ 2305@media (max-width: 480px) { 2306.faq_block .panel-title a { 2307padding: 12px 13px; 2308} 2309.faq_block .panel-body { 2310padding: 12px 13px 14px; 2311} 2312} </style>
2312<script> /** 2313* FAQ collapse + search shim (no jQuery, no Bootstrap JS, no mark.js) 2314* 2315* Restores the behavior of the legacy CMS FAQ blocks: 2316* - a[data-toggle="collapse"][href="#id"] -> toggles .panel-collapse 2317* - #search_faq -> filters panels, highlights matches 2318* - ?search=term in the URL -> pre-fills and runs the filter 2319* 2320* Deliberately does NOT define window.jQuery / window.$ â a global jQuery stub 2321* makes third-party widgets take the jQuery code path and break. 2322* 2323* Public API: window.FaqShim.refresh() â re-scan after injecting markup (ESI, 2324* Alpine, AJAX). Not needed for clicks; those are delegated on document. 2325*/ 2326(function (window, document) { 2327'use strict'; 2328var TRIGGER_SEL = '[data-toggle="collapse"]'; 2329var PANEL_SEL = '.panel'; 2330var TITLE_SEL = '.panel-title a'; 2331var SEARCH_SEL = '#search_faq, [data-faq-search]'; 2332var SCOPE_SEL = '.faq_block, .faq_table, .panel-group'; 2333var DURATION = 250; 2334var originalTitles = new WeakMap(); 2335var ready = false; 2336/* ---------------------------------------------------------- styles ---- */ 2337function injectStyles() { 2338if (document.getElementById('faq-shim-css')) return; 2339var style = document.createElement('style'); 2340style.id = 'faq-shim-css'; 2341style.textContent = [ 2342/* structural fallback so panels are closed even if Bootstrap CSS is gone */ 2343'.faq_block .panel-collapse:not(.in),', 2344'.faq_table .panel-collapse:not(.in),', 2345'[data-faq-collapse]:not(.in){display:none}', 2346'[data-faq-collapse].in{display:block}', 2347'[data-faq-collapse][data-faq-collapse]{overflow:hidden;visibility:visible}', 2348'[data-faq-collapse].faq-animating{transition:height ' + DURATION + 'ms ease}', 2349'@media (prefers-reduced-motion:reduce){', 2350'[data-faq-collapse].faq-animating{transition:none}}' 2351].join(''); 2352(document.head || document.documentElement).appendChild(style); 2353} 2354/* ------------------------------------------------------- targeting ---- */ 2355function targetOf(trigger) { 2356var sel = trigger.getAttribute('data-target') || trigger.getAttribute('href') || ''; 2357if (!sel || sel === '#') return null; 2358// Duplicate ids across repeated CMS blocks are likely, so look inside the 2359// trigger's own FAQ scope first and only then fall back to the document. 2360var scope = trigger.closest(SCOPE_SEL); 2361var el = null; 2362try { 2363if (scope) el = scope.querySelector(sel); 2364if (!el) el = document.querySelector(sel); 2365} catch (e) { 2366return null; 2367} 2368return el; 2369} 2370function claim(el) { 2371if (!el || el.hasAttribute('data-faq-collapse')) return el; 2372el.setAttribute('data-faq-collapse', ''); 2373// `collapse` is a Tailwind utility (visibility: collapse) as well as a 2374// legacy Bootstrap hook. Nothing needs it now â .in and the data attribute 2375// carry the state â so drop it rather than fight it on specificity. 2376el.classList.remove('collapse'); 2377if (!el.id) el.id = 'faq-c-' + Math.random().toString(36).slice(2, 9); 2378return el; 2379} 2380function syncAria(target) { 2381var open = target.classList.contains('in'); 2382var triggers = document.querySelectorAll( 2383TRIGGER_SEL + '[href="#' + target.id + '"], ' + 2384TRIGGER_SEL + '[data-target="#' + target.id + '"]' 2385); 2386for (var i = 0; i < triggers.length; i++) { 2387triggers[i].setAttribute('aria-expanded', open ? 'true' : 'false'); 2388triggers[i].setAttribute('aria-controls', target.id); 2389if (triggers[i].tagName === 'A') triggers[i].setAttribute('role', 'button'); 2390} 2391} 2392/* ------------------------------------------------------ open/close ---- */ 2393function settle(el) { 2394if (el._faqTimer) { clearTimeout(el._faqTimer); el._faqTimer = null; } 2395if (el._faqEnd) { el.removeEventListener('transitionend', el._faqEnd); el._faqEnd = null; } 2396el.classList.remove('faq-animating'); 2397el.style.height = ''; 2398} 2399function finish(el, cb) { 2400var done = function (e) { 2401if (e && e.target !== el) return; 2402settle(el); 2403if (cb) cb(); 2404}; 2405el._faqEnd = done; 2406el.addEventListener('transitionend', done); 2407el._faqTimer = setTimeout(done, DURATION + 60); 2408} 2409function open(el, animate) { 2410claim(el); 2411settle(el); 2412el.classList.add('in'); 2413if (animate === false) { syncAria(el); return; } 2414el.style.height = '0px'; 2415el.classList.add('faq-animating'); 2416void el.offsetHeight; // force reflow 2417el.style.height = el.scrollHeight + 'px'; 2418finish(el); 2419syncAria(el); 2420} 2421function close(el, animate) { 2422claim(el); 2423settle(el); 2424if (animate === false) { 2425el.classList.remove('in'); 2426syncAria(el); 2427return; 2428} 2429el.style.height = el.scrollHeight + 'px'; 2430el.classList.add('faq-animating'); 2431void el.offsetHeight; 2432el.style.height = '0px'; 2433finish(el, function () { el.classList.remove('in'); syncAria(el); }); 2434} 2435function toggle(el) { 2436if (el.classList.contains('in')) close(el); else open(el); 2437} 2438/* --------------------------------------------------------- marking ---- */ 2439function unmark(link) { 2440var original = originalTitles.get(link); 2441if (original !== undefined && link.innerHTML !== original) link.innerHTML = original; 2442} 2443function mark(link, term) { 2444if (!originalTitles.has(link)) originalTitles.set(link, link.innerHTML); 2445unmark(link); 2446if (!term) return false;
2447var needle = term.toLowerCase(); 2448var walker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT, null, false); 2449var nodes = []; 2450var node; 2451while ((node = walker.nextNode())) nodes.push(node); 2452var found = false; 2453for (var i = 0; i < nodes.length; i++) { 2454var text = nodes[i].nodeValue; 2455var hay = text.toLowerCase(); 2456var at = hay.indexOf(needle); 2457if (at === -1) continue; 2458var frag = document.createDocumentFragment(); 2459var pos = 0; 2460while (at !== -1) { 2461if (at > pos) frag.appendChild(document.createTextNode(text.slice(pos, at))); 2462var m = document.createElement('mark'); 2463m.textContent = text.slice(at, at + needle.length); 2464frag.appendChild(m); 2465pos = at + needle.length; 2466at = hay.indexOf(needle, pos); 2467found = true; 2468} 2469if (pos < text.length) frag.appendChild(document.createTextNode(text.slice(pos))); 2470nodes[i].parentNode.replaceChild(frag, nodes[i]); 2471} 2472return found; 2473} 2474/* ---------------------------------------------------------- search ---- */ 2475function panels() { 2476var scoped = document.querySelectorAll( 2477'.faq_block ' + PANEL_SEL + ', .faq_table ' + PANEL_SEL + ', .panel-group ' + PANEL_SEL 2478); 2479return scoped.length ? scoped : document.querySelectorAll(PANEL_SEL); 2480} 2481function search(term) { 2482term = (term || '').trim(); 2483var list = panels(); 2484for (var i = 0; i < list.length; i++) { 2485var panel = list[i]; 2486var link = panel.querySelector(TITLE_SEL); 2487var body = panel.querySelector('.panel-collapse'); 2488panel.style.display = ''; 2489if (link) mark(link, ''); 2490if (body) close(claim(body), false); 2491if (!term) continue; 2492var hit = link ? mark(link, term) : false; 2493if (hit) { 2494if (body) open(body, false); 2495} else { 2496panel.style.display = 'none'; 2497} 2498} 2499} 2500function paramValue(name) { 2501var match = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.search); 2502if (!match) return ''; 2503try { 2504return decodeURIComponent(match[1].replace(/\+/g, ' ')); 2505} catch (e) { 2506return match[1]; 2507} 2508} 2509/* ------------------------------------------------------------ init ---- */ 2510function refresh() { 2511injectStyles(); 2512var triggers = document.querySelectorAll(TRIGGER_SEL); 2513for (var i = 0; i < triggers.length; i++) { 2514var target = targetOf(triggers[i]); 2515if (!target) continue; 2516claim(target); 2517syncAria(target); 2518} 2519// deep link: /page?...#collapse84 2520if (window.location.hash.length > 1) { 2521var hashed = null; 2522try { hashed = document.querySelector(window.location.hash); } catch (e) { hashed = null; } 2523if (hashed && hashed.hasAttribute('data-faq-collapse')) open(hashed, false); 2524} 2525} 2526function init() { 2527if (ready) return; 2528ready = true; 2529refresh(); 2530var seeded = paramValue('search'); 2531var input = document.querySelector(SEARCH_SEL); 2532if (input && seeded) { 2533input.value = seeded; 2534search(seeded); 2535} 2536} 2537// Delegated â survives ESI fragments and anything injected after load. 2538document.addEventListener('click', function (e) { 2539var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null; 2540if (!trigger) return; 2541var target = targetOf(trigger); 2542if (!target) return; 2543e.preventDefault(); 2544toggle(claim(target)); 2545}); 2546document.addEventListener('keydown', function (e) { 2547if (e.key !== 'Enter' && e.key !== ' ') return; 2548var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null; 2549if (!trigger || trigger.tagName === 'A') return; // anchors fire click natively 2550var target = targetOf(trigger); 2551if (!target) return; 2552e.preventDefault(); 2553toggle(claim(target)); 2554}); 2555document.addEventListener('input', function (e) { 2556if (!e.target.matches || !e.target.matches(SEARCH_SEL)) return; 2557search(e.target.value); 2558}); 2559if (document.readyState === 'loading') { 2560document.addEventListener('DOMContentLoaded', init); 2561} else { 2562init(); 2563} 2564window.addEventListener('load', refresh); 2565window.FaqShim = { refresh: refresh, open: open, close: close, search: search }; 2566})(window, document); </script>
2566</div> <!-promo_free_logo_gift_badge--><div class="widget widget-static-block"><style> /* SimplyStamps | Free Logo Stamp Gift promo badge. 2567Renders in 4 places: homepage Logo Stamps tile, the /rubber-logo-stamps PLP + its 10 sibling logo-stamp sub-category PLPs 2568(banner right of the intro heading, wraps under on mobile), and the title of every PDP carrying the promo option (select_7643). Nowhere else. 2569To end the promo: disable this static block. */ 2570.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} 2571.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child{position:relative;width:fit-content;margin-left:auto;margin-right:auto} 2572.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} 2573@media (max-width:639px){.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child::after{top:-8%;right:-8%;width:42%;height:42%}} 2574.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))} 2575.ss-fg-badge:focus-visible{outline:2px solid #55B5E3;outline-offset:3px;border-radius:50%} 2576.ss-fg-h2row{display:flex;flex-wrap:wrap;align-items:center;gap:10px 20px;margin:0 0 12px} 2577.ss-fg-h2row>h2,.ss-fg-h2row>h1{margin:0!important;flex:0 1 auto} 2578.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} 2579.ss-fg-banner strong{font-weight:700;text-transform:uppercase;letter-spacing:.04em} 2580.ss-fg-banner:focus-visible{outline:2px solid #55B5E3;outline-offset:2px} 2581.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} 2582.ss-fg-banner .ss-fg-tip{right:0} 2583.ss-fg-banner.is-wrapped{margin-left:0;width:100%;border-radius:12px} 2584.ss-fg-title-row{position:relative} 2585.ss-fg-title-row>h1{padding-right:68px} 2586.ss-fg-pdp{position:absolute;top:-10px;right:0;width:58px;height:58px} 2587.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} 2588.ss-fg-pdp .ss-fg-tip{right:0}
2589@media (hover:hover){.ss-fg-badge:hover .ss-fg-tip,.ss-fg-banner:hover .ss-fg-tip{opacity:1;visibility:visible}} 2590.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} 2591@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}} 2592@media (prefers-reduced-motion:reduce){.ss-fg-tip{transition:none}} </style>
2592<script> (function(){ 2593var 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."; 2594function make(extra,banner){ 2595var b=document.createElement('span'); 2596b.tabIndex=0; 2597if(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>';} 2598else{b.className='ss-fg-badge '+extra;b.setAttribute('role','img');b.setAttribute('aria-label','Free gift with any logo stamp. '+TIP);} 2599var t=document.createElement('span'); 2600t.className='ss-fg-tip'; 2601t.setAttribute('aria-hidden','true'); 2602t.textContent=TIP; 2603b.appendChild(t); 2604b.addEventListener('click',function(e){e.stopPropagation();b.classList.toggle('is-open');}); 2605b.addEventListener('keydown',function(e){if(e.key==='Escape'){b.classList.remove('is-open');b.blur();}}); 2606document.addEventListener('click',function(){b.classList.remove('is-open');}); 2607return b; 2608} 2609function run(){ 2610var c=document.body.classList; 2611if(c.contains('catalog-category-view')&&c.contains('category-rubber-logo-stamps')){ 2612var h2=document.querySelector('.ss-logo-top > h2'); 2613if(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); 2614var fit=function(){bn.classList.remove('is-wrapped');if(bn.offsetTop>h2.offsetTop+h2.offsetHeight/2)bn.classList.add('is-wrapped');}; 2615fit();window.addEventListener('resize',fit);if(window.ResizeObserver)new ResizeObserver(fit).observe(row);} 2616} 2617var 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']; 2618if(c.contains('catalog-category-view')&&SIB.some(function(k){return c.contains(k);})){ 2619var h1c=document.querySelector('.flex-1 > h1'); 2620if(h1c&&!document.querySelector('.ss-fg-banner')){ 2621var rowc=document.createElement('div');rowc.className='ss-fg-h2row';h1c.parentNode.insertBefore(rowc,h1c);rowc.appendChild(h1c); 2622var bnc=make('',true);rowc.appendChild(bnc); 2623var fitc=function(){bnc.classList.remove('is-wrapped');if(bnc.offsetTop>h1c.offsetTop+h1c.offsetHeight/2)bnc.classList.add('is-wrapped');}; 2624fitc();window.addEventListener('resize',fitc);if(window.ResizeObserver)new ResizeObserver(fitc).observe(rowc); 2625} 2626} 2627if(c.contains('catalog-product-view')&&document.getElementById('select_7643')){ 2628var h1=document.querySelector('.product-header h1'); 2629if(h1&&!document.querySelector('.ss-fg-pdp')){h1.parentNode.classList.add('ss-fg-title-row');h1.parentNode.appendChild(make('ss-fg-pdp'));} 2630} 2631} 2632if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',run);else run(); 2633})(); </script>
2633</div><div class="widget widget-static-block"><div id="hcb-promo-root" hidden></div>
2633<script> (function () { 2634/* ====================================================================== 2635>>>>>>>>>>>>>>>>>>>>>> EDIT ZONE START <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 2636One list of promo phases. Each runs FROM its start UNTIL its end (Eastern). 2637MESSAGE = banner text (HTML ok â <strong> etc.) CODE = coupon 2638THRESHOLD = free-ship order minimum REWARD = what they unlock 2639Dates are Eastern: -04:00 summer, -05:00 winter. Day-end = 23:59:59. 2640====================================================================== */ 2641var PROMO = { 2642PHASES: [ 2643{ 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" }, 2644{ 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" }, 2645{ 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" }, 2646{ 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" } 2647], 2648COUNTDOWN_DAYS_BEFORE: 0, // 0 = NO timer (banner shows message only) 2649LINK_URL: "", // no link â banner is not clickable 2650SHOW_CODE: false, // native free shipping = no coupon; hide the code chip 2651// Small â bubble after the banner message. Leave "" to hide it. (Was used to explain 2652// the old coupon deducting $9.99; native free shipping needs no explainer.) 2653INFO_TIP: "", 2654LOAD_GOOGLE_FONT: true, 2655// --- ZEPHYR header banner mount --- 2656HEADER_SELECTOR: "header", // the fixed Zephyr <header> MOUNT_POSITION: "bottom", // "bottom" = below the nav (default) · "top" = above the nav rows 2657HEADER_HEIGHT_VAR: "--header-height", // Zephyr offsets <main> by padding-top:var(--header-height) 2658// --- PDP progress bar mount + subtotal source --- 2659GOAL_ENABLED: true, 2660GOAL_MOUNTS: [ 2661{ selector: ".price-pipe-container", position: "after" } // product pages (under the price block) 2662], 2663SUBTOTAL_URL: "/zephyr/cart/getItems", // cart subtotal endpoint; fetched ONCE per load (heavy) 2664POLL_MS: 1500, 2665COLORS: { 2666banner: { background: "#60B5E5", text: "#0A3550", chipBg: "#FFFFFF", chipText: "#0A3550" }, 2667goal: { text: "#0A3550", track: "#E6EEF3", fill: "#60B5E5", done: "#1A7F37" } 2668} 2669}; 2670/* ====================================================================== 2671>>>>>>>>>>>>>>>>>>>>>>> EDIT ZONE END <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 2672Do not edit below this line. 2673====================================================================== */ 2674if (window.__hcbPromoLoaded) return; 2675window.__hcbPromoLoaded = true; 2676var DAY = 86400000; 2677var 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 }; }) 2678.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; }) 2679.sort(function (a, b){ return a.from - b.from; }); 2680if (!P.length){ console.warn("[HCB Promo] no valid phases â disabled."); return; } 2681var endTs = P.reduce(function (m, p){ return Math.max(m, p.ts); }, 0); 2682var dismissKey = "hcbPromoDismissed:" + endTs; 2683try { if (sessionStorage.getItem(dismissKey)) window.__hcbDismissed = true; } catch (e) {} 2684if (Date.now() >= endTs) return; 2685function esc(s){ return String(s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } 2686function phaseIndexAt(now){ for (var i=0;i<P.length;i++) if (now >= P[i].from && now <= P[i].ts) return i; return -1; } 2687function pad(n){ return (n < 10 ? '0' : '') + n; } 2688function money(n){ return "$" + (Math.round(n*100)/100).toFixed(2).replace(/\.00$/, ""); } 2689var currentCode = P[Math.max(0, phaseIndexAt(Date.now()))].CODE; 2690var bannerInstances = [], goals = [], cachedUrlVal = null, interval = null; 2691function readSubtotal(){ return cachedUrlVal; } // populated by pollUrl() 2692function pollUrl(){ 2693if (!PROMO.SUBTOTAL_URL) return; 2694fetch(PROMO.SUBTOTAL_URL, {credentials:"include", headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(d){ 2695if (!d) return; 2696var v = (typeof d.subtotal_numeric !== "undefined") ? parseFloat(d.subtotal_numeric) 2697: (typeof d.subtotal !== "undefined") ? parseFloat(String(d.subtotal).replace(/[^0-9.]/g,"")) : NaN; 2698if (!isNaN(v)){ cachedUrlVal = v; tick(); } 2699}).catch(function(){}); 2700} 2701if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); 2702else start(); 2703function start(){ 2704if (PROMO.LOAD_GOOGLE_FONT && !document.getElementById('hcb-roboto-font')) { 2705var lk = document.createElement('link'); lk.id='hcb-roboto-font'; lk.rel='stylesheet'; 2706lk.href='https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'; 2707document.head.appendChild(lk); 2708} 2709var Cb = PROMO.COLORS.banner, Cg = PROMO.COLORS.goal; 2710var css = '' 2711/* banner â position:relative because it lives INSIDE the fixed Zephyr <header> */ 2712+ '.hcb-promo{position:relative;display:flex;align-items:center;justify-content:center;' 2713+ 'gap:.6em;padding:9px 40px;font:600 14px/1.2 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;' 2714+ 'white-space:nowrap;overflow:hidden;box-sizing:border-box;width:100%}' 2715+ '.hcb-promo *{box-sizing:border-box}' 2716+ '.hcb-promo__main{display:flex;align-items:center;justify-content:center;gap:.6em;min-width:0;color:inherit;text-decoration:none}' 2717+ '.hcb-promo__msg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;font-weight:400}' 2718+ '.hcb-promo__code{flex:none;cursor:pointer;border:0;border-radius:4px;padding:3px 9px;font:inherit;font-weight:700;letter-spacing:.03em}' 2719+ '.hcb-promo__timer{flex:none;font-variant-numeric:tabular-nums;font-weight:400}' 2720+ '.hcb-promo__timer b{font-weight:400}' 2721+ '.hcb-promo__timer--min{display:none}' 2722/* info â bubble â inline, height-safe; tooltip is portaled to<body> + fixed so nothing clips/covers it */ 2723+ '.hcb-promo__info{position:relative;flex:none;display:inline-flex;align-items:center}' 2724+ '.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}' 2725+ '.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}' 2726+ '.hcb-promo__itip::after{content:"";position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:' + Cb.text + '}' 2727+ '.hcb-promo__itip.is-open{opacity:1;visibility:visible}' 2728+ '.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}' 2729+ '.hcb-promo__x:hover{opacity:1}' 2730+ '@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}}' 2731/* goal bar */ 2732+ '.hcb-goal{font:600 14px/1.35 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;margin:10px 0;max-width:420px}' 2733+ '.hcb-goal__msg{color:' + Cg.text + ';margin:0 0 6px}.hcb-goal__msg b{font-weight:800}' 2734+ '.hcb-goal__track{height:10px;background:' + Cg.track + ';border-radius:999px;overflow:hidden}' 2735+ '.hcb-goal__fill{height:100%;width:0;background:' + Cg.fill + ';border-radius:999px;transition:width .4s ease}' 2736+ '.hcb-goal.is-done .hcb-goal__msg{color:' + Cg.done + '}.hcb-goal.is-done .hcb-goal__fill{background:' + Cg.done + '}'; 2737var style=document.createElement('style'); style.textContent=css; document.head.appendChild(style); 2738/* ---------- build header banner ---------- */ 2739var seed = P[Math.max(0, phaseIndexAt(Date.now()))]; 2740var linkOpen = PROMO.LINK_URL ? 'a href="' + esc(PROMO.LINK_URL) + '"' : 'span'; 2741var linkClose = PROMO.LINK_URL ? 'a' : 'span'; 2742var INFO_TIP = PROMO.INFO_TIP || ""; 2743function bannerMarkup(ph){ 2744return '<div class="hcb-promo" role="region" aria-label="Promotion" style="background:' + Cb.background + ';color:' + Cb.text + '">' 2745+ '<' + linkOpen + ' class="hcb-promo__main">' 2746+ '<span class="hcb-promo__msg">' + ph.MESSAGE + '</span>' 2747+ '<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>' 2748+ '<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>' 2749+ '</' + linkClose + '>' 2750+ (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>' : '') 2751+ (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>' : '') 2752+ '<button type="button" class="hcb-promo__x" aria-label="Dismiss promotion">×</button>' 2753+ '</div>'; 2754} 2755var header = document.querySelector(PROMO.HEADER_SELECTOR);
2756var mountHost = header || document.querySelector('main') || document.body; 2757var root = document.createElement('div'); root.className='hcb-promo-root'; root.innerHTML = bannerMarkup(seed); 2758if (PROMO.MOUNT_POSITION === 'top') mountHost.insertBefore(root, mountHost.firstChild); // above the nav rows 2759else mountHost.appendChild(root); // below the nav (default) 2760syncHeaderHeight(); // set --header-height now (anti-jump); Zephyr's own observer also maintains it 2761var inst={ root:root, msg:root.querySelector('.hcb-promo__msg'), units:root.querySelectorAll('[data-u]'), 2762fulls:root.querySelectorAll('.hcb-promo__timer--full'), mins:root.querySelectorAll('.hcb-promo__timer--min'), 2763code:root.querySelector('.hcb-promo__code'), x:root.querySelector('.hcb-promo__x') }; 2764if (inst.code) inst.code.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); 2765var b=inst.code, done=function(){ b.textContent='Copied!'; setTimeout(function(){ b.textContent=currentCode; },1500); }; 2766if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(currentCode).then(done,fb);}else{fb();} 2767function 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);} }); 2768inst.x.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); 2769try{sessionStorage.setItem(dismissKey,'1');}catch(err){} 2770window.__hcbDismissed=true; root.style.display='none'; syncHeaderHeight(); }); 2771// info â tooltip â portal to<body>, position:fixed under the icon 2772var info=root.querySelector('.hcb-promo__info'); 2773if(info){ 2774var ib=info.querySelector('.hcb-promo__ibtn'); 2775var tip=info.querySelector('.hcb-promo__itip'); 2776document.body.appendChild(tip); 2777var place=function(){ var r=ib.getBoundingClientRect(); tip.style.left=(r.left+r.width/2)+'px'; tip.style.top=(r.bottom+9)+'px'; }; 2778var openTip=function(){ place(); tip.classList.add('is-open'); }; 2779var closeTip=function(){ tip.classList.remove('is-open'); }; 2780ib.addEventListener('mouseenter', openTip); 2781ib.addEventListener('mouseleave', closeTip); 2782ib.addEventListener('focus', openTip); 2783ib.addEventListener('blur', closeTip); 2784ib.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); if(tip.classList.contains('is-open')) closeTip(); else openTip(); }); 2785document.addEventListener('click', function(e){ if(e.target!==ib) closeTip(); }); 2786window.addEventListener('scroll', function(){ if(tip.classList.contains('is-open')) place(); }, {passive:true}); 2787window.addEventListener('resize', function(){ if(tip.classList.contains('is-open')) place(); }); 2788} 2789bannerInstances.push(inst); 2790if (window.__hcbDismissed) root.style.display='none'; 2791/* ---------- keep --header-height in sync (includes this banner) ---------- */ 2792function syncHeaderHeight(){ 2793if (!header) return; 2794var h = Math.round(header.getBoundingClientRect().height); 2795if (h) document.documentElement.style.setProperty(PROMO.HEADER_HEIGHT_VAR, h + 'px'); 2796} 2797if (header){ 2798if (window.ResizeObserver){ try { new ResizeObserver(syncHeaderHeight).observe(header); } catch(e){} } 2799window.addEventListener('resize', syncHeaderHeight); 2800setTimeout(syncHeaderHeight,300); setTimeout(syncHeaderHeight,1200); 2801} 2802/* ---------- build PDP goal bar (only where the price container exists) ---------- */ 2803if (PROMO.GOAL_ENABLED){ 2804(PROMO.GOAL_MOUNTS||[]).forEach(function(mnt){ 2805var ghost=document.querySelector(mnt.selector); if(!ghost) return; 2806var groot=document.createElement('div'); groot.className='hcb-goal'; 2807groot.innerHTML='<p class="hcb-goal__msg"></p><div class="hcb-goal__track"><div class="hcb-goal__fill"></div></div>'; 2808var gp=mnt.position; 2809if(gp==='before') ghost.parentNode.insertBefore(groot, ghost); 2810else if(gp==='prepend') ghost.insertBefore(groot, ghost.firstChild); 2811else if(gp==='append') ghost.appendChild(groot); 2812else ghost.parentNode.insertBefore(groot, ghost.nextSibling); 2813goals.push({ root:groot, msg:groot.querySelector('.hcb-goal__msg'), fill:groot.querySelector('.hcb-goal__fill') }); 2814}); 2815} 2816if (goals.length) pollUrl(); // fetch cart subtotal ONCE (heavy endpoint) â only where a goal bar renders 2817interval=setInterval(tick, 1000); // 1s clock only (no re-fetch) 2818tick(); 2819} 2820/* ---------- single clock drives banner + goal ---------- */ 2821var lastIdx = -2; 2822function 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; } 2823function 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'; } 2824function tick(){ 2825var now = Date.now(); 2826var idx = phaseIndexAt(now); 2827if (now >= endTs || idx === -1){ 2828if (interval) clearInterval(interval);
2829bannerInstances.forEach(function(o){ o.root.style.display='none'; }); 2830goals.forEach(function(g){ g.root.style.display='none'; }); 2831return; 2832} 2833var ph = P[idx]; 2834if (idx !== lastIdx){ 2835lastIdx = idx; currentCode = ph.CODE; 2836bannerInstances.forEach(function(o){ o.msg.innerHTML=ph.MESSAGE; if(o.code){ o.code.textContent=currentCode; o.code.setAttribute('aria-label','Copy promo code '+currentCode); } }); 2837} 2838var ms = ph.ts - now, showT = ms <= PROMO.COUNTDOWN_DAYS_BEFORE*DAY; 2839var d=Math.floor(ms/DAY), h=Math.floor((ms%DAY)/3600000), m=Math.floor((ms%3600000)/60000), s=Math.floor((ms%60000)/1000); 2840bannerInstances.forEach(function(o){ 2841if (window.__hcbDismissed) return; 2842if (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); } 2843else showTimer(o,false); 2844}); 2845if (goals.length){ 2846var sub=readSubtotal(), pct, html, done=false; 2847var useCode=PROMO.SHOW_CODE; 2848if (sub===null){ pct=0; html='Spend '+money(ph.THRESHOLD)+'+ for <b>'+ph.REWARD+'</b>'+(useCode?' â use code <b>'+ph.CODE+'</b>':''); } 2849else 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.":""); } 2850else { 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+")":""); } 2851goals.forEach(function(gEl){ gEl.msg.innerHTML=html; gEl.fill.style.width=pct+'%'; gEl.root.classList.toggle('is-done',done); gEl.root.style.display=''; }); 2852} 2853} 2854})(); </script>
2854</div><div class="widget widget-static-block"> <style> /* v1.31 -- Roboto promoted to first in the stack to match OpenMage brand. 2855All child elements inherit this via font-family: inherit or unspecified. */ 2856.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; } 2857@keyframes ss-fade-in { from { opacity: 0; } to { opacity: 1; } } 2858.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; } 2859@keyframes ss-slide-up { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } 2860.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; } 2861.ss-upsell-close:hover { color: #475569; background: #f1f5f9; } 2862.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; } 2863/* v1.31 -- title case + Roboto Bold. Letter-spacing removed (was for all-caps). */ 2864.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; } 2865/* v1.31 -- subhead sits directly under the headline; smaller, softer color */ 2866.ss-upsell-subhead { font-size: 14px; line-height: 1.5; margin: 0 0 16px; color: #475569; font-weight: 400; text-align: center; } 2867/* v1.31 -- 300px image slot, padding removed so GIF fills edge-to-edge. 2868overflow:hidden clips anything spilling outside the rounded corners. */ 2869.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; } 2870.ss-upsell-image img { width: 100%; height: 100%; object-fit: cover; display: block; } 2871.ss-upsell-bullets { list-style: none; margin: 0 0 20px; padding: 0; } 2872.ss-upsell-bullets li { position: relative; padding-left: 24px; margin-bottom: 10px; color: #334155; line-height: 1.5; font-size: 14px; } 2873.ss-upsell-bullets li:before { content: "\2713"; position: absolute; left: 0; color: #16a34a; font-weight: 700; } 2874.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; } 2875.ss-upsell-price strong { color: #16a34a; font-size: 17px; } 2876.ss-upsell-actions { display: flex; flex-direction: column; gap: 4px; align-items: center; } 2877.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; } 2878.ss-upsell-accept:hover { background: #15803d; } 2879.ss-upsell-accept:focus { outline: 2px solid #15803d; outline-offset: 2px; } 2880/* v1.31 -- "No thanks" is now a small centered text link, not a ghost button. 2881Reduces visual competition with the primary Upgrade CTA. */ 2882.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; } 2883.ss-upsell-decline:hover { color: #334155; text-decoration: underline; } 2884.ss-upsell-decline:focus { outline: 2px solid #64748b; outline-offset: 2px; } 2885/* v1.31 -- config-step Cancel now inherits the small-link style from 2886.ss-upsell-decline to match Step 1's philosophy. Ghost-button CSS 2887override deliberately removed. .ss-config-cancel class kept purely 2888as a JS querySelector hook. */ 2889@media (max-width: 480px) { .ss-upsell-modal { padding: 24px 20px 20px; border-radius: 10px; } .ss-upsell-modal h2 { font-size: 18px; } } 2890/* Success state */ 2891.ss-upsell-success { text-align: center; padding-top: 40px; } 2892.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); } 2893@keyframes ss-pop { 0% { transform: scale(0.3); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } 2894.ss-upsell-success h2 { color: #15803d; margin-bottom: 8px; } 2895.ss-upsell-success-tagline { color: #334155; font-size: 15px; line-height: 1.5; margin: 0 0 18px; } 2896.ss-upsell-success-cta { color: #94a3b8; font-size: 13px; margin: 0; display: flex; align-items: center; justify-content: center; gap: 8px; } 2897.ss-upsell-spinner { width: 14px; height: 14px; border: 2px solid #cbd5e1; border-top-color: #16a34a; border-radius: 50%;
2897 animation: ss-spin 0.7s linear infinite; } 2898@keyframes ss-spin { to { transform: rotate(360deg); } } 2899.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; } 2900/* Config step (step 2) */ 2901.ss-config-intro { color: #475569; font-size: 14px; line-height: 1.5; margin: 0 0 18px; } 2902.ss-config-fields { max-height: 50vh; overflow-y: auto; margin: 0 0 20px; padding-right: 4px; } 2903.ss-opt-field { margin-bottom: 14px; } 2904.ss-opt-field label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 6px; } 2905.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; } 2906.ss-opt-field select:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; } 2907.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; } 2908.ss-opt-field input[type="text"]:focus, .ss-opt-field textarea:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; } 2909.ss-opt-field textarea { resize: vertical; min-height: 60px; } 2910.ss-opt-group-label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 8px; } 2911.ss-opt-radio { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 14px; color: #334155; cursor: pointer; font-weight: 400; } 2912.ss-opt-radio input { margin: 0; } 2913.ss-config-required { color: #dc2626; margin-left: 3px; } 2914.ss-opt-field-error select, 2915.ss-opt-field-error input[type="text"], 2916.ss-opt-field-error textarea { border-color: #dc2626; box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12); } 2917.ss-opt-field-error > label, 2918.ss-opt-field-error > .ss-opt-group-label { color: #dc2626; } 2919.ss-opt-error { color: #dc2626; font-size: 12px; margin-top: 6px; font-weight: 500; } 2920/* Variant B / C -- value messaging enhancements */ 2921.ss-upsell-badge.ss-upsell-badge-popular { background: #dcfce7; color: #15803d; } 2922.ss-upsell-badge.ss-upsell-badge-discount { background: #fef3c7; color: #92400e; } 2923.ss-upsell-cost-per-use { font-size: 12px; color: #64748b; margin-top: 4px; } 2924.ss-upsell-trust { font-size: 12px; color: #64748b; margin: 0 0 16px; text-align: center; line-height: 1.4; } 2925.ss-upsell-trust .ss-stars { color: #f59e0b; letter-spacing: 1px; } 2926.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; } 2927.ss-upsell-price-strike { text-decoration: line-through; color: #94a3b8; font-weight: 400; margin-right: 6px; } </style>
2927<script> (function () { 2928'use strict'; 2929var TIER_MAP = [ 2930{ tier: 1, name: 'Wood Handle', match: ['wood handle', 'wood'] }, 2931{ tier: 2, name: 'Self-Inking', match: ['self-inking', 'self inking', 'selfinking'] }, 2932{ tier: 3, name: 'Pre-Inked', match: ['pre-inked', 'pre inked', 'preinked'], subtype: 'pre_inked' }, 2933{ tier: 3, name: 'Professional', match: ['professional'], subtype: 'professional' } 2934]; 2935var STAMP_TYPE_LABEL_RE = /stamp\s*(type|style)/i; 2936var SESSION_ACCEPTED_KEY = 'ss_upsell_accepted_session'; 2937var SESSION_DECLINE_KEY = 'ss_upsell_decline_count'; 2938var SUCCESS_DWELL_MS = 1500; // how long the success state stays before submit 2939// After this many declines in a session, the modal stops firing -- respects 2940// a clear "no thanks" signal so we don't pester. Cancels at the config 2941// step count as declines too. Set to 0 for unlimited (no decline cap). 2942var MAX_DECLINES_PER_SESSION = 3; 2943// ============================================================ 2944// A/B VARIANT FRAMEWORK -- discount test 2945// ------------------------------------------------------------ 2946// Visitors are randomly assigned to A or B (50/50) on first PDP 2947// visit and persisted in localStorage. Same visitor always sees 2948// the same variant. URL override: ?ss_variant=B for QA. 2949// 2950// A = control: current production modal, no discount. 2951// B = treatment: same modal + 10% off coupon (UPSELL10) auto-applied. 2952// 2953// Variant content lives in VARIANT_CONFIG -- edit copy/discount here 2954// without touching the rendering logic below. 2955// ============================================================ 2956var VARIANT_STORAGE_KEY = 'ss_upsell_variant'; 2957var VARIANT_OVERRIDE_PARAM = 'ss_variant'; 2958var VARIANTS = ['A', 'B']; 2959var VARIANT_CONFIG = { 2960// A = CONTROL -- current production modal exactly as-is, no overrides. 2961A: { 2962// Empty = use defaults. Same as live production behavior. 2963}, 2964// B = DISCOUNT -- current modal + 10% discount on accept. The ONLY 2965// variable changing vs A is the discount; everything else (badge, 2966// copy, image, trust elements) stays identical so the lift can be 2967// attributed cleanly to the discount. 2968// 2969// Coupon codes are per-transition (keyed by upgrade target tier) so 2970// each transition can have its own Magento Cart Price Rule with its 2971// own conditions/scope. If a code leaks, only that specific upgrade 2972// path is exposed. 2973B: { 2974discountBanner: 'Save 10% on this upgrade -- today only', 2975couponCodes: { 2976'self-inking': 'UPSELL-SI10', // Wood Handle -> Self-Inking 2977'pre_inked': 'UPSELL-PI10', // Self-Inking -> Pre-Inked 2978'professional': 'UPSELL-PRO10' // Self-Inking -> Professional 2979}, 2980discountPct: 10 2981} 2982}; 2983// If false, the config step skips text inputs and textareas (stamp text 2984// lines, etc). Their values still carry over to the cart -- they just 2985// aren't re-shown for editing in the modal. 2986var INCLUDE_TEXT_FIELDS_IN_CONFIG = false; 2987// DEV TOGGLE -- when true, the modal fires on EVERY eligible ATC, ignoring 2988// the no-badger session lock. Flip to false before production launch so 2989// the lock kicks in after a user accepts an upgrade. 2990var DEV_MODE_ALWAYS_SHOW = false;
2991// ============================================================ 2992// CATEGORY TARGETING -- controlled rollout 2993// ------------------------------------------------------------ 2994// Use this to limit which product categories show the upsell modal. 2995// Substring match (case-insensitive) against the breadcrumb path. 2996// 2997// TARGET_CATEGORIES -- if non-empty, modal ONLY fires when the 2998// product's category breadcrumb contains one of these strings. 2999// Empty array = fire on all categories (default). 3000// 3001// EXCLUDED_CATEGORIES -- modal NEVER fires when the breadcrumb 3002// contains one of these strings, even if it also matches TARGET. 3003// Exclusions always win over targets. 3004// 3005// Examples: 3006// Target Address Stamps only: 3007// TARGET_CATEGORIES = ['Address Stamps']; 3008// Target everything EXCEPT custom/specialty stamps: 3009// EXCLUDED_CATEGORIES = ['Custom', 'Specialty']; 3010// Target Logo + Notary, exclude any "Bulk" sub-category: 3011// TARGET_CATEGORIES = ['Logo Stamps', 'Notary Stamps']; 3012// EXCLUDED_CATEGORIES = ['Bulk']; 3013// 3014// v1.32 ACTION NEEDED: these names came from the pre-Zephyr category 3015// landing page. Zephyr breadcrumbs read differently (a Duncan address 3016// stamp now shows "Address Stamps", not "Business Address Stamps"), so 3017// this whitelist must be re-mapped to the LIVE Zephyr breadcrumb strings 3018// or the modal won't fire on the categories you intend. Verify each on 3019// the live site and update below. 3020// ============================================================ 3021var TARGET_CATEGORIES = [ 3022'Office Stamps', 3023'Business Address Stamps', 3024'Signature Stamps', 3025'Date Stamps', 3026'Banking Stamps', 3027'Accounting & Tax', 3028'Postage Paid', 3029'Medical', 3030'Inspection', 3031'QR Code', 3032'Secure', 3033'Eco-Friendly', 3034'Checkbox', 3035'Custom Text' 3036]; 3037var EXCLUDED_CATEGORIES = [ 3038'Architects', 3039'Engineer', 3040'Land Surveyor', 3041'licensed Professional Stamps', 3042'Interior Design', 3043'LLC Stamps', 3044'Real Estate Appraisers' 3045]; // e.g. ['Custom', 'Specialty'] 3046// ============================================================ 3047// TIER IMAGES -- generic product image shown in the upsell modal 3048// ------------------------------------------------------------ 3049// Drop in a URL per upgrade tier. Image appears above the value 3050// checkmarks in step 1 of the modal. Leave a key empty to skip 3051// the image section for that tier (graceful fallback). 3052// 3053// Recommended: ~600x300px, transparent or white background, the 3054// bare stamp photographed straight-on. Hosted anywhere reachable 3055// by the site (Magento media folder, CDN, etc). 3056// ============================================================ 3057// v1.31 -- GIF assets, TEMPORARY LOCAL PATHS. 3058// Currently uploaded flat to wysiwyg/SimplyStamps/ with the "upsell-" prefix. 3059// TODO before prod push: move files to wysiwyg/SimplyStamps/upsell/ and rename 3060// to {tier}.gif (dropping the "upsell-" prefix), then update these paths to: 3061// 'self-inking': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/self-inking.gif', 3062// 'pre_inked': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/pre-inked.gif', 3063// 'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/professional.gif' 3064var TIER_IMAGES = { 3065'self-inking': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-self-inking.gif', 3066'pre_inked': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-pre-inked-v2.gif', 3067'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-professional-v2.gif' 3068}; 3069// ============================================================ 3070// CONFIG STEP -- tier-relevant option filtering 3071// ------------------------------------------------------------ 3072// PRIMARY MECHANISM: auto-detect tier-tagged labels. 3073// For each option label on the form, scan it for any tier name 3074// ('Pre-Inked', 'Self-Inking', 'Wood Handle', 'Professional'). 3075// - Mentions UPGRADE TARGET tier name -> SHOW 3076// - Mentions a DIFFERENT tier name -> HIDE 3077// - Mentions no tier name -> SHOW (generic option) 3078// 3079// This handles SimplyStamps's convention of naming options like 3080// "Pre-Inked Sizes", "Self-Inking Sizes", "Ink Color - Pre-Inked" 3081// with no manual config required. 3082// 3083// ESCAPE HATCH: TIER_OPTION_FILTERS below lets you override per tier 3084// for edge cases (labels that don't follow the convention). 3085// show: [] -- if non-empty, ONLY GENERIC options matching these 3086// keywords are kept. Tier-tagged auto-detection still runs. 3087// hide: [] -- additional labels to drop (substring match). 3088// 3089// Inspect what survives for a given product: 3090// window.SS_UPSELL_DEBUG = true; window.SSUpsell.inspect(); 3091// ============================================================ 3092var TIER_OPTION_FILTERS = { 3093// All upgrade tiers (Self-Inking, Pre-Inked, Professional) have 3094// built-in pads -- separate ink pad option is Wood-Handle-only. 3095'self_inking': { hide: ['ink pad', 'pad color'] }, 3096// Pre-Inked also has pre-loaded ink, so the extra-bottle option doesn't apply. 3097'pre_inked': { hide: ['ink pad', 'pad color', 'extra bottle', 'bottle of ink'] }, 3098'professional': { hide: ['ink pad', 'pad color'] } 3099}; 3100// v1.31 -- Routing flipped: default to Professional, override to Pre-Inked 3101// only if the product name/breadcrumb signals a logo/brand focus. 3102// Rationale: nearly all TARGET_CATEGORIES are professional-use stamps 3103// (office, notary, banking, medical, etc), so Professional is the norm. 3104// Pre-Inked's sweet spot is high-fidelity logo/brand reproduction -- 3105// that's the exception, so we key off those signals specifically. 3106var PRE_INKED_KEYWORDS = [ 3107'logo', 'brand', 'branding', 'monogram', 'crest', 'emblem' 3108]; 3109// v1.31 -- copy refresh per transition. New pattern: 3110// headline: short, all-caps, question-style attention grabber
3111// subhead: one-sentence value prop that reinforces the tier 3112// bullets: detail points (kept from v1.30, still useful proof) 3113// If headline is empty string, the<h2> is skipped and subhead renders alone. 3114var COPY = { 3115'1->self-inking': { 3116// Wood Handle -> Self-Inking (teacher buyup). Heading pairs with the 3117// subhead's "ink is already inside" promise -- calls out the ink pad 3118// as the pain point being solved. 3119headline: 'Skip The Ink Pad?', 3120subhead: 'The ink is already inside. Just press down and you get a clean, perfect mark.', 3121bullets: [ 3122'No separate ink pad needed -- built right in', 3123'10,000+ impressions before re-inking' 3124] 3125}, 3126'2->pre_inked': { 3127headline: 'Serious About Your Brand?', 3128subhead: 'Pre-inked stamps print sharper detail and cleaner lines. Your logo, exactly right every time.', 3129bullets: [ 3130'Sharper, crisper detail -- ideal for logos & branding', 3131'Up to 50,000 impressions per fill' 3132] 3133}, 3134'2->professional': { 3135headline: 'Are You A Professional?', 3136subhead: 'For desks that stamp all day. Heavier build, thousands of crisp impressions.', 3137bullets: [ 3138'Heavy-duty frame engineered for high-volume stamping', 3139'Durable mechanism backed by a long-term warranty' 3140] 3141} 3142}; 3143// Success state copy -- keyed the same way as COPY 3144var SUCCESS_COPY = { 3145'1->self-inking': { 3146headline: 'Upgraded to Self-Inking', 3147tagline: 'You\'ll get 10,000+ cleaner impressions without juggling a separate ink pad.' 3148}, 3149'2->pre_inked': { 3150headline: 'Upgraded to Pre-Inked', 3151tagline: 'Premium, sharper impressions -- up to 50,000 per fill. A smart pick for branding.' 3152}, 3153'2->professional': { 3154headline: 'Upgraded to Professional', 3155tagline: 'Built for daily volume -- durable mechanism backed by a long-term warranty.' 3156} 3157}; 3158// Session helpers -- once a user accepts an upgrade, don't badger them again. 3159// DEV_MODE_ALWAYS_SHOW bypasses the lock so the modal fires every ATC. 3160function hasUpgradedThisSession() { 3161if (DEV_MODE_ALWAYS_SHOW) return false; 3162try { return sessionStorage.getItem(SESSION_ACCEPTED_KEY) === '1'; } catch (e) { return false; } 3163} 3164function markUpgradedThisSession() { 3165try { sessionStorage.setItem(SESSION_ACCEPTED_KEY, '1'); } catch (e) {} 3166} 3167// Decline-cap helpers -- count declines (modal dismiss OR cancel at config) 3168// so the modal stops firing once the user has clearly opted out. 3169function getDeclineCount() { 3170try { return parseInt(sessionStorage.getItem(SESSION_DECLINE_KEY) || '0', 10) || 0; } 3171catch (e) { return 0; } 3172} 3173function incrementDeclineCount() {
3174try { sessionStorage.setItem(SESSION_DECLINE_KEY, String(getDeclineCount() + 1)); } catch (e) {} 3175} 3176function hasDeclinedTooMany() { 3177if (DEV_MODE_ALWAYS_SHOW) return false; 3178if (!MAX_DECLINES_PER_SESSION) return false; // 0 = unlimited 3179return getDeclineCount() >= MAX_DECLINES_PER_SESSION; 3180} 3181// ---------------------------------------------------------- 3182// Variant helpers (A/B/C framework) 3183// ---------------------------------------------------------- 3184function getAssignedVariant() { 3185// URL override wins (for QA) 3186try { 3187var url = new URL(window.location.href); 3188var override = (url.searchParams.get(VARIANT_OVERRIDE_PARAM) || '').toUpperCase(); 3189if (VARIANTS.indexOf(override) !== -1) return override; 3190} catch (e) {} 3191// Persisted in localStorage 3192try { 3193var stored = localStorage.getItem(VARIANT_STORAGE_KEY); 3194if (stored && VARIANTS.indexOf(stored) !== -1) return stored; 3195} catch (e) {} 3196// First visit -- random assignment 3197var idx = Math.floor(Math.random() * VARIANTS.length); 3198var variant = VARIANTS[idx]; 3199try { localStorage.setItem(VARIANT_STORAGE_KEY, variant); } catch (e) {} 3200log('Assigned new variant:', variant); 3201return variant; 3202} 3203function getVariantConfig(variant) { 3204return VARIANT_CONFIG[variant] || VARIANT_CONFIG.A || {}; 3205} 3206// Pick the right coupon code for this upgrade transition. Variant B uses 3207// a couponCodes map keyed by upgrade target ("self-inking", "pre_inked", 3208// "professional"). Legacy single couponCode also supported as fallback. 3209function resolveCouponCode(variantCfg, targetTier) { 3210if (!variantCfg) return null; 3211if (variantCfg.couponCodes && targetTier) { 3212var key = targetTier.subtype ? targetTier.subtype 3213: (targetTier.tier === 2 ? 'self-inking' : null); 3214if (key && variantCfg.couponCodes[key]) return variantCfg.couponCodes[key]; 3215} 3216return variantCfg.couponCode || null; 3217} 3218// Small local merge helper -- inline to avoid scope issues with the 3219// init()-scoped mergeParams. Used by both applyVariantCDiscount 3220// (stash phase) and applyDeferredCoupon (apply phase).
3221function _mergeParams(a, b) { 3222var out = {}; 3223for (var k in a) if (Object.prototype.hasOwnProperty.call(a, k)) out[k] = a[k]; 3224for (var k2 in b) if (Object.prototype.hasOwnProperty.call(b, k2)) out[k2] = b[k2]; 3225return out; 3226} 3227// v1.30 -- STASH the coupon code (do NOT POST from PDP). 3228// Previously we POSTed to /checkout/cart/couponPost/ from the PDP right 3229// before form.submit. That raced with the concurrent add-to-cart POST 3230// and hit an empty cart in Magento's session, so the coupon never stuck. 3231// Now we save the code + tracking context to sessionStorage; the actual 3232// apply happens on the cart page once the item is in cart (see 3233// applyDeferredCoupon below). 3234// 3235// v1.32 -- On Zephyr the AJAX add opens the cart DRAWER instead of 3236// navigating to /checkout/cart, so the stashed coupon applies the next 3237// time the shopper opens the full cart / checkout (still before purchase). 3238var PENDING_COUPON_KEY = 'ss_upsell_pending_coupon'; 3239function applyVariantCDiscount(couponCode, trackingParams) { 3240if (!couponCode) return; 3241try { 3242var payload = { 3243code: couponCode, 3244tracking: trackingParams || {}, 3245ts: Date.now() 3246}; 3247sessionStorage.setItem(PENDING_COUPON_KEY, JSON.stringify(payload)); 3248log('Coupon deferred for cart-page application:', couponCode); 3249} catch (e) { 3250log('Failed to defer coupon:', e); 3251} 3252} 3253// v1.30 -- Apply any pending upsell coupon on the cart page. 3254// Runs at module scope so it fires on every page load, cart or not. 3255// Bails immediately if not on the cart page or no pending coupon. 3256// Clears sessionStorage BEFORE posting to prevent reload loops if 3257// the POST fails or reload happens for an unrelated reason. 3258function applyDeferredCoupon() { 3259// Only run on the cart page 3260if (!/\/checkout\/cart(\/|\?|$)/.test(window.location.pathname + window.location.search)) return; 3261var raw; 3262try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; } 3263if (!raw) return; 3264var data; 3265try { data = JSON.parse(raw); } catch (e) { data = null; } 3266if (!data || !data.code) { 3267try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3268return; 3269} 3270// Clear FIRST so a failed apply doesn't cause a reload loop 3271try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3272log('Applying deferred coupon on cart page:', data.code);
3273var params = _mergeParams(data.tracking || {}, { coupon_code: data.code }); 3274try { 3275var fd = new FormData(); 3276fd.append('form_key', getFormKey()); 3277fd.append('coupon_code', data.code); 3278fd.append('remove', '0'); 3279fetch('/checkout/cart/couponPost/', { 3280method: 'POST', 3281body: fd, 3282credentials: 'same-origin', 3283headers: { 'X-Requested-With': 'XMLHttpRequest' } 3284}).then(function (r) { 3285log('Deferred coupon response:', r.status); 3286if (r.status >= 200 && r.status < 400) { 3287track('upsell_discount_applied', _mergeParams(params, { http_status: r.status })); 3288// Reload cart so the discount appears in the totals. 3289// Small delay so the event has time to fire before navigation. 3290setTimeout(function () { 3291// Guard against loop -- only reload if sessionStorage is now empty 3292var still; 3293try { still = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { still = null; } 3294if (!still) window.location.reload(); 3295}, 150); 3296} else { 3297track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response' })); 3298} 3299}).catch(function (e) { 3300log('Deferred coupon POST failed:', e); 3301track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) })); 3302}); 3303} catch (e) { 3304log('Deferred coupon apply threw:', e); 3305track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) })); 3306} 3307} 3308// v1.32b -- Apply a stashed coupon IMMEDIATELY (no page gate, no reload). 3309// On Zephyr the AJAX add opens the cart drawer and never loads /checkout/cart, 3310// so applyDeferredCoupon (cart-page only) might never run -- the shopper could 3311// reach checkout via the drawer and be charged full price on a promised 3312// discount. We call this right after the add's promise resolves, so the cart 3313// already has the item (no v1.30 race) and the coupon lands on the quote. 3314// On success we clear the stash; on failure we LEAVE it so the cart-page 3315// applyDeferredCoupon can still retry as a fallback. 3316function applyPendingCouponNow() { 3317var raw; 3318try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; } 3319if (!raw) return; 3320var data; 3321try { data = JSON.parse(raw); } catch (e) { data = null; } 3322if (!data || !data.code) { try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} return; } 3323log('Applying pending coupon immediately (post-add):', data.code);
3324var params = _mergeParams(data.tracking || {}, { coupon_code: data.code }); 3325try { 3326var fd = new FormData(); 3327fd.append('form_key', getFormKey()); 3328fd.append('coupon_code', data.code); 3329fd.append('remove', '0'); 3330fetch('/checkout/cart/couponPost/', { 3331method: 'POST', 3332body: fd, 3333credentials: 'same-origin', 3334headers: { 'X-Requested-With': 'XMLHttpRequest' } 3335}).then(function (r) { 3336log('Immediate coupon response:', r.status); 3337if (r.status >= 200 && r.status < 400) { 3338// Applied -- clear so the cart page doesn't reload to re-apply. 3339try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3340track('upsell_discount_applied', _mergeParams(params, { http_status: r.status, applied_from: 'pdp_post_add' })); 3341} else { 3342// Leave stashed -- cart-page applyDeferredCoupon will retry. 3343track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response', applied_from: 'pdp_post_add' })); 3344} 3345}).catch(function (e) { 3346log('Immediate coupon POST failed (will retry on cart page):', e); 3347track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e), applied_from: 'pdp_post_add' })); 3348}); 3349} catch (e) { 3350log('Immediate coupon apply threw:', e); 3351} 3352} 3353// Run deferred coupon apply at module scope (fires on every page load, 3354// including cart page -- bails if not cart / no pending coupon). This is the 3355// fallback for when the immediate post-add apply (applyPendingCouponNow) 3356// didn't run or didn't succeed. 3357try { applyDeferredCoupon(); } catch (e) { /* never break page load */ } 3358// Record each upgrade so the order-success page can fire upsell_purchased 3359// with order_id + revenue_delta. Stored as an array in sessionStorage; 3360// the success-page snippet reads, dispatches, and clears it. 3361function recordUpgradeForPurchaseTracking(params, qty) { 3362try { 3363var key = 'ss_upsell_pending_purchases'; 3364var existing = sessionStorage.getItem(key); 3365var arr = existing ? JSON.parse(existing) : []; 3366var rec = { qty: parseInt(qty, 10) || 1, ts: Date.now() }; 3367for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) rec[k] = params[k]; 3368arr.push(rec); 3369sessionStorage.setItem(key, JSON.stringify(arr)); 3370log('Recorded pending upgrade for purchase tracking:', rec); 3371} catch (e) {} 3372} 3373function log() { 3374if (window.SS_UPSELL_DEBUG && window.console) { 3375var args = ['[TierUpsell]'].concat([].slice.call(arguments)); 3376console.log.apply(console, args); 3377} 3378} 3379function norm(s) { return (s || '').toLowerCase().trim(); } 3380function findTierForText(text) { 3381var t = norm(text); 3382for (var i = 0; i < TIER_MAP.length; i++) { 3383var tier = TIER_MAP[i]; 3384for (var j = 0; j < tier.match.length; j++) { 3385if (t.indexOf(tier.match[j]) !== -1) return tier; 3386} 3387} 3388return null; 3389} 3390// Like findTierForText but returns ALL tiers whose name appears in the text. 3391// Used to detect which tier an option label is tagged for. 3392function findAllTiersInText(text) { 3393var t = norm(text); 3394var matches = []; 3395for (var i = 0; i < TIER_MAP.length; i++) { 3396var tier = TIER_MAP[i]; 3397for (var j = 0; j < tier.match.length; j++) { 3398if (t.indexOf(tier.match[j]) !== -1) { matches.push(tier); break; } 3399} 3400} 3401return matches; 3402} 3403function findStampTypeSelect() { 3404var labels = document.querySelectorAll('.product-options label, .product-options-wrapper label, dt label, .field label, label'); 3405for (var i = 0; i < labels.length; i++) { 3406var lbl = labels[i]; 3407if (!STAMP_TYPE_LABEL_RE.test(lbl.textContent)) continue; 3408var forId = lbl.getAttribute('for'); 3409if (forId) { 3410var byId = document.getElementById(forId); 3411if (byId && byId.tagName === 'SELECT') return byId; 3412} 3413var container = lbl.closest && (lbl.closest('.field, dd, dt, li, .input-box, .product-options li')) || lbl.parentElement; 3414if (container) { 3415var sel = container.querySelector('select'); 3416if (sel) return sel; 3417} 3418} 3419var selects = document.querySelectorAll('.product-options select, .product-options-wrapper select'); 3420for (var k = 0; k < selects.length; k++) { 3421for (var m = 0; m < selects[k].options.length; m++) { 3422if (findTierForText(selects[k].options[m].text)) return selects[k]; 3423} 3424} 3425return null; 3426} 3427function getCurrentTier(selectEl) { 3428if (!selectEl || selectEl.selectedIndex < 0) return null; 3429var opt = selectEl.options[selectEl.selectedIndex]; 3430var tier = findTierForText(opt.text); 3431return tier ? { tier: tier, optionEl: opt } : null; 3432} 3433// Scan the dropdown -- return which tiers are actually on THIS product. 3434// Keys: tier1, tier2, pre_inked, professional 3435function getAvailableTiers(selectEl) { 3436var available = {}; 3437for (var i = 0; i < selectEl.options.length; i++) { 3438var t = findTierForText(selectEl.options[i].text); 3439if (!t) continue; 3440if (t.subtype) available[t.subtype] = t; 3441else available['tier' + t.tier] = t; 3442} 3443return available; 3444} 3445// Available options trump path preference. Preferred T3 first; fall back 3446// to the other T3 before bailing. 3447function getNextTier(currentTier, upsellPath, availableTiers) { 3448if (!currentTier) return null; 3449if (currentTier.tier >= 3) return null; 3450if (currentTier.tier === 1) { 3451return availableTiers.tier2 || null; 3452} 3453if (currentTier.tier === 2) {
3454var preferred = upsellPath === 'professional' ? 'professional' : 'pre_inked'; 3455var fallback = upsellPath === 'professional' ? 'pre_inked' : 'professional'; 3456return availableTiers[preferred] || availableTiers[fallback] || null; 3457} 3458return null; 3459} 3460function findOptionInSelect(selectEl, tierDef) { 3461for (var i = 0; i < selectEl.options.length; i++) { 3462if (findTierForText(selectEl.options[i].text) === tierDef) return selectEl.options[i]; 3463} 3464return null; 3465} 3466function parsePriceModifier(text) { 3467if (!text) return 0; 3468var m = text.match(/[+\-]?\s*\$\s*([\d,]+\.\d{2})/); 3469return m ? parseFloat(m[1].replace(/,/g, '')) : 0; 3470} 3471function calcPriceDelta(currentOpt, upgradeOpt) { 3472var cur = parsePriceModifier(currentOpt.text); 3473var up = parsePriceModifier(upgradeOpt.text); 3474return Math.max(0, up - cur); 3475} 3476function getFormKey() { 3477if (window.FORM_KEY) return window.FORM_KEY; 3478var input = document.querySelector('input[name="form_key"]'); 3479return input ? input.value : ''; 3480} 3481function getOptionIdFromSelectName(selectName) { 3482var m = (selectName || '').match(/options\[(\d+)\]/); 3483return m ? m[1] : null; 3484} 3485function detectUpsellPath() { 3486var explicit = document.querySelector('[data-upsell-path]'); 3487if (explicit) { 3488var val = explicit.getAttribute('data-upsell-path'); 3489log('upsell_path from data attribute:', val); 3490return val; 3491} 3492var hay = ''; 3493hay += ' ' + (document.body.className || ''); 3494var crumbs = document.querySelectorAll('.breadcrumbs a, .breadcrumbs li, nav[aria-label*="readcrumb"] a, nav[aria-label*="readcrumb"] li'); 3495for (var i = 0; i < crumbs.length; i++) hay += ' ' + (crumbs[i].textContent || ''); 3496var h1 = document.querySelector('.product-name h1, h1.product-name, h1[itemprop="name"], h1'); 3497if (h1) hay += ' ' + (h1.textContent || ''); 3498hay = hay.toLowerCase(); 3499// v1.31 -- flipped: check for pre-inked signals (logo/brand focus). 3500// If any pre-inked keyword matches, route to pre_inked. Otherwise 3501// default to professional (aligns with our target category mix). 3502for (var k = 0; k < PRE_INKED_KEYWORDS.length; k++) { 3503if (hay.indexOf(PRE_INKED_KEYWORDS[k]) !== -1) { 3504log('upsell_path = pre_inked (matched keyword:', PRE_INKED_KEYWORDS[k], ')'); 3505return 'pre_inked'; 3506} 3507} 3508log('upsell_path = professional (default)'); 3509return 'professional'; 3510} 3511// Check if the current product's category breadcrumb passes the 3512// TARGET_CATEGORIES / EXCLUDED_CATEGORIES filters. Returns true if the 3513// upsell modal should fire for this product. 3514function isCategoryAllowed(categoryBreadcrumb) { 3515var crumb = String(categoryBreadcrumb || '').toLowerCase(); 3516// Exclusions always win 3517for (var i = 0; i < EXCLUDED_CATEGORIES.length; i++) { 3518var ex = String(EXCLUDED_CATEGORIES[i]).toLowerCase(); 3519if (ex && crumb.indexOf(ex) !== -1) { 3520log('Category excluded by rule:', EXCLUDED_CATEGORIES[i]); 3521return false; 3522} 3523} 3524// If no targets defined, allow all 3525if (TARGET_CATEGORIES.length === 0) return true; 3526// Targets defined -- must match at least one 3527for (var j = 0; j < TARGET_CATEGORIES.length; j++) { 3528var tg = String(TARGET_CATEGORIES[j]).toLowerCase(); 3529if (tg && crumb.indexOf(tg) !== -1) return true; 3530} 3531log('Category not in TARGET_CATEGORIES list -- skipping'); 3532return false; 3533} 3534function detectCategory() { 3535// v1.32 -- Zephyr renders breadcrumbs as<li><a>...</a></li>. The old 3536// query (".breadcrumbs a, .breadcrumbs li") double-counted every crumb 3537// (e.g. "Address Stamps > Address Stamps > View All > View All"). Collect 3538// ONE entry per crumb (prefer the<li> text, which contains its anchor), 3539// drop Home, and collapse consecutive duplicates. 3540var items = document.querySelectorAll('.breadcrumbs li'); 3541if (!items.length) items = document.querySelectorAll('.breadcrumbs a'); 3542var trail = []; 3543for (var i = 0; i < items.length; i++) { 3544var t = (items[i].textContent || '').replace(/\s+/g, ' ').trim(); 3545if (!t || t.toLowerCase() === 'home') continue; 3546if (trail.length && trail[trail.length - 1].toLowerCase() === t.toLowerCase()) continue; 3547trail.push(t); 3548} 3549return trail.join(' > '); 3550} 3551function track(eventName, payload) { 3552window.dataLayer = window.dataLayer || []; 3553var data = { event: eventName }; 3554for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) data[k] = payload[k]; 3555window.dataLayer.push(data); 3556log('Event:', eventName, payload); 3557} 3558// Resolve the image URL for an upgrade tier. Uses subtype for tier 3 3559// (pre_inked / professional) and 'self-inking' for tier 2. 3560function getTierImage(tierDef) { 3561if (!tierDef) return ''; 3562var key = tierDef.subtype ? tierDef.subtype 3563: (tierDef.tier === 2 ? 'self-inking' : null); 3564if (!key) return ''; 3565return TIER_IMAGES[key] || ''; 3566} 3567function buildModal(ctx) { 3568var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype; 3569var copy = COPY[key]; 3570if (!copy) return null; 3571// Resolve variant config (passed via ctx so buildModal stays pure) 3572var vCfg = ctx.variantCfg || {};
3573var overlay = document.createElement('div'); 3574overlay.className = 'ss-upsell-overlay'; 3575overlay.setAttribute('role', 'presentation'); 3576var bulletHtml = copy.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join(''); 3577var imgUrl = getTierImage(ctx.next); 3578var imgHtml = imgUrl 3579? '<div class="ss-upsell-image"><img src="' + imgUrl + '" alt="' + escHtml(ctx.next.name) + ' stamp"></div>' 3580: ''; 3581// === Variant-aware elements === 3582// v1.31 -- "Recommended upgrade" chip/badge removed from Step 1 render. 3583// The badge variables + CSS classes remain in the file (dead code) in 3584// case a future variant wants to bring back a chip. To re-enable, add 3585// badgeHtml back into the innerHTML template below. 3586var badgeText = vCfg.badgeText || 'Recommended upgrade'; 3587var badgeClass = vCfg.badgeClass || ''; 3588var badgeHtml = '<div class="ss-upsell-badge ' + badgeClass + '">' + escHtml(badgeText) + '</div>'; 3589void badgeHtml; // suppress unused warning; kept for potential future re-enable 3590// Discount banner (variant B only) 3591var discountBannerHtml = vCfg.discountBanner 3592? '<div class="ss-upsell-discount-banner">' + vCfg.discountBanner + '</div>' 3593: ''; 3594// Price line -- strikethrough + discounted price for variant C 3595var priceLineHtml; 3596if (vCfg.discountPct && ctx.priceDelta > 0) { 3597var discounted = (ctx.priceDelta * (1 - vCfg.discountPct / 100)).toFixed(2); 3598priceLineHtml = 3599'<p class="ss-upsell-price">' + 3600'Upgrade for ' + 3601'<span class="ss-upsell-price-strike">+$' + ctx.priceDelta.toFixed(2) + '</span>' + 3602'<strong>+$' + discounted + '</strong>' + 3603'</p>'; 3604} else { 3605priceLineHtml = '<p class="ss-upsell-price">Upgrade for <strong>+$' + ctx.priceDelta.toFixed(2) + '</strong></p>'; 3606} 3607// Cost-per-use line (variant B + C) 3608var costPerUseHtml = ''; 3609if (vCfg.showCostPerUse && ctx.priceDelta > 0) { 3610var impressions = ctx.next.subtype === 'pre_inked' ? 50000 : (ctx.next.tier === 2 ? 10000 : 100000); 3611var costPerUse = (ctx.priceDelta / impressions).toFixed(4); 3612costPerUseHtml = '<div class="ss-upsell-cost-per-use">~ $' + costPerUse + ' per impression over the life of this stamp</div>'; 3613} 3614// Trust signal (variant B + C) 3615var trustHtml = ''; 3616if (vCfg.trustSignal) { 3617var t = vCfg.trustSignal; 3618var parts = []; 3619if (t.ratingValue && t.ratingCount) { 3620parts.push('<span class="ss-stars">★★★★★</span> ' + escHtml(t.ratingValue) + ' from ' + escHtml(t.ratingCount) + ' reviews'); 3621} 3622if (t.returnPolicy) parts.push(escHtml(t.returnPolicy)); 3623if (parts.length) trustHtml = '<div class="ss-upsell-trust">' + parts.join(' | ') + '</div>'; 3624} 3625// v1.31 -- optional headline + subhead. If headline is empty (e.g., the 3626// Wood Handle -> Self-Inking transition until copy is finalized), 3627// skip the<h2> so the subhead reads clean on its own. 3628var headlineHtml = copy.headline 3629? '<h2 id="ss-upsell-h">' + escHtml(copy.headline) + '</h2>' 3630: ''; 3631var subheadHtml = copy.subhead 3632? '<p class="ss-upsell-subhead">' + escHtml(copy.subhead) + '</p>' 3633: ''; 3634overlay.innerHTML = 3635'<div class="ss-upsell-modal" role="dialog" aria-modal="true"' + 3636(copy.headline ? ' aria-labelledby="ss-upsell-h"' : '') + '>' + 3637'<button class="ss-upsell-close" aria-label="Close">×</button>' + 3638// v1.31 -- badge removed from render 3639headlineHtml + 3640subheadHtml + 3641imgHtml + 3642discountBannerHtml + 3643'<ul class="ss-upsell-bullets">' + bulletHtml + '</ul>' + 3644priceLineHtml + 3645costPerUseHtml + 3646trustHtml + 3647'<div class="ss-upsell-actions">' + 3648'<button class="ss-upsell-accept" type="button">Upgrade my stamp</button>' + 3649'<button class="ss-upsell-decline" type="button">No thanks</button>' + 3650'</div>' + 3651'</div>'; 3652return overlay; 3653} 3654function showModal(ctx) { 3655var overlay = buildModal(ctx); 3656if (!overlay) { log('No copy template for transition -- skipping'); return; } 3657document.body.appendChild(overlay); 3658var accept = overlay.querySelector('.ss-upsell-accept'); 3659var decline = overlay.querySelector('.ss-upsell-decline'); 3660var close = overlay.querySelector('.ss-upsell-close'); 3661// v1.32b -- once the user has Accepted, step-1 dismissal must be dead. 3662// Otherwise (Zephyr AJAX path, no navigation) an outside/backdrop click 3663// during the success-state dwell would fire onDecline -> doSubmit and add 3664// the item a SECOND time alongside the success timer's submit. 3665var accepted = false;
3666function dismiss(method) { 3667if (accepted) return; 3668if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 3669document.removeEventListener('keydown', escHandler); 3670ctx.onDecline(method); 3671} 3672function escHandler(e) { if (e.key === 'Escape') dismiss('escape'); } 3673document.addEventListener('keydown', escHandler); 3674accept.addEventListener('click', function () { 3675// Hand the overlay to onAccept -- it decides whether to chain into the 3676// config step, the success state, or simply close. 3677if (accepted) return; // v1.32b -- ignore double-click on Upgrade 3678accepted = true; 3679document.removeEventListener('keydown', escHandler); 3680ctx.onAccept(overlay); 3681}); 3682decline.addEventListener('click', function () { dismiss('decline_button'); }); 3683close.addEventListener('click', function () { dismiss('close_button'); }); 3684overlay.addEventListener('click', function (e) { if (e.target === overlay) dismiss('outside_click'); }); 3685setTimeout(function () { accept.focus(); }, 50); 3686} 3687// Pick a sane default value for a required field that has no current 3688// value selected. Prefers options labeled "No" (avoids accidentally 3689// adding paid extras like "Yes - +$5.00") over the first non-empty. 3690function autoDefaultFor(field) { 3691if (!field || !field.choices) return ''; 3692if (field.currentValue && String(field.currentValue).trim() !== '') return field.currentValue; 3693if (!field.required) return field.currentValue || ''; 3694// Pass 1: prefer an option whose text starts with "no" 3695for (var i = 0; i < field.choices.length; i++) { 3696var c = field.choices[i]; 3697if (!c.value || c.disabled) continue; 3698if (/^\s*no\b/i.test(c.text)) return c.value; 3699} 3700// Pass 2: first non-empty, non-disabled option 3701for (var j = 0; j < field.choices.length; j++) { 3702var c2 = field.choices[j]; 3703if (c2.value && !c2.disabled) return c2.value; 3704} 3705return ''; 3706} 3707// Step 2: render the config step inside the existing overlay so the 3708// user can review/adjust their other custom options for the upgrade. 3709function showConfigStep(overlay, ctx, fields, onConfirm, onCancel) { 3710var fieldsHtml = fields.map(function (field, i) { 3711var req = field.required ? ' <span class="ss-config-required">*</span>' : ''; 3712var effectiveValue = autoDefaultFor(field); 3713if (field.type === 'select') { 3714var optHtml = field.choices.map(function (c) { 3715var sel = c.value === effectiveValue ? ' selected' : ''; 3716var dis = c.disabled ? ' disabled' : ''; 3717return '<option value="' + escHtml(c.value) + '"' + sel + dis + '>' + escHtml(c.text) + '</option>'; 3718}).join(''); 3719return '<div class="ss-opt-field">' + 3720'<label>' + escHtml(field.label) + req + '</label>' + 3721'<select data-field-idx="' + i + '">' + optHtml + '</select>' + 3722'</div>'; 3723} 3724if (field.type === 'radio') { 3725var radioHtml = field.choices.map(function (c) { 3726var ch = c.value === effectiveValue ? ' checked' : ''; 3727return '<label class="ss-opt-radio">' + 3728'<input type="radio" name="ss-radio-' + i + '" value="' + escHtml(c.value) + '"' + ch + '> ' +
3729escHtml(c.text) + 3730'</label>'; 3731}).join(''); 3732return '<div class="ss-opt-field" data-field-idx="' + i + '">' + 3733'<div class="ss-opt-group-label">' + escHtml(field.label) + req + '</div>' + 3734radioHtml + 3735'</div>'; 3736} 3737if (field.type === 'text') { 3738var maxAttr = field.maxlength ? ' maxlength="' + field.maxlength + '"' : ''; 3739return '<div class="ss-opt-field">' + 3740'<label>' + escHtml(field.label) + req + '</label>' + 3741'<input type="text" data-field-idx="' + i + '" value="' + escHtml(field.currentValue) + '"' + maxAttr + '>' + 3742'</div>'; 3743} 3744if (field.type === 'textarea') { 3745return '<div class="ss-opt-field">' + 3746'<label>' + escHtml(field.label) + req + '</label>' + 3747'<textarea data-field-idx="' + i + '" rows="3">' + escHtml(field.currentValue) + '</textarea>' + 3748'</div>'; 3749} 3750return ''; 3751}).join(''); 3752// v1.31 -- config-step badge removed to match Step 1 chip removal. 3753// Cancel button now inherits the small-link style (ss-config-cancel 3754// modifier retained for JS querySelector but no longer overrides CSS). 3755overlay.innerHTML = 3756'<div class="ss-upsell-modal" role="dialog" aria-modal="true" aria-labelledby="ss-config-h">' + 3757'<button class="ss-upsell-close" aria-label="Cancel">×</button>' + 3758'<h2 id="ss-config-h">Review your options</h2>' + 3759'<p class="ss-config-intro">Confirm the selections for your upgraded stamp.</p>' + 3760'<div class="ss-config-fields">' + fieldsHtml + '</div>' + 3761'<div class="ss-upsell-actions">' + 3762'<button class="ss-upsell-accept" type="button">Confirm upgrade</button>' + 3763'<button class="ss-upsell-decline ss-config-cancel" type="button">Cancel upgrade</button>' + 3764'</div>' + 3765'</div>'; 3766var confirmBtn = overlay.querySelector('.ss-upsell-accept'); 3767var cancelBtn = overlay.querySelector('.ss-config-cancel'); 3768var closeBtn = overlay.querySelector('.ss-upsell-close'); 3769// v1.32b -- guard against a double-click on Confirm firing two submits 3770// (the Zephyr AJAX add doesn't navigate, so a second one really adds twice). 3771var confirmed = false; 3772confirmBtn.addEventListener('click', function () { 3773if (confirmed) return; 3774var values = {}; 3775var missing = []; 3776fields.forEach(function (field, i) { 3777var val = null; 3778if (field.type === 'select') { 3779var s = overlay.querySelector('select[data-field-idx="' + i + '"]'); 3780if (s) val = s.value; 3781} else if (field.type === 'radio') { 3782var r = overlay.querySelector('[data-field-idx="' + i + '"] input[type="radio"]:checked'); 3783if (r) val = r.value; 3784} else if (field.type === 'text' || field.type === 'textarea') { 3785var ti = overlay.querySelector('[data-field-idx="' + i + '"]'); 3786if (ti) val = ti.value; 3787} 3788if (val != null) values[field.name] = val; 3789if (field.required && (val == null || String(val).trim() === '')) { 3790missing.push(i); 3791} 3792}); 3793// Clear prior error styling 3794var oldErrs = overlay.querySelectorAll('.ss-opt-field-error'); 3795for (var e = 0; e < oldErrs.length; e++) oldErrs[e].classList.remove('ss-opt-field-error'); 3796var oldMsgs = overlay.querySelectorAll('.ss-opt-error'); 3797for (var m = 0; m < oldMsgs.length; m++) oldMsgs[m].parentNode && oldMsgs[m].parentNode.removeChild(oldMsgs[m]); 3798// Mark missing fields and bail if any 3799if (missing.length > 0) { 3800missing.forEach(function (idx, n) { 3801var el = overlay.querySelector('[data-field-idx="' + idx + '"]'); 3802if (!el) return; 3803var fieldEl = el.closest('.ss-opt-field') || el.parentElement; 3804if (!fieldEl) return; 3805fieldEl.classList.add('ss-opt-field-error'); 3806var msg = document.createElement('div'); 3807msg.className = 'ss-opt-error'; 3808msg.textContent = 'Please make a selection'; 3809fieldEl.appendChild(msg); 3810if (n === 0) { 3811try { fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) { fieldEl.scrollIntoView(); } 3812} 3813}); 3814track('upsell_config_validation_failed', { missing_count: missing.length }); 3815return; 3816} 3817confirmed = true; // v1.32b -- lock after a valid confirm 3818onConfirm(values); 3819}); 3820cancelBtn.addEventListener('click', function () { if (confirmed) return; onCancel('cancel_button'); }); 3821closeBtn.addEventListener('click', function () { if (confirmed) return; onCancel('close_button'); }); 3822setTimeout(function () { confirmBtn.focus(); }, 50); 3823} 3824// Replace the modal contents with a celebratory success state. After 3825// SUCCESS_DWELL_MS, run ctx.onSuccessComplete (which triggers the submit). 3826function showSuccessState(overlay, ctx) { 3827var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype; 3828var copy = SUCCESS_COPY[key] || { headline: 'Upgrade applied', tagline: 'Your stamp has been upgraded.' }; 3829overlay.innerHTML = 3830// v1.31 -- "Premium upgrade applied" chip removed to match Step 1 + Step 2. 3831// Green checkmark icon carries the celebration signal on its own now. 3832'<div class="ss-upsell-modal ss-upsell-success" role="dialog" aria-modal="true" aria-labelledby="ss-upsell-success-h">' + 3833'<div class="ss-upsell-success-icon" aria-hidden="true">✓</div>' + 3834'<h2 id="ss-upsell-success-h">' + copy.headline + '</h2>' + 3835'<p class="ss-upsell-success-tagline">
3835' + copy.tagline + '</p>' + 3836'<p class="ss-upsell-success-cta"><span class="ss-upsell-spinner"></span> Continuing to your cart...</p>' + 3837'<button class="ss-upsell-success-skip" type="button">Continue now</button>' + 3838'</div>'; 3839var skipBtn = overlay.querySelector('.ss-upsell-success-skip'); 3840var done = false; 3841function finalize() { 3842if (done) return; 3843done = true; 3844// Intentionally DO NOT remove the overlay here. We want the success 3845// state to remain visible while the browser navigates to the cart. 3846// The page transition will replace the modal naturally -- no flicker 3847// of bare PDP between success state and cart load. 3848// 3849// v1.32 note: on Zephyr the AJAX add opens the cart drawer rather than 3850// navigating away, so the success state is removed by handleAjaxAddToCart 3851// /the drawer taking over. It still dwells for SUCCESS_DWELL_MS first. 3852if (typeof ctx.onSuccessComplete === 'function') ctx.onSuccessComplete(); 3853} 3854skipBtn.addEventListener('click', finalize); 3855setTimeout(finalize, SUCCESS_DWELL_MS); 3856} 3857// ============================================================ 3858// CONFIG STEP -- extract / sync custom options 3859// ============================================================ 3860function escHtml(s) { 3861return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 3862} 3863function isElementHidden(el) { 3864if (!el) return true; 3865if (el.type === 'hidden') return true; 3866// Walk up the ancestor chain checking each computed style. 3867// Catches display:none, visibility:hidden, opacity:0 on the element 3868// OR any of its ancestors up to the form. 3869var node = el; 3870while (node && node !== document && node !== document.body) { 3871var s; 3872try { s = window.getComputedStyle(node); } catch (e) { s = null; } 3873if (s) { 3874if (s.display === 'none') return true; 3875if (s.visibility === 'hidden') return true; 3876if (parseFloat(s.opacity) === 0) return true; 3877} 3878node = node.parentElement; 3879} 3880// Final fallback -- offsetParent null catches some edge cases 3881if (el.offsetParent === null && el.tagName !== 'BODY') { 3882var rect = el.getBoundingClientRect(); 3883if (rect.width === 0 && rect.height === 0) return true; 3884} 3885return false; 3886} 3887function cleanLabel(s) { 3888return String(s || '').replace(/\*/g, '').replace(/:\s*$/, '').replace(/\s+/g, ' ').trim(); 3889} 3890function findLabelForFormField(el) { 3891if (!el) return 'Option'; 3892if (el.id) { 3893var byFor = document.querySelector('label[for="' + el.id + '"]'); 3894if (byFor) return cleanLabel(byFor.textContent); 3895} 3896// Magento default: previous DT contains the label 3897var dd = el.closest && el.closest('dd'); 3898if (dd) { 3899var prevDt = dd.previousElementSibling; 3900if (prevDt && prevDt.tagName === 'DT') { 3901var lblInDt = prevDt.querySelector('label') || prevDt; 3902return cleanLabel(lblInDt.textContent); 3903} 3904} 3905var field = el.closest && el.closest('.field, .input-box, li'); 3906if (field) { 3907var inLbl = field.querySelector('label.label, label'); 3908if (inLbl) return cleanLabel(inLbl.textContent); 3909} 3910return 'Option'; 3911} 3912// Pull the form's other custom option fields so we can render them in 3913// the config step. Skips the Stamp Type dropdown and any hidden fields. 3914function extractCustomOptions(form, excludeSelectEl) { 3915var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 3916var fields = []; 3917// Selects 3918var selects = container.querySelectorAll('select[name^="options"]'); 3919for (var i = 0; i < selects.length; i++) { 3920var sel = selects[i]; 3921if (sel === excludeSelectEl) continue; 3922if (isElementHidden(sel)) continue; 3923var choices = []; 3924for (var j = 0; j < sel.options.length; j++) { 3925var opt = sel.options[j]; 3926choices.push({ value: opt.value, text: opt.text, disabled: opt.disabled }); 3927} 3928fields.push({ 3929type: 'select', 3930name: sel.name, 3931label: findLabelForFormField(sel), 3932currentValue: sel.value, 3933required: sel.hasAttribute('required') || sel.classList.contains('required-entry') || sel.classList.contains('validate-select'), 3934choices: choices 3935}); 3936} 3937// Text inputs + textareas (stamp text, line 1, line 2, etc.) 3938// Gated by INCLUDE_TEXT_FIELDS_IN_CONFIG so the modal stays focused on 3939// tier-relevant choices. Text values still carry to the cart untouched. 3940if (INCLUDE_TEXT_FIELDS_IN_CONFIG) { 3941var texts = container.querySelectorAll('input[type="text"][name^="options"], textarea[name^="options"]'); 3942for (var t = 0; t < texts.length; t++) { 3943var inp = texts[t]; 3944if (isElementHidden(inp)) continue; 3945fields.push({ 3946type: inp.tagName === 'TEXTAREA' ? 'textarea' : 'text', 3947name: inp.name, 3948label: findLabelForFormField(inp), 3949currentValue: inp.value, 3950required: inp.hasAttribute('required') || inp.classList.contains('required-entry'), 3951maxlength: inp.maxLength > 0 ? inp.maxLength : null 3952}); 3953} 3954} 3955// Radio groups 3956var radios = container.querySelectorAll('input[type="radio"][name^="options"]'); 3957var groups = {}; 3958for (var k = 0; k < radios.length; k++) { 3959var r = radios[k]; 3960if (isElementHidden(r)) continue; 3961if (!groups[r.name]) groups[r.name] = []; 3962groups[r.name].push(r); 3963} 3964for (var name in groups) { 3965if (!Object.prototype.hasOwnProperty.call(groups, name)) continue; 3966var grp = groups[name]; 3967var checked = null; 3968var rChoices = []; 3969for (var m = 0; m < grp.length; m++) { 3970if (grp[m].checked) checked = grp[m]; 3971rChoices.push({ value: grp[m].value, text: findLabelForFormField(grp[m]) || grp[m].value }); 3972} 3973fields.push({ 3974type: 'radio', 3975name: name, 3976label: findLabelForFormField(grp[0]), 3977currentValue: checked ? checked.value : '', 3978required: grp[0].classList.contains('validate-one-required-by-name'), 3979choices: rChoices 3980}); 3981} 3982return fields; 3983} 3984// Normalize a tier definition into a key for TIER_OPTION_FILTERS. 3985// Self-Inking -> 'self_inking' 3986// Pre-Inked -> 'pre_inked' (from subtype) 3987// Professional-> 'professional' 3988function tierFilterKey(tierDef) { 3989if (!tierDef) return null; 3990if (tierDef.subtype) return tierDef.subtype; 3991return String(tierDef.name || '').toLowerCase().replace(/[\s\-]+/g, '_'); 3992} 3993// Filter custom option fields to those that apply to the upgrade target. 3994// Logic: 3995// 1. Override `hide` list (if any) drops matches first. 3996// 2. Auto-detect tier mentions in label: 3997// - mentions target tier name -> SHOW 3998// - mentions a different tier's name -> HIDE 3999// - mentions no tier -> fall through to step 3 4000// 3. Generic (no tier in label) options: 4001// - if override `show` is set, only labels matching survive 4002// - otherwise, show 4003function filterFieldsForTier(fields, tierDef) { 4004if (!tierDef) return fields; 4005var override = TIER_OPTION_FILTERS[tierFilterKey(tierDef)] || {}; 4006var overrideShow = (override.show || []).map(function (s) { return String(s).toLowerCase(); }); 4007var overrideHide = (override.hide || []).map(function (s) { return String(s).toLowerCase(); }); 4008return fields.filter(function (field) { 4009var label = String(field.label || '').toLowerCase(); 4010// Manual hide override wins 4011if (overrideHide.length && overrideHide.some(function (kw) { return label.indexOf(kw) !== -1; })) { 4012return false;
4013} 4014// Auto-detect tier tags in the label 4015var tiersInLabel = findAllTiersInText(label); 4016var mentionsTarget = tiersInLabel.indexOf(tierDef) !== -1; 4017var mentionsOther = tiersInLabel.some(function (t) { return t !== tierDef; }); 4018if (mentionsTarget) return true; // explicitly tagged for our tier 4019if (mentionsOther) return false; // explicitly tagged for a different tier 4020// Generic (no tier tag): apply manual show list if present 4021if (overrideShow.length > 0) { 4022return overrideShow.some(function (kw) { return label.indexOf(kw) !== -1; }); 4023} 4024return true; 4025}); 4026} 4027// Set the value of a single select-or-multiselect element. For multi- 4028// selects (name ends with "[]"), we must iterate options and set 4029// .selected explicitly -- assigning .value alone is unreliable across 4030// browsers and may leave stale selections active. 4031function setSelectValue(selectEl, targetValue) { 4032if (!selectEl) return; 4033if (selectEl.multiple) { 4034for (var i = 0; i < selectEl.options.length; i++) { 4035selectEl.options[i].selected = (selectEl.options[i].value === targetValue); 4036} 4037} else { 4038selectEl.value = targetValue; 4039} 4040} 4041// Cascade auto-fill: themes that use progressive reveal only show the 4042// next dropdown after the previous one has a value. After we swap 4043// Stamp Type, we may need to fill Size before Ink Color appears, fill 4044// Ink Color before Extra Bottle appears, etc. 4045// 4046// Strategy: find any required dropdown that is visible AND empty, 4047// pick a sane default ("No" if present, else first non-empty option), 4048// dispatch change, wait, repeat until DOM stops revealing new fields. 4049function cascadeAutoFill(form, stampSelect, callback, iter, maxIter) { 4050iter = iter || 0; 4051maxIter = maxIter || 10; 4052if (iter >= maxIter) { log('Cascade hit max iterations (' + maxIter + ')'); callback(); return; } 4053var container = form.querySelector('.product-options, .product-options-wrapper') || form; 4054var selects = container.querySelectorAll('select[name^="options"]'); 4055var changedAny = false; 4056for (var i = 0; i < selects.length; i++) { 4057var sel = selects[i]; 4058if (sel === stampSelect) continue; 4059if (sel.offsetParent === null) continue; // skip currently-hidden 4060if (sel.value && String(sel.value).trim() !== '') continue; // already filled 4061var isRequired = sel.hasAttribute('required') || 4062sel.classList.contains('required-entry') || 4063sel.classList.contains('validate-select'); 4064if (!isRequired) continue; 4065// Pick a sane default -- "No" preferred to avoid paid extras 4066var defaultVal = ''; 4067for (var j = 0; j < sel.options.length; j++) { 4068var opt = sel.options[j]; 4069if (!opt.value || opt.disabled) continue; 4070if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; } 4071if (!defaultVal) defaultVal = opt.value; 4072} 4073if (defaultVal) { 4074log('Cascade fill: ' + sel.name + ' = ' + defaultVal); 4075sel.value = defaultVal; 4076try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4077changedAny = true; 4078} 4079} 4080if (!changedAny) { log('Cascade stable at iter ' + iter); callback(); return; } 4081setTimeout(function () { cascadeAutoFill(form, stampSelect, callback, iter + 1, maxIter); }, 300); 4082} 4083// Safety-net pass -- runs RIGHT BEFORE form submit. Iterates one final time 4084// through every visible required empty select/radio and fills with defaults. 4085// Catches any field that cascade missed because the theme revealed it after 4086// cascade exited (chained progressive reveals with long settle times). 4087// Returns the count of fields it filled (for logging). 4088function safetyNetFill(form, stampSelect) { 4089var filled = 0; 4090var container = form.querySelector('.product-options, .product-options-wrapper') || form; 4091// Selects 4092var selects = container.querySelectorAll('select[name^="options"]'); 4093for (var i = 0; i < selects.length; i++) { 4094var sel = selects[i]; 4095if (sel === stampSelect) continue; 4096if (sel.offsetParent === null) continue; 4097if (sel.value && String(sel.value).trim() !== '') continue; 4098var isRequired = sel.hasAttribute('required') || 4099sel.classList.contains('required-entry') || 4100sel.classList.contains('validate-select'); 4101if (!isRequired) continue; 4102var defaultVal = ''; 4103for (var j = 0; j < sel.options.length; j++) { 4104var opt = sel.options[j]; 4105if (!opt.value || opt.disabled) continue; 4106if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; } 4107if (!defaultVal) defaultVal = opt.value; 4108} 4109if (defaultVal) { 4110log('Safety-net fill: ' + sel.name + ' = ' + defaultVal); 4111sel.value = defaultVal; 4112try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4113filled++; 4114} 4115} 4116// Radio groups 4117var radios = container.querySelectorAll('input[type="radio"][name^="options"]'); 4118var groups = {}; 4119for (var k = 0; k < radios.length; k++) { 4120var r = radios[k]; 4121if (r.offsetParent === null) continue; 4122if (!groups[r.name]) groups[r.name] = []; 4123groups[r.name].push(r); 4124} 4125for (var name in groups) { 4126if (!Object.prototype.hasOwnProperty.call(groups, name)) continue; 4127var grp = groups[name]; 4128var hasChecked = false;
4129for (var m = 0; m < grp.length; m++) { if (grp[m].checked) { hasChecked = true; break; } } 4130if (hasChecked) continue; 4131var isReq = grp[0].classList.contains('validate-one-required-by-name'); 4132if (!isReq) continue; 4133// Pick first non-disabled radio (or one with "no" label) 4134var pick = null; 4135for (var p = 0; p < grp.length; p++) { 4136if (grp[p].disabled) continue; 4137var lbl = findLabelForFormField(grp[p]) || ''; 4138if (/^\s*no\b/i.test(lbl)) { pick = grp[p]; break; } 4139if (!pick) pick = grp[p]; 4140} 4141if (pick) { 4142log('Safety-net fill (radio): ' + name + ' = ' + pick.value); 4143pick.checked = true; 4144try { pick.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4145filled++; 4146} 4147} 4148return filled; 4149} 4150// v1.26 -- Carry-forward: capture user's pre-upgrade visible dropdown 4151// selections (Ink Color, Size, Ink Pad, Extra Ink, etc.) by category. 4152// These are re-applied after cascade so the user's original choice 4153// survives the tier swap, even when the theme reveals a different 4154// dropdown for the new tier (e.g., product 4378: Self-Inking exposes 4155// options[7505] "Ink Color" but Professional exposes options[7504] 4156// "Ink Colors" -- same colors, different option IDs). 4157function categorizeLabel(label) { 4158var s = String(label || '').toLowerCase(); 4159if (/ink\s*colou?rs?/.test(s)) return 'ink_color'; 4160if (/replacement\s*pad|ink\s*pad|extra.*pad/.test(s)) return 'ink_pad'; 4161if (/extra\s*ink|extra.*bottle|bottle.*ink/.test(s)) return 'extra_ink'; 4162if (/size/.test(s)) return 'size'; 4163// Fallback -- use full lowercased label as its own category 4164return 'other::' + s.trim(); 4165} 4166function captureUserSelections(form, stampSelect) { 4167var captured = {}; 4168var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 4169var selects = container.querySelectorAll('select[name^="options"]'); 4170for (var i = 0; i < selects.length; i++) { 4171var sel = selects[i]; 4172if (sel === stampSelect) continue; 4173if (isElementHidden(sel)) continue; 4174if (!sel.value || String(sel.value).trim() === '') continue; 4175var selectedOpt = sel.options[sel.selectedIndex]; 4176if (!selectedOpt) continue; 4177var selectedText = String(selectedOpt.text || '').trim(); 4178if (!selectedText) continue; 4179var label = findLabelForFormField(sel) || ''; 4180var category = categorizeLabel(label); 4181// First-wins -- if two dropdowns share a category (rare), keep the first 4182if (!captured[category]) { 4183captured[category] = { text: selectedText, sourceLabel: label, sourceName: sel.name }; 4184} 4185} 4186return captured; 4187} 4188function applyCarriedForward(form, stampSelect, captured) { 4189var applied = 0; 4190if (!captured) return 0; 4191var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 4192var selects = container.querySelectorAll('select[name^="options"]'); 4193for (var i = 0; i < selects.length; i++) { 4194var sel = selects[i]; 4195if (sel === stampSelect) continue; 4196if (isElementHidden(sel)) continue; 4197var label = findLabelForFormField(sel) || ''; 4198var category = categorizeLabel(label); 4199var wanted = captured[category]; 4200if (!wanted) continue; 4201var currentText = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : ''; 4202// If already matches (e.g., cascade happened to pick right), skip 4203if (String(currentText).trim().toLowerCase() === wanted.text.toLowerCase()) continue; 4204// Find option whose text matches (case-insensitive, trimmed) 4205var matchOpt = null; 4206for (var j = 0; j < sel.options.length; j++) { 4207var opt = sel.options[j]; 4208if (!opt.value || opt.disabled) continue; 4209// Strip price suffix like " +$5.00" for match tolerance 4210var optText = String(opt.text || '').replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim(); 4211if (optText.toLowerCase() === wanted.text.replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim().toLowerCase()) { 4212matchOpt = opt; 4213break; 4214} 4215} 4216if (matchOpt) { 4217log('Carry-forward: ' + sel.name + ' (' + category + ') <- "' + wanted.text + '" from ' + wanted.sourceName); 4218sel.value = matchOpt.value; 4219try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4220applied++; 4221} else { 4222log('Carry-forward: no match for "' + wanted.text + '" in ' + sel.name + ' (' + category + ') -- cascade default stays'); 4223} 4224} 4225return applied; 4226} 4227function syncValuesToForm(form, values, fields) { 4228fields.forEach(function (field) { 4229var v = values[field.name]; 4230if (v == null) return; 4231if (field.type === 'select') { 4232var sel = form.querySelector('select[name="' + field.name + '"]'); 4233if (sel) { 4234sel.value = v; 4235try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4236} 4237} else if (field.type === 'radio') { 4238var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]'); 4239for (var i = 0; i < rs.length; i++) { 4240rs[i].checked = (rs[i].value === v); 4241if (rs[i].checked) { 4242try { rs[i].dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4243} 4244} 4245} else if (field.type === 'text' || field.type === 'textarea') { 4246var ti = form.querySelector('[name="' + field.name + '"]'); 4247if (ti) { 4248ti.value = v; 4249try { 4250ti.dispatchEvent(new Event('input', { bubbles: true })); 4251ti.dispatchEvent(new Event('change', { bubbles: true })); 4252} catch (e) {} 4253} 4254} 4255}); 4256}
4257function init() { 4258var form = document.getElementById('product_addtocart_form'); 4259if (!form) { log('Not a PDP (no ATC form) -- skipping init'); return; } 4260var stampSelect = findStampTypeSelect(); 4261if (!stampSelect) { log('No Stamp Type dropdown -- skipping init'); return; } 4262var upsellPath = detectUpsellPath(); 4263var productInput = form.querySelector('input[name="product"]'); 4264var productId = productInput ? productInput.value : ''; 4265var skuMeta = document.querySelector('meta[itemprop="sku"], [itemprop="sku"]'); 4266var productSku = skuMeta ? (skuMeta.getAttribute('content') || skuMeta.textContent || '').trim() : ''; 4267var category = detectCategory(); 4268var variant = getAssignedVariant(); 4269var variantCfg = getVariantConfig(variant); 4270log('Init', { upsellPath: upsellPath, productId: productId, category: category, variant: variant }); 4271function buildContext(cartItemId) { 4272// No-badger: if user already accepted an upgrade this session, skip. 4273if (hasUpgradedThisSession()) { log('buildContext: user already upgraded this session -- skipping'); return null; } 4274// Decline cap: if user has declined too many times this session, respect that and skip. 4275if (hasDeclinedTooMany()) { log('buildContext: decline cap reached (' + getDeclineCount() + '/' + MAX_DECLINES_PER_SESSION + ') -- skipping'); return null; } 4276// Category targeting: skip if this product's category isn't allowed. 4277if (!isCategoryAllowed(category)) { log('buildContext: category not in target/excluded by rule'); return null; } 4278var current = getCurrentTier(stampSelect); 4279if (!current) { log('buildContext: no current tier detected'); return null; } 4280var availableTiers = getAvailableTiers(stampSelect); 4281log('buildContext: available tiers on this product:', Object.keys(availableTiers)); 4282var next = getNextTier(current.tier, upsellPath, availableTiers); 4283if (!next) { log('buildContext: no upgrade tier (top tier or no upgrade options on product)'); return null; } 4284var upgradeOpt = findOptionInSelect(stampSelect, next); 4285if (!upgradeOpt) { log('buildContext: upgrade option vanished from dropdown'); return null; } 4286log('buildContext: ' + current.tier.name + ' -> ' + next.name); 4287return { 4288current: current, next: next, upgradeOpt: upgradeOpt, 4289priceDelta: calcPriceDelta(current.optionEl, upgradeOpt), 4290cartItemId: cartItemId, 4291productId: productId, productSku: productSku, category: category, upsellPath: upsellPath 4292}; 4293} 4294function mergeParams(a, b) { 4295var out = {}; for (var k in a) out[k] = a[k]; for (var k2 in b) out[k2] = b[k2]; return out; 4296} 4297// ---------------------------------------------------------- 4298// ATC interception -- v1.2 PRE-ADD flow: 4299// Show modal IMMEDIATELY on submit (no AJAX wait). 4300// On accept: change the Stamp Type dropdown to the upgrade value, 4301// then call the original submit. One cart write, right SKU. 4302// On decline: just call the original submit unchanged. 4303// 4304// v1.32 -- Zephyr's primary path is window.handleAjaxAddToCart (see 4305// patchAjaxHandler below). The Varien patch + form 'submit' listener 4306// are kept as fallbacks for any legacy/native path. 4307// 1. Patch window.handleAjaxAddToCart (Zephyr AJAX ATC) -- PRIMARY 4308// 2. Patch window.productAddToCartForm.submit (Varien) -- fallback 4309// 3. Fallback: form 'submit' event listener (non-Varien themes) 4310// ---------------------------------------------------------- 4311function buildBaseParams(ctx) { 4312return { 4313from_tier: ctx.current.tier.name, 4314to_tier: ctx.next.name, 4315upsell_path: ctx.upsellPath, 4316product_id: ctx.productId, 4317product_sku: ctx.productSku, 4318category: ctx.category, 4319price_delta: ctx.priceDelta, 4320variant: variant 4321}; 4322} 4323function showUpsellAndSubmit(ctx, doSubmit) { 4324// v1.32b -- never stack two modals if the ATC button is double-clicked. 4325if (document.querySelector('.ss-upsell-overlay')) { log('Modal already open -- ignoring duplicate trigger'); return; } 4326// v1.32b -- the underlying submit (native AJAX add on Zephyr) must run at 4327// most ONCE per flow. The old code relied on a full-page navigation to 4328// make a stray second call harmless; the AJAX add has no navigation, so 4329// we guard it explicitly. onDecline / onCancel / onSuccessComplete all 4330// route through this wrapper. 4331var _rawSubmit = doSubmit, _submitted = false;
4332doSubmit = function () { if (_submitted) return; _submitted = true; _rawSubmit(); }; 4333var baseParams = buildBaseParams(ctx); 4334var originalStampValue = ctx.current.optionEl.value; 4335track('upsell_modal_shown', baseParams); 4336// v1.28 -- Fire discount-side telemetry when variant B modal renders 4337// WITH a coupon code available for the target transition. Signals 4338// "user was exposed to the discount incentive" and lets B-only 4339// reports build without filtering upsell_modal_shown by variant. 4340if (variant === 'B') { 4341var shownCode = resolveCouponCode(variantCfg, ctx.next); 4342if (shownCode) { 4343track('upsell_discount_shown', mergeParams(baseParams, { coupon_code: shownCode })); 4344} 4345} 4346function chainToSuccessAndSubmit(overlay, userPicks, userFields, carriedForward) { 4347showSuccessState(overlay, { 4348current: ctx.current, 4349next: ctx.next, 4350onSuccessComplete: function () { 4351// Final safety net: catch any required field the cascade missed 4352// (e.g., chained progressive reveals where the theme exposes 4353// fields after our cascade exited). Without this, Magento 4354// backend validation can reject the submit and leave the user 4355// stuck on the PDP after the success state closes. 4356var filled = safetyNetFill(form, stampSelect); 4357if (filled > 0) log('Safety net filled ' + filled + ' field(s) before submit'); 4358// v1.27 -- Re-apply carry-forward AFTER safety net. When the 4359// user changes Size in the modal, the theme cascade can reveal 4360// a NEW Ink Color dropdown that our initial carry-forward 4361// (which ran pre-modal) never touched. Safety net just filled 4362// it with the default (Black). Now we override that default 4363// with the user's original pre-upgrade pick (Red) if we find 4364// a match by category + text. 4365if (carriedForward) { 4366var reCarried = applyCarriedForward(form, stampSelect, carriedForward); 4367if (reCarried > 0) log('Late carry-forward: ' + reCarried + ' field(s) restored to original pick'); 4368} 4369// CRITICAL: Re-apply user's explicit picks LAST. The theme's 4370// progressive reveal cascade can reset dependent options (e.g., 4371// Ink Color resets when Stamp Type/Size change). Without this 4372// final re-sync, the safety net's defaults overwrite the user's 4373// intended color/option picks, leading to "I picked Red but cart 4374// shows Black" bugs. Direct value writes only -- no change event 4375// dispatch -- to avoid re-triggering the theme's cascade. 4376if (userPicks && userFields) { 4377var reapplied = 0; 4378userFields.forEach(function (field) { 4379var v = userPicks[field.name]; 4380if (v == null || v === '') return; 4381if (field.type === 'select') { 4382var sel = form.querySelector('select[name="' + field.name + '"]'); 4383if (sel && String(sel.value) !== String(v)) { 4384log('Re-applying user pick: ' + field.name + ' ' + sel.value + ' -> ' + v); 4385sel.value = v; 4386reapplied++; 4387} 4388} else if (field.type === 'radio') { 4389var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]'); 4390for (var i = 0; i < rs.length; i++) { 4391var shouldCheck = (rs[i].value === v); 4392if (rs[i].checked !== shouldCheck) { 4393rs[i].checked = shouldCheck; 4394if (shouldCheck) reapplied++; 4395} 4396} 4397} else if (field.type === 'text' || field.type === 'textarea') { 4398var ti = form.querySelector('[name="' + field.name + '"]'); 4399if (ti && ti.value !== v) { 4400log('Re-applying user pick: ' + field.name + ' = ' + v); 4401ti.value = v; 4402reapplied++; 4403} 4404} 4405}); 4406if (reapplied > 0) log('Re-applied ' + reapplied + ' user pick(s) before submit'); 4407} 4408log('Success state complete -- submitting'); 4409doSubmit(); 4410} 4411}); 4412} 4413showModal({ 4414current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4415variantCfg: variantCfg, 4416onAccept: function (overlay) { 4417track('upsell_accepted', baseParams); 4418// v1.26 -- Capture user's pre-upgrade visible selections BEFORE 4419// Stamp Type swap wipes visibility (Ink Color, Size, etc.). 4420// These get carried forward to whichever dropdowns the new tier 4421// reveals, matched by category + option text (e.g., "Red"). 4422var carriedForward = captureUserSelections(form, stampSelect); 4423log('Captured for carry-forward:', carriedForward); 4424// Multiselect-safe Stamp Type swap (handles options[1508][] style) 4425setSelectValue(stampSelect, ctx.upgradeOpt.value); 4426try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4427log('Stamp Type swapped to:', ctx.upgradeOpt.text); 4428// Cascade auto-fill (themes with progressive reveal only show the 4429// next dropdown after the previous has a value). Then extract. 4430setTimeout(function () { 4431cascadeAutoFill(form, stampSelect, function () { 4432// v1.26 -- Apply carry-forward BEFORE extraction so the modal 4433// shows user's original picks as pre-selected values. User 4434// can still override in the modal if they want to change. 4435var carriedCount = applyCarriedForward(form, stampSelect, carriedForward); 4436if (carriedCount > 0) log('Carried forward ' + carriedCount + ' user selection(s) to new tier'); 4437var allFields = extractCustomOptions(form, stampSelect); 4438var filtered = filterFieldsForTier(allFields, ctx.next); 4439// Dedupe by label -- products sometimes have multiple custom 4440// options with identical labels (one per tier variant). Without 4441// tier hints in the label we can't distinguish them, so keep 4442// the first and drop the rest. 4443var seenLabels = {}; 4444var fields = filtered.filter(function (f) { 4445var key = String(f.label || '').toLowerCase().trim(); 4446if (!key) return true; 4447if (seenLabels[key]) return false;
4448seenLabels[key] = true; 4449return true; 4450}); 4451log('Extracted ' + allFields.length + ' option(s), ' + 4452filtered.length + ' apply to ' + ctx.next.name + ', ' + 4453fields.length + ' after dedupe'); 4454// Read qty from the form (used in revenue_delta calc later) 4455var qtyInput = form.querySelector('input[name="qty"]'); 4456var qty = qtyInput ? qtyInput.value : 1; 4457if (fields.length === 0) { 4458// No other options to configure -- straight to success 4459markUpgradedThisSession(); 4460track('upsell_confirmed', mergeParams(baseParams, { config_skipped: true })); 4461recordUpgradeForPurchaseTracking(baseParams, qty); 4462var skipCode = resolveCouponCode(variantCfg, ctx.next); 4463if (skipCode) { 4464log('Applying coupon for transition -> ' + ctx.next.name + ': ' + skipCode); 4465applyVariantCDiscount(skipCode, baseParams); 4466} 4467chainToSuccessAndSubmit(overlay, null, null, carriedForward); 4468return; 4469} 4470showConfigStep(overlay, ctx, fields, 4471function onConfirm(values) { 4472track('upsell_confirmed', mergeParams(baseParams, { config_field_count: fields.length })); 4473markUpgradedThisSession(); 4474syncValuesToForm(form, values, fields); 4475recordUpgradeForPurchaseTracking(baseParams, qty); 4476var confirmCode = resolveCouponCode(variantCfg, ctx.next); 4477if (confirmCode) { 4478log('Applying coupon for transition -> ' + ctx.next.name + ': ' + confirmCode); 4479applyVariantCDiscount(confirmCode, baseParams); 4480} 4481// Pass user picks + fields so they get re-applied AFTER 4482// safetyNetFill, defeating theme cascade resets. Also pass 4483// carriedForward for v1.27's late-cascade rescue: if user 4484// changed Size in modal and theme revealed a NEW Ink Color 4485// dropdown, restore original pre-upgrade pick. 4486chainToSuccessAndSubmit(overlay, values, fields, carriedForward); 4487}, 4488function onCancel(method) { 4489incrementDeclineCount();
4490track('upsell_canceled_at_config', mergeParams(baseParams, { cancel_method: method, decline_count: getDeclineCount() })); 4491// Revert Stamp Type to original -- user bailed at config 4492setSelectValue(stampSelect, originalStampValue); 4493try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4494if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 4495log('Canceled at config -- count:', getDeclineCount(), '-- submitting original selection'); 4496doSubmit(); 4497} 4498); 4499}); // end cascadeAutoFill callback 4500}, 100); 4501}, 4502onDecline: function (method) { 4503incrementDeclineCount(); 4504track('upsell_declined', mergeParams(baseParams, { dismiss_method: method, decline_count: getDeclineCount() })); 4505log('Declined (' + method + ') -- count:', getDeclineCount(), '-- submitting original selection'); 4506doSubmit(); 4507} 4508}); 4509} 4510// v1.32 -- PRIMARY Zephyr interception. The ATC button on Zephyr is 4511// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // so it never reaches Varien's submit or a native form submit event. 4512// We wrap the global handler: validate (Varien validator still exists), 4513// build the upsell context, and if an upgrade is available show the modal 4514// first. The original AJAX add runs only after accept/decline, by which 4515// point the Stamp Type has been swapped (on accept). handleAjaxAddToCart 4516// re-serializes the form when it runs, so the upgraded selection is added. 4517function patchAjaxHandler() { 4518var fn = window.handleAjaxAddToCart; 4519if (typeof fn !== 'function') return false; 4520if (fn._ssTierPatched) return true; 4521var orig = fn; 4522window.handleAjaxAddToCart = function (button) { 4523var args = arguments; 4524function proceed() { return orig.apply(window, args); } 4525// Respect Magento/Varien validation first. handleAjaxAddToCart runs 4526// the same validator internally; if the form isn't valid, hand off to 4527// the original so it surfaces the field errors (and we don't pop the 4528// modal on an incomplete form). 4529try { 4530var paf = window.productAddToCartForm; 4531if (paf && paf.validator && typeof paf.validator.validate === 'function' && !paf.validator.validate()) { 4532log('Validator failed -- passing through to native AJAX add'); 4533return proceed(); 4534} 4535} catch (e) {} 4536var ctx = buildContext(null); 4537if (!ctx) { log('No upsell context -- native AJAX add'); return proceed(); } 4538log('Upsell available -- showing modal before AJAX add (Zephyr path)'); 4539showUpsellAndSubmit(ctx, function () { 4540// Run the native AJAX add, then clean up. Unlike the old Varien 4541// path this does NOT navigate, so WE must remove the success 4542// overlay (otherwise its full-screen fixed layer covers the page 4543// forever) and apply any stashed variant-B coupon now that the 4544// item is in the cart (the drawer never loads /checkout/cart). 4545var r; 4546try { r = proceed(); } catch (e) { r = null; } 4547var _done = false; 4548var done = function () { 4549if (_done) return; _done = true; // idempotent -- promise OR timeout, whichever first 4550var o = document.querySelector('.ss-upsell-overlay'); 4551if (o && o.parentNode) o.parentNode.removeChild(o); 4552try { applyPendingCouponNow(); } catch (e) {} 4553}; 4554var thenable = r && typeof r.then === 'function'; 4555if (thenable) { r.then(done, done); } 4556// Always arm a max-timeout too: if the add promise is non-thenable 4557// (short delay) OR never settles (6s hang guard), we still remove the 4558// overlay so it can't cover the page or suppress future modals. 4559setTimeout(done, thenable ? 6000 : 1200); 4560}); 4561return false; // block the immediate add; proceed() runs after the modal resolves 4562}; 4563window.handleAjaxAddToCart._ssTierPatched = true; 4564log('Patched window.handleAjaxAddToCart (Zephyr AJAX ATC)'); 4565return true; 4566} 4567(function pollForAjaxHandler(attempts) { 4568attempts = attempts || 0; 4569if (patchAjaxHandler()) return; 4570if (attempts >= 30) { log('Gave up polling for handleAjaxAddToCart after 3s'); return; } 4571setTimeout(function () { pollForAjaxHandler(attempts + 1); }, 100); 4572})(); 4573function patchVarienForm() { 4574var paf = window.productAddToCartForm; 4575if (!paf || typeof paf.submit !== 'function' || paf._ssTierPatched) return false; 4576var origSubmit = paf.submit.bind(paf); 4577paf.submit = function (button, url) { 4578if (this.validator && typeof this.validator.validate === 'function' && !this.validator.validate()) { 4579log('Magento validator failed -- letting native submit handle the error UI'); 4580return origSubmit(button, url); 4581} 4582var ctx = buildContext(null); 4583if (!ctx) { 4584log('No upsell context -- proceeding with native ATC'); 4585return origSubmit(button, url); 4586} 4587log('Upsell available -- showing modal pre-add'); 4588showUpsellAndSubmit(ctx, function () { origSubmit(button, url); }); 4589}; 4590paf._ssTierPatched = true; 4591log('Patched window.productAddToCartForm.submit'); 4592return true; 4593} 4594(function pollForVarien(attempts) { 4595attempts = attempts || 0; 4596if (patchVarienForm()) return; 4597if (attempts >= 30) { log('Gave up polling for productAddToCartForm after 3s'); return; } 4598setTimeout(function () { pollForVarien(attempts + 1); }, 100); 4599})(); 4600// Fallback for non-Varien / Enter-key submission 4601form.addEventListener('submit', function (e) { 4602if (form._ssBypass) { form._ssBypass = false; return; } // we already showed modal 4603if (window.productAddToCartForm && window.productAddToCartForm._ssTierPatched) return; 4604// If the Zephyr AJAX handler is patched, it owns the flow -- don't
4605// double-fire the modal from a native submit that may also occur. 4606if (window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched) return; 4607var ctx = buildContext(null); 4608if (!ctx) return; 4609e.preventDefault(); 4610log('Submit event intercepted (fallback path) -- showing modal pre-add'); 4611showUpsellAndSubmit(ctx, function () { 4612form._ssBypass = true; 4613form.submit(); 4614}); 4615}); 4616window.SSUpsell = { 4617showTestModal: function () { 4618var ctx = buildContext('TEST-CART-ITEM'); 4619if (!ctx) { alert('No upsell available. Current: ' + (stampSelect.options[stampSelect.selectedIndex] || {}).text); return; } 4620var baseParams = { from_tier: ctx.current.tier.name, to_tier: ctx.next.name, upsell_path: ctx.upsellPath, product_id: ctx.productId, price_delta: ctx.priceDelta }; 4621track('upsell_modal_shown', baseParams); 4622showModal({ 4623current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4624variantCfg: variantCfg, 4625onAccept: function (overlay) { 4626track('upsell_accepted', baseParams); 4627if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); 4628alert('Accept (test mode -- no cart write)'); 4629},
4630onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); } 4631}); 4632}, 4633inspect: function () { 4634var ctx = buildContext(null); 4635var available = getAvailableTiers(stampSelect); 4636var currentTierLive = getCurrentTier(stampSelect); 4637// Dry-run the config-step extraction + filter so you can see what 4638// the user WOULD see if they accepted right now. 4639var optsPreview = null; 4640if (ctx) { 4641var all = extractCustomOptions(form, stampSelect); 4642var kept = filterFieldsForTier(all, ctx.next); 4643var seenL = {}; 4644var deduped = kept.filter(function (f) { 4645var k = String(f.label || '').toLowerCase().trim(); 4646if (!k) return true; 4647if (seenL[k]) return false; 4648seenL[k] = true; 4649return true; 4650}); 4651optsPreview = { 4652extracted: all.map(function (f) { return f.label; }), 4653keptAfterFilter: kept.map(function (f) { return f.label; }), 4654shownToUser: deduped.map(function (f) { return f.label; }), 4655filterRule: TIER_OPTION_FILTERS[tierFilterKey(ctx.next)] || '(none -- show all)' 4656}; 4657} 4658return { 4659upsellPath: upsellPath, 4660productId: productId, 4661productSku: productSku, 4662category: category, 4663currentTier: currentTierLive ? currentTierLive.tier.name : null, 4664nextTier: ctx ? ctx.next.name : null, 4665priceDelta: ctx ? ctx.priceDelta : null, 4666availableOnProduct: Object.keys(available), 4667ajaxHandlerPatched: !!(window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched), 4668ajaxHandlerPresent: typeof window.handleAjaxAddToCart === 'function', 4669varienFormPatched: !!(window.productAddToCartForm && window.productAddToCartForm._ssTierPatched), 4670varienFormPresent: !!window.productAddToCartForm, 4671alreadyUpgradedThisSession: hasUpgradedThisSession(), 4672declineCount: getDeclineCount(), 4673maxDeclinesPerSession: MAX_DECLINES_PER_SESSION, 4674declineCapReached: hasDeclinedTooMany(), 4675variant: variant, 4676variantConfig: variantCfg, 4677devModeAlwaysShow: DEV_MODE_ALWAYS_SHOW, 4678categoryAllowed: isCategoryAllowed(category), 4679targetCategories: TARGET_CATEGORIES, 4680excludedCategories: EXCLUDED_CATEGORIES, 4681configStepPreview: optsPreview 4682}; 4683}, 4684// QA helper -- clear the session flags (upgrade lock + decline count) so the modal fires again 4685resetSession: function () { 4686try { 4687sessionStorage.removeItem(SESSION_ACCEPTED_KEY); 4688sessionStorage.removeItem(SESSION_DECLINE_KEY); 4689} catch (e) {} 4690console.log('[TierUpsell] Session reset -- modal will fire on next ATC'); 4691}, 4692// QA helpers for variant testing 4693setVariant: function (v) { 4694v = String(v || '').toUpperCase(); 4695if (VARIANTS.indexOf(v) === -1) { 4696console.warn('Invalid variant. Use one of:', VARIANTS); 4697return; 4698} 4699try { localStorage.setItem(VARIANT_STORAGE_KEY, v); } catch (e) {} 4700console.log('[TierUpsell] Variant set to:', v, '-- refresh the page to see changes'); 4701}, 4702clearVariant: function () { 4703try { localStorage.removeItem(VARIANT_STORAGE_KEY); } catch (e) {} 4704console.log('[TierUpsell] Variant cleared -- next page load will assign a new one'); 4705}, 4706simulateATC: function () { 4707var ctx = buildContext('TEST-CART-ITEM'); 4708if (!ctx) { alert('No upsell available right now.'); return; } 4709var baseParams = { 4710from_tier: ctx.current.tier.name, to_tier: ctx.next.name, 4711upsell_path: ctx.upsellPath, product_id: ctx.productId, 4712product_sku: ctx.productSku, category: ctx.category, price_delta: ctx.priceDelta 4713}; 4714track('upsell_modal_shown', baseParams); 4715showModal({ 4716current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4717variantCfg: variantCfg, 4718onAccept: function (overlay) { 4719track('upsell_accepted', baseParams); 4720if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); 4721console.log('[TierUpsell] Would update cart now (simulate mode)'); 4722},
4723onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); console.log('[TierUpsell] Declined (simulate mode):', m); } 4724}); 4725} 4726}; 4727} 4728if (document.readyState === 'loading') { 4729document.addEventListener('DOMContentLoaded', init); 4730} else { 4731init(); 4732} 4733})(); </script>
4733</div><div class="widget widget-static-block">
4733<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","
4733sort_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","
4733sort_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","
4733notary 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","
4733sort_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","
4733sort_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","
4733sort_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
4733sender","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","
4733sort_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","
4733sort_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","
4733sort_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","
4733sort_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","
4733sort_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","
4733sort_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","
4733sort_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>
4733</div><div class="widget widget-static-block"> <style> /* ========================================================= 4734Scoped to #cart-kit-upsell-mount to avoid site collisions 4735========================================================= */ 4736#cart-kit-upsell-mount { 4737--ck-accent: var(--brand-primary, #1976d2); 4738--ck-accent-hover: var(--brand-primary-hover, #60B5E5); 4739--ck-success: var(--brand-success, #2e7d32); 4740--ck-success-hover: #256628; 4741} 4742#cart-kit-upsell-mount .cart-kit-upsell { 4743margin: 24px 0; 4744border-radius: 8px; 4745box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); 4746background: #ffffff; 4747border: 1px solid #e3e3e3; 4748overflow: visible; 4749font-family: inherit; 4750position: relative; 4751} 4752#cart-kit-upsell-mount .cart-kit-upsell__header { 4753width: 100%; 4754text-align: left; 4755border: 0; 4756padding: 12px 16px; 4757background: #f5fafc; 4758display: flex; 4759align-items: center; 4760justify-content: flex-start; 4761gap: 12px; 4762cursor: pointer; 4763border-radius: 8px; 4764} 4765#cart-kit-upsell-mount .cart-kit-upsell__header:focus { 4766outline: 2px solid var(--ck-accent); 4767outline-offset: -2px; 4768} 4769#cart-kit-upsell-mount .cart-kit-upsell__header-text { 4770display: flex; 4771flex-direction: column; 4772gap: 4px; 4773flex: 1 1 auto; 4774} 4775#cart-kit-upsell-mount .cart-kit-upsell__title { 4776font-size: 20px; 4777font-weight: 600; 4778color: #222; 4779} 4780#cart-kit-upsell-mount .cart-kit-upsell__chevron { 4781width: 10px; 4782height: 10px; 4783border-right: 2px solid #555; 4784border-bottom: 2px solid #555; 4785transform: rotate(45deg); 4786transition: transform 0.2s ease; 4787} 4788#cart-kit-upsell-mount .cart-kit-upsell--open .cart-kit-upsell__chevron { 4789transform: rotate(-135deg); 4790} 4791#cart-kit-upsell-mount .cart-kit-upsell__body { 4792padding: 12px 16px 14px; 4793border-top: 1px solid #e3e3e3; 4794animation: cartKitUpsellFadeIn 0.18s ease-out; 4795} 4796@keyframes cartKitUpsellFadeIn { 4797from { 4798opacity: 0; 4799transform: translateY(-2px); 4800} 4801to { 4802opacity: 1; 4803transform: translateY(0); 4804} 4805} 4806/* Icon zone */ 4807#cart-kit-upsell-mount .cart-kit-upsell__icon-wrap { 4808flex: 0 0 auto; 4809width: auto; 4810height: 36px; 4811display: flex; 4812align-items: center; 4813justify-content: center; 4814overflow: visible; 4815} 4816#cart-kit-upsell-mount .cart-kit-upsell__icon-img { 4817width: 70px; 4818height: auto; 4819display: block; 4820transform: none; 4821opacity: 1; 4822transition: none; 4823} 4824/* Options */ 4825#cart-kit-upsell-mount .cart-kit-upsell__options-group { 4826display: flex; 4827flex-direction: column; 4828gap: 10px; 4829} 4830#cart-kit-upsell-mount .cart-kit-upsell__options-label { 4831font-size: 16px; 4832font-weight: 600; 4833color: #333; 4834} 4835#cart-kit-upsell-mount .cart-kit-upsell__options-header { 4836display: flex; 4837align-items: center; 4838gap: 6px; 4839margin-bottom: 4px; 4840overflow: visible !important; 4841} 4842/* Tooltip */ 4843#cart-kit-upsell-mount .cart-kit-upsell__info { 4844position: relative; 4845border: 0; 4846padding: 0; 4847background: none; 4848cursor: pointer; 4849display: inline-flex; 4850z-index: 5; 4851} 4852#cart-kit-upsell-mount .cart-kit-upsell__info-icon { 4853width: 18px; 4854height: 18px; 4855border-radius: 999px; 4856border: 1px solid #888; 4857font-size: 11px; 4858font-weight: 600; 4859display: flex; 4860align-items: center; 4861justify-content: center; 4862color: #555; 4863background: #fff; 4864line-height: 1; 4865} 4866#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip { 4867position: absolute; 4868left: 50%; 4869bottom: calc(100% + 8px); 4870transform: translateX(-50%); 4871background: #222; 4872color: #fff; 4873font-size: 13px; 4874padding: 6px 8px; 4875border-radius: 4px; 4876box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); 4877opacity: 0; 4878pointer-events: none; 4879transition: opacity .15s ease, transform .15s ease; 4880white-space: normal; 4881max-width: min(260px, 80vw); 4882width: 180px; 4883line-height: 1.2; 4884z-index: 9999; 4885} 4886#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip::after { 4887content: ''; 4888position: absolute; 4889top: 100%; 4890left: 50%; 4891transform: translateX(-50%); 4892border-width: 6px; 4893border-style: solid; 4894border-color: #222 transparent transparent transparent; 4895} 4896#cart-kit-upsell-mount .cart-kit-upsell__info:hover .cart-kit-upsell__info-tooltip, 4897#cart-kit-upsell-mount .cart-kit-upsell__info:focus-visible .cart-kit-upsell__info-tooltip { 4898opacity: 1; 4899transform: translateX(-50%) translateY(-2px); 4900pointer-events: auto; 4901} 4902/* ========================================= 4903Grid (Accent stamps OR Notary product tiles) 4904========================================= */ 4905/* Default (Accent chips) */ 4906#cart-kit-upsell-mount .kit-stamp-grid { 4907display: flex; 4908flex-wrap: wrap; 4909gap: 16px; 4910align-items: stretch; 4911} 4912#cart-kit-upsell-mount .kit-stamp-card { 4913border: none; 4914background: transparent; 4915padding: 0 4px 4px; 4916margin: 0; 4917cursor: pointer; 4918outline: none; 4919flex: 0 0 auto; 4920} 4921/* Accent chip circle */ 4922#cart-kit-upsell-mount .kit-stamp-chip { 4923display: flex; 4924flex-direction: column; 4925align-items: center; 4926text-align: center; 4927max-width: 200px; 4928} 4929#cart-kit-upsell-mount .kit-stamp-chip__circle { 4930width: 200px; 4931height: auto; 4932border-radius: 16px; 4933background: #f3fbff; 4934box-shadow: 0 0 0 1px #e1f0fb; 4935display: flex; 4936align-items: center; 4937justify-content: center; 4938margin-bottom: 6px; 4939transition: box-shadow .15s ease, transform .1s ease, background .15s ease; 4940overflow: clip; 4941} 4942#cart-kit-upsell-mount .kit-stamp-chip__image { 4943width: 100%; 4944height: auto; 4945display: block; 4946} 4947#cart-kit-upsell-mount .kit-stamp-card:hover .kit-stamp-chip__circle { 4948box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.25); 4949transform: translateY(-1px); 4950} 4951#cart-kit-upsell-mount .kit-stamp-card.is-selected .kit-stamp-chip__circle { 4952background: #e6f2ff; 4953box-shadow: 0 0 0 2px var(--ck-accent); 4954} 4955/* ========================================= 4956Notary tile layout (only when .is-notary-grid is applied) 4957========================================= */ 4958#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4959display: inline-grid; 4960grid-template-columns: repeat(6, minmax(0, 1fr)); 4961gap: 16px; 4962align-items: stretch; 4963} 4964@media (max-width: 900px) { 4965#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4966grid-template-columns: repeat(4, minmax(0, 1fr)); 4967} 4968} 4969@media (max-width: 768px) { 4970#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4971grid-template-columns: repeat(3, minmax(0, 1fr)); 4972} 4973} 4974@media (max-width: 520px) {
4975#cart-kit-upsell-mount .cart-kit-upsell__header-text { 4976width: 50%; 4977} 4978/* Swipeable tiles on mobile */ 4979#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 4980display: flex; 4981overflow-x: auto; 4982gap: 12px; 4983scroll-snap-type: x mandatory; 4984-webkit-overflow-scrolling: touch; 4985padding-bottom: 8px; 4986padding-top: 4px; 4987padding-left: 4px; 4988padding-right: 4px; 4989} 4990/* Each tile becomes a "slide" */ 4991#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid .kit-stamp-card.kit-product-card { 4992flex: 0 0 78%; 4993scroll-snap-align: start; 4994} 4995/* Hide scrollbar (nice-to-have) */ 4996#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid::-webkit-scrollbar { 4997display: none; 4998} 4999#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 5000scrollbar-width: none; 5001} 5002/* Target the actual tile grid container */ 5003#cart-kit-upsell-mount .kit-stamp-grid { 5004display: flex !important; 5005flex-wrap: nowrap !important; 5006overflow-x: auto !important; 5007overflow-y: hidden !important; 5008gap: 12px !important; 5009scroll-snap-type: x mandatory; 5010-webkit-overflow-scrolling: touch; 5011padding-bottom: 10px; 5012} 5013/* Make each tile act like a slide */ 5014#cart-kit-upsell-mount .kit-stamp-grid>* { 5015flex: 0 0 78% !important; 5016/* change to 60% to show ~2 at a time */ 5017scroll-snap-align: start; 5018} 5019/* Optional: hide scrollbar */ 5020#cart-kit-upsell-mount .kit-stamp-grid::-webkit-scrollbar {
5021display: none; 5022} 5023#cart-kit-upsell-mount .kit-stamp-grid { 5024scrollbar-width: none; 5025} 5026#cart-kit-upsell-mount, 5027#cart-kit-upsell-mount .kit-upsell-inner, 5028#cart-kit-upsell-mount .kit-upsell-body { 5029overflow: visible !important; 5030} 5031} 5032/* Notary tile card */ 5033#cart-kit-upsell-mount .kit-stamp-card.kit-product-card { 5034background: #fff; 5035border: 1px solid #e1f0fb; 5036border-radius: 12px; 5037padding: 12px; 5038text-align: left; 5039flex: 1 1 auto; 5040transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, background .12s ease; 5041} 5042#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:hover { 5043transform: translateY(-1px); 5044border-color: rgba(25, 118, 210, 0.35); 5045box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); 5046} 5047#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.ck-swapping { 5048opacity: 0; 5049transition: opacity .18s ease; 5050} 5051#cart-kit-upsell-mount .kit-empty-tiles { 5052padding: 18px; 5053border: 1px dashed #d9d9d9; 5054border-radius: 10px; 5055margin-top: 10px; 5056font-size: 14px; 5057color: #444; 5058background: #fafafa; 5059} 5060/* Pool exhaustion: completion tile */ 5061#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete { 5062background: #f6faf7; 5063border: 1px dashed #b6dfc7; 5064cursor: default; 5065} 5066#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete:hover { 5067transform: none; 5068border-color: #b6dfc7; 5069box-shadow: none; 5070} 5071#cart-kit-upsell-mount .ck-tile-complete { 5072text-align: center; 5073padding: 16px 10px; 5074} 5075#cart-kit-upsell-mount .ck-tile-complete__icon { 5076font-size: 22px; 5077margin-bottom: 6px; 5078line-height: 1; 5079} 5080#cart-kit-upsell-mount .ck-tile-complete__title { 5081font-weight: 700; 5082margin-bottom: 4px; 5083} 5084#cart-kit-upsell-mount .ck-tile-complete__subtext { 5085font-size: 13px; 5086opacity: 0.85; 5087} 5088#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:focus-visible { 5089outline: 3px solid rgba(25, 118, 210, 0.35); 5090outline-offset: 2px; 5091} 5092#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected { 5093background: #f5f9ff; 5094border-color: var(--ck-accent); 5095box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.14); 5096} 5097/* Notary tile thumb */ 5098#cart-kit-upsell-mount .kit-product-thumb-tile { 5099width: 100%; 5100aspect-ratio: 1 / 1; 5101border-radius: 10px; 5102background: #f3f6fb; 5103border: 1px solid rgba(0, 0, 0, 0.06); 5104overflow: hidden; 5105position: relative; 5106display: flex; 5107align-items: center; 5108justify-content: center; 5109} 5110#cart-kit-upsell-mount .kit-product-thumb-tile img { 5111width: 100%; 5112height: 100%; 5113object-fit: contain; 5114display: block; 5115background-color: #fff; 5116} 5117#cart-kit-upsell-mount .kit-product-meta { 5118margin-top: 10px; 5119} 5120#cart-kit-upsell-mount .kit-product-name { 5121font-weight: 700; 5122font-size: 13px; 5123color: #222; 5124line-height: 1.2; 5125margin: 0; 5126} 5127#cart-kit-upsell-mount .kit-product-price { 5128margin-top: 6px; 5129font-size: 12px; 5130color: #4a4a4a; 5131} 5132#cart-kit-upsell-mount .kit-product-sub { 5133margin-top: 4px; 5134font-size: 11px; 5135color: #6b6b6b; 5136} 5137/* Notary tile selected checkmark */ 5138#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected .kit-product-thumb-tile::after { 5139content: "â"; 5140position: absolute; 5141right: 8px; 5142bottom: 8px; 5143width: 22px; 5144height: 22px; 5145border-radius: 999px; 5146background: var(--ck-success); 5147color: #fff; 5148font-size: 13px; 5149font-weight: 900; 5150display: flex; 5151align-items: center; 5152justify-content: center; 5153box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); 5154} 5155/* ========================================= 5156Micro PDP "Added" state (tile grey-out + overlay) 5157========================================= */ 5158#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added { 5159opacity: 0.55; 5160filter: grayscale(0.25); 5161pointer-events: none; 5162/* prevent clicking again */ 5163position: relative; 5164} 5165/* Stronger "Added" overlay */ 5166#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added::before { 5167content: "â Added to cart"; 5168position: absolute; 5169left: 10px; 5170right: 10px; 5171bottom: 10px; 5172padding: 10px 12px; 5173border-radius: 999px; 5174background: rgba(20, 20, 20, .90); 5175color: #fff; 5176font-weight: 800; 5177font-size: 13px; 5178text-align: center; 5179z-index: 6; 5180} 5181#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added img { 5182opacity: 0.75; 5183} 5184/* Module success toast */ 5185#cart-kit-upsell-mount .kit-success-toast { 5186position: absolute; 5187right: 16px; 5188bottom: 16px; 5189background: var(--ck-success-hover); 5190color: #fff; 5191padding: 10px 12px; 5192border-radius: 10px;
5193font-weight: 800; 5194font-size: 13px; 5195max-width: 340px; 5196box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); 5197opacity: 0; 5198transform: translateY(6px); 5199transition: opacity .18s ease, transform .18s ease; 5200pointer-events: none; 5201z-index: 50; 5202} 5203#cart-kit-upsell-mount .kit-success-toast.is-visible { 5204opacity: 1; 5205transform: translateY(0); 5206} 5207/* ========================================= 5208Cart row highlight after Micro PDP add 5209========================================= */ 5210#cart-kit-upsell-mount .ck-cart-highlight { 5211outline: 3px solid rgba(46, 125, 50, 0.35); 5212outline-offset: 4px; 5213border-radius: 8px; 5214animation: ckPulse 1.6s ease-in-out 0s 2; 5215} 5216@keyframes ckPulse { 52170% { 5218box-shadow: 0 0 0 rgba(46, 125, 50, 0.00); 5219} 522050% { 5221box-shadow: 0 0 0 10px rgba(46, 125, 50, 0.10); 5222} 5223100% { 5224box-shadow: 0 0 0 rgba(46, 125, 50, 0.00); 5225} 5226} 5227/* Ink swatches */ 5228#cart-kit-upsell-mount .kit-swatch-list { 5229display: flex; 5230flex-wrap: wrap; 5231gap: 8px; 5232} 5233#cart-kit-upsell-mount .kit-swatch { 5234display: inline-flex; 5235align-items: center; 5236gap: 6px; 5237padding: 4px 8px; 5238border-radius: 999px; 5239border: 1px solid #d8e0ea; 5240background: #fff; 5241cursor: pointer; 5242font-size: 14px; 5243color: #333; 5244transition: border-color .15s ease, box-shadow .15s ease, background .15s ease; 5245} 5246#cart-kit-upsell-mount .kit-swatch:hover { 5247border-color: var(--ck-accent); 5248box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); 5249} 5250#cart-kit-upsell-mount .kit-swatch.is-selected { 5251border-color: var(--ck-accent); 5252background: #f5f9ff; 5253box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.18); 5254} 5255#cart-kit-upsell-mount .kit-swatch__dot { 5256width: 14px; 5257height: 14px; 5258border-radius: 999px; 5259box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15); 5260} 5261#cart-kit-upsell-mount .kit-swatch__dot--black { 5262background: #111; 5263} 5264#cart-kit-upsell-mount .kit-swatch__dot--blue { 5265background: #1565c0; 5266} 5267#cart-kit-upsell-mount .kit-swatch__dot--red { 5268background: #c62828; 5269} 5270#cart-kit-upsell-mount .kit-swatch__dot--green { 5271background: var(--ck-success); 5272} 5273#cart-kit-upsell-mount .kit-swatch__dot--purple { 5274background: #6a1b9a; 5275} 5276#cart-kit-upsell-mount .kit-swatch__dot--none { 5277background: repeating-linear-gradient(45deg, #ccc, #ccc 3px, #eee 3px, #eee 6px); 5278} 5279/* Footer / CTA */ 5280#cart-kit-upsell-mount .cart-kit-upsell__footer { 5281display: flex; 5282flex-wrap: wrap; 5283align-items: center; 5284justify-content: space-between; 5285gap: 8px; 5286margin-top: 18px; 5287padding: 8px; 5288} 5289#cart-kit-upsell-mount .cart-kit-upsell__price { 5290font-size: 16px; 5291color: #222; 5292display: flex; 5293gap: 14px; 5294align-items: center; 5295flex-wrap: wrap; 5296} 5297#cart-kit-upsell-mount .cart-kit-upsell__cta { 5298padding: 8px 16px; 5299border-radius: 4px; 5300border: 0; 5301background: var(--ck-success); 5302color: #fff; 5303font-size: 13px; 5304font-weight: 600; 5305cursor: pointer; 5306white-space: nowrap; 5307} 5308#cart-kit-upsell-mount .cart-kit-upsell__cta:hover { 5309background: var(--ck-success-hover); 5310} 5311#cart-kit-upsell-mount .cart-kit-upsell__cta:disabled { 5312opacity: .6; 5313cursor: default; 5314} 5315#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary { 5316padding: 8px 14px; 5317border-radius: 4px; 5318border: 1px solid #ccc; 5319background: #f7f7f7; 5320color: #333; 5321font-size: 13px; 5322font-weight: 600; 5323cursor: pointer; 5324white-space: nowrap; 5325} 5326#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary:hover { 5327background: #eee; 5328} 5329/* Spinner on Add button */ 5330#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading { 5331position: relative; 5332padding-right: 30px; 5333} 5334#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading::after { 5335content: ''; 5336position: absolute; 5337right: 10px; 5338top: 50%; 5339width: 14px; 5340height: 14px; 5341margin-top: -7px; 5342border-radius: 50%; 5343border: 2px solid rgba(255, 255, 255, 0.7); 5344border-top-color: transparent; 5345animation: cartKitSpinner .6s linear infinite; 5346} 5347@keyframes cartKitSpinner { 5348to { 5349transform: rotate(360deg); 5350} 5351} 5352/* Add-to-cart progress bar (the ONLY progress bar in this script) */ 5353#cart-kit-upsell-mount .cart-kit-upsell__progresswrap { 5354margin-top: 10px; 5355} 5356#cart-kit-upsell-mount .cart-kit-upsell__progresstop { 5357display: flex; 5358justify-content: space-between; 5359gap: 12px; 5360margin-bottom: 6px; 5361} 5362#cart-kit-upsell-mount .cart-kit-upsell__progresstext, 5363#cart-kit-upsell-mount .cart-kit-upsell__progresspct { 5364font-size: 12px; 5365color: #555; 5366}
5367#cart-kit-upsell-mount .cart-kit-upsell__progressbar { 5368height: 10px; 5369border-radius: 999px; 5370background: #eef3f6; 5371overflow: hidden; 5372} 5373#cart-kit-upsell-mount .cart-kit-upsell__progressfill { 5374height: 100%; 5375background: var(--ck-success); 5376transition: width .18s ease; 5377width: 0%; 5378} 5379/* Success header state */ 5380#cart-kit-upsell-mount .cart-kit-upsell__header--success { 5381background: #f1faf3; 5382} 5383#cart-kit-upsell-mount .cart-kit-upsell__success-inline { 5384font-size: 14px; 5385color: var(--ck-success); 5386display: flex; 5387align-items: center; 5388gap: 8px; 5389font-weight: 600; 5390} 5391/* ========================================================= 5392Micro PDP Modal (iframe-based) 5393========================================================= */ 5394#cart-kit-upsell-mount .ck-micropdp-overlay { 5395position: fixed; 5396inset: 0; 5397background: rgba(0, 0, 0, .55); 5398display: none; 5399align-items: center; 5400justify-content: center; 5401z-index: 999999; 5402/* above cart + header */ 5403} 5404#cart-kit-upsell-mount .ck-micropdp-overlay.is-open { 5405display: flex; 5406} 5407#cart-kit-upsell-mount .ck-micropdp-modal { 5408width: min(960px, 92vw); 5409height: 96vh; 5410background: #fff; 5411border-radius: 12px; 5412overflow: hidden; 5413box-shadow: 0 12px 40px rgba(0, 0, 0, .35); 5414display: flex; 5415flex-direction: column; 5416} 5417@media (max-width: 600px) { 5418#cart-kit-upsell-mount .ck-micropdp-modal { 5419width: 96vw; 5420height: 98vh; 5421border-radius: 10px; 5422} 5423} 5424#cart-kit-upsell-mount .ck-micropdp-head { 5425display: flex; 5426flex-direction: column; 5427align-items: stretch; 5428justify-content: flex-start; 5429gap: 10px; 5430padding: 12px 14px; 5431border-bottom: 1px solid #e6e6e6; 5432background: #f7fbff; 5433} 5434#cart-kit-upsell-mount .ck-micropdp-head-top { 5435display: flex; 5436align-items: center; 5437justify-content: space-between; 5438gap: 12px; 5439width: 100%; 5440} 5441#cart-kit-upsell-mount .ck-micropdp-progress { 5442width: 100%; 5443display: flex; 5444flex-direction: column; 5445gap: 6px; 5446} 5447#cart-kit-upsell-mount .ck-micropdp-progressbar { 5448width: 100%; 5449height: 8px; 5450background: rgba(25, 118, 210, .14); 5451border-radius: 999px; 5452overflow: hidden; 5453} 5454#cart-kit-upsell-mount .ck-micropdp-progressfill { 5455height: 100%; 5456width: 0%; 5457background: var(--ck-accent); 5458border-radius: 999px; 5459transition: width .25s ease; 5460} 5461#cart-kit-upsell-mount .ck-micropdp-progresstext { 5462font-size: 12px; 5463color: #1b1b1b; 5464opacity: .85; 5465line-height: 1.2; 5466} 5467/* Micro PDP header progress â completion delight */ 5468#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progressfill { 5469background: var(--ck-success); 5470animation: ckMicroFillPulse .8s ease-in-out 0s 2; 5471} 5472#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progresstext { 5473font-weight: 700; 5474opacity: 1; 5475} 5476@keyframes ckMicroFillPulse { 54770% { 5478filter: brightness(1); 5479} 548050% { 5481filter: brightness(1.25); 5482} 5483100% { 5484filter: brightness(1); 5485} 5486} 5487#cart-kit-upsell-mount .ck-micropdp-title { 5488font-size: 16px; 5489font-weight: 700; 5490color: #1b1b1b; 5491margin: 0; 5492} 5493#cart-kit-upsell-mount .ck-micropdp-sub { 5494font-size: 12px; 5495color: #666; 5496margin-top: 2px; 5497} 5498#cart-kit-upsell-mount .ck-micropdp-actions { 5499display: flex; 5500gap: 8px; 5501align-items: center; 5502} 5503#cart-kit-upsell-mount .ck-micropdp-btn { 5504border: 1px solid #cfd8e3; 5505background: #fff; 5506color: #1b1b1b; 5507border-radius: 8px; 5508padding: 8px 10px; 5509cursor: pointer; 5510font-weight: 600; 5511font-size: 13px; 5512} 5513/* Loading cursor while micro PDP is submitting */ 5514body.ck-loading, 5515body.ck-loading * { 5516cursor: progress !important; 5517} 5518#cart-kit-upsell-mount .ck-micropdp-btn:hover { 5519background: #f2f5f9; 5520} 5521#cart-kit-upsell-mount .ck-micropdp-body { 5522flex: 1 1 auto; 5523position: relative; 5524} 5525#cart-kit-upsell-mount .ck-micropdp-iframe { 5526width: 100%; 5527height: 100%; 5528border: 0; 5529display: block; 5530} 5531#cart-kit-upsell-mount .ck-micropdp-loading { 5532position: absolute; 5533inset: 0; 5534display: flex; 5535align-items: center; 5536justify-content: center; 5537background: rgba(255, 255, 255, .85); 5538font-weight: 700; 5539color: #444; 5540} 5541.ck-debug-keywords { 5542margin-top: 4px; 5543font-size: 11px; 5544line-height: 1.4; 5545} 5546.ck-debug-keyword { 5547display: inline-block; 5548background: rgba(255, 255, 255, 0.08); 5549border: 1px solid rgba(255, 255, 255, 0.12); 5550border-radius: 999px; 5551padding: 2px 8px; 5552margin: 2px 6px 2px 0; 5553opacity: .95; 5554} 5555.ck-debug-keyword.muted, 5556.ck-debug-keywords.muted { 5557opacity: 0.55; 5558font-style: italic; 5559} 5560@media (max-width: 640px) {
5561#cart-kit-upsell-mount .cart-kit-upsell__title { 5562font-size: 16px; 5563} 5564} 5565@media (max-width: 520px) { 5566/* Accent chip mobile layout */ 5567#cart-kit-upsell-mount .kit-stamp-grid:not(.is-notary-grid) { 5568display: grid !important; 5569grid-template-columns: repeat(2, 1fr); 5570gap: 20px 12px; 5571justify-items: center; 5572} 5573#cart-kit-upsell-mount .kit-stamp-card { 5574width: 100%; 5575max-width: 160px; 5576} 5577#cart-kit-upsell-mount .kit-stamp-chip__circle { 5578width: 80px; 5579height: 80px; 5580} 5581#cart-kit-upsell-mount .kit-stamp-chip__image { 5582max-width: 62px; 5583max-height: 62px; 5584} 5585} </style>
5585<script> // ---- GA safety shim (prevents "gtag is not defined") ---- 5586window.dataLayer = window.dataLayer || []; 5587window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; 5588/** 5589* Cart Kit Upsell Module â Contract (Do Not Break) 5590* - Runs only on Cart page 5591* - Never blocks checkout interactions 5592* - Never modifies existing cart items (only adds new items) 5593* - Fails silently (no uncaught JS errors if PDP fetch/hydration fails) 5594* - Always has a fallback persona (generic) 5595* - Micro PDP is only used for SKUs that require custom options/uploads 5596*/ 5597document.addEventListener('DOMContentLoaded', function () { 5598// ---------------------------------------------------- 5599// 0) Render on Cart? 5600// ---------------------------------------------------- 5601// Only run SmartCart on the actual cart page. 5602// IMPORTANT: product pages and microPDP iframe pages often contain ".cart" elements (header/minicart), 5603// so the old detection was firing in the iframe and breaking. 5604var isCart = 5605document.body.classList.contains('checkout-cart-index') || 5606/\/checkout\/cart(\/|$)/.test(window.location.pathname); 5607if (!isCart) return; 5608// ---------------------------------------------------- 5609// 1) Mount BELOW .shoppingCartHeader-container 5610// ---------------------------------------------------- 5611// Zephyr cart is Tailwind markup (no #shopping-cart-table / .shoppingCartHeader-container). 5612// Mount after the "Shopping Cart" H1 header block; keep legacy anchors as fallback. 5613function ckFindZephyrHeader() { 5614var hs = document.querySelectorAll('h1'); 5615for (var i = 0; i < hs.length; i++) { 5616if (/cart/i.test(hs[i].textContent || '')) return hs[i].closest('.mb-6') || hs[i].parentElement; 5617} 5618return null; 5619} 5620var zHeader = ckFindZephyrHeader(); 5621var anchor = 5622document.querySelector('.shoppingCartHeader-container') || 5623zHeader; 5624var fallback = 5625(document.querySelector('#shopping-cart-table') && document.querySelector('#shopping-cart-table').parentElement) || 5626(zHeader && zHeader.parentElement) || 5627(document.querySelector('[data-cart-item-id]') && document.querySelector('[data-cart-item-id]').closest('.pb-32, main, #maincontent')) || 5628document.querySelector('.cart'); 5629if (!anchor && !fallback) return; 5630if (document.getElementById('cart-kit-upsell-mount')) return; 5631var mount = document.createElement('div'); 5632mount.id = 'cart-kit-upsell-mount'; 5633if (anchor && anchor.parentNode) anchor.insertAdjacentElement('afterend', mount); 5634else fallback.appendChild(mount); 5635// ---------------------------------------------------- 5636// 2) Query params 5637// ---------------------------------------------------- 5638var urlParams = new URLSearchParams(window.location.search); 5639var accentAddedQS = urlParams.get('accentAdded') === '1'; 5640var accentInCart = false; 5641var IS_ACCENT_EXPERIENCE = !!(accentAddedQS || accentInCart); 5642var accentAddMoreQS = urlParams.get('accentAddMore') === '1'; 5643var microAddedQS = urlParams.get('microAdded'); // productId 5644var microAddedNameQS = urlParams.get('microAddedName'); // product name (best effort) 5645// ===== SmartCart Persona Memory (Device-based) ===== 5646const SMARTCART_PERSONA_STORAGE_KEY = "smartcart_persona_v1"; 5647// --- Helpers --- 5648function nowMs() { return Date.now(); } 5649function safeJsonParse(str) { 5650try { return JSON.parse(str); } catch { return null; } 5651} 5652// --- Read / Write memory --- 5653function savePersonaToDevice(personaKey) { 5654if (!personaKey) return; 5655const payload = { 5656key: personaKey, 5657setAt: Date.now(), 5658version: 2 // bump version since behavior changed 5659}; 5660try { 5661localStorage.setItem( 5662SMARTCART_PERSONA_STORAGE_KEY, 5663JSON.stringify(payload) 5664); 5665} catch (e) { 5666// storage may be blocked; fail silently 5667} 5668} 5669function getPersonaFromDevice() { 5670let raw; 5671try { 5672raw = localStorage.getItem(SMARTCART_PERSONA_STORAGE_KEY); 5673} catch { 5674return null; 5675} 5676if (!raw) return null; 5677const payload = safeJsonParse(raw); 5678if (!payload?.key) return null; 5679return payload; 5680} 5681function clearPersonaFromDevice() { 5682try { localStorage.removeItem(SMARTCART_PERSONA_STORAGE_KEY); } catch { } 5683}
5684// --- GA user_property (set once per session/page lifecycle) --- 5685function setSmartCartUserPropertyOnce(personaKey) { 5686if (!personaKey) return; 5687if (window.__SMARTCART_USER_PROP_SET__) return; 5688window.__SMARTCART_USER_PROP_SET__ = true; 5689// If consent gating exists, you can wrap this (see section 4) 5690if (typeof gtag === "function") { 5691gtag("set", "user_properties", { smartcart_persona: personaKey }); 5692} 5693} 5694function onSmartCartMeaningfulEngagement(personaKey) { 5695if (!personaKey) return; 5696// Prevent repeated writes in one session/page lifecycle 5697if (window.__SMARTCART_ENGAGED__) return; 5698window.__SMARTCART_ENGAGED__ = true; 5699// Remember persona on device (UX) 5700savePersonaToDevice(personaKey); 5701// Attribute session in GA (analytics) 5702setSmartCartUserPropertyOnce(personaKey); 5703} 5704// ---------------------------------------------------- 5705// Debug exports (safe): expose helpers in console 5706// ---------------------------------------------------- 5707if (new URLSearchParams(location.search).get("sc_debug") === "1") { 5708window.__SMARTCART_DEBUG__ = window.__SMARTCART_DEBUG__ || {}; 5709window.__SMARTCART_DEBUG__.getPersonaFromDevice = getPersonaFromDevice; 5710window.__SMARTCART_DEBUG__.savePersonaToDevice = savePersonaToDevice; 5711window.__SMARTCART_DEBUG__.setSmartCartUserPropertyOnce = setSmartCartUserPropertyOnce; 5712window.__SMARTCART_DEBUG__.onSmartCartMeaningfulEngagement = onSmartCartMeaningfulEngagement; 5713} 5714// ---------------------------------------------------- 5715// 2.5) Hard SKU overrides (micro PDP required) 5716// ---------------------------------------------------- 5717var HARD_SKU_OVERRIDES = { 5718"2577": { addMode: "microPdp" }, 5719"4419": { addMode: "microPdp" }, 5720"10796": { addMode: "microPdp" } 5721}; 5722// ---------------------------------------------------- 5723// 3) PERSONA DETECTION (v2: Config-driven cart text scan) 5724// ---------------------------------------------------- 5725function loadPersonaConfigFromBlock() { 5726var el = document.getElementById('cart-kit-personas'); 5727if (!el) return null; 5728try { 5729var raw = (el.textContent || el.innerText || '').trim(); 5730if (!raw) return null; 5731return JSON.parse(raw); 5732} catch (e) { 5733console.warn('Cart Kit: Failed to parse persona JSON', e); 5734return null; 5735} 5736} 5737function buildCartSignals() { 5738// Adjust selectors to your cart markup if needed 5739var nameNodes = document.querySelectorAll('.product-name a, .product-name, .cart.item .product-item-name a, .cart.item .product-item-name'); 5740var optNodes = document.querySelectorAll('.item-options, dl.item-options, .product.options, .cart.item .item-options'); 5741var names = []; 5742for (var i = 0; i < nameNodes.length; i++) { 5743var t = (nameNodes[i].textContent || '').trim(); 5744if (t) names.push(t); 5745} 5746var opts = []; 5747for (var j = 0; j < optNodes.length; j++) { 5748var o = (optNodes[j].textContent || '').trim(); 5749if (o) opts.push(o); 5750} 5751var titleText = names.join(' | '); 5752var fullText = (names.concat(opts)).join(' | '); 5753return { titleText: titleText, fullText: fullText }; 5754} 5755// Product titles only (recommended for persona scoring) 5756function getCartTitleTextLower() { 5757try { 5758// Magento cart on SimplyStamps: h2.cartProductName a 5759var nameNodes = document.querySelectorAll( 5760'[data-cart-item-id] h3 a,' + 5761'[data-cart-item-id] h3,' + 5762'#shopping-cart-table h2.cartProductName a,' + 5763'#shopping-cart-table h2.cartProductName,' + 5764'#shopping-cart-table .product-name a,' + 5765'#shopping-cart-table .product-name,' + 5766'.product-item-name a,' + 5767'.product-item-name' 5768); 5769var names = []; 5770for (var i = 0; i < nameNodes.length; i++) { 5771var t = (nameNodes[i].textContent || '').trim(); 5772if (t) names.push(t); 5773} 5774return names.join(' | ').toLowerCase(); 5775} catch (e) { 5776return ''; 5777} 5778} 5779// Full cart text (titles + options). Keep for product scoring/debug if desired. 5780function getCartFullTextLower() { 5781try { 5782var titleLower = getCartTitleTextLower(); 5783var optNodes = document.querySelectorAll( 5784'[data-cart-item-id] .flex-wrap,' + 5785'#shopping-cart-table .item-options,' + 5786'#shopping-cart-table dl.item-options,' + 5787'.item-options,' + 5788'dl.item-options' 5789); 5790var opts = []; 5791for (var j = 0; j < optNodes.length; j++) { 5792var o = (optNodes[j].textContent || '').trim(); 5793if (o) opts.push(o); 5794} 5795// titleLower is already lowercase
5796var full = (titleLower ? [titleLower] : []).concat(opts.join(' | ').toLowerCase()).join(' | '); 5797return full.toLowerCase(); 5798} catch (e) { 5799return ''; 5800} 5801} 5802// ---------------------------------------------------- 5803// Cart product path signals (reliable for "already in cart" suppression) 5804// ---------------------------------------------------- 5805function normalizeUrlPathForCompare(url) { 5806try { 5807var u = new URL(url, window.location.origin); 5808var p = (u.pathname || ''); 5809// normalize: drop .html and trailing slashes 5810p = p.replace(/\.html$/i, '').replace(/\/+$/, ''); 5811return String(p || '').toLowerCase(); 5812} catch (e) { 5813try { 5814var s = String(url || '').split('#')[0].split('?')[0]; 5815s = s.replace(/\.html$/i, '').replace(/\/+$/, ''); 5816return String(s || '').toLowerCase(); 5817} catch (e2) { return ''; } 5818} 5819} 5820function getCartProductPathSet() { 5821var set = Object.create(null); 5822try { 5823var nodes = document.querySelectorAll( 5824'[data-cart-item-id] h3 a,' + 5825'#shopping-cart-table .product-item-name a,' + 5826'#shopping-cart-table a.product-item-link,' + 5827'.cart.table-wrapper .product-item-name a,' + 5828'.cart.table-wrapper a.product-item-link,' + 5829'.cart-container .product-item-name a,' + 5830'.cart-container a.product-item-link' 5831); 5832for (var i = 0; i < nodes.length; i++) { 5833var href = nodes[i] && (nodes[i].getAttribute('href') || nodes[i].href); 5834if (!href) continue; 5835var pth = normalizeUrlPathForCompare(href); 5836if (!pth) continue; 5837// Exclude cart actions/downloads that appear in the cart table 5838if (/^\/checkout\//i.test(pth)) continue; 5839if (/^\/sales\//i.test(pth)) continue; 5840if (/^\/download\//i.test(pth)) continue; 5841set[pth] = true; 5842} 5843} catch (e) { } 5844return set; 5845} 5846// Zephyr: exact product-id match from cart cards (data-product-id = Magento entity id) 5847function getCartProductIdSet() { 5848var set = Object.create(null); 5849try { 5850var nodes = document.querySelectorAll('[data-product-id]'); 5851for (var i = 0; i < nodes.length; i++) { 5852var pid = String(nodes[i].getAttribute('data-product-id') || '').trim(); 5853if (pid) set[pid] = true; 5854} 5855} catch (e) { } 5856return set; 5857} 5858// Compatibility alias used by existing scoring code 5859function getCartTextLower() { 5860// Persona scoring should use titles (cleanest signal) 5861return getCartTitleTextLower(); 5862// If you ever want âtitles + optionsâ instead, swap to: 5863// return getCartFullTextLower(); 5864} 5865// Spreadsheet-driven personas (local environment) 5866var PERSONAS_FALLBACK = [ 5867{ 5868key: 'brand_champions', 5869priority: 80, 5870match: [ 5871'business stamp', 'logo stamp', 'logo address stamp', 'custom logo stamp', 'company stamp', 'business address stamp', 5872'return address stamp business', 'self inking logo stamp', 'office branding', 'small business supplies', 5873'custom business stamps', 'bulk stamps' 5874], 5875headerTitle: 'Brand Essentials: finish your packaging strong.', 5876stampLabel: 'Business add-ons (popular picks)', 5877inkLabel: 'Choose an ink color for consistent branding', 5878tooltip: 'These add-ons help your brand look polished across packaging, mailers, and customer communications.', 5879defaultPrimaryIndex: 0, 5880tileSubtext: 'Popular with small businesses', 5881items: [ 5882{ productId: '2577', productUrl: 'https://www.simplystamps.com/qr-code-stamp-1-5-8-x-1-5-8', sortOrder: 2, addMode: 'microPdp' }, 5883{ productId: '4419', productUrl: 'https://www.simplystamps.com/medium-custom-logo-stamp', sortOrder: 1, addMode: 'microPdp' }, 5884{ productId: '10796', productUrl: 'https://www.simplystamps.com/mini-square-logo-stamp', sortOrder: 3, addMode: 'microPdp' } 5885] 5886}, 5887{ 5888key: 'home_sweet_stampers', 5889priority: 70, 5890match: [ 5891'return address stamp', 'address stamp', 'custom address stamp', 'family address stamp', 'wedding address stamp', 5892'holiday address stamp', 'mail stamp', 'envelope stamp', 'home address stamp', 'personalized address stamp' 5893], 5894headerTitle: 'Mail Essentials: everything you need for clean, easy stamping.', 5895stampLabel: 'Helpful add-ons for everyday mail', 5896inkLabel: 'Choose an ink color', 5897tooltip: 'Perfect for cards, envelopes, and seasonal mail â these extras make stamping easier and mess-free.', 5898defaultPrimaryIndex: 0, 5899tileSubtext: 'Great for everyday mail', 5900items: [ 5901{ productId: '5654', productUrl: 'https://www.simplystamps.com/merry-christmas-craft-rubber-stamp', sortOrder: 1 }, 5902{ productId: '6126', productUrl: 'https://www.simplystamps.com/happy-birthday-smiley-craft-stamp', sortOrder: 2 }, 5903{ productId: '5529', productUrl: 'https://www.simplystamps.com/live-well-rubber-stamp', sortOrder: 3 } 5904] 5905}, 5906{ 5907key: 'office_optimizers', 5908priority: 85, 5909match: [ 5910'signature', 'received stamp', 'approved stamp', 'paid stamp', 'office stamp', 'date stamp', 'custom office stamp', 5911'self inking stamp', 'pre inked stamp', 'inspection stamp', 'file stamp', 'reorder stamp', 'replacement ink' 5912], 5913headerTitle: 'Office Essentials: stay stocked and avoid reorders.', 5914stampLabel: 'Office add-ons (frequently reordered)', 5915inkLabel: 'Ink selection for daily office use', 5916tooltip: 'These supplies help keep workflows moving without interruptions or last-minute supply runs.', 5917defaultPrimaryIndex: 0, 5918tileSubtext: 'Frequently reordered', 5919items: [ 5920{ productId: '6560', productUrl: 'https://www.simplystamps.com/trodat-id-protection-stamp', sortOrder: 1 }, 5921{ productId: '2420', productUrl: 'https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip', sortOrder: 2 } 5922] 5923}, 5924{ 5925key: 'notary_ninjas', 5926priority: 100, 5927match: [ 5928'notary public', 'notary', 'notary stamp', 'notary seal', 'notary embosser', 'state notary stamp', 'notary supplies', 5929'notary kit', 'notary replacement stamp', 'official notary seal', 'notary expiration stamp', 5930'jurat stamp', 'acknowledgment stamp' 5931], 5932headerTitle: 'Notary Essentials: add backup ink & supplies before checkout.', 5933stampLabel: 'Notary add-ons (quick picks)', 5934inkLabel: 'Ink choice (recommended for notary work)', 5935tooltip: 'These items help ensure clean, compliant notarizations and prevent downtime if ink runs out.', 5936defaultPrimaryIndex: 0, 5937tileSubtext: 'Recommended for notary work', 5938items: [ 5939{ productId: '11712', productUrl: 'https://www.simplystamps.com/the-complete-notary-records-book', sortOrder: 1 }, 5940{ productId: '6524', productUrl: 'https://www.simplystamps.com/embossing-adhesive-foil', sortOrder: 2 }, 5941{ productId: '7986', productUrl: 'https://www.simplystamps.com/seal-impression-inker', sortOrder: 3 }, 5942{ productId: '13387', productUrl: 'https://www.simplystamps.com/embosser-vinyl-black-pouch', sortOrder: 4 } 5943] 5944}, 5945{ 5946key: 'classroom_heroes', 5947priority: 60, 5948match: [ 5949'teacher stamp', 'teacher ', 'grading stamp', 'school stamp', 'custom teacher stamp', 'classroom stamp', 5950'reward stamp', 'student feedback stamp', 'motivational stamp', 'education stamp', 'teacher name stamp' 5951], 5952headerTitle: 'Classroom Essentials: be ready for every lesson.', 5953stampLabel: 'Teacher add-ons (classroom favorites)', 5954inkLabel: 'Choose an ink color students will love', 5955tooltip: 'Extra ink and supplies help keep grading, rewards, and feedback flowing all year.', 5956defaultPrimaryIndex: 0, 5957tileSubtext: 'Classroom favorites', 5958items: [ 5959{ productId: '19215', productUrl: 'https://www.simplystamps.com/stacked-teacher-stamps-set-of-6', sortOrder: 1 }, 5960{ productId: '10965', productUrl: 'https://www.simplystamps.com/no-name-no-fame-owl-teacher-stamp', sortOrder: 2 }, 5961{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 3 } 5962] 5963}, 5964{
5965key: 'craft_crowd', 5966priority: 50, 5967match: [ 5968'craft stamp', 'handmade stamp', 'custom rubber stamp', 'craft rubber stamp', 'logo stamp small business', 'etsy stamp', 5969'thank you stamp', 'packaging stamp', 'product label stamp', 'arts and crafts stamp', 'creative stamp' 5970], 5971headerTitle: 'Creative Essentials: finish your projects with ease.', 5972stampLabel: 'Craft add-ons (popular with makers)', 5973inkLabel: 'Choose an ink color to match your style', 5974tooltip: 'These supplies are perfect for handmade goods, packaging, and creative projects.', 5975allowedInk: ['black', 'blue', 'red', 'green', 'purple'], 5976defaultPrimaryIndex: 0, 5977tileSubtext: 'Popular with makers', 5978items: [ 5979{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 1 }, 5980{ productId: '10464', productUrl: 'https://www.simplystamps.com/gem-cluster-stamp', sortOrder: 2 }, 5981{ productId: '9768', productUrl: 'https://www.simplystamps.com/seamless-eucalyptus-stamp', sortOrder: 3 } 5982] 5983}, 5984// Fallback (Accent Stamp grid) 5985{ 5986key: 'generic', 5987priority: 0, 5988match: [], 5989headerTitle: 'Choose your favorite accent stamp below and add to cart.', 5990stampLabel: 'Choose one or many Accent Stamp Designs:', 5991inkLabel: 'Would you like an Ink Pad? (Needed to make impression)', 5992tooltip: 'Stamp impression size: approx 1.75" Ã 0.67".', 5993allowedInk: ['black', 'blue', 'red', 'green', 'purple', 'none'], 5994defaultPrimaryIndex: 0, 5995tileSubtext: '', 5996items: [] 5997} 5998]; 5999function normalizePersonasConfigToArray(cfg) { 6000// Expect cfg like: { version: "...", personas: { key: { ... }, ... } } 6001if (!cfg || !cfg.personas || typeof cfg.personas !== 'object') return null; 6002var out = []; 6003Object.keys(cfg.personas).forEach(function (key) { 6004var p = cfg.personas[key] || {}; 6005// Map JSON schema â existing runtime schema 6006// Your runtime expects: key, priority, match, headerTitle, stampLabel, inkLabel, tooltip, allowedInk, defaultPrimaryIndex, tileSubtext, items[] 6007var persona = { 6008key: key, 6009priority: (typeof p.priority === 'number' ? p.priority : 0), 6010match: Array.isArray(p.match_keywords) ? p.match_keywords : [], 6011headerTitle: p.header_title || '', 6012stampLabel: p.stamp_label || '', 6013inkLabel: p.ink_label || '', 6014tooltip: p.tooltip_text || '', 6015allowedInk: Array.isArray(p.allowed_ink_colors) ? p.allowed_ink_colors : [], 6016defaultPrimaryIndex: (typeof p.default_primary_index === 'number' ? p.default_primary_index : 0), 6017tileSubtext: p.tile_subtext || '', 6018items: [] 6019}; 6020// Products â items (what buildGrid uses) 6021if (Array.isArray(p.products)) { 6022persona.items = p.products.map(function (prod) { 6023return { 6024productId: String(prod.product_id || '').trim(), 6025productUrl: prod.product_url || '', 6026sortOrder: (typeof prod.sort_order === 'number' ? prod.sort_order : 9999), 6027tileSubtext: prod.tile_subtext || '', 6028addMode: prod.addMode || prod.add_mode || undefined, 6029// NEW (traits) 6030role: prod.role || 'helper', 6031friction: prod.friction || 'low', 6032experimental: (typeof prod.experimental === 'boolean' ? prod.experimental : false), 6033// NEW (product-level keywords for scoring) 6034matchKeywords: Array.isArray(prod.match_keywords) ? prod.match_keywords : [] 6035}; 6036}).filter(function (it) { return !!it.productId || !!it.productUrl; }); 6037} 6038out.push(persona); 6039}); 6040return out; 6041} 6042var PERSONA_CONFIG = loadPersonaConfigFromBlock(); 6043// ---------------------------------------------------- 6044// 4) Now set PERSONAS like this (config â normalized array â fallback) 6045// ---------------------------------------------------- 6046var PERSONAS = (function () { 6047// Try config block first 6048var fromConfig = normalizePersonasConfigToArray(PERSONA_CONFIG); 6049// Use config if it produced at least 1 persona, otherwise fallback 6050var arr = (Array.isArray(fromConfig) && fromConfig.length) ? fromConfig : PERSONAS_FALLBACK.slice(); 6051// Ensure we ALWAYS have a generic fallback persona 6052var hasGeneric = arr.some(function (p) { return p && p.key === 'generic'; }); 6053if (!hasGeneric) { 6054var genericFallback = PERSONAS_FALLBACK.find(function (p) { return p.key === 'generic'; }); 6055if (genericFallback) arr.push(genericFallback); 6056} 6057// Normalize a few expected fields defensively 6058arr.forEach(function (p) { 6059if (!p) return; 6060if (!Array.isArray(p.match)) p.match = []; 6061if (!Array.isArray(p.items)) p.items = []; 6062if (!Array.isArray(p.allowedInk) || !p.allowedInk.length) { 6063// keep your existing default behavior (includes 'none') 6064p.allowedInk = ['black', 'blue', 'red', 'green', 'purple', 'none']; 6065} 6066}); 6067return arr; 6068})();
6069PERSONAS.forEach(function (p) { 6070if (Array.isArray(p.items)) p.items.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); }); 6071}); 6072PERSONAS.sort(function (a, b) { 6073if (a.key === 'generic') return 1; 6074if (b.key === 'generic') return -1; 6075return (b.priority || 0) - (a.priority || 0); 6076}); 6077function setLoadingCursor(on) { 6078document.body.classList.toggle('ck-loading', !!on); 6079} 6080// ---------------------------------------------------- 6081// 3.9) SCORING (persona + product) 6082// ---------------------------------------------------- 6083function keywordMatchScore(keywords, haystackLower) { 6084if (!Array.isArray(keywords) || !keywords.length) return { score: 0, hits: 0, matched: [] }; 6085var score = 0, hits = 0, matched = []; 6086for (var i = 0; i < keywords.length; i++) { 6087var k = String(keywords[i] || '').toLowerCase().trim(); 6088if (!k) continue; 6089// Exact substring match 6090if (haystackLower.indexOf(k) > -1) { 6091score += (k.indexOf(' ') > -1 ? 10 : 4); 6092hits += 1; 6093matched.push(k); 6094continue; 6095} 6096// Phrase fallback: all tokens appear anywhere 6097if (k.indexOf(' ') > -1) { 6098var parts = k.split(/\s+/).filter(Boolean); 6099if (parts.length >= 2) { 6100var all = true; 6101for (var p = 0; p < parts.length; p++) { 6102if (haystackLower.indexOf(parts[p]) === -1) { all = false; break; } 6103} 6104if (all) { 6105score += 6; 6106hits += 1; 6107matched.push(k + ' (tokens)'); 6108} 6109} 6110} 6111} 6112return { score: score, hits: hits, matched: matched }; 6113} 6114function scoreKeywordHits(keywords, haystackLower) { 6115return keywordMatchScore(keywords, haystackLower).score; 6116} 6117function scorePersonaFromCart(personaObj, cartTextLower) { 6118// Persona match keywords are in personaObj.match[] 6119var base = scoreKeywordHits(personaObj.match, cartTextLower); 6120// Priority is a tie-breaker, not a primary driver. 6121// Still, give a tiny nudge so higher-priority personas win ties. 6122var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0; 6123return base + priorityNudge; 6124} 6125function detectPersonaKey() { 6126var cartTextLower = getCartTitleTextLower(); 6127var bestKey = 'generic'; 6128var bestScore = -1; 6129for (var i = 0; i < PERSONAS.length; i++) { 6130var p = PERSONAS[i]; 6131if (!p || p.key === 'generic') continue;
6132var s = scorePersonaFromCart(p, cartTextLower); 6133if (s > bestScore) { 6134bestScore = s; 6135bestKey = p.key; 6136} 6137} 6138// If nobody matched anything, fall back 6139if (bestScore <= 0) return 'generic'; 6140return bestKey; 6141} 6142function getPersonaByKey(key) { 6143for (var i = 0; i < PERSONAS.length; i++) { 6144if (PERSONAS[i].key === key) return PERSONAS[i]; 6145} 6146return PERSONAS[PERSONAS.length - 1]; 6147} 6148var activePersonaKey = detectPersonaKey(); 6149var persona = getPersonaByKey(activePersonaKey); 6150window.__CK = { 6151personas: PERSONAS, 6152activePersonaKey: activePersonaKey, 6153persona: persona 6154}; 6155var useTiles = Array.isArray(persona.items) && persona.items.length > 0; 6156// Decide which persona to use for this render 6157function choosePersona({ detectedKey, detectedConfidence = 0, confidenceThreshold = 0.75 }) { 6158const stored = getPersonaFromDevice(); // { key, setAt... } or null 6159// Strong real-time signal wins 6160if (detectedKey && detectedConfidence >= confidenceThreshold) { 6161return { key: detectedKey, source: "detected_strong" }; 6162} 6163// Otherwise fall back to remembered persona if available 6164if (stored?.key) { 6165return { key: stored.key, source: "stored" }; 6166} 6167// Otherwise use detected (weak) or default 6168if (detectedKey) return { key: detectedKey, source: "detected_weak" }; 6169return { key: "default", source: "default" }; 6170} 6171// ---------------------------------------------------- 6172// 3b) Analytics helper (optional) 6173// ---------------------------------------------------- 6174function pushUpsellEvent(eventName, payload) { 6175payload = payload || {}; 6176// ---------------------------------------------------- 6177// Enrich payload persona context so GA4 can segment 6178// ---------------------------------------------------- 6179try { 6180var __ckPersonaKey = (typeof activePersonaKey !== 'undefined' && activePersonaKey) ? String(activePersonaKey) : 'generic'; 6181// Prefer persona.headerTitle as a human label, fall back to key 6182var __ckPersonaLabel = (typeof persona !== 'undefined' && persona && (persona.headerTitle || persona.key)) 6183? String(persona.headerTitle || persona.key) 6184: __ckPersonaKey; 6185// Only set defaults if caller didn't provide them 6186if (typeof payload.persona_key === 'undefined') payload.persona_key = __ckPersonaKey; 6187if (typeof payload.persona_label === 'undefined') payload.persona_label = __ckPersonaLabel; 6188if (typeof payload.upsell_variant === 'undefined') payload.upsell_variant = (typeof useTiles !== 'undefined' && useTiles) ? 'tiles' : 'stamps'; 6189if (typeof payload.page_path === 'undefined') payload.page_path = (window.location && window.location.pathname) ? window.location.pathname : ''; 6190} catch (eCtx) { } 6191// ---------------------------------------------------- 6192// Option 4: Meaningful engagement â remember persona + set GA user_property 6193// Trigger only on specific SmartCart events (no impressions) 6194// ---------------------------------------------------- 6195try { 6196var isMeaningful = 6197eventName === 'SmartCart_upsell_tile_selected' || 6198eventName === 'SmartCart_upsell_tile_click' || 6199eventName === 'SmartCart_upsell_tile_add' || 6200eventName === 'SmartCart_upsell_tile_added' || 6201eventName === 'SmartCart_upsell_added'; 6202if (isMeaningful) { 6203var pk = 6204(payload && (payload.persona_key || payload.personaKey)) || 6205(typeof activePersonaKey !== 'undefined' && activePersonaKey) || 6206(window.__CK && window.__CK.activePersonaKey); 6207if (pk) onSmartCartMeaningfulEngagement(String(pk)); 6208} 6209} catch (e) { } 6210// Always push to dataLayer (never depend on gtag existing) 6211try { 6212window.dataLayer = window.dataLayer || []; 6213window.dataLayer.push(Object.assign({ event: eventName }, payload)); 6214} catch (e) { } 6215// Optional: also send to gtag if it exists (but NEVER crash if missing) 6216try { 6217if (typeof window.gtag === 'function') { 6218window.gtag('event', eventName, payload); 6219} 6220} catch (e) { } 6221// Mirror to GA4 standard ecommerce (see CK_GA4 block below). Never 6222// let an analytics mirror break the module. 6223try { ckGaMirror(eventName, payload); } catch (eGa) { } 6224}
6225// ---------------------------------------------------- 6226// 3b-ii) GA4 standard ecommerce mirror [added 2026-09-15] 6227// ---------------------------------------------------- 6228// WHY: the SmartCart_* custom events carry product ids only inside a 6229// nested array (tiles_shown), and GA4 cannot report on nested arrays. 6230// The property has no custom dimension for productId, so per-product 6231// performance has been invisible since launch. GA4's `item_id` is a 6232// BUILT-IN dimension, so emitting standard ecommerce events gives us a 6233// per-product view -> select -> add funnel with zero custom-dimension 6234// setup. These are emitted ALONGSIDE the custom events, never instead. 6235// 6236// dataLayer only -- GTM is confirmed live on this page and already 6237// processing SmartCart pushes. We do not call gtag() here because line 6238// ~1015 stubs window.gtag when it is absent, so a gtag() call would be 6239// an unconditional duplicate push. 6240var CK_GA4 = { 6241EMIT: true, // master kill switch 6242EMIT_MICRO_ADD: true, // set false if the micro-PDP iframe fires its own add_to_cart 6243LIST_PREFIX: 'SmartCart', 6244HYDRATE_POLLS: 8, // 8 x 300ms = up to ~2.4s waiting for tile names 6245HYDRATE_POLL_MS: 300, 6246// SKUs for products that never go through parsePdpDoc, so nothing can 6247// hydrate a SKU for them: the ink parent (added by id + options) and 6248// the six accent stamps used by the non-tile 'generic' persona, where 6249// buildGrid returns before any PDP fetch. Without these, every add in 6250// the generic variant -- the bucket every unmatched cart falls into -- 6251// would ship an entity id and be unjoinable to purchase. 6252// All verified against prod 2026-09-16. Re-check if SKUs change. 6253STATIC_SKUS: { 6254'3631': '1009260', // Ideal Ink Pad for Traditional Wood Handle 6255'23105': '1027259', // Delicate Flower envelope accent 6256'23106': '1027260', // Simple botanical branches 6257'23107': '1027261', // (accent) 6258'23108': '1027262', // Elegant botanical branch 6259'23110': '1027264', // Hearts through arrow 6260'23113': '1027267' // Outlined heart 6261} 6262}; 6263// A SKU we are willing to put in item_id. Magento SKUs are short and 6264// token-like; a scrape that picked up a wrapper element's textContent 6265// ('SKU:\n 1012837') or a sentinel ('N/A') must not ship -- and must not 6266// be labelled id_source:'sku', which would hide it from the very 6267// diagnostic added to catch it. 6268function ckGaSkuOk(v) { 6269var s = ckGaValidId(v); 6270if (!s) return ''; 6271if (s.length > 40) return ''; 6272return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(s) ? s : ''; 6273} 6274// ckGaItem records, per built item, whether item_id is a real SKU or the 6275// entity-id fallback. ckGaPush reads and clears it -- the whole mirror runs 6276// synchronously inside one ckGaMirror call, so there is no interleaving. 6277var CK_GA4_IDSRC = []; 6278// Analytics-only mirror of tile metadata for products that entered a slot 6279// after buildGrid (swaps). Read by ckGaItem, by nothing else -- writing 6280// these into TILE_ITEM_BY_ID would change add-flow routing. 6281var CK_GA_TILE_META = Object.create(null); 6282// Reject ids that are not real product ids. TILE_ITEMS tolerates entries 6283// with no productId, and String(undefined) === 'undefined' would ship a 6284// junk item_id that also never hydrates (stalling the impression poll). 6285function ckGaValidId(v) { 6286if (v === null || typeof v === 'undefined') return ''; 6287var s = String(v).trim(); 6288if (!s || s === 'undefined' || s === 'null' || s === 'NaN') return ''; 6289return s; 6290} 6291function ckGaListId() { 6292try { 6293return String((typeof activePersonaKey !== 'undefined' && activePersonaKey) || 'generic'); 6294} catch (e) { return 'generic'; } 6295} 6296function ckGaListName() { 6297var label = ckGaListId(); 6298try { 6299if (typeof persona !== 'undefined' && persona && persona.headerTitle) { 6300label = String(persona.headerTitle); 6301} 6302} catch (e) { } 6303// GA4 truncates item_list_name at 100 chars; header_title is 6304// CMS-editable, so cap it here rather than letting GA4 split rows. 6305return (CK_GA4.LIST_PREFIX + ' - ' + label).slice(0, 100); 6306} 6307function ckGaPrice(v) { 6308if (v === null || typeof v === 'undefined') return undefined; 6309var n = parseFloat(String(v).replace(/[^0-9.\-]/g, '')); 6310return isFinite(n) ? Math.round(n * 100) / 100 : undefined; 6311} 6312// Build one GA4 item object for a SmartCart product id. 6313function ckGaItem(pid, index, listNameOverride) { 6314var id = String(pid);
6315var raw = null, hyd = null; 6316try { 6317if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null; 6318if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null; 6319} catch (e1) { } 6320try { 6321if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd = NOTARY_HYDRATED.get(id) || null; 6322} catch (e2) { } 6323// SKU is the id GA4 must see (see parsePdpDoc). Fall back to the 6324// entity id only when hydration has not produced a SKU yet -- that is 6325// recoverable and countable via the id_source param on the event, 6326// whereas dropping the item outright would lose the conversion. 6327var gaSku = ckGaSkuOk(hyd && hyd.sku) || 6328ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id]); 6329var gaId = gaSku || id; 6330try { CK_GA4_IDSRC.push(gaSku ? 'sku' : 'entity'); } catch (eSrc0) { } 6331var item = { 6332item_id: gaId, 6333item_brand: 'SimplyStamps', 6334item_category: 'SmartCart Upsell', 6335item_list_id: ckGaListId(), 6336// Item-scoped item_list_name is what GA4 item reports actually 6337// use, so an event-level override has to reach the items too. 6338item_list_name: listNameOverride || ckGaListName(), 6339quantity: 1 6340}; 6341// NEVER send a placeholder name. Tiles hydrate asynchronously, so a 6342// 'Product 12345' filler would permanently split the item_name 6343// dimension against the real name sent by later events. An absent 6344// dimension is recoverable; a wrong one is not. 6345var name = (hyd && hyd.name) || (raw && (raw.title || raw.name)) || ''; 6346if (name) item.item_name = String(name); 6347if (typeof index === 'number' && index > 0) item.index = index; 6348// Price: hydrated PDP price for tiles; the flat accent-stamp price 6349// in the non-tile variant, where nothing hydrates at all. 6350var price = ckGaPrice(hyd ? hyd.price : null); 6351if (typeof price !== 'number') { 6352try { 6353if (typeof useTiles !== 'undefined' && !useTiles && typeof STAMP_UNIT_PRICE === 'number') { 6354price = STAMP_UNIT_PRICE; 6355} 6356} catch (eP) { } 6357} 6358if (typeof price === 'number') item.price = price; 6359// Slice-able strategy attributes, so GA4 can answer 'do experimental 6360// tiles convert?' and 'does friction cost us adds?' without new dims. 6361// Only set what the config actually declares -- defaulting friction to 6362// 'low' would read back as 'low friction converts fine' off data that 6363// really means 'we did not know'. 6364if (raw) { 6365if (raw.role) item.item_variant = String(raw.role); 6366if (raw.friction) item.item_category2 = String(raw.friction); 6367if (typeof raw.experimental !== 'undefined') { 6368item.item_category3 = raw.experimental ? 'experimental' : 'core'; 6369} 6370} 6371return item; 6372} 6373// startIndex === null means 'do not emit index'. Only view_item_list 6374// knows a real display position; on select_item / add_to_cart an index 6375// would be fabricated (always 1, or add-order), and a fabricated 6376// position silently answers 'which slot converts?' wrong. 6377function ckGaItemsFor(ids, startIndex, listNameOverride) { 6378var out = []; 6379var useIdx = (startIndex !== null && typeof startIndex !== 'undefined'); 6380try { 6381(ids || []).forEach(function (pid) { 6382var id = ckGaValidId(pid); 6383if (!id) return; 6384out.push(ckGaItem(id, useIdx ? (startIndex + out.length + 1) : null, listNameOverride)); 6385}); 6386} catch (e) { } 6387return out; 6388} 6389// True once we can give this product the id GA4 needs. Gating the deferred 6390// impression on the NAME alone was not enough: ckRepaintCardAs writes a 6391// CK_TILE_FALLBACKS title into TILE_ITEM_BY_ID synchronously, before it even 6392// starts the PDP fetch, so a swapped-in fallback tile looked 'resolved' with 6393// no SKU yet -- view_item_list would ship its entity id while the shopper's 6394// later select_item shipped the real SKU. Same product, two item_ids, one 6395// session: exactly the split this is all meant to remove. 6396function ckGaHasSku(pid) { 6397var id = String(pid); 6398if (ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id])) return true; 6399try { 6400if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) { 6401var h = NOTARY_HYDRATED.get(id); 6402if (h && ckGaSkuOk(h.sku)) return true; 6403} 6404} catch (e) { } 6405return false; 6406} 6407// True once a product has a real, non-placeholder name available. 6408function ckGaHasName(pid) { 6409var id = String(pid), hyd = null, raw = null; 6410try { if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd =
6410NOTARY_HYDRATED.get(id) || null; } catch (e1) { } 6411try { 6412if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null; 6413if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null; 6414} catch (e2) { } 6415return !!((hyd && hyd.name) || (raw && (raw.title || raw.name))); 6416} 6417// Ink pads are options on a single parent SKU, not tiles -- model them as 6418// variants of that parent so ink attach is measurable too. 6419function ckGaInkItems(colorKeys, startIndex) { 6420var out = []; 6421try { 6422// The ink parent never hydrates through parsePdpDoc (it is added 6423// by id + options, not from a tile), so its SKU is pinned here. 6424var inkEnt = (typeof INK_PARENT_ID !== 'undefined' && INK_PARENT_ID) ? String(INK_PARENT_ID) : ''; 6425var inkSku = ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[inkEnt]); 6426var pid = inkSku || inkEnt || 'ink-pad'; 6427(colorKeys || []).forEach(function (ck, i) { 6428var row = { 6429item_id: pid, 6430item_name: 'Ink Pad', 6431item_brand: 'SimplyStamps', 6432item_category: 'SmartCart Upsell', 6433item_category3: 'core', 6434item_variant: String(ck), 6435item_list_id: ckGaListId(), 6436item_list_name: ckGaListName(), 6437quantity: 1 6438}; 6439// Ink is a flat-price option the module already prices itself. 6440// Without this, an ink-only add -- the module's core ask -- 6441// reports add_to_cart with no value at all. 6442if (typeof INK_PRICE === 'number') row.price = INK_PRICE; 6443if (startIndex !== null && typeof startIndex !== 'undefined') row.index = startIndex + i + 1; 6444// Report ink's id source too, or a mixed add reads as 'entity' 6445// and an analyst quarantining that label throws away good rows. 6446try { CK_GA4_IDSRC.push(inkSku ? 'sku' : 'entity'); } catch (eSrc1) { } 6447out.push(row); 6448}); 6449} catch (e) { } 6450return out; 6451} 6452function ckGaValue(items) { 6453var v = 0, any = false; 6454try { 6455(items || []).forEach(function (it) { 6456if (typeof it.price === 'number') { v += it.price * (it.quantity || 1); any = true; } 6457}); 6458} catch (e) { } 6459return any ? Math.round(v * 100) / 100 : undefined; 6460} 6461function ckGaPush(eventName, ecommerce) { 6462// Drain the id-source buffer FIRST, above every early return -- a 6463// stale entry would mislabel the next event. 6464var idsrc = CK_GA4_IDSRC; 6465CK_GA4_IDSRC = []; 6466if (!CK_GA4.EMIT) return; 6467if (!ecommerce || !Array.isArray(ecommerce.items) || !ecommerce.items.length) return; 6468try { 6469window.dataLayer = window.dataLayer || []; 6470// Clear the previous ecommerce object first so params never bleed 6471// between events (standard GTM ecommerce practice). 6472window.dataLayer.push({ ecommerce: null }); 6473// id_source tells the analyst whether these item_ids are real SKUs 6474// (joinable to purchase) or entity-id fallbacks from a tile that 6475// had not hydrated yet. Ink items are always pinned to a real SKU. 6476var srcTag = 'sku'; 6477try { 6478if (idsrc.length) { 6479var nEnt = idsrc.filter(function (x) { return x === 'entity'; }).length; 6480srcTag = (nEnt === 0) ? 'sku' : (nEnt === idsrc.length ? 'entity' : 'mixed'); 6481} 6482} catch (eSrc) { } 6483window.dataLayer.push({ 6484event: eventName, 6485smartcart: true, 6486persona_key: ckGaListId(), 6487id_source: srcTag, 6488ecommerce: ecommerce 6489}); 6490} catch (e) { } 6491} 6492// Single mirror point: called for every pushUpsellEvent. Adding a new 6493// GA4 event means adding a case here, not touching call sites. 6494function ckGaMirror(eventName, payload) { 6495if (!CK_GA4.EMIT) return; 6496payload = payload || {}; 6497var items, val, ec; 6498if (eventName === 'SmartCart_upsell_impression') { 6499// null = read the live grid at fire time. See 6500// ckGaFireListWhenHydrated for why that beats the build-time list. 6501ckGaFireListWhenHydrated(null); 6502return; 6503} 6504// Three distinct code paths end in a real selection. _select_fallback 6505// fires when the PDP prefetch failed but the tile WAS still selected, 6506// so omitting it would under-report select_item in proportion to our 6507// own fetch error rate. accent_selected is the non-tile variant. 6508if (eventName === 'SmartCart_upsell_tile_select_fallback' && payload.selected === false) { 6509return; // that path fires on deselect too 6510} 6511if (eventName === 'SmartCart_upsell_tile_selected' || 6512eventName === 'SmartCart_upsell_tile_select_fallback' || 6513eventName === 'SmartCart_upsell_accent_selected') { 6514var selId = payload.productId || payload.product_id || payload.stamp_id; 6515items = ckGaItemsFor([selId], null); 6516ckGaPush('select_item', { 6517item_list_id: ckGaListId(), 6518item_list_name: ckGaListName(), 6519items: items 6520}); 6521return; 6522} 6523if (eventName === 'SmartCart_upsell_added') { 6524items = ckGaItemsFor(payload.product_ids, null) 6525.concat(ckGaInkItems(payload.ink_colors, null)); 6526val = ckGaValue(items); 6527ec = { currency: 'USD', items: items }; 6528if (typeof val === 'number') ec.value = val; 6529ckGaPush('add_to_cart', ec); 6530return; 6531} 6532if (eventName === 'SmartCart_upsell_micro_added') { 6533if (!CK_GA4.EMIT_MICRO_ADD) return; 6534items = ckGaItemsFor(payload.product_ids, null); 6535val = ckGaValue(items); 6536ec = { currency: 'USD', items: items }; 6537if (typeof val === 'number') ec.value = val; 6538ckGaPush('add_to_cart', ec); 6539return; 6540} 6541} 6542// Read the product ids actually rendered in the grid right now. 6543function ckGaVisibleTileIds() { 6544var out = []; 6545try { 6546if (!stampGrid) return out; 6547var cards = stampGrid.querySelectorAll('[data-kit-primary]'); 6548for (var i = 0; i < cards.length; i++) { 6549var id = ckGaValidId(cards[i].getAttribute('data-kit-primary')); 6550if (id && out.indexOf(id) === -1) out.push(id); 6551} 6552} catch (e) { } 6553return out; 6554} 6555// view_item_list is deliberately NOT fired inline with the impression. 6556// 6557// Two reasons. (1) fireTilesImpressionOnce() runs synchronously right 6558// after the hydration fetches are *started*, so at that instant no tile
6559// has a real name or price -- firing then sends an id-only list whose 6560// item_name never matches the select_item / add_to_cart that follow, 6561// splitting every per-product report in two. (2) the tile quality gate 6562// drops or repaints tiles during those same milliseconds, so the 6563// build-time id list is not what the shopper ends up seeing. 6564// 6565// So: pass ids = null to re-read the LIVE grid on every poll, wait for 6566// names, then fire once. On timeout we still fire -- an id-only 6567// impression beats no impression at all, and a stalled poll would let 6568// select_item land before view_item_list and break funnel step order. 6569function ckGaFireListWhenHydrated(ids, attempts, listNameOverride) { 6570if (!CK_GA4.EMIT) return; 6571try { 6572var live = (ids === null || typeof ids === 'undefined'); 6573var list = live ? ckGaVisibleTileIds() 6574: (ids || []).map(ckGaValidId).filter(function (x) { return !!x; }); 6575if (!list.length) return; 6576attempts = (typeof attempts === 'number') ? attempts : CK_GA4.HYDRATE_POLLS; 6577var resolved = 0; 6578list.forEach(function (id) { if (ckGaHasName(id) && ckGaHasSku(id)) resolved++; }); 6579if (resolved < list.length && attempts > 0) { 6580setTimeout(function () { 6581ckGaFireListWhenHydrated(ids, attempts - 1, listNameOverride); 6582}, CK_GA4.HYDRATE_POLL_MS); 6583return; 6584} 6585ckGaPush('view_item_list', { 6586item_list_id: ckGaListId(), 6587item_list_name: listNameOverride || ckGaListName(), 6588items: ckGaItemsFor(list, 0, listNameOverride) 6589}); 6590} catch (e) { } 6591} 6592function fireTilesImpressionOnce() { 6593try { 6594if (!useTiles) return; 6595if (!Array.isArray(TILE_ITEMS) || !TILE_ITEMS.length) return; 6596// fire once per page load per persona 6597window.__ckImpressionFired = window.__ckImpressionFired || {}; 6598var key = String(activePersonaKey || 'generic'); 6599if (window.__ckImpressionFired[key]) return; 6600window.__ckImpressionFired[key] = true; 6601pushUpsellEvent('SmartCart_upsell_impression', { 6602// Flat id list -- GA4 cannot report on the nested tiles_shown 6603// array; ckGaMirror() turns this into view_item_list items[]. 6604product_ids: TILE_ITEMS.map(function (t) { return String(t.productId); }), 6605tiles_shown: TILE_ITEMS.map(function (t, idx) { 6606return { 6607productId: t.productId, 6608position: idx + 1, 6609experimental: !!t.experimental, 6610role: t.role || 'helper', 6611friction: t.friction || 'low' 6612}; 6613}) 6614}); 6615} catch (e) { } 6616} 6617function pushTileEvent(action, payload) { 6618payload = payload || {}; 6619pushUpsellEvent('SmartCart_upsell_tile_' + action, payload); 6620const personaKey = 6621payload.persona_key || 6622payload.personaKey || 6623(window.__CK && window.__CK.activePersonaKey) || 6624window.activePersonaKey; 6625if ( 6626personaKey && 6627(action === 'view' || action === 'in_view' || action === 'click' || action === 'add' || action === 'added' || action === 'selected') 6628) { 6629onSmartCartMeaningfulEngagement(personaKey); 6630} 6631} 6632// ---------------------------------------------------- 6633// 4) CONFIG 6634// ---------------------------------------------------- 6635// Ink pad parent product + options 6636var INK_PARENT_ID = '3631'; 6637var INK_SIZE_OPTION_ID = '1296'; // Size 6638var INK_COLOR_OPTION_ID = '1295'; // Ink Pad Color 6639var INK_SIZE_VALUE_ID = '6265'; // Small 6640var INK_COLOR_VALUES = { 6641black: '6257', 6642blue: '6258', 6643red: '6259', 6644green: '6260', 6645purple: '6262' 6646}; 6647// Accent stamp options 6648var ACCENT_SIZE_OPTION_ID = '1453'; 6649var ACCENT_SIZE_VALUE_ID = '6921'; 6650var ACCENT_INK_OPTION_ID = '2403'; 6651var ACCENT_INK_NO_THANKS_VALUE_ID = '9532'; 6652// Pricing model for Accent stamps (existing behavior) 6653var STAMP_UNIT_PRICE = 5.49; 6654var INK_PRICE = 4.99; 6655var STAMP_OPTIONS = [ 6656{ 6657id: '23105', 6658imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-1_800.jpg', 6659imgAlt: 'Delicate flower envelope accent craft stamp design' 6660}, 6661{ 6662id: '23106', 6663imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-2_800.jpg', 6664imgAlt: 'Simple botanical branches envelope accent craft stamp design' 6665}, 6666{ 6667id: '23108', 6668imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-4_800.jpg', 6669imgAlt: 'Elegant botanical branch envelope accent craft stamp design' 6670}, 6671{ 6672id: '23107', 6673imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-3_800.jpg', 6674imgAlt: 'Daisy flower envelope accent craft stamp design' 6675}, 6676{ 6677id: '23110', 6678imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-arrow_800.jpg', 6679imgAlt: 'Hearts through arrow envelope accent craft stamp design' 6680}, 6681{ 6682id: '23113', 6683imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-2_800.jpg', 6684imgAlt: 'Outlined heart envelope accent craft stamp design' 6685} 6686]; 6687var MAX_TILES_TO_SHOW = 6; // guardrail 6688var ALLOW_EXPERIMENTAL_TILES = true; // easy toggle 6689// Persona tile items (simple products). If none, we show Accent Stamp grid. 6690// Uses product-level matchKeywords + role/friction modifiers to pick the best N (MAX_TILES_TO_SHOW). 6691function scoreProductForCart(item, cartTextLower, allowExperimental) { 6692var score = 0; 6693// Keyword hits (product-level) 6694score += scoreKeywordHits(item.matchKeywords, cartTextLower); 6695// Role modifiers 6696switch (item.role) {
6697case 'anchor': score += 6; break; 6698case 'core': score += 4; break; 6699case 'compliance': score += 8; break; 6700case 'backup': score += 2; break; 6701case 'accessory': score += 1; break; 6702default: break; 6703} 6704// Friction modifiers 6705switch (item.friction) { 6706case 'none': score += 3; break; 6707case 'low': score += 1; break; 6708case 'medium': score -= 2; break; 6709case 'high': score -= 5; break; 6710default: break; 6711} 6712// Experimental gating 6713if (item.experimental && !allowExperimental) score -= 100; 6714return score; 6715} 6716function pickTopTilesForDisplay(items, cartTextLower, maxTiles, allowExperimental) { 6717var pool = Array.isArray(items) ? items.slice() : []; 6718// Ensure fields exist 6719pool.forEach(function (it) { 6720if (!it) return; 6721if (!Array.isArray(it.matchKeywords)) it.matchKeywords = []; 6722if (!it.role) it.role = 'helper'; 6723if (!it.friction) it.friction = 'low'; 6724}); 6725// Score + stable tiebreakers 6726var scored = pool.map(function (it, idx) { 6727return { 6728it: it, 6729idx: idx, 6730score: scoreProductForCart(it, cartTextLower, allowExperimental) 6731}; 6732}); 6733scored.sort(function (a, b) { 6734if (b.score !== a.score) return b.score - a.score; 6735// If tie: prefer anchors 6736var ar = (a.it.role === 'anchor') ? 1 : 0; 6737var br = (b.it.role === 'anchor') ? 1 : 0; 6738if (br !== ar) return br - ar; 6739// Then lower friction 6740var fr = { none: 0, low: 1, medium: 2, high: 3 }; 6741var af = fr[a.it.friction] != null ? fr[a.it.friction] : 9; 6742var bf = fr[b.it.friction] != null ? fr[b.it.friction] : 9; 6743if (af !== bf) return af - bf; 6744// Then sortOrder 6745return (a.it.sortOrder || 0) - (b.it.sortOrder || 0); 6746}); 6747// Dedupe by product id: the pool can carry the same product under two 6748// entries, and rendering it twice reads as a bug. 6749var seenPid = Object.create(null); 6750var selected = []; 6751for (var si = 0; si < scored.length && selected.length < maxTiles; si++) { 6752var cand = scored[si].it; 6753var cpid = String((cand && (cand.productId || cand.product_id)) || ''); 6754if (cpid) { 6755if (seenPid[cpid]) continue; 6756seenPid[cpid] = true; 6757} 6758selected.push(cand); 6759} 6760// Default index: highest score, but if an anchor is within 2 points of top, prefer it. 6761var defaultIndex = 0; 6762if (selected.length) { 6763var topScore = scored.length ? scored[0].score : 0; 6764for (var i = 0; i < selected.length; i++) { 6765var it = selected[i]; 6766var itScore = scoreProductForCart(it, cartTextLower, allowExperimental); 6767if (it.role === 'anchor' && itScore >= (topScore - 2)) { 6768defaultIndex = i; 6769break; 6770} 6771} 6772} 6773return { tiles: selected, defaultIndex: defaultIndex }; 6774} 6775// Respect ?upsellDebug=1 to include experimental tiles 6776var allowExperimentalNow = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1'); 6777var cartTextForScoring = getCartTitleTextLower(); 6778var tilePick = (useTiles ? pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow) : { tiles: [], defaultIndex: 0 }); 6779// Best-effort: suppress tiles that are already in the cart using cart link paths (most reliable signal we have on cart) 6780var __cartPaths0 = Object.create(null); 6781try { __cartPaths0 = getCartProductPathSet(); } catch (eCP0) { __cartPaths0 = Object.create(null); } 6782var __cartIds0 = Object.create(null); 6783try { __cartIds0 = getCartProductIdSet(); } catch (eCI0) { __cartIds0 = Object.create(null); } 6784function __isInCartByPath(it) { 6785try { 6786// Zephyr: exact product-id match first (data-product-id on cart cards) 6787var pid0 = it && (it.productId || it.product_id);
6788if (pid0 && __cartIds0[String(pid0)]) return true; 6789// Be tolerant: different shapes may use productUrl vs product_url 6790var url = ''; 6791if (it) { 6792url = it.productUrl || it.product_url || it.url || ''; 6793} 6794// If url missing but we have an id, try the lookup map (populated during tile build) 6795if (!url && it && it.productId && TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[it.productId]) { 6796url = TILE_ITEM_BY_ID[it.productId].productUrl || TILE_ITEM_BY_ID[it.productId].product_url || ''; 6797} 6798var pth = normalizeUrlPathForCompare(url); 6799return !!(pth && __cartPaths0[pth]); 6800} catch (e) { return false; } 6801} 6802// Build initial visible tiles from the ordered pool, skipping in-cart items. 6803// Keep tile count stable up to MAX_TILES_TO_SHOW. 6804var TILE_ITEMS = []; 6805var TILE_ITEM_BY_ID = {}; 6806if (useTiles && Array.isArray(tilePick.tiles)) { 6807for (var __i = 0; __i < tilePick.tiles.length; __i++) { 6808if (TILE_ITEMS.length >= MAX_TILES_TO_SHOW) break; 6809var it = tilePick.tiles[__i]; 6810if (!it) continue; 6811// Allow tiles even if productId is missing (null IDs can still be previewed). We'll block add-to-cart for them later. 6812// Skip anything already in cart (path match) 6813if (__isInCartByPath(it)) continue; 6814TILE_ITEMS.push(it); 6815try { if (it.productId) TILE_ITEM_BY_ID[it.productId] = it; } catch (eMap) { } 6816} 6817} 6818// Default index: keep "best" (0) after exclusions 6819if (useTiles) persona.defaultPrimaryIndex = 0; 6820// ---------------------------------------------------- 6821// 5) Build UI 6822// ---------------------------------------------------- 6823var inkPadHtml = 6824'<div class="cart-kit-upsell__options-group" style="margin-top:10px;">' 6825+ '<div class="cart-kit-upsell__options-label" id="kit-ink-label">Would you like an Ink Pad? (Needed to make impression)</div>' 6826+ '<div class="kit-swatch-list" id="kit-swatch-list">' 6827+ '<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>' 6828+ '<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>' 6829+ '<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>' 6830+ '<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>' 6831+ '<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>' 6832+ '<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>' 6833+ '</div>' 6834+ '</div>'; 6835var inkPadBlock = (!useTiles ? inkPadHtml : ''); 6836var upsellMarkup = '' 6837+ '<div class="cart-kit-upsell cart-kit-upsell--open" id="cart-kit-upsell">' 6838+ '<button class="cart-kit-upsell__header" type="button">' 6839+ '<div class="cart-kit-upsell__icon-wrap" aria-hidden="true">' 6840+ '<img class="cart-kit-upsell__icon-img cart-kit-upsell__icon-img--static"' 6841+ ' src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/SS_Homepage-Icon_Stamps_120x120.png"' 6842+ ' alt="" />' 6843+ '</div>' 6844+ '<div class="cart-kit-upsell__header-text">' 6845+ '<span class="cart-kit-upsell__title" id="cart-kit-header-text">$1 Accent Stamp Unlocked! Choose your favorite below and add to cart.</span>' 6846+ '</div>' 6847+ '<span class="cart-kit-upsell__chevron" aria-hidden="true"></span>' 6848+ '</button>' 6849+ '<div class="cart-kit-upsell__body">' 6850+ '<div class="cart-kit-upsell__options-group">' 6851+ '<div class="cart-kit-upsell__options-header">' 6852+ '<div class="cart-kit-upsell__options-label" id="kit-stamp-label">Choose one or many Accent Stamp Designs:</div>' 6853+ '<button type="button" class="cart-kit-upsell__info" aria-label="Stamp impression size information">' 6854+ '<span class="cart-kit-upsell__info-icon">i</span>' 6855+ '<span class="cart-kit-upsell__info-tooltip" id="kit-tooltip">Stamp impression size: approx 1.75" Ã 0.67".</span>' 6856+ '</button>' 6857+ '</div>' 6858+ '<div class="kit-stamp-grid" id="kit-stamp-grid"></div>' 6859+ '</div>' 6860+ inkPadBlock 6861+ '<div class="cart-kit-upsell__footer">' 6862+ '<div class="cart-kit-upsell__price">' 6863+ '<span class="cart-kit-upsell__price-label" id="kit-price-label">Total:</span>' 6864+ '<span class="kit-value-line__new" id="kit-price-new">$1.00</span>' 6865+ '<button type="button" class="cart-kit-upsell__cta" id="cart-kit-upsell-add">Add to Cart</button>' 6866+ '<button type="button" class="cart-kit-upsell__cta-secondary" id="cart-kit-upsell-hide">Hide Offer</button>' 6867+ '</div>' 6868+ '<div class="kit-success-toast" id="kit-success-toast"></div>' 6869+ '<div class="cart-kit-upsell__status" id="cart-kit-status" aria-l
6869ive="polite"></div>' 6870+ '<div class="cart-kit-upsell__progresswrap" id="cart-kit-progresswrap" style="display:none;">' 6871+ '<div class="cart-kit-upsell__progresstop">' 6872+ '<div class="cart-kit-upsell__progresstext" id="cart-kit-progresstext">Adding itemsâ¦</div>' 6873+ '<div class="cart-kit-upsell__progresspct" id="cart-kit-progresspct">0%</div>' 6874+ '</div>' 6875+ '<div class="cart-kit-upsell__progressbar">' 6876+ '<div class="cart-kit-upsell__progressfill" id="cart-kit-progressfill"></div>' 6877+ '</div>' 6878+ '</div>' 6879+ '</div>' 6880+ '</div>' 6881+ '</div>' 6882+ '</div>'; 6883mount.insertAdjacentHTML('beforeend', upsellMarkup); 6884var upsell = mount.querySelector('#cart-kit-upsell'); 6885var header = upsell.querySelector('.cart-kit-upsell__header'); 6886var body = upsell.querySelector('.cart-kit-upsell__body'); 6887var addBtn = upsell.querySelector('#cart-kit-upsell-add'); 6888if (!upsell || !addBtn) { 6889console.warn('[SmartCart] Required module elements missing. Aborting init.'); 6890return; 6891} 6892var hideBtn = upsell.querySelector('#cart-kit-upsell-hide'); 6893var stampGrid = upsell.querySelector('#kit-stamp-grid'); 6894var swatchList = upsell.querySelector('#kit-swatch-list'); 6895var statusEl = upsell.querySelector('#cart-kit-status'); 6896if (statusEl) { 6897statusEl.setAttribute('role', 'status'); 6898statusEl.setAttribute('aria-live', 'polite'); 6899statusEl.setAttribute('aria-atomic', 'true'); 6900} 6901var progressWrap = upsell.querySelector('#cart-kit-progresswrap'); 6902var progressFill = upsell.querySelector('#cart-kit-progressfill'); 6903var progressText = upsell.querySelector('#cart-kit-progresstext'); 6904var progressPct = upsell.querySelector('#cart-kit-progresspct'); 6905var stampLabelEl = upsell.querySelector('#kit-stamp-label'); 6906var inkLabelEl = upsell.querySelector('#kit-ink-label'); 6907var tooltipEl = upsell.querySelector('#kit-tooltip'); 6908var headerTextEl = upsell.querySelector('#cart-kit-header-text'); 6909// Animate icon 6910setTimeout(function () { 6911var wrap = upsell.querySelector('.cart-kit-upsell__icon-wrap'); 6912if (wrap) wrap.classList.add('is-entered'); 6913}, 40); 6914// ---------------------------------------------------- 6915// 6) Apply persona UI (Notary) 6916// ---------------------------------------------------- 6917function applyPersonaUI() { 6918// Header + labels + tooltip 6919if (headerTextEl) headerTextEl.textContent = persona.headerTitle || ''; 6920if (stampLabelEl) stampLabelEl.textContent = persona.stampLabel || ''; 6921if (inkLabelEl) inkLabelEl.textContent = persona.inkLabel || ''; 6922if (tooltipEl) tooltipEl.textContent = persona.tooltip || ''; 6923// Toggle tile layout when persona has items 6924if (stampGrid) stampGrid.classList.toggle('is-notary-grid', !!useTiles); 6925// Filter ink swatches based on allowedInk 6926var allowed = (persona.allowedInk && persona.allowedInk.length) ? persona.allowedInk : ['black', 'blue', 'red', 'green', 'purple', 'none']; 6927if (swatchList) { 6928var swatches = swatchList.querySelectorAll('.kit-swatch'); 6929for (var i = 0; i < swatches.length; i++) { 6930var key = swatches[i].getAttribute('data-kit-color'); 6931swatches[i].style.display = (allowed.indexOf(key) > -1) ? '' : 'none'; 6932} 6933} 6934} 6935applyPersonaUI(); 6936// ---------------------------------------------------- 6937// 7) PDP Fetch Helpers (image + real price + real name) 6938// ---------------------------------------------------- 6939var PDP_CACHE = new Map(); // url -> { name, price, img } 6940function toNumberPrice(val) { 6941if (val == null) return null; 6942var n = Number(String(val).replace(/[^0-9.]/g, '')); 6943return isFinite(n) ? n : null; 6944} 6945function absUrlMaybe(src) { 6946if (!src) return null; 6947if (/^https?:\/\//i.test(src)) return src; 6948if (src.indexOf('/') === 0) return window.location.origin + src; 6949return src; 6950} 6951function detectRequiresMicro(doc) { 6952try { 6953var form = 6954doc.querySelector('#product_addtocart_form') || 6955doc.querySelector('form[action*="checkout/cart/add"]') || 6956doc.querySelector('form'); 6957if (!form) return false; 6958// Strong signal: any file upload = micro PDP 6959if (form.querySelector('input[type="file"]')) return true; 6960// Strong signal: Magento custom options payload (options[...]) 6961if (form.querySelector('[name^="options["]')) return true; 6962// Strong signal: configurables often use super_attribute 6963if (form.querySelector('[name^="super_attribute"]')) return true; 6964// Strong signal: required fields inside options wrapper 6965var optionsWrap = 6966form.querySelector('.product-options-wrapper') || 6967form.querySelector('#product-options-wrapper') || 6968form.querySelector('.fieldset') || 6969form; 6970// Look for explicit required markers/validators within options area 6971if ( 6972optionsWrap.querySelector('[required]') || 6973optionsWrap.querySelector('[aria-required="true"]') || 6974optionsWrap.querySelector('[data-validate*="required"]') || 6975optionsWrap.querySelector('.required-entry') || 6976optionsWrap.querySelector('.required') 6977) { 6978return true; 6979} 6980// If there are selectable option controls (select/radio/checkbox/textarea) 6981// inside a known options wrapper, treat as "likely micro" 6982// (kept conservative: only if wrapper exists) 6983var hasOptionControls = 6984!!optionsWrap.querySelector('select, input[type="radio"], input[type="checkbox"], textarea'); 6985if (hasOptionControls && (optionsWrap !== form)) return true; 6986return false;
6987} catch (e) { 6988return false; 6989} 6990} 6991function requiresMicroPdp(productId, rawTile) { 6992var pid = String(productId || '').trim(); 6993if (!pid) return false; 6994// 1) Explicit tile intent 6995if (rawTile && rawTile.addMode === 'microPdp') return true; 6996// 2) Hard override (authoritative) 6997if ( 6998typeof HARD_SKU_OVERRIDES !== 'undefined' && 6999HARD_SKU_OVERRIDES && 7000HARD_SKU_OVERRIDES[pid] && 7001HARD_SKU_OVERRIDES[pid].addMode === 'microPdp' 7002) { 7003return true; 7004} 7005// 3) Autodetect fallback from cached PDP parse 7006if ( 7007rawTile && 7008rawTile.productUrl && 7009typeof PDP_CACHE !== 'undefined' && 7010PDP_CACHE && 7011PDP_CACHE.has(rawTile.productUrl) 7012) { 7013var cached = PDP_CACHE.get(rawTile.productUrl); 7014if (cached && cached.requiresMicro === true) return true; 7015} 7016return false; 7017} 7018function isAccentExperience() { 7019try { 7020var urlParams = new URLSearchParams(window.location.search); 7021var accentAddedQS = (urlParams.get('accentAdded') === '1' || urlParams.get('accentAdded') === 'true'); 7022return !!accentAddedQS; // keep it strict for now 7023} catch (e) { 7024return false; 7025} 7026} 7027function parsePdpDoc(doc) { 7028var name = 7029(doc.querySelector('meta[property="og:title"]') && 7030doc.querySelector('meta[property="og:title"]').getAttribute('content')) || 7031(doc.querySelector('h1') && (doc.querySelector('h1').textContent || '').trim()) || 7032null; 7033var img = 7034(doc.querySelector('meta[property="og:image"]') && 7035doc.querySelector('meta[property="og:image"]').getAttribute('content')) || 7036(doc.querySelector('meta[name="twitter:image"]') && 7037doc.querySelector('meta[name="twitter:image"]').getAttribute('content')) || 7038null; 7039img = absUrlMaybe(img); 7040var price = 7041(doc.querySelector('meta[property="product:price:amount"]') && 7042doc.querySelector('meta[property="product:price:amount"]').getAttribute('content')) || 7043(doc.querySelector('meta[itemprop="price"]') && 7044doc.querySelector('meta[itemprop="price"]').getAttribute('content')) || 7045(doc.querySelector('[itemprop="price"]') && 7046doc.querySelector('[itemprop="price"]').textContent) || 7047(doc.querySelector('.price') && 7048doc.querySelector('.price').textContent) || 7049null; 7050price = toNumberPrice(price); 7051// Magento SKU. GA4 item_id MUST be the SKU, not the entity id: every 7052// other ecommerce event on this site (view_cart, purchase) keys items 7053// by SKU, so sending entity ids here would make the same product two 7054// different rows in GA4 and break any SmartCart-add -> purchase join. 7055var sku = null; 7056try { 7057var skuEl = doc.querySelector('meta[itemprop="sku"]') || doc.querySelector('[itemprop="sku"]'); 7058if (skuEl) { 7059sku = (skuEl.getAttribute('content') || skuEl.textContent || '').trim() || null; 7060} 7061} catch (eSku) { } 7062var requiresMicro = detectRequiresMicro(doc); 7063// The single reliable "is this a product?" signal. An account, login, 7064// 404 or category page still yields an og:title and an og:image, so 7065// title/image alone can't tell you the scrape succeeded. 7066var isProduct = !!( 7067doc.querySelector('#product_addtocart_form') || 7068doc.querySelector('form[action*="checkout/cart/add"]') 7069); 7070return { 7071name: name, 7072img: img, 7073price: price, 7074sku: sku, 7075isProduct: isProduct, 7076requiresMicro: requiresMicro 7077}; 7078} 7079// Same-origin PDP fetch: config product_urls are absolute www.simplystamps.com; 7080// rewrite to the current origin so hydration works on local/staging AND prod (no-op on www). 7081function ckSameOrigin(u) { 7082try { 7083var p = new URL(u, window.location.origin); 7084if (/(^|\.)simplystamps\.com$/i.test(p.hostname) && p.origin !== window.location.origin) { 7085return window.location.origin + p.pathname + p.search + p.hash; 7086} 7087return p.href; 7088} catch (e) { return u; } 7089} 7090// ---------------------------------------------------- 7091// ZEPHYR SIDE-CART BRIDGE (added 2026-09-11) 7092// Zephyr ships a slide-out cart drawer driven by an Alpine store, and the 7093// cart PAGE is a SECOND, independent Alpine component (cartPage()) holding 7094// its own copy of the same /zephyr/cart/getItems/ JSON. They do not talk: 7095// cart-drawer.js store.refresh() -> fires 'cart-refreshed' 7096// cart-page.js init() -> listens 'cart-updated' 7097// Nothing bridges those two names, so ANY out-of-band cart mutation must do
7098// BOTH: refresh the store (drawer + header badge) AND dispatch 'cart-updated' 7099// (main cart list + Order Summary). Doing only one leaves half the page 7100// stale -- which is the reason this module used to hard-reload. 7101// ---------------------------------------------------- 7102function ckCartStore(win) { 7103try { 7104var w = win || window; 7105if (!w.Alpine || typeof w.Alpine.store !== 'function') return null; 7106return w.Alpine.store('cart') || null; 7107} catch (e) { 7108return null; 7109} 7110} 7111// Push a cart mutation into BOTH Zephyr cart trees, optionally revealing the 7112// side cart. Returns false when the Zephyr store is absent (legacy theme) so 7113// callers can fall back to the old page-reload path. 7114function ckAfterCartMutation(openDrawer) { 7115var st = ckCartStore(window); 7116if (!st) return false; 7117if (openDrawer && typeof st.openDrawer === 'function') { 7118try { st._scrollToNew = true; } catch (e) { } 7119try { st.openDrawer(); } catch (e) { } 7120} 7121// Mirror Zephyr's own post-add timing (handleAjaxAddToCart waits 100ms). 7122setTimeout(function () { 7123try { if (typeof st.refresh === 'function') st.refresh(); } catch (e) { } 7124try { window.dispatchEvent(new CustomEvent('cart-updated')); } catch (e) { } 7125}, 100); 7126return true; 7127} 7128function fetchPdpData(productUrl) { 7129if (!productUrl) return Promise.resolve({ name: null, img: null, price: null }); 7130if (PDP_CACHE.has(productUrl)) return Promise.resolve(PDP_CACHE.get(productUrl)); 7131var __fetchUrl = ckSameOrigin(productUrl); 7132return fetch(__fetchUrl, { credentials: 'same-origin' }) 7133.then(function (res) { return res.text(); }) 7134.then(function (html) { 7135var doc = new DOMParser().parseFromString(html, 'text/html'); 7136var data = parsePdpDoc(doc); 7137PDP_CACHE.set(productUrl, data); 7138return data; 7139}) 7140.catch(function () { 7141var data = { name: null, img: null, price: null }; 7142PDP_CACHE.set(productUrl, data); 7143return data; 7144}); 7145} 7146// ---------------------------------------------------- 7147// Tile State Model (no UX changes yet) 7148// ---------------------------------------------------- 7149var TILE_STATE = { 7150AVAILABLE: 'available', 7151SELECTED: 'selected', 7152ADDED: 'added' 7153}; 7154// Central store keyed by SKU 7155// ---------------------------------------------------- 7156// TILE QUALITY GATE + APPROVED FALLBACKS (added 2026-09-15) 7157// Some persona entries point at URLs that aren't product pages on every store 7158// view (a prod-only SKU, a retired URL). The scrape then returns whatever that 7159// page happens to be -- "Create New Customer Account", no price, the site logo 7160// -- and we rendered it as a product. Never show that. Substitute an approved 7161// alternative the shopper doesn't already have, or drop the slot entirely. 7162// ---------------------------------------------------- 7163var CK_TILE_FALLBACKS = [ 7164{ productId: '22023', productUrl: 'https://www.simplystamps.com/envelope-sealer-2oz-bottle', title: 'Envelope Sealer - 2oz Bottle' }, 7165{ productId: '25340', productUrl: 'https://www.simplystamps.com/stamp-cleaner-kit', title: 'Stamp Cleaner Kit' }, 7166{ productId: '24186', productUrl: 'https://www.simplystamps.com/gold-wax-seal-sticker-rosemary-design', title: 'Gold Wax Seal Sticker, Rosemary Design' }, 7167{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', title: 'Craft Ink Pads' }, 7168{ 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' }, 7169{ productId: '23105', productUrl: 'https://www.simplystamps.com/delicate-flower-envelope-accent-craft-stamp', title: 'Delicate Flower Envelope Accent Craft Stamp' } 7170]; 7171var CK_FALLBACKS_USED = new Set(); 7172function ckIsUsableTileData(data) { 7173if (!data) return false; 7174if (data.isProduct === false) return false; // definitely not a PDP 7175if (data.price == null && !data.img) return false; // nothing worth showing 7176var n = String(data.name || '').trim().toLowerCase(); 7177if (!n) return false; 7178if (/create new customer|customer login|^log ?in|^sign ?in|page not found|404|access denied|shopping cart|my account|dashboard/.test(n)) return false; 7179return true; 7180} 7181function ckCartIdsNow() { 7182try { return getCartProductIdSet() || Object.create(null); } 7183catch (e) { return Object.create(null); } 7184} 7185// First approved alternative that is NOT already in the cart and NOT already on 7186// screen. Null when there is nothing safe left to show. 7187function ckPickFallbackTile() { 7188var inCart = ckCartIdsNow(); 7189var onScreen = Object.create(null); 7190try { 7191var rendered = document.querySelectorAll('[data-kit-primary]'); 7192for (var r = 0; r < rendered.length; r++) { 7193onScreen[String(rendered[r].getAttribute('data-kit-primary'))] = true; 7194} 7195} catch (e) { } 7196for (var i = 0; i < CK_TILE_FALLBACKS.length; i++) { 7197var f = CK_TILE_FALLBACKS[i]; 7198var id = String(f.productId); 7199if (CK_FALLBACKS_USED.has(id)) continue; 7200if (inCart[id]) continue; 7201if (onScreen[id]) continue; 7202CK_FALLBACKS_USED.add(id); 7203return f; 7204} 7205return null; 7206} 7207// Nothing safe for this slot -- remove it rather than render junk. 7208function ckDropTile(card) { 7209try { 7210var pid = String(card.getAttribute('data-kit-primary') || ''); 7211if (pid) { try { selectedPrimaryIds.delete(pid); } catch (eS) { } } 7212if (card.parentNode) card.parentNode.removeChild(card); 7213try { updatePriceUI(); } catch (eU) { } 7214// Tiles are dropped asynchronously, so this is the moment the module 7215// can become empty -- re-check every time. 7216try { ckSyncModuleEmptyState(); } catch (eE) { } 7217} catch (e) { } 7218} 7219// Repaint an already-rendered card onto a different product, then hydrate it. 7220function ckRepaintCardAs(card, item) { 7221if (!card || !item) return; 7222var id = String(item.productId); 7223try { TILE_ITEM_BY_ID[id] = item; } catch (e) { } 7224try { if (ckSeenTileIds) ckSeenTileIds.add(id); } catch (e) { } 7225card.setAttribute('data-kit-primary', id); 7226card.setAttribute('data-sku', id); 7227card.setAttribute('data-kit-url', item.productUrl || ''); 7228try { card.classList.remove('is-added', 'is-selected', 'is-complete'); } catch (e) { } 7229try { card.removeAttribute('aria-disabled'); } catch (e) { } 7230var nameEl = card.querySelector('.kit-product-name'); 7231var priceEl = card.querySelector('.kit-product-price'); 7232var thumbEl = card.querySelector('.kit-product-thumb-tile'); 7233if (nameEl) nameEl.textContent = item.title || 'Loading\u2026'; 7234if (priceEl) priceEl.textContent = '\u2014'; 7235if (thumbEl) thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">Loading\u2026</span>'; 7236fetchPdpData(item.productUrl).then(function (d2) { 7237if (String(card.getAttribute('data-kit-primary') || '') !== id) return; // swapped again 7238if (!ckIsUsableTileData(d2)) { 7239// This alternative isn't available in this store view either. Try the 7240// next approved one; CK_FALLBACKS_USED makes the recursion terminate. 7241var nextAlt = ckPickFallbackTile(); 7242if (nextAlt) ckRepaintCardAs(card, nextAlt); 7243else ckDropTile(card); 7244return; 7245} 7246try { 7247NOTARY_HYDRATED.set(id, { 7248sku: d2.sku, 7249productId: id, productUrl: item.productUrl, 7250name: d2.name || item.title, img: d2.img || null, 7251price: (d2.price != null ? Number(d2.price) : null) 7252}); 7253} catch (e) { } 7254if (nameEl) nameEl.textContent = d2.name || item.title; 7255if (priceEl) priceEl.textContent = (d2.price != null ? ('$' + Number(d2.price).toFixed(2)) : 'See price in cart'); 7256if (thumbEl) { 7257if (d2.img) thumbEl.innerHTML = '<img src="' + d2.img + '" alt="" />'; 7258else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 7259} 7260}); 7261} 7262// ---------------------------------------------------- 7263// MODULE EMPTY STATE (added 2026-09-15) 7264// Tiles can vanish AFTER render: the quality gate drops non-products and 7265// in-cart suppression removes what the shopper already owns. The original 7266// empty-state branch only ran when the pool was empty at BUILD time, and it 7267// targeted #kit-add-btn -- which does not exist, the CTA is 7268// #cart-kit-upsell-add -- so an emptied module was left on the page as a
7269// header + "Total: $0.00" + a live Add to Cart button with nothing to add. 7270// ---------------------------------------------------- 7271function ckSyncModuleEmptyState() { 7272try { 7273var tileCount = stampGrid ? stampGrid.querySelectorAll('[data-kit-primary]').length : 0; 7274var swatchCount = swatchList ? swatchList.querySelectorAll('.kit-swatch').length : 0; 7275if (tileCount > 0 || swatchCount > 0) { 7276if (upsell) upsell.style.display = ''; 7277return false; 7278} 7279// Nothing left to offer. An upsell with no offer is noise on a cart 7280// page, so take the whole module off the page rather than leave a 7281// header and a dead CTA behind. 7282try { console.warn('[SmartCart] nothing offerable remains (all in cart or filtered); hiding the module.'); } catch (e1) { } 7283try { 7284if (addBtn) { 7285addBtn.disabled = true; 7286addBtn.setAttribute('aria-disabled', 'true'); 7287} 7288} catch (e2) { } 7289if (upsell) upsell.style.display = 'none'; 7290return true; 7291} catch (e) { return false; } 7292} 7293var TileStore = (function () { 7294var map = Object.create(null); 7295function getSkuFromTile(tileEl) { 7296return ( 7297tileEl.getAttribute('data-sku') || 7298tileEl.getAttribute('data-product-sku') || 7299tileEl.getAttribute('data-kit-sku') || 7300'' 7301).trim(); 7302} 7303function inferIsHard(tileEl) { 7304// Preferred: explicit attribute you set when building tiles 7305// data-hard="1" or data-is-hard="true" 7306var hardAttr = 7307tileEl.getAttribute('data-hard') || 7308tileEl.getAttribute('data-is-hard') || 7309''; 7310if (hardAttr) return hardAttr === '1' || hardAttr === 'true'; 7311// Fallback: if you already tag these somewhere else 7312// (you can tighten this later when we implement Step 2) 7313return tileEl.classList.contains('is-hard') || false; 7314} 7315function initFromDom(rootEl) { 7316if (!rootEl) return; 7317var tiles = Array.from(rootEl.querySelectorAll('.kit-stamp-card[data-kit-primary]')); 7318tiles.forEach(function (tileEl) { 7319var sku = getSkuFromTile(tileEl); 7320if (!sku) return; 7321try { ckSeenTileIds.add(String(sku)); } catch (e) { } 7322if (!map[sku]) { 7323map[sku] = { 7324sku: sku, 7325el: tileEl, 7326isHard: inferIsHard(tileEl), 7327state: TILE_STATE.AVAILABLE 7328}; 7329} else { 7330// re-rendered tile, keep state but update DOM ref 7331map[sku].el = tileEl; 7332// do not overwrite isHard unless you want to re-infer 7333} 7334// Bootstrap state from current UI (so no visible changes) 7335// You can adjust these classnames to match your actual markup. 7336if (tileEl.classList.contains('is-added') || tileEl.querySelector('.ck-added, .cart-kit-added, .cart-kit-check')) { 7337map[sku].state = TILE_STATE.ADDED; 7338} else if (tileEl.classList.contains('is-selected') || tileEl.getAttribute('aria-pressed') === 'true') { 7339map[sku].state = TILE_STATE.SELECTED; 7340} else { 7341map[sku].state = TILE_STATE.AVAILABLE; 7342} 7343syncDom(sku); 7344}); 7345} 7346function get(sku) { 7347return map[String(sku || '')] || null; 7348} 7349function setState(sku, nextState) { 7350sku = String(sku || ''); 7351if (!map[sku]) return; 7352var prev = map[sku].state; 7353map[sku].state = nextState; 7354// Keep DOM aligned (but we preserve current look) 7355syncDom(sku); 7356return { sku: sku, prev: prev, next: nextState }; 7357} 7358function syncDom(sku) { 7359var t = map[String(sku || '')]; 7360if (!t || !t.el) return; 7361var el = t.el; 7362// Normalize classes (keep current behavior intact) 7363el.classList.toggle('is-selected', t.state === TILE_STATE.SELECTED); 7364el.classList.toggle('is-added', t.state === TILE_STATE.ADDED); 7365// Optional aria alignment if you already use it 7366// (doesn't change visuals but improves correctness) 7367if (t.state === TILE_STATE.SELECTED) el.setAttribute('aria-pressed', 'true'); 7368else el.setAttribute('aria-pressed', 'false'); 7369} 7370function all() { 7371return map; 7372} 7373return { 7374initFromDom: initFromDom, 7375get: get, 7376setState: setState, 7377syncDom: syncDom, 7378all: all 7379}; 7380})(); 7381// ---------------------------------------------------- 7382// Micro PDP (iframe) â for hard SKUs w/ uploads + dependent options 7383// ---------------------------------------------------- 7384var MICRO_PDP = { 7385overlay: null, 7386iframe: null, 7387loading: null, 7388lastUrl: null, 7389lastProductId: null, 7390returnUrl: null, 7391progressDone: 0, 7392progressTotal: 0 7393}; 7394// ---------------------------------------------------- 7395// Micro PDP progress (header bar) 7396// ---------------------------------------------------- 7397function getMicroProgressFromUrl() { 7398try { 7399var u = new URL(window.location.href); 7400var done = parseInt(u.searchParams.get('microDone') || '0', 10); 7401var total = parseInt(u.searchParams.get('microTotal') || '0', 10); 7402if (!isFinite(done) || done < 0) done = 0; 7403if (!isFinite(total) || total < 0) total = 0; 7404return { done: done, total: total }; 7405} catch (e) { 7406return { done: 0, total: 0 }; 7407} 7408} 7409function bumpMicroDoneInUrl() { 7410try { 7411var u = new URL(window.location.href); 7412var done = parseInt(u.searchParams.get('microDone') || '0', 10); 7413var total = parseInt(u.searchParams.get('microTotal') || '0', 10); 7414if (!isFinite(done) || done < 0) done = 0; 7415if (!isFinite(total) || total < 0) total = 0; 7416if (total > 0) { 7417done = Math.min(total, done + 1); 7418u.searchParams.set('microDone', String(done)); 7419window.history.replaceState({}, document.title, u.toString()); 7420} 7421return { done: done, total: total }; 7422} catch (e) { 7423return { done: 0, total: 0 }; 7424} 7425} 7426function renderMicroProgress(done, total) { 7427var wrap = mount.querySelector('#ck-micropdp-progress'); 7428var fill = mount.querySelector('#ck-micropdp-progressfill'); 7429var txt = mount.querySelector('#ck-micropdp-progresstext'); 7430// Only show when the user is doing a multi-item add (2+ total) 7431if (!wrap || !fill || !txt || !total || total <= 1) { 7432if (wrap) wrap.hidden = true; 7433return; 7434} 7435if (!isFinite(done) || done < 0) done = 0; 7436if (!isFinite(total) || total < 1) total = 1; 7437if (done > total) done = total; 7438var pct = Math.round((done / total) * 100); 7439wrap.hidden = false;
7440fill.style.width = pct + '%'; 7441// Completion state (for âAll setâ moment) 7442if (done >= total) { 7443wrap.classList.add('is-complete'); 7444txt.textContent = 'All set â ' + total + ' of ' + total + ' items added'; 7445} else { 7446wrap.classList.remove('is-complete'); 7447txt.textContent = done + ' of ' + total + ' items added'; 7448} 7449} 7450ensureMicroPdpModal(); 7451function ensureMicroPdpModal() { 7452if (MICRO_PDP.overlay) return; 7453var 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> `; 7454// Append inside mount so scoped CSS applies 7455mount.insertAdjacentHTML('beforeend', modalMarkup); 7456MICRO_PDP.overlay = mount.querySelector('#ck-micropdp-overlay'); 7457MICRO_PDP.iframe = mount.querySelector('#ck-micropdp-iframe'); 7458MICRO_PDP.loading = mount.querySelector('#ck-micropdp-loading'); 7459var closeBtn = mount.querySelector('#ck-micropdp-close'); 7460var fullBtn = mount.querySelector('#ck-micropdp-full'); 7461// Safety guards 7462if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-open'); 7463if (closeBtn) closeBtn.addEventListener('click', function () { closeMicroPdp('button'); }); 7464if (fullBtn) fullBtn.addEventListener('click', function () { 7465if (MICRO_PDP.lastUrl) window.open(MICRO_PDP.lastUrl, '_blank', 'noopener'); 7466}); 7467// Optional overlay click closes 7468if (MICRO_PDP.overlay) { 7469MICRO_PDP.overlay.addEventListener('click', function (e) { 7470if (e.target === MICRO_PDP.overlay) closeMicroPdp('overlay'); 7471}); 7472} 7473} 7474// ESC closes 7475document.addEventListener('keydown', function (e) { 7476if (e.key === 'Escape' && MICRO_PDP.overlay && MICRO_PDP.overlay.classList.contains('is-open')) { 7477closeMicroPdp(); 7478} 7479}); 7480// On iframe load, inject âmicroâ CSS + force form submit to top window 7481MICRO_PDP.iframe.addEventListener('load', function () { 7482try { 7483var doc = MICRO_PDP.iframe.contentDocument; 7484if (!doc) return; 7485// Ensure scope class exists inside iframe (so our selectors always match) 7486doc.documentElement.classList.add('cart-kit-micro-pdp'); 7487doc.body.classList.add('cart-kit-micro-pdp'); 7488// Hide big site chrome + tighten layout (best-effort; selectors may vary) 7489var style = doc.createElement('style'); 7490style.id = 'ck-micro-css'; 7491style.textContent = ` 7492/* ========================================================= 7493Micro PDP trims â chrome off, form focus, thumbnail preview 7494========================================================= */ 7495/* 1) Remove site chrome */ 7496header, .header, 7497.nav-container, .navigation, 7498.breadcrumbs, .breadcrumb, 7499.page-header, .header.content, 7500.sections.nav-sections, 7501.message.global, .message.notice, 7502.cookie-status-message, 7503footer, .footer, .page-footer { 7504display: none !important; 7505} 7506/* 1b) Zephyr side cart must never render inside the configurator. 7507Neither the drawer nor its trigger sits inside <header>, so the 7508chrome rules above miss both. */ 7509button[aria-label="Open cart"], 7510[x-show*="drawerOpen"] { 7511display: none !important; 7512} 7513/* 2) Normalize layout */ 7514html, body { height: auto !important; } 7515body { 7516margin: 0 !important; 7517padding: 12px !important; 7518background: #fff !important; 7519overflow-x: hidden !important; 7520} 7521.page-wrapper, .page-main, main, .columns, .column.main { 7522width: 100% !important; 7523max-width: none !important; 7524margin: 0 !important; 7525padding: 0 !important; 7526} 7527/* Hide the INLINE form add-to-cart row (but keep the sticky bottom bar) */ 7528/* Safety: ensure the STICKY/mobile bar still shows */
7529.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container, 7530.cart-kit-micro-pdp .add-to-box-mobile .add-to-cart-button-container-non-grouped, 7531.cart-kit-micro-pdp .add-to-box-mobile .input-group-mobile-container { 7532display: flex !important; 7533} 7534/* 2) Re-show the sticky/mobile bar at the bottom (itâs outside the form) */ 7535.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container { 7536display: flex !important; 7537} 7538/* Hide qty+button ONLY when it's NOT inside the sticky bottom bar */ 7539.cart-kit-micro-pdp .qty-and-add-to-cart-container:not(.add-to-box-mobile .qty-and-add-to-cart-container) { 7540display: none !important; 7541} 7542/* Kill common "extra" sections we don't need */ 7543#ss-main-content { margin-top: unset !important; display:none !important; } 7544#sticky-mobile-header{ display: none !important; } 7545.gqcs-swiper-container{ display: none !important; } 7546.product-wapper-tab { display: none !important; } 7547/* 3) Preview: make it a small thumbnail WITHOUT reflowing layouts 7548(works across templates: big hero, centered preview, swiper, etc.) */ 7549.cart-kit-micro-pdp .product.media, 7550.cart-kit-micro-pdp .product-image-carousel-container, 7551.cart-kit-micro-pdp .product-image-carousel-select, 7552.cart-kit-micro-pdp #ss-image-sheet, 7553.cart-kit-micro-pdp .gallery-placeholder, 7554.cart-kit-micro-pdp .product-img-content { 7555width: 100% !important; 7556max-width: 100% !important; 7557} 7558/* Center the preview area */ 7559.cart-kit-micro-pdp .product.media, 7560.cart-kit-micro-pdp .product-image-carousel-container, 7561.cart-kit-micro-pdp .product-image-carousel-select, 7562.cart-kit-micro-pdp .gallery-placeholder { 7563display: flex !important; 7564justify-content: center !important; 7565} 7566/* Image itself: cap size (no cropping) */ 7567.cart-kit-micro-pdp img.img-responsive, 7568.cart-kit-micro-pdp .product.media img, 7569.cart-kit-micro-pdp .product-image-photo, 7570.cart-kit-micro-pdp .fotorama__img, 7571.cart-kit-micro-pdp .swiper-slide img { 7572max-height: 300px !important; 7573max-width: 300px !important; 7574width: auto !important; 7575height: auto !important; 7576object-fit: contain !important; 7577margin: 0 !important; 7578} 7579/* Prevent swiper/nav chrome from adding vertical junk */ 7580/* Swiper UI: show dots when multiple images exist (arrows stay hidden) */ 7581.cart-kit-micro-pdp .swiper-button-prev, 7582.cart-kit-micro-pdp .swiper-button-next, 7583.cart-kit-micro-pdp .swiper-scrollbar {
7584display: none !important; 7585} 7586/* Dots ON (we'll hide them via JS when only 1 image) */ 7587.cart-kit-micro-pdp .swiper-pagination { 7588display: flex !important; 7589justify-content: center !important; 7590gap: 6px !important; 7591margin-top: 6px !important; 7592position: relative !important; 7593} 7594/* Make dots smaller + subtle so they don't add height */ 7595.cart-kit-micro-pdp .swiper-pagination-bullet { 7596width: 6px !important; 7597height: 6px !important; 7598opacity: 0.35 !important; 7599} 7600.cart-kit-micro-pdp .swiper-pagination-bullet-active { 7601opacity: 0.9 !important; 7602} 7603/* Tighten spacing under preview */ 7604.cart-kit-micro-pdp .product.media, 7605.cart-kit-micro-pdp .product-image-carousel-container, 7606.cart-kit-micro-pdp .product-image-carousel-select, 7607.cart-kit-micro-pdp .gallery-placeholder { 7608margin-bottom: 64px !important; 7609} 7610/* 4) Form focus */ 7611#product_addtocart_form, .product-add-form { 7612margin-top: 10px !important; 7613} 7614/* 5) Hide âExtra bottle of ink?â field (best effort selectors) 7615Note: :has() may not be supported everywhere; we also keep the JS fallback below */ 7616.field.extra_ink, .field.extra-ink, .field.extra_bottle_ink, 7617.product-options-wrapper .field:has(select[name*="extra"]) { 7618display: none !important; 7619} 7620.full-size-container{ 7621display:none; 7622} 7623`; 7624// Prevent double-inject if iframe reloads quickly 7625var existing = doc.getElementById('ck-micro-css'); 7626if (existing && existing.parentNode) existing.parentNode.removeChild(existing); 7627doc.head.appendChild(style); 7628// Force add-to-cart form to submit in TOP window so we return to cart normally 7629var form = 7630doc.querySelector('#product_addtocart_form') || 7631doc.querySelector('form[action*="checkout/cart/add"]'); 7632if (form) { 7633form.setAttribute('target', '_top'); 7634// Ensure return_url points back to cart (best effort) 7635var existingReturn = form.querySelector('input[name="return_url"]'); 7636if (!existingReturn) { 7637existingReturn = doc.createElement('input'); 7638existingReturn.type = 'hidden'; 7639existingReturn.name = 'return_url'; 7640form.appendChild(existingReturn); 7641} 7642existingReturn.value = (MICRO_PDP.returnUrl || window.location.href); 7643// Some Magento setups use return_url_encoded; set it if present 7644var encodedReturn = form.querySelector('input[name="return_url_encoded"]'); 7645if (encodedReturn) { 7646try { 7647encodedReturn.value = btoa(unescape(encodeURIComponent(MICRO_PDP.returnUrl || window.location.href))); 7648} catch (e) { } 7649} 7650// If there's a quantity input, default to 1 7651var qty = form.querySelector('input[name="qty"]'); 7652if (qty && !qty.value) qty.value = '1'; 7653// ---- LOADING CURSOR HOOK (reliable) ---- 7654try { 7655// Zephyr's ATC is id="product-addtocart-button" and type="button", 7656// so look for it FIRST -- a bare button[type="submit"] lookup can 7657// match an unrelated submit (search / newsletter) earlier in the DOM. 7658var btn = 7659doc.querySelector('button#product-addtocart-button') || 7660form.querySelector('button[type="submit"], input[type="submit"]') || 7661doc.querySelector('button.addtocart') || 7662doc.querySelector('button.action.tocart') || 7663doc.querySelector('button[type="submit"]') || 7664doc.querySelector('input[type="submit"]'); 7665if (btn && !btn.__ckBound) { 7666btn.__ckBound = true; 7667btn.addEventListener('click', function () { 7668// NOTE: arming happens in the handleAjaxAddToCart 7669// wrapper, not here -- the button the shopper actually 7670// clicks is the sticky bar's, which lives outside 7671// #product_addtocart_form and may not be this element. 7672setLoadingCursor(true); 7673if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); 7674}, { capture: true }); 7675} 7676if (!form.__ckBound) { 7677form.__ckBound = true; 7678form.addEventListener('submit', function () { 7679setLoadingCursor(true); 7680if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); 7681}, { capture: true }); 7682} 7683} catch (e) { 7684// If iframe isn't accessible due to cross-domain, this will throw. 7685} 7686} 7687// Hook the iframe's own cart store so the side cart cannot open in 7688// here, and so the AJAX add gives us a reliable success signal. 7689try { 7690var ckSrc = String(MICRO_PDP.iframe.getAttribute('src') || ''); 7691if (ckSrc && ckSrc.indexOf('about:blank') !== 0) bindMicroPdpAddBridge(); 7692} catch (e) { } 7693// ---------------------------------------------------- 7694// Cut everything below the shipping block (keep shipping visible) 7695// Anchor: .ships-and-made-in-usa-container 7696// ---------------------------------------------------- 7697(function () { 7698var d = MICRO_PDP.iframe.contentDocument; 7699if (!d) return; 7700var shipContainer = d.querySelector('.ships-and-made-in-usa-container'); 7701if (!shipContainer) return;
7702// Find a reasonable scope that contains the PDP âcontentâ sections 7703var scope = 7704shipContainer.closest('.vpt-product, .product-shop-content, .product-info-main, .column.main, main') 7705|| d.body; 7706// We want to hide everything AFTER the shipping container's row 7707var row = shipContainer.closest('.row') || shipContainer.parentElement; 7708if (!row) return; 7709var sib = row.nextElementSibling; 7710while (sib) { 7711sib.style.display = 'none'; 7712sib = sib.nextElementSibling; 7713} 7714})(); 7715// ---------------------------------------------------- 7716// Hide âExtra bottle of ink?â by label text (fallback) 7717// ---------------------------------------------------- 7718(function () { 7719var d = MICRO_PDP.iframe.contentDocument; 7720if (!d) return; 7721var labels = Array.from(d.querySelectorAll('label')); 7722var inkLabel = labels.find(function (l) { 7723return (l.textContent || '').toLowerCase().includes('extra bottle of ink'); 7724}); 7725if (!inkLabel) return; 7726var field = inkLabel.closest('.field, .control, .admin__field, .product-options-wrapper') || inkLabel.parentElement; 7727if (field) field.style.display = 'none'; 7728})(); 7729// Done loading 7730if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 7731} catch (err) { 7732// If cross-domain or blocked, just hide loader after a beat 7733if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 7734} 7735}); 7736// ---------------------------------------------------- 7737// MICRO-PDP ADD BRIDGE (added 2026-09-11) 7738// Zephyr's PDP add-to-cart control is 7739// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // It never submits the form, so the form.target='_top' hook further down -- 7740// which used to be the ONLY way the parent learned an add had happened -- is 7741// inert on a Zephyr PDP. The add is AJAX, and on success handleAjaxAddToCart 7742// calls Alpine.store('cart').openDrawer() INSIDE this iframe: the side cart 7743// slides out within the 960px modal and the parent cart page never updates. 7744// 7745// Fix: the iframe is same-origin, so we replace openDrawer() on the iframe's 7746// own cart store. That call is our exact "add succeeded" signal (it runs only 7747// in handleAjaxAddToCart's success branch) and the drawer never renders in the 7748// frame. We then reproduce the receipt state the old reload used to deliver in 7749// the URL -- via history.replaceState instead of navigating -- so the whole 7750// existing downstream pipeline (tile -> "added", tile swap, multi-SKU chaining) 7751// keeps working untouched. form.target='_top' stays as a fallback for any PDP 7752// that still submits natively. 7753// ---------------------------------------------------- 7754var MICRO_BRIDGE = { 7755timer: null, loadTimer: null, tries: 0, 7756armed: false, handled: false, 7757addPatched: false, storePatched: false, 7758countBefore: null 7759}; 7760// Zephyr exposes a running cart count at /zephyr/cart/count/ (verified live: 7761// returns {"count": N} where N is total UNITS, not lines -- so a qty bump on an 7762// existing line still moves it). Authoritative fallback for when the in-frame 7763// success signal never arrives, so a miss degrades to a late receipt, not silence. 7764function ckCartCount() { 7765try { 7766return fetch('/zephyr/cart/count/', { 7767credentials: 'same-origin', 7768headers: { 'X-Requested-With': 'XMLHttpRequest' } 7769}) 7770.then(function (r) { 7771if (!r.ok) throw new Error('cart count HTTP ' + r.status); 7772return r.json(); 7773}) 7774.then(function (j) { return (j && typeof j.count === 'number') ? j.count : null; }) 7775.catch(function (err) { 7776try { console.warn('[SmartCart] /zephyr/cart/count/ unavailable:', err && err.message); } catch (e) { } 7777return null; 7778}); 7779} catch (e) { 7780return Promise.resolve(null); 7781} 7782} 7783// A 200 from /checkout/cart/add is NOT proof the cart changed: OpenMage 7784// answers a bare add for a product it can't sell (missing from this store 7785// view, disabled, required options) with a 200 and an empty result, and 7786// postToCart() only throws on !res.ok. Without this the module shows a full 7787// success state -- drawer, toast, tile swap -- over an unchanged cart. 7788function ckVerifySimpleAdd(baseline) { 7789if (typeof baseline !== 'number') return; // no baseline: stay optimistic 7790setTimeout(function () { 7791ckCartCount().then(function (after) { 7792if (after === null || after > baseline) return; // added, or can't tell 7793try { console.warn('[SmartCart] add returned 200 but the cart did not change (baseline ' + baseline + ').'); } catch (e) { } 7794var msg = 'That item could not be added \u2014 it may be unavailable. Please try another.'; 7795try { if (statusEl) statusEl.textContent = msg; } catch (e) { } 7796try { 7797var fl = (window.Alpine && typeof window.Alpine.store === 'function') ? window.Alpine.store('flash') : null; 7798if (fl && typeof fl.show === 'function') fl.show('error', msg); 7799} catch (e) { } 7800// Don't leave a side cart open over an unchanged cart. 7801try { 7802var st = ckCartStore(window); 7803if (st && typeof st.closeDrawer === 'function') st.closeDrawer(); 7804} catch (e) { } 7805}); 7806}, 600); 7807} 7808// Clears the visual loading state ONLY. It must never disarm: the add can 7809// still be in flight, and disarming would make the openDrawer success signal
7810// get swallowed when it finally arrives. 7811function clearMicroLoadingState() { 7812try { setLoadingCursor(false); } catch (e) { } 7813try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { } 7814} 7815// Ask the server whether the add actually landed. Callers decide when it's 7816// worth asking; this only refuses to act if a receipt already fired. 7817// Polls rather than asking once. handleAjaxAddToCart returns a BOOLEAN, not a 7818// promise (measured on local 2026-09-14), so there is no completion callback -- 7819// a real add on this catalog takes ~3.5s. A single check right after the click 7820// always misses, and treating that miss as failure is what left the modal open. 7821function confirmMicroAddByCount(reason, attemptsLeft) { 7822if (MICRO_BRIDGE.handled) return; 7823if (typeof attemptsLeft !== 'number') attemptsLeft = 0; 7824if (MICRO_BRIDGE.countBefore === null) { 7825try { console.warn('[SmartCart] cannot confirm micro-PDP add, no cart-count baseline (' + reason + ').'); } catch (e) { } 7826clearMicroLoadingState(); 7827return; 7828} 7829var baseline = MICRO_BRIDGE.countBefore; 7830ckCartCount().then(function (after) { 7831if (MICRO_BRIDGE.handled) return; 7832if (after !== null && after > baseline) { 7833try { console.warn('[SmartCart] micro-PDP add confirmed by cart count (' + reason + ').'); } catch (e) { } 7834try { onMicroPdpAddSucceeded(); } catch (e) { } 7835return; 7836} 7837if (attemptsLeft > 0) { 7838setTimeout(function () { 7839confirmMicroAddByCount(reason, attemptsLeft - 1); 7840}, 1200); 7841return; 7842} 7843// Out of attempts. Drop the spinner but STAY ARMED -- if the add is 7844// simply slow, openDrawer will still arrive and fire the receipt. 7845clearMicroLoadingState(); 7846}); 7847} 7848function resetMicroBridge() { 7849try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7850try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7851MICRO_BRIDGE.timer = null; 7852MICRO_BRIDGE.loadTimer = null; 7853MICRO_BRIDGE.tries = 0; 7854MICRO_BRIDGE.armed = false; 7855MICRO_BRIDGE.addPatched = false; 7856MICRO_BRIDGE.storePatched = false; 7857// `handled` is deliberately NOT cleared -- closeMicroPdp() runs as part of a 7858// successful receipt. openMicroPdp() clears it, so the guard really is one 7859// receipt per configurator open. 7860} 7861// Arm on the ACTUAL add call, not a button click. Validation runs inside 7862// handleAjaxAddToCart, so armed only means "an add was attempted". 7863function armMicroBridge() { 7864MICRO_BRIDGE.armed = true; 7865try { setLoadingCursor(true); } catch (e) { } 7866try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); } catch (e) { } 7867// Long backstop only. The real resolution is onMicroAddSettled(), which 7868// fires the moment handleAjaxAddToCart's own promise settles -- so a slow 7869// upload resolves when it lands rather than being cut off by a fixed timer. 7870try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7871MICRO_BRIDGE.loadTimer = setTimeout(function () { 7872if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return; 7873confirmMicroAddByCount('60s backstop', 0); 7874}, 60000); 7875} 7876// handleAjaxAddToCart returns its fetch promise. When it settles the add is 7877// decided: either our openDrawer signal already fired (handled), or it did not 7878// and we resolve it against the server instead of waiting out the backstop. 7879function onMicroAddSettled() { 7880// ~10s of polling (500ms + 8 x 1200ms), which comfortably covers a real 7881// add. The openDrawer signal normally wins long before this; this only 7882// catches an add that completes without ever reaching the Alpine store. 7883setTimeout(function () { 7884if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return; 7885confirmMicroAddByCount('no in-frame signal yet', 8); 7886}, 500); 7887} 7888function bindMicroPdpAddBridge() { 7889try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7890MICRO_BRIDGE.timer = null; 7891MICRO_BRIDGE.tries = 0; 7892MICRO_BRIDGE.addPatched = false; 7893MICRO_BRIDGE.storePatched = false; 7894// Two hooks, both inside the iframe, both same-origin: 7895// 1. window.handleAjaxAddToCart -- wrapped to ARM on a real add call. 7896// This is the pattern ss-tier-upsell-zephyr.html v1.32 proved out; 7897// binding a button click is what it proved unreliable. 7898// 2. Alpine store cart.openDrawer -- the SUCCESS signal. Zephyr calls 7899// it only in handleAjaxAddToCart's success branch, and overriding it 7900// also keeps the side cart from rendering inside the 960px modal. 7901// handleAjaxAddToCart is an inline PDP script so it lands well before 7902// Alpine boots; poll for each independently. 7903MICRO_BRIDGE.timer = setInterval(function () { 7904MICRO_BRIDGE.tries++; 7905var w = null; 7906try { w = MICRO_PDP.iframe && MICRO_PDP.iframe.contentWindow; } catch (e) { w = null; } 7907if (!w) return; 7908if (!MICRO_BRIDGE.addPatched) { 7909try { 7910if (typeof w.handleAjaxAddToCart === 'function') { 7911if (w.handleAjaxAddToCart.__ckWrapped) { 7912MICRO_BRIDGE.addPatched = true; 7913} else { 7914var origAdd = w.handleAjaxAddToCart; 7915var wrapped = function () { 7916try { armMicroBridge(); } catch (eA) { } 7917var r; 7918try { 7919r = origAdd.apply(this, arguments); 7920} catch (eR) { 7921// Do NOT disarm here: origAdd may have dispatched 7922// the fetch before throwing, so the add can still 7923// land. Resolve it against the server like any other. 7924try { console.warn('[SmartCart] handleAjaxAddToCart threw:', eR && eR.message); } catch (eW) { } 7925try { onMicroAddSettled(); } catch (eS) { } 7926throw eR; 7927} 7928// The precise completion signal -- see onMicroAddSettled. 7929try { 7930// NOTE: on this catalog handleAjaxAddToCart returns a 7931// BOOLEAN, so the thenable branch is the exception, not 7932// the rule -- onMicroAddSettled() must therefore poll. 7933if (r && typeof r.then === 'function') r.then(onMicroAddSettled, onMicroAddSettled); 7934else onMicroAddSettled(); 7935} catch (eT) { } 7936return r; 7937}; 7938wrapped.__ckWrapped = true; 7939// The ATC uses an inline onclick, which resolves the 7940// global at call time -- so replacing it is enough. 7941w.handleAjaxAddToCart = wrapped; 7942MICRO_BRIDGE.addPatched = true; 7943} 7944} 7945} catch (eP1) { } 7946} 7947if (!MICRO_BRIDGE.storePatched) { 7948var st = ckCartStore(w); 7949if (st) { 7950try { 7951if (!st.__ckMicroPatched) { 7952st.__ckMicroPatched = true; 7953var suppress = function () { 7954// The side cart must never render in the configurator. 7955try { st.drawerOpen = false; } catch (e1) { } 7956try { 7957if (w && w.document && w.document.body) w.document.body.style.overflow = ''; 7958} catch (e2) { } 7959if (!MICRO_BRIDGE.armed) return; 7960try { onMicroPdpAddSucceeded(); } catch (e3) { } 7961}; 7962st.openDrawer = suppress; 7963st.toggleDrawer = suppress; 7964} 7965MICRO_BRIDGE.storePatched = true; 7966} catch (eP2) { } 7967} 7968} 7969if (MICRO_BRIDGE.addPatched && MICRO_BRIDGE.storePatched) { 7970try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7971MICRO_BRIDGE.timer = null; 7972return; 7973} 7974if (MICRO_BRIDGE.tries > 120) { // ~6s 7975try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7976MICRO_BRIDGE.timer = null;
7977// Never give up silently -- that is how this class of bug hides. 7978try { 7979console.warn('[SmartCart] micro-PDP bridge gave up after 6s.', 7980'handleAjaxAddToCart patched:', MICRO_BRIDGE.addPatched, 7981'| cart store patched:', MICRO_BRIDGE.storePatched, 7982'| the count fallback still covers the add.'); 7983} catch (eW) { } 7984} 7985}, 50); 7986} 7987function onMicroPdpAddSucceeded() { 7988if (MICRO_BRIDGE.handled) return; // one receipt per configurator open 7989MICRO_BRIDGE.handled = true; 7990MICRO_BRIDGE.armed = false; 7991try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7992// Recreate, in the parent's own URL, the receipt state the old 7993// target="_top" reload used to deliver -- without navigating. 7994try { 7995if (MICRO_PDP.returnUrl) { 7996var ru = new URL(MICRO_PDP.returnUrl, window.location.origin); 7997window.history.replaceState({}, document.title, ru.pathname + ru.search + ru.hash); 7998} 7999} catch (e) { } 8000// If more hard SKUs are queued the next configurator opens in ~350ms -- 8001// don't pop the side cart underneath it. Reveal it only on the last one. 8002var moreQueued = false; 8003try { moreQueued = parseRemainingHardFromUrl().length > 0; } catch (e) { } 8004try { setLoadingCursor(false); } catch (e) { } 8005closeMicroPdp('added'); 8006ckAfterCartMutation(!moreQueued); 8007// Existing receipt pipeline: added-state on the tile, tile swap, strip the 8008// params, and chain the next hard SKU if one is queued. 8009try { handleMicroAddedFromQuery(); } catch (e) { } 8010} 8011function openMicroPdp(productUrl, titleText, productId, remainingHardIds, progressDone, progressTotal) { 8012ensureMicroPdpModal(); 8013MICRO_PDP.lastUrl = productUrl; 8014MICRO_PDP.lastProductId = String(productId || ''); 8015// Progress defaults (if not passed) 8016var progFromUrl = getMicroProgressFromUrl(); 8017if (typeof progressDone !== 'number') progressDone = progFromUrl.done; 8018if (typeof progressTotal !== 'number') progressTotal = progFromUrl.total; 8019MICRO_PDP.progressDone = progressDone || 0; 8020MICRO_PDP.progressTotal = progressTotal || 0; 8021// Paint header progress bar (only shows when total > 1) 8022renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal); 8023// Build a "receipt" return URL so the parent can show success + grey out tile + scroll 8024try { 8025var u = new URL(window.location.href); 8026// Set receipt params (overwrite if they already exist) 8027u.searchParams.set('microAdded', MICRO_PDP.lastProductId); 8028u.searchParams.set('microAddedName', titleText || ''); 8029// Carry micro progress counts 8030if (progressTotal && progressTotal > 0) { 8031u.searchParams.set('microTotal', String(progressTotal)); 8032u.searchParams.set('microDone', String(progressDone || 0)); 8033} else { 8034u.searchParams.delete('microTotal'); 8035u.searchParams.delete('microDone'); 8036} 8037// If we are chaining multiple HARD SKUs, persist the remaining list 8038if (Array.isArray(remainingHardIds) && remainingHardIds.length) { 8039u.searchParams.set('remainingHard', remainingHardIds.join(',')); 8040} else { 8041u.searchParams.delete('remainingHard'); 8042} 8043// Keep anchor consistent so user lands near module area 8044u.hash = 'cart-kit-upsell'; 8045MICRO_PDP.returnUrl = u.toString(); 8046} catch (e) { 8047// Fallback if URL API fails 8048MICRO_PDP.returnUrl = window.location.href + (window.location.search ? '&' : '?') 8049+ 'microAdded=' + encodeURIComponent(MICRO_PDP.lastProductId) 8050+ 'µAddedName=' + encodeURIComponent(titleText || '') 8051+ '#cart-kit-upsell'; 8052} 8053var titleEl = MICRO_PDP.overlay.querySelector('#ck-micropdp-title'); 8054if (titleEl) titleEl.textContent = titleText || 'Customize item'; 8055if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'flex'; 8056// Add a cache-buster so iframe refreshes if same SKU reopened 8057var url = ckSameOrigin(productUrl); 8058url += (url.indexOf('?') > -1 ? '&' : '?') + 'cartUpsellMicro=1&_=' + Date.now(); 8059// Fresh receipt state + a count snapshot for the fallback in armMicroBridge. 8060MICRO_BRIDGE.handled = false; 8061MICRO_BRIDGE.countBefore = null; 8062MICRO_BRIDGE.countGen = (MICRO_BRIDGE.countGen || 0) + 1; 8063try { 8064// Tagged so a slow response from the PREVIOUS chain item (opens ~350ms 8065// apart) can't land here and produce a false-positive receipt. 8066(function (gen) { 8067ckCartCount().then(function (n) { 8068if (gen === MICRO_BRIDGE.countGen) MICRO_BRIDGE.countBefore = n; 8069}); 8070})(MICRO_BRIDGE.countGen); 8071} catch (e) { }
8072MICRO_PDP.iframe.src = url; 8073MICRO_PDP.overlay.classList.add('is-open'); 8074pushUpsellEvent('SmartCart_upsell_micro_pdp_open', { url: productUrl }); 8075} 8076function closeMicroPdp(reason) { 8077if (!MICRO_PDP.overlay) return; 8078var added = (reason === 'added'); 8079var ckWasArmed = MICRO_BRIDGE.armed; 8080try { resetMicroBridge(); } catch (e) { } 8081// Shopper closed while an add was in flight. Don't drop it on the floor -- 8082// check the server before discarding the tile state and the chain. 8083if (!added && ckWasArmed) { 8084try { confirmMicroAddByCount('closed mid-add', 3); } catch (e) { } 8085} 8086try { setLoadingCursor(false); } catch (e) { } 8087try { MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { } 8088MICRO_PDP.overlay.classList.remove('is-open'); 8089// Stop network / reset iframe 8090if (MICRO_PDP.iframe) MICRO_PDP.iframe.src = 'about:blank'; 8091if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 8092// Tell anything listening that the micro PDP closed 8093try { 8094document.dispatchEvent(new CustomEvent('smartcart:microPdpClosed', { detail: { reason: reason || 'close' } })); 8095} catch (e) { } 8096// CRITICAL: refresh cart UI and un-stick the SmartCart state 8097Promise.resolve() 8098.then(function () { 8099// Force the same refresh path we use after adds 8100// 1) Reload Magento customer-data cart section (minicart + cart counters) 8101// 2) Refresh cart DOM fragments (table/totals) if weâre on the cart page 8102// Legacy (pre-Zephyr) refresh path -- a no-op on Zephyr, kept for safety. 8103return reloadCustomerDataCart() 8104.then(refreshCartDomIfOnCartPage) 8105.then(function () { 8106// After an add the caller syncs; on a plain close re-sync 8107// anyway in case the shopper added inside the configurator 8108// by some other route. 8109if (!added) ckAfterCartMutation(false); 8110}); 8111}) 8112.then(function () { 8113// reset CTA back to normal (user cancelled hard sku) 8114try { 8115addBtn.classList.remove('is-loading'); 8116// This tail resolves after handleMicroAddedFromQuery() has run. 8117// On a SUCCESSFUL add the receipt pipeline owns the status line, 8118// the progress bar and the tile grid -- buildGrid() in particular 8119// would re-render over replaceAddedTileWithNext()'s swap. Touch 8120// none of it unless the shopper cancelled. 8121if (!added) { 8122addBtn.disabled = false; 8123addBtn.textContent = 'Add to Cart'; 8124if (statusEl) statusEl.textContent = 'No worries â you can keep shopping or add something else.'; 8125resetAddProgressUI(); 8126} 8127// Safe on BOTH paths and required on the added one: derives the 8128// CTA's disabled state and label from the live selection, so a 8129// completed add leaves it correctly disabled rather than enabled 8130// with nothing selected. 8131updatePriceUI(); 8132} catch (e) { } 8133// Rebuild tiles so "already in cart" suppression is correct 8134if (!added) { try { buildGrid(); } catch (e) { } } 8135}) 8136.catch(function () { 8137// Even if refresh fails, never leave the UI stuck 8138try { 8139addBtn.disabled = false; 8140addBtn.classList.remove('is-loading'); 8141addBtn.textContent = 'Add to Cart'; 8142} catch (e) { } 8143}); 8144} 8145// ---------------------------------------------------- 8146// 8) Selection state 8147// ---------------------------------------------------- 8148var selectedPrimaryIds = new Set(); // stamp IDs OR notary product IDs (strings) 8149var selectedInkColors = new Set(); 8150var inkChoiceMade = false; 8151// Track tiles that have been added via Micro PDP (so we can grey them out) 8152var microAddedIds = new Set(); 8153var ckSeenTileIds = new Set(); 8154// Defaults 8155var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0); 8156if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId)); 8157else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id); 8158// ---------------------------------------------------- 8159// 9) Build grid (Accent or Notary) 8160// ---------------------------------------------------- 8161var NOTARY_HYDRATED = new Map(); // productId -> {productId, productUrl, name, img, price} 8162function buildGrid() { 8163// Build cart signals for duplicate suppression (4C) 8164var __cartPaths = Object.create(null); 8165try { __cartPaths = getCartProductPathSet(); } catch (eCP0) { } 8166stampGrid.innerHTML = ''; 8167// Accent stamps (unchanged) 8168if (!useTiles) { 8169for (var i = 0; i < STAMP_OPTIONS.length; i++) { 8170var opt = STAMP_OPTIONS[i]; 8171var card = document.createElement('button'); 8172card.type = 'button'; 8173card.className = 'kit-stamp-card'; 8174card.setAttribute('data-kit-primary', opt.id); 8175card.innerHTML = 8176'<div class="kit-stamp-chip">' 8177+ '<div class="kit-stamp-chip__circle">' 8178+ '<img class="kit-stamp-chip__image"' 8179+ ' src="' + opt.imgSrc + '"' 8180+ ' alt="' + (opt.imgAlt || '') + '"' 8181+ (opt.imgStyle ? ' style="' + opt.imgStyle + '"' : '') 8182+ '/>' 8183+ '</div>' 8184+ '</div>'; 8185if (selectedPrimaryIds.has(opt.id)) card.classList.add('is-selected'); 8186stampGrid.appendChild(card); 8187} 8188// Step 1: sync tile state store after grid render 8189try { TileStore.initFromDom(mount); } catch (e) { } 8190return; 8191} 8192// Notary product tiles (PDP fetched) 8193// Empty-state: if everything is already in cart (or filtered), show "All set" instead of a blank module. 8194if (!TILE_ITEMS || TILE_ITEMS.length === 0) { 8195// Nothing to build. (The old code here appended an "all set" note next 8196// to an undefined `tilesGrid` and tried to disable a #kit-add-btn that 8197// has never existed in this markup -- both silent no-ops.) 8198ckSyncModuleEmptyState(); 8199return; 8200} 8201for (var j = 0; j < TILE_ITEMS.length; j++) { 8202(function (raw) { 8203var pid = String(raw.productId || ''); 8204// Keep a reference so we can apply persona rules later (micro PDP gating, etc.) 8205TILE_ITEM_BY_ID[pid] = raw; 8206var card = document.createElement('button'); 8207card.type = 'button'; 8208card.className = 'kit-stamp-card kit-product-card'; 8209card.setAttribute('data-kit-primary', pid); 8210card.setAttribute('data-sku', pid); 8211card.setAttribute('data-kit-url', raw.productUrl || ''); 8212if (microAddedIds.has(pid)) { 8213card.classList.add('is-added'); 8214} 8215card.innerHTML = 8216'<div class="kit-product-thumb-tile"><span style="font-size:12px;color:#888;">Loadingâ¦</span></div>' 8217+ '<div class="kit-product-meta">' 8218+ '<p class="kit-product-name">Loadingâ¦</p>' 8219+ '<div class="kit-product-price">â</div>' 8220+ '<div class="kit-product-sub">' + (persona.tileSubtext || '') + '</div>' 8221+ '</div>'; 8222if (selectedPrimaryIds.has(pid)) card.classList.add('is-selected'); 8223stampGrid.appendChild(card); 8224fetchPdpData(raw.productUrl).then(function (data) { 8225// Guard: this tile may have been swapped; ignore stale hydration 8226if (String(card.getAttribute('data-kit-primary') || '') !== String(pid)) return; 8227// Quality gate: this URL did not resolve to a real product page, 8228// so do NOT render whatever came back. Substitute an approved 8229// alternative the shopper doesn't already have, else drop the slot. 8230if (!ckIsUsableTileData(data)) { 8231try { console.warn('[SmartCart] tile ' + pid + ' did not resolve to a product page; substituting.'); } catch (eW) { } 8232var alt = ckPickFallbackTile(); 8233if (alt) ckRepaintCardAs(card, alt); 8234else ckDropTile(card); 8235return; 8236} 8237var hydrated = { 8238productId: pid, 8239productUrl: raw.productUrl, 8240name: data.name || 'Notary add-on', 8241img: data.img || null, 8242price: (data.price != null ? Number(data.price) : null) 8243}; 8244try { hydrated.sku = data.sku; } catch (eSk1) { } 8245NOTARY_HYDRATED.set(pid, hydrated); 8246var nameEl = card.querySelector('.kit-product-name'); 8247var priceEl = card.querySelector('.kit-product-price'); 8248var thumbEl = card.querySelector('.kit-product-thumb-tile'); 8249if (nameEl) nameEl.textContent = hydrated.name; 8250if (priceEl) priceEl.textContent = (hydrated.price != null ? ('$' + hydrated.price.toFixed(2)) : 'See price in cart'); 8251if (thumbEl) { 8252if (hydrated.img) thumbEl.innerHTML = '<img src="' + hydrated.img + '" alt="" />'; 8253else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 8254} 8255if (selectedPrimaryIds.has(pid)) updatePriceUI(); 8256}); 8257})(TILE_ITEMS[j]); 8258} 8259fireTilesImpressionOnce(); 8260// Step 1: sync tile state store after grid render 8261try { TileStore.initFromDom(mount); } catch (e) { } 8262ckSyncModuleEmptyState(); 8263} 8264// ---------------------------------------------------- 8265// 10) Pricing 8266// ---------------------------------------------------- 8267var priceLabelEl = upsell.querySelector('#kit-price-label'); 8268var priceNewEl = upsell.querySelector('#kit-price-new'); 8269function computeTotals() { 8270var inkCount = selectedInkColors.size; 8271if (!useTiles) { 8272var stampCount = selectedPrimaryIds.size; 8273return { 8274primaryCount: stampCount, 8275inkCount: inkCount, 8276total: (stampCount * STAMP_UNIT_PRICE) + (inkCount * INK_PRICE) 8277}; 8278} 8279// Notary: sum selected notary items (real PDP price if fetched; otherwise 0 until loaded) 8280var total = 0; 8281var count = 0; 8282selectedPrimaryIds.forEach(function (pid) { 8283var it = NOTARY_HYDRATED.get(String(pid)); 8284count += 1; 8285total += (it && it.price != null) ? Number(it.price) : 0; 8286}); 8287total += inkCount * INK_PRICE; 8288return { primaryCount: count, inkCount: inkCount, total: total }; 8289} 8290function updatePriceUI() { 8291var totals = computeTotals(); 8292if (!totals.primaryCount) { 8293priceLabelEl.textContent = 'Total:'; 8294priceNewEl.textContent = '$0.00'; 8295addBtn.disabled = true; 8296addBtn.textContent = 'Add to Cart'; 8297return; 8298} 8299priceLabelEl.textContent = 'Total:'; 8300priceNewEl.textContent = '$' + totals.total.toFixed(2); 8301// CTA label: if any selected item requires configuration, guide user into Micro PDP. 8302var hasHard = false;
8303try { 8304Array.from(selectedPrimaryIds).some(function (pid) { 8305var raw = (TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[String(pid)]) ? TILE_ITEM_BY_ID[String(pid)] : null; 8306if (requiresMicroPdp(pid, raw)) { hasHard = true; return true; } 8307return false; 8308}); 8309} catch (e) { } 8310addBtn.textContent = hasHard ? 'Customize & Add' : 'Add to Cart'; 8311addBtn.disabled = false; 8312} 8313// ---------------------------------------------------- 8314// 11) Build + init 8315// ---------------------------------------------------- 8316buildGrid(); 8317updatePriceUI(); 8318pushUpsellEvent('SmartCart_upsell_shown', { matched: activePersonaKey !== 'generic' }); 8319// ---------------------------------------------------- 8320// 12) Interactions 8321// ---------------------------------------------------- 8322header.addEventListener('click', function () { 8323var isOpen = upsell.classList.toggle('cart-kit-upsell--open'); 8324body.hidden = !isOpen; 8325}); 8326// Primary selection (stamps or notary items) 8327stampGrid.addEventListener('click', function (evt) { 8328var card = evt.target.closest('.kit-stamp-card'); 8329if (!card) return; if (card.classList && card.classList.contains('is-complete')) return; 8330var id = String(card.getAttribute('data-kit-primary') || ''); 8331if (!id) return; 8332// ---- TRACK: tile click ---- 8333try { 8334if (useTiles) { 8335// find tile position in TILE_ITEMS 8336var pos = -1; 8337var rawTile = null; 8338for (var i = 0; i < TILE_ITEMS.length; i++) { 8339if (String(TILE_ITEMS[i].productId) === String(id)) { 8340pos = i + 1; 8341rawTile = TILE_ITEMS[i]; 8342break; 8343} 8344} 8345var hydrated = NOTARY_HYDRATED.get(String(id)); 8346pushUpsellEvent('SmartCart_upsell_tile_click', { 8347persona: activePersonaKey || 'generic', 8348upsell_mode: 'tiles', 8349product_id: id, 8350position: pos, 8351product_name: hydrated && hydrated.name ? hydrated.name : undefined, 8352product_price: hydrated && hydrated.price != null ? hydrated.price : undefined, 8353product_url: rawTile && rawTile.productUrl ? rawTile.productUrl : undefined, 8354requires_micro: rawTile ? requiresMicroPdp(id, rawTile) : undefined, 8355already_selected: selectedPrimaryIds.has(id) 8356}); 8357} else { 8358// Accent stamp click 8359pushUpsellEvent('SmartCart_upsell_accent_click', { 8360stamp_id: id 8361}); 8362} 8363} catch (e) { } 8364// ---------------------------------------------------- 8365// Tile tracking + micro PDP routing (tiles only) 8366// ---------------------------------------------------- 8367var raw = null; 8368if (useTiles) { 8369for (var k = 0; k < TILE_ITEMS.length; k++) { 8370if (String(TILE_ITEMS[k].productId) === String(id)) { raw = TILE_ITEMS[k]; break; } 8371} 8372if (raw && raw.productUrl) { 8373// If we already know it's micro (tile config, hard override, or cached autodetect) 8374if (requiresMicroPdp(id, raw)) { 8375// Hard SKU: tile click should ONLY toggle selection. 8376// Do not open Micro PDP here; Micro PDP opens only from the main CTA. 8377} 8378// If we don't have PDP cached yet, fetch once and decide (airbag) 8379if (!PDP_CACHE.has(raw.productUrl)) { 8380evt.preventDefault(); 8381evt.stopPropagation(); 8382setLoadingCursor(true); 8383fetchPdpData(raw.productUrl).then(function (data) { 8384setLoadingCursor(false); 8385// If autodetect says "micro", open modal 8386if (data && data.requiresMicro) { 8387// NOTE: Do NOT auto-open Micro PDP on tile click. 8388// We only open Micro PDP when the user explicitly clicks the CTA to add. 8389pushUpsellEvent('SmartCart_upsell_tile_micro_detected', { productId: id, productUrl: raw.productUrl }); 8390} 8391// Otherwise proceed with normal select toggle now 8392var willSelect = !selectedPrimaryIds.has(id); 8393if (willSelect) { 8394selectedPrimaryIds.add(id); 8395card.classList.add('is-selected'); 8396} else { 8397selectedPrimaryIds.delete(id); 8398card.classList.remove('is-selected'); 8399} 8400pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), { 8401productId: id, 8402selected_count: selectedPrimaryIds.size 8403}); 8404updatePriceUI(); 8405}).catch(function () { 8406setLoadingCursor(false); 8407// On failure, fall back to normal select so UX doesn't dead-end 8408var willSelect2 = !selectedPrimaryIds.has(id); 8409if (willSelect2) { 8410selectedPrimaryIds.add(id); 8411card.classList.add('is-selected'); 8412} else { 8413selectedPrimaryIds.delete(id); 8414card.classList.remove('is-selected'); 8415} 8416pushUpsellEvent('SmartCart_upsell_tile_select_fallback', { 8417productId: id, 8418// This path fires on DESELECT as well as select. 8419// Without this flag the GA4 mirror would count a 8420// deselect as a select_item. 8421selected: willSelect2, 8422selected_count: selectedPrimaryIds.size 8423}); 8424updatePriceUI(); 8425}); 8426return; 8427} 8428} 8429} 8430var willSelect = !selectedPrimaryIds.has(id); 8431if (willSelect) { 8432selectedPrimaryIds.add(id); 8433card.classList.add('is-selected'); 8434} else { 8435selectedPrimaryIds.delete(id); 8436card.classList.remove('is-selected'); 8437} 8438if (useTiles) { 8439pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), { 8440productId: id, 8441selected_count: selectedPrimaryIds.size 8442}); 8443} else { 8444pushUpsellEvent('SmartCart_upsell_accent_' + (willSelect ? 'selected' : 'deselected'), { 8445stamp_id: id, 8446selected_count: selectedPrimaryIds.size 8447}); 8448} 8449updatePriceUI(); 8450}); 8451// Ink multi-select with "none" 8452if (swatchList) { 8453swatchList.addEventListener('click', function (evt) { 8454var swatch = evt.target.closest('.kit-swatch'); 8455if (!swatch) return; 8456if (swatch.style.display === 'none') return; 8457var colorKey = swatch.getAttribute('data-kit-color'); 8458inkChoiceMade = true;
8459if (colorKey === 'none') { 8460selectedInkColors.clear(); 8461var all = swatchList.querySelectorAll('.kit-swatch'); 8462for (var i = 0; i < all.length; i++) { 8463var s = all[i]; 8464if (s.style.display === 'none') continue; 8465s.classList.toggle('is-selected', s.getAttribute('data-kit-color') === 'none'); 8466} 8467} else { 8468if (selectedInkColors.has(colorKey)) selectedInkColors.delete(colorKey); 8469else selectedInkColors.add(colorKey); 8470var hasAnyInk = selectedInkColors.size > 0; 8471var all2 = swatchList.querySelectorAll('.kit-swatch'); 8472for (var j = 0; j < all2.length; j++) { 8473var b = all2[j]; 8474if (b.style.display === 'none') continue; 8475var key = b.getAttribute('data-kit-color'); 8476if (key === 'none') b.classList.toggle('is-selected', !hasAnyInk); 8477else b.classList.toggle('is-selected', selectedInkColors.has(key)); 8478} 8479} 8480updatePriceUI(); 8481}); 8482} 8483if (hideBtn) { 8484hideBtn.addEventListener('click', function (evt) { 8485evt.preventDefault(); 8486upsell.classList.remove('cart-kit-upsell--open'); 8487body.hidden = true; 8488upsell.scrollIntoView({ behavior: 'smooth', block: 'start' }); 8489}); 8490} 8491// ---------------------------------------------------- 8492// 13) Add-to-cart progress bar helpers (ONLY progress bar) 8493// ---------------------------------------------------- 8494function resetAddProgressUI() { 8495if (progressWrap) progressWrap.style.display = 'none'; 8496if (progressFill) progressFill.style.width = '0%'; 8497if (progressPct) progressPct.textContent = '0%'; 8498if (progressText) progressText.textContent = ''; 8499} 8500function setAddProgress(done, total) { 8501if (!progressWrap || !progressFill) return; 8502var pct = total ? Math.round((done / total) * 100) : 0; 8503progressWrap.style.display = ''; 8504progressFill.style.width = pct + '%'; 8505if (progressPct) progressPct.textContent = pct + '%'; 8506if (progressText) progressText.textContent = 'Adding ' + done + ' of ' + total + ' itemsâ¦'; 8507if (statusEl) statusEl.textContent = 'Added ' + done + ' of ' + total + ' items'; 8508} 8509// ---------------------------------------------------- 8510// 14) Add to Cart 8511// ---------------------------------------------------- 8512function postToCart(params) { 8513return fetch('/checkout/cart/add', { 8514method: 'POST', 8515headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 8516body: params.toString(), 8517credentials: 'same-origin' 8518}).then(function (res) { 8519if (!res.ok) throw new Error('Add to cart failed: ' + res.status); 8520return res; 8521}); 8522} 8523// ---------------------------------------------------- 8524// Cart sync helpers (run after async adds so UI + tile logic are up-to-date) 8525// ---------------------------------------------------- 8526function reloadCustomerDataCart() { 8527// Zephyr: /customer/section/load/ 404s on this build (no M2 customer-data) 8528// and the Alpine store is the real cart state. Skip the hop entirely. 8529if (ckCartStore(window)) return Promise.resolve(); 8530return new Promise(function (resolve) { 8531try { 8532if (window.require) { 8533window.require(['Magento_Customer/js/customer-data'], function (customerData) { 8534try { 8535var r = customerData.reload(['cart'], true); 8536if (r && r.always) return r.always(function () { resolve(); }); 8537resolve(); 8538} catch (e) { resolve(); } 8539}, function () { resolve(); }); 8540return; 8541} 8542fetch('/customer/section/load/?sections=cart&force_new_section_timestamp=true', { 8543credentials: 'same-origin' 8544}).then(function () { resolve(); }).catch(function () { resolve(); }); 8545} catch (e) { resolve(); } 8546}); 8547} 8548function refreshCartDomIfOnCartPage() { 8549// Zephyr: the cart page is Alpine-rendered. This swaps innerHTML on 8550// .cart-summary / [data-block="cart-summary"] if either matches, which 8551// would destroy live Alpine bindings in the Order Summary -- and it costs 8552// a full-page HTML fetch every time. ckAfterCartMutation() is the Zephyr 8553// path; this one is for the legacy theme only. 8554if (ckCartStore(window)) return Promise.resolve(); 8555return new Promise(function (resolve) { 8556try { 8557// Cart page fragments we attempt to refresh. Selectors vary by theme. 8558var table = document.querySelector('#shopping-cart-table'); 8559var totals = document.querySelector('#cart-totals') 8560|| document.querySelector('.cart-summary') 8561|| document.querySelector('.cart-totals') 8562|| document.querySelector('[data-block="cart-summary"]') 8563|| document.querySelector('[data-role="cart-totals"]'); 8564if (!table && !totals) return resolve(); 8565// Bust caches (some themes/CDNs will otherwise hand back stale totals) 8566var url = String(window.location.href || '').split('#')[0]; 8567url += (url.indexOf('?') > -1 ? '&' : '?') + 'ckts=' + Date.now(); 8568fetch(url, { credentials: 'same-origin', cache: 'no-store' }) 8569.then(function (r) { return r.text(); }) 8570.then(function (html) { 8571try { 8572var doc = new DOMParser().parseFromString(html, 'text/html'); 8573var newTable = doc.querySelector('#shopping-cart-table'); 8574if (table && newTable) table.innerHTML = newTable.innerHTML; 8575// Totals / summary fragment (try multiple fallbacks)
8576var newTotals = doc.querySelector('#cart-totals') 8577|| doc.querySelector('.cart-summary') 8578|| doc.querySelector('.cart-totals') 8579|| doc.querySelector('[data-block="cart-summary"]') 8580|| doc.querySelector('[data-role="cart-totals"]'); 8581if (totals && newTotals) totals.innerHTML = newTotals.innerHTML; 8582try { document.body.dispatchEvent(new Event('contentUpdated')); } catch (e) { } 8583} catch (e2) { } 8584resolve(); 8585}) 8586.catch(function () { resolve(); }); 8587} catch (e) { resolve(); } 8588}); 8589} 8590function syncCartAfterAdds() { 8591return reloadCustomerDataCart() 8592.then(refreshCartDomIfOnCartPage) 8593.catch(function () { /* silent */ }); 8594} 8595addBtn.addEventListener('click', function () { 8596if (!selectedPrimaryIds.size) { 8597alert(useTiles ? 'Please choose at least one add-on.' : 'Please choose at least one stamp design.'); 8598return; 8599} 8600if (swatchList && !inkChoiceMade) { 8601alert('Please select an ink pad color or select you already have an ink pad'); 8602return; 8603} 8604var formKeyInput = document.querySelector('input[name="form_key"]'); 8605var formKey = formKeyInput ? formKeyInput.value : null; 8606if (!formKey) { 8607console.error('Cart kit upsell: form_key not found.'); 8608alert('Sorry, we could not add the kit to your cart. Please refresh and try again.'); 8609return; 8610} 8611// Baseline for the post-add reality check (see ckVerifySimpleAdd). 8612var ckSimpleCountBefore = null; 8613try { ckCartCount().then(function (n) { ckSimpleCountBefore = n; }); } catch (eCB) { } 8614var primaryArr = Array.from(selectedPrimaryIds); 8615var inkArr = Array.from(selectedInkColors); 8616// ---------------------------------------------------- 8617// Hard SKU gating: 8618// - NEVER add "hard" products directly (they need options/uploads) 8619// - Only open Micro PDP when user clicks the CTA 8620// ---------------------------------------------------- 8621var hardPrimaryArr = []; 8622var simplePrimaryArr = []; 8623primaryArr.forEach(function (pid) { 8624var rawTile = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? TILE_ITEM_BY_ID[String(pid)] : null; 8625if (requiresMicroPdp(pid, rawTile)) { 8626hardPrimaryArr.push(pid); 8627} else { 8628simplePrimaryArr.push(pid); 8629} 8630}); 8631// Only count items we can actually add via POST chain (hard items are handled via Micro PDP) 8632var totalItemsCount = simplePrimaryArr.length + inkArr.length; 8633// If the user only selected hard items, we skip the POST chain and jump straight to Micro PDP. 8634var willOpenMicroAfterChain = hardPrimaryArr.length > 0; 8635pushUpsellEvent('SmartCart_upsell_add_clicked', { 8636primary_count: simplePrimaryArr.length, 8637hard_primary_count: hardPrimaryArr.length, 8638ink_count: inkArr.length, 8639total_count: totalItemsCount 8640}); 8641var completedCount = 0; 8642// If there's nothing to POST (only hard items), open Micro PDP immediately. 8643if (totalItemsCount === 0 && willOpenMicroAfterChain) { 8644var firstHard = hardPrimaryArr[0]; 8645var rawFirst = TILE_ITEM_BY_ID[String(firstHard)] || {}; 8646var hydratedFirst = NOTARY_HYDRATED.get(String(firstHard)); 8647var titleFirst = (hydratedFirst && hydratedFirst.name) ? hydratedFirst.name : (rawFirst.title || rawFirst.name || 'Customize item'); 8648// Seed Micro PDP progress for HARD-only flows 8649// done = 0 (nothing added yet), total = number of hard items selected 8650var progDone0 = 0; 8651var progTotal0 = hardPrimaryArr.length; 8652openMicroPdp(rawFirst.productUrl, titleFirst, firstHard, hardPrimaryArr.slice(1), progDone0, progTotal0); 8653return; 8654} 8655addBtn.disabled = true; 8656addBtn.classList.add('is-loading'); 8657addBtn.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' itemsâ¦') : 'Adding itemâ¦'; 8658if (statusEl) statusEl.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' items to your cartâ¦') : 'Adding item to your cartâ¦'; 8659// show + init progress bar 8660setAddProgress(0, totalItemsCount); 8661var chain = Promise.resolve(); 8662// Add primary selections 8663simplePrimaryArr.forEach(function (id) { 8664chain = chain.then(function () { 8665var params = new URLSearchParams(); 8666params.append('form_key', formKey); 8667if (!useTiles) { 8668params.append('product', id); 8669params.append('qty', '1'); 8670params.append('options[' + ACCENT_SIZE_OPTION_ID + '][]', ACCENT_SIZE_VALUE_ID); 8671params.append('options[' + ACCENT_INK_OPTION_ID + ']', ACCENT_INK_NO_THANKS_VALUE_ID); 8672} else { 8673params.append('product', id); 8674params.append('qty', '1'); 8675} 8676return postToCart(params).then(function () { 8677completedCount += 1; 8678setAddProgress(completedCount, totalItemsCount); 8679}); 8680}); 8681}); 8682// Add ink pads
8683inkArr.forEach(function (colorKey) { 8684chain = chain.then(function () { 8685var colorValueId = INK_COLOR_VALUES[colorKey]; 8686if (!colorValueId) throw new Error('Unknown ink color key: ' + colorKey); 8687var inkParams = new URLSearchParams(); 8688inkParams.append('form_key', formKey); 8689inkParams.append('product', INK_PARENT_ID); 8690inkParams.append('qty', '1'); 8691inkParams.append('options[' + INK_SIZE_OPTION_ID + ']', INK_SIZE_VALUE_ID); 8692inkParams.append('options[' + INK_COLOR_OPTION_ID + ']', colorValueId); 8693return postToCart(inkParams).then(function () { 8694completedCount += 1; 8695setAddProgress(completedCount, totalItemsCount); 8696}); 8697}); 8698}); 8699chain 8700.then(function () { 8701pushUpsellEvent('SmartCart_upsell_added', { 8702// Per-product ids drive the GA4 add_to_cart mirror. 8703product_ids: simplePrimaryArr.map(function (p) { return String(p); }), 8704ink_colors: inkArr.slice(), 8705primary_count: simplePrimaryArr.length, 8706hard_primary_count: hardPrimaryArr.length, 8707ink_count: inkArr.length, 8708total_count: totalItemsCount 8709}); 8710// CRITICAL: sync cart state + DOM BEFORE swap/dedupe logic runs 8711return syncCartAfterAdds().then(function () { 8712if (willOpenMicroAfterChain) { 8713// The simple items are already in the cart but the configurator 8714// is about to take over the screen -- push them into both cart 8715// trees now, or the drawer, badge and cart column sit stale for 8716// as long as the shopper spends configuring. 8717try { ckAfterCartMutation(false); } catch (eZ) { } 8718// Mark the items we successfully added (simple + ink) without reloading. 8719try { 8720simplePrimaryArr.forEach(function (pid) { 8721var el = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]'); 8722if (el) { 8723el.classList.remove('is-selected'); 8724el.classList.add('is-added'); 8725} 8726try { microAddedIds.add(String(pid)); } catch (eM0) { } 8727try { selectedPrimaryIds.delete(pid); } catch (eD0) { } 8728// Swap AFTER cart sync so in-cart suppression works 8729setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR0) { } }, 800); 8730}); 8731} catch (e) { } 8732try { 8733// Clear ink selections after adding ink pads 8734selectedInkColors.clear(); 8735inkChoiceMade = false; 8736if (swatchList) { 8737var allSw = swatchList.querySelectorAll('.kit-swatch'); 8738for (var s = 0; s < allSw.length; s++) allSw[s].classList.remove('is-selected'); 8739} 8740} catch (e2) { } 8741addBtn.disabled = false; 8742addBtn.classList.remove('is-loading'); 8743addBtn.textContent = 'Customize & Add'; 8744if (statusEl) statusEl.textContent = 'Added to cart. Next, customize your selected item.'; 8745// Open Micro PDP for the first hard item selected 8746var firstHard2 = hardPrimaryArr[0]; 8747var rawFirst2 = TILE_ITEM_BY_ID[String(firstHard2)] || {}; 8748var hydratedFirst2 = NOTARY_HYDRATED.get(String(firstHard2)); 8749var titleFirst2 = (hydratedFirst2 && hydratedFirst2.name) ? hydratedFirst2.name : (rawFirst2.title || rawFirst2.name || 'Customize item'); 8750// Seed Micro PDP progress for mixed flows: 8751// done = count of items already added via POST chain (simple + ink) 8752// total = done + remaining hard items to configure 8753var progDone1 = totalItemsCount; 8754var progTotal1 = totalItemsCount + hardPrimaryArr.length; 8755openMicroPdp(rawFirst2.productUrl, titleFirst2, firstHard2, hardPrimaryArr.slice(1), progDone1, progTotal1); 8756pushUpsellEvent('SmartCart_upsell_micro_pdp_open_from_cta', { 8757productId: firstHard2, 8758remaining_hard_count: hardPrimaryArr.length 8759}); 8760// Keep progress visible but "complete" the chain we ran 8761setAddProgress(totalItemsCount, totalItemsCount); 8762return; // prevent redirect 8763} 8764// If we are on the full cart page, do a hard refresh so Magento totals/summary 8765// update correctly (these often won't update reliably via partial DOM swaps). 8766// We preserve the success-state UX via ?accentAdded=1. 8767// ---------------------------------------------------- 8768// Zephyr: the rest of this site mutates the cart in place, so do 8769// the same here instead of reloading. ckAfterCartMutation refreshes 8770// BOTH cart trees (drawer store + cart-page component) and pops the 8771// side cart. It returns false only when the Zephyr store is absent 8772// (legacy theme) -- then, and only then, fall back to the old 8773// ?accentAdded=1 reload. Returning true lets the in-place success UX 8774// below finally run; that block was unreachable for as long as this 8775// branch always reloaded. 8776// ---------------------------------------------------- 8777try { 8778var onCartPage = document.body.classList.contains('checkout-cart-index') 8779|| /\/checkout\/cart(\/|$)/.test(window.location.pathname) 8780|| !!document.querySelector('[data-cart-item-id]') 8781|| !!document.querySelector('#shopping-cart-table') 8782|| !!document.querySelector('form#form-validate') 8783|| !!document.querySelector('.cart-container') 8784|| !!document.querySelector('[data-block="cart-summary"]') 8785|| !!document.querySelector('.cart-summary'); 8786if (onCartPage && !ckAfterCartMutation(true)) { 8787try { 8788var u2 = new URL(window.location.href); 8789u2.searchParams.set('accentAdded', '1'); 8790window.location.href = u2.toString(); 8791} catch (eU2) { 8792window.location.reload(); 8793} 8794return; 8795} 8796} catch (eCPR) { } 8797// Reality check: confirm the cart actually changed before the 8798// success UX below is allowed to stand. 8799try { ckVerifySimpleAdd(ckSimpleCountBefore); } catch (eVS) { } 8800// ---------------------------------------------------- 8801// No hard items left â keep the module live and refresh tiles in-place (no page reload) 8802// ---------------------------------------------------- 8803try {
8804// Mark added + swap tiles (stable slots) 8805simplePrimaryArr.forEach(function (pid) { 8806try { microAddedIds.add(String(pid)); } catch (eM) { } 8807try { 8808var el2 = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]'); 8809if (el2) { 8810el2.classList.remove('is-selected'); 8811el2.classList.add('is-added'); 8812} 8813} catch (eC) { } 8814try { selectedPrimaryIds.delete(pid); } catch (eD) { } 8815// Swap AFTER cart sync so in-cart suppression works 8816setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR) { } }, 800); 8817}); 8818} catch (eS3) { } 8819try { 8820// Clear ink selections after adding ink pads 8821selectedInkColors.clear(); 8822inkChoiceMade = false; 8823if (swatchList) { 8824var allSw3 = swatchList.querySelectorAll('.kit-swatch'); 8825for (var s3 = 0; s3 < allSw3.length; s3++) allSw3[s3].classList.remove('is-selected'); 8826// restore "none" default if visible 8827var noneBtn3 = swatchList.querySelector('.kit-swatch[data-kit-color="none"]'); 8828if (noneBtn3 && noneBtn3.style.display !== 'none') { 8829noneBtn3.classList.add('is-selected'); 8830inkChoiceMade = true; 8831} 8832} 8833} catch (eS4) { } 8834// Success toast 8835try { 8836var toast2 = document.getElementById('kit-success-toast'); 8837if (toast2) { 8838var msg = (totalItemsCount > 1) ? ('â Added ' + totalItemsCount + ' items to cart') : 'â Added to cart'; 8839toast2.textContent = msg; 8840toast2.classList.add('is-visible'); 8841setTimeout(function () { toast2.classList.remove('is-visible'); }, 3000); 8842} 8843} catch (eT2) { } 8844// Reset CTA + progress 8845addBtn.disabled = false; 8846addBtn.classList.remove('is-loading'); 8847addBtn.textContent = 'Add to Cart'; 8848if (statusEl) statusEl.textContent = 'Added to cart. Want to add another?'; 8849resetAddProgressUI(); 8850updatePriceUI(); 8851// Keep module open to encourage additional add-ons 8852upsell.classList.add('cart-kit-upsell--open'); 8853body.hidden = false; 8854}); 8855}) 8856.catch(function (err) { 8857console.error('Cart kit upsell error:', err); 8858var ckErrMsg = 'Sorry, we could not add the kit to your cart. Please try again.'; 8859try { 8860var ckFlash = (window.Alpine && typeof window.Alpine.store === 'function') 8861? window.Alpine.store('flash') : null; 8862if (ckFlash && typeof ckFlash.show === 'function') ckFlash.show('error', ckErrMsg); 8863else alert(ckErrMsg); 8864} catch (eFl) { alert(ckErrMsg); } 8865addBtn.disabled = false; 8866addBtn.classList.remove('is-loading'); 8867addBtn.textContent = 'Add to Cart'; 8868if (statusEl) statusEl.textContent = ''; 8869// hide progress bar on error 8870resetAddProgressUI(); 8871}); 8872}); 8873// ---------------------------------------------------- 8874// Cart sync helpers (run after async adds so UI + tile logic are up-to-date) 8875// ---------------------------------------------------- 8876// 15) Success + Add More 8877// ---------------------------------------------------- 8878function resetSelections() { 8879selectedPrimaryIds.clear(); 8880selectedInkColors.clear(); 8881inkChoiceMade = false; 8882var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0); 8883if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId)); 8884else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id); 8885buildGrid(); 8886if (swatchList) { 8887var sw = swatchList.querySelectorAll('.kit-swatch'); 8888for (var i = 0; i < sw.length; i++) sw[i].classList.remove('is-selected'); 8889// Default to "none" (I have an ink pad) so user isn't forced into an alert loop 8890var noneBtn = swatchList.querySelector('.kit-swatch[data-kit-color="none"]'); 8891if (noneBtn && noneBtn.style.display !== 'none') { 8892noneBtn.classList.add('is-selected'); 8893// keep selectedInkColors empty, but mark a choice as made 8894inkChoiceMade = true; 8895} 8896} 8897addBtn.disabled = false; 8898addBtn.classList.remove('is-loading'); 8899addBtn.textContent = 'Add to Cart'; 8900if (statusEl) statusEl.textContent = ''; 8901resetAddProgressUI(); 8902updatePriceUI(); 8903} 8904function handleAddMoreFromQuery() { 8905if (!accentAddMoreQS) return; 8906upsell.classList.add('cart-kit-upsell--open'); 8907body.hidden = false; 8908resetSelections(); 8909try { 8910var href = window.location.href; 8911var clean = href 8912.replace(/([?&])accentAdded=1(?=(&|#|$))/, '$1') 8913.replace(/[?&]#/, '#') 8914.replace(/[?&]$/, ''); 8915window.history.replaceState({}, document.title, clean); 8916} catch (e) { } 8917} 8918function applySuccessStateFromQuery() { 8919if (!accentAddedQS) return;
8920var headerText = upsell.querySelector('#cart-kit-header-text'); 8921if (headerText) { 8922headerText.innerHTML = 8923'<span class="cart-kit-upsell__success-inline">' 8924+ 'â Items added to cart!' 8925+ ' <span style="color:#000; text-decoration: underline;">Add more?</span>' 8926+ '</span>'; 8927} 8928upsell.classList.remove('cart-kit-upsell--open'); 8929body.hidden = true; 8930try { 8931var clean2 = window.location.href.replace(/([?&])accentAdded=1(&|$)/, '$1').replace(/[?&]$/, ''); 8932window.history.replaceState({}, document.title, clean2); 8933} catch (e) { } 8934} 8935// ---------------------------------------------------- 8936// Micro PDP queue helpers (supports 2+ hard SKUs) 8937// ---------------------------------------------------- 8938function getTileInfoByProductId(pid) { 8939pid = String(pid || '').trim(); 8940if (!pid) return null; 8941var raw = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? (TILE_ITEM_BY_ID[pid] || TILE_ITEM_BY_ID[String(pid)]) : null; 8942if (!raw) return null; 8943var hydrated = (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) ? NOTARY_HYDRATED.get(pid) : null; 8944var title = (hydrated && hydrated.name) ? hydrated.name : (raw.title || raw.name || 'Customize item'); 8945var url = raw.productUrl || raw.url || raw.href || null; 8946if (!url) return null; 8947return { productId: pid, title: title, url: url }; 8948} 8949function parseRemainingHardFromUrl() { 8950try { 8951var u = new URL(window.location.href); 8952var val = (u.searchParams.get('remainingHard') || '').trim(); 8953if (!val) return []; 8954return val.split(',').map(function (s) { return String(s || '').trim(); }).filter(Boolean); 8955} catch (e) { 8956return []; 8957} 8958} 8959function stripRemainingHardParam() { 8960try { 8961var u = new URL(window.location.href); 8962u.searchParams.delete('remainingHard'); 8963u.searchParams.delete('microTotal'); 8964u.searchParams.delete('microDone'); 8965window.history.replaceState({}, document.title, u.toString()); 8966} catch (e) { } 8967} 8968function openNextHardFromRemaining() { 8969var remaining = parseRemainingHardFromUrl(); 8970if (!remaining.length) { 8971stripRemainingHardParam(); 8972return false; 8973} 8974var nextId = remaining.shift(); 8975var info = getTileInfoByProductId(nextId); 8976if (!info) { 8977// If we can't resolve the next item, stop chaining to avoid a loop 8978stripRemainingHardParam(); 8979return false; 8980} 8981// Rebuild the remainingHard param (after removing the one we're about to open) 8982try { 8983var u = new URL(window.location.href); 8984if (remaining.length) u.searchParams.set('remainingHard', remaining.join(',')); 8985else u.searchParams.delete('remainingHard'); 8986window.history.replaceState({}, document.title, u.toString()); 8987} catch (e) { } 8988// Small status hint before opening the next configurator 8989if (statusEl) statusEl.textContent = 'Next: customize ' + info.title + '.'; 8990// Open next Micro PDP 8991var prog = getMicroProgressFromUrl(); 8992openMicroPdp(info.url, info.title, info.productId, remaining, prog.done, prog.total); 8993return true; 8994} 8995// --- SmartCart: ensure cart + tiles refresh when micro-PDP closes --- 8996(function bindMicroPdpCloseRefresh() { 8997var MODAL_SEL = '#smartcartMicroPdp, .smartcart-micro-pdp, [data-smartcart-micro-pdp]'; 8998var CLOSE_SEL = '[data-smartcart-close], .smartcart-close, .close, [aria-label="Close"]'; 8999var OVERLAY_SEL = '.smartcart-overlay, .modal-backdrop, [data-smartcart-overlay]'; 9000// You likely already have these functions; map to your actual names: 9001var refresh = window.smartcartRefresh || window.SmartCartRefresh || window.renderSmartCart || null; 9002// If you *donât* have a single refresh entrypoint, use whatever you normally call after add-to-cart: 9003function hardRefreshFallback() { 9004try { 9005if (typeof refresh === 'function') return refresh(); 9006// Fallback: call the same method you use after cart update 9007if (window.SmartCart && typeof window.SmartCart.render === 'function') return window.SmartCart.render(); 9008if (window.smartcart && typeof window.smartcart.render === 'function') return window.smartcart.render(); 9009} catch (e) { 9010console.warn('[SmartCart] refresh on modal close failed:', e); 9011} 9012} 9013// Debounce so multiple close signals (button + overlay) donât double-trigger 9014var t; 9015function refreshSoon() { 9016clearTimeout(t); 9017t = setTimeout(function () { 9018hardRefreshFallback(); 9019}, 150); 9020} 9021function onDocClick(e) { 9022var modal = document.querySelector(MODAL_SEL); 9023if (!modal) return; 9024var isClose = e.target.closest && e.target.closest(CLOSE_SEL); 9025var isOverlay = e.target.closest && e.target.closest(OVERLAY_SEL); 9026if (isClose || isOverlay) { 9027// Let the DOM close animation finish, then refresh 9028refreshSoon(); 9029} 9030} 9031function onEsc(e) { 9032if (e.key === 'Escape' || e.keyCode === 27) refreshSoon(); 9033} 9034document.addEventListener('click', onDocClick, true); 9035document.addEventListener('keydown', onEsc, true); 9036// Optional: if you dispatch a custom "microPdpClosed" event already, hook that too: 9037document.addEventListener('smartcart:microPdpClosed', refreshSoon); 9038})(); 9039// ---------------------------------------------------- 9040// 4A) Replace an "Added" product tile with the next best recommendation 9041// (keeps the grid fresh without requiring a full page reload) 9042// ---------------------------------------------------- 9043function convertTileToCompleteState(card, totalCount) { 9044if (!card) return; 9045// Stable-slot swap: fade this card out before repopulating 9046try { card.classList.add('ck-swapping'); } catch (eFade0) { } 9047try { 9048card.classList.remove('is-selected'); 9049card.classList.remove('is-added'); 9050card.classList.add('is-complete'); 9051// prevent future selection toggles 9052card.setAttribute('aria-disabled', 'true'); 9053card.innerHTML = 9054'<div class="ck-tile-complete">' 9055+ '<div class="ck-tile-complete__icon">â</div>' 9056+ '<div class="ck-tile-complete__title">All set</div>' 9057+ '<div class="ck-tile-complete__subtext">You\'ve added all recommended items for this order.</div>' 9058+ '</div>'; 9059} catch (e) { } 9060}
9061function replaceAddedTileWithNext(addedPid) { 9062try { 9063if (!useTiles || !stampGrid) return; 9064addedPid = String(addedPid || '').trim(); 9065if (!addedPid) return; 9066var card = stampGrid.querySelector('.kit-product-card[data-kit-primary="' + addedPid + '"]'); 9067if (!card) return; 9068// Build an ordered pool (highest relevance first) 9069var ordered = pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow).tiles; 9070// Build exclusion map 9071var exclude = Object.create(null); 9072// Exclude currently visible tiles 9073var visible = stampGrid.querySelectorAll('.kit-product-card[data-kit-primary]'); 9074for (var i = 0; i < visible.length; i++) { 9075var v = String(visible[i].getAttribute('data-kit-primary') || '').trim(); 9076if (v) exclude[v] = true; 9077} 9078// Exclude anything already added (session + micro returns) 9079try { 9080if (microAddedIds && microAddedIds.forEach) { 9081microAddedIds.forEach(function (id) { exclude[String(id)] = true; }); 9082} 9083} catch (eAdded) { } 9084// Exclude anything already shown during this session (prevents repeats when pool is small) 9085try { 9086if (ckSeenTileIds && ckSeenTileIds.forEach) { 9087ckSeenTileIds.forEach(function (id) { exclude[String(id)] = true; }); 9088} 9089} catch (eSeen) { } 9090// Exclude queued HARD SKUs still pending configuration 9091try { 9092var queued = parseRemainingHardFromUrl(); 9093for (var q = 0; q < queued.length; q++) { 9094var qid = String(queued[q] || '').trim(); 9095if (qid) exclude[qid] = true; 9096} 9097} catch (eQ) { } 9098// Exclude anything currently selected (so swaps don't "steal" into a tile the user already picked) 9099try { 9100if (selectedPrimaryIds && selectedPrimaryIds.forEach) { 9101selectedPrimaryIds.forEach(function (sid) { 9102var s = String(sid || '').trim(); 9103if (s) exclude[s] = true; 9104}); 9105} 9106} catch (eSel) { } 9107// Exclude anything already in the cart (prefer URL-path match; fallback to title match) 9108var cartPaths = Object.create(null); 9109var cartFullLower = ''; 9110try { cartPaths = getCartProductPathSet(); } catch (eCP) { } 9111try { cartFullLower = getCartFullTextLower(); } catch (eCT) { } 9112// Find next eligible item 9113var next = null; 9114for (var k = 0; k < ordered.length; k++) { 9115var it = ordered[k]; 9116var pid = String(it && (it.productId || it.id || it.product || it.primaryId) || '').trim(); 9117if (!pid || exclude[pid]) continue; 9118// URL-path in-cart exclusion 9119var pth = ''; 9120try { pth = normalizeUrlPathForCompare(it.productUrl || it.url || ''); } catch (eNP) { } 9121if (pth && cartPaths[pth]) continue; 9122// Title fallback exclusion 9123var nm = String(it.name || it.title || '').toLowerCase(); 9124if (nm && cartFullLower && cartFullLower.indexOf(nm) !== -1) continue; 9125next = it; 9126break; 9127} 9128if (!next) { 9129// Pool exhausted -- offer an approved alternative before giving up. 9130var altNext = ckPickFallbackTile(); 9131if (altNext) { 9132try { card.classList.remove('ck-swapping'); } catch (eFA) { } 9133ckRepaintCardAs(card, altNext); 9134return; 9135} 9136// Nothing left â convert this tile to completion tile 9137try { convertTileToCompleteState(card); } catch (eDone) { } 9138setTimeout(function () { try { card.classList.remove('ck-swapping'); } catch (eFadeX) { } }, 160); 9139return; 9140} 9141// Swap this card to the new item 9142var nextId = String(next.productId || next.id || next.product || next.primaryId || '').trim(); 9143var nextUrl = next.productUrl || next.url || ''; 9144var nextHard = !!requiresMicroPdp(nextId, next); 9145// Mark seen immediately so async hydration can't cause repeats 9146try { if (ckSeenTileIds) ckSeenTileIds.add(String(nextId)); } catch (eS) { } 9147// Fade out before repopulating (stable slot) 9148try { card.classList.add('ck-swapping'); } catch (eFade0) { } 9149// Reset card state + dataset 9150try { 9151card.classList.remove('is-added', 'is-selected', 'is-complete'); 9152card.removeAttribute('aria-disabled'); 9153} catch (eState) { } 9154card.setAttribute('data-kit-primary', nextId); 9155card.setAttribute('data-kit-url', nextUrl); 9156card.setAttribute('data-kit-hard', nextHard ? '1' : '0'); 9157// Paint skeleton while hydration finishes (DOM structure used by buildGrid) 9158var titleEl = card.querySelector('.kit-product-name'); 9159var priceEl = card.querySelector('.kit-product-price'); 9160var thumbTile = card.querySelector('.kit-product-thumb-tile'); 9161if (titleEl) titleEl.textContent = (next.title || next.name || 'Loadingâ¦'); 9162if (priceEl) priceEl.textContent = ''; 9163// Reset the thumb; we'll hydrate with a real image after fetchPdpData 9164if (thumbTile) { 9165var imgSrc = (next.image || next.img || ''); 9166if (imgSrc) { 9167var altText = String(next.title || next.name || '').replace(/"/g, '"'); 9168thumbTile.innerHTML = '<img src="' + imgSrc + '" alt="' + altText + '">'; 9169} else { 9170thumbTile.innerHTML = '<span class="kit-thumb-loading">Loading...</span>'; 9171} 9172} 9173// Fade back in once the new tile has been painted 9174setTimeout(function () { 9175try { card.classList.remove('ck-swapping'); } catch (eFade1) { } 9176}, 160); 9177// Record the swapped-in product's strategy attributes (role, 9178// friction, experimental) in an ANALYTICS-ONLY map. 9179// 9180// Deliberately NOT written to TILE_ITEM_BY_ID: that map feeds 9181// requiresMicroPdp(), so registering swapped tiles there would 9182// flip the CTA to 'Customize & Add' and reroute the add through 9183// the micro-PDP. That may be the correct behaviour -- a bare 9184// product=id&qty=1 POST of a required-options SKU is wrong -- but 9185// it is an add-flow change and does not belong in an analytics 9186// patch. Tracked separately. 9187try { CK_GA_TILE_META[String(nextId)] = next; } catch (eReg) { } 9188// Hydrate: fetch canonical name/price/image (guard against stale async writes) 9189(function (el, expectedId, raw) { 9190try { 9191var url = raw.productUrl || raw.url || ''; 9192var p = fetchPdpData(url); 9193if (!p || !p.then) return; 9194p.then(function (data) { 9195try { 9196// Guard a stale async write: the slot may have been 9197// swapped again while this fetch was in flight. 9198if (!el || el.getAttribute('data-kit-primary') !== String(expectedId)) return; 9199var tEl = el.querySelector('.kit-product-name'); 9200var prEl = el.querySelector('.kit-product-price'); 9201var thEl = el.querySelector('.kit-product-thumb-tile'); 9202if (tEl && data.name) tEl.textContent = data.name;
9203// Cache the canonical name/price so analytics and 9204// the price UI see the same product the shopper does. 9205// Cache the canonical name/price. Gated by the same 9206// quality check buildGrid and ckRepaintCardAs use -- 9207// a URL that scrapes to 'Create New Customer Account' 9208// must not reach the cache, because it feeds the 9209// micro-PDP modal title and the displayed total. 9210// Also normalises price the way the other two sites do. 9211try { 9212if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED 9213&& ckIsUsableTileData(data)) { 9214NOTARY_HYDRATED.set(String(expectedId), { 9215productId: String(expectedId), 9216sku: data.sku, 9217productUrl: (raw && (raw.productUrl || raw.url)) || '', 9218name: data.name || '', 9219img: data.img || '', 9220price: (data.price != null ? Number(data.price) : null) 9221}); 9222} 9223} catch (eHyd) { } 9224// A swapped tile is newly visible and was in no 9225// view_item_list, so its funnel would otherwise start 9226// at select_item. Give it its own one-item impression. 9227// attempts = 0 -> fire now; the name is already cached. 9228try { 9229ckGaFireListWhenHydrated( 9230[String(expectedId)], 0, 9231(CK_GA4.LIST_PREFIX + ' - ' + ckGaListId() + ' (swap)').slice(0, 100) 9232); 9233} catch (eSwapGa) { } 9234if (prEl) { 9235prEl.textContent = (data.price != null ? ('$' + Number(data.price).toFixed(2)) : 'See price in cart'); 9236} 9237if (thEl) { 9238if (data.img) { 9239var alt2 = String(data.name || '').replace(/"/g, '"'); 9240thEl.innerHTML = '<img src="' + data.img + '" alt="' + alt2 + '">'; 9241} else { 9242thEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 9243} 9244} 9245} catch (eH) { } 9246}).catch(function () { }); 9247} catch (eF) { } 9248})(card, nextId, next); 9249} catch (e) { 9250// fail silently 9251} 9252} 9253function handleMicroAddedFromQuery() { 9254// The micro-PDP no longer reloads the page, so microAddedQS / 9255// microAddedNameQS -- captured from window.location.search back at 9256// DOMContentLoaded -- are stale by definition. onMicroPdpAddSucceeded() 9257// writes the receipt into the URL via history.replaceState, so re-read 9258// it live here. Still correct for the legacy reload path. 9259try { 9260var ckLive = new URL(window.location.href).searchParams; 9261microAddedQS = ckLive.get('microAdded'); 9262microAddedNameQS = ckLive.get('microAddedName'); 9263} catch (eLive) { } 9264if (!microAddedQS) return; 9265var pid = String(microAddedQS || '').trim(); 9266if (!pid) return; 9267// Mark as added so tile greys out 9268microAddedIds.add(pid); 9269// Remove from current selections (prevents stale totals when tile is swapped) 9270try { selectedPrimaryIds.delete(pid); } catch (eSel0) { } 9271// Micro progress: user just completed one configurator 9272var progNow = bumpMicroDoneInUrl(); 9273MICRO_PDP.progressDone = progNow.done; 9274MICRO_PDP.progressTotal = progNow.total; 9275renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal); 9276// Hard SKUs add inside the micro-PDP iframe, so they never reach the 9277// POST chain above and were invisible to analytics. Fire the 9278// per-product event here. Dedupe on pid + completed-count: the 9279// receipt params can survive a re-entry of this handler. 9280try { 9281// Dedupe on a short time window, NOT on progNow.done: 9282// bumpMicroDoneInUrl() increments on every call, so a re-entry 9283// would produce a different key and defeat the guard entirely. 9284// A configurator takes seconds, so two legitimate adds of the 9285// same SKU inside 5s are impossible. 9286window.__ckMicroAddReported = window.__ckMicroAddReported || {}; 9287var ckNowT = Date.now(); 9288if (ckNowT - (window.__ckMicroAddReported[pid] || 0) > 5000) { 9289window.__ckMicroAddReported[pid] = ckNowT; 9290// On the legacy reload path this handler runs in the same tick as 9291// buildGrid(), which only STARTS hydration -- so firing now would 9292// send the entity id for the highest-AOV items in the module. 9293// Wait for the SKU, then fire; fire anyway on timeout. 9294(function emitMicroAdd(tries) { 9295try { 9296if (!ckGaHasSku(pid) && tries > 0) { 9297setTimeout(function () { emitMicroAdd(tries - 1); }, CK_GA4.HYDRATE_POLL_MS); 9298return; 9299} 9300} catch (eW) { } 9301pushUpsellEvent('SmartCart_upsell_micro_added', { 9302product_ids: [pid], 9303productId: pid, 9304micro_done: progNow.done, 9305micro_total: progNow.total 9306}); 9307})(CK_GA4.HYDRATE_POLLS); 9308} 9309} catch (eGaM) { } 9310// Show a simple success message in the module status area 9311var niceName = (microAddedNameQS || '').trim(); 9312if (!niceName) { 9313// If name not provided, try hydrated cache 9314var hydrated = NOTARY_HYDRATED.get(pid);
9315if (hydrated && hydrated.name) niceName = hydrated.name; 9316} 9317if (!niceName) niceName = 'Product'; 9318var toast = document.getElementById('kit-success-toast'); 9319if (toast) { 9320toast.textContent = 'â Added to cart: ' + niceName; 9321// If this was the LAST remaining hard item, celebrate completion 9322if (progNow.total > 1 && progNow.done >= progNow.total) { 9323toast.textContent = '???? All set â ' + progNow.done + ' of ' + progNow.total + ' items added'; 9324} 9325toast.classList.add('is-visible'); 9326setTimeout(function () { 9327toast.classList.remove('is-visible'); 9328}, 3500); 9329} else if (statusEl) { 9330// fallback 9331statusEl.textContent = 'â Added to cart: ' + niceName; 9332if (progNow.total > 1 && progNow.done >= progNow.total) { 9333statusEl.textContent = '???? All set â ' + progNow.done + ' of ' + progNow.total + ' items added'; 9334} 9335} 9336// Rebuild grid so the "is-added" state is applied 9337if (useTiles) buildGrid(); 9338try { updatePriceUI(); } catch (ePU0) { } 9339// 4B) After a HARD SKU is completed, swap the added tile for the next recommendation 9340// so the customer can keep adding without stale/greyed tiles. 9341// (Run after buildGrid() so we target the freshly-rendered card.) 9342setTimeout(function () { 9343try { replaceAddedTileWithNext(pid); } catch (e) { } 9344}, 800); 9345// If there are more HARD SKUs waiting, open the next Micro PDP automatically 9346// (keeps the user in a guided configuration flow) 9347setTimeout(function () { 9348try { 9349openNextHardFromRemaining(); 9350} catch (e) { } 9351}, 350); 9352// Scroll to the newly added item in cart + highlight 9353// Strategy: find a cart row that contains the product name 9354setTimeout(function () { 9355try { 9356var table = document.querySelector('#shopping-cart-table'); 9357if (!table) return; 9358var rows = Array.from(table.querySelectorAll('tr')); 9359var targetRow = null; 9360if (niceName && niceName !== 'Product') { 9361var nameLower = niceName.toLowerCase(); 9362targetRow = rows.find(function (r) { 9363return (r.innerText || '').toLowerCase().indexOf(nameLower) > -1; 9364}) || null; 9365} 9366// Fallback: if we can't find the row by name, just scroll to cart table 9367var scrollEl = targetRow || table; 9368scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); 9369if (targetRow) { 9370targetRow.classList.add('ck-cart-highlight'); 9371setTimeout(function () { 9372targetRow.classList.remove('ck-cart-highlight'); 9373}, 3500); 9374} 9375} catch (e) { } 9376}, 250); 9377// Remove receipt params so refresh doesn't repeat the success behavior 9378try { 9379var u = new URL(window.location.href); 9380u.searchParams.delete('microAdded'); 9381u.searchParams.delete('microAddedName'); 9382window.history.replaceState({}, document.title, u.toString()); 9383} catch (e) { } 9384} 9385// Query-param handlers MUST run in this order (priority: micro receipt > add more > success) 9386handleMicroAddedFromQuery(); // receipt event â toast/greyout/highlight, then strip params 9387handleAddMoreFromQuery(); // user intent to reopen + reset 9388applySuccessStateFromQuery(); // generic success collapse/header state 9389// ---------------------------------------------------- 9390// 9) DEBUG OVERLAY (persona + product scores) 9391// Enable with ?upsellDebug=1 9392// ---------------------------------------------------- 9393(function initDebugOverlay() { 9394var debugOn = (urlParams.get('upsellDebug') === '1'); 9395if (!debugOn) return; 9396// ---- styles (scoped) ---- 9397var style = document.createElement('style'); 9398style.textContent = 9399'#ck-debug-overlay{position:fixed;right:12px;bottom:12px;z-index:999999;' + 9400'width:360px;max-width:calc(100vw - 24px);' + 9401'background:#0b1220;color:#e8eefc;border:1px solid rgba(255,255,255,.15);' + 9402'border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35);font-family:inherit;}' + 9403'#ck-debug-overlay *{box-sizing:border-box;}' + 9404'#ck-debug-head{display:flex;align-items:center;justify-content:space-between;' + 9405'padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.12);}' + 9406'#ck-debug-title{font-weight:700;font-size:13px;letter-spacing:.2px;}' + 9407'#ck-debug-actions{display:flex;gap:8px;align-items:center;}' + 9408'#ck-debug-actions button{background:rgba(255,255,255,.12);color:#e8eefc;border:1px solid rgba(255,255,255,.15);' + 9409'border-radius:8px;padding:6px 8px;font-size:12px;cursor:pointer;}' + 9410'#ck-debug-actions button:hover{background:rgba(255,255,255,.18);}' + 9411'#ck-debug-body{padding:10px 12px;max-height:55vh;overflow:auto;}' + 9412'#ck-debug-body h4{margin:10px 0 6px 0;font-size:12px;opacity:.9;}' + 9413'#ck-debug-kv{display:grid;grid-template-columns:120px 1fr;gap:6px 10px;font-size:12px;}' + 9414'#ck-debug-kv .k{opacity:.75;}' + 9415'#ck-debug-table{width:100%;border-collapse:collapse;font-size:12px;}' + 9416'#ck-debug-table th,#ck-debug-table td{padding:6px 6px;border-bottom:1px solid rgba(255,255,255,.10);vertical-align:top;}' + 9417'#ck-debug-table th{opacity:.75;text-align:left;font-weight:600;}' + 9418'#ck-debug-pill{display:inline-block;padding:2px 6px;border-radius:999px;' + 9419'background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.14);font-size:11px;}' + 9420'#ck-debug-muted{opacity:.7;font-size:11px;line-height:1.35;}' +
9421'#ck-debug-overlay.collapsed #ck-debug-body{display:none;}'; 9422document.head.appendChild(style); 9423// ---- UI ---- 9424var box = document.createElement('div'); 9425box.id = 'ck-debug-overlay'; 9426box.innerHTML = 9427'<div id="ck-debug-head">' + 9428'<div id="ck-debug-title">Cart Kit Debug</div>' + 9429'<div id="ck-debug-actions">' + 9430'<button type="button" id="ck-debug-copy">Copy</button>' + 9431'<button type="button" id="ck-debug-toggle">Collapse</button>' + 9432'</div>' + 9433'</div>' + 9434'<div id="ck-debug-body">' + 9435'<div id="ck-debug-kv"></div>' + 9436'<h4>Persona scores</h4>' + 9437'<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>' + 9438'<h4>Top products (selected persona)</h4>' + 9439'<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>' + 9440'<div id="ck-debug-muted" style="margin-top:10px;">Tip: add/remove items and watch scores update live.</div>' + 9441'</div>'; 9442document.body.appendChild(box); 9443var btnToggle = document.getElementById('ck-debug-toggle'); 9444var btnCopy = document.getElementById('ck-debug-copy'); 9445btnToggle.addEventListener('click', function () { 9446box.classList.toggle('collapsed'); 9447btnToggle.textContent = box.classList.contains('collapsed') ? 'Expand' : 'Collapse'; 9448}); 9449function safeJson(obj) { 9450try { return JSON.stringify(obj, null, 2); } catch (e) { return String(obj); } 9451} 9452btnCopy.addEventListener('click', function () { 9453var payload = window.__CK_DEBUG_LAST__ || {}; 9454var textToCopy = safeJson(payload); 9455if (navigator.clipboard && navigator.clipboard.writeText) { 9456navigator.clipboard.writeText(textToCopy).catch(function () { }); 9457} else { 9458// fallback 9459var ta = document.createElement('textarea'); 9460ta.value = textToCopy; 9461ta.style.position = 'fixed'; 9462ta.style.left = '-9999px'; 9463document.body.appendChild(ta); 9464ta.select(); 9465try { document.execCommand('copy'); } catch (e) { } 9466document.body.removeChild(ta); 9467} 9468}); 9469// ---- scoring helpers for overlay ---- 9470function personaScoreBreakdown(personaObj, cartTextLower) { 9471var kw = keywordMatchScore((personaObj.match || personaObj.matchKeywords || []), cartTextLower); 9472var base = kw.score; 9473var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0; 9474return { 9475base: base, 9476hits: kw.hits, 9477matched: kw.matched, 9478priorityNudge: priorityNudge, 9479total: base + priorityNudge 9480}; 9481} 9482function productScoreForOverlay(item, cartTextLower, allowExperimental) { 9483return scoreProductForCart(item, cartTextLower, allowExperimental); 9484} 9485function esc(s) { 9486return String(s || '').replace(/[&<>"']/g, function (c) { 9487return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]); 9488}); 9489} 9490// Debug helpers: get cart titles/options as arrays 9491function getCartTitlesList() { 9492try { 9493var nameNodes = document.querySelectorAll( 9494'[data-cart-item-id] h3 a,' + 9495'[data-cart-item-id] h3,' + 9496'#shopping-cart-table h2.cartProductName a,' + 9497'#shopping-cart-table h2.cartProductName,' + 9498'#shopping-cart-table .product-name a,' + 9499'#shopping-cart-table .product-name,' + 9500'.product-item-name a,' + 9501'.product-item-name' 9502); 9503var out = []; 9504var seen = {}; 9505for (var i = 0; i < nameNodes.length; i++) { 9506var t = (nameNodes[i].textContent || '').trim(); 9507if (!t) continue; 9508var k = t.toLowerCase(); 9509if (seen[k]) continue; 9510seen[k] = true; 9511out.push(t); 9512} 9513return out; 9514} catch (e) { return []; } 9515} 9516function getCartOptionsList() { 9517try { 9518var optNodes = document.querySelectorAll( 9519'[data-cart-item-id] .flex-wrap,' + 9520'#shopping-cart-table .item-options,' + 9521'#shopping-cart-table dl.item-options,' + 9522'.item-options,' + 9523'dl.item-options' 9524); 9525var out = []; 9526for (var j = 0; j < optNodes.length; j++) { 9527var o = (optNodes[j].textContent || '').trim(); 9528if (o) out.push(o); 9529} 9530return out; 9531} catch (e) { return []; } 9532} 9533function renderOverlay() { 9534var cartTitleLower = getCartTitleTextLower(); 9535var cartFullLower = getCartFullTextLower(); 9536var allowExperimental = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1'); 9537// Persona scoring 9538var personaRows = []; 9539var best = { key: 'generic', score: -1 }; 9540for (var i = 0; i < PERSONAS.length; i++) { 9541var p = PERSONAS[i]; 9542if (!p || p.key === 'generic') continue; 9543var br = personaScoreBreakdown(p, cartTitleLower); 9544personaRows.push({ 9545key: p.key, 9546name: p.name, 9547score: br.total, 9548hits: br.hits, 9549matched: br.matched || [], 9550priority: br.priorityNudge 9551}); 9552if (br.hits > 0 && br.total > best.score) { 9553best = { key: p.key, score: br.total }; 9554} 9555} 9556personaRows.sort(function (a, b) { return b.score - a.score; }); 9557// If nobody matched, generic 9558var chosenKey = (best.score <= 0) ? 'generic' : best.key; 9559var chosenPersona = getPersonaByKey(chosenKey); 9560// Product scoring for chosen persona 9561var pool = (chosenPersona && chosenPersona.items) ? chosenPersona.items.slice() : []; 9562var prodRows = pool.map(function (it) { 9563return { 9564id: it.productId, 9565url: it.productUrl, 9566name: it.productName || it.name || it.title || it.label || '', 9567role: it.role || '', 9568friction: it.friction || '', 9569experimental: (it.experimental === true), 9570score: productScoreForOverlay(it, cartFullLower, allowExperimental) 9571}; 9572}); 9573prodRows.sort(function (a, b) { return b.score - a.score; }); 9574// Top N shown for overlay (not just 3) 9575var topN = prodRows.slice(0, 8); 9576// Update KV 9577var kv = document.getElementById('ck-debug-kv'); 9578var titleList = getCartTitlesList(); 9579var optList = getCartOptionsList(); 9580var cartTitlesText = (titleList || []).join(' | '); 9581var cartOptionsText = (optList || []).join(' | '); 9582kv.innerHTML = 9583'<div class="k">Chosen persona</div><div><span id="ck-debug-pill" class="ck-pill"></span></div>' + 9584'<div class="k">Experimental</div><div>' + (allowExperimental ? 'ON (debug)' : 'OFF') + '</div>' + 9585'<div class="k">Cart titles (used for persona)</div><div id="ck-debug-muted">' + esc(cartTitlesText.slice(0, 220)) + (cartTitlesText.length > 220 ? 'â¦' : '') + '</div>' + 9586'<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>'; 9587var pill = document.getElementById('ck-debug-pill'); 9588if (pill) { 9589pill.textContent = chosenKey; 9590pill.className = ''; 9591pill.id = 'ck-debug-pill'; 9592pill.style.display = 'inline-block'; 9593pill.style.padding = '2px 6px'; 9594pill.style.borderRadius = '999px'; 9595pill.style.background = 'rgba(255,255,255,.12)'; 9596pill.style.border = '1px solid rgba(255,255,255,.14)'; 9597pill.style.fontSize = '11px'; 9598} 9599// Persona tables 9600var tbody = document.getElementById('ck-debug-personas'); 9601tbody.innerHTML = personaRows.map(function (r) { 9602var isChosen = (r.key === chosenKey); 9603// Build keyword pills (or fallback) 9604var kwHtml = (r.matched && r.matched.length) 9605? '<div class="ck-debug-keywords">' + 9606r.matched.slice(0, 10).map(function (k) { 9607return '<span class="ck-debug-keyword">' + esc(k) + '</span>'; 9608}).join('') + 9609(r.matched.length > 10 ? '<span class="ck-debug-keyword muted">+' + (r.matched.length - 10) + ' more</span>' : '') + 9610'</div>' 9611: '<div class="ck-debug-keywords muted">no keyword matches</div>'; 9612return '' 9613+ '<tr>' 9614+ '<td>' + esc(r.key) + (isChosen ? ' <span class="ck-badge" style="margin-left:6px;opacity:.9;">â </span>' : '') + '</td>' 9615+ '<td>
9615' + (Math.round(r.score * 100) / 100) + '</td>' 9616+ '<td>' + r.hits + '</td>' 9617+ '</tr>' 9618+ '<tr class="ck-debug-keyword-row">' 9619+ '<td colspan="3">' + kwHtml + '</td>' 9620+ '</tr>'; 9621}).join(''); 9622// Products table 9623var prodTbody = document.getElementById('ck-debug-products'); 9624if (prodTbody) { 9625if (!topN || !topN.length) { 9626prodTbody.innerHTML = '<tr><td colspan="3" style="opacity:.7;">No products found for this persona.</td></tr>'; 9627} else { 9628prodTbody.innerHTML = topN.map(function (r) { 9629var label = r.name || r.url || r.id || ''; 9630var roleBits = []; 9631if (r.role) roleBits.push(r.role); 9632if (r.friction) roleBits.push('friction: ' + r.friction); 9633if (r.experimental) roleBits.push('exp'); 9634return '' 9635+ '<tr>' 9636+ '<td>' + esc(label) + '</td>' 9637+ '<td>' + (Math.round(r.score * 100) / 100) + '</td>' 9638+ '<td>' + esc(roleBits.join(' ⢠')) + '</td>' 9639+ '</tr>'; 9640}).join(''); 9641} 9642} 9643// Store last payload for easy copy/paste to stakeholders 9644window.__CK_DEBUG_LAST__ = { 9645chosen_persona: chosenKey, 9646allow_experimental: allowExperimental, 9647persona_scores: personaRows, 9648product_scores_top: topN, 9649cart_titles: titleList, 9650cart_options: optList, 9651cart_titles_used_for_persona: cartTitleLower, 9652cart_options_text: (cartOptionsText || '').toLowerCase(), 9653cart_full_text: cartFullLower 9654}; 9655} 9656// Update live (cart can change after ajax) 9657renderOverlay(); 9658setInterval(renderOverlay, 800); 9659})(); 9660}); </script>
9660</div><div class="widget widget-static-block"></div><div class="widget widget-static-block"></div>
9660<script> /* Ported from the pre-Zephyr footer's jQuery block. Zephyr ships a jQuery 9661no-op shim, so the original jQuery(document).ready() never fired â see 9662note 1 in the header comment. Only the email-CTA rewrite was live; the 9663#blog and #corporate-accounts branches were dead and are not carried over. */ 9664(function () { 9665"use strict"; 9666var done = false; 9667function apply() { 9668if (done) return true; 9669var s = document.querySelectorAll('.email-cta > strong'); 9670if (s.length < 2) return false; 9671s[1].textContent = 'and SAVE 15%'; 9672done = true; 9673return true; 9674} 9675if (!apply()) { 9676document.addEventListener('DOMContentLoaded', apply); 9677/* the email CTA is injected by a widget block that can render late */ 9678if (window.MutationObserver) { 9679var obs = new MutationObserver(function () { if (apply()) obs.disconnect(); }); 9680obs.observe(document.documentElement, { childList: true, subtree: true }); 9681setTimeout(function () { obs.disconnect(); }, 10000); 9682} 9683} 9684})(); 9685</script>
9685<div class="widget widget-static-block"> <style> /* Hairline light, HARDENED against theme globals. ID-scoped (#fu-foot) for 9686specificity + a child reset so theme font / letter-spacing / text-transform / 9687line-height / margins / list & link styling can't bleed in. Targeted 9688!important only on the props themes tend to override. */ 9689#fu-foot.fu-foot{--fu-accent:var(--brand-primary,#012e4a);--fu-border:#e7e9ec;--fu-sep:#e3e6ea; 9690display:flex!important;align-items:center!important;justify-content:center!important; 9691gap:8px!important;width:100%!important;min-height:46px!important; 9692margin:0!important;padding:9px 12px!important;margin-top:2rem; 9693background:#fff!important;border-top:1px solid var(--fu-border)!important;border-bottom:1px solid var(--fu-border)!important; 9694box-sizing:border-box!important;font-family:inherit!important;list-style:none!important} 9695#fu-foot.fu-foot *{margin:0!important;padding:0!important;box-sizing:border-box!important; 9696font-family:inherit!important;letter-spacing:normal!important;text-transform:none!important; 9697line-height:normal!important;list-style:none!important;text-decoration:none!important;float:none!important} 9698#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} 9699#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} 9700#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} 9701#fu-foot .fu-sep{width:1px!important;height:20px!important;background:var(--fu-sep)!important;flex:0 0 1px!important} 9702/* Copy is approved marketing copy and is never trimmed to fit. nowrap keeps the 9703crisp single line on wide desktop; below 1280px the longest lines wrap instead of 9704overflowing their equal-width column. Measured on live NTW: its slot 1 ("Badges 9705Your Team Will Be Proud to Wear", 38 chars) renders 251px wide. Text space per 9706column is ~430px at 1920, ~250px at 1200, ~270px at 1280 - so 1200 was 1px short 9707and 1280 is the correct boundary. Re-measure if any line gets longer. */ 9708@media(max-width:1280px){ 9709#fu-foot .fu-head{white-space:normal!important} 9710#fu-foot.fu-foot{min-height:56px!important} 9711} 9712/* symbol library â out of the flex container, never rendered */ 9713#fu-defs{position:absolute!important;width:0!important;height:0!important;overflow:hidden!important} 9714@media(max-width:768px){ 9715#fu-foot.fu-foot{flex-wrap:wrap!important;min-height:82px!important} 9716#fu-foot .fu-item{flex:1 1 46%!important;justify-content:flex-start!important} 9717#fu-foot .fu-head{font-size:12px!important;white-space:normal!important} 9718#fu-foot .fu-sep{display:none!important} 9719} </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>
9719 <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>
9719<script type="text/javascript"> // <![CDATA[ 9720(function () { 9721"use strict"; 9722var SITE_DATA = { 9723"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"]], 9724"customsigns": [["fu-quality","Built to Last"],["fu-customization","Customized Your Way"],["fu-guarantee","Experts Since 1954"],["fu-star","Reliable Business Signs"]], 9725"allstatenotarysupplies": [["fu-guarantee","Trusted Since 2008"],["fu-compliance","Compliance Experts"],["fu-customization","Easy to Customize"],["fu-clock","Ships in 24 Hours"]], 9726"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"]], 9727"corpconnect": [["fu-quality","Professional Grade"],["fu-compliance","State Approved"],["fu-customization","Order Your Way"],["fu-shipping","Ships Next Business Day"]], 9728"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"]], 9729"904custom": [["fu-quality","Heirloom Quality"],["fu-design","Real Craftspeople"],["fu-customization","Personalize in Minutes"],["fu-shipping","Ships Next Day"]], 9730"hcbrands": [["fu-guarantee","70 Years of Making It Right"],["fu-design","Expert Craftspeople"],["fu-customization","Easy to Personalize"],["fu-shipping","Ships Next Business Day"]] 9731}; 9732// Per-brand icon/text color. Omit a site to inherit var(--brand-primary) with 9733// a navy fallback. MyKoozie is pinned to its theme blue so the strip matches 9734// the footer headers directly beneath it on legacy AND Zephyr. 9735var SITE_ACCENT = { "mykoozie": "#456FB2" }; 9736// Matching below is a SUBSTRING test against the hostname, so a shared staging 9737// host resolves to whichever key it happens to contain â mk.staging.hcbrands.com 9738// would render HCBrands copy, not MyKoozie's. Force any such host here; this map 9739// is consulted first and wins outright. Key = exact lowercase hostname. 9740var HOST_OVERRIDE = { 9741// "mk.staging.hcbrands.com": "mykoozie", 9742// "cs.staging.hcbrands.com": "customsigns" 9743}; 9744try { 9745var root = document.getElementById("fu-foot"); 9746if (!root) return; 9747var host = (location.hostname || "").toLowerCase(); 9748var key = null; 9749if (HOST_OVERRIDE[host] && SITE_DATA[HOST_OVERRIDE[host]]) { 9750key = HOST_OVERRIDE[host]; 9751} else { 9752for (var k in SITE_DATA) { 9753if (Object.prototype.hasOwnProperty.call(SITE_DATA, k) && host.indexOf(k) > -1) { key = k; break; } 9754} 9755} 9756if (!key) return; // unknown host -> keep the generic copy already in the markup 9757var cfg = SITE_DATA[key]; 9758if (SITE_ACCENT[key]) root.style.setProperty("--fu-accent", SITE_ACCENT[key]); 9759var items = root.getElementsByClassName("fu-item"); 9760var seps = root.getElementsByClassName("fu-sep"); 9761for (var i = 0; i < items.length; i++) { 9762if (!cfg[i]) { 9763items[i].style.display = "none"; 9764if (seps[i - 1]) seps[i - 1].style.display = "none"; // no orphan divider 9765continue; 9766} 9767var use = items[i].getElementsByTagName("use")[0]; 9768if (use) { 9769use.setAttribute("href", "#" + cfg[i][0]); 9770use.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#" + cfg[i][0]); 9771} 9772var head = items[i].getElementsByClassName("fu-head")[0]; 9773if (head) head.textContent = cfg[i][1]; 9774} 9775} catch (e) { /* strip keeps its default copy rather than breaking the footer */ } 9776})(); 9777// ]]> </script>
9777</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">
97772021 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>
9777<script type="application/ld+json"> { 9778"@context": "http://schema.org", 9779"@type": "LocalBusiness", 9780"name": "SimplyStamps.com", 9781"address": { 9782"@type": "PostalAddress", 9783"streetAddress": "2021 St Augustine Rd E, Suite 2", 9784"addressLocality": "Jacksonville", 9785"addressRegion": "FL", 9786"postalCode": "32207", 9787"addressCountry": "US" 9788}, 9789"geo": { 9790"@type": "GeoCoordinates", 9791"latitude": "30.3220", 9792"longitude": "-81.6690" 9793}, 9794"telephone": "+1-888-688-9998", 9795"email": "[email protected]", 9796"url": "https://www.simplystamps.com", 9797"sameAs": [ 9798"https://www.facebook.com/SimplyStamps", 9799"https://twitter.com/SimplyStamps", 9800"https://www.instagram.com/SimplyStamps", 9801"https://www.pinterest.com/SimplyStamps", 9802"https://www.linkedin.com/company/SimplyStamps" 9803], 9804"openingHoursSpecification": [ 9805{ 9806"@type": "OpeningHoursSpecification", 9807"dayOfWeek": [ 9808"Monday", 9809"Tuesday", 9810"Wednesday", 9811"Thursday" 9812], 9813"opens": "10:00", 9814"closes": "16:00" 9815} 9816], 9817"hasMerchantReturnPolicy": { 9818"@type": "MerchantReturnPolicy", 9819"name": "Return Policy", 9820"url": "https://www.simplystamps.com/returns", 9821"merchantReturnDays": 60, 9822"returnFees": "FreeReturn", 9823"returnMethod": "ReturnByMail", 9824"returnPolicyCategory": "Refundable", 9825"inStoreReturnsOffered": false, 9826"itemCondition": "New", 9827"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." 9828} 9829} </script>
9829</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 { 98300%, 100% { transform: translateX(0); } 983110%, 30%, 50%, 70%, 90% { transform: translateX(-4px); } 983220%, 40%, 60%, 80% { transform: translateX(4px); } 9833} 9834.shake { 9835animation: shake 0.5s ease-in-out; 9836} </style><div 9837x-data 9838x-show="$store.cart.drawerOpen" 9839@click="$store.cart.closeDrawer()" 9840x-transition:enter="transition-opacity ease-out duration-300" 9841x-transition:enter-start="opacity-0" 9842x-transition:enter-end="opacity-100" 9843x-transition:leave="transition-opacity ease-in duration-200" 9844x-transition:leave-start="opacity-100" 9845x-transition:leave-end="opacity-0" 9846class="fixed inset-0 z-40 transition-all duration-500" 9847:class="$store.cart.drawerFocused ? 'bg-black/15' : 'bg-transparent'" 9848style="display: none;" 9849></div><div 9850x-data 9851x-show="$store.cart.drawerOpen" 9852@click.away="if (!$store.imagePreview?.isOpen && !$store.promo?.isOpen && !$store.confirm?.isOpen && !document.getElementById('wizardModal')?._x_dataStack?.[0]?.isOpen) $store.cart.closeDrawer()" 9853@mouseenter="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = true }, 215)" 9854@mouseleave="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = false }, 100)" 9855x-transition:enter="transition ease-out duration-300" 9856x-transition:enter-start="translate-x-full" 9857x-transition:enter-end="translate-x-0" 9858x-transition:leave="transition ease-in duration-200" 9859x-transition:leave-start="translate-x-0" 9860x-transition:leave-end="translate-x-full" 9861:style="{ top: $store.ui.getHeaderOffset() + 'px' }" 9862class="fixed right-0 bottom-0 w-full md:w-[26rem] bg-white shadow-2xl z-50 flex flex-col transition-all duration-300" 9863style="display: none;" 9864><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 9865@click="$store.cart.closeDrawer()" 9866class="p-2 hover:bg-gray-100 rounded-lg transition-colors" 9867aria-label="Close cart" 9868> <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 9869x-show="$store.cart.isRefreshing" 9870x-cloak 9871x-transition:enter="transition ease-out duration-200" 9872x-transition:enter-start="opacity-0" 9873x-transition:enter-end="opacity-100" 9874x-transition:leave="transition ease-in duration-150" 9875x-transition:leave-start="opacity-100" 9876x-transition:leave-end="opacity-0" 9877class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center z-10" 9878><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">
9878Updating cart...</span></div></div><div class="flex-1 min-h-0 overflow-hidden relative" 9879x-data="{ 9880canScroll: false, 9881atBottom: false, 9882checkScroll() { 9883const el = this.$refs.scrollContainer; 9884if (!el) return; 9885this.canScroll = el.scrollHeight > el.clientHeight + 10; 9886this.atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 20; 9887}, 9888scrollToLastItem() { 9889const container = this.$refs.scrollContainer; 9890if (!container) return; 9891const items = container.querySelectorAll('[data-cart-item-id]'); 9892if (!items.length) return; 9893const lastItem = items[items.length - 1]; 9894const cRect = container.getBoundingClientRect(); 9895const iRect = lastItem.getBoundingClientRect(); 9896if (iRect.bottom > cRect.bottom || iRect.top < cRect.top) { 9897lastItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 9898} 9899this.checkScroll(); 9900} 9901}" 9902x-init="$nextTick(() => checkScroll()); $watch(() => $store.cart.drawerOpen, (open) => { if (open) $nextTick(() => checkScroll()) })" 9903@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" 9904id="cart-items-container" 9905x-ref="scrollContainer" 9906@scroll.throttle.100ms="checkScroll()" 9907@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" 9908x-transition:enter="transition ease-out duration-200" 9909x-transition:enter-start="opacity-0" 9910x-transition:enter-end="opacity-100" 9911x-transition:leave="transition ease-in duration-150" 9912x-transition:leave-start="opacity-100" 9913x-transition:leave-end="opacity-0" 9914class="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" 9915@click.prevent.stop=" 9916const container = document.getElementById('cart-items-container'); 9917if (!container) return; 9918const items = container.querySelectorAll('[data-cart-item-id]'); 9919if (!items.length) return; 9920const containerTop = container.getBoundingClientRect().top; 9921let target = null; 9922const containerBottom = container.getBoundingClientRect().bottom; 9923for (let i = 0; i < items.length; i++) { 9924if (items[i].getBoundingClientRect().bottom > containerBottom) { 9925target = items[i]; 9926break; 9927} 9928} 9929if (target) { 9930const scrollAmount = target.getBoundingClientRect().top - containerTop; 9931container.scrollBy({ top: scrollAmount, behavior: 'smooth' }); 9932} 9933" 9934class="absolute bottom-2 left-1/2 -translate-x-1/2 animate-bounce p-1 rounded-full hover:bg-gray-100 transition-colors cursor-pointer" 9935aria-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" 9936x-data 9937x-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\/aHR0cHM6Ly93d3cuc2ltcGx5c3RhbXBzLmNvbS9hYm91dC11cw,,\/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 9938onclick="clearCart(); return false;" 9939class="text-xs text-red-600 hover:text-red-700 hover:underline transition-colors">
9939 Clear Cart </button></div><div x-data="{ threshold: 79 }" 9940x-show="$store.cart.subtotalNumeric > 0 && $store.cart.subtotalNumeric >= threshold" 9941x-cloak 9942class="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 9943type="button" 9944@click="$store.promo.remove()" 9945class="text-xs text-green-700 hover:text-green-900 underline" 9946>Remove</button></div> </template> <template x-if="!$store.cart.couponCode"> <button 9947type="button" 9948@click="$store.promo.open()" 9949class="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" 9950> <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 9951x-transition:enter="transition ease-out duration-200" 9952x-transition:enter-start="opacity-0 translate-y-1" 9953x-transition:enter-end="opacity-100 translate-y-0" 9954x-transition:leave="transition ease-in duration-150" 9955x-transition:leave-start="opacity-100 translate-y-0" 9956x-transition:leave-end="opacity-0 translate-y-1" 9957class="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 9958type="button" 9959onclick="addExpeditedToCart()" 9960@mouseenter="showTip = true"
9961@mouseleave="showTip = false" 9962class="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/" 9963@click.prevent="$store.cart.hasError ? $store.cart.shake($event.currentTarget) : $store.upsell.maybeProceed('https://www.simplystamps.com/checkout/onepage/')" 9964class="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/" 9965class="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 9966@click="$store.cart.closeDrawer()" 9967class="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 9968x-data 9969x-show="$store.promo.isOpen" 9970x-cloak 9971class="fixed inset-0 z-[100] overflow-y-auto" 9972aria-labelledby="promo-modal-title" 9973role="dialog" 9974aria-modal="true" 9975><div 9976x-show="$store.promo.isOpen" 9977x-transition:enter="transition ease-out duration-200" 9978x-transition:enter-start="opacity-0" 9979x-transition:enter-end="opacity-100" 9980x-transition:leave="transition ease-in duration-150" 9981x-transition:leave-start="opacity-100" 9982x-transition:leave-end="opacity-0" 9983@click="$store.promo.close()" 9984class="fixed inset-0 bg-black/50 backdrop-blur-sm" 9985></div><div @click.self="$store.promo.close()" class="fixed inset-0 flex items-center justify-center p-4"><div 9986x-show="$store.promo.isOpen" 9987x-transition:enter="transition ease-out duration-200" 9988x-transition:enter-start="opacity-0 scale-95" 9989x-transition:enter-end="opacity-100 scale-100" 9990x-transition:leave="transition ease-in duration-150" 9991x-transition:leave-start="opacity-100 scale-100" 9992x-transition:leave-end="opacity-0 scale-95" 9993@click.stop 9994class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6" 9995> <button 9996@click="$store.promo.close()" 9997type="button" 9998class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 9999> <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 10000x-show="$store.promo.success" 10001x-transition:enter="transition ease-out duration-300" 10002x-transition:enter-start="opacity-0 scale-95" 10003x-transition:enter-end="opacity-100 scale-100" 10004class="flex flex-col items-center justify-center py-4" 10005><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 10006type="text" 10007id="promo-code-input" 10008x-model="$store.promo.code" 10009placeholder="Enter promo code" 10010class="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" 10011:disabled="$store.promo.isLoading" 10012@keydown.escape="$store.promo.close()" 10013/> <button 10014type="button" 10015@click="navigator.clipboard.readText().then(text => { $store.promo.code = text.trim(); })" 10016class="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" 10017title="Paste from clipboard" 10018:disabled="$store.promo.isLoading" 10019> <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 10020x-show="$store.promo.error" 10021x-transition 10022class="text-sm text-red-600 text-center" 10023x-text="$store.promo.error" 10024></div><div class="flex gap-3"> <button 10025@click="$store.promo.close()" 10026type="button" 10027class="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" 10028:disabled="$store.promo.isLoading" 10029> Cancel </button> <button 10030type="submit" 10031class="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" 10032:disabled="$store.promo.isLoading || !$store.promo.code.trim()" 10033> <span x-show="!$store.promo.isLoading">
10033Apply 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 10034x-data="{ open: false }" 10035@expedited-added.window="open = true" 10036x-show="open" 10037x-cloak 10038class="fixed inset-0 z-[100] overflow-y-auto" 10039aria-labelledby="expedited-modal-title" 10040role="dialog" 10041aria-modal="true" 10042><div 10043x-show="open" 10044x-transition:enter="transition ease-out duration-200" 10045x-transition:enter-start="opacity-0" 10046x-transition:enter-end="opacity-100" 10047x-transition:leave="transition ease-in duration-150" 10048x-transition:leave-start="opacity-100" 10049x-transition:leave-end="opacity-0" 10050@click="open = false" 10051class="fixed inset-0 bg-black/50 backdrop-blur-sm" 10052></div><div @click.self="open = false" class="fixed inset-0 flex items-center justify-center p-4"><div 10053x-show="open" 10054x-transition:enter="transition ease-out duration-200" 10055x-transition:enter-start="opacity-0 scale-95" 10056x-transition:enter-end="opacity-100 scale-100" 10057x-transition:leave="transition ease-in duration-150" 10058x-transition:leave-start="opacity-100 scale-100" 10059x-transition:leave-end="opacity-0 scale-95" 10060@click.stop 10061class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6" 10062> <button 10063@click="open = false" 10064type="button" 10065class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 10066> <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 10067@click="open = false" 10068type="button" 10069class="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" 10070> Close </button></div></div></div></div><div 10071x-data 10072x-show="$store.upsell.isOpen" 10073x-cloak 10074@keydown.escape.window="$store.upsell.isOpen && $store.upsell.skip()" 10075class="fixed inset-0 z-[120] overflow-y-auto" 10076role="dialog" 10077aria-modal="true" 10078aria-labelledby="upsell-modal-title" 10079style="display: none;" 10080><div 10081x-show="$store.upsell.isOpen" 10082x-transition:enter="transition ease-out duration-200" 10083x-transition:enter-start="opacity-0" 10084x-transition:enter-end="opacity-100" 10085x-transition:leave="transition ease-in duration-150" 10086x-transition:leave-start="opacity-100" 10087x-transition:leave-end="opacity-0" 10088@click="$store.upsell.skip()" 10089class="fixed inset-0 bg-black/50 backdrop-blur-sm" 10090></div><div class="fixed inset-0 flex items-center justify-center p-4"><div 10091x-show="$store.upsell.isOpen" 10092x-transition:enter="transition ease-out duration-200" 10093x-transition:enter-start="opacity-0 scale-95" 10094x-transition:enter-end="opacity-100 scale-100" 10095x-transition:leave="transition ease-in duration-150" 10096x-transition:leave-start="opacity-100 scale-100" 10097x-transition:leave-end="opacity-0 scale-95" 10098@click.stop 10099class="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col" 10100><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" 10101: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>
10101 <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" 10102@click="$store.upsell.toggle(product.id)" 10103class="w-full flex items-center justify-center gap-1.5 py-2 text-sm font-medium rounded-lg border transition-colors" 10104: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" 10105class="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()" 10106class="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()" 10107:disabled="$store.upsell.isLoading" 10108class="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 }" 10109x-show="$store.auth.isStaff" 10110x-cloak 10111@mouseenter="open = true" 10112@mouseleave="open = false" 10113class="fixed left-0 top-3/4 -translate-y-1/2 z-[9999] hidden lg:block" 10114style="display: none;"><div x-show="!open" 10115x-transition:enter="transition ease-out duration-200" 10116x-transition:enter-start="opacity-0 -translate-x-2" 10117x-transition:enter-end="opacity-100 translate-x-0" 10118x-transition:leave="transition ease-in duration-150" 10119x-transition:leave-start="opacity-100" 10120x-transition:leave-end="opacity-0" 10121class="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" 10122x-transition:enter="transition ease-out duration-200" 10123x-transition:enter-start="opacity-0 -translate-x-4" 10124x-transition:enter-end="opacity-100 translate-x-0" 10125x-transition:leave="transition ease-in duration-150" 10126x-transition:leave-start="opacity-100 translate-x-0" 10127x-transition:leave-end="opacity-0 -translate-x-4" 10128class="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/' : '#'" 10129target="_blank" 10130class="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">
10130 <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> <a :href="$store.auth.adminEditBase ? $store.auth.adminEditBase + 'type/cms_page/id/37/' : '#'" 10131target="_blank" 10132class="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"> <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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> </svg> <span class="flex flex-col leading-tight"> <span>Edit CMS Page</span> <span class="text-xs text-gray-500">About Us - Simply Stamps</span> </span> </a></li><li> <a :href="$store.auth.adminEditBase ? $store.auth.adminEditBase + 'type/zephyr_layout/id/33/' : '#'" 10133target="_blank" 10134class="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"> <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 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/> </svg> <span class="flex flex-col leading-tight"> <span>Edit Zephyr Block</span> <span class="text-xs text-gray-500">ss about us</span> </span> </a></li><li class="border-t border-gray-200 pt-2 mt-2"> <button @click="$store.navEdit.open()" 10135class="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>
10135<script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() { 10136Mage.Cookies.set( 10137'FASTLY_CDN_ENV', 10138'250e7c153723fdf375c9db11b6b2b305', 10139new Date(new Date().getTime() + 86400000) 10140); 10141}); </script>
10141
10141<script type="text/javascript"> (function () { 10142var name = 'FASTLY_CDN_FORMKEY'; 10143if (document.cookie.indexOf(name + '=') !== -1) { 10144return; 10145} 10146var expires = new Date(new Date().getTime() + 86400000); 10147document.cookie = name + '=' + 'e3374479ab86b5888526972805eb3d8f' 10148+ '; expires=' + expires.toUTCString() 10149+ '; path=/' + '; domain=.www.simplystamps.com'; 10150})(); </script>
10150<div id="wizardModal" 10151x-data="{ isOpen: false, wizardUrl: '', showConfirm: false, showResume: false, isLoading: true, hasSession: false, pendingUrl: '' }" 10152x-show="isOpen || showResume" 10153x-cloak 10154@keydown.escape.window="showConfirm ? showConfirm = false : (showResume ? showResume = false : null)" 10155class="fixed inset-0 z-50 overflow-hidden" 10156style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 transition-opacity" 10157@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" 10158x-transition:leave="transition ease-in duration-300" 10159x-transition:leave-start="opacity-100" 10160x-transition:leave-end="opacity-0" 10161class="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" 10162:src="wizardUrl" 10163@load="isLoading = false; clearTimeout(window._wizardLoadTimeout);" 10164class="w-full h-full" 10165frameborder="0" 10166allow="clipboard-read; clipboard-write" 10167title="Product Customization Wizard"> </iframe> </template></div></div><div x-show="showResume" 10168x-transition:enter="transition ease-out duration-200" 10169x-transition:enter-start="opacity-0" 10170x-transition:enter-end="opacity-100" 10171x-transition:leave="transition ease-in duration-150" 10172x-transition:leave-start="opacity-100" 10173x-transition:leave-end="opacity-0" 10174class="fixed inset-0 z-[60] flex items-center justify-center p-4" 10175style="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" 10176x-transition:enter="transition ease-out duration-200" 10177x-transition:enter-start="opacity-0 scale-95" 10178x-transition:enter-end="opacity-100 scale-100" 10179x-transition:leave="transition ease-in duration-150" 10180x-transition:leave-start="opacity-100 scale-100" 10181x-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); })" 10182class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"> 10183Start New Design </button> <button @click="showResume = false; isOpen = true;" 10184class="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" 10185x-transition:enter="transition ease-out duration-200" 10186x-transition:enter-start="opacity-0" 10187x-transition:enter-end="opacity-100" 10188x-transition:leave="transition ease-in duration-150" 10189x-transition:leave-start="opacity-100" 10190x-transition:leave-end="opacity-0" 10191class="fixed inset-0 z-[60] flex items-center justify-center p-4" 10192style="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" 10193x-transition:enter="transition ease-out duration-200" 10194x-transition:enter-start="opacity-0 scale-95" 10195x-transition:enter-end="opacity-100 scale-100" 10196x-transition:leave="transition ease-in duration-150" 10197x-transition:leave-start="opacity-100 scale-100" 10198x-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" 10199class="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); })" 10200class="px-4 py-2 text-sm font-medium text-white bg-brand-primary rounded-lg hover:bg-brand-primary/90 transition-colors"> 10201Close Designer </button></div></div></div></div>
10201<script> window._wizardLoadTimeout = null;// Internal: load a fresh wizard URL into the iframe 10202window._wizardStartNew = function(url) { 10203var modal = document.getElementById('wizardModal'); 10204if (!modal || !modal._x_dataStack) return; 10205var data = modal._x_dataStack[0]; 10206data.wizardUrl = ''; 10207setTimeout(function() { 10208data.wizardUrl = url; 10209}, 0); 10210// Fallback: if iframe hasn't loaded after 30s, show error state 10211clearTimeout(window._wizardLoadTimeout); 10212window._wizardLoadTimeout = setTimeout(function() { 10213if (data.isLoading && data.isOpen) { 10214console.error('[Wizard] Load timeout â iframe did not respond'); 10215data.isLoading = false; 10216data.hasSession = false; 10217var iframe = document.getElementById('wizard_wrapper'); 10218if (iframe) { 10219iframe.srcdoc = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;flex-direction:column;gap:16px;color:#374151;">' 10220+ '<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>' 10221+ '<p style="font-size:18px;font-weight:600;margin:0;">Something went wrong</p>' 10222+ '<p style="font-size:14px;color:#6b7280;margin:0;">The designer took too long to load.</p>' 10223+ '<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>' 10224+ '</div>'; 10225} 10226} 10227}, 30000); 10228};// Set when a customer signs in or registers inside the wizard iframe. 10229// 10230// The ajax-login module calls parent.LogInParent() across the frame boundary after a 10231// successful login or registration. Defining that name here is what makes the call land 10232// on Zephyr â without it the notification is silently dropped, since this theme has its 10233// own wizard modal and never loads the PCE one that defines LogInParent. 10234window._wizardAuthChanged = false; 10235window.LogInParent = function() { 10236console.log('[Wizard] Customer authenticated inside the iframe'); 10237window._wizardAuthChanged = true; 10238};// Internal: handle post-close actions (cart refresh etc) 10239window._wizardOnClose = function(refreshCart) { 10240clearTimeout(window._wizardLoadTimeout);// The page behind the wizard was rendered for a logged-out visitor, so everything 10241// keyed off login state is stale â most visibly the My Account entry in the nav. 10242// A reload also picks up the cart, so it supersedes the refresh below. 10243if (window._wizardAuthChanged) { 10244window._wizardAuthChanged = false; 10245console.log('[Wizard] Auth changed inside wizard, reloading host page'); 10246window.location.reload(); 10247return; 10248}if (refreshCart && window.Alpine && Alpine.store('cart')) { 10249setTimeout(function() { 10250console.log('[Wizard] Refreshing cart data...'); 10251Alpine.store('cart').refresh(); 10252}, 300); 10253} 10254};// Global function to open wizard modal 10255window.openWizardModal = function(url) { 10256console.log('[Wizard] Opening modal with URL:', url); 10257var modal = document.getElementById('wizardModal'); 10258if (modal && modal._x_dataStack) { 10259var data = modal._x_dataStack[0]; 10260// If there's an existing session, ask user what to do 10261if (data.hasSession && data.wizardUrl) { 10262data.pendingUrl = url; 10263data.showResume = true; 10264return; 10265} 10266// Fresh open â load the wizard 10267data.isLoading = true; 10268data.isOpen = true; 10269data.hasSession = false; 10270window._wizardStartNew(url); 10271} else { 10272console.error('[Wizard] Modal not found or Alpine.js not initialized'); 10273} 10274};// Allow wizard iframe to close the modal and optionally refresh cart 10275window.closeWizardModal = function(refreshCart) { 10276if (typeof refreshCart === 'undefined') refreshCart = false; 10277console.log('[Wizard] Closing modal, refresh cart:', refreshCart); 10278var modal = document.getElementById('wizardModal'); 10279if (modal && modal._x_dataStack) { 10280var data = modal._x_dataStack[0]; 10281data.isOpen = false; 10282// If the wizard completed (added to cart), destroy the session 10283if (refreshCart) { 10284data.hasSession = false; 10285data.wizardUrl = ''; 10286} else { 10287data.hasSession = true; 10288} 10289window._wizardOnClose(refreshCart); 10290} 10291};// Legacy compatibility: alias for old PCE modal functions 10292window.closeModalWindow = function() { 10293window.closeWizardModal(true); 10294};// Minimal jQuery shim for PCE wizard iframe compatibility 10295// newpce.js calls window.parent.jQuery("iframe#wizard_wrapper").height() 10296if (typeof window.jQuery === 'undefined') { 10297window.jQuery = function(selector) { 10298var el = document.querySelector(selector); 10299return { 10300height: function() { return el ? el.clientHeight : window.innerHeight; } 10301}; 10302}; 10303}// Called by wizard when user clicks close button - shows confirmation dialog 10304window.requestCloseIframe = function() {
10305console.log('[Wizard] Close requested, showing confirmation'); 10306var modal = document.getElementById('wizardModal'); 10307if (modal && modal._x_dataStack) { 10308modal._x_dataStack[0].showConfirm = true; 10309} 10310};window.redirectToCart = function() { 10311window.location.href = '/checkout/cart'; 10312}; </script>
10312<div x-data="loginModal()" 10313x-show="$store.auth.showLoginModal" 10314x-cloak 10315@keydown.escape.window="$store.auth.showLoginModal = false" 10316class="fixed inset-0 z-50 overflow-y-auto" 10317style="display: none;"><div x-show="$store.auth.showLoginModal" 10318x-transition:enter="transition ease-out duration-300" 10319x-transition:enter-start="opacity-0" 10320x-transition:enter-end="opacity-100" 10321x-transition:leave="transition ease-in duration-200" 10322x-transition:leave-start="opacity-100" 10323x-transition:leave-end="opacity-0" 10324@click="if (Date.now() - $store.auth._loginOpenedAt > 300) $store.auth.showLoginModal = false" 10325class="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" 10326x-transition:enter="transition ease-out duration-300" 10327x-transition:enter-start="opacity-0 transform scale-95" 10328x-transition:enter-end="opacity-100 transform scale-100" 10329x-transition:leave="transition ease-in duration-200" 10330x-transition:leave-start="opacity-100 transform scale-100" 10331x-transition:leave-end="opacity-0 transform scale-95" 10332@click.stop 10333class="relative bg-white rounded-lg shadow-xl w-full max-w-md"> <button @click="$store.auth.showLoginModal = false" 10334class="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" 10335: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'" 10336class="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" 10337alt="Simply Stamps" 10338class="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" 10339id="login-email" 10340name="login[username]" 10341x-model="loginForm.username" 10342required 10343class="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" 10344id="login-password" 10345name="login[password]" 10346x-model="loginForm.password" 10347required 10348class="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" 10349id="login-remember" 10350name="persistent_remember_me" 10351x-model="loginForm.rememberMe" 10352class="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" 10353:disabled="loading" 10354class="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" 10355@click="switchForm('register')" 10356class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10356ded-md transition-colors"> Create an Account </button></div><div class="mt-4 text-center"> <button type="button" 10357@click="switchForm('forgot')" 10358class="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" 10359: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'" 10360class="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" 10361alt="Simply Stamps" 10362class="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" 10363id="register-firstname" 10364name="firstname" 10365x-model="registerForm.firstname" 10366required 10367class="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" 10368id="register-lastname" 10369name="lastname" 10370x-model="registerForm.lastname" 10371required 10372class="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" 10373id="register-email" 10374name="email" 10375x-model="registerForm.email" 10376required 10377class="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" 10378id="register-password" 10379name="password" 10380x-model="registerForm.password" 10381required 10382class="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" 10383id="register-confirmation" 10384name="confirmation" 10385x-model="registerForm.confirmation" 10386required 10387class="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" 10388id="register-newsletter" 10389name="is_subscribed" 10390x-model="registerForm.newsletter" 10391value="1" 10392class="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" 10393:disabled="loading" 10394class="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" 10395@click="switchForm('login')" 10396class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10396ded-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" 10397: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'" 10398class="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" 10399alt="Simply Stamps" 10400class="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" 10401id="forgot-email" 10402name="email" 10403x-model="forgotForm.email" 10404required 10405class="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" 10406:disabled="loading" 10407class="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" 10408@click="switchForm('login')" 10409class="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>
10409<script> function loginModal() { 10410return { 10411currentForm: 'login', 10412loading: false, 10413message: '', 10414messageType: 'error', 10415formKey: 'e3374479ab86b5888526972805eb3d8f',loginForm: { 10416username: '', 10417password: '', 10418rememberMe: false 10419},registerForm: { 10420firstname: '', 10421lastname: '', 10422email: '', 10423password: '', 10424confirmation: '', 10425newsletter: false 10426},forgotForm: { 10427email: '' 10428},switchForm(formName) { 10429this.currentForm = formName; 10430this.message = ''; 10431this.loading = false; 10432},async submitLogin() { 10433this.loading = true; 10434this.message = '';const formData = new FormData(); 10435formData.append('form_key', this.formKey); 10436formData.append('login[username]', this.loginForm.username); 10437formData.append('login[password]', this.loginForm.password); 10438if (this.loginForm.rememberMe) { 10439formData.append('persistent_remember_me', 'on'); 10440}try { 10441const response = await fetch('https://www.simplystamps.com/zephyr/auth/login/', { 10442method: 'POST', 10443body: formData, 10444credentials: 'same-origin' 10445});const data = await response.json();if (data.success) { 10446if (typeof zGA4 === 'function') { 10447zGA4('login', { method: 'site' }); 10448if (data.hashedUserData) { 10449zGA4('set_user_data', { user_data: data.hashedUserData }); 10450} 10451} 10452window.location.reload(); 10453} else { 10454await new Promise(resolve => setTimeout(resolve, 800)); 10455this.message = data.error || 'Invalid login or password.'; 10456this.messageType = 'error'; 10457this.loading = false; 10458} 10459} catch (error) { 10460await new Promise(resolve => setTimeout(resolve, 800)); 10461this.message = 'An error occurred. Please try again.'; 10462this.messageType = 'error'; 10463this.loading = false; 10464} 10465},async submitRegister() { 10466this.loading = true; 10467this.message = '';if (this.registerForm.password !== this.registerForm.confirmation) { 10468await new Promise(resolve => setTimeout(resolve, 800)); 10469this.message = 'Passwords do not match.'; 10470this.messageType = 'error'; 10471this.loading = false; 10472return; 10473}const formData = new FormData(); 10474formData.append('form_key', this.formKey); 10475formData.append('firstname', this.registerForm.firstname); 10476formData.append('lastname', this.registerForm.lastname); 10477formData.append('email', this.registerForm.email); 10478formData.append('password', this.registerForm.password); 10479formData.append('confirmation', this.registerForm.confirmation); 10480if (this.registerForm.newsletter) { 10481formData.append('is_subscribed', '1'); 10482}const recaptchaToken = await getZephyrRecaptchaToken('register'); 10483if (recaptchaToken) { 10484formData.append('recaptcha_response', recaptchaToken); 10485}try { 10486const response = await fetch('https://www.simplystamps.com/zephyr/auth/register/', { 10487method: 'POST', 10488body: formData, 10489credentials: 'same-origin' 10490});const data = await response.json();if (data.success) { 10491if (typeof zGA4 === 'function') { 10492zGA4('sign_up', { method: 'site' }); 10493if (data.hashedUserData) { 10494zGA4('set_user_data', { user_data: data.hashedUserData }); 10495} 10496} 10497window.location.reload(); 10498} else { 10499await new Promise(resolve => setTimeout(resolve, 800)); 10500this.message = data.error || 'There was a problem with your registration.'; 10501this.messageType = 'error'; 10502this.loading = false; 10503} 10504} catch (error) { 10505await new Promise(resolve => setTimeout(resolve, 800)); 10506this.message = 'An error occurred. Please try again.'; 10507this.messageType = 'error'; 10508this.loading = false; 10509} 10510},async submitForgot() { 10511this.loading = true; 10512this.message = '';const formData = new FormData(); 10513formData.append('form_key', this.formKey); 10514formData.append('email', this.forgotForm.email);// reCAPTCHA v3. The forgotpasswordpost endpoint rejects a token-less post 10515// outright, so no token means no reset email. 10516const recaptchaToken = await getZephyrRecaptchaToken('forgot'); 10517if (recaptchaToken) { 10518formData.append('recaptcha_response', recaptchaToken); 10519}try { 10520const response = await fetch('https://www.simplystamps.com/customer/account/forgotpasswordpost/', { 10521method: 'POST', 10522body: formData, 10523credentials: 'same-origin' 10524});const text = await response.text();if (text.includes('success-msg') || text.includes('password-reset')) { 10525this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 10526this.messageType = 'success'; 10527this.forgotForm.email = ''; 10528} else if (text.includes('error-msg')) { 10529const parser = new DOMParser(); 10530const doc = parser.parseFromString(text, 'text/html'); 10531const errorMsg = doc.querySelector('.error-msg'); 10532this.message = errorMsg ? errorMsg.textContent.trim() : 'Unable to send password reset email.'; 10533this.messageType = 'error'; 10534} else { 10535this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 10536this.messageType = 'success'; 10537} 10538} catch (error) { 10539this.message = 'An error occurred. Please try again.'; 10540this.messageType = 'error'; 10541} finally { 10542this.loading = false;
10543} 10544} 10545} 10546} </script>
10546 <input id="searchFocusProxy" type="text" aria-hidden="true" 10547style="position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;" /><div id="searchModal" 10548x-data="searchModal()" 10549x-show="$store.search.isOpen" 10550x-cloak 10551@keydown.escape.window="$store.search.close()" 10552class="fixed inset-0 z-[70] overflow-hidden" 10553style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm transition-opacity hidden md:block" 10554x-show="$store.search.isOpen" 10555x-transition:enter="transition ease-out duration-300" 10556x-transition:enter-start="opacity-0" 10557x-transition:enter-end="opacity-100" 10558x-transition:leave="transition ease-in duration-200" 10559x-transition:leave-start="opacity-100" 10560x-transition:leave-end="opacity-0" 10561@click="$store.search.close()"></div><div class="fixed inset-0 flex items-start justify-center md:pt-12 md:px-4" 10562@mousedown.self="$store.search.close()"><div x-show="$store.search.isOpen" 10563class="relative bg-white w-full h-full flex flex-col md:h-auto md:max-w-2xl md:rounded-xl md:shadow-2xl" 10564x-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" 10565alt="Simply Stamps" 10566class="h-8 w-auto" /> </a> <button @click="$store.search.close()" 10567class="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" 10568d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg></div> <input type="text" 10569x-ref="searchInput" 10570x-model="query" 10571@input="onInput()" 10572@keydown.down.prevent="selectNext" 10573@keydown.tab.prevent="selectNext" 10574@keydown.up.prevent="selectPrev" 10575@keydown.enter.prevent="selectCurrent" 10576placeholder="Search products, articles..." 10577class="w-full pl-12 pr-24 py-4 text-lg border-2 border-gray-200 rounded-xl 10578focus:border-brand-primary focus:outline-none focus:ring-0 10579transition-colors" 10580autocomplete="off" /><div x-show="$store.search.isLoading" 10581class="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" 10582@click="clearSearch()" 10583type="button" 10584class="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()" 10585type="button" 10586:disabled="query.length < minChars" 10587class="absolute right-2 flex items-center justify-center h-10 px-3 bg-brand-primary text-white 10588rounded-lg hover:bg-brand-primary-hover transition-colors 10589disabled: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" 10590d="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" 10591@click.prevent="selectPopular('self-inking stamp')" 10592class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10593rounded-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" 10594d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> self-inking stamp </button> <button type="button" 10595@click.prevent="selectPopular('wood handle stamp')" 10596class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10597rounded-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" 10598d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> wood handle stamp </button> <button type="button" 10599@click.prevent="selectPopular('library embosser')" 10600class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10601rounded-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" 10602d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> library embosser </button> <button type="button" 10603@click.prevent="selectPopular('wedding address stamps')" 10604class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10605rounded-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" 10606d="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" 10607x-ref="resultsPane" 10608class="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)" 10609:class="{ 'bg-blue-50 text-blue-900': selectedIndex === index }" 10610class="flex items-center gap-3 px-3 py-1.5 rounded-lg 10611hover:bg-gray-50 transition-colors cursor-pointer group" 10612@mousemove="selectedIndex = index" 10613@click.prevent="selectSuggestion(suggestion)"> <svg class="w-4 h-4 text-gray-300 shrink-0" :class="{ 'text-blue-400': selectedIndex === index }" 10614fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10615d="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" 10616x-transition:enter="transition ease-out duration-200" 10617x-transition:enter-start="opacity-0" 10618x-transition:enter-end="opacity-100" 10619x-transition:leave="transition ease-in duration-150" 10620x-transition:leave-start="opacity-100" 10621x-transition:leave-end="opacity-0" 10622class="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" 10623:class="{ 10624'border-t border-gray-300': suggestionsCount > 0 || (blogFirst && blogCount > 0), 10625'opacity-40 pointer-events-none': $store.search.isLoading 10626}"><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" 10627:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (productsOffset + index) }" 10628class="flex items-center gap-4 p-3 rounded-lg border border-transparent 10629hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer" 10630@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" 10631class="w-full h-full object-cover" 10632onerror="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" 10633class="text-xs text-gray-500 line-clamp-1 mt-0.5" 10634x-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" 10635:class="{ 10636'order-3': blogFirst, 10637'order-5': !blogFirst, 10638'border-t border-gray-300': !blogFirst || suggestionsCount > 0, 10639'opacity-40 pointer-events-none': $store.search.isLoading 10640}"><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" 10641:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (blogOffset + index) }" 10642class="flex items-center gap-4 p-3 rounded-lg border border-transparent 10643hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer" 10644@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" 10645d="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" 10646class="text-xs text-gray-500 line-clamp-1 mt-0.5" 10647x-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" 10648class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10649rounded-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" 10650d="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" 10651:href="'https://www.simplystamps.com/catalogsearch/result/?q=' + encodeURIComponent(query)" 10652class="flex items-center gap-1 text-sm font-medium text-brand-primary hover:text-brand-primary-hover 10653transition-colors ml-auto"> 10654View 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>
10654<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/search-modal.js?v=1126"></script>
10654
10654<script> searchModalConfig.minChars = 3; 10655searchModalConfig.searchUrl = 'https://www.simplystamps.com/searchautocomplete/ajax/get/'; 10656searchModalConfig.suggestUrl = 'https://www.simplystamps.com/zephyr/search/suggest/'; 10657searchModalConfig.resultUrl = 'https://www.simplystamps.com/catalogsearch/result/'; 10658searchModalConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10659searchModalConfig.suggestedQueries = ["self-inking stamp","wood handle stamp","library embosser","wedding address stamps"]; 10660searchModalConfig.blogFirst = false; </script>
10660<style> .line-clamp-1 { 10661overflow: hidden; 10662display: -webkit-box; 10663-webkit-box-orient: vertical; 10664-webkit-line-clamp: 1; 10665} </style>
10665<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
10665</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>
10665<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>
10665
10665<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/rileyprint.js?v=1126" defer></script>
10665
10665<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/product-media.js?v=1126" defer></script>
10665
10665<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/file-upload.js?v=1126" defer></script>
10665
10665<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-drawer.js?v=1126" defer></script>
10665
10665<script> // Configure cart drawer URLs. cart-drawer.js is now deferred (its var cartDrawerConfig 10666// default object still runs before DOMContentLoaded per spec), and every read of 10667// cartDrawerConfig.* inside cart-drawer.js happens lazily inside user-triggered 10668// handlers (open drawer, add/remove item, checkout) â none of it runs at 10669// alpine:init â so it's safe to set these values on DOMContentLoaded too. 10670document.addEventListener('DOMContentLoaded', function() { 10671cartDrawerConfig.getItemsUrl = 'https://www.simplystamps.com/zephyr/cart/getItems/'; 10672cartDrawerConfig.deleteUrl = 'https://www.simplystamps.com/checkout/cart/delete/'; 10673cartDrawerConfig.removeItemsUrl = 'https://www.simplystamps.com/zephyr/cart/removeItems/'; 10674cartDrawerConfig.updatePostUrl = 'https://www.simplystamps.com/checkout/cart/updatePost/'; 10675cartDrawerConfig.clearUrl = 'https://www.simplystamps.com/zephyr/cart/clear/'; 10676cartDrawerConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/'; 10677cartDrawerConfig.cartUrl = 'https://www.simplystamps.com/checkout/cart/'; 10678cartDrawerConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10679// Cart drawer feature settings (from admin config) 10680cartDrawerConfig.enableCheckboxDelete = true; 10681cartDrawerConfig.enableIndividualDelete = true; 10682cartDrawerConfig.showClearCart = true; 10683cartDrawerConfig.checkoutButtonText = 'Proceed to Checkout'; 10684}); </script>
10684
10684<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-upsell.js?v=1126"></script>
10684
10684<script> // Configure cart upsell popup 10685cartUpsellConfig.getUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/getUpsells/'; 10686cartUpsellConfig.addUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/addUpsells/'; 10687cartUpsellConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/'; 10688cartUpsellConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10689cartUpsellConfig.enabled = true; 10690cartUpsellConfig.storeId = '5'; 10691cartUpsellConfig.devMode = false; </script>
10691<script> // Fetch a reCAPTCHA v3 token, lazy-loading Google's api.js on first use so it 10692// adds no page weight until a protected form is actually submitted. Shared by 10693// every Zephyr form protected by v3 (registration, contact, quote), which is 10694// also why it must be the ONLY thing loading recaptcha on modern pages â a 10695// v3 key can't execute against a grecaptcha client created by a bare v2 10696// api.js load, so don't reintroduce v2 script tags into modern templates. 10697// Resolves null when reCAPTCHA is disabled or the script/execution fails â 10698// the server decides what to do with a missing token. Never rejects or hangs. 10699window.zephyrRecaptchaKey = '6Lem-q0gAAAAAMhOqUz2tke2IN3t0Pt37x5oqRq3'; 10700window.getZephyrRecaptchaToken = function(action) { 10701var key = window.zephyrRecaptchaKey; 10702if (!key) return Promise.resolve(null); 10703return new Promise(function(resolve) { 10704var settled = false; 10705var done = function(token) { if (!settled) { settled = true; resolve(token || null); } }; 10706// Whatever happens, never leave the submit hanging 10707setTimeout(function() { done(null); }, 15000);function execute() { 10708try { 10709grecaptcha.ready(function() { 10710try { 10711grecaptcha.execute(key, { action: action }) 10712.then(done, function() { done(null); }); 10713} catch (e) { done(null); } 10714}); 10715} catch (e) { done(null); } 10716}if (window.grecaptcha) { execute(); return; } 10717var s = document.createElement('script'); 10718s.src = 'https://www.google.com/recaptcha/api.js?render=' + key; 10719s.onload = execute; 10720s.onerror = function() { done(null); }; 10721document.head.appendChild(s); 10722}); 10723}; </script>
10723
10723<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/promo-code.js?v=1126"></script>
10723
10723<script> // Configure promo code URLs 10724promoCodeConfig.applyCouponUrl = 'https://www.simplystamps.com/zephyr/cart/applyCoupon/'; 10725promoCodeConfig.removeCouponUrl = 'https://www.simplystamps.com/zephyr/cart/removeCoupon/'; 10726promoCodeConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; </script>
10726
10726<script>function setLocation(url){window.location.href=url;}</script>
10726
10726<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/alpine.min.js?v=1126" defer></script>
10726
10726<script> document.addEventListener('alpine:init', () => { 10727// Global UI state - controls visibility of shared UI elements 10728Alpine.store('ui', { 10729hideHeader: false, 10730// Header scroll state (updated by stickyHeader component) 10731headerVisible: true, 10732headerHeight: 0,// Call this when opening fullscreen overlays (lightbox, modals, etc.) 10733enterFullscreen() { 10734this.hideHeader = true; 10735},// Call this when closing fullscreen overlays 10736exitFullscreen() { 10737this.hideHeader = false; 10738},// Get the effective top position for fixed elements below the header 10739getHeaderOffset() { 10740// If header is hidden (scroll or fullscreen), no offset needed 10741if (this.hideHeader || !this.headerVisible) { 10742return 0; 10743} 10744return this.headerHeight; 10745} 10746});Alpine.store('auth', { 10747showLoginModal: false, 10748showRefreshOverlay: false, 10749refreshMessage: '', 10750isLoggedIn: false, 10751isStaff: false, 10752adminEditBase: '', 10753customerName: '', 10754customerEmail: '', 10755companyStoreUrl: '', 10756cartCount: 0, 10757cartSubtotal: 0, 10758formKey: '', 10759ready: false, 10760_loginOpenedAt: 0,init() { 10761// State is hydrated via AJAX so the page HTML stays static/compressible. 10762this.fetchStatus(); 10763},/** 10764* Fetch auth status from session via AJAX. 10765* Primary hydration path (called from init()), and also used 10766* after login/logout to refresh state reactively. 10767*/ 10768fetchStatus() { 10769var self = this; 10770fetch('https://www.simplystamps.com/zephyr/auth/status/', { 10771credentials: 'same-origin', 10772headers: { 'X-Requested-With': 'XMLHttpRequest' } 10773}) 10774.then(function(r) { return r.json(); }) 10775.then(function(data) { 10776self.isLoggedIn = !!data.isLoggedIn; 10777self.isStaff = !!data.isStaff; 10778self.adminEditBase = data.adminEditBase || ''; 10779self.customerName = data.customerName || ''; 10780self.customerEmail = data.customerEmail || ''; 10781self.companyStoreUrl = data.companyStoreUrl || ''; 10782self.cartCount = data.cartCount || 0; 10783self.cartSubtotal = data.cartSubtotal || 0; 10784self.formKey = data.formKey || '';// Hydrate the cart store's badge count / free-shipping subtotal from 10785// this same response â the async equivalent of the inline global 10786// state object the ESI block used to provide synchronously. 10787if (window.Alpine && Alpine.store('cart')) { 10788var cartStore = Alpine.store('cart'); 10789cartStore.itemCount = data.cartCount || 0; 10790cartStore.isEmpty = !(data.cartCount > 0); 10791cartStore.subtotalNumeric = data.cartSubtotal || 0; 10792cartStore._esiReady = true; 10793} 10794if (data.messages && data.messages.length && window.Alpine) { 10795var flash = Alpine.store('flash'); 10796for (var i = 0; i < data.messages.length; i++) { 10797if (flash && flash.addMessage) flash.addMessage(data.messages[i].type, data.messages[i].text); 10798} 10799} 10800self.ready = true; 10801}) 10802.catch(function() { 10803self.ready = true; 10804}); 10805},setLoggedIn(customerData) { 10806this.isLoggedIn = true; 10807this.customerName = customerData.name || '';window.dispatchEvent(new CustomEvent('customer-logged-in', { 10808detail: customerData 10809})); 10810},setLoggedOut() { 10811this.isLoggedIn = false; 10812this.isStaff = false;
10813this.adminEditBase = ''; 10814this.customerName = ''; 10815this.customerEmail = ''; 10816this.companyStoreUrl = '';window.dispatchEvent(new CustomEvent('customer-logged-out')); 10817} 10818});Alpine.store('search', { 10819isOpen: false, 10820isLoading: false, 10821hasSearched: false, 10822_openedAt: 0,open() { 10823// Focus proxy input immediately to preserve iOS user-gesture context 10824// so the virtual keyboard opens. Focus transfers to real input in $nextTick. 10825var proxy = document.getElementById('searchFocusProxy'); 10826if (proxy) proxy.focus(); 10827this.isOpen = true; 10828this._openedAt = Date.now(); 10829},close() { 10830if (Date.now() - this._openedAt < 300) return; 10831this.isOpen = false; 10832} 10833});// Confirmation modal store 10834Alpine.store('confirm', { 10835isOpen: false, 10836title: '', 10837message: '', 10838html: null, 10839confirmText: 'Confirm', 10840cancelText: 'Cancel', 10841confirmStyle: 'danger', // 'danger' or 'primary' 10842onConfirm: null, 10843onCancel: null,show(options) { 10844this.title = options.title || 'Confirm'; 10845this.message = options.message || 'Are you sure?'; 10846this.html = options.html || null; 10847this.confirmText = options.confirmText || 'Confirm'; 10848this.cancelText = options.cancelText || 'Cancel'; 10849this.confirmStyle = options.confirmStyle || 'danger'; 10850this.onConfirm = options.onConfirm || null; 10851this.onCancel = options.onCancel || null; 10852this.isOpen = true; 10853},confirm() { 10854if (this.onConfirm) { 10855this.onConfirm(); 10856} 10857this.close(); 10858},cancel() { 10859if (this.onCancel) { 10860this.onCancel(); 10861} 10862this.close(); 10863},close() { 10864this.isOpen = false; 10865this.onConfirm = null; 10866this.onCancel = null; 10867// Defer clearing html until after the 150ms leave transition 10868// so the image doesn't vanish mid-animation 10869var self = this; 10870setTimeout(function() { 10871if (!self.isOpen) self.html = null; 10872}, 200); 10873} 10874});// Flash messages store â messages arrive via auth store's fetchStatus() (AJAX), 10875// or as inline JSON on pages that embed them directly (fallback below). 10876Alpine.store('flash', { 10877messages: [], 10878_nextId: 1, 10879_seen: {},init() { 10880var self = this;// Fallback: read messages embedded as JSON in page HTML (for pages without ESI block) 10881var scriptEl = document.querySelector('script[data-flash-messages]'); 10882if (scriptEl) { 10883try { 10884var inline = JSON.parse(scriptEl.textContent); 10885for (var i = 0; i < inline.length; i++) { 10886self.addMessage(inline[i].type, inline[i].text); 10887} 10888} catch (e) {} 10889scriptEl.remove(); 10890} 10891},addMessage(type, text) { 10892// Deduplication 10893var key = type + ':' + text; 10894if (this._seen[key]) { 10895return; 10896} 10897this._seen[key] = true;var id = this._nextId++; 10898this.messages.push({ id: id, type: type, text: text, visible: true });// Auto-dismiss non-error messages after 8 seconds 10899if (type !== 'error') { 10900var self = this; 10901setTimeout(function() { 10902self.dismiss(id); 10903}, 8000); 10904} 10905},dismiss(id) { 10906var idx = this.messages.findIndex(function(m) { return m.id === id; }); 10907if (idx !== -1) { 10908this.messages[idx].visible = false; 10909var self = this; 10910setTimeout(function() { 10911self.messages = self.messages.filter(function(m) { return m.id !== id; }); 10912}, 300); 10913} 10914},show(type, text) { 10915this.addMessage(type, text); 10916} 10917});Alpine.store('flash').init(); 10918});// BFCache restoration handler â when iOS Safari restores a page from BFCache, 10919// Alpine stores are frozen with stale data. Refresh critical stores. 10920window.addEventListener('pageshow', function(event) { 10921if (event.persisted) { 10922// Page was restored from BFCache â refresh cart data and auth status 10923if (typeof Alpine !== 'undefined' && Alpine.store) { 10924var cart = Alpine.store('cart'); 10925if (cart) { 10926cart.lastFetch = null; 10927cart.refresh(); 10928} 10929var auth = Alpine.store('auth'); 10930if (auth && typeof auth.fetchStatus === 'function') { 10931auth.fetchStatus(); 10932} 10933} 10934} 10935}); 10936</script>
10936<div x-data 10937x-show="$store.auth.showRefreshOverlay" 10938x-cloak 10939x-transition:enter="transition ease-out duration-200" 10940x-transition:enter-start="opacity-0" 10941x-transition:enter-end="opacity-100" 10942class="fixed inset-0 z-[200] flex items-center justify-center bg-white/80 backdrop-blur-sm" 10943style="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 10944x-data 10945x-show="$store.confirm.isOpen" 10946x-cloak 10947@keydown.escape.window="$store.confirm.cancel()" 10948class="fixed inset-0 z-[100] overflow-y-auto" 10949aria-labelledby="confirm-modal-title" 10950role="dialog" 10951aria-modal="true" 10952><div 10953x-show="$store.confirm.isOpen" 10954x-transition:enter="transition ease-out duration-200" 10955x-transition:enter-start="opacity-0" 10956x-transition:enter-end="opacity-100" 10957x-transition:leave="transition ease-in duration-150" 10958x-transition:leave-start="opacity-100" 10959x-transition:leave-end="opacity-0" 10960@click="$store.confirm.cancel()" 10961class="fixed inset-0 bg-black/50 backdrop-blur-sm" 10962></div><div class="fixed inset-0 flex items-center justify-center p-4"><div 10963x-show="$store.confirm.isOpen" 10964x-transition:enter="transition ease-out duration-200" 10965x-transition:enter-start="opacity-0 scale-95" 10966x-transition:enter-end="opacity-100 scale-100" 10967x-transition:leave="transition ease-in duration-150" 10968x-transition:leave-start="opacity-100 scale-100" 10969x-transition:leave-end="opacity-0 scale-95" 10970@click.stop 10971class="relative bg-white rounded-xl shadow-2xl max-w-md w-full p-6" 10972> <button 10973@click="$store.confirm.cancel()" 10974type="button" 10975class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 10976> <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" 10977:class="$store.confirm.confirmStyle === 'danger' ? 'bg-red-100' : 'bg-blue-100'"> <svg x-show="$store.confirm.confirmStyle === 'danger'" 10978class="h-6 w-6 text-red-600" 10979fill="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'" 10980class="h-6 w-6 text-blue-600" 10981fill="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" 10982class="text-lg font-semibold text-gray-900 text-center mb-2" 10983x-text="$store.confirm.title"></h3><div x-show="$store.confirm.html" class="text-sm text-gray-600 text-center mb-6" 10984x-html="$store.confirm.html"></div><p x-show="!$store.confirm.html" class="text-sm text-gray-600 text-center mb-6" 10985x-text="$store.confirm.message"></p><div class="flex gap-3"> <button 10986@click="$store.confirm.cancel()" 10987type="button" 10988class="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" 10989x-text="$store.confirm.cancelText"> </button> <button 10990@click="$store.confirm.confirm()" 10991type="button" 10992class="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" 10993:class="$store.confirm.confirmStyle === 'danger' 10994? 'bg-red-600 hover:bg-red-700 focus:ring-red-500' 10995: 'bg-brand-primary hover:bg-brand-primary-hover focus:ring-brand-primary'" 10996x-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.