1<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>How to Make Custom Rubber Stamps</title><meta name="viewport" content="width=device-width, initial-scale=1" />
1<script> window.dataLayer = window.dataLayer || []; 2window.zGA4 = function(eventName, params) { 3if (params && params.ecommerce) { 4dataLayer.push({ ecommerce: null }); 5} 6params = params || {}; 7params.event = eventName; 8dataLayer.push(params); 9}; </script>
9
9<script> (function(){ 10try { 11var m = document.cookie.match(/(?:^|; )zephyr_ud=([^;]*)/); 12if (!m) return; 13var ud = JSON.parse(decodeURIComponent(m[1])); 14if (ud && Object.keys(ud).length) { 15window.dataLayer = window.dataLayer || []; 16window.dataLayer.push({ event: 'set_user_data', user_data: ud }); 17} 18} catch (e) {} 19})(); </script>
19 <style> [x-cloak] { display: none !important; } 20/* Sticky header spacing - responsive estimates, refined by JS in header.phtml */ 21:root { 22--header-height: 65px; 23} 24@media (min-width: 1024px) { 25:root { 26--header-height: 97px; 27} 28} </style><meta name="description" content="Discover the fascinating process behind creating custom rubber stamps. Learn about the materials, design, and crafting techniques in this insightful guide." /><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="How Custom Rubber Stamps Are Made" /><meta property="og:type" content="article" /><meta property="og:url" content="https://www.simplystamps.com/blog/how-custom-rubber-stamps-are-made" /><meta property="og:description" content="Discover the fascinating process behind creating custom rubber stamps. Learn about the materials, design, and crafting techniques in this insightful guide." /><meta property="og:site_name" content="Simply Stamps" /><meta property="og:image" content="https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/e/5/cache/5/ece9a24a761836a70934a998c163f8c8/e50507be5c03870a152d5ca40f4c86f1.jpg?width=1200&quality=60" /><meta property="article:published_time" content="2023-08-28T20:51:00+00:00" /><meta property="article:modified_time" content="2024-04-15T20:02:30+00:00" /><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:title" content="How Custom Rubber Stamps Are Made" /><meta name="twitter:description" content="Discover the fascinating process behind creating custom rubber stamps. Learn about the materials, design, and crafting techniques in this insightful guide." /><meta name="twitter:image" content="https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/e/5/cache/5/ece9a24a761836a70934a998c163f8c8/e50507be5c03870a152d5ca40f4c86f1.jpg?width=1200&quality=60" /><link rel="stylesheet" href="https://cdn.simplystamps.com/skin/frontend/zephyr/default/css/print.css?v1126" media="print" >
28<script src="https://cdn.simplystamps.com/js/mpblog/tagcloud/tagcanvas.min.js?v1126"></script>
28<script type="text/javascript"> //<![CDATA[ 29// mage/cookies.js now loads via a deferred
29<script> tag (see head.phtml), so Mage.Cookies 30// isn't guaranteed to exist yet at this point in parsing. Wait for DOMContentLoaded, which 31// fires only after deferred scripts have executed. 32document.addEventListener('DOMContentLoaded', function () { 33Mage.Cookies.path = '/'; 34Mage.Cookies.domain = '.www.simplystamps.com'; 35}); 36//]]> </script>
36<link rel="stylesheet" href="https://cdn.simplystamps.com/skin/frontend/zephyr/default/css/dist/theme.css?v=1126">
36<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 37new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 38j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 39'https://www.simplystamps.com/d/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 40})(window,document,'script','dataLayer','GTM-TT325PX9');</script>
40 <noscript><iframe src="https://www.simplystamps.com/d/ns.html?id=GTM-TT325PX9" 41height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
41<script type="text/javascript" src="https://chimpstatic.com/mcjs-connected/js/users/de94d9b4a6aa148b052eb1fad/fac024488a3ce12a438eb7e65.js" defer></script>
41<style id="zephyr-brand-colors"> /* Zephyr Brand Colors - Store: sstamps_en */ 42:root { 43--brand-primary: #002E49; 44--brand-primary-hover: #00456E; 45--brand-secondary: #55B5E3; 46--brand-secondary-hover: #22495C; 47--brand-success: #19A745; 48--brand-warning: #C27803; 49--brand-danger: #E02424; 50--brand-info: #3F83F8; 51--blog-bg: #FFFFFF; 52} </style><link title="Custom Rubber Stamps Guide | Simply Stamps Blog - Post Feed" href="https://www.simplystamps.com/mpblog/rss/post/store_id/5/" type="application/rss+xml" rel="alternate" /><link title="Custom Rubber Stamps Guide | Simply Stamps Blog - Comment Feed" href="https://www.simplystamps.com/mpblog/rss/comment/store_id/5/" type="application/rss+xml" rel="alternate" /><link rel="canonical" href="https://www.simplystamps.com/blog/how-custom-rubber-stamps-are-made"><link rel="author" href="https://www.simplystamps.com/blog/sabrina-gonzalez" />
52<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"How Custom Rubber Stamps Are Made","description":"Discover the fascinating process behind creating custom rubber stamps. Learn about the materials, design, and crafting techniques in this insightful guide.","image":["https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/e/5/cache/5/ece9a24a761836a70934a998c163f8c8/e50507be5c03870a152d5ca40f4c86f1.jpg?width=1200\u0026quality=60","https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/e/5/cache/5/ece9a24a761836a70934a998c163f8c8/e50507be5c03870a152d5ca40f4c86f1.jpg?width=800\u0026quality=60"],"datePublished":"2023-08-28T20:51:00+00:00","dateModified":"2024-04-15T20:02:30+00:00","author":{"@type":"Person","name":"Sabrina Gonzalez","url":"https://www.simplystamps.com/blog/sabrina-gonzalez","sameAs":["https://www.linkedin.com/in/sabrina-gonzalez-/"],"description":"Sabrina joined the Simply Stamps team in 2021 as the SEO Copywriter and brings over three years of experience in marketing and content creation. With hands-on training from stamp experts, she not only possesses a deep understanding of SEO strategies but also has comprehensive knowledge of rubber stamp assembly.Born in Miami, Florida, and proud of her Cuban heritage, Sabrina infuses her work with the richness of her background, offering fresh perspectives and insights. With a passion for storytelling and connecting with readers on a meaningful level, she holds a Bachelor's degree in Communication from the University of North Florida. Her work aims to bridge the gap between the brand and its community, leveraging her unique blend of academic knowledge and cultural depth to engage audiences. Outside of work, Sabrina is a media enthusiast with interests sprawling across books, movies, music, video games, and more. She can often be spotted participating in community events or spending quality time with her two beloved cats.Connect with Sabrina on LinkedIn: https://www.linkedin.com/in/sabrina-gonzalez-/","affiliation":{"@type":"Organization","name":"Simply Stamps","url":"https://www.simplystamps.com/","logo":{"@type":"ImageObject","url":"https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg"}}},"publisher":{"@type":"Organization","name":"Simply Stamps","url":"https://www.simplystamps.com/","logo":{"@type":"ImageObject","url":"https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.simplystamps.com/blog/how-custom-rubber-stamps-are-made"}}</script>
52
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="mpblog-index-post"><div class="flex min-h-screen flex-col"> <div class="flex flex-1 flex-col"><div x-data="stickyHeader()" x-init="init()" class="relative"><div 56x-show="mobileMenuOpen" 57@click="mobileMenuOpen = false" 58x-transition:enter="transition-opacity ease-out duration-300" 59x-transition:enter-start="opacity-0" 60x-transition:enter-end="opacity-100" 61x-transition:leave="transition-opacity ease-in duration-200" 62x-transition:leave-start="opacity-100" 63x-transition:leave-end="opacity-0" 64class="fixed inset-0 bg-black bg-opacity-50 z-40" 65style="display: none;" 66></div><div 67x-show="$store.imagePreview.isOpen" 68x-cloak 69@keydown.escape.window="$store.imagePreview.close()" 70class="fixed inset-0 z-[100] flex items-center justify-center p-4" 71style="display: none;" 72><div 73@click.stop="$store.imagePreview.close()" 74x-transition:enter="transition-opacity ease-out duration-300" 75x-transition:enter-start="opacity-0" 76x-transition:enter-end="opacity-100" 77x-transition:leave="transition-opacity ease-in duration-200" 78x-transition:leave-start="opacity-100" 79x-transition:leave-end="opacity-0" 80class="absolute inset-0 bg-black bg-opacity-80" 81></div><div 82x-transition:enter="transition ease-out duration-300" 83x-transition:enter-start="opacity-0 scale-95" 84x-transition:enter-end="opacity-100 scale-100" 85x-transition:leave="transition ease-in duration-200" 86x-transition:leave-start="opacity-100 scale-100" 87x-transition:leave-end="opacity-0 scale-95" 88class="relative w-full max-w-3xl max-h-[90vh]" 89> <button 90@click.stop="$store.imagePreview.close()" 91class="absolute -top-10 right-0 p-2 text-white hover:text-gray-300 transition-colors" 92aria-label="Close preview" 93> <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button> <img 94:src="$store.imagePreview.imageUrl" 95:alt="$store.imagePreview.imageAlt" 96class="max-w-full max-h-[85vh] mx-auto object-contain rounded-lg shadow-2xl bg-white" 97/> <template x-if="$store.imagePreview.imageAlt && $store.imagePreview.linkUrl"> <a 98:href="$store.imagePreview.linkUrl" 99x-text="$store.imagePreview.imageAlt" 100class="block text-center text-white text-sm mt-3 font-medium hover:underline transition-colors" 101></a> </template> <template x-if="$store.imagePreview.imageAlt && !$store.imagePreview.linkUrl"><p 102x-text="$store.imagePreview.imageAlt" 103class="text-center text-white text-sm mt-3 font-medium" 104></p> </template></div></div><div 105x-show="mobileMenuOpen" 106@click.away="mobileMenuOpen = false" 107x-transition:enter="transform transition ease-out duration-300" 108x-transition:enter-start="-translate-x-full" 109x-transition:enter-end="translate-x-0" 110x-transition:leave="transform transition ease-in duration-200" 111x-transition:leave-start="translate-x-0" 112x-transition:leave-end="-translate-x-full" 113class="fixed inset-0 bg-white z-[60] flex flex-col" 114style="display: none;" 115><div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 shrink-0"> <a href="https://www.simplystamps.com/" class="block"> <img 116src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 117alt="Simply Stamps" 118loading="eager" 119class="h-8 w-auto" 120/> </a> <button 121@click="mobileMenuOpen = false" 122class="p-2 hover:bg-gray-100 rounded-lg transition-colors" 123aria-label="Close menu" 124> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M18 6L6 18" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 6L18 18" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button></div><div class="flex-1 overflow-y-auto py-4"> <button 125@click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); $store.search.open()" 126class="flex items-center gap-3 w-[calc(100%-2rem)] mx-4 px-4 py-3 border-2 border-gray-300 rounded-xl text-gray-400 hover:border-brand-primary hover:text-gray-500 transition-colors" 127> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 shrink-0"> <path d="M11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19Z" 128stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="text-base">Search products...</span> </button><div x-init="loadMobileMenu()" x-html="mobileMenuContent"></div><div class="mt-4 pt-4" x-show="$store.auth.isLoggedIn" x-cloak style="display: none;"><h3 class="text-sm font-bold text-gray-700 uppercase tracking-wider px-4 mb-2">Account</h3> <a href="https://www.simplystamps.com/customer/account/" 129class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/> </svg> My Account </a> <a href="https://www.simplystamps.com/sales/order/history/" 130class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/> </svg> My Orders </a> <a x-show="$store.auth.companyStoreUrl" 131:href="$store.auth.companyStoreUrl" 132class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors" 133style="display: none;"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/> </svg> Company Store </a> <button @click="mobileMenuOpen = false;
134if (typeof window.zGA4 === 'function') { window.zGA4('logout', { method: 'site' }); } 135$store.auth.refreshMessage = 'Logging out...'; 136$store.auth.showRefreshOverlay = true; 137window.location.href = 'https://www.simplystamps.com/customer/account/logout/';" 138class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-500 hover:text-red-600 hover:bg-gray-100 transition-colors w-full mt-1"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 139d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/> </svg> Log Out </button></div><div class="mt-4 pt-4"><h3 class="text-sm font-bold text-gray-700 uppercase tracking-wider px-4 mb-2">Help</h3> <button x-show="$store.chat.available" x-cloak id="mobile-live-chat-now" class="flex items-center gap-3 px-4 py-2.5 text-base font-medium text-brand-primary hover:bg-gray-100 transition-colors w-full cursor-pointer" style="display: none;"> <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> <span class="text-left"> Chat with a Live Agent <span class="block text-sm text-brand-success font-medium">Available now</span> <span class="block text-sm text-gray-500" x-text="$store.chat.hoursLabel"></span> </span> </button> <a href="https://www.simplystamps.com/contacts" 140class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/> </svg> Track My Order </a> <a href="https://www.simplystamps.com/contacts" 141class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> Contact Us </a> <a href="https://www.simplystamps.com/faq" 142class="flex items-center gap-3 px-4 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-100 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> FAQ </a></div></div><div class="p-4 border-t border-gray-200 shrink-0" x-show="$store.auth.ready && !$store.auth.isLoggedIn" style="display: none;"> <button @click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); mobileMenuOpen = false; setTimeout(() => { $store.auth._loginOpenedAt = Date.now(); $store.auth.showLoginModal = true; }, 100)" 143class="w-full flex items-center justify-center space-x-2 px-4 py-3 bg-brand-primary hover:bg-brand-primary-hover text-white font-medium rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5"> <path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" 144stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" 145stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span>Log In</span> </button></div></div> <header 146:class="{ 147'-translate-y-full': !headerVisible || $store.ui.hideHeader 148}" 149class="fixed top-0 left-0 right-0 bg-white z-50 shadow-lg transition-all duration-300" 150><div class="lg:hidden border-b border-gray-200"><div class="container mx-auto px-4"><div class="flex items-center justify-between py-3"><div class="flex items-center"> <button 151@click="mobileMenuOpen = !mobileMenuOpen" 152class="p-2 hover:bg-gray-100 rounded-lg transition-colors" 153aria-label="Menu" 154> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6"> <path d="M3 12H21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3 6H21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3 18H21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button></div><div class="flex-1 flex justify-center"> <a href="https://www.simplystamps.com/" class="block"> <img 155src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 156alt="Simply Stamps" 157loading="eager" 158class="h-10 w-auto" 159/> </a></div><div class="flex items-center"> <button 160@click="$store.cart.openDrawer()" 161@dblclick.prevent="window.location.href='/checkout/cart/'" 162class="relative p-2 hover:bg-gray-100 rounded-lg transition-colors" 163aria-label="Open cart" 164> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6"> <path d="M9 22C9.55228 22 10 21.5523 10 21C10 20.4477 9.55228 20 9 20C8.44772 20 8 20.4477 8 21C8 21.5523 8.44772 22 9 22Z" fill="#4F4F4F" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20 22C20.5523 22 21 21.5523 21 21C21 20.4477 20.5523 20 20 20C19.4477 20 19 20.4477 19 21C19 21.5523 19.4477 22 20 22Z" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M1 1H5L7.68 14.39C7.77144 14.8504 8.02191 15.264 8.38755 15.5583C8.75318 15.8526 9.2107 16.009 9.68 16H19.4C19.8693 16.009 20.3268 15.8526 20.6925 15.5583C21.0581 15.264 21.3086 14.8504 21.4 14.39L23 6H6" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span x-cloak 165x-show="$store.cart.itemCount > 0" 166x-text="$store.cart.itemCount" 167class="absolute -top-1 -right-1 bg-brand-danger text-white text-xs rounded-full h-5 flex items-center justify-center font-bold" 168:class="$store.cart.itemCount >= 10 ? 'min-w-[1.25rem] px-1' : 'w-5'"></span> </button></div></div><div 169x-show="mobileSearchOpen" 170x-transition:enter="transition ease-out duration-200" 171x-transition:enter-start="opacity-0 -translate-y-2" 172x-transition:enter-end="opacity-100 translate-y-0" 173x-transition:leave="transition ease-in duration-150" 174x-transition:leave-start="opacity-100 translate-y-0" 175x-transition:leave-end="opacity-0 -translate-y-2" 176class="pb-4" 177style="display: none;" 178><form class="searchautocomplete UI-SEARCHAUTOCOMPLETE" action="https://www.simplystamps.com/catalogsearch/result/" method="get" 179data-tip="Search entire store here..." 180data-url="//www.simplystamps.com/searchautocomplete/ajax/get/" 181data-minchars="3" 182data-delay="500"
183data-hide-delay="1000"><label for="search">Search</label><div class="nav"><div class="nav-search-in"> <span class="category-fake UI-CATEGORY-TEXT">All</span> <span class="nav-down-arrow"></span> <select name="cat" class="category UI-CATEGORY"><option value="0">All</option><option value="713" > Address Stamps</option><option value="958" > Custom Business & Office Stamps</option><option value="749" > Craft Stamps</option><option value="1387" > Custom Logo Stamps</option><option value="770" > Embossers</option><option value="802" > Stamp Ink & Pads</option><option value="811" > Teacher Stamps</option><option value="793" > Notary</option><option value="815" > Wedding Stamps</option><option value="1432" > Redeemable Personalized Address Stamp with Gift Box</option><option value="2291" > St. Patrick's Day & Irish Theme Stamps</option><option value="2391" > Stamp of the Week</option><option value="809" > Breast Cancer Awareness</option><option value="3035" > Gift Notes</option><option value="3073" > Real Estate Supplies</option><option value="3085" > Custom Rubber Stamps</option><option value="3094" > Custom Return Address Labels</option><option value="3109" > Here Tomorrow</option><option value="3118" > StampToons</option><option value="3327" > Custom Face Stamps</option><option value="3432" > Salt & Paper Collaboration</option> </select></div><div class="nav-input UI-NAV-INPUT"> <input class="input-text UI-SEARCH" type="text" autocomplete="off" name="q" value="" maxlength="128" /></div><div class="searchautocomplete-loader UI-LOADER"><div id="g01"></div><div id="g02"></div><div id="g03"></div><div id="g04"></div><div id="g05"></div><div id="g06"></div><div id="g07"></div><div id="g08"></div></div></div><div class="nav-submit-button"> <button type="submit" title="Go" class="button">Go</button></div> <style> .store-4-search-autocomplete { 184top: 108px !important; 185right: unset !important; 186}@media (min-width: 768px) { 187.store-4-search-autocomplete { 188top: 74px !important; 189right: unset !important; 190} 191} </style><div style="display:none" class="searchautocomplete-placeholder UI-PLACEHOLDER "></div></form></div></div></div><div class="hidden lg:block border-b border-gray-200"><div class="mx-8"><div class="flex justify-between py-4"><div class="flex items-center justify-center w-1/8"> <a href="https://www.simplystamps.com/"> <img 192src="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg" 193alt="Simply Stamps" 194loading="eager" 195class="h-12 w-auto" 196/> </a></div> <nav class="relative flex-1 px-2 md:px-4 flex items-center justify-around"><div class="hidden md:block"> <nav class="relative"><ul class="flex items-center justify-center space-x-2 list-none p-0 m-0 flex-wrap"><li x-data="{ open: false, hoverTimeout: null }" 197@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 198@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 199@keydown.escape="open = false" 200class="static"> <a href="https://www.simplystamps.com/address-stamps/view-all" 201:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 202class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Address Stamps </a><div x-show="open" style="display: none;" 203x-transition:enter="transition ease-out duration-150" 204x-transition:enter-start="opacity-0 -translate-y-1" 205x-transition:enter-end="opacity-100 translate-y-0" 206x-transition:leave="transition ease-in duration-100" 207x-transition:leave-start="opacity-100 translate-y-0" 208x-transition:leave-end="opacity-0 -translate-y-1" 209class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 210defaultHref: '/address-stamps', 211defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 212defaultLabel: 'Address Stamps Collection', 213activeHref: '/address-stamps', 214activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 215activeLabel: 'Address Stamps Collection', 216set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 217reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 218}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/address-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg" alt="Address Stamps Collection" :src="activeImage" :alt="activeLabel" 219loading="lazy" width="224" height="224" 220class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 221x-text="activeLabel">Address Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-4 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Style</h3><ul class="space-y-2.5"><li><a href="/address-stamps/script-handwritten" 222@mouseenter="set('/address-stamps/script-handwritten', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Script-AS-224x224.jpg', 'Script Address Stamps')" 223class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Script Address Stamps</a></li><li><a href="/address-stamps/custom-line" 224@mouseenter="set('/address-stamps/custom-line', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Multiple-Line-A
224S-224x224.jpg', 'Multiple Line Address Stamps')" 225class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Multiple Line Address Stamps</a></li><li><a href="/address-stamps/round-address-stamps" 226@mouseenter="set('/address-stamps/round-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-AS-224x224.jpg', 'Round Address Stamps')" 227class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Address Stamps</a></li><li><a href="/address-stamps/rectangle-address-stamps" 228@mouseenter="set('/address-stamps/rectangle-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-AS-224x224.jpg', 'Rectangle Address Stamps')" 229class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Address Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 230@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Text-AS-224x224.jpg', 'Line Address Stamps')" 231class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Line Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Design</h3><ul class="space-y-2.5"><li><a href="/address-stamps/floral" 232@mouseenter="set('/address-stamps/floral', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Floral-AS-224x224.jpg', 'Floral Address Stamps')" 233class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Floral Address Stamps</a></li><li><a href="/address-stamps/wedding-couple" 234@mouseenter="set('/address-stamps/wedding-couple', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wedding-AS-224x224.jpg', 'Wedding Address Stamps')" 235class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Address Stamps</a></li><li><a href="/address-stamps/dog-address-stamps" 236@mouseenter="set('/address-stamps/dog-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Dog-AS-224x224.jpg', 'Dog Address Stamps')" 237class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Dog Address Stamps</a></li><li><a href="/address-stamps/animals" 238@mouseenter="set('/address-stamps/animals', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Animal-AS-224x224.jpg', 'Animal Address Stamps')" 239class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Animal Address Stamps</a></li><li><a href="/address-stamps/hearts" 240@mouseenter="set('/address-stamps/hearts', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Whimsical-AS-224x224.jpg', 'Whimsical Address Stamps')" 241class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Whimsical Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Seasonal</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/business-address-stamps" 242@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Business-AS-224x224.jpg', 'Business Address Stamps')" 243class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/address-stamps/patriotic-address-stamps" 244@mouseenter="set('/address-stamps/patriotic-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Patriotic-AS-224x224.jpg', 'Patriotic Address Stamps')" 245class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Patriotic Address Stamps</a></li><li><a href="/address-stamps/holiday" 246@mouseenter="set('/address-stamps/holiday', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Holiday-AS-224x224.jpg', 'Holiday Address Stamps')" 247class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Address Stamps</a></li><li><a href="/address-stamps/christmas-address-stamps" 248@mouseenter="set('/address-stamps/christmas-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Christmas-AS-224x224.jpg', 'Christmas Address Stamps')" 249class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Christmas Address Stamps</a></li><li><a href="/custom-address-labels" 250@mouseenter="set('/custom-address-labels', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/address-stamps/SS-QCS-Address-Labels-AS-400x400.jpg', 'Custom Return Address Labels')" 251class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Return Address Labels</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Collabs</h3><ul class="space-y-2.5"><li><a href="/salt-and-paper" 252@mouseenter="set('/salt-and-paper', 'https://cdn.simplystamps.com/media/catalog/category/8-simply-stamps-spco-2026-AS-Salt-Wine-1-1-lifestyle-1-2000_1.jpg', 'Salt & Paper Collaboration')" 253style='background-image:url(https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/CMSImages/SS_Nav-Salt-and-Paper-Swish.png); background-repeat:no-repeat; background-position:left center; background-size:100% 100%; padding:6px 10px; margin:0 -10px;' 254class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Salt & Paper Collaboration</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/address-stamps/view-all" @mouseenter="reset()" 255class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Address Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
255<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 256@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 257@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 258@keydown.escape="open = false" 259class="static"> <a href="https://www.simplystamps.com/rubber-logo-stamps" 260:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 261class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Logo Stamps </a><div x-show="open" style="display: none;" 262x-transition:enter="transition ease-out duration-150" 263x-transition:enter-start="opacity-0 -translate-y-1" 264x-transition:enter-end="opacity-100 translate-y-0" 265x-transition:leave="transition ease-in duration-100" 266x-transition:leave-start="opacity-100 translate-y-0" 267x-transition:leave-end="opacity-0 -translate-y-1" 268class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 269defaultHref: '/rubber-logo-stamps', 270defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 271defaultLabel: 'Logo Stamps Collection', 272activeHref: '/rubber-logo-stamps', 273activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 274activeLabel: 'Logo Stamps Collection', 275set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 276reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 277}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/rubber-logo-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg" alt="Logo Stamps Collection" :src="activeImage" :alt="activeLabel" 278loading="lazy" width="224" height="224" 279class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 280x-text="activeLabel">Logo Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Shape</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/rectangle" 281@mouseenter="set('/rubber-logo-stamps/rectangle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-Logo.jpg', 'Rectangle Logo Stamp')" 282class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Logo Stamp</a></li><li><a href="/rubber-logo-stamps/round" 283@mouseenter="set('/rubber-logo-stamps/round', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-Logo-2.jpg', 'Round Logo Stamp')" 284class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Logo Stamp</a></li><li><a href="/rubber-logo-stamps/square"
285@mouseenter="set('/rubber-logo-stamps/square', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Square-Logo.jpg', 'Square Logo Stamp')" 286class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Square Logo Stamp</a></li><li><a href="/rubber-logo-stamps/catalog-stamps" 287@mouseenter="set('/rubber-logo-stamps/catalog-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Catalog-Logo.jpg', 'Catalog Stamps')" 288class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Catalog Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Artwork</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/with-text" 289@mouseenter="set('/rubber-logo-stamps/with-text', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Logo-and-Text.jpg', 'Logo & Text')" 290class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo & Text</a></li><li><a href="/rubber-logo-stamps/upload-art" 291@mouseenter="set('/rubber-logo-stamps/upload-art', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Artwork-Logo.jpg', 'Personalized Art')" 292class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Personalized Art</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/pre-inked-logo" 293@mouseenter="set('/rubber-logo-stamps/pre-inked-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Pre-Inked-Logo.jpg', 'Pre-Inked')" 294class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Pre-Inked</a></li><li><a href="/rubber-logo-stamps/self-inking" 295@mouseenter="set('/rubber-logo-stamps/self-inking', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Self-Ink-Logo.jpg', 'Self-Inking')" 296class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Self-Inking</a></li><li><a href="/rubber-logo-stamps/traditional-wooden-handle" 297@mouseenter="set('/rubber-logo-stamps/traditional-wooden-handle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wooden-Logo.jpg', 'Wood Handle')" 298class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wood Handle</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/rubber-logo-stamps" @mouseenter="reset()" 299class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Logo Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 300@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 301@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 302@keydown.escape="open = false" 303class="static"> <a href="https://www.simplystamps.com/business-office-stamps/signature-stamps" 304:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 305class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Signature Stamps </a><div x-show="open" style="display: none;" 306x-transition:enter="transition ease-out duration-150" 307x-transition:enter-start="opacity-0 -translate-y-1" 308x-transition:enter-end="opacity-100 translate-y-0" 309x-transition:leave="transition ease-in duration-100" 310x-transition:leave-start="opacity-100 translate-y-0" 311x-transition:leave-end="opacity-0 -translate-y-1" 312class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 313defaultHref: '/business-office-stamps/signature-stamps', 314defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 315defaultLabel: 'Signature Stamps Collection', 316activeHref: '/business-office-stamps/signature-stamps', 317activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 318activeLabel: 'Signature Stamps Collection', 319set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 320reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 321}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps/signature-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg" alt="Signature Stamps Collection" :src="activeImage" :alt="activeLabel" 322loading="lazy" width="224" height="224" 323class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 324x-text="activeLabel">Signature Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature Stamp Types</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/signature-only" 325@mouseenter="set('/business-office-stamps/signature-stamps/signature-only', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-only.jpg', 'Signature Only')" 326class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature Only</a></li><li><a href="/business-office-stamps/signature-stamps/with-date"
327@mouseenter="set('/business-office-stamps/signature-stamps/with-date', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-date.jpg', 'Signature with Date')" 328class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Date</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature & More</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/with-printed-name" 329@mouseenter="set('/business-office-stamps/signature-stamps/with-printed-name', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-name.jpg', 'Signature with Printed Name')" 330class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Printed Name</a></li><li><a href="/business-office-stamps/signature-stamps/with-logo" 331@mouseenter="set('/business-office-stamps/signature-stamps/with-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-logo.jpg', 'Signature with Logo')" 332class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Logo</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps/signature-stamps" @mouseenter="reset()" 333class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Signature Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 334@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 335@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 336@keydown.escape="open = false" 337class="static"> <a href="https://www.simplystamps.com/custom-rubber-stamps" 338:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 339class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Custom Stamps </a><div x-show="open" style="display: none;" 340x-transition:enter="transition ease-out duration-150" 341x-transition:enter-start="opacity-0 -translate-y-1" 342x-transition:enter-end="opacity-100 translate-y-0" 343x-transition:leave="transition ease-in duration-100" 344x-transition:leave-start="opacity-100 translate-y-0" 345x-transition:leave-end="opacity-0 -translate-y-1" 346class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 347defaultHref: '/custom-rubber-stamps', 348defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 349defaultLabel: 'Custom Stamps Collection', 350activeHref: '/custom-rubber-stamps', 351activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 352activeLabel: 'Custom Stamps Collection', 353set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 354reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 355}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/custom-rubber-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg" alt="Custom Stamps Collection" :src="activeImage" :alt="activeLabel" 356loading="lazy" width="224" height="224" 357class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 358x-text="activeLabel">Custom Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-self-inking-stamps" 359@mouseenter="set('/custom-rubber-stamps/custom-self-inking-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Self-Inking-Stamps-Custom-400x400.jpg', 'Custom Self-Inking Stamps')" 360class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Self-Inking Stamps</a></li><li><a href="/custom-rubber-stamps/custom-pre-inked-stamps" 361@mouseenter="set('/custom-rubber-stamps/custom-pre-inked-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Pre-Inked-Stamps-Custom-400x400.jpg', 'Custom Pre-Inked Stamps')" 362class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Pre-Inked Stamps</a></li><li><a href="/custom-rubber-stamps/custom-wood-handle-stamps" 363@mouseenter="set('/custom-rubber-stamps/custom-wood-handle-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Traditional-Hand-Stamps-Custom-400x400.jpg', 'Custom Wood Handle Stamps')" 364class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Wood Handle Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Specialty</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-european-hand-stamps" 365@mouseenter="set('/custom-rubber-stamps/custom-european-hand-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-European-Hand-Stamps-Custom-400x400.jpg', 'Custom European Hand Stamps')" 366class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom European Hand Stamps</a></li><li><a href="/custom-rubber-stamps/custom-professional-stamps" 367@mouseenter="set('/custom-rubber-stamps/custom-professional-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Professional-Stamps-Custom-400x400.jpg', 'Custom Professional Stamps')" 368class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Professional Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/custom-rubber-stamps" @mouseenter="reset()" 369class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Custom Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
369<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 370@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 371@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 372@keydown.escape="open = false" 373class="static"> <a href="https://www.simplystamps.com/business-office-stamps" 374:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 375class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Office Stamps </a><div x-show="open" style="display: none;" 376x-transition:enter="transition ease-out duration-150" 377x-transition:enter-start="opacity-0 -translate-y-1" 378x-transition:enter-end="opacity-100 translate-y-0" 379x-transition:leave="transition ease-in duration-100" 380x-transition:leave-start="opacity-100 translate-y-0" 381x-transition:leave-end="opacity-0 -translate-y-1" 382class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 383defaultHref: '/business-office-stamps', 384defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 385defaultLabel: 'Office Stamps Collection', 386activeHref: '/business-office-stamps', 387activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 388activeLabel: 'Office Stamps Collection', 389set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 390reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 391}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg" alt="Office Stamps Collection" :src="activeImage" :alt="activeLabel" 392loading="lazy" width="224" height="224" 393class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 394x-text="activeLabel">Office Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Banking</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/accounting-tax-stamps" 395@mouseenter="set('/business-office-stamps/accounting-tax-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Accounting-Stamps-OS-400x400.jpg', 'Accounting Stamps')" 396class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Accounting Stamps</a></li><li><a href="/business-office-stamps/business-address-stamps" 397@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Business-Address-Stamps-OS-400x400.jpg', 'Business Address Stamps')" 398class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement" 399@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Check-Endorsement-Stamps-OS-400x400.jpg', 'Check Endorsement Stamps')" 400class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Check Endorsement Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps" 401@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Deposit-Stamps-OS-400x400.jpg', 'Deposit Stamps')" 402class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Deposit Stamps</a></li><li><a href="/business-office-stamps/accounting-tax-stamps/received" 403@mouseenter="set('/business-office-stamps/accounting-tax-stamps/received', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Received-stamps-OS-400x400.jpg', 'Received Stamps')" 404class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Received Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 405@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Custom-text-Stamps-OS-400x400.jpg', 'Custom Text Stamps')" 406class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Text Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Office Essentials</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/office-stamps" 407@mouseenter="set('/business-office-stamps/office-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Stock-office-stamps-OS-400x400.jpg', 'Stock Office Stamps')" 408class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Stock Office Stamps</a></li><li><a href="/business-office-stamps/daters" 409@mouseenter="set('/business-office-stamps/daters', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Date-Stamps-OS-400x400.jpg', 'Date Stamps')" 410class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Date Stamps</a></li><li><a href="/business-office-stamps/checkbox" 411@mouseenter="set('/business-office-stamps/checkbox', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Checkbox-Stamps-OS-400x400.jpg', 'Checkbox
411Stamps')" 412class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Checkbox Stamps</a></li><li><a href="/business-office-stamps/secure-stamps" 413@mouseenter="set('/business-office-stamps/secure-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Security-Stamps-OS-400x400.jpg', 'Security Stamps')" 414class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Security Stamps</a></li><li><a href="/business-office-stamps/inspection-stamps" 415@mouseenter="set('/business-office-stamps/inspection-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-inspection-Stamps-OS-400x400.jpg', 'Inspection Stamps')" 416class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Inspection Stamps</a></li><li><a href="/business-office-stamps/medical-stamps" 417@mouseenter="set('/business-office-stamps/medical-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Medical-Stamps-OS-400x400.jpg', 'Medical Stamps')" 418class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Medical Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Professional Stamps</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/professional-stamps/llc-stamps" 419@mouseenter="set('/business-office-stamps/professional-stamps/llc-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-LLC-Stamps-OS-400x400.jpg', 'LLC Stamps')" 420class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">LLC Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/architect" 421@mouseenter="set('/business-office-stamps/professional-stamps/architect', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Architect-Stamps-OS-400x400.jpg', 'Architect Stamps')" 422class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Architect Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/engineer" 423@mouseenter="set('/business-office-stamps/professional-stamps/engineer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Engineer-Stamps-OS-400x400.jpg', 'Engineer Stamps')" 424class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Engineer Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/land-surveyor" 425@mouseenter="set('/business-office-stamps/professional-stamps/land-surveyor', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Land-Surveyor-Stamps-OS-400x400.jpg', 'Land Surveyor Stamps')" 426class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Land Surveyor Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/real-estate-appraiser" 427@mouseenter="set('/business-office-stamps/professional-stamps/real-estate-appraiser', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Real-Estate-Appraiser-Stamps-OS-400x400.jpg', 'Real Estate Appraiser Stamps')" 428class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Real Estate Appraiser Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/geologist-stamps-other" 429@mouseenter="set('/business-office-stamps/professional-stamps/geologist-stamps-other', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Geologist-Stamps-OS-400x400.jpg', 'Geologist Stamps')" 430class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Geologist Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps" @mouseenter="reset()" 431class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Office Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
431<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 432@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 433@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 434@keydown.escape="open = false" 435class="static"> <a href="https://www.simplystamps.com/embossers" 436:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 437class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Embossers </a><div x-show="open" style="display: none;" 438x-transition:enter="transition ease-out duration-150" 439x-transition:enter-start="opacity-0 -translate-y-1" 440x-transition:enter-end="opacity-100 translate-y-0" 441x-transition:leave="transition ease-in duration-100" 442x-transition:leave-start="opacity-100 translate-y-0" 443x-transition:leave-end="opacity-0 -translate-y-1" 444class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 445defaultHref: '/embossers', 446defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 447defaultLabel: 'Embossers Collection', 448activeHref: '/embossers', 449activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 450activeLabel: 'Embossers Collection', 451set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 452reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 453}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/embossers" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg" alt="Embossers Collection" :src="activeImage" :alt="activeLabel" 454loading="lazy" width="224" height="224" 455class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 456x-text="activeLabel">Embossers Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Personal & Occasion</h3><ul class="space-y-2.5"><li><a href="/embossers/custom-address" 457@mouseenter="set('/embossers/custom-address', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Address-EMB-400x400.jpg', 'Address Embossers')" 458class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Address Embossers</a></li><li><a href="/embossers/library-book-embossers" 459@mouseenter="set('/embossers/library-book-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Library-Book-EMB-400x400.jpg', 'Library Book Embossers')" 460class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Library Book Embossers</a></li><li><a href="/embossers/wedding-embossers" 461@mouseenter="set('/embossers/wedding-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-wedding-embosser-cat-emboss.jpg', 'Wedding Embossers')" 462class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Embossers</a></li><li><a href="/embossers/luxury-embossers" 463@mouseenter="set('/embossers/luxury-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-Luxury-Embossers-QCS-Nav-Embosser-400.jpg', 'Luxury Embossers')" 464class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Luxury Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Legal</h3><ul class="space-y-2.5"><li><a href="/embossers/corporate-seal" 465@mouseenter="set('/embossers/corporate-seal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Corporate-Seals-EMB-400x400.jpg', 'Corporate Seals')" 466class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Corporate Seals</a></li><li><a href="/embossers/notary-public" 467@mouseenter="set('/embossers/notary-public', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Notary-EMB-400x400.jpg', 'Notary Embossers')" 468class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Notary Embossers</a></li><li><a href="/embossers/custom-logo" 469@mouseenter="set('/embossers/custom-logo', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-LOGO-EMB-400x400.jpg', 'Logo Embossers')" 470class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 471@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/catalog/category/SS_cat-ink_and_accessories_embosser-accesories-265
471x265.jpg', 'Embosser Accessories')" 472class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/embossers" @mouseenter="reset()" 473class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Embossers <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 474@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 475@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 476@keydown.escape="open = false" 477class="relative"> <a href="https://www.simplystamps.com/notary-public" 478:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 479class="block font-medium p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary text-gray-700"> Notary </a></li><li x-data="{ open: false, hoverTimeout: null }" 480@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 481@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 482@keydown.escape="open = false" 483class="static"> <a href="https://www.simplystamps.com/teacher-stamps" 484:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 485class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Teacher Stamps </a><div x-show="open" style="display: none;" 486x-transition:enter="transition ease-out duration-150" 487x-transition:enter-start="opacity-0 -translate-y-1" 488x-transition:enter-end="opacity-100 translate-y-0" 489x-transition:leave="transition ease-in duration-100" 490x-transition:leave-start="opacity-100 translate-y-0" 491x-transition:leave-end="opacity-0 -translate-y-1" 492class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 493defaultHref: '/teacher-stamps', 494defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 495defaultLabel: 'Teacher Stamps Collection', 496activeHref: '/teacher-stamps', 497activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 498activeLabel: 'Teacher Stamps Collection', 499set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 500reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 501}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/teacher-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg" alt="Teacher Stamps Collection" :src="activeImage" :alt="activeLabel" 502loading="lazy" width="224" height="224" 503class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 504x-text="activeLabel">Teacher Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Classroom & Grading</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/grading"
505@mouseenter="set('/teacher-stamps/grading', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Grading-stamps-TS-400x400.jpg', 'Grading Stamps')" 506class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Grading Stamps</a></li><li><a href="/teacher-stamps/feedback" 507@mouseenter="set('/teacher-stamps/feedback', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Feedback-stamps-TS-400x400.jpg', 'Feedback Stamps')" 508class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Feedback Stamps</a></li><li><a href="/teacher-stamps/name-missing" 509@mouseenter="set('/teacher-stamps/name-missing', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Missing-Name-stamps-TS-400x400.jpg', 'Name Missing Stamps')" 510class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Missing Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Fun & Motivation</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/motivational" 511@mouseenter="set('/teacher-stamps/motivational', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Motivational-stamps-TS-400x400.jpg', 'Motivational Stamps')" 512class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Motivational Stamps</a></li><li><a href="/teacher-stamps/fun-designs" 513@mouseenter="set('/teacher-stamps/fun-designs', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Fun-stamps-TS-400x400.jpg', 'Fun Stamps')" 514class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Fun Stamps</a></li><li><a href="/teacher-stamps/from-the-desk-of" 515@mouseenter="set('/teacher-stamps/from-the-desk-of', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Book-stamps-TS-400x400.jpg', 'Book Stamps')" 516class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Book Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/teacher-stamps" @mouseenter="reset()" 517class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Teacher Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 518@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 519@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 520@keydown.escape="open = false" 521class="static"> <a href="https://www.simplystamps.com/craft-stamps" 522:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 523class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Craft Stamps </a><div x-show="open" style="display: none;" 524x-transition:enter="transition ease-out duration-150" 525x-transition:enter-start="opacity-0 -translate-y-1" 526x-transition:enter-end="opacity-100 translate-y-0" 527x-transition:leave="transition ease-in duration-100" 528x-transition:leave-start="opacity-100 translate-y-0" 529x-transition:leave-end="opacity-0 -translate-y-1" 530class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 531defaultHref: '/craft-stamps', 532defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 533defaultLabel: 'Craft Stamps Collection', 534activeHref: '/craft-stamps', 535activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 536activeLabel: 'Craft Stamps Collection', 537set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 538reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 539}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/craft-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg" alt="Craft Stamps Collection" :src="activeImage" :alt="activeLabel" 540loading="lazy" width="224" height="224" 541class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 542x-text="activeLabel">Craft Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Seasonal & Holiday</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/holiday-crafting"
543@mouseenter="set('/craft-stamps/holiday-crafting', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Holiday-Stamps-CS-400x400.jpg', 'Holiday Stamps')" 544class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Stamps</a></li><li><a href="/craft-stamps/halloween" 545@mouseenter="set('/craft-stamps/halloween', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Halloween-Stamps-CS-400x400.jpg', 'Halloween Stamps')" 546class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Halloween Stamps</a></li><li><a href="/craft-stamps/thanksgiving" 547@mouseenter="set('/craft-stamps/thanksgiving', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thanksgiving-Stamps-CS-400x400.jpg', 'Thanksgiving Stamps')" 548class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thanksgiving Stamps</a></li><li><a href="/craft-stamps/autumn-fall" 549@mouseenter="set('/craft-stamps/autumn-fall', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Autumn-Stamps-CS-400x400.jpg', 'Autumn Stamps')" 550class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Autumn Stamps</a></li><li><a href="/craft-stamps/summer" 551@mouseenter="set('/craft-stamps/summer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Summer-Stamps-CS-400x400.jpg', 'Summer Stamps')" 552class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Summer Stamps</a></li><li><a href="/craft-stamps/valentine-s-day-craft-stamp" 553@mouseenter="set('/craft-stamps/valentine-s-day-craft-stamp', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Valentines-Stamps-CS-400x400.jpg', 'Valentine\'s Day Stamps')" 554class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Valentine's Day Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Occasions & Gifts</h3><ul class="space-y-2.5"><li><a href="/wedding-stamps" 555@mouseenter="set('/wedding-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Wedding-Stamps-CS-400x400.jpg', 'Wedding Stamps')" 556class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Stamps</a></li><li><a href="/craft-stamps/we-have-moved" 557@mouseenter="set('/craft-stamps/we-have-moved', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Just-Moved-Stamps-CS-400x400.jpg', 'Just Moved Stamps')" 558class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Just Moved Stamps</a></li><li><a href="/craft-stamps/thank-you-stamps" 559@mouseenter="set('/craft-stamps/thank-you-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thank-You-Stamps-CS-400x400.jpg', 'Thank You Stamps')" 560class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thank You Stamps</a></li><li><a href="/craft-stamps/sentiment" 561@mouseenter="set('/craft-stamps/sentiment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Sentiment-Stamps-CS-400x400.jpg', 'Sentiment Stamps')" 562class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Sentiment Stamps</a></li><li><a href="/teacher-stamps/clothing-stamps" 563@mouseenter="set('/teacher-stamps/clothing-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Name-Stamps-CS-400x400.jpg', 'Name Stamps')" 564class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Stamps</a></li><li><a href="/face-stamps" 565@mouseenter="set('/face-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Face-Stamps-CS-400x400.jpg', 'Face Stamps')" 566class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Face Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Crafting</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/mini-accent-stamps" 567@mouseenter="set('/craft-stamps/mini-accent-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Mini-Accent-Stamps-CS-400x400.jpg', 'Mini Accent Stamps')" 568class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mini Accent Stamps</a></li><li><a href="/craft-stamps/embellishment/bullet-journal" 569@mouseenter="set('/craft-stamps/embellishment/bullet-journal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Bullet-Journal-Stamps-CS-400x400.jpg', 'Bullet Journal Stamps')" 570class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Bullet Journal Stamps</a></li><li><a href="/craft-stamps/embellishment" 571@mouseenter="set('/craft-stamps/embellishment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Decorative-Stamps-CS-400x400.jpg', 'Decorative Stamps')" 572class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Decorative Stamps</a></li><li><a href="/rubber-flower-stamps" 573@mouseenter="set('/rubber-flower-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Flower-Stamps-CS-400x400.jpg', 'Flower Stamps')" 574class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Flower Stamps</a></li><li><a href="/craft-stamps/emoji-stamps" 575@mouseenter="set('/craft-stamps/emoji-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Emoji-Stamps-CS-400x400.jpg', 'Emoji Stamps')" 576class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Emoji Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/craft-stamps" @mouseenter="reset()" 577class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Craft Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
577<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li><li x-data="{ open: false, hoverTimeout: null }" 578@mouseenter="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = true, 300)" 579@mouseleave="clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => open = false, 150)" 580@keydown.escape="open = false" 581class="static"> <a href="https://www.simplystamps.com/stamp-ink-pads" 582:class="open ? 'underline underline-offset-8 text-brand-primary bg-brand-primary/15' : ''" 583class="block font-medium text-gray-700 p-2 text-sm lg:text-base text-center rounded-t-md transition-all duration-200 hover:underline hover:underline-offset-8 hover:text-brand-primary"> Ink & Accessories </a><div x-show="open" style="display: none;" 584x-transition:enter="transition ease-out duration-150" 585x-transition:enter-start="opacity-0 -translate-y-1" 586x-transition:enter-end="opacity-100 translate-y-0" 587x-transition:leave="transition ease-in duration-100" 588x-transition:leave-start="opacity-100 translate-y-0" 589x-transition:leave-end="opacity-0 -translate-y-1" 590class="absolute top-full left-0 right-0 bg-white shadow-xl rounded-b-md z-50 border border-gray-100"><div class="mx-8 py-6"><div x-data="{ 591defaultHref: '/stamp-ink-pads', 592defaultImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 593defaultLabel: 'Ink & Accessories Collection', 594activeHref: '/stamp-ink-pads', 595activeImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 596activeLabel: 'Ink & Accessories Collection', 597set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 598reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 599}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/stamp-ink-pads" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg" alt="Ink & Accessories Collection" :src="activeImage" :alt="activeLabel" 600loading="lazy" width="224" height="224" 601class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 602x-text="activeLabel">Ink & Accessories Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Ink & Refills</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/craft-stamp-ink" 603@mouseenter="set('/stamp-ink-pads/craft-stamp-ink', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Craft-Ink-Pads-Ink-And-Accessories-400.jpg', 'Craft Stamp Ink Pads')" 604class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Craft Stamp Ink Pads</a></li><li><a href="/stamp-ink-pads/ink-pad-replacements" 605@mouseenter="set('/stamp-ink-pads/ink-pad-replacements', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Pad-Refills-Ink-And-Accessories-400.jpg', 'Ink Pad Refills')" 606class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Pad Refills</a></li><li><a href="/stamp-ink-pads/ink-bottle-refills" 607@mouseenter="set('/stamp-ink-pads/ink-bottle-refills', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Bottle-refills-Ink-And-Accessories-400.jpg', 'Ink Bottle Refills')" 608class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Bottle Refills</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 609@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Embosser-Accessories-Ink-And-Accessories-400.jpg', 'Embosser Acce
609ssories')" 610class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li><li><a href="/mail-accessories" 611@mouseenter="set('/mail-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Mail-Accessories-Ink-And-Accessories-400.jpg', 'Mail Accessories')" 612class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mail Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/stamp-ink-pads" @mouseenter="reset()" 613class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Ink & Accessories <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div></div></div></li></ul> </nav></div><div id="mobile-menu-content" class="hidden"> <nav class="py-4"><ul class="space-y-1 list-none p-0 m-0 border-t border-gray-200"><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Address Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 614class="w-5 h-5 transition-transform duration-200" 615fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 616defaultHref: '/address-stamps', 617defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 618defaultLabel: 'Address Stamps Collection', 619activeHref: '/address-stamps', 620activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg', 621activeLabel: 'Address Stamps Collection', 622set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 623reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 624}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/address-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_AS_Love-Envelope_lifestyle_2_800.jpg" alt="Address Stamps Collection" :src="activeImage" :alt="activeLabel" 625loading="lazy" width="224" height="224" 626class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 627x-text="activeLabel">Address Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-4 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Style</h3><ul class="space-y-2.5"><li><a href="/address-stamps/script-handwritten" 628@mouseenter="set('/address-stamps/script-handwritten', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Script-AS-224x224.jpg', 'Script Address Stamps')" 629class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Script Address Stamps</a></li><li><a href="/address-stamps/custom-line" 630@mouseenter="set('/address-stamps/custom-line', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Multiple-Line-A
630S-224x224.jpg', 'Multiple Line Address Stamps')" 631class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Multiple Line Address Stamps</a></li><li><a href="/address-stamps/round-address-stamps" 632@mouseenter="set('/address-stamps/round-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-AS-224x224.jpg', 'Round Address Stamps')" 633class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Address Stamps</a></li><li><a href="/address-stamps/rectangle-address-stamps" 634@mouseenter="set('/address-stamps/rectangle-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-AS-224x224.jpg', 'Rectangle Address Stamps')" 635class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Address Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 636@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Text-AS-224x224.jpg', 'Line Address Stamps')" 637class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Line Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Design</h3><ul class="space-y-2.5"><li><a href="/address-stamps/floral" 638@mouseenter="set('/address-stamps/floral', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Floral-AS-224x224.jpg', 'Floral Address Stamps')" 639class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Floral Address Stamps</a></li><li><a href="/address-stamps/wedding-couple" 640@mouseenter="set('/address-stamps/wedding-couple', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wedding-AS-224x224.jpg', 'Wedding Address Stamps')" 641class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Address Stamps</a></li><li><a href="/address-stamps/dog-address-stamps" 642@mouseenter="set('/address-stamps/dog-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Dog-AS-224x224.jpg', 'Dog Address Stamps')" 643class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Dog Address Stamps</a></li><li><a href="/address-stamps/animals" 644@mouseenter="set('/address-stamps/animals', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Animal-AS-224x224.jpg', 'Animal Address Stamps')" 645class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Animal Address Stamps</a></li><li><a href="/address-stamps/hearts" 646@mouseenter="set('/address-stamps/hearts', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Whimsical-AS-224x224.jpg', 'Whimsical Address Stamps')" 647class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Whimsical Address Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Seasonal</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/business-address-stamps" 648@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Business-AS-224x224.jpg', 'Business Address Stamps')" 649class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/address-stamps/patriotic-address-stamps" 650@mouseenter="set('/address-stamps/patriotic-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Patriotic-AS-224x224.jpg', 'Patriotic Address Stamps')" 651class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Patriotic Address Stamps</a></li><li><a href="/address-stamps/holiday" 652@mouseenter="set('/address-stamps/holiday', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Holiday-AS-224x224.jpg', 'Holiday Address Stamps')" 653class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Address Stamps</a></li><li><a href="/address-stamps/christmas-address-stamps" 654@mouseenter="set('/address-stamps/christmas-address-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Christmas-AS-224x224.jpg', 'Christmas Address Stamps')" 655class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Christmas Address Stamps</a></li><li><a href="/custom-address-labels" 656@mouseenter="set('/custom-address-labels', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/address-stamps/SS-QCS-Address-Labels-AS-400x400.jpg', 'Custom Return Address Labels')" 657class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Return Address Labels</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Collabs</h3><ul class="space-y-2.5"><li><a href="/salt-and-paper" 658@mouseenter="set('/salt-and-paper', 'https://cdn.simplystamps.com/media/catalog/category/8-simply-stamps-spco-2026-AS-Salt-Wine-1-1-lifestyle-1-2000_1.jpg', 'Salt & Paper Collaboration')" 659style='background-image:url(https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/CMSImages/SS_Nav-Salt-and-Paper-Swish.png); background-repeat:no-repeat; background-position:left center; background-size:100% 100%; padding:6px 10px; margin:0 -10px;' 660class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Salt & Paper Collaboration</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/address-stamps/view-all" @mouseenter="reset()" 661class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Address Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
661<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/address-stamps/view-all" 662class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Logo Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 663class="w-5 h-5 transition-transform duration-200" 664fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 665defaultHref: '/rubber-logo-stamps', 666defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 667defaultLabel: 'Logo Stamps Collection', 668activeHref: '/rubber-logo-stamps', 669activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg', 670activeLabel: 'Logo Stamps Collection', 671set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 672reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 673}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/rubber-logo-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Custom-Logo-Stamps.jpg" alt="Logo Stamps Collection" :src="activeImage" :alt="activeLabel" 674loading="lazy" width="224" height="224" 675class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 676x-text="activeLabel">Logo Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Shape</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/rectangle" 677@mouseenter="set('/rubber-logo-stamps/rectangle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Rectangle-Logo.jpg', 'Rectangle Logo Stamp')" 678class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Rectangle Logo Stamp</a></li><li><a href="/rubber-logo-stamps/round" 679@mouseenter="set('/rubber-logo-stamps/round', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Round-Logo-2.jpg', 'Round Logo Stamp')" 680class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Round Logo Stamp</a></li><li><a href="/rubber-logo-stamps/square"
681@mouseenter="set('/rubber-logo-stamps/square', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Square-Logo.jpg', 'Square Logo Stamp')" 682class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Square Logo Stamp</a></li><li><a href="/rubber-logo-stamps/catalog-stamps" 683@mouseenter="set('/rubber-logo-stamps/catalog-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Catalog-Logo.jpg', 'Catalog Stamps')" 684class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Catalog Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Artwork</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/with-text" 685@mouseenter="set('/rubber-logo-stamps/with-text', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Logo-and-Text.jpg', 'Logo & Text')" 686class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo & Text</a></li><li><a href="/rubber-logo-stamps/upload-art" 687@mouseenter="set('/rubber-logo-stamps/upload-art', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Artwork-Logo.jpg', 'Personalized Art')" 688class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Personalized Art</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/rubber-logo-stamps/pre-inked-logo" 689@mouseenter="set('/rubber-logo-stamps/pre-inked-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Pre-Inked-Logo.jpg', 'Pre-Inked')" 690class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Pre-Inked</a></li><li><a href="/rubber-logo-stamps/self-inking" 691@mouseenter="set('/rubber-logo-stamps/self-inking', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Self-Ink-Logo.jpg', 'Self-Inking')" 692class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Self-Inking</a></li><li><a href="/rubber-logo-stamps/traditional-wooden-handle" 693@mouseenter="set('/rubber-logo-stamps/traditional-wooden-handle', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Wooden-Logo.jpg', 'Wood Handle')" 694class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wood Handle</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/rubber-logo-stamps" @mouseenter="reset()" 695class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Logo Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/rubber-logo-stamps" 696class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Signature Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 697class="w-5 h-5 transition-transform duration-200" 698fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 699defaultHref: '/business-office-stamps/signature-stamps', 700defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 701defaultLabel: 'Signature Stamps Collection', 702activeHref: '/business-office-stamps/signature-stamps', 703activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg', 704activeLabel: 'Signature Stamps Collection', 705set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 706reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 707}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps/signature-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-Category.jpg" alt="Signature Stamps Collection" :src="activeImage" :alt="activeLabel" 708loading="lazy" width="224" height="224" 709class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 710x-text="activeLabel">Signature Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature Stamp Types</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/signature-only" 711@mouseenter="set('/business-office-stamps/signature-stamps/signature-only', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-only.jpg', 'Signature Only')" 712class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature Only</a></li><li><a href="/business-office-stamps/signature-stamps/with-date"
713@mouseenter="set('/business-office-stamps/signature-stamps/with-date', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-date.jpg', 'Signature with Date')" 714class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Date</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Signature & More</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/signature-stamps/with-printed-name" 715@mouseenter="set('/business-office-stamps/signature-stamps/with-printed-name', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-name.jpg', 'Signature with Printed Name')" 716class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Printed Name</a></li><li><a href="/business-office-stamps/signature-stamps/with-logo" 717@mouseenter="set('/business-office-stamps/signature-stamps/with-logo', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Signature-and-logo.jpg', 'Signature with Logo')" 718class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Signature with Logo</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps/signature-stamps" @mouseenter="reset()" 719class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Signature Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/business-office-stamps/signature-stamps" 720class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Custom Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 721class="w-5 h-5 transition-transform duration-200" 722fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 723defaultHref: '/custom-rubber-stamps', 724defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 725defaultLabel: 'Custom Stamps Collection', 726activeHref: '/custom-rubber-stamps', 727activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg', 728activeLabel: 'Custom Stamps Collection', 729set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 730reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 731}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/custom-rubber-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Stamp-Assortment.jpg" alt="Custom Stamps Collection" :src="activeImage" :alt="activeLabel" 732loading="lazy" width="224" height="224" 733class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 734x-text="activeLabel">Custom Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Shop by Stamp Type</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-self-inking-stamps" 735@mouseenter="set('/custom-rubber-stamps/custom-self-inking-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Self-Inking-Stamps-Custom-400x400.jpg', 'Custom Self-Inking Stamps')" 736class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Self-Inking Stamps</a></li><li><a href="/custom-rubber-stamps/custom-pre-inked-stamps" 737@mouseenter="set('/custom-rubber-stamps/custom-pre-inked-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Pre-Inked-Stamps-Custom-400x400.jpg', 'Custom Pre-Inked Stamps')" 738class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Pre-Inked Stamps</a></li><li><a href="/custom-rubber-stamps/custom-wood-handle-stamps" 739@mouseenter="set('/custom-rubber-stamps/custom-wood-handle-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Traditional-Hand-Stamps-Custom-400x400.jpg', 'Custom Wood Handle Stamps')" 740class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Wood Handle Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Specialty</h3><ul class="space-y-2.5"><li><a href="/custom-rubber-stamps/custom-european-hand-stamps" 741@mouseenter="set('/custom-rubber-stamps/custom-european-hand-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-European-Hand-Stamps-Custom-400x400.jpg', 'Custom European Hand Stamps')" 742class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom European Hand Stamps</a></li><li><a href="/custom-rubber-stamps/custom-professional-stamps" 743@mouseenter="set('/custom-rubber-stamps/custom-professional-stamps', 'https://cdn.simplystamps.com/media/catalog/category/SS-QCustom-Professional-Stamps-Custom-400x400.jpg', 'Custom Professional Stamps')" 744class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Professional Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/custom-rubber-stamps" @mouseenter="reset()" 745class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Custom Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
745<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/custom-rubber-stamps" 746class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Office Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 747class="w-5 h-5 transition-transform duration-200" 748fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 749defaultHref: '/business-office-stamps', 750defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 751defaultLabel: 'Office Stamps Collection', 752activeHref: '/business-office-stamps', 753activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg', 754activeLabel: 'Office Stamps Collection', 755set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 756reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 757}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/business-office-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS_OfficeStamp-Cat_Bank_250x250.jpg" alt="Office Stamps Collection" :src="activeImage" :alt="activeLabel" 758loading="lazy" width="224" height="224" 759class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 760x-text="activeLabel">Office Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Banking</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/accounting-tax-stamps" 761@mouseenter="set('/business-office-stamps/accounting-tax-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Accounting-Stamps-OS-400x400.jpg', 'Accounting Stamps')" 762class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Accounting Stamps</a></li><li><a href="/business-office-stamps/business-address-stamps" 763@mouseenter="set('/business-office-stamps/business-address-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Business-Address-Stamps-OS-400x400.jpg', 'Business Address Stamps')" 764class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Business Address Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement" 765@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps/check-endorsement', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Check-Endorsement-Stamps-OS-400x400.jpg', 'Check Endorsement Stamps')" 766class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Check Endorsement Stamps</a></li><li><a href="/business-office-stamps/banking-stamps/deposit-stamps" 767@mouseenter="set('/business-office-stamps/banking-stamps/deposit-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Deposit-Stamps-OS-400x400.jpg', 'Deposit Stamps')" 768class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Deposit Stamps</a></li><li><a href="/business-office-stamps/accounting-tax-stamps/received" 769@mouseenter="set('/business-office-stamps/accounting-tax-stamps/received', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Received-stamps-OS-400x400.jpg', 'Received Stamps')" 770class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Received Stamps</a></li><li><a href="/business-office-stamps/custom-line-stamps" 771@mouseenter="set('/business-office-stamps/custom-line-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Custom-text-Stamps-OS-400x400.jpg', 'Custom Text Stamps')" 772class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Custom Text Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Office Essentials</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/office-stamps" 773@mouseenter="set('/business-office-stamps/office-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Stock-office-stamps-OS-400x400.jpg', 'Stock Office Stamps')" 774class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Stock Office Stamps</a></li><li><a href="/business-office-stamps/daters" 775@mouseenter="set('/business-office-stamps/daters', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Date-Stamps-OS-400x400.jpg', 'Date Stamps')" 776class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Date Stamps</a></li><li><a href="/business-office-stamps/checkbox" 777@mouseenter="set('/business-office-stamps/checkbox', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Checkbox-Stamps-OS-400x400.jpg', 'Checkbox
777Stamps')" 778class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Checkbox Stamps</a></li><li><a href="/business-office-stamps/secure-stamps" 779@mouseenter="set('/business-office-stamps/secure-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Security-Stamps-OS-400x400.jpg', 'Security Stamps')" 780class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Security Stamps</a></li><li><a href="/business-office-stamps/inspection-stamps" 781@mouseenter="set('/business-office-stamps/inspection-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-inspection-Stamps-OS-400x400.jpg', 'Inspection Stamps')" 782class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Inspection Stamps</a></li><li><a href="/business-office-stamps/medical-stamps" 783@mouseenter="set('/business-office-stamps/medical-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Medical-Stamps-OS-400x400.jpg', 'Medical Stamps')" 784class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Medical Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Professional Stamps</h3><ul class="space-y-2.5"><li><a href="/business-office-stamps/professional-stamps/llc-stamps" 785@mouseenter="set('/business-office-stamps/professional-stamps/llc-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-LLC-Stamps-OS-400x400.jpg', 'LLC Stamps')" 786class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">LLC Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/architect" 787@mouseenter="set('/business-office-stamps/professional-stamps/architect', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Architect-Stamps-OS-400x400.jpg', 'Architect Stamps')" 788class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Architect Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/engineer" 789@mouseenter="set('/business-office-stamps/professional-stamps/engineer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Engineer-Stamps-OS-400x400.jpg', 'Engineer Stamps')" 790class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Engineer Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/land-surveyor" 791@mouseenter="set('/business-office-stamps/professional-stamps/land-surveyor', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Land-Surveyor-Stamps-OS-400x400.jpg', 'Land Surveyor Stamps')" 792class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Land Surveyor Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/real-estate-appraiser" 793@mouseenter="set('/business-office-stamps/professional-stamps/real-estate-appraiser', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Real-Estate-Appraiser-Stamps-OS-400x400.jpg', 'Real Estate Appraiser Stamps')" 794class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Real Estate Appraiser Stamps</a></li><li><a href="/business-office-stamps/professional-stamps/geologist-stamps-other" 795@mouseenter="set('/business-office-stamps/professional-stamps/geologist-stamps-other', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/OfficeStamps/SS-QCS-Geologist-Stamps-OS-400x400.jpg', 'Geologist Stamps')" 796class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Geologist Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/business-office-stamps" @mouseenter="reset()" 797class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Office Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
797<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/business-office-stamps" 798class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Embossers </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 799class="w-5 h-5 transition-transform duration-200" 800fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 801defaultHref: '/embossers', 802defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 803defaultLabel: 'Embossers Collection', 804activeHref: '/embossers', 805activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg', 806activeLabel: 'Embossers Collection', 807set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 808reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 809}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/embossers" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-Shop-All-EMB-400x400.jpg" alt="Embossers Collection" :src="activeImage" :alt="activeLabel" 810loading="lazy" width="224" height="224" 811class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 812x-text="activeLabel">Embossers Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Personal & Occasion</h3><ul class="space-y-2.5"><li><a href="/embossers/custom-address" 813@mouseenter="set('/embossers/custom-address', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Address-EMB-400x400.jpg', 'Address Embossers')" 814class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Address Embossers</a></li><li><a href="/embossers/library-book-embossers" 815@mouseenter="set('/embossers/library-book-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Library-Book-EMB-400x400.jpg', 'Library Book Embossers')" 816class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Library Book Embossers</a></li><li><a href="/embossers/wedding-embossers" 817@mouseenter="set('/embossers/wedding-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-wedding-embosser-cat-emboss.jpg', 'Wedding Embossers')" 818class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Embossers</a></li><li><a href="/embossers/luxury-embossers" 819@mouseenter="set('/embossers/luxury-embossers', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-Luxury-Embossers-QCS-Nav-Embosser-400.jpg', 'Luxury Embossers')" 820class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Luxury Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Business & Legal</h3><ul class="space-y-2.5"><li><a href="/embossers/corporate-seal" 821@mouseenter="set('/embossers/corporate-seal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Corporate-Seals-EMB-400x400.jpg', 'Corporate Seals')" 822class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Corporate Seals</a></li><li><a href="/embossers/notary-public" 823@mouseenter="set('/embossers/notary-public', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-Notary-EMB-400x400.jpg', 'Notary Embossers')" 824class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Notary Embossers</a></li><li><a href="/embossers/custom-logo" 825@mouseenter="set('/embossers/custom-logo', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Embossers/SS-QCS-LOGO-EMB-400x400.jpg', 'Logo Embossers')" 826class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Logo Embossers</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 827@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/catalog/category/SS_cat-ink_and_accessories_embosser-accesories-265
827x265.jpg', 'Embosser Accessories')" 828class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/embossers" @mouseenter="reset()" 829class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Embossers <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/embossers" 830class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li > <a href="https://www.simplystamps.com/notary-public" 831class="block px-4 py-3 font-semibold text-lg border-b border-gray-200 transition-colors duration-200 hover:bg-gray-100 hover:text-brand-primary text-gray-700"> Notary </a></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Teacher Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 832class="w-5 h-5 transition-transform duration-200" 833fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 834defaultHref: '/teacher-stamps', 835defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 836defaultLabel: 'Teacher Stamps Collection', 837activeHref: '/teacher-stamps', 838activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg', 839activeLabel: 'Teacher Stamps Collection', 840set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 841reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 842}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/teacher-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-TS-400.jpg" alt="Teacher Stamps Collection" :src="activeImage" :alt="activeLabel" 843loading="lazy" width="224" height="224" 844class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 845x-text="activeLabel">Teacher Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Classroom & Grading</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/grading" 846@mouseenter="set('/teacher-stamps/grading', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Grading-stamps-TS-400x400.jpg', 'Grading Stamps')" 847class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Grading Stamps</a></li><li><a href="/teacher-stamps/feedback" 848@mouseenter="set('/teacher-stamps/feedback', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Feedback-stamps-TS-400x400.jpg', 'Feedback Stamps')" 849class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Feedback Stamps</a></li><li><a href="/teacher-stamps/name-missing"
850@mouseenter="set('/teacher-stamps/name-missing', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Missing-Name-stamps-TS-400x400.jpg', 'Name Missing Stamps')" 851class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Missing Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Fun & Motivation</h3><ul class="space-y-2.5"><li><a href="/teacher-stamps/motivational" 852@mouseenter="set('/teacher-stamps/motivational', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Motivational-stamps-TS-400x400.jpg', 'Motivational Stamps')" 853class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Motivational Stamps</a></li><li><a href="/teacher-stamps/fun-designs" 854@mouseenter="set('/teacher-stamps/fun-designs', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Fun-stamps-TS-400x400.jpg', 'Fun Stamps')" 855class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Fun Stamps</a></li><li><a href="/teacher-stamps/from-the-desk-of" 856@mouseenter="set('/teacher-stamps/from-the-desk-of', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Teacher/SS-QCS-Book-stamps-TS-400x400.jpg', 'Book Stamps')" 857class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Book Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/teacher-stamps" @mouseenter="reset()" 858class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Teacher Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/teacher-stamps" 859class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Craft Stamps </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 860class="w-5 h-5 transition-transform duration-200" 861fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 862defaultHref: '/craft-stamps', 863defaultImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 864defaultLabel: 'Craft Stamps Collection', 865activeHref: '/craft-stamps', 866activeImage: 'https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg', 867activeLabel: 'Craft Stamps Collection', 868set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 869reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 870}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/craft-stamps" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/catalog/category/SS-QCS-View-All-CS-2000_1.jpg" alt="Craft Stamps Collection" :src="activeImage" :alt="activeLabel" 871loading="lazy" width="224" height="224" 872class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 873x-text="activeLabel">Craft Stamps Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-3 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Seasonal & Holiday</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/holiday-crafting"
874@mouseenter="set('/craft-stamps/holiday-crafting', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Holiday-Stamps-CS-400x400.jpg', 'Holiday Stamps')" 875class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Holiday Stamps</a></li><li><a href="/craft-stamps/halloween" 876@mouseenter="set('/craft-stamps/halloween', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Halloween-Stamps-CS-400x400.jpg', 'Halloween Stamps')" 877class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Halloween Stamps</a></li><li><a href="/craft-stamps/thanksgiving" 878@mouseenter="set('/craft-stamps/thanksgiving', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thanksgiving-Stamps-CS-400x400.jpg', 'Thanksgiving Stamps')" 879class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thanksgiving Stamps</a></li><li><a href="/craft-stamps/autumn-fall" 880@mouseenter="set('/craft-stamps/autumn-fall', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Autumn-Stamps-CS-400x400.jpg', 'Autumn Stamps')" 881class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Autumn Stamps</a></li><li><a href="/craft-stamps/summer" 882@mouseenter="set('/craft-stamps/summer', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Summer-Stamps-CS-400x400.jpg', 'Summer Stamps')" 883class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Summer Stamps</a></li><li><a href="/craft-stamps/valentine-s-day-craft-stamp" 884@mouseenter="set('/craft-stamps/valentine-s-day-craft-stamp', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Valentines-Stamps-CS-400x400.jpg', 'Valentine\'s Day Stamps')" 885class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Valentine's Day Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Occasions & Gifts</h3><ul class="space-y-2.5"><li><a href="/wedding-stamps" 886@mouseenter="set('/wedding-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Wedding-Stamps-CS-400x400.jpg', 'Wedding Stamps')" 887class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Wedding Stamps</a></li><li><a href="/craft-stamps/we-have-moved" 888@mouseenter="set('/craft-stamps/we-have-moved', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Just-Moved-Stamps-CS-400x400.jpg', 'Just Moved Stamps')" 889class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Just Moved Stamps</a></li><li><a href="/craft-stamps/thank-you-stamps" 890@mouseenter="set('/craft-stamps/thank-you-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Thank-You-Stamps-CS-400x400.jpg', 'Thank You Stamps')" 891class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Thank You Stamps</a></li><li><a href="/craft-stamps/sentiment" 892@mouseenter="set('/craft-stamps/sentiment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Sentiment-Stamps-CS-400x400.jpg', 'Sentiment Stamps')" 893class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Sentiment Stamps</a></li><li><a href="/teacher-stamps/clothing-stamps" 894@mouseenter="set('/teacher-stamps/clothing-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Name-Stamps-CS-400x400.jpg', 'Name Stamps')" 895class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Name Stamps</a></li><li><a href="/face-stamps" 896@mouseenter="set('/face-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Face-Stamps-CS-400x400.jpg', 'Face Stamps')" 897class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Face Stamps</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Crafting</h3><ul class="space-y-2.5"><li><a href="/craft-stamps/mini-accent-stamps" 898@mouseenter="set('/craft-stamps/mini-accent-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Mini-Accent-Stamps-CS-400x400.jpg', 'Mini Accent Stamps')" 899class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mini Accent Stamps</a></li><li><a href="/craft-stamps/embellishment/bullet-journal" 900@mouseenter="set('/craft-stamps/embellishment/bullet-journal', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Bullet-Journal-Stamps-CS-400x400.jpg', 'Bullet Journal Stamps')" 901class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Bullet Journal Stamps</a></li><li><a href="/craft-stamps/embellishment" 902@mouseenter="set('/craft-stamps/embellishment', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Decorative-Stamps-CS-400x400.jpg', 'Decorative Stamps')" 903class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Decorative Stamps</a></li><li><a href="/rubber-flower-stamps" 904@mouseenter="set('/rubber-flower-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Flower-Stamps-CS-400x400.jpg', 'Flower Stamps')" 905class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Flower Stamps</a></li><li><a href="/craft-stamps/emoji-stamps" 906@mouseenter="set('/craft-stamps/emoji-stamps', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Craft/SS-QCS-Emoji-Stamps-CS-400x400.jpg', 'Emoji Stamps')" 907class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Emoji Stamps</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/craft-stamps" @mouseenter="reset()" 908class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Craft Stamps <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
908<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/craft-stamps" 909class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li><li x-data="{ open: false }"> <button @click="open = !open" class="flex items-center w-full border-b border-gray-200 text-left"> <span class="flex-1 px-4 py-3 font-semibold text-lg transition-colors duration-200 text-gray-700"> Ink & Accessories </span> <span class="px-3 py-3 text-gray-600 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" :class="open ? 'rotate-180' : ''" 910class="w-5 h-5 transition-transform duration-200" 911fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> </svg> </span> </button><div x-show="open" x-transition class="px-4 py-3 border-b border-gray-200" style="display: none;"><div x-data="{ 912defaultHref: '/stamp-ink-pads', 913defaultImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 914defaultLabel: 'Ink & Accessories Collection', 915activeHref: '/stamp-ink-pads', 916activeImage: 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg', 917activeLabel: 'Ink & Accessories Collection', 918set(h, i, l) { this.activeHref = h; this.activeImage = i; this.activeLabel = l; }, 919reset() { this.activeHref = this.defaultHref; this.activeImage = this.defaultImage; this.activeLabel = this.defaultLabel; } 920}" @mouseleave="reset()"><div class="flex gap-8 items-start"><div class="hidden md:block w-56 flex-shrink-0"> <a :href="activeHref" href="/stamp-ink-pads" class="block group"><div class="aspect-square w-full bg-gray-100 rounded-lg overflow-hidden mb-2"> <img src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Category-Craft-400.jpg" alt="Ink & Accessories Collection" :src="activeImage" :alt="activeLabel" 921loading="lazy" width="224" height="224" 922class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"></div><p class="text-sm font-semibold text-gray-900 group-hover:text-brand-primary transition-colors" 923x-text="activeLabel">Ink & Accessories Collection</p> </a></div><div class="flex-1"><div class="grid grid-cols-1 md:grid-cols-2 gap-8"><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Ink & Refills</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/craft-stamp-ink" 924@mouseenter="set('/stamp-ink-pads/craft-stamp-ink', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Craft-Ink-Pads-Ink-And-Accessories-400.jpg', 'Craft Stamp Ink Pads')" 925class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Craft Stamp Ink Pads</a></li><li><a href="/stamp-ink-pads/ink-pad-replacements" 926@mouseenter="set('/stamp-ink-pads/ink-pad-replacements', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Pad-Refills-Ink-And-Accessories-400.jpg', 'Ink Pad Refills')" 927class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Pad Refills</a></li><li><a href="/stamp-ink-pads/ink-bottle-refills" 928@mouseenter="set('/stamp-ink-pads/ink-bottle-refills', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Ink-Bottle-refills-Ink-And-Accessories-400.jpg', 'Ink Bottle Refills')" 929class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Ink Bottle Refills</a></li></ul></div><div><h3 class="text-sm font-bold uppercase tracking-wider text-gray-900 mb-4 pb-2 border-b border-gray-200">Accessories</h3><ul class="space-y-2.5"><li><a href="/stamp-ink-pads/embosser-accessories" 930@mouseenter="set('/stamp-ink-pads/embosser-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Embosser-Accessories-Ink-And-Accessories-400.jpg', 'Embosser Acce
930ssories')" 931class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Embosser Accessories</a></li><li><a href="/mail-accessories" 932@mouseenter="set('/mail-accessories', 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/Ink-Accessories/SS-QCS-Mail-Accessories-Ink-And-Accessories-400.jpg', 'Mail Accessories')" 933class="block text-base text-gray-700 no-underline hover:text-brand-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary focus-visible:rounded-sm transition-colors duration-150">Mail Accessories</a></li></ul></div></div><div class="mt-6 pt-4 border-t border-gray-200"> <a href="/stamp-ink-pads" @mouseenter="reset()" 934class="inline-flex items-center text-sm font-semibold text-brand-primary hover:text-brand-primary-hover no-underline hover:no-underline transition-colors duration-150"> View all Ink & Accessories <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg> </a></div></div></div></div> <a href="https://www.simplystamps.com/stamp-ink-pads" 935class="inline-block mt-3 text-sm font-semibold text-brand-primary hover:underline"> View Page → </a></div></li></ul> </nav></div> </nav><div class="flex items-center justify-center space-x-1 w-1/8"><div x-data="{ accountOpen: false }" @click.away="accountOpen = false" class="relative"> <button @click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); if ($store.auth.ready && $store.auth.isLoggedIn) { accountOpen = !accountOpen } else if ($store.auth.ready) { $store.auth._loginOpenedAt = Date.now(); $store.auth.showLoginModal = true }" 936class="flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" 937stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" 938stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="flex items-center justify-center gap-0.5"> <span class="text-sm font-medium text-gray-700" 939x-text="$store.auth.ready ? ($store.auth.isLoggedIn ? 'Account' : 'Log In') : 'Account'"></span> <svg x-show="$store.auth.ready && $store.auth.isLoggedIn" 940class="w-2.5 h-2.5 text-gray-400 shrink-0 transition-transform duration-200" 941:class="{ 'rotate-180': accountOpen }" 942fill="none" stroke="currentColor" viewBox="0 0 24 24" 943style="display: none;"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/> </svg> </span> </button><div x-show="accountOpen" 944x-transition:enter="transition ease-out duration-150" 945x-transition:enter-start="opacity-0 scale-95" 946x-transition:enter-end="opacity-100 scale-100" 947x-transition:leave="transition ease-in duration-100" 948x-transition:leave-start="opacity-100 scale-100" 949x-transition:leave-end="opacity-0 scale-95" 950class="absolute left-1/2 -translate-x-1/2 top-full mt-3 w-64 bg-white rounded-lg shadow-lg border border-gray-300 py-1.5 z-50" 951style="display: none;"><div class="absolute -top-2 left-1/2 -translate-x-1/2 w-3 h-3 bg-white border-l border-t border-gray-300 rotate-45"></div><div class="px-5 py-2.5 border-b border-gray-100 mb-1"><p class="text-sm text-gray-400 truncate" x-text="$store.auth.customerEmail"></p></div> <a href="https://www.simplystamps.com/customer/account/" 952class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/> </svg> My Account </a> <a href="https://www.simplystamps.com/sales/order/history/" 953class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/> </svg> My Orders </a> <a x-show="$store.auth.companyStoreUrl" 954:href="$store.auth.companyStoreUrl" 955class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors" 956style="display: none;"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/> </svg> Company Store </a><div class="border-t border-gray-100 my-1.5"></div> <button @click="accountOpen = false;
957if (typeof window.zGA4 === 'function') { window.zGA4('logout', { method: 'site' }); } 958$store.auth.refreshMessage = 'Logging out...'; 959$store.auth.showRefreshOverlay = true; 960window.location.href = 'https://www.simplystamps.com/customer/account/logout/';" 961class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-500 hover:text-red-600 hover:bg-gray-50 transition-colors w-full"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 962d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/> </svg> Log Out </button></div></div><button 963@click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); $store.search.open()" 964class="flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <circle cx="11" cy="11" r="6" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 16L21 21" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="text-sm font-medium text-gray-700">Search</span> </button><div x-data="{ helpOpen: false }" @click.away="helpOpen = false" class="relative"> <button @click="if ($el._clickLock) return; $el._clickLock = true; setTimeout(() => $el._clickLock = false, 300); helpOpen = !helpOpen" 965class="flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.08984 8.99959C9.32495 8.33126 9.789 7.7677 10.3998 7.40873C11.0106 7.04975 11.7287 6.91853 12.427 7.0383C13.1253 7.15808 13.7587 7.52112 14.2149 8.06312C14.6712 8.60512 14.9209 9.29112 14.9198 9.99959C14.9198 11.9996 11.9198 12.9996 11.9198 12.9996" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 17H12.01" stroke="#4F4F4F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="flex items-center justify-center gap-0.5"> <span class="text-sm font-medium text-gray-700">Help</span> <svg class="w-2.5 h-2.5 text-gray-400 shrink-0 transition-transform duration-200" 966:class="{ 'rotate-180': helpOpen }" 967fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/> </svg> </span> </button><div x-show="helpOpen" 968x-transition:enter="transition ease-out duration-150" 969x-transition:enter-start="opacity-0 scale-95" 970x-transition:enter-end="opacity-100 scale-100" 971x-transition:leave="transition ease-in duration-100" 972x-transition:leave-start="opacity-100 scale-100" 973x-transition:leave-end="opacity-0 scale-95" 974class="absolute left-1/2 -translate-x-1/2 top-full mt-3 w-64 bg-white rounded-lg shadow-lg border border-gray-300 py-1.5 z-50" 975style="display: none;"><div class="absolute -top-2 left-1/2 -translate-x-1/2 w-3 h-3 bg-white border-l border-t border-gray-300 rotate-45"></div> <a href="https://www.simplystamps.com/contacts" 976class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/> </svg> Track My Order </a> <a href="https://www.simplystamps.com/contacts" 977class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> </svg> Contact Us </a> <a href="https://www.simplystamps.com/faq" 978class="flex items-center gap-3 px-5 py-2.5 text-base text-gray-700 hover:text-brand-primary hover:bg-gray-50 transition-colors"> <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> </svg> FAQ </a> <template x-if="$store.chat.available"><div><div class="border-t border-gray-100 my-1.5"></div> <button id="live-chat-now" class="flex items-center gap-3 px-5 py-2.5 pb-1.5 text-base font-medium text-brand-primary hover:bg-gray-50 transition-colors w-full cursor-pointer"> <svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.25
9785-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> </svg> <span class="text-left"> Chat with a Live Agent <span class="block text-sm text-brand-success font-medium">Available now</span> <span class="block text-sm text-gray-400 font-normal" x-text="$store.chat.hoursLabel"></span> </span> </button></div> </template></div></div><button 979@click="$store.cart.openDrawer()" 980@dblclick.prevent="window.location.href='/checkout/cart/'" 981class="relative flex items-center flex-col space-y-1 p-2 w-[70px] hover:bg-gray-100 rounded-lg transition-colors" 982aria-label="Open cart" 983> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-7 h-7"> <path d="M9 22C9.55228 22 10 21.5523 10 21C10 20.4477 9.55228 20 9 20C8.44772 20 8 20.4477 8 21C8 21.5523 8.44772 22 9 22Z" fill="#4F4F4F" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20 22C20.5523 22 21 21.5523 21 21C21 20.4477 20.5523 20 20 20C19.4477 20 19 20.4477 19 21C19 21.5523 19.4477 22 20 22Z" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M1 1H5L7.68 14.39C7.77144 14.8504 8.02191 15.264 8.38755 15.5583C8.75318 15.8526 9.2107 16.009 9.68 16H19.4C19.8693 16.009 20.3268 15.8526 20.6925 15.5583C21.0581 15.264 21.3086 14.8504 21.4 14.39L23 6H6" stroke="#474747" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span class="text-sm font-medium text-gray-700">Cart</span> <span x-cloak 984x-show="$store.cart.itemCount > 0" 985x-text="$store.cart.itemCount" 986class="absolute -top-2 right-0 bg-brand-danger text-white text-xs rounded-full h-5 flex items-center justify-center" 987:class="$store.cart.itemCount >= 10 ? 'min-w-[1.25rem] px-1' : 'w-5'"></span> </button></div></div><div 988x-show="searchOpen" 989x-transition:enter="transition ease-out duration-200" 990x-transition:enter-start="opacity-0 -translate-y-2" 991x-transition:enter-end="opacity-100 translate-y-0" 992x-transition:leave="transition ease-in duration-150" 993x-transition:leave-start="opacity-100 translate-y-0" 994x-transition:leave-end="opacity-0 -translate-y-2" 995class="pb-4" 996style="display: none;" 997><form class="searchautocomplete UI-SEARCHAUTOCOMPLETE" action="https://www.simplystamps.com/catalogsearch/result/" method="get" 998data-tip="Search entire store here..." 999data-url="//www.simplystamps.com/searchautocomplete/ajax/get/" 1000data-minchars="3" 1001data-delay="500" 1002data-hide-delay="1000"><label for="search">Search</label><div class="nav"><div class="nav-search-in"> <span class="category-fake UI-CATEGORY-TEXT">All</span> <span class="nav-down-arrow"></span> <select name="cat" class="category UI-CATEGORY"><option value="0">All</option><option value="713" > Address Stamps</option><option value="958" > Custom Business & Office Stamps</option><option value="749" > Craft Stamps</option><option value="1387" > Custom Logo Stamps</option><option value="770" > Embossers</option><option value="802" > Stamp Ink & Pads</option><option value="811" > Teacher Stamps</option><option value="793" > Notary</option><option value="815" > Wedding Stamps</option><option value="1432" > Redeemable Personalized Address Stamp with Gift Box</option><option value="2291" > St. Patrick's Day & Irish Theme Stamps</option><option value="2391" > Stamp of the Week</option><option value="809" > Breast Cancer Awareness</option><option value="3035" > Gift Notes</option><option value="3073" > Real Estate Supplies</option><option value="3085" > Custom Rubber Stamps</option><option value="3094" > Custom Return Address Labels</option><option value="3109" > Here Tomorrow</option><option value="3118" > StampToons</option><option value="3327" > Custom Face Stamps</option><option value="3432" > Salt & Paper Collaboration</option> </select></div><div class="nav-input UI-NAV-INPUT"> <input class="input-text UI-SEARCH" type="text" autocomplete="off" name="q" value="" maxlength="128" /></div><div class="searchautocomplete-loader UI-LOADER"><div id="g01"></div><div id="g02"></div><div id="g03"></div><div id="g04"></div><div id="g05"></div><div id="g06"></div><div id="g07"></div><div id="g08"></div></div></div><div class="nav-submit-button"> <button type="submit" title="Go" class="button">Go</button></div> <style> .store-4-search-autocomplete { 1003top: 108px !important; 1004right: unset !important; 1005}@media (min-width: 768px) { 1006.store-4-search-autocomplete { 1007top: 74px !important; 1008right: unset !important; 1009} 1010} </style><div style="display:none" class="searchautocomplete-placeholder UI-PLACEHOLDER "></div></form></div></div></div><div id="zephyr-promo-banner" 1011role="region" 1012aria-label="Promotion" 1013data-key="19208366" 1014data-start="1790568000" 1015data-end="1793474220" 1016data-code="" 1017data-auto-apply="0" 1018data-countdown="0" 1019class="relative text-sm sm:text-base leading-snug " 1020style="background-color:#60B5E4;color:#012E4B;" x-data="{
1021copied: false, 1022applied: false, 1023applying: false, 1024hovering: false, 1025countdown: '', 1026timer: null,init() { 1027if (this.$root.dataset.countdown === '1') { 1028this.tick(); 1029this.timer = setInterval(() => this.tick(), 1000); 1030} 1031},// Mirrors HSC_Zephyr_Model_PromoBanner::formatCountdown() exactly. 1032formatCountdown(s) { 1033const pad = n => String(n).padStart(2, '0'); 1034const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; 1035return (d > 0 ? pad(d) + 'd ' : '') + pad(h) + 'h ' + pad(m) + 'm ' + pad(sec) + 's'; 1036},tick() { 1037const end = parseInt(this.$root.dataset.end, 10) || 0; 1038const left = Math.max(0, end - Math.floor(Date.now() / 1000)); 1039this.countdown = this.formatCountdown(left); 1040if (left <= 0) { 1041// Promo is over: take the band down the same way dismiss does. 1042clearInterval(this.timer); 1043this.$root.hidden = true; 1044window.dispatchEvent(new CustomEvent('zephyr:header-resize')); 1045} 1046},copy() { 1047// A tap fires mouseenter/focus with no matching leave, so clear the hover state here. 1048this.hovering = false; 1049const code = this.$root.dataset.code; 1050const write = navigator.clipboard 1051? navigator.clipboard.writeText(code) 1052: Promise.reject(); 1053write.then(() => { 1054this.copied = true; 1055setTimeout(() => { this.copied = false; }, 2000); 1056}).catch(() => {});if (this.$root.dataset.autoApply === '1') { 1057this.apply(code); 1058} 1059},// Mirrors Alpine.store('promo').apply() in promo-code.js, minus the modal 1060// state. Only when the cart has items; a coupon on an empty quote does 1061// not validate reliably. Rejections stay silent: the shopper still has 1062// the code on the clipboard and the cart's own promo UI reports errors. 1063apply(code) { 1064const cart = Alpine.store('cart'); 1065if (this.applying || this.applied || !cart || !cart.itemCount) return; 1066if (typeof promoCodeConfig === 'undefined' || !promoCodeConfig.applyCouponUrl) return;this.applying = true; 1067fetch(promoCodeConfig.applyCouponUrl, { 1068method: 'POST', 1069credentials: 'same-origin', 1070headers: { 1071'Content-Type': 'application/x-www-form-urlencoded', 1072'X-Requested-With': 'XMLHttpRequest' 1073}, 1074body: 'coupon_code=' + encodeURIComponent(code) + '&form_key=' + encodeURIComponent(promoCodeConfig.formKey || '') 1075}) 1076.then(r => r.json()) 1077.then(data => { 1078if (!data.success) return; 1079this.applied = true; 1080cart.refresh(); 1081if (Alpine.store('promo')) { 1082Alpine.store('promo').appliedCode = code.toUpperCase(); 1083} 1084window.dispatchEvent(new CustomEvent('cart-updated')); 1085}) 1086.catch(() => {}) 1087.finally(() => { this.applying = false; }); 1088},dismiss() { 1089try { sessionStorage.setItem('zephyr_promo_dismissed', this.$root.dataset.key); } catch (e) {} 1090this.$root.hidden = true; 1091window.dispatchEvent(new CustomEvent('zephyr:header-resize')); 1092} 1093}"><div class="container mx-auto px-9 sm:px-10 min-h-[36px] sm:min-h-[40px] py-1.5 sm:py-2 flex flex-col items-center justify-center gap-y-1.5 text-center sm:flex-row sm:flex-wrap sm:gap-x-3 sm:gap-y-1"> <span class="leading-tight sm:contents"> <span class="[&_a]:underline [&_a]:font-semibold [&_a]:text-inherit [&_img]:inline-block [&_img]:h-[1.3em] [&_img]:w-auto [&_img]:align-middle [&_img]:mx-0.5">Free shipping on orders $79+ | <b>FREE GIFT</b> with every logo stamp order</span> <a href="https://www.simplystamps.com/rubber-logo-stamps" class="ml-1 sm:ml-0 sm:order-last font-semibold underline underline-offset-2 whitespace-nowrap text-inherit hover:text-inherit hover:opacity-80 transition-opacity"> Shop logo stamps </a> </span></div><button type="button" 1094@click="dismiss()" 1095aria-label="Dismiss" 1096class="absolute right-1.5 sm:right-2 top-1/2 -translate-y-1/2 p-1.5 rounded transition-colors hover:bg-black/10"> <svg class="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/> </svg> </button> <a x-show="$store.auth.isStaff" x-cloak 1097:href="$store.auth.adminEditBase ? $store.auth.adminEditBase + 'type/config/id/hsc_zephyr/store/sstamps_en/' : '#'" 1098target="_blank" rel="noopener" 1099title="Edit Promo Banner" 1100class="absolute left-2 top-1/2 -translate-y-1/2 flex w-6 h-6 bg-blue-600 hover:bg-blue-700 text-white rounded-full items-center justify-center shadow transition-colors staff-edit-icon"> <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/> </svg> </a></div>
1100<script> // Pre-paint gate: runs before header.phtml measures --header-height. 1101(function () { 1102var el = document.getElementById('zephyr-promo-banner'); 1103if (!el) return; 1104var now = Date.now() / 1000; 1105var start = parseInt(el.dataset.start, 10) || 0; 1106var end = parseInt(el.dataset.end, 10) || 0; 1107var hide = (start && now < start) || (end && now > end); 1108if (!hide) { 1109try { hide = sessionStorage.getItem('zephyr_promo_dismissed') === el.dataset.key; } catch (e) {} 1110} 1111if (hide) el.hidden = true; 1112})(); </script>
1112 </header></div>
1112<script> // Alpine.js global stores 1113document.addEventListener('alpine:init', () => { 1114// Chat Availability Store (client-side check to bypass Fastly cache) 1115Alpine.store('chat', { 1116available: false, 1117hoursLabel: '', 1118schedule: {"1":{"from":"09:00:00","to":"20:00:00"},"2":{"from":"09:00:00","to":"20:00:00"},"3":{"from":"09:00:00","to":"20:00:00"},"4":{"from":"09:00:00","to":"20:00:00"},"5":{"from":"09:00:00","to":"20:00:00"},"6":{"from":"00:00:00","to":"00:00:00"},"7":{"from":"00:00:00","to":"00:00:00"}},init() { 1119this.check(); 1120setInterval(() => this.check(), 60000); 1121},check() { 1122if (!Object.keys(this.schedule).length) return; 1123try { 1124var etDate = new Date(new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })); 1125var dow = etDate.getDay(); 1126var isoDay = dow === 0 ? 7 : dow; 1127var daySchedule = this.schedule[isoDay]; 1128if (!daySchedule) { this.available = false; this.hoursLabel = ''; return; }var h = etDate.getHours(), m = etDate.getMinutes(), s = etDate.getSeconds(); 1129var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; 1130var currentTime = pad(h) + ':' + pad(m) + ':' + pad(s); 1131this.available = currentTime >= daySchedule.from && currentTime < daySchedule.to; 1132this.hoursLabel = this.formatTime(daySchedule.from) + '-' + this.formatTime(daySchedule.to) + ' ET'; 1133} catch (e) { 1134this.available = false; 1135} 1136},// "09:00:00" â "9AM", "13:30:00" â "1:30PM" 1137formatTime(timeStr) { 1138var p = timeStr.split(':'); 1139var h = parseInt(p[0], 10), m = parseInt(p[1], 10); 1140var suffix = h >= 12 ? 'PM' : 'AM'; 1141var h12 = h % 12 || 12; 1142return m > 0 ? h12 + ':' + (m < 10 ? '0' + m : m) + suffix : h12 + suffix; 1143} 1144});// Image Preview Store 1145Alpine.store('imagePreview', { 1146isOpen: false, 1147imageUrl: '', 1148imageAlt: '', 1149linkUrl: '',open(url, alt = '', link = '') { 1150this.imageUrl = url; 1151this.imageAlt = alt; 1152this.linkUrl = link; 1153this.isOpen = true; 1154document.body.style.overflow = 'hidden'; 1155},close() { 1156this.isOpen = false; 1157document.body.style.overflow = ''; 1158} 1159}); 1160});// Alpine.js component for sticky header and mobile menu 1161function stickyHeader() { 1162return { 1163headerVisible: true, 1164searchOpen: false, 1165mobileSearchOpen: false, 1166mobileMenuOpen: false, 1167mobileMenuContent: '', 1168headerHeight: 0, 1169scrollThreshold: 100, 1170lastScrollY: 0, 1171scrollTimeout: null, 1172// Minimum pixels to scroll before detecting direction change 1173scrollDelta: 5,init() { 1174this.lastScrollY = this.getScrollY(); 1175this.updateHeaderHeight(); 1176window.addEventListener('scroll', () => this.handleScroll()); 1177window.addEventListener('resize', () => this.updateHeaderHeight()); 1178// Fired by children (promo banner dismiss) whose size change moves the page top 1179window.addEventListener('zephyr:header-resize', () => this.updateHeaderHeight()); 1180this.$watch('mobileMenuOpen', value => { 1181document.body.style.overflow = value ? 'hidden' : ''; 1182}); 1183},getScrollY() { 1184// Use multiple methods for compatibility (window.scrollY may be overridden by legacy JS) 1185return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 1186},handleScroll() { 1187const currentScrollY = this.getScrollY(); 1188const scrollDiff = currentScrollY - this.lastScrollY;// Clear existing timeout 1189if (this.scrollTimeout) { 1190clearTimeout(this.scrollTimeout); 1191}// Determine scroll direction and header visibility 1192if (currentScrollY <= this.scrollThreshold) { 1193// At top of page - always show header 1194this.setHeaderVisible(true); 1195this.lastScrollY = currentScrollY; 1196} else if (scrollDiff < -this.scrollDelta) { 1197// Scrolling up - show header immediately 1198this.setHeaderVisible(true); 1199this.lastScrollY = currentScrollY; 1200} else if (scrollDiff > this.scrollDelta) { 1201// Scrolling down - hide header immediately 1202this.setHeaderVisible(false); 1203this.lastScrollY = currentScrollY;// Show header again after user stops scrolling for 200ms 1204this.scrollTimeout = setTimeout(() => { 1205this.setHeaderVisible(true); 1206}, 200); 1207} 1208// If scrollDiff is between -scrollDelta and +scrollDelta, ignore (too small) 1209},setHeaderVisible(visible) { 1210this.headerVisible = visible; 1211// Sync with global store for other components (cart drawer, etc.) 1212Alpine.store('ui').headerVisible = visible; 1213},updateHeaderHeight() { 1214this.$nextTick(() => { 1215const header = this.$el.querySelector('header'); 1216if (header) { 1217this.headerHeight = header.offsetHeight; 1218// Update CSS variable for main content spacing 1219document.documentElement.style.setProperty('--header-height', this.headerHeight + 'px'); 1220// Sync with global store for other components 1221Alpine.store('ui').headerHeight = this.headerHeight; 1222} 1223}); 1224}
1224,loadMobileMenu() { 1225// Load mobile menu content from the navigation template 1226const mobileMenuElement = document.getElementById('mobile-menu-content'); 1227if (mobileMenuElement) { 1228this.mobileMenuContent = mobileMenuElement.innerHTML; 1229} 1230} 1231} 1232}</script>
1232
1232<script> (function() { 1233var header = document.querySelector('header.fixed'); 1234if (header) { 1235document.documentElement.style.setProperty('--header-height', header.offsetHeight + 'px'); 1236} 1237})(); </script>
1237 <main class="flex-1" style="padding-top: var(--header-height);"><div class="container mx-auto px-4"><div> <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="https://www.simplystamps.com/" 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 blog" 1245itemprop="itemListElement" 1246itemscope 1247itemtype="https://schema.org/ListItem"><a href="https://www.simplystamps.com/blog" 1248title="Blog" 1249itemprop="item" 1250rel="follow" 1251class="text-gray-600 hover:text-brand-primary transition-colors"> <span itemprop="name">Blog</span> </a><meta itemprop="position" content="2"/> <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 post" 1252itemprop="itemListElement" 1253itemscope 1254itemtype="https://schema.org/ListItem"><span itemprop="name" class="text-gray-900 font-medium"> How Custom Rubber Stamps Are Made </span><meta itemprop="position" content="3"/></li></ol></div> </nav></div></div><div class="fixed left-0 right-0 z-40 pointer-events-none flex justify-center" style="top: var(--header-height, 80px);"><div class="w-full max-w-lg px-4 pt-4"><div class="pointer-events-auto"><div x-data x-show="$store.flash.messages.length > 0" x-cloak class="space-y-3"> <template x-for="msg in $store.flash.messages" :key="msg.id"><div x-show="msg.visible" 1255x-transition:enter="transition ease-out duration-300" 1256x-transition:enter-start="opacity-0 -translate-y-2" 1257x-transition:enter-end="opacity-100 translate-y-0" 1258x-transition:leave="transition ease-in duration-200" 1259x-transition:leave-start="opacity-100" 1260x-transition:leave-end="opacity-0" 1261role="alert" 1262:class="{ 1263'bg-green-50 border-green-400 text-green-800': msg.type === 'success', 1264'bg-red-50 border-red-400 text-red-800': msg.type === 'error', 1265'bg-yellow-50 border-yellow-400 text-yellow-800': msg.type === 'warning' || msg.type === 'notice', 1266'bg-blue-50 border-blue-400 text-blue-800': msg.type !== 'success' && msg.type !== 'error' && msg.type !== 'warning' && msg.type !== 'notice' 1267}" 1268class="border-l-4 rounded-lg p-4 shadow-lg flex items-start gap-3"><div class="shrink-0 mt-0.5"> <template x-if="msg.type === 'success'"> <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg> </template> <template x-if="msg.type === 'error'"> <svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg> </template> <template x-if="msg.type === 'warning' || msg.type === 'notice'"> <svg class="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg> </template></div><div class="flex-1 text-sm" x-html="msg.text"></div>
1268 <button @click="$store.flash.dismiss(msg.id)" class="shrink-0 ml-auto -mr-1 -mt-1 p-1 rounded hover:bg-black/5 transition-colors" aria-label="Dismiss"> <svg class="w-4 h-4 opacity-50" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg> </button></div> </template></div></div></div></div><div class="container mx-auto px-4"><div class="w-full"><div class="py-4"><div class="grid grid-cols-1 gap-8 lg:grid-cols-[20rem_minmax(0,1fr)] lg:gap-10"><div class="min-w-0 lg:order-2"> <article class="mpblog-post"> <header class="mb-6"><h1 class="text-3xl font-bold leading-tight text-gray-900 md:text-4xl"> How Custom Rubber Stamps Are Made</h1><div class="mt-3"><div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500"> <span class="inline-flex items-center gap-1.5"> <svg class="h-4 w-4 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> <span>By <a rel="author" href="https://www.simplystamps.com/blog/sabrina-gonzalez" target="_blank" 1269class="font-medium text-gray-700 hover:text-brand-primary" 1270title="Sabrina Gonzalez">Sabrina Gonzalez</a> </span> </span> <span class="inline-flex items-center gap-1.5"> <svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/> </svg> August 28, 2023 </span></div></div> </header><figure class="mb-8 overflow-hidden rounded-xl"> <img class="h-auto w-full object-cover" 1271src="https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/e/5/cache/5/ece9a24a761836a70934a998c163f8c8/e50507be5c03870a152d5ca40f4c86f1.jpg?width=1000&quality=60" 1272alt="How Custom Rubber Stamps Are Made" 1273fetchpriority="high"/> </figure><div class="prose prose-lg max-w-none"><p>I've discovered that custom rubber stamps are incredibly versatile tools that breathe life into various aspects of both personal and business projects. Whether it's about branding your own products, adding a personal touch to greeting cards, or simply making a unique mark, I've found that custom rubber stamps are not only a creative outlet but also an efficient solution. Ever wondered how these handy tools are made? Let's take a closer look at the fascinating process of creating custom rubber stamps.</p><h2>Choosing Your Stamp</h2><p>Before anything else, I've found it helpful to select the stamp you are looking to customize first.<a href="https://www.simplystamps.com/types-of-rubber-stamps/custom-self-inking-stamps" title=""> Self-inking</a>, <a href="https://www.simplystamps.com/types-of-rubber-stamps/pre-inked" title="">pre-inked</a>, and <a href="https://www.simplystamps.com/types-of-rubber-stamps/wooden-traditional" title="">wooden handle stamps</a> are all available as fully custom stamps. Simply choose the stamp type and size you are looking for, then start customizing. Any time I'm struggling to decide which stamp is best for my needs, I've found this blog post on all the <a href="https://www.simplystamps.com/blog/2016/10/18/how-to-choose-the-right-stamp-for-you/" title="">different stamp types</a> to be helpful.</p><h2>Designing Your Stamp</h2><p>You have complete control over the customization of your custom rubber stamps. This could mean anything from custom text, images, artwork, and more. Once you select a stamp and add it to your cart, the design wizard will make the designing process smooth and simple. It gives the option to choose your own fonts and sizes for a truly unique stamp design.</p><p>Here are my personal tips to guide you through the design process:</p><p><strong>1.</strong> <strong>Keep It Simple:</strong> From years of crafting and seeing countless designs come to life, I can't stress enough the importance of simplicity. A clean design with clear lines ensures your stamp is versatile and always leaves a crisp impression.</p><p><strong>2. Bold Is Better:</strong>
1273 Opt for bold lines and high contrast in your design. This isn't just about aesthetics; it's about functionality. Thicker lines and stark contrasts translate best to the medium of rubber stamps, ensuring your design is always legible, no matter the surface.</p><p><strong>3. Step Away from Fine Details:</strong> I know how tempting it is to incorporate intricate details into your design. However, through trial and error, we've learned that rubber stamps work best with solid, clear shapes. Fine lines and gradients often get lost in translation. If finer details are critical to your design, consider choosing a <a href="https://www.simplystamps.com/custom-rubber-stamps/custom-pre-inked-stamps" title="">Pre-Inked Stamp</a>, with larger sizes being better.</p><p><strong>4.</strong> <strong>Think About the End Use: </strong>Consider where you'll be using your stamp. Different surfaces might require different <a href="https://www.simplystamps.com/blog/2023/10/05/a-guide-to-choosing-the-right-stamp-ink-for-your-application/" title="">types of ink</a>, and your design may need to be adapted accordingly. If you're unsure, we're here to help guide you to the best choices for your stamp's intended use.</p><p><strong>5. Utilize Negative Space:</strong> Negative space isn't just empty space; it's a powerful design tool. It can help your main design elements pop and add an elegant simplicity to your stamp. When used cleverly, negative space can turn a good design into a great one.</p><p><strong>6. Size and Aspect Ratio Are Key</strong>: The size of your stamp and the aspect ratio of your design play crucial roles in the final outcome. When considering the size, also think about the aspect ratio of your design. If your design is too wide without enough height (or vice versa), it might not stamp as clearly as you'd hope. Match the shape of your impression to the size and shape of your stamp. Aim for an aspect ratio that complements the content of your design, whether it's a logo, text, or a combination of both. Is your design especially wide or tall? Consider choosing a <a href="https://www.simplystamps.com/custom-rubber-stamps/custom-wood-handle-stamps" title="">Wood Handle Stamp</a>, which we can adjust to fit your design.</p><h2>Transforming Your Design Into a Stamp</h2><p>When it comes to creating your custom stamp, the Simply Stamps team uses two primary methods:</p><h3>Laser Engraving</h3><p>Laser engraving machines use high-powered laser beams to remove the rubber material selectively, following your design's outline. The lasers are controlled by our software, which ensures that they engrave exactly around your unique design. The laser beam vaporizes or melts away the rubber, creating the raised portions of your stamp. This method is used to make our <a href="https://www.simplystamps.com/types-of-rubber-stamps/custom-self-inking-stamps" title="">custom self-inking</a> and <a href="https://www.simplystamps.com/types-of-rubber-stamps/wooden-traditional" title="">wood handle stamps</a>.</p><h3>Flash Machine</h3><p>Flash machines work by flashing light through a vellum with the stamp image that is being produced. The light melts away the surface of the flash die plate and leaves the surface of the desired image porous in order to allow ink to flow through. This method is used to make our <a href="https://www.simplystamps.com/types-of-rubber-stamps/pre-inked" title="">custom pre-inked stamps</a>.</p><h2>Ensuring Quality</h2><p>Before we ship your new custom rubber stamp, our team double checks that your stamp produces clear and consistent impressions. Any adjustments needed to achieve the desired result are made at this stage, so you can enjoy a high-quality product!</p></div><div class="mt-8 border-t border-gray-200 pt-6"><div class="space-y-2"><div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-gray-500"> <span class="text-gray-400">Posted in:</span> <a href="https://www.simplystamps.com/blog/category/custom-stamps" class="font-medium text-gray-700 hover:text-brand-primary"> Custom Stamps </a></div></div></div> </article></div> <aside class="min-w-0 space-y-8 lg:order-1"> <section class="rounded-xl border border-gray-200 bg-white p-5"><h3 class="mb-4 text-lg font-semibold text-gray-900">Recent Posts</h3><ul class="space-y-4"><li class="flex gap-3"> <a href="https://www.simplystamps.com/blog/salt-and-paper-collaboration-story" title="Meet Salt & Paper: The Story Behind the Collection" class="shrink-0"> <img class="h-16 w-16 rounded-lg object-cover" src="https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/9/b/cache/5/60x60/ac7fca02c77817f9150e17969b74630a/9b9ad0cdee666996ced3a4cae3baa740.jpg?width=160&quality=60" alt="Meet Salt & Paper: The Story Behind the Collection" loading="lazy"/> </a><div class="min-w-0"> <a href="https://www.simplystamps.com/blog/salt-and-paper-collaboration-story" class="line-clamp-2 text-sm font-medium text-gray-800 hover:text-brand-primary"> Meet Salt & Paper: The Story Behind the Collection </a><div class="mt-1 text-xs text-gray-500">September 1, 2026</div></div></li><li class="flex gap-3">
1273 <a href="https://www.simplystamps.com/blog/all-about-craft-ink-pads" title="All About Craft Ink Pads" class="shrink-0"> <img class="h-16 w-16 rounded-lg object-cover" src="https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/8/0/cache/5/60x60/ac7fca02c77817f9150e17969b74630a/807a342fbdd2020ee146d97480bf06b1.jpg?width=160&quality=60" alt="All About Craft Ink Pads" loading="lazy"/> </a><div class="min-w-0"> <a href="https://www.simplystamps.com/blog/all-about-craft-ink-pads" class="line-clamp-2 text-sm font-medium text-gray-800 hover:text-brand-primary"> All About Craft Ink Pads </a><div class="mt-1 text-xs text-gray-500">August 13, 2026</div></div></li><li class="flex gap-3"> <a href="https://www.simplystamps.com/blog/custom-stamp-built-for-space" title="From the Workshop to the Moon: How Simply Stamps Built a Custom Stamp for the Artemis II Mission" class="shrink-0"> <img class="h-16 w-16 rounded-lg object-cover" src="https://cdn.simplystamps.com/media/magpleasure/mpblog/list_thumbnail_file/d/b/cache/5/60x60/ac7fca02c77817f9150e17969b74630a/db1c3f514999f3bf3d5ddff9dbbaa098.jpg?width=160&quality=60" alt="From the Workshop to the Moon: How Simply Stamps Built a Custom Stamp for the Artemis II Mission" loading="lazy"/> </a><div class="min-w-0"> <a href="https://www.simplystamps.com/blog/custom-stamp-built-for-space" class="line-clamp-2 text-sm font-medium text-gray-800 hover:text-brand-primary"> From the Workshop to the Moon: How Simply Stamps Built a Custom Stamp for the Artemis II Mission </a><div class="mt-1 text-xs text-gray-500">January 15, 2026</div></div></li></ul> </section> <section class="rounded-xl border border-gray-200 bg-white p-5"><h3 class="mb-4 text-lg font-semibold text-gray-900">Categories</h3><ul class="divide-y divide-gray-100"><li> <a href="https://www.simplystamps.com/blog/category/address-stamps" 1274class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Address Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/custom-stamps" 1275class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Custom Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/logo-stamps" 1276class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Logo Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/signature-stamps" 1277class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Signature Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/business-stamps" 1278class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Business Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/embossers" 1279class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Embossers </a></li><li> <a href="https://www.simplystamps.com/blog/category/wedding" 1280class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Wedding Stamps </a></li><li>
1280 <a href="https://www.simplystamps.com/blog/category/craft-stamps" 1281class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Craft Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/teacher-stamps" 1282class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Teacher Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/library-stamps" 1283class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Library Stamps </a></li><li> <a href="https://www.simplystamps.com/blog/category/stamp-ink" 1284class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Stamp Ink </a></li><li> <a href="https://www.simplystamps.com/blog/category/video-resources" 1285class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg>
1285 Video Resources </a></li><li> <a href="https://www.simplystamps.com/blog/category/author-bios" 1286class="flex items-center gap-2 py-2 text-sm transition-colors hover:text-brand-primary text-gray-700"> <svg class="h-4 w-4 shrink-0 text-gray-300" 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"/> </svg> Author Bios </a></li></ul> </section> <section class="rounded-xl border border-gray-200 bg-white p-5"><h3 class="mb-4 text-lg font-semibold text-gray-900">Tags</h3><div class="flex max-h-80 flex-wrap items-center gap-2 overflow-y-auto pr-1"> <a href="https://www.simplystamps.com/blog/tag/address-stamps" 1287title="5 Topics" 1288class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> address stamps </a> <a href="https://www.simplystamps.com/blog/tag/featured" 1289title="1 Topics" 1290class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> featured </a> <a href="https://www.simplystamps.com/blog/tag/essential" 1291title="4 Topics" 1292class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> essential </a> <a href="https://www.simplystamps.com/blog/tag/business" 1293title="7 Topics" 1294class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> business </a> <a href="https://www.simplystamps.com/blog/tag/fall" 1295title="1 Topics" 1296class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> fall </a> <a href="https://www.simplystamps.com/blog/tag/profits" 1297title="1 Topics" 1298class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> profits </a> <a href="https://www.simplystamps.com/blog/tag/sales" 1299title="1 Topics" 1300class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> sales </a> <a href="https://www.simplystamps.com/blog/tag/social-media" 1301title="3 Topics" 1302class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> social media </a> <a href="https://www.simplystamps.com/blog/tag/summer" 1303title="2 Topics" 1304class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> summer </a> <a href="https://www.simplystamps.com/blog/tag/custom-stamps" 1305title="5 Topics" 1306class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> custom stamps </a> <a href="https://www.simplystamps.com/blog/tag/gift-ideas" 1307title="2 Topics" 1308class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> gift ideas </a> <a href="https://www.simplystamps.com/blog/tag/small-business" 1309title="5 Topics" 1310class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> small business </a> <a href="https://www.simplystamps.com/blog/tag/stamps" 1311title="21 Topics" 1312class="text-2xl rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> stamps </a> <a href="https://www.simplystamps.com/blog/tag/back-to-school" 1313title="5 Topics" 1314class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> back to school </a> <a href="https://www.simplystamps.com/blog/tag/education" 1315title="3 Topics" 1316class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> education </a> <a href="https://www.simplystamps.com/blog/tag/school" 1317title="4 Topics" 1318class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> school </a> <a href="https://www.simplystamps.com/blog/tag/teachers" 1319title="4 Topics" 1320class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> teachers </a> <a href="https://www.simplystamps.com/blog/tag/logos" 1321title="2 Topics" 1322class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> logos </a> <a href="https://www.simplystamps.com/blog/tag/rubber-stamps" 1323title="11 Topics" 1324class="text-base rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> rubber stamps </a> <a href="https://www.simplystamps.com/blog/tag/diy" 1325title="12 Topics" 1326class="text-base rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> diy </a> <a href="https://www.simplystamps.com/blog/tag/home-improvement" 1327title="1 Topics" 1328class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> home improvement </a> <a href="https://www.simplystamps.com/blog/tag/simply-stamps" 1329title="8 Topics" 1330class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Simply Stamps </a> <a href="https://www.simplystamps.com/blog/tag/stamping" 1331title="6 Topics" 1332class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> stamping </a> <a href="https://www.simplystamps.com/blog/tag/christmas" 1333title="1 Topics" 1334class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> christmas </a> <a href="https://www.simplystamps.com/blog/tag/gift-stamps" 1335title="1 Topics" 1336class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> gift stamps </a> <a href="https://www.simplystamps.com/blog/tag/hanukkah" 1337title="1 Topics" 1338class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Hanukkah </a> <a href="https://www.simplystamps.com/blog/tag/holiday-rush" 1339title="1 Topics" 1340class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> holiday rush </a> <a href="https://www.simplystamps.com/blog/tag/holiday-season" 1341title="4 Topics" 1342class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> holiday season </a> <a href="https://www.simplystamps.com/blog/tag/packaging-stamps" 1343title="2 Topics" 1344class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> packaging stamps </a> <a href="https://www.simplystamps.com/blog/tag/thanksgiving" 1345title="1 Topics" 1346class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> thanksgiving </a> <a href="https://www.simplystamps.com/blog/tag/2016" 1347title="1 Topics" 1348class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> 2016 </a> <a href="https://www.simplystamps.com/blog/tag/custom-logo-stamps" 1349title="1 Topics" 1350class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> custom logo stamps </a> <a href="https://www.simplystamps.com/blog/tag/logo-designs" 1351title="1 Topics" 1352class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> logo designs </a> <a href="https://www.simplystamps.com/blog/tag/logo-stamps" 1353title="3 Topics" 1354class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> logo stamps </a> <a href="https://www.simplystamps.com/blog/tag/trends" 1355title="1 Topics" 1356class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> trends </a> <a href="https://www.simplystamps.com/blog/tag/letter-writing" 1357title="2 Topics" 1358class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> letter writing </a> <a href="https://www.simplystamps.com/blog/tag/tutorials" 1359title="1 Topics" 1360class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> tutorials </a> <a href="https://www.simplystamps.com/blog/tag/crafts" 1361title="9 Topics" 1362class="text-base rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> crafts </a> <a href="https://www.simplystamps.com/blog/tag/greeting-cards" 1363title="2 Topics" 1364class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> greeting cards </a> <a href="https://www.simplystamps.com/blog/tag/kids" 1365title="2 Topics" 1366class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> kids </a> <a href="https://www.simplystamps.com/blog/tag/valentines-day" 1367title="1 Topics" 1368class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> valentine's day </a> <a href="https://www.simplystamps.com/blog/tag/cards" 1369title="2 Topics" 1370class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> cards </a> <a href="https://www.simplystamps.com/blog/tag/craft-stamps" 1371title="4 Topics" 1372class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> craft stamps </a> <a href="https://www.simplystamps.com/blog/tag/tutorial" 1373title="6 Topics" 1374class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> tutorial </a> <a href="https://www.simplystamps.com/blog/tag/wedding" 1375title="3 Topics" 1376class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> wedding </a> <a href="https://www.simplystamps.com/blog/tag/2014" 1377title="1 Topics" 1378class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> 2014 </a> <a href="https://www.simplystamps.com/blog/tag/business-owners" 1379title="1 Topics" 1380class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> business owners </a> <a href="https://www.simplystamps.com/blog/tag/clean-living" 1381title="1 Topics" 1382class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> clean living </a> <a href="https://www.simplystamps.com/blog/tag/food" 1383title="1 Topics" 1384class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> food </a> <a href="https://www.simplystamps.com/blog/tag/healthy" 1385title="1 Topics" 1386class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> healthy </a> <a href="https://www.simplystamps.com/blog/tag/healthy-eating" 1387title="1 Topics" 1388class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> healthy eating </a> <a href="https://www.simplystamps.com/blog/tag/new-year" 1389title="1 Topics" 1390class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> new year </a> <a href="https://www.simplystamps.com/blog/tag/new-years-resolutions" 1391title="1 Topics" 1392class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> new years resolutions </a> <a href="https://www.simplystamps.com/blog/tag/restaurants" 1393title="2 Topics" 1394class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> restaurants </a> <a href="https://www.simplystamps.com/blog/tag/video" 1395title="6 Topics" 1396class="text-sm rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> video </a> <a href="https://www.simplystamps.com/blog/tag/contest" 1397title="2 Topics" 1398class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> contest </a> <a href="https://www.simplystamps.com/blog/tag/giveaway" 1399title="2 Topics" 1400class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> giveaway </a> <a href="https://www.simplystamps.com/blog/tag/gifts" 1401title="3 Topics" 1402class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white">
1402 gifts </a> <a href="https://www.simplystamps.com/blog/tag/family" 1403title="1 Topics" 1404class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> family </a> <a href="https://www.simplystamps.com/blog/tag/ftw" 1405title="1 Topics" 1406class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> ftw </a> <a href="https://www.simplystamps.com/blog/tag/monogram" 1407title="1 Topics" 1408class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> monogram </a> <a href="https://www.simplystamps.com/blog/tag/secret" 1409title="1 Topics" 1410class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> secret </a> <a href="https://www.simplystamps.com/blog/tag/success" 1411title="1 Topics" 1412class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> success </a> <a href="https://www.simplystamps.com/blog/tag/crafting" 1413title="1 Topics" 1414class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> crafting </a> <a href="https://www.simplystamps.com/blog/tag/creative" 1415title="2 Topics" 1416class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> creative </a> <a href="https://www.simplystamps.com/blog/tag/exposed" 1417title="1 Topics" 1418class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> exposed </a> <a href="https://www.simplystamps.com/blog/tag/fads" 1419title="1 Topics" 1420class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> fads </a> <a href="https://www.simplystamps.com/blog/tag/frugal" 1421title="1 Topics" 1422class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> frugal </a> <a href="https://www.simplystamps.com/blog/tag/money-saving" 1423title="1 Topics" 1424class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> money saving </a> <a href="https://www.simplystamps.com/blog/tag/pinterest" 1425title="1 Topics" 1426class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> pinterest </a> <a href="https://www.simplystamps.com/blog/tag/products" 1427title="1 Topics" 1428class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> products </a> <a href="https://www.simplystamps.com/blog/tag/save-money" 1429title="1 Topics" 1430class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> save money </a> <a href="https://www.simplystamps.com/blog/tag/truth" 1431title="1 Topics" 1432class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> truth </a> <a href="https://www.simplystamps.com/blog/tag/custom" 1433title="2 Topics" 1434class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> custom </a> <a href="https://www.simplystamps.com/blog/tag/home" 1435title="1 Topics" 1436class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> home </a> <a href="https://www.simplystamps.com/blog/tag/house-hunting" 1437title="1 Topics" 1438class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> house hunting </a> <a href="https://www.simplystamps.com/blog/tag/houses" 1439title="1 Topics" 1440class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> houses </a> <a href="https://www.simplystamps.com/blog/tag/realtor" 1441title="1 Topics" 1442class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> realtor </a> <a href="https://www.simplystamps.com/blog/tag/realty" 1443title="1 Topics" 1444class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> realty </a> <a href="https://www.simplystamps.com/blog/tag/creativity" 1445title="1 Topics" 1446class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> creativity </a> <a href="https://www.simplystamps.com/blog/tag/diversity" 1447title="1 Topics" 1448class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> diversity </a> <a href="https://www.simplystamps.com/blog/tag/equal-opportunity" 1449title="1 Topics" 1450class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> equal opportunity </a> <a href="https://www.simplystamps.com/blog/tag/jacksonville" 1451title="1 Topics" 1452class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> jacksonville </a> <a href="https://www.simplystamps.com/blog/tag/gifts-ideas" 1453title="1 Topics" 1454class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> gifts ideas </a> <a href="https://www.simplystamps.com/blog/tag/mothers-day" 1455title="1 Topics" 1456class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> mothers day </a> <a href="https://www.simplystamps.com/blog/tag/business-tips" 1457title="1 Topics" 1458class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> business tips </a> <a href="https://www.simplystamps.com/blog/tag/budget" 1459title="1 Topics" 1460class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> budget </a> <a href="https://www.simplystamps.com/blog/tag/dad" 1461title="1 Topics" 1462class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white">
1462 Dad </a> <a href="https://www.simplystamps.com/blog/tag/fathers-day" 1463title="1 Topics" 1464class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Father's Day </a> <a href="https://www.simplystamps.com/blog/tag/free" 1465title="1 Topics" 1466class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> free </a> <a href="https://www.simplystamps.com/blog/tag/recipes" 1467title="1 Topics" 1468class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> recipes </a> <a href="https://www.simplystamps.com/blog/tag/save-the-date" 1469title="1 Topics" 1470class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> save the date </a> <a href="https://www.simplystamps.com/blog/tag/wedding-decor" 1471title="1 Topics" 1472class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> wedding decor </a> <a href="https://www.simplystamps.com/blog/tag/wedding-favors" 1473title="1 Topics" 1474class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> wedding favors </a> <a href="https://www.simplystamps.com/blog/tag/wedding-invitations" 1475title="2 Topics" 1476class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> wedding invitations </a> <a href="https://www.simplystamps.com/blog/tag/weddings" 1477title="1 Topics" 1478class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> weddings </a> <a href="https://www.simplystamps.com/blog/tag/summer-vacation" 1479title="1 Topics" 1480class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> summer vacation </a> <a href="https://www.simplystamps.com/blog/tag/teacher-stamps" 1481title="2 Topics" 1482class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> teacher stamps </a> <a href="https://www.simplystamps.com/blog/tag/history" 1483title="1 Topics" 1484class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> history </a> <a href="https://www.simplystamps.com/blog/tag/rubber" 1485title="1 Topics" 1486class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> rubber </a> <a href="https://www.simplystamps.com/blog/tag/chicago" 1487title="1 Topics" 1488class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> chicago </a> <a href="https://www.simplystamps.com/blog/tag/packaging" 1489title="1 Topics" 1490class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> packaging </a> <a href="https://www.simplystamps.com/blog/tag/rak" 1491title="1 Topics" 1492class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> RAK </a> <a href="https://www.simplystamps.com/blog/tag/random-acts-of-kindness" 1493title="1 Topics" 1494class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> random acts of kindness </a> <a href="https://www.simplystamps.com/blog/tag/how-to-guide" 1495title="1 Topics" 1496class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> how to guide </a> <a href="https://www.simplystamps.com/blog/tag/monograms" 1497title="1 Topics" 1498class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> monograms </a> <a href="https://www.simplystamps.com/blog/tag/custom-logo-stamp" 1499title="1 Topics" 1500class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> custom logo stamp </a> <a href="https://www.simplystamps.com/blog/tag/etiquette" 1501title="2 Topics" 1502class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> etiquette </a> <a href="https://www.simplystamps.com/blog/tag/craft" 1503title="1 Topics" 1504class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> craft </a> <a href="https://www.simplystamps.com/blog/tag/inks" 1505title="1 Topics" 1506class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> inks </a> <a href="https://www.simplystamps.com/blog/tag/holiday-cards" 1507title="1 Topics" 1508class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> holiday cards </a> <a href="https://www.simplystamps.com/blog/tag/tips" 1509title="4 Topics" 1510class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> tips </a> <a href="https://www.simplystamps.com/blog/tag/gift-giving" 1511title="1 Topics" 1512class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> gift giving </a> <a href="https://www.simplystamps.com/blog/tag/gift-wrapping" 1513title="1 Topics" 1514class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> gift wrapping </a> <a href="https://www.simplystamps.com/blog/tag/ornaments" 1515title="1 Topics" 1516class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> ornaments </a> <a href="https://www.simplystamps.com/blog/tag/presents" 1517title="1 Topics" 1518class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> presents </a> <a href="https://www.simplystamps.com/blog/tag/simplysendmoreletters" 1519title="1 Topics" 1520class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> #simplysendmoreletters </a> <a href="https://www.simplystamps.com/blog/tag/charitable-cause" 1521title="1 Topics" 1522class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> charitable cause </a> <a href="https://www.simplystamps.com/blog/tag/simply-send-more-letters" 1523title="1 Topics" 1524class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Simply Send More Letters </a> <a href="https://www.simplystamps.com/blog/tag/snail-mail" 1525title="1 Topics" 1526class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> snail mail </a> <a href="https://www.simplystamps.com/blog/tag/write-more-letters" 1527title="1 Topics" 1528class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> write more letters </a> <a href="https://www.simplystamps.com/blog/tag/save-the-dates" 1529title="1 Topics" 1530class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> save the dates </a> <a href="https://www.simplystamps.com/blog/tag/wedding-season" 1531title="1 Topics" 1532class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> wedding season </a> <a href="https://www.simplystamps.com/blog/tag/artwork" 1533title="1 Topics" 1534class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> artwork </a> <a href="https://www.simplystamps.com/blog/tag/a-story-before-bed-app" 1535title="1 Topics" 1536class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> A Story Before Bed app </a> <a href="https://www.simplystamps.com/blog/tag/booster-app" 1537title="1 Topics" 1538class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Booster app </a> <a href="https://www.simplystamps.com/blog/tag/childrens-books" 1539title="1 Topics" 1540class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> children's books </a> <a href="https://www.simplystamps.com/blog/tag/library-organization" 1541title="1 Topics" 1542class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> library organization </a> <a href="https://www.simplystamps.com/blog/tag/library-organizing-tips" 1543title="1 Topics" 1544class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> library organizing tips </a> <a href="https://www.simplystamps.com/blog/tag/library-plates" 1545title="1 Topics" 1546class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> library plates </a> <a href="https://www.simplystamps.com/blog/tag/library-stamps" 1547title="1 Topics" 1548class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> library stamps </a> <a href="https://www.simplystamps.com/blog/tag/meegenius-app" 1549title="1 Topics" 1550class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> MeeGenius app </a> <a href="https://www.simplystamps.com/blog/tag/national-library-week" 1551title="1 Topics" 1552class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> National Library Week </a> <a href="https://www.simplystamps.com/blog/tag/personalized-kids-stamps" 1553title="1 Topics" 1554class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> personalized kids stamps </a> <a href="https://www.simplystamps.com/blog/tag/personalized-library-stamps" 1555title="1 Topics" 1556class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> personalized library stamps </a> <a href="https://www.simplystamps.com/blog/tag/school-library-month" 1557title="1 Topics" 1558class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> School Library Month </a> <a href="https://www.simplystamps.com/blog/tag/tales2go-app" 1559title="1 Topics" 1560class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Tales2Go app </a> <a href="https://www.simplystamps.com/blog/tag/tickatok-storyspark-app" 1561title="1 Topics" 1562class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Tickatok StorySpark app </a> <a href="https://www.simplystamps.com/blog/tag/top-reading-apps-for-children" 1563title="1 Topics" 1564class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> top reading apps for children </a> <a href="https://www.simplystamps.com/blog/tag/touchybooks-app" 1565title="1 Topics" 1566class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> TouchyBooks app </a> <a href="https://www.simplystamps.com/blog/tag/classroom-kindness-campaign" 1567title="1 Topics" 1568class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> classroom kindness campaign </a> <a href="https://www.simplystamps.com/blog/tag/classroom-stamps" 1569title="1 Topics" 1570class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> classroom stamps </a> <a href="https://www.simplystamps.com/blog/tag/classroom-tips" 1571title="1 Topics" 1572class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> classroom tips </a> <a href="https://www.simplystamps.com/blog/tag/effective-lesson-planning" 1573title="1 Topics" 1574class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> effective lesson planning </a> <a href="https://www.simplystamps.com/blog/tag/grading-stamps" 1575title="1 Topics" 1576class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> grading stamps </a> <a href="https://www.simplystamps.com/blog/tag/grading-tools" 1577title="1 Topics" 1578class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> grading tools </a> <a href="https://www.simplystamps.com/blog/tag/kindness-campaign" 1579title="1 Topics" 1580class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> kindness campaign </a> <a href="https://www.simplystamps.com/blog/tag/kindness-in-the-classroom" 1581title="1 Topics" 1582class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> kindness in the classroom </a> <a href="https://www.simplystamps.com/blog/tag/lesson-planning" 1583title="1 Topics" 1584class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> lesson planning </a> <a href="https://www.simplystamps.com/blog/tag/motivational-stamps" 1585title="1 Topics" 1586class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> motivational stamps </a> <a href="https://www.simplystamps.com/blog/tag/teacher-tips" 1587title="1 Topics" 1588class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> teacher tips </a> <a href="https://www.simplystamps.com/blog/tag/how-to" 1589title="3 Topics" 1590class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> how-to </a> <a href="https://www.simplystamps.com/blog/tag/art-of-writing-letters" 1591title="1 Topics" 1592class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> Art of Writing Letters </a> <a href="https://www.simplystamps.com/blog/tag/personalized" 1593title="2 Topics" 1594class="text-xs rounded-full px-3 py-1 font-medium
1594leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> personalized </a> <a href="https://www.simplystamps.com/blog/tag/school-supplies" 1595title="1 Topics" 1596class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> school supplies </a> <a href="https://www.simplystamps.com/blog/tag/decor" 1597title="1 Topics" 1598class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> decor </a> <a href="https://www.simplystamps.com/blog/tag/invitations" 1599title="1 Topics" 1600class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> invitations </a> <a href="https://www.simplystamps.com/blog/tag/embosser" 1601title="1 Topics" 1602class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> embosser </a> <a href="https://www.simplystamps.com/blog/tag/embossing" 1603title="1 Topics" 1604class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> embossing </a> <a href="https://www.simplystamps.com/blog/tag/ideas" 1605title="1 Topics" 1606class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> ideas </a> <a href="https://www.simplystamps.com/blog/tag/mail" 1607title="2 Topics" 1608class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> mail </a> <a href="https://www.simplystamps.com/blog/tag/mail-art" 1609title="1 Topics" 1610class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> mail art </a> <a href="https://www.simplystamps.com/blog/tag/faq" 1611title="1 Topics" 1612class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> FAQ </a> <a href="https://www.simplystamps.com/blog/tag/gift-list" 1613title="1 Topics" 1614class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> gift list </a> <a href="https://www.simplystamps.com/blog/tag/holidays" 1615title="1 Topics" 1616class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> holidays </a> <a href="https://www.simplystamps.com/blog/tag/thank-you-card" 1617title="1 Topics" 1618class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> thank you card </a> <a href="https://www.simplystamps.com/blog/tag/thank-you-note" 1619title="1 Topics" 1620class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> thank you note </a> <a href="https://www.simplystamps.com/blog/tag/writing" 1621title="1 Topics" 1622class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> writing </a> <a href="https://www.simplystamps.com/blog/tag/embossers" 1623title="1 Topics" 1624class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> embossers </a> <a href="https://www.simplystamps.com/blog/tag/faqs" 1625title="1 Topics" 1626class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> FAQs </a> <a href="https://www.simplystamps.com/blog/tag/signature-stamps" 1627title="3 Topics" 1628class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> signature stamps </a> <a href="https://www.simplystamps.com/blog/tag/handmade" 1629title="1 Topics" 1630class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> handmade </a> <a href="https://www.simplystamps.com/blog/tag/party-favors" 1631title="1 Topics" 1632class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> party favors </a> <a href="https://www.simplystamps.com/blog/tag/tips-impression" 1633title="1 Topics" 1634class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> tips impression </a> <a href="https://www.simplystamps.com/blog/tag/is-your-signature-stamp-legal" 1635title="1 Topics" 1636class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> is your signature stamp legal </a> <a href="https://www.simplystamps.com/blog/tag/catalog-paper" 1637title="1 Topics" 1638class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> catalog paper </a> <a href="https://www.simplystamps.com/blog/tag/non-porous" 1639title="1 Topics" 1640class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> non-porous </a> <a href="https://www.simplystamps.com/blog/tag/paper" 1641title="1 Topics" 1642class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> paper </a> <a href="https://www.simplystamps.com/blog/tag/porous" 1643title="1 Topics" 1644class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> porous </a> <a href="https://www.simplystamps.com/blog/tag/stationery" 1645title="1 Topics" 1646class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> stationery </a> <a href="https://www.simplystamps.com/blog/tag/blog-series" 1647title="2 Topics" 1648class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> blog series </a> <a href="https://www.simplystamps.com/blog/tag/educators" 1649title="2 Topics" 1650class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> educators </a> <a href="https://www.simplystamps.com/blog/tag/classroom" 1651title="1 Topics" 1652class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> classroom </a> <a href="https://www.simplystamps.com/blog/tag/educator" 1653title="1 Topics" 1654class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> educator </a> <a href="https://www.simplystamps.com/blog/tag/teacher" 1655title="1 Topics" 1656class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> teacher </a> <a href="https://www.simplystamps.com/blog/tag/teacher-appreciation" 1657title="1 Topics" 1658class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> teacher appreciation </a> <a href="https://www.simplystamps.com/blog/tag/pre-inked-stamps" 1659title="1 Topics" 1660class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white">
1660 pre-inked stamps </a> <a href="https://www.simplystamps.com/blog/tag/custom-rubber-stamps" 1661title="1 Topics" 1662class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> custom rubber stamps </a> <a href="https://www.simplystamps.com/blog/tag/types-of-stamps" 1663title="1 Topics" 1664class="text-xs rounded-full px-3 py-1 font-medium leading-none transition-colors bg-gray-100 text-gray-600 hover:bg-brand-primary hover:text-white"> types of stamps </a></div> </section> </aside></div></div></div></div> </main> <footer id="site-footer" class="w-full"><div class="relative"><div class="widget widget-static-block"><style> /* ââ SimplyStamps Shipping ETA Timer ââ */ 1665/* v4.0 â Unified copy, color does the talking */ 1666.ss-ship-eta { 1667display: none; 1668flex-direction: column; 1669gap: 5px; 1670padding: 12px 16px; 1671border-radius: 8px 8px 0px 0px; 1672background: #eef7ff; 1673width: 100%; 1674box-sizing: border-box; 1675margin-top: 0; 1676margin-bottom: 0px; 1677border: 1px solid #d1e9ff; 1678transition: background 0.35s ease, border-color 0.35s ease; 1679font-family: inherit; 1680} 1681.ss-ship-eta__topline { 1682font-size: 14px; 1683font-weight: 600; 1684color: #374151; 1685line-height: 1.4; 1686margin: 0; 1687} 1688.ss-ship-eta__hero { 1689font-size: 14px; 1690color: #374151; 1691line-height: 1.5; 1692} 1693.ss-ship-eta__hero-date { 1694font-weight: 700; 1695} 1696.ss-ship-eta__hero-tier { 1697font-size: 12px; 1698color: #9ca3af; 1699margin-left: 6px; 1700} 1701.ss-ship-eta__toggle { 1702background: none; 1703border: none; 1704padding: 0; 1705margin-top: 3px; 1706font-size: 12px; 1707font-weight: 600; 1708color: #1d4ed8; 1709cursor: pointer; 1710display: inline-flex; 1711align-items: center; 1712gap: 3px; 1713font-family: inherit; 1714} 1715.ss-ship-eta__toggle:hover { text-decoration: underline; } 1716.ss-ship-eta__toggle-arrow { 1717font-size: 10px; 1718transition: transform 0.2s ease; 1719display: inline-block; 1720} 1721.ss-ship-eta__toggle-arrow.ss-open { 1722transform: rotate(180deg); 1723} 1724.ss-ship-eta__more { 1725display: none; 1726flex-direction: column; 1727gap: 1px; 1728margin-top: 4px; 1729font-size: 14px; 1730color: #374151; 1731line-height: 1.5; 1732} 1733.ss-ship-eta__more.ss-visible { 1734display: flex; 1735} 1736.ss-ship-eta__more-row { 1737display: flex; 1738align-items: baseline; 1739gap: 6px; 1740} 1741.ss-ship-eta__more-label { 1742font-size: 12px; 1743font-weight: 500; 1744color: #9ca3af; 1745min-width: 52px; 1746} 1747.ss-ship-eta__more-date { 1748font-weight: 500; 1749} 1750.ss-eta-cd { 1751font-weight: 800; 1752color: #1d4ed8; 1753} 1754/* ââ Warn state ââ */ 1755.ss-ship-eta.ss-warn { 1756background: #fffbeb; 1757border-color: #fde68a; 1758} 1759.ss-ship-eta.ss-warn .ss-ship-eta__topline, 1760.ss-ship-eta.ss-warn .ss-ship-eta__hero { color: #78350f; } 1761.ss-ship-eta.ss-warn .ss-ship-eta__more { color: #78350f; } 1762.ss-ship-eta.ss-warn .ss-eta-cd { color: #d97706; } 1763.ss-ship-eta.ss-warn .ss-ship-eta__toggle { color: #b45309; } 1764/* ââ Urgent state ââ */ 1765.ss-ship-eta.ss-urgent { 1766background: #fff1f2; 1767border-color: #fecdd3; 1768} 1769.ss-ship-eta.ss-urgent .ss-ship-eta__topline, 1770.ss-ship-eta.ss-urgent .ss-ship-eta__hero { color: #7f1d1d; } 1771.ss-ship-eta.ss-urgent .ss-ship-eta__more { color: #7f1d1d; } 1772.ss-ship-eta.ss-urgent .ss-eta-cd { 1773color: #dc2626; 1774animation: ss-pulse 1.1s ease-in-out 5; 1775} 1776.ss-ship-eta.ss-urgent .ss-ship-eta__toggle { color: #dc2626; } 1777@keyframes ss-pulse { 17780%, 100% { opacity: 1; } 177950% { opacity: 0.5; } 1780} 1781/* ââ After-cutoff state ââ */ 1782.ss-ship-eta.ss-after { 1783background: #f9fafb; 1784border-color: #e5e7eb; 1785} 1786.ss-ship-eta.ss-after .ss-ship-eta__topline, 1787.ss-ship-eta.ss-after .ss-ship-eta__hero { color: #6b7280; } 1788.ss-ship-eta.ss-after .ss-ship-eta__more { color: #6b7280; } 1789.ss-ship-eta.ss-after .ss-ship-eta__toggle { color: #6b7280; } </style>
1789<script> (() => { 1790var TIMEZONE = "America/New_York"; 1791var CUTOFF_HOUR_ET = 17; 1792var CUTOFF_MINUTE_ET = 0; 1793var HOLIDAYS_MMDD = new Set([ 1794"01/01","01/19","02/16","05/25","07/03","09/07", 1795"11/26","11/27","12/24","12/25","12/31" 1796]); 1797var OFFSETS = { rush: 2, express: 3, standard: 5, economy: 6 }; 1798var STD_LABEL = "Preferred"; 1799var SHIPPING_CONTAINER_SELECTOR = '.ships-and-made-in-usa-container'; 1800var FALLBACK_PARENT_SELECTOR = '.vpt-product'; 1801var PFX = "ss"; 1802function createTimerEl() { 1803var div = document.createElement("div"); 1804div.className = PFX + "-ship-eta"; 1805div.setAttribute("aria-label", "Delivery estimates"); 1806div.innerHTML = 1807'<div class="' + PFX + '-ship-eta__topline"></div>' + 1808'<div class="' + PFX + '-ship-eta__hero"></div>' + 1809'<button class="' + PFX + '-ship-eta__toggle" type="button" aria-expanded="false">' + 1810'All shipping options <span class="' + PFX + '-ship-eta__toggle-arrow">\u25BC</span>' + 1811'</button>' + 1812'<div class="' + PFX + '-ship-eta__more">' + 1813'<div class="' + PFX + '-ship-eta__more-row" data-tier="express"></div>' + 1814'<div class="' + PFX + '-ship-eta__more-row" data-tier="standard"></div>' + 1815'<div class="' + PFX + '-ship-eta__more-row" data-tier="economy"></div>' + 1816'</div>'; 1817var toggleEl = div.querySelector('.' + PFX + '-ship-eta__toggle'); 1818var arrowEl = div.querySelector('.' + PFX + '-ship-eta__toggle-arrow'); 1819var moreEl = div.querySelector('.' + PFX + '-ship-eta__more'); 1820var expanded = false; 1821toggleEl.addEventListener("click", function() { 1822expanded = !expanded; 1823moreEl.classList.toggle(PFX + "-visible", expanded); 1824arrowEl.classList.toggle(PFX + "-open", expanded); 1825toggleEl.setAttribute("aria-expanded", expanded); 1826toggleEl.firstChild.textContent = expanded ? "Hide options " : "All shipping options "; 1827}); 1828return div; 1829} 1830function getEasternParts(date) { 1831date = date || new Date(); 1832var fmt = new Intl.DateTimeFormat("en-US", { 1833timeZone: TIMEZONE, 1834year: "numeric", month: "2-digit", day: "2-digit", 1835hour: "2-digit", minute: "2-digit", second: "2-digit", 1836hour12: false 1837}); 1838var out = {}; 1839var parts = fmt.formatToParts(date); 1840for (var i = 0; i < parts.length; i++) out[parts[i].type] = parts[i].value; 1841return out; 1842} 1843function getNowET() { 1844var p = getEasternParts(); 1845return new Date(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second); 1846} 1847function mmdd(d) { 1848return String(d.getMonth()+1).padStart(2,"0") + "/" + String(d.getDate()).padStart(2,"0"); 1849} 1850function isWeekend(d) { var w = d.getDay(); return w === 0 || w === 6; } 1851function isHoliday(d) { return HOLIDAYS_MMDD.has(mmdd(d)); } 1852function isBusinessDay(d) { return !isWeekend(d) && !isHoliday(d); } 1853function startOfDay(d) { var x = new Date(d); x.setHours(0,0,0,0); return x; } 1854function nextDay(d) { var x = new Date(d); x.setDate(x.getDate()+1); return x; } 1855function nextBusinessDay(from) { 1856var x = nextDay(from); 1857while (!isBusinessDay(x)) x = nextDay(x); 1858return startOfDay(x); 1859} 1860function addBusinessDays(from, n) { 1861var x = startOfDay(from); 1862var added = 0; 1863while (added < n) { x = nextDay(x); if (isBusinessDay(x)) added++; } 1864return x; 1865} 1866function formatDate(d) { 1867return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "
1867numeric" }); 1868} 1869function countdownStr(diffMs) { 1870var totalSecs = Math.max(0, Math.floor(diffMs / 1000)); 1871var totalMins = Math.floor(totalSecs / 60); 1872var hrs = Math.floor(totalSecs / 3600); 1873var mins = Math.floor((totalSecs % 3600) / 60); 1874var secs = totalSecs % 60; 1875if (totalMins < 5) { 1876return hrs + "h " + String(mins).padStart(2,"0") + "m " + String(secs).padStart(2,"0") + "s"; 1877} 1878return hrs + " hr" + (hrs !== 1 ? "s" : "") + " " + String(mins).padStart(2,"0") + " min"; 1879} 1880var instances = []; 1881function ensureInstances() { 1882var containers = document.querySelectorAll(SHIPPING_CONTAINER_SELECTOR); 1883var placed = false; 1884for (var i = 0; i < containers.length; i++) { 1885var container = containers[i]; 1886var prev = container.previousElementSibling; 1887if (prev && prev.classList.contains(PFX + "-ship-eta")) { 1888placed = true; 1889continue; 1890} 1891var timer = createTimerEl(); 1892container.insertAdjacentElement("beforebegin", timer); 1893instances.push(timer); 1894placed = true; 1895} 1896if (!placed) { 1897var fallback = document.querySelector(FALLBACK_PARENT_SELECTOR); 1898if (fallback) { 1899var first = fallback.firstElementChild; 1900if (!first || !first.classList.contains(PFX + "-ship-eta")) { 1901var timer = createTimerEl(); 1902fallback.insertAdjacentElement("afterbegin", timer); 1903instances.push(timer); 1904placed = true; 1905} 1906} 1907} 1908return placed; 1909} 1910function renderAll() { 1911var nowET = getNowET(); 1912var cutoff = new Date(nowET); 1913cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0); 1914var diffMs = cutoff - nowET; 1915var totalMins = Math.floor(diffMs / 60000); 1916var isPastCutoff = nowET >= cutoff; 1917var isOffDay = !isBusinessDay(nowET); 1918var urgencyClass = null; 1919var toplineHTML = ""; 1920var base; 1921if (isPastCutoff || isOffDay) { 1922urgencyClass = PFX + "-after"; 1923base = nextBusinessDay(nowET); 1924toplineHTML = 'Get as early as:'; 1925} else { 1926var cdStr = countdownStr(diffMs); 1927base = startOfDay(nowET); 1928toplineHTML = 'Order in <span class="' + PFX + '-eta-cd">' + cdStr + '</span> to get as early as:'; 1929if (totalMins <= 120) { 1930urgencyClass = PFX + "-urgent"; 1931} else if (totalMins <= 240) { 1932urgencyClass = PFX + "-warn"; 1933} 1934} 1935var rushDate = addBusinessDays(base, OFFSETS.rush); 1936var expressDate = addBusinessDays(base, OFFSETS.express); 1937var stdDate = addBusinessDays(base, OFFSETS.standard); 1938var ecoDate = addBusinessDays(base, OFFSETS.economy); 1939var heroHTML = '<span class="' + PFX + '-ship-eta__hero-date">' + formatDate(rushDate) + '</span><span class="' + PFX + '-ship-eta__hero-tier">via Rush</span>'; 1940var expressHTML = '<span class="' + PFX + '-ship-eta__more-label">Express</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(expressDate) + '</span>'; 1941var stdHTML = '<span class="' + PFX + '-ship-eta__more-label">' + STD_LABEL + '</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(stdDate) + '</span>'; 1942var ecoHTML = '<span class="' + PFX + '-ship-eta__more-label">Economy</span><span class="' + PFX + '-ship-eta__more-date">' + formatDate(ecoDate) + '</span>'; 1943for (var i = 0; i < instances.length; i++) { 1944var el = instances[i]; 1945el.classList.remove(PFX + "-warn", PFX + "-urgent", PFX + "-after"); 1946if (urgencyClass) el.classList.add(urgencyClass); 1947el.querySelector('.' + PFX + '-ship-eta__topline').innerHTML = toplineHTML; 1948el.querySelector('.' + PFX + '-ship-eta__hero').innerHTML = heroHTML; 1949el.querySelector('[data-tier="express"]').innerHTML = expressHTML; 1950el.querySelector('[data-tier="standard"]').innerHTML = stdHTML; 1951el.querySelector('[data-tier="economy"]').innerHTML = ecoHTML; 1952el.style.display = "flex"; 1953} 1954} 1955function tick() { 1956var placed = ensureInstances(); 1957if (placed && instances.length > 0) renderAll(); 1958var nowET = getNowET(); 1959var cutoff = new Date(nowET); 1960cutoff.setHours(CUTOFF_HOUR_ET, CUTOFF_MINUTE_ET, 0, 0); 1961var minsLeft = Math.floor((cutoff - nowET) / 60000); 1962var interval = !placed ? 250 1963: (minsLeft >= 0 && minsLeft < 5) ? 1000 1964: 60000; 1965setTimeout(tick, interval); 1966} 1967tick(); 1968if (window.MutationObserver) { 1969var mo = new MutationObserver(function() { ensureInstances(); }); 1970mo.observe(document.body, { childList: true, subtree: true }); 1971} 1972})(); 1973</script>
1973</div> <style> .full-size-container.footer-marketing { 1974background-color: #60b5e5; 1975width: 100vw; 1976margin-top: 30px; 1977} </style> <style> .CustomerServiceNotice img { 1978width: 100%; 1979margin-bottom: 2%; 1980} 1981.CustomerServiceNotice .modal-footer { 1982display: none; 1983} </style></div> <section id="zephyr-signup-band" 1984class="relative w-full py-10 sm:py-12 bg-brand-primary text-white" 1985x-data="zephyrSignupBand()" 1986aria-labelledby="zephyr-signup-headline"><div class="relative max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"><div x-ref="stage" 1987class="relative flex flex-col justify-center"><div x-ref="panelOffers" 1988class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1989:class="view === 'offers' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'" 1990:aria-hidden="view !== 'offers'"><p id="zephyr-signup-headline" class="text-3xl sm:text-4xl font-bold text-center"> Save On Your Order Today & On Future Deals!</p><div class="mt-8 sm:mt-10 grid gap-4 sm:gap-8 sm:grid-cols-2 max-w-5xl mx-auto items-stretch"><div class="group relative bg-white rounded-2xl shadow-xl p-6 pt-10 sm:p-8 sm:pt-12 flex flex-col text-center transition-transform duration-200 hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:translate-y-0 ring-4 ring-brand-success lg:scale-[1.03]"> <span class="absolute -top-4 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-brand-success px-5 py-1.5 text-sm font-bold uppercase tracking-widest text-white shadow-md"> Most Popular! </span><div class="flex flex-col-reverse items-center gap-4 text-center sm:block sm:gap-0"><p class="text-sm font-semibold uppercase tracking-wider text-gray-500"> Subscribe To Email + Text and get</p><p class="text-4xl sm:text-6xl font-extrabold leading-none sm:mt-4 text-brand-success"> 15% Off</p></div><ul class="mt-5 sm:mt-6 space-y-2 w-fit max-w-full mx-auto text-left text-sm text-gray-700"><li class="flex items-start gap-2"> <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Everything in the email list</span></li><li class="flex items-start gap-2"> <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Early access to promotions</span></li><li class="flex items-start gap-2"> <svg class="h-5 w-5 shrink-0 text-brand-success" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0l-3.5-3.5a1 1 0 011.4-1.4l2.8 2.79 6.8-6.79a1 1 0 011.4 0z" /></svg> <span>Text-only flash sales</span></li></ul><div class="mt-auto pt-5 sm:pt-6"> <button type="button" 1991@click="choose('email_sms')" 1992class="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">
1992 <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" 1993@click="choose('email')" 1994class="w-full min-h-[56px] border-2 border-brand-primary text-brand-primary hover:bg-brand-primary hover:text-white text-base font-semibold px-4 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-primary"> Subscribe To Email List </button></div></div></div><p class="mt-10 text-center text-xs leading-relaxed opacity-90 max-w-2xl mx-auto"> One use per email address/cell-phone number. By signing up, you agree to our <a href="/terms-and-conditions" class="text-inherit underline hover:no-underline">Terms and Conditions</a> & <a href="/privacy-policy" class="text-inherit underline hover:no-underline">Privacy Policy</a></p></div><div x-ref="panelForm" 1995x-cloak 1996class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 1997:class="view === 'email' || view === 'email_sms' 1998? 'relative opacity-100 visible' 1999: 'absolute inset-x-0 top-0 opacity-0 invisible'" 2000: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" 2001x-transition 2002role="alert" 2003aria-live="polite" 2004class="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" 2005id="zephyr-signup-email" 2006x-model="email" 2007required 2008autocomplete="email" 2009placeholder="[email protected]" 2010class="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" 2011aria-hidden="true">US +1</span> <input type="tel" 2012id="zephyr-signup-phone" 2013x-model="phone" 2014autocomplete="tel-national" 2015inputmode="tel" 2016placeholder="(555) 555-5555" 2017aria-describedby="zephyr-signup-phone-hint" 2018class="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" 2019id="zephyr-signup-consent" 2020x-model="consent" 2021class="mt-0.5 h-5 w-5 shrink-0 rounded border-gray-300 text-brand-primary focus:ring-brand-primary"> <label for="zephyr-signup-consent" class="text-xs leading-relaxed text-gray-600"> By checking this box, you agree to receive recurring automated promotional and personalized marketing text messages (e.g., cart reminders) from Simply Stamps at the mobile number provided at signup. Consent is not a condition of purchase. Reply HELP for help and STOP to cancel. Message frequency may vary. Message and data rates may apply. <a href="/terms-and-conditions" class="underline hover:no-underline">Terms and Conditions</a> & <a href="/privacy-policy" class="underline hover:no-underline">Privacy Policy</a> </label></div><div class="flex flex-col sm:flex-row sm:items-center gap-4 pt-1"> <button type="submit" 2022:disabled="loading" 2023class="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" 2024@click="back()"
2025class="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" 2026x-cloak 2027class="transition-opacity duration-200 ease-out motion-reduce:transition-none" 2028:class="view === 'done' ? 'relative opacity-100 visible' : 'absolute inset-x-0 top-0 opacity-0 invisible'" 2029: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>
2029<script> /* 2030* Hide the band synchronously, before paint, for anyone who has already signed 2031* up. This runs inline rather than through Alpine on purpose: the footer is 2032* served from the Fastly cache, so the cookie is the only signal available, and 2033* Alpine boots after first paint â gating on x-show alone would render the band 2034* and then yank it, shifting the footer. 2035*/ 2036(function () { 2037if (document.cookie.indexOf('zephyr_signup=') !== -1) { 2038var band = document.getElementById('zephyr-signup-band'); 2039if (band) { 2040band.parentNode.removeChild(band); 2041} 2042} 2043})();function zephyrSignupBand() { 2044return { 2045view: 'offers', 2046email: '', 2047phone: '', 2048consent: false, 2049loading: false, 2050errorMessage: '', 2051successMessage: '', 2052postUrl: 'https://www.simplystamps.com/zephyr/signup/post/', 2053hideDays: 365,/* Per-offer copy, so the two form states share one set of markup. */ 2054copy: { 2055email: { 2056heading: 'Save 10% - Subscribe to Email Only', 2057subheading: 'Please enter your email below to receive your discount code!', 2058submit: 'Get My Discount Code' 2059}, 2060email_sms: { 2061heading: 'Save 15% - Subscribe to Email + Text', 2062subheading: 'Please enter your email address and your phone number to receive the 20% off discount code.', 2063submit: 'Get my 15% Off Now!' 2064} 2065},/* 2066* While `measuring` is on, the form panel renders in its tallest 2067* configuration so it can be sized. See measureTallest(). 2068*/ 2069formCopy: function () { 2070return this.copy[this.view] 2071|| (this.measuring ? this.copy.email_sms : null); 2072},headingText: function () { 2073var copy = this.formCopy(); 2074return copy ? copy.heading : ''; 2075},subheadingText: function () { 2076var copy = this.formCopy(); 2077return copy ? copy.subheading : ''; 2078},submitText: function () { 2079var copy = this.formCopy(); 2080return copy ? copy.submit : ''; 2081},measuring: false,init: function () { 2082this.measureTallest();// Panel heights change with the breakpoint, so re-pin on resize. 2083var self = this; 2084var timer = null; 2085window.addEventListener('resize', function () { 2086clearTimeout(timer); 2087timer = setTimeout(function () { self.measureTallest(); }, 150); 2088}); 2089},/* 2090* Pin the stage to the tallest panel so switching states moves nothing 2091* on the page at all. 2092* 2093* The form panel has to be measured in its email+SMS configuration â 2094* its phone and consent blocks are x-show'd off in the email-only view, 2095* and its heading is bound to the current view, so it otherwise reports 2096* a short height. `measuring` forces that configuration on, and because 2097* an inactive panel is `absolute` + `invisible` (never display:none) it 2098* is measurable without ever being seen. 2099* 2100* Skipped while a form is on screen: `measuring` would briefly reveal 2101* the phone and consent fields in the email-only view. The pin just 2102* stays as-is until the visitor returns to the offers. 2103*/ 2104measureTallest: function () { 2105var stage = this.$refs.stage; 2106if (!stage || this.view === 'email' || this.view === 'email_sms') { 2107return; 2108}var self = this; 2109this.measuring = true;this.$nextTick(function () { 2110var tallest = 0; 2111['panelOffers', 'panelForm', 'panelDone'].forEach(function (ref) { 2112var panel = self.$refs[ref]; 2113if (panel) { 2114tallest = Math.max(tallest, panel.offsetHeight); 2115} 2116});self.measuring = false; 2117if (tallest) { 2118stage.style.minHeight = tallest + 'px'; 2119} 2120}); 2121},choose: function (which) { 2122this.errorMessage = ''; 2123this.view = which;// After the DOM updates, not before: the field sits in a panel that 2124// is still `invisible` at this point, and focus() is a silent no-op 2125// on a hidden element. 2126var self = this; 2127this.$nextTick(function () { 2128// Both offers open the same panel, so email is always first. 2129self.focusLater('zephyr-signup-email'); 2130}); 2131},back: function () { 2132this.errorMessage = ''; 2133this.view = 'offers'; 2134},/* Move focus to the first field without yanking the page around. */ 2135focusLater: function (focusId) { 2136if (!focusId) { 2137return; 2138} 2139var el = document.getElementById(focusId); 2140if (!el) { 2141return; 2142} 2143try { 2144el.focus({ preventScroll: true }); 2145} catch (e) { 2146el.focus(); 2147} 2148},submit: function () { 2149var self = this; 2150if (this.loading) { 2151return; 2152}// Client-side checks mirror the server's; the server is still the 2153// authority, these just save a round trip. 2154if (!this.email || this.email.indexOf('@') === -1) { 2155this.errorMessage = 'Enter a valid email address.'; 2156return; 2157} 2158if (this.view === 'email_sms') { 2159if (this.phone.replace(/[^0-9]/g, '').length < 10) { 2160this.errorMessage = 'Enter a valid 10-digit US mobile number.'; 2161return; 2162} 2163if (!this.consent) { 2164this.errorMessage = 'Check the consent box to receive text messages.'; 2165return; 2166} 2167}this.loading = true; 2168this.errorMessage = '';var body = new FormData(); 2169body.append('email', this.email); 2170body.append('signup_type', this.view); 2171if (this.view === 'email_sms') { 2172body.append('phone', this.phone); 2173body.append('sms_consent', this.consent ? '1' : '0'); 2174}var send = function (token) { 2175if (token) { 2176body.append('recaptcha_response', token); 2177}fetch(self.postUrl, { 2178method: 'POST', 2179body: body, 2180credentials: 'same-origin' 2181}).then(function (response) { 2182return response.json(); 2183}).then(function (data) { 2184if (data && data.success) { 2185self.successMessage = data.message || ''; 2186self.view = 'done'; 2187self.setHideCookie(data.hide_days); 2188if (typeof zGA4 === 'function') { 2189zGA4('generate_lead', {}); 2190} 2191} else { 2192self.errorMessage = (data && data.error) 2193? data.error 2194: 'We could not complete your signup. Please try again in a moment.'; 2195} 2196}).catch(function () { 2197self.errorMessage = 'We could not complete your signup. Please try again in a moment.'; 2198}).then(function () { 2199self.loading = false;
2200}); 2201};if (typeof window.getZephyrRecaptchaToken === 'function') { 2202window.getZephyrRecaptchaToken('signup').then(send, function () { send(null); }); 2203} else { 2204send(null); 2205} 2206},/* Keeps the band hidden on later visits. 0 days means always show it. */ 2207setHideCookie: function (days) { 2208var lifetime = typeof days === 'number' ? days : this.hideDays; 2209if (!lifetime || lifetime < 1) { 2210return; 2211} 2212var expires = new Date(); 2213expires.setTime(expires.getTime() + (lifetime * 24 * 60 * 60 * 1000)); 2214document.cookie = 'zephyr_signup=1; expires=' + expires.toUTCString() 2215+ '; path=/; SameSite=Lax' 2216+ (location.protocol === 'https:' ? '; Secure' : ''); 2217} 2218}; 2219} 2220</script>
2220<div class="relative"><div class="widget widget-static-block"></div> <!-On PDP FAQ Accordion Compatibility Shim--><div class="widget widget-static-block"><style> /* --------------------------------------------------------------------------- 2221FAQ blocks â standalone styles for the legacy Bootstrap panel markup. 2222Pairs with faq-collapse.js. No Bootstrap, no dependencies. 2223Scoped to .faq_block / .faq_table so nothing else on the page inherits 2224these rules. Retune the custom properties below to match the theme. 2225--------------------------------------------------------------------------- */ 2226.faq_block { 2227--faq-border: #dcdcdc; 2228--faq-radius: 6px; 2229--faq-gap: 10px; 2230--faq-surface: #fff; 2231--faq-heading: #f7f7f7; 2232--faq-heading-hover: #efefef; 2233--faq-focus: #2563eb; 2234--faq-mark: #ffe9a8; 2235--faq-muted: #616161; 2236line-height: 1.55; 2237} 2238/* --- neutralize the layout table ---------------------------------------- */ 2239.faq_block .faq_table, 2240.faq_block .faq_table > tbody, 2241.faq_block .faq_table > tbody > tr, 2242.faq_block .faq_table > tbody > tr > td { 2243display: block; 2244width: 100%; 2245max-width: 100%; 2246margin: 0; 2247padding: 0; 2248border: 0; 2249background: none; 2250} 2251.faq_block .faq_table > tbody > tr > td + td { 2252margin-top: var(--faq-gap); 2253} 2254/* the intro row holds the section heading, not a panel */ 2255.faq_block .faq_table h3 { 2256margin: 0 0 calc(var(--faq-gap) * 1.5); 2257font-size: 1.5em; 2258line-height: 1.25; 2259} 2260/* --- panel ---------------------------------------------------------------- */ 2261.faq_block .panel { 2262margin: 0 0 var(--faq-gap); 2263background: var(--faq-surface); 2264border: 1px solid var(--faq-border); 2265border-radius: var(--faq-radius); 2266overflow: hidden; 2267} 2268.faq_block .panel:last-child { 2269margin-bottom: 0; 2270} 2271.faq_block .panel-heading { 2272padding: 0; 2273background: var(--faq-heading); 2274border-bottom: 1px solid transparent; 2275transition: background-color .15s ease; 2276} 2277.faq_block .panel-heading:hover { 2278background: var(--faq-heading-hover); 2279} 2280.faq_block .panel-title { 2281margin: 0; 2282font-size: 1em; 2283font-weight: 600; 2284line-height: inherit; 2285} 2286/* the trigger fills the whole heading so the entire bar is clickable */ 2287.faq_block .panel-title a { 2288display: flex; 2289align-items: center; 2290justify-content: space-between; 2291gap: 1em; 2292padding: 14px 16px; 2293color: inherit; 2294text-decoration: none; 2295cursor: pointer; 2296} 2297.faq_block .panel-title a:hover, 2298.faq_block .panel-title a:focus { 2299color: inherit; 2300text-decoration: none; 2301} 2302.faq_block .panel-title a:focus-visible { 2303outline: 2px solid var(--faq-focus); 2304outline-offset: -2px; 2305} 2306/* chevron â driven by aria-expanded, which the shim keeps in sync */ 2307.faq_block .panel-title a::after { 2308content: ''; 2309flex: 0 0 auto; 2310width: .45em; 2311height: .45em; 2312margin-top: -.22em; 2313border-right: 2px solid currentColor; 2314border-bottom: 2px solid currentColor; 2315transform: rotate(45deg); 2316transition: transform .2s ease, margin-top .2s ease; 2317opacity: .65; 2318} 2319.faq_block .panel-title a[aria-expanded="true"]::after { 2320margin-top: .18em; 2321transform: rotate(-135deg); 2322} 2323/* --- answer body ---------------------------------------------------------- */ 2324/* Fallback for a JS failure: the browser still reveals a panel when its id is 2325in the URL, so the answers stay reachable. */ 2326.faq_block .panel-collapse:target { 2327display: block !important; 2328} 2329/* divider only appears once a panel is open (:has() â no-op on old browsers) */ 2330.faq_block .panel:has(.panel-collapse.in) .panel-heading { 2331border-bottom-color: var(--faq-border); 2332} 2333/* Tailwind ships a `.collapse` utility (visibility: collapse) that collides with 2334the legacy Bootstrap class of the same name in this markup. Two class-level 2335selectors here so this wins on specificity regardless of stylesheet order. */ 2336.faq_block .panel-collapse, 2337.faq_table .panel-collapse { 2338visibility: visible; 2339} 2340/* padding lives on .panel-body, never margins â the height animation measures 2341scrollHeight on .panel-collapse and collapsing margins would break it */ 2342.faq_block .panel-body { 2343padding: 14px 16px 16px; 2344} 2345.faq_block .panel-body > :first-child { 2346margin-top: 0; 2347} 2348.faq_block .panel-body > :last-child { 2349margin-bottom: 0; 2350} 2351.faq_block .panel-body ol, 2352.faq_block .panel-body ul { 2353margin: .75em 0; 2354padding-left: 1.4em; 2355} 2356.faq_block .panel-body li { 2357margin-bottom: .35em; 2358} 2359.faq_block .panel-body em { 2360display: block; 2361margin-top: .9em; 2362color: var(--faq-muted); 2363} 2364/* legacy inline font sizes inside answers (span style="font-size: medium") 2365fight the 14px wrapper â normalize them back to the block's scale */ 2366.faq_block .panel-body [style*="font-size"] { 2367font-size: inherit !important; 2368} 2369/* --- search --------------------------------------------------------------- */ 2370.faq_block mark, 2371.faq_table mark { 2372padding: 0 1px; 2373background: var(--faq-mark); 2374color: inherit; 2375border-radius: 2px; 2376} 2377#search_faq, 2378[data-faq-search] { 2379width: 100%; 2380max-width: 420px; 2381margin-bottom: calc(var(--faq-gap) * 1.5); 2382padding: 10px 12px; 2383font: inherit; 2384color: inherit; 2385background: #fff; 2386border: 1px solid #dcdcdc; 2387border-radius: 6px; 2388} 2389#search_faq:focus, 2390[data-faq-search]:focus { 2391border-color: #2563eb; 2392outline: 2px solid rgba(37, 99, 235, .25); 2393outline-offset: 0; 2394} 2395/* --- small screens -------------------------------------------------------- */ 2396@media (max-width: 480px) { 2397.faq_block .panel-title a { 2398padding: 12px 13px; 2399} 2400.faq_block .panel-body { 2401padding: 12px 13px 14px; 2402} 2403} </style>
2403<script> /** 2404* FAQ collapse + search shim (no jQuery, no Bootstrap JS, no mark.js) 2405* 2406* Restores the behavior of the legacy CMS FAQ blocks: 2407* - a[data-toggle="collapse"][href="#id"] -> toggles .panel-collapse 2408* - #search_faq -> filters panels, highlights matches 2409* - ?search=term in the URL -> pre-fills and runs the filter 2410* 2411* Deliberately does NOT define window.jQuery / window.$ â a global jQuery stub 2412* makes third-party widgets take the jQuery code path and break. 2413* 2414* Public API: window.FaqShim.refresh() â re-scan after injecting markup (ESI, 2415* Alpine, AJAX). Not needed for clicks; those are delegated on document. 2416*/ 2417(function (window, document) { 2418'use strict'; 2419var TRIGGER_SEL = '[data-toggle="collapse"]'; 2420var PANEL_SEL = '.panel'; 2421var TITLE_SEL = '.panel-title a'; 2422var SEARCH_SEL = '#search_faq, [data-faq-search]'; 2423var SCOPE_SEL = '.faq_block, .faq_table, .panel-group'; 2424var DURATION = 250; 2425var originalTitles = new WeakMap(); 2426var ready = false; 2427/* ---------------------------------------------------------- styles ---- */ 2428function injectStyles() { 2429if (document.getElementById('faq-shim-css')) return; 2430var style = document.createElement('style'); 2431style.id = 'faq-shim-css'; 2432style.textContent = [ 2433/* structural fallback so panels are closed even if Bootstrap CSS is gone */ 2434'.faq_block .panel-collapse:not(.in),', 2435'.faq_table .panel-collapse:not(.in),', 2436'[data-faq-collapse]:not(.in){display:none}', 2437'[data-faq-collapse].in{display:block}', 2438'[data-faq-collapse][data-faq-collapse]{overflow:hidden;visibility:visible}', 2439'[data-faq-collapse].faq-animating{transition:height ' + DURATION + 'ms ease}', 2440'@media (prefers-reduced-motion:reduce){', 2441'[data-faq-collapse].faq-animating{transition:none}}' 2442].join(''); 2443(document.head || document.documentElement).appendChild(style); 2444} 2445/* ------------------------------------------------------- targeting ---- */ 2446function targetOf(trigger) { 2447var sel = trigger.getAttribute('data-target') || trigger.getAttribute('href') || ''; 2448if (!sel || sel === '#') return null; 2449// Duplicate ids across repeated CMS blocks are likely, so look inside the 2450// trigger's own FAQ scope first and only then fall back to the document. 2451var scope = trigger.closest(SCOPE_SEL); 2452var el = null; 2453try { 2454if (scope) el = scope.querySelector(sel); 2455if (!el) el = document.querySelector(sel); 2456} catch (e) { 2457return null; 2458} 2459return el; 2460} 2461function claim(el) { 2462if (!el || el.hasAttribute('data-faq-collapse')) return el; 2463el.setAttribute('data-faq-collapse', ''); 2464// `collapse` is a Tailwind utility (visibility: collapse) as well as a 2465// legacy Bootstrap hook. Nothing needs it now â .in and the data attribute 2466// carry the state â so drop it rather than fight it on specificity. 2467el.classList.remove('collapse'); 2468if (!el.id) el.id = 'faq-c-' + Math.random().toString(36).slice(2, 9); 2469return el; 2470} 2471function syncAria(target) { 2472var open = target.classList.contains('in'); 2473var triggers = document.querySelectorAll( 2474TRIGGER_SEL + '[href="#' + target.id + '"], ' + 2475TRIGGER_SEL + '[data-target="#' + target.id + '"]' 2476); 2477for (var i = 0; i < triggers.length; i++) { 2478triggers[i].setAttribute('aria-expanded', open ? 'true' : 'false'); 2479triggers[i].setAttribute('aria-controls', target.id); 2480if (triggers[i].tagName === 'A') triggers[i].setAttribute('role', 'button'); 2481} 2482} 2483/* ------------------------------------------------------ open/close ---- */ 2484function settle(el) { 2485if (el._faqTimer) { clearTimeout(el._faqTimer); el._faqTimer = null; } 2486if (el._faqEnd) { el.removeEventListener('transitionend', el._faqEnd); el._faqEnd = null; } 2487el.classList.remove('faq-animating'); 2488el.style.height = ''; 2489} 2490function finish(el, cb) { 2491var done = function (e) { 2492if (e && e.target !== el) return; 2493settle(el); 2494if (cb) cb(); 2495}; 2496el._faqEnd = done; 2497el.addEventListener('transitionend', done); 2498el._faqTimer = setTimeout(done, DURATION + 60); 2499} 2500function open(el, animate) { 2501claim(el); 2502settle(el); 2503el.classList.add('in'); 2504if (animate === false) { syncAria(el); return; } 2505el.style.height = '0px'; 2506el.classList.add('faq-animating'); 2507void el.offsetHeight; // force reflow 2508el.style.height = el.scrollHeight + 'px'; 2509finish(el); 2510syncAria(el); 2511} 2512function close(el, animate) { 2513claim(el); 2514settle(el); 2515if (animate === false) { 2516el.classList.remove('in'); 2517syncAria(el); 2518return; 2519} 2520el.style.height = el.scrollHeight + 'px'; 2521el.classList.add('faq-animating'); 2522void el.offsetHeight; 2523el.style.height = '0px'; 2524finish(el, function () { el.classList.remove('in'); syncAria(el); }); 2525} 2526function toggle(el) { 2527if (el.classList.contains('in')) close(el); else open(el); 2528} 2529/* --------------------------------------------------------- marking ---- */ 2530function unmark(link) { 2531var original = originalTitles.get(link); 2532if (original !== undefined && link.innerHTML !== original) link.innerHTML = original; 2533} 2534function mark(link, term) { 2535if (!originalTitles.has(link)) originalTitles.set(link, link.innerHTML); 2536unmark(link); 2537if (!term) return false;
2538var needle = term.toLowerCase(); 2539var walker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT, null, false); 2540var nodes = []; 2541var node; 2542while ((node = walker.nextNode())) nodes.push(node); 2543var found = false; 2544for (var i = 0; i < nodes.length; i++) { 2545var text = nodes[i].nodeValue; 2546var hay = text.toLowerCase(); 2547var at = hay.indexOf(needle); 2548if (at === -1) continue; 2549var frag = document.createDocumentFragment(); 2550var pos = 0; 2551while (at !== -1) { 2552if (at > pos) frag.appendChild(document.createTextNode(text.slice(pos, at))); 2553var m = document.createElement('mark'); 2554m.textContent = text.slice(at, at + needle.length); 2555frag.appendChild(m); 2556pos = at + needle.length; 2557at = hay.indexOf(needle, pos); 2558found = true; 2559} 2560if (pos < text.length) frag.appendChild(document.createTextNode(text.slice(pos))); 2561nodes[i].parentNode.replaceChild(frag, nodes[i]); 2562} 2563return found; 2564} 2565/* ---------------------------------------------------------- search ---- */ 2566function panels() { 2567var scoped = document.querySelectorAll( 2568'.faq_block ' + PANEL_SEL + ', .faq_table ' + PANEL_SEL + ', .panel-group ' + PANEL_SEL 2569); 2570return scoped.length ? scoped : document.querySelectorAll(PANEL_SEL); 2571} 2572function search(term) { 2573term = (term || '').trim(); 2574var list = panels(); 2575for (var i = 0; i < list.length; i++) { 2576var panel = list[i]; 2577var link = panel.querySelector(TITLE_SEL); 2578var body = panel.querySelector('.panel-collapse'); 2579panel.style.display = ''; 2580if (link) mark(link, ''); 2581if (body) close(claim(body), false); 2582if (!term) continue; 2583var hit = link ? mark(link, term) : false; 2584if (hit) { 2585if (body) open(body, false); 2586} else { 2587panel.style.display = 'none'; 2588} 2589} 2590} 2591function paramValue(name) { 2592var match = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.search); 2593if (!match) return ''; 2594try { 2595return decodeURIComponent(match[1].replace(/\+/g, ' ')); 2596} catch (e) { 2597return match[1]; 2598} 2599} 2600/* ------------------------------------------------------------ init ---- */ 2601function refresh() { 2602injectStyles(); 2603var triggers = document.querySelectorAll(TRIGGER_SEL); 2604for (var i = 0; i < triggers.length; i++) { 2605var target = targetOf(triggers[i]); 2606if (!target) continue; 2607claim(target); 2608syncAria(target); 2609} 2610// deep link: /page?...#collapse84 2611if (window.location.hash.length > 1) { 2612var hashed = null; 2613try { hashed = document.querySelector(window.location.hash); } catch (e) { hashed = null; } 2614if (hashed && hashed.hasAttribute('data-faq-collapse')) open(hashed, false); 2615} 2616} 2617function init() { 2618if (ready) return; 2619ready = true; 2620refresh(); 2621var seeded = paramValue('search'); 2622var input = document.querySelector(SEARCH_SEL); 2623if (input && seeded) { 2624input.value = seeded; 2625search(seeded); 2626} 2627} 2628// Delegated â survives ESI fragments and anything injected after load. 2629document.addEventListener('click', function (e) { 2630var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null; 2631if (!trigger) return; 2632var target = targetOf(trigger); 2633if (!target) return; 2634e.preventDefault(); 2635toggle(claim(target)); 2636}); 2637document.addEventListener('keydown', function (e) { 2638if (e.key !== 'Enter' && e.key !== ' ') return; 2639var trigger = e.target.closest ? e.target.closest(TRIGGER_SEL) : null; 2640if (!trigger || trigger.tagName === 'A') return; // anchors fire click natively 2641var target = targetOf(trigger); 2642if (!target) return; 2643e.preventDefault(); 2644toggle(claim(target)); 2645}); 2646document.addEventListener('input', function (e) { 2647if (!e.target.matches || !e.target.matches(SEARCH_SEL)) return; 2648search(e.target.value); 2649}); 2650if (document.readyState === 'loading') { 2651document.addEventListener('DOMContentLoaded', init); 2652} else { 2653init(); 2654} 2655window.addEventListener('load', refresh); 2656window.FaqShim = { refresh: refresh, open: open, close: close, search: search }; 2657})(window, document); </script>
2657</div> <!-promo_free_logo_gift_badge--><div class="widget widget-static-block"><style> /* SimplyStamps | Free Logo Stamp Gift promo badge. 2658Renders in 4 places: homepage Logo Stamps tile, the /rubber-logo-stamps PLP + its 10 sibling logo-stamp sub-category PLPs 2659(banner right of the intro heading, wraps under on mobile), and the title of every PDP carrying the promo option (select_7643). Nowhere else. 2660To end the promo: disable this static block. */ 2661.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} 2662.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child{position:relative;width:fit-content;margin-left:auto;margin-right:auto} 2663.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} 2664@media (max-width:639px){.zephyr-builder-block a[href$="/rubber-logo-stamps"] > div:first-child::after{top:-8%;right:-8%;width:42%;height:42%}} 2665.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))} 2666.ss-fg-badge:focus-visible{outline:2px solid #55B5E3;outline-offset:3px;border-radius:50%} 2667.ss-fg-h2row{display:flex;flex-wrap:wrap;align-items:center;gap:10px 20px;margin:0 0 12px} 2668.ss-fg-h2row>h2,.ss-fg-h2row>h1{margin:0!important;flex:0 1 auto} 2669.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} 2670.ss-fg-banner strong{font-weight:700;text-transform:uppercase;letter-spacing:.04em} 2671.ss-fg-banner:focus-visible{outline:2px solid #55B5E3;outline-offset:2px} 2672.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} 2673.ss-fg-banner .ss-fg-tip{right:0} 2674.ss-fg-banner.is-wrapped{margin-left:0;width:100%;border-radius:12px} 2675.ss-fg-title-row{position:relative} 2676.ss-fg-title-row>h1{padding-right:68px} 2677.ss-fg-pdp{position:absolute;top:-10px;right:0;width:58px;height:58px} 2678.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} 2679.ss-fg-pdp .ss-fg-tip{right:0}
2680@media (hover:hover){.ss-fg-badge:hover .ss-fg-tip,.ss-fg-banner:hover .ss-fg-tip{opacity:1;visibility:visible}} 2681.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} 2682@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}} 2683@media (prefers-reduced-motion:reduce){.ss-fg-tip{transition:none}} </style>
2683<script> (function(){ 2684var 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."; 2685function make(extra,banner){ 2686var b=document.createElement('span'); 2687b.tabIndex=0; 2688if(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>';} 2689else{b.className='ss-fg-badge '+extra;b.setAttribute('role','img');b.setAttribute('aria-label','Free gift with any logo stamp. '+TIP);} 2690var t=document.createElement('span'); 2691t.className='ss-fg-tip'; 2692t.setAttribute('aria-hidden','true'); 2693t.textContent=TIP; 2694b.appendChild(t); 2695b.addEventListener('click',function(e){e.stopPropagation();b.classList.toggle('is-open');}); 2696b.addEventListener('keydown',function(e){if(e.key==='Escape'){b.classList.remove('is-open');b.blur();}}); 2697document.addEventListener('click',function(){b.classList.remove('is-open');}); 2698return b; 2699} 2700function run(){ 2701var c=document.body.classList; 2702if(c.contains('catalog-category-view')&&c.contains('category-rubber-logo-stamps')){ 2703var h2=document.querySelector('.ss-logo-top > h2'); 2704if(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); 2705var fit=function(){bn.classList.remove('is-wrapped');if(bn.offsetTop>h2.offsetTop+h2.offsetHeight/2)bn.classList.add('is-wrapped');}; 2706fit();window.addEventListener('resize',fit);if(window.ResizeObserver)new ResizeObserver(fit).observe(row);} 2707} 2708var 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']; 2709if(c.contains('catalog-category-view')&&SIB.some(function(k){return c.contains(k);})){ 2710var h1c=document.querySelector('.flex-1 > h1'); 2711if(h1c&&!document.querySelector('.ss-fg-banner')){ 2712var rowc=document.createElement('div');rowc.className='ss-fg-h2row';h1c.parentNode.insertBefore(rowc,h1c);rowc.appendChild(h1c); 2713var bnc=make('',true);rowc.appendChild(bnc); 2714var fitc=function(){bnc.classList.remove('is-wrapped');if(bnc.offsetTop>h1c.offsetTop+h1c.offsetHeight/2)bnc.classList.add('is-wrapped');}; 2715fitc();window.addEventListener('resize',fitc);if(window.ResizeObserver)new ResizeObserver(fitc).observe(rowc); 2716} 2717} 2718if(c.contains('catalog-product-view')&&document.getElementById('select_7643')){ 2719var h1=document.querySelector('.product-header h1'); 2720if(h1&&!document.querySelector('.ss-fg-pdp')){h1.parentNode.classList.add('ss-fg-title-row');h1.parentNode.appendChild(make('ss-fg-pdp'));} 2721} 2722} 2723if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',run);else run(); 2724})(); </script>
2724</div><div class="widget widget-static-block"><div id="hcb-promo-root" hidden></div>
2724<script> (function () { 2725/* ====================================================================== 2726>>>>>>>>>>>>>>>>>>>>>> EDIT ZONE START <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 2727One list of promo phases. Each runs FROM its start UNTIL its end (Eastern). 2728MESSAGE = banner text (HTML ok â <strong> etc.) CODE = coupon 2729THRESHOLD = free-ship order minimum REWARD = what they unlock 2730Dates are Eastern: -04:00 summer, -05:00 winter. Day-end = 23:59:59. 2731====================================================================== */ 2732var PROMO = { 2733PHASES: [ 2734{ 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" }, 2735{ 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" }, 2736{ 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" }, 2737{ 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" } 2738], 2739COUNTDOWN_DAYS_BEFORE: 0, // 0 = NO timer (banner shows message only) 2740LINK_URL: "", // no link â banner is not clickable 2741SHOW_CODE: false, // native free shipping = no coupon; hide the code chip 2742// Small â bubble after the banner message. Leave "" to hide it. (Was used to explain 2743// the old coupon deducting $9.99; native free shipping needs no explainer.) 2744INFO_TIP: "", 2745LOAD_GOOGLE_FONT: true, 2746// --- ZEPHYR header banner mount --- 2747HEADER_SELECTOR: "header", // the fixed Zephyr <header> MOUNT_POSITION: "bottom", // "bottom" = below the nav (default) · "top" = above the nav rows 2748HEADER_HEIGHT_VAR: "--header-height", // Zephyr offsets <main> by padding-top:var(--header-height) 2749// --- PDP progress bar mount + subtotal source --- 2750GOAL_ENABLED: true, 2751GOAL_MOUNTS: [ 2752{ selector: ".price-pipe-container", position: "after" } // product pages (under the price block) 2753], 2754SUBTOTAL_URL: "/zephyr/cart/getItems", // cart subtotal endpoint; fetched ONCE per load (heavy) 2755POLL_MS: 1500, 2756COLORS: { 2757banner: { background: "#60B5E5", text: "#0A3550", chipBg: "#FFFFFF", chipText: "#0A3550" }, 2758goal: { text: "#0A3550", track: "#E6EEF3", fill: "#60B5E5", done: "#1A7F37" } 2759} 2760}; 2761/* ====================================================================== 2762>>>>>>>>>>>>>>>>>>>>>>> EDIT ZONE END <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 2763Do not edit below this line. 2764====================================================================== */ 2765if (window.__hcbPromoLoaded) return; 2766window.__hcbPromoLoaded = true; 2767var DAY = 86400000; 2768var 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 }; }) 2769.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; }) 2770.sort(function (a, b){ return a.from - b.from; }); 2771if (!P.length){ console.warn("[HCB Promo] no valid phases â disabled."); return; } 2772var endTs = P.reduce(function (m, p){ return Math.max(m, p.ts); }, 0); 2773var dismissKey = "hcbPromoDismissed:" + endTs; 2774try { if (sessionStorage.getItem(dismissKey)) window.__hcbDismissed = true; } catch (e) {} 2775if (Date.now() >= endTs) return; 2776function esc(s){ return String(s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } 2777function phaseIndexAt(now){ for (var i=0;i<P.length;i++) if (now >= P[i].from && now <= P[i].ts) return i; return -1; } 2778function pad(n){ return (n < 10 ? '0' : '') + n; } 2779function money(n){ return "$" + (Math.round(n*100)/100).toFixed(2).replace(/\.00$/, ""); } 2780var currentCode = P[Math.max(0, phaseIndexAt(Date.now()))].CODE; 2781var bannerInstances = [], goals = [], cachedUrlVal = null, interval = null; 2782function readSubtotal(){ return cachedUrlVal; } // populated by pollUrl() 2783function pollUrl(){ 2784if (!PROMO.SUBTOTAL_URL) return; 2785fetch(PROMO.SUBTOTAL_URL, {credentials:"include", headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(d){ 2786if (!d) return; 2787var v = (typeof d.subtotal_numeric !== "undefined") ? parseFloat(d.subtotal_numeric) 2788: (typeof d.subtotal !== "undefined") ? parseFloat(String(d.subtotal).replace(/[^0-9.]/g,"")) : NaN; 2789if (!isNaN(v)){ cachedUrlVal = v; tick(); } 2790}).catch(function(){}); 2791} 2792if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); 2793else start(); 2794function start(){ 2795if (PROMO.LOAD_GOOGLE_FONT && !document.getElementById('hcb-roboto-font')) { 2796var lk = document.createElement('link'); lk.id='hcb-roboto-font'; lk.rel='stylesheet'; 2797lk.href='https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'; 2798document.head.appendChild(lk); 2799} 2800var Cb = PROMO.COLORS.banner, Cg = PROMO.COLORS.goal; 2801var css = '' 2802/* banner â position:relative because it lives INSIDE the fixed Zephyr <header> */ 2803+ '.hcb-promo{position:relative;display:flex;align-items:center;justify-content:center;' 2804+ 'gap:.6em;padding:9px 40px;font:600 14px/1.2 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;' 2805+ 'white-space:nowrap;overflow:hidden;box-sizing:border-box;width:100%}' 2806+ '.hcb-promo *{box-sizing:border-box}' 2807+ '.hcb-promo__main{display:flex;align-items:center;justify-content:center;gap:.6em;min-width:0;color:inherit;text-decoration:none}' 2808+ '.hcb-promo__msg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;font-weight:400}' 2809+ '.hcb-promo__code{flex:none;cursor:pointer;border:0;border-radius:4px;padding:3px 9px;font:inherit;font-weight:700;letter-spacing:.03em}' 2810+ '.hcb-promo__timer{flex:none;font-variant-numeric:tabular-nums;font-weight:400}' 2811+ '.hcb-promo__timer b{font-weight:400}' 2812+ '.hcb-promo__timer--min{display:none}' 2813/* info â bubble â inline, height-safe; tooltip is portaled to<body> + fixed so nothing clips/covers it */ 2814+ '.hcb-promo__info{position:relative;flex:none;display:inline-flex;align-items:center}' 2815+ '.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}' 2816+ '.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}' 2817+ '.hcb-promo__itip::after{content:"";position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:' + Cb.text + '}' 2818+ '.hcb-promo__itip.is-open{opacity:1;visibility:visible}' 2819+ '.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}' 2820+ '.hcb-promo__x:hover{opacity:1}' 2821+ '@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}}' 2822/* goal bar */ 2823+ '.hcb-goal{font:600 14px/1.35 "Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;margin:10px 0;max-width:420px}' 2824+ '.hcb-goal__msg{color:' + Cg.text + ';margin:0 0 6px}.hcb-goal__msg b{font-weight:800}' 2825+ '.hcb-goal__track{height:10px;background:' + Cg.track + ';border-radius:999px;overflow:hidden}' 2826+ '.hcb-goal__fill{height:100%;width:0;background:' + Cg.fill + ';border-radius:999px;transition:width .4s ease}' 2827+ '.hcb-goal.is-done .hcb-goal__msg{color:' + Cg.done + '}.hcb-goal.is-done .hcb-goal__fill{background:' + Cg.done + '}'; 2828var style=document.createElement('style'); style.textContent=css; document.head.appendChild(style); 2829/* ---------- build header banner ---------- */ 2830var seed = P[Math.max(0, phaseIndexAt(Date.now()))]; 2831var linkOpen = PROMO.LINK_URL ? 'a href="' + esc(PROMO.LINK_URL) + '"' : 'span'; 2832var linkClose = PROMO.LINK_URL ? 'a' : 'span'; 2833var INFO_TIP = PROMO.INFO_TIP || ""; 2834function bannerMarkup(ph){ 2835return '<div class="hcb-promo" role="region" aria-label="Promotion" style="background:' + Cb.background + ';color:' + Cb.text + '">' 2836+ '<' + linkOpen + ' class="hcb-promo__main">' 2837+ '<span class="hcb-promo__msg">' + ph.MESSAGE + '</span>' 2838+ '<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>' 2839+ '<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>' 2840+ '</' + linkClose + '>' 2841+ (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>' : '') 2842+ (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>' : '') 2843+ '<button type="button" class="hcb-promo__x" aria-label="Dismiss promotion">×</button>' 2844+ '</div>'; 2845} 2846var header = document.querySelector(PROMO.HEADER_SELECTOR);
2847var mountHost = header || document.querySelector('main') || document.body; 2848var root = document.createElement('div'); root.className='hcb-promo-root'; root.innerHTML = bannerMarkup(seed); 2849if (PROMO.MOUNT_POSITION === 'top') mountHost.insertBefore(root, mountHost.firstChild); // above the nav rows 2850else mountHost.appendChild(root); // below the nav (default) 2851syncHeaderHeight(); // set --header-height now (anti-jump); Zephyr's own observer also maintains it 2852var inst={ root:root, msg:root.querySelector('.hcb-promo__msg'), units:root.querySelectorAll('[data-u]'), 2853fulls:root.querySelectorAll('.hcb-promo__timer--full'), mins:root.querySelectorAll('.hcb-promo__timer--min'), 2854code:root.querySelector('.hcb-promo__code'), x:root.querySelector('.hcb-promo__x') }; 2855if (inst.code) inst.code.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); 2856var b=inst.code, done=function(){ b.textContent='Copied!'; setTimeout(function(){ b.textContent=currentCode; },1500); }; 2857if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(currentCode).then(done,fb);}else{fb();} 2858function 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);} }); 2859inst.x.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); 2860try{sessionStorage.setItem(dismissKey,'1');}catch(err){} 2861window.__hcbDismissed=true; root.style.display='none'; syncHeaderHeight(); }); 2862// info â tooltip â portal to<body>, position:fixed under the icon 2863var info=root.querySelector('.hcb-promo__info'); 2864if(info){ 2865var ib=info.querySelector('.hcb-promo__ibtn'); 2866var tip=info.querySelector('.hcb-promo__itip'); 2867document.body.appendChild(tip); 2868var place=function(){ var r=ib.getBoundingClientRect(); tip.style.left=(r.left+r.width/2)+'px'; tip.style.top=(r.bottom+9)+'px'; }; 2869var openTip=function(){ place(); tip.classList.add('is-open'); }; 2870var closeTip=function(){ tip.classList.remove('is-open'); }; 2871ib.addEventListener('mouseenter', openTip); 2872ib.addEventListener('mouseleave', closeTip); 2873ib.addEventListener('focus', openTip); 2874ib.addEventListener('blur', closeTip); 2875ib.addEventListener('click', function(e){ e.preventDefault(); e.stopPropagation(); if(tip.classList.contains('is-open')) closeTip(); else openTip(); }); 2876document.addEventListener('click', function(e){ if(e.target!==ib) closeTip(); }); 2877window.addEventListener('scroll', function(){ if(tip.classList.contains('is-open')) place(); }, {passive:true}); 2878window.addEventListener('resize', function(){ if(tip.classList.contains('is-open')) place(); }); 2879} 2880bannerInstances.push(inst); 2881if (window.__hcbDismissed) root.style.display='none'; 2882/* ---------- keep --header-height in sync (includes this banner) ---------- */ 2883function syncHeaderHeight(){ 2884if (!header) return; 2885var h = Math.round(header.getBoundingClientRect().height); 2886if (h) document.documentElement.style.setProperty(PROMO.HEADER_HEIGHT_VAR, h + 'px'); 2887} 2888if (header){ 2889if (window.ResizeObserver){ try { new ResizeObserver(syncHeaderHeight).observe(header); } catch(e){} } 2890window.addEventListener('resize', syncHeaderHeight); 2891setTimeout(syncHeaderHeight,300); setTimeout(syncHeaderHeight,1200); 2892} 2893/* ---------- build PDP goal bar (only where the price container exists) ---------- */ 2894if (PROMO.GOAL_ENABLED){ 2895(PROMO.GOAL_MOUNTS||[]).forEach(function(mnt){ 2896var ghost=document.querySelector(mnt.selector); if(!ghost) return; 2897var groot=document.createElement('div'); groot.className='hcb-goal'; 2898groot.innerHTML='<p class="hcb-goal__msg"></p><div class="hcb-goal__track"><div class="hcb-goal__fill"></div></div>'; 2899var gp=mnt.position; 2900if(gp==='before') ghost.parentNode.insertBefore(groot, ghost); 2901else if(gp==='prepend') ghost.insertBefore(groot, ghost.firstChild); 2902else if(gp==='append') ghost.appendChild(groot); 2903else ghost.parentNode.insertBefore(groot, ghost.nextSibling); 2904goals.push({ root:groot, msg:groot.querySelector('.hcb-goal__msg'), fill:groot.querySelector('.hcb-goal__fill') }); 2905}); 2906} 2907if (goals.length) pollUrl(); // fetch cart subtotal ONCE (heavy endpoint) â only where a goal bar renders 2908interval=setInterval(tick, 1000); // 1s clock only (no re-fetch) 2909tick(); 2910} 2911/* ---------- single clock drives banner + goal ---------- */ 2912var lastIdx = -2; 2913function 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; } 2914function 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'; } 2915function tick(){ 2916var now = Date.now(); 2917var idx = phaseIndexAt(now); 2918if (now >= endTs || idx === -1){ 2919if (interval) clearInterval(interval);
2920bannerInstances.forEach(function(o){ o.root.style.display='none'; }); 2921goals.forEach(function(g){ g.root.style.display='none'; }); 2922return; 2923} 2924var ph = P[idx]; 2925if (idx !== lastIdx){ 2926lastIdx = idx; currentCode = ph.CODE; 2927bannerInstances.forEach(function(o){ o.msg.innerHTML=ph.MESSAGE; if(o.code){ o.code.textContent=currentCode; o.code.setAttribute('aria-label','Copy promo code '+currentCode); } }); 2928} 2929var ms = ph.ts - now, showT = ms <= PROMO.COUNTDOWN_DAYS_BEFORE*DAY; 2930var d=Math.floor(ms/DAY), h=Math.floor((ms%DAY)/3600000), m=Math.floor((ms%3600000)/60000), s=Math.floor((ms%60000)/1000); 2931bannerInstances.forEach(function(o){ 2932if (window.__hcbDismissed) return; 2933if (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); } 2934else showTimer(o,false); 2935}); 2936if (goals.length){ 2937var sub=readSubtotal(), pct, html, done=false; 2938var useCode=PROMO.SHOW_CODE; 2939if (sub===null){ pct=0; html='Spend '+money(ph.THRESHOLD)+'+ for <b>'+ph.REWARD+'</b>'+(useCode?' â use code <b>'+ph.CODE+'</b>':''); } 2940else 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.":""); } 2941else { 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+")":""); } 2942goals.forEach(function(gEl){ gEl.msg.innerHTML=html; gEl.fill.style.width=pct+'%'; gEl.root.classList.toggle('is-done',done); gEl.root.style.display=''; }); 2943} 2944} 2945})(); </script>
2945</div><div class="widget widget-static-block"> <style> /* v1.31 -- Roboto promoted to first in the stack to match OpenMage brand. 2946All child elements inherit this via font-family: inherit or unspecified. */ 2947.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; } 2948@keyframes ss-fade-in { from { opacity: 0; } to { opacity: 1; } } 2949.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; } 2950@keyframes ss-slide-up { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } 2951.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; } 2952.ss-upsell-close:hover { color: #475569; background: #f1f5f9; } 2953.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; } 2954/* v1.31 -- title case + Roboto Bold. Letter-spacing removed (was for all-caps). */ 2955.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; } 2956/* v1.31 -- subhead sits directly under the headline; smaller, softer color */ 2957.ss-upsell-subhead { font-size: 14px; line-height: 1.5; margin: 0 0 16px; color: #475569; font-weight: 400; text-align: center; } 2958/* v1.31 -- 300px image slot, padding removed so GIF fills edge-to-edge. 2959overflow:hidden clips anything spilling outside the rounded corners. */ 2960.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; } 2961.ss-upsell-image img { width: 100%; height: 100%; object-fit: cover; display: block; } 2962.ss-upsell-bullets { list-style: none; margin: 0 0 20px; padding: 0; } 2963.ss-upsell-bullets li { position: relative; padding-left: 24px; margin-bottom: 10px; color: #334155; line-height: 1.5; font-size: 14px; } 2964.ss-upsell-bullets li:before { content: "\2713"; position: absolute; left: 0; color: #16a34a; font-weight: 700; } 2965.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; } 2966.ss-upsell-price strong { color: #16a34a; font-size: 17px; } 2967.ss-upsell-actions { display: flex; flex-direction: column; gap: 4px; align-items: center; } 2968.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; } 2969.ss-upsell-accept:hover { background: #15803d; } 2970.ss-upsell-accept:focus { outline: 2px solid #15803d; outline-offset: 2px; } 2971/* v1.31 -- "No thanks" is now a small centered text link, not a ghost button. 2972Reduces visual competition with the primary Upgrade CTA. */ 2973.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; } 2974.ss-upsell-decline:hover { color: #334155; text-decoration: underline; } 2975.ss-upsell-decline:focus { outline: 2px solid #64748b; outline-offset: 2px; } 2976/* v1.31 -- config-step Cancel now inherits the small-link style from 2977.ss-upsell-decline to match Step 1's philosophy. Ghost-button CSS 2978override deliberately removed. .ss-config-cancel class kept purely 2979as a JS querySelector hook. */ 2980@media (max-width: 480px) { .ss-upsell-modal { padding: 24px 20px 20px; border-radius: 10px; } .ss-upsell-modal h2 { font-size: 18px; } } 2981/* Success state */ 2982.ss-upsell-success { text-align: center; padding-top: 40px; } 2983.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); } 2984@keyframes ss-pop { 0% { transform: scale(0.3); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } 2985.ss-upsell-success h2 { color: #15803d; margin-bottom: 8px; } 2986.ss-upsell-success-tagline { color: #334155; font-size: 15px; line-height: 1.5; margin: 0 0 18px; } 2987.ss-upsell-success-cta { color: #94a3b8; font-size: 13px; margin: 0; display: flex; align-items: center; justify-content: center; gap: 8px; } 2988.ss-upsell-spinner { width: 14px; height: 14px; border: 2px solid #cbd5e1; border-top-color: #16a34a; border-radius: 50%;
2988 animation: ss-spin 0.7s linear infinite; } 2989@keyframes ss-spin { to { transform: rotate(360deg); } } 2990.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; } 2991/* Config step (step 2) */ 2992.ss-config-intro { color: #475569; font-size: 14px; line-height: 1.5; margin: 0 0 18px; } 2993.ss-config-fields { max-height: 50vh; overflow-y: auto; margin: 0 0 20px; padding-right: 4px; } 2994.ss-opt-field { margin-bottom: 14px; } 2995.ss-opt-field label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 6px; } 2996.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; } 2997.ss-opt-field select:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; } 2998.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; } 2999.ss-opt-field input[type="text"]:focus, .ss-opt-field textarea:focus { outline: 2px solid #16a34a; outline-offset: 1px; border-color: #16a34a; } 3000.ss-opt-field textarea { resize: vertical; min-height: 60px; } 3001.ss-opt-group-label { display: block; font-weight: 600; font-size: 13px; color: #334155; margin-bottom: 8px; } 3002.ss-opt-radio { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 14px; color: #334155; cursor: pointer; font-weight: 400; } 3003.ss-opt-radio input { margin: 0; } 3004.ss-config-required { color: #dc2626; margin-left: 3px; } 3005.ss-opt-field-error select, 3006.ss-opt-field-error input[type="text"], 3007.ss-opt-field-error textarea { border-color: #dc2626; box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12); } 3008.ss-opt-field-error > label, 3009.ss-opt-field-error > .ss-opt-group-label { color: #dc2626; } 3010.ss-opt-error { color: #dc2626; font-size: 12px; margin-top: 6px; font-weight: 500; } 3011/* Variant B / C -- value messaging enhancements */ 3012.ss-upsell-badge.ss-upsell-badge-popular { background: #dcfce7; color: #15803d; } 3013.ss-upsell-badge.ss-upsell-badge-discount { background: #fef3c7; color: #92400e; } 3014.ss-upsell-cost-per-use { font-size: 12px; color: #64748b; margin-top: 4px; } 3015.ss-upsell-trust { font-size: 12px; color: #64748b; margin: 0 0 16px; text-align: center; line-height: 1.4; } 3016.ss-upsell-trust .ss-stars { color: #f59e0b; letter-spacing: 1px; } 3017.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; } 3018.ss-upsell-price-strike { text-decoration: line-through; color: #94a3b8; font-weight: 400; margin-right: 6px; } </style>
3018<script> (function () { 3019'use strict'; 3020var TIER_MAP = [ 3021{ tier: 1, name: 'Wood Handle', match: ['wood handle', 'wood'] }, 3022{ tier: 2, name: 'Self-Inking', match: ['self-inking', 'self inking', 'selfinking'] }, 3023{ tier: 3, name: 'Pre-Inked', match: ['pre-inked', 'pre inked', 'preinked'], subtype: 'pre_inked' }, 3024{ tier: 3, name: 'Professional', match: ['professional'], subtype: 'professional' } 3025]; 3026var STAMP_TYPE_LABEL_RE = /stamp\s*(type|style)/i; 3027var SESSION_ACCEPTED_KEY = 'ss_upsell_accepted_session'; 3028var SESSION_DECLINE_KEY = 'ss_upsell_decline_count'; 3029var SUCCESS_DWELL_MS = 1500; // how long the success state stays before submit 3030// After this many declines in a session, the modal stops firing -- respects 3031// a clear "no thanks" signal so we don't pester. Cancels at the config 3032// step count as declines too. Set to 0 for unlimited (no decline cap). 3033var MAX_DECLINES_PER_SESSION = 3; 3034// ============================================================ 3035// A/B VARIANT FRAMEWORK -- discount test 3036// ------------------------------------------------------------ 3037// Visitors are randomly assigned to A or B (50/50) on first PDP 3038// visit and persisted in localStorage. Same visitor always sees 3039// the same variant. URL override: ?ss_variant=B for QA. 3040// 3041// A = control: current production modal, no discount. 3042// B = treatment: same modal + 10% off coupon (UPSELL10) auto-applied. 3043// 3044// Variant content lives in VARIANT_CONFIG -- edit copy/discount here 3045// without touching the rendering logic below. 3046// ============================================================ 3047var VARIANT_STORAGE_KEY = 'ss_upsell_variant'; 3048var VARIANT_OVERRIDE_PARAM = 'ss_variant'; 3049var VARIANTS = ['A', 'B']; 3050var VARIANT_CONFIG = { 3051// A = CONTROL -- current production modal exactly as-is, no overrides. 3052A: { 3053// Empty = use defaults. Same as live production behavior. 3054}, 3055// B = DISCOUNT -- current modal + 10% discount on accept. The ONLY 3056// variable changing vs A is the discount; everything else (badge, 3057// copy, image, trust elements) stays identical so the lift can be 3058// attributed cleanly to the discount. 3059// 3060// Coupon codes are per-transition (keyed by upgrade target tier) so 3061// each transition can have its own Magento Cart Price Rule with its 3062// own conditions/scope. If a code leaks, only that specific upgrade 3063// path is exposed. 3064B: { 3065discountBanner: 'Save 10% on this upgrade -- today only', 3066couponCodes: { 3067'self-inking': 'UPSELL-SI10', // Wood Handle -> Self-Inking 3068'pre_inked': 'UPSELL-PI10', // Self-Inking -> Pre-Inked 3069'professional': 'UPSELL-PRO10' // Self-Inking -> Professional 3070}, 3071discountPct: 10 3072} 3073}; 3074// If false, the config step skips text inputs and textareas (stamp text 3075// lines, etc). Their values still carry over to the cart -- they just 3076// aren't re-shown for editing in the modal. 3077var INCLUDE_TEXT_FIELDS_IN_CONFIG = false; 3078// DEV TOGGLE -- when true, the modal fires on EVERY eligible ATC, ignoring 3079// the no-badger session lock. Flip to false before production launch so 3080// the lock kicks in after a user accepts an upgrade. 3081var DEV_MODE_ALWAYS_SHOW = false;
3082// ============================================================ 3083// CATEGORY TARGETING -- controlled rollout 3084// ------------------------------------------------------------ 3085// Use this to limit which product categories show the upsell modal. 3086// Substring match (case-insensitive) against the breadcrumb path. 3087// 3088// TARGET_CATEGORIES -- if non-empty, modal ONLY fires when the 3089// product's category breadcrumb contains one of these strings. 3090// Empty array = fire on all categories (default). 3091// 3092// EXCLUDED_CATEGORIES -- modal NEVER fires when the breadcrumb 3093// contains one of these strings, even if it also matches TARGET. 3094// Exclusions always win over targets. 3095// 3096// Examples: 3097// Target Address Stamps only: 3098// TARGET_CATEGORIES = ['Address Stamps']; 3099// Target everything EXCEPT custom/specialty stamps: 3100// EXCLUDED_CATEGORIES = ['Custom', 'Specialty']; 3101// Target Logo + Notary, exclude any "Bulk" sub-category: 3102// TARGET_CATEGORIES = ['Logo Stamps', 'Notary Stamps']; 3103// EXCLUDED_CATEGORIES = ['Bulk']; 3104// 3105// v1.32 ACTION NEEDED: these names came from the pre-Zephyr category 3106// landing page. Zephyr breadcrumbs read differently (a Duncan address 3107// stamp now shows "Address Stamps", not "Business Address Stamps"), so 3108// this whitelist must be re-mapped to the LIVE Zephyr breadcrumb strings 3109// or the modal won't fire on the categories you intend. Verify each on 3110// the live site and update below. 3111// ============================================================ 3112var TARGET_CATEGORIES = [ 3113'Office Stamps', 3114'Business Address Stamps', 3115'Signature Stamps', 3116'Date Stamps', 3117'Banking Stamps', 3118'Accounting & Tax', 3119'Postage Paid', 3120'Medical', 3121'Inspection', 3122'QR Code', 3123'Secure', 3124'Eco-Friendly', 3125'Checkbox', 3126'Custom Text' 3127]; 3128var EXCLUDED_CATEGORIES = [ 3129'Architects', 3130'Engineer', 3131'Land Surveyor', 3132'licensed Professional Stamps', 3133'Interior Design', 3134'LLC Stamps', 3135'Real Estate Appraisers' 3136]; // e.g. ['Custom', 'Specialty'] 3137// ============================================================ 3138// TIER IMAGES -- generic product image shown in the upsell modal 3139// ------------------------------------------------------------ 3140// Drop in a URL per upgrade tier. Image appears above the value 3141// checkmarks in step 1 of the modal. Leave a key empty to skip 3142// the image section for that tier (graceful fallback). 3143// 3144// Recommended: ~600x300px, transparent or white background, the 3145// bare stamp photographed straight-on. Hosted anywhere reachable 3146// by the site (Magento media folder, CDN, etc). 3147// ============================================================ 3148// v1.31 -- GIF assets, TEMPORARY LOCAL PATHS. 3149// Currently uploaded flat to wysiwyg/SimplyStamps/ with the "upsell-" prefix. 3150// TODO before prod push: move files to wysiwyg/SimplyStamps/upsell/ and rename 3151// to {tier}.gif (dropping the "upsell-" prefix), then update these paths to: 3152// 'self-inking': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/self-inking.gif', 3153// 'pre_inked': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/pre-inked.gif', 3154// 'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/professional.gif' 3155var TIER_IMAGES = { 3156'self-inking': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-self-inking.gif', 3157'pre_inked': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-pre-inked-v2.gif', 3158'professional': 'https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/upsell/upsell-professional-v2.gif' 3159}; 3160// ============================================================ 3161// CONFIG STEP -- tier-relevant option filtering 3162// ------------------------------------------------------------ 3163// PRIMARY MECHANISM: auto-detect tier-tagged labels. 3164// For each option label on the form, scan it for any tier name 3165// ('Pre-Inked', 'Self-Inking', 'Wood Handle', 'Professional'). 3166// - Mentions UPGRADE TARGET tier name -> SHOW 3167// - Mentions a DIFFERENT tier name -> HIDE 3168// - Mentions no tier name -> SHOW (generic option) 3169// 3170// This handles SimplyStamps's convention of naming options like 3171// "Pre-Inked Sizes", "Self-Inking Sizes", "Ink Color - Pre-Inked" 3172// with no manual config required. 3173// 3174// ESCAPE HATCH: TIER_OPTION_FILTERS below lets you override per tier 3175// for edge cases (labels that don't follow the convention). 3176// show: [] -- if non-empty, ONLY GENERIC options matching these 3177// keywords are kept. Tier-tagged auto-detection still runs. 3178// hide: [] -- additional labels to drop (substring match). 3179// 3180// Inspect what survives for a given product: 3181// window.SS_UPSELL_DEBUG = true; window.SSUpsell.inspect(); 3182// ============================================================ 3183var TIER_OPTION_FILTERS = { 3184// All upgrade tiers (Self-Inking, Pre-Inked, Professional) have 3185// built-in pads -- separate ink pad option is Wood-Handle-only. 3186'self_inking': { hide: ['ink pad', 'pad color'] }, 3187// Pre-Inked also has pre-loaded ink, so the extra-bottle option doesn't apply. 3188'pre_inked': { hide: ['ink pad', 'pad color', 'extra bottle', 'bottle of ink'] }, 3189'professional': { hide: ['ink pad', 'pad color'] } 3190}; 3191// v1.31 -- Routing flipped: default to Professional, override to Pre-Inked 3192// only if the product name/breadcrumb signals a logo/brand focus. 3193// Rationale: nearly all TARGET_CATEGORIES are professional-use stamps 3194// (office, notary, banking, medical, etc), so Professional is the norm. 3195// Pre-Inked's sweet spot is high-fidelity logo/brand reproduction -- 3196// that's the exception, so we key off those signals specifically. 3197var PRE_INKED_KEYWORDS = [ 3198'logo', 'brand', 'branding', 'monogram', 'crest', 'emblem' 3199]; 3200// v1.31 -- copy refresh per transition. New pattern: 3201// headline: short, all-caps, question-style attention grabber
3202// subhead: one-sentence value prop that reinforces the tier 3203// bullets: detail points (kept from v1.30, still useful proof) 3204// If headline is empty string, the<h2> is skipped and subhead renders alone. 3205var COPY = { 3206'1->self-inking': { 3207// Wood Handle -> Self-Inking (teacher buyup). Heading pairs with the 3208// subhead's "ink is already inside" promise -- calls out the ink pad 3209// as the pain point being solved. 3210headline: 'Skip The Ink Pad?', 3211subhead: 'The ink is already inside. Just press down and you get a clean, perfect mark.', 3212bullets: [ 3213'No separate ink pad needed -- built right in', 3214'10,000+ impressions before re-inking' 3215] 3216}, 3217'2->pre_inked': { 3218headline: 'Serious About Your Brand?', 3219subhead: 'Pre-inked stamps print sharper detail and cleaner lines. Your logo, exactly right every time.', 3220bullets: [ 3221'Sharper, crisper detail -- ideal for logos & branding', 3222'Up to 50,000 impressions per fill' 3223] 3224}, 3225'2->professional': { 3226headline: 'Are You A Professional?', 3227subhead: 'For desks that stamp all day. Heavier build, thousands of crisp impressions.', 3228bullets: [ 3229'Heavy-duty frame engineered for high-volume stamping', 3230'Durable mechanism backed by a long-term warranty' 3231] 3232} 3233}; 3234// Success state copy -- keyed the same way as COPY 3235var SUCCESS_COPY = { 3236'1->self-inking': { 3237headline: 'Upgraded to Self-Inking', 3238tagline: 'You\'ll get 10,000+ cleaner impressions without juggling a separate ink pad.' 3239}, 3240'2->pre_inked': { 3241headline: 'Upgraded to Pre-Inked', 3242tagline: 'Premium, sharper impressions -- up to 50,000 per fill. A smart pick for branding.' 3243}, 3244'2->professional': { 3245headline: 'Upgraded to Professional', 3246tagline: 'Built for daily volume -- durable mechanism backed by a long-term warranty.' 3247} 3248}; 3249// Session helpers -- once a user accepts an upgrade, don't badger them again. 3250// DEV_MODE_ALWAYS_SHOW bypasses the lock so the modal fires every ATC. 3251function hasUpgradedThisSession() { 3252if (DEV_MODE_ALWAYS_SHOW) return false; 3253try { return sessionStorage.getItem(SESSION_ACCEPTED_KEY) === '1'; } catch (e) { return false; } 3254} 3255function markUpgradedThisSession() { 3256try { sessionStorage.setItem(SESSION_ACCEPTED_KEY, '1'); } catch (e) {} 3257} 3258// Decline-cap helpers -- count declines (modal dismiss OR cancel at config) 3259// so the modal stops firing once the user has clearly opted out. 3260function getDeclineCount() { 3261try { return parseInt(sessionStorage.getItem(SESSION_DECLINE_KEY) || '0', 10) || 0; } 3262catch (e) { return 0; } 3263} 3264function incrementDeclineCount() {
3265try { sessionStorage.setItem(SESSION_DECLINE_KEY, String(getDeclineCount() + 1)); } catch (e) {} 3266} 3267function hasDeclinedTooMany() { 3268if (DEV_MODE_ALWAYS_SHOW) return false; 3269if (!MAX_DECLINES_PER_SESSION) return false; // 0 = unlimited 3270return getDeclineCount() >= MAX_DECLINES_PER_SESSION; 3271} 3272// ---------------------------------------------------------- 3273// Variant helpers (A/B/C framework) 3274// ---------------------------------------------------------- 3275function getAssignedVariant() { 3276// URL override wins (for QA) 3277try { 3278var url = new URL(window.location.href); 3279var override = (url.searchParams.get(VARIANT_OVERRIDE_PARAM) || '').toUpperCase(); 3280if (VARIANTS.indexOf(override) !== -1) return override; 3281} catch (e) {} 3282// Persisted in localStorage 3283try { 3284var stored = localStorage.getItem(VARIANT_STORAGE_KEY); 3285if (stored && VARIANTS.indexOf(stored) !== -1) return stored; 3286} catch (e) {} 3287// First visit -- random assignment 3288var idx = Math.floor(Math.random() * VARIANTS.length); 3289var variant = VARIANTS[idx]; 3290try { localStorage.setItem(VARIANT_STORAGE_KEY, variant); } catch (e) {} 3291log('Assigned new variant:', variant); 3292return variant; 3293} 3294function getVariantConfig(variant) { 3295return VARIANT_CONFIG[variant] || VARIANT_CONFIG.A || {}; 3296} 3297// Pick the right coupon code for this upgrade transition. Variant B uses 3298// a couponCodes map keyed by upgrade target ("self-inking", "pre_inked", 3299// "professional"). Legacy single couponCode also supported as fallback. 3300function resolveCouponCode(variantCfg, targetTier) { 3301if (!variantCfg) return null; 3302if (variantCfg.couponCodes && targetTier) { 3303var key = targetTier.subtype ? targetTier.subtype 3304: (targetTier.tier === 2 ? 'self-inking' : null); 3305if (key && variantCfg.couponCodes[key]) return variantCfg.couponCodes[key]; 3306} 3307return variantCfg.couponCode || null; 3308} 3309// Small local merge helper -- inline to avoid scope issues with the 3310// init()-scoped mergeParams. Used by both applyVariantCDiscount 3311// (stash phase) and applyDeferredCoupon (apply phase).
3312function _mergeParams(a, b) { 3313var out = {}; 3314for (var k in a) if (Object.prototype.hasOwnProperty.call(a, k)) out[k] = a[k]; 3315for (var k2 in b) if (Object.prototype.hasOwnProperty.call(b, k2)) out[k2] = b[k2]; 3316return out; 3317} 3318// v1.30 -- STASH the coupon code (do NOT POST from PDP). 3319// Previously we POSTed to /checkout/cart/couponPost/ from the PDP right 3320// before form.submit. That raced with the concurrent add-to-cart POST 3321// and hit an empty cart in Magento's session, so the coupon never stuck. 3322// Now we save the code + tracking context to sessionStorage; the actual 3323// apply happens on the cart page once the item is in cart (see 3324// applyDeferredCoupon below). 3325// 3326// v1.32 -- On Zephyr the AJAX add opens the cart DRAWER instead of 3327// navigating to /checkout/cart, so the stashed coupon applies the next 3328// time the shopper opens the full cart / checkout (still before purchase). 3329var PENDING_COUPON_KEY = 'ss_upsell_pending_coupon'; 3330function applyVariantCDiscount(couponCode, trackingParams) { 3331if (!couponCode) return; 3332try { 3333var payload = { 3334code: couponCode, 3335tracking: trackingParams || {}, 3336ts: Date.now() 3337}; 3338sessionStorage.setItem(PENDING_COUPON_KEY, JSON.stringify(payload)); 3339log('Coupon deferred for cart-page application:', couponCode); 3340} catch (e) { 3341log('Failed to defer coupon:', e); 3342} 3343} 3344// v1.30 -- Apply any pending upsell coupon on the cart page. 3345// Runs at module scope so it fires on every page load, cart or not. 3346// Bails immediately if not on the cart page or no pending coupon. 3347// Clears sessionStorage BEFORE posting to prevent reload loops if 3348// the POST fails or reload happens for an unrelated reason. 3349function applyDeferredCoupon() { 3350// Only run on the cart page 3351if (!/\/checkout\/cart(\/|\?|$)/.test(window.location.pathname + window.location.search)) return; 3352var raw; 3353try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; } 3354if (!raw) return; 3355var data; 3356try { data = JSON.parse(raw); } catch (e) { data = null; } 3357if (!data || !data.code) { 3358try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3359return; 3360} 3361// Clear FIRST so a failed apply doesn't cause a reload loop 3362try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3363log('Applying deferred coupon on cart page:', data.code);
3364var params = _mergeParams(data.tracking || {}, { coupon_code: data.code }); 3365try { 3366var fd = new FormData(); 3367fd.append('form_key', getFormKey()); 3368fd.append('coupon_code', data.code); 3369fd.append('remove', '0'); 3370fetch('/checkout/cart/couponPost/', { 3371method: 'POST', 3372body: fd, 3373credentials: 'same-origin', 3374headers: { 'X-Requested-With': 'XMLHttpRequest' } 3375}).then(function (r) { 3376log('Deferred coupon response:', r.status); 3377if (r.status >= 200 && r.status < 400) { 3378track('upsell_discount_applied', _mergeParams(params, { http_status: r.status })); 3379// Reload cart so the discount appears in the totals. 3380// Small delay so the event has time to fire before navigation. 3381setTimeout(function () { 3382// Guard against loop -- only reload if sessionStorage is now empty 3383var still; 3384try { still = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { still = null; } 3385if (!still) window.location.reload(); 3386}, 150); 3387} else { 3388track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response' })); 3389} 3390}).catch(function (e) { 3391log('Deferred coupon POST failed:', e); 3392track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) })); 3393}); 3394} catch (e) { 3395log('Deferred coupon apply threw:', e); 3396track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e) })); 3397} 3398} 3399// v1.32b -- Apply a stashed coupon IMMEDIATELY (no page gate, no reload). 3400// On Zephyr the AJAX add opens the cart drawer and never loads /checkout/cart, 3401// so applyDeferredCoupon (cart-page only) might never run -- the shopper could 3402// reach checkout via the drawer and be charged full price on a promised 3403// discount. We call this right after the add's promise resolves, so the cart 3404// already has the item (no v1.30 race) and the coupon lands on the quote. 3405// On success we clear the stash; on failure we LEAVE it so the cart-page 3406// applyDeferredCoupon can still retry as a fallback. 3407function applyPendingCouponNow() { 3408var raw; 3409try { raw = sessionStorage.getItem(PENDING_COUPON_KEY); } catch (e) { return; } 3410if (!raw) return; 3411var data; 3412try { data = JSON.parse(raw); } catch (e) { data = null; } 3413if (!data || !data.code) { try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} return; } 3414log('Applying pending coupon immediately (post-add):', data.code);
3415var params = _mergeParams(data.tracking || {}, { coupon_code: data.code }); 3416try { 3417var fd = new FormData(); 3418fd.append('form_key', getFormKey()); 3419fd.append('coupon_code', data.code); 3420fd.append('remove', '0'); 3421fetch('/checkout/cart/couponPost/', { 3422method: 'POST', 3423body: fd, 3424credentials: 'same-origin', 3425headers: { 'X-Requested-With': 'XMLHttpRequest' } 3426}).then(function (r) { 3427log('Immediate coupon response:', r.status); 3428if (r.status >= 200 && r.status < 400) { 3429// Applied -- clear so the cart page doesn't reload to re-apply. 3430try { sessionStorage.removeItem(PENDING_COUPON_KEY); } catch (e) {} 3431track('upsell_discount_applied', _mergeParams(params, { http_status: r.status, applied_from: 'pdp_post_add' })); 3432} else { 3433// Leave stashed -- cart-page applyDeferredCoupon will retry. 3434track('upsell_discount_failed', _mergeParams(params, { http_status: r.status, error: 'non-2xx-response', applied_from: 'pdp_post_add' })); 3435} 3436}).catch(function (e) { 3437log('Immediate coupon POST failed (will retry on cart page):', e); 3438track('upsell_discount_failed', _mergeParams(params, { error: String(e && e.message || e), applied_from: 'pdp_post_add' })); 3439}); 3440} catch (e) { 3441log('Immediate coupon apply threw:', e); 3442} 3443} 3444// Run deferred coupon apply at module scope (fires on every page load, 3445// including cart page -- bails if not cart / no pending coupon). This is the 3446// fallback for when the immediate post-add apply (applyPendingCouponNow) 3447// didn't run or didn't succeed. 3448try { applyDeferredCoupon(); } catch (e) { /* never break page load */ } 3449// Record each upgrade so the order-success page can fire upsell_purchased 3450// with order_id + revenue_delta. Stored as an array in sessionStorage; 3451// the success-page snippet reads, dispatches, and clears it. 3452function recordUpgradeForPurchaseTracking(params, qty) { 3453try { 3454var key = 'ss_upsell_pending_purchases'; 3455var existing = sessionStorage.getItem(key); 3456var arr = existing ? JSON.parse(existing) : []; 3457var rec = { qty: parseInt(qty, 10) || 1, ts: Date.now() }; 3458for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) rec[k] = params[k]; 3459arr.push(rec); 3460sessionStorage.setItem(key, JSON.stringify(arr)); 3461log('Recorded pending upgrade for purchase tracking:', rec); 3462} catch (e) {} 3463} 3464function log() { 3465if (window.SS_UPSELL_DEBUG && window.console) { 3466var args = ['[TierUpsell]'].concat([].slice.call(arguments)); 3467console.log.apply(console, args); 3468} 3469} 3470function norm(s) { return (s || '').toLowerCase().trim(); } 3471function findTierForText(text) { 3472var t = norm(text); 3473for (var i = 0; i < TIER_MAP.length; i++) { 3474var tier = TIER_MAP[i]; 3475for (var j = 0; j < tier.match.length; j++) { 3476if (t.indexOf(tier.match[j]) !== -1) return tier; 3477} 3478} 3479return null; 3480} 3481// Like findTierForText but returns ALL tiers whose name appears in the text. 3482// Used to detect which tier an option label is tagged for. 3483function findAllTiersInText(text) { 3484var t = norm(text); 3485var matches = []; 3486for (var i = 0; i < TIER_MAP.length; i++) { 3487var tier = TIER_MAP[i]; 3488for (var j = 0; j < tier.match.length; j++) { 3489if (t.indexOf(tier.match[j]) !== -1) { matches.push(tier); break; } 3490} 3491} 3492return matches; 3493} 3494function findStampTypeSelect() { 3495var labels = document.querySelectorAll('.product-options label, .product-options-wrapper label, dt label, .field label, label'); 3496for (var i = 0; i < labels.length; i++) { 3497var lbl = labels[i]; 3498if (!STAMP_TYPE_LABEL_RE.test(lbl.textContent)) continue; 3499var forId = lbl.getAttribute('for'); 3500if (forId) { 3501var byId = document.getElementById(forId); 3502if (byId && byId.tagName === 'SELECT') return byId; 3503} 3504var container = lbl.closest && (lbl.closest('.field, dd, dt, li, .input-box, .product-options li')) || lbl.parentElement; 3505if (container) { 3506var sel = container.querySelector('select'); 3507if (sel) return sel; 3508} 3509} 3510var selects = document.querySelectorAll('.product-options select, .product-options-wrapper select'); 3511for (var k = 0; k < selects.length; k++) { 3512for (var m = 0; m < selects[k].options.length; m++) { 3513if (findTierForText(selects[k].options[m].text)) return selects[k]; 3514} 3515} 3516return null; 3517} 3518function getCurrentTier(selectEl) { 3519if (!selectEl || selectEl.selectedIndex < 0) return null; 3520var opt = selectEl.options[selectEl.selectedIndex]; 3521var tier = findTierForText(opt.text); 3522return tier ? { tier: tier, optionEl: opt } : null; 3523} 3524// Scan the dropdown -- return which tiers are actually on THIS product. 3525// Keys: tier1, tier2, pre_inked, professional 3526function getAvailableTiers(selectEl) { 3527var available = {}; 3528for (var i = 0; i < selectEl.options.length; i++) { 3529var t = findTierForText(selectEl.options[i].text); 3530if (!t) continue; 3531if (t.subtype) available[t.subtype] = t; 3532else available['tier' + t.tier] = t; 3533} 3534return available; 3535} 3536// Available options trump path preference. Preferred T3 first; fall back 3537// to the other T3 before bailing. 3538function getNextTier(currentTier, upsellPath, availableTiers) { 3539if (!currentTier) return null; 3540if (currentTier.tier >= 3) return null; 3541if (currentTier.tier === 1) { 3542return availableTiers.tier2 || null; 3543} 3544if (currentTier.tier === 2) {
3545var preferred = upsellPath === 'professional' ? 'professional' : 'pre_inked'; 3546var fallback = upsellPath === 'professional' ? 'pre_inked' : 'professional'; 3547return availableTiers[preferred] || availableTiers[fallback] || null; 3548} 3549return null; 3550} 3551function findOptionInSelect(selectEl, tierDef) { 3552for (var i = 0; i < selectEl.options.length; i++) { 3553if (findTierForText(selectEl.options[i].text) === tierDef) return selectEl.options[i]; 3554} 3555return null; 3556} 3557function parsePriceModifier(text) { 3558if (!text) return 0; 3559var m = text.match(/[+\-]?\s*\$\s*([\d,]+\.\d{2})/); 3560return m ? parseFloat(m[1].replace(/,/g, '')) : 0; 3561} 3562function calcPriceDelta(currentOpt, upgradeOpt) { 3563var cur = parsePriceModifier(currentOpt.text); 3564var up = parsePriceModifier(upgradeOpt.text); 3565return Math.max(0, up - cur); 3566} 3567function getFormKey() { 3568if (window.FORM_KEY) return window.FORM_KEY; 3569var input = document.querySelector('input[name="form_key"]'); 3570return input ? input.value : ''; 3571} 3572function getOptionIdFromSelectName(selectName) { 3573var m = (selectName || '').match(/options\[(\d+)\]/); 3574return m ? m[1] : null; 3575} 3576function detectUpsellPath() { 3577var explicit = document.querySelector('[data-upsell-path]'); 3578if (explicit) { 3579var val = explicit.getAttribute('data-upsell-path'); 3580log('upsell_path from data attribute:', val); 3581return val; 3582} 3583var hay = ''; 3584hay += ' ' + (document.body.className || ''); 3585var crumbs = document.querySelectorAll('.breadcrumbs a, .breadcrumbs li, nav[aria-label*="readcrumb"] a, nav[aria-label*="readcrumb"] li'); 3586for (var i = 0; i < crumbs.length; i++) hay += ' ' + (crumbs[i].textContent || ''); 3587var h1 = document.querySelector('.product-name h1, h1.product-name, h1[itemprop="name"], h1'); 3588if (h1) hay += ' ' + (h1.textContent || ''); 3589hay = hay.toLowerCase(); 3590// v1.31 -- flipped: check for pre-inked signals (logo/brand focus). 3591// If any pre-inked keyword matches, route to pre_inked. Otherwise 3592// default to professional (aligns with our target category mix). 3593for (var k = 0; k < PRE_INKED_KEYWORDS.length; k++) { 3594if (hay.indexOf(PRE_INKED_KEYWORDS[k]) !== -1) { 3595log('upsell_path = pre_inked (matched keyword:', PRE_INKED_KEYWORDS[k], ')'); 3596return 'pre_inked'; 3597} 3598} 3599log('upsell_path = professional (default)'); 3600return 'professional'; 3601} 3602// Check if the current product's category breadcrumb passes the 3603// TARGET_CATEGORIES / EXCLUDED_CATEGORIES filters. Returns true if the 3604// upsell modal should fire for this product. 3605function isCategoryAllowed(categoryBreadcrumb) { 3606var crumb = String(categoryBreadcrumb || '').toLowerCase(); 3607// Exclusions always win 3608for (var i = 0; i < EXCLUDED_CATEGORIES.length; i++) { 3609var ex = String(EXCLUDED_CATEGORIES[i]).toLowerCase(); 3610if (ex && crumb.indexOf(ex) !== -1) { 3611log('Category excluded by rule:', EXCLUDED_CATEGORIES[i]); 3612return false; 3613} 3614} 3615// If no targets defined, allow all 3616if (TARGET_CATEGORIES.length === 0) return true; 3617// Targets defined -- must match at least one 3618for (var j = 0; j < TARGET_CATEGORIES.length; j++) { 3619var tg = String(TARGET_CATEGORIES[j]).toLowerCase(); 3620if (tg && crumb.indexOf(tg) !== -1) return true; 3621} 3622log('Category not in TARGET_CATEGORIES list -- skipping'); 3623return false; 3624} 3625function detectCategory() { 3626// v1.32 -- Zephyr renders breadcrumbs as<li><a>...</a></li>. The old 3627// query (".breadcrumbs a, .breadcrumbs li") double-counted every crumb 3628// (e.g. "Address Stamps > Address Stamps > View All > View All"). Collect 3629// ONE entry per crumb (prefer the<li> text, which contains its anchor), 3630// drop Home, and collapse consecutive duplicates. 3631var items = document.querySelectorAll('.breadcrumbs li'); 3632if (!items.length) items = document.querySelectorAll('.breadcrumbs a'); 3633var trail = []; 3634for (var i = 0; i < items.length; i++) { 3635var t = (items[i].textContent || '').replace(/\s+/g, ' ').trim(); 3636if (!t || t.toLowerCase() === 'home') continue; 3637if (trail.length && trail[trail.length - 1].toLowerCase() === t.toLowerCase()) continue; 3638trail.push(t); 3639} 3640return trail.join(' > '); 3641} 3642function track(eventName, payload) { 3643window.dataLayer = window.dataLayer || []; 3644var data = { event: eventName }; 3645for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) data[k] = payload[k]; 3646window.dataLayer.push(data); 3647log('Event:', eventName, payload); 3648} 3649// Resolve the image URL for an upgrade tier. Uses subtype for tier 3 3650// (pre_inked / professional) and 'self-inking' for tier 2. 3651function getTierImage(tierDef) { 3652if (!tierDef) return ''; 3653var key = tierDef.subtype ? tierDef.subtype 3654: (tierDef.tier === 2 ? 'self-inking' : null); 3655if (!key) return ''; 3656return TIER_IMAGES[key] || ''; 3657} 3658function buildModal(ctx) { 3659var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype; 3660var copy = COPY[key]; 3661if (!copy) return null; 3662// Resolve variant config (passed via ctx so buildModal stays pure) 3663var vCfg = ctx.variantCfg || {};
3664var overlay = document.createElement('div'); 3665overlay.className = 'ss-upsell-overlay'; 3666overlay.setAttribute('role', 'presentation'); 3667var bulletHtml = copy.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join(''); 3668var imgUrl = getTierImage(ctx.next); 3669var imgHtml = imgUrl 3670? '<div class="ss-upsell-image"><img src="' + imgUrl + '" alt="' + escHtml(ctx.next.name) + ' stamp"></div>' 3671: ''; 3672// === Variant-aware elements === 3673// v1.31 -- "Recommended upgrade" chip/badge removed from Step 1 render. 3674// The badge variables + CSS classes remain in the file (dead code) in 3675// case a future variant wants to bring back a chip. To re-enable, add 3676// badgeHtml back into the innerHTML template below. 3677var badgeText = vCfg.badgeText || 'Recommended upgrade'; 3678var badgeClass = vCfg.badgeClass || ''; 3679var badgeHtml = '<div class="ss-upsell-badge ' + badgeClass + '">' + escHtml(badgeText) + '</div>'; 3680void badgeHtml; // suppress unused warning; kept for potential future re-enable 3681// Discount banner (variant B only) 3682var discountBannerHtml = vCfg.discountBanner 3683? '<div class="ss-upsell-discount-banner">' + vCfg.discountBanner + '</div>' 3684: ''; 3685// Price line -- strikethrough + discounted price for variant C 3686var priceLineHtml; 3687if (vCfg.discountPct && ctx.priceDelta > 0) { 3688var discounted = (ctx.priceDelta * (1 - vCfg.discountPct / 100)).toFixed(2); 3689priceLineHtml = 3690'<p class="ss-upsell-price">' + 3691'Upgrade for ' + 3692'<span class="ss-upsell-price-strike">+$' + ctx.priceDelta.toFixed(2) + '</span>' + 3693'<strong>+$' + discounted + '</strong>' + 3694'</p>'; 3695} else { 3696priceLineHtml = '<p class="ss-upsell-price">Upgrade for <strong>+$' + ctx.priceDelta.toFixed(2) + '</strong></p>'; 3697} 3698// Cost-per-use line (variant B + C) 3699var costPerUseHtml = ''; 3700if (vCfg.showCostPerUse && ctx.priceDelta > 0) { 3701var impressions = ctx.next.subtype === 'pre_inked' ? 50000 : (ctx.next.tier === 2 ? 10000 : 100000); 3702var costPerUse = (ctx.priceDelta / impressions).toFixed(4); 3703costPerUseHtml = '<div class="ss-upsell-cost-per-use">~ $' + costPerUse + ' per impression over the life of this stamp</div>'; 3704} 3705// Trust signal (variant B + C) 3706var trustHtml = ''; 3707if (vCfg.trustSignal) { 3708var t = vCfg.trustSignal; 3709var parts = []; 3710if (t.ratingValue && t.ratingCount) { 3711parts.push('<span class="ss-stars">★★★★★</span> ' + escHtml(t.ratingValue) + ' from ' + escHtml(t.ratingCount) + ' reviews'); 3712} 3713if (t.returnPolicy) parts.push(escHtml(t.returnPolicy)); 3714if (parts.length) trustHtml = '<div class="ss-upsell-trust">' + parts.join(' | ') + '</div>'; 3715} 3716// v1.31 -- optional headline + subhead. If headline is empty (e.g., the 3717// Wood Handle -> Self-Inking transition until copy is finalized), 3718// skip the<h2> so the subhead reads clean on its own. 3719var headlineHtml = copy.headline 3720? '<h2 id="ss-upsell-h">' + escHtml(copy.headline) + '</h2>' 3721: ''; 3722var subheadHtml = copy.subhead 3723? '<p class="ss-upsell-subhead">' + escHtml(copy.subhead) + '</p>' 3724: ''; 3725overlay.innerHTML = 3726'<div class="ss-upsell-modal" role="dialog" aria-modal="true"' + 3727(copy.headline ? ' aria-labelledby="ss-upsell-h"' : '') + '>' + 3728'<button class="ss-upsell-close" aria-label="Close">×</button>' + 3729// v1.31 -- badge removed from render 3730headlineHtml + 3731subheadHtml + 3732imgHtml + 3733discountBannerHtml + 3734'<ul class="ss-upsell-bullets">' + bulletHtml + '</ul>' + 3735priceLineHtml + 3736costPerUseHtml + 3737trustHtml + 3738'<div class="ss-upsell-actions">' + 3739'<button class="ss-upsell-accept" type="button">Upgrade my stamp</button>' + 3740'<button class="ss-upsell-decline" type="button">No thanks</button>' + 3741'</div>' + 3742'</div>'; 3743return overlay; 3744} 3745function showModal(ctx) { 3746var overlay = buildModal(ctx); 3747if (!overlay) { log('No copy template for transition -- skipping'); return; } 3748document.body.appendChild(overlay); 3749var accept = overlay.querySelector('.ss-upsell-accept'); 3750var decline = overlay.querySelector('.ss-upsell-decline'); 3751var close = overlay.querySelector('.ss-upsell-close'); 3752// v1.32b -- once the user has Accepted, step-1 dismissal must be dead. 3753// Otherwise (Zephyr AJAX path, no navigation) an outside/backdrop click 3754// during the success-state dwell would fire onDecline -> doSubmit and add 3755// the item a SECOND time alongside the success timer's submit. 3756var accepted = false;
3757function dismiss(method) { 3758if (accepted) return; 3759if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 3760document.removeEventListener('keydown', escHandler); 3761ctx.onDecline(method); 3762} 3763function escHandler(e) { if (e.key === 'Escape') dismiss('escape'); } 3764document.addEventListener('keydown', escHandler); 3765accept.addEventListener('click', function () { 3766// Hand the overlay to onAccept -- it decides whether to chain into the 3767// config step, the success state, or simply close. 3768if (accepted) return; // v1.32b -- ignore double-click on Upgrade 3769accepted = true; 3770document.removeEventListener('keydown', escHandler); 3771ctx.onAccept(overlay); 3772}); 3773decline.addEventListener('click', function () { dismiss('decline_button'); }); 3774close.addEventListener('click', function () { dismiss('close_button'); }); 3775overlay.addEventListener('click', function (e) { if (e.target === overlay) dismiss('outside_click'); }); 3776setTimeout(function () { accept.focus(); }, 50); 3777} 3778// Pick a sane default value for a required field that has no current 3779// value selected. Prefers options labeled "No" (avoids accidentally 3780// adding paid extras like "Yes - +$5.00") over the first non-empty. 3781function autoDefaultFor(field) { 3782if (!field || !field.choices) return ''; 3783if (field.currentValue && String(field.currentValue).trim() !== '') return field.currentValue; 3784if (!field.required) return field.currentValue || ''; 3785// Pass 1: prefer an option whose text starts with "no" 3786for (var i = 0; i < field.choices.length; i++) { 3787var c = field.choices[i]; 3788if (!c.value || c.disabled) continue; 3789if (/^\s*no\b/i.test(c.text)) return c.value; 3790} 3791// Pass 2: first non-empty, non-disabled option 3792for (var j = 0; j < field.choices.length; j++) { 3793var c2 = field.choices[j]; 3794if (c2.value && !c2.disabled) return c2.value; 3795} 3796return ''; 3797} 3798// Step 2: render the config step inside the existing overlay so the 3799// user can review/adjust their other custom options for the upgrade. 3800function showConfigStep(overlay, ctx, fields, onConfirm, onCancel) { 3801var fieldsHtml = fields.map(function (field, i) { 3802var req = field.required ? ' <span class="ss-config-required">*</span>' : ''; 3803var effectiveValue = autoDefaultFor(field); 3804if (field.type === 'select') { 3805var optHtml = field.choices.map(function (c) { 3806var sel = c.value === effectiveValue ? ' selected' : ''; 3807var dis = c.disabled ? ' disabled' : ''; 3808return '<option value="' + escHtml(c.value) + '"' + sel + dis + '>' + escHtml(c.text) + '</option>'; 3809}).join(''); 3810return '<div class="ss-opt-field">' + 3811'<label>' + escHtml(field.label) + req + '</label>' + 3812'<select data-field-idx="' + i + '">' + optHtml + '</select>' + 3813'</div>'; 3814} 3815if (field.type === 'radio') { 3816var radioHtml = field.choices.map(function (c) { 3817var ch = c.value === effectiveValue ? ' checked' : ''; 3818return '<label class="ss-opt-radio">' + 3819'<input type="radio" name="ss-radio-' + i + '" value="' + escHtml(c.value) + '"' + ch + '> ' +
3820escHtml(c.text) + 3821'</label>'; 3822}).join(''); 3823return '<div class="ss-opt-field" data-field-idx="' + i + '">' + 3824'<div class="ss-opt-group-label">' + escHtml(field.label) + req + '</div>' + 3825radioHtml + 3826'</div>'; 3827} 3828if (field.type === 'text') { 3829var maxAttr = field.maxlength ? ' maxlength="' + field.maxlength + '"' : ''; 3830return '<div class="ss-opt-field">' + 3831'<label>' + escHtml(field.label) + req + '</label>' + 3832'<input type="text" data-field-idx="' + i + '" value="' + escHtml(field.currentValue) + '"' + maxAttr + '>' + 3833'</div>'; 3834} 3835if (field.type === 'textarea') { 3836return '<div class="ss-opt-field">' + 3837'<label>' + escHtml(field.label) + req + '</label>' + 3838'<textarea data-field-idx="' + i + '" rows="3">' + escHtml(field.currentValue) + '</textarea>' + 3839'</div>'; 3840} 3841return ''; 3842}).join(''); 3843// v1.31 -- config-step badge removed to match Step 1 chip removal. 3844// Cancel button now inherits the small-link style (ss-config-cancel 3845// modifier retained for JS querySelector but no longer overrides CSS). 3846overlay.innerHTML = 3847'<div class="ss-upsell-modal" role="dialog" aria-modal="true" aria-labelledby="ss-config-h">' + 3848'<button class="ss-upsell-close" aria-label="Cancel">×</button>' + 3849'<h2 id="ss-config-h">Review your options</h2>' + 3850'<p class="ss-config-intro">Confirm the selections for your upgraded stamp.</p>' + 3851'<div class="ss-config-fields">' + fieldsHtml + '</div>' + 3852'<div class="ss-upsell-actions">' + 3853'<button class="ss-upsell-accept" type="button">Confirm upgrade</button>' + 3854'<button class="ss-upsell-decline ss-config-cancel" type="button">Cancel upgrade</button>' + 3855'</div>' + 3856'</div>'; 3857var confirmBtn = overlay.querySelector('.ss-upsell-accept'); 3858var cancelBtn = overlay.querySelector('.ss-config-cancel'); 3859var closeBtn = overlay.querySelector('.ss-upsell-close'); 3860// v1.32b -- guard against a double-click on Confirm firing two submits 3861// (the Zephyr AJAX add doesn't navigate, so a second one really adds twice). 3862var confirmed = false; 3863confirmBtn.addEventListener('click', function () { 3864if (confirmed) return; 3865var values = {}; 3866var missing = []; 3867fields.forEach(function (field, i) { 3868var val = null; 3869if (field.type === 'select') { 3870var s = overlay.querySelector('select[data-field-idx="' + i + '"]'); 3871if (s) val = s.value; 3872} else if (field.type === 'radio') { 3873var r = overlay.querySelector('[data-field-idx="' + i + '"] input[type="radio"]:checked'); 3874if (r) val = r.value; 3875} else if (field.type === 'text' || field.type === 'textarea') { 3876var ti = overlay.querySelector('[data-field-idx="' + i + '"]'); 3877if (ti) val = ti.value; 3878} 3879if (val != null) values[field.name] = val; 3880if (field.required && (val == null || String(val).trim() === '')) { 3881missing.push(i); 3882} 3883}); 3884// Clear prior error styling 3885var oldErrs = overlay.querySelectorAll('.ss-opt-field-error'); 3886for (var e = 0; e < oldErrs.length; e++) oldErrs[e].classList.remove('ss-opt-field-error'); 3887var oldMsgs = overlay.querySelectorAll('.ss-opt-error'); 3888for (var m = 0; m < oldMsgs.length; m++) oldMsgs[m].parentNode && oldMsgs[m].parentNode.removeChild(oldMsgs[m]); 3889// Mark missing fields and bail if any 3890if (missing.length > 0) { 3891missing.forEach(function (idx, n) { 3892var el = overlay.querySelector('[data-field-idx="' + idx + '"]'); 3893if (!el) return; 3894var fieldEl = el.closest('.ss-opt-field') || el.parentElement; 3895if (!fieldEl) return; 3896fieldEl.classList.add('ss-opt-field-error'); 3897var msg = document.createElement('div'); 3898msg.className = 'ss-opt-error'; 3899msg.textContent = 'Please make a selection'; 3900fieldEl.appendChild(msg); 3901if (n === 0) { 3902try { fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) { fieldEl.scrollIntoView(); } 3903} 3904}); 3905track('upsell_config_validation_failed', { missing_count: missing.length }); 3906return; 3907} 3908confirmed = true; // v1.32b -- lock after a valid confirm 3909onConfirm(values); 3910}); 3911cancelBtn.addEventListener('click', function () { if (confirmed) return; onCancel('cancel_button'); }); 3912closeBtn.addEventListener('click', function () { if (confirmed) return; onCancel('close_button'); }); 3913setTimeout(function () { confirmBtn.focus(); }, 50); 3914} 3915// Replace the modal contents with a celebratory success state. After 3916// SUCCESS_DWELL_MS, run ctx.onSuccessComplete (which triggers the submit). 3917function showSuccessState(overlay, ctx) { 3918var key = ctx.current.tier.tier === 1 ? '1->self-inking' : '2->' + ctx.next.subtype; 3919var copy = SUCCESS_COPY[key] || { headline: 'Upgrade applied', tagline: 'Your stamp has been upgraded.' }; 3920overlay.innerHTML = 3921// v1.31 -- "Premium upgrade applied" chip removed to match Step 1 + Step 2. 3922// Green checkmark icon carries the celebration signal on its own now. 3923'<div class="ss-upsell-modal ss-upsell-success" role="dialog" aria-modal="true" aria-labelledby="ss-upsell-success-h">' + 3924'<div class="ss-upsell-success-icon" aria-hidden="true">✓</div>' + 3925'<h2 id="ss-upsell-success-h">' + copy.headline + '</h2>' + 3926'<p class="ss-upsell-success-tagline">
3926' + copy.tagline + '</p>' + 3927'<p class="ss-upsell-success-cta"><span class="ss-upsell-spinner"></span> Continuing to your cart...</p>' + 3928'<button class="ss-upsell-success-skip" type="button">Continue now</button>' + 3929'</div>'; 3930var skipBtn = overlay.querySelector('.ss-upsell-success-skip'); 3931var done = false; 3932function finalize() { 3933if (done) return; 3934done = true; 3935// Intentionally DO NOT remove the overlay here. We want the success 3936// state to remain visible while the browser navigates to the cart. 3937// The page transition will replace the modal naturally -- no flicker 3938// of bare PDP between success state and cart load. 3939// 3940// v1.32 note: on Zephyr the AJAX add opens the cart drawer rather than 3941// navigating away, so the success state is removed by handleAjaxAddToCart 3942// /the drawer taking over. It still dwells for SUCCESS_DWELL_MS first. 3943if (typeof ctx.onSuccessComplete === 'function') ctx.onSuccessComplete(); 3944} 3945skipBtn.addEventListener('click', finalize); 3946setTimeout(finalize, SUCCESS_DWELL_MS); 3947} 3948// ============================================================ 3949// CONFIG STEP -- extract / sync custom options 3950// ============================================================ 3951function escHtml(s) { 3952return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 3953} 3954function isElementHidden(el) { 3955if (!el) return true; 3956if (el.type === 'hidden') return true; 3957// Walk up the ancestor chain checking each computed style. 3958// Catches display:none, visibility:hidden, opacity:0 on the element 3959// OR any of its ancestors up to the form. 3960var node = el; 3961while (node && node !== document && node !== document.body) { 3962var s; 3963try { s = window.getComputedStyle(node); } catch (e) { s = null; } 3964if (s) { 3965if (s.display === 'none') return true; 3966if (s.visibility === 'hidden') return true; 3967if (parseFloat(s.opacity) === 0) return true; 3968} 3969node = node.parentElement; 3970} 3971// Final fallback -- offsetParent null catches some edge cases 3972if (el.offsetParent === null && el.tagName !== 'BODY') { 3973var rect = el.getBoundingClientRect(); 3974if (rect.width === 0 && rect.height === 0) return true; 3975} 3976return false; 3977} 3978function cleanLabel(s) { 3979return String(s || '').replace(/\*/g, '').replace(/:\s*$/, '').replace(/\s+/g, ' ').trim(); 3980} 3981function findLabelForFormField(el) { 3982if (!el) return 'Option'; 3983if (el.id) { 3984var byFor = document.querySelector('label[for="' + el.id + '"]'); 3985if (byFor) return cleanLabel(byFor.textContent); 3986} 3987// Magento default: previous DT contains the label 3988var dd = el.closest && el.closest('dd'); 3989if (dd) { 3990var prevDt = dd.previousElementSibling; 3991if (prevDt && prevDt.tagName === 'DT') { 3992var lblInDt = prevDt.querySelector('label') || prevDt; 3993return cleanLabel(lblInDt.textContent); 3994} 3995} 3996var field = el.closest && el.closest('.field, .input-box, li'); 3997if (field) { 3998var inLbl = field.querySelector('label.label, label'); 3999if (inLbl) return cleanLabel(inLbl.textContent); 4000} 4001return 'Option'; 4002} 4003// Pull the form's other custom option fields so we can render them in 4004// the config step. Skips the Stamp Type dropdown and any hidden fields. 4005function extractCustomOptions(form, excludeSelectEl) { 4006var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 4007var fields = []; 4008// Selects 4009var selects = container.querySelectorAll('select[name^="options"]'); 4010for (var i = 0; i < selects.length; i++) { 4011var sel = selects[i]; 4012if (sel === excludeSelectEl) continue; 4013if (isElementHidden(sel)) continue; 4014var choices = []; 4015for (var j = 0; j < sel.options.length; j++) { 4016var opt = sel.options[j]; 4017choices.push({ value: opt.value, text: opt.text, disabled: opt.disabled }); 4018} 4019fields.push({ 4020type: 'select', 4021name: sel.name, 4022label: findLabelForFormField(sel), 4023currentValue: sel.value, 4024required: sel.hasAttribute('required') || sel.classList.contains('required-entry') || sel.classList.contains('validate-select'), 4025choices: choices 4026}); 4027} 4028// Text inputs + textareas (stamp text, line 1, line 2, etc.) 4029// Gated by INCLUDE_TEXT_FIELDS_IN_CONFIG so the modal stays focused on 4030// tier-relevant choices. Text values still carry to the cart untouched. 4031if (INCLUDE_TEXT_FIELDS_IN_CONFIG) { 4032var texts = container.querySelectorAll('input[type="text"][name^="options"], textarea[name^="options"]'); 4033for (var t = 0; t < texts.length; t++) { 4034var inp = texts[t]; 4035if (isElementHidden(inp)) continue; 4036fields.push({ 4037type: inp.tagName === 'TEXTAREA' ? 'textarea' : 'text', 4038name: inp.name, 4039label: findLabelForFormField(inp), 4040currentValue: inp.value, 4041required: inp.hasAttribute('required') || inp.classList.contains('required-entry'), 4042maxlength: inp.maxLength > 0 ? inp.maxLength : null 4043}); 4044} 4045} 4046// Radio groups 4047var radios = container.querySelectorAll('input[type="radio"][name^="options"]'); 4048var groups = {}; 4049for (var k = 0; k < radios.length; k++) { 4050var r = radios[k]; 4051if (isElementHidden(r)) continue; 4052if (!groups[r.name]) groups[r.name] = []; 4053groups[r.name].push(r); 4054} 4055for (var name in groups) { 4056if (!Object.prototype.hasOwnProperty.call(groups, name)) continue; 4057var grp = groups[name]; 4058var checked = null; 4059var rChoices = []; 4060for (var m = 0; m < grp.length; m++) { 4061if (grp[m].checked) checked = grp[m]; 4062rChoices.push({ value: grp[m].value, text: findLabelForFormField(grp[m]) || grp[m].value }); 4063} 4064fields.push({ 4065type: 'radio', 4066name: name, 4067label: findLabelForFormField(grp[0]), 4068currentValue: checked ? checked.value : '', 4069required: grp[0].classList.contains('validate-one-required-by-name'), 4070choices: rChoices 4071}); 4072} 4073return fields; 4074} 4075// Normalize a tier definition into a key for TIER_OPTION_FILTERS. 4076// Self-Inking -> 'self_inking' 4077// Pre-Inked -> 'pre_inked' (from subtype) 4078// Professional-> 'professional' 4079function tierFilterKey(tierDef) { 4080if (!tierDef) return null; 4081if (tierDef.subtype) return tierDef.subtype; 4082return String(tierDef.name || '').toLowerCase().replace(/[\s\-]+/g, '_'); 4083} 4084// Filter custom option fields to those that apply to the upgrade target. 4085// Logic: 4086// 1. Override `hide` list (if any) drops matches first. 4087// 2. Auto-detect tier mentions in label: 4088// - mentions target tier name -> SHOW 4089// - mentions a different tier's name -> HIDE 4090// - mentions no tier -> fall through to step 3 4091// 3. Generic (no tier in label) options: 4092// - if override `show` is set, only labels matching survive 4093// - otherwise, show 4094function filterFieldsForTier(fields, tierDef) { 4095if (!tierDef) return fields; 4096var override = TIER_OPTION_FILTERS[tierFilterKey(tierDef)] || {}; 4097var overrideShow = (override.show || []).map(function (s) { return String(s).toLowerCase(); }); 4098var overrideHide = (override.hide || []).map(function (s) { return String(s).toLowerCase(); }); 4099return fields.filter(function (field) { 4100var label = String(field.label || '').toLowerCase(); 4101// Manual hide override wins 4102if (overrideHide.length && overrideHide.some(function (kw) { return label.indexOf(kw) !== -1; })) { 4103return false;
4104} 4105// Auto-detect tier tags in the label 4106var tiersInLabel = findAllTiersInText(label); 4107var mentionsTarget = tiersInLabel.indexOf(tierDef) !== -1; 4108var mentionsOther = tiersInLabel.some(function (t) { return t !== tierDef; }); 4109if (mentionsTarget) return true; // explicitly tagged for our tier 4110if (mentionsOther) return false; // explicitly tagged for a different tier 4111// Generic (no tier tag): apply manual show list if present 4112if (overrideShow.length > 0) { 4113return overrideShow.some(function (kw) { return label.indexOf(kw) !== -1; }); 4114} 4115return true; 4116}); 4117} 4118// Set the value of a single select-or-multiselect element. For multi- 4119// selects (name ends with "[]"), we must iterate options and set 4120// .selected explicitly -- assigning .value alone is unreliable across 4121// browsers and may leave stale selections active. 4122function setSelectValue(selectEl, targetValue) { 4123if (!selectEl) return; 4124if (selectEl.multiple) { 4125for (var i = 0; i < selectEl.options.length; i++) { 4126selectEl.options[i].selected = (selectEl.options[i].value === targetValue); 4127} 4128} else { 4129selectEl.value = targetValue; 4130} 4131} 4132// Cascade auto-fill: themes that use progressive reveal only show the 4133// next dropdown after the previous one has a value. After we swap 4134// Stamp Type, we may need to fill Size before Ink Color appears, fill 4135// Ink Color before Extra Bottle appears, etc. 4136// 4137// Strategy: find any required dropdown that is visible AND empty, 4138// pick a sane default ("No" if present, else first non-empty option), 4139// dispatch change, wait, repeat until DOM stops revealing new fields. 4140function cascadeAutoFill(form, stampSelect, callback, iter, maxIter) { 4141iter = iter || 0; 4142maxIter = maxIter || 10; 4143if (iter >= maxIter) { log('Cascade hit max iterations (' + maxIter + ')'); callback(); return; } 4144var container = form.querySelector('.product-options, .product-options-wrapper') || form; 4145var selects = container.querySelectorAll('select[name^="options"]'); 4146var changedAny = false; 4147for (var i = 0; i < selects.length; i++) { 4148var sel = selects[i]; 4149if (sel === stampSelect) continue; 4150if (sel.offsetParent === null) continue; // skip currently-hidden 4151if (sel.value && String(sel.value).trim() !== '') continue; // already filled 4152var isRequired = sel.hasAttribute('required') || 4153sel.classList.contains('required-entry') || 4154sel.classList.contains('validate-select'); 4155if (!isRequired) continue; 4156// Pick a sane default -- "No" preferred to avoid paid extras 4157var defaultVal = ''; 4158for (var j = 0; j < sel.options.length; j++) { 4159var opt = sel.options[j]; 4160if (!opt.value || opt.disabled) continue; 4161if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; } 4162if (!defaultVal) defaultVal = opt.value; 4163} 4164if (defaultVal) { 4165log('Cascade fill: ' + sel.name + ' = ' + defaultVal); 4166sel.value = defaultVal; 4167try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4168changedAny = true; 4169} 4170} 4171if (!changedAny) { log('Cascade stable at iter ' + iter); callback(); return; } 4172setTimeout(function () { cascadeAutoFill(form, stampSelect, callback, iter + 1, maxIter); }, 300); 4173} 4174// Safety-net pass -- runs RIGHT BEFORE form submit. Iterates one final time 4175// through every visible required empty select/radio and fills with defaults. 4176// Catches any field that cascade missed because the theme revealed it after 4177// cascade exited (chained progressive reveals with long settle times). 4178// Returns the count of fields it filled (for logging). 4179function safetyNetFill(form, stampSelect) { 4180var filled = 0; 4181var container = form.querySelector('.product-options, .product-options-wrapper') || form; 4182// Selects 4183var selects = container.querySelectorAll('select[name^="options"]'); 4184for (var i = 0; i < selects.length; i++) { 4185var sel = selects[i]; 4186if (sel === stampSelect) continue; 4187if (sel.offsetParent === null) continue; 4188if (sel.value && String(sel.value).trim() !== '') continue; 4189var isRequired = sel.hasAttribute('required') || 4190sel.classList.contains('required-entry') || 4191sel.classList.contains('validate-select'); 4192if (!isRequired) continue; 4193var defaultVal = ''; 4194for (var j = 0; j < sel.options.length; j++) { 4195var opt = sel.options[j]; 4196if (!opt.value || opt.disabled) continue; 4197if (/^\s*no\b/i.test(opt.text)) { defaultVal = opt.value; break; } 4198if (!defaultVal) defaultVal = opt.value; 4199} 4200if (defaultVal) { 4201log('Safety-net fill: ' + sel.name + ' = ' + defaultVal); 4202sel.value = defaultVal; 4203try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4204filled++; 4205} 4206} 4207// Radio groups 4208var radios = container.querySelectorAll('input[type="radio"][name^="options"]'); 4209var groups = {}; 4210for (var k = 0; k < radios.length; k++) { 4211var r = radios[k]; 4212if (r.offsetParent === null) continue; 4213if (!groups[r.name]) groups[r.name] = []; 4214groups[r.name].push(r); 4215} 4216for (var name in groups) { 4217if (!Object.prototype.hasOwnProperty.call(groups, name)) continue; 4218var grp = groups[name]; 4219var hasChecked = false;
4220for (var m = 0; m < grp.length; m++) { if (grp[m].checked) { hasChecked = true; break; } } 4221if (hasChecked) continue; 4222var isReq = grp[0].classList.contains('validate-one-required-by-name'); 4223if (!isReq) continue; 4224// Pick first non-disabled radio (or one with "no" label) 4225var pick = null; 4226for (var p = 0; p < grp.length; p++) { 4227if (grp[p].disabled) continue; 4228var lbl = findLabelForFormField(grp[p]) || ''; 4229if (/^\s*no\b/i.test(lbl)) { pick = grp[p]; break; } 4230if (!pick) pick = grp[p]; 4231} 4232if (pick) { 4233log('Safety-net fill (radio): ' + name + ' = ' + pick.value); 4234pick.checked = true; 4235try { pick.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4236filled++; 4237} 4238} 4239return filled; 4240} 4241// v1.26 -- Carry-forward: capture user's pre-upgrade visible dropdown 4242// selections (Ink Color, Size, Ink Pad, Extra Ink, etc.) by category. 4243// These are re-applied after cascade so the user's original choice 4244// survives the tier swap, even when the theme reveals a different 4245// dropdown for the new tier (e.g., product 4378: Self-Inking exposes 4246// options[7505] "Ink Color" but Professional exposes options[7504] 4247// "Ink Colors" -- same colors, different option IDs). 4248function categorizeLabel(label) { 4249var s = String(label || '').toLowerCase(); 4250if (/ink\s*colou?rs?/.test(s)) return 'ink_color'; 4251if (/replacement\s*pad|ink\s*pad|extra.*pad/.test(s)) return 'ink_pad'; 4252if (/extra\s*ink|extra.*bottle|bottle.*ink/.test(s)) return 'extra_ink'; 4253if (/size/.test(s)) return 'size'; 4254// Fallback -- use full lowercased label as its own category 4255return 'other::' + s.trim(); 4256} 4257function captureUserSelections(form, stampSelect) { 4258var captured = {}; 4259var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 4260var selects = container.querySelectorAll('select[name^="options"]'); 4261for (var i = 0; i < selects.length; i++) { 4262var sel = selects[i]; 4263if (sel === stampSelect) continue; 4264if (isElementHidden(sel)) continue; 4265if (!sel.value || String(sel.value).trim() === '') continue; 4266var selectedOpt = sel.options[sel.selectedIndex]; 4267if (!selectedOpt) continue; 4268var selectedText = String(selectedOpt.text || '').trim(); 4269if (!selectedText) continue; 4270var label = findLabelForFormField(sel) || ''; 4271var category = categorizeLabel(label); 4272// First-wins -- if two dropdowns share a category (rare), keep the first 4273if (!captured[category]) { 4274captured[category] = { text: selectedText, sourceLabel: label, sourceName: sel.name }; 4275} 4276} 4277return captured; 4278} 4279function applyCarriedForward(form, stampSelect, captured) { 4280var applied = 0; 4281if (!captured) return 0; 4282var container = form.querySelector('.product-options, .product-options-wrapper, dl.product-options') || form; 4283var selects = container.querySelectorAll('select[name^="options"]'); 4284for (var i = 0; i < selects.length; i++) { 4285var sel = selects[i]; 4286if (sel === stampSelect) continue; 4287if (isElementHidden(sel)) continue; 4288var label = findLabelForFormField(sel) || ''; 4289var category = categorizeLabel(label); 4290var wanted = captured[category]; 4291if (!wanted) continue; 4292var currentText = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : ''; 4293// If already matches (e.g., cascade happened to pick right), skip 4294if (String(currentText).trim().toLowerCase() === wanted.text.toLowerCase()) continue; 4295// Find option whose text matches (case-insensitive, trimmed) 4296var matchOpt = null; 4297for (var j = 0; j < sel.options.length; j++) { 4298var opt = sel.options[j]; 4299if (!opt.value || opt.disabled) continue; 4300// Strip price suffix like " +$5.00" for match tolerance 4301var optText = String(opt.text || '').replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim(); 4302if (optText.toLowerCase() === wanted.text.replace(/\s*[+\-]?\$[\d.,]+.*$/, '').trim().toLowerCase()) { 4303matchOpt = opt; 4304break; 4305} 4306} 4307if (matchOpt) { 4308log('Carry-forward: ' + sel.name + ' (' + category + ') <- "' + wanted.text + '" from ' + wanted.sourceName); 4309sel.value = matchOpt.value; 4310try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4311applied++; 4312} else { 4313log('Carry-forward: no match for "' + wanted.text + '" in ' + sel.name + ' (' + category + ') -- cascade default stays'); 4314} 4315} 4316return applied; 4317} 4318function syncValuesToForm(form, values, fields) { 4319fields.forEach(function (field) { 4320var v = values[field.name]; 4321if (v == null) return; 4322if (field.type === 'select') { 4323var sel = form.querySelector('select[name="' + field.name + '"]'); 4324if (sel) { 4325sel.value = v; 4326try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4327} 4328} else if (field.type === 'radio') { 4329var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]'); 4330for (var i = 0; i < rs.length; i++) { 4331rs[i].checked = (rs[i].value === v); 4332if (rs[i].checked) { 4333try { rs[i].dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4334} 4335} 4336} else if (field.type === 'text' || field.type === 'textarea') { 4337var ti = form.querySelector('[name="' + field.name + '"]'); 4338if (ti) { 4339ti.value = v; 4340try { 4341ti.dispatchEvent(new Event('input', { bubbles: true })); 4342ti.dispatchEvent(new Event('change', { bubbles: true })); 4343} catch (e) {} 4344} 4345} 4346}); 4347}
4348function init() { 4349var form = document.getElementById('product_addtocart_form'); 4350if (!form) { log('Not a PDP (no ATC form) -- skipping init'); return; } 4351var stampSelect = findStampTypeSelect(); 4352if (!stampSelect) { log('No Stamp Type dropdown -- skipping init'); return; } 4353var upsellPath = detectUpsellPath(); 4354var productInput = form.querySelector('input[name="product"]'); 4355var productId = productInput ? productInput.value : ''; 4356var skuMeta = document.querySelector('meta[itemprop="sku"], [itemprop="sku"]'); 4357var productSku = skuMeta ? (skuMeta.getAttribute('content') || skuMeta.textContent || '').trim() : ''; 4358var category = detectCategory(); 4359var variant = getAssignedVariant(); 4360var variantCfg = getVariantConfig(variant); 4361log('Init', { upsellPath: upsellPath, productId: productId, category: category, variant: variant }); 4362function buildContext(cartItemId) { 4363// No-badger: if user already accepted an upgrade this session, skip. 4364if (hasUpgradedThisSession()) { log('buildContext: user already upgraded this session -- skipping'); return null; } 4365// Decline cap: if user has declined too many times this session, respect that and skip. 4366if (hasDeclinedTooMany()) { log('buildContext: decline cap reached (' + getDeclineCount() + '/' + MAX_DECLINES_PER_SESSION + ') -- skipping'); return null; } 4367// Category targeting: skip if this product's category isn't allowed. 4368if (!isCategoryAllowed(category)) { log('buildContext: category not in target/excluded by rule'); return null; } 4369var current = getCurrentTier(stampSelect); 4370if (!current) { log('buildContext: no current tier detected'); return null; } 4371var availableTiers = getAvailableTiers(stampSelect); 4372log('buildContext: available tiers on this product:', Object.keys(availableTiers)); 4373var next = getNextTier(current.tier, upsellPath, availableTiers); 4374if (!next) { log('buildContext: no upgrade tier (top tier or no upgrade options on product)'); return null; } 4375var upgradeOpt = findOptionInSelect(stampSelect, next); 4376if (!upgradeOpt) { log('buildContext: upgrade option vanished from dropdown'); return null; } 4377log('buildContext: ' + current.tier.name + ' -> ' + next.name); 4378return { 4379current: current, next: next, upgradeOpt: upgradeOpt, 4380priceDelta: calcPriceDelta(current.optionEl, upgradeOpt), 4381cartItemId: cartItemId, 4382productId: productId, productSku: productSku, category: category, upsellPath: upsellPath 4383}; 4384} 4385function mergeParams(a, b) { 4386var out = {}; for (var k in a) out[k] = a[k]; for (var k2 in b) out[k2] = b[k2]; return out; 4387} 4388// ---------------------------------------------------------- 4389// ATC interception -- v1.2 PRE-ADD flow: 4390// Show modal IMMEDIATELY on submit (no AJAX wait). 4391// On accept: change the Stamp Type dropdown to the upgrade value, 4392// then call the original submit. One cart write, right SKU. 4393// On decline: just call the original submit unchanged. 4394// 4395// v1.32 -- Zephyr's primary path is window.handleAjaxAddToCart (see 4396// patchAjaxHandler below). The Varien patch + form 'submit' listener 4397// are kept as fallbacks for any legacy/native path. 4398// 1. Patch window.handleAjaxAddToCart (Zephyr AJAX ATC) -- PRIMARY 4399// 2. Patch window.productAddToCartForm.submit (Varien) -- fallback 4400// 3. Fallback: form 'submit' event listener (non-Varien themes) 4401// ---------------------------------------------------------- 4402function buildBaseParams(ctx) { 4403return { 4404from_tier: ctx.current.tier.name, 4405to_tier: ctx.next.name, 4406upsell_path: ctx.upsellPath, 4407product_id: ctx.productId, 4408product_sku: ctx.productSku, 4409category: ctx.category, 4410price_delta: ctx.priceDelta, 4411variant: variant 4412}; 4413} 4414function showUpsellAndSubmit(ctx, doSubmit) { 4415// v1.32b -- never stack two modals if the ATC button is double-clicked. 4416if (document.querySelector('.ss-upsell-overlay')) { log('Modal already open -- ignoring duplicate trigger'); return; } 4417// v1.32b -- the underlying submit (native AJAX add on Zephyr) must run at 4418// most ONCE per flow. The old code relied on a full-page navigation to 4419// make a stray second call harmless; the AJAX add has no navigation, so 4420// we guard it explicitly. onDecline / onCancel / onSuccessComplete all 4421// route through this wrapper. 4422var _rawSubmit = doSubmit, _submitted = false;
4423doSubmit = function () { if (_submitted) return; _submitted = true; _rawSubmit(); }; 4424var baseParams = buildBaseParams(ctx); 4425var originalStampValue = ctx.current.optionEl.value; 4426track('upsell_modal_shown', baseParams); 4427// v1.28 -- Fire discount-side telemetry when variant B modal renders 4428// WITH a coupon code available for the target transition. Signals 4429// "user was exposed to the discount incentive" and lets B-only 4430// reports build without filtering upsell_modal_shown by variant. 4431if (variant === 'B') { 4432var shownCode = resolveCouponCode(variantCfg, ctx.next); 4433if (shownCode) { 4434track('upsell_discount_shown', mergeParams(baseParams, { coupon_code: shownCode })); 4435} 4436} 4437function chainToSuccessAndSubmit(overlay, userPicks, userFields, carriedForward) { 4438showSuccessState(overlay, { 4439current: ctx.current, 4440next: ctx.next, 4441onSuccessComplete: function () { 4442// Final safety net: catch any required field the cascade missed 4443// (e.g., chained progressive reveals where the theme exposes 4444// fields after our cascade exited). Without this, Magento 4445// backend validation can reject the submit and leave the user 4446// stuck on the PDP after the success state closes. 4447var filled = safetyNetFill(form, stampSelect); 4448if (filled > 0) log('Safety net filled ' + filled + ' field(s) before submit'); 4449// v1.27 -- Re-apply carry-forward AFTER safety net. When the 4450// user changes Size in the modal, the theme cascade can reveal 4451// a NEW Ink Color dropdown that our initial carry-forward 4452// (which ran pre-modal) never touched. Safety net just filled 4453// it with the default (Black). Now we override that default 4454// with the user's original pre-upgrade pick (Red) if we find 4455// a match by category + text. 4456if (carriedForward) { 4457var reCarried = applyCarriedForward(form, stampSelect, carriedForward); 4458if (reCarried > 0) log('Late carry-forward: ' + reCarried + ' field(s) restored to original pick'); 4459} 4460// CRITICAL: Re-apply user's explicit picks LAST. The theme's 4461// progressive reveal cascade can reset dependent options (e.g., 4462// Ink Color resets when Stamp Type/Size change). Without this 4463// final re-sync, the safety net's defaults overwrite the user's 4464// intended color/option picks, leading to "I picked Red but cart 4465// shows Black" bugs. Direct value writes only -- no change event 4466// dispatch -- to avoid re-triggering the theme's cascade. 4467if (userPicks && userFields) { 4468var reapplied = 0; 4469userFields.forEach(function (field) { 4470var v = userPicks[field.name]; 4471if (v == null || v === '') return; 4472if (field.type === 'select') { 4473var sel = form.querySelector('select[name="' + field.name + '"]'); 4474if (sel && String(sel.value) !== String(v)) { 4475log('Re-applying user pick: ' + field.name + ' ' + sel.value + ' -> ' + v); 4476sel.value = v; 4477reapplied++; 4478} 4479} else if (field.type === 'radio') { 4480var rs = form.querySelectorAll('input[type="radio"][name="' + field.name + '"]'); 4481for (var i = 0; i < rs.length; i++) { 4482var shouldCheck = (rs[i].value === v); 4483if (rs[i].checked !== shouldCheck) { 4484rs[i].checked = shouldCheck; 4485if (shouldCheck) reapplied++; 4486} 4487} 4488} else if (field.type === 'text' || field.type === 'textarea') { 4489var ti = form.querySelector('[name="' + field.name + '"]'); 4490if (ti && ti.value !== v) { 4491log('Re-applying user pick: ' + field.name + ' = ' + v); 4492ti.value = v; 4493reapplied++; 4494} 4495} 4496}); 4497if (reapplied > 0) log('Re-applied ' + reapplied + ' user pick(s) before submit'); 4498} 4499log('Success state complete -- submitting'); 4500doSubmit(); 4501} 4502}); 4503} 4504showModal({ 4505current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4506variantCfg: variantCfg, 4507onAccept: function (overlay) { 4508track('upsell_accepted', baseParams); 4509// v1.26 -- Capture user's pre-upgrade visible selections BEFORE 4510// Stamp Type swap wipes visibility (Ink Color, Size, etc.). 4511// These get carried forward to whichever dropdowns the new tier 4512// reveals, matched by category + option text (e.g., "Red"). 4513var carriedForward = captureUserSelections(form, stampSelect); 4514log('Captured for carry-forward:', carriedForward); 4515// Multiselect-safe Stamp Type swap (handles options[1508][] style) 4516setSelectValue(stampSelect, ctx.upgradeOpt.value); 4517try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4518log('Stamp Type swapped to:', ctx.upgradeOpt.text); 4519// Cascade auto-fill (themes with progressive reveal only show the 4520// next dropdown after the previous has a value). Then extract. 4521setTimeout(function () { 4522cascadeAutoFill(form, stampSelect, function () { 4523// v1.26 -- Apply carry-forward BEFORE extraction so the modal 4524// shows user's original picks as pre-selected values. User 4525// can still override in the modal if they want to change. 4526var carriedCount = applyCarriedForward(form, stampSelect, carriedForward); 4527if (carriedCount > 0) log('Carried forward ' + carriedCount + ' user selection(s) to new tier'); 4528var allFields = extractCustomOptions(form, stampSelect); 4529var filtered = filterFieldsForTier(allFields, ctx.next); 4530// Dedupe by label -- products sometimes have multiple custom 4531// options with identical labels (one per tier variant). Without 4532// tier hints in the label we can't distinguish them, so keep 4533// the first and drop the rest. 4534var seenLabels = {}; 4535var fields = filtered.filter(function (f) { 4536var key = String(f.label || '').toLowerCase().trim(); 4537if (!key) return true; 4538if (seenLabels[key]) return false;
4539seenLabels[key] = true; 4540return true; 4541}); 4542log('Extracted ' + allFields.length + ' option(s), ' + 4543filtered.length + ' apply to ' + ctx.next.name + ', ' + 4544fields.length + ' after dedupe'); 4545// Read qty from the form (used in revenue_delta calc later) 4546var qtyInput = form.querySelector('input[name="qty"]'); 4547var qty = qtyInput ? qtyInput.value : 1; 4548if (fields.length === 0) { 4549// No other options to configure -- straight to success 4550markUpgradedThisSession(); 4551track('upsell_confirmed', mergeParams(baseParams, { config_skipped: true })); 4552recordUpgradeForPurchaseTracking(baseParams, qty); 4553var skipCode = resolveCouponCode(variantCfg, ctx.next); 4554if (skipCode) { 4555log('Applying coupon for transition -> ' + ctx.next.name + ': ' + skipCode); 4556applyVariantCDiscount(skipCode, baseParams); 4557} 4558chainToSuccessAndSubmit(overlay, null, null, carriedForward); 4559return; 4560} 4561showConfigStep(overlay, ctx, fields, 4562function onConfirm(values) { 4563track('upsell_confirmed', mergeParams(baseParams, { config_field_count: fields.length })); 4564markUpgradedThisSession(); 4565syncValuesToForm(form, values, fields); 4566recordUpgradeForPurchaseTracking(baseParams, qty); 4567var confirmCode = resolveCouponCode(variantCfg, ctx.next); 4568if (confirmCode) { 4569log('Applying coupon for transition -> ' + ctx.next.name + ': ' + confirmCode); 4570applyVariantCDiscount(confirmCode, baseParams); 4571} 4572// Pass user picks + fields so they get re-applied AFTER 4573// safetyNetFill, defeating theme cascade resets. Also pass 4574// carriedForward for v1.27's late-cascade rescue: if user 4575// changed Size in modal and theme revealed a NEW Ink Color 4576// dropdown, restore original pre-upgrade pick. 4577chainToSuccessAndSubmit(overlay, values, fields, carriedForward); 4578}, 4579function onCancel(method) { 4580incrementDeclineCount();
4581track('upsell_canceled_at_config', mergeParams(baseParams, { cancel_method: method, decline_count: getDeclineCount() })); 4582// Revert Stamp Type to original -- user bailed at config 4583setSelectValue(stampSelect, originalStampValue); 4584try { stampSelect.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} 4585if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 4586log('Canceled at config -- count:', getDeclineCount(), '-- submitting original selection'); 4587doSubmit(); 4588} 4589); 4590}); // end cascadeAutoFill callback 4591}, 100); 4592}, 4593onDecline: function (method) { 4594incrementDeclineCount(); 4595track('upsell_declined', mergeParams(baseParams, { dismiss_method: method, decline_count: getDeclineCount() })); 4596log('Declined (' + method + ') -- count:', getDeclineCount(), '-- submitting original selection'); 4597doSubmit(); 4598} 4599}); 4600} 4601// v1.32 -- PRIMARY Zephyr interception. The ATC button on Zephyr is 4602// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // so it never reaches Varien's submit or a native form submit event. 4603// We wrap the global handler: validate (Varien validator still exists), 4604// build the upsell context, and if an upgrade is available show the modal 4605// first. The original AJAX add runs only after accept/decline, by which 4606// point the Stamp Type has been swapped (on accept). handleAjaxAddToCart 4607// re-serializes the form when it runs, so the upgraded selection is added. 4608function patchAjaxHandler() { 4609var fn = window.handleAjaxAddToCart; 4610if (typeof fn !== 'function') return false; 4611if (fn._ssTierPatched) return true; 4612var orig = fn; 4613window.handleAjaxAddToCart = function (button) { 4614var args = arguments; 4615function proceed() { return orig.apply(window, args); } 4616// Respect Magento/Varien validation first. handleAjaxAddToCart runs 4617// the same validator internally; if the form isn't valid, hand off to 4618// the original so it surfaces the field errors (and we don't pop the 4619// modal on an incomplete form). 4620try { 4621var paf = window.productAddToCartForm; 4622if (paf && paf.validator && typeof paf.validator.validate === 'function' && !paf.validator.validate()) { 4623log('Validator failed -- passing through to native AJAX add'); 4624return proceed(); 4625} 4626} catch (e) {} 4627var ctx = buildContext(null); 4628if (!ctx) { log('No upsell context -- native AJAX add'); return proceed(); } 4629log('Upsell available -- showing modal before AJAX add (Zephyr path)'); 4630showUpsellAndSubmit(ctx, function () { 4631// Run the native AJAX add, then clean up. Unlike the old Varien 4632// path this does NOT navigate, so WE must remove the success 4633// overlay (otherwise its full-screen fixed layer covers the page 4634// forever) and apply any stashed variant-B coupon now that the 4635// item is in the cart (the drawer never loads /checkout/cart). 4636var r; 4637try { r = proceed(); } catch (e) { r = null; } 4638var _done = false; 4639var done = function () { 4640if (_done) return; _done = true; // idempotent -- promise OR timeout, whichever first 4641var o = document.querySelector('.ss-upsell-overlay'); 4642if (o && o.parentNode) o.parentNode.removeChild(o); 4643try { applyPendingCouponNow(); } catch (e) {} 4644}; 4645var thenable = r && typeof r.then === 'function'; 4646if (thenable) { r.then(done, done); } 4647// Always arm a max-timeout too: if the add promise is non-thenable 4648// (short delay) OR never settles (6s hang guard), we still remove the 4649// overlay so it can't cover the page or suppress future modals. 4650setTimeout(done, thenable ? 6000 : 1200); 4651}); 4652return false; // block the immediate add; proceed() runs after the modal resolves 4653}; 4654window.handleAjaxAddToCart._ssTierPatched = true; 4655log('Patched window.handleAjaxAddToCart (Zephyr AJAX ATC)'); 4656return true; 4657} 4658(function pollForAjaxHandler(attempts) { 4659attempts = attempts || 0; 4660if (patchAjaxHandler()) return; 4661if (attempts >= 30) { log('Gave up polling for handleAjaxAddToCart after 3s'); return; } 4662setTimeout(function () { pollForAjaxHandler(attempts + 1); }, 100); 4663})(); 4664function patchVarienForm() { 4665var paf = window.productAddToCartForm; 4666if (!paf || typeof paf.submit !== 'function' || paf._ssTierPatched) return false; 4667var origSubmit = paf.submit.bind(paf); 4668paf.submit = function (button, url) { 4669if (this.validator && typeof this.validator.validate === 'function' && !this.validator.validate()) { 4670log('Magento validator failed -- letting native submit handle the error UI'); 4671return origSubmit(button, url); 4672} 4673var ctx = buildContext(null); 4674if (!ctx) { 4675log('No upsell context -- proceeding with native ATC'); 4676return origSubmit(button, url); 4677} 4678log('Upsell available -- showing modal pre-add'); 4679showUpsellAndSubmit(ctx, function () { origSubmit(button, url); }); 4680}; 4681paf._ssTierPatched = true; 4682log('Patched window.productAddToCartForm.submit'); 4683return true; 4684} 4685(function pollForVarien(attempts) { 4686attempts = attempts || 0; 4687if (patchVarienForm()) return; 4688if (attempts >= 30) { log('Gave up polling for productAddToCartForm after 3s'); return; } 4689setTimeout(function () { pollForVarien(attempts + 1); }, 100); 4690})(); 4691// Fallback for non-Varien / Enter-key submission 4692form.addEventListener('submit', function (e) { 4693if (form._ssBypass) { form._ssBypass = false; return; } // we already showed modal 4694if (window.productAddToCartForm && window.productAddToCartForm._ssTierPatched) return; 4695// If the Zephyr AJAX handler is patched, it owns the flow -- don't
4696// double-fire the modal from a native submit that may also occur. 4697if (window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched) return; 4698var ctx = buildContext(null); 4699if (!ctx) return; 4700e.preventDefault(); 4701log('Submit event intercepted (fallback path) -- showing modal pre-add'); 4702showUpsellAndSubmit(ctx, function () { 4703form._ssBypass = true; 4704form.submit(); 4705}); 4706}); 4707window.SSUpsell = { 4708showTestModal: function () { 4709var ctx = buildContext('TEST-CART-ITEM'); 4710if (!ctx) { alert('No upsell available. Current: ' + (stampSelect.options[stampSelect.selectedIndex] || {}).text); return; } 4711var baseParams = { from_tier: ctx.current.tier.name, to_tier: ctx.next.name, upsell_path: ctx.upsellPath, product_id: ctx.productId, price_delta: ctx.priceDelta }; 4712track('upsell_modal_shown', baseParams); 4713showModal({ 4714current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4715variantCfg: variantCfg, 4716onAccept: function (overlay) { 4717track('upsell_accepted', baseParams); 4718if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); 4719alert('Accept (test mode -- no cart write)'); 4720},
4721onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); } 4722}); 4723}, 4724inspect: function () { 4725var ctx = buildContext(null); 4726var available = getAvailableTiers(stampSelect); 4727var currentTierLive = getCurrentTier(stampSelect); 4728// Dry-run the config-step extraction + filter so you can see what 4729// the user WOULD see if they accepted right now. 4730var optsPreview = null; 4731if (ctx) { 4732var all = extractCustomOptions(form, stampSelect); 4733var kept = filterFieldsForTier(all, ctx.next); 4734var seenL = {}; 4735var deduped = kept.filter(function (f) { 4736var k = String(f.label || '').toLowerCase().trim(); 4737if (!k) return true; 4738if (seenL[k]) return false; 4739seenL[k] = true; 4740return true; 4741}); 4742optsPreview = { 4743extracted: all.map(function (f) { return f.label; }), 4744keptAfterFilter: kept.map(function (f) { return f.label; }), 4745shownToUser: deduped.map(function (f) { return f.label; }), 4746filterRule: TIER_OPTION_FILTERS[tierFilterKey(ctx.next)] || '(none -- show all)' 4747}; 4748} 4749return { 4750upsellPath: upsellPath, 4751productId: productId, 4752productSku: productSku, 4753category: category, 4754currentTier: currentTierLive ? currentTierLive.tier.name : null, 4755nextTier: ctx ? ctx.next.name : null, 4756priceDelta: ctx ? ctx.priceDelta : null, 4757availableOnProduct: Object.keys(available), 4758ajaxHandlerPatched: !!(window.handleAjaxAddToCart && window.handleAjaxAddToCart._ssTierPatched), 4759ajaxHandlerPresent: typeof window.handleAjaxAddToCart === 'function', 4760varienFormPatched: !!(window.productAddToCartForm && window.productAddToCartForm._ssTierPatched), 4761varienFormPresent: !!window.productAddToCartForm, 4762alreadyUpgradedThisSession: hasUpgradedThisSession(), 4763declineCount: getDeclineCount(), 4764maxDeclinesPerSession: MAX_DECLINES_PER_SESSION, 4765declineCapReached: hasDeclinedTooMany(), 4766variant: variant, 4767variantConfig: variantCfg, 4768devModeAlwaysShow: DEV_MODE_ALWAYS_SHOW, 4769categoryAllowed: isCategoryAllowed(category), 4770targetCategories: TARGET_CATEGORIES, 4771excludedCategories: EXCLUDED_CATEGORIES, 4772configStepPreview: optsPreview 4773}; 4774}, 4775// QA helper -- clear the session flags (upgrade lock + decline count) so the modal fires again 4776resetSession: function () { 4777try { 4778sessionStorage.removeItem(SESSION_ACCEPTED_KEY); 4779sessionStorage.removeItem(SESSION_DECLINE_KEY); 4780} catch (e) {} 4781console.log('[TierUpsell] Session reset -- modal will fire on next ATC'); 4782}, 4783// QA helpers for variant testing 4784setVariant: function (v) { 4785v = String(v || '').toUpperCase(); 4786if (VARIANTS.indexOf(v) === -1) { 4787console.warn('Invalid variant. Use one of:', VARIANTS); 4788return; 4789} 4790try { localStorage.setItem(VARIANT_STORAGE_KEY, v); } catch (e) {} 4791console.log('[TierUpsell] Variant set to:', v, '-- refresh the page to see changes'); 4792}, 4793clearVariant: function () { 4794try { localStorage.removeItem(VARIANT_STORAGE_KEY); } catch (e) {} 4795console.log('[TierUpsell] Variant cleared -- next page load will assign a new one'); 4796}, 4797simulateATC: function () { 4798var ctx = buildContext('TEST-CART-ITEM'); 4799if (!ctx) { alert('No upsell available right now.'); return; } 4800var baseParams = { 4801from_tier: ctx.current.tier.name, to_tier: ctx.next.name, 4802upsell_path: ctx.upsellPath, product_id: ctx.productId, 4803product_sku: ctx.productSku, category: ctx.category, price_delta: ctx.priceDelta 4804}; 4805track('upsell_modal_shown', baseParams); 4806showModal({ 4807current: ctx.current, next: ctx.next, priceDelta: ctx.priceDelta, 4808variantCfg: variantCfg, 4809onAccept: function (overlay) { 4810track('upsell_accepted', baseParams); 4811if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); 4812console.log('[TierUpsell] Would update cart now (simulate mode)'); 4813},
4814onDecline: function (m) { track('upsell_declined', mergeParams(baseParams, { dismiss_method: m })); console.log('[TierUpsell] Declined (simulate mode):', m); } 4815}); 4816} 4817}; 4818} 4819if (document.readyState === 'loading') { 4820document.addEventListener('DOMContentLoaded', init); 4821} else { 4822init(); 4823} 4824})(); </script>
4824</div><div class="widget widget-static-block">
4824<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","
4824sort_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","
4824sort_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","
4824notary 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","
4824sort_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","
4824sort_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","
4824sort_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
4824sender","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","
4824sort_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","
4824sort_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","
4824sort_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","
4824sort_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","
4824sort_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","
4824sort_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","
4824sort_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>
4824</div><div class="widget widget-static-block"> <style> /* ========================================================= 4825Scoped to #cart-kit-upsell-mount to avoid site collisions 4826========================================================= */ 4827#cart-kit-upsell-mount { 4828--ck-accent: var(--brand-primary, #1976d2); 4829--ck-accent-hover: var(--brand-primary-hover, #60B5E5); 4830--ck-success: var(--brand-success, #2e7d32); 4831--ck-success-hover: #256628; 4832} 4833#cart-kit-upsell-mount .cart-kit-upsell { 4834margin: 24px 0; 4835border-radius: 8px; 4836box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); 4837background: #ffffff; 4838border: 1px solid #e3e3e3; 4839overflow: visible; 4840font-family: inherit; 4841position: relative; 4842} 4843#cart-kit-upsell-mount .cart-kit-upsell__header { 4844width: 100%; 4845text-align: left; 4846border: 0; 4847padding: 12px 16px; 4848background: #f5fafc; 4849display: flex; 4850align-items: center; 4851justify-content: flex-start; 4852gap: 12px; 4853cursor: pointer; 4854border-radius: 8px; 4855} 4856#cart-kit-upsell-mount .cart-kit-upsell__header:focus { 4857outline: 2px solid var(--ck-accent); 4858outline-offset: -2px; 4859} 4860#cart-kit-upsell-mount .cart-kit-upsell__header-text { 4861display: flex; 4862flex-direction: column; 4863gap: 4px; 4864flex: 1 1 auto; 4865} 4866#cart-kit-upsell-mount .cart-kit-upsell__title { 4867font-size: 20px; 4868font-weight: 600; 4869color: #222; 4870} 4871#cart-kit-upsell-mount .cart-kit-upsell__chevron { 4872width: 10px; 4873height: 10px; 4874border-right: 2px solid #555; 4875border-bottom: 2px solid #555; 4876transform: rotate(45deg); 4877transition: transform 0.2s ease; 4878} 4879#cart-kit-upsell-mount .cart-kit-upsell--open .cart-kit-upsell__chevron { 4880transform: rotate(-135deg); 4881} 4882#cart-kit-upsell-mount .cart-kit-upsell__body { 4883padding: 12px 16px 14px; 4884border-top: 1px solid #e3e3e3; 4885animation: cartKitUpsellFadeIn 0.18s ease-out; 4886} 4887@keyframes cartKitUpsellFadeIn { 4888from { 4889opacity: 0; 4890transform: translateY(-2px); 4891} 4892to { 4893opacity: 1; 4894transform: translateY(0); 4895} 4896} 4897/* Icon zone */ 4898#cart-kit-upsell-mount .cart-kit-upsell__icon-wrap { 4899flex: 0 0 auto; 4900width: auto; 4901height: 36px; 4902display: flex; 4903align-items: center; 4904justify-content: center; 4905overflow: visible; 4906} 4907#cart-kit-upsell-mount .cart-kit-upsell__icon-img { 4908width: 70px; 4909height: auto; 4910display: block; 4911transform: none; 4912opacity: 1; 4913transition: none; 4914} 4915/* Options */ 4916#cart-kit-upsell-mount .cart-kit-upsell__options-group { 4917display: flex; 4918flex-direction: column; 4919gap: 10px; 4920} 4921#cart-kit-upsell-mount .cart-kit-upsell__options-label { 4922font-size: 16px; 4923font-weight: 600; 4924color: #333; 4925} 4926#cart-kit-upsell-mount .cart-kit-upsell__options-header { 4927display: flex; 4928align-items: center; 4929gap: 6px; 4930margin-bottom: 4px; 4931overflow: visible !important; 4932} 4933/* Tooltip */ 4934#cart-kit-upsell-mount .cart-kit-upsell__info { 4935position: relative; 4936border: 0; 4937padding: 0; 4938background: none; 4939cursor: pointer; 4940display: inline-flex; 4941z-index: 5; 4942} 4943#cart-kit-upsell-mount .cart-kit-upsell__info-icon { 4944width: 18px; 4945height: 18px; 4946border-radius: 999px; 4947border: 1px solid #888; 4948font-size: 11px; 4949font-weight: 600; 4950display: flex; 4951align-items: center; 4952justify-content: center; 4953color: #555; 4954background: #fff; 4955line-height: 1; 4956} 4957#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip { 4958position: absolute; 4959left: 50%; 4960bottom: calc(100% + 8px); 4961transform: translateX(-50%); 4962background: #222; 4963color: #fff; 4964font-size: 13px; 4965padding: 6px 8px; 4966border-radius: 4px; 4967box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); 4968opacity: 0; 4969pointer-events: none; 4970transition: opacity .15s ease, transform .15s ease; 4971white-space: normal; 4972max-width: min(260px, 80vw); 4973width: 180px; 4974line-height: 1.2; 4975z-index: 9999; 4976} 4977#cart-kit-upsell-mount .cart-kit-upsell__info-tooltip::after { 4978content: ''; 4979position: absolute; 4980top: 100%; 4981left: 50%; 4982transform: translateX(-50%); 4983border-width: 6px; 4984border-style: solid; 4985border-color: #222 transparent transparent transparent; 4986} 4987#cart-kit-upsell-mount .cart-kit-upsell__info:hover .cart-kit-upsell__info-tooltip, 4988#cart-kit-upsell-mount .cart-kit-upsell__info:focus-visible .cart-kit-upsell__info-tooltip { 4989opacity: 1; 4990transform: translateX(-50%) translateY(-2px); 4991pointer-events: auto; 4992} 4993/* ========================================= 4994Grid (Accent stamps OR Notary product tiles) 4995========================================= */ 4996/* Default (Accent chips) */ 4997#cart-kit-upsell-mount .kit-stamp-grid { 4998display: flex; 4999flex-wrap: wrap; 5000gap: 16px; 5001align-items: stretch; 5002} 5003#cart-kit-upsell-mount .kit-stamp-card { 5004border: none; 5005background: transparent; 5006padding: 0 4px 4px; 5007margin: 0; 5008cursor: pointer; 5009outline: none; 5010flex: 0 0 auto; 5011} 5012/* Accent chip circle */ 5013#cart-kit-upsell-mount .kit-stamp-chip { 5014display: flex; 5015flex-direction: column; 5016align-items: center; 5017text-align: center; 5018max-width: 200px; 5019} 5020#cart-kit-upsell-mount .kit-stamp-chip__circle { 5021width: 200px; 5022height: auto; 5023border-radius: 16px; 5024background: #f3fbff; 5025box-shadow: 0 0 0 1px #e1f0fb; 5026display: flex; 5027align-items: center; 5028justify-content: center; 5029margin-bottom: 6px; 5030transition: box-shadow .15s ease, transform .1s ease, background .15s ease; 5031overflow: clip; 5032} 5033#cart-kit-upsell-mount .kit-stamp-chip__image { 5034width: 100%; 5035height: auto; 5036display: block; 5037} 5038#cart-kit-upsell-mount .kit-stamp-card:hover .kit-stamp-chip__circle { 5039box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.25); 5040transform: translateY(-1px); 5041} 5042#cart-kit-upsell-mount .kit-stamp-card.is-selected .kit-stamp-chip__circle { 5043background: #e6f2ff; 5044box-shadow: 0 0 0 2px var(--ck-accent); 5045} 5046/* ========================================= 5047Notary tile layout (only when .is-notary-grid is applied) 5048========================================= */ 5049#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 5050display: inline-grid; 5051grid-template-columns: repeat(6, minmax(0, 1fr)); 5052gap: 16px; 5053align-items: stretch; 5054} 5055@media (max-width: 900px) { 5056#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 5057grid-template-columns: repeat(4, minmax(0, 1fr)); 5058} 5059} 5060@media (max-width: 768px) { 5061#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 5062grid-template-columns: repeat(3, minmax(0, 1fr)); 5063} 5064} 5065@media (max-width: 520px) {
5066#cart-kit-upsell-mount .cart-kit-upsell__header-text { 5067width: 50%; 5068} 5069/* Swipeable tiles on mobile */ 5070#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 5071display: flex; 5072overflow-x: auto; 5073gap: 12px; 5074scroll-snap-type: x mandatory; 5075-webkit-overflow-scrolling: touch; 5076padding-bottom: 8px; 5077padding-top: 4px; 5078padding-left: 4px; 5079padding-right: 4px; 5080} 5081/* Each tile becomes a "slide" */ 5082#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid .kit-stamp-card.kit-product-card { 5083flex: 0 0 78%; 5084scroll-snap-align: start; 5085} 5086/* Hide scrollbar (nice-to-have) */ 5087#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid::-webkit-scrollbar { 5088display: none; 5089} 5090#cart-kit-upsell-mount .kit-stamp-grid.is-notary-grid { 5091scrollbar-width: none; 5092} 5093/* Target the actual tile grid container */ 5094#cart-kit-upsell-mount .kit-stamp-grid { 5095display: flex !important; 5096flex-wrap: nowrap !important; 5097overflow-x: auto !important; 5098overflow-y: hidden !important; 5099gap: 12px !important; 5100scroll-snap-type: x mandatory; 5101-webkit-overflow-scrolling: touch; 5102padding-bottom: 10px; 5103} 5104/* Make each tile act like a slide */ 5105#cart-kit-upsell-mount .kit-stamp-grid>* { 5106flex: 0 0 78% !important; 5107/* change to 60% to show ~2 at a time */ 5108scroll-snap-align: start; 5109} 5110/* Optional: hide scrollbar */ 5111#cart-kit-upsell-mount .kit-stamp-grid::-webkit-scrollbar {
5112display: none; 5113} 5114#cart-kit-upsell-mount .kit-stamp-grid { 5115scrollbar-width: none; 5116} 5117#cart-kit-upsell-mount, 5118#cart-kit-upsell-mount .kit-upsell-inner, 5119#cart-kit-upsell-mount .kit-upsell-body { 5120overflow: visible !important; 5121} 5122} 5123/* Notary tile card */ 5124#cart-kit-upsell-mount .kit-stamp-card.kit-product-card { 5125background: #fff; 5126border: 1px solid #e1f0fb; 5127border-radius: 12px; 5128padding: 12px; 5129text-align: left; 5130flex: 1 1 auto; 5131transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, background .12s ease; 5132} 5133#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:hover { 5134transform: translateY(-1px); 5135border-color: rgba(25, 118, 210, 0.35); 5136box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); 5137} 5138#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.ck-swapping { 5139opacity: 0; 5140transition: opacity .18s ease; 5141} 5142#cart-kit-upsell-mount .kit-empty-tiles { 5143padding: 18px; 5144border: 1px dashed #d9d9d9; 5145border-radius: 10px; 5146margin-top: 10px; 5147font-size: 14px; 5148color: #444; 5149background: #fafafa; 5150} 5151/* Pool exhaustion: completion tile */ 5152#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete { 5153background: #f6faf7; 5154border: 1px dashed #b6dfc7; 5155cursor: default; 5156} 5157#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-complete:hover { 5158transform: none; 5159border-color: #b6dfc7; 5160box-shadow: none; 5161} 5162#cart-kit-upsell-mount .ck-tile-complete { 5163text-align: center; 5164padding: 16px 10px; 5165} 5166#cart-kit-upsell-mount .ck-tile-complete__icon { 5167font-size: 22px; 5168margin-bottom: 6px; 5169line-height: 1; 5170} 5171#cart-kit-upsell-mount .ck-tile-complete__title { 5172font-weight: 700; 5173margin-bottom: 4px; 5174} 5175#cart-kit-upsell-mount .ck-tile-complete__subtext { 5176font-size: 13px; 5177opacity: 0.85; 5178} 5179#cart-kit-upsell-mount .kit-stamp-card.kit-product-card:focus-visible { 5180outline: 3px solid rgba(25, 118, 210, 0.35); 5181outline-offset: 2px; 5182} 5183#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected { 5184background: #f5f9ff; 5185border-color: var(--ck-accent); 5186box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.14); 5187} 5188/* Notary tile thumb */ 5189#cart-kit-upsell-mount .kit-product-thumb-tile { 5190width: 100%; 5191aspect-ratio: 1 / 1; 5192border-radius: 10px; 5193background: #f3f6fb; 5194border: 1px solid rgba(0, 0, 0, 0.06); 5195overflow: hidden; 5196position: relative; 5197display: flex; 5198align-items: center; 5199justify-content: center; 5200} 5201#cart-kit-upsell-mount .kit-product-thumb-tile img { 5202width: 100%; 5203height: 100%; 5204object-fit: contain; 5205display: block; 5206background-color: #fff; 5207} 5208#cart-kit-upsell-mount .kit-product-meta { 5209margin-top: 10px; 5210} 5211#cart-kit-upsell-mount .kit-product-name { 5212font-weight: 700; 5213font-size: 13px; 5214color: #222; 5215line-height: 1.2; 5216margin: 0; 5217} 5218#cart-kit-upsell-mount .kit-product-price { 5219margin-top: 6px; 5220font-size: 12px; 5221color: #4a4a4a; 5222} 5223#cart-kit-upsell-mount .kit-product-sub { 5224margin-top: 4px; 5225font-size: 11px; 5226color: #6b6b6b; 5227} 5228/* Notary tile selected checkmark */ 5229#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-selected .kit-product-thumb-tile::after { 5230content: "â"; 5231position: absolute; 5232right: 8px; 5233bottom: 8px; 5234width: 22px; 5235height: 22px; 5236border-radius: 999px; 5237background: var(--ck-success); 5238color: #fff; 5239font-size: 13px; 5240font-weight: 900; 5241display: flex; 5242align-items: center; 5243justify-content: center; 5244box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); 5245} 5246/* ========================================= 5247Micro PDP "Added" state (tile grey-out + overlay) 5248========================================= */ 5249#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added { 5250opacity: 0.55; 5251filter: grayscale(0.25); 5252pointer-events: none; 5253/* prevent clicking again */ 5254position: relative; 5255} 5256/* Stronger "Added" overlay */ 5257#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added::before { 5258content: "â Added to cart"; 5259position: absolute; 5260left: 10px; 5261right: 10px; 5262bottom: 10px; 5263padding: 10px 12px; 5264border-radius: 999px; 5265background: rgba(20, 20, 20, .90); 5266color: #fff; 5267font-weight: 800; 5268font-size: 13px; 5269text-align: center; 5270z-index: 6; 5271} 5272#cart-kit-upsell-mount .kit-stamp-card.kit-product-card.is-added img { 5273opacity: 0.75; 5274} 5275/* Module success toast */ 5276#cart-kit-upsell-mount .kit-success-toast { 5277position: absolute; 5278right: 16px; 5279bottom: 16px; 5280background: var(--ck-success-hover); 5281color: #fff; 5282padding: 10px 12px; 5283border-radius: 10px;
5284font-weight: 800; 5285font-size: 13px; 5286max-width: 340px; 5287box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); 5288opacity: 0; 5289transform: translateY(6px); 5290transition: opacity .18s ease, transform .18s ease; 5291pointer-events: none; 5292z-index: 50; 5293} 5294#cart-kit-upsell-mount .kit-success-toast.is-visible { 5295opacity: 1; 5296transform: translateY(0); 5297} 5298/* ========================================= 5299Cart row highlight after Micro PDP add 5300========================================= */ 5301#cart-kit-upsell-mount .ck-cart-highlight { 5302outline: 3px solid rgba(46, 125, 50, 0.35); 5303outline-offset: 4px; 5304border-radius: 8px; 5305animation: ckPulse 1.6s ease-in-out 0s 2; 5306} 5307@keyframes ckPulse { 53080% { 5309box-shadow: 0 0 0 rgba(46, 125, 50, 0.00); 5310} 531150% { 5312box-shadow: 0 0 0 10px rgba(46, 125, 50, 0.10); 5313} 5314100% { 5315box-shadow: 0 0 0 rgba(46, 125, 50, 0.00); 5316} 5317} 5318/* Ink swatches */ 5319#cart-kit-upsell-mount .kit-swatch-list { 5320display: flex; 5321flex-wrap: wrap; 5322gap: 8px; 5323} 5324#cart-kit-upsell-mount .kit-swatch { 5325display: inline-flex; 5326align-items: center; 5327gap: 6px; 5328padding: 4px 8px; 5329border-radius: 999px; 5330border: 1px solid #d8e0ea; 5331background: #fff; 5332cursor: pointer; 5333font-size: 14px; 5334color: #333; 5335transition: border-color .15s ease, box-shadow .15s ease, background .15s ease; 5336} 5337#cart-kit-upsell-mount .kit-swatch:hover { 5338border-color: var(--ck-accent); 5339box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); 5340} 5341#cart-kit-upsell-mount .kit-swatch.is-selected { 5342border-color: var(--ck-accent); 5343background: #f5f9ff; 5344box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.18); 5345} 5346#cart-kit-upsell-mount .kit-swatch__dot { 5347width: 14px; 5348height: 14px; 5349border-radius: 999px; 5350box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15); 5351} 5352#cart-kit-upsell-mount .kit-swatch__dot--black { 5353background: #111; 5354} 5355#cart-kit-upsell-mount .kit-swatch__dot--blue { 5356background: #1565c0; 5357} 5358#cart-kit-upsell-mount .kit-swatch__dot--red { 5359background: #c62828; 5360} 5361#cart-kit-upsell-mount .kit-swatch__dot--green { 5362background: var(--ck-success); 5363} 5364#cart-kit-upsell-mount .kit-swatch__dot--purple { 5365background: #6a1b9a; 5366} 5367#cart-kit-upsell-mount .kit-swatch__dot--none { 5368background: repeating-linear-gradient(45deg, #ccc, #ccc 3px, #eee 3px, #eee 6px); 5369} 5370/* Footer / CTA */ 5371#cart-kit-upsell-mount .cart-kit-upsell__footer { 5372display: flex; 5373flex-wrap: wrap; 5374align-items: center; 5375justify-content: space-between; 5376gap: 8px; 5377margin-top: 18px; 5378padding: 8px; 5379} 5380#cart-kit-upsell-mount .cart-kit-upsell__price { 5381font-size: 16px; 5382color: #222; 5383display: flex; 5384gap: 14px; 5385align-items: center; 5386flex-wrap: wrap; 5387} 5388#cart-kit-upsell-mount .cart-kit-upsell__cta { 5389padding: 8px 16px; 5390border-radius: 4px; 5391border: 0; 5392background: var(--ck-success); 5393color: #fff; 5394font-size: 13px; 5395font-weight: 600; 5396cursor: pointer; 5397white-space: nowrap; 5398} 5399#cart-kit-upsell-mount .cart-kit-upsell__cta:hover { 5400background: var(--ck-success-hover); 5401} 5402#cart-kit-upsell-mount .cart-kit-upsell__cta:disabled { 5403opacity: .6; 5404cursor: default; 5405} 5406#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary { 5407padding: 8px 14px; 5408border-radius: 4px; 5409border: 1px solid #ccc; 5410background: #f7f7f7; 5411color: #333; 5412font-size: 13px; 5413font-weight: 600; 5414cursor: pointer; 5415white-space: nowrap; 5416} 5417#cart-kit-upsell-mount .cart-kit-upsell__cta-secondary:hover { 5418background: #eee; 5419} 5420/* Spinner on Add button */ 5421#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading { 5422position: relative; 5423padding-right: 30px; 5424} 5425#cart-kit-upsell-mount .cart-kit-upsell__cta.is-loading::after { 5426content: ''; 5427position: absolute; 5428right: 10px; 5429top: 50%; 5430width: 14px; 5431height: 14px; 5432margin-top: -7px; 5433border-radius: 50%; 5434border: 2px solid rgba(255, 255, 255, 0.7); 5435border-top-color: transparent; 5436animation: cartKitSpinner .6s linear infinite; 5437} 5438@keyframes cartKitSpinner { 5439to { 5440transform: rotate(360deg); 5441} 5442} 5443/* Add-to-cart progress bar (the ONLY progress bar in this script) */ 5444#cart-kit-upsell-mount .cart-kit-upsell__progresswrap { 5445margin-top: 10px; 5446} 5447#cart-kit-upsell-mount .cart-kit-upsell__progresstop { 5448display: flex; 5449justify-content: space-between; 5450gap: 12px; 5451margin-bottom: 6px; 5452} 5453#cart-kit-upsell-mount .cart-kit-upsell__progresstext, 5454#cart-kit-upsell-mount .cart-kit-upsell__progresspct { 5455font-size: 12px; 5456color: #555; 5457}
5458#cart-kit-upsell-mount .cart-kit-upsell__progressbar { 5459height: 10px; 5460border-radius: 999px; 5461background: #eef3f6; 5462overflow: hidden; 5463} 5464#cart-kit-upsell-mount .cart-kit-upsell__progressfill { 5465height: 100%; 5466background: var(--ck-success); 5467transition: width .18s ease; 5468width: 0%; 5469} 5470/* Success header state */ 5471#cart-kit-upsell-mount .cart-kit-upsell__header--success { 5472background: #f1faf3; 5473} 5474#cart-kit-upsell-mount .cart-kit-upsell__success-inline { 5475font-size: 14px; 5476color: var(--ck-success); 5477display: flex; 5478align-items: center; 5479gap: 8px; 5480font-weight: 600; 5481} 5482/* ========================================================= 5483Micro PDP Modal (iframe-based) 5484========================================================= */ 5485#cart-kit-upsell-mount .ck-micropdp-overlay { 5486position: fixed; 5487inset: 0; 5488background: rgba(0, 0, 0, .55); 5489display: none; 5490align-items: center; 5491justify-content: center; 5492z-index: 999999; 5493/* above cart + header */ 5494} 5495#cart-kit-upsell-mount .ck-micropdp-overlay.is-open { 5496display: flex; 5497} 5498#cart-kit-upsell-mount .ck-micropdp-modal { 5499width: min(960px, 92vw); 5500height: 96vh; 5501background: #fff; 5502border-radius: 12px; 5503overflow: hidden; 5504box-shadow: 0 12px 40px rgba(0, 0, 0, .35); 5505display: flex; 5506flex-direction: column; 5507} 5508@media (max-width: 600px) { 5509#cart-kit-upsell-mount .ck-micropdp-modal { 5510width: 96vw; 5511height: 98vh; 5512border-radius: 10px; 5513} 5514} 5515#cart-kit-upsell-mount .ck-micropdp-head { 5516display: flex; 5517flex-direction: column; 5518align-items: stretch; 5519justify-content: flex-start; 5520gap: 10px; 5521padding: 12px 14px; 5522border-bottom: 1px solid #e6e6e6; 5523background: #f7fbff; 5524} 5525#cart-kit-upsell-mount .ck-micropdp-head-top { 5526display: flex; 5527align-items: center; 5528justify-content: space-between; 5529gap: 12px; 5530width: 100%; 5531} 5532#cart-kit-upsell-mount .ck-micropdp-progress { 5533width: 100%; 5534display: flex; 5535flex-direction: column; 5536gap: 6px; 5537} 5538#cart-kit-upsell-mount .ck-micropdp-progressbar { 5539width: 100%; 5540height: 8px; 5541background: rgba(25, 118, 210, .14); 5542border-radius: 999px; 5543overflow: hidden; 5544} 5545#cart-kit-upsell-mount .ck-micropdp-progressfill { 5546height: 100%; 5547width: 0%; 5548background: var(--ck-accent); 5549border-radius: 999px; 5550transition: width .25s ease; 5551} 5552#cart-kit-upsell-mount .ck-micropdp-progresstext { 5553font-size: 12px; 5554color: #1b1b1b; 5555opacity: .85; 5556line-height: 1.2; 5557} 5558/* Micro PDP header progress â completion delight */ 5559#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progressfill { 5560background: var(--ck-success); 5561animation: ckMicroFillPulse .8s ease-in-out 0s 2; 5562} 5563#cart-kit-upsell-mount .ck-micropdp-progress.is-complete .ck-micropdp-progresstext { 5564font-weight: 700; 5565opacity: 1; 5566} 5567@keyframes ckMicroFillPulse { 55680% { 5569filter: brightness(1); 5570} 557150% { 5572filter: brightness(1.25); 5573} 5574100% { 5575filter: brightness(1); 5576} 5577} 5578#cart-kit-upsell-mount .ck-micropdp-title { 5579font-size: 16px; 5580font-weight: 700; 5581color: #1b1b1b; 5582margin: 0; 5583} 5584#cart-kit-upsell-mount .ck-micropdp-sub { 5585font-size: 12px; 5586color: #666; 5587margin-top: 2px; 5588} 5589#cart-kit-upsell-mount .ck-micropdp-actions { 5590display: flex; 5591gap: 8px; 5592align-items: center; 5593} 5594#cart-kit-upsell-mount .ck-micropdp-btn { 5595border: 1px solid #cfd8e3; 5596background: #fff; 5597color: #1b1b1b; 5598border-radius: 8px; 5599padding: 8px 10px; 5600cursor: pointer; 5601font-weight: 600; 5602font-size: 13px; 5603} 5604/* Loading cursor while micro PDP is submitting */ 5605body.ck-loading, 5606body.ck-loading * { 5607cursor: progress !important; 5608} 5609#cart-kit-upsell-mount .ck-micropdp-btn:hover { 5610background: #f2f5f9; 5611} 5612#cart-kit-upsell-mount .ck-micropdp-body { 5613flex: 1 1 auto; 5614position: relative; 5615} 5616#cart-kit-upsell-mount .ck-micropdp-iframe { 5617width: 100%; 5618height: 100%; 5619border: 0; 5620display: block; 5621} 5622#cart-kit-upsell-mount .ck-micropdp-loading { 5623position: absolute; 5624inset: 0; 5625display: flex; 5626align-items: center; 5627justify-content: center; 5628background: rgba(255, 255, 255, .85); 5629font-weight: 700; 5630color: #444; 5631} 5632.ck-debug-keywords { 5633margin-top: 4px; 5634font-size: 11px; 5635line-height: 1.4; 5636} 5637.ck-debug-keyword { 5638display: inline-block; 5639background: rgba(255, 255, 255, 0.08); 5640border: 1px solid rgba(255, 255, 255, 0.12); 5641border-radius: 999px; 5642padding: 2px 8px; 5643margin: 2px 6px 2px 0; 5644opacity: .95; 5645} 5646.ck-debug-keyword.muted, 5647.ck-debug-keywords.muted { 5648opacity: 0.55; 5649font-style: italic; 5650} 5651@media (max-width: 640px) {
5652#cart-kit-upsell-mount .cart-kit-upsell__title { 5653font-size: 16px; 5654} 5655} 5656@media (max-width: 520px) { 5657/* Accent chip mobile layout */ 5658#cart-kit-upsell-mount .kit-stamp-grid:not(.is-notary-grid) { 5659display: grid !important; 5660grid-template-columns: repeat(2, 1fr); 5661gap: 20px 12px; 5662justify-items: center; 5663} 5664#cart-kit-upsell-mount .kit-stamp-card { 5665width: 100%; 5666max-width: 160px; 5667} 5668#cart-kit-upsell-mount .kit-stamp-chip__circle { 5669width: 80px; 5670height: 80px; 5671} 5672#cart-kit-upsell-mount .kit-stamp-chip__image { 5673max-width: 62px; 5674max-height: 62px; 5675} 5676} </style>
5676<script> // ---- GA safety shim (prevents "gtag is not defined") ---- 5677window.dataLayer = window.dataLayer || []; 5678window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; 5679/** 5680* Cart Kit Upsell Module â Contract (Do Not Break) 5681* - Runs only on Cart page 5682* - Never blocks checkout interactions 5683* - Never modifies existing cart items (only adds new items) 5684* - Fails silently (no uncaught JS errors if PDP fetch/hydration fails) 5685* - Always has a fallback persona (generic) 5686* - Micro PDP is only used for SKUs that require custom options/uploads 5687*/ 5688document.addEventListener('DOMContentLoaded', function () { 5689// ---------------------------------------------------- 5690// 0) Render on Cart? 5691// ---------------------------------------------------- 5692// Only run SmartCart on the actual cart page. 5693// IMPORTANT: product pages and microPDP iframe pages often contain ".cart" elements (header/minicart), 5694// so the old detection was firing in the iframe and breaking. 5695var isCart = 5696document.body.classList.contains('checkout-cart-index') || 5697/\/checkout\/cart(\/|$)/.test(window.location.pathname); 5698if (!isCart) return; 5699// ---------------------------------------------------- 5700// 1) Mount BELOW .shoppingCartHeader-container 5701// ---------------------------------------------------- 5702// Zephyr cart is Tailwind markup (no #shopping-cart-table / .shoppingCartHeader-container). 5703// Mount after the "Shopping Cart" H1 header block; keep legacy anchors as fallback. 5704function ckFindZephyrHeader() { 5705var hs = document.querySelectorAll('h1'); 5706for (var i = 0; i < hs.length; i++) { 5707if (/cart/i.test(hs[i].textContent || '')) return hs[i].closest('.mb-6') || hs[i].parentElement; 5708} 5709return null; 5710} 5711var zHeader = ckFindZephyrHeader(); 5712var anchor = 5713document.querySelector('.shoppingCartHeader-container') || 5714zHeader; 5715var fallback = 5716(document.querySelector('#shopping-cart-table') && document.querySelector('#shopping-cart-table').parentElement) || 5717(zHeader && zHeader.parentElement) || 5718(document.querySelector('[data-cart-item-id]') && document.querySelector('[data-cart-item-id]').closest('.pb-32, main, #maincontent')) || 5719document.querySelector('.cart'); 5720if (!anchor && !fallback) return; 5721if (document.getElementById('cart-kit-upsell-mount')) return; 5722var mount = document.createElement('div'); 5723mount.id = 'cart-kit-upsell-mount'; 5724if (anchor && anchor.parentNode) anchor.insertAdjacentElement('afterend', mount); 5725else fallback.appendChild(mount); 5726// ---------------------------------------------------- 5727// 2) Query params 5728// ---------------------------------------------------- 5729var urlParams = new URLSearchParams(window.location.search); 5730var accentAddedQS = urlParams.get('accentAdded') === '1'; 5731var accentInCart = false; 5732var IS_ACCENT_EXPERIENCE = !!(accentAddedQS || accentInCart); 5733var accentAddMoreQS = urlParams.get('accentAddMore') === '1'; 5734var microAddedQS = urlParams.get('microAdded'); // productId 5735var microAddedNameQS = urlParams.get('microAddedName'); // product name (best effort) 5736// ===== SmartCart Persona Memory (Device-based) ===== 5737const SMARTCART_PERSONA_STORAGE_KEY = "smartcart_persona_v1"; 5738// --- Helpers --- 5739function nowMs() { return Date.now(); } 5740function safeJsonParse(str) { 5741try { return JSON.parse(str); } catch { return null; } 5742} 5743// --- Read / Write memory --- 5744function savePersonaToDevice(personaKey) { 5745if (!personaKey) return; 5746const payload = { 5747key: personaKey, 5748setAt: Date.now(), 5749version: 2 // bump version since behavior changed 5750}; 5751try { 5752localStorage.setItem( 5753SMARTCART_PERSONA_STORAGE_KEY, 5754JSON.stringify(payload) 5755); 5756} catch (e) { 5757// storage may be blocked; fail silently 5758} 5759} 5760function getPersonaFromDevice() { 5761let raw; 5762try { 5763raw = localStorage.getItem(SMARTCART_PERSONA_STORAGE_KEY); 5764} catch { 5765return null; 5766} 5767if (!raw) return null; 5768const payload = safeJsonParse(raw); 5769if (!payload?.key) return null; 5770return payload; 5771} 5772function clearPersonaFromDevice() { 5773try { localStorage.removeItem(SMARTCART_PERSONA_STORAGE_KEY); } catch { } 5774}
5775// --- GA user_property (set once per session/page lifecycle) --- 5776function setSmartCartUserPropertyOnce(personaKey) { 5777if (!personaKey) return; 5778if (window.__SMARTCART_USER_PROP_SET__) return; 5779window.__SMARTCART_USER_PROP_SET__ = true; 5780// If consent gating exists, you can wrap this (see section 4) 5781if (typeof gtag === "function") { 5782gtag("set", "user_properties", { smartcart_persona: personaKey }); 5783} 5784} 5785function onSmartCartMeaningfulEngagement(personaKey) { 5786if (!personaKey) return; 5787// Prevent repeated writes in one session/page lifecycle 5788if (window.__SMARTCART_ENGAGED__) return; 5789window.__SMARTCART_ENGAGED__ = true; 5790// Remember persona on device (UX) 5791savePersonaToDevice(personaKey); 5792// Attribute session in GA (analytics) 5793setSmartCartUserPropertyOnce(personaKey); 5794} 5795// ---------------------------------------------------- 5796// Debug exports (safe): expose helpers in console 5797// ---------------------------------------------------- 5798if (new URLSearchParams(location.search).get("sc_debug") === "1") { 5799window.__SMARTCART_DEBUG__ = window.__SMARTCART_DEBUG__ || {}; 5800window.__SMARTCART_DEBUG__.getPersonaFromDevice = getPersonaFromDevice; 5801window.__SMARTCART_DEBUG__.savePersonaToDevice = savePersonaToDevice; 5802window.__SMARTCART_DEBUG__.setSmartCartUserPropertyOnce = setSmartCartUserPropertyOnce; 5803window.__SMARTCART_DEBUG__.onSmartCartMeaningfulEngagement = onSmartCartMeaningfulEngagement; 5804} 5805// ---------------------------------------------------- 5806// 2.5) Hard SKU overrides (micro PDP required) 5807// ---------------------------------------------------- 5808var HARD_SKU_OVERRIDES = { 5809"2577": { addMode: "microPdp" }, 5810"4419": { addMode: "microPdp" }, 5811"10796": { addMode: "microPdp" } 5812}; 5813// ---------------------------------------------------- 5814// 3) PERSONA DETECTION (v2: Config-driven cart text scan) 5815// ---------------------------------------------------- 5816function loadPersonaConfigFromBlock() { 5817var el = document.getElementById('cart-kit-personas'); 5818if (!el) return null; 5819try { 5820var raw = (el.textContent || el.innerText || '').trim(); 5821if (!raw) return null; 5822return JSON.parse(raw); 5823} catch (e) { 5824console.warn('Cart Kit: Failed to parse persona JSON', e); 5825return null; 5826} 5827} 5828function buildCartSignals() { 5829// Adjust selectors to your cart markup if needed 5830var nameNodes = document.querySelectorAll('.product-name a, .product-name, .cart.item .product-item-name a, .cart.item .product-item-name'); 5831var optNodes = document.querySelectorAll('.item-options, dl.item-options, .product.options, .cart.item .item-options'); 5832var names = []; 5833for (var i = 0; i < nameNodes.length; i++) { 5834var t = (nameNodes[i].textContent || '').trim(); 5835if (t) names.push(t); 5836} 5837var opts = []; 5838for (var j = 0; j < optNodes.length; j++) { 5839var o = (optNodes[j].textContent || '').trim(); 5840if (o) opts.push(o); 5841} 5842var titleText = names.join(' | '); 5843var fullText = (names.concat(opts)).join(' | '); 5844return { titleText: titleText, fullText: fullText }; 5845} 5846// Product titles only (recommended for persona scoring) 5847function getCartTitleTextLower() { 5848try { 5849// Magento cart on SimplyStamps: h2.cartProductName a 5850var nameNodes = document.querySelectorAll( 5851'[data-cart-item-id] h3 a,' + 5852'[data-cart-item-id] h3,' + 5853'#shopping-cart-table h2.cartProductName a,' + 5854'#shopping-cart-table h2.cartProductName,' + 5855'#shopping-cart-table .product-name a,' + 5856'#shopping-cart-table .product-name,' + 5857'.product-item-name a,' + 5858'.product-item-name' 5859); 5860var names = []; 5861for (var i = 0; i < nameNodes.length; i++) { 5862var t = (nameNodes[i].textContent || '').trim(); 5863if (t) names.push(t); 5864} 5865return names.join(' | ').toLowerCase(); 5866} catch (e) { 5867return ''; 5868} 5869} 5870// Full cart text (titles + options). Keep for product scoring/debug if desired. 5871function getCartFullTextLower() { 5872try { 5873var titleLower = getCartTitleTextLower(); 5874var optNodes = document.querySelectorAll( 5875'[data-cart-item-id] .flex-wrap,' + 5876'#shopping-cart-table .item-options,' + 5877'#shopping-cart-table dl.item-options,' + 5878'.item-options,' + 5879'dl.item-options' 5880); 5881var opts = []; 5882for (var j = 0; j < optNodes.length; j++) { 5883var o = (optNodes[j].textContent || '').trim(); 5884if (o) opts.push(o); 5885} 5886// titleLower is already lowercase
5887var full = (titleLower ? [titleLower] : []).concat(opts.join(' | ').toLowerCase()).join(' | '); 5888return full.toLowerCase(); 5889} catch (e) { 5890return ''; 5891} 5892} 5893// ---------------------------------------------------- 5894// Cart product path signals (reliable for "already in cart" suppression) 5895// ---------------------------------------------------- 5896function normalizeUrlPathForCompare(url) { 5897try { 5898var u = new URL(url, window.location.origin); 5899var p = (u.pathname || ''); 5900// normalize: drop .html and trailing slashes 5901p = p.replace(/\.html$/i, '').replace(/\/+$/, ''); 5902return String(p || '').toLowerCase(); 5903} catch (e) { 5904try { 5905var s = String(url || '').split('#')[0].split('?')[0]; 5906s = s.replace(/\.html$/i, '').replace(/\/+$/, ''); 5907return String(s || '').toLowerCase(); 5908} catch (e2) { return ''; } 5909} 5910} 5911function getCartProductPathSet() { 5912var set = Object.create(null); 5913try { 5914var nodes = document.querySelectorAll( 5915'[data-cart-item-id] h3 a,' + 5916'#shopping-cart-table .product-item-name a,' + 5917'#shopping-cart-table a.product-item-link,' + 5918'.cart.table-wrapper .product-item-name a,' + 5919'.cart.table-wrapper a.product-item-link,' + 5920'.cart-container .product-item-name a,' + 5921'.cart-container a.product-item-link' 5922); 5923for (var i = 0; i < nodes.length; i++) { 5924var href = nodes[i] && (nodes[i].getAttribute('href') || nodes[i].href); 5925if (!href) continue; 5926var pth = normalizeUrlPathForCompare(href); 5927if (!pth) continue; 5928// Exclude cart actions/downloads that appear in the cart table 5929if (/^\/checkout\//i.test(pth)) continue; 5930if (/^\/sales\//i.test(pth)) continue; 5931if (/^\/download\//i.test(pth)) continue; 5932set[pth] = true; 5933} 5934} catch (e) { } 5935return set; 5936} 5937// Zephyr: exact product-id match from cart cards (data-product-id = Magento entity id) 5938function getCartProductIdSet() { 5939var set = Object.create(null); 5940try { 5941var nodes = document.querySelectorAll('[data-product-id]'); 5942for (var i = 0; i < nodes.length; i++) { 5943var pid = String(nodes[i].getAttribute('data-product-id') || '').trim(); 5944if (pid) set[pid] = true; 5945} 5946} catch (e) { } 5947return set; 5948} 5949// Compatibility alias used by existing scoring code 5950function getCartTextLower() { 5951// Persona scoring should use titles (cleanest signal) 5952return getCartTitleTextLower(); 5953// If you ever want âtitles + optionsâ instead, swap to: 5954// return getCartFullTextLower(); 5955} 5956// Spreadsheet-driven personas (local environment) 5957var PERSONAS_FALLBACK = [ 5958{ 5959key: 'brand_champions', 5960priority: 80, 5961match: [ 5962'business stamp', 'logo stamp', 'logo address stamp', 'custom logo stamp', 'company stamp', 'business address stamp', 5963'return address stamp business', 'self inking logo stamp', 'office branding', 'small business supplies', 5964'custom business stamps', 'bulk stamps' 5965], 5966headerTitle: 'Brand Essentials: finish your packaging strong.', 5967stampLabel: 'Business add-ons (popular picks)', 5968inkLabel: 'Choose an ink color for consistent branding', 5969tooltip: 'These add-ons help your brand look polished across packaging, mailers, and customer communications.', 5970defaultPrimaryIndex: 0, 5971tileSubtext: 'Popular with small businesses', 5972items: [ 5973{ productId: '2577', productUrl: 'https://www.simplystamps.com/qr-code-stamp-1-5-8-x-1-5-8', sortOrder: 2, addMode: 'microPdp' }, 5974{ productId: '4419', productUrl: 'https://www.simplystamps.com/medium-custom-logo-stamp', sortOrder: 1, addMode: 'microPdp' }, 5975{ productId: '10796', productUrl: 'https://www.simplystamps.com/mini-square-logo-stamp', sortOrder: 3, addMode: 'microPdp' } 5976] 5977}, 5978{ 5979key: 'home_sweet_stampers', 5980priority: 70, 5981match: [ 5982'return address stamp', 'address stamp', 'custom address stamp', 'family address stamp', 'wedding address stamp', 5983'holiday address stamp', 'mail stamp', 'envelope stamp', 'home address stamp', 'personalized address stamp' 5984], 5985headerTitle: 'Mail Essentials: everything you need for clean, easy stamping.', 5986stampLabel: 'Helpful add-ons for everyday mail', 5987inkLabel: 'Choose an ink color', 5988tooltip: 'Perfect for cards, envelopes, and seasonal mail â these extras make stamping easier and mess-free.', 5989defaultPrimaryIndex: 0, 5990tileSubtext: 'Great for everyday mail', 5991items: [ 5992{ productId: '5654', productUrl: 'https://www.simplystamps.com/merry-christmas-craft-rubber-stamp', sortOrder: 1 }, 5993{ productId: '6126', productUrl: 'https://www.simplystamps.com/happy-birthday-smiley-craft-stamp', sortOrder: 2 }, 5994{ productId: '5529', productUrl: 'https://www.simplystamps.com/live-well-rubber-stamp', sortOrder: 3 } 5995] 5996}, 5997{ 5998key: 'office_optimizers', 5999priority: 85, 6000match: [ 6001'signature', 'received stamp', 'approved stamp', 'paid stamp', 'office stamp', 'date stamp', 'custom office stamp', 6002'self inking stamp', 'pre inked stamp', 'inspection stamp', 'file stamp', 'reorder stamp', 'replacement ink' 6003], 6004headerTitle: 'Office Essentials: stay stocked and avoid reorders.', 6005stampLabel: 'Office add-ons (frequently reordered)', 6006inkLabel: 'Ink selection for daily office use', 6007tooltip: 'These supplies help keep workflows moving without interruptions or last-minute supply runs.', 6008defaultPrimaryIndex: 0, 6009tileSubtext: 'Frequently reordered', 6010items: [ 6011{ productId: '6560', productUrl: 'https://www.simplystamps.com/trodat-id-protection-stamp', sortOrder: 1 }, 6012{ productId: '2420', productUrl: 'https://www.simplystamps.com/35305-br-black-eksc-4-br-secure-marker-br-4mm-chisel-tip', sortOrder: 2 } 6013] 6014}, 6015{ 6016key: 'notary_ninjas', 6017priority: 100, 6018match: [ 6019'notary public', 'notary', 'notary stamp', 'notary seal', 'notary embosser', 'state notary stamp', 'notary supplies', 6020'notary kit', 'notary replacement stamp', 'official notary seal', 'notary expiration stamp', 6021'jurat stamp', 'acknowledgment stamp' 6022], 6023headerTitle: 'Notary Essentials: add backup ink & supplies before checkout.', 6024stampLabel: 'Notary add-ons (quick picks)', 6025inkLabel: 'Ink choice (recommended for notary work)', 6026tooltip: 'These items help ensure clean, compliant notarizations and prevent downtime if ink runs out.', 6027defaultPrimaryIndex: 0, 6028tileSubtext: 'Recommended for notary work', 6029items: [ 6030{ productId: '11712', productUrl: 'https://www.simplystamps.com/the-complete-notary-records-book', sortOrder: 1 }, 6031{ productId: '6524', productUrl: 'https://www.simplystamps.com/embossing-adhesive-foil', sortOrder: 2 }, 6032{ productId: '7986', productUrl: 'https://www.simplystamps.com/seal-impression-inker', sortOrder: 3 }, 6033{ productId: '13387', productUrl: 'https://www.simplystamps.com/embosser-vinyl-black-pouch', sortOrder: 4 } 6034] 6035}, 6036{ 6037key: 'classroom_heroes', 6038priority: 60, 6039match: [ 6040'teacher stamp', 'teacher ', 'grading stamp', 'school stamp', 'custom teacher stamp', 'classroom stamp', 6041'reward stamp', 'student feedback stamp', 'motivational stamp', 'education stamp', 'teacher name stamp' 6042], 6043headerTitle: 'Classroom Essentials: be ready for every lesson.', 6044stampLabel: 'Teacher add-ons (classroom favorites)', 6045inkLabel: 'Choose an ink color students will love', 6046tooltip: 'Extra ink and supplies help keep grading, rewards, and feedback flowing all year.', 6047defaultPrimaryIndex: 0, 6048tileSubtext: 'Classroom favorites', 6049items: [ 6050{ productId: '19215', productUrl: 'https://www.simplystamps.com/stacked-teacher-stamps-set-of-6', sortOrder: 1 }, 6051{ productId: '10965', productUrl: 'https://www.simplystamps.com/no-name-no-fame-owl-teacher-stamp', sortOrder: 2 }, 6052{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 3 } 6053] 6054}, 6055{
6056key: 'craft_crowd', 6057priority: 50, 6058match: [ 6059'craft stamp', 'handmade stamp', 'custom rubber stamp', 'craft rubber stamp', 'logo stamp small business', 'etsy stamp', 6060'thank you stamp', 'packaging stamp', 'product label stamp', 'arts and crafts stamp', 'creative stamp' 6061], 6062headerTitle: 'Creative Essentials: finish your projects with ease.', 6063stampLabel: 'Craft add-ons (popular with makers)', 6064inkLabel: 'Choose an ink color to match your style', 6065tooltip: 'These supplies are perfect for handmade goods, packaging, and creative projects.', 6066allowedInk: ['black', 'blue', 'red', 'green', 'purple'], 6067defaultPrimaryIndex: 0, 6068tileSubtext: 'Popular with makers', 6069items: [ 6070{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', sortOrder: 1 }, 6071{ productId: '10464', productUrl: 'https://www.simplystamps.com/gem-cluster-stamp', sortOrder: 2 }, 6072{ productId: '9768', productUrl: 'https://www.simplystamps.com/seamless-eucalyptus-stamp', sortOrder: 3 } 6073] 6074}, 6075// Fallback (Accent Stamp grid) 6076{ 6077key: 'generic', 6078priority: 0, 6079match: [], 6080headerTitle: 'Choose your favorite accent stamp below and add to cart.', 6081stampLabel: 'Choose one or many Accent Stamp Designs:', 6082inkLabel: 'Would you like an Ink Pad? (Needed to make impression)', 6083tooltip: 'Stamp impression size: approx 1.75" Ã 0.67".', 6084allowedInk: ['black', 'blue', 'red', 'green', 'purple', 'none'], 6085defaultPrimaryIndex: 0, 6086tileSubtext: '', 6087items: [] 6088} 6089]; 6090function normalizePersonasConfigToArray(cfg) { 6091// Expect cfg like: { version: "...", personas: { key: { ... }, ... } } 6092if (!cfg || !cfg.personas || typeof cfg.personas !== 'object') return null; 6093var out = []; 6094Object.keys(cfg.personas).forEach(function (key) { 6095var p = cfg.personas[key] || {}; 6096// Map JSON schema â existing runtime schema 6097// Your runtime expects: key, priority, match, headerTitle, stampLabel, inkLabel, tooltip, allowedInk, defaultPrimaryIndex, tileSubtext, items[] 6098var persona = { 6099key: key, 6100priority: (typeof p.priority === 'number' ? p.priority : 0), 6101match: Array.isArray(p.match_keywords) ? p.match_keywords : [], 6102headerTitle: p.header_title || '', 6103stampLabel: p.stamp_label || '', 6104inkLabel: p.ink_label || '', 6105tooltip: p.tooltip_text || '', 6106allowedInk: Array.isArray(p.allowed_ink_colors) ? p.allowed_ink_colors : [], 6107defaultPrimaryIndex: (typeof p.default_primary_index === 'number' ? p.default_primary_index : 0), 6108tileSubtext: p.tile_subtext || '', 6109items: [] 6110}; 6111// Products â items (what buildGrid uses) 6112if (Array.isArray(p.products)) { 6113persona.items = p.products.map(function (prod) { 6114return { 6115productId: String(prod.product_id || '').trim(), 6116productUrl: prod.product_url || '', 6117sortOrder: (typeof prod.sort_order === 'number' ? prod.sort_order : 9999), 6118tileSubtext: prod.tile_subtext || '', 6119addMode: prod.addMode || prod.add_mode || undefined, 6120// NEW (traits) 6121role: prod.role || 'helper', 6122friction: prod.friction || 'low', 6123experimental: (typeof prod.experimental === 'boolean' ? prod.experimental : false), 6124// NEW (product-level keywords for scoring) 6125matchKeywords: Array.isArray(prod.match_keywords) ? prod.match_keywords : [] 6126}; 6127}).filter(function (it) { return !!it.productId || !!it.productUrl; }); 6128} 6129out.push(persona); 6130}); 6131return out; 6132} 6133var PERSONA_CONFIG = loadPersonaConfigFromBlock(); 6134// ---------------------------------------------------- 6135// 4) Now set PERSONAS like this (config â normalized array â fallback) 6136// ---------------------------------------------------- 6137var PERSONAS = (function () { 6138// Try config block first 6139var fromConfig = normalizePersonasConfigToArray(PERSONA_CONFIG); 6140// Use config if it produced at least 1 persona, otherwise fallback 6141var arr = (Array.isArray(fromConfig) && fromConfig.length) ? fromConfig : PERSONAS_FALLBACK.slice(); 6142// Ensure we ALWAYS have a generic fallback persona 6143var hasGeneric = arr.some(function (p) { return p && p.key === 'generic'; }); 6144if (!hasGeneric) { 6145var genericFallback = PERSONAS_FALLBACK.find(function (p) { return p.key === 'generic'; }); 6146if (genericFallback) arr.push(genericFallback); 6147} 6148// Normalize a few expected fields defensively 6149arr.forEach(function (p) { 6150if (!p) return; 6151if (!Array.isArray(p.match)) p.match = []; 6152if (!Array.isArray(p.items)) p.items = []; 6153if (!Array.isArray(p.allowedInk) || !p.allowedInk.length) { 6154// keep your existing default behavior (includes 'none') 6155p.allowedInk = ['black', 'blue', 'red', 'green', 'purple', 'none']; 6156} 6157}); 6158return arr; 6159})();
6160PERSONAS.forEach(function (p) { 6161if (Array.isArray(p.items)) p.items.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); }); 6162}); 6163PERSONAS.sort(function (a, b) { 6164if (a.key === 'generic') return 1; 6165if (b.key === 'generic') return -1; 6166return (b.priority || 0) - (a.priority || 0); 6167}); 6168function setLoadingCursor(on) { 6169document.body.classList.toggle('ck-loading', !!on); 6170} 6171// ---------------------------------------------------- 6172// 3.9) SCORING (persona + product) 6173// ---------------------------------------------------- 6174function keywordMatchScore(keywords, haystackLower) { 6175if (!Array.isArray(keywords) || !keywords.length) return { score: 0, hits: 0, matched: [] }; 6176var score = 0, hits = 0, matched = []; 6177for (var i = 0; i < keywords.length; i++) { 6178var k = String(keywords[i] || '').toLowerCase().trim(); 6179if (!k) continue; 6180// Exact substring match 6181if (haystackLower.indexOf(k) > -1) { 6182score += (k.indexOf(' ') > -1 ? 10 : 4); 6183hits += 1; 6184matched.push(k); 6185continue; 6186} 6187// Phrase fallback: all tokens appear anywhere 6188if (k.indexOf(' ') > -1) { 6189var parts = k.split(/\s+/).filter(Boolean); 6190if (parts.length >= 2) { 6191var all = true; 6192for (var p = 0; p < parts.length; p++) { 6193if (haystackLower.indexOf(parts[p]) === -1) { all = false; break; } 6194} 6195if (all) { 6196score += 6; 6197hits += 1; 6198matched.push(k + ' (tokens)'); 6199} 6200} 6201} 6202} 6203return { score: score, hits: hits, matched: matched }; 6204} 6205function scoreKeywordHits(keywords, haystackLower) { 6206return keywordMatchScore(keywords, haystackLower).score; 6207} 6208function scorePersonaFromCart(personaObj, cartTextLower) { 6209// Persona match keywords are in personaObj.match[] 6210var base = scoreKeywordHits(personaObj.match, cartTextLower); 6211// Priority is a tie-breaker, not a primary driver. 6212// Still, give a tiny nudge so higher-priority personas win ties. 6213var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0; 6214return base + priorityNudge; 6215} 6216function detectPersonaKey() { 6217var cartTextLower = getCartTitleTextLower(); 6218var bestKey = 'generic'; 6219var bestScore = -1; 6220for (var i = 0; i < PERSONAS.length; i++) { 6221var p = PERSONAS[i]; 6222if (!p || p.key === 'generic') continue;
6223var s = scorePersonaFromCart(p, cartTextLower); 6224if (s > bestScore) { 6225bestScore = s; 6226bestKey = p.key; 6227} 6228} 6229// If nobody matched anything, fall back 6230if (bestScore <= 0) return 'generic'; 6231return bestKey; 6232} 6233function getPersonaByKey(key) { 6234for (var i = 0; i < PERSONAS.length; i++) { 6235if (PERSONAS[i].key === key) return PERSONAS[i]; 6236} 6237return PERSONAS[PERSONAS.length - 1]; 6238} 6239var activePersonaKey = detectPersonaKey(); 6240var persona = getPersonaByKey(activePersonaKey); 6241window.__CK = { 6242personas: PERSONAS, 6243activePersonaKey: activePersonaKey, 6244persona: persona 6245}; 6246var useTiles = Array.isArray(persona.items) && persona.items.length > 0; 6247// Decide which persona to use for this render 6248function choosePersona({ detectedKey, detectedConfidence = 0, confidenceThreshold = 0.75 }) { 6249const stored = getPersonaFromDevice(); // { key, setAt... } or null 6250// Strong real-time signal wins 6251if (detectedKey && detectedConfidence >= confidenceThreshold) { 6252return { key: detectedKey, source: "detected_strong" }; 6253} 6254// Otherwise fall back to remembered persona if available 6255if (stored?.key) { 6256return { key: stored.key, source: "stored" }; 6257} 6258// Otherwise use detected (weak) or default 6259if (detectedKey) return { key: detectedKey, source: "detected_weak" }; 6260return { key: "default", source: "default" }; 6261} 6262// ---------------------------------------------------- 6263// 3b) Analytics helper (optional) 6264// ---------------------------------------------------- 6265function pushUpsellEvent(eventName, payload) { 6266payload = payload || {}; 6267// ---------------------------------------------------- 6268// Enrich payload persona context so GA4 can segment 6269// ---------------------------------------------------- 6270try { 6271var __ckPersonaKey = (typeof activePersonaKey !== 'undefined' && activePersonaKey) ? String(activePersonaKey) : 'generic'; 6272// Prefer persona.headerTitle as a human label, fall back to key 6273var __ckPersonaLabel = (typeof persona !== 'undefined' && persona && (persona.headerTitle || persona.key)) 6274? String(persona.headerTitle || persona.key) 6275: __ckPersonaKey; 6276// Only set defaults if caller didn't provide them 6277if (typeof payload.persona_key === 'undefined') payload.persona_key = __ckPersonaKey; 6278if (typeof payload.persona_label === 'undefined') payload.persona_label = __ckPersonaLabel; 6279if (typeof payload.upsell_variant === 'undefined') payload.upsell_variant = (typeof useTiles !== 'undefined' && useTiles) ? 'tiles' : 'stamps'; 6280if (typeof payload.page_path === 'undefined') payload.page_path = (window.location && window.location.pathname) ? window.location.pathname : ''; 6281} catch (eCtx) { } 6282// ---------------------------------------------------- 6283// Option 4: Meaningful engagement â remember persona + set GA user_property 6284// Trigger only on specific SmartCart events (no impressions) 6285// ---------------------------------------------------- 6286try { 6287var isMeaningful = 6288eventName === 'SmartCart_upsell_tile_selected' || 6289eventName === 'SmartCart_upsell_tile_click' || 6290eventName === 'SmartCart_upsell_tile_add' || 6291eventName === 'SmartCart_upsell_tile_added' || 6292eventName === 'SmartCart_upsell_added'; 6293if (isMeaningful) { 6294var pk = 6295(payload && (payload.persona_key || payload.personaKey)) || 6296(typeof activePersonaKey !== 'undefined' && activePersonaKey) || 6297(window.__CK && window.__CK.activePersonaKey); 6298if (pk) onSmartCartMeaningfulEngagement(String(pk)); 6299} 6300} catch (e) { } 6301// Always push to dataLayer (never depend on gtag existing) 6302try { 6303window.dataLayer = window.dataLayer || []; 6304window.dataLayer.push(Object.assign({ event: eventName }, payload)); 6305} catch (e) { } 6306// Optional: also send to gtag if it exists (but NEVER crash if missing) 6307try { 6308if (typeof window.gtag === 'function') { 6309window.gtag('event', eventName, payload); 6310} 6311} catch (e) { } 6312// Mirror to GA4 standard ecommerce (see CK_GA4 block below). Never 6313// let an analytics mirror break the module. 6314try { ckGaMirror(eventName, payload); } catch (eGa) { } 6315}
6316// ---------------------------------------------------- 6317// 3b-ii) GA4 standard ecommerce mirror [added 2026-09-15] 6318// ---------------------------------------------------- 6319// WHY: the SmartCart_* custom events carry product ids only inside a 6320// nested array (tiles_shown), and GA4 cannot report on nested arrays. 6321// The property has no custom dimension for productId, so per-product 6322// performance has been invisible since launch. GA4's `item_id` is a 6323// BUILT-IN dimension, so emitting standard ecommerce events gives us a 6324// per-product view -> select -> add funnel with zero custom-dimension 6325// setup. These are emitted ALONGSIDE the custom events, never instead. 6326// 6327// dataLayer only -- GTM is confirmed live on this page and already 6328// processing SmartCart pushes. We do not call gtag() here because line 6329// ~1015 stubs window.gtag when it is absent, so a gtag() call would be 6330// an unconditional duplicate push. 6331var CK_GA4 = { 6332EMIT: true, // master kill switch 6333EMIT_MICRO_ADD: true, // set false if the micro-PDP iframe fires its own add_to_cart 6334LIST_PREFIX: 'SmartCart', 6335HYDRATE_POLLS: 8, // 8 x 300ms = up to ~2.4s waiting for tile names 6336HYDRATE_POLL_MS: 300, 6337// SKUs for products that never go through parsePdpDoc, so nothing can 6338// hydrate a SKU for them: the ink parent (added by id + options) and 6339// the six accent stamps used by the non-tile 'generic' persona, where 6340// buildGrid returns before any PDP fetch. Without these, every add in 6341// the generic variant -- the bucket every unmatched cart falls into -- 6342// would ship an entity id and be unjoinable to purchase. 6343// All verified against prod 2026-09-16. Re-check if SKUs change. 6344STATIC_SKUS: { 6345'3631': '1009260', // Ideal Ink Pad for Traditional Wood Handle 6346'23105': '1027259', // Delicate Flower envelope accent 6347'23106': '1027260', // Simple botanical branches 6348'23107': '1027261', // (accent) 6349'23108': '1027262', // Elegant botanical branch 6350'23110': '1027264', // Hearts through arrow 6351'23113': '1027267' // Outlined heart 6352} 6353}; 6354// A SKU we are willing to put in item_id. Magento SKUs are short and 6355// token-like; a scrape that picked up a wrapper element's textContent 6356// ('SKU:\n 1012837') or a sentinel ('N/A') must not ship -- and must not 6357// be labelled id_source:'sku', which would hide it from the very 6358// diagnostic added to catch it. 6359function ckGaSkuOk(v) { 6360var s = ckGaValidId(v); 6361if (!s) return ''; 6362if (s.length > 40) return ''; 6363return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(s) ? s : ''; 6364} 6365// ckGaItem records, per built item, whether item_id is a real SKU or the 6366// entity-id fallback. ckGaPush reads and clears it -- the whole mirror runs 6367// synchronously inside one ckGaMirror call, so there is no interleaving. 6368var CK_GA4_IDSRC = []; 6369// Analytics-only mirror of tile metadata for products that entered a slot 6370// after buildGrid (swaps). Read by ckGaItem, by nothing else -- writing 6371// these into TILE_ITEM_BY_ID would change add-flow routing. 6372var CK_GA_TILE_META = Object.create(null); 6373// Reject ids that are not real product ids. TILE_ITEMS tolerates entries 6374// with no productId, and String(undefined) === 'undefined' would ship a 6375// junk item_id that also never hydrates (stalling the impression poll). 6376function ckGaValidId(v) { 6377if (v === null || typeof v === 'undefined') return ''; 6378var s = String(v).trim(); 6379if (!s || s === 'undefined' || s === 'null' || s === 'NaN') return ''; 6380return s; 6381} 6382function ckGaListId() { 6383try { 6384return String((typeof activePersonaKey !== 'undefined' && activePersonaKey) || 'generic'); 6385} catch (e) { return 'generic'; } 6386} 6387function ckGaListName() { 6388var label = ckGaListId(); 6389try { 6390if (typeof persona !== 'undefined' && persona && persona.headerTitle) { 6391label = String(persona.headerTitle); 6392} 6393} catch (e) { } 6394// GA4 truncates item_list_name at 100 chars; header_title is 6395// CMS-editable, so cap it here rather than letting GA4 split rows. 6396return (CK_GA4.LIST_PREFIX + ' - ' + label).slice(0, 100); 6397} 6398function ckGaPrice(v) { 6399if (v === null || typeof v === 'undefined') return undefined; 6400var n = parseFloat(String(v).replace(/[^0-9.\-]/g, '')); 6401return isFinite(n) ? Math.round(n * 100) / 100 : undefined; 6402} 6403// Build one GA4 item object for a SmartCart product id. 6404function ckGaItem(pid, index, listNameOverride) { 6405var id = String(pid);
6406var raw = null, hyd = null; 6407try { 6408if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null; 6409if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null; 6410} catch (e1) { } 6411try { 6412if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd = NOTARY_HYDRATED.get(id) || null; 6413} catch (e2) { } 6414// SKU is the id GA4 must see (see parsePdpDoc). Fall back to the 6415// entity id only when hydration has not produced a SKU yet -- that is 6416// recoverable and countable via the id_source param on the event, 6417// whereas dropping the item outright would lose the conversion. 6418var gaSku = ckGaSkuOk(hyd && hyd.sku) || 6419ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id]); 6420var gaId = gaSku || id; 6421try { CK_GA4_IDSRC.push(gaSku ? 'sku' : 'entity'); } catch (eSrc0) { } 6422var item = { 6423item_id: gaId, 6424item_brand: 'SimplyStamps', 6425item_category: 'SmartCart Upsell', 6426item_list_id: ckGaListId(), 6427// Item-scoped item_list_name is what GA4 item reports actually 6428// use, so an event-level override has to reach the items too. 6429item_list_name: listNameOverride || ckGaListName(), 6430quantity: 1 6431}; 6432// NEVER send a placeholder name. Tiles hydrate asynchronously, so a 6433// 'Product 12345' filler would permanently split the item_name 6434// dimension against the real name sent by later events. An absent 6435// dimension is recoverable; a wrong one is not. 6436var name = (hyd && hyd.name) || (raw && (raw.title || raw.name)) || ''; 6437if (name) item.item_name = String(name); 6438if (typeof index === 'number' && index > 0) item.index = index; 6439// Price: hydrated PDP price for tiles; the flat accent-stamp price 6440// in the non-tile variant, where nothing hydrates at all. 6441var price = ckGaPrice(hyd ? hyd.price : null); 6442if (typeof price !== 'number') { 6443try { 6444if (typeof useTiles !== 'undefined' && !useTiles && typeof STAMP_UNIT_PRICE === 'number') { 6445price = STAMP_UNIT_PRICE; 6446} 6447} catch (eP) { } 6448} 6449if (typeof price === 'number') item.price = price; 6450// Slice-able strategy attributes, so GA4 can answer 'do experimental 6451// tiles convert?' and 'does friction cost us adds?' without new dims. 6452// Only set what the config actually declares -- defaulting friction to 6453// 'low' would read back as 'low friction converts fine' off data that 6454// really means 'we did not know'. 6455if (raw) { 6456if (raw.role) item.item_variant = String(raw.role); 6457if (raw.friction) item.item_category2 = String(raw.friction); 6458if (typeof raw.experimental !== 'undefined') { 6459item.item_category3 = raw.experimental ? 'experimental' : 'core'; 6460} 6461} 6462return item; 6463} 6464// startIndex === null means 'do not emit index'. Only view_item_list 6465// knows a real display position; on select_item / add_to_cart an index 6466// would be fabricated (always 1, or add-order), and a fabricated 6467// position silently answers 'which slot converts?' wrong. 6468function ckGaItemsFor(ids, startIndex, listNameOverride) { 6469var out = []; 6470var useIdx = (startIndex !== null && typeof startIndex !== 'undefined'); 6471try { 6472(ids || []).forEach(function (pid) { 6473var id = ckGaValidId(pid); 6474if (!id) return; 6475out.push(ckGaItem(id, useIdx ? (startIndex + out.length + 1) : null, listNameOverride)); 6476}); 6477} catch (e) { } 6478return out; 6479} 6480// True once we can give this product the id GA4 needs. Gating the deferred 6481// impression on the NAME alone was not enough: ckRepaintCardAs writes a 6482// CK_TILE_FALLBACKS title into TILE_ITEM_BY_ID synchronously, before it even 6483// starts the PDP fetch, so a swapped-in fallback tile looked 'resolved' with 6484// no SKU yet -- view_item_list would ship its entity id while the shopper's 6485// later select_item shipped the real SKU. Same product, two item_ids, one 6486// session: exactly the split this is all meant to remove. 6487function ckGaHasSku(pid) { 6488var id = String(pid); 6489if (ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[id])) return true; 6490try { 6491if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) { 6492var h = NOTARY_HYDRATED.get(id); 6493if (h && ckGaSkuOk(h.sku)) return true; 6494} 6495} catch (e) { } 6496return false; 6497} 6498// True once a product has a real, non-placeholder name available. 6499function ckGaHasName(pid) { 6500var id = String(pid), hyd = null, raw = null; 6501try { if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) hyd =
6501NOTARY_HYDRATED.get(id) || null; } catch (e1) { } 6502try { 6503if (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) raw = TILE_ITEM_BY_ID[id] || null; 6504if (!raw && CK_GA_TILE_META) raw = CK_GA_TILE_META[id] || null; 6505} catch (e2) { } 6506return !!((hyd && hyd.name) || (raw && (raw.title || raw.name))); 6507} 6508// Ink pads are options on a single parent SKU, not tiles -- model them as 6509// variants of that parent so ink attach is measurable too. 6510function ckGaInkItems(colorKeys, startIndex) { 6511var out = []; 6512try { 6513// The ink parent never hydrates through parsePdpDoc (it is added 6514// by id + options, not from a tile), so its SKU is pinned here. 6515var inkEnt = (typeof INK_PARENT_ID !== 'undefined' && INK_PARENT_ID) ? String(INK_PARENT_ID) : ''; 6516var inkSku = ckGaSkuOk(CK_GA4.STATIC_SKUS && CK_GA4.STATIC_SKUS[inkEnt]); 6517var pid = inkSku || inkEnt || 'ink-pad'; 6518(colorKeys || []).forEach(function (ck, i) { 6519var row = { 6520item_id: pid, 6521item_name: 'Ink Pad', 6522item_brand: 'SimplyStamps', 6523item_category: 'SmartCart Upsell', 6524item_category3: 'core', 6525item_variant: String(ck), 6526item_list_id: ckGaListId(), 6527item_list_name: ckGaListName(), 6528quantity: 1 6529}; 6530// Ink is a flat-price option the module already prices itself. 6531// Without this, an ink-only add -- the module's core ask -- 6532// reports add_to_cart with no value at all. 6533if (typeof INK_PRICE === 'number') row.price = INK_PRICE; 6534if (startIndex !== null && typeof startIndex !== 'undefined') row.index = startIndex + i + 1; 6535// Report ink's id source too, or a mixed add reads as 'entity' 6536// and an analyst quarantining that label throws away good rows. 6537try { CK_GA4_IDSRC.push(inkSku ? 'sku' : 'entity'); } catch (eSrc1) { } 6538out.push(row); 6539}); 6540} catch (e) { } 6541return out; 6542} 6543function ckGaValue(items) { 6544var v = 0, any = false; 6545try { 6546(items || []).forEach(function (it) { 6547if (typeof it.price === 'number') { v += it.price * (it.quantity || 1); any = true; } 6548}); 6549} catch (e) { } 6550return any ? Math.round(v * 100) / 100 : undefined; 6551} 6552function ckGaPush(eventName, ecommerce) { 6553// Drain the id-source buffer FIRST, above every early return -- a 6554// stale entry would mislabel the next event. 6555var idsrc = CK_GA4_IDSRC; 6556CK_GA4_IDSRC = []; 6557if (!CK_GA4.EMIT) return; 6558if (!ecommerce || !Array.isArray(ecommerce.items) || !ecommerce.items.length) return; 6559try { 6560window.dataLayer = window.dataLayer || []; 6561// Clear the previous ecommerce object first so params never bleed 6562// between events (standard GTM ecommerce practice). 6563window.dataLayer.push({ ecommerce: null }); 6564// id_source tells the analyst whether these item_ids are real SKUs 6565// (joinable to purchase) or entity-id fallbacks from a tile that 6566// had not hydrated yet. Ink items are always pinned to a real SKU. 6567var srcTag = 'sku'; 6568try { 6569if (idsrc.length) { 6570var nEnt = idsrc.filter(function (x) { return x === 'entity'; }).length; 6571srcTag = (nEnt === 0) ? 'sku' : (nEnt === idsrc.length ? 'entity' : 'mixed'); 6572} 6573} catch (eSrc) { } 6574window.dataLayer.push({ 6575event: eventName, 6576smartcart: true, 6577persona_key: ckGaListId(), 6578id_source: srcTag, 6579ecommerce: ecommerce 6580}); 6581} catch (e) { } 6582} 6583// Single mirror point: called for every pushUpsellEvent. Adding a new 6584// GA4 event means adding a case here, not touching call sites. 6585function ckGaMirror(eventName, payload) { 6586if (!CK_GA4.EMIT) return; 6587payload = payload || {}; 6588var items, val, ec; 6589if (eventName === 'SmartCart_upsell_impression') { 6590// null = read the live grid at fire time. See 6591// ckGaFireListWhenHydrated for why that beats the build-time list. 6592ckGaFireListWhenHydrated(null); 6593return; 6594} 6595// Three distinct code paths end in a real selection. _select_fallback 6596// fires when the PDP prefetch failed but the tile WAS still selected, 6597// so omitting it would under-report select_item in proportion to our 6598// own fetch error rate. accent_selected is the non-tile variant. 6599if (eventName === 'SmartCart_upsell_tile_select_fallback' && payload.selected === false) { 6600return; // that path fires on deselect too 6601} 6602if (eventName === 'SmartCart_upsell_tile_selected' || 6603eventName === 'SmartCart_upsell_tile_select_fallback' || 6604eventName === 'SmartCart_upsell_accent_selected') { 6605var selId = payload.productId || payload.product_id || payload.stamp_id; 6606items = ckGaItemsFor([selId], null); 6607ckGaPush('select_item', { 6608item_list_id: ckGaListId(), 6609item_list_name: ckGaListName(), 6610items: items 6611}); 6612return; 6613} 6614if (eventName === 'SmartCart_upsell_added') { 6615items = ckGaItemsFor(payload.product_ids, null) 6616.concat(ckGaInkItems(payload.ink_colors, null)); 6617val = ckGaValue(items); 6618ec = { currency: 'USD', items: items }; 6619if (typeof val === 'number') ec.value = val; 6620ckGaPush('add_to_cart', ec); 6621return; 6622} 6623if (eventName === 'SmartCart_upsell_micro_added') { 6624if (!CK_GA4.EMIT_MICRO_ADD) return; 6625items = ckGaItemsFor(payload.product_ids, null); 6626val = ckGaValue(items); 6627ec = { currency: 'USD', items: items }; 6628if (typeof val === 'number') ec.value = val; 6629ckGaPush('add_to_cart', ec); 6630return; 6631} 6632} 6633// Read the product ids actually rendered in the grid right now. 6634function ckGaVisibleTileIds() { 6635var out = []; 6636try { 6637if (!stampGrid) return out; 6638var cards = stampGrid.querySelectorAll('[data-kit-primary]'); 6639for (var i = 0; i < cards.length; i++) { 6640var id = ckGaValidId(cards[i].getAttribute('data-kit-primary')); 6641if (id && out.indexOf(id) === -1) out.push(id); 6642} 6643} catch (e) { } 6644return out; 6645} 6646// view_item_list is deliberately NOT fired inline with the impression. 6647// 6648// Two reasons. (1) fireTilesImpressionOnce() runs synchronously right 6649// after the hydration fetches are *started*, so at that instant no tile
6650// has a real name or price -- firing then sends an id-only list whose 6651// item_name never matches the select_item / add_to_cart that follow, 6652// splitting every per-product report in two. (2) the tile quality gate 6653// drops or repaints tiles during those same milliseconds, so the 6654// build-time id list is not what the shopper ends up seeing. 6655// 6656// So: pass ids = null to re-read the LIVE grid on every poll, wait for 6657// names, then fire once. On timeout we still fire -- an id-only 6658// impression beats no impression at all, and a stalled poll would let 6659// select_item land before view_item_list and break funnel step order. 6660function ckGaFireListWhenHydrated(ids, attempts, listNameOverride) { 6661if (!CK_GA4.EMIT) return; 6662try { 6663var live = (ids === null || typeof ids === 'undefined'); 6664var list = live ? ckGaVisibleTileIds() 6665: (ids || []).map(ckGaValidId).filter(function (x) { return !!x; }); 6666if (!list.length) return; 6667attempts = (typeof attempts === 'number') ? attempts : CK_GA4.HYDRATE_POLLS; 6668var resolved = 0; 6669list.forEach(function (id) { if (ckGaHasName(id) && ckGaHasSku(id)) resolved++; }); 6670if (resolved < list.length && attempts > 0) { 6671setTimeout(function () { 6672ckGaFireListWhenHydrated(ids, attempts - 1, listNameOverride); 6673}, CK_GA4.HYDRATE_POLL_MS); 6674return; 6675} 6676ckGaPush('view_item_list', { 6677item_list_id: ckGaListId(), 6678item_list_name: listNameOverride || ckGaListName(), 6679items: ckGaItemsFor(list, 0, listNameOverride) 6680}); 6681} catch (e) { } 6682} 6683function fireTilesImpressionOnce() { 6684try { 6685if (!useTiles) return; 6686if (!Array.isArray(TILE_ITEMS) || !TILE_ITEMS.length) return; 6687// fire once per page load per persona 6688window.__ckImpressionFired = window.__ckImpressionFired || {}; 6689var key = String(activePersonaKey || 'generic'); 6690if (window.__ckImpressionFired[key]) return; 6691window.__ckImpressionFired[key] = true; 6692pushUpsellEvent('SmartCart_upsell_impression', { 6693// Flat id list -- GA4 cannot report on the nested tiles_shown 6694// array; ckGaMirror() turns this into view_item_list items[]. 6695product_ids: TILE_ITEMS.map(function (t) { return String(t.productId); }), 6696tiles_shown: TILE_ITEMS.map(function (t, idx) { 6697return { 6698productId: t.productId, 6699position: idx + 1, 6700experimental: !!t.experimental, 6701role: t.role || 'helper', 6702friction: t.friction || 'low' 6703}; 6704}) 6705}); 6706} catch (e) { } 6707} 6708function pushTileEvent(action, payload) { 6709payload = payload || {}; 6710pushUpsellEvent('SmartCart_upsell_tile_' + action, payload); 6711const personaKey = 6712payload.persona_key || 6713payload.personaKey || 6714(window.__CK && window.__CK.activePersonaKey) || 6715window.activePersonaKey; 6716if ( 6717personaKey && 6718(action === 'view' || action === 'in_view' || action === 'click' || action === 'add' || action === 'added' || action === 'selected') 6719) { 6720onSmartCartMeaningfulEngagement(personaKey); 6721} 6722} 6723// ---------------------------------------------------- 6724// 4) CONFIG 6725// ---------------------------------------------------- 6726// Ink pad parent product + options 6727var INK_PARENT_ID = '3631'; 6728var INK_SIZE_OPTION_ID = '1296'; // Size 6729var INK_COLOR_OPTION_ID = '1295'; // Ink Pad Color 6730var INK_SIZE_VALUE_ID = '6265'; // Small 6731var INK_COLOR_VALUES = { 6732black: '6257', 6733blue: '6258', 6734red: '6259', 6735green: '6260', 6736purple: '6262' 6737}; 6738// Accent stamp options 6739var ACCENT_SIZE_OPTION_ID = '1453'; 6740var ACCENT_SIZE_VALUE_ID = '6921'; 6741var ACCENT_INK_OPTION_ID = '2403'; 6742var ACCENT_INK_NO_THANKS_VALUE_ID = '9532'; 6743// Pricing model for Accent stamps (existing behavior) 6744var STAMP_UNIT_PRICE = 5.49; 6745var INK_PRICE = 4.99; 6746var STAMP_OPTIONS = [ 6747{ 6748id: '23105', 6749imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-1_800.jpg', 6750imgAlt: 'Delicate flower envelope accent craft stamp design' 6751}, 6752{ 6753id: '23106', 6754imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-2_800.jpg', 6755imgAlt: 'Simple botanical branches envelope accent craft stamp design' 6756}, 6757{ 6758id: '23108', 6759imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-4_800.jpg', 6760imgAlt: 'Elegant botanical branch envelope accent craft stamp design' 6761}, 6762{ 6763id: '23107', 6764imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_floral-3_800.jpg', 6765imgAlt: 'Daisy flower envelope accent craft stamp design' 6766}, 6767{ 6768id: '23110', 6769imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-arrow_800.jpg', 6770imgAlt: 'Hearts through arrow envelope accent craft stamp design' 6771}, 6772{ 6773id: '23113', 6774imgSrc: 'https://cdn.simplystamps.com/media/catalog/product/s/s/ss_envelope-accent_lead-image_heart-2_800.jpg', 6775imgAlt: 'Outlined heart envelope accent craft stamp design' 6776} 6777]; 6778var MAX_TILES_TO_SHOW = 6; // guardrail 6779var ALLOW_EXPERIMENTAL_TILES = true; // easy toggle 6780// Persona tile items (simple products). If none, we show Accent Stamp grid. 6781// Uses product-level matchKeywords + role/friction modifiers to pick the best N (MAX_TILES_TO_SHOW). 6782function scoreProductForCart(item, cartTextLower, allowExperimental) { 6783var score = 0; 6784// Keyword hits (product-level) 6785score += scoreKeywordHits(item.matchKeywords, cartTextLower); 6786// Role modifiers 6787switch (item.role) {
6788case 'anchor': score += 6; break; 6789case 'core': score += 4; break; 6790case 'compliance': score += 8; break; 6791case 'backup': score += 2; break; 6792case 'accessory': score += 1; break; 6793default: break; 6794} 6795// Friction modifiers 6796switch (item.friction) { 6797case 'none': score += 3; break; 6798case 'low': score += 1; break; 6799case 'medium': score -= 2; break; 6800case 'high': score -= 5; break; 6801default: break; 6802} 6803// Experimental gating 6804if (item.experimental && !allowExperimental) score -= 100; 6805return score; 6806} 6807function pickTopTilesForDisplay(items, cartTextLower, maxTiles, allowExperimental) { 6808var pool = Array.isArray(items) ? items.slice() : []; 6809// Ensure fields exist 6810pool.forEach(function (it) { 6811if (!it) return; 6812if (!Array.isArray(it.matchKeywords)) it.matchKeywords = []; 6813if (!it.role) it.role = 'helper'; 6814if (!it.friction) it.friction = 'low'; 6815}); 6816// Score + stable tiebreakers 6817var scored = pool.map(function (it, idx) { 6818return { 6819it: it, 6820idx: idx, 6821score: scoreProductForCart(it, cartTextLower, allowExperimental) 6822}; 6823}); 6824scored.sort(function (a, b) { 6825if (b.score !== a.score) return b.score - a.score; 6826// If tie: prefer anchors 6827var ar = (a.it.role === 'anchor') ? 1 : 0; 6828var br = (b.it.role === 'anchor') ? 1 : 0; 6829if (br !== ar) return br - ar; 6830// Then lower friction 6831var fr = { none: 0, low: 1, medium: 2, high: 3 }; 6832var af = fr[a.it.friction] != null ? fr[a.it.friction] : 9; 6833var bf = fr[b.it.friction] != null ? fr[b.it.friction] : 9; 6834if (af !== bf) return af - bf; 6835// Then sortOrder 6836return (a.it.sortOrder || 0) - (b.it.sortOrder || 0); 6837}); 6838// Dedupe by product id: the pool can carry the same product under two 6839// entries, and rendering it twice reads as a bug. 6840var seenPid = Object.create(null); 6841var selected = []; 6842for (var si = 0; si < scored.length && selected.length < maxTiles; si++) { 6843var cand = scored[si].it; 6844var cpid = String((cand && (cand.productId || cand.product_id)) || ''); 6845if (cpid) { 6846if (seenPid[cpid]) continue; 6847seenPid[cpid] = true; 6848} 6849selected.push(cand); 6850} 6851// Default index: highest score, but if an anchor is within 2 points of top, prefer it. 6852var defaultIndex = 0; 6853if (selected.length) { 6854var topScore = scored.length ? scored[0].score : 0; 6855for (var i = 0; i < selected.length; i++) { 6856var it = selected[i]; 6857var itScore = scoreProductForCart(it, cartTextLower, allowExperimental); 6858if (it.role === 'anchor' && itScore >= (topScore - 2)) { 6859defaultIndex = i; 6860break; 6861} 6862} 6863} 6864return { tiles: selected, defaultIndex: defaultIndex }; 6865} 6866// Respect ?upsellDebug=1 to include experimental tiles 6867var allowExperimentalNow = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1'); 6868var cartTextForScoring = getCartTitleTextLower(); 6869var tilePick = (useTiles ? pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow) : { tiles: [], defaultIndex: 0 }); 6870// Best-effort: suppress tiles that are already in the cart using cart link paths (most reliable signal we have on cart) 6871var __cartPaths0 = Object.create(null); 6872try { __cartPaths0 = getCartProductPathSet(); } catch (eCP0) { __cartPaths0 = Object.create(null); } 6873var __cartIds0 = Object.create(null); 6874try { __cartIds0 = getCartProductIdSet(); } catch (eCI0) { __cartIds0 = Object.create(null); } 6875function __isInCartByPath(it) { 6876try { 6877// Zephyr: exact product-id match first (data-product-id on cart cards) 6878var pid0 = it && (it.productId || it.product_id);
6879if (pid0 && __cartIds0[String(pid0)]) return true; 6880// Be tolerant: different shapes may use productUrl vs product_url 6881var url = ''; 6882if (it) { 6883url = it.productUrl || it.product_url || it.url || ''; 6884} 6885// If url missing but we have an id, try the lookup map (populated during tile build) 6886if (!url && it && it.productId && TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[it.productId]) { 6887url = TILE_ITEM_BY_ID[it.productId].productUrl || TILE_ITEM_BY_ID[it.productId].product_url || ''; 6888} 6889var pth = normalizeUrlPathForCompare(url); 6890return !!(pth && __cartPaths0[pth]); 6891} catch (e) { return false; } 6892} 6893// Build initial visible tiles from the ordered pool, skipping in-cart items. 6894// Keep tile count stable up to MAX_TILES_TO_SHOW. 6895var TILE_ITEMS = []; 6896var TILE_ITEM_BY_ID = {}; 6897if (useTiles && Array.isArray(tilePick.tiles)) { 6898for (var __i = 0; __i < tilePick.tiles.length; __i++) { 6899if (TILE_ITEMS.length >= MAX_TILES_TO_SHOW) break; 6900var it = tilePick.tiles[__i]; 6901if (!it) continue; 6902// Allow tiles even if productId is missing (null IDs can still be previewed). We'll block add-to-cart for them later. 6903// Skip anything already in cart (path match) 6904if (__isInCartByPath(it)) continue; 6905TILE_ITEMS.push(it); 6906try { if (it.productId) TILE_ITEM_BY_ID[it.productId] = it; } catch (eMap) { } 6907} 6908} 6909// Default index: keep "best" (0) after exclusions 6910if (useTiles) persona.defaultPrimaryIndex = 0; 6911// ---------------------------------------------------- 6912// 5) Build UI 6913// ---------------------------------------------------- 6914var inkPadHtml = 6915'<div class="cart-kit-upsell__options-group" style="margin-top:10px;">' 6916+ '<div class="cart-kit-upsell__options-label" id="kit-ink-label">Would you like an Ink Pad? (Needed to make impression)</div>' 6917+ '<div class="kit-swatch-list" id="kit-swatch-list">' 6918+ '<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>' 6919+ '<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>' 6920+ '<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>' 6921+ '<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>' 6922+ '<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>' 6923+ '<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>' 6924+ '</div>' 6925+ '</div>'; 6926var inkPadBlock = (!useTiles ? inkPadHtml : ''); 6927var upsellMarkup = '' 6928+ '<div class="cart-kit-upsell cart-kit-upsell--open" id="cart-kit-upsell">' 6929+ '<button class="cart-kit-upsell__header" type="button">' 6930+ '<div class="cart-kit-upsell__icon-wrap" aria-hidden="true">' 6931+ '<img class="cart-kit-upsell__icon-img cart-kit-upsell__icon-img--static"' 6932+ ' src="https://cdn.simplystamps.com/media/wysiwyg/SimplyStamps/SS_Homepage-Icon_Stamps_120x120.png"' 6933+ ' alt="" />' 6934+ '</div>' 6935+ '<div class="cart-kit-upsell__header-text">' 6936+ '<span class="cart-kit-upsell__title" id="cart-kit-header-text">$1 Accent Stamp Unlocked! Choose your favorite below and add to cart.</span>' 6937+ '</div>' 6938+ '<span class="cart-kit-upsell__chevron" aria-hidden="true"></span>' 6939+ '</button>' 6940+ '<div class="cart-kit-upsell__body">' 6941+ '<div class="cart-kit-upsell__options-group">' 6942+ '<div class="cart-kit-upsell__options-header">' 6943+ '<div class="cart-kit-upsell__options-label" id="kit-stamp-label">Choose one or many Accent Stamp Designs:</div>' 6944+ '<button type="button" class="cart-kit-upsell__info" aria-label="Stamp impression size information">' 6945+ '<span class="cart-kit-upsell__info-icon">i</span>' 6946+ '<span class="cart-kit-upsell__info-tooltip" id="kit-tooltip">Stamp impression size: approx 1.75" Ã 0.67".</span>' 6947+ '</button>' 6948+ '</div>' 6949+ '<div class="kit-stamp-grid" id="kit-stamp-grid"></div>' 6950+ '</div>' 6951+ inkPadBlock 6952+ '<div class="cart-kit-upsell__footer">' 6953+ '<div class="cart-kit-upsell__price">' 6954+ '<span class="cart-kit-upsell__price-label" id="kit-price-label">Total:</span>' 6955+ '<span class="kit-value-line__new" id="kit-price-new">$1.00</span>' 6956+ '<button type="button" class="cart-kit-upsell__cta" id="cart-kit-upsell-add">Add to Cart</button>' 6957+ '<button type="button" class="cart-kit-upsell__cta-secondary" id="cart-kit-upsell-hide">Hide Offer</button>' 6958+ '</div>' 6959+ '<div class="kit-success-toast" id="kit-success-toast"></div>' 6960+ '<div class="cart-kit-upsell__status" id="cart-kit-status" aria-l
6960ive="polite"></div>' 6961+ '<div class="cart-kit-upsell__progresswrap" id="cart-kit-progresswrap" style="display:none;">' 6962+ '<div class="cart-kit-upsell__progresstop">' 6963+ '<div class="cart-kit-upsell__progresstext" id="cart-kit-progresstext">Adding itemsâ¦</div>' 6964+ '<div class="cart-kit-upsell__progresspct" id="cart-kit-progresspct">0%</div>' 6965+ '</div>' 6966+ '<div class="cart-kit-upsell__progressbar">' 6967+ '<div class="cart-kit-upsell__progressfill" id="cart-kit-progressfill"></div>' 6968+ '</div>' 6969+ '</div>' 6970+ '</div>' 6971+ '</div>' 6972+ '</div>' 6973+ '</div>'; 6974mount.insertAdjacentHTML('beforeend', upsellMarkup); 6975var upsell = mount.querySelector('#cart-kit-upsell'); 6976var header = upsell.querySelector('.cart-kit-upsell__header'); 6977var body = upsell.querySelector('.cart-kit-upsell__body'); 6978var addBtn = upsell.querySelector('#cart-kit-upsell-add'); 6979if (!upsell || !addBtn) { 6980console.warn('[SmartCart] Required module elements missing. Aborting init.'); 6981return; 6982} 6983var hideBtn = upsell.querySelector('#cart-kit-upsell-hide'); 6984var stampGrid = upsell.querySelector('#kit-stamp-grid'); 6985var swatchList = upsell.querySelector('#kit-swatch-list'); 6986var statusEl = upsell.querySelector('#cart-kit-status'); 6987if (statusEl) { 6988statusEl.setAttribute('role', 'status'); 6989statusEl.setAttribute('aria-live', 'polite'); 6990statusEl.setAttribute('aria-atomic', 'true'); 6991} 6992var progressWrap = upsell.querySelector('#cart-kit-progresswrap'); 6993var progressFill = upsell.querySelector('#cart-kit-progressfill'); 6994var progressText = upsell.querySelector('#cart-kit-progresstext'); 6995var progressPct = upsell.querySelector('#cart-kit-progresspct'); 6996var stampLabelEl = upsell.querySelector('#kit-stamp-label'); 6997var inkLabelEl = upsell.querySelector('#kit-ink-label'); 6998var tooltipEl = upsell.querySelector('#kit-tooltip'); 6999var headerTextEl = upsell.querySelector('#cart-kit-header-text'); 7000// Animate icon 7001setTimeout(function () { 7002var wrap = upsell.querySelector('.cart-kit-upsell__icon-wrap'); 7003if (wrap) wrap.classList.add('is-entered'); 7004}, 40); 7005// ---------------------------------------------------- 7006// 6) Apply persona UI (Notary) 7007// ---------------------------------------------------- 7008function applyPersonaUI() { 7009// Header + labels + tooltip 7010if (headerTextEl) headerTextEl.textContent = persona.headerTitle || ''; 7011if (stampLabelEl) stampLabelEl.textContent = persona.stampLabel || ''; 7012if (inkLabelEl) inkLabelEl.textContent = persona.inkLabel || ''; 7013if (tooltipEl) tooltipEl.textContent = persona.tooltip || ''; 7014// Toggle tile layout when persona has items 7015if (stampGrid) stampGrid.classList.toggle('is-notary-grid', !!useTiles); 7016// Filter ink swatches based on allowedInk 7017var allowed = (persona.allowedInk && persona.allowedInk.length) ? persona.allowedInk : ['black', 'blue', 'red', 'green', 'purple', 'none']; 7018if (swatchList) { 7019var swatches = swatchList.querySelectorAll('.kit-swatch'); 7020for (var i = 0; i < swatches.length; i++) { 7021var key = swatches[i].getAttribute('data-kit-color'); 7022swatches[i].style.display = (allowed.indexOf(key) > -1) ? '' : 'none'; 7023} 7024} 7025} 7026applyPersonaUI(); 7027// ---------------------------------------------------- 7028// 7) PDP Fetch Helpers (image + real price + real name) 7029// ---------------------------------------------------- 7030var PDP_CACHE = new Map(); // url -> { name, price, img } 7031function toNumberPrice(val) { 7032if (val == null) return null; 7033var n = Number(String(val).replace(/[^0-9.]/g, '')); 7034return isFinite(n) ? n : null; 7035} 7036function absUrlMaybe(src) { 7037if (!src) return null; 7038if (/^https?:\/\//i.test(src)) return src; 7039if (src.indexOf('/') === 0) return window.location.origin + src; 7040return src; 7041} 7042function detectRequiresMicro(doc) { 7043try { 7044var form = 7045doc.querySelector('#product_addtocart_form') || 7046doc.querySelector('form[action*="checkout/cart/add"]') || 7047doc.querySelector('form'); 7048if (!form) return false; 7049// Strong signal: any file upload = micro PDP 7050if (form.querySelector('input[type="file"]')) return true; 7051// Strong signal: Magento custom options payload (options[...]) 7052if (form.querySelector('[name^="options["]')) return true; 7053// Strong signal: configurables often use super_attribute 7054if (form.querySelector('[name^="super_attribute"]')) return true; 7055// Strong signal: required fields inside options wrapper 7056var optionsWrap = 7057form.querySelector('.product-options-wrapper') || 7058form.querySelector('#product-options-wrapper') || 7059form.querySelector('.fieldset') || 7060form; 7061// Look for explicit required markers/validators within options area 7062if ( 7063optionsWrap.querySelector('[required]') || 7064optionsWrap.querySelector('[aria-required="true"]') || 7065optionsWrap.querySelector('[data-validate*="required"]') || 7066optionsWrap.querySelector('.required-entry') || 7067optionsWrap.querySelector('.required') 7068) { 7069return true; 7070} 7071// If there are selectable option controls (select/radio/checkbox/textarea) 7072// inside a known options wrapper, treat as "likely micro" 7073// (kept conservative: only if wrapper exists) 7074var hasOptionControls = 7075!!optionsWrap.querySelector('select, input[type="radio"], input[type="checkbox"], textarea'); 7076if (hasOptionControls && (optionsWrap !== form)) return true; 7077return false;
7078} catch (e) { 7079return false; 7080} 7081} 7082function requiresMicroPdp(productId, rawTile) { 7083var pid = String(productId || '').trim(); 7084if (!pid) return false; 7085// 1) Explicit tile intent 7086if (rawTile && rawTile.addMode === 'microPdp') return true; 7087// 2) Hard override (authoritative) 7088if ( 7089typeof HARD_SKU_OVERRIDES !== 'undefined' && 7090HARD_SKU_OVERRIDES && 7091HARD_SKU_OVERRIDES[pid] && 7092HARD_SKU_OVERRIDES[pid].addMode === 'microPdp' 7093) { 7094return true; 7095} 7096// 3) Autodetect fallback from cached PDP parse 7097if ( 7098rawTile && 7099rawTile.productUrl && 7100typeof PDP_CACHE !== 'undefined' && 7101PDP_CACHE && 7102PDP_CACHE.has(rawTile.productUrl) 7103) { 7104var cached = PDP_CACHE.get(rawTile.productUrl); 7105if (cached && cached.requiresMicro === true) return true; 7106} 7107return false; 7108} 7109function isAccentExperience() { 7110try { 7111var urlParams = new URLSearchParams(window.location.search); 7112var accentAddedQS = (urlParams.get('accentAdded') === '1' || urlParams.get('accentAdded') === 'true'); 7113return !!accentAddedQS; // keep it strict for now 7114} catch (e) { 7115return false; 7116} 7117} 7118function parsePdpDoc(doc) { 7119var name = 7120(doc.querySelector('meta[property="og:title"]') && 7121doc.querySelector('meta[property="og:title"]').getAttribute('content')) || 7122(doc.querySelector('h1') && (doc.querySelector('h1').textContent || '').trim()) || 7123null; 7124var img = 7125(doc.querySelector('meta[property="og:image"]') && 7126doc.querySelector('meta[property="og:image"]').getAttribute('content')) || 7127(doc.querySelector('meta[name="twitter:image"]') && 7128doc.querySelector('meta[name="twitter:image"]').getAttribute('content')) || 7129null; 7130img = absUrlMaybe(img); 7131var price = 7132(doc.querySelector('meta[property="product:price:amount"]') && 7133doc.querySelector('meta[property="product:price:amount"]').getAttribute('content')) || 7134(doc.querySelector('meta[itemprop="price"]') && 7135doc.querySelector('meta[itemprop="price"]').getAttribute('content')) || 7136(doc.querySelector('[itemprop="price"]') && 7137doc.querySelector('[itemprop="price"]').textContent) || 7138(doc.querySelector('.price') && 7139doc.querySelector('.price').textContent) || 7140null; 7141price = toNumberPrice(price); 7142// Magento SKU. GA4 item_id MUST be the SKU, not the entity id: every 7143// other ecommerce event on this site (view_cart, purchase) keys items 7144// by SKU, so sending entity ids here would make the same product two 7145// different rows in GA4 and break any SmartCart-add -> purchase join. 7146var sku = null; 7147try { 7148var skuEl = doc.querySelector('meta[itemprop="sku"]') || doc.querySelector('[itemprop="sku"]'); 7149if (skuEl) { 7150sku = (skuEl.getAttribute('content') || skuEl.textContent || '').trim() || null; 7151} 7152} catch (eSku) { } 7153var requiresMicro = detectRequiresMicro(doc); 7154// The single reliable "is this a product?" signal. An account, login, 7155// 404 or category page still yields an og:title and an og:image, so 7156// title/image alone can't tell you the scrape succeeded. 7157var isProduct = !!( 7158doc.querySelector('#product_addtocart_form') || 7159doc.querySelector('form[action*="checkout/cart/add"]') 7160); 7161return { 7162name: name, 7163img: img, 7164price: price, 7165sku: sku, 7166isProduct: isProduct, 7167requiresMicro: requiresMicro 7168}; 7169} 7170// Same-origin PDP fetch: config product_urls are absolute www.simplystamps.com; 7171// rewrite to the current origin so hydration works on local/staging AND prod (no-op on www). 7172function ckSameOrigin(u) { 7173try { 7174var p = new URL(u, window.location.origin); 7175if (/(^|\.)simplystamps\.com$/i.test(p.hostname) && p.origin !== window.location.origin) { 7176return window.location.origin + p.pathname + p.search + p.hash; 7177} 7178return p.href; 7179} catch (e) { return u; } 7180} 7181// ---------------------------------------------------- 7182// ZEPHYR SIDE-CART BRIDGE (added 2026-09-11) 7183// Zephyr ships a slide-out cart drawer driven by an Alpine store, and the 7184// cart PAGE is a SECOND, independent Alpine component (cartPage()) holding 7185// its own copy of the same /zephyr/cart/getItems/ JSON. They do not talk: 7186// cart-drawer.js store.refresh() -> fires 'cart-refreshed' 7187// cart-page.js init() -> listens 'cart-updated' 7188// Nothing bridges those two names, so ANY out-of-band cart mutation must do
7189// BOTH: refresh the store (drawer + header badge) AND dispatch 'cart-updated' 7190// (main cart list + Order Summary). Doing only one leaves half the page 7191// stale -- which is the reason this module used to hard-reload. 7192// ---------------------------------------------------- 7193function ckCartStore(win) { 7194try { 7195var w = win || window; 7196if (!w.Alpine || typeof w.Alpine.store !== 'function') return null; 7197return w.Alpine.store('cart') || null; 7198} catch (e) { 7199return null; 7200} 7201} 7202// Push a cart mutation into BOTH Zephyr cart trees, optionally revealing the 7203// side cart. Returns false when the Zephyr store is absent (legacy theme) so 7204// callers can fall back to the old page-reload path. 7205function ckAfterCartMutation(openDrawer) { 7206var st = ckCartStore(window); 7207if (!st) return false; 7208if (openDrawer && typeof st.openDrawer === 'function') { 7209try { st._scrollToNew = true; } catch (e) { } 7210try { st.openDrawer(); } catch (e) { } 7211} 7212// Mirror Zephyr's own post-add timing (handleAjaxAddToCart waits 100ms). 7213setTimeout(function () { 7214try { if (typeof st.refresh === 'function') st.refresh(); } catch (e) { } 7215try { window.dispatchEvent(new CustomEvent('cart-updated')); } catch (e) { } 7216}, 100); 7217return true; 7218} 7219function fetchPdpData(productUrl) { 7220if (!productUrl) return Promise.resolve({ name: null, img: null, price: null }); 7221if (PDP_CACHE.has(productUrl)) return Promise.resolve(PDP_CACHE.get(productUrl)); 7222var __fetchUrl = ckSameOrigin(productUrl); 7223return fetch(__fetchUrl, { credentials: 'same-origin' }) 7224.then(function (res) { return res.text(); }) 7225.then(function (html) { 7226var doc = new DOMParser().parseFromString(html, 'text/html'); 7227var data = parsePdpDoc(doc); 7228PDP_CACHE.set(productUrl, data); 7229return data; 7230}) 7231.catch(function () { 7232var data = { name: null, img: null, price: null }; 7233PDP_CACHE.set(productUrl, data); 7234return data; 7235}); 7236} 7237// ---------------------------------------------------- 7238// Tile State Model (no UX changes yet) 7239// ---------------------------------------------------- 7240var TILE_STATE = { 7241AVAILABLE: 'available', 7242SELECTED: 'selected', 7243ADDED: 'added' 7244}; 7245// Central store keyed by SKU 7246// ---------------------------------------------------- 7247// TILE QUALITY GATE + APPROVED FALLBACKS (added 2026-09-15) 7248// Some persona entries point at URLs that aren't product pages on every store 7249// view (a prod-only SKU, a retired URL). The scrape then returns whatever that 7250// page happens to be -- "Create New Customer Account", no price, the site logo 7251// -- and we rendered it as a product. Never show that. Substitute an approved 7252// alternative the shopper doesn't already have, or drop the slot entirely. 7253// ---------------------------------------------------- 7254var CK_TILE_FALLBACKS = [ 7255{ productId: '22023', productUrl: 'https://www.simplystamps.com/envelope-sealer-2oz-bottle', title: 'Envelope Sealer - 2oz Bottle' }, 7256{ productId: '25340', productUrl: 'https://www.simplystamps.com/stamp-cleaner-kit', title: 'Stamp Cleaner Kit' }, 7257{ productId: '24186', productUrl: 'https://www.simplystamps.com/gold-wax-seal-sticker-rosemary-design', title: 'Gold Wax Seal Sticker, Rosemary Design' }, 7258{ productId: '18507', productUrl: 'https://www.simplystamps.com/craft-ink-pads', title: 'Craft Ink Pads' }, 7259{ 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' }, 7260{ productId: '23105', productUrl: 'https://www.simplystamps.com/delicate-flower-envelope-accent-craft-stamp', title: 'Delicate Flower Envelope Accent Craft Stamp' } 7261]; 7262var CK_FALLBACKS_USED = new Set(); 7263function ckIsUsableTileData(data) { 7264if (!data) return false; 7265if (data.isProduct === false) return false; // definitely not a PDP 7266if (data.price == null && !data.img) return false; // nothing worth showing 7267var n = String(data.name || '').trim().toLowerCase(); 7268if (!n) return false; 7269if (/create new customer|customer login|^log ?in|^sign ?in|page not found|404|access denied|shopping cart|my account|dashboard/.test(n)) return false; 7270return true; 7271} 7272function ckCartIdsNow() { 7273try { return getCartProductIdSet() || Object.create(null); } 7274catch (e) { return Object.create(null); } 7275} 7276// First approved alternative that is NOT already in the cart and NOT already on 7277// screen. Null when there is nothing safe left to show. 7278function ckPickFallbackTile() { 7279var inCart = ckCartIdsNow(); 7280var onScreen = Object.create(null); 7281try { 7282var rendered = document.querySelectorAll('[data-kit-primary]'); 7283for (var r = 0; r < rendered.length; r++) { 7284onScreen[String(rendered[r].getAttribute('data-kit-primary'))] = true; 7285} 7286} catch (e) { } 7287for (var i = 0; i < CK_TILE_FALLBACKS.length; i++) { 7288var f = CK_TILE_FALLBACKS[i]; 7289var id = String(f.productId); 7290if (CK_FALLBACKS_USED.has(id)) continue; 7291if (inCart[id]) continue; 7292if (onScreen[id]) continue; 7293CK_FALLBACKS_USED.add(id); 7294return f; 7295} 7296return null; 7297} 7298// Nothing safe for this slot -- remove it rather than render junk. 7299function ckDropTile(card) { 7300try { 7301var pid = String(card.getAttribute('data-kit-primary') || ''); 7302if (pid) { try { selectedPrimaryIds.delete(pid); } catch (eS) { } } 7303if (card.parentNode) card.parentNode.removeChild(card); 7304try { updatePriceUI(); } catch (eU) { } 7305// Tiles are dropped asynchronously, so this is the moment the module 7306// can become empty -- re-check every time. 7307try { ckSyncModuleEmptyState(); } catch (eE) { } 7308} catch (e) { } 7309} 7310// Repaint an already-rendered card onto a different product, then hydrate it. 7311function ckRepaintCardAs(card, item) { 7312if (!card || !item) return; 7313var id = String(item.productId); 7314try { TILE_ITEM_BY_ID[id] = item; } catch (e) { } 7315try { if (ckSeenTileIds) ckSeenTileIds.add(id); } catch (e) { } 7316card.setAttribute('data-kit-primary', id); 7317card.setAttribute('data-sku', id); 7318card.setAttribute('data-kit-url', item.productUrl || ''); 7319try { card.classList.remove('is-added', 'is-selected', 'is-complete'); } catch (e) { } 7320try { card.removeAttribute('aria-disabled'); } catch (e) { } 7321var nameEl = card.querySelector('.kit-product-name'); 7322var priceEl = card.querySelector('.kit-product-price'); 7323var thumbEl = card.querySelector('.kit-product-thumb-tile'); 7324if (nameEl) nameEl.textContent = item.title || 'Loading\u2026'; 7325if (priceEl) priceEl.textContent = '\u2014'; 7326if (thumbEl) thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">Loading\u2026</span>'; 7327fetchPdpData(item.productUrl).then(function (d2) { 7328if (String(card.getAttribute('data-kit-primary') || '') !== id) return; // swapped again 7329if (!ckIsUsableTileData(d2)) { 7330// This alternative isn't available in this store view either. Try the 7331// next approved one; CK_FALLBACKS_USED makes the recursion terminate. 7332var nextAlt = ckPickFallbackTile(); 7333if (nextAlt) ckRepaintCardAs(card, nextAlt); 7334else ckDropTile(card); 7335return; 7336} 7337try { 7338NOTARY_HYDRATED.set(id, { 7339sku: d2.sku, 7340productId: id, productUrl: item.productUrl, 7341name: d2.name || item.title, img: d2.img || null, 7342price: (d2.price != null ? Number(d2.price) : null) 7343}); 7344} catch (e) { } 7345if (nameEl) nameEl.textContent = d2.name || item.title; 7346if (priceEl) priceEl.textContent = (d2.price != null ? ('$' + Number(d2.price).toFixed(2)) : 'See price in cart'); 7347if (thumbEl) { 7348if (d2.img) thumbEl.innerHTML = '<img src="' + d2.img + '" alt="" />'; 7349else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 7350} 7351}); 7352} 7353// ---------------------------------------------------- 7354// MODULE EMPTY STATE (added 2026-09-15) 7355// Tiles can vanish AFTER render: the quality gate drops non-products and 7356// in-cart suppression removes what the shopper already owns. The original 7357// empty-state branch only ran when the pool was empty at BUILD time, and it 7358// targeted #kit-add-btn -- which does not exist, the CTA is 7359// #cart-kit-upsell-add -- so an emptied module was left on the page as a
7360// header + "Total: $0.00" + a live Add to Cart button with nothing to add. 7361// ---------------------------------------------------- 7362function ckSyncModuleEmptyState() { 7363try { 7364var tileCount = stampGrid ? stampGrid.querySelectorAll('[data-kit-primary]').length : 0; 7365var swatchCount = swatchList ? swatchList.querySelectorAll('.kit-swatch').length : 0; 7366if (tileCount > 0 || swatchCount > 0) { 7367if (upsell) upsell.style.display = ''; 7368return false; 7369} 7370// Nothing left to offer. An upsell with no offer is noise on a cart 7371// page, so take the whole module off the page rather than leave a 7372// header and a dead CTA behind. 7373try { console.warn('[SmartCart] nothing offerable remains (all in cart or filtered); hiding the module.'); } catch (e1) { } 7374try { 7375if (addBtn) { 7376addBtn.disabled = true; 7377addBtn.setAttribute('aria-disabled', 'true'); 7378} 7379} catch (e2) { } 7380if (upsell) upsell.style.display = 'none'; 7381return true; 7382} catch (e) { return false; } 7383} 7384var TileStore = (function () { 7385var map = Object.create(null); 7386function getSkuFromTile(tileEl) { 7387return ( 7388tileEl.getAttribute('data-sku') || 7389tileEl.getAttribute('data-product-sku') || 7390tileEl.getAttribute('data-kit-sku') || 7391'' 7392).trim(); 7393} 7394function inferIsHard(tileEl) { 7395// Preferred: explicit attribute you set when building tiles 7396// data-hard="1" or data-is-hard="true" 7397var hardAttr = 7398tileEl.getAttribute('data-hard') || 7399tileEl.getAttribute('data-is-hard') || 7400''; 7401if (hardAttr) return hardAttr === '1' || hardAttr === 'true'; 7402// Fallback: if you already tag these somewhere else 7403// (you can tighten this later when we implement Step 2) 7404return tileEl.classList.contains('is-hard') || false; 7405} 7406function initFromDom(rootEl) { 7407if (!rootEl) return; 7408var tiles = Array.from(rootEl.querySelectorAll('.kit-stamp-card[data-kit-primary]')); 7409tiles.forEach(function (tileEl) { 7410var sku = getSkuFromTile(tileEl); 7411if (!sku) return; 7412try { ckSeenTileIds.add(String(sku)); } catch (e) { } 7413if (!map[sku]) { 7414map[sku] = { 7415sku: sku, 7416el: tileEl, 7417isHard: inferIsHard(tileEl), 7418state: TILE_STATE.AVAILABLE 7419}; 7420} else { 7421// re-rendered tile, keep state but update DOM ref 7422map[sku].el = tileEl; 7423// do not overwrite isHard unless you want to re-infer 7424} 7425// Bootstrap state from current UI (so no visible changes) 7426// You can adjust these classnames to match your actual markup. 7427if (tileEl.classList.contains('is-added') || tileEl.querySelector('.ck-added, .cart-kit-added, .cart-kit-check')) { 7428map[sku].state = TILE_STATE.ADDED; 7429} else if (tileEl.classList.contains('is-selected') || tileEl.getAttribute('aria-pressed') === 'true') { 7430map[sku].state = TILE_STATE.SELECTED; 7431} else { 7432map[sku].state = TILE_STATE.AVAILABLE; 7433} 7434syncDom(sku); 7435}); 7436} 7437function get(sku) { 7438return map[String(sku || '')] || null; 7439} 7440function setState(sku, nextState) { 7441sku = String(sku || ''); 7442if (!map[sku]) return; 7443var prev = map[sku].state; 7444map[sku].state = nextState; 7445// Keep DOM aligned (but we preserve current look) 7446syncDom(sku); 7447return { sku: sku, prev: prev, next: nextState }; 7448} 7449function syncDom(sku) { 7450var t = map[String(sku || '')]; 7451if (!t || !t.el) return; 7452var el = t.el; 7453// Normalize classes (keep current behavior intact) 7454el.classList.toggle('is-selected', t.state === TILE_STATE.SELECTED); 7455el.classList.toggle('is-added', t.state === TILE_STATE.ADDED); 7456// Optional aria alignment if you already use it 7457// (doesn't change visuals but improves correctness) 7458if (t.state === TILE_STATE.SELECTED) el.setAttribute('aria-pressed', 'true'); 7459else el.setAttribute('aria-pressed', 'false'); 7460} 7461function all() { 7462return map; 7463} 7464return { 7465initFromDom: initFromDom, 7466get: get, 7467setState: setState, 7468syncDom: syncDom, 7469all: all 7470}; 7471})(); 7472// ---------------------------------------------------- 7473// Micro PDP (iframe) â for hard SKUs w/ uploads + dependent options 7474// ---------------------------------------------------- 7475var MICRO_PDP = { 7476overlay: null, 7477iframe: null, 7478loading: null, 7479lastUrl: null, 7480lastProductId: null, 7481returnUrl: null, 7482progressDone: 0, 7483progressTotal: 0 7484}; 7485// ---------------------------------------------------- 7486// Micro PDP progress (header bar) 7487// ---------------------------------------------------- 7488function getMicroProgressFromUrl() { 7489try { 7490var u = new URL(window.location.href); 7491var done = parseInt(u.searchParams.get('microDone') || '0', 10); 7492var total = parseInt(u.searchParams.get('microTotal') || '0', 10); 7493if (!isFinite(done) || done < 0) done = 0; 7494if (!isFinite(total) || total < 0) total = 0; 7495return { done: done, total: total }; 7496} catch (e) { 7497return { done: 0, total: 0 }; 7498} 7499} 7500function bumpMicroDoneInUrl() { 7501try { 7502var u = new URL(window.location.href); 7503var done = parseInt(u.searchParams.get('microDone') || '0', 10); 7504var total = parseInt(u.searchParams.get('microTotal') || '0', 10); 7505if (!isFinite(done) || done < 0) done = 0; 7506if (!isFinite(total) || total < 0) total = 0; 7507if (total > 0) { 7508done = Math.min(total, done + 1); 7509u.searchParams.set('microDone', String(done)); 7510window.history.replaceState({}, document.title, u.toString()); 7511} 7512return { done: done, total: total }; 7513} catch (e) { 7514return { done: 0, total: 0 }; 7515} 7516} 7517function renderMicroProgress(done, total) { 7518var wrap = mount.querySelector('#ck-micropdp-progress'); 7519var fill = mount.querySelector('#ck-micropdp-progressfill'); 7520var txt = mount.querySelector('#ck-micropdp-progresstext'); 7521// Only show when the user is doing a multi-item add (2+ total) 7522if (!wrap || !fill || !txt || !total || total <= 1) { 7523if (wrap) wrap.hidden = true; 7524return; 7525} 7526if (!isFinite(done) || done < 0) done = 0; 7527if (!isFinite(total) || total < 1) total = 1; 7528if (done > total) done = total; 7529var pct = Math.round((done / total) * 100); 7530wrap.hidden = false;
7531fill.style.width = pct + '%'; 7532// Completion state (for âAll setâ moment) 7533if (done >= total) { 7534wrap.classList.add('is-complete'); 7535txt.textContent = 'All set â ' + total + ' of ' + total + ' items added'; 7536} else { 7537wrap.classList.remove('is-complete'); 7538txt.textContent = done + ' of ' + total + ' items added'; 7539} 7540} 7541ensureMicroPdpModal(); 7542function ensureMicroPdpModal() { 7543if (MICRO_PDP.overlay) return; 7544var 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> `; 7545// Append inside mount so scoped CSS applies 7546mount.insertAdjacentHTML('beforeend', modalMarkup); 7547MICRO_PDP.overlay = mount.querySelector('#ck-micropdp-overlay'); 7548MICRO_PDP.iframe = mount.querySelector('#ck-micropdp-iframe'); 7549MICRO_PDP.loading = mount.querySelector('#ck-micropdp-loading'); 7550var closeBtn = mount.querySelector('#ck-micropdp-close'); 7551var fullBtn = mount.querySelector('#ck-micropdp-full'); 7552// Safety guards 7553if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-open'); 7554if (closeBtn) closeBtn.addEventListener('click', function () { closeMicroPdp('button'); }); 7555if (fullBtn) fullBtn.addEventListener('click', function () { 7556if (MICRO_PDP.lastUrl) window.open(MICRO_PDP.lastUrl, '_blank', 'noopener'); 7557}); 7558// Optional overlay click closes 7559if (MICRO_PDP.overlay) { 7560MICRO_PDP.overlay.addEventListener('click', function (e) { 7561if (e.target === MICRO_PDP.overlay) closeMicroPdp('overlay'); 7562}); 7563} 7564} 7565// ESC closes 7566document.addEventListener('keydown', function (e) { 7567if (e.key === 'Escape' && MICRO_PDP.overlay && MICRO_PDP.overlay.classList.contains('is-open')) { 7568closeMicroPdp(); 7569} 7570}); 7571// On iframe load, inject âmicroâ CSS + force form submit to top window 7572MICRO_PDP.iframe.addEventListener('load', function () { 7573try { 7574var doc = MICRO_PDP.iframe.contentDocument; 7575if (!doc) return; 7576// Ensure scope class exists inside iframe (so our selectors always match) 7577doc.documentElement.classList.add('cart-kit-micro-pdp'); 7578doc.body.classList.add('cart-kit-micro-pdp'); 7579// Hide big site chrome + tighten layout (best-effort; selectors may vary) 7580var style = doc.createElement('style'); 7581style.id = 'ck-micro-css'; 7582style.textContent = ` 7583/* ========================================================= 7584Micro PDP trims â chrome off, form focus, thumbnail preview 7585========================================================= */ 7586/* 1) Remove site chrome */ 7587header, .header, 7588.nav-container, .navigation, 7589.breadcrumbs, .breadcrumb, 7590.page-header, .header.content, 7591.sections.nav-sections, 7592.message.global, .message.notice, 7593.cookie-status-message, 7594footer, .footer, .page-footer { 7595display: none !important; 7596} 7597/* 1b) Zephyr side cart must never render inside the configurator. 7598Neither the drawer nor its trigger sits inside <header>, so the 7599chrome rules above miss both. */ 7600button[aria-label="Open cart"], 7601[x-show*="drawerOpen"] { 7602display: none !important; 7603} 7604/* 2) Normalize layout */ 7605html, body { height: auto !important; } 7606body { 7607margin: 0 !important; 7608padding: 12px !important; 7609background: #fff !important; 7610overflow-x: hidden !important; 7611} 7612.page-wrapper, .page-main, main, .columns, .column.main { 7613width: 100% !important; 7614max-width: none !important; 7615margin: 0 !important; 7616padding: 0 !important; 7617} 7618/* Hide the INLINE form add-to-cart row (but keep the sticky bottom bar) */ 7619/* Safety: ensure the STICKY/mobile bar still shows */
7620.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container, 7621.cart-kit-micro-pdp .add-to-box-mobile .add-to-cart-button-container-non-grouped, 7622.cart-kit-micro-pdp .add-to-box-mobile .input-group-mobile-container { 7623display: flex !important; 7624} 7625/* 2) Re-show the sticky/mobile bar at the bottom (itâs outside the form) */ 7626.cart-kit-micro-pdp .add-to-box-mobile .qty-and-add-to-cart-container { 7627display: flex !important; 7628} 7629/* Hide qty+button ONLY when it's NOT inside the sticky bottom bar */ 7630.cart-kit-micro-pdp .qty-and-add-to-cart-container:not(.add-to-box-mobile .qty-and-add-to-cart-container) { 7631display: none !important; 7632} 7633/* Kill common "extra" sections we don't need */ 7634#ss-main-content { margin-top: unset !important; display:none !important; } 7635#sticky-mobile-header{ display: none !important; } 7636.gqcs-swiper-container{ display: none !important; } 7637.product-wapper-tab { display: none !important; } 7638/* 3) Preview: make it a small thumbnail WITHOUT reflowing layouts 7639(works across templates: big hero, centered preview, swiper, etc.) */ 7640.cart-kit-micro-pdp .product.media, 7641.cart-kit-micro-pdp .product-image-carousel-container, 7642.cart-kit-micro-pdp .product-image-carousel-select, 7643.cart-kit-micro-pdp #ss-image-sheet, 7644.cart-kit-micro-pdp .gallery-placeholder, 7645.cart-kit-micro-pdp .product-img-content { 7646width: 100% !important; 7647max-width: 100% !important; 7648} 7649/* Center the preview area */ 7650.cart-kit-micro-pdp .product.media, 7651.cart-kit-micro-pdp .product-image-carousel-container, 7652.cart-kit-micro-pdp .product-image-carousel-select, 7653.cart-kit-micro-pdp .gallery-placeholder { 7654display: flex !important; 7655justify-content: center !important; 7656} 7657/* Image itself: cap size (no cropping) */ 7658.cart-kit-micro-pdp img.img-responsive, 7659.cart-kit-micro-pdp .product.media img, 7660.cart-kit-micro-pdp .product-image-photo, 7661.cart-kit-micro-pdp .fotorama__img, 7662.cart-kit-micro-pdp .swiper-slide img { 7663max-height: 300px !important; 7664max-width: 300px !important; 7665width: auto !important; 7666height: auto !important; 7667object-fit: contain !important; 7668margin: 0 !important; 7669} 7670/* Prevent swiper/nav chrome from adding vertical junk */ 7671/* Swiper UI: show dots when multiple images exist (arrows stay hidden) */ 7672.cart-kit-micro-pdp .swiper-button-prev, 7673.cart-kit-micro-pdp .swiper-button-next, 7674.cart-kit-micro-pdp .swiper-scrollbar {
7675display: none !important; 7676} 7677/* Dots ON (we'll hide them via JS when only 1 image) */ 7678.cart-kit-micro-pdp .swiper-pagination { 7679display: flex !important; 7680justify-content: center !important; 7681gap: 6px !important; 7682margin-top: 6px !important; 7683position: relative !important; 7684} 7685/* Make dots smaller + subtle so they don't add height */ 7686.cart-kit-micro-pdp .swiper-pagination-bullet { 7687width: 6px !important; 7688height: 6px !important; 7689opacity: 0.35 !important; 7690} 7691.cart-kit-micro-pdp .swiper-pagination-bullet-active { 7692opacity: 0.9 !important; 7693} 7694/* Tighten spacing under preview */ 7695.cart-kit-micro-pdp .product.media, 7696.cart-kit-micro-pdp .product-image-carousel-container, 7697.cart-kit-micro-pdp .product-image-carousel-select, 7698.cart-kit-micro-pdp .gallery-placeholder { 7699margin-bottom: 64px !important; 7700} 7701/* 4) Form focus */ 7702#product_addtocart_form, .product-add-form { 7703margin-top: 10px !important; 7704} 7705/* 5) Hide âExtra bottle of ink?â field (best effort selectors) 7706Note: :has() may not be supported everywhere; we also keep the JS fallback below */ 7707.field.extra_ink, .field.extra-ink, .field.extra_bottle_ink, 7708.product-options-wrapper .field:has(select[name*="extra"]) { 7709display: none !important; 7710} 7711.full-size-container{ 7712display:none; 7713} 7714`; 7715// Prevent double-inject if iframe reloads quickly 7716var existing = doc.getElementById('ck-micro-css'); 7717if (existing && existing.parentNode) existing.parentNode.removeChild(existing); 7718doc.head.appendChild(style); 7719// Force add-to-cart form to submit in TOP window so we return to cart normally 7720var form = 7721doc.querySelector('#product_addtocart_form') || 7722doc.querySelector('form[action*="checkout/cart/add"]'); 7723if (form) { 7724form.setAttribute('target', '_top'); 7725// Ensure return_url points back to cart (best effort) 7726var existingReturn = form.querySelector('input[name="return_url"]'); 7727if (!existingReturn) { 7728existingReturn = doc.createElement('input'); 7729existingReturn.type = 'hidden'; 7730existingReturn.name = 'return_url'; 7731form.appendChild(existingReturn); 7732} 7733existingReturn.value = (MICRO_PDP.returnUrl || window.location.href); 7734// Some Magento setups use return_url_encoded; set it if present 7735var encodedReturn = form.querySelector('input[name="return_url_encoded"]'); 7736if (encodedReturn) { 7737try { 7738encodedReturn.value = btoa(unescape(encodeURIComponent(MICRO_PDP.returnUrl || window.location.href))); 7739} catch (e) { } 7740} 7741// If there's a quantity input, default to 1 7742var qty = form.querySelector('input[name="qty"]'); 7743if (qty && !qty.value) qty.value = '1'; 7744// ---- LOADING CURSOR HOOK (reliable) ---- 7745try { 7746// Zephyr's ATC is id="product-addtocart-button" and type="button", 7747// so look for it FIRST -- a bare button[type="submit"] lookup can 7748// match an unrelated submit (search / newsletter) earlier in the DOM. 7749var btn = 7750doc.querySelector('button#product-addtocart-button') || 7751form.querySelector('button[type="submit"], input[type="submit"]') || 7752doc.querySelector('button.addtocart') || 7753doc.querySelector('button.action.tocart') || 7754doc.querySelector('button[type="submit"]') || 7755doc.querySelector('input[type="submit"]'); 7756if (btn && !btn.__ckBound) { 7757btn.__ckBound = true; 7758btn.addEventListener('click', function () { 7759// NOTE: arming happens in the handleAjaxAddToCart 7760// wrapper, not here -- the button the shopper actually 7761// clicks is the sticky bar's, which lives outside 7762// #product_addtocart_form and may not be this element. 7763setLoadingCursor(true); 7764if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); 7765}, { capture: true }); 7766} 7767if (!form.__ckBound) { 7768form.__ckBound = true; 7769form.addEventListener('submit', function () { 7770setLoadingCursor(true); 7771if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); 7772}, { capture: true }); 7773} 7774} catch (e) { 7775// If iframe isn't accessible due to cross-domain, this will throw. 7776} 7777} 7778// Hook the iframe's own cart store so the side cart cannot open in 7779// here, and so the AJAX add gives us a reliable success signal. 7780try { 7781var ckSrc = String(MICRO_PDP.iframe.getAttribute('src') || ''); 7782if (ckSrc && ckSrc.indexOf('about:blank') !== 0) bindMicroPdpAddBridge(); 7783} catch (e) { } 7784// ---------------------------------------------------- 7785// Cut everything below the shipping block (keep shipping visible) 7786// Anchor: .ships-and-made-in-usa-container 7787// ---------------------------------------------------- 7788(function () { 7789var d = MICRO_PDP.iframe.contentDocument; 7790if (!d) return; 7791var shipContainer = d.querySelector('.ships-and-made-in-usa-container'); 7792if (!shipContainer) return;
7793// Find a reasonable scope that contains the PDP âcontentâ sections 7794var scope = 7795shipContainer.closest('.vpt-product, .product-shop-content, .product-info-main, .column.main, main') 7796|| d.body; 7797// We want to hide everything AFTER the shipping container's row 7798var row = shipContainer.closest('.row') || shipContainer.parentElement; 7799if (!row) return; 7800var sib = row.nextElementSibling; 7801while (sib) { 7802sib.style.display = 'none'; 7803sib = sib.nextElementSibling; 7804} 7805})(); 7806// ---------------------------------------------------- 7807// Hide âExtra bottle of ink?â by label text (fallback) 7808// ---------------------------------------------------- 7809(function () { 7810var d = MICRO_PDP.iframe.contentDocument; 7811if (!d) return; 7812var labels = Array.from(d.querySelectorAll('label')); 7813var inkLabel = labels.find(function (l) { 7814return (l.textContent || '').toLowerCase().includes('extra bottle of ink'); 7815}); 7816if (!inkLabel) return; 7817var field = inkLabel.closest('.field, .control, .admin__field, .product-options-wrapper') || inkLabel.parentElement; 7818if (field) field.style.display = 'none'; 7819})(); 7820// Done loading 7821if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 7822} catch (err) { 7823// If cross-domain or blocked, just hide loader after a beat 7824if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 7825} 7826}); 7827// ---------------------------------------------------- 7828// MICRO-PDP ADD BRIDGE (added 2026-09-11) 7829// Zephyr's PDP add-to-cart control is 7830// <button type="button" onclick="handleAjaxAddToCart(this); return false;"> // It never submits the form, so the form.target='_top' hook further down -- 7831// which used to be the ONLY way the parent learned an add had happened -- is 7832// inert on a Zephyr PDP. The add is AJAX, and on success handleAjaxAddToCart 7833// calls Alpine.store('cart').openDrawer() INSIDE this iframe: the side cart 7834// slides out within the 960px modal and the parent cart page never updates. 7835// 7836// Fix: the iframe is same-origin, so we replace openDrawer() on the iframe's 7837// own cart store. That call is our exact "add succeeded" signal (it runs only 7838// in handleAjaxAddToCart's success branch) and the drawer never renders in the 7839// frame. We then reproduce the receipt state the old reload used to deliver in 7840// the URL -- via history.replaceState instead of navigating -- so the whole 7841// existing downstream pipeline (tile -> "added", tile swap, multi-SKU chaining) 7842// keeps working untouched. form.target='_top' stays as a fallback for any PDP 7843// that still submits natively. 7844// ---------------------------------------------------- 7845var MICRO_BRIDGE = { 7846timer: null, loadTimer: null, tries: 0, 7847armed: false, handled: false, 7848addPatched: false, storePatched: false, 7849countBefore: null 7850}; 7851// Zephyr exposes a running cart count at /zephyr/cart/count/ (verified live: 7852// returns {"count": N} where N is total UNITS, not lines -- so a qty bump on an 7853// existing line still moves it). Authoritative fallback for when the in-frame 7854// success signal never arrives, so a miss degrades to a late receipt, not silence. 7855function ckCartCount() { 7856try { 7857return fetch('/zephyr/cart/count/', { 7858credentials: 'same-origin', 7859headers: { 'X-Requested-With': 'XMLHttpRequest' } 7860}) 7861.then(function (r) { 7862if (!r.ok) throw new Error('cart count HTTP ' + r.status); 7863return r.json(); 7864}) 7865.then(function (j) { return (j && typeof j.count === 'number') ? j.count : null; }) 7866.catch(function (err) { 7867try { console.warn('[SmartCart] /zephyr/cart/count/ unavailable:', err && err.message); } catch (e) { } 7868return null; 7869}); 7870} catch (e) { 7871return Promise.resolve(null); 7872} 7873} 7874// A 200 from /checkout/cart/add is NOT proof the cart changed: OpenMage 7875// answers a bare add for a product it can't sell (missing from this store 7876// view, disabled, required options) with a 200 and an empty result, and 7877// postToCart() only throws on !res.ok. Without this the module shows a full 7878// success state -- drawer, toast, tile swap -- over an unchanged cart. 7879function ckVerifySimpleAdd(baseline) { 7880if (typeof baseline !== 'number') return; // no baseline: stay optimistic 7881setTimeout(function () { 7882ckCartCount().then(function (after) { 7883if (after === null || after > baseline) return; // added, or can't tell 7884try { console.warn('[SmartCart] add returned 200 but the cart did not change (baseline ' + baseline + ').'); } catch (e) { } 7885var msg = 'That item could not be added \u2014 it may be unavailable. Please try another.'; 7886try { if (statusEl) statusEl.textContent = msg; } catch (e) { } 7887try { 7888var fl = (window.Alpine && typeof window.Alpine.store === 'function') ? window.Alpine.store('flash') : null; 7889if (fl && typeof fl.show === 'function') fl.show('error', msg); 7890} catch (e) { } 7891// Don't leave a side cart open over an unchanged cart. 7892try { 7893var st = ckCartStore(window); 7894if (st && typeof st.closeDrawer === 'function') st.closeDrawer(); 7895} catch (e) { } 7896}); 7897}, 600); 7898} 7899// Clears the visual loading state ONLY. It must never disarm: the add can 7900// still be in flight, and disarming would make the openDrawer success signal
7901// get swallowed when it finally arrives. 7902function clearMicroLoadingState() { 7903try { setLoadingCursor(false); } catch (e) { } 7904try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { } 7905} 7906// Ask the server whether the add actually landed. Callers decide when it's 7907// worth asking; this only refuses to act if a receipt already fired. 7908// Polls rather than asking once. handleAjaxAddToCart returns a BOOLEAN, not a 7909// promise (measured on local 2026-09-14), so there is no completion callback -- 7910// a real add on this catalog takes ~3.5s. A single check right after the click 7911// always misses, and treating that miss as failure is what left the modal open. 7912function confirmMicroAddByCount(reason, attemptsLeft) { 7913if (MICRO_BRIDGE.handled) return; 7914if (typeof attemptsLeft !== 'number') attemptsLeft = 0; 7915if (MICRO_BRIDGE.countBefore === null) { 7916try { console.warn('[SmartCart] cannot confirm micro-PDP add, no cart-count baseline (' + reason + ').'); } catch (e) { } 7917clearMicroLoadingState(); 7918return; 7919} 7920var baseline = MICRO_BRIDGE.countBefore; 7921ckCartCount().then(function (after) { 7922if (MICRO_BRIDGE.handled) return; 7923if (after !== null && after > baseline) { 7924try { console.warn('[SmartCart] micro-PDP add confirmed by cart count (' + reason + ').'); } catch (e) { } 7925try { onMicroPdpAddSucceeded(); } catch (e) { } 7926return; 7927} 7928if (attemptsLeft > 0) { 7929setTimeout(function () { 7930confirmMicroAddByCount(reason, attemptsLeft - 1); 7931}, 1200); 7932return; 7933} 7934// Out of attempts. Drop the spinner but STAY ARMED -- if the add is 7935// simply slow, openDrawer will still arrive and fire the receipt. 7936clearMicroLoadingState(); 7937}); 7938} 7939function resetMicroBridge() { 7940try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7941try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7942MICRO_BRIDGE.timer = null; 7943MICRO_BRIDGE.loadTimer = null; 7944MICRO_BRIDGE.tries = 0; 7945MICRO_BRIDGE.armed = false; 7946MICRO_BRIDGE.addPatched = false; 7947MICRO_BRIDGE.storePatched = false; 7948// `handled` is deliberately NOT cleared -- closeMicroPdp() runs as part of a 7949// successful receipt. openMicroPdp() clears it, so the guard really is one 7950// receipt per configurator open. 7951} 7952// Arm on the ACTUAL add call, not a button click. Validation runs inside 7953// handleAjaxAddToCart, so armed only means "an add was attempted". 7954function armMicroBridge() { 7955MICRO_BRIDGE.armed = true; 7956try { setLoadingCursor(true); } catch (e) { } 7957try { if (MICRO_PDP.overlay) MICRO_PDP.overlay.classList.add('is-loading'); } catch (e) { } 7958// Long backstop only. The real resolution is onMicroAddSettled(), which 7959// fires the moment handleAjaxAddToCart's own promise settles -- so a slow 7960// upload resolves when it lands rather than being cut off by a fixed timer. 7961try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 7962MICRO_BRIDGE.loadTimer = setTimeout(function () { 7963if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return; 7964confirmMicroAddByCount('60s backstop', 0); 7965}, 60000); 7966} 7967// handleAjaxAddToCart returns its fetch promise. When it settles the add is 7968// decided: either our openDrawer signal already fired (handled), or it did not 7969// and we resolve it against the server instead of waiting out the backstop. 7970function onMicroAddSettled() { 7971// ~10s of polling (500ms + 8 x 1200ms), which comfortably covers a real 7972// add. The openDrawer signal normally wins long before this; this only 7973// catches an add that completes without ever reaching the Alpine store. 7974setTimeout(function () { 7975if (MICRO_BRIDGE.handled || !MICRO_BRIDGE.armed) return; 7976confirmMicroAddByCount('no in-frame signal yet', 8); 7977}, 500); 7978} 7979function bindMicroPdpAddBridge() { 7980try { if (MICRO_BRIDGE.timer) clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 7981MICRO_BRIDGE.timer = null; 7982MICRO_BRIDGE.tries = 0; 7983MICRO_BRIDGE.addPatched = false; 7984MICRO_BRIDGE.storePatched = false; 7985// Two hooks, both inside the iframe, both same-origin: 7986// 1. window.handleAjaxAddToCart -- wrapped to ARM on a real add call. 7987// This is the pattern ss-tier-upsell-zephyr.html v1.32 proved out; 7988// binding a button click is what it proved unreliable. 7989// 2. Alpine store cart.openDrawer -- the SUCCESS signal. Zephyr calls 7990// it only in handleAjaxAddToCart's success branch, and overriding it 7991// also keeps the side cart from rendering inside the 960px modal. 7992// handleAjaxAddToCart is an inline PDP script so it lands well before 7993// Alpine boots; poll for each independently. 7994MICRO_BRIDGE.timer = setInterval(function () { 7995MICRO_BRIDGE.tries++; 7996var w = null; 7997try { w = MICRO_PDP.iframe && MICRO_PDP.iframe.contentWindow; } catch (e) { w = null; } 7998if (!w) return; 7999if (!MICRO_BRIDGE.addPatched) { 8000try { 8001if (typeof w.handleAjaxAddToCart === 'function') { 8002if (w.handleAjaxAddToCart.__ckWrapped) { 8003MICRO_BRIDGE.addPatched = true; 8004} else { 8005var origAdd = w.handleAjaxAddToCart; 8006var wrapped = function () { 8007try { armMicroBridge(); } catch (eA) { } 8008var r; 8009try { 8010r = origAdd.apply(this, arguments); 8011} catch (eR) { 8012// Do NOT disarm here: origAdd may have dispatched 8013// the fetch before throwing, so the add can still 8014// land. Resolve it against the server like any other. 8015try { console.warn('[SmartCart] handleAjaxAddToCart threw:', eR && eR.message); } catch (eW) { } 8016try { onMicroAddSettled(); } catch (eS) { } 8017throw eR; 8018} 8019// The precise completion signal -- see onMicroAddSettled. 8020try { 8021// NOTE: on this catalog handleAjaxAddToCart returns a 8022// BOOLEAN, so the thenable branch is the exception, not 8023// the rule -- onMicroAddSettled() must therefore poll. 8024if (r && typeof r.then === 'function') r.then(onMicroAddSettled, onMicroAddSettled); 8025else onMicroAddSettled(); 8026} catch (eT) { } 8027return r; 8028}; 8029wrapped.__ckWrapped = true; 8030// The ATC uses an inline onclick, which resolves the 8031// global at call time -- so replacing it is enough. 8032w.handleAjaxAddToCart = wrapped; 8033MICRO_BRIDGE.addPatched = true; 8034} 8035} 8036} catch (eP1) { } 8037} 8038if (!MICRO_BRIDGE.storePatched) { 8039var st = ckCartStore(w); 8040if (st) { 8041try { 8042if (!st.__ckMicroPatched) { 8043st.__ckMicroPatched = true; 8044var suppress = function () { 8045// The side cart must never render in the configurator. 8046try { st.drawerOpen = false; } catch (e1) { } 8047try { 8048if (w && w.document && w.document.body) w.document.body.style.overflow = ''; 8049} catch (e2) { } 8050if (!MICRO_BRIDGE.armed) return; 8051try { onMicroPdpAddSucceeded(); } catch (e3) { } 8052}; 8053st.openDrawer = suppress; 8054st.toggleDrawer = suppress; 8055} 8056MICRO_BRIDGE.storePatched = true; 8057} catch (eP2) { } 8058} 8059} 8060if (MICRO_BRIDGE.addPatched && MICRO_BRIDGE.storePatched) { 8061try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 8062MICRO_BRIDGE.timer = null; 8063return; 8064} 8065if (MICRO_BRIDGE.tries > 120) { // ~6s 8066try { clearInterval(MICRO_BRIDGE.timer); } catch (e) { } 8067MICRO_BRIDGE.timer = null;
8068// Never give up silently -- that is how this class of bug hides. 8069try { 8070console.warn('[SmartCart] micro-PDP bridge gave up after 6s.', 8071'handleAjaxAddToCart patched:', MICRO_BRIDGE.addPatched, 8072'| cart store patched:', MICRO_BRIDGE.storePatched, 8073'| the count fallback still covers the add.'); 8074} catch (eW) { } 8075} 8076}, 50); 8077} 8078function onMicroPdpAddSucceeded() { 8079if (MICRO_BRIDGE.handled) return; // one receipt per configurator open 8080MICRO_BRIDGE.handled = true; 8081MICRO_BRIDGE.armed = false; 8082try { if (MICRO_BRIDGE.loadTimer) clearTimeout(MICRO_BRIDGE.loadTimer); } catch (e) { } 8083// Recreate, in the parent's own URL, the receipt state the old 8084// target="_top" reload used to deliver -- without navigating. 8085try { 8086if (MICRO_PDP.returnUrl) { 8087var ru = new URL(MICRO_PDP.returnUrl, window.location.origin); 8088window.history.replaceState({}, document.title, ru.pathname + ru.search + ru.hash); 8089} 8090} catch (e) { } 8091// If more hard SKUs are queued the next configurator opens in ~350ms -- 8092// don't pop the side cart underneath it. Reveal it only on the last one. 8093var moreQueued = false; 8094try { moreQueued = parseRemainingHardFromUrl().length > 0; } catch (e) { } 8095try { setLoadingCursor(false); } catch (e) { } 8096closeMicroPdp('added'); 8097ckAfterCartMutation(!moreQueued); 8098// Existing receipt pipeline: added-state on the tile, tile swap, strip the 8099// params, and chain the next hard SKU if one is queued. 8100try { handleMicroAddedFromQuery(); } catch (e) { } 8101} 8102function openMicroPdp(productUrl, titleText, productId, remainingHardIds, progressDone, progressTotal) { 8103ensureMicroPdpModal(); 8104MICRO_PDP.lastUrl = productUrl; 8105MICRO_PDP.lastProductId = String(productId || ''); 8106// Progress defaults (if not passed) 8107var progFromUrl = getMicroProgressFromUrl(); 8108if (typeof progressDone !== 'number') progressDone = progFromUrl.done; 8109if (typeof progressTotal !== 'number') progressTotal = progFromUrl.total; 8110MICRO_PDP.progressDone = progressDone || 0; 8111MICRO_PDP.progressTotal = progressTotal || 0; 8112// Paint header progress bar (only shows when total > 1) 8113renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal); 8114// Build a "receipt" return URL so the parent can show success + grey out tile + scroll 8115try { 8116var u = new URL(window.location.href); 8117// Set receipt params (overwrite if they already exist) 8118u.searchParams.set('microAdded', MICRO_PDP.lastProductId); 8119u.searchParams.set('microAddedName', titleText || ''); 8120// Carry micro progress counts 8121if (progressTotal && progressTotal > 0) { 8122u.searchParams.set('microTotal', String(progressTotal)); 8123u.searchParams.set('microDone', String(progressDone || 0)); 8124} else { 8125u.searchParams.delete('microTotal'); 8126u.searchParams.delete('microDone'); 8127} 8128// If we are chaining multiple HARD SKUs, persist the remaining list 8129if (Array.isArray(remainingHardIds) && remainingHardIds.length) { 8130u.searchParams.set('remainingHard', remainingHardIds.join(',')); 8131} else { 8132u.searchParams.delete('remainingHard'); 8133} 8134// Keep anchor consistent so user lands near module area 8135u.hash = 'cart-kit-upsell'; 8136MICRO_PDP.returnUrl = u.toString(); 8137} catch (e) { 8138// Fallback if URL API fails 8139MICRO_PDP.returnUrl = window.location.href + (window.location.search ? '&' : '?') 8140+ 'microAdded=' + encodeURIComponent(MICRO_PDP.lastProductId) 8141+ 'µAddedName=' + encodeURIComponent(titleText || '') 8142+ '#cart-kit-upsell'; 8143} 8144var titleEl = MICRO_PDP.overlay.querySelector('#ck-micropdp-title'); 8145if (titleEl) titleEl.textContent = titleText || 'Customize item'; 8146if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'flex'; 8147// Add a cache-buster so iframe refreshes if same SKU reopened 8148var url = ckSameOrigin(productUrl); 8149url += (url.indexOf('?') > -1 ? '&' : '?') + 'cartUpsellMicro=1&_=' + Date.now(); 8150// Fresh receipt state + a count snapshot for the fallback in armMicroBridge. 8151MICRO_BRIDGE.handled = false; 8152MICRO_BRIDGE.countBefore = null; 8153MICRO_BRIDGE.countGen = (MICRO_BRIDGE.countGen || 0) + 1; 8154try { 8155// Tagged so a slow response from the PREVIOUS chain item (opens ~350ms 8156// apart) can't land here and produce a false-positive receipt. 8157(function (gen) { 8158ckCartCount().then(function (n) { 8159if (gen === MICRO_BRIDGE.countGen) MICRO_BRIDGE.countBefore = n; 8160}); 8161})(MICRO_BRIDGE.countGen); 8162} catch (e) { }
8163MICRO_PDP.iframe.src = url; 8164MICRO_PDP.overlay.classList.add('is-open'); 8165pushUpsellEvent('SmartCart_upsell_micro_pdp_open', { url: productUrl }); 8166} 8167function closeMicroPdp(reason) { 8168if (!MICRO_PDP.overlay) return; 8169var added = (reason === 'added'); 8170var ckWasArmed = MICRO_BRIDGE.armed; 8171try { resetMicroBridge(); } catch (e) { } 8172// Shopper closed while an add was in flight. Don't drop it on the floor -- 8173// check the server before discarding the tile state and the chain. 8174if (!added && ckWasArmed) { 8175try { confirmMicroAddByCount('closed mid-add', 3); } catch (e) { } 8176} 8177try { setLoadingCursor(false); } catch (e) { } 8178try { MICRO_PDP.overlay.classList.remove('is-loading'); } catch (e) { } 8179MICRO_PDP.overlay.classList.remove('is-open'); 8180// Stop network / reset iframe 8181if (MICRO_PDP.iframe) MICRO_PDP.iframe.src = 'about:blank'; 8182if (MICRO_PDP.loading) MICRO_PDP.loading.style.display = 'none'; 8183// Tell anything listening that the micro PDP closed 8184try { 8185document.dispatchEvent(new CustomEvent('smartcart:microPdpClosed', { detail: { reason: reason || 'close' } })); 8186} catch (e) { } 8187// CRITICAL: refresh cart UI and un-stick the SmartCart state 8188Promise.resolve() 8189.then(function () { 8190// Force the same refresh path we use after adds 8191// 1) Reload Magento customer-data cart section (minicart + cart counters) 8192// 2) Refresh cart DOM fragments (table/totals) if weâre on the cart page 8193// Legacy (pre-Zephyr) refresh path -- a no-op on Zephyr, kept for safety. 8194return reloadCustomerDataCart() 8195.then(refreshCartDomIfOnCartPage) 8196.then(function () { 8197// After an add the caller syncs; on a plain close re-sync 8198// anyway in case the shopper added inside the configurator 8199// by some other route. 8200if (!added) ckAfterCartMutation(false); 8201}); 8202}) 8203.then(function () { 8204// reset CTA back to normal (user cancelled hard sku) 8205try { 8206addBtn.classList.remove('is-loading'); 8207// This tail resolves after handleMicroAddedFromQuery() has run. 8208// On a SUCCESSFUL add the receipt pipeline owns the status line, 8209// the progress bar and the tile grid -- buildGrid() in particular 8210// would re-render over replaceAddedTileWithNext()'s swap. Touch 8211// none of it unless the shopper cancelled. 8212if (!added) { 8213addBtn.disabled = false; 8214addBtn.textContent = 'Add to Cart'; 8215if (statusEl) statusEl.textContent = 'No worries â you can keep shopping or add something else.'; 8216resetAddProgressUI(); 8217} 8218// Safe on BOTH paths and required on the added one: derives the 8219// CTA's disabled state and label from the live selection, so a 8220// completed add leaves it correctly disabled rather than enabled 8221// with nothing selected. 8222updatePriceUI(); 8223} catch (e) { } 8224// Rebuild tiles so "already in cart" suppression is correct 8225if (!added) { try { buildGrid(); } catch (e) { } } 8226}) 8227.catch(function () { 8228// Even if refresh fails, never leave the UI stuck 8229try { 8230addBtn.disabled = false; 8231addBtn.classList.remove('is-loading'); 8232addBtn.textContent = 'Add to Cart'; 8233} catch (e) { } 8234}); 8235} 8236// ---------------------------------------------------- 8237// 8) Selection state 8238// ---------------------------------------------------- 8239var selectedPrimaryIds = new Set(); // stamp IDs OR notary product IDs (strings) 8240var selectedInkColors = new Set(); 8241var inkChoiceMade = false; 8242// Track tiles that have been added via Micro PDP (so we can grey them out) 8243var microAddedIds = new Set(); 8244var ckSeenTileIds = new Set(); 8245// Defaults 8246var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0); 8247if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId)); 8248else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id); 8249// ---------------------------------------------------- 8250// 9) Build grid (Accent or Notary) 8251// ---------------------------------------------------- 8252var NOTARY_HYDRATED = new Map(); // productId -> {productId, productUrl, name, img, price} 8253function buildGrid() { 8254// Build cart signals for duplicate suppression (4C) 8255var __cartPaths = Object.create(null); 8256try { __cartPaths = getCartProductPathSet(); } catch (eCP0) { } 8257stampGrid.innerHTML = ''; 8258// Accent stamps (unchanged) 8259if (!useTiles) { 8260for (var i = 0; i < STAMP_OPTIONS.length; i++) { 8261var opt = STAMP_OPTIONS[i]; 8262var card = document.createElement('button'); 8263card.type = 'button'; 8264card.className = 'kit-stamp-card'; 8265card.setAttribute('data-kit-primary', opt.id); 8266card.innerHTML = 8267'<div class="kit-stamp-chip">' 8268+ '<div class="kit-stamp-chip__circle">' 8269+ '<img class="kit-stamp-chip__image"' 8270+ ' src="' + opt.imgSrc + '"' 8271+ ' alt="' + (opt.imgAlt || '') + '"' 8272+ (opt.imgStyle ? ' style="' + opt.imgStyle + '"' : '') 8273+ '/>' 8274+ '</div>' 8275+ '</div>'; 8276if (selectedPrimaryIds.has(opt.id)) card.classList.add('is-selected'); 8277stampGrid.appendChild(card); 8278} 8279// Step 1: sync tile state store after grid render 8280try { TileStore.initFromDom(mount); } catch (e) { } 8281return; 8282} 8283// Notary product tiles (PDP fetched) 8284// Empty-state: if everything is already in cart (or filtered), show "All set" instead of a blank module. 8285if (!TILE_ITEMS || TILE_ITEMS.length === 0) { 8286// Nothing to build. (The old code here appended an "all set" note next 8287// to an undefined `tilesGrid` and tried to disable a #kit-add-btn that 8288// has never existed in this markup -- both silent no-ops.) 8289ckSyncModuleEmptyState(); 8290return; 8291} 8292for (var j = 0; j < TILE_ITEMS.length; j++) { 8293(function (raw) { 8294var pid = String(raw.productId || ''); 8295// Keep a reference so we can apply persona rules later (micro PDP gating, etc.) 8296TILE_ITEM_BY_ID[pid] = raw; 8297var card = document.createElement('button'); 8298card.type = 'button'; 8299card.className = 'kit-stamp-card kit-product-card'; 8300card.setAttribute('data-kit-primary', pid); 8301card.setAttribute('data-sku', pid); 8302card.setAttribute('data-kit-url', raw.productUrl || ''); 8303if (microAddedIds.has(pid)) { 8304card.classList.add('is-added'); 8305} 8306card.innerHTML = 8307'<div class="kit-product-thumb-tile"><span style="font-size:12px;color:#888;">Loadingâ¦</span></div>' 8308+ '<div class="kit-product-meta">' 8309+ '<p class="kit-product-name">Loadingâ¦</p>' 8310+ '<div class="kit-product-price">â</div>' 8311+ '<div class="kit-product-sub">' + (persona.tileSubtext || '') + '</div>' 8312+ '</div>'; 8313if (selectedPrimaryIds.has(pid)) card.classList.add('is-selected'); 8314stampGrid.appendChild(card); 8315fetchPdpData(raw.productUrl).then(function (data) { 8316// Guard: this tile may have been swapped; ignore stale hydration 8317if (String(card.getAttribute('data-kit-primary') || '') !== String(pid)) return; 8318// Quality gate: this URL did not resolve to a real product page, 8319// so do NOT render whatever came back. Substitute an approved 8320// alternative the shopper doesn't already have, else drop the slot. 8321if (!ckIsUsableTileData(data)) { 8322try { console.warn('[SmartCart] tile ' + pid + ' did not resolve to a product page; substituting.'); } catch (eW) { } 8323var alt = ckPickFallbackTile(); 8324if (alt) ckRepaintCardAs(card, alt); 8325else ckDropTile(card); 8326return; 8327} 8328var hydrated = { 8329productId: pid, 8330productUrl: raw.productUrl, 8331name: data.name || 'Notary add-on', 8332img: data.img || null, 8333price: (data.price != null ? Number(data.price) : null) 8334}; 8335try { hydrated.sku = data.sku; } catch (eSk1) { } 8336NOTARY_HYDRATED.set(pid, hydrated); 8337var nameEl = card.querySelector('.kit-product-name'); 8338var priceEl = card.querySelector('.kit-product-price'); 8339var thumbEl = card.querySelector('.kit-product-thumb-tile'); 8340if (nameEl) nameEl.textContent = hydrated.name; 8341if (priceEl) priceEl.textContent = (hydrated.price != null ? ('$' + hydrated.price.toFixed(2)) : 'See price in cart'); 8342if (thumbEl) { 8343if (hydrated.img) thumbEl.innerHTML = '<img src="' + hydrated.img + '" alt="" />'; 8344else thumbEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 8345} 8346if (selectedPrimaryIds.has(pid)) updatePriceUI(); 8347}); 8348})(TILE_ITEMS[j]); 8349} 8350fireTilesImpressionOnce(); 8351// Step 1: sync tile state store after grid render 8352try { TileStore.initFromDom(mount); } catch (e) { } 8353ckSyncModuleEmptyState(); 8354} 8355// ---------------------------------------------------- 8356// 10) Pricing 8357// ---------------------------------------------------- 8358var priceLabelEl = upsell.querySelector('#kit-price-label'); 8359var priceNewEl = upsell.querySelector('#kit-price-new'); 8360function computeTotals() { 8361var inkCount = selectedInkColors.size; 8362if (!useTiles) { 8363var stampCount = selectedPrimaryIds.size; 8364return { 8365primaryCount: stampCount, 8366inkCount: inkCount, 8367total: (stampCount * STAMP_UNIT_PRICE) + (inkCount * INK_PRICE) 8368}; 8369} 8370// Notary: sum selected notary items (real PDP price if fetched; otherwise 0 until loaded) 8371var total = 0; 8372var count = 0; 8373selectedPrimaryIds.forEach(function (pid) { 8374var it = NOTARY_HYDRATED.get(String(pid)); 8375count += 1; 8376total += (it && it.price != null) ? Number(it.price) : 0; 8377}); 8378total += inkCount * INK_PRICE; 8379return { primaryCount: count, inkCount: inkCount, total: total }; 8380} 8381function updatePriceUI() { 8382var totals = computeTotals(); 8383if (!totals.primaryCount) { 8384priceLabelEl.textContent = 'Total:'; 8385priceNewEl.textContent = '$0.00'; 8386addBtn.disabled = true; 8387addBtn.textContent = 'Add to Cart'; 8388return; 8389} 8390priceLabelEl.textContent = 'Total:'; 8391priceNewEl.textContent = '$' + totals.total.toFixed(2); 8392// CTA label: if any selected item requires configuration, guide user into Micro PDP. 8393var hasHard = false;
8394try { 8395Array.from(selectedPrimaryIds).some(function (pid) { 8396var raw = (TILE_ITEM_BY_ID && TILE_ITEM_BY_ID[String(pid)]) ? TILE_ITEM_BY_ID[String(pid)] : null; 8397if (requiresMicroPdp(pid, raw)) { hasHard = true; return true; } 8398return false; 8399}); 8400} catch (e) { } 8401addBtn.textContent = hasHard ? 'Customize & Add' : 'Add to Cart'; 8402addBtn.disabled = false; 8403} 8404// ---------------------------------------------------- 8405// 11) Build + init 8406// ---------------------------------------------------- 8407buildGrid(); 8408updatePriceUI(); 8409pushUpsellEvent('SmartCart_upsell_shown', { matched: activePersonaKey !== 'generic' }); 8410// ---------------------------------------------------- 8411// 12) Interactions 8412// ---------------------------------------------------- 8413header.addEventListener('click', function () { 8414var isOpen = upsell.classList.toggle('cart-kit-upsell--open'); 8415body.hidden = !isOpen; 8416}); 8417// Primary selection (stamps or notary items) 8418stampGrid.addEventListener('click', function (evt) { 8419var card = evt.target.closest('.kit-stamp-card'); 8420if (!card) return; if (card.classList && card.classList.contains('is-complete')) return; 8421var id = String(card.getAttribute('data-kit-primary') || ''); 8422if (!id) return; 8423// ---- TRACK: tile click ---- 8424try { 8425if (useTiles) { 8426// find tile position in TILE_ITEMS 8427var pos = -1; 8428var rawTile = null; 8429for (var i = 0; i < TILE_ITEMS.length; i++) { 8430if (String(TILE_ITEMS[i].productId) === String(id)) { 8431pos = i + 1; 8432rawTile = TILE_ITEMS[i]; 8433break; 8434} 8435} 8436var hydrated = NOTARY_HYDRATED.get(String(id)); 8437pushUpsellEvent('SmartCart_upsell_tile_click', { 8438persona: activePersonaKey || 'generic', 8439upsell_mode: 'tiles', 8440product_id: id, 8441position: pos, 8442product_name: hydrated && hydrated.name ? hydrated.name : undefined, 8443product_price: hydrated && hydrated.price != null ? hydrated.price : undefined, 8444product_url: rawTile && rawTile.productUrl ? rawTile.productUrl : undefined, 8445requires_micro: rawTile ? requiresMicroPdp(id, rawTile) : undefined, 8446already_selected: selectedPrimaryIds.has(id) 8447}); 8448} else { 8449// Accent stamp click 8450pushUpsellEvent('SmartCart_upsell_accent_click', { 8451stamp_id: id 8452}); 8453} 8454} catch (e) { } 8455// ---------------------------------------------------- 8456// Tile tracking + micro PDP routing (tiles only) 8457// ---------------------------------------------------- 8458var raw = null; 8459if (useTiles) { 8460for (var k = 0; k < TILE_ITEMS.length; k++) { 8461if (String(TILE_ITEMS[k].productId) === String(id)) { raw = TILE_ITEMS[k]; break; } 8462} 8463if (raw && raw.productUrl) { 8464// If we already know it's micro (tile config, hard override, or cached autodetect) 8465if (requiresMicroPdp(id, raw)) { 8466// Hard SKU: tile click should ONLY toggle selection. 8467// Do not open Micro PDP here; Micro PDP opens only from the main CTA. 8468} 8469// If we don't have PDP cached yet, fetch once and decide (airbag) 8470if (!PDP_CACHE.has(raw.productUrl)) { 8471evt.preventDefault(); 8472evt.stopPropagation(); 8473setLoadingCursor(true); 8474fetchPdpData(raw.productUrl).then(function (data) { 8475setLoadingCursor(false); 8476// If autodetect says "micro", open modal 8477if (data && data.requiresMicro) { 8478// NOTE: Do NOT auto-open Micro PDP on tile click. 8479// We only open Micro PDP when the user explicitly clicks the CTA to add. 8480pushUpsellEvent('SmartCart_upsell_tile_micro_detected', { productId: id, productUrl: raw.productUrl }); 8481} 8482// Otherwise proceed with normal select toggle now 8483var willSelect = !selectedPrimaryIds.has(id); 8484if (willSelect) { 8485selectedPrimaryIds.add(id); 8486card.classList.add('is-selected'); 8487} else { 8488selectedPrimaryIds.delete(id); 8489card.classList.remove('is-selected'); 8490} 8491pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), { 8492productId: id, 8493selected_count: selectedPrimaryIds.size 8494}); 8495updatePriceUI(); 8496}).catch(function () { 8497setLoadingCursor(false); 8498// On failure, fall back to normal select so UX doesn't dead-end 8499var willSelect2 = !selectedPrimaryIds.has(id); 8500if (willSelect2) { 8501selectedPrimaryIds.add(id); 8502card.classList.add('is-selected'); 8503} else { 8504selectedPrimaryIds.delete(id); 8505card.classList.remove('is-selected'); 8506} 8507pushUpsellEvent('SmartCart_upsell_tile_select_fallback', { 8508productId: id, 8509// This path fires on DESELECT as well as select. 8510// Without this flag the GA4 mirror would count a 8511// deselect as a select_item. 8512selected: willSelect2, 8513selected_count: selectedPrimaryIds.size 8514}); 8515updatePriceUI(); 8516}); 8517return; 8518} 8519} 8520} 8521var willSelect = !selectedPrimaryIds.has(id); 8522if (willSelect) { 8523selectedPrimaryIds.add(id); 8524card.classList.add('is-selected'); 8525} else { 8526selectedPrimaryIds.delete(id); 8527card.classList.remove('is-selected'); 8528} 8529if (useTiles) { 8530pushUpsellEvent('SmartCart_upsell_tile_' + (willSelect ? 'selected' : 'deselected'), { 8531productId: id, 8532selected_count: selectedPrimaryIds.size 8533}); 8534} else { 8535pushUpsellEvent('SmartCart_upsell_accent_' + (willSelect ? 'selected' : 'deselected'), { 8536stamp_id: id, 8537selected_count: selectedPrimaryIds.size 8538}); 8539} 8540updatePriceUI(); 8541}); 8542// Ink multi-select with "none" 8543if (swatchList) { 8544swatchList.addEventListener('click', function (evt) { 8545var swatch = evt.target.closest('.kit-swatch'); 8546if (!swatch) return; 8547if (swatch.style.display === 'none') return; 8548var colorKey = swatch.getAttribute('data-kit-color'); 8549inkChoiceMade = true;
8550if (colorKey === 'none') { 8551selectedInkColors.clear(); 8552var all = swatchList.querySelectorAll('.kit-swatch'); 8553for (var i = 0; i < all.length; i++) { 8554var s = all[i]; 8555if (s.style.display === 'none') continue; 8556s.classList.toggle('is-selected', s.getAttribute('data-kit-color') === 'none'); 8557} 8558} else { 8559if (selectedInkColors.has(colorKey)) selectedInkColors.delete(colorKey); 8560else selectedInkColors.add(colorKey); 8561var hasAnyInk = selectedInkColors.size > 0; 8562var all2 = swatchList.querySelectorAll('.kit-swatch'); 8563for (var j = 0; j < all2.length; j++) { 8564var b = all2[j]; 8565if (b.style.display === 'none') continue; 8566var key = b.getAttribute('data-kit-color'); 8567if (key === 'none') b.classList.toggle('is-selected', !hasAnyInk); 8568else b.classList.toggle('is-selected', selectedInkColors.has(key)); 8569} 8570} 8571updatePriceUI(); 8572}); 8573} 8574if (hideBtn) { 8575hideBtn.addEventListener('click', function (evt) { 8576evt.preventDefault(); 8577upsell.classList.remove('cart-kit-upsell--open'); 8578body.hidden = true; 8579upsell.scrollIntoView({ behavior: 'smooth', block: 'start' }); 8580}); 8581} 8582// ---------------------------------------------------- 8583// 13) Add-to-cart progress bar helpers (ONLY progress bar) 8584// ---------------------------------------------------- 8585function resetAddProgressUI() { 8586if (progressWrap) progressWrap.style.display = 'none'; 8587if (progressFill) progressFill.style.width = '0%'; 8588if (progressPct) progressPct.textContent = '0%'; 8589if (progressText) progressText.textContent = ''; 8590} 8591function setAddProgress(done, total) { 8592if (!progressWrap || !progressFill) return; 8593var pct = total ? Math.round((done / total) * 100) : 0; 8594progressWrap.style.display = ''; 8595progressFill.style.width = pct + '%'; 8596if (progressPct) progressPct.textContent = pct + '%'; 8597if (progressText) progressText.textContent = 'Adding ' + done + ' of ' + total + ' itemsâ¦'; 8598if (statusEl) statusEl.textContent = 'Added ' + done + ' of ' + total + ' items'; 8599} 8600// ---------------------------------------------------- 8601// 14) Add to Cart 8602// ---------------------------------------------------- 8603function postToCart(params) { 8604return fetch('/checkout/cart/add', { 8605method: 'POST', 8606headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 8607body: params.toString(), 8608credentials: 'same-origin' 8609}).then(function (res) { 8610if (!res.ok) throw new Error('Add to cart failed: ' + res.status); 8611return res; 8612}); 8613} 8614// ---------------------------------------------------- 8615// Cart sync helpers (run after async adds so UI + tile logic are up-to-date) 8616// ---------------------------------------------------- 8617function reloadCustomerDataCart() { 8618// Zephyr: /customer/section/load/ 404s on this build (no M2 customer-data) 8619// and the Alpine store is the real cart state. Skip the hop entirely. 8620if (ckCartStore(window)) return Promise.resolve(); 8621return new Promise(function (resolve) { 8622try { 8623if (window.require) { 8624window.require(['Magento_Customer/js/customer-data'], function (customerData) { 8625try { 8626var r = customerData.reload(['cart'], true); 8627if (r && r.always) return r.always(function () { resolve(); }); 8628resolve(); 8629} catch (e) { resolve(); } 8630}, function () { resolve(); }); 8631return; 8632} 8633fetch('/customer/section/load/?sections=cart&force_new_section_timestamp=true', { 8634credentials: 'same-origin' 8635}).then(function () { resolve(); }).catch(function () { resolve(); }); 8636} catch (e) { resolve(); } 8637}); 8638} 8639function refreshCartDomIfOnCartPage() { 8640// Zephyr: the cart page is Alpine-rendered. This swaps innerHTML on 8641// .cart-summary / [data-block="cart-summary"] if either matches, which 8642// would destroy live Alpine bindings in the Order Summary -- and it costs 8643// a full-page HTML fetch every time. ckAfterCartMutation() is the Zephyr 8644// path; this one is for the legacy theme only. 8645if (ckCartStore(window)) return Promise.resolve(); 8646return new Promise(function (resolve) { 8647try { 8648// Cart page fragments we attempt to refresh. Selectors vary by theme. 8649var table = document.querySelector('#shopping-cart-table'); 8650var totals = document.querySelector('#cart-totals') 8651|| document.querySelector('.cart-summary') 8652|| document.querySelector('.cart-totals') 8653|| document.querySelector('[data-block="cart-summary"]') 8654|| document.querySelector('[data-role="cart-totals"]'); 8655if (!table && !totals) return resolve(); 8656// Bust caches (some themes/CDNs will otherwise hand back stale totals) 8657var url = String(window.location.href || '').split('#')[0]; 8658url += (url.indexOf('?') > -1 ? '&' : '?') + 'ckts=' + Date.now(); 8659fetch(url, { credentials: 'same-origin', cache: 'no-store' }) 8660.then(function (r) { return r.text(); }) 8661.then(function (html) { 8662try { 8663var doc = new DOMParser().parseFromString(html, 'text/html'); 8664var newTable = doc.querySelector('#shopping-cart-table'); 8665if (table && newTable) table.innerHTML = newTable.innerHTML; 8666// Totals / summary fragment (try multiple fallbacks)
8667var newTotals = doc.querySelector('#cart-totals') 8668|| doc.querySelector('.cart-summary') 8669|| doc.querySelector('.cart-totals') 8670|| doc.querySelector('[data-block="cart-summary"]') 8671|| doc.querySelector('[data-role="cart-totals"]'); 8672if (totals && newTotals) totals.innerHTML = newTotals.innerHTML; 8673try { document.body.dispatchEvent(new Event('contentUpdated')); } catch (e) { } 8674} catch (e2) { } 8675resolve(); 8676}) 8677.catch(function () { resolve(); }); 8678} catch (e) { resolve(); } 8679}); 8680} 8681function syncCartAfterAdds() { 8682return reloadCustomerDataCart() 8683.then(refreshCartDomIfOnCartPage) 8684.catch(function () { /* silent */ }); 8685} 8686addBtn.addEventListener('click', function () { 8687if (!selectedPrimaryIds.size) { 8688alert(useTiles ? 'Please choose at least one add-on.' : 'Please choose at least one stamp design.'); 8689return; 8690} 8691if (swatchList && !inkChoiceMade) { 8692alert('Please select an ink pad color or select you already have an ink pad'); 8693return; 8694} 8695var formKeyInput = document.querySelector('input[name="form_key"]'); 8696var formKey = formKeyInput ? formKeyInput.value : null; 8697if (!formKey) { 8698console.error('Cart kit upsell: form_key not found.'); 8699alert('Sorry, we could not add the kit to your cart. Please refresh and try again.'); 8700return; 8701} 8702// Baseline for the post-add reality check (see ckVerifySimpleAdd). 8703var ckSimpleCountBefore = null; 8704try { ckCartCount().then(function (n) { ckSimpleCountBefore = n; }); } catch (eCB) { } 8705var primaryArr = Array.from(selectedPrimaryIds); 8706var inkArr = Array.from(selectedInkColors); 8707// ---------------------------------------------------- 8708// Hard SKU gating: 8709// - NEVER add "hard" products directly (they need options/uploads) 8710// - Only open Micro PDP when user clicks the CTA 8711// ---------------------------------------------------- 8712var hardPrimaryArr = []; 8713var simplePrimaryArr = []; 8714primaryArr.forEach(function (pid) { 8715var rawTile = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? TILE_ITEM_BY_ID[String(pid)] : null; 8716if (requiresMicroPdp(pid, rawTile)) { 8717hardPrimaryArr.push(pid); 8718} else { 8719simplePrimaryArr.push(pid); 8720} 8721}); 8722// Only count items we can actually add via POST chain (hard items are handled via Micro PDP) 8723var totalItemsCount = simplePrimaryArr.length + inkArr.length; 8724// If the user only selected hard items, we skip the POST chain and jump straight to Micro PDP. 8725var willOpenMicroAfterChain = hardPrimaryArr.length > 0; 8726pushUpsellEvent('SmartCart_upsell_add_clicked', { 8727primary_count: simplePrimaryArr.length, 8728hard_primary_count: hardPrimaryArr.length, 8729ink_count: inkArr.length, 8730total_count: totalItemsCount 8731}); 8732var completedCount = 0; 8733// If there's nothing to POST (only hard items), open Micro PDP immediately. 8734if (totalItemsCount === 0 && willOpenMicroAfterChain) { 8735var firstHard = hardPrimaryArr[0]; 8736var rawFirst = TILE_ITEM_BY_ID[String(firstHard)] || {}; 8737var hydratedFirst = NOTARY_HYDRATED.get(String(firstHard)); 8738var titleFirst = (hydratedFirst && hydratedFirst.name) ? hydratedFirst.name : (rawFirst.title || rawFirst.name || 'Customize item'); 8739// Seed Micro PDP progress for HARD-only flows 8740// done = 0 (nothing added yet), total = number of hard items selected 8741var progDone0 = 0; 8742var progTotal0 = hardPrimaryArr.length; 8743openMicroPdp(rawFirst.productUrl, titleFirst, firstHard, hardPrimaryArr.slice(1), progDone0, progTotal0); 8744return; 8745} 8746addBtn.disabled = true; 8747addBtn.classList.add('is-loading'); 8748addBtn.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' itemsâ¦') : 'Adding itemâ¦'; 8749if (statusEl) statusEl.textContent = totalItemsCount > 1 ? ('Adding ' + totalItemsCount + ' items to your cartâ¦') : 'Adding item to your cartâ¦'; 8750// show + init progress bar 8751setAddProgress(0, totalItemsCount); 8752var chain = Promise.resolve(); 8753// Add primary selections 8754simplePrimaryArr.forEach(function (id) { 8755chain = chain.then(function () { 8756var params = new URLSearchParams(); 8757params.append('form_key', formKey); 8758if (!useTiles) { 8759params.append('product', id); 8760params.append('qty', '1'); 8761params.append('options[' + ACCENT_SIZE_OPTION_ID + '][]', ACCENT_SIZE_VALUE_ID); 8762params.append('options[' + ACCENT_INK_OPTION_ID + ']', ACCENT_INK_NO_THANKS_VALUE_ID); 8763} else { 8764params.append('product', id); 8765params.append('qty', '1'); 8766} 8767return postToCart(params).then(function () { 8768completedCount += 1; 8769setAddProgress(completedCount, totalItemsCount); 8770}); 8771}); 8772}); 8773// Add ink pads
8774inkArr.forEach(function (colorKey) { 8775chain = chain.then(function () { 8776var colorValueId = INK_COLOR_VALUES[colorKey]; 8777if (!colorValueId) throw new Error('Unknown ink color key: ' + colorKey); 8778var inkParams = new URLSearchParams(); 8779inkParams.append('form_key', formKey); 8780inkParams.append('product', INK_PARENT_ID); 8781inkParams.append('qty', '1'); 8782inkParams.append('options[' + INK_SIZE_OPTION_ID + ']', INK_SIZE_VALUE_ID); 8783inkParams.append('options[' + INK_COLOR_OPTION_ID + ']', colorValueId); 8784return postToCart(inkParams).then(function () { 8785completedCount += 1; 8786setAddProgress(completedCount, totalItemsCount); 8787}); 8788}); 8789}); 8790chain 8791.then(function () { 8792pushUpsellEvent('SmartCart_upsell_added', { 8793// Per-product ids drive the GA4 add_to_cart mirror. 8794product_ids: simplePrimaryArr.map(function (p) { return String(p); }), 8795ink_colors: inkArr.slice(), 8796primary_count: simplePrimaryArr.length, 8797hard_primary_count: hardPrimaryArr.length, 8798ink_count: inkArr.length, 8799total_count: totalItemsCount 8800}); 8801// CRITICAL: sync cart state + DOM BEFORE swap/dedupe logic runs 8802return syncCartAfterAdds().then(function () { 8803if (willOpenMicroAfterChain) { 8804// The simple items are already in the cart but the configurator 8805// is about to take over the screen -- push them into both cart 8806// trees now, or the drawer, badge and cart column sit stale for 8807// as long as the shopper spends configuring. 8808try { ckAfterCartMutation(false); } catch (eZ) { } 8809// Mark the items we successfully added (simple + ink) without reloading. 8810try { 8811simplePrimaryArr.forEach(function (pid) { 8812var el = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]'); 8813if (el) { 8814el.classList.remove('is-selected'); 8815el.classList.add('is-added'); 8816} 8817try { microAddedIds.add(String(pid)); } catch (eM0) { } 8818try { selectedPrimaryIds.delete(pid); } catch (eD0) { } 8819// Swap AFTER cart sync so in-cart suppression works 8820setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR0) { } }, 800); 8821}); 8822} catch (e) { } 8823try { 8824// Clear ink selections after adding ink pads 8825selectedInkColors.clear(); 8826inkChoiceMade = false; 8827if (swatchList) { 8828var allSw = swatchList.querySelectorAll('.kit-swatch'); 8829for (var s = 0; s < allSw.length; s++) allSw[s].classList.remove('is-selected'); 8830} 8831} catch (e2) { } 8832addBtn.disabled = false; 8833addBtn.classList.remove('is-loading'); 8834addBtn.textContent = 'Customize & Add'; 8835if (statusEl) statusEl.textContent = 'Added to cart. Next, customize your selected item.'; 8836// Open Micro PDP for the first hard item selected 8837var firstHard2 = hardPrimaryArr[0]; 8838var rawFirst2 = TILE_ITEM_BY_ID[String(firstHard2)] || {}; 8839var hydratedFirst2 = NOTARY_HYDRATED.get(String(firstHard2)); 8840var titleFirst2 = (hydratedFirst2 && hydratedFirst2.name) ? hydratedFirst2.name : (rawFirst2.title || rawFirst2.name || 'Customize item'); 8841// Seed Micro PDP progress for mixed flows: 8842// done = count of items already added via POST chain (simple + ink) 8843// total = done + remaining hard items to configure 8844var progDone1 = totalItemsCount; 8845var progTotal1 = totalItemsCount + hardPrimaryArr.length; 8846openMicroPdp(rawFirst2.productUrl, titleFirst2, firstHard2, hardPrimaryArr.slice(1), progDone1, progTotal1); 8847pushUpsellEvent('SmartCart_upsell_micro_pdp_open_from_cta', { 8848productId: firstHard2, 8849remaining_hard_count: hardPrimaryArr.length 8850}); 8851// Keep progress visible but "complete" the chain we ran 8852setAddProgress(totalItemsCount, totalItemsCount); 8853return; // prevent redirect 8854} 8855// If we are on the full cart page, do a hard refresh so Magento totals/summary 8856// update correctly (these often won't update reliably via partial DOM swaps). 8857// We preserve the success-state UX via ?accentAdded=1. 8858// ---------------------------------------------------- 8859// Zephyr: the rest of this site mutates the cart in place, so do 8860// the same here instead of reloading. ckAfterCartMutation refreshes 8861// BOTH cart trees (drawer store + cart-page component) and pops the 8862// side cart. It returns false only when the Zephyr store is absent 8863// (legacy theme) -- then, and only then, fall back to the old 8864// ?accentAdded=1 reload. Returning true lets the in-place success UX 8865// below finally run; that block was unreachable for as long as this 8866// branch always reloaded. 8867// ---------------------------------------------------- 8868try { 8869var onCartPage = document.body.classList.contains('checkout-cart-index') 8870|| /\/checkout\/cart(\/|$)/.test(window.location.pathname) 8871|| !!document.querySelector('[data-cart-item-id]') 8872|| !!document.querySelector('#shopping-cart-table') 8873|| !!document.querySelector('form#form-validate') 8874|| !!document.querySelector('.cart-container') 8875|| !!document.querySelector('[data-block="cart-summary"]') 8876|| !!document.querySelector('.cart-summary'); 8877if (onCartPage && !ckAfterCartMutation(true)) { 8878try { 8879var u2 = new URL(window.location.href); 8880u2.searchParams.set('accentAdded', '1'); 8881window.location.href = u2.toString(); 8882} catch (eU2) { 8883window.location.reload(); 8884} 8885return; 8886} 8887} catch (eCPR) { } 8888// Reality check: confirm the cart actually changed before the 8889// success UX below is allowed to stand. 8890try { ckVerifySimpleAdd(ckSimpleCountBefore); } catch (eVS) { } 8891// ---------------------------------------------------- 8892// No hard items left â keep the module live and refresh tiles in-place (no page reload) 8893// ---------------------------------------------------- 8894try {
8895// Mark added + swap tiles (stable slots) 8896simplePrimaryArr.forEach(function (pid) { 8897try { microAddedIds.add(String(pid)); } catch (eM) { } 8898try { 8899var el2 = stampGrid && stampGrid.querySelector('[data-kit-primary="' + pid + '"]'); 8900if (el2) { 8901el2.classList.remove('is-selected'); 8902el2.classList.add('is-added'); 8903} 8904} catch (eC) { } 8905try { selectedPrimaryIds.delete(pid); } catch (eD) { } 8906// Swap AFTER cart sync so in-cart suppression works 8907setTimeout(function () { try { replaceAddedTileWithNext(pid); } catch (eR) { } }, 800); 8908}); 8909} catch (eS3) { } 8910try { 8911// Clear ink selections after adding ink pads 8912selectedInkColors.clear(); 8913inkChoiceMade = false; 8914if (swatchList) { 8915var allSw3 = swatchList.querySelectorAll('.kit-swatch'); 8916for (var s3 = 0; s3 < allSw3.length; s3++) allSw3[s3].classList.remove('is-selected'); 8917// restore "none" default if visible 8918var noneBtn3 = swatchList.querySelector('.kit-swatch[data-kit-color="none"]'); 8919if (noneBtn3 && noneBtn3.style.display !== 'none') { 8920noneBtn3.classList.add('is-selected'); 8921inkChoiceMade = true; 8922} 8923} 8924} catch (eS4) { } 8925// Success toast 8926try { 8927var toast2 = document.getElementById('kit-success-toast'); 8928if (toast2) { 8929var msg = (totalItemsCount > 1) ? ('â Added ' + totalItemsCount + ' items to cart') : 'â Added to cart'; 8930toast2.textContent = msg; 8931toast2.classList.add('is-visible'); 8932setTimeout(function () { toast2.classList.remove('is-visible'); }, 3000); 8933} 8934} catch (eT2) { } 8935// Reset CTA + progress 8936addBtn.disabled = false; 8937addBtn.classList.remove('is-loading'); 8938addBtn.textContent = 'Add to Cart'; 8939if (statusEl) statusEl.textContent = 'Added to cart. Want to add another?'; 8940resetAddProgressUI(); 8941updatePriceUI(); 8942// Keep module open to encourage additional add-ons 8943upsell.classList.add('cart-kit-upsell--open'); 8944body.hidden = false; 8945}); 8946}) 8947.catch(function (err) { 8948console.error('Cart kit upsell error:', err); 8949var ckErrMsg = 'Sorry, we could not add the kit to your cart. Please try again.'; 8950try { 8951var ckFlash = (window.Alpine && typeof window.Alpine.store === 'function') 8952? window.Alpine.store('flash') : null; 8953if (ckFlash && typeof ckFlash.show === 'function') ckFlash.show('error', ckErrMsg); 8954else alert(ckErrMsg); 8955} catch (eFl) { alert(ckErrMsg); } 8956addBtn.disabled = false; 8957addBtn.classList.remove('is-loading'); 8958addBtn.textContent = 'Add to Cart'; 8959if (statusEl) statusEl.textContent = ''; 8960// hide progress bar on error 8961resetAddProgressUI(); 8962}); 8963}); 8964// ---------------------------------------------------- 8965// Cart sync helpers (run after async adds so UI + tile logic are up-to-date) 8966// ---------------------------------------------------- 8967// 15) Success + Add More 8968// ---------------------------------------------------- 8969function resetSelections() { 8970selectedPrimaryIds.clear(); 8971selectedInkColors.clear(); 8972inkChoiceMade = false; 8973var defaultIdx = (typeof persona.defaultPrimaryIndex === 'number' ? persona.defaultPrimaryIndex : 0); 8974if (useTiles && TILE_ITEMS[defaultIdx] && TILE_ITEMS[defaultIdx].productId) selectedPrimaryIds.add(String(TILE_ITEMS[defaultIdx].productId)); 8975else if (STAMP_OPTIONS.length) selectedPrimaryIds.add(STAMP_OPTIONS[0].id); 8976buildGrid(); 8977if (swatchList) { 8978var sw = swatchList.querySelectorAll('.kit-swatch'); 8979for (var i = 0; i < sw.length; i++) sw[i].classList.remove('is-selected'); 8980// Default to "none" (I have an ink pad) so user isn't forced into an alert loop 8981var noneBtn = swatchList.querySelector('.kit-swatch[data-kit-color="none"]'); 8982if (noneBtn && noneBtn.style.display !== 'none') { 8983noneBtn.classList.add('is-selected'); 8984// keep selectedInkColors empty, but mark a choice as made 8985inkChoiceMade = true; 8986} 8987} 8988addBtn.disabled = false; 8989addBtn.classList.remove('is-loading'); 8990addBtn.textContent = 'Add to Cart'; 8991if (statusEl) statusEl.textContent = ''; 8992resetAddProgressUI(); 8993updatePriceUI(); 8994} 8995function handleAddMoreFromQuery() { 8996if (!accentAddMoreQS) return; 8997upsell.classList.add('cart-kit-upsell--open'); 8998body.hidden = false; 8999resetSelections(); 9000try { 9001var href = window.location.href; 9002var clean = href 9003.replace(/([?&])accentAdded=1(?=(&|#|$))/, '$1') 9004.replace(/[?&]#/, '#') 9005.replace(/[?&]$/, ''); 9006window.history.replaceState({}, document.title, clean); 9007} catch (e) { } 9008} 9009function applySuccessStateFromQuery() { 9010if (!accentAddedQS) return;
9011var headerText = upsell.querySelector('#cart-kit-header-text'); 9012if (headerText) { 9013headerText.innerHTML = 9014'<span class="cart-kit-upsell__success-inline">' 9015+ 'â Items added to cart!' 9016+ ' <span style="color:#000; text-decoration: underline;">Add more?</span>' 9017+ '</span>'; 9018} 9019upsell.classList.remove('cart-kit-upsell--open'); 9020body.hidden = true; 9021try { 9022var clean2 = window.location.href.replace(/([?&])accentAdded=1(&|$)/, '$1').replace(/[?&]$/, ''); 9023window.history.replaceState({}, document.title, clean2); 9024} catch (e) { } 9025} 9026// ---------------------------------------------------- 9027// Micro PDP queue helpers (supports 2+ hard SKUs) 9028// ---------------------------------------------------- 9029function getTileInfoByProductId(pid) { 9030pid = String(pid || '').trim(); 9031if (!pid) return null; 9032var raw = (typeof TILE_ITEM_BY_ID !== 'undefined' && TILE_ITEM_BY_ID) ? (TILE_ITEM_BY_ID[pid] || TILE_ITEM_BY_ID[String(pid)]) : null; 9033if (!raw) return null; 9034var hydrated = (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED) ? NOTARY_HYDRATED.get(pid) : null; 9035var title = (hydrated && hydrated.name) ? hydrated.name : (raw.title || raw.name || 'Customize item'); 9036var url = raw.productUrl || raw.url || raw.href || null; 9037if (!url) return null; 9038return { productId: pid, title: title, url: url }; 9039} 9040function parseRemainingHardFromUrl() { 9041try { 9042var u = new URL(window.location.href); 9043var val = (u.searchParams.get('remainingHard') || '').trim(); 9044if (!val) return []; 9045return val.split(',').map(function (s) { return String(s || '').trim(); }).filter(Boolean); 9046} catch (e) { 9047return []; 9048} 9049} 9050function stripRemainingHardParam() { 9051try { 9052var u = new URL(window.location.href); 9053u.searchParams.delete('remainingHard'); 9054u.searchParams.delete('microTotal'); 9055u.searchParams.delete('microDone'); 9056window.history.replaceState({}, document.title, u.toString()); 9057} catch (e) { } 9058} 9059function openNextHardFromRemaining() { 9060var remaining = parseRemainingHardFromUrl(); 9061if (!remaining.length) { 9062stripRemainingHardParam(); 9063return false; 9064} 9065var nextId = remaining.shift(); 9066var info = getTileInfoByProductId(nextId); 9067if (!info) { 9068// If we can't resolve the next item, stop chaining to avoid a loop 9069stripRemainingHardParam(); 9070return false; 9071} 9072// Rebuild the remainingHard param (after removing the one we're about to open) 9073try { 9074var u = new URL(window.location.href); 9075if (remaining.length) u.searchParams.set('remainingHard', remaining.join(',')); 9076else u.searchParams.delete('remainingHard'); 9077window.history.replaceState({}, document.title, u.toString()); 9078} catch (e) { } 9079// Small status hint before opening the next configurator 9080if (statusEl) statusEl.textContent = 'Next: customize ' + info.title + '.'; 9081// Open next Micro PDP 9082var prog = getMicroProgressFromUrl(); 9083openMicroPdp(info.url, info.title, info.productId, remaining, prog.done, prog.total); 9084return true; 9085} 9086// --- SmartCart: ensure cart + tiles refresh when micro-PDP closes --- 9087(function bindMicroPdpCloseRefresh() { 9088var MODAL_SEL = '#smartcartMicroPdp, .smartcart-micro-pdp, [data-smartcart-micro-pdp]'; 9089var CLOSE_SEL = '[data-smartcart-close], .smartcart-close, .close, [aria-label="Close"]'; 9090var OVERLAY_SEL = '.smartcart-overlay, .modal-backdrop, [data-smartcart-overlay]'; 9091// You likely already have these functions; map to your actual names: 9092var refresh = window.smartcartRefresh || window.SmartCartRefresh || window.renderSmartCart || null; 9093// If you *donât* have a single refresh entrypoint, use whatever you normally call after add-to-cart: 9094function hardRefreshFallback() { 9095try { 9096if (typeof refresh === 'function') return refresh(); 9097// Fallback: call the same method you use after cart update 9098if (window.SmartCart && typeof window.SmartCart.render === 'function') return window.SmartCart.render(); 9099if (window.smartcart && typeof window.smartcart.render === 'function') return window.smartcart.render(); 9100} catch (e) { 9101console.warn('[SmartCart] refresh on modal close failed:', e); 9102} 9103} 9104// Debounce so multiple close signals (button + overlay) donât double-trigger 9105var t; 9106function refreshSoon() { 9107clearTimeout(t); 9108t = setTimeout(function () { 9109hardRefreshFallback(); 9110}, 150); 9111} 9112function onDocClick(e) { 9113var modal = document.querySelector(MODAL_SEL); 9114if (!modal) return; 9115var isClose = e.target.closest && e.target.closest(CLOSE_SEL); 9116var isOverlay = e.target.closest && e.target.closest(OVERLAY_SEL); 9117if (isClose || isOverlay) { 9118// Let the DOM close animation finish, then refresh 9119refreshSoon(); 9120} 9121} 9122function onEsc(e) { 9123if (e.key === 'Escape' || e.keyCode === 27) refreshSoon(); 9124} 9125document.addEventListener('click', onDocClick, true); 9126document.addEventListener('keydown', onEsc, true); 9127// Optional: if you dispatch a custom "microPdpClosed" event already, hook that too: 9128document.addEventListener('smartcart:microPdpClosed', refreshSoon); 9129})(); 9130// ---------------------------------------------------- 9131// 4A) Replace an "Added" product tile with the next best recommendation 9132// (keeps the grid fresh without requiring a full page reload) 9133// ---------------------------------------------------- 9134function convertTileToCompleteState(card, totalCount) { 9135if (!card) return; 9136// Stable-slot swap: fade this card out before repopulating 9137try { card.classList.add('ck-swapping'); } catch (eFade0) { } 9138try { 9139card.classList.remove('is-selected'); 9140card.classList.remove('is-added'); 9141card.classList.add('is-complete'); 9142// prevent future selection toggles 9143card.setAttribute('aria-disabled', 'true'); 9144card.innerHTML = 9145'<div class="ck-tile-complete">' 9146+ '<div class="ck-tile-complete__icon">â</div>' 9147+ '<div class="ck-tile-complete__title">All set</div>' 9148+ '<div class="ck-tile-complete__subtext">You\'ve added all recommended items for this order.</div>' 9149+ '</div>'; 9150} catch (e) { } 9151}
9152function replaceAddedTileWithNext(addedPid) { 9153try { 9154if (!useTiles || !stampGrid) return; 9155addedPid = String(addedPid || '').trim(); 9156if (!addedPid) return; 9157var card = stampGrid.querySelector('.kit-product-card[data-kit-primary="' + addedPid + '"]'); 9158if (!card) return; 9159// Build an ordered pool (highest relevance first) 9160var ordered = pickTopTilesForDisplay(persona.items, cartTextForScoring, 999, allowExperimentalNow).tiles; 9161// Build exclusion map 9162var exclude = Object.create(null); 9163// Exclude currently visible tiles 9164var visible = stampGrid.querySelectorAll('.kit-product-card[data-kit-primary]'); 9165for (var i = 0; i < visible.length; i++) { 9166var v = String(visible[i].getAttribute('data-kit-primary') || '').trim(); 9167if (v) exclude[v] = true; 9168} 9169// Exclude anything already added (session + micro returns) 9170try { 9171if (microAddedIds && microAddedIds.forEach) { 9172microAddedIds.forEach(function (id) { exclude[String(id)] = true; }); 9173} 9174} catch (eAdded) { } 9175// Exclude anything already shown during this session (prevents repeats when pool is small) 9176try { 9177if (ckSeenTileIds && ckSeenTileIds.forEach) { 9178ckSeenTileIds.forEach(function (id) { exclude[String(id)] = true; }); 9179} 9180} catch (eSeen) { } 9181// Exclude queued HARD SKUs still pending configuration 9182try { 9183var queued = parseRemainingHardFromUrl(); 9184for (var q = 0; q < queued.length; q++) { 9185var qid = String(queued[q] || '').trim(); 9186if (qid) exclude[qid] = true; 9187} 9188} catch (eQ) { } 9189// Exclude anything currently selected (so swaps don't "steal" into a tile the user already picked) 9190try { 9191if (selectedPrimaryIds && selectedPrimaryIds.forEach) { 9192selectedPrimaryIds.forEach(function (sid) { 9193var s = String(sid || '').trim(); 9194if (s) exclude[s] = true; 9195}); 9196} 9197} catch (eSel) { } 9198// Exclude anything already in the cart (prefer URL-path match; fallback to title match) 9199var cartPaths = Object.create(null); 9200var cartFullLower = ''; 9201try { cartPaths = getCartProductPathSet(); } catch (eCP) { } 9202try { cartFullLower = getCartFullTextLower(); } catch (eCT) { } 9203// Find next eligible item 9204var next = null; 9205for (var k = 0; k < ordered.length; k++) { 9206var it = ordered[k]; 9207var pid = String(it && (it.productId || it.id || it.product || it.primaryId) || '').trim(); 9208if (!pid || exclude[pid]) continue; 9209// URL-path in-cart exclusion 9210var pth = ''; 9211try { pth = normalizeUrlPathForCompare(it.productUrl || it.url || ''); } catch (eNP) { } 9212if (pth && cartPaths[pth]) continue; 9213// Title fallback exclusion 9214var nm = String(it.name || it.title || '').toLowerCase(); 9215if (nm && cartFullLower && cartFullLower.indexOf(nm) !== -1) continue; 9216next = it; 9217break; 9218} 9219if (!next) { 9220// Pool exhausted -- offer an approved alternative before giving up. 9221var altNext = ckPickFallbackTile(); 9222if (altNext) { 9223try { card.classList.remove('ck-swapping'); } catch (eFA) { } 9224ckRepaintCardAs(card, altNext); 9225return; 9226} 9227// Nothing left â convert this tile to completion tile 9228try { convertTileToCompleteState(card); } catch (eDone) { } 9229setTimeout(function () { try { card.classList.remove('ck-swapping'); } catch (eFadeX) { } }, 160); 9230return; 9231} 9232// Swap this card to the new item 9233var nextId = String(next.productId || next.id || next.product || next.primaryId || '').trim(); 9234var nextUrl = next.productUrl || next.url || ''; 9235var nextHard = !!requiresMicroPdp(nextId, next); 9236// Mark seen immediately so async hydration can't cause repeats 9237try { if (ckSeenTileIds) ckSeenTileIds.add(String(nextId)); } catch (eS) { } 9238// Fade out before repopulating (stable slot) 9239try { card.classList.add('ck-swapping'); } catch (eFade0) { } 9240// Reset card state + dataset 9241try { 9242card.classList.remove('is-added', 'is-selected', 'is-complete'); 9243card.removeAttribute('aria-disabled'); 9244} catch (eState) { } 9245card.setAttribute('data-kit-primary', nextId); 9246card.setAttribute('data-kit-url', nextUrl); 9247card.setAttribute('data-kit-hard', nextHard ? '1' : '0'); 9248// Paint skeleton while hydration finishes (DOM structure used by buildGrid) 9249var titleEl = card.querySelector('.kit-product-name'); 9250var priceEl = card.querySelector('.kit-product-price'); 9251var thumbTile = card.querySelector('.kit-product-thumb-tile'); 9252if (titleEl) titleEl.textContent = (next.title || next.name || 'Loadingâ¦'); 9253if (priceEl) priceEl.textContent = ''; 9254// Reset the thumb; we'll hydrate with a real image after fetchPdpData 9255if (thumbTile) { 9256var imgSrc = (next.image || next.img || ''); 9257if (imgSrc) { 9258var altText = String(next.title || next.name || '').replace(/"/g, '"'); 9259thumbTile.innerHTML = '<img src="' + imgSrc + '" alt="' + altText + '">'; 9260} else { 9261thumbTile.innerHTML = '<span class="kit-thumb-loading">Loading...</span>'; 9262} 9263} 9264// Fade back in once the new tile has been painted 9265setTimeout(function () { 9266try { card.classList.remove('ck-swapping'); } catch (eFade1) { } 9267}, 160); 9268// Record the swapped-in product's strategy attributes (role, 9269// friction, experimental) in an ANALYTICS-ONLY map. 9270// 9271// Deliberately NOT written to TILE_ITEM_BY_ID: that map feeds 9272// requiresMicroPdp(), so registering swapped tiles there would 9273// flip the CTA to 'Customize & Add' and reroute the add through 9274// the micro-PDP. That may be the correct behaviour -- a bare 9275// product=id&qty=1 POST of a required-options SKU is wrong -- but 9276// it is an add-flow change and does not belong in an analytics 9277// patch. Tracked separately. 9278try { CK_GA_TILE_META[String(nextId)] = next; } catch (eReg) { } 9279// Hydrate: fetch canonical name/price/image (guard against stale async writes) 9280(function (el, expectedId, raw) { 9281try { 9282var url = raw.productUrl || raw.url || ''; 9283var p = fetchPdpData(url); 9284if (!p || !p.then) return; 9285p.then(function (data) { 9286try { 9287// Guard a stale async write: the slot may have been 9288// swapped again while this fetch was in flight. 9289if (!el || el.getAttribute('data-kit-primary') !== String(expectedId)) return; 9290var tEl = el.querySelector('.kit-product-name'); 9291var prEl = el.querySelector('.kit-product-price'); 9292var thEl = el.querySelector('.kit-product-thumb-tile'); 9293if (tEl && data.name) tEl.textContent = data.name;
9294// Cache the canonical name/price so analytics and 9295// the price UI see the same product the shopper does. 9296// Cache the canonical name/price. Gated by the same 9297// quality check buildGrid and ckRepaintCardAs use -- 9298// a URL that scrapes to 'Create New Customer Account' 9299// must not reach the cache, because it feeds the 9300// micro-PDP modal title and the displayed total. 9301// Also normalises price the way the other two sites do. 9302try { 9303if (typeof NOTARY_HYDRATED !== 'undefined' && NOTARY_HYDRATED 9304&& ckIsUsableTileData(data)) { 9305NOTARY_HYDRATED.set(String(expectedId), { 9306productId: String(expectedId), 9307sku: data.sku, 9308productUrl: (raw && (raw.productUrl || raw.url)) || '', 9309name: data.name || '', 9310img: data.img || '', 9311price: (data.price != null ? Number(data.price) : null) 9312}); 9313} 9314} catch (eHyd) { } 9315// A swapped tile is newly visible and was in no 9316// view_item_list, so its funnel would otherwise start 9317// at select_item. Give it its own one-item impression. 9318// attempts = 0 -> fire now; the name is already cached. 9319try { 9320ckGaFireListWhenHydrated( 9321[String(expectedId)], 0, 9322(CK_GA4.LIST_PREFIX + ' - ' + ckGaListId() + ' (swap)').slice(0, 100) 9323); 9324} catch (eSwapGa) { } 9325if (prEl) { 9326prEl.textContent = (data.price != null ? ('$' + Number(data.price).toFixed(2)) : 'See price in cart'); 9327} 9328if (thEl) { 9329if (data.img) { 9330var alt2 = String(data.name || '').replace(/"/g, '"'); 9331thEl.innerHTML = '<img src="' + data.img + '" alt="' + alt2 + '">'; 9332} else { 9333thEl.innerHTML = '<span style="font-size:12px;color:#888;">No image</span>'; 9334} 9335} 9336} catch (eH) { } 9337}).catch(function () { }); 9338} catch (eF) { } 9339})(card, nextId, next); 9340} catch (e) { 9341// fail silently 9342} 9343} 9344function handleMicroAddedFromQuery() { 9345// The micro-PDP no longer reloads the page, so microAddedQS / 9346// microAddedNameQS -- captured from window.location.search back at 9347// DOMContentLoaded -- are stale by definition. onMicroPdpAddSucceeded() 9348// writes the receipt into the URL via history.replaceState, so re-read 9349// it live here. Still correct for the legacy reload path. 9350try { 9351var ckLive = new URL(window.location.href).searchParams; 9352microAddedQS = ckLive.get('microAdded'); 9353microAddedNameQS = ckLive.get('microAddedName'); 9354} catch (eLive) { } 9355if (!microAddedQS) return; 9356var pid = String(microAddedQS || '').trim(); 9357if (!pid) return; 9358// Mark as added so tile greys out 9359microAddedIds.add(pid); 9360// Remove from current selections (prevents stale totals when tile is swapped) 9361try { selectedPrimaryIds.delete(pid); } catch (eSel0) { } 9362// Micro progress: user just completed one configurator 9363var progNow = bumpMicroDoneInUrl(); 9364MICRO_PDP.progressDone = progNow.done; 9365MICRO_PDP.progressTotal = progNow.total; 9366renderMicroProgress(MICRO_PDP.progressDone, MICRO_PDP.progressTotal); 9367// Hard SKUs add inside the micro-PDP iframe, so they never reach the 9368// POST chain above and were invisible to analytics. Fire the 9369// per-product event here. Dedupe on pid + completed-count: the 9370// receipt params can survive a re-entry of this handler. 9371try { 9372// Dedupe on a short time window, NOT on progNow.done: 9373// bumpMicroDoneInUrl() increments on every call, so a re-entry 9374// would produce a different key and defeat the guard entirely. 9375// A configurator takes seconds, so two legitimate adds of the 9376// same SKU inside 5s are impossible. 9377window.__ckMicroAddReported = window.__ckMicroAddReported || {}; 9378var ckNowT = Date.now(); 9379if (ckNowT - (window.__ckMicroAddReported[pid] || 0) > 5000) { 9380window.__ckMicroAddReported[pid] = ckNowT; 9381// On the legacy reload path this handler runs in the same tick as 9382// buildGrid(), which only STARTS hydration -- so firing now would 9383// send the entity id for the highest-AOV items in the module. 9384// Wait for the SKU, then fire; fire anyway on timeout. 9385(function emitMicroAdd(tries) { 9386try { 9387if (!ckGaHasSku(pid) && tries > 0) { 9388setTimeout(function () { emitMicroAdd(tries - 1); }, CK_GA4.HYDRATE_POLL_MS); 9389return; 9390} 9391} catch (eW) { } 9392pushUpsellEvent('SmartCart_upsell_micro_added', { 9393product_ids: [pid], 9394productId: pid, 9395micro_done: progNow.done, 9396micro_total: progNow.total 9397}); 9398})(CK_GA4.HYDRATE_POLLS); 9399} 9400} catch (eGaM) { } 9401// Show a simple success message in the module status area 9402var niceName = (microAddedNameQS || '').trim(); 9403if (!niceName) { 9404// If name not provided, try hydrated cache 9405var hydrated = NOTARY_HYDRATED.get(pid);
9406if (hydrated && hydrated.name) niceName = hydrated.name; 9407} 9408if (!niceName) niceName = 'Product'; 9409var toast = document.getElementById('kit-success-toast'); 9410if (toast) { 9411toast.textContent = 'â Added to cart: ' + niceName; 9412// If this was the LAST remaining hard item, celebrate completion 9413if (progNow.total > 1 && progNow.done >= progNow.total) { 9414toast.textContent = '???? All set â ' + progNow.done + ' of ' + progNow.total + ' items added'; 9415} 9416toast.classList.add('is-visible'); 9417setTimeout(function () { 9418toast.classList.remove('is-visible'); 9419}, 3500); 9420} else if (statusEl) { 9421// fallback 9422statusEl.textContent = 'â Added to cart: ' + niceName; 9423if (progNow.total > 1 && progNow.done >= progNow.total) { 9424statusEl.textContent = '???? All set â ' + progNow.done + ' of ' + progNow.total + ' items added'; 9425} 9426} 9427// Rebuild grid so the "is-added" state is applied 9428if (useTiles) buildGrid(); 9429try { updatePriceUI(); } catch (ePU0) { } 9430// 4B) After a HARD SKU is completed, swap the added tile for the next recommendation 9431// so the customer can keep adding without stale/greyed tiles. 9432// (Run after buildGrid() so we target the freshly-rendered card.) 9433setTimeout(function () { 9434try { replaceAddedTileWithNext(pid); } catch (e) { } 9435}, 800); 9436// If there are more HARD SKUs waiting, open the next Micro PDP automatically 9437// (keeps the user in a guided configuration flow) 9438setTimeout(function () { 9439try { 9440openNextHardFromRemaining(); 9441} catch (e) { } 9442}, 350); 9443// Scroll to the newly added item in cart + highlight 9444// Strategy: find a cart row that contains the product name 9445setTimeout(function () { 9446try { 9447var table = document.querySelector('#shopping-cart-table'); 9448if (!table) return; 9449var rows = Array.from(table.querySelectorAll('tr')); 9450var targetRow = null; 9451if (niceName && niceName !== 'Product') { 9452var nameLower = niceName.toLowerCase(); 9453targetRow = rows.find(function (r) { 9454return (r.innerText || '').toLowerCase().indexOf(nameLower) > -1; 9455}) || null; 9456} 9457// Fallback: if we can't find the row by name, just scroll to cart table 9458var scrollEl = targetRow || table; 9459scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); 9460if (targetRow) { 9461targetRow.classList.add('ck-cart-highlight'); 9462setTimeout(function () { 9463targetRow.classList.remove('ck-cart-highlight'); 9464}, 3500); 9465} 9466} catch (e) { } 9467}, 250); 9468// Remove receipt params so refresh doesn't repeat the success behavior 9469try { 9470var u = new URL(window.location.href); 9471u.searchParams.delete('microAdded'); 9472u.searchParams.delete('microAddedName'); 9473window.history.replaceState({}, document.title, u.toString()); 9474} catch (e) { } 9475} 9476// Query-param handlers MUST run in this order (priority: micro receipt > add more > success) 9477handleMicroAddedFromQuery(); // receipt event â toast/greyout/highlight, then strip params 9478handleAddMoreFromQuery(); // user intent to reopen + reset 9479applySuccessStateFromQuery(); // generic success collapse/header state 9480// ---------------------------------------------------- 9481// 9) DEBUG OVERLAY (persona + product scores) 9482// Enable with ?upsellDebug=1 9483// ---------------------------------------------------- 9484(function initDebugOverlay() { 9485var debugOn = (urlParams.get('upsellDebug') === '1'); 9486if (!debugOn) return; 9487// ---- styles (scoped) ---- 9488var style = document.createElement('style'); 9489style.textContent = 9490'#ck-debug-overlay{position:fixed;right:12px;bottom:12px;z-index:999999;' + 9491'width:360px;max-width:calc(100vw - 24px);' + 9492'background:#0b1220;color:#e8eefc;border:1px solid rgba(255,255,255,.15);' + 9493'border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35);font-family:inherit;}' + 9494'#ck-debug-overlay *{box-sizing:border-box;}' + 9495'#ck-debug-head{display:flex;align-items:center;justify-content:space-between;' + 9496'padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.12);}' + 9497'#ck-debug-title{font-weight:700;font-size:13px;letter-spacing:.2px;}' + 9498'#ck-debug-actions{display:flex;gap:8px;align-items:center;}' + 9499'#ck-debug-actions button{background:rgba(255,255,255,.12);color:#e8eefc;border:1px solid rgba(255,255,255,.15);' + 9500'border-radius:8px;padding:6px 8px;font-size:12px;cursor:pointer;}' + 9501'#ck-debug-actions button:hover{background:rgba(255,255,255,.18);}' + 9502'#ck-debug-body{padding:10px 12px;max-height:55vh;overflow:auto;}' + 9503'#ck-debug-body h4{margin:10px 0 6px 0;font-size:12px;opacity:.9;}' + 9504'#ck-debug-kv{display:grid;grid-template-columns:120px 1fr;gap:6px 10px;font-size:12px;}' + 9505'#ck-debug-kv .k{opacity:.75;}' + 9506'#ck-debug-table{width:100%;border-collapse:collapse;font-size:12px;}' + 9507'#ck-debug-table th,#ck-debug-table td{padding:6px 6px;border-bottom:1px solid rgba(255,255,255,.10);vertical-align:top;}' + 9508'#ck-debug-table th{opacity:.75;text-align:left;font-weight:600;}' + 9509'#ck-debug-pill{display:inline-block;padding:2px 6px;border-radius:999px;' + 9510'background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.14);font-size:11px;}' + 9511'#ck-debug-muted{opacity:.7;font-size:11px;line-height:1.35;}' +
9512'#ck-debug-overlay.collapsed #ck-debug-body{display:none;}'; 9513document.head.appendChild(style); 9514// ---- UI ---- 9515var box = document.createElement('div'); 9516box.id = 'ck-debug-overlay'; 9517box.innerHTML = 9518'<div id="ck-debug-head">' + 9519'<div id="ck-debug-title">Cart Kit Debug</div>' + 9520'<div id="ck-debug-actions">' + 9521'<button type="button" id="ck-debug-copy">Copy</button>' + 9522'<button type="button" id="ck-debug-toggle">Collapse</button>' + 9523'</div>' + 9524'</div>' + 9525'<div id="ck-debug-body">' + 9526'<div id="ck-debug-kv"></div>' + 9527'<h4>Persona scores</h4>' + 9528'<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>' + 9529'<h4>Top products (selected persona)</h4>' + 9530'<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>' + 9531'<div id="ck-debug-muted" style="margin-top:10px;">Tip: add/remove items and watch scores update live.</div>' + 9532'</div>'; 9533document.body.appendChild(box); 9534var btnToggle = document.getElementById('ck-debug-toggle'); 9535var btnCopy = document.getElementById('ck-debug-copy'); 9536btnToggle.addEventListener('click', function () { 9537box.classList.toggle('collapsed'); 9538btnToggle.textContent = box.classList.contains('collapsed') ? 'Expand' : 'Collapse'; 9539}); 9540function safeJson(obj) { 9541try { return JSON.stringify(obj, null, 2); } catch (e) { return String(obj); } 9542} 9543btnCopy.addEventListener('click', function () { 9544var payload = window.__CK_DEBUG_LAST__ || {}; 9545var textToCopy = safeJson(payload); 9546if (navigator.clipboard && navigator.clipboard.writeText) { 9547navigator.clipboard.writeText(textToCopy).catch(function () { }); 9548} else { 9549// fallback 9550var ta = document.createElement('textarea'); 9551ta.value = textToCopy; 9552ta.style.position = 'fixed'; 9553ta.style.left = '-9999px'; 9554document.body.appendChild(ta); 9555ta.select(); 9556try { document.execCommand('copy'); } catch (e) { } 9557document.body.removeChild(ta); 9558} 9559}); 9560// ---- scoring helpers for overlay ---- 9561function personaScoreBreakdown(personaObj, cartTextLower) { 9562var kw = keywordMatchScore((personaObj.match || personaObj.matchKeywords || []), cartTextLower); 9563var base = kw.score; 9564var priorityNudge = (typeof personaObj.priority === 'number') ? (personaObj.priority / 1000) : 0; 9565return { 9566base: base, 9567hits: kw.hits, 9568matched: kw.matched, 9569priorityNudge: priorityNudge, 9570total: base + priorityNudge 9571}; 9572} 9573function productScoreForOverlay(item, cartTextLower, allowExperimental) { 9574return scoreProductForCart(item, cartTextLower, allowExperimental); 9575} 9576function esc(s) { 9577return String(s || '').replace(/[&<>"']/g, function (c) { 9578return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]); 9579}); 9580} 9581// Debug helpers: get cart titles/options as arrays 9582function getCartTitlesList() { 9583try { 9584var nameNodes = document.querySelectorAll( 9585'[data-cart-item-id] h3 a,' + 9586'[data-cart-item-id] h3,' + 9587'#shopping-cart-table h2.cartProductName a,' + 9588'#shopping-cart-table h2.cartProductName,' + 9589'#shopping-cart-table .product-name a,' + 9590'#shopping-cart-table .product-name,' + 9591'.product-item-name a,' + 9592'.product-item-name' 9593); 9594var out = []; 9595var seen = {}; 9596for (var i = 0; i < nameNodes.length; i++) { 9597var t = (nameNodes[i].textContent || '').trim(); 9598if (!t) continue; 9599var k = t.toLowerCase(); 9600if (seen[k]) continue; 9601seen[k] = true; 9602out.push(t); 9603} 9604return out; 9605} catch (e) { return []; } 9606} 9607function getCartOptionsList() { 9608try { 9609var optNodes = document.querySelectorAll( 9610'[data-cart-item-id] .flex-wrap,' + 9611'#shopping-cart-table .item-options,' + 9612'#shopping-cart-table dl.item-options,' + 9613'.item-options,' + 9614'dl.item-options' 9615); 9616var out = []; 9617for (var j = 0; j < optNodes.length; j++) { 9618var o = (optNodes[j].textContent || '').trim(); 9619if (o) out.push(o); 9620} 9621return out; 9622} catch (e) { return []; } 9623} 9624function renderOverlay() { 9625var cartTitleLower = getCartTitleTextLower(); 9626var cartFullLower = getCartFullTextLower(); 9627var allowExperimental = (ALLOW_EXPERIMENTAL_TILES === true) || (urlParams.get('upsellDebug') === '1'); 9628// Persona scoring 9629var personaRows = []; 9630var best = { key: 'generic', score: -1 }; 9631for (var i = 0; i < PERSONAS.length; i++) { 9632var p = PERSONAS[i]; 9633if (!p || p.key === 'generic') continue; 9634var br = personaScoreBreakdown(p, cartTitleLower); 9635personaRows.push({ 9636key: p.key, 9637name: p.name, 9638score: br.total, 9639hits: br.hits, 9640matched: br.matched || [], 9641priority: br.priorityNudge 9642}); 9643if (br.hits > 0 && br.total > best.score) { 9644best = { key: p.key, score: br.total }; 9645} 9646} 9647personaRows.sort(function (a, b) { return b.score - a.score; }); 9648// If nobody matched, generic 9649var chosenKey = (best.score <= 0) ? 'generic' : best.key; 9650var chosenPersona = getPersonaByKey(chosenKey); 9651// Product scoring for chosen persona 9652var pool = (chosenPersona && chosenPersona.items) ? chosenPersona.items.slice() : []; 9653var prodRows = pool.map(function (it) { 9654return { 9655id: it.productId, 9656url: it.productUrl, 9657name: it.productName || it.name || it.title || it.label || '', 9658role: it.role || '', 9659friction: it.friction || '', 9660experimental: (it.experimental === true), 9661score: productScoreForOverlay(it, cartFullLower, allowExperimental) 9662}; 9663}); 9664prodRows.sort(function (a, b) { return b.score - a.score; }); 9665// Top N shown for overlay (not just 3) 9666var topN = prodRows.slice(0, 8); 9667// Update KV 9668var kv = document.getElementById('ck-debug-kv'); 9669var titleList = getCartTitlesList(); 9670var optList = getCartOptionsList(); 9671var cartTitlesText = (titleList || []).join(' | '); 9672var cartOptionsText = (optList || []).join(' | '); 9673kv.innerHTML = 9674'<div class="k">Chosen persona</div><div><span id="ck-debug-pill" class="ck-pill"></span></div>' + 9675'<div class="k">Experimental</div><div>' + (allowExperimental ? 'ON (debug)' : 'OFF') + '</div>' + 9676'<div class="k">Cart titles (used for persona)</div><div id="ck-debug-muted">' + esc(cartTitlesText.slice(0, 220)) + (cartTitlesText.length > 220 ? 'â¦' : '') + '</div>' + 9677'<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>'; 9678var pill = document.getElementById('ck-debug-pill'); 9679if (pill) { 9680pill.textContent = chosenKey; 9681pill.className = ''; 9682pill.id = 'ck-debug-pill'; 9683pill.style.display = 'inline-block'; 9684pill.style.padding = '2px 6px'; 9685pill.style.borderRadius = '999px'; 9686pill.style.background = 'rgba(255,255,255,.12)'; 9687pill.style.border = '1px solid rgba(255,255,255,.14)'; 9688pill.style.fontSize = '11px'; 9689} 9690// Persona tables 9691var tbody = document.getElementById('ck-debug-personas'); 9692tbody.innerHTML = personaRows.map(function (r) { 9693var isChosen = (r.key === chosenKey); 9694// Build keyword pills (or fallback) 9695var kwHtml = (r.matched && r.matched.length) 9696? '<div class="ck-debug-keywords">' + 9697r.matched.slice(0, 10).map(function (k) { 9698return '<span class="ck-debug-keyword">' + esc(k) + '</span>'; 9699}).join('') + 9700(r.matched.length > 10 ? '<span class="ck-debug-keyword muted">+' + (r.matched.length - 10) + ' more</span>' : '') + 9701'</div>' 9702: '<div class="ck-debug-keywords muted">no keyword matches</div>'; 9703return '' 9704+ '<tr>' 9705+ '<td>' + esc(r.key) + (isChosen ? ' <span class="ck-badge" style="margin-left:6px;opacity:.9;">â </span>' : '') + '</td>' 9706+ '<td>
9706' + (Math.round(r.score * 100) / 100) + '</td>' 9707+ '<td>' + r.hits + '</td>' 9708+ '</tr>' 9709+ '<tr class="ck-debug-keyword-row">' 9710+ '<td colspan="3">' + kwHtml + '</td>' 9711+ '</tr>'; 9712}).join(''); 9713// Products table 9714var prodTbody = document.getElementById('ck-debug-products'); 9715if (prodTbody) { 9716if (!topN || !topN.length) { 9717prodTbody.innerHTML = '<tr><td colspan="3" style="opacity:.7;">No products found for this persona.</td></tr>'; 9718} else { 9719prodTbody.innerHTML = topN.map(function (r) { 9720var label = r.name || r.url || r.id || ''; 9721var roleBits = []; 9722if (r.role) roleBits.push(r.role); 9723if (r.friction) roleBits.push('friction: ' + r.friction); 9724if (r.experimental) roleBits.push('exp'); 9725return '' 9726+ '<tr>' 9727+ '<td>' + esc(label) + '</td>' 9728+ '<td>' + (Math.round(r.score * 100) / 100) + '</td>' 9729+ '<td>' + esc(roleBits.join(' ⢠')) + '</td>' 9730+ '</tr>'; 9731}).join(''); 9732} 9733} 9734// Store last payload for easy copy/paste to stakeholders 9735window.__CK_DEBUG_LAST__ = { 9736chosen_persona: chosenKey, 9737allow_experimental: allowExperimental, 9738persona_scores: personaRows, 9739product_scores_top: topN, 9740cart_titles: titleList, 9741cart_options: optList, 9742cart_titles_used_for_persona: cartTitleLower, 9743cart_options_text: (cartOptionsText || '').toLowerCase(), 9744cart_full_text: cartFullLower 9745}; 9746} 9747// Update live (cart can change after ajax) 9748renderOverlay(); 9749setInterval(renderOverlay, 800); 9750})(); 9751}); </script>
9751</div><div class="widget widget-static-block"></div><div class="widget widget-static-block"></div>
9751<script> /* Ported from the pre-Zephyr footer's jQuery block. Zephyr ships a jQuery 9752no-op shim, so the original jQuery(document).ready() never fired â see 9753note 1 in the header comment. Only the email-CTA rewrite was live; the 9754#blog and #corporate-accounts branches were dead and are not carried over. */ 9755(function () { 9756"use strict"; 9757var done = false; 9758function apply() { 9759if (done) return true; 9760var s = document.querySelectorAll('.email-cta > strong'); 9761if (s.length < 2) return false; 9762s[1].textContent = 'and SAVE 15%'; 9763done = true; 9764return true; 9765} 9766if (!apply()) { 9767document.addEventListener('DOMContentLoaded', apply); 9768/* the email CTA is injected by a widget block that can render late */ 9769if (window.MutationObserver) { 9770var obs = new MutationObserver(function () { if (apply()) obs.disconnect(); }); 9771obs.observe(document.documentElement, { childList: true, subtree: true }); 9772setTimeout(function () { obs.disconnect(); }, 10000); 9773} 9774} 9775})(); 9776</script>
9776<div class="widget widget-static-block"> <style> /* Hairline light, HARDENED against theme globals. ID-scoped (#fu-foot) for 9777specificity + a child reset so theme font / letter-spacing / text-transform / 9778line-height / margins / list & link styling can't bleed in. Targeted 9779!important only on the props themes tend to override. */ 9780#fu-foot.fu-foot{--fu-accent:var(--brand-primary,#012e4a);--fu-border:#e7e9ec;--fu-sep:#e3e6ea; 9781display:flex!important;align-items:center!important;justify-content:center!important; 9782gap:8px!important;width:100%!important;min-height:46px!important; 9783margin:0!important;padding:9px 12px!important;margin-top:2rem; 9784background:#fff!important;border-top:1px solid var(--fu-border)!important;border-bottom:1px solid var(--fu-border)!important; 9785box-sizing:border-box!important;font-family:inherit!important;list-style:none!important} 9786#fu-foot.fu-foot *{margin:0!important;padding:0!important;box-sizing:border-box!important; 9787font-family:inherit!important;letter-spacing:normal!important;text-transform:none!important; 9788line-height:normal!important;list-style:none!important;text-decoration:none!important;float:none!important} 9789#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} 9790#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} 9791#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} 9792#fu-foot .fu-sep{width:1px!important;height:20px!important;background:var(--fu-sep)!important;flex:0 0 1px!important} 9793/* Copy is approved marketing copy and is never trimmed to fit. nowrap keeps the 9794crisp single line on wide desktop; below 1280px the longest lines wrap instead of 9795overflowing their equal-width column. Measured on live NTW: its slot 1 ("Badges 9796Your Team Will Be Proud to Wear", 38 chars) renders 251px wide. Text space per 9797column is ~430px at 1920, ~250px at 1200, ~270px at 1280 - so 1200 was 1px short 9798and 1280 is the correct boundary. Re-measure if any line gets longer. */ 9799@media(max-width:1280px){ 9800#fu-foot .fu-head{white-space:normal!important} 9801#fu-foot.fu-foot{min-height:56px!important} 9802} 9803/* symbol library â out of the flex container, never rendered */ 9804#fu-defs{position:absolute!important;width:0!important;height:0!important;overflow:hidden!important} 9805@media(max-width:768px){ 9806#fu-foot.fu-foot{flex-wrap:wrap!important;min-height:82px!important} 9807#fu-foot .fu-item{flex:1 1 46%!important;justify-content:flex-start!important} 9808#fu-foot .fu-head{font-size:12px!important;white-space:normal!important} 9809#fu-foot .fu-sep{display:none!important} 9810} </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>
9810 <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>
9810<script type="text/javascript"> // <![CDATA[ 9811(function () { 9812"use strict"; 9813var SITE_DATA = { 9814"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"]], 9815"customsigns": [["fu-quality","Built to Last"],["fu-customization","Customized Your Way"],["fu-guarantee","Experts Since 1954"],["fu-star","Reliable Business Signs"]], 9816"allstatenotarysupplies": [["fu-guarantee","Trusted Since 2008"],["fu-compliance","Compliance Experts"],["fu-customization","Easy to Customize"],["fu-clock","Ships in 24 Hours"]], 9817"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"]], 9818"corpconnect": [["fu-quality","Professional Grade"],["fu-compliance","State Approved"],["fu-customization","Order Your Way"],["fu-shipping","Ships Next Business Day"]], 9819"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"]], 9820"904custom": [["fu-quality","Heirloom Quality"],["fu-design","Real Craftspeople"],["fu-customization","Personalize in Minutes"],["fu-shipping","Ships Next Day"]], 9821"hcbrands": [["fu-guarantee","70 Years of Making It Right"],["fu-design","Expert Craftspeople"],["fu-customization","Easy to Personalize"],["fu-shipping","Ships Next Business Day"]] 9822}; 9823// Per-brand icon/text color. Omit a site to inherit var(--brand-primary) with 9824// a navy fallback. MyKoozie is pinned to its theme blue so the strip matches 9825// the footer headers directly beneath it on legacy AND Zephyr. 9826var SITE_ACCENT = { "mykoozie": "#456FB2" }; 9827// Matching below is a SUBSTRING test against the hostname, so a shared staging 9828// host resolves to whichever key it happens to contain â mk.staging.hcbrands.com 9829// would render HCBrands copy, not MyKoozie's. Force any such host here; this map 9830// is consulted first and wins outright. Key = exact lowercase hostname. 9831var HOST_OVERRIDE = { 9832// "mk.staging.hcbrands.com": "mykoozie", 9833// "cs.staging.hcbrands.com": "customsigns" 9834}; 9835try { 9836var root = document.getElementById("fu-foot"); 9837if (!root) return; 9838var host = (location.hostname || "").toLowerCase(); 9839var key = null; 9840if (HOST_OVERRIDE[host] && SITE_DATA[HOST_OVERRIDE[host]]) { 9841key = HOST_OVERRIDE[host]; 9842} else { 9843for (var k in SITE_DATA) { 9844if (Object.prototype.hasOwnProperty.call(SITE_DATA, k) && host.indexOf(k) > -1) { key = k; break; } 9845} 9846} 9847if (!key) return; // unknown host -> keep the generic copy already in the markup 9848var cfg = SITE_DATA[key]; 9849if (SITE_ACCENT[key]) root.style.setProperty("--fu-accent", SITE_ACCENT[key]); 9850var items = root.getElementsByClassName("fu-item"); 9851var seps = root.getElementsByClassName("fu-sep"); 9852for (var i = 0; i < items.length; i++) { 9853if (!cfg[i]) { 9854items[i].style.display = "none"; 9855if (seps[i - 1]) seps[i - 1].style.display = "none"; // no orphan divider 9856continue; 9857} 9858var use = items[i].getElementsByTagName("use")[0]; 9859if (use) { 9860use.setAttribute("href", "#" + cfg[i][0]); 9861use.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#" + cfg[i][0]); 9862} 9863var head = items[i].getElementsByClassName("fu-head")[0]; 9864if (head) head.textContent = cfg[i][1]; 9865} 9866} catch (e) { /* strip keeps its default copy rather than breaking the footer */ } 9867})(); 9868// ]]> </script>
9868</div><div class="bg-white p-12"><div class="max-w-7xl mx-auto"><div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-10"> <nav aria-labelledby="footer-products" class="mb-6 sm:mb-0"><h3 id="footer-products" class="text-base font-bold text-gray-900 mb-5">Products</h3><ul class="space-y-2.5"><li><a href="/address-stamps/view-all" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Address Stamps</a></li><li><a href="/custom-rubber-stamps" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Custom Stamps</a></li><li><a href="/rubber-logo-stamps" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Logo Stamps</a></li><li><a href="/business-office-stamps/signature-stamps" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Signature Stamps</a></li></ul> </nav> <nav aria-labelledby="footer-company" class="mb-6 sm:mb-0"><h3 id="footer-company" class="text-base font-bold text-gray-900 mb-5">Company</h3><ul class="space-y-2.5"><li><a href="/about-us" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">About Us</a></li><li><a href="/buy-in-america" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">American Made</a></li><li><a href="/rubber-stamp-videos" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Videos</a></li><li><a href="/blog" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Blog</a></li></ul> </nav> <nav aria-labelledby="footer-account" class="mb-6 sm:mb-0"><h3 id="footer-account" class="text-base font-bold text-gray-900 mb-5">My Account</h3><ul class="space-y-2.5"><li><a href="/customer/account/login/" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Account</a></li><li><a href="/sales/order/history/" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Reorders</a></li><li><a href="/customer/account/forgotpassword/" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Forgot Password</a></li><li><a href="/contacts" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Track My Order</a></li></ul> </nav> <nav aria-labelledby="footer-support" class="mb-6 sm:mb-0"><h3 id="footer-support" class="text-base font-bold text-gray-900 mb-5">Support</h3><ul class="space-y-2.5"><li><a href="/contacts" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Contact Us</a></li><li><a href="/shipping" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Shipping</a></li><li><a href="/returns" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Returns</a></li><li><a href="/faq" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">FAQ</a></li><li><a href="/accessibility" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">Accessibility</a></li></ul> </nav><div class="mb-6 sm:mb-0"><h3 class="text-base font-bold text-gray-900 mb-5">Customer Service</h3> <address class="not-italic"><ul class="space-y-2.5"><li><a href="tel:+18886889998" aria-label="Call customer service at (888) 688-9998" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">(888) 688-9998</a></li><li><span class="text-sm text-gray-700">M-Th: 10am - 4pm ET</span></li><li><a href="mailto:[email protected]" aria-label="Email sales at [email protected]" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">[email protected]</a></li><li><a href="https://www.google.com/maps/search/?api=1&query=2021+St+Augustine+Rd+E+Suite+2+Jacksonville+FL+32207" target="_blank" rel="noopener noreferrer" aria-label="Open address in Google Maps (opens in new tab)" class="text-sm text-gray-700 hover:text-brand-primary hover:underline focus:outline-none focus:ring-2 focus:ring-brand-primary focus:ring-offset-2 rounded-sm transition-colors">
98682021 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>
9868<script type="application/ld+json"> { 9869"@context": "http://schema.org", 9870"@type": "LocalBusiness", 9871"name": "SimplyStamps.com", 9872"address": { 9873"@type": "PostalAddress", 9874"streetAddress": "2021 St Augustine Rd E, Suite 2", 9875"addressLocality": "Jacksonville", 9876"addressRegion": "FL", 9877"postalCode": "32207", 9878"addressCountry": "US" 9879}, 9880"geo": { 9881"@type": "GeoCoordinates", 9882"latitude": "30.3220", 9883"longitude": "-81.6690" 9884}, 9885"telephone": "+1-888-688-9998", 9886"email": "[email protected]", 9887"url": "https://www.simplystamps.com", 9888"sameAs": [ 9889"https://www.facebook.com/SimplyStamps", 9890"https://twitter.com/SimplyStamps", 9891"https://www.instagram.com/SimplyStamps", 9892"https://www.pinterest.com/SimplyStamps", 9893"https://www.linkedin.com/company/SimplyStamps" 9894], 9895"openingHoursSpecification": [ 9896{ 9897"@type": "OpeningHoursSpecification", 9898"dayOfWeek": [ 9899"Monday", 9900"Tuesday", 9901"Wednesday", 9902"Thursday" 9903], 9904"opens": "10:00", 9905"closes": "16:00" 9906} 9907], 9908"hasMerchantReturnPolicy": { 9909"@type": "MerchantReturnPolicy", 9910"name": "Return Policy", 9911"url": "https://www.simplystamps.com/returns", 9912"merchantReturnDays": 60, 9913"returnFees": "FreeReturn", 9914"returnMethod": "ReturnByMail", 9915"returnPolicyCategory": "Refundable", 9916"inStoreReturnsOffered": false, 9917"itemCondition": "New", 9918"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." 9919} 9920} </script>
9920</div><div itemscope itemtype="https://schema.org/WebSite" class="hidden"><meta itemprop="name" content="Simply Stamps"><link itemprop="url" href="https://www.simplystamps.com/"><div itemprop="potentialAction" itemscope itemtype="https://schema.org/SearchAction"><meta itemprop="target" content="https://www.simplystamps.com/catalogsearch/result/?q={search_term_string}"><meta itemprop="query-input" content="required name=search_term_string"></div></div><div class="bg-gray-800 text-white" itemscope itemtype="https://schema.org/Organization"><meta itemprop="name" content="Simply Stamps"><link itemprop="url" href="https://www.simplystamps.com/"><meta itemprop="logo" content="https://cdn.simplystamps.com/skin/frontend/zephyr/sstamps-modern/images/SS-Desktop-logo.svg"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <address class="text-center text-sm text-gray-400 not-italic"> © 2026 Simply Stamps. All Rights Reserved.<p><a href="/terms-and-conditions">Terms & Conditions</a> | <a href="/privacy-policy">Privacy</a></p> </address></div></div> </footer> <style> @keyframes shake { 99210%, 100% { transform: translateX(0); } 992210%, 30%, 50%, 70%, 90% { transform: translateX(-4px); } 992320%, 40%, 60%, 80% { transform: translateX(4px); } 9924} 9925.shake { 9926animation: shake 0.5s ease-in-out; 9927} </style><div 9928x-data 9929x-show="$store.cart.drawerOpen" 9930@click="$store.cart.closeDrawer()" 9931x-transition:enter="transition-opacity ease-out duration-300" 9932x-transition:enter-start="opacity-0" 9933x-transition:enter-end="opacity-100" 9934x-transition:leave="transition-opacity ease-in duration-200" 9935x-transition:leave-start="opacity-100" 9936x-transition:leave-end="opacity-0" 9937class="fixed inset-0 z-40 transition-all duration-500" 9938:class="$store.cart.drawerFocused ? 'bg-black/15' : 'bg-transparent'" 9939style="display: none;" 9940></div><div 9941x-data 9942x-show="$store.cart.drawerOpen" 9943@click.away="if (!$store.imagePreview?.isOpen && !$store.promo?.isOpen && !$store.confirm?.isOpen && !document.getElementById('wizardModal')?._x_dataStack?.[0]?.isOpen) $store.cart.closeDrawer()" 9944@mouseenter="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = true }, 215)" 9945@mouseleave="clearTimeout($store.cart._focusTimer); $store.cart._focusTimer = setTimeout(() => { $store.cart.drawerFocused = false }, 100)" 9946x-transition:enter="transition ease-out duration-300" 9947x-transition:enter-start="translate-x-full" 9948x-transition:enter-end="translate-x-0" 9949x-transition:leave="transition ease-in duration-200" 9950x-transition:leave-start="translate-x-0" 9951x-transition:leave-end="translate-x-full" 9952:style="{ top: $store.ui.getHeaderOffset() + 'px' }" 9953class="fixed right-0 bottom-0 w-full md:w-[26rem] bg-white shadow-2xl z-50 flex flex-col transition-all duration-300" 9954style="display: none;" 9955><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 9956@click="$store.cart.closeDrawer()" 9957class="p-2 hover:bg-gray-100 rounded-lg transition-colors" 9958aria-label="Close cart" 9959> <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 9960x-show="$store.cart.isRefreshing" 9961x-cloak 9962x-transition:enter="transition ease-out duration-200" 9963x-transition:enter-start="opacity-0" 9964x-transition:enter-end="opacity-100" 9965x-transition:leave="transition ease-in duration-150" 9966x-transition:leave-start="opacity-100" 9967x-transition:leave-end="opacity-0" 9968class="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center z-10" 9969><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">
9969Updating cart...</span></div></div><div class="flex-1 min-h-0 overflow-hidden relative" 9970x-data="{ 9971canScroll: false, 9972atBottom: false, 9973checkScroll() { 9974const el = this.$refs.scrollContainer; 9975if (!el) return; 9976this.canScroll = el.scrollHeight > el.clientHeight + 10; 9977this.atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 20; 9978}, 9979scrollToLastItem() { 9980const container = this.$refs.scrollContainer; 9981if (!container) return; 9982const items = container.querySelectorAll('[data-cart-item-id]'); 9983if (!items.length) return; 9984const lastItem = items[items.length - 1]; 9985const cRect = container.getBoundingClientRect(); 9986const iRect = lastItem.getBoundingClientRect(); 9987if (iRect.bottom > cRect.bottom || iRect.top < cRect.top) { 9988lastItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 9989} 9990this.checkScroll(); 9991} 9992}" 9993x-init="$nextTick(() => checkScroll()); $watch(() => $store.cart.drawerOpen, (open) => { if (open) $nextTick(() => checkScroll()) })" 9994@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" 9995id="cart-items-container" 9996x-ref="scrollContainer" 9997@scroll.throttle.100ms="checkScroll()" 9998@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" 9999x-transition:enter="transition ease-out duration-200" 10000x-transition:enter-start="opacity-0" 10001x-transition:enter-end="opacity-100" 10002x-transition:leave="transition ease-in duration-150" 10003x-transition:leave-start="opacity-100" 10004x-transition:leave-end="opacity-0" 10005class="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" 10006@click.prevent.stop=" 10007const container = document.getElementById('cart-items-container'); 10008if (!container) return; 10009const items = container.querySelectorAll('[data-cart-item-id]'); 10010if (!items.length) return; 10011const containerTop = container.getBoundingClientRect().top; 10012let target = null; 10013const containerBottom = container.getBoundingClientRect().bottom; 10014for (let i = 0; i < items.length; i++) { 10015if (items[i].getBoundingClientRect().bottom > containerBottom) { 10016target = items[i]; 10017break; 10018} 10019} 10020if (target) { 10021const scrollAmount = target.getBoundingClientRect().top - containerTop; 10022container.scrollBy({ top: scrollAmount, behavior: 'smooth' }); 10023} 10024" 10025class="absolute bottom-2 left-1/2 -translate-x-1/2 animate-bounce p-1 rounded-full hover:bg-gray-100 transition-colors cursor-pointer" 10026aria-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" 10027x-data 10028x-init="$store.cart.hasError = false; $store.cart.subtotal = "$0.00"; $store.cart.originalSubtotal = null; $store.cart.grandTotal = "$0.00"; $store.cart.totals = []; $store.cart.couponCode = ""; $store.cart.expeditedProduct = {"id":"12060","price":"$7.99","add_url":"https:\/\/www.simplystamps.com\/checkout\/cart\/add\/uenc\/aHR0cHM6Ly93d3cuc2ltcGx5c3RhbXBzLmNvbS9ibG9nL2hvdy1jdXN0b20tcnViYmVyLXN0YW1wcy1hcmUtbWFkZQ,,\/product\/12060\/form_key\/e3374479ab86b5888526972805eb3d8f\/qty\/1\/"}; $store.cart.expeditedInCart = false; $store.cart.totalSavings = null; $store.cart.savings = {"label":null,"has_savings":false,"sale_savings":null,"price_adjustments":[],"your_price":null}"> <template x-if="!$store.cart.isEmpty"><div><div class="mb-3 text-right"> <button 10029onclick="clearCart(); return false;" 10030class="text-xs text-red-600 hover:text-red-700 hover:underline transition-colors">
10030 Clear Cart </button></div><div x-data="{ threshold: 79 }" 10031x-show="$store.cart.subtotalNumeric > 0 && $store.cart.subtotalNumeric >= threshold" 10032x-cloak 10033class="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 10034type="button" 10035@click="$store.promo.remove()" 10036class="text-xs text-green-700 hover:text-green-900 underline" 10037>Remove</button></div> </template> <template x-if="!$store.cart.couponCode"> <button 10038type="button" 10039@click="$store.promo.open()" 10040class="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" 10041> <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 10042x-transition:enter="transition ease-out duration-200" 10043x-transition:enter-start="opacity-0 translate-y-1" 10044x-transition:enter-end="opacity-100 translate-y-0" 10045x-transition:leave="transition ease-in duration-150" 10046x-transition:leave-start="opacity-100 translate-y-0" 10047x-transition:leave-end="opacity-0 translate-y-1" 10048class="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 10049type="button" 10050onclick="addExpeditedToCart()" 10051@mouseenter="showTip = true"
10052@mouseleave="showTip = false" 10053class="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/" 10054@click.prevent="$store.cart.hasError ? $store.cart.shake($event.currentTarget) : $store.upsell.maybeProceed('https://www.simplystamps.com/checkout/onepage/')" 10055class="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/" 10056class="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 10057@click="$store.cart.closeDrawer()" 10058class="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 10059x-data 10060x-show="$store.promo.isOpen" 10061x-cloak 10062class="fixed inset-0 z-[100] overflow-y-auto" 10063aria-labelledby="promo-modal-title" 10064role="dialog" 10065aria-modal="true" 10066><div 10067x-show="$store.promo.isOpen" 10068x-transition:enter="transition ease-out duration-200" 10069x-transition:enter-start="opacity-0" 10070x-transition:enter-end="opacity-100" 10071x-transition:leave="transition ease-in duration-150" 10072x-transition:leave-start="opacity-100" 10073x-transition:leave-end="opacity-0" 10074@click="$store.promo.close()" 10075class="fixed inset-0 bg-black/50 backdrop-blur-sm" 10076></div><div @click.self="$store.promo.close()" class="fixed inset-0 flex items-center justify-center p-4"><div 10077x-show="$store.promo.isOpen" 10078x-transition:enter="transition ease-out duration-200" 10079x-transition:enter-start="opacity-0 scale-95" 10080x-transition:enter-end="opacity-100 scale-100" 10081x-transition:leave="transition ease-in duration-150" 10082x-transition:leave-start="opacity-100 scale-100" 10083x-transition:leave-end="opacity-0 scale-95" 10084@click.stop 10085class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6" 10086> <button 10087@click="$store.promo.close()" 10088type="button" 10089class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 10090> <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 10091x-show="$store.promo.success" 10092x-transition:enter="transition ease-out duration-300" 10093x-transition:enter-start="opacity-0 scale-95" 10094x-transition:enter-end="opacity-100 scale-100" 10095class="flex flex-col items-center justify-center py-4" 10096><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 10097type="text" 10098id="promo-code-input" 10099x-model="$store.promo.code" 10100placeholder="Enter promo code" 10101class="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" 10102:disabled="$store.promo.isLoading" 10103@keydown.escape="$store.promo.close()" 10104/> <button 10105type="button" 10106@click="navigator.clipboard.readText().then(text => { $store.promo.code = text.trim(); })" 10107class="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" 10108title="Paste from clipboard" 10109:disabled="$store.promo.isLoading" 10110> <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 10111x-show="$store.promo.error" 10112x-transition 10113class="text-sm text-red-600 text-center" 10114x-text="$store.promo.error" 10115></div><div class="flex gap-3"> <button 10116@click="$store.promo.close()" 10117type="button" 10118class="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" 10119:disabled="$store.promo.isLoading" 10120> Cancel </button> <button 10121type="submit" 10122class="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" 10123:disabled="$store.promo.isLoading || !$store.promo.code.trim()" 10124> <span x-show="!$store.promo.isLoading">
10124Apply 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 10125x-data="{ open: false }" 10126@expedited-added.window="open = true" 10127x-show="open" 10128x-cloak 10129class="fixed inset-0 z-[100] overflow-y-auto" 10130aria-labelledby="expedited-modal-title" 10131role="dialog" 10132aria-modal="true" 10133><div 10134x-show="open" 10135x-transition:enter="transition ease-out duration-200" 10136x-transition:enter-start="opacity-0" 10137x-transition:enter-end="opacity-100" 10138x-transition:leave="transition ease-in duration-150" 10139x-transition:leave-start="opacity-100" 10140x-transition:leave-end="opacity-0" 10141@click="open = false" 10142class="fixed inset-0 bg-black/50 backdrop-blur-sm" 10143></div><div @click.self="open = false" class="fixed inset-0 flex items-center justify-center p-4"><div 10144x-show="open" 10145x-transition:enter="transition ease-out duration-200" 10146x-transition:enter-start="opacity-0 scale-95" 10147x-transition:enter-end="opacity-100 scale-100" 10148x-transition:leave="transition ease-in duration-150" 10149x-transition:leave-start="opacity-100 scale-100" 10150x-transition:leave-end="opacity-0 scale-95" 10151@click.stop 10152class="relative bg-white rounded-xl shadow-2xl max-w-sm w-full p-6" 10153> <button 10154@click="open = false" 10155type="button" 10156class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 10157> <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 10158@click="open = false" 10159type="button" 10160class="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" 10161> Close </button></div></div></div></div><div 10162x-data 10163x-show="$store.upsell.isOpen" 10164x-cloak 10165@keydown.escape.window="$store.upsell.isOpen && $store.upsell.skip()" 10166class="fixed inset-0 z-[120] overflow-y-auto" 10167role="dialog" 10168aria-modal="true" 10169aria-labelledby="upsell-modal-title" 10170style="display: none;" 10171><div 10172x-show="$store.upsell.isOpen" 10173x-transition:enter="transition ease-out duration-200" 10174x-transition:enter-start="opacity-0" 10175x-transition:enter-end="opacity-100" 10176x-transition:leave="transition ease-in duration-150" 10177x-transition:leave-start="opacity-100" 10178x-transition:leave-end="opacity-0" 10179@click="$store.upsell.skip()" 10180class="fixed inset-0 bg-black/50 backdrop-blur-sm" 10181></div><div class="fixed inset-0 flex items-center justify-center p-4"><div 10182x-show="$store.upsell.isOpen" 10183x-transition:enter="transition ease-out duration-200" 10184x-transition:enter-start="opacity-0 scale-95" 10185x-transition:enter-end="opacity-100 scale-100" 10186x-transition:leave="transition ease-in duration-150" 10187x-transition:leave-start="opacity-100 scale-100" 10188x-transition:leave-end="opacity-0 scale-95" 10189@click.stop 10190class="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col" 10191><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" 10192: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>
10192 <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" 10193@click="$store.upsell.toggle(product.id)" 10194class="w-full flex items-center justify-center gap-1.5 py-2 text-sm font-medium rounded-lg border transition-colors" 10195: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" 10196class="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()" 10197class="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()" 10198:disabled="$store.upsell.isLoading" 10199class="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 }" 10200x-show="$store.auth.isStaff" 10201x-cloak 10202@mouseenter="open = true" 10203@mouseleave="open = false" 10204class="fixed left-0 top-3/4 -translate-y-1/2 z-[9999] hidden lg:block" 10205style="display: none;"><div x-show="!open" 10206x-transition:enter="transition ease-out duration-200" 10207x-transition:enter-start="opacity-0 -translate-x-2" 10208x-transition:enter-end="opacity-100 translate-x-0" 10209x-transition:leave="transition ease-in duration-150" 10210x-transition:leave-start="opacity-100" 10211x-transition:leave-end="opacity-0" 10212class="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" 10213x-transition:enter="transition ease-out duration-200" 10214x-transition:enter-start="opacity-0 -translate-x-4" 10215x-transition:enter-end="opacity-100 translate-x-0" 10216x-transition:leave="transition ease-in duration-150" 10217x-transition:leave-start="opacity-100 translate-x-0" 10218x-transition:leave-end="opacity-0 -translate-x-4" 10219class="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/' : '#'" 10220target="_blank" 10221class="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">
10221 <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> </svg> Zephyr Theme Settings </a></li><li> <a :href="$store.auth.adminEditBase ? $store.auth.adminEditBase + 'type/blog_post/id/1019/' : '#'" 10222target="_blank" 10223class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:text-brand-primary hover:bg-gray-100 rounded-md transition-colors"> <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m0 0h2a1 1 0 011 1v9a2 2 0 01-2 2m-2-12v10m-9-8h4m-4 4h4"/> </svg> <span class="flex flex-col leading-tight"> <span>Edit Blog Post</span> <span class="text-xs text-gray-500">How Custom Rubber Stamps Are Made</span> </span> </a></li><li class="border-t border-gray-200 pt-2 mt-2"> <button @click="$store.navEdit.open()" 10224class="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>
10224<script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() { 10225Mage.Cookies.set( 10226'FASTLY_CDN_ENV', 10227'250e7c153723fdf375c9db11b6b2b305', 10228new Date(new Date().getTime() + 86400000) 10229); 10230}); </script>
10230
10230<script type="text/javascript"> (function () { 10231var name = 'FASTLY_CDN_FORMKEY'; 10232if (document.cookie.indexOf(name + '=') !== -1) { 10233return; 10234} 10235var expires = new Date(new Date().getTime() + 86400000); 10236document.cookie = name + '=' + 'e3374479ab86b5888526972805eb3d8f' 10237+ '; expires=' + expires.toUTCString() 10238+ '; path=/' + '; domain=.www.simplystamps.com'; 10239})(); </script>
10239<div id="wizardModal" 10240x-data="{ isOpen: false, wizardUrl: '', showConfirm: false, showResume: false, isLoading: true, hasSession: false, pendingUrl: '' }" 10241x-show="isOpen || showResume" 10242x-cloak 10243@keydown.escape.window="showConfirm ? showConfirm = false : (showResume ? showResume = false : null)" 10244class="fixed inset-0 z-50 overflow-hidden" 10245style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 transition-opacity" 10246@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" 10247x-transition:leave="transition ease-in duration-300" 10248x-transition:leave-start="opacity-100" 10249x-transition:leave-end="opacity-0" 10250class="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" 10251:src="wizardUrl" 10252@load="isLoading = false; clearTimeout(window._wizardLoadTimeout);" 10253class="w-full h-full" 10254frameborder="0" 10255allow="clipboard-read; clipboard-write" 10256title="Product Customization Wizard"> </iframe> </template></div></div><div x-show="showResume" 10257x-transition:enter="transition ease-out duration-200" 10258x-transition:enter-start="opacity-0" 10259x-transition:enter-end="opacity-100" 10260x-transition:leave="transition ease-in duration-150" 10261x-transition:leave-start="opacity-100" 10262x-transition:leave-end="opacity-0" 10263class="fixed inset-0 z-[60] flex items-center justify-center p-4" 10264style="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" 10265x-transition:enter="transition ease-out duration-200" 10266x-transition:enter-start="opacity-0 scale-95" 10267x-transition:enter-end="opacity-100 scale-100" 10268x-transition:leave="transition ease-in duration-150" 10269x-transition:leave-start="opacity-100 scale-100" 10270x-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); })" 10271class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"> 10272Start New Design </button> <button @click="showResume = false; isOpen = true;" 10273class="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" 10274x-transition:enter="transition ease-out duration-200" 10275x-transition:enter-start="opacity-0" 10276x-transition:enter-end="opacity-100" 10277x-transition:leave="transition ease-in duration-150" 10278x-transition:leave-start="opacity-100" 10279x-transition:leave-end="opacity-0" 10280class="fixed inset-0 z-[60] flex items-center justify-center p-4" 10281style="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" 10282x-transition:enter="transition ease-out duration-200" 10283x-transition:enter-start="opacity-0 scale-95" 10284x-transition:enter-end="opacity-100 scale-100" 10285x-transition:leave="transition ease-in duration-150" 10286x-transition:leave-start="opacity-100 scale-100" 10287x-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" 10288class="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); })" 10289class="px-4 py-2 text-sm font-medium text-white bg-brand-primary rounded-lg hover:bg-brand-primary/90 transition-colors"> 10290Close Designer </button></div></div></div></div>
10290<script> window._wizardLoadTimeout = null;// Internal: load a fresh wizard URL into the iframe 10291window._wizardStartNew = function(url) { 10292var modal = document.getElementById('wizardModal'); 10293if (!modal || !modal._x_dataStack) return; 10294var data = modal._x_dataStack[0]; 10295data.wizardUrl = ''; 10296setTimeout(function() { 10297data.wizardUrl = url; 10298}, 0); 10299// Fallback: if iframe hasn't loaded after 30s, show error state 10300clearTimeout(window._wizardLoadTimeout); 10301window._wizardLoadTimeout = setTimeout(function() { 10302if (data.isLoading && data.isOpen) { 10303console.error('[Wizard] Load timeout â iframe did not respond'); 10304data.isLoading = false; 10305data.hasSession = false; 10306var iframe = document.getElementById('wizard_wrapper'); 10307if (iframe) { 10308iframe.srcdoc = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;flex-direction:column;gap:16px;color:#374151;">' 10309+ '<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>' 10310+ '<p style="font-size:18px;font-weight:600;margin:0;">Something went wrong</p>' 10311+ '<p style="font-size:14px;color:#6b7280;margin:0;">The designer took too long to load.</p>' 10312+ '<button onclick="window.parent.closeWizardModal(false)" style="margin-top:8px;padding:8px 20px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer;">Close & Try Again</button>' 10313+ '</div>'; 10314} 10315} 10316}, 30000); 10317};// Set when a customer signs in or registers inside the wizard iframe. 10318// 10319// The ajax-login module calls parent.LogInParent() across the frame boundary after a 10320// successful login or registration. Defining that name here is what makes the call land 10321// on Zephyr â without it the notification is silently dropped, since this theme has its 10322// own wizard modal and never loads the PCE one that defines LogInParent. 10323window._wizardAuthChanged = false; 10324window.LogInParent = function() { 10325console.log('[Wizard] Customer authenticated inside the iframe'); 10326window._wizardAuthChanged = true; 10327};// Internal: handle post-close actions (cart refresh etc) 10328window._wizardOnClose = function(refreshCart) { 10329clearTimeout(window._wizardLoadTimeout);// The page behind the wizard was rendered for a logged-out visitor, so everything 10330// keyed off login state is stale â most visibly the My Account entry in the nav. 10331// A reload also picks up the cart, so it supersedes the refresh below. 10332if (window._wizardAuthChanged) { 10333window._wizardAuthChanged = false; 10334console.log('[Wizard] Auth changed inside wizard, reloading host page'); 10335window.location.reload(); 10336return; 10337}if (refreshCart && window.Alpine && Alpine.store('cart')) { 10338setTimeout(function() { 10339console.log('[Wizard] Refreshing cart data...'); 10340Alpine.store('cart').refresh(); 10341}, 300); 10342} 10343};// Global function to open wizard modal 10344window.openWizardModal = function(url) { 10345console.log('[Wizard] Opening modal with URL:', url); 10346var modal = document.getElementById('wizardModal'); 10347if (modal && modal._x_dataStack) { 10348var data = modal._x_dataStack[0]; 10349// If there's an existing session, ask user what to do 10350if (data.hasSession && data.wizardUrl) { 10351data.pendingUrl = url; 10352data.showResume = true; 10353return; 10354} 10355// Fresh open â load the wizard 10356data.isLoading = true; 10357data.isOpen = true; 10358data.hasSession = false; 10359window._wizardStartNew(url); 10360} else { 10361console.error('[Wizard] Modal not found or Alpine.js not initialized'); 10362} 10363};// Allow wizard iframe to close the modal and optionally refresh cart 10364window.closeWizardModal = function(refreshCart) { 10365if (typeof refreshCart === 'undefined') refreshCart = false; 10366console.log('[Wizard] Closing modal, refresh cart:', refreshCart); 10367var modal = document.getElementById('wizardModal'); 10368if (modal && modal._x_dataStack) { 10369var data = modal._x_dataStack[0]; 10370data.isOpen = false; 10371// If the wizard completed (added to cart), destroy the session 10372if (refreshCart) { 10373data.hasSession = false; 10374data.wizardUrl = ''; 10375} else { 10376data.hasSession = true; 10377} 10378window._wizardOnClose(refreshCart); 10379} 10380};// Legacy compatibility: alias for old PCE modal functions 10381window.closeModalWindow = function() { 10382window.closeWizardModal(true); 10383};// Minimal jQuery shim for PCE wizard iframe compatibility 10384// newpce.js calls window.parent.jQuery("iframe#wizard_wrapper").height() 10385if (typeof window.jQuery === 'undefined') { 10386window.jQuery = function(selector) { 10387var el = document.querySelector(selector); 10388return { 10389height: function() { return el ? el.clientHeight : window.innerHeight; } 10390}; 10391}; 10392}// Called by wizard when user clicks close button - shows confirmation dialog 10393window.requestCloseIframe = function() {
10394console.log('[Wizard] Close requested, showing confirmation'); 10395var modal = document.getElementById('wizardModal'); 10396if (modal && modal._x_dataStack) { 10397modal._x_dataStack[0].showConfirm = true; 10398} 10399};window.redirectToCart = function() { 10400window.location.href = '/checkout/cart'; 10401}; </script>
10401<div x-data="loginModal()" 10402x-show="$store.auth.showLoginModal" 10403x-cloak 10404@keydown.escape.window="$store.auth.showLoginModal = false" 10405class="fixed inset-0 z-50 overflow-y-auto" 10406style="display: none;"><div x-show="$store.auth.showLoginModal" 10407x-transition:enter="transition ease-out duration-300" 10408x-transition:enter-start="opacity-0" 10409x-transition:enter-end="opacity-100" 10410x-transition:leave="transition ease-in duration-200" 10411x-transition:leave-start="opacity-100" 10412x-transition:leave-end="opacity-0" 10413@click="if (Date.now() - $store.auth._loginOpenedAt > 300) $store.auth.showLoginModal = false" 10414class="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" 10415x-transition:enter="transition ease-out duration-300" 10416x-transition:enter-start="opacity-0 transform scale-95" 10417x-transition:enter-end="opacity-100 transform scale-100" 10418x-transition:leave="transition ease-in duration-200" 10419x-transition:leave-start="opacity-100 transform scale-100" 10420x-transition:leave-end="opacity-0 transform scale-95" 10421@click.stop 10422class="relative bg-white rounded-lg shadow-xl w-full max-w-md"> <button @click="$store.auth.showLoginModal = false" 10423class="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" 10424: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'" 10425class="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" 10426alt="Simply Stamps" 10427class="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" 10428id="login-email" 10429name="login[username]" 10430x-model="loginForm.username" 10431required 10432class="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" 10433id="login-password" 10434name="login[password]" 10435x-model="loginForm.password" 10436required 10437class="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" 10438id="login-remember" 10439name="persistent_remember_me" 10440x-model="loginForm.rememberMe" 10441class="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" 10442:disabled="loading" 10443class="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" 10444@click="switchForm('register')" 10445class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10445ded-md transition-colors"> Create an Account </button></div><div class="mt-4 text-center"> <button type="button" 10446@click="switchForm('forgot')" 10447class="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" 10448: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'" 10449class="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" 10450alt="Simply Stamps" 10451class="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" 10452id="register-firstname" 10453name="firstname" 10454x-model="registerForm.firstname" 10455required 10456class="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" 10457id="register-lastname" 10458name="lastname" 10459x-model="registerForm.lastname" 10460required 10461class="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" 10462id="register-email" 10463name="email" 10464x-model="registerForm.email" 10465required 10466class="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" 10467id="register-password" 10468name="password" 10469x-model="registerForm.password" 10470required 10471class="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" 10472id="register-confirmation" 10473name="confirmation" 10474x-model="registerForm.confirmation" 10475required 10476class="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" 10477id="register-newsletter" 10478name="is_subscribed" 10479x-model="registerForm.newsletter" 10480value="1" 10481class="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" 10482:disabled="loading" 10483class="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" 10484@click="switchForm('login')" 10485class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-4 roun
10485ded-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" 10486: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'" 10487class="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" 10488alt="Simply Stamps" 10489class="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" 10490id="forgot-email" 10491name="email" 10492x-model="forgotForm.email" 10493required 10494class="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" 10495:disabled="loading" 10496class="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" 10497@click="switchForm('login')" 10498class="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>
10498<script> function loginModal() { 10499return { 10500currentForm: 'login', 10501loading: false, 10502message: '', 10503messageType: 'error', 10504formKey: 'e3374479ab86b5888526972805eb3d8f',loginForm: { 10505username: '', 10506password: '', 10507rememberMe: false 10508},registerForm: { 10509firstname: '', 10510lastname: '', 10511email: '', 10512password: '', 10513confirmation: '', 10514newsletter: false 10515},forgotForm: { 10516email: '' 10517},switchForm(formName) { 10518this.currentForm = formName; 10519this.message = ''; 10520this.loading = false; 10521},async submitLogin() { 10522this.loading = true; 10523this.message = '';const formData = new FormData(); 10524formData.append('form_key', this.formKey); 10525formData.append('login[username]', this.loginForm.username); 10526formData.append('login[password]', this.loginForm.password); 10527if (this.loginForm.rememberMe) { 10528formData.append('persistent_remember_me', 'on'); 10529}try { 10530const response = await fetch('https://www.simplystamps.com/zephyr/auth/login/', { 10531method: 'POST', 10532body: formData, 10533credentials: 'same-origin' 10534});const data = await response.json();if (data.success) { 10535if (typeof zGA4 === 'function') { 10536zGA4('login', { method: 'site' }); 10537if (data.hashedUserData) { 10538zGA4('set_user_data', { user_data: data.hashedUserData }); 10539} 10540} 10541window.location.reload(); 10542} else { 10543await new Promise(resolve => setTimeout(resolve, 800)); 10544this.message = data.error || 'Invalid login or password.'; 10545this.messageType = 'error'; 10546this.loading = false; 10547} 10548} catch (error) { 10549await new Promise(resolve => setTimeout(resolve, 800)); 10550this.message = 'An error occurred. Please try again.'; 10551this.messageType = 'error'; 10552this.loading = false; 10553} 10554},async submitRegister() { 10555this.loading = true; 10556this.message = '';if (this.registerForm.password !== this.registerForm.confirmation) { 10557await new Promise(resolve => setTimeout(resolve, 800)); 10558this.message = 'Passwords do not match.'; 10559this.messageType = 'error'; 10560this.loading = false; 10561return; 10562}const formData = new FormData(); 10563formData.append('form_key', this.formKey); 10564formData.append('firstname', this.registerForm.firstname); 10565formData.append('lastname', this.registerForm.lastname); 10566formData.append('email', this.registerForm.email); 10567formData.append('password', this.registerForm.password); 10568formData.append('confirmation', this.registerForm.confirmation); 10569if (this.registerForm.newsletter) { 10570formData.append('is_subscribed', '1'); 10571}const recaptchaToken = await getZephyrRecaptchaToken('register'); 10572if (recaptchaToken) { 10573formData.append('recaptcha_response', recaptchaToken); 10574}try { 10575const response = await fetch('https://www.simplystamps.com/zephyr/auth/register/', { 10576method: 'POST', 10577body: formData, 10578credentials: 'same-origin' 10579});const data = await response.json();if (data.success) { 10580if (typeof zGA4 === 'function') { 10581zGA4('sign_up', { method: 'site' }); 10582if (data.hashedUserData) { 10583zGA4('set_user_data', { user_data: data.hashedUserData }); 10584} 10585} 10586window.location.reload(); 10587} else { 10588await new Promise(resolve => setTimeout(resolve, 800)); 10589this.message = data.error || 'There was a problem with your registration.'; 10590this.messageType = 'error'; 10591this.loading = false; 10592} 10593} catch (error) { 10594await new Promise(resolve => setTimeout(resolve, 800)); 10595this.message = 'An error occurred. Please try again.'; 10596this.messageType = 'error'; 10597this.loading = false; 10598} 10599},async submitForgot() { 10600this.loading = true; 10601this.message = '';const formData = new FormData(); 10602formData.append('form_key', this.formKey); 10603formData.append('email', this.forgotForm.email);// reCAPTCHA v3. The forgotpasswordpost endpoint rejects a token-less post 10604// outright, so no token means no reset email. 10605const recaptchaToken = await getZephyrRecaptchaToken('forgot'); 10606if (recaptchaToken) { 10607formData.append('recaptcha_response', recaptchaToken); 10608}try { 10609const response = await fetch('https://www.simplystamps.com/customer/account/forgotpasswordpost/', { 10610method: 'POST', 10611body: formData, 10612credentials: 'same-origin' 10613});const text = await response.text();if (text.includes('success-msg') || text.includes('password-reset')) { 10614this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 10615this.messageType = 'success'; 10616this.forgotForm.email = ''; 10617} else if (text.includes('error-msg')) { 10618const parser = new DOMParser(); 10619const doc = parser.parseFromString(text, 'text/html'); 10620const errorMsg = doc.querySelector('.error-msg'); 10621this.message = errorMsg ? errorMsg.textContent.trim() : 'Unable to send password reset email.'; 10622this.messageType = 'error'; 10623} else { 10624this.message = 'If there is an account associated with this email, you will receive an email with a link to reset your password.'; 10625this.messageType = 'success'; 10626} 10627} catch (error) { 10628this.message = 'An error occurred. Please try again.'; 10629this.messageType = 'error'; 10630} finally { 10631this.loading = false;
10632} 10633} 10634} 10635} </script>
10635 <input id="searchFocusProxy" type="text" aria-hidden="true" 10636style="position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;" /><div id="searchModal" 10637x-data="searchModal()" 10638x-show="$store.search.isOpen" 10639x-cloak 10640@keydown.escape.window="$store.search.close()" 10641class="fixed inset-0 z-[70] overflow-hidden" 10642style="display: none;"><div class="fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm transition-opacity hidden md:block" 10643x-show="$store.search.isOpen" 10644x-transition:enter="transition ease-out duration-300" 10645x-transition:enter-start="opacity-0" 10646x-transition:enter-end="opacity-100" 10647x-transition:leave="transition ease-in duration-200" 10648x-transition:leave-start="opacity-100" 10649x-transition:leave-end="opacity-0" 10650@click="$store.search.close()"></div><div class="fixed inset-0 flex items-start justify-center md:pt-12 md:px-4" 10651@mousedown.self="$store.search.close()"><div x-show="$store.search.isOpen" 10652class="relative bg-white w-full h-full flex flex-col md:h-auto md:max-w-2xl md:rounded-xl md:shadow-2xl" 10653x-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" 10654alt="Simply Stamps" 10655class="h-8 w-auto" /> </a> <button @click="$store.search.close()" 10656class="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" 10657d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg></div> <input type="text" 10658x-ref="searchInput" 10659x-model="query" 10660@input="onInput()" 10661@keydown.down.prevent="selectNext" 10662@keydown.tab.prevent="selectNext" 10663@keydown.up.prevent="selectPrev" 10664@keydown.enter.prevent="selectCurrent" 10665placeholder="Search products, articles..." 10666class="w-full pl-12 pr-24 py-4 text-lg border-2 border-gray-200 rounded-xl 10667focus:border-brand-primary focus:outline-none focus:ring-0 10668transition-colors" 10669autocomplete="off" /><div x-show="$store.search.isLoading" 10670class="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" 10671@click="clearSearch()" 10672type="button" 10673class="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()" 10674type="button" 10675:disabled="query.length < minChars" 10676class="absolute right-2 flex items-center justify-center h-10 px-3 bg-brand-primary text-white 10677rounded-lg hover:bg-brand-primary-hover transition-colors 10678disabled: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" 10679d="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" 10680@click.prevent="selectPopular('self-inking stamp')" 10681class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10682rounded-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" 10683d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> self-inking stamp </button> <button type="button" 10684@click.prevent="selectPopular('wood handle stamp')" 10685class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10686rounded-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" 10687d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> wood handle stamp </button> <button type="button" 10688@click.prevent="selectPopular('library embosser')" 10689class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10690rounded-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" 10691d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> </svg> library embosser </button> <button type="button" 10692@click.prevent="selectPopular('wedding address stamps')" 10693class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10694rounded-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" 10695d="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" 10696x-ref="resultsPane" 10697class="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)" 10698:class="{ 'bg-blue-50 text-blue-900': selectedIndex === index }" 10699class="flex items-center gap-3 px-3 py-1.5 rounded-lg 10700hover:bg-gray-50 transition-colors cursor-pointer group" 10701@mousemove="selectedIndex = index" 10702@click.prevent="selectSuggestion(suggestion)"> <svg class="w-4 h-4 text-gray-300 shrink-0" :class="{ 'text-blue-400': selectedIndex === index }" 10703fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 10704d="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" 10705x-transition:enter="transition ease-out duration-200" 10706x-transition:enter-start="opacity-0" 10707x-transition:enter-end="opacity-100" 10708x-transition:leave="transition ease-in duration-150" 10709x-transition:leave-start="opacity-100" 10710x-transition:leave-end="opacity-0" 10711class="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" 10712:class="{ 10713'border-t border-gray-300': suggestionsCount > 0 || (blogFirst && blogCount > 0), 10714'opacity-40 pointer-events-none': $store.search.isLoading 10715}"><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" 10716:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (productsOffset + index) }" 10717class="flex items-center gap-4 p-3 rounded-lg border border-transparent 10718hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer" 10719@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" 10720class="w-full h-full object-cover" 10721onerror="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" 10722class="text-xs text-gray-500 line-clamp-1 mt-0.5" 10723x-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" 10724:class="{ 10725'order-3': blogFirst, 10726'order-5': !blogFirst, 10727'border-t border-gray-300': !blogFirst || suggestionsCount > 0, 10728'opacity-40 pointer-events-none': $store.search.isLoading 10729}"><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" 10730:class="{ 'bg-blue-50 border-blue-200': selectedIndex === (blogOffset + index) }" 10731class="flex items-center gap-4 p-3 rounded-lg border border-transparent 10732hover:bg-gray-50 hover:border-gray-200 transition-colors cursor-pointer" 10733@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" 10734d="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" 10735class="text-xs text-gray-500 line-clamp-1 mt-0.5" 10736x-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" 10737class="inline-flex items-center px-3 py-2 bg-gray-100 hover:bg-gray-200 10738rounded-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" 10739d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path> </svg> <span x-text="category.name"></span> </a> </template></div></div> </template></div><div class="p-4 border-t border-gray-300 bg-gray-50 md:rounded-b-xl shrink-0"><div class="flex items-center justify-between"><div class="hidden md:flex items-center gap-6 text-xs text-gray-400"> <span class="flex items-center gap-1"> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">↑</kbd> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">↓</kbd> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">Tab</kbd> to navigate </span> <span class="flex items-center gap-1"> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">Enter</kbd> to select </span> <span class="flex items-center gap-1"> <kbd class="px-2 py-1 bg-gray-200 rounded text-gray-600">Esc</kbd> to close </span></div> <a x-show="query.length >= minChars" 10740:href="'https://www.simplystamps.com/catalogsearch/result/?q=' + encodeURIComponent(query)" 10741class="flex items-center gap-1 text-sm font-medium text-brand-primary hover:text-brand-primary-hover 10742transition-colors ml-auto"> 10743View 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>
10743<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/search-modal.js?v=1126"></script>
10743
10743<script> searchModalConfig.minChars = 3; 10744searchModalConfig.searchUrl = 'https://www.simplystamps.com/searchautocomplete/ajax/get/'; 10745searchModalConfig.suggestUrl = 'https://www.simplystamps.com/zephyr/search/suggest/'; 10746searchModalConfig.resultUrl = 'https://www.simplystamps.com/catalogsearch/result/'; 10747searchModalConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10748searchModalConfig.suggestedQueries = ["self-inking stamp","wood handle stamp","library embosser","wedding address stamps"]; 10749searchModalConfig.blogFirst = true; </script>
10749<style> .line-clamp-1 { 10750overflow: hidden; 10751display: -webkit-box; 10752-webkit-box-orient: vertical; 10753-webkit-line-clamp: 1; 10754} </style></div></div><link rel="preload" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"></noscript>
10754<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>
10754
10754<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/rileyprint.js?v=1126" defer></script>
10754
10754<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/product-media.js?v=1126" defer></script>
10754
10754<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/file-upload.js?v=1126" defer></script>
10754
10754<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-drawer.js?v=1126" defer></script>
10754
10754<script> // Configure cart drawer URLs. cart-drawer.js is now deferred (its var cartDrawerConfig 10755// default object still runs before DOMContentLoaded per spec), and every read of 10756// cartDrawerConfig.* inside cart-drawer.js happens lazily inside user-triggered 10757// handlers (open drawer, add/remove item, checkout) â none of it runs at 10758// alpine:init â so it's safe to set these values on DOMContentLoaded too. 10759document.addEventListener('DOMContentLoaded', function() { 10760cartDrawerConfig.getItemsUrl = 'https://www.simplystamps.com/zephyr/cart/getItems/'; 10761cartDrawerConfig.deleteUrl = 'https://www.simplystamps.com/checkout/cart/delete/'; 10762cartDrawerConfig.removeItemsUrl = 'https://www.simplystamps.com/zephyr/cart/removeItems/'; 10763cartDrawerConfig.updatePostUrl = 'https://www.simplystamps.com/checkout/cart/updatePost/'; 10764cartDrawerConfig.clearUrl = 'https://www.simplystamps.com/zephyr/cart/clear/'; 10765cartDrawerConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/'; 10766cartDrawerConfig.cartUrl = 'https://www.simplystamps.com/checkout/cart/'; 10767cartDrawerConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10768// Cart drawer feature settings (from admin config) 10769cartDrawerConfig.enableCheckboxDelete = true; 10770cartDrawerConfig.enableIndividualDelete = true; 10771cartDrawerConfig.showClearCart = true; 10772cartDrawerConfig.checkoutButtonText = 'Proceed to Checkout'; 10773}); </script>
10773
10773<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/cart-upsell.js?v=1126"></script>
10773
10773<script> // Configure cart upsell popup 10774cartUpsellConfig.getUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/getUpsells/'; 10775cartUpsellConfig.addUpsellsUrl = 'https://www.simplystamps.com/zephyr/cart/addUpsells/'; 10776cartUpsellConfig.checkoutUrl = 'https://www.simplystamps.com/checkout/onepage/'; 10777cartUpsellConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; 10778cartUpsellConfig.enabled = true; 10779cartUpsellConfig.storeId = '5'; 10780cartUpsellConfig.devMode = false; </script>
10780<script> // Fetch a reCAPTCHA v3 token, lazy-loading Google's api.js on first use so it 10781// adds no page weight until a protected form is actually submitted. Shared by 10782// every Zephyr form protected by v3 (registration, contact, quote), which is 10783// also why it must be the ONLY thing loading recaptcha on modern pages â a 10784// v3 key can't execute against a grecaptcha client created by a bare v2 10785// api.js load, so don't reintroduce v2 script tags into modern templates. 10786// Resolves null when reCAPTCHA is disabled or the script/execution fails â 10787// the server decides what to do with a missing token. Never rejects or hangs. 10788window.zephyrRecaptchaKey = '6Lem-q0gAAAAAMhOqUz2tke2IN3t0Pt37x5oqRq3'; 10789window.getZephyrRecaptchaToken = function(action) { 10790var key = window.zephyrRecaptchaKey; 10791if (!key) return Promise.resolve(null); 10792return new Promise(function(resolve) { 10793var settled = false; 10794var done = function(token) { if (!settled) { settled = true; resolve(token || null); } }; 10795// Whatever happens, never leave the submit hanging 10796setTimeout(function() { done(null); }, 15000);function execute() { 10797try { 10798grecaptcha.ready(function() { 10799try { 10800grecaptcha.execute(key, { action: action }) 10801.then(done, function() { done(null); }); 10802} catch (e) { done(null); } 10803}); 10804} catch (e) { done(null); } 10805}if (window.grecaptcha) { execute(); return; } 10806var s = document.createElement('script'); 10807s.src = 'https://www.google.com/recaptcha/api.js?render=' + key; 10808s.onload = execute; 10809s.onerror = function() { done(null); }; 10810document.head.appendChild(s); 10811}); 10812}; </script>
10812
10812<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/components/promo-code.js?v=1126"></script>
10812
10812<script> // Configure promo code URLs 10813promoCodeConfig.applyCouponUrl = 'https://www.simplystamps.com/zephyr/cart/applyCoupon/'; 10814promoCodeConfig.removeCouponUrl = 'https://www.simplystamps.com/zephyr/cart/removeCoupon/'; 10815promoCodeConfig.formKey = 'e3374479ab86b5888526972805eb3d8f'; </script>
10815
10815<script>function setLocation(url){window.location.href=url;}</script>
10815
10815<script src="https://cdn.simplystamps.com/skin/frontend/zephyr/default/js/alpine.min.js?v=1126" defer></script>
10815
10815<script> document.addEventListener('alpine:init', () => { 10816// Global UI state - controls visibility of shared UI elements 10817Alpine.store('ui', { 10818hideHeader: false, 10819// Header scroll state (updated by stickyHeader component) 10820headerVisible: true, 10821headerHeight: 0,// Call this when opening fullscreen overlays (lightbox, modals, etc.) 10822enterFullscreen() { 10823this.hideHeader = true; 10824},// Call this when closing fullscreen overlays 10825exitFullscreen() { 10826this.hideHeader = false; 10827},// Get the effective top position for fixed elements below the header 10828getHeaderOffset() { 10829// If header is hidden (scroll or fullscreen), no offset needed 10830if (this.hideHeader || !this.headerVisible) { 10831return 0; 10832} 10833return this.headerHeight; 10834} 10835});Alpine.store('auth', { 10836showLoginModal: false, 10837showRefreshOverlay: false, 10838refreshMessage: '', 10839isLoggedIn: false, 10840isStaff: false, 10841adminEditBase: '', 10842customerName: '', 10843customerEmail: '', 10844companyStoreUrl: '', 10845cartCount: 0, 10846cartSubtotal: 0, 10847formKey: '', 10848ready: false, 10849_loginOpenedAt: 0,init() { 10850// State is hydrated via AJAX so the page HTML stays static/compressible. 10851this.fetchStatus(); 10852},/** 10853* Fetch auth status from session via AJAX. 10854* Primary hydration path (called from init()), and also used 10855* after login/logout to refresh state reactively. 10856*/ 10857fetchStatus() { 10858var self = this; 10859fetch('https://www.simplystamps.com/zephyr/auth/status/', { 10860credentials: 'same-origin', 10861headers: { 'X-Requested-With': 'XMLHttpRequest' } 10862}) 10863.then(function(r) { return r.json(); }) 10864.then(function(data) { 10865self.isLoggedIn = !!data.isLoggedIn; 10866self.isStaff = !!data.isStaff; 10867self.adminEditBase = data.adminEditBase || ''; 10868self.customerName = data.customerName || ''; 10869self.customerEmail = data.customerEmail || ''; 10870self.companyStoreUrl = data.companyStoreUrl || ''; 10871self.cartCount = data.cartCount || 0; 10872self.cartSubtotal = data.cartSubtotal || 0; 10873self.formKey = data.formKey || '';// Hydrate the cart store's badge count / free-shipping subtotal from 10874// this same response â the async equivalent of the inline global 10875// state object the ESI block used to provide synchronously. 10876if (window.Alpine && Alpine.store('cart')) { 10877var cartStore = Alpine.store('cart'); 10878cartStore.itemCount = data.cartCount || 0; 10879cartStore.isEmpty = !(data.cartCount > 0); 10880cartStore.subtotalNumeric = data.cartSubtotal || 0; 10881cartStore._esiReady = true; 10882} 10883if (data.messages && data.messages.length && window.Alpine) { 10884var flash = Alpine.store('flash'); 10885for (var i = 0; i < data.messages.length; i++) { 10886if (flash && flash.addMessage) flash.addMessage(data.messages[i].type, data.messages[i].text); 10887} 10888} 10889self.ready = true; 10890}) 10891.catch(function() { 10892self.ready = true; 10893}); 10894},setLoggedIn(customerData) { 10895this.isLoggedIn = true; 10896this.customerName = customerData.name || '';window.dispatchEvent(new CustomEvent('customer-logged-in', { 10897detail: customerData 10898})); 10899},setLoggedOut() { 10900this.isLoggedIn = false; 10901this.isStaff = false;
10902this.adminEditBase = ''; 10903this.customerName = ''; 10904this.customerEmail = ''; 10905this.companyStoreUrl = '';window.dispatchEvent(new CustomEvent('customer-logged-out')); 10906} 10907});Alpine.store('search', { 10908isOpen: false, 10909isLoading: false, 10910hasSearched: false, 10911_openedAt: 0,open() { 10912// Focus proxy input immediately to preserve iOS user-gesture context 10913// so the virtual keyboard opens. Focus transfers to real input in $nextTick. 10914var proxy = document.getElementById('searchFocusProxy'); 10915if (proxy) proxy.focus(); 10916this.isOpen = true; 10917this._openedAt = Date.now(); 10918},close() { 10919if (Date.now() - this._openedAt < 300) return; 10920this.isOpen = false; 10921} 10922});// Confirmation modal store 10923Alpine.store('confirm', { 10924isOpen: false, 10925title: '', 10926message: '', 10927html: null, 10928confirmText: 'Confirm', 10929cancelText: 'Cancel', 10930confirmStyle: 'danger', // 'danger' or 'primary' 10931onConfirm: null, 10932onCancel: null,show(options) { 10933this.title = options.title || 'Confirm'; 10934this.message = options.message || 'Are you sure?'; 10935this.html = options.html || null; 10936this.confirmText = options.confirmText || 'Confirm'; 10937this.cancelText = options.cancelText || 'Cancel'; 10938this.confirmStyle = options.confirmStyle || 'danger'; 10939this.onConfirm = options.onConfirm || null; 10940this.onCancel = options.onCancel || null; 10941this.isOpen = true; 10942},confirm() { 10943if (this.onConfirm) { 10944this.onConfirm(); 10945} 10946this.close(); 10947},cancel() { 10948if (this.onCancel) { 10949this.onCancel(); 10950} 10951this.close(); 10952},close() { 10953this.isOpen = false; 10954this.onConfirm = null; 10955this.onCancel = null; 10956// Defer clearing html until after the 150ms leave transition 10957// so the image doesn't vanish mid-animation 10958var self = this; 10959setTimeout(function() { 10960if (!self.isOpen) self.html = null; 10961}, 200); 10962} 10963});// Flash messages store â messages arrive via auth store's fetchStatus() (AJAX), 10964// or as inline JSON on pages that embed them directly (fallback below). 10965Alpine.store('flash', { 10966messages: [], 10967_nextId: 1, 10968_seen: {},init() { 10969var self = this;// Fallback: read messages embedded as JSON in page HTML (for pages without ESI block) 10970var scriptEl = document.querySelector('script[data-flash-messages]'); 10971if (scriptEl) { 10972try { 10973var inline = JSON.parse(scriptEl.textContent); 10974for (var i = 0; i < inline.length; i++) { 10975self.addMessage(inline[i].type, inline[i].text); 10976} 10977} catch (e) {} 10978scriptEl.remove(); 10979} 10980},addMessage(type, text) { 10981// Deduplication 10982var key = type + ':' + text; 10983if (this._seen[key]) { 10984return; 10985} 10986this._seen[key] = true;var id = this._nextId++; 10987this.messages.push({ id: id, type: type, text: text, visible: true });// Auto-dismiss non-error messages after 8 seconds 10988if (type !== 'error') { 10989var self = this; 10990setTimeout(function() { 10991self.dismiss(id); 10992}, 8000); 10993} 10994},dismiss(id) { 10995var idx = this.messages.findIndex(function(m) { return m.id === id; }); 10996if (idx !== -1) { 10997this.messages[idx].visible = false; 10998var self = this; 10999setTimeout(function() { 11000self.messages = self.messages.filter(function(m) { return m.id !== id; }); 11001}, 300); 11002} 11003},show(type, text) { 11004this.addMessage(type, text); 11005} 11006});Alpine.store('flash').init(); 11007});// BFCache restoration handler â when iOS Safari restores a page from BFCache, 11008// Alpine stores are frozen with stale data. Refresh critical stores. 11009window.addEventListener('pageshow', function(event) { 11010if (event.persisted) { 11011// Page was restored from BFCache â refresh cart data and auth status 11012if (typeof Alpine !== 'undefined' && Alpine.store) { 11013var cart = Alpine.store('cart'); 11014if (cart) { 11015cart.lastFetch = null; 11016cart.refresh(); 11017} 11018var auth = Alpine.store('auth'); 11019if (auth && typeof auth.fetchStatus === 'function') { 11020auth.fetchStatus(); 11021} 11022} 11023} 11024}); 11025</script>
11025<div x-data 11026x-show="$store.auth.showRefreshOverlay" 11027x-cloak 11028x-transition:enter="transition ease-out duration-200" 11029x-transition:enter-start="opacity-0" 11030x-transition:enter-end="opacity-100" 11031class="fixed inset-0 z-[200] flex items-center justify-center bg-white/80 backdrop-blur-sm" 11032style="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 11033x-data 11034x-show="$store.confirm.isOpen" 11035x-cloak 11036@keydown.escape.window="$store.confirm.cancel()" 11037class="fixed inset-0 z-[100] overflow-y-auto" 11038aria-labelledby="confirm-modal-title" 11039role="dialog" 11040aria-modal="true" 11041><div 11042x-show="$store.confirm.isOpen" 11043x-transition:enter="transition ease-out duration-200" 11044x-transition:enter-start="opacity-0" 11045x-transition:enter-end="opacity-100" 11046x-transition:leave="transition ease-in duration-150" 11047x-transition:leave-start="opacity-100" 11048x-transition:leave-end="opacity-0" 11049@click="$store.confirm.cancel()" 11050class="fixed inset-0 bg-black/50 backdrop-blur-sm" 11051></div><div class="fixed inset-0 flex items-center justify-center p-4"><div 11052x-show="$store.confirm.isOpen" 11053x-transition:enter="transition ease-out duration-200" 11054x-transition:enter-start="opacity-0 scale-95" 11055x-transition:enter-end="opacity-100 scale-100" 11056x-transition:leave="transition ease-in duration-150" 11057x-transition:leave-start="opacity-100 scale-100" 11058x-transition:leave-end="opacity-0 scale-95" 11059@click.stop 11060class="relative bg-white rounded-xl shadow-2xl max-w-md w-full p-6" 11061> <button 11062@click="$store.confirm.cancel()" 11063type="button" 11064class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10" 11065> <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" 11066:class="$store.confirm.confirmStyle === 'danger' ? 'bg-red-100' : 'bg-blue-100'"> <svg x-show="$store.confirm.confirmStyle === 'danger'" 11067class="h-6 w-6 text-red-600" 11068fill="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'" 11069class="h-6 w-6 text-blue-600" 11070fill="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" 11071class="text-lg font-semibold text-gray-900 text-center mb-2" 11072x-text="$store.confirm.title"></h3><div x-show="$store.confirm.html" class="text-sm text-gray-600 text-center mb-6" 11073x-html="$store.confirm.html"></div><p x-show="!$store.confirm.html" class="text-sm text-gray-600 text-center mb-6" 11074x-text="$store.confirm.message"></p><div class="flex gap-3"> <button 11075@click="$store.confirm.cancel()" 11076type="button" 11077class="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" 11078x-text="$store.confirm.cancelText"> </button> <button 11079@click="$store.confirm.confirm()" 11080type="button" 11081class="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" 11082:class="$store.confirm.confirmStyle === 'danger' 11083? 'bg-red-600 hover:bg-red-700 focus:ring-red-500' 11084: 'bg-brand-primary hover:bg-brand-primary-hover focus:ring-brand-primary'" 11085x-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.