PageSourceSearch

https://www.simplystamps.com/contacts

html simplystamps.com collected 2026-10-02 06:57:37 UTC 803,605 bytes, 11,526 lines download raw bytes

1<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>Contact Us</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&#039;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="Contact Us" /><meta property="og:type" content="article" /><meta property="og:url" content="https://www.simplystamps.com/contacts" /><meta property="og:description" content="Order today, get next business day shipping on custom stamps like self-inking, pre-inked rubber stamps, embossers, office and address stamps. We've customized rubber stamps since 1954 and offer stamps for business, professional and craft." /><meta property="og:site_name" content="Simply Stamps" /><meta property="og:image" content="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" /><link rel="stylesheet" href="https://cdn.simplystamps.com/skin/frontend/zephyr/default/css/print.css?v1126" media="print" >
28<script type="text/javascript"> //<![CDATA[
29// mage/cookies.js now loads via a deferred 
29<script> tag (see head.phtml), so Mage.Cookies
30// isn't guaranteed to exist yet at this point in parsing. Wait for DOMContentLoaded, which
31// fires only after deferred scripts have executed.
32document.addEventListener('DOMContentLoaded', function () {
33Mage.Cookies.path     = '/';
34Mage.Cookies.domain   = '.www.simplystamps.com';
35});
36//]]> </script>
36<link rel="stylesheet" href="https://cdn.simplystamps.com/skin/frontend/zephyr/default/css/dist/theme.css?v=1126"> 
36<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
37new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
38j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
39'https://www.simplystamps.com/d/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
40})(window,document,'script','dataLayer','GTM-TT325PX9');</script>
40 <noscript><iframe src="https://www.simplystamps.com/d/ns.html?id=GTM-TT325PX9"
41height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> 
41<script type="text/javascript" src="https://chimpstatic.com/mcjs-connected/js/users/de94d9b4a6aa148b052eb1fad/fac024488a3ce12a438eb7e65.js" defer></script>
41<style id="zephyr-brand-colors"> /* Zephyr Brand Colors - Store: sstamps_en */
42:root {
43--brand-primary: #002E49;
44--brand-primary-hover: #00456E;
45--brand-secondary: #55B5E3;
46--brand-secondary-hover: #22495C;
47--brand-success: #19A745;
48--brand-warning: #C27803;
49--brand-danger: #E02424;
50--brand-info: #3F83F8;
51--blog-bg: #FFFFFF;
52} </style> 
52<script>if(typeof Translate==='undefined'){var Translate=function(d){this._data=d||{}};Translate.prototype={translate:function(t){return this._data[t]||t},add:function(){}}}</script>
52 
52<script type="text/javascript">//<![CDATA[
53var Translator = new Translate([]);
54//]]></script>
54<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7201785794839578"
55crossorigin="anonymous"></script>
55 
55<script src="https://cdn.simplystamps.com/js/mage/cookies.js" defer></script>
55 
55<script type="text/javascript" src="https://onsite.optimonk.com/script.js?account=25642" async> </script>
55</head><body class="contacts-index-index"><div class="min-h-screen flex 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &rarr; </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 &amp; 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 &rarr; </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 &amp; 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 &rarr; </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 &rarr; </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 &amp; 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 &rarr; </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 &amp; 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 &amp; 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 &rarr; </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 &amp; 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 &amp; 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 &rarr; </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 &amp; 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 &amp; 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 &rarr; </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 &amp; 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 &amp; 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 &rarr; </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 bg-white" style="padding-top: var(--header-height);"><div class="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-2 pb-8"><div> <nav class="breadcrumbs py-4" aria-label="Breadcrumb"><div class="mx-auto"><ol class="flex flex-wrap items-center gap-2 text-sm" itemscope itemtype="https://schema.org/BreadcrumbList"><li class="flex items-center gap-2 home"
1238itemprop="itemListElement"
1239itemscope
1240itemtype="https://schema.org/ListItem"><a href="/"
1241title="Home"
1242itemprop="item"
1243rel="follow"
1244class="text-gray-600 hover:text-brand-primary transition-colors"> <span itemprop="name">Home</span> </a><meta itemprop="position" content="1"/> <svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path> </svg></li><li class="flex items-center gap-2 contacts"
1245itemprop="itemListElement"
1246itemscope
1247itemtype="https://schema.org/ListItem"><span itemprop="name" class="text-gray-900 font-medium"> Contact Us </span><meta itemprop="position" content="2"/></li></ol></div> </nav></div><div x-data x-show="$store.flash.messages.length > 0" x-cloak class="space-y-3 mb-4"> <template x-for="msg in $store.flash.messages" :key="msg.id"><div x-show="msg.visible"
1248x-transition:enter="transition ease-out duration-300"
1249x-transition:enter-start="opacity-0 -translate-y-2"
1250x-transition:enter-end="opacity-100 translate-y-0"
1251x-transition:leave="transition ease-in duration-200"
1252x-transition:leave-start="opacity-100"
1253x-transition:leave-end="opacity-0"
1254role="alert"
1255:class="{
1256'bg-green-50 border-green-400 text-green-800': msg.type === 'success',
1257'bg-red-50 border-red-400 text-red-800': msg.type === 'error',
1258'bg-yellow-50 border-yellow-400 text-yellow-800': msg.type === 'warning' || msg.type === 'notice',
1259'bg-blue-50 border-blue-400 text-blue-800': msg.type !== 'success' && msg.type !== 'error' && msg.type !== 'warning' && msg.type !== 'notice'
1260}"
1261class="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="content-area"><div x-data="contactPage()" class="space-y-8"><div class="text-center mb-8"><h1 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">Contact Us</h1><p class="text-lg text-gray-600 max-w-2xl mx-auto"> Our Florida based customer service team looks forward to assisting you however you need.</p></div><div class="flex flex-col md:flex-row gap-6 mb-8"><div class="flex-1 min-w-0 bg-white rounded-xl shadow-sm border border-gray-200 p-6 pt-3 text-center hover:shadow-md transition-shadow"><div class="w-16 h-16 bg-brand-primary/10 rounded-full flex items-center justify-center mx-auto"> <svg class="w-10 h-10 text-brand-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg></div><h2 class="text-xl font-semibold text-gray-900 mb-2">Track My Order</h2><p class="text-sm text-gray-600 mb-4">Enter your order number and email address to see the status of your order</p><form action="/trackmyorder/lookup" method="post" class="space-y-3"> <input type="hidden" name="form_key" value="e3374479ab86b5888526972805eb3d8f"> <input type="text"
1262name="ordernumber"
1263placeholder="Order Number*"
1264required
1265class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors text-sm"> <input type="email"
1266name="emailaddress"
1267placeholder="Email Address*"
1268required
1269class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors text-sm"> <button type="submit"
1270class="w-full bg-brand-primary hover:bg-brand-primary-hover text-white font-medium py-2 px-4 roun
1270ded-lg transition-colors"> Search </button></form></div><div class="flex-1 min-w-0 bg-white rounded-xl shadow-sm border border-gray-200 p-6 pt-3 text-center hover:shadow-md transition-shadow"><div class="w-16 h-16 bg-brand-primary/10 rounded-full flex items-center justify-center mx-auto"> <svg class="w-10 h-10 text-brand-primary" 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"></path> </svg></div><h2 class="text-xl font-semibold text-gray-900 mb-2">Email</h2><p class="text-xs text-gray-500 mb-2">Available 24/7</p><p class="text-sm text-gray-600 mb-4">Leave us a message and we will get back to you as soon as possible.</p> <button @click="showEmailModal = true"
1271class="w-full bg-brand-primary hover:bg-brand-primary-hover text-white font-medium py-2 px-4 rounded-lg transition-colors"> Email Now </button></div><div class="flex-1 min-w-0 bg-white rounded-xl shadow-sm border border-gray-200 p-6 pt-3 text-center hover:shadow-md transition-shadow"><div class="w-16 h-16 bg-brand-primary/10 rounded-full flex items-center justify-center mx-auto"> <svg class="w-10 h-10 text-brand-primary" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /> </svg></div><h2 class="text-xl font-semibold text-gray-900 mb-2">Quote</h2><p class="text-xs text-gray-500 mb-2">Got a large order?</p><p class="text-sm text-gray-600 mb-4">Request a quote, and our team will <br>assist you with your large order needs.</p> <button @click="showQuoteModal = true"
1272class="w-full bg-brand-primary hover:bg-brand-primary-hover text-white font-medium py-2 px-4 rounded-lg transition-colors">Get a Quote</button></div><div class="flex-1 min-w-0 bg-white rounded-xl shadow-sm border border-gray-200 p-6 pt-3 text-center hover:shadow-md transition-shadow"><div class="w-16 h-16 bg-brand-primary/10 rounded-full flex items-center justify-center mx-auto"> <svg class="w-10 h-10 text-brand-primary" 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"></path> </svg></div><h2 class="text-xl font-semibold text-gray-900 mb-2">Live Chat</h2><p class="text-xs text-gray-500 mb-2">Hours of Operation</p><p class="text-sm text-gray-600 mb-2">Monday - Friday 9:00AM - 8:00PM (ET)</p><p class="text-sm text-gray-600 mb-4">Closed Saturday & Sunday</p><p class="text-sm text-gray-500 mb-3">Chat currently unavailable</p> <button disabled
1273class="w-full bg-gray-300 text-gray-500 font-medium py-2 px-4 rounded-lg cursor-not-allowed"> Chat Now </button></div></div><div class="bg-brand-secondary/10 border border-brand-secondary/30 rounded-xl p-6 pt-3 flex flex-col sm:flex-row items-center justify-between gap-4"><div class="flex items-center gap-4"><div class="w-12 h-12 bg-brand-secondary/20 rounded-full flex items-center justify-center flex-shrink-0"> <svg class="w-6 h-6 text-brand-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path> </svg></div><div><p class="font-medium text-gray-900">Call Us Toll-Free</p><p class="text-sm text-gray-600">M-Th: 10am - 4pm ET</p></div></div> <a href="tel:(888) 688-9998"
1274class="inline-flex items-center gap-2 bg-brand-secondary hover:bg-brand-secondary/90 text-white font-medium py-2 px-6 rounded-lg transition-colors"> <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="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 
127411.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path> </svg> (888) 688-9998 </a></div><div class="bg-brand-warning/10 border border-brand-warning/30 rounded-xl p-6"><h2 class="text-lg font-semibold text-gray-900 mb-2">Need To Cancel Or Modify Your Order?</h2><p class="text-gray-700"> Due to the nature of most of our products being personalized and that we make every effort to process your order the minute it's placed, we cannot guarantee that changes can be made to your order once your order has been submitted. <strong>If an update is needed after you place an order, please Contact Us urgently via chat or telephone during our hours of operation and we will do what we can to make it right.</strong></p></div><div class="zephyr-builder-layout"><div class="zephyr-builder-section"
1275data-section-index="0"
1276data-section-id="sec_66009ef2"
1277data-layout-id="39"
1278data-bg-color=""
1279data-bg-image=""
1280data-overlay-color=""
1281data-overlay-opacity="0"
1282data-full-width="lg"
1283><div class="mx-auto max-w-screen-lg"><div class="zephyr-builder-row"
1284data-section-index="0"
1285data-row-index="0"><div class="zephyr-builder-block"
1286data-placement-id="2971"
1287data-layout-id="39"
1288data-library-block="content_seo_header"> <section class="pt-8 pb-4 border-0"><div class="w-full px-4 sm:px-0 justify-center text-center"><div class="text-center"><h1 class=" text-2xl sm:text-3xl md:text-4xl font-bold  mb-4" style="color: #012e4b;"> FREQUENTLY ASKED QUESTIONS</h1></div></div> </section></div><div class="zephyr-builder-block"
1289data-placement-id="2972"
1290data-layout-id="39"
1291data-library-block="content_faq"> 
1291<script type="application/ld+json"> {
1292"@context": "https://schema.org",
1293"@type": "FAQPage",
1294"mainEntity": [
1295{
1296"@type": "Question",
1297"name": "Do you offer same day shipping?",
1298"acceptedAnswer": {
1299"@type": "Answer",
1300"text": "We do not offer same day shipping. Delivery date will depend on method of shipping chosen at checkout."
1301}
1302},
1303{
1304"@type": "Question",
1305"name": "What is expedited processing?",
1306"acceptedAnswer": {
1307"@type": "Answer",
1308"text": "Expedited processing moves your order to the front of our production line! Your order will be processed and shipped before other orders in queue. Pair with an expedited shipping option to receive your order as fast as possible."
1309}
1310},
1311{
1312"@type": "Question",
1313"name": "How do I check the status of my order?",
1314"acceptedAnswer": {
1315"@type": "Answer",
1316"text": "Once your package is shipped, you will receive a shipping confirmation email that contains your tracking number."
1317}
1318},
1319{
1320"@type": "Question",
1321"name": "Do you provide proofs?",
1322"acceptedAnswer": {
1323"@type": "Answer",
1324"text": "Proof requests should be made in the comments box above the order button on the final checkout page. We will not begin your order until we receive your approval. This may delay production time."
1325}
1326},
1327{
1328"@type": "Question",
1329"name": "I made a mistake on my order, can this be corrected?",
1330"acceptedAnswer": {
1331"@type": "Answer",
1332"text": "You may cancel at any time prior to the order going into production. Once items have been customized, we can no longer accept a cancellation or modification. If you need to change your order for any reason, please contact here and we will be happy to assist you."
1333}
1334},
1335{
1336"@type": "Question",
1337"name": "How do I order different products with the same design or personalization?",
1338"acceptedAnswer": {
1339"@type": "Answer",
1340"text": "Simply create your first item and add it to your cart. From the cart, select ‘Order another with this design’ that will take you back to your original product. Navigate to the new product you would like to order and select ‘yes’ when asked if you would like to use your last design. Add the new item to your cart!"
1341}
1342},
1343{
1344"@type": "Question",
1345"name": "How do I provide my signature for a signature stamp?",
1346"acceptedAnswer": {
1347"@type": "Answer",
1348"text": "Simply create your first item and add it to your cart. From the cart, select ‘Order another with this design’ that will take you back to your original product. Navigate to the new product you would like to order and select ‘yes’ when asked if you would like to use your last design. Add the new item to your cart!"
1349}
1350},
1351{
1352"@type": "Question",
1353"name": "Can I place bulk personalized orders rather than entering items one by one?",
1354"acceptedAnswer": {
1355"@type": "Answer",
1356"text": "Yes! Simply choose your product specifications and then easily upload your names list from the ordering screen."
1357}
1358},
1359{
1360"@type": "Question",
1361"name": "Can I cancel an existing order?",
1362"acceptedAnswer": {
1363"@type": "Answer",
1364"text": "You may cancel at any time prior to the order going into production. Once items have been customized, we can no longer accept a cancellation or modification. If you need to change your order for any reason, please contact here and we will be happy to assist you."
1365}
1366},
1367{
1368"@type": "Question",
1369"name": "Tax Exempt Orders",
1370"acceptedAnswer": {
1371"@type": "Answer",
1372"text": "For orders that are placed by tax exempt organizations, please contact our Customer Focus Team to assign tax exempt status to your account."
1373}
1374},
1375{
1376"@type": "Question",
1377"name": "Can I place an order to be billed separately, rather than by paying online?",
1378"acceptedAnswer": {
1379"@type": "Answer",
1380"text": "Please contact our accounting department for a billing agreement application. Once approved, your online account will include a separate billing option."
1381}
1382}
1383]
1384}</script>
1384<section class="py-12 md:py-16 border-0"><div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><h2 class=" text-xl sm:text-2xl font-bold text-gray-900 text-center mb-2"> Placing and Shipping Your Order</h2><div class="mb-8"></div><div x-data="{ open: null }"><div class="border-b border-gray-200"> <button type="button"
1385class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1386@click="open = open === 0 ? null : 0"
1387:aria-expanded="open === 0"><h3 class="text-lg font-medium text-gray-900">Do you offer same day shipping?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1388:class="{ 'rotate-180': open === 0 }"
1389fill="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 x-show="open === 0"
1390x-transition:enter="transition ease-out duration-200"
1391x-transition:enter-start="opacity-0 -translate-y-1"
1392x-transition:enter-end="opacity-100 translate-y-0"
1393x-transition:leave="transition ease-in duration-150"
1394x-transition:leave-start="opacity-100 translate-y-0"
1395x-transition:leave-end="opacity-0 -translate-y-1"
1396x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">We do not offer same day shipping. Delivery date will depend on method of shipping chosen at checkout.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1397class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1398@click="open = open === 1 ? null : 1"
1399:aria-expanded="open === 1"><h3 class="text-lg font-medium text-gray-900">What is expedited processing?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1400:class="{ 'rotate-180': open === 1 }"
1401fill="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 x-show="open === 1"
1402x-transition:enter="transition ease-out duration-200"
1403x-transition:enter-start="opacity-0 -translate-y-1"
1404x-transition:enter-end="opacity-100 translate-y-0"
1405x-transition:leave="transition ease-in duration-150"
1406x-transition:leave-start="opacity-100 translate-y-0"
1407x-transition:leave-end="opacity-0 -translate-y-1"
1408x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Expedited processing moves your order to the front of our production line! Your order will be processed and shipped before other orders in queue. Pair with an expedited shipping option to receive your order as fast as possible.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1409class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1410@click="open = open === 2 ? null : 2"
1411:aria-expanded="open === 2"><h3 class="text-lg font-medium text-gray-900">How do I check the status of my order?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1412:class="{ 'rotate-180': open === 2 }"
1413fill="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 x-show="open === 2"
1414x-transition:enter="transition ease-out duration-200"
1415x-transition:enter-start="opacity-0 -translate-y-1"
1416x-transition:enter-end="opacity-100 translate-y-0"
1417x-transition:leave="transition ease-in duration-150"
1418x-transition:leave-start="opacity-100 translate-y-0"
1419x-transition:leave-end="opacity-0 -translate-y-1"
1420x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Once your package is shipped, you will receive a shipping confirmation email that contains your tracking number.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1421class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1422@click="open = open === 3 ? null : 3"
1423:aria-expanded="open === 3"><h3 class="text-lg font-medium text-gray-900">Do you provide proofs?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1424:class="{ 'rotate-180': open === 3 }"
1425fill="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 x-show="open === 3"
1426x-transition:enter="transition ease-out duration-200"
1427x-transition:enter-start="opacity-0 -translate-y-1"
1428x-transition:enter-end="opacity-100 translate-y-0"
1429x-transition:leave="transition ease-in duration-150"
1430x-transition:leave-start="opacity-100 translate-y-0"
1431x-transition:leave-end="opacity-0 -translate-y-1"
1432x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Proof requests should be made in the comments box above the order button on the final checkout page. We will not begin your order until we receive your approval. This may delay production time.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1433class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1434@click="open = open === 4 ? null : 4"
1435:aria-expanded="open === 4"><h3 class="text-lg font-medium text-gray-900">I made a mistake on my order, can this be corrected?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1436:class="{ 'rotate-180': open === 4 }"
1437fill="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 x-show="open === 4"
1438x-transition:enter="transition ease-out duration-200"
1439x-transition:enter-start="opacity-0 -translate-y-1"
1440x-transition:enter-end="opacity-100 translate-y-0"
1441x-transition:leave="transition ease-in duration-150"
1442x-transition:leave-start="opacity-100 translate-y-0"
1443x-transition:leave-end="opacity-0 -translate-y-1"
1444x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">You may cancel at any time </span><strong>
1444prior</strong><span style="font-size: 14px"> to the order going into production. Once items have been customized, we can no longer accept a cancellation or modification. If you need to change your order for any reason, please contact </span><a target="_blank" rel="noopener noreferrer" href="https://www.simplystamps.com/contacts">here</a><span style="font-size: 14px"> and we will be happy to assist you.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1445class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1446@click="open = open === 5 ? null : 5"
1447:aria-expanded="open === 5"><h3 class="text-lg font-medium text-gray-900">How do I order different products with the same design or personalization?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1448:class="{ 'rotate-180': open === 5 }"
1449fill="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 x-show="open === 5"
1450x-transition:enter="transition ease-out duration-200"
1451x-transition:enter-start="opacity-0 -translate-y-1"
1452x-transition:enter-end="opacity-100 translate-y-0"
1453x-transition:leave="transition ease-in duration-150"
1454x-transition:leave-start="opacity-100 translate-y-0"
1455x-transition:leave-end="opacity-0 -translate-y-1"
1456x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Simply create your first item and add it to your cart. From the cart, select ‘Order another with this design’ that will take you back to your original product. Navigate to the new product you would like to order and select ‘yes’ when asked if you would like to use your last design. Add the new item to your cart!</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1457class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1458@click="open = open === 6 ? null : 6"
1459:aria-expanded="open === 6"><h3 class="text-lg font-medium text-gray-900">How do I provide my signature for a signature stamp?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1460:class="{ 'rotate-180': open === 6 }"
1461fill="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 x-show="open === 6"
1462x-transition:enter="transition ease-out duration-200"
1463x-transition:enter-start="opacity-0 -translate-y-1"
1464x-transition:enter-end="opacity-100 translate-y-0"
1465x-transition:leave="transition ease-in duration-150"
1466x-transition:leave-start="opacity-100 translate-y-0"
1467x-transition:leave-end="opacity-0 -translate-y-1"
1468x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Simply create your first item and add it to your cart. From the cart, select ‘Order another with this design’ that will take you back to your original product. Navigate to the new product you would like to order and select ‘yes’ when asked if you would like to use your last design. Add the new item to your cart!</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1469class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1470@click="open = open === 7 ? null : 7"
1471:aria-expanded="open === 7"><h3 class="text-lg font-medium text-gray-900">Can I place bulk personalized orders rather than entering items one by one?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1472:class="{ 'rotate-180': open === 7 }"
1473fill="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 x-show="open === 7"
1474x-transition:enter="transition ease-out duration-200"
1475x-transition:enter-start="opacity-0 -translate-y-1"
1476x-transition:enter-end="opacity-100 translate-y-0"
1477x-transition:leave="transition ease-in duration-150"
1478x-transition:leave-start="opacity-100 translate-y-0"
1479x-transition:leave-end="opacity-0 -translate-y-1"
1480x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Yes! Simply choose your product specifications and then easily upload your names list from the ordering screen.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1481class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1482@click="open = open === 8 ? null : 8"
1483:aria-expanded="open === 8"><h3 class="text-lg font-medium text-gray-900">Can I cancel an existing order?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1484:class="{ 'rotate-180': open === 8 }"
1485fill="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 x-show="open === 8"
1486x-transition:enter="transition ease-out duration-200"
1487x-transition:enter-start="opacity-0 -translate-y-1"
1488x-transition:enter-end="opacity-100 translate-y-0"
1489x-transition:leave="transition ease-in duration-150"
1490x-transition:leave-start="opacity-100 translate-y-0"
1491x-transition:leave-end="opacity-0 -translate-y-1"
1492x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">You may cancel at any time </span><strong>
1492prior</strong><span style="font-size: 14px"> to the order going into production. Once items have been customized, we can no longer accept a cancellation or modification. If you need to change your order for any reason, please contact </span><a target="_blank" rel="noopener noreferrer" href="https://www.simplystamps.com/contacts">here</a><span style="font-size: 14px"> and we will be happy to assist you.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1493class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1494@click="open = open === 9 ? null : 9"
1495:aria-expanded="open === 9"><h3 class="text-lg font-medium text-gray-900">Tax Exempt Orders</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1496:class="{ 'rotate-180': open === 9 }"
1497fill="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 x-show="open === 9"
1498x-transition:enter="transition ease-out duration-200"
1499x-transition:enter-start="opacity-0 -translate-y-1"
1500x-transition:enter-end="opacity-100 translate-y-0"
1501x-transition:leave="transition ease-in duration-150"
1502x-transition:leave-start="opacity-100 translate-y-0"
1503x-transition:leave-end="opacity-0 -translate-y-1"
1504x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">For orders that are placed by tax exempt organizations, please contact our </span><a target="_blank" rel="noopener noreferrer" href="https://www.simplystamps.com/contacts">Customer Focus Team</a><span style="font-size: 14px"> to assign tax exempt status to your account.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1505class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1506@click="open = open === 10 ? null : 10"
1507:aria-expanded="open === 10"><h3 class="text-lg font-medium text-gray-900">Can I place an order to be billed separately, rather than by paying online?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1508:class="{ 'rotate-180': open === 10 }"
1509fill="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 x-show="open === 10"
1510x-transition:enter="transition ease-out duration-200"
1511x-transition:enter-start="opacity-0 -translate-y-1"
1512x-transition:enter-end="opacity-100 translate-y-0"
1513x-transition:leave="transition ease-in duration-150"
1514x-transition:leave-start="opacity-100 translate-y-0"
1515x-transition:leave-end="opacity-0 -translate-y-1"
1516x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Please contact our accounting department for a billing agreement application. Once approved, your online account will include a separate billing option.</span></p></div></div></div></div></div> </section></div><div class="zephyr-builder-block"
1517data-placement-id="2973"
1518data-layout-id="39"
1519data-library-block="content_faq"> 
1519<script type="application/ld+json"> {
1520"@context": "https://schema.org",
1521"@type": "FAQPage",
1522"mainEntity": [
1523{
1524"@type": "Question",
1525"name": "Do you offer coupon codes?",
1526"acceptedAnswer": {
1527"@type": "Answer",
1528"text": "Yes! Current promotions are at the top of every page on our site. Receive an exclusive promo code by joining our email list. "
1529}
1530},
1531{
1532"@type": "Question",
1533"name": "Why isn't my coupon code working?",
1534"acceptedAnswer": {
1535"@type": "Answer",
1536"text": "Be sure the code is entered correctly with no spaces before or between the letters or numbers. Verify that all promotional conditions have been met, such as ordering minimums or other requirements. If the code still does not work, briefly enter the details in the comments box above the order button on the final checkout page. Our order entry team will issue a refund the discounted amount. Please note: One coupon code per order."
1537}
1538},
1539{
1540"@type": "Question",
1541"name": "I forgot to enter my coupon code, can I update my order?",
1542"acceptedAnswer": {
1543"@type": "Answer",
1544"text": "Yes! Please contact us and we will be happy to add a valid coupon code to your order and refund the discounted amount."
1545}
1546},
1547{
1548"@type": "Question",
1549"name": "Do you offer quantity discounts?",
1550"acceptedAnswer": {
1551"@type": "Answer",
1552"text": "Yes! We offer the following discounts on same item purchases of 25 or more:Qty 25-49 10% off MSRP | Qty 50-99 15% off MSRP | Qty 100-249 20% off MSRP | Qty 250-499 25% off MSRP"
1553}
1554}
1555]
1556}</script>
1556<section class="py-12 md:py-16 border-0"><div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><h2 class=" text-xl sm:text-2xl font-bold text-gray-900 text-center mb-2"> Coupon Codes and Quantity Discounts</h2><div class="mb-8"></div><div x-data="{ open: null }"><div class="border-b border-gray-200"> <button type="button"
1557class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1558@click="open = open === 0 ? null : 0"
1559:aria-expanded="open === 0"><h3 class="text-lg font-medium text-gray-900">Do you offer coupon codes?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1560:class="{ 'rotate-180': open === 0 }"
1561fill="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 x-show="open === 0"
1562x-transition:enter="transition ease-out duration-200"
1563x-transition:enter-start="opacity-0 -translate-y-1"
1564x-transition:enter-end="opacity-100 translate-y-0"
1565x-transition:leave="transition ease-in duration-150"
1566x-transition:leave-start="opacity-100 translate-y-0"
1567x-transition:leave-end="opacity-0 -translate-y-1"
1568x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 16px">Yes! Current promotions are at the top of every page on our site. Receive an exclusive promo code by joining our email list. </span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1569class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1570@click="open = open === 1 ? null : 1"
1571:aria-expanded="open === 1"><h3 class="text-lg font-medium text-gray-900">Why isn't my coupon code working?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1572:class="{ 'rotate-180': open === 1 }"
1573fill="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 x-show="open === 1"
1574x-transition:enter="transition ease-out duration-200"
1575x-transition:enter-start="opacity-0 -translate-y-1"
1576x-transition:enter-end="opacity-100 translate-y-0"
1577x-transition:leave="transition ease-in duration-150"
1578x-transition:leave-start="opacity-100 translate-y-0"
1579x-transition:leave-end="opacity-0 -translate-y-1"
1580x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 16px">Be sure the code is entered correctly with no spaces before or between the letters or numbers. Verify that all promotional conditions have been met, such as ordering minimums or other requirements. If the code still does not work, briefly enter the details in the comments box above the order button on the final checkout page. Our order entry team will issue a refund the discounted amount. Please note: One coupon code per order.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1581class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1582@click="open = open === 2 ? null : 2"
1583:aria-expanded="open === 2"><h3 class="text-lg font-medium text-gray-900">I forgot to enter my coupon code, can I update my order?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1584:class="{ 'rotate-180': open === 2 }"
1585fill="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 x-show="open === 2"
1586x-transition:enter="transition ease-out duration-200"
1587x-transition:enter-start="opacity-0 -translate-y-1"
1588x-transition:enter-end="opacity-100 translate-y-0"
1589x-transition:leave="transition ease-in duration-150"
1590x-transition:leave-start="opacity-100 translate-y-0"
1591x-transition:leave-end="opacity-0 -translate-y-1"
1592x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 16px">Yes! Please </span><a target="_blank" rel="noopener noreferrer" href="https://www.simplystamps.com/contacts"><span style="font-size: 16px">contact us</span></a><span style="font-size: 16px"> and we will be happy to add a valid coupon code to your order and refund the discounted amount.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1593class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1594@click="open = open === 3 ? null : 3"
1595:aria-expanded="open === 3"><h3 class="text-lg font-medium text-gray-900">Do you offer quantity discounts?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1596:class="{ 'rotate-180': open === 3 }"
1597fill="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 x-show="open === 3"
1598x-transition:enter="transition ease-out duration-200"
1599x-transition:enter-start="opacity-0 -translate-y-1"
1600x-transition:enter-end="opacity-100 translate-y-0"
1601x-transition:leave="transition ease-in duration-150"
1602x-transition:leave-start="opacity-100 translate-y-0"
1603x-transition:leave-end="opacity-0 -translate-y-1"
1604x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Yes! We offer the following discounts on same item purchases of 25 or more:</span></p><p style="text-align: left">Qty 25-49 10% off MSRP | Qty 50-99 15% off MSRP | Qty 100-249 20% off MSRP | Qty 250-499 25% off MSRP</p><hr></div></div></div></div></div> </section></div>
1604<div class="zephyr-builder-block"
1605data-placement-id="2974"
1606data-layout-id="39"
1607data-library-block="content_faq"> 
1607<script type="application/ld+json"> {
1608"@context": "https://schema.org",
1609"@type": "FAQPage",
1610"mainEntity": [
1611{
1612"@type": "Question",
1613"name": "Are your products made in the USA?",
1614"acceptedAnswer": {
1615"@type": "Answer",
1616"text": "All products are personalized at one of our US fulfillment centers. Some products or materials may originate from locations outside the US."
1617}
1618},
1619{
1620"@type": "Question",
1621"name": "What is the difference between self-inking and pre-inked stamps?",
1622"acceptedAnswer": {
1623"@type": "Answer",
1624"text": "Pre-inked stamps are filled with oil-based ink and leave 50,000+ crisp, clear impressions before needing additional ink. Self-inking stamps include a built-in, water-soluble ink pad that leaves 7,000+ clear impressions before needing additional ink. Pre-inked and self-inking stamps are not recommended for glossy and other non-porous surfaces."
1625}
1626},
1627{
1628"@type": "Question",
1629"name": "What is the difference between engraved and full color products?",
1630"acceptedAnswer": {
1631"@type": "Answer",
1632"text": "Engraved products are made by etching directly into the material, creating an engraved monochrome image. Full color products are UV printed directly onto the material surface, creating a smooth, vibrantly colored image."
1633}
1634},
1635{
1636"@type": "Question",
1637"name": "What stamp is best for glossy and other non-porous surfaces?",
1638"acceptedAnswer": {
1639"@type": "Answer",
1640"text": "A traditional wood handle hand stamp and specially formulated super marking ink are recommended for glossy and other non-porous surfaces."
1641}
1642},
1643{
1644"@type": "Question",
1645"name": "How do I re-ink my self-inking or pre-inked stamp?",
1646"acceptedAnswer": {
1647"@type": "Answer",
1648"text": "Watch our easy to do-it-yourself videos about how to change the ink pad on self-inking stamps and how to re-ink pre-inked stamps."
1649}
1650},
1651{
1652"@type": "Question",
1653"name": "What is the difference between a stamp and an embosser?",
1654"acceptedAnswer": {
1655"@type": "Answer",
1656"text": "A stamp leaves a flat, inked imprint on a variety of papers and other non-porous surfaces. An embosser creates a raised imprint on lightweight paper (20 lb paper recommended) that can be inked for photocopying and scanning with a seal impression inker. See our embossing blog for additional information."
1657}
1658},
1659{
1660"@type": "Question",
1661"name": "What fonts do you offer and what do they look like?",
1662"acceptedAnswer": {
1663"@type": "Answer",
1664"text": "Please refer to individual products for available font styles."
1665}
1666},
1667{
1668"@type": "Question",
1669"name": "PROP 65 WARNING",
1670"acceptedAnswer": {
1671"@type": "Answer",
1672"text": "Prop 65 warnings are required on all products that ship to California that were made with specific chemicals to inform residents about potential exposure to substances that cause cancer, birth defects, or other reproductive harm. For more information, please visit Proposition 65 Your Right to Know!"
1673}
1674},
1675{
1676"@type": "Question",
1677"name": "Can I order a multi-colored stamp?",
1678"acceptedAnswer": {
1679"@type": "Answer",
1680"text": "Dater stamps offer two-color combinations. All other stamps are available in a wide variety of single color options."
1681}
1682}
1683]
1684}</script>
1684<section class="py-12 md:py-16 border-0"><div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><h2 class=" text-xl sm:text-2xl font-bold text-gray-900 text-center mb-2"> About our Products</h2><div class="mb-8"></div><div x-data="{ open: null }"><div class="border-b border-gray-200"> <button type="button"
1685class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1686@click="open = open === 0 ? null : 0"
1687:aria-expanded="open === 0"><h3 class="text-lg font-medium text-gray-900">Are your products made in the USA?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1688:class="{ 'rotate-180': open === 0 }"
1689fill="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 x-show="open === 0"
1690x-transition:enter="transition ease-out duration-200"
1691x-transition:enter-start="opacity-0 -translate-y-1"
1692x-transition:enter-end="opacity-100 translate-y-0"
1693x-transition:leave="transition ease-in duration-150"
1694x-transition:leave-start="opacity-100 translate-y-0"
1695x-transition:leave-end="opacity-0 -translate-y-1"
1696x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">All products are personalized at one of our US fulfillment centers. Some products or materials may originate from locations outside the US.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1697class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1698@click="open = open === 1 ? null : 1"
1699:aria-expanded="open === 1"><h3 class="text-lg font-medium text-gray-900">What is the difference between self-inking and pre-inked stamps?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1700:class="{ 'rotate-180': open === 1 }"
1701fill="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 x-show="open === 1"
1702x-transition:enter="transition ease-out duration-200"
1703x-transition:enter-start="opacity-0 -translate-y-1"
1704x-transition:enter-end="opacity-100 translate-y-0"
1705x-transition:leave="transition ease-in duration-150"
1706x-transition:leave-start="opacity-100 translate-y-0"
1707x-transition:leave-end="opacity-0 -translate-y-1"
1708x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Pre-inked stamps are filled with oil-based ink and leave 50,000+ crisp, clear impressions before needing additional ink. Self-inking stamps include a built-in, water-soluble ink pad that leaves 7,000+ clear impressions before needing additional ink. Pre-inked and self-inking stamps are not recommended for glossy and other non-porous surfaces.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1709class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1710@click="open = open === 2 ? null : 2"
1711:aria-expanded="open === 2"><h3 class="text-lg font-medium text-gray-900">What is the difference between engraved and full color products?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1712:class="{ 'rotate-180': open === 2 }"
1713fill="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 x-show="open === 2"
1714x-transition:enter="transition ease-out duration-200"
1715x-transition:enter-start="opacity-0 -translate-y-1"
1716x-transition:enter-end="opacity-100 translate-y-0"
1717x-transition:leave="transition ease-in duration-150"
1718x-transition:leave-start="opacity-100 translate-y-0"
1719x-transition:leave-end="opacity-0 -translate-y-1"
1720x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Engraved products are made by etching directly into the material, creating an engraved monochrome image. Full color products are UV printed directly onto the material surface, creating a smooth, vibrantly colored image.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1721class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1722@click="open = open === 3 ? null : 3"
1723:aria-expanded="open === 3"><h3 class="text-lg font-medium text-gray-900">What stamp is best for glossy and other non-porous surfaces?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1724:class="{ 'rotate-180': open === 3 }"
1725fill="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 x-show="open === 3"
1726x-transition:enter="transition ease-out duration-200"
1727x-transition:enter-start="opacity-0 -translate-y-1"
1728x-transition:enter-end="opacity-100 translate-y-0"
1729x-transition:leave="transition ease-in duration-150"
1730x-transition:leave-start="opacity-100 translate-y-0"
1731x-transition:leave-end="opacity-0 -translate-y-1"
1732x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">A traditional wood handle hand stamp and specially formulated super marking ink are recommended for glossy and other non-porous surfaces.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1733class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1734@click="open = open === 4 ? null : 4"
1735:aria-expanded="open === 4"><h3 class="text-lg font-medium text-gray-900">
1735How do I re-ink my self-inking or pre-inked stamp?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1736:class="{ 'rotate-180': open === 4 }"
1737fill="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 x-show="open === 4"
1738x-transition:enter="transition ease-out duration-200"
1739x-transition:enter-start="opacity-0 -translate-y-1"
1740x-transition:enter-end="opacity-100 translate-y-0"
1741x-transition:leave="transition ease-in duration-150"
1742x-transition:leave-start="opacity-100 translate-y-0"
1743x-transition:leave-end="opacity-0 -translate-y-1"
1744x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Watch our easy to </span><a target="_blank" rel="noopener noreferrer" href="https://www.simplystamps.com/blog">do-it-yourself videos</a><span style="font-size: 14px"> about how to change the ink pad on self-inking stamps and how to re-ink pre-inked stamps.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1745class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1746@click="open = open === 5 ? null : 5"
1747:aria-expanded="open === 5"><h3 class="text-lg font-medium text-gray-900">What is the difference between a stamp and an embosser?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1748:class="{ 'rotate-180': open === 5 }"
1749fill="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 x-show="open === 5"
1750x-transition:enter="transition ease-out duration-200"
1751x-transition:enter-start="opacity-0 -translate-y-1"
1752x-transition:enter-end="opacity-100 translate-y-0"
1753x-transition:leave="transition ease-in duration-150"
1754x-transition:leave-start="opacity-100 translate-y-0"
1755x-transition:leave-end="opacity-0 -translate-y-1"
1756x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">A stamp leaves a flat, inked imprint on a variety of papers and other non-porous surfaces. An embosser creates a raised imprint on lightweight paper (20 lb paper recommended) that can be inked for photocopying and scanning with a seal impression inker. See our </span><a target="_blank" rel="noopener noreferrer" href="https://www.simplystamps.com/blog/ordering-an-embosser-faq">embossing blog</a><span style="font-size: 14px"> for additional information.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1757class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1758@click="open = open === 6 ? null : 6"
1759:aria-expanded="open === 6"><h3 class="text-lg font-medium text-gray-900">What fonts do you offer and what do they look like?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1760:class="{ 'rotate-180': open === 6 }"
1761fill="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 x-show="open === 6"
1762x-transition:enter="transition ease-out duration-200"
1763x-transition:enter-start="opacity-0 -translate-y-1"
1764x-transition:enter-end="opacity-100 translate-y-0"
1765x-transition:leave="transition ease-in duration-150"
1766x-transition:leave-start="opacity-100 translate-y-0"
1767x-transition:leave-end="opacity-0 -translate-y-1"
1768x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Please refer to individual products for available font styles.</span></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1769class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1770@click="open = open === 7 ? null : 7"
1771:aria-expanded="open === 7"><h3 class="text-lg font-medium text-gray-900">PROP 65 WARNING</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1772:class="{ 'rotate-180': open === 7 }"
1773fill="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 x-show="open === 7"
1774x-transition:enter="transition ease-out duration-200"
1775x-transition:enter-start="opacity-0 -translate-y-1"
1776x-transition:enter-end="opacity-100 translate-y-0"
1777x-transition:leave="transition ease-in duration-150"
1778x-transition:leave-start="opacity-100 translate-y-0"
1779x-transition:leave-end="opacity-0 -translate-y-1"
1780x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Prop 65 warnings are required on all products that ship to California that were made with specific chemicals to inform residents about potential exposure to substances that cause cancer, birth defects, or other reproductive harm. For more information, please visit </span><a target="_blank" rel="noopener noreferrer" href="https://www.p65warnings.ca.gov/">Proposition 65 Your Right to Know!</a></p></div></div></div><div class="border-b border-gray-200"> <button type="button"
1781class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1782@click="open = open === 8 ? null : 8"
1783:aria-expanded="open === 8"><h3 class="text-lg font-medium text-gray-900">Can I order a multi-colored stamp?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1784:class="{ 'rotate-180': open === 8 }"
1785fill="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 x-show="open === 8"
1786x-transition:enter="transition ease-out duration-200"
1787x-transition:enter-start="opacity-0 -translate-y-1"
1788x-transition:enter-end="opacity-100 translate-y-0"
1789x-transition:leave="transition ease-in duration-150"
1790x-transition:leave-start="opacity-100 translate-y-0"
1791x-transition:leave-end="opacity-0 -translate-y-1"
1792x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">Dater stamps offer two-color combinations. All other stamps are available in a wide variety of single color options.
1792</span></p></div></div></div></div></div> </section></div><div class="zephyr-builder-block"
1793data-placement-id="2975"
1794data-layout-id="39"
1795data-library-block="content_faq"> 
1795<script type="application/ld+json"> {
1796"@context": "https://schema.org",
1797"@type": "FAQPage",
1798"mainEntity": [
1799{
1800"@type": "Question",
1801"name": "What is your return policy?",
1802"acceptedAnswer": {
1803"@type": "Answer",
1804"text": "It is our company’s mission every day to process your order timely, and accurately as you have entered it. Returns are not typically accepted for customized orders. However, we want you to be happy with your product. Therefore, for customized orders where errors were made due to the reasons listed below, we will make the necessary corrections and reship your original order at no charge to you.The order was damaged or lost during shipping.Item includes a spelling error made by Simply Stamps.Item had incorrect customization features. &nbsp;Note: If the customer made a misspelling or chose the incorrect options, this would disqualify the product from being returned"
1805}
1806}
1807]
1808}</script>
1808<section class="py-12 md:py-16 border-0"><div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><h2 class=" text-xl sm:text-2xl font-bold text-gray-900 text-center mb-2"> Returns and Exchanges</h2><div class="mb-8"></div><div x-data="{ open: null }"><div class="border-b border-gray-200"> <button type="button"
1809class="w-full flex items-center justify-between py-4 text-left focus:outline-none"
1810@click="open = open === 0 ? null : 0"
1811:aria-expanded="open === 0"><h3 class="text-lg font-medium text-gray-900">What is your return policy?</h3> <svg class="w-5 h-5 text-gray-500 shrink-0 ml-4 transition-transform duration-200"
1812:class="{ 'rotate-180': open === 0 }"
1813fill="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 x-show="open === 0"
1814x-transition:enter="transition ease-out duration-200"
1815x-transition:enter-start="opacity-0 -translate-y-1"
1816x-transition:enter-end="opacity-100 translate-y-0"
1817x-transition:leave="transition ease-in duration-150"
1818x-transition:leave-start="opacity-100 translate-y-0"
1819x-transition:leave-end="opacity-0 -translate-y-1"
1820x-cloak><div class="pb-4 text-gray-600 leading-relaxed prose prose-sm max-w-none"><p><span style="font-size: 14px">It is our company’s mission every day to process your order timely, and accurately as you have entered it. Returns are not typically accepted for customized orders. However, we want you to be happy with your product. Therefore, for customized orders where errors were made due to the reasons listed below, we will make the necessary corrections and reship your original order at no charge to you.</span></p><ol><li><p><span>The order was damaged or lost during shipping.</span></p></li><li><p><span>Item includes a spelling error made by Simply Stamps.</span></p></li><li><p><span>Item had incorrect customization features.  </span></p></li></ol><p><em>Note: If the customer made a misspelling or chose the incorrect options, this would disqualify the product from being returned</em></p></div></div></div></div></div> </section></div></div></div></div></div><div
1821x-show="showEmailModal"
1822x-cloak
1823@keydown.escape.window="closeEmailModal()"
1824class="fixed inset-0 z-50 overflow-y-auto email-modal"
1825style="display: none;"><div x-show="showEmailModal"
1826x-transition:enter="transition ease-out duration-300"
1827x-transition:enter-start="opacity-0"
1828x-transition:enter-end="opacity-100"
1829x-transition:leave="transition ease-in duration-200"
1830x-transition:leave-start="opacity-100"
1831x-transition:leave-end="opacity-0"
1832@click="closeEmailModal()"
1833class="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm"></div><div class="flex min-h-full items-center justify-center p-4"><div x-show="showEmailModal"
1834x-transition:enter="transition ease-out duration-300"
1835x-transition:enter-start="opacity-0 transform scale-95"
1836x-transition:enter-end="opacity-100 transform scale-100"
1837x-transition:leave="transition ease-in duration-200"
1838x-transition:leave-start="opacity-100 transform scale-100"
1839x-transition:leave-end="opacity-0 transform scale-95"
1840@click.stop
1841class="relative bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto"> <button @click="closeEmailModal()"
1842class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10"> <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="submitSuccess" class="p-8 flex flex-col items-center justify-center min-h-[400px]"><div class="w-20 h-20 bg-brand-success/20 rounded-full flex items-center justify-center mb-6"> <svg class="w-12 h-12 text-brand-success" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path> </svg></div><h2 class="text-2xl font-bold text-gray-900 mb-2 text-center">Message Sent!</h2><p class="text-gray-600 text-center" x-text="successMessage"></p> <button @click="closeEmailModal()"
1843class="mt-6 bg-brand-primary hover:bg-brand-primary-hover text-white font-medium py-2 px-6 rounded-lg transition-colors"> Close </button></div><div x-show="!submitSuccess" class="p-6"><h2 class="text-2xl font-bold text-gray-900 mb-2">Email Us</h2><p class="text-sm text-gray-600 mb-6"> We aim to reply to all emails next business day, but <strong>if you need urgent assistance completing, modifying or cancelling an order, please phone or chat us ASAP during our open hours.</strong></p><div x-show="submitError" x-transition class="mb-4 p-4 bg-brand-danger/10 border border-brand-danger/30 rounded-lg"><p class="text-sm text-brand-danger" x-text="errorMessage"></p></div><form @submit.prevent="submitContact()" id="contact-form" enctype="multipart/form-data"> <input type="hidden" name="form_key" value="e3374479ab86b5888526972805eb3d8f"><div class="space-y-4"><div> <label for="contact-name" class="block text-sm font-medium text-gray-700 mb-1"> Name <span class="text-brand-danger">*</span> </label> <input type="text"
1844id="contact-name"
1845name="name"
1846x-model="form.name"
1847required
1848class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-email" class="block text-sm font-medium text-gray-700 mb-1"> Email <span class="text-brand-danger">*</span> </label> <input type="email"
1849id="contact-email"
1850name="email_address"
1851x-model="form.email"
1852required
1853class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-phone" class="block text-sm font-medium text-gray-700 mb-1"> Telephone </label> <input type="tel"
1854id="contact-phone"
1855name="telephone"
1856x-model="form.telephone"
1857class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-order" class="block text-sm font-medium text-gray-700 mb-1"> Order Number <span x-show="orderNumberHint" x-text="orderNumberHint" class="text-brand-warning text-xs ml-1"></span> </label> <input type="text"
1858id="contact-order"
1859name="order_number"
1860x-model="form.orderNumber"
1861class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-inquiry" class="block text-sm font-medium text-gray-700 mb-1"> Reason for Inquiry <span class="text-brand-danger">*</span> </label> <select id="contact-inquiry"
1862name="inquiry"
1863x-model="form.inquiry"
1864@change="handleInquiryChange()"
1865required
1866class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"><option value="">Choose reason for inquiry</option><option value="status">I would like to know the status of my order</option><option value="help">I would like help placing my order</option><option value="product">I have a question about a product</option><option value="modify/cancel">I would like to modify/cancel my order</option><option value="return">I would like a return or refund</option><option value="quote">I would like a quote</option><option value="tax_exempt">I would like to place a tax-exempt order</option><option value="custom_reason">My inquiry reason is not listed here</option> </select></div><div x-show="form.inquiry === 'modify/cancel'" x-transition> <label class="block text-sm font-medium text-gray-700 mb-1"> Reason Why <span class="text-brand-danger">*</span> </label> <select name="modify_cancel"
1867x-model="form.modifyCancel"
1868:required="form.inquiry === 'modify/cancel'"
1869class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"><option value="">Choose reason</option><option value="Purchased by mistake">Purchased by mistake</option><option value="Item(s) will not arrive on time">Item(s) will not arrive on time</option><option value="Need to update shipping address">Need to update shipping address</option><option value="Need to update shipping speed">Need to update shipping speed</option><option value="other">Other</option> </select></div><div x-show="form.inquiry === 'return'" x-transition> <label class="block text-sm font-medium text-gray-700 mb-1"> Reason Why <span class="text-brand-danger">*</span> </label> <select name="return"
1870x-model="form.returnReason"
1871:required="form.inquiry === 'return'"
1872class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"><option value="">Choose reason</option><option value="Purchased by mistake">Purchased by mistake</option><option value="Item(s) did not arrive on time">Item(s) did not arrive on time</option><option value="Wrong item sent">Wrong item sent</option><option value="Error on product">Error on product</option><option value="other">Other</option> </select></div><div x-show="form.inquiry === 'quote'" x-transition class="space-y-4 p-4 bg-gray-50 rounded-lg"><div> <label class="block text-sm font-medium text-gray-700 mb-1">Item Name</label> <input type="text" name="item_name" x-model="form.itemName"
1873class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label class="block text-sm font-medium text-gray-700 mb-1">Item Number (if available)</label> <input type="text" name="item_number" x-model="form.itemNumber"
1874class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label class="block text-sm font-medium text-gray-700 mb-1">Desired Quantity</label> <input type="text" name="quote_quantity" x-model="form.quoteQuantity"
1875class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label class="block text-sm font-medium text-gray-700 mb-1">Desired "In Hands Date"</label> <input type="date" name="finished_date" x-model="form.finishedDate"
1876class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div></div><div x-show="form.inquiry === 'custom_reason'" x-transition> <label class="block text-sm font-medium text-gray-700 mb-1"> Please specify <span class="text-brand-danger">*</span> </label> <input type="text"
1877name="other_inquiry"
1878x-model="form.otherInquiry"
1879:required="form.inquiry === 'custom_reason'"
1880class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-comment" class="block text-sm font-medium text-gray-700 mb-1"> Comment <span x-show="commentHint" x-text="commentHint" class="text-brand-warning text-xs ml-1"></span> </label> <textarea id="contact-comment"
1881name="comment"
1882x-model="form.comment"
1883rows="4"
1884class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors resize-y"></textarea></div><div> <label class="block text-sm font-medium text-gray-700 mb-1">Upload Logo (optional)</label> <input type="file"
1885name="upload_logo"
1886accept=".jpg,.jpeg,.png,.gif,.pdf"
1887class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors text-sm file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-brand-primary/10 file:text-brand-primary file:font-medium hover:file:bg-brand-primary/20"><p class="text-xs text-gray-500 mt-1">JPG, PNG, GIF, or PDF. Max 5MB.</p></div><p class="text-xs text-gray-600"> By clicking submit, you agree to our <a href="/terms-and-conditions" target="_blank" class="text-brand-primary hover:underline">Terms and Conditions</a> & <a href="/privacy-policy" target="_blank" class="text-brand-primary hover:underline">Privacy Policy</a></p> <button type="submit"
1888:disabled="loading"
1889class="w-full h-[48px] flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover disabled:bg-brand-primary/70 text-white font-medium py-3 px-4 roun
1889ded-lg 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></div></form></div></div></div></div><div  x-show="showQuoteModal"
1890x-cloak
1891@keydown.escape.window="closeQuoteModal()"
1892class="fixed inset-0 z-50 overflow-y-auto quote-modal"
1893style="display: none;"><div x-show="showQuoteModal"
1894x-transition:enter="transition ease-out duration-300"
1895x-transition:enter-start="opacity-0"
1896x-transition:enter-end="opacity-100"
1897x-transition:leave="transition ease-in duration-200"
1898x-transition:leave-start="opacity-100"
1899x-transition:leave-end="opacity-0"
1900@click="closeQuoteModal()"
1901class="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm"></div><div class="flex min-h-full items-center justify-center p-4"><div x-show="showQuoteModal"
1902x-transition:enter="transition ease-out duration-300"
1903x-transition:enter-start="opacity-0 transform scale-95"
1904x-transition:enter-end="opacity-100 transform scale-100"
1905x-transition:leave="transition ease-in duration-200"
1906x-transition:leave-start="opacity-100 transform scale-100"
1907x-transition:leave-end="opacity-0 transform scale-95"
1908@click.stop
1909class="relative bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto"> <button @click="closeQuoteModal()"
1910class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10"> <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"
1911d="M6 18L18 6M6 6l12 12"></path> </svg> </button><div x-show="submitSuccess" class="p-8 flex flex-col items-center justify-center min-h-[400px]"><div class="w-20 h-20 bg-brand-success/20 rounded-full flex items-center justify-center mb-6"> <svg class="w-12 h-12 text-brand-success" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path> </svg></div><h2 class="text-2xl font-bold text-gray-900 mb-2 text-center">Message Sent!</h2><p class="text-gray-600 text-center" x-text="successMessage"></p> <button @click="closeQuoteModal()"
1912class="mt-6 bg-brand-primary hover:bg-brand-primary-hover text-white font-medium py-2 px-6 rounded-lg transition-colors"> Close </button></div><div x-show="!submitSuccess" class="p-6"><h2 class="text-2xl font-bold text-gray-900 mb-2">Get a Quote</h2><p class="text-sm text-gray-600 mb-6"> We specialize in fulfilling large orders for all your custom sign
1913business needs.
1914Dedicating our time to providing you the best service FAST! <strong>Please fill out the below form.
1915We look forward to speaking with you!</strong></p><form @submit.prevent="submitQuote()" id="quoteForm"> <input name="form_key" type="hidden"
1916value="e3374479ab86b5888526972805eb3d8f"/><div class="space-y-4"><div> <label for="contact-first-name" class="block text-sm font-medium text-gray-700 mb-1"> First Name <span class="text-brand-danger">*</span> </label> <input type="text"
1917id="contact-first-name"
1918name="first_name"
1919x-model="form.first_name"
1920required
1921class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-last-name" class="block text-sm font-medium text-gray-700 mb-1"> Last Name <span class="text-brand-danger">*</span> </label> <input type="text"
1922id="contact-last-name"
1923name="last_name"
1924x-model="form.last_name"
1925required
1926class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-company-name" class="block text-sm font-medium text-gray-700 mb-1"> Company Name </label> <input type="text"
1927id="contact-company-name"
1928name="company_name"
1929x-model="form.company_name"class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-item-name" class="block text-sm font-medium text-gray-700 mb-1"> Item Name <span class="text-brand-danger">*</span> </label> <input type="text"
1930id="contact-item-name"
1931name="item_name"
1932x-model="form.item_name"
1933required
1934class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-item_number" class="block text-sm font-medium text-gray-700 mb-1"> SKU Item Number (if available) </label> <input type="text"
1935id="contact-item_number"
1936name="item_number"
1937x-model="form.item_number"class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-quote_quantity" class="block text-sm font-medium text-gray-700 mb-1"> Desired Quantity* </label> <input type="text"
1938id="contact-quote_quantity"
1939name="quote_quantity"
1940x-model="form.quote_quantity"
1941required
1942class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-finished_date" class="block text-sm font-medium text-gray-700 mb-1"> Desired "In Hands Date" of finished job? </label> <input type="date"
1943id="contact-finished_date"
1944name="finished_date"
1945x-model="form.finished_date"class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-email_address" class="block text-sm font-medium text-gray-700 mb-1"> Email Address <span class="text-brand-danger">*</span> </label> <input type="email"
1946id="contact-email_address"
1947name="email_address"
1948x-model="form.email_address"class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-telephone" class="block text-sm font-medium text-gray-700 mb-1"> Telephone <span class="text-brand-danger">*</span> </label> <input type="tel"
1949id="contact-telephone"
1950name="telephone"
1951x-model="form.telephone"class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors"></div><div> <label for="contact-quote_comment" class="block text-sm font-medium text-gray-700 mb-1"> Comment <span class="text-brand-danger">*</span> </label> <textarea id="contact-comment"
1952name="comment"
1953x-model="form.comment"
1954rows="4"
1955class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-primary focus:border-brand-primary transition-colors resize-y"></textarea></div></div><p class="text-xs text-red-600">Required Fields*</p> <br><p class="text-xs text-gray-600"> By clicking submit, you agree to our <a href="/terms-and-conditions" target="_blank" class="text-brand-primary hover:underline">Terms
1956and Conditions</a> & <a href="/privacy-policy" target="_blank" class="text-brand-primary hover:underline">Privacy
1957Policy</a></p> <br><div x-show="submitQuoteError" x-transition class="mb-4 p-4 bg-brand-danger/10 border border-brand-danger/30 rounded-lg"><p class="text-sm text-brand-danger" x-text="errorQuoteMessage"></p></div><button type="submit" id="submit_quote"
1958:disabled="loading"
1959class="w-full h-[48px] flex items-center justify-center bg-brand-primary hover:bg-brand-primary-hover disabled:bg-brand-primary/70 text-white font-medium py-3 px-4 roun
1959ded-lg 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"
1960xmlns="http://www.w3.org/2000/svg" fill="none"
1961viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10"
1962stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor"
1963d="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></form></div></div></div></div></div>
1963<script> function contactPage() {
1964return {
1965showEmailModal: false,
1966showQuoteModal: false,
1967loading: false,
1968submitSuccess: false,
1969submitError: false,
1970submitQuoteError: false,
1971successMessage: '',
1972errorMessage: '',
1973errorQuoteMessage: '',
1974orderNumberHint: '',
1975commentHint: '',
1976form: {
1977name: ' ',
1978email: '',
1979email_address: '',
1980first_name: '',
1981last_name: '',
1982company_name: '',
1983quote_quantity: '',
1984item_name: '',
1985item_number: '',
1986telephone: '',
1987orderNumber: '',
1988inquiry: '',
1989modifyCancel: '',
1990returnReason: '',
1991otherInquiry: '',
1992itemName: '',
1993itemNumber: '',
1994quoteQuantity: '',
1995finishedDate: '',
1996comment: ''
1997},handleInquiryChange() {
1998// Reset conditional field requirements/hints
1999this.orderNumberHint = '';
2000this.commentHint = '';const inquiry = this.form.inquiry;if (inquiry === 'modify/cancel' || inquiry === 'status') {
2001this.orderNumberHint = '(If available, please enter your order number)';
2002}if (inquiry === 'product' || inquiry === 'help') {
2003this.commentHint = '(Please enter your question in the comments)';
2004}
2005},closeEmailModal() {
2006this.showEmailModal = false;
2007// Reset form state after modal closes
2008setTimeout(() => {
2009if (this.submitSuccess) {
2010this.resetForm();
2011}
2012}, 300);
2013},
2014closeQuoteModal() {
2015this.showQuoteModal = false;
2016// Reset form state after modal closes
2017setTimeout(() => {
2018if (this.submitSuccess) {
2019this.resetForm();
2020}
2021}, 300);
2022},resetForm() {
2023this.submitSuccess = false;
2024this.submitError = false;
2025this.submitQuoteError = false;
2026this.successMessage = '';
2027this.errorMessage = '';
2028this.errorQuoteMessage = '';
2029this.form.telephone = '';
2030this.form.orderNumber = '';
2031this.form.inquiry = '';
2032this.form.modifyCancel = '';
2033this.form.returnReason = '';
2034this.form.otherInquiry = '';
2035this.form.itemName = '';
2036this.form.itemNumber = '';
2037this.form.quoteQuantity = '';
2038this.form.finishedDate = '';
2039this.form.comment = '';
2040this.orderNumberHint = '';
2041this.commentHint = '';
2042},async submitContact() {
2043this.loading = true;
2044this.submitError = false;
2045this.errorMessage = '';try {
2046const form = document.getElementById('contact-form');
2047const formData = new FormData(form);const recaptchaToken = await getZephyrRecaptchaToken('contact');
2048if (recaptchaToken) {
2049formData.append('recaptcha_response', recaptchaToken);
2050}const response = await fetch('https://www.simplystamps.com/zephyr/contact/submit/', {
2051method: 'POST',
2052body: formData,
2053credentials: 'same-origin'
2054});const data = await response.json();if (data.success) {
2055// GA4 generate_lead event
2056if (typeof zGA4 === 'function') {
2057zGA4('generate_lead', {});
2058}
2059this.submitSuccess = true;
2060this.successMessage = data.message || 'Your message has been sent successfully.';
2061} else {
2062this.submitError = true;
2063this.errorMessage = data.error || 'An error occurred. Please try again.';
2064}
2065} catch (error) {
2066console.error('Contact form error:', error);
2067this.submitError = true;
2068this.errorMessage = 'An error occurred. Please try again.';
2069} finally {
2070this.loading = false;
2071}
2072},
2073async submitQuote() {
2074this.loading = true;
2075this.submitQuoteError = false;
2076this.errorQuoteMessage = '';try {
2077const form = document.getElementById('quoteForm');
2078const formData = new FormData(form);const recaptchaToken = await getZephyrRecaptchaToken('quote');
2079if (recaptchaToken) {
2080formData.append('recaptcha_response', recaptchaToken);
2081}const response = await fetch('https://www.simplystamps.com/freshdesk/index/new_zephyrQuote/', {
2082method: 'POST',
2083body: formData,
2084credentials: 'same-origin'
2085});const data = await response.json();
2086console.log(data);
2087if (data.success) {
2088// GA4 generate_lead event
2089if (typeof zGA4 === 'function') {
2090zGA4('generate_lead', {});
2091}
2092this.submitSuccess = true;
2093this.successMessage = data.message || 'Your quote has been sent successfully.';
2094} else {
2095this.submitQuoteError = true;
2096this.errorQuoteMessage = data.error || 'An error occurred. Please try again.';
2097}
2098} catch (error) {
2099console.error('Contact form error:', error);
2100this.submitQuoteError = true;
2101this.errorQuoteMessage = 'An error occurred. Please try again.';
2102} finally {
2103this.loading = false;
2104}
2105}
2106};
2107} </script>
2107<style> [x-cloak] { display: none !important; } </style></div></div> </main> <footer id="site-footer" class="w-full"><div class="relative"><div class="widget widget-static-block"><style> /* ── SimplyStamps Shipping ETA Timer ── */
2108/* v4.0 — Unified copy, color does the talking */
2109.ss-ship-eta {
2110display: none;
2111flex-direction: column;
2112gap: 5px;
2113padding: 12px 16px;
2114border-radius: 8px 8px 0px 0px;
2115background: #eef7ff;
2116width: 100%;
2117box-sizing: border-box;
2118margin-top: 0;
2119margin-bottom: 0px;
2120border: 1px solid #d1e9ff;
2121transition: background 0.35s ease, border-color 0.35s ease;
2122font-family: inherit;
2123}
2124.ss-ship-eta__topline {
2125font-size: 14px;
2126font-weight: 600;
2127color: #374151;
2128line-height: 1.4;
2129margin: 0;
2130}
2131.ss-ship-eta__hero {
2132font-size: 14px;
2133color: #374151;
2134line-height: 1.5;
2135}
2136.ss-ship-eta__hero-date {
2137font-weight: 700;
2138}
2139.ss-ship-eta__hero-tier {
2140font-size: 12px;
2141color: #9ca3af;
2142margin-left: 6px;
2143}
2144.ss-ship-eta__toggle {
2145background: none;
2146border: none;
2147padding: 0;
2148margin-top: 3px;
2149font-size: 12px;
2150font-weight: 600;
2151color: #1d4ed8;
2152cursor: pointer;
2153display: inline-flex;
2154align-items: center;
2155gap: 3px;
2156font-family: inherit;
2157}
2158.ss-ship-eta__toggle:hover { text-decoration: underline; }
2159.ss-ship-eta__toggle-arrow {
2160font-size: 10px;
2161transition: transform 0.2s ease;
2162display: inline-block;
2163}
2164.ss-ship-eta__toggle-arrow.ss-open {
2165transform: rotate(180deg);
2166}
2167.ss-ship-eta__more {
2168display: none;
2169flex-direction: column;
2170gap: 1px;
2171margin-top: 4px;
2172font-size: 14px;
2173color: #374151;
2174line-height: 1.5;
2175}
2176.ss-ship-eta__more.ss-visible {
2177display: flex;
2178}
2179.ss-ship-eta__more-row {
2180display: flex;
2181align-items: baseline;
2182gap: 6px;
2183}
2184.ss-ship-eta__more-label {
2185font-size: 12px;
2186font-weight: 500;
2187color: #9ca3af;
2188min-width: 52px;
2189}
2190.ss-ship-eta__more-date {
2191font-weight: 500;
2192}
2193.ss-eta-cd {
2194font-weight: 800;
2195color: #1d4ed8;
2196}
2197/* ── Warn state ── */
2198.ss-ship-eta.ss-warn {
2199background: #fffbeb;
2200border-color: #fde68a;
2201}
2202.ss-ship-eta.ss-warn .ss-ship-eta__topline,
2203.ss-ship-eta.ss-warn .ss-ship-eta__hero { color: #78350f; }
2204.ss-ship-eta.ss-warn .ss-ship-eta__more { color: #78350f; }
2205.ss-ship-eta.ss-warn .ss-eta-cd { color: #d97706; }
2206.ss-ship-eta.ss-warn .ss-ship-eta__toggle { color: #b45309; }
2207/* ── Urgent state ── */
2208.ss-ship-eta.ss-urgent {
2209background: #fff1f2;
2210border-color: #fecdd3;
2211}
2212.ss-ship-eta.ss-urgent .ss-ship-eta__topline,
2213.ss-ship-eta.ss-urgent .ss-ship-eta__hero { color: #7f1d1d; }
2214.ss-ship-eta.ss-urgent .ss-ship-eta__more { color: #7f1d1d; }
2215.ss-ship-eta.ss-urgent .ss-eta-cd {
2216color: #dc2626;
2217animation: ss-pulse 1.1s ease-in-out 5;
2218}
2219.ss-ship-eta.ss-urgent .ss-ship-eta__toggle { color: #dc2626; }
2220@keyframes ss-pulse {
22210%, 100% { opacity: 1; }
222250%       { opacity: 0.5; }
2223}
2224/* ── After-cutoff state ── */
2225.ss-ship-eta.ss-after {
2226background: #f9fafb;
2227border-color: #e5e7eb;
2228}
2229.ss-ship-eta.ss-after .ss-ship-eta__topline,
2230.ss-ship-eta.ss-after .ss-ship-eta__hero { color: #6b7280; }
2231.ss-ship-eta.ss-after .ss-ship-eta__more { color: #6b7280; }
2232.ss-ship-eta.ss-after .ss-ship-eta__toggle { color: #6b7280; } </style> 
2232<script> (() => {
2233var TIMEZONE         = "America/New_York";
2234var CUTOFF_HOUR_ET   = 17;
2235var CUTOFF_MINUTE_ET = 0;
2236var HOLIDAYS_MMDD = new Set([
2237"01/01","01/19","02/16","05/25","07/03","09/07",
2238"11/26","11/27","12/24","12/25","12/31"
2239]);
2240var OFFSETS = { rush: 2, express: 3, standard: 5, economy: 6 };
2241var STD_LABEL = "Preferred";
2242var SHIPPING_CONTAINER_SELECTOR = '.ships-and-made-in-usa-container';
2243var FALLBACK_PARENT_SELECTOR    = '.vpt-product';
2244var PFX = "ss";
2245function createTimerEl() {
2246var div = document.createElement("div");
2247div.className = PFX + "-ship-eta";
2248div.setAttribute("aria-label", "Delivery estimates");
2249div.innerHTML =
2250'<div class="' + PFX + '-ship-eta__topline"></div>' +
2251'<div class="' + PFX + '-ship-eta__hero"></div>' +
2252'<button class="' + PFX + '-ship-eta__toggle" type="button" aria-expanded="false">' +
2253'All shipping options <span class="' + PFX + '-ship-eta__toggle-arrow">\u25BC</span>' +
2254'</button>' +
2255'<div class="' + PFX + '-ship-eta__more">' +
2256'<div class="' + PFX + '-ship-eta__more-row" data-tier="express"></div>' +
2257'<div class="' + PFX + '-ship-eta__more-row" data-tier="standard"></div>' +
2258'<div class="' + PFX + '-ship-eta__more-row" data-tier="economy"></div>' +
2259'</div>';
2260var toggleEl = div.querySelector('.' + PFX + '-ship-eta__toggle');
2261var arrowEl = div.querySelector('.' + PFX + '-ship-eta__toggle-arrow');
2262var moreEl = div.querySelector('.' + PFX + '-ship-eta__more');
2263var expanded = false;
2264toggleEl.addEventListener("click", function() {
2265expanded = !expanded;
2266moreEl.classList.toggle(PFX + "-visible", expanded);
2267arrowEl.classList.toggle(PFX + "-open", expanded);
2268toggleEl.setAttribute("aria-expanded", expanded);
2269toggleEl.firstChild.textContent = expanded ? "Hide options " : "All shipping options ";
2270});
2271return div;
2272}
2273function getEasternParts(date) {
2274date = date || new Date();
2275var fmt = new Intl.DateTimeFormat("en-US", {
2276timeZone: TIMEZONE,
2277year: "numeric", month: "2-digit", day: "2-digit",
2278hour: "2-digit", minute: "2-digit", second: "2-digit",
2279hour12: false
2280});
2281var out = {};
2282var parts = fmt.formatToParts(date);
2283for (var i = 0; i < parts.length; i++) out[parts[i].type] = parts[i].value;
2284return out;
2285}
2286function getNowET() {
2287var p = getEasternParts();
2288return new Date(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);
2289}
2290function mmdd(d) {
2291return String(d.getMonth()+1).padStart(2,"0") + "/" + String(d.getDate()).padStart(2,"0");
2292}
2293function isWeekend(d)     { var w = d.getDay(); return w === 0 || w === 6; }
2294function isHoliday(d)     { return HOLIDAYS_MMDD.has(mmdd(d)); }
2295function isBusinessDay(d) { return !isWeekend(d) && !isHoliday(d); }
2296function startOfDay(d)    { var x = new Date(d); x.setHours(0,0,0,0); return x; }
2297function nextDay(d)       { var x = new Date(d); x.setDate(x.getDate()+1); return x; }
2298function nextBusinessDay(from) {
2299var x = nextDay(from);
2300while (!isBusinessDay(x)) x = nextDay(x);
2301return startOfDay(x);
2302}
2303function addBusinessDays(from, n) {
2304var x = startOfDay(from);
2305var added = 0;
2306while (added < n) { x = nextDay(x); if (isBusinessDay(x)) added++; }
2307return x;
2308}
2309function formatDate(d) {
2310return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "
2310numeric" });
2311}
2312function countdownStr(diffMs) {
2313var totalSecs = Math.max(0, Math.floor(diffMs / 1000));
2314var totalMins = Math.floor(totalSecs / 60);
2315var hrs  = Math.floor(totalSecs / 3600);
2316var mins = Math.floor((totalSecs % 3600) / 60);
2317var secs = totalSecs % 60;
2318if (totalMins < 5) {
2319return hrs + "h " + String(mins).padStart(2,"0") + "m " + String(secs).padStart(2,"0") + "s";
2320}
2321return hrs + " hr" + (hrs !== 1 ? "s" : "") + " " + String(mins).padStart(2,"0") + " min";
2322}
2323var instances = [];
2324function ensureInstances() {
2325var containers = document.querySelectorAll(SHIPPING_CONTAINER_SELECTOR);
2326var placed = false;
2327for (var i = 0; i < containers.length; i++) {
2328var container = containers[i];
2329var prev = container.previousElementSibling;
2330if (prev && prev.classList.contains(PFX + "-ship-eta")) {
2331placed = true;
2332continue;
2333}
2334var timer = createTimerEl();
2335container.insertAdjacentElement("beforebegin", timer);
2336instances.push(timer);
2337placed = true;
2338}
2339if (!placed) {
2340var fallback = document.querySelector(FALLBACK_PARENT_SELECTOR);
2341if (fallback) {
2342var first = fallback.firstElementChild;
2343if (!first || !first.classList.contains(PFX + "-ship-eta")) {
2344var timer = createTimerEl();
2345fallback.insertAdjacentElement("afterbegin", timer);
2346instances.push(timer);
2347placed = true;
2348}
2349}
2350}
2351return placed;
2352}
2353function renderAll() {
2354var nowET  = getNowET();
2355var cutoff = new Date(nowET);
2356cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0);
2357var diffMs       = cutoff - nowET;
2358var totalMins    = Math.floor(diffMs / 60000);
2359var isPastCutoff = nowET >= cutoff;
2360var isOffDay     = !isBusinessDay(nowET);
2361var urgencyClass = null;
2362var toplineHTML  = "";
2363var base;
2364if (isPastCutoff || isOffDay) {
2365urgencyClass = PFX + "-after";
2366base = nextBusinessDay(nowET);
2367toplineHTML = 'Get as early as:';
2368} else {
2369var cdStr = countdownStr(diffMs);
2370base = startOfDay(nowET);
2371toplineHTML = 'Order in <span class="' + PFX + '-eta-cd">' + cdStr + '</span> to get as early as:';
2372if (totalMins <= 120) {
2373urgencyClass = PFX + "-urgent";
2374} else if (totalMins <= 240) {
2375urgencyClass = PFX + "-warn";
2376}
2377}
2378var rushDate    = addBusinessDays(base, OFFSETS.rush);
2379var expressDate = addBusinessDays(base, OFFSETS.express);
2380var stdDate     = addBusinessDays(base, OFFSETS.standard);
2381var ecoDate     = addBusinessDays(base, OFFSETS.economy);
2382var heroHTML    = '<span class="' + PFX + '-ship-eta__hero-date">' + formatDate(rushDate) + '</span><span class="' + PFX + '-ship-eta__hero-tier">via Rush</span>';
2383var expressHTML = '<span class="' + PFX + '-ship-eta__more-label">Express</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(expressDate) + '</span>';
2384var stdHTML     = '<span class="' + PFX + '-ship-eta__more-label">' + STD_LABEL + '</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(stdDate) + '</span>';
2385var ecoHTML     = '<span class="' + PFX + '-ship-eta__more-label">Economy</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(ecoDate) + '</span>';
2386for (var i = 0; i < instances.length; i++) {
2387var el = instances[i];
2388el.classList.remove(PFX + "-warn", PFX + "-urgent", PFX + "-after");
2389if (urgencyClass) el.classList.add(urgencyClass);
2390el.querySelector('.' + PFX + '-ship-eta__topline').innerHTML = toplineHTML;
2391el.querySelector('.' + PFX + '-ship-eta__hero').innerHTML = heroHTML;
2392el.querySelector('[data-tier="express"]').innerHTML = expressHTML;
2393el.querySelector('[data-tier="standard"]').innerHTML = stdHTML;
2394el.querySelector('[data-tier="economy"]').innerHTML = ecoHTML;
2395el.style.display = "flex";
2396}
2397}
2398function tick() {
2399var placed = ensureInstances();
2400if (placed && instances.length > 0) renderAll();
2401var nowET  = getNowET();
2402var cutoff = new Date(nowET);
2403cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0);
2404var minsLeft = Math.floor((cutoff - nowET) / 60000);
2405var interval = !placed ? 250
2406: (minsLeft >= 0 && minsLeft < 5) ? 1000
2407: 60000;
2408setTimeout(tick, interval);
2409}
2410tick();
2411if (window.MutationObserver) {
2412var mo = new MutationObserver(function() { ensureInstances(); });
2413mo.observe(document.body, { childList: true, subtree: true });
2414}
2415})();
2416</script>
2416</div> <style> .full-size-container.footer-marketing {
2417background-color: #60b5e5;
2418width: 100vw;
2419margin-top: 30px;
2420} </style> <style> .CustomerServiceNotice img {
2421width: 100%;
2422margin-bottom: 2%;
2423}
2424.CustomerServiceNotice .modal-footer {
2425display: none;
2426} </style></div> <section id="zephyr-signup-band"
2427class="relative w-full py-10 sm:py-12 bg-brand-primary text-white"
2428x-data="zephyrSignupBand()"
2429aria-labelledby="zephyr-signup-headline"><div class="relative max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><div x-ref="stage"
2430class="relative flex flex-col justify-center"><div x-ref="panelOffers"
2431class="transition-opacity duration-200 ease-out motion-reduce:transition-none"
2432:class="view === 'offers' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'"
2433:aria-hidden="view !== 'offers'"><p id="zephyr-signup-headline" class="text-3xl sm:text-4xl font-bold text-center"> Save On Your Order Today &amp; 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"
2434@click="choose('email_sms')"
2435class="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">
2435 <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"
2436@click="choose('email')"
2437class="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> &amp; <a href="/privacy-policy" class="text-inherit underline hover:no-underline">Privacy Policy</a></p></div><div x-ref="panelForm"
2438x-cloak
2439class="transition-opacity duration-200 ease-out motion-reduce:transition-none"
2440:class="view === 'email' || view === 'email_sms'
2441? 'relative opacity-100 visible'
2442: 'absolute inset-x-0 top-0 opacity-0 invisible'"
2443: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"
2444x-transition
2445role="alert"
2446aria-live="polite"
2447class="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"
2448id="zephyr-signup-email"
2449x-model="email"
2450required
2451autocomplete="email"
2452placeholder="[email protected]"
2453class="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"
2454aria-hidden="true">US +1</span> <input type="tel"
2455id="zephyr-signup-phone"
2456x-model="phone"
2457autocomplete="tel-national"
2458inputmode="tel"
2459placeholder="(555) 555-5555"
2460aria-describedby="zephyr-signup-phone-hint"
2461class="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"
2462id="zephyr-signup-consent"
2463x-model="consent"
2464class="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> &amp; <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"
2465:disabled="loading"
2466class="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"
2467@click="back()"
2468class="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"
2469x-cloak
2470class="transition-opacity duration-200 ease-out motion-reduce:transition-none"
2471:class="view === 'done' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'"
2472: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>
2472<script> /*
2473* Hide the band synchronously, before paint, for anyone who has already signed
2474* up. This runs inline rather than through Alpine on purpose: the footer is
2475* served from the Fastly cache, so the cookie is the only signal available, and
2476* Alpine boots after first paint — gating on x-show alone would render the band
2477* and then yank it, shifting the footer.
2478*/
2479(function () {
2480if (document.cookie.indexOf('zephyr_signup=') !== -1) {
2481var band = document.getElementById('zephyr-signup-band');
2482if (band) {
2483band.parentNode.removeChild(band);
2484}
2485}
2486})();function zephyrSignupBand() {
2487return {
2488view: 'offers',
2489email: '',
2490phone: '',
2491consent: false,
2492loading: false,
2493errorMessage: '',
2494successMessage: '',
2495postUrl: 'https://www.simplystamps.com/zephyr/signup/post/',
2496hideDays: 365,/* Per-offer copy, so the two form states share one set of markup. */
2497copy: {
2498email: {
2499heading: 'Save 10% - Subscribe to Email Only',
2500subheading: 'Please enter your email below to receive your discount code!',
2501submit: 'Get My Discount Code'
2502},
2503email_sms: {
2504heading: 'Save 15% - Subscribe to Email + Text',
2505subheading: 'Please enter your email address and your phone number to receive the 20% off discount code.',
2506submit: 'Get my 15% Off Now!'
2507}
2508},/*
2509* While `measuring` is on, the form panel renders in its tallest
2510* configuration so it can be sized. See measureTallest().
2511*/
2512formCopy: function () {
2513return this.copy[this.view]
2514|| (this.measuring ? this.copy.email_sms : null);
2515},headingText: function () {
2516var copy = this.formCopy();
2517return copy ? copy.heading : '';
2518},subheadingText: function () {
2519var copy = this.formCopy();
2520return copy ? copy.subheading : '';
2521},submitText: function () {
2522var copy = this.formCopy();
2523return copy ? copy.submit : '';
2524},measuring: false,init: function () {
2525this.measureTallest();// Panel heights change with the breakpoint, so re-pin on resize.
2526var self = this;
2527var timer = null;
2528window.addEventListener('resize', function () {
2529clearTimeout(timer);
2530timer = setTimeout(function () { self.measureTallest(); }, 150);
2531});
2532},/*
2533* Pin the stage to the tallest panel so switching states moves nothing
2534* on the page at all.
2535*
2536* The form panel has to be measured in its email+SMS configuration —
2537* its phone and consent blocks are x-show'd off in the email-only view,
2538* and its heading is bound to the current view, so it otherwise reports
2539* a short height. `measuring` forces that configuration on, and because
2540* an inactive panel is `absolute` + `invisible` (never display:none) it
2541* is measurable without ever being seen.
2542*
2543* Skipped while a form is on screen: `measuring` would briefly reveal
2544* the phone and consent fields in the email-only view. The pin just
2545* stays as-is until the visitor returns to the offers.
2546*/
2547measureTallest: function () {
2548var stage = this.$refs.stage;
2549if (!stage || this.view === 'email' || this.view === 'email_sms') {
2550return;
2551}var self = this;
2552this.measuring = true;this.$nextTick(function () {
2553var tallest = 0;
2554['panelOffers', 'panelForm', 'panelDone'].forEach(function (ref) {
2555var panel = self.$refs[ref];
2556if (panel) {
2557tallest = Math.max(tallest, panel.offsetHeight);
2558}
2559});self.measuring = false;
2560if (tallest) {
2561stage.style.minHeight = tallest + 'px';
2562}
2563});
2564},choose: function (which) {
2565this.errorMessage = '';
2566this.view = which;// After the DOM updates, not before: the field sits in a panel that
2567// is still `invisible` at this point, and focus() is a silent no-op
2568// on a hidden element.
2569var self = this;
2570this.$nextTick(function () {
2571// Both offers open the same panel, so email is always first.
2572self.focusLater('zephyr-signup-email');
2573});
2574},back: function () {
2575this.errorMessage = '';
2576this.view = 'offers';
2577},/* Move focus to the first field without yanking the page around. */
2578focusLater: function (focusId) {
2579if (!focusId) {
2580return;
2581}
2582var el = document.getElementById(focusId);
2583if (!el) {
2584return;
2585}
2586try {
2587el.focus({ preventScroll: true });
2588} catch (e) {
2589el.focus();
2590}
2591},submit: function () {
2592var self = this;
2593if (this.loading) {
2594return;
2595}// Client-side checks mirror the server's; the server is still the
2596// authority, these just save a round trip.
2597if (!this.email || this.email.indexOf('@') === -1) {
2598this.errorMessage = 'Enter a valid email address.';
2599return;
2600}
2601if (this.view === 'email_sms') {
2602if (this.phone.replace(/[^0-9]/g, '').length < 10) {
2603this.errorMessage = 'Enter a valid 10-digit US mobile number.';
2604return;
2605}
2606if (!this.consent) {
2607this.errorMessage = 'Check the consent box to receive text messages.';
2608return;
2609}
2610}this.loading = true;
2611this.errorMessage = '';var body = new FormData();
2612body.append('email', this.email);
2613body.append('signup_type', this.view);
2614if (this.view === 'email_sms') {
2615body.append('phone', this.phone);
2616body.append('sms_consent', this.consent ? '1' : '0');
2617}var send = function (token) {
2618if (token) {
2619body.append('recaptcha_response', token);
2620}fetch(self.postUrl, {
2621method: 'POST',
2622body: body,
2623credentials: 'same-origin'
2624}).then(function (response) {
2625return response.json();
2626}).then(function (data) {
2627if (data && data.success) {
2628self.successMessage = data.message || '';
2629self.view = 'done';
2630self.setHideCookie(data.hide_days);
2631if (typeof zGA4 === 'function') {
2632zGA4('generate_lead', {});
2633}
2634} else {
2635self.errorMessage = (data && data.error)
2636? data.error
2637: 'We could not complete your signup. Please try again in a moment.';
2638}
2639}).catch(function () {
2640self.errorMessage = 'We could not complete your signup. Please try again in a moment.';
2641}).then(function () {
2642self.loading = false;
2643});
2644};if (typeof window.getZephyrRecaptchaToken === 'function') {
2645window.getZephyrRecaptchaToken('signup').then(send, function () { send(null); });
2646} else {
2647send(null);
2648}
2649},/* Keeps the band hidden on later visits. 0 days means always show it. */
2650setHideCookie: function (days) {
2651var lifetime = typeof days === 'number' ? days : this.hideDays;
2652if (!lifetime || lifetime < 1) {
2653return;
2654}
2655var expires = new Date();
2656expires.setTime(expires.getTime() + (lifetime * 24 * 60 * 60 * 1000));
2657document.cookie = 'zephyr_signup=1; expires=' + expires.toUTCString()
2658+ '; path=/; SameSite=Lax'
2659+ (location.protocol === 'https:' ? '; Secure' : '');
2660}
2661};
2662}
2663</script>
2663<div class="relative"><div class="widget widget-static-block"></div> <!-On PDP FAQ Accordion Compatibility Shim--><div class="widget widget-static-block"><style> /* ---------------------------------------------------------------------------
2664FAQ blocks — standalone styles for the legacy Bootstrap panel markup.
2665Pairs with faq-collapse.js. No Bootstrap, no dependencies.
2666Scoped to .faq_block / .faq_table so nothing else on the page inherits
2667these rules. Retune the custom properties below to match the theme.
2668--------------------------------------------------------------------------- */
2669.faq_block {
2670--faq-border: #dcdcdc;
2671--faq-radius: 6px;
2672--faq-gap: 10px;
2673--faq-surface: #fff;
2674--faq-heading: #f7f7f7;
2675--faq-heading-hover: #efefef;
2676--faq-focus: #2563eb;
2677--faq-mark: #ffe9a8;
2678--faq-muted: #616161;
2679line-height: 1.55;
2680}
2681/* --- neutralize the layout table ---------------------------------------- */
2682.faq_block .faq_table,
2683.faq_block .faq_table > tbody,
2684.faq_block .faq_table > tbody > tr,
2685.faq_block .faq_table > tbody > tr > td {
2686display: block;
2687width: 100%;
2688max-width: 100%;
2689margin: 0;
2690padding: 0;
2691border: 0;
2692background: none;
2693}
2694.faq_block .faq_table > tbody > tr > td + td {
2695margin-top: var(--faq-gap);
2696}
2697/* the intro row holds the section heading, not a panel */
2698.faq_block .faq_table h3 {
2699margin: 0 0 calc(var(--faq-gap) * 1.5);
2700font-size: 1.5em;
2701line-height: 1.25;
2702}
2703/* --- panel ---------------------------------------------------------------- */
2704.faq_block .panel {
2705margin: 0 0 var(--faq-gap);
2706background: var(--faq-surface);
2707border: 1px solid var(--faq-border);
2708border-radius: var(--faq-radius);
2709overflow: hidden;
2710}
2711.faq_block .panel:last-child {
2712margin-bottom: 0;
2713}
2714.faq_block .panel-heading {
2715padding: 0;
2716background: var(--faq-heading);
2717border-bottom: 1px solid transparent;
2718transition: background-color .15s ease;
2719}
2720.faq_block .panel-heading:hover {
2721background: var(--faq-heading-hover);
2722}
2723.faq_block .panel-title {
2724margin: 0;
2725font-size: 1em;
2726font-weight: 600;
2727line-height: inherit;
2728}
2729/* the trigger fills the whole heading so the entire bar is clickable */
2730.faq_block .panel-title a {
2731display: flex;
2732align-items: center;
2733justify-content: space-between;
2734gap: 1em;
2735padding: 14px 16px;
2736color: inherit;
2737text-decoration: none;
2738cursor: pointer;
2739}
2740.faq_block .panel-title a:hover,
2741.faq_block .panel-title a:focus {
2742color: inherit;
2743text-decoration: none;
2744}
2745.faq_block .panel-title a:focus-visible {
2746outline: 2px solid var(--faq-focus);
2747outline-offset: -2px;
2748}
2749/* chevron — driven by aria-expanded, which the shim keeps in sync */
2750.faq_block .panel-title a::after {
2751content: '';
2752flex: 0 0 auto;
2753width: .45em;
2754height: .45em;
2755margin-top: -.22em;
2756border-right: 2px solid currentColor;
2757border-bottom: 2px solid currentColor;
2758transform: rotate(45deg);
2759transition: transform .2s ease, margin-top .2s ease;
2760opacity: .65;
2761}
2762.faq_block .panel-title a[aria-expanded="true"]::after {
2763margin-top: .18em;
2764transform: rotate(-135deg);
2765}
2766/* --- answer body ---------------------------------------------------------- */
2767/* Fallback for a JS failure: the browser still reveals a panel when its id is
2768in the URL, so the answers stay reachable. */
2769.faq_block .panel-collapse:target {
2770display: block !important;
2771}
2772/* divider only appears once a panel is open (:has() — no-op on old browsers) */
2773.faq_block .panel:has(.panel-collapse.in) .panel-heading {
2774border-bottom-color: var(--faq-border);
2775}
2776/* Tailwind ships a `.collapse` utility (visibility: collapse) that collides with
2777the legacy Bootstrap class of the same name in this markup. Two class-level
2778selectors here so this wins on specificity regardless of stylesheet order. */
2779.faq_block .panel-collapse,
2780.faq_table .panel-collapse {
2781visibility: visible;
2782}
2783/* padding lives on .panel-body, never margins — the height animation measures
2784scrollHeight on .panel-collapse and collapsing margins would break it */
2785.faq_block .panel-body {
2786padding: 14px 16px 16px;
2787}
2788.faq_block .panel-body > :first-child {
2789margin-top: 0;
2790}
2791.faq_block .panel-body > :last-child {
2792margin-bottom: 0;
2793}
2794.faq_block .panel-body ol,
2795.faq_block .panel-body ul {
2796margin: .75em 0;
2797padding-left: 1.4em;
2798}
2799.faq_block .panel-body li {
2800margin-bottom: .35em;
2801}
2802.faq_block .panel-body em {
2803display: block;
2804margin-top: .9em;
2805color: var(--faq-muted);
2806}
2807/* legacy inline font sizes inside answers (span style="font-size: medium")
2808fight the 14px wrapper — normalize them back to the block's scale */
2809.faq_block .panel-body [style*="font-size"] {
2810font-size: inherit !important;
2811}
2812/* --- search --------------------------------------------------------------- */
2813.faq_block mark,
2814.faq_table mark {
2815padding: 0 1px;
2816background: var(--faq-mark);
2817color: inherit;
2818border-radius: 2px;
2819}
2820#search_faq,
2821[data-faq-search] {
2822width: 100%;
2823max-width: 420px;
2824margin-bottom: calc(var(--faq-gap) * 1.5);
2825padding: 10px 12px;
2826font: inherit;
2827color: inherit;
2828background: #fff;
2829border: 1px solid #dcdcdc;
2830border-radius: 6px;
2831}
2832#search_faq:focus,
2833[data-faq-search]:focus {
2834border-color: #2563eb;
2835outline: 2px solid rgba(37, 99, 235, .25);
2836outline-offset: 0;
2837}
2838/* --- small screens -------------------------------------------------------- */
2839@media (max-width: 480px) {
2840.faq_block .panel-title a {
2841padding: 12px 13px;
2842}
2843.faq_block .panel-body {
2844padding: 12px 13px 14px;
2845}
2846} </style> 
2846<script> /**
2847* FAQ collapse + search shim (no jQuery, no Bootstrap JS, no mark.js)
2848*
2849* Restores the behavior of the legacy CMS FAQ blocks:
2850*   - a[data-toggle="collapse"][href="#id"]  ->  toggles .panel-collapse
2851*   - #search_faq                            ->  filters panels, highlights matches
2852*   - ?search=term in the URL                ->  pre-fills and runs the filter
2853*
2854* Deliberately does NOT define window.jQuery / window.$ — a global jQuery stub
2855* makes third-party widgets take the jQuery code path and break.
2856*
2857* Public API: window.FaqShim.refresh() — re-scan after injecting markup (ESI,
2858* Alpine, AJAX). Not needed for clicks; those are delegated on document.
2859*/
2860(function (window, document) {
2861'use strict';
2862var TRIGGER_SEL = '[data-toggle="collapse"]';
2863var PANEL_SEL = '.panel';
2864var TITLE_SEL = '.panel-title a';
2865var SEARCH_SEL = '#search_faq, [data-faq-search]';
2866var SCOPE_SEL = '.faq_block, .faq_table, .panel-group';
2867var DURATION = 250;
2868var originalTitles = new WeakMap();
2869var ready = false;
2870/* ---------------------------------------------------------- styles ---- */
2871function injectStyles() {
2872if (document.getElementById('faq-shim-css')) return;
2873var style = document.createElement('style');
2874style.id = 'faq-shim-css';
2875style.textContent = [
2876/* structural fallback so panels are closed even if Bootstrap CSS is gone */
2877'.faq_block .panel-collapse:not(.in),',
2878'.faq_table .panel-collapse:not(.in),',
2879'[data-faq-collapse]:not(.in){display:none}',
2880'[data-faq-collapse].in{display:block}',
2881'[data-faq-collapse][data-faq-collapse]{overflow:hidden;visibility:visible}',
2882'[data-faq-collapse].faq-animating{transition:height ' + DURATION + 'ms ease}',
2883'@media (prefers-reduced-motion:reduce){',
2884'[data-faq-collapse].faq-animating{transition:none}}'
2885].join('');
2886(document.head || document.documentElement).appendChild(style);
2887}
2888/* ------------------------------------------------------- targeting ---- */
2889function targetOf(trigger) {
2890var sel = trigger.getAttribute('data-target') || trigger.getAttribute('href') || '';
2891if (!sel || sel === '#') return null;
2892// Duplicate ids across repeated CMS blocks are likely, so look inside the
2893// trigger's own FAQ scope first and only then fall back to the document.
2894var scope = trigger.closest(SCOPE_SEL);
2895var el = null;
2896try {
2897if (scope) el = scope.querySelector(sel);
2898if (!el) el = document.querySelector(sel);
2899} catch (e) {
2900return null;
2901}
2902return el;
2903}
2904function claim(el) {
2905if (!el || el.hasAttribute('data-faq-collapse')) return el;
2906el.setAttribute('data-faq-collapse', '');
2907// `collapse` is a Tailwind utility (visibility: collapse) as well as a
2908// legacy Bootstrap hook. Nothing needs it now — .in and the data attribute
2909// carry the state — so drop it rather than fight it on specificity.
2910el.classList.remove('collapse');
2911if (!el.id) el.id = 'faq-c-' + Math.random().toString(36).slice(2, 9);
2912return el;
2913}
2914function syncAria(target) {
2915var open = target.classList.contains('in');
2916var triggers = document.querySelectorAll(
2917TRIGGER_SEL + '[href="#' + target.id + '"], ' +
2918TRIGGER_SEL + '[data-target="#' + target.id + '"]'
2919);
2920for (var i = 0; i < triggers.length; i++) {
2921triggers[i].setAttribute('aria-expanded', open ? 'true' : 'false');
2922triggers[i].setAttribute('aria-controls', target.id);
2923if (triggers[i].tagName === 'A') triggers[i].setAttribute('role', 'button');
2924}
2925}
2926/* ------------------------------------------------------ open/close ---- */
2927function settle(el) {
2928if (el._faqTimer) { clearTimeout(el._faqTimer); el._faqTimer = null; }
2929if (el._faqEnd) { el.removeEventListener('transitionend', el._faqEnd); el._faqEnd = null; }
2930el.classList.remove('faq-animating');
2931el.style.height = '';
2932}
2933function finish(el, cb) {
2934var done = function (e) {
2935if (e && e.target !== el) return;
2936settle(el);
2937if (cb) cb();
2938};
2939el._faqEnd = done;
2940el.addEventListener('transitionend', done);
2941el._faqTimer = setTimeout(done, DURATION + 60);
2942}
2943function open(el, animate) {
2944claim(el);
2945settle(el);
2946el.classList.add('in');
2947if (animate === false) { syncAria(el); return; }
2948el.style.height = '0px';
2949el.classList.add('faq-animating');
2950void el.offsetHeight;                       // force reflow
2951el.style.height = el.scrollHeight + 'px';
2952finish(el);
2953syncAria(el);
2954}
2955function close(el, animate) {
2956claim(el);
2957settle(el);
2958if (animate === false) {
2959el.classList.remove('in');
2960syncAria(el);
2961return;
2962}
2963el.style.height = el.scrollHeight + 'px';
2964el.classList.add('faq-animating');
2965void el.offsetHeight;
2966el.style.height = '0px';
2967finish(el, function () { el.classList.remove('in'); syncAria(el); });
2968}
2969function toggle(el) {
2970if (el.classList.contains('in')) close(el); else open(el);
2971}
2972/* --------------------------------------------------------- marking ---- */
2973function unmark(link) {
2974var original = originalTitles.get(link);
2975if (original !== undefined && link.innerHTML !== original) link.innerHTML = original;
2976}
2977function mark(link, term) {
2978if (!originalTitles.has(link)) originalTitles.set(link, link.innerHTML);
2979unmark(link);
2980if (!term) return false;
2981var needle = term.toLowerCase();
2982var walker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT, null, false);
2983var nodes = [];
2984var node;
2985while ((node = walker.nextNode())) nodes.push(node);
2986var found = false;
2987for (var i = 0; i < nodes.length; i++) {
2988var text = nodes[i].nodeValue;
2989var hay = text.toLowerCase();
2990var at = hay.indexOf(needle);
2991if (at === -1) continue;
2992var frag = document.createDocumentFragment();
2993var pos = 0;
2994while (at !== -1) {
2995if (at > pos) frag.appendChild(document.createTextNode(text.slice(pos, at)));
2996var m = document.createElement('mark');
2997m.textContent = text.slice(at, at + needle.length);
2998frag.appendChild(m);
2999pos = at + needle.length;
3000at = hay.indexOf(needle, pos);
3001found = true;
3002}
3003if (pos < text.length) frag.appendChild(document.createTextNode(text.slice(pos)));
3004nodes[i].parentNode.replaceChild(frag, nodes[i]);
3005}
3006return found;
3007}
3008/* ---------------------------------------------------------- search ---- */
3009function panels() {
3010var scoped = document.querySelectorAll(
3011'.faq_block ' + PANEL_SEL + ', .faq_table ' + PANEL_SEL + ', .panel-group ' + PANEL_SEL
3012);
3013return scoped.length ? scoped : document.querySelectorAll(PANEL_SEL);
3014}
3015function search(term) {
3016term = (term || '').trim();
3017var list = panels();
3018for (var i = 0; i < list.length; i++) {
3019var panel = list[i];
3020var link = panel.querySelector(TITLE_SEL);
3021var body = panel.querySelector('.panel-collapse');
3022panel.style.display = '';
3023if (link) mark(link, '');
3024if (body) close(claim(body), false);
3025if (!term) continue;
3026var hit = link ? mark(link, term) : false;
3027if (hit) {
3028if (body) open(body, false);
3029} else {
3030panel.style.display = 'none';
3031}
3032}
3033}
3034function paramValue(name) {
3035var match = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.search);
3036if (!match) return '';
3037try {
3038return decodeURIComponent(match[1].replace(/\+/g, ' '));
3039} catch (e) {
3040return match[1];
3041}
3042}
3043/* ------------------------------------------------------------ init ---- */
3044function refresh() {
3045injectStyles();
3046var triggers = document.querySelectorAll(TRIGGER_SEL);
3047for (var i = 0; i < triggers.length; i++) {
3048var target = targetOf(triggers[i]);
3049if (!target) continue;
3050claim(target);
3051syncAria(target);
3052}
3053// deep link: /page?...#collapse84
3054if (window.location.hash.length > 1) {
3055var hashed = null;
3056try { hashed = document.querySelector(window.location.hash); } catch (e) { hashed = null; }
3057if (hashed && hashed.hasAttribute('data-faq-collapse')) open(hashed, false);
3058}
3059}
3060function init() {
3061if (ready) return;
3062ready = true;
3063refresh();
3064var seeded = paramValue('search');
3065var input = document.querySelector(SEARCH_SEL);
3066if (input && seeded) {
3067input.value = seeded;
3068search(seeded);
3069}
3070}
3071// Delegated — survives ESI fragments and anything injected after load.
3072document.addEventListener('click', function (e) {
3073var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null;
3074if (!trigger) return;
3075var target = targetOf(trigger);
3076if (!target) return;
3077e.preventDefault();
3078toggle(claim(target));
3079});
3080document.addEventListener('keydown', function (e) {
3081if (e.key !== 'Enter' && e.key !== ' ') return;
3082var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null;
3083if (!trigger || trigger.tagName === 'A') return;   // anchors fire click natively
3084var target = targetOf(trigger);
3085if (!target) return;
3086e.preventDefault();
3087toggle(claim(target));
3088});
3089document.addEventListener('input', function (e) {
3090if (!e.target.matches || !e.target.matches(SEARCH_SEL)) return;
3091search(e.target.value);
3092});
3093if (document.readyState === 'loading') {
3094document.addEventListener('DOMContentLoaded', init);
3095} else {
3096init();
3097}
3098window.addEventListener('load', refresh);
3099window.FaqShim = { refresh: refresh, open: open, close: close, search: search };
3100})(window, document); </script>
3100</div> <!-promo_free_logo_gift_badge--><div class="widget widget-static-block"><style> /* SimplyStamps | Free Logo Stamp Gift promo badge.
3101Renders in 4 places: homepage Logo Stamps tile, the /rubber-logo-stamps PLP + its 10 sibling logo-stamp sub-category PLPs
3102(banner right of the intro heading, wraps under on mobile), and the title of every PDP carrying the promo option (select_7643). Nowhere else.
3103To end the promo: disable this static block. */
3104.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}
3105.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child{position:relative;width:fit-content;margin-left:auto;margin-right:auto}
3106.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}
3107@media (max-width:639px){.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child::after{top:-8%;right:-8%;width:42%;height:42%}}
3108.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))}
3109.ss-fg-badge:focus-visible{outline:2px solid #55B5E3;outline-offset:3px;border-radius:50%}
3110.ss-fg-h2row{display:flex;flex-wrap:wrap;align-items:center;gap:10px 20px;margin:0 0 12px}
3111.ss-fg-h2row>h2,.ss-fg-h2row>h1{margin:0!important;flex:0 1 auto}
3112.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}
3113.ss-fg-banner strong{font-weight:700;text-transform:uppercase;letter-spacing:.04em}
3114.ss-fg-banner:focus-visible{outline:2px solid #55B5E3;outline-offset:2px}
3115.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}
3116.ss-fg-banner .ss-fg-tip{right:0}
3117.ss-fg-banner.is-wrapped{margin-left:0;width:100%;border-radius:12px}
3118.ss-fg-title-row{position:relative}
3119.ss-fg-title-row>h1{padding-right:68px}
3120.ss-fg-pdp{position:absolute;top:-10px;right:0;width:58px;height:58px}
3121.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}
3122.ss-fg-pdp .ss-fg-tip{right:0}
3123@media (hover:hover){.ss-fg-badge:hover .ss-fg-tip,.ss-fg-banner:hover .ss-fg-tip{opacity:1;visibility:visible}}
3124.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}
3125@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}}
3126@media (prefers-reduced-motion:reduce){.ss-fg-tip{transition:none}} </style> 
3126<script> (function(){
3127var 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.";
3128function make(extra,banner){
3129var b=document.createElement('span');
3130b.tabIndex=0;
3131if(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>';}
3132else{b.className='ss-fg-badge '+extra;b.setAttribute('role','img');b.setAttribute('aria-label','Free gift with any logo stamp. '+TIP);}
3133var t=document.createElement('span');
3134t.className='ss-fg-tip';
3135t.setAttribute('aria-hidden','true');
3136t.textContent=TIP;
3137b.appendChild(t);
3138b.addEventListener('click',function(e){e.stopPropagation();b.classList.toggle('is-open');});
3139b.addEventListener('keydown',function(e){if(e.key==='Escape'){b.classList.remove('is-open');b.blur();}});
3140document.addEventListener('click',function(){b.classList.remove('is-open');});
3141return b;
3142}
3143function run(){
3144var c=document.body.classList;
3145if(c.contains('catalog-category-view')&&c.contains('category-rubber-logo-stamps')){
3146var h2=document.querySelector('.ss-logo-top > h2');
3147if(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);
3148var fit=function(){bn.classList.remove('is-wrapped');if(bn.offsetTop>h2.offsetTop+h2.offsetHeight/2)bn.classList.add('is-wrapped');};
3149fit();window.addEventListener('resize',fit);if(window.ResizeObserver)new ResizeObserver(fit).observe(row);}
3150}
3151var 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'];
3152if(c.contains('catalog-category-view')&&SIB.some(function(k){return c.contains(k);})){
3153var h1c=document.querySelector('.flex-1 > h1');
3154if(h1c&&!document.querySelector('.ss-fg-banner')){
3155var rowc=document.createElement('div');rowc.className='ss-fg-h2row';h1c.parentNode.insertBefore(rowc,h1c);rowc.appendChild(h1c);
3156var bnc=make('',true);rowc.appendChild(bnc);
3157var fitc=function(){bnc.classList.remove('is-wrapped');if(bnc.offsetTop>h1c.offsetTop+h1c.offsetHeight/2)bnc.classList.add('is-wrapped');};
3158fitc();window.addEventListener('resize',fitc);if(window.ResizeObserver)new ResizeObserver(fitc).observe(rowc);
3159}
3160}
3161if(c.contains('catalog-product-view')&&document.getElementById('select_7643')){
3162var h1=document.querySelector('.product-header h1');
3163if(h1&&!document.querySelector('.ss-fg-pdp')){h1.parentNode.classList.add('ss-fg-title-row');h1.parentNode.appendChild(make('ss-fg-pdp'));}
3164}
3165}
3166if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',run);else run();
3167})(); </script>
3167</div><div class="widget widget-static-block"><div id="hcb-promo-root" hidden></div> 
3167<script> (function () {
3168/* ======================================================================
3169>>>>>>>>>>>>>>>>>>>>>>  EDIT ZONE START <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
3170One list of promo phases. Each runs FROM its start UNTIL its end (Eastern).
3171MESSAGE = banner text (HTML ok — <strong> etc.)   CODE = coupon
3172THRESHOLD = free-ship order minimum   REWARD = what they unlock
3173Dates are Eastern: -04:00 summer, -05:00 winter. Day-end = 23:59:59.
3174====================================================================== */
3175var PROMO = {
3176PHASES: [
3177{ 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" },
3178{ 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" },
3179{ 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" },
3180{ 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" }
3181],
3182COUNTDOWN_DAYS_BEFORE: 0,          // 0 = NO timer (banner shows message only)
3183LINK_URL: "",                     // no link — banner is not clickable
3184SHOW_CODE: false,                 // native free shipping = no coupon; hide the code chip
3185// Small ⓘ bubble after the banner message. Leave "" to hide it. (Was used to explain
3186// the old coupon deducting $9.99; native free shipping needs no explainer.)
3187INFO_TIP: "",
3188LOAD_GOOGLE_FONT: true,
3189// --- ZEPHYR header banner mount ---
3190HEADER_SELECTOR: "header",         // the fixed Zephyr <header> MOUNT_POSITION: "bottom",          // "bottom" = below the nav (default) · "top" = above the nav rows
3191HEADER_HEIGHT_VAR: "--header-height", // Zephyr offsets <main> by padding-top:var(--header-height)
3192// --- PDP progress bar mount + subtotal source ---
3193GOAL_ENABLED: true,
3194GOAL_MOUNTS: [
3195{ selector: ".price-pipe-container", position: "after" }   // product pages (under the price block)
3196],
3197SUBTOTAL_URL: "/zephyr/cart/getItems",   // cart subtotal endpoint; fetched ONCE per load (heavy)
3198POLL_MS: 1500,
3199COLORS: {
3200banner: { background: "#60B5E5", text: "#0A3550", chipBg: "#FFFFFF", chipText: "#0A3550" },
3201goal:   { text: "#0A3550", track: "#E6EEF3", fill: "#60B5E5", done: "#1A7F37" }
3202}
3203};
3204/* ======================================================================
3205>>>>>>>>>>>>>>>>>>>>>>>  EDIT ZONE END <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
3206Do not edit below this line.
3207====================================================================== */
3208if (window.__hcbPromoLoaded) return;
3209window.__hcbPromoLoaded = true;
3210var DAY = 86400000;
3211var 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 }; })
3212.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; })
3213.sort(function (a, b){ return a.from - b.from; });
3214if (!P.length){ console.warn("[HCB Promo] no valid phases — disabled."); return; }
3215var endTs = P.reduce(function (m, p){ return Math.max(m, p.ts); }, 0);
3216var dismissKey = "hcbPromoDismissed:" + endTs;
3217try { if (sessionStorage.getItem(dismissKey)) window.__hcbDismissed = true; } catch (e) {}
3218if (Date.now() >= endTs) return;
3219function esc(s){ return String(s).replace(/[&<>"']/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]; }); }
3220function phaseIndexAt(now){ for (var i=0;i<P.length;i++) if (now >= P[i].from && now <= P[i].ts) return i; return -1; }
3221function pad(n){ return (n < 10 ? '0' : '') + n; }
3222function money(n){ return "$" + (Math.round(n*100)/100).toFixed(2).replace(/\.00$/, ""); }
3223var currentCode = P[Math.max(0, phaseIndexAt(Date.now()))].CODE;
3224var bannerInstances = [], goals = [], cachedUrlVal = null, interval = null;
3225function readSubtotal(){ return cachedUrlVal; }   // populated by pollUrl()
3226function pollUrl(){
3227if (!PROMO.SUBTOTAL_URL) return;
3228fetch(PROMO.SUBTOTAL_URL, {credentials:"include", headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(d){
3229if (!d) return;
3230var v = (typeof d.subtotal_numeric !== "undefined") ? parseFloat(d.subtotal_numeric)
3231: (typeof d.subtotal !== "undefined") ? parseFloat(String(d.subtotal).replace(/[^0-9.]/g,"")) : NaN;
3232if (!isNaN(v)){ cachedUrlVal = v; tick(); }
3233}).catch(function(){});
3234}
3235if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
3236else start();
3237function start(){
3238if (PROMO.LOAD_GOOGLE_FONT && !document.getElementById('hcb-roboto-font')) {
3239var lk = document.createElement('link'); lk.id='hcb-roboto-font'; lk.rel='stylesheet';
3240lk.href='https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap';
3241document.head.appendChild(lk);
3242}
3243var Cb = PROMO.COLORS.banner, Cg = PROMO.COLORS.goal;
3244var css = ''
3245/* banner — position:relative because it lives INSIDE the fixed Zephyr <header> */
3246+ '.hcb-promo{position:relative;display:flex;align-items:center;justify-content:center;'
3247+   'gap:.6em;padding:9px 40px;font:600 14px/1.2 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;'
3248+   'white-space:nowrap;overflow:hidden;box-sizing:border-box;width:100%}'
3249+ '.hcb-promo *{box-sizing:border-box}'
3250+ '.hcb-promo__main{display:flex;align-items:center;justify-content:center;gap:.6em;min-width:0;color:inherit;text-decoration:none}'
3251+ '.hcb-promo__msg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;font-weight:400}'
3252+ '.hcb-promo__code{flex:none;cursor:pointer;border:0;border-radius:4px;padding:3px 9px;font:inherit;font-weight:700;letter-spacing:.03em}'
3253+ '.hcb-promo__timer{flex:none;font-variant-numeric:tabular-nums;font-weight:400}'
3254+ '.hcb-promo__timer b{font-weight:400}'
3255+ '.hcb-promo__timer--min{display:none}'
3256/* info ⓘ bubble — inline, height-safe; tooltip is portaled to<body> + fixed so nothing clips/covers it */
3257+ '.hcb-promo__info{position:relative;flex:none;display:inline-flex;align-items:center}'
3258+ '.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}'
3259+ '.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}'
3260+ '.hcb-promo__itip::after{content:"";position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:' + Cb.text + '}'
3261+ '.hcb-promo__itip.is-open{opacity:1;visibility:visible}'
3262+ '.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}'
3263+ '.hcb-promo__x:hover{opacity:1}'
3264+ '@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}}'
3265/* goal bar */
3266+ '.hcb-goal{font:600 14px/1.35 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;margin:10px 0;max-width:420px}'
3267+ '.hcb-goal__msg{color:' + Cg.text + ';margin:0 0 6px}.hcb-goal__msg b{font-weight:800}'
3268+ '.hcb-goal__track{height:10px;background:' + Cg.track + ';border-radius:999px;overflow:hidden}'
3269+ '.hcb-goal__fill{height:100%;width:0;background:' + Cg.fill + ';border-radius:999px;transition:width .4s ease}'
3270+ '.hcb-goal.is-done .hcb-goal__msg{color:' + Cg.done + '}.hcb-goal.is-done .hcb-goal__fill{background:' + Cg.done + '}';
3271var style=document.createElement('style'); style.textContent=css; document.head.appendChild(style);
3272/* ---------- build header banner ---------- */
3273var seed = P[Math.max(0, phaseIndexAt(Date.now()))];
3274var linkOpen  = PROMO.LINK_URL ? 'a href="' + esc(PROMO.LINK_URL) + '"' : 'span';
3275var linkClose = PROMO.LINK_URL ? 'a' : 'span';
3276var INFO_TIP  = PROMO.INFO_TIP || "";
3277function bannerMarkup(ph){
3278return '<div class="hcb-promo" role="region" aria-label="Promotion" style="background:' + Cb.background + ';color:' + Cb.text + '">'
3279+ '<' + linkOpen + ' class="hcb-promo__main">'
3280+   '<span class="hcb-promo__msg">' + ph.MESSAGE + '</span>'
3281+   '<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>'
3282+   '<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>'
3283+ '</' + linkClose + '>'
3284+ (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>' : '')
3285+ (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>' : '')
3286+ '<button type="button" class="hcb-promo__x" aria-label="Dismiss promotion">&times;</button>'
3287+ '</div>';
3288}
3289var header = document.querySelector(PROMO.HEADER_SELECTOR);
3290var mountHost = header || document.querySelector('main') || document.body;
3291var root = document.createElement('div'); root.className='hcb-promo-root'; root.innerHTML = bannerMarkup(seed);
3292if (PROMO.MOUNT_POSITION === 'top') mountHost.insertBefore(root, mountHost.firstChild);  // above the nav rows
3293else mountHost.appendChild(root);                                                        // below the nav (default)
3294syncHeaderHeight();   // set --header-height now (anti-jump); Zephyr's own observer also maintains it
3295var inst={ root:root, msg:root.querySelector('.hcb-promo__msg'), units:root.querySelectorAll('[data-u]'),
3296fulls:root.querySelectorAll('.hcb-promo__timer--full'), mins:root.querySelectorAll('.hcb-promo__timer--min'),
3297code:root.querySelector('.hcb-promo__code'), x:root.querySelector('.hcb-promo__x') };
3298if (inst.code) inst.code.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation();
3299var b=inst.code, done=function(){ b.textContent='Copied!'; setTimeout(function(){ b.textContent=currentCode; },1500); };
3300if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(currentCode).then(done,fb);}else{fb();}
3301function 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);} });
3302inst.x.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation();
3303try{sessionStorage.setItem(dismissKey,'1');}catch(err){}
3304window.__hcbDismissed=true; root.style.display='none'; syncHeaderHeight(); });
3305// info ⓘ tooltip — portal to<body>, position:fixed under the icon
3306var info=root.querySelector('.hcb-promo__info');
3307if(info){
3308var ib=info.querySelector('.hcb-promo__ibtn');
3309var tip=info.querySelector('.hcb-promo__itip');
3310document.body.appendChild(tip);
3311var place=function(){ var r=ib.getBoundingClientRect(); tip.style.left=(r.left+r.width/2)+'px'; tip.style.top=(r.bottom+9)+'px'; };
3312var openTip=function(){ place(); tip.classList.add('is-open'); };
3313var closeTip=function(){ tip.classList.remove('is-open'); };
3314ib.addEventListener('mouseenter', openTip);
3315ib.addEventListener('mouseleave', closeTip);
3316ib.addEventListener('focus', openTip);
3317ib.addEventListener('blur', closeTip);
3318ib.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); if(tip.classList.contains('is-open')) closeTip(); else openTip(); });
3319document.addEventListener('click', function(e){ if(e.target!==ib) closeTip(); });
3320window.addEventListener('scroll', function(){ if(tip.classList.contains('is-open')) place(); }, {passive:true});
3321window.addEventListener('resize', function(){ if(tip.classList.contains('is-open')) place(); });
3322}
3323bannerInstances.push(inst);
3324if (window.__hcbDismissed) root.style.display='none';
3325/* ---------- keep --header-height in sync (includes this banner) ---------- */
3326function syncHeaderHeight(){
3327if (!header) return;
3328var h = Math.round(header.getBoundingClientRect().height);
3329if (h) document.documentElement.style.setProperty(PROMO.HEADER_HEIGHT_VAR, h + 'px');
3330}
3331if (header){
3332if (window.ResizeObserver){ try { new ResizeObserver(syncHeaderHeight).observe(header); } catch(e){} }
3333window.addEventListener('resize', syncHeaderHeight);
3334setTimeout(syncHeaderHeight,300); setTimeout(syncHeaderHeight,1200);
3335}
3336/* ---------- build PDP goal bar (only where the price container exists) ---------- */
3337if (PROMO.GOAL_ENABLED){
3338(PROMO.GOAL_MOUNTS||[]).forEach(function(mnt){
3339var ghost=document.querySelector(mnt.selector); if(!ghost) return;
3340var groot=document.createElement('div'); groot.className='hcb-goal';
3341groot.innerHTML='<p class="hcb-goal__msg"></p><div class="hcb-goal__track"><div class="hcb-goal__fill"></div></div>';
3342var gp=mnt.position;
3343if(gp==='before') ghost.parentNode.insertBefore(groot, ghost);
3344else if(gp==='prepend') ghost.insertBefore(groot, ghost.firstChild);
3345else if(gp==='append') ghost.appendChild(groot);
3346else ghost.parentNode.insertBefore(groot, ghost.nextSibling);
3347goals.push({ root:groot, msg:groot.querySelector('.hcb-goal__msg'), fill:groot.querySelector('.hcb-goal__fill') });
3348});
3349}
3350if (goals.length) pollUrl();       // fetch cart subtotal ONCE (heavy endpoint) — only where a goal bar renders
3351interval=setInterval(tick, 1000);  // 1s clock only (no re-fetch)
3352tick();
3353}
3354/* ---------- single clock drives banner + goal ---------- */
3355var lastIdx = -2;
3356function 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; }
3357function 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'; }
3358function tick(){
3359var now = Date.now();
3360var idx = phaseIndexAt(now);
3361if (now >= endTs || idx === -1){
3362if (interval) clearInterval(interval);
3363bannerInstances.forEach(function(o){ o.root.style.display='none'; });
3364goals.forEach(function(g){ g.root.style.display='none'; });
3365return;
3366}
3367var ph = P[idx];
3368if (idx !== lastIdx){
3369lastIdx = idx; currentCode = ph.CODE;
3370bannerInstances.forEach(function(o){ o.msg.innerHTML=ph.MESSAGE; if(o.code){ o.code.textContent=currentCode; o.code.setAttribute('aria-label','Copy promo code '+currentCode); } });
3371}
3372var ms = ph.ts - now, showT = ms <= PROMO.COUNTDOWN_DAYS_BEFORE*DAY;
3373var d=Math.floor(ms/DAY), h=Math.floor((ms%DAY)/3600000), m=Math.floor((ms%3600000)/60000), s=Math.floor((ms%60000)/1000);
3374bannerInstances.forEach(function(o){
3375if (window.__hcbDismissed) return;
3376if (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); }
3377else showTimer(o,false);
3378});
3379if (goals.length){
3380var sub=readSubtotal(), pct, html, done=false;
3381var useCode=PROMO.SHOW_CODE;
3382if (sub===null){ pct=0; html='Spend '+money(ph.THRESHOLD)+'+ for <b>'+ph.REWARD+'</b>'+(useCode?' — use code <b>'+ph.CODE+'</b>':''); }
3383else 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.":""); }
3384else { 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+")":""); }
3385goals.forEach(function(gEl){ gEl.msg.innerHTML=html; gEl.fill.style.width=pct+'%'; gEl.root.classList.toggle('is-done',done); gEl.root.style.display=''; });
3386}
3387}
3388})(); </script>
3388</div><div class="widget widget-static-block"> <style> /* v1.31 -- Roboto promoted to first in the stack to match OpenMage brand.
3389All child elements inherit this via font-family: inherit or unspecified. */
3390.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; }
3391@keyframes ss-fade-in { from { opacity: 0; } to { opacity: 1; } }
3392.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; }
3393@keyframes ss-slide-up { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
3394.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; }
3395.ss-upsell-close:hover { color: #475569; background: #f1f5f9; }
3396.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; }
3397/* v1.31 -- title case + Roboto Bold. Letter-spacing removed (was for all-caps). */
3398.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; }
3399/* v1.31 -- subhead sits directly under the headline; smaller, softer color */
3400.ss-upsell-subhead { font-size: 14px; line-height: 1.5; margin: 0 0 16px; color: #475569; font-weight: 400; text-align: center; }
3401/* v1.31 -- 300px image slot, padding removed so GIF fills edge-to-edge.
3402overflow:hidden clips anything spilling outside the rounded corners. */
3403.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; }
3404.ss-upsell-image img { width: 100%; height: 100%; object-fit: cover; display: block; }
3405.ss-upsell-bullets { list-style: none; margin: 0 0 20px; padding: 0; }
3406.ss-upsell-bullets li { position: relative; padding-left: 24px; margin-bottom: 10px; color: #334155; line-height: 1.5; font-size: 14px; }
3407.ss-upsell-bullets li:before { content: "\2713"; position: absolute; left: 0; color: #16a34a; font-weight: 700; }
3408.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; }
3409.ss-upsell-price strong { color: #16a34a; font-size: 17px; }
3410.ss-upsell-actions { display: flex; flex-direction: column; gap: 4px; align-items: center; }
3411.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; }
3412.ss-upsell-accept:hover { background: #15803d; }
3413.ss-upsell-accept:focus { outline: 2px solid #15803d; outline-offset: 2px; }
3414/* v1.31 -- "No thanks" is now a small centered text link, not a ghost button.
3415Reduces visual competition with the primary Upgrade CTA. */
3416.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; }
3417.ss-upsell-decline:hover { color: #334155; text-decoration: underline; }
3418.ss-upsell-decline:focus { outline: 2px solid #64748b; outline-offset: 2px; }
3419/* v1.31 -- config-step Cancel now inherits the small-link style from
3420.ss-upsell-decline to match Step 1's philosophy. Ghost-button CSS
3421override deliberately removed. .ss-config-cancel class kept purely
3422as a JS querySelector hook. */
3423@media (max-width: 480px) { .ss-upsell-modal { padding: 24px 20px 20px; border-radius: 10px; } .ss-upsell-modal h2 { font-size: 18px; } }
3424/* Success state */
3425.ss-upsell-success { text-align: center; padding-top: 40px; }
3426.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); }
3427@keyframes ss-pop { 0% { transform: scale(0.3); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
3428.ss-upsell-success h2 { color: #15803d; margin-bottom: 8px; }
3429.ss-upsell-success-tagline { color: #334155; font-size: 15px; line-height: 1.5; margin: 0 0 18px; }
3430.ss-upsell-success-cta { color: #94a3b8; font-size: 13px; margin: 0; display: flex; align-items: center; justify-content: center; gap: 8px; }
3431.ss-upsell-spinner { width: 14px; height: 14px; border: 2px solid #cbd5e1; border-top-color: #16a34a; border-radius: 50%;
3431 animation: ss-spin 0.7s linear infinite; }
3432@keyframes ss-spin { to { transform: rotate(360deg); } }
3433.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; }
3434/* Config step (step 2) */
3435.ss-config-intro { color: #475569; font-size: 14px; line-height: 1.5; margin: 0 0 18px; }
3436.ss-config-fields { max-height: 50vh; overflow-y: auto; margin: 0 0 20px; padding-right: 4px; }
3437.ss-opt-field { margin-bottom: 14px; }
3438.ss-opt-field label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 6px; }
3439.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; }
3440.ss-opt-field select:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; }
3441.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; }
3442.ss-opt-field input[type="text"]:focus, .ss-opt-field textarea:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; }
3443.ss-opt-field textarea { resize: vertical; min-height: 60px; }
3444.ss-opt-group-label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 8px; }
3445.ss-opt-radio { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 14px; color: #334155; cursor: pointer; font-weight: 400; }
3446.ss-opt-radio input { margin: 0; }
3447.ss-config-required { color: #dc2626; margin-left: 3px; }
3448.ss-opt-field-error select,
3449.ss-opt-field-error input[type="text"],
3450.ss-opt-field-error textarea { border-color: #dc2626; box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12); }
3451.ss-opt-field-error > label,
3452.ss-opt-field-error > .ss-opt-group-label { color: #dc2626; }
3453.ss-opt-error { color: #dc2626; font-size: 12px; margin-top: 6px; font-weight: 500; }
3454/* Variant B / C -- value messaging enhancements */
3455.ss-upsell-badge.ss-upsell-badge-popular { background: #dcfce7; color: #15803d; }
3456.ss-upsell-badge.ss-upsell-badge-discount { background: #fef3c7; color: #92400e; }
3457.ss-upsell-cost-per-use { font-size: 12px; color: #64748b; margin-top: 4px; }
3458.ss-upsell-trust { font-size: 12px; color: #64748b; margin: 0 0 16px; text-align: center; line-height: 1.4; }
3459.ss-upsell-trust .ss-stars { color: #f59e0b; letter-spacing: 1px; }
3460.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; }
3461.ss-upsell-price-strike { text-decoration: line-through; color: #94a3b8; font-weight: 400; margin-right: 6px; } </style> 
3461<script> (function () {
3462'use strict';
3463var TIER_MAP = [
3464{ tier: 1, name: 'Wood Handle',  match: ['wood handle', 'wood'] },
3465{ tier: 2, name: 'Self-Inking',  match: ['self-inking', 'self inking', 'selfinking'] },
3466{ tier: 3, name: 'Pre-Inked',    match: ['pre-inked', 'pre inked', 'preinked'], subtype: 'pre_inked' },
3467{ tier: 3, name: 'Professional', match: ['professional'],                        subtype: 'professional' }
3468];
3469var STAMP_TYPE_LABEL_RE = /stamp\s*(type|style)/i;
3470var SESSION_ACCEPTED_KEY = 'ss_upsell_accepted_session';
3471var SESSION_DECLINE_KEY  = 'ss_upsell_decline_count';
3472var SUCCESS_DWELL_MS = 1500; // how long the success state stays before submit
3473// After this many declines in a session, the modal stops firing -- respects
3474// a clear "no thanks" signal so we don't pester. Cancels at the config
3475// step count as declines too. Set to 0 for unlimited (no decline cap).
3476var MAX_DECLINES_PER_SESSION = 3;
3477// ============================================================
3478// A/B VARIANT FRAMEWORK -- discount test
3479// ------------------------------------------------------------
3480// Visitors are randomly assigned to A or B (50/50) on first PDP
3481// visit and persisted in localStorage. Same visitor always sees
3482// the same variant. URL override: ?ss_variant=B for QA.
3483//
3484//   A = control: current production modal, no discount.
3485//   B = treatment: same modal + 10% off coupon (UPSELL10) auto-applied.
3486//
3487// Variant content lives in VARIANT_CONFIG -- edit copy/discount here
3488// without touching the rendering logic below.
3489// ============================================================
3490var VARIANT_STORAGE_KEY   = 'ss_upsell_variant';
3491var VARIANT_OVERRIDE_PARAM = 'ss_variant';
3492var VARIANTS = ['A', 'B'];
3493var VARIANT_CONFIG = {
3494// A = CONTROL -- current production modal exactly as-is, no overrides.
3495A: {
3496// Empty = use defaults. Same as live production behavior.
3497},
3498// B = DISCOUNT -- current modal + 10% discount on accept. The ONLY
3499// variable changing vs A is the discount; everything else (badge,
3500// copy, image, trust elements) stays identical so the lift can be
3501// attributed cleanly to the discount.
3502//
3503// Coupon codes are per-transition (keyed by upgrade target tier) so
3504// each transition can have its own Magento Cart Price Rule with its
3505// own conditions/scope. If a code leaks, only that specific upgrade
3506// path is exposed.
3507B: {
3508discountBanner: 'Save 10% on this upgrade -- today only',
3509couponCodes: {
3510'self-inking':  'UPSELL-SI10',   // Wood Handle -> Self-Inking
3511'pre_inked':    'UPSELL-PI10',   // Self-Inking -> Pre-Inked
3512'professional': 'UPSELL-PRO10'   // Self-Inking -> Professional
3513},
3514discountPct: 10
3515}
3516};
3517// If false, the config step skips text inputs and textareas (stamp text
3518// lines, etc). Their values still carry over to the cart -- they just
3519// aren't re-shown for editing in the modal.
3520var INCLUDE_TEXT_FIELDS_IN_CONFIG = false;
3521// DEV TOGGLE -- when true, the modal fires on EVERY eligible ATC, ignoring
3522// the no-badger session lock. Flip to false before production launch so
3523// the lock kicks in after a user accepts an upgrade.
3524var DEV_MODE_ALWAYS_SHOW = false;
3525// ============================================================
3526// CATEGORY TARGETING -- controlled rollout
3527// ------------------------------------------------------------
3528// Use this to limit which product categories show the upsell modal.
3529// Substring match (case-insensitive) against the breadcrumb path.
3530//
3531// TARGET_CATEGORIES -- if non-empty, modal ONLY fires when the
3532//   product's category breadcrumb contains one of these strings.
3533//   Empty array = fire on all categories (default).
3534//
3535// EXCLUDED_CATEGORIES -- modal NEVER fires when the breadcrumb
3536//   contains one of these strings, even if it also matches TARGET.
3537//   Exclusions always win over targets.
3538//
3539// Examples:
3540//   Target Address Stamps only:
3541//     TARGET_CATEGORIES = ['Address Stamps'];
3542//   Target everything EXCEPT custom/specialty stamps:
3543//     EXCLUDED_CATEGORIES = ['Custom', 'Specialty'];
3544//   Target Logo + Notary, exclude any "Bulk" sub-category:
3545//     TARGET_CATEGORIES = ['Logo Stamps', 'Notary Stamps'];
3546//     EXCLUDED_CATEGORIES = ['Bulk'];
3547//
3548// v1.32 ACTION NEEDED: these names came from the pre-Zephyr category
3549// landing page. Zephyr breadcrumbs read differently (a Duncan address
3550// stamp now shows "Address Stamps", not "Business Address Stamps"), so
3551// this whitelist must be re-mapped to the LIVE Zephyr breadcrumb strings
3552// or the modal won't fire on the categories you intend. Verify each on
3553// the live site and update below.
3554// ============================================================
3555var TARGET_CATEGORIES = [
3556'Office Stamps',
3557'Business Address Stamps',
3558'Signature Stamps',
3559'Date Stamps',
3560'Banking Stamps',
3561'Accounting & Tax',
3562'Postage Paid',
3563'Medical',
3564'Inspection',
3565'QR Code',
3566'Secure',
3567'Eco-Friendly',
3568'Checkbox',
3569'Custom Text'
3570];
3571var EXCLUDED_CATEGORIES = [
3572'Architects',
3573'Engineer',
3574'Land Surveyor',
3575'licensed Professional Stamps',
3576'Interior Design',
3577'LLC Stamps',
3578'Real Estate Appraisers'
3579]; // e.g. ['Custom', 'Specialty']
3580// ============================================================
3581// TIER IMAGES -- generic product image shown in the upsell modal
3582// ------------------------------------------------------------
3583// Drop in a URL per upgrade tier. Image appears above the value
3584// checkmarks in step 1 of the modal. Leave a key empty to skip
3585// the image section for that tier (graceful fallback).
3586//
3587// Recommended: ~600x300px, transparent or white background, the
3588// bare stamp photographed straight-on. Hosted anywhere reachable
3589// by the site (Magento media folder, CDN, etc).
3590// ============================================================
3591// v1.31 -- GIF assets, TEMPORARY LOCAL PATHS.
3592// Currently uploaded flat to wysiwyg/SimplyStamps/ with the "upsell-" prefix.
3593// TODO before prod push: move files to wysiwyg/SimplyStamps/upsell/ and rename
3594// to {tier}.gif (dropping the "upsell-" prefix), then update these paths to:
3595//   'self-inking':  'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/self-inking.gif',
3596//   'pre_inked':    'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/pre-inked.gif',
3597//   'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/professional.gif'
3598var TIER_IMAGES = {
3599'self-inking':  'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-self-inking.gif',
3600'pre_inked':    'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-pre-inked-v2.gif',
3601'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-professional-v2.gif'
3602};
3603// ============================================================
3604// CONFIG STEP -- tier-relevant option filtering
3605// ------------------------------------------------------------
3606// PRIMARY MECHANISM: auto-detect tier-tagged labels.
3607//   For each option label on the form, scan it for any tier name
3608//   ('Pre-Inked', 'Self-Inking', 'Wood Handle', 'Professional').
3609//     - Mentions UPGRADE TARGET tier name -> SHOW
3610//     - Mentions a DIFFERENT tier name    -> HIDE
3611//     - Mentions no tier name             -> SHOW (generic option)
3612//
3613// This handles SimplyStamps's convention of naming options like
3614// "Pre-Inked Sizes", "Self-Inking Sizes", "Ink Color - Pre-Inked"
3615// with no manual config required.
3616//
3617// ESCAPE HATCH: TIER_OPTION_FILTERS below lets you override per tier
3618// for edge cases (labels that don't follow the convention).
3619//   show: []  -- if non-empty, ONLY GENERIC options matching these
3620//               keywords are kept. Tier-tagged auto-detection still runs.
3621//   hide: []  -- additional labels to drop (substring match).
3622//
3623// Inspect what survives for a given product:
3624//   window.SS_UPSELL_DEBUG = true; window.SSUpsell.inspect();
3625// ============================================================
3626var TIER_OPTION_FILTERS = {
3627// All upgrade tiers (Self-Inking, Pre-Inked, Professional) have
3628// built-in pads -- separate ink pad option is Wood-Handle-only.
3629'self_inking':  { hide: ['ink pad', 'pad color'] },
3630// Pre-Inked also has pre-loaded ink, so the extra-bottle option doesn't apply.
3631'pre_inked':    { hide: ['ink pad', 'pad color', 'extra bottle', 'bottle of ink'] },
3632'professional': { hide: ['ink pad', 'pad color'] }
3633};
3634// v1.31 -- Routing flipped: default to Professional, override to Pre-Inked
3635// only if the product name/breadcrumb signals a logo/brand focus.
3636// Rationale: nearly all TARGET_CATEGORIES are professional-use stamps
3637// (office, notary, banking, medical, etc), so Professional is the norm.
3638// Pre-Inked's sweet spot is high-fidelity logo/brand reproduction --
3639// that's the exception, so we key off those signals specifically.
3640var PRE_INKED_KEYWORDS = [
3641'logo', 'brand', 'branding', 'monogram', 'crest', 'emblem'
3642];
3643// v1.31 -- copy refresh per transition. New pattern:
3644//   headline: short, all-caps, question-style attention grabber
3645//   subhead:  one-sentence value prop that reinforces the tier
3646//   bullets:  detail points (kept from v1.30, still useful proof)
3647// If headline is empty string, the<h2> is skipped and subhead renders alone.
3648var COPY = {
3649'1->self-inking': {
3650// Wood Handle -> Self-Inking (teacher buyup). Heading pairs with the
3651// subhead's "ink is already inside" promise -- calls out the ink pad
3652// as the pain point being solved.
3653headline: 'Skip The Ink Pad?',
3654subhead:  'The ink is already inside. Just press down and you get a clean, perfect mark.',
3655bullets: [
3656'No separate ink pad needed -- built right in',
3657'10,000+ impressions before re-inking'
3658]
3659},
3660'2->pre_inked': {
3661headline: 'Serious About Your Brand?',
3662subhead:  'Pre-inked stamps print sharper detail and cleaner lines. Your logo, exactly right every time.',
3663bullets: [
3664'Sharper, crisper detail -- ideal for logos & branding',
3665'Up to 50,000 impressions per fill'
3666]
3667},
3668'2->professional': {
3669headline: 'Are You A Professional?',
3670subhead:  'For desks that stamp all day. Heavier build, thousands of crisp impressions.',
3671bullets: [
3672'Heavy-duty frame engineered for high-volume stamping',
3673'Durable mechanism backed by a long-term warranty'
3674]
3675}
3676};
3677// Success state copy -- keyed the same way as COPY
3678var SUCCESS_COPY = {
3679'1->self-inking': {
3680headline: 'Upgraded to Self-Inking',
3681tagline: 'You\'ll get 10,000+ cleaner impressions without juggling a separate ink pad.'
3682},
3683'2->pre_inked': {
3684headline: 'Upgraded to Pre-Inked',
3685tagline: 'Premium, sharper impressions -- up to 50,000 per fill. A smart pick for branding.'
3686},
3687'2->professional': {
3688headline: 'Upgraded to Professional',
3689tagline: 'Built for daily volume -- durable mechanism backed by a long-term warranty.'
3690}
3691};
3692// Session helpers -- once a user accepts an upgrade, don't badger them again.
3693// DEV_MODE_ALWAYS_SHOW bypasses the lock so the modal fires every ATC.
3694function hasUpgradedThisSession() {
3695if (DEV_MODE_ALWAYS_SHOW) return false;
3696try { return sessionStorage.getItem(SESSION_ACCEPTED_KEY) === '1'; } catch (e) { return false; }
3697}
3698function markUpgradedThisSession() {
3699try { sessionStorage.setItem(SESSION_ACCEPTED_KEY, '1'); } catch (e) {}
3700}
3701// Decline-cap helpers -- count declines (modal dismiss OR cancel at config)
3702// so the modal stops firing once the user has clearly opted out.
3703function getDeclineCount() {
3704try { return parseInt(sessionStorage.getItem(SESSION_DECLINE_KEY) || '0', 10) || 0; }
3705catch (e) { return 0; }
3706}
3707function incrementDeclineCount() {
3708try { sessionStorage.setItem(SESSION_DECLINE_KEY, String(getDeclineCount() + 1)); } catch (e) {}
3709}
3710function hasDeclinedTooMany() {
3711if (DEV_MODE_ALWAYS_SHOW) return false;
3712if (!MAX_DECLINES_PER_SESSION) return false; // 0 = unlimited
3713return getDeclineCount() >= MAX_DECLINES_PER_SESSION;
3714}
3715// ----------------------------------------------------------
3716// Variant helpers (A/B/C framework)
3717// ----------------------------------------------------------
3718function getAssignedVariant() {
3719// URL override wins (for QA)
3720try {
3721var url = new URL(window.location.href);
3722var override = (url.searchParams.get(VARIANT_OVERRIDE_PARAM) || '').toUpperCase();
3723if (VARIANTS.indexOf(override) !== -1) return override;
3724} catch (e) {}
3725// Persisted in localStorage
3726try {
3727var stored = localStorage.getItem(VARIANT_STORAGE_KEY);
3728if (stored && VARIANTS.indexOf(stored) !== -1) return stored;
3729} catch (e) {}
3730// First visit -- random assignment
3731var idx = Math.floor(Math.random() * VARIANTS.length);
3732var variant = VARIANTS[idx];
3733try { localStorage.setItem(VARIANT_STORAGE_KEY, variant); } catch (e) {}
3734log('Assigned new variant:', variant);
3735return variant;
3736}
3737function getVariantConfig(variant) {
3738return VARIANT_CONFIG[variant] || VARIANT_CONFIG.A || {};
3739}
3740// Pick the right coupon code for this upgrade transition. Variant B uses
3741// a couponCodes map keyed by upgrade target ("self-inking", "pre_inked",
3742// "professional"). Legacy single couponCode also supported as fallback.
3743function resolveCouponCode(variantCfg, targetTier) {
3744if (!variantCfg) return null;
3745if (variantCfg.couponCodes && targetTier) {
3746var key = targetTier.subtype ? targetTier.subtype
3747: (targetTier.tier === 2 ? 'self-inking' : null);
3748if (key && variantCfg.couponCodes[key]) return variantCfg.couponCodes[key];
3749}
3750return variantCfg.couponCode || null;
3751}
3752// Small local merge helper -- inline to avoid scope issues with the
3753// init()-scoped mergeParams. Used by both applyVariantCDiscount
3754// (stash phase) and applyDeferredCoupon (apply phase).
3755function _mergeParams(a, b) {
3756var out = {};
3757for (var k in a)  if (Object.prototype.hasOwnProperty.call(a, k))  out[k]  = a[k];
3758for (var k2 in b) if (Object.prototype.hasOwnProperty.call(b, k2)) out[k2] = b[k2];
3759return out;
3760}
3761// v1.30 -- STASH the coupon code (do NOT POST from PDP).
3762// Previously we POSTed to /checkout/cart/couponPost/ from the PDP right
3763// before form.submit. That raced with the concurrent add-to-cart POST
3764// and hit an empty cart in Magento's session, so the coupon never stuck.
3765// Now we save the code + tracking context to sessionStorage; the actual
3766// apply happens on the cart page once the item is in cart (see
3767// applyDeferredCoupon below).
3768//
3769// v1.32 -- On Zephyr the AJAX add opens the cart DRAWER instead of
3770// navigating to /checkout/cart, so the stashed coupon applies the next
3771// time the shopper opens the full cart / checkout (still before purchase).
3772var PENDING_COUPON_KEY = 'ss_upsell_pending_coupon';
3773function applyVariantCDiscount(couponCode, trackingParams) {
3774if (!couponCode) return;
3775try {
3776var payload = {
3777code: couponCode,
3778tracking: trackingParams || {},
3779ts: Date.now()
3780};
3781sessionStorage.setItem(PENDING_COUPON_KEY, JSON.stringify(payload));
3782log('Coupon deferred for cart-page application:', couponCode);
3783} catch (e) {
3784log('Failed to defer coupon:', e);
3785}
3786}
3787// v1.30 -- Apply any pending upsell coupon on the cart page.
3788// Runs at module scope so it fires on every page load, cart or not.
3789// Bails immediately if not on the cart page or no pending coupon.
3790// Clears sessionStorage BEFORE posting to prevent reload loops if
3791// the POST fails or reload happens for an unrelated reason.
3792function applyDeferredCoupon() {
3793// Only run on the cart page
3794if (!/\/checkout\/cart(\/|\?|$)/.test(window.location.pathname + window.location.search)) return;
3795var raw;
3796try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; }
3797if (!raw) return;
3798var data;
3799try { data = JSON.parse(raw); } catch (e) { data = null; }
3800if (!data || !data.code) {
3801try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {}
3802return;
3803}
3804// Clear FIRST so a failed apply doesn't cause a reload loop
3805try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {}
3806log('Applying deferred coupon on cart page:', data.code);
3807var params = _mergeParams(data.tracking || {}, { coupon_code: data.code });
3808try {
3809var fd = new FormData();
3810fd.append('form_key', getFormKey());
3811fd.append('coupon_code', data.code);
3812fd.append('remove', '0');
3813fetch('/checkout/cart/couponPost/', {
3814method: 'POST',
3815body: fd,
3816credentials: 'same-origin',
3817headers: { 'X-Requested-With': 'XMLHttpRequest' }
3818}).then(function (r) {
3819log('Deferred coupon response:', r.status);
3820if (r.status >= 200 && r.status < 400) {
3821track('upsell_discount_applied', _mergeParams(params, { http_status: r.status }));
3822// Reload cart so the discount appears in the totals.
3823// Small delay so the event has time to fire before navigation.
3824setTimeout(function () {
3825// Guard against loop -- only reload if sessionStorage is now empty
3826var still;
3827try { still = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { still = null; }
3828if (!still) window.location.reload();
3829}, 150);
3830} else {
3831track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response' }));
3832}
3833}).catch(function (e) {
3834log('Deferred coupon POST failed:', e);
3835track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) }));
3836});
3837} catch (e) {
3838log('Deferred coupon apply threw:', e);
3839track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) }));
3840}
3841}
3842// v1.32b -- Apply a stashed coupon IMMEDIATELY (no page gate, no reload).
3843// On Zephyr the AJAX add opens the cart drawer and never loads /checkout/cart,
3844// so applyDeferredCoupon (cart-page only) might never run -- the shopper could
3845// reach checkout via the drawer and be charged full price on a promised
3846// discount. We call this right after the add's promise resolves, so the cart
3847// already has the item (no v1.30 race) and the coupon lands on the quote.
3848// On success we clear the stash; on failure we LEAVE it so the cart-page
3849// applyDeferredCoupon can still retry as a fallback.
3850function applyPendingCouponNow() {
3851var raw;
3852try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; }
3853if (!raw) return;
3854var data;
3855try { data = JSON.parse(raw); } catch (e) { data = null; }
3856if (!data || !data.code) { try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} return; }
3857log('Applying pending coupon immediately (post-add):', data.code);
3858var params = _mergeParams(data.tracking || {}, { coupon_code: data.code });
3859try {
3860var fd = new FormData();
3861fd.append('form_key', getFormKey());
3862fd.append('coupon_code', data.code);
3863fd.append('remove', '0');
3864fetch('/checkout/cart/couponPost/', {
3865method: 'POST',
3866body: fd,
3867credentials: 'same-origin',
3868headers: { 'X-Requested-With': 'XMLHttpRequest' }
3869}).then(function (r) {
3870log('Immediate coupon response:', r.status);
3871if (r.status >= 200 && r.status < 400) {
3872// Applied -- clear so the cart page doesn't reload to re-apply.
3873try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {}
3874track('upsell_discount_applied', _mergeParams(params, { http_status: r.status, applied_from: 'pdp_post_add' }));
3875} else {
3876// Leave stashed -- cart-page applyDeferredCoupon will retry.
3877track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response', applied_from: 'pdp_post_add' }));
3878}
3879}).catch(function (e) {
3880log('Immediate coupon POST failed (will retry on cart page):', e);
3881track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e), applied_from: 'pdp_post_add' }));
3882});
3883} catch (e) {
3884log('Immediate coupon apply threw:', e);
3885}
3886}
3887// Run deferred coupon apply at module scope (fires on every page load,
3888// including cart page -- bails if not cart / no pending coupon). This is the
3889// fallback for when the immediate post-add apply (applyPendingCouponNow)
3890// didn't run or didn't succeed.
3891try { applyDeferredCoupon(); } catch (e) { /* never break page load */ }
3892// Record each upgrade so the order-success page can fire upsell_purchased
3893// with order_id + revenue_delta. Stored as an array in sessionStorage;
3894// the success-page snippet reads, dispatches, and clears it.
3895function recordUpgradeForPurchaseTracking(params, qty) {
3896try {
3897var key = 'ss_upsell_pending_purchases';
3898var existing = sessionStorage.getItem(key);
3899var arr = existing ? JSON.parse(existing) : [];
3900var rec = { qty: parseInt(qty, 10) || 1, ts: Date.now() };
3901for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) rec[k] = params[k];
3902arr.push(rec);
3903sessionStorage.setItem(key, JSON.stringify(arr));
3904log('Recorded pending upgrade for purchase tracking:', rec);
3905} catch (e) {}
3906}
3907function log() {
3908if (window.SS_UPSELL_DEBUG && window.console) {
3909var args = ['[TierUpsell]'].concat([].slice.call(arguments));
3910console.log.apply(console, args);
3911}
3912}
3913function norm(s) { return (s || '').toLowerCase().trim(); }
3914function findTierForText(text) {
3915var t = norm(text);
3916for (var i = 0; i < TIER_MAP.length; i++) {
3917var tier = TIER_MAP[i];
3918for (var j = 0; j < tier.match.length; j++) {
3919if (t.indexOf(tier.match[j]) !== -1) return tier;
3920}
3921}
3922return null;
3923}
3924// Like findTierForText but returns ALL tiers whose name appears in the text.
3925// Used to detect which tier an option label is tagged for.
3926function findAllTiersInText(text) {
3927var t = norm(text);
3928var matches = [];
3929for (var i = 0; i < TIER_MAP.length; i++) {
3930var tier = TIER_MAP[i];
3931for (var j = 0; j < tier.match.length; j++) {
3932if (t.indexOf(tier.match[j]) !== -1) { matches.push(tier); break; }
3933}
3934}
3935return matches;
3936}
3937function findStampTypeSelect() {
3938var labels = document.querySelectorAll('.product-options label, .product-options-wrapper label, dt label, .field label, label');
3939for (var i = 0; i < labels.length; i++) {
3940var lbl = labels[i];
3941if (!STAMP_TYPE_LABEL_RE.test(lbl.textContent)) continue;
3942var forId = lbl.getAttribute('for');
3943if (forId) {
3944var byId = document.getElementById(forId);
3945if (byId && byId.tagName === 'SELECT') return byId;
3946}
3947var container = lbl.closest && (lbl.closest('.field, dd, dt, li, .input-box, .product-options li')) || lbl.parentElement;
3948if (container) {
3949var sel = container.querySelector('select');
3950if (sel) return sel;
3951}
3952}
3953var selects = document.querySelectorAll('.product-options select, .product-options-wrapper select');
3954for (var k = 0; k < selects.length; k++) {
3955for (var m = 0; m < selects[k].options.length; m++) {
3956if (findTierForText(selects[k].options[m].text)) return selects[k];
3957}
3958}
3959return null;
3960}
3961function getCurrentTier(selectEl) {
3962if (!selectEl || selectEl.selectedIndex < 0) return null;
3963var opt = selectEl.options[selectEl.selectedIndex];
3964var tier = findTierForText(opt.text);
3965return tier ? { tier: tier, optionEl: opt } : null;
3966}
3967// Scan the dropdown -- return which tiers are actually on THIS product.
3968// Keys: tier1, tier2, pre_inked, professional
3969function getAvailableTiers(selectEl) {
3970var available = {};
3971for (var i = 0; i < selectEl.options.length; i++) {
3972var t = findTierForText(selectEl.options[i].text);
3973if (!t) continue;
3974if (t.subtype) available[t.subtype] = t;
3975else available['tier' + t.tier] = t;
3976}
3977return available;
3978}
3979// Available options trump path preference. Preferred T3 first; fall back
3980// to the other T3 before bailing.
3981function getNextTier(currentTier, upsellPath, availableTiers) {
3982if (!currentTier) return null;
3983if (currentTier.tier >= 3) return null;
3984if (currentTier.tier === 1) {
3985return availableTiers.tier2 || null;
3986}
3987if (currentTier.tier === 2) {
3988var preferred = upsellPath === 'professional' ? 'professional' : 'pre_inked';
3989var fallback  = upsellPath === 'professional' ? 'pre_inked'    : 'professional';
3990return availableTiers[preferred] || availableTiers[fallback] || null;
3991}
3992return null;
3993}
3994function findOptionInSelect(selectEl, tierDef) {
3995for (var i = 0; i < selectEl.options.length; i++) {
3996if (findTierForText(selectEl.options[i].text) === tierDef) return selectEl.options[i];
3997}
3998return null;
3999}
4000function parsePriceModifier(text) {
4001if (!text) return 0;
4002var m = text.match(/[+\-]?\s*\$\s*([\d,]+\.\d{2})/);
4003return m ? parseFloat(m[1].replace(/,/g, '')) : 0;
4004}
4005function calcPriceDelta(currentOpt, upgradeOpt) {
4006var cur = parsePriceModifier(currentOpt.text);
4007var up  = parsePriceModifier(upgradeOpt.text);
4008return Math.max(0, up - cur);
4009}
4010function getFormKey() {
4011if (window.FORM_KEY) return window.FORM_KEY;
4012var input = document.querySelector('input[name="form_key"]');
4013return input ? input.value : '';
4014}
4015function getOptionIdFromSelectName(selectName) {
4016var m = (selectName || '').match(/options\[(\d+)\]/);
4017return m ? m[1] : null;
4018}
4019function detectUpsellPath() {
4020var explicit = document.querySelector('[data-upsell-path]');
4021if (explicit) {
4022var val = explicit.getAttribute('data-upsell-path');
4023log('upsell_path from data attribute:', val);
4024return val;
4025}
4026var hay = '';
4027hay += ' ' + (document.body.className || '');
4028var crumbs = document.querySelectorAll('.breadcrumbs a, .breadcrumbs li, nav[aria-label*="readcrumb"] a, nav[aria-label*="readcrumb"] li');
4029for (var i = 0; i < crumbs.length; i++) hay += ' ' + (crumbs[i].textContent || '');
4030var h1 = document.querySelector('.product-name h1, h1.product-name, h1[itemprop="name"], h1');
4031if (h1) hay += ' ' + (h1.textContent || '');
4032hay = hay.toLowerCase();
4033// v1.31 -- flipped: check for pre-inked signals (logo/brand focus).
4034// If any pre-inked keyword matches, route to pre_inked. Otherwise
4035// default to professional (aligns with our target category mix).
4036for (var k = 0; k < PRE_INKED_KEYWORDS.length; k++) {
4037if (hay.indexOf(PRE_INKED_KEYWORDS[k]) !== -1) {
4038log('upsell_path = pre_inked (matched keyword:', PRE_INKED_KEYWORDS[k], ')');
4039return 'pre_inked';
4040}
4041}
4042log('upsell_path = professional (default)');
4043return 'professional';
4044}
4045// Check if the current product's category breadcrumb passes the
4046// TARGET_CATEGORIES / EXCLUDED_CATEGORIES filters. Returns true if the
4047// upsell modal should fire for this product.
4048function isCategoryAllowed(categoryBreadcrumb) {
4049var crumb = String(categoryBreadcrumb || '').toLowerCase();
4050// Exclusions always win
4051for (var i = 0; i < EXCLUDED_CATEGORIES.length; i++) {
4052var ex = String(EXCLUDED_CATEGORIES[i]).toLowerCase();
4053if (ex && crumb.indexOf(ex) !== -1) {
4054log('Category excluded by rule:', EXCLUDED_CATEGORIES[i]);
4055return false;
4056}
4057}
4058// If no targets defined, allow all
4059if (TARGET_CATEGORIES.length === 0) return true;
4060// Targets defined -- must match at least one
4061for (var j = 0; j < TARGET_CATEGORIES.length; j++) {
4062var tg = String(TARGET_CATEGORIES[j]).toLowerCase();
4063if (tg && crumb.indexOf(tg) !== -1) return true;
4064}
4065log('Category not in TARGET_CATEGORIES list -- skipping');
4066return false;
4067}
4068function detectCategory() {
4069// v1.32 -- Zephyr renders breadcrumbs as<li><a>...</a></li>. The old
4070// query (".breadcrumbs a, .breadcrumbs li") double-counted every crumb
4071// (e.g. "Address Stamps > Address Stamps > View All > View All"). Collect
4072// ONE entry per crumb (prefer the<li> text, which contains its anchor),
4073// drop Home, and collapse consecutive duplicates.
4074var items = document.querySelectorAll('.breadcrumbs li');
4075if (!items.length) items = document.querySelectorAll('.breadcrumbs a');
4076var trail = [];
4077for (var i = 0; i < items.length; i++) {
4078var t = (items[i].textContent || '').replace(/\s+/g, ' ').trim();
4079if (!t || t.toLowerCase() === 'home') continue;
4080if (trail.length && trail[trail.length - 1].toLowerCase() === t.toLowerCase()) continue;
4081trail.push(t);
4082}
4083return trail.join(' > ');
4084}
4085function track(eventName, payload) {
4086window.dataLayer = window.dataLayer || [];
4087var data = { event: eventName };
4088for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) data[k] = payload[k];
4089window.dataLayer.push(data);
4090log('Event:', eventName, payload);
4091}
4092// Resolve the image URL for an upgrade tier. Uses subtype for tier 3
4093// (pre_inked / professional) and 'self-inking' for tier 2.
4094function getTierImage(tierDef) {
4095if (!tierDef) return '';
4096var key = tierDef.subtype ? tierDef.subtype
4097: (tierDef.tier === 2 ? 'self-inking' : null);
4098if (!key) return '';
4099return TIER_IMAGES[key] || '';
4100}
4101function buildModal(ctx) {
4102var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype;
4103var copy = COPY[key];
4104if (!copy) return null;
4105// Resolve variant config (passed via ctx so buildModal stays pure)
4106var vCfg = ctx.variantCfg || {};
4107var overlay = document.createElement('div');
4108overlay.className = 'ss-upsell-overlay';
4109overlay.setAttribute('role', 'presentation');
4110var bulletHtml = copy.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join('');
4111var imgUrl = getTierImage(ctx.next);
4112var imgHtml = imgUrl
4113? '<div class="ss-upsell-image"><img src="' + imgUrl + '" alt="' + escHtml(ctx.next.name) + ' stamp"></div>'
4114: '';
4115// === Variant-aware elements ===
4116// v1.31 -- "Recommended upgrade" chip/badge removed from Step 1 render.
4117// The badge variables + CSS classes remain in the file (dead code) in
4118// case a future variant wants to bring back a chip. To re-enable, add
4119// badgeHtml back into the innerHTML template below.
4120var badgeText  = vCfg.badgeText || 'Recommended upgrade';
4121var badgeClass = vCfg.badgeClass || '';
4122var badgeHtml  = '<div class="ss-upsell-badge ' + badgeClass + '">' + escHtml(badgeText) + '</div>';
4123void badgeHtml; // suppress unused warning; kept for potential future re-enable
4124// Discount banner (variant B only)
4125var discountBannerHtml = vCfg.discountBanner
4126? '<div class="ss-upsell-discount-banner">' + vCfg.discountBanner + '</div>'
4127: '';
4128// Price line -- strikethrough + discounted price for variant C
4129var priceLineHtml;
4130if (vCfg.discountPct && ctx.priceDelta > 0) {
4131var discounted = (ctx.priceDelta * (1 - vCfg.discountPct / 100)).toFixed(2);
4132priceLineHtml =
4133'<p class="ss-upsell-price">' +
4134'Upgrade for ' +
4135'<span class="ss-upsell-price-strike">+$' + ctx.priceDelta.toFixed(2) + '</span>' +
4136'<strong>+$' + discounted + '</strong>' +
4137'</p>';
4138} else {
4139priceLineHtml = '<p class="ss-upsell-price">Upgrade for <strong>+$' + ctx.priceDelta.toFixed(2) + '</strong></p>';
4140}
4141// Cost-per-use line (variant B + C)
4142var costPerUseHtml = '';
4143if (vCfg.showCostPerUse && ctx.priceDelta > 0) {
4144var impressions = ctx.next.subtype === 'pre_inked' ? 50000 : (ctx.next.tier === 2 ? 10000 : 100000);
4145var costPerUse = (ctx.priceDelta / impressions).toFixed(4);
4146costPerUseHtml = '<div class="ss-upsell-cost-per-use">~ $' + costPerUse + ' per impression over the life of this stamp</div>';
4147}
4148// Trust signal (variant B + C)
4149var trustHtml = '';
4150if (vCfg.trustSignal) {
4151var t = vCfg.trustSignal;
4152var parts = [];
4153if (t.ratingValue && t.ratingCount) {
4154parts.push('<span class="ss-stars">&#9733;&#9733;&#9733;&#9733;&#9733;</span> ' + escHtml(t.ratingValue) + ' from ' + escHtml(t.ratingCount) + ' reviews');
4155}
4156if (t.returnPolicy) parts.push(escHtml(t.returnPolicy));
4157if (parts.length) trustHtml = '<div class="ss-upsell-trust">' + parts.join(' &nbsp;|&nbsp; ') + '</div>';
4158}
4159// v1.31 -- optional headline + subhead. If headline is empty (e.g., the
4160// Wood Handle -> Self-Inking transition until copy is finalized),
4161// skip the<h2> so the subhead reads clean on its own.
4162var headlineHtml = copy.headline
4163? '<h2 id="ss-upsell-h">' + escHtml(copy.headline) + '</h2>'
4164: '';
4165var subheadHtml = copy.subhead
4166? '<p class="ss-upsell-subhead">' + escHtml(copy.subhead) + '</p>'
4167: '';
4168overlay.innerHTML =
4169'<div class="ss-upsell-modal" role="dialog" aria-modal="true"' +
4170(copy.headline ? ' aria-labelledby="ss-upsell-h"' : '') + '>' +
4171'<button class="ss-upsell-close" aria-label="Close">&times;</button>' +
4172// v1.31 -- badge removed from render
4173headlineHtml +
4174subheadHtml +
4175imgHtml +
4176discountBannerHtml +
4177'<ul class="ss-upsell-bullets">' + bulletHtml + '</ul>' +
4178priceLineHtml +
4179costPerUseHtml +
4180trustHtml +
4181'<div class="ss-upsell-actions">' +
4182'<button class="ss-upsell-accept" type="button">Upgrade my stamp</button>' +
4183'<button class="ss-upsell-decline" type="button">No thanks</button>' +
4184'</div>' +
4185'</div>';
4186return overlay;
4187}
4188function showModal(ctx) {
4189var overlay = buildModal(ctx);
4190if (!overlay) { log('No copy template for transition -- skipping'); return; }
4191document.body.appendChild(overlay);
4192var accept  = overlay.querySelector('.ss-upsell-accept');
4193var decline = overlay.querySelector('.ss-upsell-decline');
4194var close   = overlay.querySelector('.ss-upsell-close');
4195// v1.32b -- once the user has Accepted, step-1 dismissal must be dead.
4196// Otherwise (Zephyr AJAX path, no navigation) an outside/backdrop click
4197// during the success-state dwell would fire onDecline -> doSubmit and add
4198// the item a SECOND time alongside the success timer's submit.
4199var accepted = false;
4200function dismiss(method) {
4201if (accepted) return;
4202if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
4203document.removeEventListener('keydown', escHandler);
4204ctx.onDecline(method);
4205}
4206function escHandler(e) { if (e.key === 'Escape') dismiss('escape'); }
4207document.addEventListener('keydown', escHandler);
4208accept.addEventListener('click', function () {
4209// Hand the overlay to onAccept -- it decides whether to chain into the
4210// config step, the success state, or simply close.
4211if (accepted) return;              // v1.32b -- ignore double-click on Upgrade
4212accepted = true;
4213document.removeEventListener('keydown', escHandler);
4214ctx.onAccept(overlay);
4215});
4216decline.addEventListener('click', function () { dismiss('decline_button'); });
4217close.addEventListener('click',   function () { dismiss('close_button'); });
4218overlay.addEventListener('click', function (e) { if (e.target === overlay) dismiss('outside_click'); });
4219setTimeout(function () { accept.focus(); }, 50);
4220}
4221// Pick a sane default value for a required field that has no current
4222// value selected. Prefers options labeled "No" (avoids accidentally
4223// adding paid extras like "Yes - +$5.00") over the first non-empty.
4224function autoDefaultFor(field) {
4225if (!field || !field.choices) return '';
4226if (field.currentValue && String(field.currentValue).trim() !== '') return field.currentValue;
4227if (!field.required) return field.currentValue || '';
4228// Pass 1: prefer an option whose text starts with "no"
4229for (var i = 0; i < field.choices.length; i++) {
4230var c = field.choices[i];
4231if (!c.value || c.disabled) continue;
4232if (/^\s*no\b/i.test(c.text)) return c.value;
4233}
4234// Pass 2: first non-empty, non-disabled option
4235for (var j = 0; j < field.choices.length; j++) {
4236var c2 = field.choices[j];
4237if (c2.value && !c2.disabled) return c2.value;
4238}
4239return '';
4240}
4241// Step 2: render the config step inside the existing overlay so the
4242// user can review/adjust their other custom options for the upgrade.
4243function showConfigStep(overlay, ctx, fields, onConfirm, onCancel) {
4244var fieldsHtml = fields.map(function (field, i) {
4245var req = field.required ? ' <span class="ss-config-required">*</span>' : '';
4246var effectiveValue = autoDefaultFor(field);
4247if (field.type === 'select') {
4248var optHtml = field.choices.map(function (c) {
4249var sel = c.value === effectiveValue ? ' selected' : '';
4250var dis = c.disabled ? ' disabled' : '';
4251return '<option value="' + escHtml(c.value) + '"' + sel + dis + '>' + escHtml(c.text) + '</option>';
4252}).join('');
4253return '<div class="ss-opt-field">' +
4254'<label>' + escHtml(field.label) + req + '</label>' +
4255'<select data-field-idx="' + i + '">' + optHtml + '</select>' +
4256'</div>';
4257}
4258if (field.type === 'radio') {
4259var radioHtml = field.choices.map(function (c) {
4260var ch = c.value === effectiveValue ? ' checked' : '';
4261return '<label class="ss-opt-radio">' +
4262'<input type="radio" name="ss-radio-' + i + '" value="' + escHtml(c.value) + '"' + ch + '> ' +
4263escHtml(c.text) +
4264'</label>';
4265}).join('');
4266return '<div class="ss-opt-field" data-field-idx="' + i + '">' +
4267'<div class="ss-opt-group-label">' + escHtml(field.label) + req + '</div>' +
4268radioHtml +
4269'</div>';
4270}
4271if (field.type === 'text') {
4272var maxAttr = field.maxlength ? ' maxlength="' + field.maxlength + '"' : '';
4273return '<div class="ss-opt-field">' +
4274'<label>' + escHtml(field.label) + req + '</label>' +
4275'<input type="text" data-field-idx="' + i + '" value="' + escHtml(field.currentValue) + '"' + maxAttr + '>' +
4276'</div>';
4277}
4278if (field.type === 'textarea') {
4279return '<div class="ss-opt-field">' +
4280'<label>' + escHtml(field.label) + req + '</label>' +
4281'<textarea data-field-idx="' + i + '" rows="3">' + escHtml(field.currentValue) + '</textarea>' +
4282'</div>';
4283}
4284return '';
4285}).join('');
4286// v1.31 -- config-step badge removed to match Step 1 chip removal.
4287// Cancel button now inherits the small-link style (ss-config-cancel
4288// modifier retained for JS querySelector but no longer overrides CSS).
4289overlay.innerHTML =
4290'<div class="ss-upsell-modal" role="dialog" aria-modal="true" aria-labelledby="ss-config-h">' +
4291'<button class="ss-upsell-close" aria-label="Cancel">&times;</button>' +
4292'<h2 id="ss-config-h">Review your options</h2>' +
4293'<p class="ss-config-intro">Confirm the selections for your upgraded stamp.</p>' +
4294'<div class="ss-config-fields">' + fieldsHtml + '</div>' +
4295'<div class="ss-upsell-actions">' +
4296'<button class="ss-upsell-accept" type="button">Confirm upgrade</button>' +
4297'<button class="ss-upsell-decline ss-config-cancel" type="button">Cancel upgrade</button>' +
4298'</div>' +
4299'</div>';
4300var confirmBtn = overlay.querySelector('.ss-upsell-accept');
4301var cancelBtn  = overlay.querySelector('.ss-config-cancel');
4302var closeBtn   = overlay.querySelector('.ss-upsell-close');
4303// v1.32b -- guard against a double-click on Confirm firing two submits
4304// (the Zephyr AJAX add doesn't navigate, so a second one really adds twice).
4305var confirmed = false;
4306confirmBtn.addEventListener('click', function () {
4307if (confirmed) return;
4308var values = {};
4309var missing = [];
4310fields.forEach(function (field, i) {
4311var val = null;
4312if (field.type === 'select') {
4313var s = overlay.querySelector('select[data-field-idx="' + i + '"]');
4314if (s) val = s.value;
4315} else if (field.type === 'radio') {
4316var r = overlay.querySelector('[data-field-idx="' + i + '"] input[type="radio"]:checked');
4317if (r) val = r.value;
4318} else if (field.type === 'text' || field.type === 'textarea') {
4319var ti = overlay.querySelector('[data-field-idx="' + i + '"]');
4320if (ti) val = ti.value;
4321}
4322if (val != null) values[field.name] = val;
4323if (field.required && (val == null || String(val).trim() === '')) {
4324missing.push(i);
4325}
4326});
4327// Clear prior error styling
4328var oldErrs = overlay.querySelectorAll('.ss-opt-field-error');
4329for (var e = 0; e < oldErrs.length; e++) oldErrs[e].classList.remove('ss-opt-field-error');
4330var oldMsgs = overlay.querySelectorAll('.ss-opt-error');
4331for (var m = 0; m < oldMsgs.length; m++) oldMsgs[m].parentNode && oldMsgs[m].parentNode.removeChild(oldMsgs[m]);
4332// Mark missing fields and bail if any
4333if (missing.length > 0) {
4334missing.forEach(function (idx, n) {
4335var el = overlay.querySelector('[data-field-idx="' + idx + '"]');
4336if (!el) return;
4337var fieldEl = el.closest('.ss-opt-field') || el.parentElement;
4338if (!fieldEl) return;
4339fieldEl.classList.add('ss-opt-field-error');
4340var msg = document.createElement('div');
4341msg.className = 'ss-opt-error';
4342msg.textContent = 'Please make a selection';
4343fieldEl.appendChild(msg);
4344if (n === 0) {
4345try { fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) { fieldEl.scrollIntoView(); }
4346}
4347});
4348track('upsell_config_validation_failed', { missing_count: missing.length });
4349return;
4350}
4351confirmed = true;                  // v1.32b -- lock after a valid confirm
4352onConfirm(values);
4353});
4354cancelBtn.addEventListener('click', function () { if (confirmed) return; onCancel('cancel_button'); });
4355closeBtn.addEventListener('click',  function () { if (confirmed) return; onCancel('close_button'); });
4356setTimeout(function () { confirmBtn.focus(); }, 50);
4357}
4358// Replace the modal contents with a celebratory success state. After
4359// SUCCESS_DWELL_MS, run ctx.onSuccessComplete (which triggers the submit).
4360function showSuccessState(overlay, ctx) {
4361var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype;
4362var copy = SUCCESS_COPY[key] || { headline: 'Upgrade applied', tagline: 'Your stamp has been upgraded.' };
4363overlay.innerHTML =
4364// v1.31 -- "Premium upgrade applied" chip removed to match Step 1 + Step 2.
4365// Green checkmark icon carries the celebration signal on its own now.
4366'<div class="ss-upsell-modal ss-upsell-success" role="dialog" aria-modal="true" aria-labelledby="ss-upsell-success-h">' +
4367'<div class="ss-upsell-success-icon" aria-hidden="true">&#10003;</div>' +
4368'<h2 id="ss-upsell-success-h">' + copy.headline + '</h2>' +
4369'<p class="ss-upsell-success-tagline">
4369' + copy.tagline + '</p>' +
4370'<p class="ss-upsell-success-cta"><span class="ss-upsell-spinner"></span> Continuing to your cart...</p>' +
4371'<button class="ss-upsell-success-skip" type="button">Continue now</button>' +
4372'</div>';
4373var skipBtn = overlay.querySelector('.ss-upsell-success-skip');
4374var done = false;
4375function finalize() {
4376if (done) return;
4377done = true;
4378// Intentionally DO NOT remove the overlay here. We want the success
4379// state to remain visible while the browser navigates to the cart.
4380// The page transition will replace the modal naturally -- no flicker
4381// of bare PDP between success state and cart load.
4382//
4383// v1.32 note: on Zephyr the AJAX add opens the cart drawer rather than
4384// navigating away, so the success state is removed by handleAjaxAddToCart
4385// /the drawer taking over. It still dwells for SUCCESS_DWELL_MS first.
4386if (typeof ctx.onSuccessComplete === 'function') ctx.onSuccessComplete();
4387}
4388skipBtn.addEventListener('click', finalize);
4389setTimeout(finalize, SUCCESS_DWELL_MS);
4390}
4391// ============================================================
4392// CONFIG STEP -- extract / sync custom options
4393// ============================================================
4394function escHtml(s) {
4395return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
4396}
4397function isElementHidden(el) {
4398if (!el) return true;
4399if (el.type === 'hidden') return true;
4400// Walk up the ancestor chain checking each computed style.
4401// Catches display:none, visibility:hidden, opacity:0 on the element
4402// OR any of its ancestors up to the form.
4403var node = el;
4404while (node && node !== document && node !== document.body) {
4405var s;
4406try { s = window.getComputedStyle(node); } catch (e) { s = null; }
4407if (s) {
4408if (s.display === 'none')        return true;
4409if (s.visibility === 'hidden')   return true;
4410if (parseFloat(s.opacity) === 0) return true;
4411}
4412node = node.parentElement;
4413}
4414// Final fallback -- offsetParent null catches some edge cases
4415if (el.offsetParent === null && el.tagName !== 'BODY') {
4416var rect = el.getBoundingClientRect();
4417if (rect.width === 0 && rect.height === 0) return true;
4418}
4419return false;
4420}
4421function cleanLabel(s) {
4422return String(s || '').replace(/\*/g, '').replace(/:\s*$/, '').replace(/\s+/g, ' ').trim();
4423}
4424function findLabelForFormField(el) {
4425if (!el) return 'Option';
4426if (el.id) {
4427var byFor = document.querySelector('label[for="' + el.id + '"]');
4428if (byFor) return cleanLabel(byFor.textContent);
4429}
4430// Magento default: previous DT contains the label
4431var dd = el.closest && el.closest('dd');
4432if (dd) {
4433var prevDt = dd.previousElementSibling;
4434if (prevDt && prevDt.tagName === 'DT') {
4435var lblInDt = prevDt.querySelector('label') || prevDt;
4436return cleanLabel(lblInDt.textContent);
4437}
4438}
4439var field = el.closest && el.closest('.field, .input-box, li');
4440if (field) {
4441var inLbl = field.querySelector('label.label, label');
4442if (inLbl) return cleanLabel(inLbl.textContent);
4443}
4444return 'Option';
4445}
4446// Pull the form's other custom option fields so we can render them in
4447// the config step. Skips the Stamp Type dropdown and any hidden fields.
4448function extractCustomOptions(form, excludeSelectEl) {
4449var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form;
4450var fields = [];
4451// Selects
4452var selects = container.querySelectorAll('select[name^="options"]');
4453for (var i = 0; i < selects.length; i++) {
4454var sel = selects[i];
4455if (sel === excludeSelectEl) continue;
4456if (isElementHidden(sel)) continue;
4457var choices = [];
4458for (var j = 0; j < sel.options.length; j++) {
4459var opt = sel.options[j];
4460choices.push({ value: opt.value, text: opt.text, disabled: opt.disabled });
4461}
4462fields.push({
4463type: 'select',
4464name: sel.name,
4465label: findLabelForFormField(sel),
4466currentValue: sel.value,
4467required: sel.hasAttribute('required') || sel.classList.contains('required-entry') || sel.classList.contains('validate-select'),
4468choices: choices
4469});
4470}
4471// Text inputs + textareas (stamp text, line 1, line 2, etc.)
4472// Gated by INCLUDE_TEXT_FIELDS_IN_CONFIG so the modal stays focused on
4473// tier-relevant choices. Text values still carry to the cart untouched.
4474if (INCLUDE_TEXT_FIELDS_IN_CONFIG) {
4475var texts = container.querySelectorAll('input[type="text"][name^="options"], textarea[name^="options"]');
4476for (var t = 0; t < texts.length; t++) {
4477var inp = texts[t];
4478if (isElementHidden(inp)) continue;
4479fields.push({
4480type: inp.tagName === 'TEXTAREA' ? 'textarea' : 'text',
4481name: inp.name,
4482label: findLabelForFormField(inp),
4483currentValue: inp.value,
4484required: inp.hasAttribute('required') || inp.classList.contains('required-entry'),
4485maxlength: inp.maxLength > 0 ? inp.maxLength : null
4486});
4487}
4488}
4489// Radio groups
4490var radios = container.querySelectorAll('input[type="radio"][name^="options"]');
4491var groups = {};
4492for (var k = 0; k < radios.length; k++) {
4493var r = radios[k];
4494if (isElementHidden(r)) continue;
4495if (!groups[r.name]) groups[r.name] = [];
4496groups[r.name].push(r);
4497}
4498for (var name in groups) {
4499if (!Object.prototype.hasOwnProperty.call(groups, name)) continue;
4500var grp = groups[name];
4501var checked = null;
4502var rChoices = [];
4503for (var m = 0; m < grp.length; m++) {
4504if (grp[m].checked) checked = grp[m];
4505rChoices.push({ value: grp[m].value, text: findLabelForFormField(grp[m]) || grp[m].value });
4506}
4507fields.push({
4508type: 'radio',
4509name: name,
4510label: findLabelForFormField(grp[0]),
4511currentValue: checked ? checked.value : '',
4512required: grp[0].classList.contains('validate-one-required-by-name'),
4513choices: rChoices
4514});
4515}
4516return fields;
4517}
4518// Normalize a tier definition into a key for TIER_OPTION_FILTERS.
4519//   Self-Inking -> 'self_inking'
4520//   Pre-Inked   -> 'pre_inked'   (from subtype)
4521//   Professional-> 'professional'
4522function tierFilterKey(tierDef) {
4523if (!tierDef) return null;
4524if (tierDef.subtype) return tierDef.subtype;
4525return String(tierDef.name || '').toLowerCase().replace(/[\s\-]+/g, '_');
4526}
4527// Filter custom option fields to those that apply to the upgrade target.
4528// Logic:
4529//   1. Override `hide` list (if any) drops matches first.
4530//   2. Auto-detect tier mentions in label:
4531//        - mentions target tier name        -> SHOW
4532//        - mentions a different tier's name -> HIDE
4533//        - mentions no tier                 -> fall through to step 3
4534//   3. Generic (no tier in label) options:
4535//        - if override `show` is set, only labels matching survive
4536//        - otherwise, show
4537function filterFieldsForTier(fields, tierDef) {
4538if (!tierDef) return fields;
4539var override = TIER_OPTION_FILTERS[tierFilterKey(tierDef)] || {};
4540var overrideShow = (override.show || []).map(function (s) { return String(s).toLowerCase(); });
4541var overrideHide = (override.hide || []).map(function (s) { return String(s).toLowerCase(); });
4542return fields.filter(function (field) {
4543var label = String(field.label || '').toLowerCase();
4544// Manual hide override wins
4545if (overrideHide.length && overrideHide.some(function (kw) { return label.indexOf(kw) !== -1; })) {
4546return false;
4547}
4548// Auto-detect tier tags in the label
4549var tiersInLabel = findAllTiersInText(label);
4550var mentionsTarget = tiersInLabel.indexOf(tierDef) !== -1;
4551var mentionsOther  = tiersInLabel.some(function (t) { return t !== tierDef; });
4552if (mentionsTarget) return true;          // explicitly tagged for our tier
4553if (mentionsOther)  return false;         // explicitly tagged for a different tier
4554// Generic (no tier tag): apply manual show list if present
4555if (overrideShow.length > 0) {
4556return overrideShow.some(function (kw) { return label.indexOf(kw) !== -1; });
4557}
4558return true;
4559});
4560}
4561// Set the value of a single select-or-multiselect element. For multi-
4562// selects (name ends with "[]"), we must iterate options and set
4563// .selected explicitly -- assigning .value alone is unreliable across
4564// browsers and may leave stale selections active.
4565function setSelectValue(selectEl, targetValue) {
4566if (!selectEl) return;
4567if (selectEl.multiple) {
4568for (var i = 0; i < selectEl.options.length; i++) {
4569selectEl.options[i].selected = (selectEl.options[i].value === targetValue);
4570}
4571} else {
4572selectEl.value = targetValue;
4573}
4574}
4575// Cascade auto-fill: themes that use progressive reveal only show the
4576// next dropdown after the previous one has a value. After we swap
4577// Stamp Type, we may need to fill Size before Ink Color appears, fill
4578// Ink Color before Extra Bottle appears, etc.
4579//
4580// Strategy: find any required dropdown that is visible AND empty,
4581// pick a sane default ("No" if present, else first non-empty option),
4582// dispatch change, wait, repeat until DOM stops revealing new fields.
4583function cascadeAutoFill(form, stampSelect, callback, iter, maxIter) {
4584iter = iter || 0;
4585maxIter = maxIter || 10;
4586if (iter >= maxIter) { log('Cascade hit max iterations (' + maxIter + ')'); callback(); return; }
4587var container = form.querySelector('.product-options, .product-options-wrapper') || form;
4588var selects = container.querySelectorAll('select[name^="options"]');
4589var changedAny = false;
4590for (var i = 0; i < selects.length; i++) {
4591var sel = selects[i];
4592if (sel === stampSelect) continue;
4593if (sel.offsetParent === null) continue;          // skip currently-hidden
4594if (sel.value && String(sel.value).trim() !== '') continue; // already filled
4595var isRequired = sel.hasAttribute('required') ||
4596sel.classList.contains('required-entry') ||
4597sel.classList.contains('validate-select');
4598if (!isRequired) continue;
4599// Pick a sane default -- "No" preferred to avoid paid extras
4600var defaultVal = '';
4601for (var j = 0; j < sel.options.length; j++) {
4602var opt = sel.options[j];
4603if (!opt.value || opt.disabled) continue;
4604if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; }
4605if (!defaultVal) defaultVal = opt.value;
4606}
4607if (defaultVal) {
4608log('Cascade fill: ' + sel.name + ' = ' + defaultVal);
4609sel.value = defaultVal;
4610try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
4611changedAny = true;
4612}
4613}
4614if (!changedAny) { log('Cascade stable at iter ' + iter); callback(); return; }
4615setTimeout(function () { cascadeAutoFill(form, stampSelect, callback, iter + 1, maxIter); }, 300);
4616}
4617// Safety-net pass -- runs RIGHT BEFORE form submit. Iterates one final time
4618// through every visible required empty select/radio and fills with defaults.
4619// Catches any field that cascade missed because the theme revealed it after
4620// cascade exited (chained progressive reveals with long settle times).
4621// Returns the count of fields it filled (for logging).
4622function safetyNetFill(form, stampSelect) {
4623var filled = 0;
4624var container = form.querySelector('.product-options, .product-options-wrapper') || form;
4625// Selects
4626var selects = container.querySelectorAll('select[name^="options"]');
4627for (var i = 0; i < selects.length; i++) {
4628var sel = selects[i];
4629if (sel === stampSelect) continue;
4630if (sel.offsetParent === null) continue;
4631if (sel.value && String(sel.value).trim() !== '') continue;
4632var isRequired = sel.hasAttribute('required') ||
4633sel.classList.contains('required-entry') ||
4634sel.classList.contains('validate-select');
4635if (!isRequired) continue;
4636var defaultVal = '';
4637for (var j = 0; j < sel.options.length; j++) {
4638var opt = sel.options[j];
4639if (!opt.value || opt.disabled) continue;
4640if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; }
4641if (!defaultVal) defaultVal = opt.value;
4642}
4643if (defaultVal) {
4644log('Safety-net fill: ' + sel.name + ' = ' + defaultVal);
4645sel.value = defaultVal;
4646try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
4647filled++;
4648}
4649}
4650// Radio groups
4651var radios = container.querySelectorAll('input[type="radio"][name^="options"]');
4652var groups = {};
4653for (var k = 0; k < radios.length; k++) {
4654var r = radios[k];
4655if (r.offsetParent === null) continue;
4656if (!groups[r.name]) groups[r.name] = [];
4657groups[r.name].push(r);
4658}
4659for (var name in groups) {
4660if (!Object.prototype.hasOwnProperty.call(groups, name)) continue;
4661var grp = groups[name];
4662var hasChecked = false;
4663for (var m = 0; m < grp.length; m++) { if (grp[m].checked) { hasChecked = true; break; } }
4664if (hasChecked) continue;
4665var isReq = grp[0].classList.contains('validate-one-required-by-name');
4666if (!isReq) continue;
4667// Pick first non-disabled radio (or one with "no" label)
4668var pick = null;
4669for (var p = 0; p < grp.length; p++) {
4670if (grp[p].disabled) continue;
4671var lbl = findLabelForFormField(grp[p]) || '';
4672if (/^\s*no\b/i.test(lbl)) { pick = grp[p]; break; }
4673if (!pick) pick = grp[p];
4674}
4675if (pick) {
4676log('Safety-net fill (radio): ' + name + ' = ' + pick.value);
4677pick.checked = true;
4678try { pick.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
4679filled++;
4680}
4681}
4682return filled;
4683}
4684// v1.26 -- Carry-forward: capture user's pre-upgrade visible dropdown
4685// selections (Ink Color, Size, Ink Pad, Extra Ink, etc.) by category.
4686// These are re-applied after cascade so the user's original choice
4687// survives the tier swap, even when the theme reveals a different
4688// dropdown for the new tier (e.g., product 4378: Self-Inking exposes
4689// options[7505] "Ink Color" but Professional exposes options[7504]
4690// "Ink Colors" -- same colors, different option IDs).
4691function categorizeLabel(label) {
4692var s = String(label || '').toLowerCase();
4693if (/ink\s*colou?rs?/.test(s))                return 'ink_color';
4694if (/replacement\s*pad|ink\s*pad|extra.*pad/.test(s)) return 'ink_pad';
4695if (/extra\s*ink|extra.*bottle|bottle.*ink/.test(s))  return 'extra_ink';
4696if (/size/.test(s))                           return 'size';
4697// Fallback -- use full lowercased label as its own category
4698return 'other::' + s.trim();
4699}
4700function captureUserSelections(form, stampSelect) {
4701var captured = {};
4702var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form;
4703var selects = container.querySelectorAll('select[name^="options"]');
4704for (var i = 0; i < selects.length; i++) {
4705var sel = selects[i];
4706if (sel === stampSelect) continue;
4707if (isElementHidden(sel)) continue;
4708if (!sel.value || String(sel.value).trim() === '') continue;
4709var selectedOpt = sel.options[sel.selectedIndex];
4710if (!selectedOpt) continue;
4711var selectedText = String(selectedOpt.text || '').trim();
4712if (!selectedText) continue;
4713var label = findLabelForFormField(sel) || '';
4714var category = categorizeLabel(label);
4715// First-wins -- if two dropdowns share a category (rare), keep the first
4716if (!captured[category]) {
4717captured[category] = { text: selectedText, sourceLabel: label, sourceName: sel.name };
4718}
4719}
4720return captured;
4721}
4722function applyCarriedForward(form, stampSelect, captured) {
4723var applied = 0;
4724if (!captured) return 0;
4725var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form;
4726var selects = container.querySelectorAll('select[name^="options"]');
4727for (var i = 0; i < selects.length; i++) {
4728var sel = selects[i];
4729if (sel === stampSelect) continue;
4730if (isElementHidden(sel)) continue;
4731var label = findLabelForFormField(sel) || '';
4732var category = categorizeLabel(label);
4733var wanted = captured[category];
4734if (!wanted) continue;
4735var currentText = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : '';
4736// If already matches (e.g., cascade happened to pick right), skip
4737if (String(currentText).trim().toLowerCase() === wanted.text.toLowerCase()) continue;
4738// Find option whose text matches (case-insensitive, trimmed)
4739var matchOpt = null;
4740for (var j = 0; j < sel.options.length; j++) {
4741var opt = sel.options[j];
4742if (!opt.value || opt.disabled) continue;
4743// Strip price suffix like " +$5.00" for match tolerance
4744var optText = String(opt.text || '').replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim();
4745if (optText.toLowerCase() === wanted.text.replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim().toLowerCase()) {
4746matchOpt = opt;
4747break;
4748}
4749}
4750if (matchOpt) {
4751log('Carry-forward: ' + sel.name + ' (' + category + ') <- "' + wanted.text + '" from ' + wanted.sourceName);
4752sel.value = matchOpt.value;
4753try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
4754applied++;
4755} else {
4756log('Carry-forward: no match for "' + wanted.text + '" in ' + sel.name + ' (' + category + ') -- cascade default stays');
4757}
4758}
4759return applied;
4760}
4761function syncValuesToForm(form, values, fields) {
4762fields.forEach(function (field) {
4763var v = values[field.name];
4764if (v == null) return;
4765if (field.type === 'select') {
4766var sel = form.querySelector('select[name="' + field.name + '"]');
4767if (sel) {
4768sel.value = v;
4769try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
4770}
4771} else if (field.type === 'radio') {
4772var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]');
4773for (var i = 0; i < rs.length; i++) {
4774rs[i].checked = (rs[i].value === v);
4775if (rs[i].checked) {
4776try { rs[i].dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
4777}
4778}
4779} else if (field.type === 'text' || field.type === 'textarea') {
4780var ti = form.querySelector('[name="' + field.name + '"]');
4781if (ti) {
4782ti.value = v;
4783try {
4784ti.dispatchEvent(new Event('input',  { bubbles: true }));
4785ti.dispatchEvent(new Event('change', { bubbles: true }));
4786} catch (e) {}
4787}
4788}
4789});
4790}
4791function init() {
4792var form = document.getElementById('product_addtocart_form');
4793if (!form) { log('Not a PDP (no ATC form) -- skipping init'); return; }
4794var stampSelect = findStampTypeSelect();
4795if (!stampSelect) { log('No Stamp Type dropdown -- skipping init'); return; }
4796var upsellPath = detectUpsellPath();
4797var productInput = form.querySelector('input[name="product"]');
4798var productId    = productInput ? productInput.value : '';
4799var skuMeta      = document.querySelector('meta[itemprop="sku"], [itemprop="sku"]');
4800var productSku   = skuMeta ? (skuMeta.getAttribute('content') || skuMeta.textContent || '').trim() : '';
4801var category     = detectCategory();
4802var variant      = getAssignedVariant();
4803var variantCfg   = getVariantConfig(variant);
4804log('Init', { upsellPath: upsellPath, productId: productId, category: category, variant: variant });
4805function buildContext(cartItemId) {
4806// No-badger: if user already accepted an upgrade this session, skip.
4807if (hasUpgradedThisSession()) { log('buildContext: user already upgraded this session -- skipping'); return null; }
4808// Decline cap: if user has declined too many times this session, respect that and skip.
4809if (hasDeclinedTooMany()) { log('buildContext: decline cap reached (' + getDeclineCount() + '/' + MAX_DECLINES_PER_SESSION + ') -- skipping'); return null; }
4810// Category targeting: skip if this product's category isn't allowed.
4811if (!isCategoryAllowed(category)) { log('buildContext: category not in target/excluded by rule'); return null; }
4812var current = getCurrentTier(stampSelect);
4813if (!current) { log('buildContext: no current tier detected'); return null; }
4814var availableTiers = getAvailableTiers(stampSelect);
4815log('buildContext: available tiers on this product:', Object.keys(availableTiers));
4816var next = getNextTier(current.tier, upsellPath, availableTiers);
4817if (!next) { log('buildContext: no upgrade tier (top tier or no upgrade options on product)'); return null; }
4818var upgradeOpt = findOptionInSelect(stampSelect, next);
4819if (!upgradeOpt) { log('buildContext: upgrade option vanished from dropdown'); return null; }
4820log('buildContext: ' + current.tier.name + ' -> ' + next.name);
4821return {
4822current: current, next: next, upgradeOpt: upgradeOpt,
4823priceDelta: calcPriceDelta(current.optionEl, upgradeOpt),
4824cartItemId: cartItemId,
4825productId: productId, productSku: productSku, category: category, upsellPath: upsellPath
4826};
4827}
4828function mergeParams(a, b) {
4829var out = {}; for (var k in a) out[k] = a[k]; for (var k2 in b) out[k2] = b[k2]; return out;
4830}
4831// ----------------------------------------------------------
4832// ATC interception -- v1.2 PRE-ADD flow:
4833//   Show modal IMMEDIATELY on submit (no AJAX wait).
4834//   On accept: change the Stamp Type dropdown to the upgrade value,
4835//              then call the original submit. One cart write, right SKU.
4836//   On decline: just call the original submit unchanged.
4837//
4838//   v1.32 -- Zephyr's primary path is window.handleAjaxAddToCart (see
4839//   patchAjaxHandler below). The Varien patch + form 'submit' listener
4840//   are kept as fallbacks for any legacy/native path.
4841//   1. Patch window.handleAjaxAddToCart (Zephyr AJAX ATC) -- PRIMARY
4842//   2. Patch window.productAddToCartForm.submit (Varien) -- fallback
4843//   3. Fallback: form 'submit' event listener (non-Varien themes)
4844// ----------------------------------------------------------
4845function buildBaseParams(ctx) {
4846return {
4847from_tier:   ctx.current.tier.name,
4848to_tier:     ctx.next.name,
4849upsell_path: ctx.upsellPath,
4850product_id:  ctx.productId,
4851product_sku: ctx.productSku,
4852category:    ctx.category,
4853price_delta: ctx.priceDelta,
4854variant:     variant
4855};
4856}
4857function showUpsellAndSubmit(ctx, doSubmit) {
4858// v1.32b -- never stack two modals if the ATC button is double-clicked.
4859if (document.querySelector('.ss-upsell-overlay')) { log('Modal already open -- ignoring duplicate trigger'); return; }
4860// v1.32b -- the underlying submit (native AJAX add on Zephyr) must run at
4861// most ONCE per flow. The old code relied on a full-page navigation to
4862// make a stray second call harmless; the AJAX add has no navigation, so
4863// we guard it explicitly. onDecline / onCancel / onSuccessComplete all
4864// route through this wrapper.
4865var _rawSubmit = doSubmit, _submitted = false;
4866doSubmit = function () { if (_submitted) return; _submitted = true; _rawSubmit(); };
4867var baseParams = buildBaseParams(ctx);
4868var originalStampValue = ctx.current.optionEl.value;
4869track('upsell_modal_shown', baseParams);
4870// v1.28 -- Fire discount-side telemetry when variant B modal renders
4871// WITH a coupon code available for the target transition. Signals
4872// "user was exposed to the discount incentive" and lets B-only
4873// reports build without filtering upsell_modal_shown by variant.
4874if (variant === 'B') {
4875var shownCode = resolveCouponCode(variantCfg, ctx.next);
4876if (shownCode) {
4877track('upsell_discount_shown', mergeParams(baseParams, { coupon_code: shownCode }));
4878}
4879}
4880function chainToSuccessAndSubmit(overlay, userPicks, userFields, carriedForward) {
4881showSuccessState(overlay, {
4882current: ctx.current,
4883next: ctx.next,
4884onSuccessComplete: function () {
4885// Final safety net: catch any required field the cascade missed
4886// (e.g., chained progressive reveals where the theme exposes
4887// fields after our cascade exited). Without this, Magento
4888// backend validation can reject the submit and leave the user
4889// stuck on the PDP after the success state closes.
4890var filled = safetyNetFill(form, stampSelect);
4891if (filled > 0) log('Safety net filled ' + filled + ' field(s) before submit');
4892// v1.27 -- Re-apply carry-forward AFTER safety net. When the
4893// user changes Size in the modal, the theme cascade can reveal
4894// a NEW Ink Color dropdown that our initial carry-forward
4895// (which ran pre-modal) never touched. Safety net just filled
4896// it with the default (Black). Now we override that default
4897// with the user's original pre-upgrade pick (Red) if we find
4898// a match by category + text.
4899if (carriedForward) {
4900var reCarried = applyCarriedForward(form, stampSelect, carriedForward);
4901if (reCarried > 0) log('Late carry-forward: ' + reCarried + ' field(s) restored to original pick');
4902}
4903// CRITICAL: Re-apply user's explicit picks LAST. The theme's
4904// progressive reveal cascade can reset dependent options (e.g.,
4905// Ink Color resets when Stamp Type/Size change). Without this
4906// final re-sync, the safety net's defaults overwrite the user's
4907// intended color/option picks, leading to "I picked Red but cart
4908// shows Black" bugs. Direct value writes only -- no change event
4909// dispatch -- to avoid re-triggering the theme's cascade.
4910if (userPicks && userFields) {
4911var reapplied = 0;
4912userFields.forEach(function (field) {
4913var v = userPicks[field.name];
4914if (v == null || v === '') return;
4915if (field.type === 'select') {
4916var sel = form.querySelector('select[name="' + field.name + '"]');
4917if (sel && String(sel.value) !== String(v)) {
4918log('Re-applying user pick: ' + field.name + ' ' + sel.value + ' -> ' + v);
4919sel.value = v;
4920reapplied++;
4921}
4922} else if (field.type === 'radio') {
4923var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]');
4924for (var i = 0; i < rs.length; i++) {
4925var shouldCheck = (rs[i].value === v);
4926if (rs[i].checked !== shouldCheck) {
4927rs[i].checked = shouldCheck;
4928if (shouldCheck) reapplied++;
4929}
4930}
4931} else if (field.type === 'text' || field.type === 'textarea') {
4932var ti = form.querySelector('[name="' + field.name + '"]');
4933if (ti && ti.value !== v) {
4934log('Re-applying user pick: ' + field.name + ' = ' + v);
4935ti.value = v;
4936reapplied++;
4937}
4938}
4939});
4940if (reapplied > 0) log('Re-applied ' + reapplied + ' user pick(s) before submit');
4941}
4942log('Success state complete -- submitting');
4943doSubmit();
4944}
4945});
4946}
4947showModal({
4948current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta,
4949variantCfg: variantCfg,
4950onAccept: function (overlay) {
4951track('upsell_accepted', baseParams);
4952// v1.26 -- Capture user's pre-upgrade visible selections BEFORE
4953// Stamp Type swap wipes visibility (Ink Color, Size, etc.).
4954// These get carried forward to whichever dropdowns the new tier
4955// reveals, matched by category + option text (e.g., "Red").
4956var carriedForward = captureUserSelections(form, stampSelect);
4957log('Captured for carry-forward:', carriedForward);
4958// Multiselect-safe Stamp Type swap (handles options[1508][] style)
4959setSelectValue(stampSelect, ctx.upgradeOpt.value);
4960try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
4961log('Stamp Type swapped to:', ctx.upgradeOpt.text);
4962// Cascade auto-fill (themes with progressive reveal only show the
4963// next dropdown after the previous has a value). Then extract.
4964setTimeout(function () {
4965cascadeAutoFill(form, stampSelect, function () {
4966// v1.26 -- Apply carry-forward BEFORE extraction so the modal
4967// shows user's original picks as pre-selected values. User
4968// can still override in the modal if they want to change.
4969var carriedCount = applyCarriedForward(form, stampSelect, carriedForward);
4970if (carriedCount > 0) log('Carried forward ' + carriedCount + ' user selection(s) to new tier');
4971var allFields = extractCustomOptions(form, stampSelect);
4972var filtered  = filterFieldsForTier(allFields, ctx.next);
4973// Dedupe by label -- products sometimes have multiple custom
4974// options with identical labels (one per tier variant). Without
4975// tier hints in the label we can't distinguish them, so keep
4976// the first and drop the rest.
4977var seenLabels = {};
4978var fields = filtered.filter(function (f) {
4979var key = String(f.label || '').toLowerCase().trim();
4980if (!key) return true;
4981if (seenLabels[key]) return false;
4982seenLabels[key] = true;
4983return true;
4984});
4985log('Extracted ' + allFields.length + ' option(s), ' +
4986filtered.length + ' apply to ' + ctx.next.name + ', ' +
4987fields.length + ' after dedupe');
4988// Read qty from the form (used in revenue_delta calc later)
4989var qtyInput = form.querySelector('input[name="qty"]');
4990var qty = qtyInput ? qtyInput.value : 1;
4991if (fields.length === 0) {
4992// No other options to configure -- straight to success
4993markUpgradedThisSession();
4994track('upsell_confirmed', mergeParams(baseParams, { config_skipped: true }));
4995recordUpgradeForPurchaseTracking(baseParams, qty);
4996var skipCode = resolveCouponCode(variantCfg, ctx.next);
4997if (skipCode) {
4998log('Applying coupon for transition -> ' + ctx.next.name + ': ' + skipCode);
4999applyVariantCDiscount(skipCode, baseParams);
5000}
5001chainToSuccessAndSubmit(overlay, null, null, carriedForward);
5002return;
5003}
5004showConfigStep(overlay, ctx, fields,
5005function onConfirm(values) {
5006track('upsell_confirmed', mergeParams(baseParams, { config_field_count: fields.length }));
5007markUpgradedThisSession();
5008syncValuesToForm(form, values, fields);
5009recordUpgradeForPurchaseTracking(baseParams, qty);
5010var confirmCode = resolveCouponCode(variantCfg, ctx.next);
5011if (confirmCode) {
5012log('Applying coupon for transition -> ' + ctx.next.name + ': ' + confirmCode);
5013applyVariantCDiscount(confirmCode, baseParams);
5014}
5015// Pass user picks + fields so they get re-applied AFTER
5016// safetyNetFill, defeating theme cascade resets. Also pass
5017// carriedForward for v1.27's late-cascade rescue: if user
5018// changed Size in modal and theme revealed a NEW Ink Color
5019// dropdown, restore original pre-upgrade pick.
5020chainToSuccessAndSubmit(overlay, values, fields, carriedForward);
5021},
5022function onCancel(method) {
5023incrementDeclineCount();
5024track('upsell_canceled_at_config', mergeParams(baseParams, { cancel_method: method, decline_count: getDeclineCount() }));
5025// Revert Stamp Type to original -- user bailed at config
5026setSelectValue(stampSelect, originalStampValue);
5027try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
5028if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
5029log('Canceled at config -- count:', getDeclineCount(), '-- submitting original selection');
5030doSubmit();
5031}
5032);
5033});  // end cascadeAutoFill callback
5034}, 100);
5035},
5036onDecline: function (method) {
5037incrementDeclineCount();
5038track('upsell_declined', mergeParams(baseParams, { dismiss_method: method, decline_count: getDeclineCount() }));
5039log('Declined (' + method + ') -- count:', getDeclineCount(), '-- submitting original selection');
5040doSubmit();
5041}
5042});
5043}
5044// v1.32 -- PRIMARY Zephyr interception. The ATC button on Zephyr is
5045// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // so it never reaches Varien's submit or a native form submit event.
5046// We wrap the global handler: validate (Varien validator still exists),
5047// build the upsell context, and if an upgrade is available show the modal
5048// first. The original AJAX add runs only after accept/decline, by which
5049// point the Stamp Type has been swapped (on accept). handleAjaxAddToCart
5050// re-serializes the form when it runs, so the upgraded selection is added.
5051function patchAjaxHandler() {
5052var fn = window.handleAjaxAddToCart;
5053if (typeof fn !== 'function') return false;
5054if (fn._ssTierPatched) return true;
5055var orig = fn;
5056window.handleAjaxAddToCart = function (button) {
5057var args = arguments;
5058function proceed() { return orig.apply(window, args); }
5059// Respect Magento/Varien validation first. handleAjaxAddToCart runs
5060// the same validator internally; if the form isn't valid, hand off to
5061// the original so it surfaces the field errors (and we don't pop the
5062// modal on an incomplete form).
5063try {
5064var paf = window.productAddToCartForm;
5065if (paf && paf.validator && typeof paf.validator.validate === 'function' && !paf.validator.validate()) {
5066log('Validator failed -- passing through to native AJAX add');
5067return proceed();
5068}
5069} catch (e) {}
5070var ctx = buildContext(null);
5071if (!ctx) { log('No upsell context -- native AJAX add'); return proceed(); }
5072log('Upsell available -- showing modal before AJAX add (Zephyr path)');
5073showUpsellAndSubmit(ctx, function () {
5074// Run the native AJAX add, then clean up. Unlike the old Varien
5075// path this does NOT navigate, so WE must remove the success
5076// overlay (otherwise its full-screen fixed layer covers the page
5077// forever) and apply any stashed variant-B coupon now that the
5078// item is in the cart (the drawer never loads /checkout/cart).
5079var r;
5080try { r = proceed(); } catch (e) { r = null; }
5081var _done = false;
5082var done = function () {
5083if (_done) return; _done = true;   // idempotent -- promise OR timeout, whichever first
5084var o = document.querySelector('.ss-upsell-overlay');
5085if (o && o.parentNode) o.parentNode.removeChild(o);
5086try { applyPendingCouponNow(); } catch (e) {}
5087};
5088var thenable = r && typeof r.then === 'function';
5089if (thenable) { r.then(done, done); }
5090// Always arm a max-timeout too: if the add promise is non-thenable
5091// (short delay) OR never settles (6s hang guard), we still remove the
5092// overlay so it can't cover the page or suppress future modals.
5093setTimeout(done, thenable ? 6000 : 1200);
5094});
5095return false; // block the immediate add; proceed() runs after the modal resolves
5096};
5097window.handleAjaxAddToCart._ssTierPatched = true;
5098log('Patched window.handleAjaxAddToCart (Zephyr AJAX ATC)');
5099return true;
5100}
5101(function pollForAjaxHandler(attempts) {
5102attempts = attempts || 0;
5103if (patchAjaxHandler()) return;
5104if (attempts >= 30) { log('Gave up polling for handleAjaxAddToCart after 3s'); return; }
5105setTimeout(function () { pollForAjaxHandler(attempts + 1); }, 100);
5106})();
5107function patchVarienForm() {
5108var paf = window.productAddToCartForm;
5109if (!paf || typeof paf.submit !== 'function' || paf._ssTierPatched) return false;
5110var origSubmit = paf.submit.bind(paf);
5111paf.submit = function (button, url) {
5112if (this.validator && typeof this.validator.validate === 'function' && !this.validator.validate()) {
5113log('Magento validator failed -- letting native submit handle the error UI');
5114return origSubmit(button, url);
5115}
5116var ctx = buildContext(null);
5117if (!ctx) {
5118log('No upsell context -- proceeding with native ATC');
5119return origSubmit(button, url);
5120}
5121log('Upsell available -- showing modal pre-add');
5122showUpsellAndSubmit(ctx, function () { origSubmit(button, url); });
5123};
5124paf._ssTierPatched = true;
5125log('Patched window.productAddToCartForm.submit');
5126return true;
5127}
5128(function pollForVarien(attempts) {
5129attempts = attempts || 0;
5130if (patchVarienForm()) return;
5131if (attempts >= 30) { log('Gave up polling for productAddToCartForm after 3s'); return; }
5132setTimeout(function () { pollForVarien(attempts + 1); }, 100);
5133})();
5134// Fallback for non-Varien / Enter-key submission
5135form.addEventListener('submit', function (e) {
5136if (form._ssBypass) { form._ssBypass = false; return; } // we already showed modal
5137if (window.productAddToCartForm && window.productAddToCartForm._ssTierPatched) return;
5138// If the Zephyr AJAX handler is patched, it owns the flow -- don't
5139// double-fire the modal from a native submit that may also occur.
5140if (window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched) return;
5141var ctx = buildContext(null);
5142if (!ctx) return;
5143e.preventDefault();
5144log('Submit event intercepted (fallback path) -- showing modal pre-add');
5145showUpsellAndSubmit(ctx, function () {
5146form._ssBypass = true;
5147form.submit();
5148});
5149});
5150window.SSUpsell = {
5151showTestModal: function () {
5152var ctx = buildContext('TEST-CART-ITEM');
5153if (!ctx) { alert('No upsell available. Current: ' + (stampSelect.options[stampSelect.selectedIndex] || {}).text); return; }
5154var baseParams = { from_tier: ctx.current.tier.name, to_tier: ctx.next.name, upsell_path: ctx.upsellPath, product_id: ctx.productId, price_delta: ctx.priceDelta };
5155track('upsell_modal_shown', baseParams);
5156showModal({
5157current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta,
5158variantCfg: variantCfg,
5159onAccept:  function (overlay) {
5160track('upsell_accepted', baseParams);
5161if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
5162alert('Accept (test mode -- no cart write)');
5163},
5164onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); }
5165});
5166},
5167inspect: function () {
5168var ctx = buildContext(null);
5169var available = getAvailableTiers(stampSelect);
5170var currentTierLive = getCurrentTier(stampSelect);
5171// Dry-run the config-step extraction + filter so you can see what
5172// the user WOULD see if they accepted right now.
5173var optsPreview = null;
5174if (ctx) {
5175var all = extractCustomOptions(form, stampSelect);
5176var kept = filterFieldsForTier(all, ctx.next);
5177var seenL = {};
5178var deduped = kept.filter(function (f) {
5179var k = String(f.label || '').toLowerCase().trim();
5180if (!k) return true;
5181if (seenL[k]) return false;
5182seenL[k] = true;
5183return true;
5184});
5185optsPreview = {
5186extracted: all.map(function (f) { return f.label; }),
5187keptAfterFilter: kept.map(function (f) { return f.label; }),
5188shownToUser: deduped.map(function (f) { return f.label; }),
5189filterRule: TIER_OPTION_FILTERS[tierFilterKey(ctx.next)] || '(none -- show all)'
5190};
5191}
5192return {
5193upsellPath: upsellPath,
5194productId: productId,
5195productSku: productSku,
5196category: category,
5197currentTier: currentTierLive ? currentTierLive.tier.name : null,
5198nextTier:    ctx ? ctx.next.name : null,
5199priceDelta:  ctx ? ctx.priceDelta : null,
5200availableOnProduct: Object.keys(available),
5201ajaxHandlerPatched: !!(window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched),
5202ajaxHandlerPresent: typeof window.handleAjaxAddToCart === 'function',
5203varienFormPatched: !!(window.productAddToCartForm && window.productAddToCartForm._ssTierPatched),
5204varienFormPresent: !!window.productAddToCartForm,
5205alreadyUpgradedThisSession: hasUpgradedThisSession(),
5206declineCount: getDeclineCount(),
5207maxDeclinesPerSession: MAX_DECLINES_PER_SESSION,
5208declineCapReached: hasDeclinedTooMany(),
5209variant: variant,
5210variantConfig: variantCfg,
5211devModeAlwaysShow: DEV_MODE_ALWAYS_SHOW,
5212categoryAllowed: isCategoryAllowed(category),
5213targetCategories: TARGET_CATEGORIES,
5214excludedCategories: EXCLUDED_CATEGORIES,
5215configStepPreview: optsPreview
5216};
5217},
5218// QA helper -- clear the session flags (upgrade lock + decline count) so the modal fires again
5219resetSession: function () {
5220try {
5221sessionStorage.removeItem(SESSION_ACCEPTED_KEY);
5222sessionStorage.removeItem(SESSION_DECLINE_KEY);
5223} catch (e) {}
5224console.log('[TierUpsell] Session reset -- modal will fire on next ATC');
5225},
5226// QA helpers for variant testing
5227setVariant: function (v) {
5228v = String(v || '').toUpperCase();
5229if (VARIANTS.indexOf(v) === -1) {
5230console.warn('Invalid variant. Use one of:', VARIANTS);
5231return;
5232}
5233try { localStorage.setItem(VARIANT_STORAGE_KEY, v); } catch (e) {}
5234console.log('[TierUpsell] Variant set to:', v, '-- refresh the page to see changes');
5235},
5236clearVariant: function () {
5237try { localStorage.removeItem(VARIANT_STORAGE_KEY); } catch (e) {}
5238console.log('[TierUpsell] Variant cleared -- next page load will assign a new one');
5239},
5240simulateATC: function () {
5241var ctx = buildContext('TEST-CART-ITEM');
5242if (!ctx) { alert('No upsell available right now.'); return; }
5243var baseParams = {
5244from_tier: ctx.current.tier.name, to_tier: ctx.next.name,
5245upsell_path: ctx.upsellPath, product_id: ctx.productId,
5246product_sku: ctx.productSku, category: ctx.category, price_delta: ctx.priceDelta
5247};
5248track('upsell_modal_shown', baseParams);
5249showModal({
5250current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta,
5251variantCfg: variantCfg,
5252onAccept:  function (overlay) {
5253track('upsell_accepted', baseParams);
5254if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
5255console.log('[TierUpsell] Would update cart now (simulate mode)');
5256},
5257onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); console.log('[TierUpsell] Declined (simulate mode):', m); }
5258});
5259}
5260};
5261}
5262if (document.readyState === 'loading') {
5263document.addEventListener('DOMContentLoaded', init);
5264} else {
5265init();
5266}
5267})(); </script>
5267</div><div class="widget widget-static-block">
5267<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","
5267sort_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","
5267sort_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","
5267notary 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","
5267sort_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","
5267sort_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","
5267sort_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 
5267sender","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","
5267sort_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","
5267sort_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","
5267sort_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","
5267sort_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","
5267sort_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","
5267sort_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","
5267sort_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>
5267</div><div class="widget widget-static-block"> <style> /* =========================================================
5268Scoped to #cart-kit-upsell-mount to avoid site collisions
5269========================================================= */
5270#cart-kit-upsell-mount {
5271--ck-accent: var(--brand-primary, #1976d2);
5272--ck-accent-hover: var(--brand-primary-hover, #60B5E5);
5273--ck-success: var(--brand-success, #2e7d32);
5274--ck-success-hover: #256628;
5275}
5276#cart-kit-upsell-mount .cart-kit-upsell {
5277margin: 24px 0;
5278border-radius: 8px;
5279box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
5280background: #ffffff;
5281border: 1px solid #e3e3e3;
5282overflow: visible;
5283font-family: inherit;
5284position: relative;
5285}
5286#cart-kit-upsell-mount .cart-kit-upsell__header {
5287width: 100%;
5288text-align: left;
5289border: 0;
5290padding: 12px 16px;
5291background: #f5fafc;
5292display: flex;
5293align-items: center;
5294justify-content: flex-start;
5295gap: 12px;
5296cursor: pointer;
5297border-radius: 8px;
5298}
5299#cart-kit-upsell-mount .cart-kit-upsell__header:focus {
5300outline: 2px solid var(--ck-accent);
5301outline-offset: -2px;
5302}
5303#cart-kit-upsell-mount .cart-kit-upsell__header-text {
5304display: flex;
5305flex-direction: column;
5306gap: 4px;
5307flex: 1 1 auto;
5308}
5309#cart-kit-upsell-mount .cart-kit-upsell__title {
5310font-size: 20px;
5311font-weight: 600;
5312color: #222;
5313}
5314#cart-kit-upsell-mount .cart-kit-upsell__chevron {
5315width: 10px;
5316height: 10px;
5317border-right: 2px solid #555;
5318border-bottom: 2px solid #555;
5319transform: rotate(45deg);
5320transition: transform 0.2s ease;
5321}
5322#cart-kit-upsell-mount .cart-kit-upsell--open .cart-kit-upsell__chevron {
5323transform: rotate(-135deg);
5324}
5325#cart-kit-upsell-mount .cart-kit-upsell__body {
5326padding: 12px 16px 14px;
5327border-top: 1px solid #e3e3e3;
5328animation: cartKitUpsellFadeIn 0.18s ease-out;
5329}
5330@keyframes cartKitUpsellFadeIn {
5331from {
5332opacity: 0;
5333transform: translateY(-2px);
5334}
5335to {
5336opacity: 1;
5337transform: translateY(0);
5338}
5339}
5340/* Icon zone */
5341#cart-kit-upsell-mount .cart-kit-upsell__icon-wrap {
5342flex: 0 0 auto;
5343width: auto;
5344height: 36px;
5345display: flex;
5346align-items: center;
5347justify-content: center;
5348overflow: visible;
5349}
5350#cart-kit-upsell-mount .cart-kit-upsell__icon-img {
5351width: 70px;
5352height: auto;
5353display: block;
5354transform: none;
5355opacity: 1;
5356transition: none;
5357}
5358/* Options */
5359#cart-kit-upsell-mount .cart-kit-upsell__options-group {
5360display: flex;
5361flex-direction: column;
5362gap: 10px;
5363}
5364#cart-kit-upsell-mount .cart-kit-upsell__options-label {
5365font-size: 16px;
5366font-weight: 600;
5367color: #333;
5368}
5369#cart-kit-upsell-mount .cart-kit-upsell__options-header {
5370display: flex;
5371align-items: center;
5372gap: 6px;
5373margin-bottom: 4px;
5374overflow: visible !important;
5375}
5376/* Tooltip */
5377#cart-kit-upsell-mount .cart-kit-upsell__info {
5378position: relative;
5379border: 0;
5380padding: 0;
5381background: none;
5382cursor: pointer;
5383display: inline-flex;
5384z-index: 5;
5385}
5386#cart-kit-upsell-mount .cart-kit-upsell__info-icon {
5387width: 18px;
5388height: 18px;
5389border-radius: 999px;
5390border: 1px solid #888;
5391font-size: 11px;
5392font-weight: 600;
5393display: flex;
5394align-items: center;
5395justify-content: center;
5396color: #555;
5397background: #fff;
5398line-height: 1;
5399}
5400#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip {
5401position: absolute;
5402left: 50%;
5403bottom: calc(100% + 8px);
5404transform: translateX(-50%);
5405background: #222;
5406color: #fff;
5407font-size: 13px;
5408padding: 6px 8px;
5409border-radius: 4px;
5410box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
5411opacity: 0;
5412pointer-events: none;
5413transition: opacity .15s ease, transform .15s ease;
5414white-space: normal;
5415max-width: min(260px, 80vw);
5416width: 180px;
5417line-height: 1.2;
5418z-index: 9999;
5419}
5420#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip::after {
5421content: '';
5422position: absolute;
5423top: 100%;
5424left: 50%;
5425transform: translateX(-50%);
5426border-width: 6px;
5427border-style: solid;
5428border-color: #222 transparent transparent transparent;
5429}
5430#cart-kit-upsell-mount .cart-kit-upsell__info:hover .cart-kit-upsell__info-tooltip,
5431#cart-kit-upsell-mount .cart-kit-upsell__info:focus-visible .cart-kit-upsell__info-tooltip {
5432opacity: 1;
5433transform: translateX(-50%) translateY(-2px);
5434pointer-events: auto;
5435}
5436/* =========================================
5437Grid (Accent stamps OR Notary product tiles)
5438========================================= */
5439/* Default (Accent chips) */
5440#cart-kit-upsell-mount .kit-stamp-grid {
5441display: flex;
5442flex-wrap: wrap;
5443gap: 16px;
5444align-items: stretch;
5445}
5446#cart-kit-upsell-mount .kit-stamp-card {
5447border: none;
5448background: transparent;
5449padding: 0 4px 4px;
5450margin: 0;
5451cursor: pointer;
5452outline: none;
5453flex: 0 0 auto;
5454}
5455/* Accent chip circle */
5456#cart-kit-upsell-mount .kit-stamp-chip {
5457display: flex;
5458flex-direction: column;
5459align-items: center;
5460text-align: center;
5461max-width: 200px;
5462}
5463#cart-kit-upsell-mount .kit-stamp-chip__circle {
5464width: 200px;
5465height: auto;
5466border-radius: 16px;
5467background: #f3fbff;
5468box-shadow: 0 0 0 1px #e1f0fb;
5469display: flex;
5470align-items: center;
5471justify-content: center;
5472margin-bottom: 6px;
5473transition: box-shadow .15s ease, transform .1s ease, background .15s ease;
5474overflow: clip;
5475}
5476#cart-kit-upsell-mount .kit-stamp-chip__image {
5477width: 100%;
5478height: auto;
5479display: block;
5480}
5481#cart-kit-upsell-mount .kit-stamp-card:hover .kit-stamp-chip__circle {
5482box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.25);
5483transform: translateY(-1px);
5484}
5485#cart-kit-upsell-mount .kit-stamp-card.is-selected .kit-stamp-chip__circle {
5486background: #e6f2ff;
5487box-shadow: 0 0 0 2px var(--ck-accent);
5488}
5489/* =========================================
5490Notary tile layout (only when .is-notary-grid is applied)
5491========================================= */
5492#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid {
5493display: inline-grid;
5494grid-template-columns: repeat(6, minmax(0, 1fr));
5495gap: 16px;
5496align-items: stretch;
5497}
5498@media (max-width: 900px) {
5499#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid {
5500grid-template-columns: repeat(4, minmax(0, 1fr));
5501}
5502}
5503@media (max-width: 768px) {
5504#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid {
5505grid-template-columns: repeat(3, minmax(0, 1fr));
5506}
5507}
5508@media (max-width: 520px) {
5509#cart-kit-upsell-mount .cart-kit-upsell__header-text {
5510width: 50%;
5511}
5512/* Swipeable tiles on mobile */
5513#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid {
5514display: flex;
5515overflow-x: auto;
5516gap: 12px;
5517scroll-snap-type: x mandatory;
5518-webkit-overflow-scrolling: touch;
5519padding-bottom: 8px;
5520padding-top: 4px;
5521padding-left: 4px;
5522padding-right: 4px;
5523}
5524/* Each tile becomes a "slide" */
5525#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid .kit-stamp-card.kit-product-card {
5526flex: 0 0 78%;
5527scroll-snap-align: start;
5528}
5529/* Hide scrollbar (nice-to-have) */
5530#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid::-webkit-scrollbar {
5531display: none;
5532}
5533#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid {
5534scrollbar-width: none;
5535}
5536/* Target the actual tile grid container */
5537#cart-kit-upsell-mount .kit-stamp-grid {
5538display: flex !important;
5539flex-wrap: nowrap !important;
5540overflow-x: auto !important;
5541overflow-y: hidden !important;
5542gap: 12px !important;
5543scroll-snap-type: x mandatory;
5544-webkit-overflow-scrolling: touch;
5545padding-bottom: 10px;
5546}
5547/* Make each tile act like a slide */
5548#cart-kit-upsell-mount .kit-stamp-grid>* {
5549flex: 0 0 78% !important;
5550/* change to 60% to show ~2 at a time */
5551scroll-snap-align: start;
5552}
5553/* Optional: hide scrollbar */
5554#cart-kit-upsell-mount .kit-stamp-grid::-webkit-scrollbar {
5555display: none;
5556}
5557#cart-kit-upsell-mount .kit-stamp-grid {
5558scrollbar-width: none;
5559}
5560#cart-kit-upsell-mount,
5561#cart-kit-upsell-mount .kit-upsell-inner,
5562#cart-kit-upsell-mount .kit-upsell-body {
5563overflow: visible !important;
5564}
5565}
5566/* Notary tile card */
5567#cart-kit-upsell-mount .kit-stamp-card.kit-product-card {
5568background: #fff;
5569border: 1px solid #e1f0fb;
5570border-radius: 12px;
5571padding: 12px;
5572text-align: left;
5573flex: 1 1 auto;
5574transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, background .12s ease;
5575}
5576#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:hover {
5577transform: translateY(-1px);
5578border-color: rgba(25, 118, 210, 0.35);
5579box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
5580}
5581#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.ck-swapping {
5582opacity: 0;
5583transition: opacity .18s ease;
5584}
5585#cart-kit-upsell-mount .kit-empty-tiles {
5586padding: 18px;
5587border: 1px dashed #d9d9d9;
5588border-radius: 10px;
5589margin-top: 10px;
5590font-size: 14px;
5591color: #444;
5592background: #fafafa;
5593}
5594/* Pool exhaustion: completion tile */
5595#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete {
5596background: #f6faf7;
5597border: 1px dashed #b6dfc7;
5598cursor: default;
5599}
5600#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete:hover {
5601transform: none;
5602border-color: #b6dfc7;
5603box-shadow: none;
5604}
5605#cart-kit-upsell-mount .ck-tile-complete {
5606text-align: center;
5607padding: 16px 10px;
5608}
5609#cart-kit-upsell-mount .ck-tile-complete__icon {
5610font-size: 22px;
5611margin-bottom: 6px;
5612line-height: 1;
5613}
5614#cart-kit-upsell-mount .ck-tile-complete__title {
5615font-weight: 700;
5616margin-bottom: 4px;
5617}
5618#cart-kit-upsell-mount .ck-tile-complete__subtext {
5619font-size: 13px;
5620opacity: 0.85;
5621}
5622#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:focus-visible {
5623outline: 3px solid rgba(25, 118, 210, 0.35);
5624outline-offset: 2px;
5625}
5626#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected {
5627background: #f5f9ff;
5628border-color: var(--ck-accent);
5629box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.14);
5630}
5631/* Notary tile thumb */
5632#cart-kit-upsell-mount .kit-product-thumb-tile {
5633width: 100%;
5634aspect-ratio: 1 / 1;
5635border-radius: 10px;
5636background: #f3f6fb;
5637border: 1px solid rgba(0, 0, 0, 0.06);
5638overflow: hidden;
5639position: relative;
5640display: flex;
5641align-items: center;
5642justify-content: center;
5643}
5644#cart-kit-upsell-mount .kit-product-thumb-tile img {
5645width: 100%;
5646height: 100%;
5647object-fit: contain;
5648display: block;
5649background-color: #fff;
5650}
5651#cart-kit-upsell-mount .kit-product-meta {
5652margin-top: 10px;
5653}
5654#cart-kit-upsell-mount .kit-product-name {
5655font-weight: 700;
5656font-size: 13px;
5657color: #222;
5658line-height: 1.2;
5659margin: 0;
5660}
5661#cart-kit-upsell-mount .kit-product-price {
5662margin-top: 6px;
5663font-size: 12px;
5664color: #4a4a4a;
5665}
5666#cart-kit-upsell-mount .kit-product-sub {
5667margin-top: 4px;
5668font-size: 11px;
5669color: #6b6b6b;
5670}
5671/* Notary tile selected checkmark */
5672#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected .kit-product-thumb-tile::after {
5673content: "✓";
5674position: absolute;
5675right: 8px;
5676bottom: 8px;
5677width: 22px;
5678height: 22px;
5679border-radius: 999px;
5680background: var(--ck-success);
5681color: #fff;
5682font-size: 13px;
5683font-weight: 900;
5684display: flex;
5685align-items: center;
5686justify-content: center;
5687box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
5688}
5689/* =========================================
5690Micro PDP "Added" state (tile grey-out + overlay)
5691========================================= */
5692#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added {
5693opacity: 0.55;
5694filter: grayscale(0.25);
5695pointer-events: none;
5696/* prevent clicking again */
5697position: relative;
5698}
5699/* Stronger "Added" overlay */
5700#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added::before {
5701content: "✓ Added to cart";
5702position: absolute;
5703left: 10px;
5704right: 10px;
5705bottom: 10px;
5706padding: 10px 12px;
5707border-radius: 999px;
5708background: rgba(20, 20, 20, .90);
5709color: #fff;
5710font-weight: 800;
5711font-size: 13px;
5712text-align: center;
5713z-index: 6;
5714}
5715#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added img {
5716opacity: 0.75;
5717}
5718/* Module success toast */
5719#cart-kit-upsell-mount .kit-success-toast {
5720position: absolute;
5721right: 16px;
5722bottom: 16px;
5723background: var(--ck-success-hover);
5724color: #fff;
5725padding: 10px 12px;
5726border-radius: 10px;
5727font-weight: 800;
5728font-size: 13px;
5729max-width: 340px;
5730box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
5731opacity: 0;
5732transform: translateY(6px);
5733transition: opacity .18s ease, transform .18s ease;
5734pointer-events: none;
5735z-index: 50;
5736}
5737#cart-kit-upsell-mount .kit-success-toast.is-visible {
5738opacity: 1;
5739transform: translateY(0);
5740}
5741/* =========================================
5742Cart row highlight after Micro PDP add
5743========================================= */
5744#cart-kit-upsell-mount .ck-cart-highlight {
5745outline: 3px solid rgba(46, 125, 50, 0.35);
5746outline-offset: 4px;
5747border-radius: 8px;
5748animation: ckPulse 1.6s ease-in-out 0s 2;
5749}
5750@keyframes ckPulse {
57510% {
5752box-shadow: 0 0 0 rgba(46, 125, 50, 0.00);
5753}
575450% {
5755box-shadow: 0 0 0 10px rgba(46, 125, 50, 0.10);
5756}
5757100% {
5758box-shadow: 0 0 0 rgba(46, 125, 50, 0.00);
5759}
5760}
5761/* Ink swatches */
5762#cart-kit-upsell-mount .kit-swatch-list {
5763display: flex;
5764flex-wrap: wrap;
5765gap: 8px;
5766}
5767#cart-kit-upsell-mount .kit-swatch {
5768display: inline-flex;
5769align-items: center;
5770gap: 6px;
5771padding: 4px 8px;
5772border-radius: 999px;
5773border: 1px solid #d8e0ea;
5774background: #fff;
5775cursor: pointer;
5776font-size: 14px;
5777color: #333;
5778transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
5779}
5780#cart-kit-upsell-mount .kit-swatch:hover {
5781border-color: var(--ck-accent);
5782box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
5783}
5784#cart-kit-upsell-mount .kit-swatch.is-selected {
5785border-color: var(--ck-accent);
5786background: #f5f9ff;
5787box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.18);
5788}
5789#cart-kit-upsell-mount .kit-swatch__dot {
5790width: 14px;
5791height: 14px;
5792border-radius: 999px;
5793box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
5794}
5795#cart-kit-upsell-mount .kit-swatch__dot--black {
5796background: #111;
5797}
5798#cart-kit-upsell-mount .kit-swatch__dot--blue {
5799background: #1565c0;
5800}
5801#cart-kit-upsell-mount .kit-swatch__dot--red {
5802background: #c62828;
5803}
5804#cart-kit-upsell-mount .kit-swatch__dot--green {
5805background: var(--ck-success);
5806}
5807#cart-kit-upsell-mount .kit-swatch__dot--purple {
5808background: #6a1b9a;
5809}
5810#cart-kit-upsell-mount .kit-swatch__dot--none {
5811background: repeating-linear-gradient(45deg, #ccc, #ccc 3px, #eee 3px, #eee 6px);
5812}
5813/* Footer / CTA */
5814#cart-kit-upsell-mount .cart-kit-upsell__footer {
5815display: flex;
5816flex-wrap: wrap;
5817align-items: center;
5818justify-content: space-between;
5819gap: 8px;
5820margin-top: 18px;
5821padding: 8px;
5822}
5823#cart-kit-upsell-mount .cart-kit-upsell__price {
5824font-size: 16px;
5825color: #222;
5826display: flex;
5827gap: 14px;
5828align-items: center;
5829flex-wrap: wrap;
5830}
5831#cart-kit-upsell-mount .cart-kit-upsell__cta {
5832padding: 8px 16px;
5833border-radius: 4px;
5834border: 0;
5835background: var(--ck-success);
5836color: #fff;
5837font-size: 13px;
5838font-weight: 600;
5839cursor: pointer;
5840white-space: nowrap;
5841}
5842#cart-kit-upsell-mount .cart-kit-upsell__cta:hover {
5843background: var(--ck-success-hover);
5844}
5845#cart-kit-upsell-mount .cart-kit-upsell__cta:disabled {
5846opacity: .6;
5847cursor: default;
5848}
5849#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary {
5850padding: 8px 14px;
5851border-radius: 4px;
5852border: 1px solid #ccc;
5853background: #f7f7f7;
5854color: #333;
5855font-size: 13px;
5856font-weight: 600;
5857cursor: pointer;
5858white-space: nowrap;
5859}
5860#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary:hover {
5861background: #eee;
5862}
5863/* Spinner on Add button */
5864#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading {
5865position: relative;
5866padding-right: 30px;
5867}
5868#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading::after {
5869content: '';
5870position: absolute;
5871right: 10px;
5872top: 50%;
5873width: 14px;
5874height: 14px;
5875margin-top: -7px;
5876border-radius: 50%;
5877border: 2px solid rgba(255, 255, 255, 0.7);
5878border-top-color: transparent;
5879animation: cartKitSpinner .6s linear infinite;
5880}
5881@keyframes cartKitSpinner {
5882to {
5883transform: rotate(360deg);
5884}
5885}
5886/*  Add-to-cart progress bar (the ONLY progress bar in this script) */
5887#cart-kit-upsell-mount .cart-kit-upsell__progresswrap {
5888margin-top: 10px;
5889}
5890#cart-kit-upsell-mount .cart-kit-upsell__progresstop {
5891display: flex;
5892justify-content: space-between;
5893gap: 12px;
5894margin-bottom: 6px;
5895}
5896#cart-kit-upsell-mount .cart-kit-upsell__progresstext,
5897#cart-kit-upsell-mount .cart-kit-upsell__progresspct {
5898font-size: 12px;
5899color: #555;
5900}
5901#cart-kit-upsell-mount .cart-kit-upsell__progressbar {
5902height: 10px;
5903border-radius: 999px;
5904background: #eef3f6;
5905overflow: hidden;
5906}
5907#cart-kit-upsell-mount .cart-kit-upsell__progressfill {
5908height: 100%;
5909background: var(--ck-success);
5910transition: width .18s ease;
5911width: 0%;
5912}
5913/* Success header state */
5914#cart-kit-upsell-mount .cart-kit-upsell__header--success {
5915background: #f1faf3;
5916}
5917#cart-kit-upsell-mount .cart-kit-upsell__success-inline {
5918font-size: 14px;
5919color: var(--ck-success);
5920display: flex;
5921align-items: center;
5922gap: 8px;
5923font-weight: 600;
5924}
5925/* =========================================================
5926Micro PDP Modal (iframe-based)
5927========================================================= */
5928#cart-kit-upsell-mount .ck-micropdp-overlay {
5929position: fixed;
5930inset: 0;
5931background: rgba(0, 0, 0, .55);
5932display: none;
5933align-items: center;
5934justify-content: center;
5935z-index: 999999;
5936/* above cart + header */
5937}
5938#cart-kit-upsell-mount .ck-micropdp-overlay.is-open {
5939display: flex;
5940}
5941#cart-kit-upsell-mount .ck-micropdp-modal {
5942width: min(960px, 92vw);
5943height: 96vh;
5944background: #fff;
5945border-radius: 12px;
5946overflow: hidden;
5947box-shadow: 0 12px 40px rgba(0, 0, 0, .35);
5948display: flex;
5949flex-direction: column;
5950}
5951@media (max-width: 600px) {
5952#cart-kit-upsell-mount .ck-micropdp-modal {
5953width: 96vw;
5954height: 98vh;
5955border-radius: 10px;
5956}
5957}
5958#cart-kit-upsell-mount .ck-micropdp-head {
5959display: flex;
5960flex-direction: column;
5961align-items: stretch;
5962justify-content: flex-start;
5963gap: 10px;
5964padding: 12px 14px;
5965border-bottom: 1px solid #e6e6e6;
5966background: #f7fbff;
5967}
5968#cart-kit-upsell-mount .ck-micropdp-head-top {
5969display: flex;
5970align-items: center;
5971justify-content: space-between;
5972gap: 12px;
5973width: 100%;
5974}
5975#cart-kit-upsell-mount .ck-micropdp-progress {
5976width: 100%;
5977display: flex;
5978flex-direction: column;
5979gap: 6px;
5980}
5981#cart-kit-upsell-mount .ck-micropdp-progressbar {
5982width: 100%;
5983height: 8px;
5984background: rgba(25, 118, 210, .14);
5985border-radius: 999px;
5986overflow: hidden;
5987}
5988#cart-kit-upsell-mount .ck-micropdp-progressfill {
5989height: 100%;
5990width: 0%;
5991background: var(--ck-accent);
5992border-radius: 999px;
5993transition: width .25s ease;
5994}
5995#cart-kit-upsell-mount .ck-micropdp-progresstext {
5996font-size: 12px;
5997color: #1b1b1b;
5998opacity: .85;
5999line-height: 1.2;
6000}
6001/* Micro PDP header progress — completion delight */
6002#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progressfill {
6003background: var(--ck-success);
6004animation: ckMicroFillPulse .8s ease-in-out 0s 2;
6005}
6006#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progresstext {
6007font-weight: 700;
6008opacity: 1;
6009}
6010@keyframes ckMicroFillPulse {
60110% {
6012filter: brightness(1);
6013}
601450% {
6015filter: brightness(1.25);
6016}
6017100% {
6018filter: brightness(1);
6019}
6020}
6021#cart-kit-upsell-mount .ck-micropdp-title {
6022font-size: 16px;
6023font-weight: 700;
6024color: #1b1b1b;
6025margin: 0;
6026}
6027#cart-kit-upsell-mount .ck-micropdp-sub {
6028font-size: 12px;
6029color: #666;
6030margin-top: 2px;
6031}
6032#cart-kit-upsell-mount .ck-micropdp-actions {
6033display: flex;
6034gap: 8px;
6035align-items: center;
6036}
6037#cart-kit-upsell-mount .ck-micropdp-btn {
6038border: 1px solid #cfd8e3;
6039background: #fff;
6040color: #1b1b1b;
6041border-radius: 8px;
6042padding: 8px 10px;
6043cursor: pointer;
6044font-weight: 600;
6045font-size: 13px;
6046}
6047/* Loading cursor while micro PDP is submitting */
6048body.ck-loading,
6049body.ck-loading * {
6050cursor: progress !important;
6051}
6052#cart-kit-upsell-mount .ck-micropdp-btn:hover {
6053background: #f2f5f9;
6054}
6055#cart-kit-upsell-mount .ck-micropdp-body {
6056flex: 1 1 auto;
6057position: relative;
6058}
6059#cart-kit-upsell-mount .ck-micropdp-iframe {
6060width: 100%;
6061height: 100%;
6062border: 0;
6063display: block;
6064}
6065#cart-kit-upsell-mount .ck-micropdp-loading {
6066position: absolute;
6067inset: 0;
6068display: flex;
6069align-items: center;
6070justify-content: center;
6071background: rgba(255, 255, 255, .85);
6072font-weight: 700;
6073color: #444;
6074}
6075.ck-debug-keywords {
6076margin-top: 4px;
6077font-size: 11px;
6078line-height: 1.4;
6079}
6080.ck-debug-keyword {
6081display: inline-block;
6082background: rgba(255, 255, 255, 0.08);
6083border: 1px solid rgba(255, 255, 255, 0.12);
6084border-radius: 999px;
6085padding: 2px 8px;
6086margin: 2px 6px 2px 0;
6087opacity: .95;
6088}
6089.ck-debug-keyword.muted,
6090.ck-debug-keywords.muted {
6091opacity: 0.55;
6092font-style: italic;
6093}
6094@media (max-width: 640px) {
6095#cart-kit-upsell-mount .cart-kit-upsell__title {
6096font-size: 16px;
6097}
6098}
6099@media (max-width: 520px) {
6100/* Accent chip mobile layout */
6101#cart-kit-upsell-mount .kit-stamp-grid:not(.is-notary-grid) {
6102display: grid !important;
6103grid-template-columns: repeat(2, 1fr);
6104gap: 20px 12px;
6105justify-items: center;
6106}
6107#cart-kit-upsell-mount .kit-stamp-card {
6108width: 100%;
6109max-width: 160px;
6110}
6111#cart-kit-upsell-mount .kit-stamp-chip__circle {
6112width: 80px;
6113height: 80px;
6114}
6115#cart-kit-upsell-mount .kit-stamp-chip__image {
6116max-width: 62px;
6117max-height: 62px;
6118}
6119} </style> 
6119<script> // ---- GA safety shim (prevents "gtag is not defined") ----
6120window.dataLayer = window.dataLayer || [];
6121window.gtag = window.gtag || function () { window.dataLayer.push(arguments); };
6122/**
6123* Cart Kit Upsell Module — Contract (Do Not Break)
6124* - Runs only on Cart page
6125* - Never blocks checkout interactions
6126* - Never modifies existing cart items (only adds new items)
6127* - Fails silently (no uncaught JS errors if PDP fetch/hydration fails)
6128* - Always has a fallback persona (generic)
6129* - Micro PDP is only used for SKUs that require custom options/uploads
6130*/
6131document.addEventListener('DOMContentLoaded', function () {
6132// ----------------------------------------------------
6133// 0) Render on Cart?
6134// ----------------------------------------------------
6135// Only run SmartCart on the actual cart page.
6136// IMPORTANT: product pages and microPDP iframe pages often contain ".cart" elements (header/minicart),
6137// so the old detection was firing in the iframe and breaking.
6138var isCart =
6139document.body.classList.contains('checkout-cart-index') ||
6140/\/checkout\/cart(\/|$)/.test(window.location.pathname);
6141if (!isCart) return;
6142// ----------------------------------------------------
6143// 1) Mount BELOW .shoppingCartHeader-container
6144// ----------------------------------------------------
6145// Zephyr cart is Tailwind markup (no #shopping-cart-table / .shoppingCartHeader-container).
6146// Mount after the "Shopping Cart" H1 header block; keep legacy anchors as fallback.
6147function ckFindZephyrHeader() {
6148var hs = document.querySelectorAll('h1');
6149for (var i = 0; i < hs.length; i++) {
6150if (/cart/i.test(hs[i].textContent || '')) return hs[i].closest('.mb-6') || hs[i].parentElement;
6151}
6152return null;
6153}
6154var zHeader = ckFindZephyrHeader();
6155var anchor =
6156document.querySelector('.shoppingCartHeader-container') ||
6157zHeader;
6158var fallback =
6159(document.querySelector('#shopping-cart-table') && document.querySelector('#shopping-cart-table').parentElement) ||
6160(zHeader && zHeader.parentElement) ||
6161(document.querySelector('[data-cart-item-id]') && document.querySelector('[data-cart-item-id]').closest('.pb-32, main, #maincontent')) ||
6162document.querySelector('.cart');
6163if (!anchor && !fallback) return;
6164if (document.getElementById('cart-kit-upsell-mount')) return;
6165var mount = document.createElement('div');
6166mount.id = 'cart-kit-upsell-mount';
6167if (anchor && anchor.parentNode) anchor.insertAdjacentElement('afterend', mount);
6168else fallback.appendChild(mount);
6169// ----------------------------------------------------
6170// 2) Query params
6171// ----------------------------------------------------
6172var urlParams = new URLSearchParams(window.location.search);
6173var accentAddedQS = urlParams.get('accentAdded') === '1';
6174var accentInCart = false;
6175var IS_ACCENT_EXPERIENCE = !!(accentAddedQS || accentInCart);
6176var accentAddMoreQS = urlParams.get('accentAddMore') === '1';
6177var microAddedQS = urlParams.get('microAdded'); // productId
6178var microAddedNameQS = urlParams.get('microAddedName'); // product name (best effort)
6179// ===== SmartCart Persona Memory (Device-based) =====
6180const SMARTCART_PERSONA_STORAGE_KEY = "smartcart_persona_v1";
6181// --- Helpers ---
6182function nowMs() { return Date.now(); }
6183function safeJsonParse(str) {
6184try { return JSON.parse(str); } catch { return null; }
6185}
6186// --- Read / Write memory ---
6187function savePersonaToDevice(personaKey) {
6188if (!personaKey) return;
6189const payload = {
6190key: personaKey,
6191setAt: Date.now(),
6192version: 2 // bump version since behavior changed
6193};
6194try {
6195localStorage.setItem(
6196SMARTCART_PERSONA_STORAGE_KEY,
6197JSON.stringify(payload)
6198);
6199} catch (e) {
6200// storage may be blocked; fail silently
6201}
6202}
6203function getPersonaFromDevice() {
6204let raw;
6205try {
6206raw = localStorage.getItem(SMARTCART_PERSONA_STORAGE_KEY);
6207} catch {
6208return null;
6209}
6210if (!raw) return null;
6211const payload = safeJsonParse(raw);
6212if (!payload?.key) return null;
6213return payload;
6214}
6215function clearPersonaFromDevice() {
6216try { localStorage.removeItem(SMARTCART_PERSONA_STORAGE_KEY); } catch { }
6217}
6218// --- GA user_property (set once per session/page lifecycle) ---
6219function setSmartCartUserPropertyOnce(personaKey) {
6220if (!personaKey) return;
6221if (window.__SMARTCART_USER_PROP_SET__) return;
6222window.__SMARTCART_USER_PROP_SET__ = true;
6223// If consent gating exists, you can wrap this (see section 4)
6224if (typeof gtag === "function") {
6225gtag("set", "user_properties", { smartcart_persona: personaKey });
6226}
6227}
6228function onSmartCartMeaningfulEngagement(personaKey) {
6229if (!personaKey) return;
6230// Prevent repeated writes in one session/page lifecycle
6231if (window.__SMARTCART_ENGAGED__) return;
6232window.__SMARTCART_ENGAGED__ = true;
6233// Remember persona on device (UX)
6234savePersonaToDevice(personaKey);
6235// Attribute session in GA (analytics)
6236setSmartCartUserPropertyOnce(personaKey);
6237}
6238// ----------------------------------------------------
6239// Debug exports (safe): expose helpers in console
6240// ----------------------------------------------------
6241if (new URLSearchParams(location.search).get("sc_debug") === "1") {
6242window.__SMARTCART_DEBUG__ = window.__SMARTCART_DEBUG__ || {};
6243window.__SMARTCART_DEBUG__.getPersonaFromDevice = getPersonaFromDevice;
6244window.__SMARTCART_DEBUG__.savePersonaToDevice = savePersonaToDevice;
6245window.__SMARTCART_DEBUG__.setSmartCartUserPropertyOnce = setSmartCartUserPropertyOnce;
6246window.__SMARTCART_DEBUG__.onSmartCartMeaningfulEngagement = onSmartCartMeaningfulEngagement;
6247}
6248// ----------------------------------------------------
6249// 2.5) Hard SKU overrides (micro PDP required)
6250// ----------------------------------------------------
6251var HARD_SKU_OVERRIDES = {
6252"2577": { addMode: "microPdp" },
6253"4419": { addMode: "microPdp" },
6254"10796": { addMode: "microPdp" }
6255};
6256// ----------------------------------------------------
6257// 3) PERSONA DETECTION (v2: Config-driven cart text scan)
6258// ----------------------------------------------------
6259function loadPersonaConfigFromBlock() {
6260var el = document.getElementById('cart-kit-personas');
6261if (!el) return null;
6262try {
6263var raw = (el.textContent || el.innerText || '').trim();
6264if (!raw) return null;
6265return JSON.parse(raw);
6266} catch (e) {
6267console.warn('Cart Kit: Failed to parse persona JSON', e);
6268return null;
6269}
6270}
6271function buildCartSignals() {
6272// Adjust selectors to your cart markup if needed
6273var nameNodes = document.querySelectorAll('.product-name a, .product-name, .cart.item .product-item-name a, .cart.item .product-item-name');
6274var optNodes = document.querySelectorAll('.item-options, dl.item-options, .product.options, .cart.item .item-options');
6275var names = [];
6276for (var i = 0; i < nameNodes.length; i++) {
6277var t = (nameNodes[i].textContent || '').trim();
6278if (t) names.push(t);
6279}
6280var opts = [];
6281for (var j = 0; j < optNodes.length; j++) {
6282var o = (optNodes[j].textContent || '').trim();
6283if (o) opts.push(o);
6284}
6285var titleText = names.join(' | ');
6286var fullText = (names.concat(opts)).join(' | ');
6287return { titleText: titleText, fullText: fullText };
6288}
6289// Product titles only (recommended for persona scoring)
6290function getCartTitleTextLower() {
6291try {
6292// Magento cart on SimplyStamps: h2.cartProductName a
6293var nameNodes = document.querySelectorAll(
6294'[data-cart-item-id] h3 a,' +
6295'[data-cart-item-id] h3,' +
6296'#shopping-cart-table h2.cartProductName a,' +
6297'#shopping-cart-table h2.cartProductName,' +
6298'#shopping-cart-table .product-name a,' +
6299'#shopping-cart-table .product-name,' +
6300'.product-item-name a,' +
6301'.product-item-name'
6302);
6303var names = [];
6304for (var i = 0; i < nameNodes.length; i++) {
6305var t = (nameNodes[i].textContent || '').trim();
6306if (t) names.push(t);
6307}
6308return names.join(' | ').toLowerCase();
6309} catch (e) {
6310return '';
6311}
6312}
6313// Full cart text (titles + options). Keep for product scoring/debug if desired.
6314function getCartFullTextLower() {
6315try {
6316var titleLower = getCartTitleTextLower();
6317var optNodes = document.querySelectorAll(
6318'[data-cart-item-id] .flex-wrap,' +
6319'#shopping-cart-table .item-options,' +
6320'#shopping-cart-table dl.item-options,' +
6321'.item-options,' +
6322'dl.item-options'
6323);
6324var opts = [];
6325for (var j = 0; j < optNodes.length; j++) {
6326var o = (optNodes[j].textContent || '').trim();
6327if (o) opts.push(o);
6328}
6329// titleLower is already lowercase
6330var full = (titleLower ? [titleLower] : []).concat(opts.join(' | ').toLowerCase()).join(' | ');
6331return full.toLowerCase();
6332} catch (e) {
6333return '';
6334}
6335}
6336// ----------------------------------------------------
6337// Cart product path signals (reliable for "already in cart" suppression)
6338// ----------------------------------------------------
6339function normalizeUrlPathForCompare(url) {
6340try {
6341var u = new URL(url, window.location.origin);
6342var p = (u.pathname || '');
6343// normalize: drop .html and trailing slashes
6344p = p.replace(/\.html$/i, '').replace(/\/+$/, '');
6345return String(p || '').toLowerCase();
6346} catch (e) {
6347try {
6348var s = String(url || '').split('#')[0].split('?')[0];
6349s = s.replace(/\.html$/i, '').replace(/\/+$/, '');
6350return String(s || '').toLowerCase();
6351} catch (e2) { return ''; }
6352}
6353}
6354function getCartProductPathSet() {
6355var set = Object.create(null);
6356try {
6357var nodes = document.querySelectorAll(
6358'[data-cart-item-id] h3 a,' +
6359'#shopping-cart-table .product-item-name a,' +
6360'#shopping-cart-table a.product-item-link,' +
6361'.cart.table-wrapper .product-item-name a,' +
6362'.cart.table-wrapper a.product-item-link,' +
6363'.cart-container .product-item-name a,' +
6364'.cart-container a.product-item-link'
6365);
6366for (var i = 0; i < nodes.length; i++) {
6367var href = nodes[i] && (nodes[i].getAttribute('href') || nodes[i].href);
6368if (!href) continue;
6369var pth = normalizeUrlPathForCompare(href);
6370if (!pth) continue;
6371// Exclude cart actions/downloads that appear in the cart table
6372if (/^\/checkout\//i.test(pth)) continue;
6373if (/^\/sales\//i.test(pth)) continue;
6374if (/^\/download\//i.test(pth)) continue;
6375set[pth] = true;
6376}
6377} catch (e) { }
6378return set;
6379}
6380// Zephyr: exact product-id match from cart cards (data-product-id = Magento entity id)
6381function getCartProductIdSet() {
6382var set = Object.create(null);
6383try {
6384var nodes = document.querySelectorAll('[data-product-id]');
6385for (var i = 0; i < nodes.length; i++) {
6386var pid = String(nodes[i].getAttribute('data-product-id') || '').trim();
6387if (pid) set[pid] = true;
6388}
6389} catch (e) { }
6390return set;
6391}
6392// Compatibility alias used by existing scoring code
6393function getCartTextLower() {
6394// Persona scoring should use titles (cleanest signal)
6395return getCartTitleTextLower();
6396// If you ever want “titles + options” instead, swap to:
6397// return getCartFullTextLower();
6398}
6399// Spreadsheet-driven personas (local environment)
6400var PERSONAS_FALLBACK = [
6401{
6402key: 'brand_champions',
6403priority: 80,
6404match: [
6405'business stamp', 'logo stamp', 'logo address stamp', 'custom logo stamp', 'company stamp', 'business address stamp',
6406'return address stamp business', 'self inking logo stamp', 'office branding', 'small business supplies',
6407'custom business stamps', 'bulk stamps'
6408],
6409headerTitle: 'Brand Essentials: finish your packaging strong.',
6410stampLabel: 'Business add-ons (popular picks)',
6411inkLabel: 'Choose an ink color for consistent branding',
6412tooltip: 'These add-ons help your brand look polished across packaging, mailers, and customer communications.',
6413defaultPrimaryIndex: 0,
6414tileSubtext: 'Popular with small businesses',
6415items: [
6416{ productId: '2577', productUrl: 'https://www.simplystamps.com/qr-code-stamp-1-5-8-x-1-5-8', sortOrder: 2, addMode: 'microPdp' },
6417{ productId: '4419', productUrl: 'https://www.simplystamps.com/medium-custom-logo-stamp', sortOrder: 1, addMode: 'microPdp' },
6418{ productId: '10796', productUrl: 'https://www.simplystamps.com/mini-square-logo-stamp', sortOrder: 3, addMode: 'microPdp' }
6419]
6420},
6421{
6422key: 'home_sweet_stampers',
6423priority: 70,
6424match: [
6425'return address stamp', 'address stamp', 'custom address stamp', 'family address stamp', 'wedding address stamp',
6426'holiday address stamp', 'mail stamp', 'envelope stamp', 'home address stamp', 'personalized address stamp'
6427],
6428headerTitle: 'Mail Essentials: everything you need for clean, easy stamping.',
6429stampLabel: 'Helpful add-ons for everyday mail',
6430inkLabel: 'Choose an ink color',
6431tooltip: 'Perfect for cards, envelopes, and seasonal mail — these extras make stamping easier and mess-free.',
6432defaultPrimaryIndex: 0,
6433tileSubtext: 'Great for everyday mail',
6434items: [
6435{ productId: '5654', productUrl: 'https://www.simplystamps.com/merry-christmas-craft-rubber-stamp', sortOrder: 1 },
6436{ productId: '6126', productUrl: 'https://www.simplystamps.com/happy-birthday-smiley-craft-stamp', sortOrder: 2 },
6437{ productId: '5529', productUrl: 'https://www.simplystamps.com/live-well-rubber-stamp', sortOrder: 3 }
6438]
6439},
6440{
6441key: 'office_optimizers',
6442priority: 85,
6443match: [
6444'signature', 'received stamp', 'approved stamp', 'paid stamp', 'office stamp', 'date stamp', 'custom office stamp',
6445'self inking stamp', 'pre inked stamp', 'inspection stamp', 'file stamp', 'reorder stamp', 'replacement ink'
6446],
6447headerTitle: 'Office Essentials: stay stocked and avoid reorders.',
6448stampLabel: 'Office add-ons (frequently reordered)',
6449inkLabel: 'Ink selection for daily office use',
6450tooltip: 'These supplies help keep workflows moving without interruptions or last-minute supply runs.',
6451defaultPrimaryIndex: 0,
6452tileSubtext: 'Frequently reordered',
6453items: [
6454{ productId: '6560', productUrl: 'https://www.simplystamps.com/trodat-id-protection-stamp', sortOrder: 1 },
6455{ productId: '2420', productUrl: 'https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip', sortOrder: 2 }
6456]
6457},
6458{
6459key: 'notary_ninjas',
6460priority: 100,
6461match: [
6462'notary public', 'notary', 'notary stamp', 'notary seal', 'notary embosser', 'state notary stamp', 'notary supplies',
6463'notary kit', 'notary replacement stamp', 'official notary seal', 'notary expiration stamp',
6464'jurat stamp', 'acknowledgment stamp'
6465],
6466headerTitle: 'Notary Essentials: add backup ink & supplies before checkout.',
6467stampLabel: 'Notary add-ons (quick picks)',
6468inkLabel: 'Ink choice (recommended for notary work)',
6469tooltip: 'These items help ensure clean, compliant notarizations and prevent downtime if ink runs out.',
6470defaultPrimaryIndex: 0,
6471tileSubtext: 'Recommended for notary work',
6472items: [
6473{ productId: '11712', productUrl: 'https://www.simplystamps.com/the-complete-notary-records-book', sortOrder: 1 },
6474{ productId: '6524', productUrl: 'https://www.simplystamps.com/embossing-adhesive-foil', sortOrder: 2 },
6475{ productId: '7986', productUrl: 'https://www.simplystamps.com/seal-impression-inker', sortOrder: 3 },
6476{ productId: '13387', productUrl: 'https://www.simplystamps.com/embosser-vinyl-black-pouch', sortOrder: 4 }
6477]
6478},
6479{
6480key: 'classroom_heroes',
6481priority: 60,
6482match: [
6483'teacher stamp', 'teacher ', 'grading stamp', 'school stamp', 'custom teacher stamp', 'classroom stamp',
6484'reward stamp', 'student feedback stamp', 'motivational stamp', 'education stamp', 'teacher name stamp'
6485],
6486headerTitle: 'Classroom Essentials: be ready for every lesson.',
6487stampLabel: 'Teacher add-ons (classroom favorites)',
6488inkLabel: 'Choose an ink color students will love',
6489tooltip: 'Extra ink and supplies help keep grading, rewards, and feedback flowing all year.',
6490defaultPrimaryIndex: 0,
6491tileSubtext: 'Classroom favorites',
6492items: [
6493{ productId: '19215', productUrl: 'https://www.simplystamps.com/stacked-teacher-stamps-set-of-6', sortOrder: 1 },
6494{ productId: '10965', productUrl: 'https://www.simplystamps.com/no-name-no-fame-owl-teacher-stamp', sortOrder: 2 },
6495{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 3 }
6496]
6497},
6498{
6499key: 'craft_crowd',
6500priority: 50,
6501match: [
6502'craft stamp', 'handmade stamp', 'custom rubber stamp', 'craft rubber stamp', 'logo stamp small business', 'etsy stamp',
6503'thank you stamp', 'packaging stamp', 'product label stamp', 'arts and crafts stamp', 'creative stamp'
6504],
6505headerTitle: 'Creative Essentials: finish your projects with ease.',
6506stampLabel: 'Craft add-ons (popular with makers)',
6507inkLabel: 'Choose an ink color to match your style',
6508tooltip: 'These supplies are perfect for handmade goods, packaging, and creative projects.',
6509allowedInk: ['black', 'blue', 'red', 'green', 'purple'],
6510defaultPrimaryIndex: 0,
6511tileSubtext: 'Popular with makers',
6512items: [
6513{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 1 },
6514{ productId: '10464', productUrl: 'https://www.simplystamps.com/gem-cluster-stamp', sortOrder: 2 },
6515{ productId: '9768', productUrl: 'https://www.simplystamps.com/seamless-eucalyptus-stamp', sortOrder: 3 }
6516]
6517},
6518// Fallback (Accent Stamp grid)
6519{
6520key: 'generic',
6521priority: 0,
6522match: [],
6523headerTitle: 'Choose your favorite accent stamp below and add to cart.',
6524stampLabel: 'Choose one or many Accent Stamp Designs:',
6525inkLabel: 'Would you like an Ink Pad? (Needed to make impression)',
6526tooltip: 'Stamp impression size: approx 1.75" × 0.67".',
6527allowedInk: ['black', 'blue', 'red', 'green', 'purple', 'none'],
6528defaultPrimaryIndex: 0,
6529tileSubtext: '',
6530items: []
6531}
6532];
6533function normalizePersonasConfigToArray(cfg) {
6534// Expect cfg like: { version: "...", personas: { key: { ... }, ... } }
6535if (!cfg || !cfg.personas || typeof cfg.personas !== 'object') return null;
6536var out = [];
6537Object.keys(cfg.personas).forEach(function (key) {
6538var p = cfg.personas[key] || {};
6539// Map JSON schema → existing runtime schema
6540// Your runtime expects: key, priority, match, headerTitle, stampLabel, inkLabel, tooltip, allowedInk, defaultPrimaryIndex, tileSubtext, items[]
6541var persona = {
6542key: key,
6543priority: (typeof p.priority === 'number' ? p.priority : 0),
6544match: Array.isArray(p.match_keywords) ? p.match_keywords : [],
6545headerTitle: p.header_title || '',
6546stampLabel: p.stamp_label || '',
6547inkLabel: p.ink_label || '',
6548tooltip: p.tooltip_text || '',
6549allowedInk: Array.isArray(p.allowed_ink_colors) ? p.allowed_ink_colors : [],
6550defaultPrimaryIndex: (typeof p.default_primary_index === 'number' ? p.default_primary_index : 0),
6551tileSubtext: p.tile_subtext || '',
6552items: []
6553};
6554// Products → items (what buildGrid uses)
6555if (Array.isArray(p.products)) {
6556persona.items = p.products.map(function (prod) {
6557return {
6558productId: String(prod.product_id || '').trim(),
6559productUrl: prod.product_url || '',
6560sortOrder: (typeof prod.sort_order === 'number' ? prod.sort_order : 9999),
6561tileSubtext: prod.tile_subtext || '',
6562addMode: prod.addMode || prod.add_mode || undefined,
6563//  NEW (traits)
6564role: prod.role || 'helper',
6565friction: prod.friction || 'low',
6566experimental: (typeof prod.experimental === 'boolean' ? prod.experimental : false),
6567//  NEW (product-level keywords for scoring)
6568matchKeywords: Array.isArray(prod.match_keywords) ? prod.match_keywords : []
6569};
6570}).filter(function (it) { return !!it.productId || !!it.productUrl; });
6571}
6572out.push(persona);
6573});
6574return out;
6575}
6576var PERSONA_CONFIG = loadPersonaConfigFromBlock();
6577// ----------------------------------------------------
6578// 4) Now set PERSONAS like this (config → normalized array → fallback)
6579// ----------------------------------------------------
6580var PERSONAS = (function () {
6581// Try config block first
6582var fromConfig = normalizePersonasConfigToArray(PERSONA_CONFIG);
6583// Use config if it produced at least 1 persona, otherwise fallback
6584var arr = (Array.isArray(fromConfig) && fromConfig.length) ? fromConfig : PERSONAS_FALLBACK.slice();
6585// Ensure we ALWAYS have a generic fallback persona
6586var hasGeneric = arr.some(function (p) { return p && p.key === 'generic'; });
6587if (!hasGeneric) {
6588var genericFallback = PERSONAS_FALLBACK.find(function (p) { return p.key === 'generic'; });
6589if (genericFallback) arr.push(genericFallback);
6590}
6591// Normalize a few expected fields defensively
6592arr.forEach(function (p) {
6593if (!p) return;
6594if (!Array.isArray(p.match)) p.match = [];
6595if (!Array.isArray(p.items)) p.items = [];
6596if (!Array.isArray(p.allowedInk) || !p.allowedInk.length) {
6597// keep your existing default behavior (includes 'none')
6598p.allowedInk = ['black', 'blue', 'red', 'green', 'purple', 'none'];
6599}
6600});
6601return arr;
6602})();
6603PERSONAS.forEach(function (p) {
6604if (Array.isArray(p.items)) p.items.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); });
6605});
6606PERSONAS.sort(function (a, b) {
6607if (a.key === 'generic') return 1;
6608if (b.key === 'generic') return -1;
6609return (b.priority || 0) - (a.priority || 0);
6610});
6611function setLoadingCursor(on) {
6612document.body.classList.toggle('ck-loading', !!on);
6613}
6614// ----------------------------------------------------
6615// 3.9) SCORING (persona + product)
6616// ----------------------------------------------------
6617function keywordMatchScore(keywords, haystackLower) {
6618if (!Array.isArray(keywords) || !keywords.length) return { score: 0, hits: 0, matched: [] };
6619var score = 0, hits = 0, matched = [];
6620for (var i = 0; i < keywords.length; i++) {
6621var k = String(keywords[i] || '').toLowerCase().trim();
6622if (!k) continue;
6623// Exact substring match
6624if (haystackLower.indexOf(k) > -1) {
6625score += (k.indexOf(' ') > -1 ? 10 : 4);
6626hits += 1;
6627matched.push(k);
6628continue;
6629}
6630// Phrase fallback: all tokens appear anywhere
6631if (k.indexOf(' ') > -1) {
6632var parts = k.split(/\s+/).filter(Boolean);
6633if (parts.length >= 2) {
6634var all = true;
6635for (var p = 0; p < parts.length; p++) {
6636if (haystackLower.indexOf(parts[p]) === -1) { all = false; break; }
6637}
6638if (all) {
6639score += 6;
6640hits += 1;
6641matched.push(k + ' (tokens)');
6642}
6643}
6644}
6645}
6646return { score: score, hits: hits, matched: matched };
6647}
6648function scoreKeywordHits(keywords, haystackLower) {
6649return keywordMatchScore(keywords, haystackLower).score;
6650}
6651function scorePersonaFromCart(personaObj, cartTextLower) {
6652// Persona match keywords are in personaObj.match[]
6653var base = scoreKeywordHits(personaObj.match, cartTextLower);
6654// Priority is a tie-breaker, not a primary driver.
6655// Still, give a tiny nudge so higher-priority personas win ties.
6656var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0;
6657return base + priorityNudge;
6658}
6659function detectPersonaKey() {
6660var cartTextLower = getCartTitleTextLower();
6661var bestKey = 'generic';
6662var bestScore = -1;
6663for (var i = 0; i < PERSONAS.length; i++) {
6664var p = PERSONAS[i];
6665if (!p || p.key === 'generic') continue;
6666var s = scorePersonaFromCart(p, cartTextLower);
6667if (s > bestScore) {
6668bestScore = s;
6669bestKey = p.key;
6670}
6671}
6672// If nobody matched anything, fall back
6673if (bestScore <= 0) return 'generic';
6674return bestKey;
6675}
6676function getPersonaByKey(key) {
6677for (var i = 0; i < PERSONAS.length; i++) {
6678if (PERSONAS[i].key === key) return PERSONAS[i];
6679}
6680return PERSONAS[PERSONAS.length - 1];
6681}
6682var activePersonaKey = detectPersonaKey();
6683var persona = getPersonaByKey(activePersonaKey);
6684window.__CK = {
6685personas: PERSONAS,
6686activePersonaKey: activePersonaKey,
6687persona: persona
6688};
6689var useTiles = Array.isArray(persona.items) && persona.items.length > 0;
6690// Decide which persona to use for this render
6691function choosePersona({ detectedKey, detectedConfidence = 0, confidenceThreshold = 0.75 }) {
6692const stored = getPersonaFromDevice(); // { key, setAt... } or null
6693// Strong real-time signal wins
6694if (detectedKey && detectedConfidence >= confidenceThreshold) {
6695return { key: detectedKey, source: "detected_strong" };
6696}
6697// Otherwise fall back to remembered persona if available
6698if (stored?.key) {
6699return { key: stored.key, source: "stored" };
6700}
6701// Otherwise use detected (weak) or default
6702if (detectedKey) return { key: detectedKey, source: "detected_weak" };
6703return { key: "default", source: "default" };
6704}
6705// ----------------------------------------------------
6706// 3b) Analytics helper (optional)
6707// ----------------------------------------------------
6708function pushUpsellEvent(eventName, payload) {
6709payload = payload || {};
6710// ----------------------------------------------------
6711// Enrich payload persona context so GA4 can segment
6712// ----------------------------------------------------
6713try {
6714var __ckPersonaKey = (typeof activePersonaKey !== 'undefined' && activePersonaKey) ? String(activePersonaKey) : 'generic';
6715// Prefer persona.headerTitle as a human label, fall back to key
6716var __ckPersonaLabel = (typeof persona !== 'undefined' && persona && (persona.headerTitle || persona.key))
6717? String(persona.headerTitle || persona.key)
6718: __ckPersonaKey;
6719// Only set defaults if caller didn't provide them
6720if (typeof payload.persona_key === 'undefined') payload.persona_key = __ckPersonaKey;
6721if (typeof payload.persona_label === 'undefined') payload.persona_label = __ckPersonaLabel;
6722if (typeof payload.upsell_variant === 'undefined') payload.upsell_variant = (typeof useTiles !== 'undefined' && useTiles) ? 'tiles' : 'stamps';
6723if (typeof payload.page_path === 'undefined') payload.page_path = (window.location && window.location.pathname) ? window.location.pathname : '';
6724} catch (eCtx) { }
6725// ----------------------------------------------------
6726// Option 4: Meaningful engagement → remember persona + set GA user_property
6727// Trigger only on specific SmartCart events (no impressions)
6728// ----------------------------------------------------
6729try {
6730var isMeaningful =
6731eventName === 'SmartCart_upsell_tile_selected' ||
6732eventName === 'SmartCart_upsell_tile_click' ||
6733eventName === 'SmartCart_upsell_tile_add' ||
6734eventName === 'SmartCart_upsell_tile_added' ||
6735eventName === 'SmartCart_upsell_added';
6736if (isMeaningful) {
6737var pk =
6738(payload && (payload.persona_key || payload.personaKey)) ||
6739(typeof activePersonaKey !== 'undefined' && activePersonaKey) ||
6740(window.__CK && window.__CK.activePersonaKey);
6741if (pk) onSmartCartMeaningfulEngagement(String(pk));
6742}
6743} catch (e) { }
6744// Always push to dataLayer (never depend on gtag existing)
6745try {
6746window.dataLayer = window.dataLayer || [];
6747window.dataLayer.push(Object.assign({ event: eventName }, payload));
6748} catch (e) { }
6749// Optional: also send to gtag if it exists (but NEVER crash if missing)
6750try {
6751if (typeof window.gtag === 'function') {
6752window.gtag('event', eventName, payload);
6753}
6754} catch (e) { }
6755// Mirror to GA4 standard ecommerce (see CK_GA4 block below). Never
6756// let an analytics mirror break the module.
6757try { ckGaMirror(eventName, payload); } catch (eGa) { }
6758}
6759// ----------------------------------------------------
6760// 3b-ii) GA4 standard ecommerce mirror   [added 2026-09-15]
6761// ----------------------------------------------------
6762// WHY: the SmartCart_* custom events carry product ids only inside a
6763// nested array (tiles_shown), and GA4 cannot report on nested arrays.
6764// The property has no custom dimension for productId, so per-product
6765// performance has been invisible since launch. GA4's `item_id` is a
6766// BUILT-IN dimension, so emitting standard ecommerce events gives us a
6767// per-product view -> select -> add funnel with zero custom-dimension
6768// setup. These are emitted ALONGSIDE the custom events, never instead.
6769//
6770// dataLayer only -- GTM is confirmed live on this page and already
6771// processing SmartCart pushes. We do not call gtag() here because line
6772// ~1015 stubs window.gtag when it is absent, so a gtag() call would be
6773// an unconditional duplicate push.
6774var CK_GA4 = {
6775EMIT: true,             // master kill switch
6776EMIT_MICRO_ADD: true,   // set false if the micro-PDP iframe fires its own add_to_cart
6777LIST_PREFIX: 'SmartCart',
6778HYDRATE_POLLS: 8,       // 8 x 300ms = up to ~2.4s waiting for tile names
6779HYDRATE_POLL_MS: 300,
6780// SKUs for products that never go through parsePdpDoc, so nothing can
6781// hydrate a SKU for them: the ink parent (added by id + options) and
6782// the six accent stamps used by the non-tile 'generic' persona, where
6783// buildGrid returns before any PDP fetch. Without these, every add in
6784// the generic variant -- the bucket every unmatched cart falls into --
6785// would ship an entity id and be unjoinable to purchase.
6786// All verified against prod 2026-09-16. Re-check if SKUs change.
6787STATIC_SKUS: {
6788'3631': '1009260',   // Ideal Ink Pad for Traditional Wood Handle
6789'23105': '1027259',  // Delicate Flower envelope accent
6790'23106': '1027260',  // Simple botanical branches
6791'23107': '1027261',  // (accent)
6792'23108': '1027262',  // Elegant botanical branch
6793'23110': '1027264',  // Hearts through arrow
6794'23113': '1027267'   // Outlined heart
6795}
6796};
6797// A SKU we are willing to put in item_id. Magento SKUs are short and
6798// token-like; a scrape that picked up a wrapper element's textContent
6799// ('SKU:\n  1012837') or a sentinel ('N/A') must not ship -- and must not
6800// be labelled id_source:'sku', which would hide it from the very
6801// diagnostic added to catch it.
6802function ckGaSkuOk(v) {
6803var s = ckGaValidId(v);
6804if (!s) return '';
6805if (s.length > 40) return '';
6806return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(s) ? s : '';
6807}
6808// ckGaItem records, per built item, whether item_id is a real SKU or the
6809// entity-id fallback. ckGaPush reads and clears it -- the whole mirror runs
6810// synchronously inside one ckGaMirror call, so there is no interleaving.
6811var CK_GA4_IDSRC = [];
6812// Analytics-only mirror of tile metadata for products that entered a slot
6813// after buildGrid (swaps). Read by ckGaItem, by nothing else -- writing
6814// these into TILE_ITEM_BY_ID would change add-flow routing.
6815var CK_GA_TILE_META = Object.create(null);
6816// Reject ids that are not real product ids. TILE_ITEMS tolerates entries
6817// with no productId, and String(undefined) === 'undefined' would ship a
6818// junk item_id that also never hydrates (stalling the impression poll).
6819function ckGaValidId(v) {
6820if (v === null || typeof v === 'undefined') return '';
6821var s = String(v).trim();
6822if (!s || s === 'undefined' || s === 'null' || s === 'NaN') return '';
6823return s;
6824}
6825function ckGaListId() {
6826try {
6827return String((typeof activePersonaKey !== 'undefined' && activePersonaKey) || 'generic');
6828} catch (e) { return 'generic'; }
6829}
6830function ckGaListName() {
6831var label = ckGaListId();
6832try {
6833if (typeof persona !== 'undefined' && persona && persona.headerTitle) {
6834label = String(persona.headerTitle);
6835}
6836} catch (e) { }
6837// GA4 truncates item_list_name at 100 chars; header_title is
6838// CMS-editable, so cap it here rather than letting GA4 split rows.
6839return (CK_GA4.LIST_PREFIX + ' - ' + label).slice(0, 100);
6840}
6841function ckGaPrice(v) {
6842if (v === null || typeof v === 'undefined') return undefined;
6843var n = parseFloat(String(v).replace(/[^0-9.\-]/g, ''));
6844return isFinite(n) ? Math.round(n * 100) / 100 : undefined;
6845}
6846// Build one GA4 item object for a SmartCart product id.
6847function ckGaItem(pid, index, listNameOverride) {
6848var id = String(pid);
6849var raw = null, hyd = null;
6850try {
6851if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null;
6852if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null;
6853} catch (e1) { }
6854try {
6855if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd = NOTARY_HYDRATED.get(id) || null;
6856} catch (e2) { }
6857// SKU is the id GA4 must see (see parsePdpDoc). Fall back to the
6858// entity id only when hydration has not produced a SKU yet -- that is
6859// recoverable and countable via the id_source param on the event,
6860// whereas dropping the item outright would lose the conversion.
6861var gaSku = ckGaSkuOk(hyd && hyd.sku) ||
6862ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id]);
6863var gaId = gaSku || id;
6864try { CK_GA4_IDSRC.push(gaSku ? 'sku' : 'entity'); } catch (eSrc0) { }
6865var item = {
6866item_id: gaId,
6867item_brand: 'SimplyStamps',
6868item_category: 'SmartCart Upsell',
6869item_list_id: ckGaListId(),
6870// Item-scoped item_list_name is what GA4 item reports actually
6871// use, so an event-level override has to reach the items too.
6872item_list_name: listNameOverride || ckGaListName(),
6873quantity: 1
6874};
6875// NEVER send a placeholder name. Tiles hydrate asynchronously, so a
6876// 'Product 12345' filler would permanently split the item_name
6877// dimension against the real name sent by later events. An absent
6878// dimension is recoverable; a wrong one is not.
6879var name = (hyd && hyd.name) || (raw && (raw.title || raw.name)) || '';
6880if (name) item.item_name = String(name);
6881if (typeof index === 'number' && index > 0) item.index = index;
6882// Price: hydrated PDP price for tiles; the flat accent-stamp price
6883// in the non-tile variant, where nothing hydrates at all.
6884var price = ckGaPrice(hyd ? hyd.price : null);
6885if (typeof price !== 'number') {
6886try {
6887if (typeof useTiles !== 'undefined' && !useTiles && typeof STAMP_UNIT_PRICE === 'number') {
6888price = STAMP_UNIT_PRICE;
6889}
6890} catch (eP) { }
6891}
6892if (typeof price === 'number') item.price = price;
6893// Slice-able strategy attributes, so GA4 can answer 'do experimental
6894// tiles convert?' and 'does friction cost us adds?' without new dims.
6895// Only set what the config actually declares -- defaulting friction to
6896// 'low' would read back as 'low friction converts fine' off data that
6897// really means 'we did not know'.
6898if (raw) {
6899if (raw.role) item.item_variant = String(raw.role);
6900if (raw.friction) item.item_category2 = String(raw.friction);
6901if (typeof raw.experimental !== 'undefined') {
6902item.item_category3 = raw.experimental ? 'experimental' : 'core';
6903}
6904}
6905return item;
6906}
6907// startIndex === null means 'do not emit index'. Only view_item_list
6908// knows a real display position; on select_item / add_to_cart an index
6909// would be fabricated (always 1, or add-order), and a fabricated
6910// position silently answers 'which slot converts?' wrong.
6911function ckGaItemsFor(ids, startIndex, listNameOverride) {
6912var out = [];
6913var useIdx = (startIndex !== null && typeof startIndex !== 'undefined');
6914try {
6915(ids || []).forEach(function (pid) {
6916var id = ckGaValidId(pid);
6917if (!id) return;
6918out.push(ckGaItem(id, useIdx ? (startIndex + out.length + 1) : null, listNameOverride));
6919});
6920} catch (e) { }
6921return out;
6922}
6923// True once we can give this product the id GA4 needs. Gating the deferred
6924// impression on the NAME alone was not enough: ckRepaintCardAs writes a
6925// CK_TILE_FALLBACKS title into TILE_ITEM_BY_ID synchronously, before it even
6926// starts the PDP fetch, so a swapped-in fallback tile looked 'resolved' with
6927// no SKU yet -- view_item_list would ship its entity id while the shopper's
6928// later select_item shipped the real SKU. Same product, two item_ids, one
6929// session: exactly the split this is all meant to remove.
6930function ckGaHasSku(pid) {
6931var id = String(pid);
6932if (ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id])) return true;
6933try {
6934if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) {
6935var h = NOTARY_HYDRATED.get(id);
6936if (h && ckGaSkuOk(h.sku)) return true;
6937}
6938} catch (e) { }
6939return false;
6940}
6941// True once a product has a real, non-placeholder name available.
6942function ckGaHasName(pid) {
6943var id = String(pid), hyd = null, raw = null;
6944try { if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd = 
6944NOTARY_HYDRATED.get(id) || null; } catch (e1) { }
6945try {
6946if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null;
6947if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null;
6948} catch (e2) { }
6949return !!((hyd && hyd.name) || (raw && (raw.title || raw.name)));
6950}
6951// Ink pads are options on a single parent SKU, not tiles -- model them as
6952// variants of that parent so ink attach is measurable too.
6953function ckGaInkItems(colorKeys, startIndex) {
6954var out = [];
6955try {
6956// The ink parent never hydrates through parsePdpDoc (it is added
6957// by id + options, not from a tile), so its SKU is pinned here.
6958var inkEnt = (typeof INK_PARENT_ID !== 'undefined' && INK_PARENT_ID) ? String(INK_PARENT_ID) : '';
6959var inkSku = ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[inkEnt]);
6960var pid = inkSku || inkEnt || 'ink-pad';
6961(colorKeys || []).forEach(function (ck, i) {
6962var row = {
6963item_id: pid,
6964item_name: 'Ink Pad',
6965item_brand: 'SimplyStamps',
6966item_category: 'SmartCart Upsell',
6967item_category3: 'core',
6968item_variant: String(ck),
6969item_list_id: ckGaListId(),
6970item_list_name: ckGaListName(),
6971quantity: 1
6972};
6973// Ink is a flat-price option the module already prices itself.
6974// Without this, an ink-only add -- the module's core ask --
6975// reports add_to_cart with no value at all.
6976if (typeof INK_PRICE === 'number') row.price = INK_PRICE;
6977if (startIndex !== null && typeof startIndex !== 'undefined') row.index = startIndex + i + 1;
6978// Report ink's id source too, or a mixed add reads as 'entity'
6979// and an analyst quarantining that label throws away good rows.
6980try { CK_GA4_IDSRC.push(inkSku ? 'sku' : 'entity'); } catch (eSrc1) { }
6981out.push(row);
6982});
6983} catch (e) { }
6984return out;
6985}
6986function ckGaValue(items) {
6987var v = 0, any = false;
6988try {
6989(items || []).forEach(function (it) {
6990if (typeof it.price === 'number') { v += it.price * (it.quantity || 1); any = true; }
6991});
6992} catch (e) { }
6993return any ? Math.round(v * 100) / 100 : undefined;
6994}
6995function ckGaPush(eventName, ecommerce) {
6996// Drain the id-source buffer FIRST, above every early return -- a
6997// stale entry would mislabel the next event.
6998var idsrc = CK_GA4_IDSRC;
6999CK_GA4_IDSRC = [];
7000if (!CK_GA4.EMIT) return;
7001if (!ecommerce || !Array.isArray(ecommerce.items) || !ecommerce.items.length) return;
7002try {
7003window.dataLayer = window.dataLayer || [];
7004// Clear the previous ecommerce object first so params never bleed
7005// between events (standard GTM ecommerce practice).
7006window.dataLayer.push({ ecommerce: null });
7007// id_source tells the analyst whether these item_ids are real SKUs
7008// (joinable to purchase) or entity-id fallbacks from a tile that
7009// had not hydrated yet. Ink items are always pinned to a real SKU.
7010var srcTag = 'sku';
7011try {
7012if (idsrc.length) {
7013var nEnt = idsrc.filter(function (x) { return x === 'entity'; }).length;
7014srcTag = (nEnt === 0) ? 'sku' : (nEnt === idsrc.length ? 'entity' : 'mixed');
7015}
7016} catch (eSrc) { }
7017window.dataLayer.push({
7018event: eventName,
7019smartcart: true,
7020persona_key: ckGaListId(),
7021id_source: srcTag,
7022ecommerce: ecommerce
7023});
7024} catch (e) { }
7025}
7026// Single mirror point: called for every pushUpsellEvent. Adding a new
7027// GA4 event means adding a case here, not touching call sites.
7028function ckGaMirror(eventName, payload) {
7029if (!CK_GA4.EMIT) return;
7030payload = payload || {};
7031var items, val, ec;
7032if (eventName === 'SmartCart_upsell_impression') {
7033// null = read the live grid at fire time. See
7034// ckGaFireListWhenHydrated for why that beats the build-time list.
7035ckGaFireListWhenHydrated(null);
7036return;
7037}
7038// Three distinct code paths end in a real selection. _select_fallback
7039// fires when the PDP prefetch failed but the tile WAS still selected,
7040// so omitting it would under-report select_item in proportion to our
7041// own fetch error rate. accent_selected is the non-tile variant.
7042if (eventName === 'SmartCart_upsell_tile_select_fallback' && payload.selected === false) {
7043return; // that path fires on deselect too
7044}
7045if (eventName === 'SmartCart_upsell_tile_selected' ||
7046eventName === 'SmartCart_upsell_tile_select_fallback' ||
7047eventName === 'SmartCart_upsell_accent_selected') {
7048var selId = payload.productId || payload.product_id || payload.stamp_id;
7049items = ckGaItemsFor([selId], null);
7050ckGaPush('select_item', {
7051item_list_id: ckGaListId(),
7052item_list_name: ckGaListName(),
7053items: items
7054});
7055return;
7056}
7057if (eventName === 'SmartCart_upsell_added') {
7058items = ckGaItemsFor(payload.product_ids, null)
7059.concat(ckGaInkItems(payload.ink_colors, null));
7060val = ckGaValue(items);
7061ec = { currency: 'USD', items: items };
7062if (typeof val === 'number') ec.value = val;
7063ckGaPush('add_to_cart', ec);
7064return;
7065}
7066if (eventName === 'SmartCart_upsell_micro_added') {
7067if (!CK_GA4.EMIT_MICRO_ADD) return;
7068items = ckGaItemsFor(payload.product_ids, null);
7069val = ckGaValue(items);
7070ec = { currency: 'USD', items: items };
7071if (typeof val === 'number') ec.value = val;
7072ckGaPush('add_to_cart', ec);
7073return;
7074}
7075}
7076// Read the product ids actually rendered in the grid right now.
7077function ckGaVisibleTileIds() {
7078var out = [];
7079try {
7080if (!stampGrid) return out;
7081var cards = stampGrid.querySelectorAll('[data-kit-primary]');
7082for (var i = 0; i < cards.length; i++) {
7083var id = ckGaValidId(cards[i].getAttribute('data-kit-primary'));
7084if (id && out.indexOf(id) === -1) out.push(id);
7085}
7086} catch (e) { }
7087return out;
7088}
7089// view_item_list is deliberately NOT fired inline with the impression.
7090//
7091// Two reasons. (1) fireTilesImpressionOnce() runs synchronously right
7092// after the hydration fetches are *started*, so at that instant no tile
7093// has a real name or price -- firing then sends an id-only list whose
7094// item_name never matches the select_item / add_to_cart that follow,
7095// splitting every per-product report in two. (2) the tile quality gate
7096// drops or repaints tiles during those same milliseconds, so the
7097// build-time id list is not what the shopper ends up seeing.
7098//
7099// So: pass ids = null to re-read the LIVE grid on every poll, wait for
7100// names, then fire once. On timeout we still fire -- an id-only
7101// impression beats no impression at all, and a stalled poll would let
7102// select_item land before view_item_list and break funnel step order.
7103function ckGaFireListWhenHydrated(ids, attempts, listNameOverride) {
7104if (!CK_GA4.EMIT) return;
7105try {
7106var live = (ids === null || typeof ids === 'undefined');
7107var list = live ? ckGaVisibleTileIds()
7108: (ids || []).map(ckGaValidId).filter(function (x) { return !!x; });
7109if (!list.length) return;
7110attempts = (typeof attempts === 'number') ? attempts : CK_GA4.HYDRATE_POLLS;
7111var resolved = 0;
7112list.forEach(function (id) { if (ckGaHasName(id) && ckGaHasSku(id)) resolved++; });
7113if (resolved < list.length && attempts > 0) {
7114setTimeout(function () {
7115ckGaFireListWhenHydrated(ids, attempts - 1, listNameOverride);
7116}, CK_GA4.HYDRATE_POLL_MS);
7117return;
7118}
7119ckGaPush('view_item_list', {
7120item_list_id: ckGaListId(),
7121item_list_name: listNameOverride || ckGaListName(),
7122items: ckGaItemsFor(list, 0, listNameOverride)
7123});
7124} catch (e) { }
7125}
7126function fireTilesImpressionOnce() {
7127try {
7128if (!useTiles) return;
7129if (!Array.isArray(TILE_ITEMS) || !TILE_ITEMS.length) return;
7130// fire once per page load per persona
7131window.__ckImpressionFired = window.__ckImpressionFired || {};
7132var key = String(activePersonaKey || 'generic');
7133if (window.__ckImpressionFired[key]) return;
7134window.__ckImpressionFired[key] = true;
7135pushUpsellEvent('SmartCart_upsell_impression', {
7136// Flat id list -- GA4 cannot report on the nested tiles_shown
7137// array; ckGaMirror() turns this into view_item_list items[].
7138product_ids: TILE_ITEMS.map(function (t) { return String(t.productId); }),
7139tiles_shown: TILE_ITEMS.map(function (t, idx) {
7140return {
7141productId: t.productId,
7142position: idx + 1,
7143experimental: !!t.experimental,
7144role: t.role || 'helper',
7145friction: t.friction || 'low'
7146};
7147})
7148});
7149} catch (e) { }
7150}
7151function pushTileEvent(action, payload) {
7152payload = payload || {};
7153pushUpsellEvent('SmartCart_upsell_tile_' + action, payload);
7154const personaKey =
7155payload.persona_key ||
7156payload.personaKey ||
7157(window.__CK && window.__CK.activePersonaKey) ||
7158window.activePersonaKey;
7159if (
7160personaKey &&
7161(action === 'view' || action === 'in_view' || action === 'click' || action === 'add' || action === 'added' || action === 'selected')
7162) {
7163onSmartCartMeaningfulEngagement(personaKey);
7164}
7165}
7166// ----------------------------------------------------
7167// 4) CONFIG
7168// ----------------------------------------------------
7169// Ink pad parent product + options
7170var INK_PARENT_ID = '3631';
7171var INK_SIZE_OPTION_ID = '1296'; // Size
7172var INK_COLOR_OPTION_ID = '1295'; // Ink Pad Color
7173var INK_SIZE_VALUE_ID = '6265'; // Small
7174var INK_COLOR_VALUES = {
7175black: '6257',
7176blue: '6258',
7177red: '6259',
7178green: '6260',
7179purple: '6262'
7180};
7181// Accent stamp options
7182var ACCENT_SIZE_OPTION_ID = '1453';
7183var ACCENT_SIZE_VALUE_ID = '6921';
7184var ACCENT_INK_OPTION_ID = '2403';
7185var ACCENT_INK_NO_THANKS_VALUE_ID = '9532';
7186// Pricing model for Accent stamps (existing behavior)
7187var STAMP_UNIT_PRICE = 5.49;
7188var INK_PRICE = 4.99;
7189var STAMP_OPTIONS = [
7190{
7191id: '23105',
7192imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-1_800.jpg',
7193imgAlt: 'Delicate flower envelope accent craft stamp design'
7194},
7195{
7196id: '23106',
7197imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-2_800.jpg',
7198imgAlt: 'Simple botanical branches envelope accent craft stamp design'
7199},
7200{
7201id: '23108',
7202imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-4_800.jpg',
7203imgAlt: 'Elegant botanical branch envelope accent craft stamp design'
7204},
7205{
7206id: '23107',
7207imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-3_800.jpg',
7208imgAlt: 'Daisy flower envelope accent craft stamp design'
7209},
7210{
7211id: '23110',
7212imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-arrow_800.jpg',
7213imgAlt: 'Hearts through arrow envelope accent craft stamp design'
7214},
7215{
7216id: '23113',
7217imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-2_800.jpg',
7218imgAlt: 'Outlined heart envelope accent craft stamp design'
7219}
7220];
7221var MAX_TILES_TO_SHOW = 6;                 // guardrail
7222var ALLOW_EXPERIMENTAL_TILES = true;       // easy toggle
7223// Persona tile items (simple products). If none, we show Accent Stamp grid.
7224// Uses product-level matchKeywords + role/friction modifiers to pick the best N (MAX_TILES_TO_SHOW).
7225function scoreProductForCart(item, cartTextLower, allowExperimental) {
7226var score = 0;
7227// Keyword hits (product-level)
7228score += scoreKeywordHits(item.matchKeywords, cartTextLower);
7229// Role modifiers
7230switch (item.role) {
7231case 'anchor': score += 6; break;
7232case 'core': score += 4; break;
7233case 'compliance': score += 8; break;
7234case 'backup': score += 2; break;
7235case 'accessory': score += 1; break;
7236default: break;
7237}
7238// Friction modifiers
7239switch (item.friction) {
7240case 'none': score += 3; break;
7241case 'low': score += 1; break;
7242case 'medium': score -= 2; break;
7243case 'high': score -= 5; break;
7244default: break;
7245}
7246// Experimental gating
7247if (item.experimental && !allowExperimental) score -= 100;
7248return score;
7249}
7250function pickTopTilesForDisplay(items, cartTextLower, maxTiles, allowExperimental) {
7251var pool = Array.isArray(items) ? items.slice() : [];
7252// Ensure fields exist
7253pool.forEach(function (it) {
7254if (!it) return;
7255if (!Array.isArray(it.matchKeywords)) it.matchKeywords = [];
7256if (!it.role) it.role = 'helper';
7257if (!it.friction) it.friction = 'low';
7258});
7259// Score + stable tiebreakers
7260var scored = pool.map(function (it, idx) {
7261return {
7262it: it,
7263idx: idx,
7264score: scoreProductForCart(it, cartTextLower, allowExperimental)
7265};
7266});
7267scored.sort(function (a, b) {
7268if (b.score !== a.score) return b.score - a.score;
7269// If tie: prefer anchors
7270var ar = (a.it.role === 'anchor') ? 1 : 0;
7271var br = (b.it.role === 'anchor') ? 1 : 0;
7272if (br !== ar) return br - ar;
7273// Then lower friction
7274var fr = { none: 0, low: 1, medium: 2, high: 3 };
7275var af = fr[a.it.friction] != null ? fr[a.it.friction] : 9;
7276var bf = fr[b.it.friction] != null ? fr[b.it.friction] : 9;
7277if (af !== bf) return af - bf;
7278// Then sortOrder
7279return (a.it.sortOrder || 0) - (b.it.sortOrder || 0);
7280});
7281// Dedupe by product id: the pool can carry the same product under two
7282// entries, and rendering it twice reads as a bug.
7283var seenPid = Object.create(null);
7284var selected = [];
7285for (var si = 0; si < scored.length && selected.length < maxTiles; si++) {
7286var cand = scored[si].it;
7287var cpid = String((cand && (cand.productId || cand.product_id)) || '');
7288if (cpid) {
7289if (seenPid[cpid]) continue;
7290seenPid[cpid] = true;
7291}
7292selected.push(cand);
7293}
7294// Default index: highest score, but if an anchor is within 2 points of top, prefer it.
7295var defaultIndex = 0;
7296if (selected.length) {
7297var topScore = scored.length ? scored[0].score : 0;
7298for (var i = 0; i < selected.length; i++) {
7299var it = selected[i];
7300var itScore = scoreProductForCart(it, cartTextLower, allowExperimental);
7301if (it.role === 'anchor' && itScore >= (topScore - 2)) {
7302defaultIndex = i;
7303break;
7304}
7305}
7306}
7307return { tiles: selected, defaultIndex: defaultIndex };
7308}
7309// Respect ?upsellDebug=1 to include experimental tiles
7310var allowExperimentalNow = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1');
7311var cartTextForScoring = getCartTitleTextLower();
7312var tilePick = (useTiles ? pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow) : { tiles: [], defaultIndex: 0 });
7313// Best-effort: suppress tiles that are already in the cart using cart link paths (most reliable signal we have on cart)
7314var __cartPaths0 = Object.create(null);
7315try { __cartPaths0 = getCartProductPathSet(); } catch (eCP0) { __cartPaths0 = Object.create(null); }
7316var __cartIds0 = Object.create(null);
7317try { __cartIds0 = getCartProductIdSet(); } catch (eCI0) { __cartIds0 = Object.create(null); }
7318function __isInCartByPath(it) {
7319try {
7320// Zephyr: exact product-id match first (data-product-id on cart cards)
7321var pid0 = it && (it.productId || it.product_id);
7322if (pid0 && __cartIds0[String(pid0)]) return true;
7323// Be tolerant: different shapes may use productUrl vs product_url
7324var url = '';
7325if (it) {
7326url = it.productUrl || it.product_url || it.url || '';
7327}
7328// If url missing but we have an id, try the lookup map (populated during tile build)
7329if (!url && it && it.productId && TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[it.productId]) {
7330url = TILE_ITEM_BY_ID[it.productId].productUrl || TILE_ITEM_BY_ID[it.productId].product_url || '';
7331}
7332var pth = normalizeUrlPathForCompare(url);
7333return !!(pth && __cartPaths0[pth]);
7334} catch (e) { return false; }
7335}
7336// Build initial visible tiles from the ordered pool, skipping in-cart items.
7337// Keep tile count stable up to MAX_TILES_TO_SHOW.
7338var TILE_ITEMS = [];
7339var TILE_ITEM_BY_ID = {};
7340if (useTiles && Array.isArray(tilePick.tiles)) {
7341for (var __i = 0; __i < tilePick.tiles.length; __i++) {
7342if (TILE_ITEMS.length >= MAX_TILES_TO_SHOW) break;
7343var it = tilePick.tiles[__i];
7344if (!it) continue;
7345// Allow tiles even if productId is missing (null IDs can still be previewed). We'll block add-to-cart for them later.
7346// Skip anything already in cart (path match)
7347if (__isInCartByPath(it)) continue;
7348TILE_ITEMS.push(it);
7349try { if (it.productId) TILE_ITEM_BY_ID[it.productId] = it; } catch (eMap) { }
7350}
7351}
7352// Default index: keep "best" (0) after exclusions
7353if (useTiles) persona.defaultPrimaryIndex = 0;
7354// ----------------------------------------------------
7355// 5) Build UI
7356// ----------------------------------------------------
7357var inkPadHtml =
7358'<div class="cart-kit-upsell__options-group" style="margin-top:10px;">'
7359+ '<div class="cart-kit-upsell__options-label" id="kit-ink-label">Would you like an Ink Pad? (Needed to make impression)</div>'
7360+ '<div class="kit-swatch-list" id="kit-swatch-list">'
7361+ '<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>'
7362+ '<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>'
7363+ '<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>'
7364+ '<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>'
7365+ '<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>'
7366+ '<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>'
7367+ '</div>'
7368+ '</div>';
7369var inkPadBlock = (!useTiles ? inkPadHtml : '');
7370var upsellMarkup = ''
7371+ '<div class="cart-kit-upsell cart-kit-upsell--open" id="cart-kit-upsell">'
7372+ '<button class="cart-kit-upsell__header" type="button">'
7373+ '<div class="cart-kit-upsell__icon-wrap" aria-hidden="true">'
7374+ '<img class="cart-kit-upsell__icon-img cart-kit-upsell__icon-img--static"'
7375+ ' src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/SS_Homepage-Icon_Stamps_120x120.png"'
7376+ ' alt="" />'
7377+ '</div>'
7378+ '<div class="cart-kit-upsell__header-text">'
7379+ '<span class="cart-kit-upsell__title" id="cart-kit-header-text">$1 Accent Stamp Unlocked! Choose your favorite below and add to cart.</span>'
7380+ '</div>'
7381+ '<span class="cart-kit-upsell__chevron" aria-hidden="true"></span>'
7382+ '</button>'
7383+ '<div class="cart-kit-upsell__body">'
7384+ '<div class="cart-kit-upsell__options-group">'
7385+ '<div class="cart-kit-upsell__options-header">'
7386+ '<div class="cart-kit-upsell__options-label" id="kit-stamp-label">Choose one or many Accent Stamp Designs:</div>'
7387+ '<button type="button" class="cart-kit-upsell__info" aria-label="Stamp impression size information">'
7388+ '<span class="cart-kit-upsell__info-icon">i</span>'
7389+ '<span class="cart-kit-upsell__info-tooltip" id="kit-tooltip">Stamp impression size: approx 1.75&quot; × 0.67&quot;.</span>'
7390+ '</button>'
7391+ '</div>'
7392+ '<div class="kit-stamp-grid" id="kit-stamp-grid"></div>'
7393+ '</div>'
7394+ inkPadBlock
7395+ '<div class="cart-kit-upsell__footer">'
7396+ '<div class="cart-kit-upsell__price">'
7397+ '<span class="cart-kit-upsell__price-label" id="kit-price-label">Total:</span>'
7398+ '<span class="kit-value-line__new" id="kit-price-new">$1.00</span>'
7399+ '<button type="button" class="cart-kit-upsell__cta" id="cart-kit-upsell-add">Add to Cart</button>'
7400+ '<button type="button" class="cart-kit-upsell__cta-secondary" id="cart-kit-upsell-hide">Hide Offer</button>'
7401+ '</div>'
7402+ '<div class="kit-success-toast" id="kit-success-toast"></div>'
7403+ '<div class="cart-kit-upsell__status" id="cart-kit-status" aria-l
7403ive="polite"></div>'
7404+ '<div class="cart-kit-upsell__progresswrap" id="cart-kit-progresswrap" style="display:none;">'
7405+ '<div class="cart-kit-upsell__progresstop">'
7406+ '<div class="cart-kit-upsell__progresstext" id="cart-kit-progresstext">Adding items…</div>'
7407+ '<div class="cart-kit-upsell__progresspct" id="cart-kit-progresspct">0%</div>'
7408+ '</div>'
7409+ '<div class="cart-kit-upsell__progressbar">'
7410+ '<div class="cart-kit-upsell__progressfill" id="cart-kit-progressfill"></div>'
7411+ '</div>'
7412+ '</div>'
7413+ '</div>'
7414+ '</div>'
7415+ '</div>'
7416+ '</div>';
7417mount.insertAdjacentHTML('beforeend', upsellMarkup);
7418var upsell = mount.querySelector('#cart-kit-upsell');
7419var header = upsell.querySelector('.cart-kit-upsell__header');
7420var body = upsell.querySelector('.cart-kit-upsell__body');
7421var addBtn = upsell.querySelector('#cart-kit-upsell-add');
7422if (!upsell || !addBtn) {
7423console.warn('[SmartCart] Required module elements missing. Aborting init.');
7424return;
7425}
7426var hideBtn = upsell.querySelector('#cart-kit-upsell-hide');
7427var stampGrid = upsell.querySelector('#kit-stamp-grid');
7428var swatchList = upsell.querySelector('#kit-swatch-list');
7429var statusEl = upsell.querySelector('#cart-kit-status');
7430if (statusEl) {
7431statusEl.setAttribute('role', 'status');
7432statusEl.setAttribute('aria-live', 'polite');
7433statusEl.setAttribute('aria-atomic', 'true');
7434}
7435var progressWrap = upsell.querySelector('#cart-kit-progresswrap');
7436var progressFill = upsell.querySelector('#cart-kit-progressfill');
7437var progressText = upsell.querySelector('#cart-kit-progresstext');
7438var progressPct = upsell.querySelector('#cart-kit-progresspct');
7439var stampLabelEl = upsell.querySelector('#kit-stamp-label');
7440var inkLabelEl = upsell.querySelector('#kit-ink-label');
7441var tooltipEl = upsell.querySelector('#kit-tooltip');
7442var headerTextEl = upsell.querySelector('#cart-kit-header-text');
7443// Animate icon
7444setTimeout(function () {
7445var wrap = upsell.querySelector('.cart-kit-upsell__icon-wrap');
7446if (wrap) wrap.classList.add('is-entered');
7447}, 40);
7448// ----------------------------------------------------
7449// 6) Apply persona UI (Notary)
7450// ----------------------------------------------------
7451function applyPersonaUI() {
7452// Header + labels + tooltip
7453if (headerTextEl) headerTextEl.textContent = persona.headerTitle || '';
7454if (stampLabelEl) stampLabelEl.textContent = persona.stampLabel || '';
7455if (inkLabelEl) inkLabelEl.textContent = persona.inkLabel || '';
7456if (tooltipEl) tooltipEl.textContent = persona.tooltip || '';
7457// Toggle tile layout when persona has items
7458if (stampGrid) stampGrid.classList.toggle('is-notary-grid', !!useTiles);
7459// Filter ink swatches based on allowedInk
7460var allowed = (persona.allowedInk && persona.allowedInk.length) ? persona.allowedInk : ['black', 'blue', 'red', 'green', 'purple', 'none'];
7461if (swatchList) {
7462var swatches = swatchList.querySelectorAll('.kit-swatch');
7463for (var i = 0; i < swatches.length; i++) {
7464var key = swatches[i].getAttribute('data-kit-color');
7465swatches[i].style.display = (allowed.indexOf(key) > -1) ? '' : 'none';
7466}
7467}
7468}
7469applyPersonaUI();
7470// ----------------------------------------------------
7471// 7) PDP Fetch Helpers (image + real price + real name)
7472// ----------------------------------------------------
7473var PDP_CACHE = new Map(); // url -> { name, price, img }
7474function toNumberPrice(val) {
7475if (val == null) return null;
7476var n = Number(String(val).replace(/[^0-9.]/g, ''));
7477return isFinite(n) ? n : null;
7478}
7479function absUrlMaybe(src) {
7480if (!src) return null;
7481if (/^https?:\/\//i.test(src)) return src;
7482if (src.indexOf('/') === 0) return window.location.origin + src;
7483return src;
7484}
7485function detectRequiresMicro(doc) {
7486try {
7487var form =
7488doc.querySelector('#product_addtocart_form') ||
7489doc.querySelector('form[action*="checkout/cart/add"]') ||
7490doc.querySelector('form');
7491if (!form) return false;
7492// Strong signal: any file upload = micro PDP
7493if (form.querySelector('input[type="file"]')) return true;
7494// Strong signal: Magento custom options payload (options[...])
7495if (form.querySelector('[name^="options["]')) return true;
7496// Strong signal: configurables often use super_attribute
7497if (form.querySelector('[name^="super_attribute"]')) return true;
7498// Strong signal: required fields inside options wrapper
7499var optionsWrap =
7500form.querySelector('.product-options-wrapper') ||
7501form.querySelector('#product-options-wrapper') ||
7502form.querySelector('.fieldset') ||
7503form;
7504// Look for explicit required markers/validators within options area
7505if (
7506optionsWrap.querySelector('[required]') ||
7507optionsWrap.querySelector('[aria-required="true"]') ||
7508optionsWrap.querySelector('[data-validate*="required"]') ||
7509optionsWrap.querySelector('.required-entry') ||
7510optionsWrap.querySelector('.required')
7511) {
7512return true;
7513}
7514// If there are selectable option controls (select/radio/checkbox/textarea)
7515// inside a known options wrapper, treat as "likely micro"
7516// (kept conservative: only if wrapper exists)
7517var hasOptionControls =
7518!!optionsWrap.querySelector('select, input[type="radio"], input[type="checkbox"], textarea');
7519if (hasOptionControls && (optionsWrap !== form)) return true;
7520return false;
7521} catch (e) {
7522return false;
7523}
7524}
7525function requiresMicroPdp(productId, rawTile) {
7526var pid = String(productId || '').trim();
7527if (!pid) return false;
7528// 1) Explicit tile intent
7529if (rawTile && rawTile.addMode === 'microPdp') return true;
7530// 2) Hard override (authoritative)
7531if (
7532typeof HARD_SKU_OVERRIDES !== 'undefined' &&
7533HARD_SKU_OVERRIDES &&
7534HARD_SKU_OVERRIDES[pid] &&
7535HARD_SKU_OVERRIDES[pid].addMode === 'microPdp'
7536) {
7537return true;
7538}
7539// 3) Autodetect fallback from cached PDP parse
7540if (
7541rawTile &&
7542rawTile.productUrl &&
7543typeof PDP_CACHE !== 'undefined' &&
7544PDP_CACHE &&
7545PDP_CACHE.has(rawTile.productUrl)
7546) {
7547var cached = PDP_CACHE.get(rawTile.productUrl);
7548if (cached && cached.requiresMicro === true) return true;
7549}
7550return false;
7551}
7552function isAccentExperience() {
7553try {
7554var urlParams = new URLSearchParams(window.location.search);
7555var accentAddedQS = (urlParams.get('accentAdded') === '1' || urlParams.get('accentAdded') === 'true');
7556return !!accentAddedQS; // keep it strict for now
7557} catch (e) {
7558return false;
7559}
7560}
7561function parsePdpDoc(doc) {
7562var name =
7563(doc.querySelector('meta[property="og:title"]') &&
7564doc.querySelector('meta[property="og:title"]').getAttribute('content')) ||
7565(doc.querySelector('h1') && (doc.querySelector('h1').textContent || '').trim()) ||
7566null;
7567var img =
7568(doc.querySelector('meta[property="og:image"]') &&
7569doc.querySelector('meta[property="og:image"]').getAttribute('content')) ||
7570(doc.querySelector('meta[name="twitter:image"]') &&
7571doc.querySelector('meta[name="twitter:image"]').getAttribute('content')) ||
7572null;
7573img = absUrlMaybe(img);
7574var price =
7575(doc.querySelector('meta[property="product:price:amount"]') &&
7576doc.querySelector('meta[property="product:price:amount"]').getAttribute('content')) ||
7577(doc.querySelector('meta[itemprop="price"]') &&
7578doc.querySelector('meta[itemprop="price"]').getAttribute('content')) ||
7579(doc.querySelector('[itemprop="price"]') &&
7580doc.querySelector('[itemprop="price"]').textContent) ||
7581(doc.querySelector('.price') &&
7582doc.querySelector('.price').textContent) ||
7583null;
7584price = toNumberPrice(price);
7585// Magento SKU. GA4 item_id MUST be the SKU, not the entity id: every
7586// other ecommerce event on this site (view_cart, purchase) keys items
7587// by SKU, so sending entity ids here would make the same product two
7588// different rows in GA4 and break any SmartCart-add -> purchase join.
7589var sku = null;
7590try {
7591var skuEl = doc.querySelector('meta[itemprop="sku"]') || doc.querySelector('[itemprop="sku"]');
7592if (skuEl) {
7593sku = (skuEl.getAttribute('content') || skuEl.textContent || '').trim() || null;
7594}
7595} catch (eSku) { }
7596var requiresMicro = detectRequiresMicro(doc);
7597// The single reliable "is this a product?" signal. An account, login,
7598// 404 or category page still yields an og:title and an og:image, so
7599// title/image alone can't tell you the scrape succeeded.
7600var isProduct = !!(
7601doc.querySelector('#product_addtocart_form') ||
7602doc.querySelector('form[action*="checkout/cart/add"]')
7603);
7604return {
7605name: name,
7606img: img,
7607price: price,
7608sku: sku,
7609isProduct: isProduct,
7610requiresMicro: requiresMicro
7611};
7612}
7613// Same-origin PDP fetch: config product_urls are absolute www.simplystamps.com;
7614// rewrite to the current origin so hydration works on local/staging AND prod (no-op on www).
7615function ckSameOrigin(u) {
7616try {
7617var p = new URL(u, window.location.origin);
7618if (/(^|\.)simplystamps\.com$/i.test(p.hostname) && p.origin !== window.location.origin) {
7619return window.location.origin + p.pathname + p.search + p.hash;
7620}
7621return p.href;
7622} catch (e) { return u; }
7623}
7624// ----------------------------------------------------
7625// ZEPHYR SIDE-CART BRIDGE (added 2026-09-11)
7626// Zephyr ships a slide-out cart drawer driven by an Alpine store, and the
7627// cart PAGE is a SECOND, independent Alpine component (cartPage()) holding
7628// its own copy of the same /zephyr/cart/getItems/ JSON. They do not talk:
7629//   cart-drawer.js  store.refresh()  -> fires  'cart-refreshed'
7630//   cart-page.js    init()           -> listens 'cart-updated'
7631// Nothing bridges those two names, so ANY out-of-band cart mutation must do
7632// BOTH: refresh the store (drawer + header badge) AND dispatch 'cart-updated'
7633// (main cart list + Order Summary). Doing only one leaves half the page
7634// stale -- which is the reason this module used to hard-reload.
7635// ----------------------------------------------------
7636function ckCartStore(win) {
7637try {
7638var w = win || window;
7639if (!w.Alpine || typeof w.Alpine.store !== 'function') return null;
7640return w.Alpine.store('cart') || null;
7641} catch (e) {
7642return null;
7643}
7644}
7645// Push a cart mutation into BOTH Zephyr cart trees, optionally revealing the
7646// side cart. Returns false when the Zephyr store is absent (legacy theme) so
7647// callers can fall back to the old page-reload path.
7648function ckAfterCartMutation(openDrawer) {
7649var st = ckCartStore(window);
7650if (!st) return false;
7651if (openDrawer && typeof st.openDrawer === 'function') {
7652try { st._scrollToNew = true; } catch (e) { }
7653try { st.openDrawer(); } catch (e) { }
7654}
7655// Mirror Zephyr's own post-add timing (handleAjaxAddToCart waits 100ms).
7656setTimeout(function () {
7657try { if (typeof st.refresh === 'function') st.refresh(); } catch (e) { }
7658try { window.dispatchEvent(new CustomEvent('cart-updated')); } catch (e) { }
7659}, 100);
7660return true;
7661}
7662function fetchPdpData(productUrl) {
7663if (!productUrl) return Promise.resolve({ name: null, img: null, price: null });
7664if (PDP_CACHE.has(productUrl)) return Promise.resolve(PDP_CACHE.get(productUrl));
7665var __fetchUrl = ckSameOrigin(productUrl);
7666return fetch(__fetchUrl, { credentials: 'same-origin' })
7667.then(function (res) { return res.text(); })
7668.then(function (html) {
7669var doc = new DOMParser().parseFromString(html, 'text/html');
7670var data = parsePdpDoc(doc);
7671PDP_CACHE.set(productUrl, data);
7672return data;
7673})
7674.catch(function () {
7675var data = { name: null, img: null, price: null };
7676PDP_CACHE.set(productUrl, data);
7677return data;
7678});
7679}
7680// ----------------------------------------------------
7681// Tile State Model (no UX changes yet)
7682// ----------------------------------------------------
7683var TILE_STATE = {
7684AVAILABLE: 'available',
7685SELECTED: 'selected',
7686ADDED: 'added'
7687};
7688// Central store keyed by SKU
7689// ----------------------------------------------------
7690// TILE QUALITY GATE + APPROVED FALLBACKS (added 2026-09-15)
7691// Some persona entries point at URLs that aren't product pages on every store
7692// view (a prod-only SKU, a retired URL). The scrape then returns whatever that
7693// page happens to be -- "Create New Customer Account", no price, the site logo
7694// -- and we rendered it as a product. Never show that. Substitute an approved
7695// alternative the shopper doesn't already have, or drop the slot entirely.
7696// ----------------------------------------------------
7697var CK_TILE_FALLBACKS = [
7698{ productId: '22023', productUrl: 'https://www.simplystamps.com/envelope-sealer-2oz-bottle', title: 'Envelope Sealer - 2oz Bottle' },
7699{ productId: '25340', productUrl: 'https://www.simplystamps.com/stamp-cleaner-kit', title: 'Stamp Cleaner Kit' },
7700{ productId: '24186', productUrl: 'https://www.simplystamps.com/gold-wax-seal-sticker-rosemary-design', title: 'Gold Wax Seal Sticker, Rosemary Design' },
7701{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', title: 'Craft Ink Pads' },
7702{ 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' },
7703{ productId: '23105', productUrl: 'https://www.simplystamps.com/delicate-flower-envelope-accent-craft-stamp', title: 'Delicate Flower Envelope Accent Craft Stamp' }
7704];
7705var CK_FALLBACKS_USED = new Set();
7706function ckIsUsableTileData(data) {
7707if (!data) return false;
7708if (data.isProduct === false) return false;          // definitely not a PDP
7709if (data.price == null && !data.img) return false;   // nothing worth showing
7710var n = String(data.name || '').trim().toLowerCase();
7711if (!n) return false;
7712if (/create new customer|customer login|^log ?in|^sign ?in|page not found|404|access denied|shopping cart|my account|dashboard/.test(n)) return false;
7713return true;
7714}
7715function ckCartIdsNow() {
7716try { return getCartProductIdSet() || Object.create(null); }
7717catch (e) { return Object.create(null); }
7718}
7719// First approved alternative that is NOT already in the cart and NOT already on
7720// screen. Null when there is nothing safe left to show.
7721function ckPickFallbackTile() {
7722var inCart = ckCartIdsNow();
7723var onScreen = Object.create(null);
7724try {
7725var rendered = document.querySelectorAll('[data-kit-primary]');
7726for (var r = 0; r < rendered.length; r++) {
7727onScreen[String(rendered[r].getAttribute('data-kit-primary'))] = true;
7728}
7729} catch (e) { }
7730for (var i = 0; i < CK_TILE_FALLBACKS.length; i++) {
7731var f = CK_TILE_FALLBACKS[i];
7732var id = String(f.productId);
7733if (CK_FALLBACKS_USED.has(id)) continue;
7734if (inCart[id]) continue;
7735if (onScreen[id]) continue;
7736CK_FALLBACKS_USED.add(id);
7737return f;
7738}
7739return null;
7740}
7741// Nothing safe for this slot -- remove it rather than render junk.
7742function ckDropTile(card) {
7743try {
7744var pid = String(card.getAttribute('data-kit-primary') || '');
7745if (pid) { try { selectedPrimaryIds.delete(pid); } catch (eS) { } }
7746if (card.parentNode) card.parentNode.removeChild(card);
7747try { updatePriceUI(); } catch (eU) { }
7748// Tiles are dropped asynchronously, so this is the moment the module
7749// can become empty -- re-check every time.
7750try { ckSyncModuleEmptyState(); } catch (eE) { }
7751} catch (e) { }
7752}
7753// Repaint an already-rendered card onto a different product, then hydrate it.
7754function ckRepaintCardAs(card, item) {
7755if (!card || !item) return;
7756var id = String(item.productId);
7757try { TILE_ITEM_BY_ID[id] = item; } catch (e) { }
7758try { if (ckSeenTileIds) ckSeenTileIds.add(id); } catch (e) { }
7759card.setAttribute('data-kit-primary', id);
7760card.setAttribute('data-sku', id);
7761card.setAttribute('data-kit-url', item.productUrl || '');
7762try { card.classList.remove('is-added', 'is-selected', 'is-complete'); } catch (e) { }
7763try { card.removeAttribute('aria-disabled'); } catch (e) { }
7764var nameEl = card.querySelector('.kit-product-name');
7765var priceEl = card.querySelector('.kit-product-price');
7766var thumbEl = card.querySelector('.kit-product-thumb-tile');
7767if (nameEl) nameEl.textContent = item.title || 'Loading\u2026';
7768if (priceEl) priceEl.textContent = '\u2014';
7769if (thumbEl) thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">Loading\u2026</span>';
7770fetchPdpData(item.productUrl).then(function (d2) {
7771if (String(card.getAttribute('data-kit-primary') || '') !== id) return; // swapped again
7772if (!ckIsUsableTileData(d2)) {
7773// This alternative isn't available in this store view either. Try the
7774// next approved one; CK_FALLBACKS_USED makes the recursion terminate.
7775var nextAlt = ckPickFallbackTile();
7776if (nextAlt) ckRepaintCardAs(card, nextAlt);
7777else ckDropTile(card);
7778return;
7779}
7780try {
7781NOTARY_HYDRATED.set(id, {
7782sku: d2.sku,
7783productId: id, productUrl: item.productUrl,
7784name: d2.name || item.title, img: d2.img || null,
7785price: (d2.price != null ? Number(d2.price) : null)
7786});
7787} catch (e) { }
7788if (nameEl) nameEl.textContent = d2.name || item.title;
7789if (priceEl) priceEl.textContent = (d2.price != null ? ('$' + Number(d2.price).toFixed(2)) : 'See price in cart');
7790if (thumbEl) {
7791if (d2.img) thumbEl.innerHTML = '<img src="' + d2.img + '" alt="" />';
7792else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>';
7793}
7794});
7795}
7796// ----------------------------------------------------
7797// MODULE EMPTY STATE (added 2026-09-15)
7798// Tiles can vanish AFTER render: the quality gate drops non-products and
7799// in-cart suppression removes what the shopper already owns. The original
7800// empty-state branch only ran when the pool was empty at BUILD time, and it
7801// targeted #kit-add-btn -- which does not exist, the CTA is
7802// #cart-kit-upsell-add -- so an emptied module was left on the page as a
7803// header + "Total: $0.00" + a live Add to Cart button with nothing to add.
7804// ----------------------------------------------------
7805function ckSyncModuleEmptyState() {
7806try {
7807var tileCount = stampGrid ? stampGrid.querySelectorAll('[data-kit-primary]').length : 0;
7808var swatchCount = swatchList ? swatchList.querySelectorAll('.kit-swatch').length : 0;
7809if (tileCount > 0 || swatchCount > 0) {
7810if (upsell) upsell.style.display = '';
7811return false;
7812}
7813// Nothing left to offer. An upsell with no offer is noise on a cart
7814// page, so take the whole module off the page rather than leave a
7815// header and a dead CTA behind.
7816try { console.warn('[SmartCart] nothing offerable remains (all in cart or filtered); hiding the module.'); } catch (e1) { }
7817try {
7818if (addBtn) {
7819addBtn.disabled = true;
7820addBtn.setAttribute('aria-disabled', 'true');
7821}
7822} catch (e2) { }
7823if (upsell) upsell.style.display = 'none';
7824return true;
7825} catch (e) { return false; }
7826}
7827var TileStore = (function () {
7828var map = Object.create(null);
7829function getSkuFromTile(tileEl) {
7830return (
7831tileEl.getAttribute('data-sku') ||
7832tileEl.getAttribute('data-product-sku') ||
7833tileEl.getAttribute('data-kit-sku') ||
7834''
7835).trim();
7836}
7837function inferIsHard(tileEl) {
7838// Preferred: explicit attribute you set when building tiles
7839// data-hard="1" or data-is-hard="true"
7840var hardAttr =
7841tileEl.getAttribute('data-hard') ||
7842tileEl.getAttribute('data-is-hard') ||
7843'';
7844if (hardAttr) return hardAttr === '1' || hardAttr === 'true';
7845// Fallback: if you already tag these somewhere else
7846// (you can tighten this later when we implement Step 2)
7847return tileEl.classList.contains('is-hard') || false;
7848}
7849function initFromDom(rootEl) {
7850if (!rootEl) return;
7851var tiles = Array.from(rootEl.querySelectorAll('.kit-stamp-card[data-kit-primary]'));
7852tiles.forEach(function (tileEl) {
7853var sku = getSkuFromTile(tileEl);
7854if (!sku) return;
7855try { ckSeenTileIds.add(String(sku)); } catch (e) { }
7856if (!map[sku]) {
7857map[sku] = {
7858sku: sku,
7859el: tileEl,
7860isHard: inferIsHard(tileEl),
7861state: TILE_STATE.AVAILABLE
7862};
7863} else {
7864// re-rendered tile, keep state but update DOM ref
7865map[sku].el = tileEl;
7866// do not overwrite isHard unless you want to re-infer
7867}
7868// Bootstrap state from current UI (so no visible changes)
7869// You can adjust these classnames to match your actual markup.
7870if (tileEl.classList.contains('is-added') || tileEl.querySelector('.ck-added, .cart-kit-added, .cart-kit-check')) {
7871map[sku].state = TILE_STATE.ADDED;
7872} else if (tileEl.classList.contains('is-selected') || tileEl.getAttribute('aria-pressed') === 'true') {
7873map[sku].state = TILE_STATE.SELECTED;
7874} else {
7875map[sku].state = TILE_STATE.AVAILABLE;
7876}
7877syncDom(sku);
7878});
7879}
7880function get(sku) {
7881return map[String(sku || '')] || null;
7882}
7883function setState(sku, nextState) {
7884sku = String(sku || '');
7885if (!map[sku]) return;
7886var prev = map[sku].state;
7887map[sku].state = nextState;
7888// Keep DOM aligned (but we preserve current look)
7889syncDom(sku);
7890return { sku: sku, prev: prev, next: nextState };
7891}
7892function syncDom(sku) {
7893var t = map[String(sku || '')];
7894if (!t || !t.el) return;
7895var el = t.el;
7896// Normalize classes (keep current behavior intact)
7897el.classList.toggle('is-selected', t.state === TILE_STATE.SELECTED);
7898el.classList.toggle('is-added', t.state === TILE_STATE.ADDED);
7899// Optional aria alignment if you already use it
7900// (doesn't change visuals but improves correctness)
7901if (t.state === TILE_STATE.SELECTED) el.setAttribute('aria-pressed', 'true');
7902else el.setAttribute('aria-pressed', 'false');
7903}
7904function all() {
7905return map;
7906}
7907return {
7908initFromDom: initFromDom,
7909get: get,
7910setState: setState,
7911syncDom: syncDom,
7912all: all
7913};
7914})();
7915// ----------------------------------------------------
7916// Micro PDP (iframe) — for hard SKUs w/ uploads + dependent options
7917// ----------------------------------------------------
7918var MICRO_PDP = {
7919overlay: null,
7920iframe: null,
7921loading: null,
7922lastUrl: null,
7923lastProductId: null,
7924returnUrl: null,
7925progressDone: 0,
7926progressTotal: 0
7927};
7928// ----------------------------------------------------
7929// Micro PDP progress (header bar)
7930// ----------------------------------------------------
7931function getMicroProgressFromUrl() {
7932try {
7933var u = new URL(window.location.href);
7934var done = parseInt(u.searchParams.get('microDone') || '0', 10);
7935var total = parseInt(u.searchParams.get('microTotal') || '0', 10);
7936if (!isFinite(done) || done < 0) done = 0;
7937if (!isFinite(total) || total < 0) total = 0;
7938return { done: done, total: total };
7939} catch (e) {
7940return { done: 0, total: 0 };
7941}
7942}
7943function bumpMicroDoneInUrl() {
7944try {
7945var u = new URL(window.location.href);
7946var done = parseInt(u.searchParams.get('microDone') || '0', 10);
7947var total = parseInt(u.searchParams.get('microTotal') || '0', 10);
7948if (!isFinite(done) || done < 0) done = 0;
7949if (!isFinite(total) || total < 0) total = 0;
7950if (total > 0) {
7951done = Math.min(total, done + 1);
7952u.searchParams.set('microDone', String(done));
7953window.history.replaceState({}, document.title, u.toString());
7954}
7955return { done: done, total: total };
7956} catch (e) {
7957return { done: 0, total: 0 };
7958}
7959}
7960function renderMicroProgress(done, total) {
7961var wrap = mount.querySelector('#ck-micropdp-progress');
7962var fill = mount.querySelector('#ck-micropdp-progressfill');
7963var txt = mount.querySelector('#ck-micropdp-progresstext');
7964// Only show when the user is doing a multi-item add (2+ total)
7965if (!wrap || !fill || !txt || !total || total <= 1) {
7966if (wrap) wrap.hidden = true;
7967return;
7968}
7969if (!isFinite(done) || done < 0) done = 0;
7970if (!isFinite(total) || total < 1) total = 1;
7971if (done > total) done = total;
7972var pct = Math.round((done / total) * 100);
7973wrap.hidden = false;
7974fill.style.width = pct + '%';
7975// Completion state (for “All set” moment)
7976if (done >= total) {
7977wrap.classList.add('is-complete');
7978txt.textContent = 'All set — ' + total + ' of ' + total + ' items added';
7979} else {
7980wrap.classList.remove('is-complete');
7981txt.textContent = done + ' of ' + total + ' items added';
7982}
7983}
7984ensureMicroPdpModal();
7985function ensureMicroPdpModal() {
7986if (MICRO_PDP.overlay) return;
7987var 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> `;
7988// Append inside mount so scoped CSS applies
7989mount.insertAdjacentHTML('beforeend', modalMarkup);
7990MICRO_PDP.overlay = mount.querySelector('#ck-micropdp-overlay');
7991MICRO_PDP.iframe = mount.querySelector('#ck-micropdp-iframe');
7992MICRO_PDP.loading = mount.querySelector('#ck-micropdp-loading');
7993var closeBtn = mount.querySelector('#ck-micropdp-close');
7994var fullBtn = mount.querySelector('#ck-micropdp-full');
7995// Safety guards
7996if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-open');
7997if (closeBtn) closeBtn.addEventListener('click', function () { closeMicroPdp('button'); });
7998if (fullBtn) fullBtn.addEventListener('click', function () {
7999if (MICRO_PDP.lastUrl) window.open(MICRO_PDP.lastUrl, '_blank', 'noopener');
8000});
8001// Optional overlay click closes
8002if (MICRO_PDP.overlay) {
8003MICRO_PDP.overlay.addEventListener('click', function (e) {
8004if (e.target === MICRO_PDP.overlay) closeMicroPdp('overlay');
8005});
8006}
8007}
8008// ESC closes
8009document.addEventListener('keydown', function (e) {
8010if (e.key === 'Escape' && MICRO_PDP.overlay && MICRO_PDP.overlay.classList.contains('is-open')) {
8011closeMicroPdp();
8012}
8013});
8014// On iframe load, inject “micro” CSS + force form submit to top window
8015MICRO_PDP.iframe.addEventListener('load', function () {
8016try {
8017var doc = MICRO_PDP.iframe.contentDocument;
8018if (!doc) return;
8019// Ensure scope class exists inside iframe (so our selectors always match)
8020doc.documentElement.classList.add('cart-kit-micro-pdp');
8021doc.body.classList.add('cart-kit-micro-pdp');
8022// Hide big site chrome + tighten layout (best-effort; selectors may vary)
8023var style = doc.createElement('style');
8024style.id = 'ck-micro-css';
8025style.textContent = `
8026/* =========================================================
8027Micro PDP trims — chrome off, form focus, thumbnail preview
8028========================================================= */
8029/* 1) Remove site chrome */
8030header, .header,
8031.nav-container, .navigation,
8032.breadcrumbs, .breadcrumb,
8033.page-header, .header.content,
8034.sections.nav-sections,
8035.message.global, .message.notice,
8036.cookie-status-message,
8037footer, .footer, .page-footer {
8038display: none !important;
8039}
8040/* 1b) Zephyr side cart must never render inside the configurator.
8041Neither the drawer nor its trigger sits inside <header>, so the
8042chrome rules above miss both. */
8043button[aria-label="Open cart"],
8044[x-show*="drawerOpen"] {
8045display: none !important;
8046}
8047/* 2) Normalize layout */
8048html, body { height: auto !important; }
8049body {
8050margin: 0 !important;
8051padding: 12px !important;
8052background: #fff !important;
8053overflow-x: hidden !important;
8054}
8055.page-wrapper, .page-main, main, .columns, .column.main {
8056width: 100% !important;
8057max-width: none !important;
8058margin: 0 !important;
8059padding: 0 !important;
8060}
8061/* Hide the INLINE form add-to-cart row (but keep the sticky bottom bar) */
8062/* Safety: ensure the STICKY/mobile bar still shows */
8063.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container,
8064.cart-kit-micro-pdp .add-to-box-mobile .add-to-cart-button-container-non-grouped,
8065.cart-kit-micro-pdp .add-to-box-mobile .input-group-mobile-container {
8066display: flex !important;
8067}
8068/* 2) Re-show the sticky/mobile bar at the bottom (it’s outside the form) */
8069.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container {
8070display: flex !important;
8071}
8072/* Hide qty+button ONLY when it's NOT inside the sticky bottom bar */
8073.cart-kit-micro-pdp .qty-and-add-to-cart-container:not(.add-to-box-mobile .qty-and-add-to-cart-container) {
8074display: none !important;
8075}
8076/* Kill common "extra" sections we don't need */
8077#ss-main-content { margin-top: unset !important; display:none !important; }
8078#sticky-mobile-header{ display: none !important; }
8079.gqcs-swiper-container{ display: none !important; }
8080.product-wapper-tab { display: none !important; }
8081/* 3) Preview: make it a small thumbnail WITHOUT reflowing layouts
8082(works across templates: big hero, centered preview, swiper, etc.) */
8083.cart-kit-micro-pdp .product.media,
8084.cart-kit-micro-pdp .product-image-carousel-container,
8085.cart-kit-micro-pdp .product-image-carousel-select,
8086.cart-kit-micro-pdp #ss-image-sheet,
8087.cart-kit-micro-pdp .gallery-placeholder,
8088.cart-kit-micro-pdp .product-img-content {
8089width: 100% !important;
8090max-width: 100% !important;
8091}
8092/* Center the preview area */
8093.cart-kit-micro-pdp .product.media,
8094.cart-kit-micro-pdp .product-image-carousel-container,
8095.cart-kit-micro-pdp .product-image-carousel-select,
8096.cart-kit-micro-pdp .gallery-placeholder {
8097display: flex !important;
8098justify-content: center !important;
8099}
8100/* Image itself: cap size (no cropping) */
8101.cart-kit-micro-pdp img.img-responsive,
8102.cart-kit-micro-pdp .product.media img,
8103.cart-kit-micro-pdp .product-image-photo,
8104.cart-kit-micro-pdp .fotorama__img,
8105.cart-kit-micro-pdp .swiper-slide img {
8106max-height: 300px !important;
8107max-width: 300px !important;
8108width: auto !important;
8109height: auto !important;
8110object-fit: contain !important;
8111margin: 0 !important;
8112}
8113/* Prevent swiper/nav chrome from adding vertical junk */
8114/* Swiper UI: show dots when multiple images exist (arrows stay hidden) */
8115.cart-kit-micro-pdp .swiper-button-prev,
8116.cart-kit-micro-pdp .swiper-button-next,
8117.cart-kit-micro-pdp .swiper-scrollbar {
8118display: none !important;
8119}
8120/* Dots ON (we'll hide them via JS when only 1 image) */
8121.cart-kit-micro-pdp .swiper-pagination {
8122display: flex !important;
8123justify-content: center !important;
8124gap: 6px !important;
8125margin-top: 6px !important;
8126position: relative !important;
8127}
8128/* Make dots smaller + subtle so they don't add height */
8129.cart-kit-micro-pdp .swiper-pagination-bullet {
8130width: 6px !important;
8131height: 6px !important;
8132opacity: 0.35 !important;
8133}
8134.cart-kit-micro-pdp .swiper-pagination-bullet-active {
8135opacity: 0.9 !important;
8136}
8137/* Tighten spacing under preview */
8138.cart-kit-micro-pdp .product.media,
8139.cart-kit-micro-pdp .product-image-carousel-container,
8140.cart-kit-micro-pdp .product-image-carousel-select,
8141.cart-kit-micro-pdp .gallery-placeholder {
8142margin-bottom: 64px !important;
8143}
8144/* 4) Form focus */
8145#product_addtocart_form, .product-add-form {
8146margin-top: 10px !important;
8147}
8148/* 5) Hide “Extra bottle of ink?” field (best effort selectors)
8149Note: :has() may not be supported everywhere; we also keep the JS fallback below */
8150.field.extra_ink, .field.extra-ink, .field.extra_bottle_ink,
8151.product-options-wrapper .field:has(select[name*="extra"]) {
8152display: none !important;
8153}
8154.full-size-container{
8155display:none;
8156}
8157`;
8158// Prevent double-inject if iframe reloads quickly
8159var existing = doc.getElementById('ck-micro-css');
8160if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
8161doc.head.appendChild(style);
8162// Force add-to-cart form to submit in TOP window so we return to cart normally
8163var form =
8164doc.querySelector('#product_addtocart_form') ||
8165doc.querySelector('form[action*="checkout/cart/add"]');
8166if (form) {
8167form.setAttribute('target', '_top');
8168// Ensure return_url points back to cart (best effort)
8169var existingReturn = form.querySelector('input[name="return_url"]');
8170if (!existingReturn) {
8171existingReturn = doc.createElement('input');
8172existingReturn.type = 'hidden';
8173existingReturn.name = 'return_url';
8174form.appendChild(existingReturn);
8175}
8176existingReturn.value = (MICRO_PDP.returnUrl || window.location.href);
8177// Some Magento setups use return_url_encoded; set it if present
8178var encodedReturn = form.querySelector('input[name="return_url_encoded"]');
8179if (encodedReturn) {
8180try {
8181encodedReturn.value = btoa(unescape(encodeURIComponent(MICRO_PDP.returnUrl || window.location.href)));
8182} catch (e) { }
8183}
8184// If there's a quantity input, default to 1
8185var qty = form.querySelector('input[name="qty"]');
8186if (qty && !qty.value) qty.value = '1';
8187// ---- LOADING CURSOR HOOK (reliable) ----
8188try {
8189// Zephyr's ATC is id="product-addtocart-button" and type="button",
8190// so look for it FIRST -- a bare button[type="submit"] lookup can
8191// match an unrelated submit (search / newsletter) earlier in the DOM.
8192var btn =
8193doc.querySelector('button#product-addtocart-button') ||
8194form.querySelector('button[type="submit"], input[type="submit"]') ||
8195doc.querySelector('button.addtocart') ||
8196doc.querySelector('button.action.tocart') ||
8197doc.querySelector('button[type="submit"]') ||
8198doc.querySelector('input[type="submit"]');
8199if (btn && !btn.__ckBound) {
8200btn.__ckBound = true;
8201btn.addEventListener('click', function () {
8202// NOTE: arming happens in the handleAjaxAddToCart
8203// wrapper, not here -- the button the shopper actually
8204// clicks is the sticky bar's, which lives outside
8205// #product_addtocart_form and may not be this element.
8206setLoadingCursor(true);
8207if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading');
8208}, { capture: true });
8209}
8210if (!form.__ckBound) {
8211form.__ckBound = true;
8212form.addEventListener('submit', function () {
8213setLoadingCursor(true);
8214if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading');
8215}, { capture: true });
8216}
8217} catch (e) {
8218// If iframe isn't accessible due to cross-domain, this will throw.
8219}
8220}
8221// Hook the iframe's own cart store so the side cart cannot open in
8222// here, and so the AJAX add gives us a reliable success signal.
8223try {
8224var ckSrc = String(MICRO_PDP.iframe.getAttribute('src') || '');
8225if (ckSrc && ckSrc.indexOf('about:blank') !== 0) bindMicroPdpAddBridge();
8226} catch (e) { }
8227// ----------------------------------------------------
8228// Cut everything below the shipping block (keep shipping visible)
8229// Anchor: .ships-and-made-in-usa-container
8230// ----------------------------------------------------
8231(function () {
8232var d = MICRO_PDP.iframe.contentDocument;
8233if (!d) return;
8234var shipContainer = d.querySelector('.ships-and-made-in-usa-container');
8235if (!shipContainer) return;
8236// Find a reasonable scope that contains the PDP “content” sections
8237var scope =
8238shipContainer.closest('.vpt-product, .product-shop-content, .product-info-main, .column.main, main')
8239|| d.body;
8240// We want to hide everything AFTER the shipping container's row
8241var row = shipContainer.closest('.row') || shipContainer.parentElement;
8242if (!row) return;
8243var sib = row.nextElementSibling;
8244while (sib) {
8245sib.style.display = 'none';
8246sib = sib.nextElementSibling;
8247}
8248})();
8249// ----------------------------------------------------
8250// Hide “Extra bottle of ink?” by label text (fallback)
8251// ----------------------------------------------------
8252(function () {
8253var d = MICRO_PDP.iframe.contentDocument;
8254if (!d) return;
8255var labels = Array.from(d.querySelectorAll('label'));
8256var inkLabel = labels.find(function (l) {
8257return (l.textContent || '').toLowerCase().includes('extra bottle of ink');
8258});
8259if (!inkLabel) return;
8260var field = inkLabel.closest('.field, .control, .admin__field, .product-options-wrapper') || inkLabel.parentElement;
8261if (field) field.style.display = 'none';
8262})();
8263// Done loading
8264if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none';
8265} catch (err) {
8266// If cross-domain or blocked, just hide loader after a beat
8267if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none';
8268}
8269});
8270// ----------------------------------------------------
8271// MICRO-PDP ADD BRIDGE (added 2026-09-11)
8272// Zephyr's PDP add-to-cart control is
8273// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // It never submits the form, so the form.target='_top' hook further down --
8274// which used to be the ONLY way the parent learned an add had happened -- is
8275// inert on a Zephyr PDP. The add is AJAX, and on success handleAjaxAddToCart
8276// calls Alpine.store('cart').openDrawer() INSIDE this iframe: the side cart
8277// slides out within the 960px modal and the parent cart page never updates.
8278//
8279// Fix: the iframe is same-origin, so we replace openDrawer() on the iframe's
8280// own cart store. That call is our exact "add succeeded" signal (it runs only
8281// in handleAjaxAddToCart's success branch) and the drawer never renders in the
8282// frame. We then reproduce the receipt state the old reload used to deliver in
8283// the URL -- via history.replaceState instead of navigating -- so the whole
8284// existing downstream pipeline (tile -> "added", tile swap, multi-SKU chaining)
8285// keeps working untouched. form.target='_top' stays as a fallback for any PDP
8286// that still submits natively.
8287// ----------------------------------------------------
8288var MICRO_BRIDGE = {
8289timer: null, loadTimer: null, tries: 0,
8290armed: false, handled: false,
8291addPatched: false, storePatched: false,
8292countBefore: null
8293};
8294// Zephyr exposes a running cart count at /zephyr/cart/count/ (verified live:
8295// returns {"count": N} where N is total UNITS, not lines -- so a qty bump on an
8296// existing line still moves it). Authoritative fallback for when the in-frame
8297// success signal never arrives, so a miss degrades to a late receipt, not silence.
8298function ckCartCount() {
8299try {
8300return fetch('/zephyr/cart/count/', {
8301credentials: 'same-origin',
8302headers: { 'X-Requested-With': 'XMLHttpRequest' }
8303})
8304.then(function (r) {
8305if (!r.ok) throw new Error('cart count HTTP ' + r.status);
8306return r.json();
8307})
8308.then(function (j) { return (j && typeof j.count === 'number') ? j.count : null; })
8309.catch(function (err) {
8310try { console.warn('[SmartCart] /zephyr/cart/count/ unavailable:', err && err.message); } catch (e) { }
8311return null;
8312});
8313} catch (e) {
8314return Promise.resolve(null);
8315}
8316}
8317// A 200 from /checkout/cart/add is NOT proof the cart changed: OpenMage
8318// answers a bare add for a product it can't sell (missing from this store
8319// view, disabled, required options) with a 200 and an empty result, and
8320// postToCart() only throws on !res.ok. Without this the module shows a full
8321// success state -- drawer, toast, tile swap -- over an unchanged cart.
8322function ckVerifySimpleAdd(baseline) {
8323if (typeof baseline !== 'number') return;   // no baseline: stay optimistic
8324setTimeout(function () {
8325ckCartCount().then(function (after) {
8326if (after === null || after > baseline) return;   // added, or can't tell
8327try { console.warn('[SmartCart] add returned 200 but the cart did not change (baseline ' + baseline + ').'); } catch (e) { }
8328var msg = 'That item could not be added \u2014 it may be unavailable. Please try another.';
8329try { if (statusEl) statusEl.textContent = msg; } catch (e) { }
8330try {
8331var fl = (window.Alpine && typeof window.Alpine.store === 'function') ? window.Alpine.store('flash') : null;
8332if (fl && typeof fl.show === 'function') fl.show('error', msg);
8333} catch (e) { }
8334// Don't leave a side cart open over an unchanged cart.
8335try {
8336var st = ckCartStore(window);
8337if (st && typeof st.closeDrawer === 'function') st.closeDrawer();
8338} catch (e) { }
8339});
8340}, 600);
8341}
8342// Clears the visual loading state ONLY. It must never disarm: the add can
8343// still be in flight, and disarming would make the openDrawer success signal
8344// get swallowed when it finally arrives.
8345function clearMicroLoadingState() {
8346try { setLoadingCursor(false); } catch (e) { }
8347try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { }
8348}
8349// Ask the server whether the add actually landed. Callers decide when it's
8350// worth asking; this only refuses to act if a receipt already fired.
8351// Polls rather than asking once. handleAjaxAddToCart returns a BOOLEAN, not a
8352// promise (measured on local 2026-09-14), so there is no completion callback --
8353// a real add on this catalog takes ~3.5s. A single check right after the click
8354// always misses, and treating that miss as failure is what left the modal open.
8355function confirmMicroAddByCount(reason, attemptsLeft) {
8356if (MICRO_BRIDGE.handled) return;
8357if (typeof attemptsLeft !== 'number') attemptsLeft = 0;
8358if (MICRO_BRIDGE.countBefore === null) {
8359try { console.warn('[SmartCart] cannot confirm micro-PDP add, no cart-count baseline (' + reason + ').'); } catch (e) { }
8360clearMicroLoadingState();
8361return;
8362}
8363var baseline = MICRO_BRIDGE.countBefore;
8364ckCartCount().then(function (after) {
8365if (MICRO_BRIDGE.handled) return;
8366if (after !== null && after > baseline) {
8367try { console.warn('[SmartCart] micro-PDP add confirmed by cart count (' + reason + ').'); } catch (e) { }
8368try { onMicroPdpAddSucceeded(); } catch (e) { }
8369return;
8370}
8371if (attemptsLeft > 0) {
8372setTimeout(function () {
8373confirmMicroAddByCount(reason, attemptsLeft - 1);
8374}, 1200);
8375return;
8376}
8377// Out of attempts. Drop the spinner but STAY ARMED -- if the add is
8378// simply slow, openDrawer will still arrive and fire the receipt.
8379clearMicroLoadingState();
8380});
8381}
8382function resetMicroBridge() {
8383try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { }
8384try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { }
8385MICRO_BRIDGE.timer = null;
8386MICRO_BRIDGE.loadTimer = null;
8387MICRO_BRIDGE.tries = 0;
8388MICRO_BRIDGE.armed = false;
8389MICRO_BRIDGE.addPatched = false;
8390MICRO_BRIDGE.storePatched = false;
8391// `handled` is deliberately NOT cleared -- closeMicroPdp() runs as part of a
8392// successful receipt. openMicroPdp() clears it, so the guard really is one
8393// receipt per configurator open.
8394}
8395// Arm on the ACTUAL add call, not a button click. Validation runs inside
8396// handleAjaxAddToCart, so armed only means "an add was attempted".
8397function armMicroBridge() {
8398MICRO_BRIDGE.armed = true;
8399try { setLoadingCursor(true); } catch (e) { }
8400try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); } catch (e) { }
8401// Long backstop only. The real resolution is onMicroAddSettled(), which
8402// fires the moment handleAjaxAddToCart's own promise settles -- so a slow
8403// upload resolves when it lands rather than being cut off by a fixed timer.
8404try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { }
8405MICRO_BRIDGE.loadTimer = setTimeout(function () {
8406if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return;
8407confirmMicroAddByCount('60s backstop', 0);
8408}, 60000);
8409}
8410// handleAjaxAddToCart returns its fetch promise. When it settles the add is
8411// decided: either our openDrawer signal already fired (handled), or it did not
8412// and we resolve it against the server instead of waiting out the backstop.
8413function onMicroAddSettled() {
8414// ~10s of polling (500ms + 8 x 1200ms), which comfortably covers a real
8415// add. The openDrawer signal normally wins long before this; this only
8416// catches an add that completes without ever reaching the Alpine store.
8417setTimeout(function () {
8418if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return;
8419confirmMicroAddByCount('no in-frame signal yet', 8);
8420}, 500);
8421}
8422function bindMicroPdpAddBridge() {
8423try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { }
8424MICRO_BRIDGE.timer = null;
8425MICRO_BRIDGE.tries = 0;
8426MICRO_BRIDGE.addPatched = false;
8427MICRO_BRIDGE.storePatched = false;
8428// Two hooks, both inside the iframe, both same-origin:
8429//   1. window.handleAjaxAddToCart -- wrapped to ARM on a real add call.
8430//      This is the pattern ss-tier-upsell-zephyr.html v1.32 proved out;
8431//      binding a button click is what it proved unreliable.
8432//   2. Alpine store cart.openDrawer -- the SUCCESS signal. Zephyr calls
8433//      it only in handleAjaxAddToCart's success branch, and overriding it
8434//      also keeps the side cart from rendering inside the 960px modal.
8435// handleAjaxAddToCart is an inline PDP script so it lands well before
8436// Alpine boots; poll for each independently.
8437MICRO_BRIDGE.timer = setInterval(function () {
8438MICRO_BRIDGE.tries++;
8439var w = null;
8440try { w = MICRO_PDP.iframe && MICRO_PDP.iframe.contentWindow; } catch (e) { w = null; }
8441if (!w) return;
8442if (!MICRO_BRIDGE.addPatched) {
8443try {
8444if (typeof w.handleAjaxAddToCart === 'function') {
8445if (w.handleAjaxAddToCart.__ckWrapped) {
8446MICRO_BRIDGE.addPatched = true;
8447} else {
8448var origAdd = w.handleAjaxAddToCart;
8449var wrapped = function () {
8450try { armMicroBridge(); } catch (eA) { }
8451var r;
8452try {
8453r = origAdd.apply(this, arguments);
8454} catch (eR) {
8455// Do NOT disarm here: origAdd may have dispatched
8456// the fetch before throwing, so the add can still
8457// land. Resolve it against the server like any other.
8458try { console.warn('[SmartCart] handleAjaxAddToCart threw:', eR && eR.message); } catch (eW) { }
8459try { onMicroAddSettled(); } catch (eS) { }
8460throw eR;
8461}
8462// The precise completion signal -- see onMicroAddSettled.
8463try {
8464// NOTE: on this catalog handleAjaxAddToCart returns a
8465// BOOLEAN, so the thenable branch is the exception, not
8466// the rule -- onMicroAddSettled() must therefore poll.
8467if (r && typeof r.then === 'function') r.then(onMicroAddSettled, onMicroAddSettled);
8468else onMicroAddSettled();
8469} catch (eT) { }
8470return r;
8471};
8472wrapped.__ckWrapped = true;
8473// The ATC uses an inline onclick, which resolves the
8474// global at call time -- so replacing it is enough.
8475w.handleAjaxAddToCart = wrapped;
8476MICRO_BRIDGE.addPatched = true;
8477}
8478}
8479} catch (eP1) { }
8480}
8481if (!MICRO_BRIDGE.storePatched) {
8482var st = ckCartStore(w);
8483if (st) {
8484try {
8485if (!st.__ckMicroPatched) {
8486st.__ckMicroPatched = true;
8487var suppress = function () {
8488// The side cart must never render in the configurator.
8489try { st.drawerOpen = false; } catch (e1) { }
8490try {
8491if (w && w.document && w.document.body) w.document.body.style.overflow = '';
8492} catch (e2) { }
8493if (!MICRO_BRIDGE.armed) return;
8494try { onMicroPdpAddSucceeded(); } catch (e3) { }
8495};
8496st.openDrawer = suppress;
8497st.toggleDrawer = suppress;
8498}
8499MICRO_BRIDGE.storePatched = true;
8500} catch (eP2) { }
8501}
8502}
8503if (MICRO_BRIDGE.addPatched && MICRO_BRIDGE.storePatched) {
8504try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { }
8505MICRO_BRIDGE.timer = null;
8506return;
8507}
8508if (MICRO_BRIDGE.tries > 120) { // ~6s
8509try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { }
8510MICRO_BRIDGE.timer = null;
8511// Never give up silently -- that is how this class of bug hides.
8512try {
8513console.warn('[SmartCart] micro-PDP bridge gave up after 6s.',
8514'handleAjaxAddToCart patched:', MICRO_BRIDGE.addPatched,
8515'| cart store patched:', MICRO_BRIDGE.storePatched,
8516'| the count fallback still covers the add.');
8517} catch (eW) { }
8518}
8519}, 50);
8520}
8521function onMicroPdpAddSucceeded() {
8522if (MICRO_BRIDGE.handled) return; // one receipt per configurator open
8523MICRO_BRIDGE.handled = true;
8524MICRO_BRIDGE.armed = false;
8525try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { }
8526// Recreate, in the parent's own URL, the receipt state the old
8527// target="_top" reload used to deliver -- without navigating.
8528try {
8529if (MICRO_PDP.returnUrl) {
8530var ru = new URL(MICRO_PDP.returnUrl, window.location.origin);
8531window.history.replaceState({}, document.title, ru.pathname + ru.search + ru.hash);
8532}
8533} catch (e) { }
8534// If more hard SKUs are queued the next configurator opens in ~350ms --
8535// don't pop the side cart underneath it. Reveal it only on the last one.
8536var moreQueued = false;
8537try { moreQueued = parseRemainingHardFromUrl().length > 0; } catch (e) { }
8538try { setLoadingCursor(false); } catch (e) { }
8539closeMicroPdp('added');
8540ckAfterCartMutation(!moreQueued);
8541// Existing receipt pipeline: added-state on the tile, tile swap, strip the
8542// params, and chain the next hard SKU if one is queued.
8543try { handleMicroAddedFromQuery(); } catch (e) { }
8544}
8545function openMicroPdp(productUrl, titleText, productId, remainingHardIds, progressDone, progressTotal) {
8546ensureMicroPdpModal();
8547MICRO_PDP.lastUrl = productUrl;
8548MICRO_PDP.lastProductId = String(productId || '');
8549// Progress defaults (if not passed)
8550var progFromUrl = getMicroProgressFromUrl();
8551if (typeof progressDone !== 'number') progressDone = progFromUrl.done;
8552if (typeof progressTotal !== 'number') progressTotal = progFromUrl.total;
8553MICRO_PDP.progressDone = progressDone || 0;
8554MICRO_PDP.progressTotal = progressTotal || 0;
8555// Paint header progress bar (only shows when total > 1)
8556renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal);
8557// Build a "receipt" return URL so the parent can show success + grey out tile + scroll
8558try {
8559var u = new URL(window.location.href);
8560// Set receipt params (overwrite if they already exist)
8561u.searchParams.set('microAdded', MICRO_PDP.lastProductId);
8562u.searchParams.set('microAddedName', titleText || '');
8563// Carry micro progress counts
8564if (progressTotal && progressTotal > 0) {
8565u.searchParams.set('microTotal', String(progressTotal));
8566u.searchParams.set('microDone', String(progressDone || 0));
8567} else {
8568u.searchParams.delete('microTotal');
8569u.searchParams.delete('microDone');
8570}
8571// If we are chaining multiple HARD SKUs, persist the remaining list
8572if (Array.isArray(remainingHardIds) && remainingHardIds.length) {
8573u.searchParams.set('remainingHard', remainingHardIds.join(','));
8574} else {
8575u.searchParams.delete('remainingHard');
8576}
8577// Keep anchor consistent so user lands near module area
8578u.hash = 'cart-kit-upsell';
8579MICRO_PDP.returnUrl = u.toString();
8580} catch (e) {
8581// Fallback if URL API fails
8582MICRO_PDP.returnUrl = window.location.href + (window.location.search ? '&' : '?')
8583+ 'microAdded=' + encodeURIComponent(MICRO_PDP.lastProductId)
8584+ '&microAddedName=' + encodeURIComponent(titleText || '')
8585+ '#cart-kit-upsell';
8586}
8587var titleEl = MICRO_PDP.overlay.querySelector('#ck-micropdp-title');
8588if (titleEl) titleEl.textContent = titleText || 'Customize item';
8589if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'flex';
8590// Add a cache-buster so iframe refreshes if same SKU reopened
8591var url = ckSameOrigin(productUrl);
8592url += (url.indexOf('?') > -1 ? '&' : '?') + 'cartUpsellMicro=1&_=' + Date.now();
8593// Fresh receipt state + a count snapshot for the fallback in armMicroBridge.
8594MICRO_BRIDGE.handled = false;
8595MICRO_BRIDGE.countBefore = null;
8596MICRO_BRIDGE.countGen = (MICRO_BRIDGE.countGen || 0) + 1;
8597try {
8598// Tagged so a slow response from the PREVIOUS chain item (opens ~350ms
8599// apart) can't land here and produce a false-positive receipt.
8600(function (gen) {
8601ckCartCount().then(function (n) {
8602if (gen === MICRO_BRIDGE.countGen) MICRO_BRIDGE.countBefore = n;
8603});
8604})(MICRO_BRIDGE.countGen);
8605} catch (e) { }
8606MICRO_PDP.iframe.src = url;
8607MICRO_PDP.overlay.classList.add('is-open');
8608pushUpsellEvent('SmartCart_upsell_micro_pdp_open', { url: productUrl });
8609}
8610function closeMicroPdp(reason) {
8611if (!MICRO_PDP.overlay) return;
8612var added = (reason === 'added');
8613var ckWasArmed = MICRO_BRIDGE.armed;
8614try { resetMicroBridge(); } catch (e) { }
8615// Shopper closed while an add was in flight. Don't drop it on the floor --
8616// check the server before discarding the tile state and the chain.
8617if (!added && ckWasArmed) {
8618try { confirmMicroAddByCount('closed mid-add', 3); } catch (e) { }
8619}
8620try { setLoadingCursor(false); } catch (e) { }
8621try { MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { }
8622MICRO_PDP.overlay.classList.remove('is-open');
8623// Stop network / reset iframe
8624if (MICRO_PDP.iframe) MICRO_PDP.iframe.src = 'about:blank';
8625if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none';
8626// Tell anything listening that the micro PDP closed
8627try {
8628document.dispatchEvent(new CustomEvent('smartcart:microPdpClosed', { detail: { reason: reason || 'close' } }));
8629} catch (e) { }
8630// CRITICAL: refresh cart UI and un-stick the SmartCart state
8631Promise.resolve()
8632.then(function () {
8633// Force the same refresh path we use after adds
8634// 1) Reload Magento customer-data cart section (minicart + cart counters)
8635// 2) Refresh cart DOM fragments (table/totals) if we’re on the cart page
8636// Legacy (pre-Zephyr) refresh path -- a no-op on Zephyr, kept for safety.
8637return reloadCustomerDataCart()
8638.then(refreshCartDomIfOnCartPage)
8639.then(function () {
8640// After an add the caller syncs; on a plain close re-sync
8641// anyway in case the shopper added inside the configurator
8642// by some other route.
8643if (!added) ckAfterCartMutation(false);
8644});
8645})
8646.then(function () {
8647// reset CTA back to normal (user cancelled hard sku)
8648try {
8649addBtn.classList.remove('is-loading');
8650// This tail resolves after handleMicroAddedFromQuery() has run.
8651// On a SUCCESSFUL add the receipt pipeline owns the status line,
8652// the progress bar and the tile grid -- buildGrid() in particular
8653// would re-render over replaceAddedTileWithNext()'s swap. Touch
8654// none of it unless the shopper cancelled.
8655if (!added) {
8656addBtn.disabled = false;
8657addBtn.textContent = 'Add to Cart';
8658if (statusEl) statusEl.textContent = 'No worries — you can keep shopping or add something else.';
8659resetAddProgressUI();
8660}
8661// Safe on BOTH paths and required on the added one: derives the
8662// CTA's disabled state and label from the live selection, so a
8663// completed add leaves it correctly disabled rather than enabled
8664// with nothing selected.
8665updatePriceUI();
8666} catch (e) { }
8667// Rebuild tiles so "already in cart" suppression is correct
8668if (!added) { try { buildGrid(); } catch (e) { } }
8669})
8670.catch(function () {
8671// Even if refresh fails, never leave the UI stuck
8672try {
8673addBtn.disabled = false;
8674addBtn.classList.remove('is-loading');
8675addBtn.textContent = 'Add to Cart';
8676} catch (e) { }
8677});
8678}
8679// ----------------------------------------------------
8680// 8) Selection state
8681// ----------------------------------------------------
8682var selectedPrimaryIds = new Set(); // stamp IDs OR notary product IDs (strings)
8683var selectedInkColors = new Set();
8684var inkChoiceMade = false;
8685// Track tiles that have been added via Micro PDP (so we can grey them out)
8686var microAddedIds = new Set();
8687var ckSeenTileIds = new Set();
8688// Defaults
8689var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0);
8690if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId));
8691else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id);
8692// ----------------------------------------------------
8693// 9) Build grid (Accent or Notary)
8694// ----------------------------------------------------
8695var NOTARY_HYDRATED = new Map(); // productId -> {productId, productUrl, name, img, price}
8696function buildGrid() {
8697// Build cart signals for duplicate suppression (4C)
8698var __cartPaths = Object.create(null);
8699try { __cartPaths = getCartProductPathSet(); } catch (eCP0) { }
8700stampGrid.innerHTML = '';
8701// Accent stamps (unchanged)
8702if (!useTiles) {
8703for (var i = 0; i < STAMP_OPTIONS.length; i++) {
8704var opt = STAMP_OPTIONS[i];
8705var card = document.createElement('button');
8706card.type = 'button';
8707card.className = 'kit-stamp-card';
8708card.setAttribute('data-kit-primary', opt.id);
8709card.innerHTML =
8710'<div class="kit-stamp-chip">'
8711+ '<div class="kit-stamp-chip__circle">'
8712+ '<img class="kit-stamp-chip__image"'
8713+ ' src="' + opt.imgSrc + '"'
8714+ ' alt="' + (opt.imgAlt || '') + '"'
8715+ (opt.imgStyle ? ' style="' + opt.imgStyle + '"' : '')
8716+ '/>'
8717+ '</div>'
8718+ '</div>';
8719if (selectedPrimaryIds.has(opt.id)) card.classList.add('is-selected');
8720stampGrid.appendChild(card);
8721}
8722// Step 1: sync tile state store after grid render
8723try { TileStore.initFromDom(mount); } catch (e) { }
8724return;
8725}
8726// Notary product tiles (PDP fetched)
8727// Empty-state: if everything is already in cart (or filtered), show "All set" instead of a blank module.
8728if (!TILE_ITEMS || TILE_ITEMS.length === 0) {
8729// Nothing to build. (The old code here appended an "all set" note next
8730// to an undefined `tilesGrid` and tried to disable a #kit-add-btn that
8731// has never existed in this markup -- both silent no-ops.)
8732ckSyncModuleEmptyState();
8733return;
8734}
8735for (var j = 0; j < TILE_ITEMS.length; j++) {
8736(function (raw) {
8737var pid = String(raw.productId || '');
8738// Keep a reference so we can apply persona rules later (micro PDP gating, etc.)
8739TILE_ITEM_BY_ID[pid] = raw;
8740var card = document.createElement('button');
8741card.type = 'button';
8742card.className = 'kit-stamp-card kit-product-card';
8743card.setAttribute('data-kit-primary', pid);
8744card.setAttribute('data-sku', pid);
8745card.setAttribute('data-kit-url', raw.productUrl || '');
8746if (microAddedIds.has(pid)) {
8747card.classList.add('is-added');
8748}
8749card.innerHTML =
8750'<div class="kit-product-thumb-tile"><span style="font-size:12px;color:#888;">Loading…</span></div>'
8751+ '<div class="kit-product-meta">'
8752+ '<p class="kit-product-name">Loading…</p>'
8753+ '<div class="kit-product-price">—</div>'
8754+ '<div class="kit-product-sub">' + (persona.tileSubtext || '') + '</div>'
8755+ '</div>';
8756if (selectedPrimaryIds.has(pid)) card.classList.add('is-selected');
8757stampGrid.appendChild(card);
8758fetchPdpData(raw.productUrl).then(function (data) {
8759// Guard: this tile may have been swapped; ignore stale hydration
8760if (String(card.getAttribute('data-kit-primary') || '') !== String(pid)) return;
8761// Quality gate: this URL did not resolve to a real product page,
8762// so do NOT render whatever came back. Substitute an approved
8763// alternative the shopper doesn't already have, else drop the slot.
8764if (!ckIsUsableTileData(data)) {
8765try { console.warn('[SmartCart] tile ' + pid + ' did not resolve to a product page; substituting.'); } catch (eW) { }
8766var alt = ckPickFallbackTile();
8767if (alt) ckRepaintCardAs(card, alt);
8768else ckDropTile(card);
8769return;
8770}
8771var hydrated = {
8772productId: pid,
8773productUrl: raw.productUrl,
8774name: data.name || 'Notary add-on',
8775img: data.img || null,
8776price: (data.price != null ? Number(data.price) : null)
8777};
8778try { hydrated.sku = data.sku; } catch (eSk1) { }
8779NOTARY_HYDRATED.set(pid, hydrated);
8780var nameEl = card.querySelector('.kit-product-name');
8781var priceEl = card.querySelector('.kit-product-price');
8782var thumbEl = card.querySelector('.kit-product-thumb-tile');
8783if (nameEl) nameEl.textContent = hydrated.name;
8784if (priceEl) priceEl.textContent = (hydrated.price != null ? ('$' + hydrated.price.toFixed(2)) : 'See price in cart');
8785if (thumbEl) {
8786if (hydrated.img) thumbEl.innerHTML = '<img src="' + hydrated.img + '" alt="" />';
8787else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>';
8788}
8789if (selectedPrimaryIds.has(pid)) updatePriceUI();
8790});
8791})(TILE_ITEMS[j]);
8792}
8793fireTilesImpressionOnce();
8794// Step 1: sync tile state store after grid render
8795try { TileStore.initFromDom(mount); } catch (e) { }
8796ckSyncModuleEmptyState();
8797}
8798// ----------------------------------------------------
8799// 10) Pricing
8800// ----------------------------------------------------
8801var priceLabelEl = upsell.querySelector('#kit-price-label');
8802var priceNewEl = upsell.querySelector('#kit-price-new');
8803function computeTotals() {
8804var inkCount = selectedInkColors.size;
8805if (!useTiles) {
8806var stampCount = selectedPrimaryIds.size;
8807return {
8808primaryCount: stampCount,
8809inkCount: inkCount,
8810total: (stampCount * STAMP_UNIT_PRICE) + (inkCount * INK_PRICE)
8811};
8812}
8813// Notary: sum selected notary items (real PDP price if fetched; otherwise 0 until loaded)
8814var total = 0;
8815var count = 0;
8816selectedPrimaryIds.forEach(function (pid) {
8817var it = NOTARY_HYDRATED.get(String(pid));
8818count += 1;
8819total += (it && it.price != null) ? Number(it.price) : 0;
8820});
8821total += inkCount * INK_PRICE;
8822return { primaryCount: count, inkCount: inkCount, total: total };
8823}
8824function updatePriceUI() {
8825var totals = computeTotals();
8826if (!totals.primaryCount) {
8827priceLabelEl.textContent = 'Total:';
8828priceNewEl.textContent = '$0.00';
8829addBtn.disabled = true;
8830addBtn.textContent = 'Add to Cart';
8831return;
8832}
8833priceLabelEl.textContent = 'Total:';
8834priceNewEl.textContent = '$' + totals.total.toFixed(2);
8835// CTA label: if any selected item requires configuration, guide user into Micro PDP.
8836var hasHard = false;
8837try {
8838Array.from(selectedPrimaryIds).some(function (pid) {
8839var raw = (TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[String(pid)]) ? TILE_ITEM_BY_ID[String(pid)] : null;
8840if (requiresMicroPdp(pid, raw)) { hasHard = true; return true; }
8841return false;
8842});
8843} catch (e) { }
8844addBtn.textContent = hasHard ? 'Customize & Add' : 'Add to Cart';
8845addBtn.disabled = false;
8846}
8847// ----------------------------------------------------
8848// 11) Build + init
8849// ----------------------------------------------------
8850buildGrid();
8851updatePriceUI();
8852pushUpsellEvent('SmartCart_upsell_shown', { matched: activePersonaKey !== 'generic' });
8853// ----------------------------------------------------
8854// 12) Interactions
8855// ----------------------------------------------------
8856header.addEventListener('click', function () {
8857var isOpen = upsell.classList.toggle('cart-kit-upsell--open');
8858body.hidden = !isOpen;
8859});
8860// Primary selection (stamps or notary items)
8861stampGrid.addEventListener('click', function (evt) {
8862var card = evt.target.closest('.kit-stamp-card');
8863if (!card) return; if (card.classList && card.classList.contains('is-complete')) return;
8864var id = String(card.getAttribute('data-kit-primary') || '');
8865if (!id) return;
8866// ---- TRACK: tile click ----
8867try {
8868if (useTiles) {
8869// find tile position in TILE_ITEMS
8870var pos = -1;
8871var rawTile = null;
8872for (var i = 0; i < TILE_ITEMS.length; i++) {
8873if (String(TILE_ITEMS[i].productId) === String(id)) {
8874pos = i + 1;
8875rawTile = TILE_ITEMS[i];
8876break;
8877}
8878}
8879var hydrated = NOTARY_HYDRATED.get(String(id));
8880pushUpsellEvent('SmartCart_upsell_tile_click', {
8881persona: activePersonaKey || 'generic',
8882upsell_mode: 'tiles',
8883product_id: id,
8884position: pos,
8885product_name: hydrated && hydrated.name ? hydrated.name : undefined,
8886product_price: hydrated && hydrated.price != null ? hydrated.price : undefined,
8887product_url: rawTile && rawTile.productUrl ? rawTile.productUrl : undefined,
8888requires_micro: rawTile ? requiresMicroPdp(id, rawTile) : undefined,
8889already_selected: selectedPrimaryIds.has(id)
8890});
8891} else {
8892// Accent stamp click
8893pushUpsellEvent('SmartCart_upsell_accent_click', {
8894stamp_id: id
8895});
8896}
8897} catch (e) { }
8898// ----------------------------------------------------
8899// Tile tracking + micro PDP routing (tiles only)
8900// ----------------------------------------------------
8901var raw = null;
8902if (useTiles) {
8903for (var k = 0; k < TILE_ITEMS.length; k++) {
8904if (String(TILE_ITEMS[k].productId) === String(id)) { raw = TILE_ITEMS[k]; break; }
8905}
8906if (raw && raw.productUrl) {
8907// If we already know it's micro (tile config, hard override, or cached autodetect)
8908if (requiresMicroPdp(id, raw)) {
8909// Hard SKU: tile click should ONLY toggle selection.
8910// Do not open Micro PDP here; Micro PDP opens only from the main CTA.
8911}
8912// If we don't have PDP cached yet, fetch once and decide (airbag)
8913if (!PDP_CACHE.has(raw.productUrl)) {
8914evt.preventDefault();
8915evt.stopPropagation();
8916setLoadingCursor(true);
8917fetchPdpData(raw.productUrl).then(function (data) {
8918setLoadingCursor(false);
8919// If autodetect says "micro", open modal
8920if (data && data.requiresMicro) {
8921// NOTE: Do NOT auto-open Micro PDP on tile click.
8922// We only open Micro PDP when the user explicitly clicks the CTA to add.
8923pushUpsellEvent('SmartCart_upsell_tile_micro_detected', { productId: id, productUrl: raw.productUrl });
8924}
8925// Otherwise proceed with normal select toggle now
8926var willSelect = !selectedPrimaryIds.has(id);
8927if (willSelect) {
8928selectedPrimaryIds.add(id);
8929card.classList.add('is-selected');
8930} else {
8931selectedPrimaryIds.delete(id);
8932card.classList.remove('is-selected');
8933}
8934pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), {
8935productId: id,
8936selected_count: selectedPrimaryIds.size
8937});
8938updatePriceUI();
8939}).catch(function () {
8940setLoadingCursor(false);
8941// On failure, fall back to normal select so UX doesn't dead-end
8942var willSelect2 = !selectedPrimaryIds.has(id);
8943if (willSelect2) {
8944selectedPrimaryIds.add(id);
8945card.classList.add('is-selected');
8946} else {
8947selectedPrimaryIds.delete(id);
8948card.classList.remove('is-selected');
8949}
8950pushUpsellEvent('SmartCart_upsell_tile_select_fallback', {
8951productId: id,
8952// This path fires on DESELECT as well as select.
8953// Without this flag the GA4 mirror would count a
8954// deselect as a select_item.
8955selected: willSelect2,
8956selected_count: selectedPrimaryIds.size
8957});
8958updatePriceUI();
8959});
8960return;
8961}
8962}
8963}
8964var willSelect = !selectedPrimaryIds.has(id);
8965if (willSelect) {
8966selectedPrimaryIds.add(id);
8967card.classList.add('is-selected');
8968} else {
8969selectedPrimaryIds.delete(id);
8970card.classList.remove('is-selected');
8971}
8972if (useTiles) {
8973pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), {
8974productId: id,
8975selected_count: selectedPrimaryIds.size
8976});
8977} else {
8978pushUpsellEvent('SmartCart_upsell_accent_' + (willSelect ? 'selected' : 'deselected'), {
8979stamp_id: id,
8980selected_count: selectedPrimaryIds.size
8981});
8982}
8983updatePriceUI();
8984});
8985// Ink multi-select with "none"
8986if (swatchList) {
8987swatchList.addEventListener('click', function (evt) {
8988var swatch = evt.target.closest('.kit-swatch');
8989if (!swatch) return;
8990if (swatch.style.display === 'none') return;
8991var colorKey = swatch.getAttribute('data-kit-color');
8992inkChoiceMade = true;
8993if (colorKey === 'none') {
8994selectedInkColors.clear();
8995var all = swatchList.querySelectorAll('.kit-swatch');
8996for (var i = 0; i < all.length; i++) {
8997var s = all[i];
8998if (s.style.display === 'none') continue;
8999s.classList.toggle('is-selected', s.getAttribute('data-kit-color') === 'none');
9000}
9001} else {
9002if (selectedInkColors.has(colorKey)) selectedInkColors.delete(colorKey);
9003else selectedInkColors.add(colorKey);
9004var hasAnyInk = selectedInkColors.size > 0;
9005var all2 = swatchList.querySelectorAll('.kit-swatch');
9006for (var j = 0; j < all2.length; j++) {
9007var b = all2[j];
9008if (b.style.display === 'none') continue;
9009var key = b.getAttribute('data-kit-color');
9010if (key === 'none') b.classList.toggle('is-selected', !hasAnyInk);
9011else b.classList.toggle('is-selected', selectedInkColors.has(key));
9012}
9013}
9014updatePriceUI();
9015});
9016}
9017if (hideBtn) {
9018hideBtn.addEventListener('click', function (evt) {
9019evt.preventDefault();
9020upsell.classList.remove('cart-kit-upsell--open');
9021body.hidden = true;
9022upsell.scrollIntoView({ behavior: 'smooth', block: 'start' });
9023});
9024}
9025// ----------------------------------------------------
9026// 13)  Add-to-cart progress bar helpers (ONLY progress bar)
9027// ----------------------------------------------------
9028function resetAddProgressUI() {
9029if (progressWrap) progressWrap.style.display = 'none';
9030if (progressFill) progressFill.style.width = '0%';
9031if (progressPct) progressPct.textContent = '0%';
9032if (progressText) progressText.textContent = '';
9033}
9034function setAddProgress(done, total) {
9035if (!progressWrap || !progressFill) return;
9036var pct = total ? Math.round((done / total) * 100) : 0;
9037progressWrap.style.display = '';
9038progressFill.style.width = pct + '%';
9039if (progressPct) progressPct.textContent = pct + '%';
9040if (progressText) progressText.textContent = 'Adding ' + done + ' of ' + total + ' items…';
9041if (statusEl) statusEl.textContent = 'Added ' + done + ' of ' + total + ' items';
9042}
9043// ----------------------------------------------------
9044// 14) Add to Cart
9045// ----------------------------------------------------
9046function postToCart(params) {
9047return fetch('/checkout/cart/add', {
9048method: 'POST',
9049headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
9050body: params.toString(),
9051credentials: 'same-origin'
9052}).then(function (res) {
9053if (!res.ok) throw new Error('Add to cart failed: ' + res.status);
9054return res;
9055});
9056}
9057// ----------------------------------------------------
9058// Cart sync helpers (run after async adds so UI + tile logic are up-to-date)
9059// ----------------------------------------------------
9060function reloadCustomerDataCart() {
9061// Zephyr: /customer/section/load/ 404s on this build (no M2 customer-data)
9062// and the Alpine store is the real cart state. Skip the hop entirely.
9063if (ckCartStore(window)) return Promise.resolve();
9064return new Promise(function (resolve) {
9065try {
9066if (window.require) {
9067window.require(['Magento_Customer/js/customer-data'], function (customerData) {
9068try {
9069var r = customerData.reload(['cart'], true);
9070if (r && r.always) return r.always(function () { resolve(); });
9071resolve();
9072} catch (e) { resolve(); }
9073}, function () { resolve(); });
9074return;
9075}
9076fetch('/customer/section/load/?sections=cart&force_new_section_timestamp=true', {
9077credentials: 'same-origin'
9078}).then(function () { resolve(); }).catch(function () { resolve(); });
9079} catch (e) { resolve(); }
9080});
9081}
9082function refreshCartDomIfOnCartPage() {
9083// Zephyr: the cart page is Alpine-rendered. This swaps innerHTML on
9084// .cart-summary / [data-block="cart-summary"] if either matches, which
9085// would destroy live Alpine bindings in the Order Summary -- and it costs
9086// a full-page HTML fetch every time. ckAfterCartMutation() is the Zephyr
9087// path; this one is for the legacy theme only.
9088if (ckCartStore(window)) return Promise.resolve();
9089return new Promise(function (resolve) {
9090try {
9091// Cart page fragments we attempt to refresh. Selectors vary by theme.
9092var table = document.querySelector('#shopping-cart-table');
9093var totals = document.querySelector('#cart-totals')
9094|| document.querySelector('.cart-summary')
9095|| document.querySelector('.cart-totals')
9096|| document.querySelector('[data-block="cart-summary"]')
9097|| document.querySelector('[data-role="cart-totals"]');
9098if (!table && !totals) return resolve();
9099// Bust caches (some themes/CDNs will otherwise hand back stale totals)
9100var url = String(window.location.href || '').split('#')[0];
9101url += (url.indexOf('?') > -1 ? '&' : '?') + 'ckts=' + Date.now();
9102fetch(url, { credentials: 'same-origin', cache: 'no-store' })
9103.then(function (r) { return r.text(); })
9104.then(function (html) {
9105try {
9106var doc = new DOMParser().parseFromString(html, 'text/html');
9107var newTable = doc.querySelector('#shopping-cart-table');
9108if (table && newTable) table.innerHTML = newTable.innerHTML;
9109// Totals / summary fragment (try multiple fallbacks)
9110var newTotals = doc.querySelector('#cart-totals')
9111|| doc.querySelector('.cart-summary')
9112|| doc.querySelector('.cart-totals')
9113|| doc.querySelector('[data-block="cart-summary"]')
9114|| doc.querySelector('[data-role="cart-totals"]');
9115if (totals && newTotals) totals.innerHTML = newTotals.innerHTML;
9116try { document.body.dispatchEvent(new Event('contentUpdated')); } catch (e) { }
9117} catch (e2) { }
9118resolve();
9119})
9120.catch(function () { resolve(); });
9121} catch (e) { resolve(); }
9122});
9123}
9124function syncCartAfterAdds() {
9125return reloadCustomerDataCart()
9126.then(refreshCartDomIfOnCartPage)
9127.catch(function () { /* silent */ });
9128}
9129addBtn.addEventListener('click', function () {
9130if (!selectedPrimaryIds.size) {
9131alert(useTiles ? 'Please choose at least one add-on.' : 'Please choose at least one stamp design.');
9132return;
9133}
9134if (swatchList && !inkChoiceMade) {
9135alert('Please select an ink pad color or select you already have an ink pad');
9136return;
9137}
9138var formKeyInput = document.querySelector('input[name="form_key"]');
9139var formKey = formKeyInput ? formKeyInput.value : null;
9140if (!formKey) {
9141console.error('Cart kit upsell: form_key not found.');
9142alert('Sorry, we could not add the kit to your cart. Please refresh and try again.');
9143return;
9144}
9145// Baseline for the post-add reality check (see ckVerifySimpleAdd).
9146var ckSimpleCountBefore = null;
9147try { ckCartCount().then(function (n) { ckSimpleCountBefore = n; }); } catch (eCB) { }
9148var primaryArr = Array.from(selectedPrimaryIds);
9149var inkArr = Array.from(selectedInkColors);
9150// ----------------------------------------------------
9151// Hard SKU gating:
9152// - NEVER add "hard" products directly (they need options/uploads)
9153// - Only open Micro PDP when user clicks the CTA
9154// ----------------------------------------------------
9155var hardPrimaryArr = [];
9156var simplePrimaryArr = [];
9157primaryArr.forEach(function (pid) {
9158var rawTile = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? TILE_ITEM_BY_ID[String(pid)] : null;
9159if (requiresMicroPdp(pid, rawTile)) {
9160hardPrimaryArr.push(pid);
9161} else {
9162simplePrimaryArr.push(pid);
9163}
9164});
9165// Only count items we can actually add via POST chain (hard items are handled via Micro PDP)
9166var totalItemsCount = simplePrimaryArr.length + inkArr.length;
9167// If the user only selected hard items, we skip the POST chain and jump straight to Micro PDP.
9168var willOpenMicroAfterChain = hardPrimaryArr.length > 0;
9169pushUpsellEvent('SmartCart_upsell_add_clicked', {
9170primary_count: simplePrimaryArr.length,
9171hard_primary_count: hardPrimaryArr.length,
9172ink_count: inkArr.length,
9173total_count: totalItemsCount
9174});
9175var completedCount = 0;
9176// If there's nothing to POST (only hard items), open Micro PDP immediately.
9177if (totalItemsCount === 0 && willOpenMicroAfterChain) {
9178var firstHard = hardPrimaryArr[0];
9179var rawFirst = TILE_ITEM_BY_ID[String(firstHard)] || {};
9180var hydratedFirst = NOTARY_HYDRATED.get(String(firstHard));
9181var titleFirst = (hydratedFirst && hydratedFirst.name) ? hydratedFirst.name : (rawFirst.title || rawFirst.name || 'Customize item');
9182// Seed Micro PDP progress for HARD-only flows
9183// done = 0 (nothing added yet), total = number of hard items selected
9184var progDone0 = 0;
9185var progTotal0 = hardPrimaryArr.length;
9186openMicroPdp(rawFirst.productUrl, titleFirst, firstHard, hardPrimaryArr.slice(1), progDone0, progTotal0);
9187return;
9188}
9189addBtn.disabled = true;
9190addBtn.classList.add('is-loading');
9191addBtn.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' items…') : 'Adding item…';
9192if (statusEl) statusEl.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' items to your cart…') : 'Adding item to your cart…';
9193//  show + init progress bar
9194setAddProgress(0, totalItemsCount);
9195var chain = Promise.resolve();
9196// Add primary selections
9197simplePrimaryArr.forEach(function (id) {
9198chain = chain.then(function () {
9199var params = new URLSearchParams();
9200params.append('form_key', formKey);
9201if (!useTiles) {
9202params.append('product', id);
9203params.append('qty', '1');
9204params.append('options[' + ACCENT_SIZE_OPTION_ID + '][]', ACCENT_SIZE_VALUE_ID);
9205params.append('options[' + ACCENT_INK_OPTION_ID + ']', ACCENT_INK_NO_THANKS_VALUE_ID);
9206} else {
9207params.append('product', id);
9208params.append('qty', '1');
9209}
9210return postToCart(params).then(function () {
9211completedCount += 1;
9212setAddProgress(completedCount, totalItemsCount);
9213});
9214});
9215});
9216// Add ink pads
9217inkArr.forEach(function (colorKey) {
9218chain = chain.then(function () {
9219var colorValueId = INK_COLOR_VALUES[colorKey];
9220if (!colorValueId) throw new Error('Unknown ink color key: ' + colorKey);
9221var inkParams = new URLSearchParams();
9222inkParams.append('form_key', formKey);
9223inkParams.append('product', INK_PARENT_ID);
9224inkParams.append('qty', '1');
9225inkParams.append('options[' + INK_SIZE_OPTION_ID + ']', INK_SIZE_VALUE_ID);
9226inkParams.append('options[' + INK_COLOR_OPTION_ID + ']', colorValueId);
9227return postToCart(inkParams).then(function () {
9228completedCount += 1;
9229setAddProgress(completedCount, totalItemsCount);
9230});
9231});
9232});
9233chain
9234.then(function () {
9235pushUpsellEvent('SmartCart_upsell_added', {
9236// Per-product ids drive the GA4 add_to_cart mirror.
9237product_ids: simplePrimaryArr.map(function (p) { return String(p); }),
9238ink_colors: inkArr.slice(),
9239primary_count: simplePrimaryArr.length,
9240hard_primary_count: hardPrimaryArr.length,
9241ink_count: inkArr.length,
9242total_count: totalItemsCount
9243});
9244// CRITICAL: sync cart state + DOM BEFORE swap/dedupe logic runs
9245return syncCartAfterAdds().then(function () {
9246if (willOpenMicroAfterChain) {
9247// The simple items are already in the cart but the configurator
9248// is about to take over the screen -- push them into both cart
9249// trees now, or the drawer, badge and cart column sit stale for
9250// as long as the shopper spends configuring.
9251try { ckAfterCartMutation(false); } catch (eZ) { }
9252// Mark the items we successfully added (simple + ink) without reloading.
9253try {
9254simplePrimaryArr.forEach(function (pid) {
9255var el = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]');
9256if (el) {
9257el.classList.remove('is-selected');
9258el.classList.add('is-added');
9259}
9260try { microAddedIds.add(String(pid)); } catch (eM0) { }
9261try { selectedPrimaryIds.delete(pid); } catch (eD0) { }
9262//  Swap AFTER cart sync so in-cart suppression works
9263setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR0) { } }, 800);
9264});
9265} catch (e) { }
9266try {
9267// Clear ink selections after adding ink pads
9268selectedInkColors.clear();
9269inkChoiceMade = false;
9270if (swatchList) {
9271var allSw = swatchList.querySelectorAll('.kit-swatch');
9272for (var s = 0; s < allSw.length; s++) allSw[s].classList.remove('is-selected');
9273}
9274} catch (e2) { }
9275addBtn.disabled = false;
9276addBtn.classList.remove('is-loading');
9277addBtn.textContent = 'Customize & Add';
9278if (statusEl) statusEl.textContent = 'Added to cart. Next, customize your selected item.';
9279// Open Micro PDP for the first hard item selected
9280var firstHard2 = hardPrimaryArr[0];
9281var rawFirst2 = TILE_ITEM_BY_ID[String(firstHard2)] || {};
9282var hydratedFirst2 = NOTARY_HYDRATED.get(String(firstHard2));
9283var titleFirst2 = (hydratedFirst2 && hydratedFirst2.name) ? hydratedFirst2.name : (rawFirst2.title || rawFirst2.name || 'Customize item');
9284// Seed Micro PDP progress for mixed flows:
9285// done = count of items already added via POST chain (simple + ink)
9286// total = done + remaining hard items to configure
9287var progDone1 = totalItemsCount;
9288var progTotal1 = totalItemsCount + hardPrimaryArr.length;
9289openMicroPdp(rawFirst2.productUrl, titleFirst2, firstHard2, hardPrimaryArr.slice(1), progDone1, progTotal1);
9290pushUpsellEvent('SmartCart_upsell_micro_pdp_open_from_cta', {
9291productId: firstHard2,
9292remaining_hard_count: hardPrimaryArr.length
9293});
9294// Keep progress visible but "complete" the chain we ran
9295setAddProgress(totalItemsCount, totalItemsCount);
9296return; // prevent redirect
9297}
9298// If we are on the full cart page, do a hard refresh so Magento totals/summary
9299// update correctly (these often won't update reliably via partial DOM swaps).
9300// We preserve the success-state UX via ?accentAdded=1.
9301// ----------------------------------------------------
9302// Zephyr: the rest of this site mutates the cart in place, so do
9303// the same here instead of reloading. ckAfterCartMutation refreshes
9304// BOTH cart trees (drawer store + cart-page component) and pops the
9305// side cart. It returns false only when the Zephyr store is absent
9306// (legacy theme) -- then, and only then, fall back to the old
9307// ?accentAdded=1 reload. Returning true lets the in-place success UX
9308// below finally run; that block was unreachable for as long as this
9309// branch always reloaded.
9310// ----------------------------------------------------
9311try {
9312var onCartPage = document.body.classList.contains('checkout-cart-index')
9313|| /\/checkout\/cart(\/|$)/.test(window.location.pathname)
9314|| !!document.querySelector('[data-cart-item-id]')
9315|| !!document.querySelector('#shopping-cart-table')
9316|| !!document.querySelector('form#form-validate')
9317|| !!document.querySelector('.cart-container')
9318|| !!document.querySelector('[data-block="cart-summary"]')
9319|| !!document.querySelector('.cart-summary');
9320if (onCartPage && !ckAfterCartMutation(true)) {
9321try {
9322var u2 = new URL(window.location.href);
9323u2.searchParams.set('accentAdded', '1');
9324window.location.href = u2.toString();
9325} catch (eU2) {
9326window.location.reload();
9327}
9328return;
9329}
9330} catch (eCPR) { }
9331// Reality check: confirm the cart actually changed before the
9332// success UX below is allowed to stand.
9333try { ckVerifySimpleAdd(ckSimpleCountBefore); } catch (eVS) { }
9334// ----------------------------------------------------
9335// No hard items left → keep the module live and refresh tiles in-place (no page reload)
9336// ----------------------------------------------------
9337try {
9338// Mark added + swap tiles (stable slots)
9339simplePrimaryArr.forEach(function (pid) {
9340try { microAddedIds.add(String(pid)); } catch (eM) { }
9341try {
9342var el2 = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]');
9343if (el2) {
9344el2.classList.remove('is-selected');
9345el2.classList.add('is-added');
9346}
9347} catch (eC) { }
9348try { selectedPrimaryIds.delete(pid); } catch (eD) { }
9349//  Swap AFTER cart sync so in-cart suppression works
9350setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR) { } }, 800);
9351});
9352} catch (eS3) { }
9353try {
9354// Clear ink selections after adding ink pads
9355selectedInkColors.clear();
9356inkChoiceMade = false;
9357if (swatchList) {
9358var allSw3 = swatchList.querySelectorAll('.kit-swatch');
9359for (var s3 = 0; s3 < allSw3.length; s3++) allSw3[s3].classList.remove('is-selected');
9360// restore "none" default if visible
9361var noneBtn3 = swatchList.querySelector('.kit-swatch[data-kit-color="none"]');
9362if (noneBtn3 && noneBtn3.style.display !== 'none') {
9363noneBtn3.classList.add('is-selected');
9364inkChoiceMade = true;
9365}
9366}
9367} catch (eS4) { }
9368// Success toast
9369try {
9370var toast2 = document.getElementById('kit-success-toast');
9371if (toast2) {
9372var msg = (totalItemsCount > 1) ? ('✓ Added ' + totalItemsCount + ' items to cart') : '✓ Added to cart';
9373toast2.textContent = msg;
9374toast2.classList.add('is-visible');
9375setTimeout(function () { toast2.classList.remove('is-visible'); }, 3000);
9376}
9377} catch (eT2) { }
9378// Reset CTA + progress
9379addBtn.disabled = false;
9380addBtn.classList.remove('is-loading');
9381addBtn.textContent = 'Add to Cart';
9382if (statusEl) statusEl.textContent = 'Added to cart. Want to add another?';
9383resetAddProgressUI();
9384updatePriceUI();
9385// Keep module open to encourage additional add-ons
9386upsell.classList.add('cart-kit-upsell--open');
9387body.hidden = false;
9388});
9389})
9390.catch(function (err) {
9391console.error('Cart kit upsell error:', err);
9392var ckErrMsg = 'Sorry, we could not add the kit to your cart. Please try again.';
9393try {
9394var ckFlash = (window.Alpine && typeof window.Alpine.store === 'function')
9395? window.Alpine.store('flash') : null;
9396if (ckFlash && typeof ckFlash.show === 'function') ckFlash.show('error', ckErrMsg);
9397else alert(ckErrMsg);
9398} catch (eFl) { alert(ckErrMsg); }
9399addBtn.disabled = false;
9400addBtn.classList.remove('is-loading');
9401addBtn.textContent = 'Add to Cart';
9402if (statusEl) statusEl.textContent = '';
9403// hide progress bar on error
9404resetAddProgressUI();
9405});
9406});
9407// ----------------------------------------------------
9408// Cart sync helpers (run after async adds so UI + tile logic are up-to-date)
9409// ----------------------------------------------------
9410// 15) Success + Add More
9411// ----------------------------------------------------
9412function resetSelections() {
9413selectedPrimaryIds.clear();
9414selectedInkColors.clear();
9415inkChoiceMade = false;
9416var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0);
9417if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId));
9418else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id);
9419buildGrid();
9420if (swatchList) {
9421var sw = swatchList.querySelectorAll('.kit-swatch');
9422for (var i = 0; i < sw.length; i++) sw[i].classList.remove('is-selected');
9423// Default to "none" (I have an ink pad) so user isn't forced into an alert loop
9424var noneBtn = swatchList.querySelector('.kit-swatch[data-kit-color="none"]');
9425if (noneBtn && noneBtn.style.display !== 'none') {
9426noneBtn.classList.add('is-selected');
9427// keep selectedInkColors empty, but mark a choice as made
9428inkChoiceMade = true;
9429}
9430}
9431addBtn.disabled = false;
9432addBtn.classList.remove('is-loading');
9433addBtn.textContent = 'Add to Cart';
9434if (statusEl) statusEl.textContent = '';
9435resetAddProgressUI();
9436updatePriceUI();
9437}
9438function handleAddMoreFromQuery() {
9439if (!accentAddMoreQS) return;
9440upsell.classList.add('cart-kit-upsell--open');
9441body.hidden = false;
9442resetSelections();
9443try {
9444var href = window.location.href;
9445var clean = href
9446.replace(/([?&])accentAdded=1(?=(&|#|$))/, '$1')
9447.replace(/[?&]#/, '#')
9448.replace(/[?&]$/, '');
9449window.history.replaceState({}, document.title, clean);
9450} catch (e) { }
9451}
9452function applySuccessStateFromQuery() {
9453if (!accentAddedQS) return;
9454var headerText = upsell.querySelector('#cart-kit-header-text');
9455if (headerText) {
9456headerText.innerHTML =
9457'<span class="cart-kit-upsell__success-inline">'
9458+ '✓ Items added to cart!'
9459+ ' <span style="color:#000; text-decoration: underline;">Add more?</span>'
9460+ '</span>';
9461}
9462upsell.classList.remove('cart-kit-upsell--open');
9463body.hidden = true;
9464try {
9465var clean2 = window.location.href.replace(/([?&])accentAdded=1(&|$)/, '$1').replace(/[?&]$/, '');
9466window.history.replaceState({}, document.title, clean2);
9467} catch (e) { }
9468}
9469// ----------------------------------------------------
9470// Micro PDP queue helpers (supports 2+ hard SKUs)
9471// ----------------------------------------------------
9472function getTileInfoByProductId(pid) {
9473pid = String(pid || '').trim();
9474if (!pid) return null;
9475var raw = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? (TILE_ITEM_BY_ID[pid] || TILE_ITEM_BY_ID[String(pid)]) : null;
9476if (!raw) return null;
9477var hydrated = (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) ? NOTARY_HYDRATED.get(pid) : null;
9478var title = (hydrated && hydrated.name) ? hydrated.name : (raw.title || raw.name || 'Customize item');
9479var url = raw.productUrl || raw.url || raw.href || null;
9480if (!url) return null;
9481return { productId: pid, title: title, url: url };
9482}
9483function parseRemainingHardFromUrl() {
9484try {
9485var u = new URL(window.location.href);
9486var val = (u.searchParams.get('remainingHard') || '').trim();
9487if (!val) return [];
9488return val.split(',').map(function (s) { return String(s || '').trim(); }).filter(Boolean);
9489} catch (e) {
9490return [];
9491}
9492}
9493function stripRemainingHardParam() {
9494try {
9495var u = new URL(window.location.href);
9496u.searchParams.delete('remainingHard');
9497u.searchParams.delete('microTotal');
9498u.searchParams.delete('microDone');
9499window.history.replaceState({}, document.title, u.toString());
9500} catch (e) { }
9501}
9502function openNextHardFromRemaining() {
9503var remaining = parseRemainingHardFromUrl();
9504if (!remaining.length) {
9505stripRemainingHardParam();
9506return false;
9507}
9508var nextId = remaining.shift();
9509var info = getTileInfoByProductId(nextId);
9510if (!info) {
9511// If we can't resolve the next item, stop chaining to avoid a loop
9512stripRemainingHardParam();
9513return false;
9514}
9515// Rebuild the remainingHard param (after removing the one we're about to open)
9516try {
9517var u = new URL(window.location.href);
9518if (remaining.length) u.searchParams.set('remainingHard', remaining.join(','));
9519else u.searchParams.delete('remainingHard');
9520window.history.replaceState({}, document.title, u.toString());
9521} catch (e) { }
9522// Small status hint before opening the next configurator
9523if (statusEl) statusEl.textContent = 'Next: customize ' + info.title + '.';
9524// Open next Micro PDP
9525var prog = getMicroProgressFromUrl();
9526openMicroPdp(info.url, info.title, info.productId, remaining, prog.done, prog.total);
9527return true;
9528}
9529// --- SmartCart: ensure cart + tiles refresh when micro-PDP closes ---
9530(function bindMicroPdpCloseRefresh() {
9531var MODAL_SEL = '#smartcartMicroPdp, .smartcart-micro-pdp, [data-smartcart-micro-pdp]';
9532var CLOSE_SEL = '[data-smartcart-close], .smartcart-close, .close, [aria-label="Close"]';
9533var OVERLAY_SEL = '.smartcart-overlay, .modal-backdrop, [data-smartcart-overlay]';
9534// You likely already have these functions; map to your actual names:
9535var refresh = window.smartcartRefresh || window.SmartCartRefresh || window.renderSmartCart || null;
9536// If you *don’t* have a single refresh entrypoint, use whatever you normally call after add-to-cart:
9537function hardRefreshFallback() {
9538try {
9539if (typeof refresh === 'function') return refresh();
9540// Fallback: call the same method you use after cart update
9541if (window.SmartCart && typeof window.SmartCart.render === 'function') return window.SmartCart.render();
9542if (window.smartcart && typeof window.smartcart.render === 'function') return window.smartcart.render();
9543} catch (e) {
9544console.warn('[SmartCart] refresh on modal close failed:', e);
9545}
9546}
9547// Debounce so multiple close signals (button + overlay) don’t double-trigger
9548var t;
9549function refreshSoon() {
9550clearTimeout(t);
9551t = setTimeout(function () {
9552hardRefreshFallback();
9553}, 150);
9554}
9555function onDocClick(e) {
9556var modal = document.querySelector(MODAL_SEL);
9557if (!modal) return;
9558var isClose = e.target.closest && e.target.closest(CLOSE_SEL);
9559var isOverlay = e.target.closest && e.target.closest(OVERLAY_SEL);
9560if (isClose || isOverlay) {
9561// Let the DOM close animation finish, then refresh
9562refreshSoon();
9563}
9564}
9565function onEsc(e) {
9566if (e.key === 'Escape' || e.keyCode === 27) refreshSoon();
9567}
9568document.addEventListener('click', onDocClick, true);
9569document.addEventListener('keydown', onEsc, true);
9570// Optional: if you dispatch a custom "microPdpClosed" event already, hook that too:
9571document.addEventListener('smartcart:microPdpClosed', refreshSoon);
9572})();
9573// ----------------------------------------------------
9574// 4A) Replace an "Added" product tile with the next best recommendation
9575//     (keeps the grid fresh without requiring a full page reload)
9576// ----------------------------------------------------
9577function convertTileToCompleteState(card, totalCount) {
9578if (!card) return;
9579// Stable-slot swap: fade this card out before repopulating
9580try { card.classList.add('ck-swapping'); } catch (eFade0) { }
9581try {
9582card.classList.remove('is-selected');
9583card.classList.remove('is-added');
9584card.classList.add('is-complete');
9585// prevent future selection toggles
9586card.setAttribute('aria-disabled', 'true');
9587card.innerHTML =
9588'<div class="ck-tile-complete">'
9589+ '<div class="ck-tile-complete__icon">✓</div>'
9590+ '<div class="ck-tile-complete__title">All set</div>'
9591+ '<div class="ck-tile-complete__subtext">You\'ve added all recommended items for this order.</div>'
9592+ '</div>';
9593} catch (e) { }
9594}
9595function replaceAddedTileWithNext(addedPid) {
9596try {
9597if (!useTiles || !stampGrid) return;
9598addedPid = String(addedPid || '').trim();
9599if (!addedPid) return;
9600var card = stampGrid.querySelector('.kit-product-card[data-kit-primary="' + addedPid + '"]');
9601if (!card) return;
9602// Build an ordered pool (highest relevance first)
9603var ordered = pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow).tiles;
9604// Build exclusion map
9605var exclude = Object.create(null);
9606// Exclude currently visible tiles
9607var visible = stampGrid.querySelectorAll('.kit-product-card[data-kit-primary]');
9608for (var i = 0; i < visible.length; i++) {
9609var v = String(visible[i].getAttribute('data-kit-primary') || '').trim();
9610if (v) exclude[v] = true;
9611}
9612// Exclude anything already added (session + micro returns)
9613try {
9614if (microAddedIds && microAddedIds.forEach) {
9615microAddedIds.forEach(function (id) { exclude[String(id)] = true; });
9616}
9617} catch (eAdded) { }
9618// Exclude anything already shown during this session (prevents repeats when pool is small)
9619try {
9620if (ckSeenTileIds && ckSeenTileIds.forEach) {
9621ckSeenTileIds.forEach(function (id) { exclude[String(id)] = true; });
9622}
9623} catch (eSeen) { }
9624// Exclude queued HARD SKUs still pending configuration
9625try {
9626var queued = parseRemainingHardFromUrl();
9627for (var q = 0; q < queued.length; q++) {
9628var qid = String(queued[q] || '').trim();
9629if (qid) exclude[qid] = true;
9630}
9631} catch (eQ) { }
9632// Exclude anything currently selected (so swaps don't "steal" into a tile the user already picked)
9633try {
9634if (selectedPrimaryIds && selectedPrimaryIds.forEach) {
9635selectedPrimaryIds.forEach(function (sid) {
9636var s = String(sid || '').trim();
9637if (s) exclude[s] = true;
9638});
9639}
9640} catch (eSel) { }
9641// Exclude anything already in the cart (prefer URL-path match; fallback to title match)
9642var cartPaths = Object.create(null);
9643var cartFullLower = '';
9644try { cartPaths = getCartProductPathSet(); } catch (eCP) { }
9645try { cartFullLower = getCartFullTextLower(); } catch (eCT) { }
9646// Find next eligible item
9647var next = null;
9648for (var k = 0; k < ordered.length; k++) {
9649var it = ordered[k];
9650var pid = String(it && (it.productId || it.id || it.product || it.primaryId) || '').trim();
9651if (!pid || exclude[pid]) continue;
9652// URL-path in-cart exclusion
9653var pth = '';
9654try { pth = normalizeUrlPathForCompare(it.productUrl || it.url || ''); } catch (eNP) { }
9655if (pth && cartPaths[pth]) continue;
9656// Title fallback exclusion
9657var nm = String(it.name || it.title || '').toLowerCase();
9658if (nm && cartFullLower && cartFullLower.indexOf(nm) !== -1) continue;
9659next = it;
9660break;
9661}
9662if (!next) {
9663// Pool exhausted -- offer an approved alternative before giving up.
9664var altNext = ckPickFallbackTile();
9665if (altNext) {
9666try { card.classList.remove('ck-swapping'); } catch (eFA) { }
9667ckRepaintCardAs(card, altNext);
9668return;
9669}
9670// Nothing left → convert this tile to completion tile
9671try { convertTileToCompleteState(card); } catch (eDone) { }
9672setTimeout(function () { try { card.classList.remove('ck-swapping'); } catch (eFadeX) { } }, 160);
9673return;
9674}
9675// Swap this card to the new item
9676var nextId = String(next.productId || next.id || next.product || next.primaryId || '').trim();
9677var nextUrl = next.productUrl || next.url || '';
9678var nextHard = !!requiresMicroPdp(nextId, next);
9679// Mark seen immediately so async hydration can't cause repeats
9680try { if (ckSeenTileIds) ckSeenTileIds.add(String(nextId)); } catch (eS) { }
9681// Fade out before repopulating (stable slot)
9682try { card.classList.add('ck-swapping'); } catch (eFade0) { }
9683// Reset card state + dataset
9684try {
9685card.classList.remove('is-added', 'is-selected', 'is-complete');
9686card.removeAttribute('aria-disabled');
9687} catch (eState) { }
9688card.setAttribute('data-kit-primary', nextId);
9689card.setAttribute('data-kit-url', nextUrl);
9690card.setAttribute('data-kit-hard', nextHard ? '1' : '0');
9691// Paint skeleton while hydration finishes (DOM structure used by buildGrid)
9692var titleEl = card.querySelector('.kit-product-name');
9693var priceEl = card.querySelector('.kit-product-price');
9694var thumbTile = card.querySelector('.kit-product-thumb-tile');
9695if (titleEl) titleEl.textContent = (next.title || next.name || 'Loading…');
9696if (priceEl) priceEl.textContent = '';
9697// Reset the thumb; we'll hydrate with a real image after fetchPdpData
9698if (thumbTile) {
9699var imgSrc = (next.image || next.img || '');
9700if (imgSrc) {
9701var altText = String(next.title || next.name || '').replace(/"/g, '&quot;');
9702thumbTile.innerHTML = '<img src="' + imgSrc + '" alt="' + altText + '">';
9703} else {
9704thumbTile.innerHTML = '<span class="kit-thumb-loading">Loading...</span>';
9705}
9706}
9707// Fade back in once the new tile has been painted
9708setTimeout(function () {
9709try { card.classList.remove('ck-swapping'); } catch (eFade1) { }
9710}, 160);
9711// Record the swapped-in product's strategy attributes (role,
9712// friction, experimental) in an ANALYTICS-ONLY map.
9713//
9714// Deliberately NOT written to TILE_ITEM_BY_ID: that map feeds
9715// requiresMicroPdp(), so registering swapped tiles there would
9716// flip the CTA to 'Customize & Add' and reroute the add through
9717// the micro-PDP. That may be the correct behaviour -- a bare
9718// product=id&qty=1 POST of a required-options SKU is wrong -- but
9719// it is an add-flow change and does not belong in an analytics
9720// patch. Tracked separately.
9721try { CK_GA_TILE_META[String(nextId)] = next; } catch (eReg) { }
9722// Hydrate: fetch canonical name/price/image (guard against stale async writes)
9723(function (el, expectedId, raw) {
9724try {
9725var url = raw.productUrl || raw.url || '';
9726var p = fetchPdpData(url);
9727if (!p || !p.then) return;
9728p.then(function (data) {
9729try {
9730// Guard a stale async write: the slot may have been
9731// swapped again while this fetch was in flight.
9732if (!el || el.getAttribute('data-kit-primary') !== String(expectedId)) return;
9733var tEl = el.querySelector('.kit-product-name');
9734var prEl = el.querySelector('.kit-product-price');
9735var thEl = el.querySelector('.kit-product-thumb-tile');
9736if (tEl && data.name) tEl.textContent = data.name;
9737// Cache the canonical name/price so analytics and
9738// the price UI see the same product the shopper does.
9739// Cache the canonical name/price. Gated by the same
9740// quality check buildGrid and ckRepaintCardAs use --
9741// a URL that scrapes to 'Create New Customer Account'
9742// must not reach the cache, because it feeds the
9743// micro-PDP modal title and the displayed total.
9744// Also normalises price the way the other two sites do.
9745try {
9746if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED
9747&& ckIsUsableTileData(data)) {
9748NOTARY_HYDRATED.set(String(expectedId), {
9749productId: String(expectedId),
9750sku: data.sku,
9751productUrl: (raw && (raw.productUrl || raw.url)) || '',
9752name: data.name || '',
9753img: data.img || '',
9754price: (data.price != null ? Number(data.price) : null)
9755});
9756}
9757} catch (eHyd) { }
9758// A swapped tile is newly visible and was in no
9759// view_item_list, so its funnel would otherwise start
9760// at select_item. Give it its own one-item impression.
9761// attempts = 0 -> fire now; the name is already cached.
9762try {
9763ckGaFireListWhenHydrated(
9764[String(expectedId)], 0,
9765(CK_GA4.LIST_PREFIX + ' - ' + ckGaListId() + ' (swap)').slice(0, 100)
9766);
9767} catch (eSwapGa) { }
9768if (prEl) {
9769prEl.textContent = (data.price != null ? ('$' + Number(data.price).toFixed(2)) : 'See price in cart');
9770}
9771if (thEl) {
9772if (data.img) {
9773var alt2 = String(data.name || '').replace(/"/g, '&quot;');
9774thEl.innerHTML = '<img src="' + data.img + '" alt="' + alt2 + '">';
9775} else {
9776thEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>';
9777}
9778}
9779} catch (eH) { }
9780}).catch(function () { });
9781} catch (eF) { }
9782})(card, nextId, next);
9783} catch (e) {
9784// fail silently
9785}
9786}
9787function handleMicroAddedFromQuery() {
9788// The micro-PDP no longer reloads the page, so microAddedQS /
9789// microAddedNameQS -- captured from window.location.search back at
9790// DOMContentLoaded -- are stale by definition. onMicroPdpAddSucceeded()
9791// writes the receipt into the URL via history.replaceState, so re-read
9792// it live here. Still correct for the legacy reload path.
9793try {
9794var ckLive = new URL(window.location.href).searchParams;
9795microAddedQS = ckLive.get('microAdded');
9796microAddedNameQS = ckLive.get('microAddedName');
9797} catch (eLive) { }
9798if (!microAddedQS) return;
9799var pid = String(microAddedQS || '').trim();
9800if (!pid) return;
9801// Mark as added so tile greys out
9802microAddedIds.add(pid);
9803// Remove from current selections (prevents stale totals when tile is swapped)
9804try { selectedPrimaryIds.delete(pid); } catch (eSel0) { }
9805// Micro progress: user just completed one configurator
9806var progNow = bumpMicroDoneInUrl();
9807MICRO_PDP.progressDone = progNow.done;
9808MICRO_PDP.progressTotal = progNow.total;
9809renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal);
9810// Hard SKUs add inside the micro-PDP iframe, so they never reach the
9811// POST chain above and were invisible to analytics. Fire the
9812// per-product event here. Dedupe on pid + completed-count: the
9813// receipt params can survive a re-entry of this handler.
9814try {
9815// Dedupe on a short time window, NOT on progNow.done:
9816// bumpMicroDoneInUrl() increments on every call, so a re-entry
9817// would produce a different key and defeat the guard entirely.
9818// A configurator takes seconds, so two legitimate adds of the
9819// same SKU inside 5s are impossible.
9820window.__ckMicroAddReported = window.__ckMicroAddReported || {};
9821var ckNowT = Date.now();
9822if (ckNowT - (window.__ckMicroAddReported[pid] || 0) > 5000) {
9823window.__ckMicroAddReported[pid] = ckNowT;
9824// On the legacy reload path this handler runs in the same tick as
9825// buildGrid(), which only STARTS hydration -- so firing now would
9826// send the entity id for the highest-AOV items in the module.
9827// Wait for the SKU, then fire; fire anyway on timeout.
9828(function emitMicroAdd(tries) {
9829try {
9830if (!ckGaHasSku(pid) && tries > 0) {
9831setTimeout(function () { emitMicroAdd(tries - 1); }, CK_GA4.HYDRATE_POLL_MS);
9832return;
9833}
9834} catch (eW) { }
9835pushUpsellEvent('SmartCart_upsell_micro_added', {
9836product_ids: [pid],
9837productId: pid,
9838micro_done: progNow.done,
9839micro_total: progNow.total
9840});
9841})(CK_GA4.HYDRATE_POLLS);
9842}
9843} catch (eGaM) { }
9844// Show a simple success message in the module status area
9845var niceName = (microAddedNameQS || '').trim();
9846if (!niceName) {
9847// If name not provided, try hydrated cache
9848var hydrated = NOTARY_HYDRATED.get(pid);
9849if (hydrated && hydrated.name) niceName = hydrated.name;
9850}
9851if (!niceName) niceName = 'Product';
9852var toast = document.getElementById('kit-success-toast');
9853if (toast) {
9854toast.textContent = '✓ Added to cart: ' + niceName;
9855// If this was the LAST remaining hard item, celebrate completion
9856if (progNow.total > 1 && progNow.done >= progNow.total) {
9857toast.textContent = '???? All set — ' + progNow.done + ' of ' + progNow.total + ' items added';
9858}
9859toast.classList.add('is-visible');
9860setTimeout(function () {
9861toast.classList.remove('is-visible');
9862}, 3500);
9863} else if (statusEl) {
9864// fallback
9865statusEl.textContent = '✓ Added to cart: ' + niceName;
9866if (progNow.total > 1 && progNow.done >= progNow.total) {
9867statusEl.textContent = '???? All set — ' + progNow.done + ' of ' + progNow.total + ' items added';
9868}
9869}
9870// Rebuild grid so the "is-added" state is applied
9871if (useTiles) buildGrid();
9872try { updatePriceUI(); } catch (ePU0) { }
9873// 4B) After a HARD SKU is completed, swap the added tile for the next recommendation
9874// so the customer can keep adding without stale/greyed tiles.
9875// (Run after buildGrid() so we target the freshly-rendered card.)
9876setTimeout(function () {
9877try { replaceAddedTileWithNext(pid); } catch (e) { }
9878}, 800);
9879// If there are more HARD SKUs waiting, open the next Micro PDP automatically
9880// (keeps the user in a guided configuration flow)
9881setTimeout(function () {
9882try {
9883openNextHardFromRemaining();
9884} catch (e) { }
9885}, 350);
9886// Scroll to the newly added item in cart + highlight
9887// Strategy: find a cart row that contains the product name
9888setTimeout(function () {
9889try {
9890var table = document.querySelector('#shopping-cart-table');
9891if (!table) return;
9892var rows = Array.from(table.querySelectorAll('tr'));
9893var targetRow = null;
9894if (niceName && niceName !== 'Product') {
9895var nameLower = niceName.toLowerCase();
9896targetRow = rows.find(function (r) {
9897return (r.innerText || '').toLowerCase().indexOf(nameLower) > -1;
9898}) || null;
9899}
9900// Fallback: if we can't find the row by name, just scroll to cart table
9901var scrollEl = targetRow || table;
9902scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
9903if (targetRow) {
9904targetRow.classList.add('ck-cart-highlight');
9905setTimeout(function () {
9906targetRow.classList.remove('ck-cart-highlight');
9907}, 3500);
9908}
9909} catch (e) { }
9910}, 250);
9911// Remove receipt params so refresh doesn't repeat the success behavior
9912try {
9913var u = new URL(window.location.href);
9914u.searchParams.delete('microAdded');
9915u.searchParams.delete('microAddedName');
9916window.history.replaceState({}, document.title, u.toString());
9917} catch (e) { }
9918}
9919// Query-param handlers MUST run in this order (priority: micro receipt > add more > success)
9920handleMicroAddedFromQuery();     // receipt event → toast/greyout/highlight, then strip params
9921handleAddMoreFromQuery();        // user intent to reopen + reset
9922applySuccessStateFromQuery();    // generic success collapse/header state
9923// ----------------------------------------------------
9924// 9) DEBUG OVERLAY (persona + product scores)
9925//    Enable with ?upsellDebug=1
9926// ----------------------------------------------------
9927(function initDebugOverlay() {
9928var debugOn = (urlParams.get('upsellDebug') === '1');
9929if (!debugOn) return;
9930// ---- styles (scoped) ----
9931var style = document.createElement('style');
9932style.textContent =
9933'#ck-debug-overlay{position:fixed;right:12px;bottom:12px;z-index:999999;' +
9934'width:360px;max-width:calc(100vw - 24px);' +
9935'background:#0b1220;color:#e8eefc;border:1px solid rgba(255,255,255,.15);' +
9936'border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35);font-family:inherit;}' +
9937'#ck-debug-overlay *{box-sizing:border-box;}' +
9938'#ck-debug-head{display:flex;align-items:center;justify-content:space-between;' +
9939'padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.12);}' +
9940'#ck-debug-title{font-weight:700;font-size:13px;letter-spacing:.2px;}' +
9941'#ck-debug-actions{display:flex;gap:8px;align-items:center;}' +
9942'#ck-debug-actions button{background:rgba(255,255,255,.12);color:#e8eefc;border:1px solid rgba(255,255,255,.15);' +
9943'border-radius:8px;padding:6px 8px;font-size:12px;cursor:pointer;}' +
9944'#ck-debug-actions button:hover{background:rgba(255,255,255,.18);}' +
9945'#ck-debug-body{padding:10px 12px;max-height:55vh;overflow:auto;}' +
9946'#ck-debug-body h4{margin:10px 0 6px 0;font-size:12px;opacity:.9;}' +
9947'#ck-debug-kv{display:grid;grid-template-columns:120px 1fr;gap:6px 10px;font-size:12px;}' +
9948'#ck-debug-kv .k{opacity:.75;}' +
9949'#ck-debug-table{width:100%;border-collapse:collapse;font-size:12px;}' +
9950'#ck-debug-table th,#ck-debug-table td{padding:6px 6px;border-bottom:1px solid rgba(255,255,255,.10);vertical-align:top;}' +
9951'#ck-debug-table th{opacity:.75;text-align:left;font-weight:600;}' +
9952'#ck-debug-pill{display:inline-block;padding:2px 6px;border-radius:999px;' +
9953'background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.14);font-size:11px;}' +
9954'#ck-debug-muted{opacity:.7;font-size:11px;line-height:1.35;}' +
9955'#ck-debug-overlay.collapsed #ck-debug-body{display:none;}';
9956document.head.appendChild(style);
9957// ---- UI ----
9958var box = document.createElement('div');
9959box.id = 'ck-debug-overlay';
9960box.innerHTML =
9961'<div id="ck-debug-head">' +
9962'<div id="ck-debug-title">Cart Kit Debug</div>' +
9963'<div id="ck-debug-actions">' +
9964'<button type="button" id="ck-debug-copy">Copy</button>' +
9965'<button type="button" id="ck-debug-toggle">Collapse</button>' +
9966'</div>' +
9967'</div>' +
9968'<div id="ck-debug-body">' +
9969'<div id="ck-debug-kv"></div>' +
9970'<h4>Persona scores</h4>' +
9971'<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>' +
9972'<h4>Top products (selected persona)</h4>' +
9973'<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>' +
9974'<div id="ck-debug-muted" style="margin-top:10px;">Tip: add/remove items and watch scores update live.</div>' +
9975'</div>';
9976document.body.appendChild(box);
9977var btnToggle = document.getElementById('ck-debug-toggle');
9978var btnCopy = document.getElementById('ck-debug-copy');
9979btnToggle.addEventListener('click', function () {
9980box.classList.toggle('collapsed');
9981btnToggle.textContent = box.classList.contains('collapsed') ? 'Expand' : 'Collapse';
9982});
9983function safeJson(obj) {
9984try { return JSON.stringify(obj, null, 2); } catch (e) { return String(obj); }
9985}
9986btnCopy.addEventListener('click', function () {
9987var payload = window.__CK_DEBUG_LAST__ || {};
9988var textToCopy = safeJson(payload);
9989if (navigator.clipboard && navigator.clipboard.writeText) {
9990navigator.clipboard.writeText(textToCopy).catch(function () { });
9991} else {
9992// fallback
9993var ta = document.createElement('textarea');
9994ta.value = textToCopy;
9995ta.style.position = 'fixed';
9996ta.style.left = '-9999px';
9997document.body.appendChild(ta);
9998ta.select();
9999try { document.execCommand('copy'); } catch (e) { }
10000document.body.removeChild(ta);
10001}
10002});
10003// ---- scoring helpers for overlay ----
10004function personaScoreBreakdown(personaObj, cartTextLower) {
10005var kw = keywordMatchScore((personaObj.match || personaObj.matchKeywords || []), cartTextLower);
10006var base = kw.score;
10007var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0;
10008return {
10009base: base,
10010hits: kw.hits,
10011matched: kw.matched,
10012priorityNudge: priorityNudge,
10013total: base + priorityNudge
10014};
10015}
10016function productScoreForOverlay(item, cartTextLower, allowExperimental) {
10017return scoreProductForCart(item, cartTextLower, allowExperimental);
10018}
10019function esc(s) {
10020return String(s || '').replace(/[&<>"']/g, function (c) {
10021return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]);
10022});
10023}
10024// Debug helpers: get cart titles/options as arrays
10025function getCartTitlesList() {
10026try {
10027var nameNodes = document.querySelectorAll(
10028'[data-cart-item-id] h3 a,' +
10029'[data-cart-item-id] h3,' +
10030'#shopping-cart-table h2.cartProductName a,' +
10031'#shopping-cart-table h2.cartProductName,' +
10032'#shopping-cart-table .product-name a,' +
10033'#shopping-cart-table .product-name,' +
10034'.product-item-name a,' +
10035'.product-item-name'
10036);
10037var out = [];
10038var seen = {};
10039for (var i = 0; i < nameNodes.length; i++) {
10040var t = (nameNodes[i].textContent || '').trim();
10041if (!t) continue;
10042var k = t.toLowerCase();
10043if (seen[k]) continue;
10044seen[k] = true;
10045out.push(t);
10046}
10047return out;
10048} catch (e) { return []; }
10049}
10050function getCartOptionsList() {
10051try {
10052var optNodes = document.querySelectorAll(
10053'[data-cart-item-id] .flex-wrap,' +
10054'#shopping-cart-table .item-options,' +
10055'#shopping-cart-table dl.item-options,' +
10056'.item-options,' +
10057'dl.item-options'
10058);
10059var out = [];
10060for (var j = 0; j < optNodes.length; j++) {
10061var o = (optNodes[j].textContent || '').trim();
10062if (o) out.push(o);
10063}
10064return out;
10065} catch (e) { return []; }
10066}
10067function renderOverlay() {
10068var cartTitleLower = getCartTitleTextLower();
10069var cartFullLower = getCartFullTextLower();
10070var allowExperimental = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1');
10071// Persona scoring
10072var personaRows = [];
10073var best = { key: 'generic', score: -1 };
10074for (var i = 0; i < PERSONAS.length; i++) {
10075var p = PERSONAS[i];
10076if (!p || p.key === 'generic') continue;
10077var br = personaScoreBreakdown(p, cartTitleLower);
10078personaRows.push({
10079key: p.key,
10080name: p.name,
10081score: br.total,
10082hits: br.hits,
10083matched: br.matched || [],
10084priority: br.priorityNudge
10085});
10086if (br.hits > 0 && br.total > best.score) {
10087best = { key: p.key, score: br.total };
10088}
10089}
10090personaRows.sort(function (a, b) { return b.score - a.score; });
10091// If nobody matched, generic
10092var chosenKey = (best.score <= 0) ? 'generic' : best.key;
10093var chosenPersona = getPersonaByKey(chosenKey);
10094// Product scoring for chosen persona
10095var pool = (chosenPersona && chosenPersona.items) ? chosenPersona.items.slice() : [];
10096var prodRows = pool.map(function (it) {
10097return {
10098id: it.productId,
10099url: it.productUrl,
10100name: it.productName || it.name || it.title || it.label || '',
10101role: it.role || '',
10102friction: it.friction || '',
10103experimental: (it.experimental === true),
10104score: productScoreForOverlay(it, cartFullLower, allowExperimental)
10105};
10106});
10107prodRows.sort(function (a, b) { return b.score - a.score; });
10108// Top N shown for overlay (not just 3)
10109var topN = prodRows.slice(0, 8);
10110// Update KV
10111var kv = document.getElementById('ck-debug-kv');
10112var titleList = getCartTitlesList();
10113var optList = getCartOptionsList();
10114var cartTitlesText = (titleList || []).join(' | ');
10115var cartOptionsText = (optList || []).join(' | ');
10116kv.innerHTML =
10117'<div class="k">Chosen persona</div><div><span id="ck-debug-pill" class="ck-pill"></span></div>' +
10118'<div class="k">Experimental</div><div>' + (allowExperimental ? 'ON (debug)' : 'OFF') + '</div>' +
10119'<div class="k">Cart titles (used for persona)</div><div id="ck-debug-muted">' + esc(cartTitlesText.slice(0, 220)) + (cartTitlesText.length > 220 ? '…' : '') + '</div>' +
10120'<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>';
10121var pill = document.getElementById('ck-debug-pill');
10122if (pill) {
10123pill.textContent = chosenKey;
10124pill.className = '';
10125pill.id = 'ck-debug-pill';
10126pill.style.display = 'inline-block';
10127pill.style.padding = '2px 6px';
10128pill.style.borderRadius = '999px';
10129pill.style.background = 'rgba(255,255,255,.12)';
10130pill.style.border = '1px solid rgba(255,255,255,.14)';
10131pill.style.fontSize = '11px';
10132}
10133// Persona tables
10134var tbody = document.getElementById('ck-debug-personas');
10135tbody.innerHTML = personaRows.map(function (r) {
10136var isChosen = (r.key === chosenKey);
10137// Build keyword pills (or fallback)
10138var kwHtml = (r.matched && r.matched.length)
10139? '<div class="ck-debug-keywords">' +
10140r.matched.slice(0, 10).map(function (k) {
10141return '<span class="ck-debug-keyword">' + esc(k) + '</span>';
10142}).join('') +
10143(r.matched.length > 10 ? '<span class="ck-debug-keyword muted">+' + (r.matched.length - 10) + ' more</span>' : '') +
10144'</div>'
10145: '<div class="ck-debug-keywords muted">no keyword matches</div>';
10146return ''
10147+ '<tr>'
10148+ '<td>' + esc(r.key) + (isChosen ? ' <span class="ck-badge" style="margin-left:6px;opacity:.9;">★</span>' : '') + '</td>'
10149+ '<td>
10149' + (Math.round(r.score * 100) / 100) + '</td>'
10150+ '<td>' + r.hits + '</td>'
10151+ '</tr>'
10152+ '<tr class="ck-debug-keyword-row">'
10153+ '<td colspan="3">' + kwHtml + '</td>'
10154+ '</tr>';
10155}).join('');
10156// Products table
10157var prodTbody = document.getElementById('ck-debug-products');
10158if (prodTbody) {
10159if (!topN || !topN.length) {
10160prodTbody.innerHTML = '<tr><td colspan="3" style="opacity:.7;">No products found for this persona.</td></tr>';
10161} else {
10162prodTbody.innerHTML = topN.map(function (r) {
10163var label = r.name || r.url || r.id || '';
10164var roleBits = [];
10165if (r.role) roleBits.push(r.role);
10166if (r.friction) roleBits.push('friction: ' + r.friction);
10167if (r.experimental) roleBits.push('exp');
10168return ''
10169+ '<tr>'
10170+ '<td>' + esc(label) + '</td>'
10171+ '<td>' + (Math.round(r.score * 100) / 100) + '</td>'
10172+ '<td>' + esc(roleBits.join(' • ')) + '</td>'
10173+ '</tr>';
10174}).join('');
10175}
10176}
10177// Store last payload for easy copy/paste to stakeholders
10178window.__CK_DEBUG_LAST__ = {
10179chosen_persona: chosenKey,
10180allow_experimental: allowExperimental,
10181persona_scores: personaRows,
10182product_scores_top: topN,
10183cart_titles: titleList,
10184cart_options: optList,
10185cart_titles_used_for_persona: cartTitleLower,
10186cart_options_text: (cartOptionsText || '').toLowerCase(),
10187cart_full_text: cartFullLower
10188};
10189}
10190// Update live (cart can change after ajax)
10191renderOverlay();
10192setInterval(renderOverlay, 800);
10193})();
10194}); </script>
10194</div><div class="widget widget-static-block"></div><div class="widget widget-static-block"></div> 
10194<script> /* Ported from the pre-Zephyr footer's jQuery block. Zephyr ships a jQuery
10195no-op shim, so the original jQuery(document).ready() never fired — see
10196note 1 in the header comment. Only the email-CTA rewrite was live; the
10197#blog and #corporate-accounts branches were dead and are not carried over. */
10198(function () {
10199"use strict";
10200var done = false;
10201function apply() {
10202if (done) return true;
10203var s = document.querySelectorAll('.email-cta > strong');
10204if (s.length < 2) return false;
10205s[1].textContent = 'and SAVE 15%';
10206done = true;
10207return true;
10208}
10209if (!apply()) {
10210document.addEventListener('DOMContentLoaded', apply);
10211/* the email CTA is injected by a widget block that can render late */
10212if (window.MutationObserver) {
10213var obs = new MutationObserver(function () { if (apply()) obs.disconnect(); });
10214obs.observe(document.documentElement, { childList: true, subtree: true });
10215setTimeout(function () { obs.disconnect(); }, 10000);
10216}
10217}
10218})();
10219</script>
10219<div class="widget widget-static-block"> <style> /* Hairline light, HARDENED against theme globals. ID-scoped (#fu-foot) for
10220specificity + a child reset so theme font / letter-spacing / text-transform /
10221line-height / margins / list & link styling can't bleed in. Targeted
10222!important only on the props themes tend to override. */
10223#fu-foot.fu-foot{--fu-accent:var(--brand-primary,#012e4a);--fu-border:#e7e9ec;--fu-sep:#e3e6ea;
10224display:flex!important;align-items:center!important;justify-content:center!important;
10225gap:8px!important;width:100%!important;min-height:46px!important;
10226margin:0!important;padding:9px 12px!important;margin-top:2rem;
10227background:#fff!important;border-top:1px solid var(--fu-border)!important;border-bottom:1px solid var(--fu-border)!important;
10228box-sizing:border-box!important;font-family:inherit!important;list-style:none!important}
10229#fu-foot.fu-foot *{margin:0!important;padding:0!important;box-sizing:border-box!important;
10230font-family:inherit!important;letter-spacing:normal!important;text-transform:none!important;
10231line-height:normal!important;list-style:none!important;text-decoration:none!important;float:none!important}
10232#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}
10233#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}
10234#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}
10235#fu-foot .fu-sep{width:1px!important;height:20px!important;background:var(--fu-sep)!important;flex:0 0 1px!important}
10236/* Copy is approved marketing copy and is never trimmed to fit. nowrap keeps the
10237crisp single line on wide desktop; below 1280px the longest lines wrap instead of
10238overflowing their equal-width column. Measured on live NTW: its slot 1 ("Badges
10239Your Team Will Be Proud to Wear", 38 chars) renders 251px wide. Text space per
10240column is ~430px at 1920, ~250px at 1200, ~270px at 1280 - so 1200 was 1px short
10241and 1280 is the correct boundary. Re-measure if any line gets longer. */
10242@media(max-width:1280px){
10243#fu-foot .fu-head{white-space:normal!important}
10244#fu-foot.fu-foot{min-height:56px!important}
10245}
10246/* symbol library — out of the flex container, never rendered */
10247#fu-defs{position:absolute!important;width:0!important;height:0!important;overflow:hidden!important}
10248@media(max-width:768px){
10249#fu-foot.fu-foot{flex-wrap:wrap!important;min-height:82px!important}
10250#fu-foot .fu-item{flex:1 1 46%!important;justify-content:flex-start!important}
10251#fu-foot .fu-head{font-size:12px!important;white-space:normal!important}
10252#fu-foot .fu-sep{display:none!important}
10253} </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>
10253 <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> 
10253<script type="text/javascript"> // <![CDATA[
10254(function () {
10255"use strict";
10256var SITE_DATA = {
10257"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"]],
10258"customsigns":            [["fu-quality","Built to Last"],["fu-customization","Customized Your Way"],["fu-guarantee","Experts Since 1954"],["fu-star","Reliable Business Signs"]],
10259"allstatenotarysupplies": [["fu-guarantee","Trusted Since 2008"],["fu-compliance","Compliance Experts"],["fu-customization","Easy to Customize"],["fu-clock","Ships in 24 Hours"]],
10260"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"]],
10261"corpconnect":            [["fu-quality","Professional Grade"],["fu-compliance","State Approved"],["fu-customization","Order Your Way"],["fu-shipping","Ships Next Business Day"]],
10262"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"]],
10263"904custom":              [["fu-quality","Heirloom Quality"],["fu-design","Real Craftspeople"],["fu-customization","Personalize in Minutes"],["fu-shipping","Ships Next Day"]],
10264"hcbrands":               [["fu-guarantee","70 Years of Making It Right"],["fu-design","Expert Craftspeople"],["fu-customization","Easy to Personalize"],["fu-shipping","Ships Next Business Day"]]
10265};
10266// Per-brand icon/text color. Omit a site to inherit var(--brand-primary) with
10267// a navy fallback. MyKoozie is pinned to its theme blue so the strip matches
10268// the footer headers directly beneath it on legacy AND Zephyr.
10269var SITE_ACCENT = { "mykoozie": "#456FB2" };
10270// Matching below is a SUBSTRING test against the hostname, so a shared staging
10271// host resolves to whichever key it happens to contain — mk.staging.hcbrands.com
10272// would render HCBrands copy, not MyKoozie's. Force any such host here; this map
10273// is consulted first and wins outright. Key = exact lowercase hostname.
10274var HOST_OVERRIDE = {
10275// "mk.staging.hcbrands.com": "mykoozie",
10276// "cs.staging.hcbrands.com": "customsigns"
10277};
10278try {
10279var root = document.getElementById("fu-foot");
10280if (!root) return;
10281var host = (location.hostname || "").toLowerCase();
10282var key = null;
10283if (HOST_OVERRIDE[host] && SITE_DATA[HOST_OVERRIDE[host]]) {
10284key = HOST_OVERRIDE[host];
10285} else {
10286for (var k in SITE_DATA) {
10287if (Object.prototype.hasOwnProperty.call(SITE_DATA, k) && host.indexOf(k) > -1) { key = k; break; }
10288}
10289}
10290if (!key) return; // unknown host -> keep the generic copy already in the markup
10291var cfg = SITE_DATA[key];
10292if (SITE_ACCENT[key]) root.style.setProperty("--fu-accent", SITE_ACCENT[key]);
10293var items = root.getElementsByClassName("fu-item");
10294var seps  = root.getElementsByClassName("fu-sep");
10295for (var i = 0; i < items.length; i++) {
10296if (!cfg[i]) {
10297items[i].style.display = "none";
10298if (seps[i - 1]) seps[i - 1].style.display = "none"; // no orphan divider
10299continue;
10300}
10301var use = items[i].getElementsByTagName("use")[0];
10302if (use) {
10303use.setAttribute("href", "#" + cfg[i][0]);
10304use.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#" + cfg[i][0]);
10305}
10306var head = items[i].getElementsByClassName("fu-head")[0];
10307if (head) head.textContent = cfg[i][1];
10308}
10309} catch (e) { /* strip keeps its default copy rather than breaking the footer */ }
10310})();
10311// ]]> </script>
10311</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&amp;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">
103112021 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> 
10311<script type="application/ld+json"> {
10312"@context": "http://schema.org",
10313"@type": "LocalBusiness",
10314"name": "SimplyStamps.com",
10315"address": {
10316"@type": "PostalAddress",
10317"streetAddress": "2021 St Augustine Rd E, Suite 2",
10318"addressLocality": "Jacksonville",
10319"addressRegion": "FL",
10320"postalCode": "32207",
10321"addressCountry": "US"
10322},
10323"geo": {
10324"@type": "GeoCoordinates",
10325"latitude": "30.3220",
10326"longitude": "-81.6690"
10327},
10328"telephone": "+1-888-688-9998",
10329"email": "[email protected]",
10330"url": "https://www.simplystamps.com",
10331"sameAs": [
10332"https://www.facebook.com/SimplyStamps",
10333"https://twitter.com/SimplyStamps",
10334"https://www.instagram.com/SimplyStamps",
10335"https://www.pinterest.com/SimplyStamps",
10336"https://www.linkedin.com/company/SimplyStamps"
10337],
10338"openingHoursSpecification": [
10339{
10340"@type": "OpeningHoursSpecification",
10341"dayOfWeek": [
10342"Monday",
10343"Tuesday",
10344"Wednesday",
10345"Thursday"
10346],
10347"opens": "10:00",
10348"closes": "16:00"
10349}
10350],
10351"hasMerchantReturnPolicy": {
10352"@type": "MerchantReturnPolicy",
10353"name": "Return Policy",
10354"url": "https://www.simplystamps.com/returns",
10355"merchantReturnDays": 60,
10356"returnFees": "FreeReturn",
10357"returnMethod": "ReturnByMail",
10358"returnPolicyCategory": "Refundable",
10359"inStoreReturnsOffered": false,
10360"itemCondition": "New",
10361"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."
10362}
10363} </script>
10363</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"> &copy; 2026 Simply Stamps. All Rights Reserved.<p><a href="/terms-and-conditions">Terms & Conditions</a> | <a href="/privacy-policy">Privacy</a></p> &nbsp;&nbsp; </address></div></div> </footer> <style> @keyframes shake {
103640%, 100% { transform: translateX(0); }
1036510%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
1036620%, 40%, 60%, 80% { transform: translateX(4px); }
10367}
10368.shake {
10369animation: shake 0.5s ease-in-out;
10370} </style><div
10371x-data
10372x-show="$store.cart.drawerOpen"
10373@click="$store.cart.closeDrawer()"
10374x-transition:enter="transition-opacity ease-out duration-300"
10375x-transition:enter-start="opacity-0"
10376x-transition:enter-end="opacity-100"
10377x-transition:leave="transition-opacity ease-in duration-200"
10378x-transition:leave-start="opacity-100"
10379x-transition:leave-end="opacity-0"
10380class="fixed inset-0 z-40 transition-all duration-500"
10381:class="$store.cart.drawerFocused ? 'bg-black/15' : 'bg-transparent'"
10382style="display: none;"
10383></div><div
10384x-data
10385x-show="$store.cart.drawerOpen"
10386@click.away="if (!$store.imagePreview?.isOpen && !$store.promo?.isOpen && !$store.confirm?.isOpen && !document.getElementById('wizardModal')?._x_dataStack?.[0]?.isOpen) $store.cart.closeDrawer()"
10387@mouseenter="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = true }, 215)"
10388@mouseleave="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = false }, 100)"
10389x-transition:enter="transition ease-out duration-300"
10390x-transition:enter-start="translate-x-full"
10391x-transition:enter-end="translate-x-0"
10392x-transition:leave="transition ease-in duration-200"
10393x-transition:leave-start="translate-x-0"
10394x-transition:leave-end="translate-x-full"
10395:style="{ top: $store.ui.getHeaderOffset() + 'px' }"
10396class="fixed right-0 bottom-0 w-full md:w-[26rem] bg-white shadow-2xl z-50 flex flex-col transition-all duration-300"
10397style="display: none;"
10398><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
10399@click="$store.cart.closeDrawer()"
10400class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
10401aria-label="Close cart"
10402> <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
10403x-show="$store.cart.isRefreshing"
10404x-cloak
10405x-transition:enter="transition ease-out duration-200"
10406x-transition:enter-start="opacity-0"
10407x-transition:enter-end="opacity-100"
10408x-transition:leave="transition ease-in duration-150"
10409x-transition:leave-start="opacity-100"
10410x-transition:leave-end="opacity-0"
10411class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center z-10"
10412><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">
10412Updating cart...</span></div></div><div class="flex-1 min-h-0 overflow-hidden relative"
10413x-data="{
10414canScroll: false,
10415atBottom: false,
10416checkScroll() {
10417const el = this.$refs.scrollContainer;
10418if (!el) return;
10419this.canScroll = el.scrollHeight > el.clientHeight + 10;
10420this.atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 20;
10421},
10422scrollToLastItem() {
10423const container = this.$refs.scrollContainer;
10424if (!container) return;
10425const items = container.querySelectorAll('[data-cart-item-id]');
10426if (!items.length) return;
10427const lastItem = items[items.length - 1];
10428const cRect = container.getBoundingClientRect();
10429const iRect = lastItem.getBoundingClientRect();
10430if (iRect.bottom > cRect.bottom || iRect.top < cRect.top) {
10431lastItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
10432}
10433this.checkScroll();
10434}
10435}"
10436x-init="$nextTick(() => checkScroll()); $watch(() => $store.cart.drawerOpen, (open) => { if (open) $nextTick(() => checkScroll()) })"
10437@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"
10438id="cart-items-container"
10439x-ref="scrollContainer"
10440@scroll.throttle.100ms="checkScroll()"
10441@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"
10442x-transition:enter="transition ease-out duration-200"
10443x-transition:enter-start="opacity-0"
10444x-transition:enter-end="opacity-100"
10445x-transition:leave="transition ease-in duration-150"
10446x-transition:leave-start="opacity-100"
10447x-transition:leave-end="opacity-0"
10448class="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"
10449@click.prevent.stop="
10450const container = document.getElementById('cart-items-container');
10451if (!container) return;
10452const items = container.querySelectorAll('[data-cart-item-id]');
10453if (!items.length) return;
10454const containerTop = container.getBoundingClientRect().top;
10455let target = null;
10456const containerBottom = container.getBoundingClientRect().bottom;
10457for (let i = 0; i < items.length; i++) {
10458if (items[i].getBoundingClientRect().bottom > containerBottom) {
10459target = items[i];
10460break;
10461}
10462}
10463if (target) {
10464const scrollAmount = target.getBoundingClientRect().top - containerTop;
10465container.scrollBy({ top: scrollAmount, behavior: 'smooth' });
10466}
10467"
10468class="absolute bottom-2 left-1/2 -translate-x-1/2 animate-bounce p-1 rounded-full hover:bg-gray-100 transition-colors cursor-pointer"
10469aria-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"
10470x-data
10471x-init="$store.cart.hasError = false; $store.cart.subtotal = &quot;$0.00&quot;; $store.cart.originalSubtotal = null; $store.cart.grandTotal = &quot;$0.00&quot;; $store.cart.totals = []; $store.cart.couponCode = &quot;&quot;; $store.cart.expeditedProduct = {&quot;id&quot;:&quot;12060&quot;,&quot;price&quot;:&quot;$7.99&quot;,&quot;add_url&quot;:&quot;https:\/\/www.simplystamps.com\/checkout\/cart\/add\/uenc\/aHR0cHM6Ly93d3cuc2ltcGx5c3RhbXBzLmNvbS9jb250YWN0cw,,\/product\/12060\/form_key\/e3374479ab86b5888526972805eb3d8f\/qty\/1\/&quot;}; $store.cart.expeditedInCart = false; $store.cart.totalSavings = null; $store.cart.savings = {&quot;label&quot;:null,&quot;has_savings&quot;:false,&quot;sale_savings&quot;:null,&quot;price_adjustments&quot;:[],&quot;your_price&quot;:null}"> <template x-if="!$store.cart.isEmpty"><div><div class="mb-3 text-right"> <button
10472onclick="clearCart(); return false;"
10473class="text-xs text-red-600 hover:text-red-700 hover:underline transition-colors">
10473 Clear Cart </button></div><div x-data="{ threshold: 79 }"
10474x-show="$store.cart.subtotalNumeric > 0 && $store.cart.subtotalNumeric >= threshold"
10475x-cloak
10476class="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
10477type="button"
10478@click="$store.promo.remove()"
10479class="text-xs text-green-700 hover:text-green-900 underline"
10480>Remove</button></div> </template> <template x-if="!$store.cart.couponCode"> <button
10481type="button"
10482@click="$store.promo.open()"
10483class="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"
10484> <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
10485x-transition:enter="transition ease-out duration-200"
10486x-transition:enter-start="opacity-0 translate-y-1"
10487x-transition:enter-end="opacity-100 translate-y-0"
10488x-transition:leave="transition ease-in duration-150"
10489x-transition:leave-start="opacity-100 translate-y-0"
10490x-transition:leave-end="opacity-0 translate-y-1"
10491class="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
10492type="button"
10493onclick="addExpeditedToCart()"
10494@mouseenter="showTip = true"
10495@mouseleave="showTip = false"
10496class="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/"
10497@click.prevent="$store.cart.hasError ? $store.cart.shake($event.currentTarget) : $store.upsell.maybeProceed('https://www.simplystamps.com/checkout/onepage/')"
10498class="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/"
10499class="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
10500@click="$store.cart.closeDrawer()"
10501class="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
10502x-data
10503x-show="$store.promo.isOpen"
10504x-cloak
10505class="fixed inset-0 z-[100] overflow-y-auto"
10506aria-labelledby="promo-modal-title"
10507role="dialog"
10508aria-modal="true"
10509><div
10510x-show="$store.promo.isOpen"
10511x-transition:enter="transition ease-out duration-200"
10512x-transition:enter-start="opacity-0"
10513x-transition:enter-end="opacity-100"
10514x-transition:leave="transition ease-in duration-150"
10515x-transition:leave-start="opacity-100"
10516x-transition:leave-end="opacity-0"
10517@click="$store.promo.close()"
10518class="fixed inset-0 bg-black/50 backdrop-blur-sm"
10519></div><div @click.self="$store.promo.close()" class="fixed inset-0 flex items-center justify-center p-4"><div
10520x-show="$store.promo.isOpen"
10521x-transition:enter="transition ease-out duration-200"
10522x-transition:enter-start="opacity-0 scale-95"
10523x-transition:enter-end="opacity-100 scale-100"
10524x-transition:leave="transition ease-in duration-150"
10525x-transition:leave-start="opacity-100 scale-100"
10526x-transition:leave-end="opacity-0 scale-95"
10527@click.stop
10528class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6"
10529> <button
10530@click="$store.promo.close()"
10531type="button"
10532class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10"
10533> <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
10534x-show="$store.promo.success"
10535x-transition:enter="transition ease-out duration-300"
10536x-transition:enter-start="opacity-0 scale-95"
10537x-transition:enter-end="opacity-100 scale-100"
10538class="flex flex-col items-center justify-center py-4"
10539><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
10540type="text"
10541id="promo-code-input"
10542x-model="$store.promo.code"
10543placeholder="Enter promo code"
10544class="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"
10545:disabled="$store.promo.isLoading"
10546@keydown.escape="$store.promo.close()"
10547/> <button
10548type="button"
10549@click="navigator.clipboard.readText().then(text => { $store.promo.code = text.trim(); })"
10550class="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"
10551title="Paste from clipboard"
10552:disabled="$store.promo.isLoading"
10553> <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
10554x-show="$store.promo.error"
10555x-transition
10556class="text-sm text-red-600 text-center"
10557x-text="$store.promo.error"
10558></div><div class="flex gap-3"> <button
10559@click="$store.promo.close()"
10560type="button"
10561class="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"
10562:disabled="$store.promo.isLoading"
10563> Cancel </button> <button
10564type="submit"
10565class="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"
10566:disabled="$store.promo.isLoading || !$store.promo.code.trim()"
10567> <span x-show="!$store.promo.isLoading">
10567Apply 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
10568x-data="{ open: false }"
10569@expedited-added.window="open = true"
10570x-show="open"
10571x-cloak
10572class="fixed inset-0 z-[100] overflow-y-auto"
10573aria-labelledby="expedited-modal-title"
10574role="dialog"
10575aria-modal="true"
10576><div
10577x-show="open"
10578x-transition:enter="transition ease-out duration-200"
10579x-transition:enter-start="opacity-0"
10580x-transition:enter-end="opacity-100"
10581x-transition:leave="transition ease-in duration-150"
10582x-transition:leave-start="opacity-100"
10583x-transition:leave-end="opacity-0"
10584@click="open = false"
10585class="fixed inset-0 bg-black/50 backdrop-blur-sm"
10586></div><div @click.self="open = false" class="fixed inset-0 flex items-center justify-center p-4"><div
10587x-show="open"
10588x-transition:enter="transition ease-out duration-200"
10589x-transition:enter-start="opacity-0 scale-95"
10590x-transition:enter-end="opacity-100 scale-100"
10591x-transition:leave="transition ease-in duration-150"
10592x-transition:leave-start="opacity-100 scale-100"
10593x-transition:leave-end="opacity-0 scale-95"
10594@click.stop
10595class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6"
10596> <button
10597@click="open = false"
10598type="button"
10599class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10"
10600> <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
10601@click="open = false"
10602type="button"
10603class="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"
10604> Close </button></div></div></div></div><div
10605x-data
10606x-show="$store.upsell.isOpen"
10607x-cloak
10608@keydown.escape.window="$store.upsell.isOpen && $store.upsell.skip()"
10609class="fixed inset-0 z-[120] overflow-y-auto"
10610role="dialog"
10611aria-modal="true"
10612aria-labelledby="upsell-modal-title"
10613style="display: none;"
10614><div
10615x-show="$store.upsell.isOpen"
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"
10622@click="$store.upsell.skip()"
10623class="fixed inset-0 bg-black/50 backdrop-blur-sm"
10624></div><div class="fixed inset-0 flex items-center justify-center p-4"><div
10625x-show="$store.upsell.isOpen"
10626x-transition:enter="transition ease-out duration-200"
10627x-transition:enter-start="opacity-0 scale-95"
10628x-transition:enter-end="opacity-100 scale-100"
10629x-transition:leave="transition ease-in duration-150"
10630x-transition:leave-start="opacity-100 scale-100"
10631x-transition:leave-end="opacity-0 scale-95"
10632@click.stop
10633class="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col"
10634><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"
10635: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>
10635 <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"
10636@click="$store.upsell.toggle(product.id)"
10637class="w-full flex items-center justify-center gap-1.5 py-2 text-sm font-medium rounded-lg border transition-colors"
10638: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"
10639class="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()"
10640class="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()"
10641:disabled="$store.upsell.isLoading"
10642class="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 }"
10643x-show="$store.auth.isStaff"
10644x-cloak
10645@mouseenter="open = true"
10646@mouseleave="open = false"
10647class="fixed left-0 top-3/4 -translate-y-1/2 z-[9999] hidden lg:block"
10648style="display: none;"><div x-show="!open"
10649x-transition:enter="transition ease-out duration-200"
10650x-transition:enter-start="opacity-0 -translate-x-2"
10651x-transition:enter-end="opacity-100 translate-x-0"
10652x-transition:leave="transition ease-in duration-150"
10653x-transition:leave-start="opacity-100"
10654x-transition:leave-end="opacity-0"
10655class="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"
10656x-transition:enter="transition ease-out duration-200"
10657x-transition:enter-start="opacity-0 -translate-x-4"
10658x-transition:enter-end="opacity-100 translate-x-0"
10659x-transition:leave="transition ease-in duration-150"
10660x-transition:leave-start="opacity-100 translate-x-0"
10661x-transition:leave-end="opacity-0 -translate-x-4"
10662class="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/' : '#'"
10663target="_blank"
10664class="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">
10664 <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> </svg> Zephyr Theme Settings </a></li><li class="border-t border-gray-200 pt-2 mt-2"> <button @click="$store.navEdit.open()"
10665class="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> 
10665<script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() {
10666Mage.Cookies.set(
10667'FASTLY_CDN_ENV',
10668'250e7c153723fdf375c9db11b6b2b305',
10669new Date(new Date().getTime() + 86400000)
10670);
10671}); </script>
10671 
10671<script type="text/javascript"> (function () {
10672var name = 'FASTLY_CDN_FORMKEY';
10673if (document.cookie.indexOf(name + '=') !== -1) {
10674return;
10675}
10676var expires = new Date(new Date().getTime() + 86400000);
10677document.cookie = name + '=' + 'e3374479ab86b5888526972805eb3d8f'
10678+ '; expires=' + expires.toUTCString()
10679+ '; path=/'        + '; domain=.www.simplystamps.com';
10680})(); </script>
10680<div id="wizardModal"
10681x-data="{ isOpen: false, wizardUrl: '', showConfirm: false, showResume: false, isLoading: true, hasSession: false, pendingUrl: '' }"
10682x-show="isOpen || showResume"
10683x-cloak
10684@keydown.escape.window="showConfirm ? showConfirm = false : (showResume ? showResume = false : null)"
10685class="fixed inset-0 z-50 overflow-hidden"
10686style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 transition-opacity"
10687@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"
10688x-transition:leave="transition ease-in duration-300"
10689x-transition:leave-start="opacity-100"
10690x-transition:leave-end="opacity-0"
10691class="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"
10692:src="wizardUrl"
10693@load="isLoading = false; clearTimeout(window._wizardLoadTimeout);"
10694class="w-full h-full"
10695frameborder="0"
10696allow="clipboard-read; clipboard-write"
10697title="Product Customization Wizard"> </iframe> </template></div></div><div x-show="showResume"
10698x-transition:enter="transition ease-out duration-200"
10699x-transition:enter-start="opacity-0"
10700x-transition:enter-end="opacity-100"
10701x-transition:leave="transition ease-in duration-150"
10702x-transition:leave-start="opacity-100"
10703x-transition:leave-end="opacity-0"
10704class="fixed inset-0 z-[60] flex items-center justify-center p-4"
10705style="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"
10706x-transition:enter="transition ease-out duration-200"
10707x-transition:enter-start="opacity-0 scale-95"
10708x-transition:enter-end="opacity-100 scale-100"
10709x-transition:leave="transition ease-in duration-150"
10710x-transition:leave-start="opacity-100 scale-100"
10711x-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); })"
10712class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
10713Start New Design </button> <button @click="showResume = false; isOpen = true;"
10714class="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"
10715x-transition:enter="transition ease-out duration-200"
10716x-transition:enter-start="opacity-0"
10717x-transition:enter-end="opacity-100"
10718x-transition:leave="transition ease-in duration-150"
10719x-transition:leave-start="opacity-100"
10720x-transition:leave-end="opacity-0"
10721class="fixed inset-0 z-[60] flex items-center justify-center p-4"
10722style="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"
10723x-transition:enter="transition ease-out duration-200"
10724x-transition:enter-start="opacity-0 scale-95"
10725x-transition:enter-end="opacity-100 scale-100"
10726x-transition:leave="transition ease-in duration-150"
10727x-transition:leave-start="opacity-100 scale-100"
10728x-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"
10729class="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); })"
10730class="px-4 py-2 text-sm font-medium text-white bg-brand-primary rounded-lg hover:bg-brand-primary/90 transition-colors">
10731Close Designer </button></div></div></div></div>
10731<script> window._wizardLoadTimeout = null;// Internal: load a fresh wizard URL into the iframe
10732window._wizardStartNew = function(url) {
10733var modal = document.getElementById('wizardModal');
10734if (!modal || !modal._x_dataStack) return;
10735var data = modal._x_dataStack[0];
10736data.wizardUrl = '';
10737setTimeout(function() {
10738data.wizardUrl = url;
10739}, 0);
10740// Fallback: if iframe hasn't loaded after 30s, show error state
10741clearTimeout(window._wizardLoadTimeout);
10742window._wizardLoadTimeout = setTimeout(function() {
10743if (data.isLoading && data.isOpen) {
10744console.error('[Wizard] Load timeout — iframe did not respond');
10745data.isLoading = false;
10746data.hasSession = false;
10747var iframe = document.getElementById('wizard_wrapper');
10748if (iframe) {
10749iframe.srcdoc = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;flex-direction:column;gap:16px;color:#374151;">'
10750+ '<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>'
10751+ '<p style="font-size:18px;font-weight:600;margin:0;">Something went wrong</p>'
10752+ '<p style="font-size:14px;color:#6b7280;margin:0;">The designer took too long to load.</p>'
10753+ '<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 &amp; Try Again</button>'
10754+ '</div>';
10755}
10756}
10757}, 30000);
10758};// Set when a customer signs in or registers inside the wizard iframe.
10759//
10760// The ajax-login module calls parent.LogInParent() across the frame boundary after a
10761// successful login or registration. Defining that name here is what makes the call land
10762// on Zephyr — without it the notification is silently dropped, since this theme has its
10763// own wizard modal and never loads the PCE one that defines LogInParent.
10764window._wizardAuthChanged = false;
10765window.LogInParent = function() {
10766console.log('[Wizard] Customer authenticated inside the iframe');
10767window._wizardAuthChanged = true;
10768};// Internal: handle post-close actions (cart refresh etc)
10769window._wizardOnClose = function(refreshCart) {
10770clearTimeout(window._wizardLoadTimeout);// The page behind the wizard was rendered for a logged-out visitor, so everything
10771// keyed off login state is stale — most visibly the My Account entry in the nav.
10772// A reload also picks up the cart, so it supersedes the refresh below.
10773if (window._wizardAuthChanged) {
10774window._wizardAuthChanged = false;
10775console.log('[Wizard] Auth changed inside wizard, reloading host page');
10776window.location.reload();
10777return;
10778}if (refreshCart && window.Alpine && Alpine.store('cart')) {
10779setTimeout(function() {
10780console.log('[Wizard] Refreshing cart data...');
10781Alpine.store('cart').refresh();
10782}, 300);
10783}
10784};// Global function to open wizard modal
10785window.openWizardModal = function(url) {
10786console.log('[Wizard] Opening modal with URL:', url);
10787var modal = document.getElementById('wizardModal');
10788if (modal && modal._x_dataStack) {
10789var data = modal._x_dataStack[0];
10790// If there's an existing session, ask user what to do
10791if (data.hasSession && data.wizardUrl) {
10792data.pendingUrl = url;
10793data.showResume = true;
10794return;
10795}
10796// Fresh open — load the wizard
10797data.isLoading = true;
10798data.isOpen = true;
10799data.hasSession = false;
10800window._wizardStartNew(url);
10801} else {
10802console.error('[Wizard] Modal not found or Alpine.js not initialized');
10803}
10804};// Allow wizard iframe to close the modal and optionally refresh cart
10805window.closeWizardModal = function(refreshCart) {
10806if (typeof refreshCart === 'undefined') refreshCart = false;
10807console.log('[Wizard] Closing modal, refresh cart:', refreshCart);
10808var modal = document.getElementById('wizardModal');
10809if (modal && modal._x_dataStack) {
10810var data = modal._x_dataStack[0];
10811data.isOpen = false;
10812// If the wizard completed (added to cart), destroy the session
10813if (refreshCart) {
10814data.hasSession = false;
10815data.wizardUrl = '';
10816} else {
10817data.hasSession = true;
10818}
10819window._wizardOnClose(refreshCart);
10820}
10821};// Legacy compatibility: alias for old PCE modal functions
10822window.closeModalWindow = function() {
10823window.closeWizardModal(true);
10824};// Minimal jQuery shim for PCE wizard iframe compatibility
10825// newpce.js calls window.parent.jQuery("iframe#wizard_wrapper").height()
10826if (typeof window.jQuery === 'undefined') {
10827window.jQuery = function(selector) {
10828var el = document.querySelector(selector);
10829return {
10830height: function() { return el ? el.clientHeight : window.innerHeight; }
10831};
10832};
10833}// Called by wizard when user clicks close button - shows confirmation dialog
10834window.requestCloseIframe = function() {
10835console.log('[Wizard] Close requested, showing confirmation');
10836var modal = document.getElementById('wizardModal');
10837if (modal && modal._x_dataStack) {
10838modal._x_dataStack[0].showConfirm = true;
10839}
10840};window.redirectToCart = function() {
10841window.location.href = '/checkout/cart';
10842}; </script>
10842<div x-data="loginModal()"
10843x-show="$store.auth.showLoginModal"
10844x-cloak
10845@keydown.escape.window="$store.auth.showLoginModal = false"
10846class="fixed inset-0 z-50 overflow-y-auto"
10847style="display: none;"><div x-show="$store.auth.showLoginModal"
10848x-transition:enter="transition ease-out duration-300"
10849x-transition:enter-start="opacity-0"
10850x-transition:enter-end="opacity-100"
10851x-transition:leave="transition ease-in duration-200"
10852x-transition:leave-start="opacity-100"
10853x-transition:leave-end="opacity-0"
10854@click="if (Date.now() - $store.auth._loginOpenedAt > 300) $store.auth.showLoginModal = false"
10855class="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"
10856x-transition:enter="transition ease-out duration-300"
10857x-transition:enter-start="opacity-0 transform scale-95"
10858x-transition:enter-end="opacity-100 transform scale-100"
10859x-transition:leave="transition ease-in duration-200"
10860x-transition:leave-start="opacity-100 transform scale-100"
10861x-transition:leave-end="opacity-0 transform scale-95"
10862@click.stop
10863class="relative bg-white rounded-lg shadow-xl w-full max-w-md"> <button @click="$store.auth.showLoginModal = false"
10864class="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"
10865: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'"
10866class="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"
10867alt="Simply Stamps"
10868class="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"
10869id="login-email"
10870name="login[username]"
10871x-model="loginForm.username"
10872required
10873class="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"
10874id="login-password"
10875name="login[password]"
10876x-model="loginForm.password"
10877required
10878class="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"
10879id="login-remember"
10880name="persistent_remember_me"
10881x-model="loginForm.rememberMe"
10882class="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"
10883:disabled="loading"
10884class="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"
10885@click="switchForm('register')"
10886class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10886ded-md transition-colors"> Create an Account </button></div><div class="mt-4 text-center"> <button type="button"
10887@click="switchForm('forgot')"
10888class="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"
10889: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'"
10890class="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"
10891alt="Simply Stamps"
10892class="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"
10893id="register-firstname"
10894name="firstname"
10895x-model="registerForm.firstname"
10896required
10897class="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"
10898id="register-lastname"
10899name="lastname"
10900x-model="registerForm.lastname"
10901required
10902class="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"
10903id="register-email"
10904name="email"
10905x-model="registerForm.email"
10906required
10907class="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"
10908id="register-password"
10909name="password"
10910x-model="registerForm.password"
10911required
10912class="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"
10913id="register-confirmation"
10914name="confirmation"
10915x-model="registerForm.confirmation"
10916required
10917class="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"
10918id="register-newsletter"
10919name="is_subscribed"
10920x-model="registerForm.newsletter"
10921value="1"
10922class="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"
10923:disabled="loading"
10924class="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"
10925@click="switchForm('login')"
10926class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10926ded-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"
10927: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'"
10928class="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"
10929alt="Simply Stamps"
10930class="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"
10931id="forgot-email"
10932name="email"
10933x-model="forgotForm.email"
10934required
10935class="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"
10936:disabled="loading"
10937class="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"
10938@click="switchForm('login')"
10939class="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>
10939<script> function loginModal() {
10940return {
10941currentForm: 'login',
10942loading: false,
10943message: '',
10944messageType: 'error',
10945formKey: 'e3374479ab86b5888526972805eb3d8f',loginForm: {
10946username: '',
10947password: '',
10948rememberMe: false
10949},registerForm: {
10950firstname: '',
10951lastname: '',
10952email: '',
10953password: '',
10954confirmation: '',
10955newsletter: false
10956},forgotForm: {
10957email: ''
10958},switchForm(formName) {
10959this.currentForm = formName;
10960this.message = '';
10961this.loading = false;
10962},async submitLogin() {
10963this.loading = true;
10964this.message = '';const formData = new FormData();
10965formData.append('form_key', this.formKey);
10966formData.append('login[username]', this.loginForm.username);
10967formData.append('login[password]', this.loginForm.password);
10968if (this.loginForm.rememberMe) {
10969formData.append('persistent_remember_me', 'on');
10970}try {
10971const response = await fetch('https://www.simplystamps.com/zephyr/auth/login/', {
10972method: 'POST',
10973body: formData,
10974credentials: 'same-origin'
10975});const data = await response.json();if (data.success) {
10976if (typeof zGA4 === 'function') {
10977zGA4('login', { method: 'site' });
10978if (data.hashedUserData) {
10979zGA4('set_user_data', { user_data: data.hashedUserData });
10980}
10981}
10982window.location.reload();
10983} else {
10984await new Promise(resolve => setTimeout(resolve, 800));
10985this.message = data.error || 'Invalid login or password.';
10986this.messageType = 'error';
10987this.loading = false;
10988}
10989} catch (error) {
10990await new Promise(resolve => setTimeout(resolve, 800));
10991this.message = 'An error occurred. Please try again.';
10992this.messageType = 'error';
10993this.loading = false;
10994}
10995},async submitRegister() {
10996this.loading = true;
10997this.message = '';if (this.registerForm.password !== this.registerForm.confirmation) {
10998await new Promise(resolve => setTimeout(resolve, 800));
10999this.message = 'Passwords do not match.';
11000this.messageType = 'error';
11001this.loading = false;
11002return;
11003}const formData = new FormData();
11004formData.append('form_key', this.formKey);
11005formData.append('firstname', this.registerForm.firstname);
11006formData.append('lastname', this.registerForm.lastname);
11007formData.append('email', this.registerForm.email);
11008formData.append('password', this.registerForm.password);
11009formData.append('confirmation', this.registerForm.confirmation);
11010if (this.registerForm.newsletter) {
11011formData.append('is_subscribed', '1');
11012}const recaptchaToken = await getZephyrRecaptchaToken('register');
11013if (recaptchaToken) {
11014formData.append('recaptcha_response', recaptchaToken);
11015}try {
11016const response = await fetch('https://www.simplystamps.com/zephyr/auth/register/', {
11017method: 'POST',
11018body: formData,
11019credentials: 'same-origin'
11020});const data = await response.json();if (data.success) {
11021if (typeof zGA4 === 'function') {
11022zGA4('sign_up', { method: 'site' });
11023if (data.hashedUserData) {
11024zGA4('set_user_data', { user_data: data.hashedUserData });
11025}
11026}
11027window.location.reload();
11028} else {
11029await new Promise(resolve => setTimeout(resolve, 800));
11030this.message = data.error || 'There was a problem with your registration.';
11031this.messageType = 'error';
11032this.loading = false;
11033}
11034} catch (error) {
11035await new Promise(resolve => setTimeout(resolve, 800));
11036this.message = 'An error occurred. Please try again.';
11037this.messageType = 'error';
11038this.loading = false;
11039}
11040},async submitForgot() {
11041this.loading = true;
11042this.message = '';const formData = new FormData();
11043formData.append('form_key', this.formKey);
11044formData.append('email', this.forgotForm.email);// reCAPTCHA v3. The forgotpasswordpost endpoint rejects a token-less post
11045// outright, so no token means no reset email.
11046const recaptchaToken = await getZephyrRecaptchaToken('forgot');
11047if (recaptchaToken) {
11048formData.append('recaptcha_response', recaptchaToken);
11049}try {
11050const response = await fetch('https://www.simplystamps.com/customer/account/forgotpasswordpost/', {
11051method: 'POST',
11052body: formData,
11053credentials: 'same-origin'
11054});const text = await response.text();if (text.includes('success-msg') || text.includes('password-reset')) {
11055this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.';
11056this.messageType = 'success';
11057this.forgotForm.email = '';
11058} else if (text.includes('error-msg')) {
11059const parser = new DOMParser();
11060const doc = parser.parseFromString(text, 'text/html');
11061const errorMsg = doc.querySelector('.error-msg');
11062this.message = errorMsg ? errorMsg.textContent.trim() : 'Unable to send password reset email.';
11063this.messageType = 'error';
11064} else {
11065this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.';
11066this.messageType = 'success';
11067}
11068} catch (error) {
11069this.message = 'An error occurred. Please try again.';
11070this.messageType = 'error';
11071} finally {
11072this.loading = false;
11073}
11074}
11075}
11076} </script>
11076 <input id="searchFocusProxy" type="text" aria-hidden="true"
11077style="position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;" /><div id="searchModal"
11078x-data="searchModal()"
11079x-show="$store.search.isOpen"
11080x-cloak
11081@keydown.escape.window="$store.search.close()"
11082class="fixed inset-0 z-[70] overflow-hidden"
11083style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm transition-opacity hidden md:block"
11084x-show="$store.search.isOpen"
11085x-transition:enter="transition ease-out duration-300"
11086x-transition:enter-start="opacity-0"
11087x-transition:enter-end="opacity-100"
11088x-transition:leave="transition ease-in duration-200"
11089x-transition:leave-start="opacity-100"
11090x-transition:leave-end="opacity-0"
11091@click="$store.search.close()"></div><div class="fixed inset-0 flex items-start justify-center md:pt-12 md:px-4"
11092@mousedown.self="$store.search.close()"><div x-show="$store.search.isOpen"
11093class="relative bg-white w-full h-full flex flex-col md:h-auto md:max-w-2xl md:rounded-xl md:shadow-2xl"
11094x-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"
11095alt="Simply Stamps"
11096class="h-8 w-auto" /> </a> <button @click="$store.search.close()"
11097class="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"
11098d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg></div> <input type="text"
11099x-ref="searchInput"
11100x-model="query"
11101@input="onInput()"
11102@keydown.down.prevent="selectNext"
11103@keydown.tab.prevent="selectNext"
11104@keydown.up.prevent="selectPrev"
11105@keydown.enter.prevent="selectCurrent"
11106placeholder="Search products, articles..."
11107class="w-full pl-12 pr-24 py-4 text-lg border-2 border-gray-200 rounded-xl
11108focus:border-brand-primary focus:outline-none focus:ring-0
11109transition-colors"
11110autocomplete="off" /><div x-show="$store.search.isLoading"
11111class="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"
11112@click="clearSearch()"
11113type="button"
11114class="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()"
11115type="button"
11116:disabled="query.length < minChars"
11117class="absolute right-2 flex items-center justify-center h-10 px-3 bg-brand-primary text-white
11118rounded-lg hover:bg-brand-primary-hover transition-colors
11119disabled: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"
11120d="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"
11121@click.prevent="selectPopular('self-inking stamp')"
11122class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200
11123rounded-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"
11124d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> self-inking stamp </button> <button type="button"
11125@click.prevent="selectPopular('wood handle stamp')"
11126class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200
11127rounded-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"
11128d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> wood handle stamp </button> <button type="button"
11129@click.prevent="selectPopular('library embosser')"
11130class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200
11131rounded-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"
11132d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> library embosser </button> <button type="button"
11133@click.prevent="selectPopular('wedding address stamps')"
11134class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200
11135rounded-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"
11136d="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"
11137x-ref="resultsPane"
11138class="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)"
11139:class="{ 'bg-blue-50 text-blue-900': selectedIndex === index }"
11140class="flex items-center gap-3 px-3 py-1.5 rounded-lg
11141hover:bg-gray-50 transition-colors cursor-pointer group"
11142@mousemove="selectedIndex = index"
11143@click.prevent="selectSuggestion(suggestion)"> <svg class="w-4 h-4 text-gray-300 shrink-0" :class="{ 'text-blue-400': selectedIndex === index }"
11144fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
11145d="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"
11146x-transition:enter="transition ease-out duration-200"
11147x-transition:enter-start="opacity-0"
11148x-transition:enter-end="opacity-100"
11149x-transition:leave="transition ease-in duration-150"
11150x-transition:leave-start="opacity-100"
11151x-transition:leave-end="opacity-0"
11152class="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"
11153:class="{
11154'border-t border-gray-300': suggestionsCount > 0 || (blogFirst && blogCount > 0),
11155'opacity-40 pointer-events-none': $store.search.isLoading
11156}"><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"
11157:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (productsOffset + index) }"
11158class="flex items-center gap-4 p-3 rounded-lg border border-transparent
11159hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer"
11160@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"
11161class="w-full h-full object-cover"
11162onerror="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"
11163class="text-xs text-gray-500 line-clamp-1 mt-0.5"
11164x-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"
11165:class="{
11166'order-3': blogFirst,
11167'order-5': !blogFirst,
11168'border-t border-gray-300': !blogFirst || suggestionsCount > 0,
11169'opacity-40 pointer-events-none': $store.search.isLoading
11170}"><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"
11171:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (blogOffset + index) }"
11172class="flex items-center gap-4 p-3 rounded-lg border border-transparent
11173hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer"
11174@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"
11175d="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"
11176class="text-xs text-gray-500 line-clamp-1 mt-0.5"
11177x-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"
11178class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200
11179rounded-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"
11180d="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">&#8593;</kbd> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">&#8595;</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"
11181:href="'https://www.simplystamps.com/catalogsearch/result/?q=' + encodeURIComponent(query)"
11182class="flex items-center gap-1 text-sm font-medium text-brand-primary hover:text-brand-primary-hover
11183transition-colors ml-auto">
11184View 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>
11184<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/search-modal.js?v=1126"></script>
11184 
11184<script> searchModalConfig.minChars = 3;
11185searchModalConfig.searchUrl = 'https://www.simplystamps.com/searchautocomplete/ajax/get/';
11186searchModalConfig.suggestUrl = 'https://www.simplystamps.com/zephyr/search/suggest/';
11187searchModalConfig.resultUrl = 'https://www.simplystamps.com/catalogsearch/result/';
11188searchModalConfig.formKey = 'e3374479ab86b5888526972805eb3d8f';
11189searchModalConfig.suggestedQueries = ["self-inking stamp","wood handle stamp","library embosser","wedding address stamps"];
11190searchModalConfig.blogFirst = false; </script>
11190<style> .line-clamp-1 {
11191overflow: hidden;
11192display: -webkit-box;
11193-webkit-box-orient: vertical;
11194-webkit-line-clamp: 1;
11195} </style></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> 
11195<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>
11195 
11195<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/rileyprint.js?v=1126" defer></script>
11195 
11195<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/product-media.js?v=1126" defer></script>
11195 
11195<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/file-upload.js?v=1126" defer></script>
11195 
11195<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-drawer.js?v=1126" defer></script>
11195 
11195<script> // Configure cart drawer URLs. cart-drawer.js is now deferred (its var cartDrawerConfig
11196// default object still runs before DOMContentLoaded per spec), and every read of
11197// cartDrawerConfig.* inside cart-drawer.js happens lazily inside user-triggered
11198// handlers (open drawer, add/remove item, checkout) — none of it runs at
11199// alpine:init — so it's safe to set these values on DOMContentLoaded too.
11200document.addEventListener('DOMContentLoaded', function() {
11201cartDrawerConfig.getItemsUrl = 'https://www.simplystamps.com/zephyr/cart/getItems/';
11202cartDrawerConfig.deleteUrl = 'https://www.simplystamps.com/checkout/cart/delete/';
11203cartDrawerConfig.removeItemsUrl = 'https://www.simplystamps.com/zephyr/cart/removeItems/';
11204cartDrawerConfig.updatePostUrl = 'https://www.simplystamps.com/checkout/cart/updatePost/';
11205cartDrawerConfig.clearUrl = 'https://www.simplystamps.com/zephyr/cart/clear/';
11206cartDrawerConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/';
11207cartDrawerConfig.cartUrl = 'https://www.simplystamps.com/checkout/cart/';
11208cartDrawerConfig.formKey = 'e3374479ab86b5888526972805eb3d8f';
11209// Cart drawer feature settings (from admin config)
11210cartDrawerConfig.enableCheckboxDelete = true;
11211cartDrawerConfig.enableIndividualDelete = true;
11212cartDrawerConfig.showClearCart = true;
11213cartDrawerConfig.checkoutButtonText = 'Proceed to Checkout';
11214}); </script>
11214 
11214<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-upsell.js?v=1126"></script>
11214 
11214<script> // Configure cart upsell popup
11215cartUpsellConfig.getUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/getUpsells/';
11216cartUpsellConfig.addUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/addUpsells/';
11217cartUpsellConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/';
11218cartUpsellConfig.formKey = 'e3374479ab86b5888526972805eb3d8f';
11219cartUpsellConfig.enabled = true;
11220cartUpsellConfig.storeId = '5';
11221cartUpsellConfig.devMode = false; </script>
11221<script> // Fetch a reCAPTCHA v3 token, lazy-loading Google's api.js on first use so it
11222// adds no page weight until a protected form is actually submitted. Shared by
11223// every Zephyr form protected by v3 (registration, contact, quote), which is
11224// also why it must be the ONLY thing loading recaptcha on modern pages — a
11225// v3 key can't execute against a grecaptcha client created by a bare v2
11226// api.js load, so don't reintroduce v2 script tags into modern templates.
11227// Resolves null when reCAPTCHA is disabled or the script/execution fails —
11228// the server decides what to do with a missing token. Never rejects or hangs.
11229window.zephyrRecaptchaKey = '6Lem-q0gAAAAAMhOqUz2tke2IN3t0Pt37x5oqRq3';
11230window.getZephyrRecaptchaToken = function(action) {
11231var key = window.zephyrRecaptchaKey;
11232if (!key) return Promise.resolve(null);
11233return new Promise(function(resolve) {
11234var settled = false;
11235var done = function(token) { if (!settled) { settled = true; resolve(token || null); } };
11236// Whatever happens, never leave the submit hanging
11237setTimeout(function() { done(null); }, 15000);function execute() {
11238try {
11239grecaptcha.ready(function() {
11240try {
11241grecaptcha.execute(key, { action: action })
11242.then(done, function() { done(null); });
11243} catch (e) { done(null); }
11244});
11245} catch (e) { done(null); }
11246}if (window.grecaptcha) { execute(); return; }
11247var s = document.createElement('script');
11248s.src = 'https://www.google.com/recaptcha/api.js?render=' + key;
11249s.onload = execute;
11250s.onerror = function() { done(null); };
11251document.head.appendChild(s);
11252});
11253}; </script>
11253 
11253<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/promo-code.js?v=1126"></script>
11253 
11253<script> // Configure promo code URLs
11254promoCodeConfig.applyCouponUrl = 'https://www.simplystamps.com/zephyr/cart/applyCoupon/';
11255promoCodeConfig.removeCouponUrl = 'https://www.simplystamps.com/zephyr/cart/removeCoupon/';
11256promoCodeConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; </script>
11256 
11256<script>function setLocation(url){window.location.href=url;}</script>
11256 
11256<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/alpine.min.js?v=1126" defer></script>
11256 
11256<script> document.addEventListener('alpine:init', () => {
11257// Global UI state - controls visibility of shared UI elements
11258Alpine.store('ui', {
11259hideHeader: false,
11260// Header scroll state (updated by stickyHeader component)
11261headerVisible: true,
11262headerHeight: 0,// Call this when opening fullscreen overlays (lightbox, modals, etc.)
11263enterFullscreen() {
11264this.hideHeader = true;
11265},// Call this when closing fullscreen overlays
11266exitFullscreen() {
11267this.hideHeader = false;
11268},// Get the effective top position for fixed elements below the header
11269getHeaderOffset() {
11270// If header is hidden (scroll or fullscreen), no offset needed
11271if (this.hideHeader || !this.headerVisible) {
11272return 0;
11273}
11274return this.headerHeight;
11275}
11276});Alpine.store('auth', {
11277showLoginModal: false,
11278showRefreshOverlay: false,
11279refreshMessage: '',
11280isLoggedIn: false,
11281isStaff: false,
11282adminEditBase: '',
11283customerName: '',
11284customerEmail: '',
11285companyStoreUrl: '',
11286cartCount: 0,
11287cartSubtotal: 0,
11288formKey: '',
11289ready: false,
11290_loginOpenedAt: 0,init() {
11291// State is hydrated via AJAX so the page HTML stays static/compressible.
11292this.fetchStatus();
11293},/**
11294* Fetch auth status from session via AJAX.
11295* Primary hydration path (called from init()), and also used
11296* after login/logout to refresh state reactively.
11297*/
11298fetchStatus() {
11299var self = this;
11300fetch('https://www.simplystamps.com/zephyr/auth/status/', {
11301credentials: 'same-origin',
11302headers: { 'X-Requested-With': 'XMLHttpRequest' }
11303})
11304.then(function(r) { return r.json(); })
11305.then(function(data) {
11306self.isLoggedIn = !!data.isLoggedIn;
11307self.isStaff = !!data.isStaff;
11308self.adminEditBase = data.adminEditBase || '';
11309self.customerName = data.customerName || '';
11310self.customerEmail = data.customerEmail || '';
11311self.companyStoreUrl = data.companyStoreUrl || '';
11312self.cartCount = data.cartCount || 0;
11313self.cartSubtotal = data.cartSubtotal || 0;
11314self.formKey = data.formKey || '';// Hydrate the cart store's badge count / free-shipping subtotal from
11315// this same response — the async equivalent of the inline global
11316// state object the ESI block used to provide synchronously.
11317if (window.Alpine && Alpine.store('cart')) {
11318var cartStore = Alpine.store('cart');
11319cartStore.itemCount = data.cartCount || 0;
11320cartStore.isEmpty = !(data.cartCount > 0);
11321cartStore.subtotalNumeric = data.cartSubtotal || 0;
11322cartStore._esiReady = true;
11323}
11324if (data.messages && data.messages.length && window.Alpine) {
11325var flash = Alpine.store('flash');
11326for (var i = 0; i < data.messages.length; i++) {
11327if (flash && flash.addMessage) flash.addMessage(data.messages[i].type, data.messages[i].text);
11328}
11329}
11330self.ready = true;
11331})
11332.catch(function() {
11333self.ready = true;
11334});
11335},setLoggedIn(customerData) {
11336this.isLoggedIn = true;
11337this.customerName = customerData.name || '';window.dispatchEvent(new CustomEvent('customer-logged-in', {
11338detail: customerData
11339}));
11340},setLoggedOut() {
11341this.isLoggedIn = false;
11342this.isStaff = false;
11343this.adminEditBase = '';
11344this.customerName = '';
11345this.customerEmail = '';
11346this.companyStoreUrl = '';window.dispatchEvent(new CustomEvent('customer-logged-out'));
11347}
11348});Alpine.store('search', {
11349isOpen: false,
11350isLoading: false,
11351hasSearched: false,
11352_openedAt: 0,open() {
11353// Focus proxy input immediately to preserve iOS user-gesture context
11354// so the virtual keyboard opens. Focus transfers to real input in $nextTick.
11355var proxy = document.getElementById('searchFocusProxy');
11356if (proxy) proxy.focus();
11357this.isOpen = true;
11358this._openedAt = Date.now();
11359},close() {
11360if (Date.now() - this._openedAt < 300) return;
11361this.isOpen = false;
11362}
11363});// Confirmation modal store
11364Alpine.store('confirm', {
11365isOpen: false,
11366title: '',
11367message: '',
11368html: null,
11369confirmText: 'Confirm',
11370cancelText: 'Cancel',
11371confirmStyle: 'danger', // 'danger' or 'primary'
11372onConfirm: null,
11373onCancel: null,show(options) {
11374this.title = options.title || 'Confirm';
11375this.message = options.message || 'Are you sure?';
11376this.html = options.html || null;
11377this.confirmText = options.confirmText || 'Confirm';
11378this.cancelText = options.cancelText || 'Cancel';
11379this.confirmStyle = options.confirmStyle || 'danger';
11380this.onConfirm = options.onConfirm || null;
11381this.onCancel = options.onCancel || null;
11382this.isOpen = true;
11383},confirm() {
11384if (this.onConfirm) {
11385this.onConfirm();
11386}
11387this.close();
11388},cancel() {
11389if (this.onCancel) {
11390this.onCancel();
11391}
11392this.close();
11393},close() {
11394this.isOpen = false;
11395this.onConfirm = null;
11396this.onCancel = null;
11397// Defer clearing html until after the 150ms leave transition
11398// so the image doesn't vanish mid-animation
11399var self = this;
11400setTimeout(function() {
11401if (!self.isOpen) self.html = null;
11402}, 200);
11403}
11404});// Flash messages store — messages arrive via auth store's fetchStatus() (AJAX),
11405// or as inline JSON on pages that embed them directly (fallback below).
11406Alpine.store('flash', {
11407messages: [],
11408_nextId: 1,
11409_seen: {},init() {
11410var self = this;// Fallback: read messages embedded as JSON in page HTML (for pages without ESI block)
11411var scriptEl = document.querySelector('script[data-flash-messages]');
11412if (scriptEl) {
11413try {
11414var inline = JSON.parse(scriptEl.textContent);
11415for (var i = 0; i < inline.length; i++) {
11416self.addMessage(inline[i].type, inline[i].text);
11417}
11418} catch (e) {}
11419scriptEl.remove();
11420}
11421},addMessage(type, text) {
11422// Deduplication
11423var key = type + ':' + text;
11424if (this._seen[key]) {
11425return;
11426}
11427this._seen[key] = true;var id = this._nextId++;
11428this.messages.push({ id: id, type: type, text: text, visible: true });// Auto-dismiss non-error messages after 8 seconds
11429if (type !== 'error') {
11430var self = this;
11431setTimeout(function() {
11432self.dismiss(id);
11433}, 8000);
11434}
11435},dismiss(id) {
11436var idx = this.messages.findIndex(function(m) { return m.id === id; });
11437if (idx !== -1) {
11438this.messages[idx].visible = false;
11439var self = this;
11440setTimeout(function() {
11441self.messages = self.messages.filter(function(m) { return m.id !== id; });
11442}, 300);
11443}
11444},show(type, text) {
11445this.addMessage(type, text);
11446}
11447});Alpine.store('flash').init();
11448});// BFCache restoration handler — when iOS Safari restores a page from BFCache,
11449// Alpine stores are frozen with stale data. Refresh critical stores.
11450window.addEventListener('pageshow', function(event) {
11451if (event.persisted) {
11452// Page was restored from BFCache — refresh cart data and auth status
11453if (typeof Alpine !== 'undefined' && Alpine.store) {
11454var cart = Alpine.store('cart');
11455if (cart) {
11456cart.lastFetch = null;
11457cart.refresh();
11458}
11459var auth = Alpine.store('auth');
11460if (auth && typeof auth.fetchStatus === 'function') {
11461auth.fetchStatus();
11462}
11463}
11464}
11465});
11466</script>
11466<div x-data
11467x-show="$store.auth.showRefreshOverlay"
11468x-cloak
11469x-transition:enter="transition ease-out duration-200"
11470x-transition:enter-start="opacity-0"
11471x-transition:enter-end="opacity-100"
11472class="fixed inset-0 z-[200] flex items-center justify-center bg-white/80 backdrop-blur-sm"
11473style="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
11474x-data
11475x-show="$store.confirm.isOpen"
11476x-cloak
11477@keydown.escape.window="$store.confirm.cancel()"
11478class="fixed inset-0 z-[100] overflow-y-auto"
11479aria-labelledby="confirm-modal-title"
11480role="dialog"
11481aria-modal="true"
11482><div
11483x-show="$store.confirm.isOpen"
11484x-transition:enter="transition ease-out duration-200"
11485x-transition:enter-start="opacity-0"
11486x-transition:enter-end="opacity-100"
11487x-transition:leave="transition ease-in duration-150"
11488x-transition:leave-start="opacity-100"
11489x-transition:leave-end="opacity-0"
11490@click="$store.confirm.cancel()"
11491class="fixed inset-0 bg-black/50 backdrop-blur-sm"
11492></div><div class="fixed inset-0 flex items-center justify-center p-4"><div
11493x-show="$store.confirm.isOpen"
11494x-transition:enter="transition ease-out duration-200"
11495x-transition:enter-start="opacity-0 scale-95"
11496x-transition:enter-end="opacity-100 scale-100"
11497x-transition:leave="transition ease-in duration-150"
11498x-transition:leave-start="opacity-100 scale-100"
11499x-transition:leave-end="opacity-0 scale-95"
11500@click.stop
11501class="relative bg-white rounded-xl shadow-2xl max-w-md w-full p-6"
11502> <button
11503@click="$store.confirm.cancel()"
11504type="button"
11505class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10"
11506> <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"
11507:class="$store.confirm.confirmStyle === 'danger' ? 'bg-red-100' : 'bg-blue-100'"> <svg x-show="$store.confirm.confirmStyle === 'danger'"
11508class="h-6 w-6 text-red-600"
11509fill="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'"
11510class="h-6 w-6 text-blue-600"
11511fill="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"
11512class="text-lg font-semibold text-gray-900 text-center mb-2"
11513x-text="$store.confirm.title"></h3><div x-show="$store.confirm.html" class="text-sm text-gray-600 text-center mb-6"
11514x-html="$store.confirm.html"></div><p x-show="!$store.confirm.html" class="text-sm text-gray-600 text-center mb-6"
11515x-text="$store.confirm.message"></p><div class="flex gap-3"> <button
11516@click="$store.confirm.cancel()"
11517type="button"
11518class="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"
11519x-text="$store.confirm.cancelText"> </button> <button
11520@click="$store.confirm.confirm()"
11521type="button"
11522class="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"
11523:class="$store.confirm.confirmStyle === 'danger'
11524? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
11525: 'bg-brand-primary hover:bg-brand-primary-hover focus:ring-brand-primary'"
11526x-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.