{"product_id":"designer-fragrances-choose-your-perfume","title":"Designer Fragrances –  Giorgio Armani, CK Shock, CK One Or Nautica Voyage","description":"\u003cp class=\"PDq2pG_selectionAnchorContainer\"\u003eChoose the fragrance that suits you from this curated selection of designer perfumes, with each option offering its own size, scent profile and price.\u003cspan class=\"PDq2pG_selectionAnchor\"\u003e\u003c\/span\u003e\u003c\/p\u003e\u003cp\u003eSelect your preferred perfume from the options below to view the corresponding price. The selection includes Nautica Voyage Eau de Toilette 100ml, Giorgio Armani Acqua di Giò Refillable Eau de Parfum 40ml, Calvin Klein CK One Shock for Him Eau de Toilette 100ml and Calvin Klein CK One Eau de Toilette 50ml.\u003c\/p\u003e\u003cp\u003eWhether you are shopping for everyday wear, a signature scent or a thoughtful gift, this multi-option fragrance product makes it simple to choose the perfume you want in the size and format shown.\u003c\/p\u003e\u003ch3\u003eKey features \u003c\/h3\u003e\u003cul\u003e\n\u003cli\u003eFour perfume options in one product listing\u003c\/li\u003e\n\u003cli\u003eNautica Voyage Eau de Toilette – 100ml\u003c\/li\u003e\n\u003cli\u003eGiorgio Armani Acqua di Giò Refillable Eau de Parfum – 40ml\u003c\/li\u003e\n\u003cli\u003eCalvin Klein CK One Shock for Him Eau de Toilette – 100ml\u003c\/li\u003e\n\u003cli\u003eCalvin Klein CK One Eau de Toilette – 50ml\u003c\/li\u003e\n\u003cli\u003eOption-specific pricing that changes with your selection\u003c\/li\u003e\n\u003cli\u003eSuitable for everyday wear, special occasions and gifting\u003c\/li\u003e\n\u003c\/ul\u003e\n\n\u003c!--- CODE FOR LISTENING TO GALLERY CLICKS ---\u003e\n\n\u003cstyle\u003e\n  \/* Price stays hidden until a bottle (variant) is chosen.\n     The container itself stays visible so the drop note shows. *\/\n  html.sb-pick-first [ref=\"priceContainer\"] .price__regular,\n  html.sb-pick-first [ref=\"priceContainer\"] .price__sale,\n  html.sb-pick-first [ref=\"priceContainer\"] .price-item__group,\n  html.sb-pick-first [ref=\"priceContainer\"] .product-on-sale-label {\n    display: none !important;\n  }\n\n  \/* Blur the price while the theme loads the new variant's price *\/\n  [ref=\"priceContainer\"] .price__regular,\n  [ref=\"priceContainer\"] .price__sale {\n    transition: filter .2s ease, opacity .2s ease;\n  }\n\n  html.sb-price-loading [ref=\"priceContainer\"] .price__regular,\n  html.sb-price-loading [ref=\"priceContainer\"] .price__sale {\n    filter: blur(6px);\n    opacity: .6;\n    pointer-events: none;\n  }\n\n  \/* Selected thumbnail *\/\n  .custom-product-gallery__thumb.is-active {\n    border: 2px solid #111 !important;\n  }\n\n  \/* ---------- Out of stock banner ---------- *\/\n  .custom-product-gallery__thumb,\n  .sb-main-image-wrap {\n    position: relative;\n  }\n\n  \/* Banner colours: change these to restyle it *\/\n  :root {\n    --sb-oos-bg: #0057d9;     \/* blue band *\/\n    --sb-oos-text: #ffffff;   \/* white text *\/\n    --sb-oos-border: #ffffff; \/* white borders *\/\n  }\n\n  \/* Anything with a banner clips it to its own edges *\/\n  .is-sold-out {\n    overflow: hidden !important;\n  }\n\n  \/* Band running corner to corner (bottom-left to top-right).\n     The script sets the exact angle and length for each image. *\/\n  .sb-oos-banner {\n    position: absolute !important;\n    left: 50% !important;\n    top: 50% !important;\n    width: var(--sb-oos-len, 150%) !important;\n    transform: translate(-50%, -50%) rotate(var(--sb-oos-angle, -45deg));\n    transform-origin: center;\n    z-index: 5 !important;\n    display: block !important;\n    box-sizing: border-box;\n    padding: 3px 0;\n    background: var(--sb-oos-bg);\n    color: var(--sb-oos-text);\n    border-top: 2px solid var(--sb-oos-border);\n    border-bottom: 2px solid var(--sb-oos-border);\n    box-shadow: 0 2px 6px rgba(0, 0, 0, .25);\n    font-size: 9px;\n    font-weight: 700;\n    letter-spacing: .08em;\n    line-height: 1.2;\n    text-align: center;\n    text-transform: uppercase;\n    white-space: nowrap;\n    pointer-events: none; \/* never blocks clicks, so it stays selectable *\/\n  }\n\n  .sb-main-image-wrap \u003e .sb-oos-banner {\n    padding: 12px 0;\n    border-top-width: 4px;\n    border-bottom-width: 4px;\n    font-size: 22px;\n    letter-spacing: .12em;\n  }\n\n  .sb-oos-wrap {\n    position: relative;\n    display: inline-block;\n    line-height: 0;\n  }\n\n  \/* Slightly fade out-of-stock images *\/\n  .sb-oos-wrap.is-sold-out img,\n  .custom-product-gallery__thumb.is-sold-out img,\n  .sb-main-image-wrap.is-sold-out img {\n    opacity: .6;\n  }\n\u003c\/style\u003e\n\n\u003cscript\u003e\n(function () {\n\n  \/* =====================================================================\n     OUT OF STOCK SWITCHES\n     true  = out of stock (banner shows on its image)\n     false = in stock (or just delete the line)\n\n     The name must match the text on the variant pill (the button the\n     customer clicks). Capitals, extra spaces and the type of dash\n     (- – —) don't matter. You can also use the variant ID instead.\n     ===================================================================== *\/\n  const OUT_OF_STOCK = {\n    \"Calvin Klein CK One Shock for Him Eau de Toilette – 100ml\": true,\n    \"Calvin Klein CK One Eau de Toilette – 50ml\": true,\n  };\n\n  const OUT_OF_STOCK_TEXT = \"Sold out\";\n  \/* ===================================================================== *\/\n\n\n  var gallery = document.querySelector('.custom-product-gallery');\n  if (!gallery) return;\n\n  var map = {};\n  try {\n    map = JSON.parse(document.getElementById('CustomVariantImages').textContent);\n  } catch (e) {}\n\n  \/\/ Only run on products with more than one variant image\n  if (Object.keys(map).length \u003c 2) return;\n\n  var root = document.documentElement;\n  root.classList.add('sb-pick-first');\n\n  \/\/ Compare images by file name so width\/version params don't matter\n  function fileKey(url) {\n    if (!url) return '';\n    return String(url).split('?')[0].split('\/').pop().toLowerCase();\n  }\n\n  var fileToVariant = {};\n  Object.keys(map).forEach(function (id) {\n    fileToVariant[fileKey(map[id])] = id;\n  });\n\n  function thumbs() {\n    return gallery.querySelectorAll('.custom-product-gallery__thumb');\n  }\n\n  \/* ---------- Work out which variants are out of stock ---------- *\/\n  function normalise(text) {\n    return String(text || '')\n      .toLowerCase()\n      .replace(\/[\\u2013\\u2014\\u2012\\u2212-]\/g, '-') \/\/ all dashes the same\n      .replace(\/\\s+\/g, ' ')\n      .trim();\n  }\n\n  function words(text) {\n    return normalise(text).split(\/[^a-z0-9.]+\/).filter(Boolean);\n  }\n\n  var oosNames = {};\n  Object.keys(OUT_OF_STOCK).forEach(function (name) {\n    if (OUT_OF_STOCK[name] === true) oosNames[normalise(name)] = true;\n  });\n\n  var soldOutVariants = {}; \/\/ variantId -\u003e true\n  var soldOutFiles = {};    \/\/ image file -\u003e true\n\n  function markSoldOut(id) {\n    soldOutVariants[id] = true;\n    if (map[id]) soldOutFiles[fileKey(map[id])] = true;\n  }\n\n  \/* Every variant with every name it could go by.\n     Names come from three places, so it doesn't matter how the pills\n     are labelled: the pills themselves, Shopify's page data and\n     Shopify's product feed (\/products\/handle.js). *\/\n  var variants = {}; \/\/ id -\u003e [names]\n\n  function addName(id, name) {\n    id = String(id || '');\n    name = normalise(name);\n    if (!id || !name) return;\n    variants[id] = variants[id] || [];\n    if (variants[id].indexOf(name) === -1) variants[id].push(name);\n  }\n\n  \/\/ 1. The pills\n  document.querySelectorAll('.variant-picker__form input[data-variant-id]').forEach(function (input) {\n    var id = input.dataset.variantId;\n    addName(id, input.value);\n    var label = input.closest('label') ||\n      (input.id \u0026\u0026 document.querySelector('label[for=\"' + input.id + '\"]'));\n    if (label) {\n      \/\/ Visible text only (themes often hide \"sold out\" text inside the pill)\n      var clone = label.cloneNode(true);\n      clone.querySelectorAll('.visually-hidden, .sr-only, [hidden], input').forEach(function (n) { n.remove(); });\n      addName(id, clone.textContent);\n    }\n  });\n\n  \/\/ 2. Shopify's page data\n  try {\n    var meta = window.ShopifyAnalytics \u0026\u0026 ShopifyAnalytics.meta \u0026\u0026 ShopifyAnalytics.meta.product;\n    (meta \u0026\u0026 meta.variants || []).forEach(function (v) {\n      addName(v.id, v.public_title);\n      addName(v.id, v.name);\n    });\n  } catch (e) {}\n\n  \/\/ Also make sure every variant in the image map is listed\n  Object.keys(map).forEach(function (id) { variants[id] = variants[id] || []; });\n\n  \/* How well a variant name matches a name in the list (0 = no match).\n     Counts shared words; sizes like \"100ml\" must agree. *\/\n  function sizes(ws) {\n    return ws.filter(function (w) { return \/\\d\/.test(w); });\n  }\n\n  \/\/ Words that appear in fewer of your listed names count for more,\n  \/\/ so \"shock\" decides between CK One and CK Shock better than \"ck\" does\n  function weightOf(w) {\n    var n = Object.keys(oosNames).filter(function (k) { return words(k).indexOf(w) !== -1; }).length;\n    return n ? 1 \/ n : 1;\n  }\n\n  function score(wanted, name) {\n    if (wanted === name) return 100000;\n    var a = words(wanted), b = words(name);\n    if (!a.length || !b.length) return 0;\n\n    \/\/ Sizes \/ numbers must not clash\n    var sa = sizes(a), sb = sizes(b);\n    if (sa.length \u0026\u0026 sb.length \u0026\u0026 !sa.some(function (w) { return sb.indexOf(w) !== -1; })) return 0;\n\n    var sharedWords = b.filter(function (w) { return a.indexOf(w) !== -1; });\n    var ratio = sharedWords.length \/ Math.min(a.length, b.length);\n    if (ratio \u003c 0.6) return 0;\n    var weighted = sharedWords.reduce(function (t, w) { return t + weightOf(w); }, 0);\n    return ratio * 100 + weighted;\n  }\n\n  function matchAll() {\n    soldOutVariants = {};\n    soldOutFiles = {};\n\n    \/\/ Score every (listed name, variant) pair, then hand out the best\n    \/\/ matches first so each variant is used once\n    var pairs = [];\n    Object.keys(oosNames).forEach(function (wanted) {\n      if (variants[wanted]) { pairs.push({ w: wanted, id: wanted, s: 1e9 }); return; }\n      Object.keys(variants).forEach(function (id) {\n        var best = 0;\n        variants[id].forEach(function (n) { best = Math.max(best, score(wanted, n)); });\n        if (best \u003e 0) pairs.push({ w: wanted, id: id, s: best });\n      });\n    });\n    pairs.sort(function (x, y) { return y.s - x.s; });\n\n    var doneNames = {}, doneIds = {};\n    pairs.forEach(function (pr) {\n      if (doneNames[pr.w] || doneIds[pr.id]) return;\n      doneNames[pr.w] = doneIds[pr.id] = true;\n      markSoldOut(pr.id);\n    });\n    var unmatched = Object.keys(oosNames).filter(function (w) { return !doneNames[w]; });\n\n    \/\/ Help for checking names: open the browser console (F12) to see this\n    console.log('[Out of stock] variants on this page:\\n' + Object.keys(variants).map(function (id) {\n      return '  ' + id + ' = ' + (variants[id].join(' | ') || '(no name found)') +\n        (soldOutVariants[id] ? '   \u003c-- OUT OF STOCK' : '');\n    }).join('\\n'));\n    if (unmatched.length) console.warn('[Out of stock] no variant found for:', unmatched);\n  }\n\n  function makeBanner() {\n    var b = document.createElement('span');\n    b.className = 'sb-oos-banner';\n    b.textContent = OUT_OF_STOCK_TEXT;\n    return b;\n  }\n\n  \/\/ Angle and length so the band runs exactly corner to corner\n  function fitBanner(el) {\n    var b = el.querySelector(':scope \u003e .sb-oos-banner');\n    if (!b) return;\n    var w = el.clientWidth, h = el.clientHeight;\n    if (!w || !h) return;\n    var angle = -Math.atan2(h, w) * 180 \/ Math.PI;\n    var len = Math.sqrt(w * w + h * h) + 40; \/\/ a little extra so the ends go past the corners\n    b.style.setProperty('--sb-oos-angle', angle + 'deg');\n    b.style.setProperty('--sb-oos-len', len + 'px');\n  }\n\n  var resizeObs = window.ResizeObserver\n    ? new ResizeObserver(function (entries) { entries.forEach(function (e) { fitBanner(e.target); }); })\n    : null;\n\n  function setBanner(el, soldOut) {\n    el.classList.toggle('is-sold-out', soldOut);\n    var existing = el.querySelector(':scope \u003e .sb-oos-banner');\n    if (soldOut \u0026\u0026 !existing) el.appendChild(makeBanner());\n    if (!soldOut \u0026\u0026 existing) existing.remove();\n    if (soldOut) {\n      fitBanner(el);\n      if (resizeObs) resizeObs.observe(el);\n      \/\/ Re-fit once the image has loaded and has its real size\n      el.querySelectorAll('img').forEach(function (img) {\n        if (!img.complete) img.addEventListener('load', function () { fitBanner(el); }, { once: true });\n      });\n    } else if (resizeObs) {\n      resizeObs.unobserve(el);\n    }\n  }\n\n  \/\/ A banner can't go inside an \u003cimg\u003e or \u003cpicture\u003e, so use the box around it\n  function bannerBox(el) {\n    while (el \u0026\u0026 (el.tagName === 'IMG' || el.tagName === 'PICTURE')) el = el.parentElement;\n    if (el \u0026\u0026 getComputedStyle(el).position === 'static') el.style.position = 'relative';\n    return el;\n  }\n\n  \/\/ If the thumbnail itself is an \u003cimg\u003e, give it its own little wrapper\n  \/\/ (its parent usually holds ALL the thumbs, so we can't use that)\n  function thumbBox(t) {\n    if (t.tagName !== 'IMG' \u0026\u0026 t.tagName !== 'PICTURE') return bannerBox(t);\n    if (t.parentElement \u0026\u0026 t.parentElement.classList.contains('sb-oos-wrap')) return t.parentElement;\n    var wrap = document.createElement('span');\n    wrap.className = 'sb-oos-wrap';\n    t.parentNode.insertBefore(wrap, t);\n    wrap.appendChild(t);\n    return wrap;\n  }\n\n  function applyThumbBanners() {\n    thumbs().forEach(function (t) {\n      var soldOut = !!soldOutFiles[fileKey(t.dataset.fullImage)];\n      var box = (soldOut || t.querySelector('.sb-oos-banner') ||\n        (t.parentElement \u0026\u0026 t.parentElement.classList.contains('sb-oos-wrap'))) ? thumbBox(t) : null;\n      if (box) setBanner(box, soldOut);\n    });\n  }\n\n  function refresh() {\n    matchAll();\n    applyThumbBanners();\n    updateMainBanner();\n  }\n\n  \/\/ Main image: follows whatever image is currently shown\n  var mainImg = document.getElementById('CustomMainProductImage');\n  var mainWrap = mainImg \u0026\u0026 bannerBox(mainImg.parentElement);\n  if (mainWrap) mainWrap.classList.add('sb-main-image-wrap');\n\n  function updateMainBanner() {\n    if (!mainImg || !mainWrap) return;\n    setBanner(mainWrap, !!soldOutFiles[fileKey(mainImg.getAttribute('src'))]);\n  }\n\n  if (mainImg) {\n    new MutationObserver(updateMainBanner)\n      .observe(mainImg, { attributes: true, attributeFilter: ['src'] });\n  }\n\n  refresh();\n\n  \/\/ 3. Shopify's product feed: the most reliable variant names\n  try {\n    var jsUrl = location.pathname.replace(\/\\\/$\/, '') + '.js';\n    if (\/\\\/products\\\/\/.test(jsUrl)) {\n      fetch(jsUrl, { headers: { Accept: 'application\/json' } })\n        .then(function (r) { return r.ok ? r.json() : null; })\n        .then(function (product) {\n          if (!product || !product.variants) return;\n          product.variants.forEach(function (v) {\n            addName(v.id, v.title);\n            addName(v.id, v.name);\n            addName(v.id, v.public_title);\n            (v.options || []).forEach(function (o) { addName(v.id, o); });\n          });\n          refresh();\n        })\n        .catch(function () {});\n    }\n  } catch (e) {}\n\n  \/\/ If the theme redraws the thumbnails, put the banners back\n  var redrawTimer = null;\n  new MutationObserver(function (muts) {\n    var ours = muts.every(function (m) {\n      return [].slice.call(m.addedNodes).concat([].slice.call(m.removedNodes)).every(function (n) {\n        return n.nodeType === 1 \u0026\u0026 (n.classList.contains('sb-oos-banner') || n.classList.contains('sb-oos-wrap') ||\n          n.classList.contains('custom-product-gallery__thumb'));\n      });\n    });\n    if (ours) return;\n    clearTimeout(redrawTimer);\n    redrawTimer = setTimeout(applyThumbBanners, 50);\n  }).observe(gallery, { childList: true, subtree: true });\n\n  \/* ---------- Thumbnails \/ main image ---------- *\/\n  function setActive(thumb) {\n    thumbs().forEach(function (t) {\n      t.classList.toggle('is-active', t === thumb);\n    });\n  }\n\n  function setActiveByVariant(variantId) {\n    var file = fileKey(map[variantId]);\n    thumbs().forEach(function (t) {\n      t.classList.toggle('is-active', fileKey(t.dataset.fullImage) === file);\n    });\n  }\n\n  function showMain(url) {\n    if (!mainImg || !url) return;\n    mainImg.src = url;\n    mainImg.removeAttribute('srcset');\n    mainImg.removeAttribute('sizes');\n    mainImg.loading = 'eager';\n    updateMainBanner();\n  }\n\n  function revealPrice() { root.classList.remove('sb-pick-first'); }\n  function hidePrice() { root.classList.add('sb-pick-first'); }\n\n  \/* ---------- Blur until the theme has updated the price ---------- *\/\n  var doneTimer = null;\n  var fallbackTimer = null;\n\n  function stopLoading() {\n    clearTimeout(doneTimer);\n    clearTimeout(fallbackTimer);\n    root.classList.remove('sb-price-loading');\n  }\n\n  function startLoading() {\n    root.classList.add('sb-price-loading');\n    clearTimeout(fallbackTimer);\n    \/\/ Safety net in case the update is never detected\n    fallbackTimer = setTimeout(stopLoading, 2500);\n  }\n\n  function inPrice(node) {\n    if (!node) return false;\n    var el = node.nodeType === 1 ? node : node.parentElement;\n    if (!el) return false;\n    return !!(el.closest('product-price') || (el.querySelector \u0026\u0026 el.querySelector('product-price')));\n  }\n\n  var details = document.querySelector('.product-details') || document.body;\n  new MutationObserver(function (mutations) {\n    if (!root.classList.contains('sb-price-loading')) return;\n\n    var touched = mutations.some(function (m) {\n      if (inPrice(m.target) \u0026\u0026 m.target.nodeType !== 1) return true;\n      if (m.target.nodeType === 1 \u0026\u0026 m.target.closest('product-price')) return true;\n      for (var i = 0; i \u003c m.addedNodes.length; i++) {\n        if (inPrice(m.addedNodes[i])) return true;\n      }\n      return false;\n    });\n\n    if (touched) {\n      \/\/ Wait for the burst of updates to settle, then unblur\n      clearTimeout(doneTimer);\n      doneTimer = setTimeout(stopLoading, 80);\n    }\n  }).observe(details, { childList: true, subtree: true, characterData: true });\n\n  \/* ---------- Variant selection ---------- *\/\n  function selectVariant(id) {\n    var input = document.querySelector('.variant-picker__form input[data-variant-id=\"' + id + '\"]');\n    if (!input) return false;\n    \/\/ Out-of-stock bottles must stay selectable\n    if (input.disabled) input.disabled = false;\n    \/\/ A real click lets the theme update price, URL and form as normal\n    if (!input.checked) input.click();\n    return true;\n  }\n\n  \/\/ On load: first gallery image, price hidden\n  var first = thumbs()[0];\n  if (first) {\n    showMain(first.dataset.fullImage);\n    setActive(first);\n  }\n\n  \/\/ Thumbnail click: pick the matching variant and show the price\n  gallery.addEventListener('click', function (e) {\n    var thumb = e.target.closest('.custom-product-gallery__thumb');\n    if (!thumb) return;\n\n    setActive(thumb);\n    showMain(thumb.dataset.fullImage);\n\n    var id = fileToVariant[fileKey(thumb.dataset.fullImage)];\n    if (id \u0026\u0026 selectVariant(id)) {\n      revealPrice();\n    } else {\n      \/\/ Image with no variant (e.g. the group shot) hides the price again\n      stopLoading();\n      hidePrice();\n    }\n  });\n\n  function getInput(e) {\n    if (!e.target.closest) return null;\n    var label = e.target.closest('.variant-picker__form label');\n    return label\n      ? label.querySelector('input[data-variant-id]')\n      : e.target.closest('.variant-picker__form input[data-variant-id]');\n  }\n\n  \/\/ Picking a variant pill directly also shows the price and syncs the thumb\n  document.addEventListener('click', function (e) {\n    var input = getInput(e);\n    if (!input) return;\n    revealPrice();\n    setActiveByVariant(input.dataset.variantId);\n    showMain(map[input.dataset.variantId]);\n  });\n\n  \/\/ 'change' only fires when the variant actually changes, so blur here\n  document.addEventListener('change', function (e) {\n    var input = getInput(e);\n    if (!input) return;\n    startLoading();\n    revealPrice();\n    setActiveByVariant(input.dataset.variantId);\n  }, true);\n})();\n\u003c\/script\u003e","brand":"Direct Deals","offers":[{"title":"Nautica Voyage Eau de Toilette – 100ml","offer_id":45985865498803,"sku":"SB000507","price":329.0,"currency_code":"ZAR","in_stock":false},{"title":"Calvin Klein CK One Shock for Him Eau de Toilette – 100ml","offer_id":45985865531571,"sku":"SB000488","price":399.0,"currency_code":"ZAR","in_stock":true},{"title":"Calvin Klein CK One Eau de Toilette – 50ml","offer_id":45985865564339,"sku":"SB000484","price":349.0,"currency_code":"ZAR","in_stock":true},{"title":"Giorgio Armani Acqua di Giò Refillable Eau de Parfum – 40ml","offer_id":45985865597107,"sku":"SB000502","price":999.0,"currency_code":"ZAR","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0749\/5299\/9091\/files\/ChatGPTImageSep9_2026_02_37_35PM.png?v=1788957492","url":"https:\/\/www.specialbuyssa.co.za\/products\/designer-fragrances-choose-your-perfume","provider":"Special Buys","version":"1.0","type":"link"}