{
  "version": 3,
  "sources": ["/cdn/shop/t/26/assets/theme.js?nocache=20914"],
  "sourcesContent": ["/* ============================================================\n   JUCE 3.0 \u00B7 theme.js - one file, vanilla, event-delegated.\n   One controller per system; each no-ops without its DOM hook,\n   self-gates on reduced motion / pointer coarseness. data-*\n   attributes are the ONLY JS hooks; classes are style/state only.\n   Cart truth is server-rendered via the Section Rendering API.\n   ============================================================ */\n\nconst reduceMotion = matchMedia('(prefers-reduced-motion: reduce)');\nconst finePointer = matchMedia('(pointer: fine)');\nconst cfg = window.theme || {};\nconst routes = cfg.routes ||\n  { cartAdd: '/cart/add', cartChange: '/cart/change', cartUpdate: '/cart/update', cart: '/cart' };\nif (!routes.cartUpdate) routes.cartUpdate = '/cart/update';\n\n/* Every 50ml ships with its free 3ml tester. The property rides on the\n   freebie line so it reads as a gift, merges cleanly on repeat adds, and\n   stays hidden from the order (leading underscore). */\nconst SAMPLE_PROPS = { _free_sample: 'Free 3ml tester - try before you commit' };\n\n/* ---- Layer sets: what makes a group a group in the cart ----\n   Scents added TOGETHER - the layering pair, the minis pair, a story finale,\n   the discovery trio - carry this property, and snippets/cart-lines groups the\n   rows that share it into one titled block. Written at the moment of the add\n   rather than inferred in the drawer, so the grouping survives a reload, the\n   Section Rendering API and the order itself.\n\n   The value IS the printed label, deliberately: two sets bought at different\n   times merge into one block if they are adjacent and named the same, which is\n   both true and what a shopper would expect. A unique id per add would prevent\n   that, at the cost of splitting a repeat add of the SAME set into two lines.\n   Hidden from the order by the leading underscore, like the tester. */\nfunction layerSetProps(label) {\n  return { _layer_set: label || 'Layer set' };\n}\n\n/* ---- WHAT THE BLOCK OVER A SET IS CALLED (owner, 2026-08-25: \"in the cart it\n   must show THE DUET, or the other one, not WORKS WELL TOGETHER, when it was\n   added that way\") ----\n\n   There are two completely different objects wearing one heading today, and\n   that is the whole complaint:\n\n     \u00B7 A set the SHOPPER chose. Two bottles added in one action, carrying\n       `_layer_set`, whose value IS the name of the thing they chose - \"The\n       duet\" off the layering room's buy bar. The block over those bottles was\n       telling them \"works well together\", which is a weaker claim than the one\n       they actually made, and it is not what they did.\n     \u00B7 A pair the MODEL noticed. Bottles collected one at a time over three\n       visits that the chemistry read bonds after the fact (makeAutoGroup).\n       \"Works well together\" is exactly right for that one - it is an\n       observation, and it should keep saying so.\n\n   So the tag decides, and the rule is small enough to mirror in Liquid\n   (snippets/cart-lines.liquid does, for the no-JS render):\n\n     no tag              -> \"Works well together\"   the model's observation\n     the tag \"Layer set\" -> \"Chosen together\"       the INTERNAL placeholder\n     any other tag       -> the tag itself          \"The duet\"\n\n   The middle line is the one that matters. `Layer set` is the default in\n   layerSetProps above and is what the PDP band and the finder write; printing\n   it raw is the 2026-08-22 bug that put the words LAYER SET in a shopper's bag\n   and got the heading hard-coded in the first place. Everything else is a name\n   a human wrote for a shopper to read, so it is printed as it stands - a\n   future set can name itself without touching this. */\nfunction groupKind(tag) {\n  const t = String(tag || '').trim();\n  if (!t) return 'Works well together';\n  if (t.toLowerCase() === 'layer set') return 'Chosen together';\n  return t;\n}\n\n/* Tag a set's BOTTLE lines (never the free testers - a tester belongs to its\n   bottle, and the fold in cart-lines already puts it there). */\nfunction tagSet(items, label) {\n  const props = layerSetProps(label);\n  items.forEach((item) => {\n    if (item.properties && item.properties._free_sample) return;\n    item.properties = Object.assign({}, item.properties, props);\n  });\n  return items;\n}\n\n/* ============================================================\n   The resize bus - ONE listener, one frame, one settle.\n\n   This file used to bind eleven separate window resize handlers. Six kept\n   their OWN debounce timer (120ms, 150ms, three at 200ms, one ad hoc) and\n   four ran raw on every event. A window drag fires resize continuously, so\n   the result was four handlers measuring and writing the DOM on every single\n   event - each read forcing the layout the previous write had invalidated -\n   followed, when the drag stopped, by six unsynchronised timers each doing\n   another measure-and-write pass at a different moment. That is why resizing\n   juddered, and why it kept juddering after you let go.\n\n   Everything now goes through one listener with two channels:\n\n     onResize(fn)         cheap, positional work. Coalesced into ONE\n                          requestAnimationFrame, so N subscribers cost one\n                          layout pass per frame instead of N.\n     onResizeSettled(fn)  expensive re-measure work (rebuilding a lane,\n                          re-placing a thumb, repainting a carousel). ONE\n                          shared timer for all of them, so they land together\n                          in a single pass instead of a staggered cascade.\n\n   Both hand the subscriber (vw, vh). Pass { widthOnly: true } for work that\n   only responds to width: mobile browsers fire resize on every address-bar\n   show/hide, which changes height only, and skipping those is free.\n   See steering/12-performance-playbook.md \u00A76.\n   ============================================================ */\nconst resizeFrameSubs = new Set();\nconst resizeSettledSubs = new Set();\nlet resizeRaf = 0;\nlet resizeSettleTimer = 0;\nlet lastVW = window.innerWidth;\nlet lastVH = window.innerHeight;\nlet settledVW = window.innerWidth;     /* width at the last SETTLED flush */\nconst RESIZE_SETTLE_MS = 160;\n\nfunction runResizeSubs(subs, vw, vh, widthChanged) {\n  for (const entry of subs) {\n    if (entry.widthOnly && !widthChanged) continue;\n    try { entry.fn(vw, vh); }\n    catch (err) { console.warn('[juce] resize subscriber failed', err); }\n  }\n}\n\nfunction flushResizeFrame() {\n  resizeRaf = 0;\n  const vw = window.innerWidth, vh = window.innerHeight;\n  const widthChanged = vw !== lastVW;\n  lastVW = vw; lastVH = vh;\n  runResizeSubs(resizeFrameSubs, vw, vh, widthChanged);\n}\n\nfunction flushResizeSettled() {\n  resizeSettleTimer = 0;\n  const vw = window.innerWidth, vh = window.innerHeight;\n  /* widthOnly IS HONOURED HERE TOO. It used to be passed as `true` on this\n     path, which made the option a no-op for the expensive work it was\n     written for: on a phone every address-bar show/hide (every change of\n     scroll direction) was a full settled resize, so the looped lanes tore\n     their clones out and rebuilt them each time - mid-swipe when the swipe\n     carried a little vertical. Compared against the width at the LAST\n     settled flush, not the last frame, so a width that moved and came back\n     inside the settle window counts as unmoved. */\n  const widthChanged = vw !== settledVW;\n  settledVW = vw;\n  /* Subscribers run BEFORE .is-resizing comes off, deliberately. They are the\n     re-measure pass, and while the class is still on, transitions are still\n     suppressed - so every capsule and thumb lands at its new position without\n     animating there from its old one. Restoring the skin afterwards is one\n     style recalc on an already-settled layout. */\n  runResizeSubs(resizeSettledSubs, vw, vh, widthChanged);\n  clearResizingClass();\n}\n\nfunction onResize(fn, opts) {\n  resizeFrameSubs.add({ fn, widthOnly: !!(opts && opts.widthOnly) });\n}\nfunction onResizeSettled(fn, opts) {\n  resizeSettledSubs.add({ fn, widthOnly: !!(opts && opts.widthOnly) });\n}\n\n/* ---- html.is-resizing: the drag is a different budget to the rest ----\n   (owner, 2026-08-14: \"why is it when I resize that it feels like everything is\n   lagging is it because of alot of calculation on the sizes\".)\n\n   Partly, and that half is fixed in placeCardThumbs. But the JS only runs when\n   the drag STOPS, and the judder is there while it is moving - so the rest of\n   it is paint, not script. A catalogue at >=1024px lays out ~42 posters, and\n   each one carries a .dk-src glass bar that inherits .dk-dock's\n   `backdrop-filter: blur(12px) saturate(180%)` over a mix-blend-mode image.\n   Every frame of a drag re-rasterises every one of those that is on screen,\n   and a resize invalidates the content-visibility boundaries too, so the set\n   being re-rasterised keeps changing.\n\n   Note the comment on .grid-dock still says the card glass bars are gone. They\n   came back on desktop on 2026-08-13, and the 42 live blurs came back with\n   them - that regression is what this class pays for.\n\n   So the drag gets a cheaper skin: blurs off, transitions off, aura paused.\n   None of it is visible while the window is in motion, all of it returns on the\n   same settle timer that already exists. Nothing is disabled at rest. */\nlet resizingClass = false;\nfunction clearResizingClass() {\n  if (!resizingClass) return;\n  resizingClass = false;\n  document.documentElement.classList.remove('is-resizing');\n}\n\nwindow.addEventListener('resize', () => {\n  /* A PHONE'S ADDRESS BAR IS NOT A WINDOW BEING DRAGGED. iOS Safari and\n     Chrome for Android fire `resize` every time the toolbar shows or hides,\n     and the width never moves. The cheaper skin is for the judder of a drag,\n     so a height-only change on a touch device leaves it off - with it on,\n     every glass bar and transition on the page switched off and back on\n     160ms after each page scroll - a flicker for nothing. A fine pointer\n     keeps the old behaviour, height-only drags included. */\n  if (!resizingClass && (window.innerWidth !== settledVW || finePointer.matches)) {\n    resizingClass = true;\n    document.documentElement.classList.add('is-resizing');\n  }\n  if (!resizeRaf) resizeRaf = requestAnimationFrame(flushResizeFrame);\n  clearTimeout(resizeSettleTimer);\n  resizeSettleTimer = setTimeout(flushResizeSettled, RESIZE_SETTLE_MS);\n}, { passive: true });\n\n/* ---- Glass kill switch: low-memory / Save-Data devices get T0 ---- */\nfunction initGlassSwitch() {\n  const mem = navigator.deviceMemory;\n  const saveData = navigator.connection && navigator.connection.saveData;\n  if ((mem && mem < 4) || saveData) document.documentElement.dataset.glass = 'off';\n}\n\n/* ---- Pointer shine: --mx / --my / --glint-angle / --glint-o ---- */\nfunction initShine() {\n  if (!finePointer.matches || reduceMotion.matches) return;\n  let raf = 0;\n  const queue = new Map();\n  document.addEventListener('pointermove', (e) => {\n    const el = e.target.closest('[data-shine]');\n    if (!el) return;\n    queue.set(el, e);\n    if (raf) return;\n    raf = requestAnimationFrame(() => {\n      raf = 0;\n      for (const [node, ev] of queue) {\n        const r = node.getBoundingClientRect();\n        const mx = ((ev.clientX - r.left) / r.width) * 100;\n        const my = ((ev.clientY - r.top) / r.height) * 100;\n        const angle = (Math.atan2(ev.clientY - (r.top + r.height / 2),\n                                  ev.clientX - (r.left + r.width / 2)) * 180) / Math.PI + 90;\n        node.style.setProperty('--mx', mx.toFixed(1) + '%');\n        node.style.setProperty('--my', my.toFixed(1) + '%');\n        node.style.setProperty('--glint-angle', angle.toFixed(1) + 'deg');\n      }\n      queue.clear();\n    });\n  }, { passive: true });\n  document.addEventListener('pointerover', (e) => {\n    const el = e.target.closest('[data-shine]');\n    if (el) el.style.setProperty('--glint-o', '1');\n  });\n  document.addEventListener('pointerout', (e) => {\n    const el = e.target.closest('[data-shine]');\n    if (el && !el.contains(e.relatedTarget)) {\n      el.style.setProperty('--glint-o', '0');\n      el.style.setProperty('--mx', '50%');\n      el.style.setProperty('--my', '0%');\n    }\n  });\n}\n\n/* ---- Diffusion reveal - scroll-driven CSS is primary (html.sda),\n   the IntersectionObserver class flavour is the fallback (05 \u00A73) ---- */\nfunction initReveal() {\n  if (!reduceMotion.matches &&\n      window.CSS && CSS.supports('animation-timeline: view()')) {\n    document.documentElement.classList.add('sda');\n    return;                       /* CSS owns every [data-reveal] from here */\n  }\n  /* THE MARKER THE HEAD'S FAILSAFE READS (2026-08-30). layout/theme.liquid\n     makes the same decision above, before first paint, and arms a 4s timeout\n     that adds `sda` if neither class ever appears - the case where this module\n     did not run at all and every [data-reveal] would otherwise stay at\n     opacity 0 forever. Stamping `rvl` here is how the observer branch says \"I\n     am in charge, stand down\": it must be set BEFORE the early return below,\n     because a page with no [data-reveal] elements has still booted. */\n  document.documentElement.classList.add('rvl');\n  const targets = document.querySelectorAll('[data-reveal]');\n  if (!targets.length) return;\n  if (reduceMotion.matches || !('IntersectionObserver' in window)) {\n    targets.forEach((el) => el.classList.add('reveal'));\n    return;\n  }\n  let ioFired = false;\n  const io = new IntersectionObserver((entries) => {\n    ioFired = true;\n    entries.forEach((en) => {\n      if (en.isIntersecting) { en.target.classList.add('reveal'); io.unobserve(en.target); }\n    });\n  }, { threshold: 0.1, rootMargin: '0px 0px -5% 0px' });\n  targets.forEach((el) => io.observe(el));\n  /* Safety net: throttled webviews/prerender contexts can starve IO -\n     content must never stay invisible. */\n  setTimeout(() => {\n    if (!ioFired) { targets.forEach((el) => el.classList.add('reveal')); io.disconnect(); }\n  }, 1200);\n}\n\n/* ---- Card size switch: variant + price swap + opt-pop + the capsule ----\n   The bubble rides the card's bar as well as the island's (owner, 2026-08-13:\n   \"we want the bubble selector on both mobile and desktop\"). The island is\n   hidden >=1024px, so without this the capsule simply did not exist on a\n   desktop, where .dk-src is the only bar that sells.\n\n   placeSizeThumb is the island's own measuring function, reused verbatim - it\n   was written against a .sizes group rather than against the dock, so it needed\n   no change. See its own note for why it measures instead of hard-coding.\n\n   Placement happens three times, and all three are needed: on select (the\n   capsule travels), on first paint (it has to START behind the open size, not\n   fly in from the left - hence instant), and again after fonts land and on\n   resize, because both change the label widths the capsule is measured from. */\nlet cardThumbIO;\nfunction placeCardThumbs(instant) {\n  /* A group that is not on screen measures zero, and placeSizeThumb rightly\n     refuses to write a zero-width capsule - so a bulk pass alone leaves every\n     card that was hidden at the time without a bubble, permanently. Three\n     ordinary states do exactly that: .dk-src is display:none below 1024px, the\n     collection grid hides filtered-out cards, and cards below the fold are not\n     laid out yet on a long catalogue.\n\n     So the bulk pass is paired with an observer that places each group the\n     first time it actually becomes visible. Re-observing an element already\n     being observed is a no-op, which is what makes this safe to call again from\n     resize and from fonts.ready. */\n  if (!cardThumbIO && 'IntersectionObserver' in window) {\n    cardThumbIO = new IntersectionObserver((entries) => {\n      entries.forEach((en) => { if (en.isIntersecting) placeSizeThumb(en.target, true); });\n    }, { rootMargin: '200px' });\n  }\n  /* ---- ONE LAYOUT PASS FOR THE WHOLE GRID, NOT ONE PER CARD ----\n     (owner, 2026-08-14: \"why is it when I resize that it feels like everything\n     is lagging\".)\n\n     This used to call placeSizeThumb in a loop, and that function interleaves\n     writes with reads by design: drop .thumb-on, read offsetLeft, write\n     --thumb-x, read offsetWidth, force a reflow, add .thumb-on. Correct for ONE\n     group reacting to a tap; over a whole collection it is one forced\n     synchronous layout per card instead of one for the grid.\n\n     Split into phases instead: all the class removals, then all the reads, then\n     all the writes, then ONE reflow, then all the class adds. Same pixels, one\n     layout. placeSizeThumb is left exactly as it is for the single-group case -\n     this is a bulk path, not a replacement.\n\n     MEASURED, because the first version of this comment guessed and guessed\n     high: 40 groups, old 1.2ms vs new 0.8ms median. Real, worth keeping, and\n     NOT on its own the judder the owner is describing - .thumb-on only touches\n     opacity and transition, so the repeated reads were never invalidating\n     layout the way an interleaved width write would. Do not cite this as the\n     fix for resize lag; see html.is-resizing in components.css for the other\n     half and for what is still unproven. */\n  const groups = [...document.querySelectorAll('.dk-src .sizes')];\n  if (cardThumbIO) groups.forEach((g) => cardThumbIO.observe(g));\n  if (!groups.length) return;\n\n  const work = [];\n  if (instant) groups.forEach((g) => g.classList.remove('thumb-on'));\n  groups.forEach((group) => {\n    const on = group.querySelector('.size-opt.is-active');\n    /* Same two bail-outs placeSizeThumb makes, for the same reasons: no active\n       option, or a group with no box yet (display:none below 1024px, filtered\n       out, or still below the fold). A zero here would write a collapsed\n       capsule with its transition armed. */\n    if (!on || !on.offsetWidth) { work.push({ group, ok: false }); return; }\n    work.push({ group, ok: true, x: on.offsetLeft, w: on.offsetWidth });\n  });\n  work.forEach((item) => {\n    if (!item.ok) { item.group.classList.remove('thumb-on'); return; }\n    item.group.style.setProperty('--thumb-x', item.x + 'px');\n    item.group.style.setProperty('--thumb-w', item.w + 'px');\n  });\n  /* The flush is what makes `instant` instant: the new position has to be the\n     browser's committed state BEFORE .thumb-on arms the transition, or every\n     capsule on the page animates from wherever it used to be. One flush for the\n     whole grid, where there used to be one per card. */\n  if (instant) void document.body.offsetWidth;\n  work.forEach((item) => { if (item.ok) item.group.classList.add('thumb-on'); });\n}\n\n/* ---- THE PRICE SLASH - one place that knows how a price is painted ----\n   The Bundle variant carries a real compare_at_price (the 50ml + 10ml pair\n   total), so selecting it has to strike the old number through and quote the\n   new one. Four surfaces show a price - the card, the island, the card-add\n   sheet and the PDP (twice) - and every one of them now paints through here,\n   which is what keeps a struck-through R 1 025 from being left behind when the\n   shopper switches back down to the 50ml.\n\n   The struck price is a SIBLING of the live one, never a child: three of those\n   surfaces copy a price by reading .textContent, and nesting would have them\n   quote both numbers run together. `hidden` rather than removal so the element\n   is there to fill on the next switch. */\nfunction paintPrice(nowEl, label, compare) {\n  if (!nowEl) return;\n  if (label) nowEl.textContent = label;\n  const was = nowEl.parentElement && nowEl.parentElement.querySelector('[data-price-was]');\n  if (!was) return;\n  if (compare) { was.textContent = compare; was.hidden = false; }\n  else { was.textContent = ''; was.hidden = true; }\n}\n\n/* The struck price belonging to a live price element, for the surfaces that\n   mirror another surface's markup rather than reading variant data. */\nfunction priceWasOf(nowEl) {\n  return nowEl && nowEl.parentElement\n    ? nowEl.parentElement.querySelector('[data-price-was]')\n    : null;\n}\n\n/* ---- The size picked on a card travels with the click ----\n\n   A card is a little product page: the shopper chooses 50ml / 10ml / Bundle on\n   it, and until now the link under the poster went to a bare /products/handle,\n   so the PDP re-ran its own ladder and opened on the 50ml. Picking the Bundle\n   and landing on the 50ml is the whole complaint (owner, 2026-08-24).\n\n   The id is not re-derived here. [data-variant-input] is the hidden field the\n   card already posts to the cart, so the link and the Add button can never\n   disagree about which variant the card is showing - including the opening\n   one, which is why this also runs once at load rather than only on a click.\n   That opening case is the one that actually bites on the minis grid, where\n   the cards are rendered with prefer:'10ml'.\n\n   snippets/size-options.liquid reads the ?variant back on the other side. */\nfunction syncCardLink(card) {\n  if (!card) return;\n  const link = card.querySelector('a.shot-wrap[href], a[href*=\"/products/\"]');\n  const input = card.querySelector('[data-variant-input]');\n  if (!link || !input || !input.value) return;\n  const href = link.getAttribute('href');\n  if (!href || href.charAt(0) === '#') return;\n  /* through the URL parser so an existing query (?ref=, a utm) survives and\n     a second size click replaces the variant rather than appending another */\n  const u = new URL(href, location.origin);\n  u.searchParams.set('variant', input.value);\n  link.setAttribute('href', u.pathname + u.search + u.hash);\n}\n\nfunction initSizes() {\n  document.addEventListener('click', (e) => {\n    const btn = e.target.closest('[data-size-opt]');\n    if (!btn) return;\n    const card = btn.closest('[data-product-form]');\n    if (!card) return;\n    card.querySelectorAll('[data-size-opt]').forEach((b) => {\n      b.classList.toggle('is-active', b === btn);\n      b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');\n    });\n    const idInput = card.querySelector('[data-variant-input]');\n    if (idInput && btn.dataset.variantId) {\n      idInput.value = btn.dataset.variantId;\n      idInput.dataset.variantLabel = btn.textContent.trim();\n    }\n    paintPrice(card.querySelector('[data-card-price]'),\n               btn.dataset.priceLabel, btn.dataset.compareLabel);\n    syncCardLink(btn.closest('[data-card]'));\n    btn.classList.remove('opt-pop');\n    void btn.offsetWidth;                    /* re-trigger keyframe */\n    btn.classList.add('opt-pop');\n    placeSizeThumb(btn.closest('.sizes'));\n  });\n\n  placeCardThumbs(true);\n  document.querySelectorAll('[data-card]').forEach(syncCardLink);\n  if (document.fonts && document.fonts.ready) {\n    document.fonts.ready.then(() => placeCardThumbs(true));\n  }\n  onResizeSettled(() => placeCardThumbs(true));\n}\n\n/* ---- One measured header height, published as --head-h ----\n   Everything that sticks BELOW the site header offsets by it: the collection\n   toolbar, the desktop filter rail, the PDP view-switch dodge. All four wrote a\n   literal `top: 56px` / `64px`, which is .site-head__bar's MIN-height - correct\n   only while nothing else contributes to the header's box.\n\n   That is not a safe assumption any more. The wordmark is rem, so it rides the\n   fluid root and can push the bar past its minimum on a large screen; any\n   padding added to .site-head, a second row, or the announcement bar ever being\n   made sticky all do the same. When the literal is too small the toolbar tucks\n   under an opaque header (invisible); when it is too large a band of the page\n   shows through above it, which is the visible fault.\n\n   So it is measured, not assumed, and a ResizeObserver keeps it honest. */\nfunction initHeadHeight() {\n  const head = document.querySelector('.site-head');\n  if (!head) return;\n  const root = document.documentElement;\n  const set = () => {\n    const h = Math.round(head.getBoundingClientRect().height);\n    if (h > 0) root.style.setProperty('--head-h', h + 'px');\n  };\n  set();\n  if ('ResizeObserver' in window) new ResizeObserver(set).observe(head);\n  else onResize(set);\n}\n\n/* ---- Quantity stepper on the buy row (PDP) ----\n   A real <input name=\"quantity\">, so it posts with the form and initCart's\n   existing `fd.get('quantity')` picks it up - the island pill and the desktop\n   buy bar submit that same form, so all three paths agree without a second\n   code path anywhere.\n\n   The stepper answers on the TAP, not on the response (05 \u00A72): the number pops\n   the instant it is pressed. Nothing here talks to the cart - this is the\n   quantity you are ABOUT to add, and it is only read at submit. */\nfunction qtyClamp(input) {\n  const min = parseInt(input.min, 10) || 1;\n  const max = parseInt(input.max, 10) || 99;\n  const raw = parseInt(input.value, 10);\n  const next = Number.isNaN(raw) ? min : Math.min(max, Math.max(min, raw));\n  if (String(next) !== input.value) input.value = String(next);\n  /* inert at the floor rather than gone - a control that vanishes shifts the\n     row under the thumb. aria-disabled keeps it focusable (see the CSS note). */\n  const wrap = input.closest('[data-qty]');\n  const down = wrap && wrap.querySelector('[data-qty-step=\"-1\"]');\n  const up = wrap && wrap.querySelector('[data-qty-step=\"1\"]');\n  if (down) down.setAttribute('aria-disabled', next <= min ? 'true' : 'false');\n  if (up) up.setAttribute('aria-disabled', next >= max ? 'true' : 'false');\n  return next;\n}\nfunction initQty() {\n  document.addEventListener('click', (e) => {\n    const step = e.target.closest('[data-qty-step]');\n    if (!step || step.getAttribute('aria-disabled') === 'true') return;\n    const wrap = step.closest('[data-qty]');\n    const input = wrap && wrap.querySelector('[data-qty-input]');\n    if (!input) return;\n    const before = parseInt(input.value, 10) || 1;\n    input.value = String(before + (parseInt(step.dataset.qtyStep, 10) || 0));\n    if (qtyClamp(input) === before) return;              /* hit a bound - no beat */\n    input.classList.remove('opt-pop');\n    void input.offsetWidth;                              /* re-trigger the keyframe */\n    input.classList.add('opt-pop');\n  });\n  /* typed input: a stray \"0\", \"-3\" or \"abc\" must never reach the cart */\n  document.addEventListener('change', (e) => {\n    const input = e.target.closest('[data-qty-input]');\n    if (input) qtyClamp(input);\n  });\n  document.querySelectorAll('[data-qty-input]').forEach(qtyClamp);\n}\n\n/* ============================================================================\n   ONE BAG, TWO SURFACES\n   (owner, 2026-08-26: \"We require https://juce.co.za/cart to work and feel\n   exactly like the slide in cart but just in the middle nicely\".)\n\n   /cart and the drawer render the SAME snippet now (snippets/cart-inner), so\n   every function below - the optimistic totals, the size switch, the collapse,\n   the undo, the suggestion - has to be able to run against either of them. What\n   it must never do is run against BOTH at once: on /cart the layout still ships\n   the drawer over the page, so document.querySelector('[data-drawer-inner]')\n   would answer with whichever came first in the document and half the sheet\n   would be painting the other one's numbers.\n\n   So nothing in the cart code addresses the document any more. It asks for the\n   SURFACE that is on screen - the dialog while it is open, the page otherwise -\n   and queries inside it. Two functions, and every selector that used to start\n   with '[data-drawer-inner] ' goes through them.\n\n   The hooks:\n     [data-drawer]        the dialog                     (drawer only)\n     [data-cart-page]     the page's outer wrapper       (page only)\n     [data-cart-inner]    the swapped subtree            (both)\n     [data-drawer-inner]  the Section Rendering target   (drawer only)\n   ========================================================================= */\nfunction bagSurface() {\n  const drawer = document.querySelector('[data-drawer]');\n  if (drawer && drawer.open) return drawer;\n  return document.querySelector('[data-cart-page]') || drawer;\n}\nfunction bagScope() {\n  const surface = bagSurface();\n  return surface ? surface.querySelector('[data-cart-inner]') : null;\n}\n/* The two ways this file used to write `[data-drawer-inner] <sel>`. */\nfunction bagPick(sel) {\n  const scope = bagScope();\n  return scope ? scope.querySelector(sel) : null;\n}\nfunction bagPickAll(sel) {\n  const scope = bagScope();\n  return scope ? Array.from(scope.querySelectorAll(sel)) : [];\n}\n/* Things that live OUTSIDE the swapped subtree because they have to survive it:\n   the announcer, the failure strip, the undo pill. Both surfaces carry their\n   own set, so they are found on the surface rather than in the document. */\nfunction bagOuter(sel) {\n  const surface = bagSurface();\n  return surface ? surface.querySelector(sel) : null;\n}\n/* Is the bag on screen the PAGE? Three things branch on it and all three are\n   about the document rather than about the cart: there is no dialog to open\n   over /cart, no scroller inside the panel, and no foot to float a pill above. */\nfunction bagIsPage() {\n  const surface = bagSurface();\n  return !!surface && surface.hasAttribute('data-cart-page');\n}\n\n/* ---- Cart: add / change via Section Rendering (server truth) ----\n   Every mutation takes a ticket. Two taps on \"+\" can answer out of order, and\n   a late render carrying the older quantity would stamp itself back over the\n   newer one, so only the newest ticket is allowed to paint. */\nlet cartTicket = 0;\n/* The rooms take tickets too (assets/theme-rooms.js), and a module cannot\n   write another module's binding, so they ask for the next one here. */\nfunction nextCartTicket() { return ++cartTicket; }\n\n/* WHICH SECTIONS A CART MUTATION HAS TO BRING BACK. The drawer is on every\n   page, so it is always asked for. `main-cart` only exists on /cart - asking\n   for it anywhere else would be asking Shopify for a section that is not on\n   the page - so the list is built from what is actually rendered rather than\n   being a constant. (The id is the KEY in templates/cart.json, which is why\n   that key is named after the section rather than the \"main\" it used to be.) */\nfunction cartSections() {\n  return document.querySelector('[data-cart-page]') ? 'cart-drawer,main-cart' : 'cart-drawer';\n}\n\n/* Paint one freshly rendered inner over the one on screen. Both surfaces go\n   through this - the only thing that differs is which section carried the HTML\n   and whether there is a scroller to keep in place. */\nfunction swapCartInner(current, next) {\n  /* everything that has to survive the swap is read off the OLD subtree first */\n  const focusKey = drawerFocusKey(current);\n  const fill = current.querySelector('[data-ship-fill]');\n  const fromScale = fill ? fill.style.transform : '';\n  const wasUnlocked = shipUnlocked(current);\n  /* The sheet scrolls as ONE body, and replaceChildren throws its offset away:\n     a shopper who had scrolled down to the fourth bottle to change its quantity\n     was thrown back to the first one by their own tap. Read before, written\n     after - the new body is a different node, so it cannot be carried across\n     any other way. The page has no scroller of its own and reports 0, which is\n     the right answer there. */\n  const bodyWas = current.querySelector('[data-cart-body]');\n  const scrolled = bodyWas ? bodyWas.scrollTop : 0;\n  /* Replace inner content, never the wrapper - the dialog keeps its\n     open state and focus (07 \u00A73 carry-over bug, fixed by design). */\n  current.replaceChildren(...next.childNodes);\n  const bodyNow = current.querySelector('[data-cart-body]');\n  if (bodyNow && scrolled) bodyNow.scrollTop = scrolled;\n  const lines = next.getAttribute('data-item-count');\n  if (current.getAttribute('data-item-count') !== null) current.setAttribute('data-item-count', lines);\n  const count = next.getAttribute('data-bottles') || '0';\n  current.setAttribute('data-bottles', count);\n  /* Server truth lands: forget whatever the optimistic pass was predicting, so\n     the next roll starts from the number now on screen rather than from a\n     guess the server may have corrected. */\n  seedBagTotals(current);\n  animateShipBar(current, fromScale, wasUnlocked);\n  restoreDrawerFocus(current, focusKey);\n  return count;\n}\n\n/* The one entry point for \"the cart changed, here is the server's answer\".\n   Named for the drawer because that is what it painted for months and there are\n   two dozen call sites; it paints whichever surfaces the response carried. */\nfunction swapDrawer(sections, ticket) {\n  if (!sections) return;\n  if (ticket !== undefined && ticket !== cartTicket) return;   /* a stale answer */\n  let count = null;\n  /* THE PAGE FIRST, because on /cart it is the surface the shopper is looking\n     at and the drawer behind it is only being kept honest. */\n  const pageInner = document.querySelector('[data-cart-page] [data-cart-inner]');\n  if (pageInner && sections['main-cart']) {\n    const doc = new DOMParser().parseFromString(sections['main-cart'], 'text/html');\n    const next = doc.querySelector('[data-cart-inner]');\n    if (next) count = swapCartInner(pageInner, next);\n  }\n  const drawerInner = document.querySelector('[data-drawer-inner]');\n  if (drawerInner && sections['cart-drawer']) {\n    const doc = new DOMParser().parseFromString(sections['cart-drawer'], 'text/html');\n    const next = doc.querySelector('[data-drawer-inner]');\n    if (next) count = swapCartInner(drawerInner, next);\n  }\n  if (count === null) return;\n  setCartCount(count);\n  /* the foot the undo bar is sitting above is a brand-new node, and a removal\n     can change its height (the saved row leaves with the bundle that earned it) */\n  measureBagFoot();\n  const scope = bagScope();\n  if (scope) announceCart(scope);\n  /* The PDP layering band is NOT inside the inner - it is a section further\n     down the product page - but what it should be offering just changed with\n     the bag, so it is re-read from the same freshly painted markup. FIRST of\n     the three, and deliberately: it is synchronous, it touches nothing but the\n     band, and what it prevents is a shopper being sold a bottle they already\n     own. The two below reach the network and the model; neither should be able\n     to strand the offer by throwing. */\n  refreshLayerOffer();\n  refreshDrawerPairs();\n  /* The credit control is inside the inner and was just destroyed with it.\n     Same position as refreshDrawerPairs, and for the same reason - the shell\n     is new, and the amount Flits offers depends on the cart total that just\n     changed. */\n  refreshCartCredit();\n}\n\n/* Which control the shopper was on, described so it can be found again in a\n   subtree that no longer contains the original node. Without this, every\n   quantity tap threw focus back to <body> and a keyboard user lost the row. */\nfunction drawerFocusKey(root) {\n  const a = document.activeElement;\n  if (!a || !root.contains(a)) return null;\n  const line = a.closest('[data-line-key]');\n  if (line) {\n    const key = line.getAttribute('data-line-key');\n    if (a.hasAttribute('data-line-qty')) {\n      return { line: key, sel: '[data-line-qty=\"' + cssAttr(a.getAttribute('data-line-qty')) + '\"]' };\n    }\n    if (a.hasAttribute('data-line-remove')) return { line: key, sel: '[data-line-remove]' };\n  }\n  if (a.closest('[data-drawer-close]')) return { sel: '[data-drawer-close]' };\n  return null;\n}\nfunction restoreDrawerFocus(root, key) {\n  if (!key) return;\n  let target = null;\n  if (key.line) {\n    const line = root.querySelector('[data-line-key=\"' + cssAttr(key.line) + '\"]');\n    if (line) target = line.querySelector(key.sel);\n  } else {\n    target = root.querySelector(key.sel);\n  }\n  /* the row is gone (removed, or emptied the cart) - land somewhere sane\n     inside the sheet rather than dumping focus on the document */\n  if (!target) target = root.querySelector('[data-drawer-close]');\n  if (target) target.focus({ preventScroll: true });\n}\n\nfunction shipUnlocked(root) {\n  const bar = root.querySelector('[data-ship-bar]');\n  return !!bar && bar.getAttribute('data-ship-unlocked') === '1';\n}\n/* The bar is a brand-new node after every swap, so it would otherwise paint\n   straight at its final width and the .8s transition would never run. Start it\n   where the old one stood, force the style to commit, then let it travel. */\nfunction animateShipBar(root, fromScale, wasUnlocked) {\n  const bar = root.querySelector('[data-ship-bar]');\n  if (!bar) return;\n  const fill = bar.querySelector('[data-ship-fill]');\n  if (fill && fromScale) {\n    const to = fill.style.transform;\n    if (to && to !== fromScale) {\n      fill.style.transform = fromScale;\n      void fill.offsetWidth;\n      fill.style.transform = to;\n    }\n  }\n  /* the crossing is a moment, and only on the update that crossed */\n  if (!wasUnlocked && shipUnlocked(root)) bar.classList.add('is-just-unlocked');\n}\n\n/* One spoken sentence per update, from the persistent announcer: what is in\n   the bag, what it costs, and where the shopper stands on free delivery. */\nfunction announceCart(root) {\n  const bottles = parseInt(root.getAttribute('data-bottles'), 10);\n  const total = root.querySelector('[data-cart-total]');\n  const ship = root.querySelector('[data-ship-bar] .ship-bar__label');\n  let msg;\n  if (!bottles) msg = 'Your cart is empty';\n  else msg = bottles + (bottles === 1 ? ' item' : ' items');\n  if (bottles && total) msg += ', subtotal ' + total.textContent.trim();\n  if (ship) msg += '. ' + ship.textContent.trim();\n  announceDrawer(msg);\n}\n\n/* The drawer's own failure surface. A toast cannot be seen from here - the\n   dialog sits in the top layer and .toast-region is only z-indexed - so an\n   open drawer speaks for itself and a closed one falls back to the toast. */\nlet drawerNoteTimer = 0;\nfunction drawerNote(msg) {\n  const el = bagOuter('[data-drawer-note]');\n  if (!el) return;\n  clearTimeout(drawerNoteTimer);\n  if (!msg) { el.textContent = ''; el.hidden = true; return; }\n  el.textContent = msg;\n  el.hidden = false;\n  drawerNoteTimer = setTimeout(() => { el.textContent = ''; el.hidden = true; }, 7000);\n}\nfunction cartFail(msg) {\n  const drawer = document.querySelector('[data-drawer]');\n  if (drawer && drawer.open) drawerNote(msg);\n  else toast(msg);\n}\n/* BOTTLES, NOT LINES, and the server is the one who says so.\n\n   A free 3ml tester is its own line in the cart, so cart.item_count reads 2\n   the moment somebody adds ONE bottle. The bag has always folded that tester\n   into the bottle that earned it; the three badges printed the raw count, so\n   the island said 2 and the bag it opened said \"1 ITEM\" (measured 2026-08-28).\n\n   snippets/cart-bottles.liquid owns the rule and every rendered surface now\n   carries the answer on data-bottles, which is what swapCartInner reads - so\n   the number on the badge after a change is the SERVER's, not a second\n   opinion computed here.\n\n   This function exists for the two paths that never see a rendered section:\n   resyncCart(), which asks /cart.js after a bfcache restore, and the Ajax\n   responses that carry item_count without sections. It mirrors the snippet\n   line for line. IF YOU CHANGE THE RULE, CHANGE IT IN BOTH - the snippet's\n   comment points back here. When the payload has no items array (an add\n   response describes the line added, not the cart) there is nothing to fold\n   and the raw count is the honest answer. */\nfunction cartBottles(cart) {\n  if (!cart) return 0;\n  const items = Array.isArray(cart.items) ? cart.items : null;\n  if (!items) return typeof cart.item_count === 'number' ? cart.item_count : 0;\n  let n = 0;\n  for (const item of items) {\n    const vt = String(item.variant_title || '').toLowerCase();\n    let folded = false;\n    if (item.price === 0 || vt.includes('sample')) {\n      folded = items.some((other) => {\n        const ovt = String(other.variant_title || '').toLowerCase();\n        return other.product_id === item.product_id && other.price > 0\n          && (ovt.includes('50ml') || ovt.includes('bundle'));\n      });\n    }\n    if (!folded) n += item.quantity;\n  }\n  return n;\n}\n/* The badge value is the server's bottle count - the only thing added here is\n   that a number which actually MOVED says so. A swell on a value that did not\n   change would be a lie about a lie. */\nfunction setCartCount(n) {\n  const next = String(n);\n  document.querySelectorAll('[data-cart-count]').forEach((el) => {\n    if (el.textContent === next) return;\n    el.textContent = next;\n    const badge = el.closest('.dock__count') || el;\n    badge.classList.remove('is-bumped');\n    void badge.offsetWidth;                    /* re-trigger the keyframe */\n    badge.classList.add('is-bumped');\n  });\n}\nfunction resyncCart() {\n  fetch(routes.cart + '.js')\n    .then((r) => (r.ok ? r.json() : null))\n    .then((c) => { if (c) setCartCount(cartBottles(c)); })\n    .catch(() => {});\n}\nfunction cartAdd(items) {\n  return fetch(routes.cartAdd + '.js', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n    body: JSON.stringify({ items, sections: cartSections() })\n  }).then((r) => r.json());\n}\n/* ============================================================\n   THE CART BRIDGE - one public way in, for code that is not this file.\n\n   Owner, 2026-08-28: \"when clicking add to cart on the chat widgets it does\n   not refresh and update the cart\".\n\n   It did not, and the reason is that assets/jennie.js had grown a SECOND add\n   path. That path posted to /cart/add.js with no `sections`, so nothing came\n   back to paint and the drawer, the free-delivery bar, the store-credit\n   control and the pair rail all kept showing the bag as it was before the\n   tap. It then wrote the badge by hand from cart.item_count - which is\n   Shopify's count over every ROW, testers included - so adding one 50ml made\n   the badge say 2 over a bag holding one bottle. cartBottles() exists to fold\n   exactly that, and the second path did not call it. And it never sent the\n   free 3ml at all: the tester rides along because the product FORM carries\n   data-sample-id and data-tester-ids, and a card rendered inside the chat\n   panel has no form.\n\n   None of that is jennie.js being careless - it is what happens when a\n   surface outside this file has to reimplement the cart. So it does not have\n   to any more. This is the whole contract, and it is deliberately small:\n\n     window.theme.cart.add(items)  -> Promise<{ ok, count, why }>\n     window.theme.cart.resync()    -> repaints the badge from the server\n     window.theme.cart.read()      -> Promise<cart json | null>\n     window.theme.cart.bottles(c)  -> the folded count for a cart body\n\n   add() is the ordinary path with the form parts taken out: it posts through\n   cartAdd (so `sections` rides along), paints whatever came back through\n   swapDrawer (so every cart-derived surface updates at once, and the badge\n   takes the server's own data-bottles rather than a guess), and then runs\n   repairTesters, which is what puts the free 3ml in for a caller that had no\n   form to carry it. The drawer inner is in the document whether or not the\n   dialog is open, so this works with the bag shut.\n\n   It reads a REFUSAL properly. Shopify answers a rejected add with 200 and a\n   body carrying status 422 plus a sentence worth more than any apology we\n   could write, so that sentence comes back as `why`.\n\n   No ticket is taken. cartTicket guards against a stale answer overwriting a\n   newer one; a caller outside this file has no way to reason about that, and\n   passing undefined makes swapDrawer paint unconditionally, which is the\n   right default for a one-off add.\n   ============================================================ */\nfunction cartBridgeAdd(items) {\n  const list = (Array.isArray(items) ? items : [items]).filter(Boolean);\n  if (!list.length) return Promise.resolve({ ok: false, count: null, why: 'nothing to add' });\n  return cartAdd(list)\n    .then((res) => {\n      if (!res) return { ok: false, count: null, why: '' };\n      if (res.status && res.status !== 200) {\n        return { ok: false, count: null, why: String(res.description || res.message || '') };\n      }\n      swapDrawer(res.sections);\n      repairTesters();\n      const badge = document.querySelector('[data-cart-count]');\n      const n = badge ? parseInt(badge.textContent, 10) : NaN;\n      return { ok: true, count: Number.isFinite(n) ? n : null, why: '' };\n    })\n    .catch(() => ({ ok: false, count: null, why: '' }));\n}\nfunction cartBridgeRead() {\n  return fetch(routes.cart + '.js', { headers: { Accept: 'application/json' } })\n    .then((r) => (r.ok ? r.json() : null))\n    .catch(() => null);\n}\nfunction publishCartBridge() {\n  const t = (window.theme = window.theme || {});\n  t.cart = {\n    add: cartBridgeAdd,\n    resync: resyncCart,\n    read: cartBridgeRead,\n    bottles: cartBottles\n  };\n}\n\n/* ---- The tester repair ----\n   Every 50ml (and every Bundle, which contains one) ships with a free 3ml, and\n   the cart carries that as its own R0 line so the warehouse can pick it. When a\n   line is owed one and does not have it, cart-lines marks the row\n   data-tester-owed=\"<variant id>\" and this puts it right.\n\n   It exists because the promise was only ever as reliable as the add path: a\n   cart built before the tester shipped, or by any route that missed it, showed\n   a bottle with no tester, no mark and no explanation - and the shopper would\n   not have received one. Repairing beats hiding, and beats printing a promise\n   the picking list does not carry.\n\n   Safe to call on every open: it is driven entirely by what the SERVER just\n   rendered, so once the lines come back carrying data-sample-key there is\n   nothing left to match and it stops. The in-flight guard is belt and braces\n   against a second open landing before the first repair's render does. */\nlet testerRepairBusy = false;\nfunction repairTesters() {\n  if (testerRepairBusy) return;\n  const owed = bagPickAll('[data-tester-owed]');\n  if (!owed.length) return;\n  const items = owed.map((row) => ({\n    id: row.dataset.testerOwed,\n    /* THE SHORTFALL, WHICH IS NOT ALWAYS THE WHOLE ROW (2026-08-27).\n       One R0 line serves every row of a product that earns a tester, so a bag\n       holding a 50ml and a Bundle can be owed ONE tester by a row of quantity\n       one that already has none, or by a row of quantity three holding two.\n       cart-lines does that subtraction where it has the cart, and publishes\n       the answer; the stepper below is the fallback for a row rendered before\n       this attribute existed, where the two are the same number anyway. */\n    quantity: Math.max(1, parseInt(row.dataset.testerOwedQty, 10)\n      || parseInt(row.querySelector('.qty output')?.textContent, 10) || 1),\n    properties: SAMPLE_PROPS\n  }));\n  testerRepairBusy = true;\n  const ticket = ++cartTicket;\n  cartAdd(items)\n    .then((res) => {\n      if (res && (!res.status || res.status === 200)) swapDrawer(res.sections, ticket);\n    })\n    /* A tester that will not add is not worth a word to the shopper - the\n       bottle they chose is in the bag and the foot still states the promise. */\n    .catch(() => {})\n    .finally(() => { testerRepairBusy = false; });\n}\n\nfunction openDrawer() {\n  /* ---- NEVER OVER /cart (2026-08-26) ----\n     The page IS the bag now, rendered from the same snippet, and it has already\n     been repainted by the swap that got here. Sliding a sheet holding the same\n     three bottles over the top of it would be the shop showing a shopper their\n     cart because they were looking at their cart. The caller falls back to a\n     toast, which is the right receipt on a surface that updated in place. */\n  if (document.querySelector('[data-cart-page]')) return null;\n  const drawer = document.querySelector('[data-drawer]');\n  if (!drawer || typeof drawer.showModal !== 'function') return null;\n  const wasOpen = drawer.open;\n  if (!wasOpen) drawer.showModal();\n  /* Take focus on the sheet itself rather than leaving it on the close\n     button: the dialog's own label is then the first thing announced, and\n     the first Tab still lands on Close. */\n  drawer.focus({ preventScroll: true });\n  drawerNote('');\n  refreshDrawerPairs();\n  refreshCartCredit();\n  repairTesters();\n  /* A FRESH open only. The list assembles row by row and the rolls take the\n     rendered numbers as their baseline - doing either on a sheet that is\n     already open would re-deal the bag on every quantity tap. */\n  if (!wasOpen) { seedBagTotals(); staggerBag(); }\n  return drawer;\n}\n/* ---- THE PAGE'S OWN \"OPEN\" ----\n   openDrawer does five things the moment the sheet appears: it seeds the roll\n   baselines, deals the rows in, repairs a missing tester, asks the model for a\n   suggestion and re-quotes the store credit. /cart never opens, so none of it\n   would ever run there - the page would render, and then sit with no\n   suggestion, no group blocks and no credit control until the shopper touched\n   something. This is that same moment, fired once when the page arrives.\n\n   Deliberately NOT the stagger: the rows are already on screen when the\n   document paints, and re-dealing a list a shopper is looking at is a flourish\n   played after the fact. The sheet earns it by not existing a moment earlier. */\nfunction initCartPage() {\n  if (!document.querySelector('[data-cart-page]')) return;\n  seedBagTotals();\n  refreshDrawerPairs();\n  refreshCartCredit();\n  repairTesters();\n}\n\nfunction openDrawerConfirm() {\n  if (openDrawer()) {\n    /* the open drawer IS the confirmation - a toast would sit inert\n       behind the modal top layer */\n    announceDrawer('Added to your ritual');\n  } else {\n    toast('Added to your ritual');\n  }\n}\nfunction initCart() {\n  /* Add - enhance every product form (card + PDP). A 50ml carries its\n     free 3ml tester along in one atomic add; if only the freebie fails\n     (e.g. sold out) the bottle is retried on its own so the sale stands. */\n  document.addEventListener('submit', (e) => {\n    const form = e.target.closest('[data-product-form]');\n    if (!form) return;\n    e.preventDefault();\n    const add = form.querySelector('[data-add]');\n    if (add && add.classList.contains('is-added')) return;\n    const fd = new FormData(form);\n    /* deterministic variant: the checked size radio / hidden input wins,\n       whatever order Shopify's product form injects its own id input */\n    const picked = form.querySelector('input[name=\"id\"]:checked, input[name=\"id\"][data-variant-input]');\n    const id = picked ? picked.value : fd.get('id');\n    if (!id) return;\n    const quantity = Math.max(1, parseInt(fd.get('quantity'), 10) || 1);\n\n    /* ---- LINE ITEM PROPERTIES ----\n       Whatever the form named properties[...], forwarded to the cart. Shopify\n       reads that shape natively on a plain POST; this add is a fetch, so the\n       AJAX path has to reproduce it or every property a form carries is\n       silently dropped on the way to the bag. It was, until the gift card\n       needed to send a recipient, a message and a delivery date.\n\n       Silently is the word that matters: nothing failed and nothing warned,\n       the properties simply were not in the JSON. Generic on purpose, so the\n       next form that needs one does not have to discover this again.\n\n       An empty VISIBLE property is dropped rather than sent as \"\", because a\n       property with no underscore is printed on the order and in the\n       confirmation email, and an empty one reads there as an unanswered\n       question. An empty UNDERSCORE property is kept: those are machine\n       fields, invisible to the customer, and an app on the other end may be\n       reading the key rather than the value. The gift card's six postal\n       fields are exactly that - it does not sell a physical card, so they are\n       always empty, and they must still arrive because empty is the shape the\n       app has been handed by this store since it was installed. */\n    const properties = {};\n    fd.forEach((value, key) => {\n      const m = /^properties\\[(.+)\\]$/.exec(key);\n      if (!m || typeof value !== 'string') return;\n      if (value !== '' || m[1].charAt(0) === '_') properties[m[1]] = value;\n    });\n\n    const items = [{ id: id, quantity: quantity }];\n    if (Object.keys(properties).length) items[0].properties = properties;\n    /* Which sizes ship with the free 3ml. A LIST, not one id: the 50ml earns it\n       and so does the Bundle, because a Bundle contains a 50ml. It was\n       data-full-id (singular) and the freebie went missing the day the bundle\n       became a size a shopper could pick. */\n    const sampleId = form.dataset.sampleId;\n    const testerIds = (form.dataset.testerIds || '').split(/\\s+/).filter(Boolean);\n    const bundling = !!sampleId && testerIds.includes(String(id));\n    if (bundling) items.push({ id: sampleId, quantity: quantity, properties: SAMPLE_PROPS });\n\n    const ticket = ++cartTicket;\n    /* The control the shopper actually pressed. On a phone that is the island\n       pill, which submits this form from the bottom of the screen - the form's\n       own button is marked too, so the double-tap guard above still holds. */\n    const acted = takeAddTrigger() || add;\n    const marks = [add, acted].filter((el, i, all) => el && all.indexOf(el) === i);\n    marks.forEach((el) => addWorking(el, true));\n    cartAdd(items)\n      .then((res) => {\n        if (res && res.status && res.status !== 200 && bundling) return cartAdd([items[0]]);\n        return res;\n      })\n      .then((res) => {\n        marks.forEach((el) => addWorking(el, false));\n        if (!res) return;\n        /* nothing was added, so nothing may look like it was */\n        if (res.status && res.status !== 200) { cartFail(res.description || 'Could not add that just now'); return; }\n        marks.forEach((el) => addedFeedback(el));\n        /* the bottle and its free tester travel as ONE parcel; the drawer is\n           handed the sheet only once that parcel has landed on the count */\n        flyToCart(acted, addArt(form)).then(() => {\n          swapDrawer(res.sections, ticket);\n          openDrawerConfirm();\n        });\n      })\n      .catch(() => {\n        marks.forEach((el) => addWorking(el, false));\n        cartFail('Could not add that just now');\n      });\n  });\n\n  /* Qty steppers + line removal inside the drawer. A bundled line drives\n     its bottle AND its tester together (data-sample-key) in one update. */\n  document.addEventListener('click', (e) => {\n    const btn = e.target.closest('[data-line-qty]');\n    if (!btn) return;\n    e.preventDefault();\n    const line = btn.closest('[data-line-key]');\n    if (line) stepLine(line, parseInt(btn.dataset.lineQty, 10) || 0);\n  });\n  document.addEventListener('click', (e) => {\n    const rm = e.target.closest('[data-line-remove]');\n    if (!rm) return;\n    e.preventDefault();\n    const line = rm.closest('[data-line-key]');\n    if (line) setLine(line, 0);\n  });\n\n  /* THE SIZE SWITCH - the part the owner asked for when they said the cart\n     should be \"intelligent like the card\". The card sells every size from one\n     poster; the bag lets a shopper move between them without going back to the\n     product and starting over, and that is also where the bundle stops being a\n     thing that happened to them and becomes a thing they can choose. Opening\n     the switch is free; choosing a rung is an add plus a zero (swapLineSize). */\n  document.addEventListener('click', (e) => {\n    const toggle = e.target.closest('[data-size-toggle]');\n    if (toggle) {\n      e.preventDefault();\n      const line = toggle.closest('[data-line-key]');\n      if (line) toggleSizePanel(line);\n      return;\n    }\n    const rung = e.target.closest('[data-line-size]');\n    if (!rung) return;\n    e.preventDefault();\n    const line = rung.closest('[data-line-key]');\n    if (line) swapLineSize(line, rung);\n  });\n\n  /* Undo, and only for as long as the sheet it was offered in is open - an\n     offer waiting behind a closed drawer is a trap for the next tap. */\n  document.addEventListener('click', (e) => {\n    if (!e.target.closest('[data-undo-do]')) return;\n    e.preventDefault();\n    takeItBack();\n  });\n  /* The broken-duet card rides the same rule as the pill and for the same\n     reason: an offer waiting behind a closed sheet is a trap for the next tap,\n     and by the time the bag is opened again the shopper is shopping, not\n     undoing. Cleared without a redraw - the next open re-renders the slot. */\n  const sheet = document.querySelector('[data-drawer]');\n  if (sheet) sheet.addEventListener('close', () => { hideUndo(); clearBagUnpair(false); });\n\n  /* Cart page (no-JS fallback, but enhanced when JS runs): keep each\n     tester's hidden update input glued to its bottle's quantity input. */\n  document.addEventListener('input', (e) => {\n    const input = e.target;\n    if (!input.matches || !input.matches('[data-cart-page] [data-line-input]')) return;\n    const line = input.closest('[data-line-key]');\n    if (!line) return;\n    const pid = line.getAttribute('data-product-id');\n    if (!pid) return;\n    const sample = document.querySelector('[data-sample-input][data-parent-key=\"' + cssAttr(pid) + '\"]');\n    /* The sum across every row of this scent that earns a tester, not the one\n       number just typed - two sizes of one scent share a single R0 line. */\n    if (sample) sample.value = String(sampleTotalFor(line, parseInt(input.value, 10) || 0));\n  });\n\n  /* bfcache / webview return: the server is the truth */\n  window.addEventListener('pageshow', (e) => { if (e.persisted) resyncCart(); });\n  document.addEventListener('visibilitychange', () => {\n    if (document.visibilityState === 'visible') resyncCart();\n  });\n\n  /* Prerender activation: same rule, different trigger. A prerendered product\n     page may have been rendered minutes before the shopper actually landed on\n     it, and they may have added something from another tab in between, so the\n     cart it drew is a guess until this fires. */\n  if (document.prerendering) {\n    document.addEventListener('prerenderingchange', resyncCart, { once: true });\n  }\n}\n/* escape a value for use inside an attribute selector's quotes */\nfunction cssAttr(v) { return String(v).replace(/[\"\\\\]/g, '\\\\$&'); }\n\n/* ---- WHAT THE SHARED TESTER LINE SHOULD SAY AFTER THIS EDIT ----\n   (2026-08-27.) The free 3ml is the SAME R0 variant whatever bottle earned it,\n   so Shopify holds one line for it however many rows of that scent earn one -\n   a Driftwood 50ml and a Driftwood Bundle share a single R0 line of quantity\n   two. Every write path here used to mirror ONE row's number onto that shared\n   line, which quietly broke the other row:\n\n     bump the 50ml to 2   -> tester set to 2, but three are earned\n     remove the Bundle    -> tester set to 0, and the 50ml loses its tester\n     swap the 50ml to 10ml-> tester zeroed, and the Bundle loses its tester\n\n   The invariant is one line: the R0 quantity equals the SUM of the quantities\n   of every row that earns from it. This computes that sum with `line` counted\n   at the quantity it is about to hold, so a caller can hand it the number it\n   is on its way to writing (0 for a removal or a swap away from a size that\n   earns one). A row already collapsing out of the bag counts as nothing - its\n   removal is what started this. */\nfunction sampleTotalFor(line, quantity) {\n  const key = line.getAttribute('data-sample-key');\n  if (!key) return quantity;\n  let total = 0;\n  bagPickAll('[data-sample-key=\"' + cssAttr(key) + '\"]').forEach((row) => {\n    if (row === line) { total += Math.max(0, quantity); return; }\n    if (row.classList.contains('is-leaving')) return;\n    /* /cart carries both controls and the shopper may have typed into the\n       number input without the stepper having been touched, so the input is\n       read first where it exists. The drawer has only the stepper. */\n    const field = row.querySelector('[data-line-input]');\n    const out = row.querySelector('output');\n    const n = field ? parseInt(field.value, 10)\n      : (out ? parseInt(out.textContent, 10) : 0);\n    total += Math.max(0, n || 0);\n  });\n  return total;\n}\n\n/* ---- The stepper, answered on the tap ----\n   A shopper tapping \"+\" four times used to fire four round trips and watch the\n   number crawl. The count now moves immediately and ONE request goes out with\n   the value they settled on. cartPending remembers the quantity the line held\n   before the burst started, which is the only thing a failure can be rolled\n   back to. */\nconst cartPending = new Map();\nconst CART_SETTLE = 260;\n\nfunction lineName(line) {\n  const n = line.querySelector('.line-item__name');\n  return n ? n.textContent.trim() : 'that item';\n}\nfunction stepLine(line, delta) {\n  const out = line.querySelector('output');\n  const shown = out ? parseInt(out.textContent, 10) || 0 : 0;\n  setLine(line, Math.max(0, shown + delta));\n}\nfunction setLine(line, quantity) {\n  const key = line.getAttribute('data-line-key');\n  if (!key) return;\n  const out = line.querySelector('output');\n  const shown = out ? parseInt(out.textContent, 10) || 0 : 0;\n  const entry = cartPending.get(key) || { was: shown, timer: 0 };\n  entry.qty = quantity;\n  /* ---- THE PAGE'S NO-JS INPUT FOLLOWS THE STEPPER ----\n     /cart carries both controls (see snippets/cart-lines): the stepper a\n     shopper sees and the `updates[...]` number input the page's form POSTs.\n     The input is hidden, not absent, so a Checkout pressed inside the 260ms\n     settle would otherwise post the quantity they had just changed away from.\n     The folded tester rides its bottle here exactly as it does on the wire. */\n  const nojs = line.querySelector('[data-line-input]');\n  if (nojs) nojs.value = String(quantity);\n  entry.sampleKey = line.getAttribute('data-sample-key');\n  /* The tester line is shared by every row of this scent that earns one, so\n     what goes on the wire is the SUM, not this row's number. Computed here,\n     while the row is still in hand, and carried on the entry because the\n     settle fires changeLine with nothing but a key. */\n  entry.sampleQty = sampleTotalFor(line, quantity);\n  /* ---- THE DUPLICATE TESTER LINES, COLLAPSED ON THE NEXT TOUCH ----\n     (2026-09-04.) Two adds of the same R0 variant with different line\n     properties are two Shopify lines that never merge, and cart-lines can only\n     fold ONE of them into this row. The rest are published on the row as\n     data-sample-extra and go to zero here: the primary key already carries the\n     whole earned number (sampleQty is the sum across every earning row), so\n     anything else of that variant in the bag is a duplicate the shopper never\n     chose. Read now, while the row is still in hand, for the same reason\n     sampleQty is - the settle fires changeLine with nothing but a key. */\n  entry.sampleExtra = line.getAttribute('data-sample-extra') || '';\n  const pid = line.getAttribute('data-product-id');\n  if (nojs && pid) {\n    const twin = bagPick('[data-sample-input][data-parent-key=\"' + cssAttr(pid) + '\"]');\n    if (twin) twin.value = String(entry.sampleQty);\n  }\n  entry.name = lineName(line);\n  cartPending.set(key, entry);\n\n  if (quantity === 0) {\n    /* Removal is not a quantity edit: it answers instantly, the row closes out\n       of the list under its own collapse, and the offer to put it back goes up\n       before the network has even been told. entry.holdUntil is when the server\n       render may land - held for the length of the collapse so the rows\n       underneath travel up with it instead of snapping when the answer\n       arrives. Everything the undo needs is read off the row first, because a\n       moment later there is no row to read. */\n    clearTimeout(entry.timer);\n    closeSizePanel(line);\n    const payload = undoPayloadFor(line);\n    /* A SECOND REMOVAL SUPERSEDES THE FIRST. Whatever the slot was still\n       saying about the last broken pair either stops being true right now (the\n       half it named is the one leaving) or is about to be replaced by this\n       one. Cleared without a redraw: the swap below rebuilds the slot anyway. */\n    clearBagUnpair(false);\n    /* Was this half of a pair? Read off the row BEFORE it collapses, because a\n       moment later there is no row and no property to read the pair from. */\n    const broke = takeUnpair(line, payload);\n    entry.holdUntil = Date.now() + collapseRow(line);\n    paintBagTotals();\n    flushLine(key);\n    /* ONE OFFER, NOT TWO (2026-08-21). The card carries a better reason than\n       the pill and it outlives the pill's seven seconds, so when the removal\n       broke a pair the snackbar stands down and the card is the only way back.\n       Mounted here rather than on the answer - inside the collapse, off the\n       index already in hand - which is the \"quite re-active\" the owner asked\n       for: the sheet reacts to the tap, not to the network. */\n    if (broke) { hideUndo(); refreshDrawerPairs(); }\n    else offerUndo(payload);\n    return;\n  }\n  if (out) {\n    out.textContent = String(quantity);\n    out.classList.remove('opt-pop');\n    void out.offsetWidth;                    /* re-trigger keyframe */\n    out.classList.add('opt-pop');\n  }\n  /* the money answers on the same tap as the number that drives it - a\n     quantity that moves over a subtotal that does not is what made the old\n     sheet feel like a form being filled in rather than a bag being held */\n  paintBagTotals();\n  clearTimeout(entry.timer);\n  entry.timer = setTimeout(() => flushLine(key), CART_SETTLE);\n}\nfunction flushLine(key) {\n  const entry = cartPending.get(key);\n  if (!entry) return;\n  clearTimeout(entry.timer);\n  cartPending.delete(key);\n  changeLine(key, entry.qty, entry.sampleKey, entry);\n}\n/* Put the line back the way it was and say so, in the sheet, where it can\n   actually be read. */\nfunction revertLine(key, entry) {\n  const line = bagPick('[data-line-key=\"' + cssAttr(key) + '\"]');\n  if (!line) return;\n  line.classList.remove('is-leaving');\n  line.style.height = '';                    /* the collapse pinned one */\n  const out = line.querySelector('output');\n  if (out && entry && typeof entry.was === 'number') out.textContent = String(entry.was);\n  /* the offer to undo a removal that did not happen is a lie about the cart */\n  hideUndo();\n  paintBagTotals();\n}\n\n/* Fold every duplicate R0 line of this product into the primary one by taking\n   the duplicates to zero. Keys come from cart-lines as a space separated list\n   on the row; a stale one is simply a key Shopify does not know, which it\n   ignores, so this can never fail a write that would otherwise have landed. */\nfunction zeroExtraSamples(updates, extra) {\n  if (!extra) return;\n  String(extra).split(/\\s+/).forEach((k) => {\n    if (k && !(k in updates)) updates[k] = 0;\n  });\n}\n\nfunction changeLine(key, quantity, sampleKey, entry) {\n  if (!key) return;\n  const ticket = ++cartTicket;\n  let request;\n  if (sampleKey) {\n    /* bottle + tester move as one, keyed by line so quantities never drift.\n       The tester's number is the sum across every row that earns from it\n       (setLine computed it while it still had the row), NOT this row's - one\n       R0 line can be serving a 50ml and a Bundle at once. */\n    const updates = {};\n    updates[key] = quantity;\n    updates[sampleKey] = entry && typeof entry.sampleQty === 'number' ? entry.sampleQty : quantity;\n    zeroExtraSamples(updates, entry && entry.sampleExtra);\n    request = fetch(routes.cartUpdate + '.js', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n      body: JSON.stringify({ updates, sections: cartSections() })\n    });\n  } else {\n    request = fetch(routes.cartChange + '.js', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n      body: JSON.stringify({ id: key, quantity, sections: cartSections() })\n    });\n  }\n  request\n    .then((r) => (r.ok ? r.json() : Promise.reject(new Error('cart returned ' + r.status))))\n    .then((res) => {\n      drawerNote('');                        /* a good answer clears the last bad one */\n      /* A removal is still closing when its answer arrives. Landing the render\n         mid-collapse replaces the shrinking row with nothing and the rows below\n         jump the distance the collapse had left to travel, which is the exact\n         unfinished-looking snap the collapse exists to remove. Held only for\n         what is LEFT of it - a slow network has already served the wait. */\n      const land = () => {\n        swapDrawer(res.sections, ticket);\n        if (typeof res.item_count === 'number') setCartCount(cartBottles(res));\n      };\n      const wait = entry && entry.holdUntil ? entry.holdUntil - Date.now() : 0;\n      if (wait > 0) setTimeout(land, wait);\n      else land();\n    })\n    .catch(() => {\n      revertLine(key, entry);\n      const what = entry && entry.name ? entry.name : 'that line';\n      cartFail('We could not update ' + what + '. Check your connection and try again.');\n      resyncCart();\n    });\n}\nfunction addedFeedback(add) {\n  if (!add) return;\n  /* An icon control has no words to swap - the card's \"+\" says yes by morphing\n     its own glyph to a check under .is-added (see .dk-add--plus in\n     components.css). Writing \"Added \u2713\" into it would delete the SVG and leave\n     a text stub in a 38px disc, and the icon would never come back. */\n  const icon = add.hasAttribute('data-add-icon');\n  let label = '';\n  if (!icon) {\n    add.style.minWidth = add.offsetWidth + 'px';   /* no reflow on swap */\n    label = add.textContent;\n    add.textContent = 'Added \u2713';\n  }\n  add.classList.add('is-added');\n  /* one short press-in on the control itself, then the parcel takes over */\n  add.classList.remove('is-committed');\n  void add.offsetWidth;                          /* re-trigger the keyframe */\n  add.classList.add('is-committed');\n  setTimeout(() => {\n    if (!icon) {\n      add.textContent = label;\n      add.style.minWidth = '';\n    }\n    add.classList.remove('is-added');\n    add.classList.remove('is-committed');\n  }, 1600);\n}\n\n/* ============================================================================\n   THE BAG ANSWERS ON THE TAP - optimistic totals, the size switch, undo\n   (redesigned 2026-08-18; owner: \"app-like and instant ... intelligent like\n   the card\")\n\n   Everything in this block exists because a cart that waits for a round trip\n   before it agrees with you does not feel like software you are holding, it\n   feels like a page you are submitting. The stepper already answered on the\n   tap; the numbers underneath it did not, so a shopper pressing \"+\" watched\n   the quantity change and the subtotal, the saving and the free-delivery rail\n   all sit still for 300ms and then jump. The arithmetic to avoid that is the\n   arithmetic the cart already renders, so it is done here as well and the\n   server render simply confirms it.\n\n   THE SERVER IS STILL THE TRUTH. Nothing below ever decides what the cart\n   contains - it decides what the sheet SHOWS while the cart is being told.\n   Every path ends in a Section Rendering swap, seedBagTotals resets the\n   predicted numbers to the rendered ones, and a failure reverts and says so.\n   ========================================================================= */\n\n/* Money the way snippets/price.liquid renders it, and only that way. Two\n   surfaces printing the same number differently is the fastest way to make a\n   cart look broken: \"R 922.50\" from the server and \"R922.5\" from the client\n   reads as a bug even though both are the same amount. Integer rands, cents\n   only when there are cents, non-breaking space after the R. */\nfunction bagMoney(cents) {\n  const c = Math.max(0, Math.round(cents || 0));\n  const rem = c % 100;\n  return 'R\u00A0' + Math.floor(c / 100) + (rem ? '.' + String(rem).padStart(2, '0') : '');\n}\n\n/* ---- a number that moves, moves ----\n   The subtotal and the saving count from where they were to where they are\n   over one short beat. It is the difference between a figure that was replaced\n   and a figure that changed, and on the two numbers a shopper is actually\n   watching that difference is most of what \"app-like\" means.\n\n   Deliberately NOT a CSS animation: there is no way to hand a compositor a\n   number, so this is the one rAF loop in the cart. It is bounded (380ms), it\n   only ever runs on two elements, and reduced motion skips it entirely. */\nconst bagRolls = new WeakMap();\nfunction rollMoney(el, to) {\n  if (!el) return;\n  const target = Math.max(0, Math.round(to || 0));\n  const running = bagRolls.get(el);\n  if (running) cancelAnimationFrame(running.raf);\n  const from = running ? running.value : parseInt(el.dataset.cents || '', 10);\n  el.dataset.cents = String(target);\n  /* PAINT THE DESTINATION FIRST, then travel to it. The roll is decoration;\n     the subtotal is not. Anywhere frames are not being painted - a backgrounded\n     tab, a throttled clock, a browser that defers rAF - the correct figure is\n     already on screen instead of a stale one waiting for an animation that may\n     never tick. The first frame overwrites this within ~16ms, so nothing is\n     visible except in the case this exists to protect. */\n  el.textContent = bagMoney(target);\n  if (!Number.isFinite(from) || from === target || reduceMotion.matches) {\n    bagRolls.delete(el);\n    return;\n  }\n  const entry = { value: from, raf: 0 };\n  const started = performance.now();\n  const step = (now) => {\n    const t = Math.min(1, (now - started) / 380);\n    /* ease-out cubic - the curve --ease-glass approximates, evaluated */\n    const eased = 1 - Math.pow(1 - t, 3);\n    entry.value = Math.round(from + (target - from) * eased);\n    el.textContent = bagMoney(entry.value);\n    if (t < 1) { entry.raf = requestAnimationFrame(step); return; }\n    bagRolls.delete(el);\n    el.textContent = bagMoney(target);\n  };\n  bagRolls.set(el, entry);\n  entry.raf = requestAnimationFrame(step);\n}\n/* Line prices are set, not rolled: a dozen simultaneous odometers is a slot\n   machine, not a cart. They get one beat of motion instead, and only when the\n   value genuinely moved. */\nfunction setMoney(el, cents) {\n  if (!el) return;\n  const next = bagMoney(cents);\n  if (el.textContent === next) return;\n  el.textContent = next;\n  if (reduceMotion.matches) return;\n  el.classList.remove('is-ticking');\n  void el.offsetWidth;                       /* re-trigger the keyframe */\n  el.classList.add('is-ticking');\n}\n\n/* The rows that count. A row on its way out has already been subtracted by the\n   shopper's own tap, so it must not be counted while its collapse plays. */\nfunction bagRows(root) {\n  const scope = root || bagScope();\n  if (!scope) return [];\n  return Array.from(scope.querySelectorAll('.drawer__lines .line-item'))\n    .filter((row) => !row.classList.contains('is-leaving'));\n}\nfunction bagQty(row) {\n  const out = row.querySelector('output');\n  if (out) return parseInt(out.textContent, 10) || 0;\n  const input = row.querySelector('[data-line-input]');\n  return input ? parseInt(input.value, 10) || 0 : 0;\n}\n\n/* Take the freshly rendered numbers as the new starting point. Without this a\n   roll would animate from the last thing the client PREDICTED rather than from\n   the number that is on screen, and any correction the server made would be\n   skipped over instead of shown. */\nfunction seedBagTotals(root) {\n  const scope = root || bagScope();\n  if (!scope) return;\n  const total = scope.querySelector('[data-cart-total]');\n  if (total) { bagRolls.delete(total); total.dataset.cents = scope.getAttribute('data-total-cents') || '0'; }\n  const saved = scope.querySelector('[data-cart-saved]');\n  if (saved) { bagRolls.delete(saved); saved.dataset.cents = scope.getAttribute('data-saved-cents') || '0'; }\n}\n\n/* THE WHOLE FOOT, PREDICTED. Same arithmetic as sections/cart-drawer.liquid\n   (subtotal from the lines, saving from compare_at minus price) run against\n   what the rows currently show. Folded R0 testers are worth nothing and\n   contribute nothing, exactly as they do on the server. */\nfunction paintBagTotals() {\n  const inner = bagScope();\n  if (!inner) return;\n  /* A sheet that reached the page without going through openDrawer or\n     swapDrawer has no baseline for the roll to count FROM, and rollMoney would\n     silently fall back to setting the number outright. Seed it off the server\n     attributes that are still on the wrapper at this point - they are only\n     overwritten at the end of this function. */\n  const totalEl = inner.querySelector('[data-cart-total]');\n  if (totalEl && totalEl.dataset.cents === undefined) seedBagTotals(inner);\n  let total = 0;\n  let saved = 0;\n  bagRows(inner).forEach((row) => {\n    const qty = bagQty(row);\n    const unit = parseInt(row.dataset.unitCents, 10) || 0;\n    const compare = parseInt(row.dataset.compareCents, 10) || 0;\n    total += unit * qty;\n    if (compare > unit) saved += (compare - unit) * qty;\n    const price = row.querySelector('[data-line-price]');\n    if (price && unit > 0) setMoney(price, unit * qty);\n    const was = row.querySelector('[data-line-was]');\n    if (was) {\n      if (compare > unit) { was.hidden = false; setMoney(was, compare * qty); }\n      else was.hidden = true;\n    }\n  });\n  rollMoney(inner.querySelector('[data-cart-total]'), total);\n  rollMoney(inner.querySelector('[data-cart-saved]'), saved);\n  const savedRow = inner.querySelector('[data-saved-row]');\n  if (savedRow) savedRow.hidden = saved <= 0;\n  inner.setAttribute('data-total-cents', String(total));\n  inner.setAttribute('data-saved-cents', String(saved));\n  paintShipBar(total);\n}\n\n/* The free-delivery rail, moved by the same prediction. It reads its threshold\n   off the bar itself (server-rendered from settings) rather than carrying a\n   number in the JS that could drift out of step with the shipping rate the\n   checkout actually applies. */\nfunction paintShipBar(total) {\n  const bar = bagPick('[data-ship-bar]');\n  if (!bar) return;\n  const threshold = parseInt(bar.dataset.shipThreshold, 10) || 0;\n  if (threshold <= 0) return;\n  const fill = bar.querySelector('[data-ship-fill]');\n  if (fill) fill.style.transform = 'scaleX(' + Math.min(1, total / threshold).toFixed(4) + ')';\n  const remaining = threshold - total;\n  const unlocked = remaining <= 0;\n  const label = bar.querySelector('[data-ship-label]');\n  if (label) {\n    label.textContent = unlocked\n      ? 'Free delivery unlocked'\n      : bagMoney(remaining) + ' away from free delivery';\n  }\n  const wasUnlocked = bar.getAttribute('data-ship-unlocked') === '1';\n  bar.setAttribute('data-ship-unlocked', unlocked ? '1' : '0');\n  bar.classList.toggle('is-unlocked', unlocked);\n  /* ---- \"YOU SAVE ... + ~ R 100 SHIPPING\" FOLLOWS THE SAME CROSSING ----\n     (owner, 2026-08-26: \"Cart You save, should show + free shipping at btm when\n     qualify aswell\".) The clause is a SIBLING of [data-cart-saved] rather than\n     part of it (see snippets/cart-inner), so this is the only line that has to\n     know about it and rollMoney can never wipe it. Hidden rather than removed,\n     because the very next tap can take the bag back under the threshold and a\n     node that has to be created first cannot be un-hidden. */\n  const savedShip = bagPick('[data-saved-ship]');\n  if (savedShip) savedShip.hidden = !unlocked;\n  /* the crossing is a moment, and only on the update that crossed it */\n  if (unlocked && !wasUnlocked && !reduceMotion.matches) {\n    bar.classList.remove('is-just-unlocked');\n    void bar.offsetWidth;\n    bar.classList.add('is-just-unlocked');\n  }\n}\n\n/* ---- the list assembles, once ----\n   The first open is the one moment the bag has a shopper's whole attention,\n   and a list that lands row by row reads as something being handed over. Only\n   on a fresh open (never on a re-render, which would make every quantity tap\n   re-deal the deck), never on a long list (a twelfth row 500ms late is not a\n   flourish, it is a wait), and never under reduced motion. */\nfunction staggerBag() {\n  if (reduceMotion.matches) return;\n  const list = bagPick('.drawer__lines');\n  if (!list) return;\n  const rows = Array.from(list.children);\n  if (!rows.length || rows.length > 10) return;\n  rows.forEach((row, i) => row.style.setProperty('--i', String(i)));\n  list.classList.remove('is-opening');\n  void list.offsetWidth;                     /* re-trigger the keyframes */\n  list.classList.add('is-opening');\n  setTimeout(() => {\n    list.classList.remove('is-opening');\n    rows.forEach((row) => row.style.removeProperty('--i'));\n  }, 420 + rows.length * 42);\n}\n\n/* Bring a row (and anything that just grew under it) into the scroller. Used\n   after a size switch, which moves the line to the end of the cart - Shopify\n   appends new lines, so a swapped row cannot keep its position, and a row that\n   silently teleports is worse than one that is followed. */\nfunction scrollLineIntoView(target) {\n  const line = typeof target === 'string'\n    ? bagPick('.drawer__lines [data-handle=\"' + cssAttr(target) + '\"]')\n    : target;\n  if (!line) return;\n  /* ON THE PAGE THE SCROLLER IS THE DOCUMENT. The panel does not scroll inside\n     itself (see .cart-panel in components.css - two scrollbars and a trapped\n     thumb is what that would be), so there is no box to measure against and the\n     platform's own scrollIntoView is exactly right. */\n  const body = bagIsPage() ? null : bagPick('[data-cart-body]');\n  if (!body) {\n    line.scrollIntoView({ block: 'nearest', behavior: reduceMotion.matches ? 'auto' : 'smooth' });\n    return;\n  }\n  const box = body.getBoundingClientRect();\n  const row = line.getBoundingClientRect();\n  if (row.top >= box.top && row.bottom <= box.bottom) return;\n  const delta = row.bottom > box.bottom ? row.bottom - box.bottom + 12 : row.top - box.top - 12;\n  body.scrollTo({ top: body.scrollTop + delta, behavior: reduceMotion.matches ? 'auto' : 'smooth' });\n}\n\n/* ---- THE SIZE SWITCH ----\n   Opening travels a real height (grid-template-rows 0fr -> 1fr in CSS), so the\n   hidden attribute has to come off a frame BEFORE the class goes on: a node\n   that is display:none has no height to transition from. */\nfunction closeSizePanel(line) {\n  const btn = line.querySelector('[data-size-toggle]');\n  if (btn) btn.setAttribute('aria-expanded', 'false');\n  const panel = line.querySelector('[data-size-panel]');\n  if (!panel || panel.hidden) return;\n  panel.classList.remove('is-open');\n  if (reduceMotion.matches) { panel.hidden = true; return; }\n  setTimeout(() => { if (!panel.classList.contains('is-open')) panel.hidden = true; }, 260);\n}\nfunction openSizePanel(line) {\n  const panel = line.querySelector('[data-size-panel]');\n  if (!panel) return;\n  /* one open switch at a time - two expanded rows in a 430px sheet is a list of\n     panels rather than a list of bottles */\n  bagPickAll('.line-item').forEach((other) => {\n    if (other !== line) closeSizePanel(other);\n  });\n  const btn = line.querySelector('[data-size-toggle]');\n  if (btn) btn.setAttribute('aria-expanded', 'true');\n  panel.hidden = false;\n  /* Force the layout that makes 0fr the committed from-value, then open in the\n     same tick. Deliberately NOT requestAnimationFrame: a control that only\n     becomes visible once a frame has been painted is invisible anywhere frames\n     are not being painted (a background tab, a headless check), and a size\n     switch that opens to nothing is worse than one that opens without a\n     transition. A forced reflow gives the transition its start value with no\n     such dependency. */\n  void panel.offsetHeight;\n  panel.classList.add('is-open');\n  /* let the height finish travelling before deciding whether it needs\n     scrolling to - a panel measured mid-open reports the wrong bottom */\n  setTimeout(() => scrollLineIntoView(line), reduceMotion.matches ? 0 : 280);\n}\nfunction toggleSizePanel(line) {\n  const panel = line.querySelector('[data-size-panel]');\n  if (!panel) return;\n  if (panel.hidden) openSizePanel(line);\n  else closeSizePanel(line);\n}\n\n/* ---- CHANGING A SIZE IN THE BAG ----\n   Shopify cannot change the variant on a line, so this is an ADD followed by a\n   zero, in that order: the bottle is never absent from the cart, so a failure\n   half way through leaves the shopper with what they already had rather than\n   with nothing. The free tester follows the size that earns it - each rung\n   carries data-tester from the same Liquid rule that decides the fold - and a\n   layer set's property is carried across so a swapped bottle does not fall out\n   of its own set. */\nfunction swapLineSize(line, rung) {\n  const key = line.getAttribute('data-line-key');\n  const variantId = rung.getAttribute('data-variant-id');\n  if (!key || !variantId || line.classList.contains('is-swapping')) return;\n  if (rung.classList.contains('is-active')) { closeSizePanel(line); return; }\n  const quantity = bagQty(line);\n  if (!quantity) return;\n\n  const items = [{ id: variantId, quantity: quantity }];\n  const layer = line.getAttribute('data-layer-set');\n  if (layer) items[0].properties = { _layer_set: layer };\n\n  /* THE TESTER IS A LINE OF ITS OWN, AND IT IS THE SAME VARIANT WHATEVER SIZE\n     THE BOTTLE IS. So a 50ml -> Bundle swap must LEAVE IT ALONE: adding it\n     again would bump the existing R0 line to two, and zeroing the old line's\n     sample key would then delete both - which is exactly what happened the\n     first time this ran, and the bundle arrived with no tester at all.\n       had && wants  -> touch nothing, it is already the right line\n       had && !wants -> zero it (a 10ml does not earn one)\n       !had && wants -> add it\n       neither       -> nothing to do */\n  const sampleKey = line.getAttribute('data-sample-key');\n  const testerId = line.getAttribute('data-tester-id');\n  const hadTester = !!sampleKey;\n  const wantsTester = !!(testerId && rung.hasAttribute('data-tester'));\n  if (wantsTester && !hadTester) {\n    items.push({ id: testerId, quantity: quantity, properties: SAMPLE_PROPS });\n  }\n  const handle = line.getAttribute('data-handle');\n  const name = lineName(line);\n\n  /* a hold, never a spinner (04 \u00A73) - and it also stops a second rung being\n     tapped into the same round trip */\n  line.classList.add('is-swapping');\n  closeSizePanel(line);\n  hideUndo();\n\n  const ticket = ++cartTicket;\n  cartAdd(items)\n    .then((res) => {\n      /* only the freebie failed (sold out): the size change still stands */\n      if (res && res.status && res.status !== 200 && items.length > 1) return cartAdd([items[0]]);\n      return res;\n    })\n    .then((res) => {\n      if (!res || (res.status && res.status !== 200)) {\n        return Promise.reject(new Error((res && res.description) || 'size add failed'));\n      }\n      const updates = {};\n      updates[key] = 0;\n      /* Zeroing the SHARED line would have taken the other rows' testers with\n         it - a 50ml swapped down to a 10ml used to strip the tester off the\n         Bundle sitting beside it. What is left is this row's share removed\n         and everybody else's kept. */\n      /* The duplicates go with the swap rather than being left behind as free\n         lines with no bottle - the same rule a quantity write follows. Only\n         when this row is giving its tester up: a swap that KEEPS one keeps the\n         primary line exactly as it is, which is what the note above protects. */\n      if (hadTester && !wantsTester) {\n        updates[sampleKey] = sampleTotalFor(line, 0);\n        zeroExtraSamples(updates, line.getAttribute('data-sample-extra'));\n      }\n      return fetch(routes.cartUpdate + '.js', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n        body: JSON.stringify({ updates: updates, sections: 'cart-drawer' })\n      }).then((r) => (r.ok ? r.json() : Promise.reject(new Error('cart returned ' + r.status))));\n    })\n    .then((res) => {\n      drawerNote('');\n      swapDrawer(res.sections, ticket);\n      if (typeof res.item_count === 'number') setCartCount(cartBottles(res));\n      spotlightLine(handle);\n      scrollLineIntoView(handle);\n      announceDrawer(name + ' changed to ' + (rung.textContent || '').trim().split('\\n')[0]);\n    })\n    .catch(() => {\n      line.classList.remove('is-swapping');\n      cartFail('We could not change the size of ' + name + '. Please try again.');\n      resyncCart();\n    });\n}\n\n/* ---- REMOVAL, AND TAKING IT BACK ----\n   One tap next to a stepper, on a phone, is how a bottle leaves the bag. An\n   offer to put it back costs one line and turns a mis-tap from a re-shopping\n   trip into a tap. Everything the re-add needs is read off the row BEFORE it\n   goes, because a moment later the row does not exist. */\nlet undoTimer = 0;\nlet undoPayload = null;\nfunction undoPayloadFor(line) {\n  const layer = line.getAttribute('data-layer-set');\n  return {\n    variantId: line.getAttribute('data-variant-id'),\n    /* it had a folded tester, so the size it holds is one that earns one */\n    testerId: line.hasAttribute('data-sample-key') ? line.getAttribute('data-tester-id') : '',\n    quantity: Math.max(1, bagQty(line)),\n    handle: line.getAttribute('data-handle'),\n    name: lineName(line),\n    properties: layer ? { _layer_set: layer } : null\n  };\n}\n/* The snackbar floats over the sheet rather than displacing it, so it has to be\n   told what to clear - and everything under it moves: the foot with the saved\n   row and the wallet buttons, the suggestion card with the length of its\n   reason. Measured, never assumed (the same rule as --head-h).\n\n   Two numbers come out of one read. --bag-foot-h is the foot. --bag-clear-h is\n   what the pill actually sits above, which is the SUGGESTION when one is up\n   (owner, 2026-08-19) - the pill used to land across \"Layers with your bag\" and\n   cut its two size buttons in half. The card lives in the scroller, so the\n   clearance is read off its live box rather than its height: scrolled out of\n   view it asks for nothing and the pill drops back onto the foot. Capped at\n   half the sheet, so a tall card can never throw the pill up into the bag. */\nfunction measureBagFoot() {\n  /* PAGE: THERE IS NOTHING TO MEASURE. The pill is fixed to the viewport there\n     (.cart-panel .drawer__undo), because a foot that scrolls away takes any\n     pill pinned above it with it. The clearance maths below is the SHEET's\n     problem - a pinned foot with a card that slides under it. */\n  if (bagIsPage()) return;\n  const drawer = document.querySelector('[data-drawer]');\n  if (!drawer) return;\n  const foot = drawer.querySelector('.drawer__foot');\n  const h = foot ? Math.round(foot.getBoundingClientRect().height) : 0;\n  drawer.style.setProperty('--bag-foot-h', h + 'px');\n  let clear = h;\n  const pairs = drawer.querySelector('[data-drawer-pairs]');\n  if (pairs && !pairs.hidden) {\n    const sheet = drawer.getBoundingClientRect();\n    const card = pairs.getBoundingClientRect();\n    /* how far the TOP of the suggestion sits off the bottom of the sheet */\n    const over = Math.round(sheet.bottom - card.top);\n    clear = Math.max(h, Math.min(over, Math.round(sheet.height * .5)));\n  }\n  drawer.style.setProperty('--bag-clear-h', clear + 'px');\n}\n/* The card the pill is clearing scrolls, so the clearance is only true for the\n   offset it was read at. Bound on the DIALOG with capture rather than on the\n   body, because scroll does not bubble and the body is a brand-new node after\n   every cart render - the dialog is the one box that survives the swap. */\nlet undoRaf = 0;\nfunction onBagScroll() {\n  if (undoRaf) return;\n  undoRaf = requestAnimationFrame(() => { undoRaf = 0; measureBagFoot(); });\n}\nfunction watchBagClearance(on) {\n  const drawer = document.querySelector('[data-drawer]');\n  if (!drawer) return;\n  drawer.removeEventListener('scroll', onBagScroll, true);\n  if (on) drawer.addEventListener('scroll', onBagScroll, { capture: true, passive: true });\n}\nfunction offerUndo(payload) {\n  const bar = bagOuter('[data-cart-undo]');\n  if (!bar || !payload || !payload.variantId) return;\n  measureBagFoot();\n  watchBagClearance(true);\n  undoPayload = payload;\n  const text = bar.querySelector('[data-undo-text]');\n  if (text) text.textContent = payload.name + ' removed';\n  bar.classList.remove('is-leaving');\n  bar.hidden = false;\n  clearTimeout(undoTimer);\n  undoTimer = setTimeout(hideUndo, 7000);\n}\nfunction hideUndo() {\n  clearTimeout(undoTimer);\n  undoPayload = null;\n  watchBagClearance(false);\n  const bar = bagOuter('[data-cart-undo]');\n  if (!bar || bar.hidden) return;\n  if (reduceMotion.matches) { bar.hidden = true; return; }\n  bar.classList.add('is-leaving');\n  setTimeout(() => { bar.hidden = true; bar.classList.remove('is-leaving'); }, 240);\n}\n/* PUTTING A LINE BACK, exactly as it was. Extracted from takeItBack (2026-08-21)\n   so the broken-duet card can offer the same way back the snackbar does and\n   they cannot drift apart. It has to be this and not the suggestion's\n   addPairToCart: the payload restores the QUANTITY the row held, the tester it\n   was owed, and - the reason it matters here - its `_layer_set`, which is the\n   only thing that puts the bottle back INTO its set rather than beside it.\n   Rejects rather than reporting, so each caller says its own failure. */\nfunction readdLine(payload) {\n  if (!payload || !payload.variantId) return Promise.reject(new Error('nothing to put back'));\n  const items = [{ id: payload.variantId, quantity: payload.quantity }];\n  if (payload.properties) items[0].properties = payload.properties;\n  if (payload.testerId) {\n    items.push({ id: payload.testerId, quantity: payload.quantity, properties: SAMPLE_PROPS });\n  }\n  const ticket = ++cartTicket;\n  return cartAdd(items)\n    .then((res) => {\n      if (res && res.status && res.status !== 200 && items.length > 1) return cartAdd([items[0]]);\n      return res;\n    })\n    .then((res) => {\n      if (!res || (res.status && res.status !== 200)) return Promise.reject(new Error('undo failed'));\n      swapDrawer(res.sections, ticket);\n      spotlightLine(payload.handle);\n      scrollLineIntoView(payload.handle);\n      announceDrawer(payload.name + ' is back in your ritual');\n      return res;\n    });\n}\nfunction takeItBack() {\n  const payload = undoPayload;\n  hideUndo();\n  if (!payload) return;\n  readdLine(payload)\n    .catch(() => cartFail('We could not put ' + payload.name + ' back. Please add it again.'));\n}\n\n/* The row leaves by closing, not by vanishing. Pinning the from-height is what\n   makes a transition to 0 possible at all (auto has nothing to travel from),\n   and the caller uses the returned duration to hold the server render back so\n   the rows below do not snap up mid-collapse. */\nfunction collapseRow(row) {\n  if (reduceMotion.matches) { row.classList.add('is-leaving'); return 0; }\n  row.style.height = row.getBoundingClientRect().height + 'px';\n  void row.offsetHeight;                     /* commit the from-height */\n  row.classList.add('is-leaving');\n  return 240;\n}\n\n/* ---- Adding to the cart, choreographed --------------------------------------\n   An add used to be silent: the drawer simply existed one moment later. The\n   sequence now reads as one gesture in four beats, and every beat is a fact -\n\n     1. the control the shopper pressed holds while the network answers\n        (no spinner, 04 \u00A73), and says \"Added\" only once the server has said so;\n     2. a small parcel of the bottle leaves that control and travels to\n        wherever the cart lives on this viewport - the header icon at 1024px\n        and up, the island pill below it;\n     3. the count changes as the parcel lands, so the number is the receipt;\n     4. the drawer takes over.\n\n   A 50ml and its free tester post as two line items and play ONE parcel: the\n   shopper bought one thing. A failure plays none of it - the working state is\n   simply let go and the error is said out loud (see initCart / addPairToCart).\n\n   Transform and opacity only. No filter, no backdrop-filter, no width/height/\n   top/left anywhere in here (05 \u00A75) - the whole site's scroll was fixed by\n   taking exactly that kind of work off the main thread and it stays off.\n---------------------------------------------------------------------------- */\nconst FLY_SIZE = 46;\n\n/* The island's Add pill submits the PDP form from the other end of the screen.\n   Without this the confirmation would play on a button the shopper cannot see.\n   Set immediately before requestSubmit(), consumed by the submit handler. */\nlet addTrigger = null;\nfunction takeAddTrigger() {\n  const el = addTrigger;\n  addTrigger = null;\n  return el && el.isConnected ? el : null;\n}\n/* requestSubmit() is Safari 16+ (iOS 16.0). On an older iPhone every Add that\n   went through it - the island pill, the card sheet, the lone-size card - threw\n   a TypeError and nothing reached the bag. The fallback does what requestSubmit\n   does: validate, then fire the submit event initCart's delegated handler\n   consumes. That handler always preventDefaults a [data-product-form], so the\n   synthetic event never posts natively. */\nfunction submitForm(form) {\n  if (typeof form.requestSubmit === 'function') { form.requestSubmit(); return; }\n  if (typeof form.reportValidity === 'function' && !form.reportValidity()) return;\n  form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));\n}\n/* Held while the request is out. Not a disabled attribute: a disabled button\n   loses focus, and a keyboard shopper would be dropped onto <body> mid-add. */\nfunction addWorking(el, on) {\n  if (!el) return;\n  el.classList.toggle('is-working', !!on);\n  if (on) el.setAttribute('aria-busy', 'true');\n  else el.removeAttribute('aria-busy');\n}\n/* The parcel carries the packshot of what was added, so the thing that travels\n   is the bottle the shopper was looking at and not an abstract dot. */\nfunction addArt(form) {\n  const card = form.closest('[data-card]');\n  if (card) return card.querySelector('img.shot');\n  const pdp = form.closest('.pdp');\n  if (pdp) return pdp.querySelector('.pdp__shot.is-active') || pdp.querySelector('.pdp__shot');\n  /* Anything else that knows which of its pictures is the thing being added.\n     The two branches above are position-based - \"the shot inside the card\",\n     \"the active shot inside the PDP\" - and neither describes a surface that\n     is not one of those two. The gift card composer is the first, and it\n     points at the card artwork itself rather than at a bottle. */\n  const host = form.closest('[data-add-art-host]') || document;\n  return host.querySelector('[data-add-art]');\n}\n/* Where the cart lives on THIS viewport. The header utilities and the island\n   are shown at mutually exclusive widths, so the first badge with a real box\n   is the right one; a closed menu sheet has none. */\nfunction cartTarget() {\n  const nodes = document.querySelectorAll('[data-cart-count]');\n  for (let i = 0; i < nodes.length; i++) {\n    const host = nodes[i].closest('.dock__count') || nodes[i];\n    const r = host.getBoundingClientRect();\n    if (r.width > 0 && r.height > 0) return host;\n  }\n  return null;\n}\n/* Resolves when the parcel has landed - or straight away when there is nothing\n   to play (reduced motion, no cart on screen, no Web Animations). The cart\n   never waits on a frame that may not be coming: the caller gets its promise\n   either way, and a hard timer backs up the finish event for throttled tabs. */\nfunction flyToCart(source, art) {\n  if (reduceMotion.matches || !source || typeof source.animate !== 'function') return Promise.resolve(false);\n  const target = cartTarget();\n  if (!target) return Promise.resolve(false);\n  const from = source.getBoundingClientRect();\n  const to = target.getBoundingClientRect();\n  if (!from.width || !to.width) return Promise.resolve(false);\n\n  const parcel = document.createElement('span');\n  parcel.className = 'fly-parcel';\n  parcel.setAttribute('aria-hidden', 'true');\n  const src = art && (art.currentSrc || art.src);\n  if (src) {\n    const img = document.createElement('img');\n    img.src = src; img.alt = ''; img.decoding = 'async';\n    parcel.appendChild(img);\n  } else {\n    parcel.classList.add('fly-parcel--plain');\n  }\n  document.body.appendChild(parcel);\n\n  const half = FLY_SIZE / 2;\n  const sx = from.left + from.width / 2 - half;\n  const sy = from.top + from.height / 2 - half;\n  /* The cart badge can be off screen - the header is static, so on a long page\n     it has scrolled away entirely. The landing point is clamped back into view:\n     a parcel has to be seen arriving somewhere, even when its badge is not. */\n  const tx = Math.max(8, Math.min(window.innerWidth - FLY_SIZE - 8, to.left + to.width / 2 - half));\n  const ty = Math.max(8, Math.min(window.innerHeight - FLY_SIZE - 8, to.top + to.height / 2 - half));\n  const dist = Math.sqrt((tx - sx) * (tx - sx) + (ty - sy) * (ty - sy));\n  const dur = Math.max(360, Math.min(620, 240 + dist * 0.4));\n  /* one gentle arc, lifted off the straight line. Fixed px in every keyframe,\n     never calc(var()) - that is what keeps it on the compositor. */\n  const lift = Math.min(130, dist * 0.26);\n  const at = (x, y, s) => 'translate3d(' + Math.round(x) + 'px, ' + Math.round(y) + 'px, 0) scale(' + s + ')';\n  const anim = parcel.animate([\n    { transform: at(sx, sy, 0.62), opacity: 0, easing: 'cubic-bezier(.2,0,.4,1)' },\n    { transform: at(sx, sy, 1), opacity: 1, offset: 0.14, easing: 'linear' },\n    { transform: at((sx + tx) / 2, (sy + ty) / 2 - lift, 0.82), opacity: 1, offset: 0.58,\n      easing: 'cubic-bezier(.3,0,.2,1)' },\n    { transform: at(tx, ty, 0.24), opacity: 0 }\n  ], { duration: dur, fill: 'both' });\n\n  return new Promise((resolve) => {\n    let settled = false;\n    const land = () => {\n      if (settled) return;\n      settled = true;\n      parcel.remove();\n      resolve(true);\n    };\n    anim.addEventListener('finish', land);\n    anim.addEventListener('cancel', land);\n    setTimeout(land, dur + 400);               /* a backgrounded tab never finishes */\n  });\n}\n/* The row that just arrived from the server, told apart from the ones that were\n   already there. Used only when the drawer was ALREADY open (the suggestion\n   row): when the sheet itself is sliding in, the sheet is the motion. */\nfunction spotlightLine(handle) {\n  if (!handle || reduceMotion.matches) return;\n  const line = bagPick('.drawer__lines [data-handle=\"' + cssAttr(handle) + '\"]');\n  if (!line) return;\n  line.classList.remove('is-arriving');\n  void line.offsetWidth;\n  line.classList.add('is-arriving');\n  line.addEventListener('animationend', () => line.classList.remove('is-arriving'), { once: true });\n}\n\n/* ---- Drawer announcer: persistent live region inside the dialog\n   (swapDrawer replaces the inner subtree, so fresh aria-live nodes\n   there never announce; this one survives every swap) ---- */\nfunction announceDrawer(msg) {\n  const el = bagOuter('[data-drawer-announce]');\n  if (!el) return;\n  el.textContent = '';\n  requestAnimationFrame(() => { el.textContent = msg; });\n}\n\n/* ---- Toast queue ---- */\nfunction toast(msg) {\n  const region = document.querySelector('[data-toast-region]');\n  if (!region) return;\n  const el = document.createElement('div');\n  el.className = 'toast';\n  el.textContent = msg;\n  region.append(el);\n  setTimeout(() => {\n    el.classList.add('is-leaving');\n    el.addEventListener('animationend', () => el.remove(), { once: true });\n    setTimeout(() => el.remove(), 600);            /* safety net */\n  }, 2600);\n}\n\n/* ---- Lane geometry: one card + gap ---- */\nfunction laneStep(lane) {\n  const card = lane.firstElementChild;\n  const gap = parseFloat(getComputedStyle(lane).columnGap) || 2;\n  return card ? card.getBoundingClientRect().width + gap : lane.clientWidth * 0.8;\n}\n\n/* ---- Infinite lane: clone the real set fore AND aft, then jump by one\n   set-width the instant the scroll crosses out of the middle copy. Native\n   scroll / snap / drag all keep working; the seam is invisible because the\n   reposition is instant (snap off \u2192 set scrollLeft \u2192 snap on) and the clones\n   are pixel-identical. Opt-in via [data-lane-loop]; rebuilt on resize.\n\n   IT USED TO GIVE UP WHEN THE SET FIT, and that is exactly when a monitor\n   showed the lane's end. Four vibe doors on the 4.3-up ladder (>1921px) come\n   to 93% of the rail, so `scrollWidth <= clientWidth` was true, build()\n   returned, and Browse by Vibe sat there with four doors, dead space to their\n   right and two arrows that did nothing. A lane that asked to loop loops:\n   fitting is a reason to clone MORE, not to stop.\n\n   So the padding is COPIES, not one copy. One set each side is enough while\n   the set is wider than the rail (every width up to 1920, and the phone), but\n   a set narrower than the rail runs out of cards before the jump can happen -\n   you would scroll to the end of the tail and see the rail finish. `copies`\n   is how many sets it takes to cover the rail, and the same number goes on\n   both sides; the jump is still ONE set-width, so the seam maths is unchanged\n   and the rest position is simply `copies` sets in. ---- */\nfunction initLaneLoop() {\n  document.querySelectorAll('[data-lane-loop]').forEach((lane) => {\n    const originals = Array.from(lane.children);\n    if (originals.length < 2) return;\n\n    let setW = 0, rest = 0, copies = 1, built = false, jumping = false;\n    /* DOTS. data-lane-dots names an element whose children are one dot per\n       REAL card (the perks band draws them in Liquid); the one under the\n       rail's current card gets `is-on`. Only the loop can say which card\n       that is, because only it knows where the real set rests and how wide\n       a set is - so the paint lives here, off the same scroll listener the\n       settle timer uses, one rAF at a time. Offsets of the real cards are\n       taken once per build; the nearest offset to the rail's position within\n       the set, allowing for the wrap at either end, is the card. */\n    const dots = lane.dataset.laneDots ? document.getElementById(lane.dataset.laneDots) : null;\n    let offsets = [], dotRaf = 0, dotAt = 0, dotBase = 0;\n    const paintDots = () => {\n      dotRaf = 0;\n      if (!dots || !built || !setW) return;\n      /* The nearest (copy, card) to the rail's position, searched over every\n         copy the rail holds. `dotBase` is the start of that copy: it is what\n         a tap on a dot scrolls relative to, so the move stays inside the copy\n         the rail is already in and never crosses a seam. */\n      let at = 0, base = rest, best = Infinity;\n      for (let k = -copies; k <= copies; k++) {\n        const b = rest + k * setW;\n        offsets.forEach((o, i) => {\n          const d = Math.abs(lane.scrollLeft - (b + o));\n          if (d < best) { best = d; at = i; base = b; }\n        });\n      }\n      dotAt = at; dotBase = base;\n      Array.from(dots.children).forEach((d, i) => {\n        const on = i === at;\n        d.classList.toggle('is-on', on);\n        if (d.hasAttribute('data-lane-dot')) {\n          if (on) d.setAttribute('aria-current', 'true'); else d.removeAttribute('aria-current');\n        }\n      });\n    };\n    const askDots = () => { if (dots && !dotRaf) dotRaf = requestAnimationFrame(paintDots); };\n    /* A TAPPED DOT SCROLLS TO ITS CARD, IN THE DOTS' OWN ORDER: an earlier\n       dot moves the rail back, a later one forward, by exactly the cards\n       between - not to the nearest twin of that card, which on a set of\n       three would take the first dot forward. Smooth unless motion is\n       reduced; the scroll events it raises repaint the dots on the way and\n       the settle handler below reads the rail once it stops. */\n    if (dots) dots.addEventListener('click', (e) => {\n      const dot = e.target.closest('[data-lane-dot]');\n      if (!dot || !built || !setW) return;\n      const to = Math.min(originals.length - 1, Math.max(0, parseInt(dot.dataset.laneDot, 10) || 0));\n      if (to === dotAt) return;\n      lane.scrollTo({ left: dotBase + offsets[to], behavior: reduceMotion.matches ? 'auto' : 'smooth' });\n    });\n\n    const measure = () => {\n      const first = lane.querySelector('[data-loop-real]');\n      const tail = lane.querySelector('[data-loop-tail]');\n      /* A clone that is display:none (the perks band hides them once its\n         list is a grid) has no offsetParent and an offsetLeft of 0, which\n         would make the set width NEGATIVE and recenter() would then jump on\n         every pointerup. No tail on screen means no set to loop. */\n      /* offsetLeft, NOT getBoundingClientRect: rects come back in visual\n         pixels, so under any page scale (a scaled device emulation, a CSS\n         zoom) they are not scrollLeft's units and the set width measured off\n         them lands the jump a whole card short. Measured 2026-09-07 in the\n         Browser pane's scaled 375px preset: 2974.7 by rect against 3020 by\n         offset, and only the second is a seam. offsetLeft rounds to the\n         pixel, which at worst puts the jump 1px off a snap point. */\n      setW = first && tail && tail.offsetParent ? tail.offsetLeft - first.offsetLeft : 0;\n      return setW;\n    };\n\n    const clone = (node, tag) => {\n      const c = node.cloneNode(true);\n      c.setAttribute('data-clone', tag);\n      c.setAttribute('aria-hidden', 'true');\n      c.removeAttribute('data-loop-real');\n      c.setAttribute('tabindex', '-1');\n      c.querySelectorAll('a, button, input, select, textarea, [tabindex]')\n        .forEach((el) => el.setAttribute('tabindex', '-1'));\n      /* clones are born after initReveal ran - never leave them gated */\n      c.querySelectorAll('[data-reveal]').forEach((el) => el.classList.add('reveal'));\n      if (c.hasAttribute('data-reveal')) c.classList.add('reveal');\n      return c;\n    };\n\n    const jumpTo = (x) => {\n      jumping = true;\n      const snap = lane.style.scrollSnapType;\n      const beh = lane.style.scrollBehavior;\n      lane.style.scrollSnapType = 'none';\n      lane.style.scrollBehavior = 'auto';\n      lane.scrollLeft = x;\n      void lane.offsetWidth;                 /* commit before snap re-engages */\n      lane.style.scrollSnapType = snap;\n      lane.style.scrollBehavior = beh;\n      jumping = false;\n    };\n\n    /* The set's own pitch, measured off the REAL cards and independent of\n       whether it overflows. lane.scrollWidth cannot be used for this: it\n       reports the RAIL's width once the content fits inside it, which is the\n       one case that has to be got right. */\n    const naturalSet = () => {\n      const first = originals[0];\n      const last = originals[originals.length - 1];\n      const gap = parseFloat(getComputedStyle(lane).columnGap) || 0;\n      return (last.offsetLeft + last.offsetWidth) - first.offsetLeft + gap;\n    };\n\n    const build = () => {\n      if (built) return;\n      lane.querySelectorAll('[data-clone]').forEach((n) => n.remove());\n      originals.forEach((n) => n.setAttribute('data-loop-real', ''));\n\n      const natural = naturalSet();\n      if (natural <= 0 || lane.clientWidth <= 0) return;       /* not laid out yet */\n      /* A SMALL SET GETS TWO COPIES A SIDE. Three perks are one set of 1000px;\n         with a single copy each way, the rail past the last real card is the\n         same three cells and then the end, and a seam that is not honoured\n         mid-gesture (see the settle handler below) would let a second flick\n         reach it. Six extra <li>s of words cost nothing; ten more posters\n         would, so the wide rails keep the one copy they always had. */\n      copies = Math.max(originals.length < 5 ? 2 : 1, Math.ceil(lane.clientWidth / natural));\n\n      for (let c = 0; c < copies; c++) {                       /* copies AFTER  */\n        originals.map((n) => clone(n, 'tail')).forEach((n, i) => {\n          if (c === 0 && i === 0) n.setAttribute('data-loop-tail', '');\n          lane.appendChild(n);\n        });\n      }\n      for (let c = 0; c < copies; c++) {                       /* copies BEFORE */\n        originals.map((n) => clone(n, 'head'))\n          .reverse().forEach((n) => lane.insertBefore(n, lane.firstChild));\n      }\n      built = true;\n      measure();\n      rest = copies * setW;\n      /* PARK FURTHER IN WHEN ASKED. data-lane-loop-start is the index of the\n         real card the rail opens on (the perks band's phone swipe opens on\n         the exchange promise, which is its third block). The rest position\n         is still the first real card - recenter()'s window is [rest, rest +\n         setW] and a start inside the set is inside that window - so only\n         the initial jump moves; the seam maths is untouched. */\n      const startAt = Math.min(originals.length - 1, Math.max(0, parseInt(lane.dataset.laneLoopStart, 10) || 0));\n      offsets = originals.map((n) => n.offsetLeft - originals[0].offsetLeft);\n      jumpTo(rest + offsets[startAt]);                         /* rest on the real set */\n      askDots();\n    };\n\n    /* THE WINDOW IS AS WIDE AS THE COPIES ALLOW. With one copy a side it is\n       the real set, as it always was: leave it and jump one set back in. With\n       two copies (the small sets above) it is the real set AND the copy on\n       either side of it, so the seam is crossed one full set later and never\n       on the first swipe from wherever the rail was parked - the jump then\n       happens from a clone to its twin one set in, with a whole copy of slack\n       still beyond it. Either way the jump is one set-width, so nothing the\n       resize path relies on changes. */\n    const recenter = () => {\n      if (!built || jumping || !setW) return;\n      const lo = rest - (copies - 1) * setW;\n      const hi = rest + copies * setW;\n      if (lane.scrollLeft < lo - 1) jumpTo(lane.scrollLeft + setW);\n      else if (lane.scrollLeft > hi + 1) jumpTo(lane.scrollLeft - setW);\n    };\n\n    /* ---- A TOUCH GESTURE IS NEVER INTERRUPTED ----\n       (owner, 2026-09-07: \"carousels glitchy mobile, iPhone ... when I slide\n       between them on mobile they sometimes glitch, get stuck or snap\n       weirdly\".)\n\n       The seam jump used to run LIVE, from the scroll handler, the instant\n       the rail left the middle copy - and once more on every pointerup. On a\n       phone both land inside the gesture: pointerup fires as the finger\n       lifts, which is the moment the momentum starts, and the live handler\n       fires mid-momentum. iOS treats a scrollLeft set during a touch scroll\n       as a new scroll of its own. The momentum dies where it stood (stuck);\n       the mandatory snap re-engages from wherever that was and animates to\n       the nearest card, which is not the card the flick was going to (the\n       weird snap); and a finger still down has its next move measured from\n       a position the compositor no longer agrees with (the glitch). All of\n       it only near the seam, which is why it was \"sometimes\".\n\n       So from touchstart until the scroll that touch started has gone quiet,\n       the live handler stands aside and the seam is settled ONCE, after the\n       momentum has finished and the finger is up. The clones make that safe:\n       the window has a whole copy of slack either side, more than any one\n       flick on a mandatory-snap rail travels, so the rail's end is never\n       seen before the settle. A finger put down to stop a flick is a\n       touchstart, so it holds the jump too. The live jump stays for a mouse\n       wheel and the arrows, where it always worked; a mouse drag suppresses\n       it through is-dragging and settles on release, exactly as before.\n\n       The settle is scrollend where the engine has it, and a short quiet\n       timer everywhere; touchend arms the same timer so a lift with no\n       momentum settles as well. */\n    let touching = false, touchScroll = false, settleTimer = 0;\n    const QUIET = 160;\n    const settle = () => {\n      clearTimeout(settleTimer); settleTimer = 0;\n      if (!built || touching || lane.classList.contains('is-dragging')) return;\n      touchScroll = false;\n      recenter();\n    };\n    const armSettle = () => { clearTimeout(settleTimer); settleTimer = setTimeout(settle, QUIET); };\n    lane.addEventListener('touchstart', () => { touching = true; touchScroll = true; }, { passive: true });\n    const lift = () => { touching = false; armSettle(); };\n    lane.addEventListener('touchend', lift, { passive: true });\n    lane.addEventListener('touchcancel', lift, { passive: true });\n\n    lane.addEventListener('scroll', () => {\n      askDots();\n      armSettle();\n      if (!built || jumping || touchScroll || lane.classList.contains('is-dragging')) return;\n      recenter();\n    }, { passive: true });\n    lane.addEventListener('scrollend', settle);\n    /* drag suppresses the live handler; settle it once the grab releases.\n       Mouse only: on touch this fired as the momentum began (see above). */\n    lane.addEventListener('pointerup', (e) => {\n      if (built && e.pointerType !== 'touch') requestAnimationFrame(recenter);\n    });\n\n    /* A resize can cross the ladder's breakpoints, so it changes both the set\n       width AND how many copies the rail needs. Rebuilding from the originals\n       is the only honest answer; the position within the set is kept so the\n       door you were looking at is still the door you are looking at.\n\n       WIDTH ONLY, AND ONLY WHEN THE GEOMETRY MOVED. A phone's address bar\n       fires resize on every change of scroll direction with the width\n       unchanged, and this rebuild - clones out, clones in, a jump - ran\n       160ms after each of them, mid-swipe when the swipe had a little\n       vertical in it. A resize that leaves the set the same width and the\n       rail needing the same copies (a desktop drag inside one rung of the\n       ladder that happens to land where it started) has nothing to rebuild\n       either. */\n    onResizeSettled(() => {\n      if (built) {\n        const natural = naturalSet();\n        const need = Math.max(originals.length < 5 ? 2 : 1, Math.ceil(lane.clientWidth / natural));\n        if (Math.abs(natural - setW) < 0.5 && need === copies) return;\n      }\n      /* Held as a FRACTION of the set, not as pixels: the card widths change\n         across a breakpoint, so pixels would land somewhere else. */\n      const frac = built && setW ? (((lane.scrollLeft % setW) + setW) % setW) / setW : 0;\n      built = false;\n      build();\n      if (built && frac) jumpTo(rest + frac * setW);\n      askDots();\n    }, { widthOnly: true });\n\n    /* THE LANE MAY HAVE NO SIZE AT THE LOAD EVENT. The home page skips the\n       layout of everything from the fourth section down until it is near the\n       viewport (content-visibility: auto, theme.css), so a lane that far down\n       reports clientWidth 0 when the page loads and build() steps aside -\n       which is exactly what happened to the perks band when it stood third\n       (2026-09-04: \"mobile can't swipe\"). The section is laid out the moment\n       it comes near, the lane gets its width, and a ResizeObserver is what\n       hears that. A lane that has already built ignores the observer; the\n       resize path above still owns rebuilds. */\n    if ('ResizeObserver' in window) {\n      new ResizeObserver(() => { if (!built) build(); }).observe(lane);\n    }\n\n    if (document.readyState === 'complete') build();\n    else window.addEventListener('load', build, { once: true });\n  });\n}\n\n/* ---- Lane arrows (gap-aware step) ---- */\nfunction initLanes() {\n  document.addEventListener('click', (e) => {\n    const arrow = e.target.closest('[data-lane-arrow]');\n    if (!arrow) return;\n    const lane = arrow.closest('[data-lane-wrap]')?.querySelector('[data-lane]');\n    if (!lane) return;\n    lane.scrollBy({ left: arrow.dataset.laneArrow === 'prev' ? -laneStep(lane) : laneStep(lane),\n                    behavior: reduceMotion.matches ? 'auto' : 'smooth' });\n  });\n}\n\n/* ---- Lane auto-advance: DESKTOP ONLY (mouse present), gently advances\n   while the cursor is away and pauses the moment you hover or focus it.\n   Never runs on touch (phones) or under reduced motion. ---- */\nfunction initLaneAuto() {\n  if (reduceMotion.matches) return;\n  if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;\n  document.querySelectorAll('[data-lane-auto]').forEach((lane) => {\n    const isLoop = lane.hasAttribute('data-lane-loop');\n    let paused = false;\n    const tick = () => {\n      if (paused || document.hidden || lane.classList.contains('is-dragging')) return;\n      const max = lane.scrollWidth - lane.clientWidth;\n      if (max <= 4) return;                                  /* nothing to scroll */\n      /* Loop lanes just keep advancing - initLaneLoop wraps the seam.\n         Flat lanes rewind gently when they reach the end. */\n      if (!isLoop && lane.scrollLeft >= max - 4) lane.scrollTo({ left: 0, behavior: 'smooth' });\n      else lane.scrollBy({ left: laneStep(lane), behavior: 'smooth' });\n    };\n    setInterval(tick, 4000);\n    lane.addEventListener('pointerenter', () => { paused = true; });\n    lane.addEventListener('pointerleave', () => { paused = false; });\n    lane.addEventListener('focusin', () => { paused = true; });\n    lane.addEventListener('focusout', (e) => { if (!lane.contains(e.relatedTarget)) paused = false; });\n  });\n}\n\n/* ---- Lane drag-to-scroll: on desktop the mouse grabs and swipes the\n   lane like a touch surface. Touch already scrolls natively, so this is\n   mouse-only. A real drag suppresses the trailing click so you never\n   open a card (or add to cart) by dragging. ---- */\nfunction initLaneDrag() {\n  if (!finePointer.matches) return;\n  document.querySelectorAll('[data-lane]').forEach((lane) => {\n    let down = false, moved = false, startX = 0, startScroll = 0;\n    lane.addEventListener('pointerdown', (e) => {\n      if (e.pointerType === 'touch') return;               /* touch = native scroll */\n      down = true; moved = false; startX = e.clientX; startScroll = lane.scrollLeft;\n    });\n    lane.addEventListener('pointermove', (e) => {\n      if (!down) return;\n      const dx = e.clientX - startX;\n      if (!moved && Math.abs(dx) < 6) return;               /* threshold preserves clicks */\n      moved = true;\n      lane.classList.add('is-dragging');\n      lane.scrollLeft = startScroll - dx;\n    });\n    const release = () => {\n      if (!down) return;\n      down = false;\n      lane.classList.remove('is-dragging');                /* snap re-engages \u2192 settles */\n    };\n    lane.addEventListener('pointerup', release);\n    lane.addEventListener('pointercancel', release);\n    lane.addEventListener('pointerleave', release);\n    lane.addEventListener('dragstart', (e) => { if (down) e.preventDefault(); });\n    lane.addEventListener('click', (e) => {\n      if (moved) { e.preventDefault(); e.stopPropagation(); moved = false; }\n    }, true);\n  });\n}\n\n/* ---- initUgcVideo and initIgFeed are GONE (owner, 2026-08-19: \"The social\n   media, instragram can be completely remove, remove that integration and\n   cleanup\") ----\n\n   They drove the home page's community strip: an IntersectionObserver that\n   played and paused looping UGC clips, and a deferred fetch to the theme's own\n   proxy Worker that pulled the latest posts and built .ugc-tile tiles out of\n   them. The strip is deleted with its markup, its CSS, its section settings and\n   the three curated blocks in templates/index.json, so both controllers had no\n   DOM hook left to find - and a controller that can only ever no-op is a\n   controller nobody can tell is dead.\n\n   The proxy Worker in instagram-proxy/ went with them. Nothing in the theme\n   calls it now, so leaving it deployed would be an access token in production\n   serving a page that does not exist. Removed with `git rm`, so the Worker's\n   source and its README are recoverable from history if the feed is ever\n   wanted back - but the token in the Cloudflare dashboard is not, and should\n   be revoked there. */\n\n/* ---- Desktop nav dropdowns: CSS reveals; JS mirrors aria state ---- */\nfunction initNav() {\n  document.querySelectorAll('[data-nav-group]').forEach((group) => {\n    const link = group.querySelector('.site-nav__link');\n    if (!link) return;\n    link.setAttribute('aria-expanded', 'false');\n    const set = (open) => link.setAttribute('aria-expanded', open ? 'true' : 'false');\n    group.addEventListener('pointerenter', () => set(true));\n    group.addEventListener('pointerleave', () => set(false));\n    group.addEventListener('focusin', () => set(true));\n    group.addEventListener('focusout', (e) => {\n      if (!group.contains(e.relatedTarget)) set(false);\n    });\n    group.addEventListener('keydown', (e) => {\n      if (e.key === 'Escape') {\n        /* the panel is CSS :focus-within-driven - refocusing the link\n           alone keeps it open, so a close-lock class does the hiding */\n        set(false);\n        link.focus();\n        group.classList.add('is-closed');\n      }\n    });\n    group.addEventListener('pointerenter', () => group.classList.remove('is-closed'));\n    group.addEventListener('focusout', (e) => {\n      if (!group.contains(e.relatedTarget)) group.classList.remove('is-closed');\n    });\n  });\n}\n\n/* ---- Photo swap: the ONLY thing that changes a card's photograph on touch ----\n   Two dots on the card's top line (.shot-swap). A dot names a FRAME, not a\n   toggle: tapping the one you are already on does nothing, which is what an\n   indicator has to do or it stops being an indicator.\n\n   initScrollFeed is GONE (owner, 2026-08-12: \"on mobile do not automatically\n   swap images on scroll on cards or when clicked on, only when using the\n   switch\"). It was the M\u00B704 idea that a card centred in the viewport should\n   dress itself, and it had two costs on a real phone: photographs changed\n   under a shopper who had not asked for anything, and it fought this control -\n   the dots needed a `flipHold` flag purely so a manual choice would survive\n   until the card left the screen. With nothing else touching .show-alt, the\n   hold has no reason to exist either, so it went with it. The deck's `.is-lit`\n   and the card's `:focus-within` were the other two automatic triggers; both\n   were removed in CSS. See the reveal-trigger block in components.css. */\nfunction initShotSwap() {\n  document.addEventListener('click', (e) => {\n    const dot = e.target.closest('[data-swap-dot]');\n    if (!dot) return;\n    const card = dot.closest('[data-card]');\n    if (!card) return;\n    e.preventDefault();\n    showShot(card, dot.dataset.swapDot === '1');\n  });\n}\n\n/* ---- THE SCENE IS NOT ASKED FOR UNTIL SOMETHING WANTS IT (2026-09-12) ----\n   product-card now renders the styled reveal with its URL in data-src /\n   data-srcset instead of src / srcset, everywhere except main-collection's\n   leaf wall, where that frame is what a phone paints first. The long note\n   beside the markup has the measurement; the short version is that 511KB of\n   card photography was starting before the home page banner had finished, for\n   a frame a phone only ever shows on request, and the banner was down to\n   about a twelfth of the pipe because of it.\n\n   Moving the URL back onto the element is this one function, and everything\n   that could want the frame comes through it: showShot below (the dots, the\n   swipe, the leaf wall), initMiniSwap when a size change snapshots the frame,\n   the pointer and focus wake next to it, and a blanket pass at load.\n\n   sizes is left on the element by Liquid and stays AHEAD of srcset on\n   purpose: WebKit runs its candidate selection the moment srcset lands and\n   does not revisit the choice, so an iPhone handed srcset first would pick\n   against a default 100vw and buy the widest rung on the ladder.\n\n   The attribute is the flag rather than dataset.shotDefer, because an empty\n   data-* value reads back as '' and '' is falsy - the guard would never fire. */\nfunction hydrateShot(alt) {\n  if (!alt || !alt.hasAttribute('data-shot-defer')) return alt;\n  const set = alt.getAttribute('data-srcset');\n  const src = alt.getAttribute('data-src');\n  if (set) alt.setAttribute('srcset', set);\n  if (src) alt.setAttribute('src', src);\n  alt.removeAttribute('data-shot-defer');\n  alt.removeAttribute('data-srcset');\n  alt.removeAttribute('data-src');\n  return alt;\n}\n\n/* ---- AND FAILING ANY OF THAT, ONCE THE FIRST PICTURE HAS BEEN AND GONE ----\n   The load event is the same line the web banner's film draws, for the same\n   reason: before it these bytes are taken from the picture the shopper is\n   looking at, and after it there is nothing left to protect. loading=\"lazy\"\n   stays on every one of them, so a card far down the page still waits for the\n   scroll - this only hands back the URL. No requestIdleCallback: Safari did\n   not ship it until 17 and this has to behave the same on an iPhone. */\nfunction initShotDefer() {\n  /* Ask for the frame on the gesture BEFORE the one that commits to it, so a\n     reveal is never a fade to bare --ground. pointerover covers a desktop\n     hover - which is pure CSS (.dk-card:hover .shot--alt) and calls nothing,\n     so nothing else would ever have hydrated it - pointerdown covers a phone,\n     where iOS raises it a good 100-300ms before the click reaches a swap dot,\n     and focusin is the keyboard's read of the same hover rule. All three\n     bubble, so one delegated listener each and no capture. */\n  const wake = (e) => {\n    const t = e.target;\n    if (!t || !t.closest) return;\n    const card = t.closest('[data-card]');\n    if (!card) return;\n    hydrateShot(card.querySelector('[data-shot-defer]'));\n  };\n  document.addEventListener('pointerover', wake, { passive: true });\n  document.addEventListener('pointerdown', wake, { passive: true });\n  document.addEventListener('focusin', wake);\n\n  const all = () => document.querySelectorAll('[data-shot-defer]').forEach(hydrateShot);\n  if (document.readyState === 'complete') all();\n  else addEventListener('load', all, { once: true });\n}\n\n/* One place that paints a card's frame, so the dots and the swipe below can\n   never disagree about which shot is showing. */\nfunction showShot(card, styled) {\n  /* Before the class, not after: .show-alt takes the packshot to opacity 0,\n     so a frame revealed with no src behind it is a blank card. */\n  if (styled) hydrateShot(card.querySelector('[data-shot-defer]'));\n  card.classList.toggle('show-alt', styled);\n  card.querySelectorAll('[data-swap-dot]').forEach((d) => {\n    const mine = (d.dataset.swapDot === '1') === styled;\n    d.classList.toggle('is-active', mine);\n    d.setAttribute('aria-pressed', mine ? 'true' : 'false');\n  });\n}\n\n/* ---- BOTTLE OR LEAF: WHICH FRAME A COLLECTION CARD RESTS ON ----\n   (owner, 2026-08-26: \"Shop All / Collections show secondary images FIRST, Only\n   on mobile view (Single, secondary | Double, secondary, List always stays\n   bottle) - add a little switch for mobile to swap between styled and bottle\n   only top right (Bottle ico | leaf ico)\".)\n\n   THE MODE DOES NOT PAINT ANYTHING ITSELF. It decides which way round the two\n   frames a card already has should sit, and then says so in the ONE class every\n   other frame controller already reads: .show-alt. The dots, the swipe and the\n   data-dark identity flip all keep working because none of them learned a new\n   word - showShot above is still the only function that moves a frame.\n\n   The alternative was a CSS-only inversion hung off [data-shot=\"leaf\"], and it\n   was wrong in two ways that mattered: the swap dots underneath would have gone\n   on pointing at the bottle while the scene was showing, and a shopper on a\n   phone would have been the one person to see an indicator that lies about the\n   picture above it. This way a visit with no JavaScript is simply the page as\n   it was yesterday.\n\n   `shotModeLeaf` is module scope because initMiniSwap needs the same answer\n   when a size change hands a card its second frame back, and a card cannot be\n   asked to remember a preference that belongs to the grid. */\nlet shotModeLeaf = false;\n/* THE REMEMBERED LAYOUT, NAMED ONCE. initCollectionFilter writes this key and\n   initShotMode has to read it, because the two boot a tier apart and the photo\n   mode's first decision cannot wait for the second one. Two string literals\n   would have drifted the first time either moved. */\nconst CF_VIEW_KEY = 'juce:cf-view:v1';\nconst cardRestsOnScene = (card) =>\n  shotModeLeaf && !card.classList.contains('is-oneframe') && !!card.closest('.cx[data-shot]');\n\nfunction initShotMode() {\n  const root = document.querySelector('.cx[data-shot]');\n  if (!root) return;\n  /* ---- .is-shotlive HANDS THE FIRST FRAME BACK (2026-09-10, audit T5) ----\n     Until this function has decided, components.css paints a phone's leaf\n     default itself - the scene up, the bottle down - so the wall's first frame\n     is already the one this would have set, instead of bottles that swap to\n     scenes a beat after theme.js arrives. From here on .show-alt is the only\n     truth again, so every way out of this function says so: the first paint()\n     pass below adds the class in the same beat as the real .show-alt, and the\n     early returns add it so a grid this function stands down on falls back to\n     the bottle, which is what it has always shown. */\n  const btns = Array.from(root.querySelectorAll('[data-cf-shot]'));\n  if (!btns.length) { root.classList.add('is-shotlive'); return; }\n\n  const KEY = 'juce:cf-shot:v1';\n  const MODES = ['bottle', 'leaf'];\n  /* The same query .cx-shot is shown under, so there is no width at which the\n     control is on screen and the mode it sets is inert, or the reverse. */\n  const narrow = matchMedia('(max-width: 1023.98px)');\n\n  /* Server default first, the shopper's own answer over it - exactly how the\n     layout switcher restores data-view further down this file. */\n  let mode = root.dataset.shot === 'bottle' ? 'bottle' : 'leaf';\n  try {\n    const saved = localStorage.getItem(KEY);\n    if (MODES.indexOf(saved) !== -1) mode = saved;\n  } catch (e) { /* private mode */ }\n\n  /* ---- WHICH LAYOUT ARE WE ACTUALLY IN, RIGHT NOW ----\n     LIST IS ALWAYS THE BOTTLE (owner: \"List always stays bottle\") and desktop\n     is untouched, so the mode is inert in both - which makes this the question\n     everything below turns on.\n\n     The DOM cannot answer it yet. main-collection server-renders\n     data-view=\"single\" and initCollectionFilter restores the shopper's real\n     choice a tier later, so at this moment a page that is about to be a LIST\n     still says single. Reading the DOM alone would have dressed forty-one\n     posters in their scenes and then undressed them a beat later, and worse,\n     handed the priority hint below to an image that list view hides.\n\n     So the remembered value wins until something actually writes the\n     attribute - initCollectionFilter's restore, or a shopper pressing a layout\n     button - and from that moment the DOM is the only truth again. */\n  let bootView = '';\n  try { bootView = localStorage.getItem(CF_VIEW_KEY) || ''; } catch (e) { /* private mode */ }\n  const viewNow = () => bootView || root.dataset.view;\n  const live = () => narrow.matches && viewNow() !== 'list';\n\n  const sync = () => {\n    root.dataset.shot = mode;\n    btns.forEach((b) => b.setAttribute('aria-pressed', b.dataset.cfShot === mode ? 'true' : 'false'));\n  };\n\n  /* NOTHING TO SWITCH BETWEEN, SO THERE IS NO SWITCH. /pages/minis renders\n     every card as a mini and a mini has one frame (see product-card), and a\n     collection could be shot that thinly one day. A control that presses and\n     changes nothing is the same fault this file argues about in list view, so\n     it goes the same way. `hidden` rather than a class: base.css forces\n     [hidden] to display:none unlayered, which beats the media query that shows\n     the group. sync() runs first so the attribute and the buttons still agree\n     with each other and with the stored preference, hidden or not. */\n  if (!root.querySelector('.dk-card .shot--alt')) {\n    sync();\n    const group = root.querySelector('.cx-shot');\n    if (group) group.hidden = true;\n    root.classList.add('is-shotlive');\n    return;\n  }\n\n  let painted = null;\n  const paint = () => {\n    const leaf = mode === 'leaf' && live();\n    shotModeLeaf = leaf;\n    if (painted === leaf) return;\n    /* The first pass is the one that arrives just after the HTML has painted,\n       so it is the only one that must not be seen as a crossfade - forty-two\n       posters dissolving at once on a page nobody has touched. Later passes are\n       a shopper pressing something and SHOULD animate. */\n    const first = painted === null;\n    if (first) root.classList.add('is-shotsync');\n    root.querySelectorAll('[data-card]').forEach((card) => {\n      /* A card with no second frame - Mystic Haze, a card set to the Bundle,\n         anything set to the 10ml - is pinned to false. Leaving .show-alt on it\n         would blank the poster: that class takes the packshot to opacity 0 and\n         there is nothing behind it. */\n      const two = !!card.querySelector('.shot--alt') && !card.classList.contains('is-oneframe');\n      showShot(card, two && leaf);\n    });\n    painted = leaf;\n    /* Same task as the .show-alt above and still inside .is-shotsync, so the\n       CSS first frame and the class-driven one meet in one recalc - identical\n       values on a leaf phone, and no transition on a Bottle or desktop one. */\n    if (first) root.classList.add('is-shotlive');\n    if (first) requestAnimationFrame(() => root.classList.remove('is-shotsync'));\n  };\n\n  /* ---- THE EAGER IMAGE HAS TO BE THE ONE ON SCREEN ----\n     main-collection hands exactly one card `priority`, and product-card turns\n     that into loading=\"eager\" fetchpriority=\"high\" on the PACKSHOT - the LCP\n     candidate on a phone, where the grid is one card wide. In leaf mode the\n     packshot is the frame at opacity 0 and the scene is what the shopper is\n     looking at, so the hint has to move with it or the largest paint on the\n     page is the one image on it that was told to wait.\n\n     NO THIRD IMAGE AND NO EXTRA REQUEST: both frames are already in this\n     markup, and the reveal on the first card is inside the viewport, so the\n     lazy loader was going to fetch it in the same beat either way. All that\n     changes is the order the two are asked for. Once, at boot - by the time\n     anybody presses the switch both have long since landed. */\n  const hint = () => {\n    if (mode !== 'leaf' || !live()) return;\n    const lead = root.querySelector('.dk-card .shot[fetchpriority=\"high\"]');\n    const card = lead && lead.closest('[data-card]');\n    const alt = card && card.querySelector('.shot--alt');\n    if (!alt || card.classList.contains('is-oneframe')) return;\n    alt.setAttribute('loading', 'eager');\n    alt.setAttribute('fetchpriority', 'high');\n    lead.setAttribute('fetchpriority', 'auto');\n  };\n\n  btns.forEach((b) => b.addEventListener('click', () => {\n    if (MODES.indexOf(b.dataset.cfShot) === -1) return;\n    mode = b.dataset.cfShot;\n    try { localStorage.setItem(KEY, mode); } catch (e) { /* private mode */ }\n    sync();\n    paint();\n  }));\n\n  narrow.addEventListener('change', paint);\n  /* attributeFilter, so this fires on a view change and on nothing else - the\n     grid mutates on every filter pass, and sync() writes data-shot on this very\n     element. Same guard, same reason, as the one in initDockCommerce. */\n  if ('MutationObserver' in window) {\n    new MutationObserver(() => { bootView = ''; paint(); })\n      .observe(root, { attributes: true, attributeFilter: ['data-view'] });\n  }\n\n  sync();\n  hint();\n  paint();\n}\n\n/* ---- SWIPE THE POSTER TO CHANGE THE FRAME (owner, 2026-08-14: \"on the cards\n   when not in a carousel, we want to be able to swipe to go to the next image\")\n\n   The dots were the only way to change a card's photograph on touch, and they\n   are 7px targets asking to be found. A card with two frames should behave like\n   every other two-frame surface on a phone: you drag it.\n\n   NOT IN A LANE, which is the owner's own condition and also the only one that\n   matters technically - a .lane is a horizontal scroller, so a horizontal drag\n   inside one is already spoken for. Binding here would fight the scroll and\n   lose, or worse, win. The deck (.lane--deck) is a lane too, so it is covered\n   by the same test.\n\n   THE HARD PART IS THE LINK. .shot-wrap is an <a> to the product, so a swipe\n   that ends over the photograph is also a tap on the anchor - the shopper would\n   change the frame and immediately be navigated away from it. So a completed\n   swipe stamps the card, and one capture-phase listener swallows a click on a\n   card stamped within the last GUARD ms.\n\n   The stamp EXPIRES rather than being consumed. An \"eat the next click\" flag is\n   the obvious version and it is wrong: browsers do not reliably fire a click\n   after a long drag, so on the runs where none arrives the flag survives and\n   eats a real tap later - a card that silently refuses to open, once, minutes\n   after the gesture that broke it. A timestamp cannot outlive the gesture.\n\n   A tap that never moved is not a swipe, stamps nothing, and goes to the link.\n\n   Pointer events, not touchstart: the same code then serves a trackpad drag and\n   a stylus, and setPointerCapture keeps the gesture on the element the drag\n   started on even when the finger leaves it. Listeners are passive - the\n   gesture is read, never preventDefault'd, so a vertical scroll that begins on\n   a poster scrolls the page at full speed and is simply not counted. */\n/* ---- IT FOLLOWS THE FINGER NOW (owner, 2026-08-25: \"on mobile, must be able\n   to swipe between the card images\", over a screenshot of a card showing TWO\n   DOTS - so the second frame was there and the gesture was not) ----\n\n   The controller below already flipped the frame on a completed 40px drag, and\n   a synthetic drag on the live page proved it: show-alt went false -> true.\n   What was missing was every part of the gesture a shopper can perceive.\n   Nothing moved while the finger was down, so a drag that was 39px did nothing\n   and a drag that was 41px produced a crossfade half a second after the thumb\n   had already left - which is indistinguishable, from the shopper's side, from\n   a card that does not swipe.\n\n   Three changes:\n\n   1. THE DRAG IS DRAWN. Once the gesture is decided horizontal the card gets\n      .is-swiping, and two custom properties are written on it every move:\n      --shot-mix (0..1, how far toward the other frame) drives the crossfade,\n      and --shot-dx moves the packshot with the finger. The photograph changes\n      UNDER the thumb; letting go past the threshold simply keeps what is\n      already on screen.\n   2. ONLY THE PACKSHOT TRAVELS. See the CSS note - the styled scene is padded\n      to exactly the card ratio and wears scale(1.02), so it has about 1% of\n      overhang to spare and would show bare --ground down one edge if it moved.\n      The packshot is a square photograph in a 5:8 box, so it has ~30% of crop\n      in hand and can move as far as this ever needs.\n   3. THE NATIVE DRAG IS CALLED OFF. .shot-wrap is an <a> around an <img> and\n      both are draggable by default, so a mouse or trackpad drag started a link\n      drag and the browser fired pointercancel a few frames in - the same class\n      of failure that touch-action: pan-y fixed for touch. A drag on a poster\n      is now ours in both input models.\n\n   Still passive listeners: the gesture is read, never preventDefault'd, so a\n   vertical scroll that starts on a poster scrolls at full speed and is simply\n   not counted. touch-action: pan-y (components.css) is what claims the\n   horizontal axis; nothing here fights the scroller.\n\n   THE HARD PART IS STILL THE LINK. .shot-wrap goes to the product, so a swipe\n   that ends over the photograph is also a tap on the anchor. A completed swipe\n   stamps the card and one capture-phase listener swallows a click on a card\n   stamped within the last GUARD ms. The stamp EXPIRES rather than being\n   consumed: browsers do not reliably fire a click after a long drag, and an\n   \"eat the next click\" flag would survive the runs where none arrives and then\n   eat a real tap minutes later. A tap that never moved stamps nothing and goes\n   to the link. */\nfunction initShotSwipe() {\n  const DIST = 40;        /* px before a drag counts as a swipe at all      */\n  const SLOPE = 1.4;      /* how much more horizontal than vertical it must be */\n  const GUARD = 500;      /* ms a completed swipe suppresses the card's link */\n  const WAKE = 6;         /* px before the gesture commits to an axis       */\n  const PULL = 0.45;      /* how much of the finger the packshot follows    */\n  const CAP = 28;         /* px ceiling on that travel - see note 2 above   */\n  let swipedCard = null, swipedAt = 0;\n\n  document.addEventListener('click', (e) => {\n    if (!swipedCard) return;\n    if (Date.now() - swipedAt > GUARD) { swipedCard = null; return; }\n    if (!swipedCard.contains(e.target)) return;\n    swipedCard = null;\n    e.preventDefault();\n    e.stopPropagation();\n  }, true);\n\n  /* The browser's own drag, called off before it can cancel ours. Document\n     level and capture-free: a poster is the only thing in the theme that wants\n     this, and it wants it on every surface a card appears on. */\n  document.addEventListener('dragstart', (e) => {\n    const t = e.target;\n    if (t && t.closest && t.closest('.dk-card .shot-wrap')) e.preventDefault();\n  });\n\n  document.addEventListener('pointerdown', (e) => {\n    if (e.pointerType === 'mouse' && e.button !== 0) return;\n    const wrap = e.target.closest('.shot-wrap');\n    if (!wrap) return;\n    const card = wrap.closest('[data-card]');\n    /* Two frames or nothing to swipe to; a lane owns horizontal drags itself.\n       .is-oneframe is the second half of \"two frames\": the element is still in\n       the card, hidden, because the size showing right now was never shot (the\n       10ml, every Bundle). Without this test a drag would crossfade to a frame\n       that is display:none and leave the poster empty under the thumb - and it\n       would be doing it on a card whose touch-action rule has already handed\n       the horizontal axis back to the browser. */\n    if (!card || !card.querySelector('.shot--alt') || card.classList.contains('is-oneframe') ||\n        card.closest('.lane')) return;\n\n    const startX = e.clientX, startY = e.clientY;\n    let live = false, dead = false;\n\n    const paint = (dx) => {\n      const mix = Math.min(1, Math.abs(dx) / DIST);\n      /* Reduced motion keeps the crossfade - which is the information - and\n         drops the travel, which is the decoration (05 section 6). */\n      const off = reduceMotion.matches ? 0 : Math.max(-CAP, Math.min(CAP, dx * PULL));\n      card.style.setProperty('--shot-mix', mix.toFixed(3));\n      card.style.setProperty('--shot-dx', off.toFixed(1) + 'px');\n    };\n    /* Hands the frame back to the classes. Whatever is on screen at this\n       moment is what the class state paints, so there is no flash: the\n       properties are removed, the transitions come back with .is-swiping, and\n       an uncommitted drag settles home over --dur-base. */\n    const rest = () => {\n      card.classList.remove('is-swiping');\n      card.style.removeProperty('--shot-mix');\n      card.style.removeProperty('--shot-dx');\n    };\n\n    const detach = (ev) => {\n      wrap.removeEventListener('pointermove', move);\n      wrap.removeEventListener('pointerup', end);\n      wrap.removeEventListener('pointercancel', cancel);\n      try { wrap.releasePointerCapture(ev.pointerId); } catch (err) { /* already gone */ }\n    };\n    const move = (ev) => {\n      if (dead) return;\n      const dx = ev.clientX - startX, dy = ev.clientY - startY;\n      if (!live) {\n        if (Math.abs(dx) < WAKE && Math.abs(dy) < WAKE) return;\n        /* The axis is decided ONCE, on the first real movement. A gesture that\n           starts vertical is the page scrolling and must never repaint a card\n           halfway through. */\n        if (Math.abs(dx) < Math.abs(dy) * SLOPE) { dead = true; return; }\n        live = true;\n        card.classList.add('is-swiping');\n      }\n      paint(dx);\n    };\n    const end = (ev) => {\n      detach(ev);\n      if (!live) return;\n      /* THE STAMP IS FOR THE GESTURE, NOT FOR THE COMMIT. A drag of 22px is an\n         abandoned swipe, not a tap, and the browser will still fire a click on\n         the anchor underneath it - so stamping only on a completed swipe would\n         send a shopper to the product page every time they changed their mind\n         halfway. `live` is only true once the gesture has been decided\n         horizontal, so a tap and a vertical scroll are untouched. */\n      swipedCard = card; swipedAt = Date.now();   /* suppress the link, briefly */\n      const dx = ev.clientX - startX;\n      if (Math.abs(dx) >= DIST) {\n        /* Either direction moves to the OTHER frame. With exactly two there is\n           no \"next\" that differs from \"previous\", and pretending otherwise\n           would make one of the two swipes do nothing. */\n        showShot(card, !card.classList.contains('show-alt'));\n      }\n      rest();\n    };\n    const cancel = (ev) => { detach(ev); rest(); };\n\n    try { wrap.setPointerCapture(e.pointerId); } catch (err) { /* fine without */ }\n    wrap.addEventListener('pointermove', move, { passive: true });\n    wrap.addEventListener('pointerup', end, { passive: true });\n    wrap.addEventListener('pointercancel', cancel, { passive: true });\n  }, { passive: true });\n}\n\n/* ---- Segmented jelly switch: radios + FLIP transform thumb ---- */\nfunction initSeg() {\n  document.querySelectorAll('[data-seg]').forEach((seg) => {\n    const thumb = seg.querySelector('[data-seg-thumb]');\n    const inputs = [...seg.querySelectorAll('input[type=\"radio\"]')];\n    if (!thumb || !inputs.length) return;\n    const place = (opt) => {\n      thumb.style.left = opt.offsetLeft + 'px';\n      thumb.style.width = opt.offsetWidth + 'px';\n    };\n    const flipTo = (opt) => {\n      const first = thumb.getBoundingClientRect();\n      place(opt);\n      if (reduceMotion.matches) return;\n      const last = thumb.getBoundingClientRect();\n      const dx = first.left - last.left;\n      const sx = last.width ? first.width / last.width : 1;\n      thumb.classList.remove('is-moving');\n      thumb.style.transform = `translateX(${dx}px) scaleX(${sx})`;\n      void thumb.offsetWidth;\n      thumb.classList.add('is-moving');\n      thumb.style.transform = '';\n      thumb.classList.remove('is-squish');\n      void thumb.offsetWidth;\n      thumb.classList.add('is-squish');\n    };\n    const checked = inputs.find((i) => i.checked) || inputs[0];\n    if (checked) place(checked.closest('label'));\n    seg.addEventListener('change', (e) => {\n      const label = e.target.closest('label');\n      if (label) flipTo(label);\n    });\n  });\n}\n\n/* ---- PDP variant switch: price, Payflex quarter, island mirror ---- */\nfunction initPdpVariant() {\n  const form = document.querySelector('[data-pdp-form]');\n  if (!form) return;\n  const update = () => {\n    const picked = form.querySelector('input[name=\"id\"]:checked');\n    if (!picked) return;\n    const label = picked.dataset.priceLabel || '';\n    const compare = picked.dataset.compareLabel || '';\n    /* Both PDP prices - the title row and the sticky buy bar - and both their\n       strikethroughs. Painted together so the bar can never quote the previous\n       size's number after a switch. */\n    document.querySelectorAll('[data-pdp-price]').forEach((el) => paintPrice(el, label, compare));\n    /* WHAT SHIPS IN THE BOX DEPENDS ON THE SIZE (owner, 2026-08-18): the free\n       3ml rides with the 50ml and with the bundle, never with the 10ml, and the\n       10ml cannot come back once it is opened. All three sentences are\n       server-rendered in the assurance strip; this shows the one belonging to\n       the size that is actually selected. data-kind comes off the radio\n       (size-options.liquid) and is derived from the same test as the form's\n       data-tester-ids, so the copy and the cart can never disagree.\n\n       It replaced [data-bundle-note], the caption under the old seg - the\n       picker's bundle tile carries the composition and the saving itself. */\n    const kind = picked.dataset.kind || 'full';\n    document.querySelectorAll('[data-gift-for]').forEach((el) => {\n      el.hidden = el.dataset.giftFor !== kind;\n    });\n    const cents = parseInt(picked.dataset.priceCents, 10);\n    const pf = document.querySelector('[data-payflex-quarter]');\n    if (pf && !Number.isNaN(cents)) {\n      const q = Math.floor(cents / 4) / 100;\n      /* NBSP, matching snippets/price.liquid - a normal space here let \"R\" wrap\n         off its own number at the narrow end of the assurance grid. */\n      pf.textContent = 'R\u00A0' + (Number.isInteger(q) ? q : q.toFixed(2));\n    }\n  };\n  form.addEventListener('change', update);\n  update();\n}\n\n/* ---- PDP hero: a top-right switch swaps the product shot for the styled\n   shot and HOLDS there. Manual only - no hover reveal, no auto timer: the\n   image never moves on its own, and a toggle stays put until toggled back.\n   No-ops with a single shot. The switch thumb slides under the active label. */\n/* ---- ONE LIGHT, TWO BOTTLES ----\n   (owner, 2026-08-24: \"an artificial light that animates and moves based on\n   mouse that is cohesive as 2 items\".)\n\n   The cut-out PNGs carry no shadow - deliberately, so they can overlap without\n   dragging a studio bed with them - which leaves them floating on the plate.\n   This lends the pair a light: ONE source at the pointer, and every [data-lit]\n   bottle casts from its OWN centre relative to it. The two shadows therefore\n   swing in opposite directions as the pointer crosses between them, which is\n   the whole trick - two shadows that agree about where the light is read as two\n   objects in one scene; two identical shadows read as two stickers.\n\n   The bottle nearest the light also lifts slightly (brightness), which is the\n   cheapest possible specular cue and costs no extra render surface.\n\n   WHY IT IS SAFE HERE, when this theme has a standing rule against filters:\n     \u00B7 POINTER, never scroll. The lag this repo paid for came from driving blur\n       off a scroll timeline; this only moves while a pointer is inside one band.\n     \u00B7 hover+fine only. A phone never runs any of it and gets the static shadow.\n     \u00B7 rAF-coalesced. pointermove fires far faster than the compositor cares\n       about, so the handler only stores coordinates and one frame does the work.\n     \u00B7 it lets go. On pointerleave the light eases back to its resting place, so\n       nothing is left pinned mid-transform.\n*/\n/* ---- AND IT BREATHES WHEN NOBODY IS TOUCHING IT, 2026-08-25 ----\n   (owner, on the layering room: \"the shadow and lighting system: more subtle,\n   more sophisticated, and we want MOVEMENT\".)\n\n   The pointer light above only ever moved while a mouse was inside the band,\n   which means it never moved at all on a phone and sat perfectly still on a\n   desktop nobody had touched yet - i.e. in both of the states a shopper\n   actually arrives in. A host may now opt IN to an idle orbit: the same one\n   light, walking a slow Lissajous around its resting place, writing the same\n   five custom properties.\n\n   OPT-IN PER HOST, and the attribute is the whole safety argument. This\n   function is shared with the PDP layering band, which lives on a page that\n   SCROLLS - and a permanent loop touching a filter on a scrolling page is\n   precisely the bargain this theme has already paid for once (see\n   juce-scroll-performance). Only the duet's stage and cover carry\n   data-lit-idle, and that room cannot scroll: html:has(.dt){overflow:hidden}.\n\n   What keeps it cheap: a 72ms interval, not a rAF loop, so the main thread\n   gets to idle between ticks; the write itself is rAF-coalesced through the\n   same queue() the pointer uses; it does nothing while the tab is hidden,\n   while a pointer is inside, while the panel it belongs to is not .is-on, or\n   once its host has left the DOM. Two elements, fourteen writes a second.\n   Cut entirely under prefers-reduced-motion, at the top of the function. */\nfunction armPairLight(pair) {\n  /* the two globals this file already keeps, rather than a third matchMedia */\n  if (!pair || reduceMotion.matches) return;\n  const wantsIdle = pair.hasAttribute('data-lit-idle');\n  /* a coarse pointer still gets the light IF the host asked for the idle\n     orbit - that is the whole point of it - but never the pointer listeners */\n  if (!finePointer.matches && !wantsIdle) return;\n  if (pair.dataset.litArmed) return;            /* a rebuilt stage must not stack listeners */\n  pair.dataset.litArmed = '1';\n  [pair].forEach((pair) => {\n    const lits = [...pair.querySelectorAll('[data-lit]')];\n    if (!lits.length) return;\n    let px = 0.5, py = 0.25, raf = 0, inside = false;\n\n    /* READ EVERY BOX, THEN WRITE (2026-09-22, lane 8). This measured one\n       bottle, wrote its five properties, then measured the next, so the\n       second read forced the style the first write had just dirtied - on\n       every tick of the orbit. The rects are taken together first. */\n    const paint = () => {\n      raf = 0;\n      const box = pair.getBoundingClientRect();\n      if (!box.width) return;\n      const rects = lits.map((el) => el.getBoundingClientRect());\n      lits.forEach((el, li) => {\n        const r = rects[li];\n        /* the bottle's centre and the light, both in the PAIR's own box, so the\n           two bottles are measured against one coordinate space */\n        const cx = (r.left - box.left + r.width / 2) / box.width;\n        const cy = (r.top - box.top + r.height / 2) / box.height;\n        let dx = cx - px, dy = cy - py;              /* light -> bottle */\n        const d = Math.hypot(dx, dy) || 1e-4;\n        /* normalise, then let distance lengthen and soften the cast the way a\n           real lamp does: further from the light, longer and blurrier.\n\n           RETUNED 2026-08-25, subtler on all four terms (owner: \"more subtle,\n           more sophisticated\"). The throw came down (10+30r -> 6+18r), the\n           penumbra went up (16+26r -> 20+26r) and the ink came down\n           (.24-.10r -> .175-.07r): the same move as walking a hard desk lamp\n           back and putting a softbox on it. The specular lift halved too -\n           5% brightness on a glass bottle read as a highlight switching on. */\n        const reach = Math.min(1, d * 1.6);\n        const throwLen = 6 + reach * 18;\n        const sx = (dx / d) * throwLen;\n        const sy = (dy / d) * throwLen + 4;          /* +4: the light sits above */\n        el.style.setProperty('--sx', sx.toFixed(1) + 'px');\n        el.style.setProperty('--sy', sy.toFixed(1) + 'px');\n        el.style.setProperty('--sb', (20 + reach * 26).toFixed(1) + 'px');\n        el.style.setProperty('--sa', (0.175 - reach * 0.07).toFixed(3));\n        el.style.setProperty('--lit', (1 + (1 - Math.min(1, d * 2.2)) * 0.03).toFixed(3));\n      });\n    };\n    const queue = () => { if (!raf) raf = requestAnimationFrame(paint); };\n\n    /* where the orbit has to pick the light up from when a pointer lets go */\n    let handOff = null, handOffAt = 0;\n\n    if (finePointer.matches) {\n      pair.addEventListener('pointermove', (e) => {\n        const box = pair.getBoundingClientRect();\n        px = (e.clientX - box.left) / box.width;\n        py = (e.clientY - box.top) / box.height;\n        inside = true;\n        handOff = null;\n        queue();\n      }, { passive: true });\n\n      pair.addEventListener('pointerleave', () => {\n        inside = false;\n        if (wantsIdle) {\n          /* hand the light to the orbit rather than easing it to a rest it is\n             about to leave again - the blend below starts from here */\n          handOff = [px, py];\n          handOffAt = performance.now();\n          return;\n        }\n        /* ease home rather than snapping - a shadow that teleports on mouse-out\n           is more distracting than the effect is worth */\n        const t0 = performance.now(), sx = px, sy = py;\n        const home = (now) => {\n          const k = Math.min(1, (now - t0) / 420);\n          const e2 = 1 - Math.pow(1 - k, 3);\n          px = sx + (0.5 - sx) * e2;\n          py = sy + (0.25 - sy) * e2;\n          paint();\n          if (k < 1 && !inside) requestAnimationFrame(home);\n        };\n        requestAnimationFrame(home);\n      }, { passive: true });\n    }\n\n    /* THE IDLE ORBIT IS FOR A MOUSE'S ABSENCE, NOT FOR A PHONE (2026-09-22,\n       lane 8). It ticked every 72ms, forever, and each tick rewrote five\n       custom properties that feed `filter: drop-shadow() brightness()` on two\n       720px cut-outs - so a phone re-rasterised both filtered bottles\n       fourteen times a second while the shopper was only reading. Measured on\n       a 4x throttled phone, that was the Duet's cover and reveal never going\n       idle. A touch screen now gets the light placed once, at its resting\n       point above the pair; the pointer and the orbit stay for a mouse. */\n    if (wantsIdle && finePointer.matches) {\n      const born = performance.now();\n      const tick = window.setInterval(() => {\n        if (!pair.isConnected) { window.clearInterval(tick); return; }\n        if (inside || document.hidden) return;\n        /* a panel that has been pushed off screen still has layout, so the\n           class is the honest test of whether anyone can see this */\n        const panel = pair.closest('.dt-panel');\n        if (panel && !panel.classList.contains('is-on')) return;\n        const now = performance.now();\n        const t = (now - born) / 1000;\n        /* two periods that do not divide each other, so the light never\n           retraces its own path inside a shopper's visit */\n        const ox = 0.5 + Math.cos(t * 0.36) * 0.17;\n        const oy = 0.25 + Math.sin(t * 0.23) * 0.11;\n        if (handOff) {\n          const k = Math.min(1, (now - handOffAt) / 900);\n          const e2 = 1 - Math.pow(1 - k, 3);\n          px = handOff[0] + (ox - handOff[0]) * e2;\n          py = handOff[1] + (oy - handOff[1]) * e2;\n          if (k >= 1) handOff = null;\n        } else {\n          px = ox; py = oy;\n        }\n        queue();\n      }, 72);\n    }\n\n    paint();\n  });\n}\n\n/* Boot pass for the containers Liquid rendered. Anything built later - the\n   duet's stage is assembled by JS after a pairing is chosen - calls\n   armPairLight itself once its images are in. */\nfunction initPairLight() {\n  document.querySelectorAll('[data-pair-light]').forEach(armPairLight);\n}\n\nfunction initPdpHero() {\n  const stage = document.querySelector('[data-pdp-hero]');\n  if (!stage) return;\n  /* THE VARIANT LAYER IS NOT A SLIDE. It is an overlay painted on top by\n     paintHero whenever the picked size is not the one on the base shot, and it\n     is driven by .is-variant on the stage, not by .is-active. Including it here\n     put THREE shots behind TWO dots, so `show(1)` activated the variant layer\n     and the styled shot - the thing the second dot is for - could not be\n     reached at all. */\n  const shots = [...stage.querySelectorAll('.pdp__shot:not([data-pdp-variant])')];\n  if (shots.length < 2) return;                     /* nothing to shift to */\n  const dots = [...stage.querySelectorAll('[data-hero-dot]')];\n\n  /* Which frame the stage RESTS on while a hover borrows it - see the peek\n     block at the foot of this function. -1 means nothing is on loan. */\n  let peekFrom = -1;\n\n  /* The switch is two thumbnails of the shots themselves, so the active state\n     is a ring in CSS - no sliding jelly thumb to measure and place, and no\n     resize listener to keep it honest. */\n  const show = (n) => {\n    const i = (n + shots.length) % shots.length;\n    const was = shots.findIndex((s) => s.classList.contains('is-active'));\n    shots.forEach((s, k) => s.classList.toggle('is-active', k === i));\n    dots.forEach((d, k) => {\n      const on = k === i;\n      d.classList.toggle('is-active', on);\n      d.setAttribute('aria-pressed', on ? 'true' : 'false');\n    });\n    /* ---- A CROSSFADE NEEDS BOTH LAYERS PAINTED, WHICH IS WHAT .is-settling IS\n       FOR ---- (2026-09-04, measured: the frame that is leaving vanishes on the\n       spot and the one arriving comes up out of the stage's white ground.)\n\n       The idle rule pairs opacity:0 with content-visibility:hidden, and\n       content-visibility is not animatable - so the moment .is-active moves,\n       the old frame stops being painted while its opacity is still 1. The\n       swipe already met this and answered it with a window that keeps both\n       layers rendered for the length of the fade; every other way of changing\n       frame was fading through white instead, which nobody had said out loud\n       because a press is once and the two packshots are both on the same near\n       white. Under a HOVER it is constant, and against the styled scene - a\n       darker, busier picture - it reads as a flash.\n\n       So the window moves into show(), where every route to a new frame goes\n       through it: the dots, the drag's commit, the size reset and the peek.\n       One rule, one place, and the swipe's own rest() still owns the class\n       while a finger is down (hence the is-swiping guard, or the two would\n       fight over the same timer).\n\n       ONLY WHEN THE FRAME ACTUALLY CHANGES, and never on the opening call:\n       .is-settling makes the styled layer content-visibility:visible, which is\n       precisely the deferral that keeps a second 1600px photograph off the\n       critical path on a phone. A frame that is already up has nothing to\n       settle, so the guard is not an optimisation - it is what keeps that\n       promise. */\n    if (was > -1 && was !== i && !stage.classList.contains('is-swiping')) {\n      stage.classList.add('is-settling');\n      clearTimeout(settleTimer);\n      settleTimer = setTimeout(() => stage.classList.remove('is-settling'), SETTLE);\n    }\n  };\n\n  /* A press is a DELIBERATE choice of frame, so it ends any hover loan: the\n     frame it lands on is the stage's new resting one and the cursor leaving\n     must not undo it. This is what keeps \"a click swaps the shot and HOLDS\n     there\" true now that the mouse can move the frame too. */\n  dots.forEach((d, k) => d.addEventListener('click', () => { peekFrom = -1; show(k); }));\n\n  /* ---- THE DRAG BELONGS TO THE PHOTOGRAPH, NOT TO THE CATALOGUE ----\n     (owner, 2026-09-03: \"Product page when swipe it swipes to next product\n     instead of image\".)\n\n     initPdpSwipe used to read a 48px drag on .pdp__media and set location.href\n     to the rel=next neighbour. That is what the owner asked for on 2026-08-19\n     and it is not what a photograph with two frames and a pager under it can\n     mean to a thumb: a surface that carries an indicator is a carousel, and\n     dragging a carousel moves the carousel. So the drag moves the FRAME now,\n     and the neighbours stay what they always were underneath - two\n     visually-hidden rel=prev/next links, real navigation for a screen reader\n     and for search, reached by nothing a thumb can do by accident.\n\n     Two faults died with that function, and both were invisible in the code\n     and loud on a phone:\n\n       \u00B7 its move/up listeners were attached to .pdp__media on every\n         pointerdown and only removed by a pointerup ON .pdp__media. A finger\n         that left the photograph - which a swipe does, and which a scroll does\n         constantly - left the move handler attached with a STALE startX. They\n         stacked up, so a later small movement could clear 48px against an\n         origin from three gestures ago and navigate the page on its own.\n       \u00B7 no pointer capture, so the second half of a real swipe was not heard\n         at all once the finger crossed the edge of the image.\n\n     It reads like the card's swipe (initShotSwipe) because it is the same\n     gesture on the same kind of surface: WAKE before an axis is chosen, SLOPE\n     to leave vertical scrolling alone, DIST to commit, and the frames crossfade\n     UNDER the thumb through --shot-mix instead of a beat after it has lifted.\n     Passive listeners throughout - the drag is read, never preventDefault'd -\n     and touch-action: pan-y (theme.css) is what claims the horizontal axis.\n\n     CLAMPED, NOT WRAPPED. show() wraps, because two dots are a toggle and a\n     press on the one you are already on should still land somewhere. A drag is\n     a pager: dragging past the last frame has to do nothing rather than loop\n     silently back to the first.\n\n     TOUCH ONLY, AND ONLY UNDER 1024px, which is exactly the envelope the CSS\n     claims - .pdp__media takes touch-action: pan-y at those widths and nowhere\n     else, and above them the stage sits beside the buy column with its dots in\n     easy reach of a pointer that can hit a 20px target. */\n  const media = stage.closest('.pdp__media') || stage;\n  const narrow = matchMedia('(max-width: 1023px)');\n  const DIST = 40;        /* px before a drag counts as a swipe at all         */\n  const SLOPE = 1.4;      /* how much more horizontal than vertical it must be */\n  const WAKE = 6;         /* px before the gesture commits to an axis          */\n  const PULL = 0.45;      /* how much of the finger the packshot follows       */\n  const CAP = 28;         /* px ceiling on that travel                         */\n  const SETTLE = 600;     /* ms the layers stay rendered while opacity lands    */\n  let settleTimer = 0;\n\n  media.addEventListener('pointerdown', (e) => {\n    if (e.pointerType === 'mouse' || !narrow.matches) return;\n    /* The dots are taps. Starting a drag from one would put .is-swiping on the\n       stage for the length of a press and fight the class the tap is about. */\n    if (e.target.closest && e.target.closest('[data-hero-dot]')) return;\n    const from = shots.findIndex((s) => s.classList.contains('is-active'));\n    if (from < 0) return;\n\n    const startX = e.clientX, startY = e.clientY;\n    let live = false, dead = false;\n\n    /* Drag LEFT to go forward, the direction the content moves under the\n       finger. null at either end - see CLAMPED above. */\n    const aim = (dx) => shots[dx < 0 ? from + 1 : from - 1] || null;\n\n    const paint = (dx) => {\n      const to = aim(dx);\n      shots.forEach((s) => s.classList.toggle('is-incoming', s === to));\n      /* Reduced motion keeps the crossfade, which is the information, and drops\n         the travel, which is the decoration (05 section 6). */\n      const off = to && !reduceMotion.matches\n        ? Math.max(-CAP, Math.min(CAP, dx * PULL)) : 0;\n      stage.style.setProperty('--shot-mix', to ? Math.min(1, Math.abs(dx) / DIST).toFixed(3) : '0');\n      stage.style.setProperty('--shot-dx', off.toFixed(1) + 'px');\n    };\n    /* Hands the frames back to the classes. .is-settling keeps both layers\n       RENDERED for the length of the fade: the idle rule pairs opacity:0 with\n       content-visibility:hidden, and content-visibility is not an animatable\n       property, so without this window the layer that is leaving would blink\n       out instead of fading. Whatever is on screen at this moment is what the\n       classes paint, so there is no flash either way. */\n    const rest = () => {\n      stage.classList.remove('is-swiping');\n      stage.classList.add('is-settling');\n      shots.forEach((s) => s.classList.remove('is-incoming'));\n      stage.style.removeProperty('--shot-mix');\n      stage.style.removeProperty('--shot-dx');\n      clearTimeout(settleTimer);\n      settleTimer = setTimeout(() => stage.classList.remove('is-settling'), SETTLE);\n    };\n\n    const detach = (ev) => {\n      media.removeEventListener('pointermove', move);\n      media.removeEventListener('pointerup', end);\n      media.removeEventListener('pointercancel', cancel);\n      try { media.releasePointerCapture(ev.pointerId); } catch (err) { /* already gone */ }\n    };\n    const move = (ev) => {\n      if (dead) return;\n      const dx = ev.clientX - startX, dy = ev.clientY - startY;\n      if (!live) {\n        if (Math.abs(dx) < WAKE && Math.abs(dy) < WAKE) return;\n        /* The axis is decided ONCE, on the first real movement: a gesture that\n           starts vertical is the page scrolling and must never repaint the\n           stage halfway. */\n        if (Math.abs(dx) < Math.abs(dy) * SLOPE) { dead = true; return; }\n        live = true;\n        stage.classList.add('is-swiping');\n      }\n      paint(dx);\n    };\n    const end = (ev) => {\n      detach(ev);\n      if (!live) return;\n      const dx = ev.clientX - startX;\n      const to = aim(dx);\n      /* At the threshold --shot-mix is already 1, so the frames are where the\n         classes are about to put them and the handover cannot flash. */\n      if (to && Math.abs(dx) >= DIST) show(shots.indexOf(to));\n      rest();\n    };\n    const cancel = (ev) => { detach(ev); rest(); };\n\n    try { media.setPointerCapture(e.pointerId); } catch (err) { /* fine without */ }\n    media.addEventListener('pointermove', move, { passive: true });\n    media.addEventListener('pointerup', end, { passive: true });\n    media.addEventListener('pointercancel', cancel, { passive: true });\n  }, { passive: true });\n\n  /* BACK TO THE PRODUCT SHOT WHEN THE SIZE CHANGES (owner, 2026-08-24: \"when\n     swapping between variants the carousel image thingy needs to reset\").\n     Picking a new size repaints the variant layer underneath whatever slide is\n     showing; left on the styled shot, the switch appeared to do nothing at all,\n     because the styled scene is the same picture for every size. */\n  const heroForm = document.querySelector('[data-pdp-form]');\n  if (heroForm) heroForm.addEventListener('change', () => {\n    /* ---- THE STYLED LAYER FOLLOWS THE SIZE, AND NEVER STANDS DOWN ----\n       (owner, 2026-09-01: \"on product page regardless of variant, if I click\n       the swap images it would show the styled shot\".)\n\n       The rung's own scene wins where it has one. Where it does not - and only\n       the 50ml has one now, see snippets/styled-shot - the layer keeps the\n       scene the page opened on rather than disappearing.\n\n       IT USED TO DISAPPEAR, and that was a deliberate 2026-08-26 decision:\n       two dots where one of them is another size's photograph is worse than\n       one dot. That reasoning still holds on a CARD, where the second frame is\n       a hover reveal of the size in the poster, and initMiniSwap still drops a\n       10ml or Bundle card to one frame for exactly this reason. It does not\n       hold here. This page is about the SCENT, not about one rung of it: the\n       scene is the same editorial picture of the same fragrance whichever\n       bottle you are buying, the switch is a deliberate press rather than\n       something that happens under the cursor, and a control that vanishes\n       when you change size reads as a broken page, not as restraint.\n\n       data-pdp-styled is no longer read. It marked \"this came from a per-size\n       metafield\" so the standing-down could be scoped to those layers; with\n       nothing standing down there is nothing to scope. The attribute stays in\n       the markup as a note about where the picture came from. */\n    const styled = stage.querySelector('.pdp__shot--styled');\n    const picked = heroForm.querySelector('input[name=\"id\"]:checked');\n    if (styled && picked) {\n      if (!styled.dataset.openingSrc) styled.dataset.openingSrc = styled.getAttribute('src') || '';\n      const next = picked.dataset.variantStyled || styled.dataset.openingSrc;\n      if (next && styled.getAttribute('src') !== next) {\n        styled.removeAttribute('srcset');   /* or the browser keeps a candidate from the old set */\n        styled.setAttribute('src', next);\n      }\n      styled.hidden = false;\n      const swap = stage.querySelector('.pdp__viewswitch');\n      if (swap) swap.hidden = false;\n    }\n    peekFrom = -1;\n    show(0);\n    /* The rung that was just picked may be the first one a hover can answer -\n       and it may have swapped src above, which resets `complete`. Idempotent:\n       warm() is a no-op for a scene already decoded. */\n    warm();\n  });\n\n  /* ---- ON A DESKTOP, HOVER PEEKS AT THE SCENE - AND ONLY OFF THE 50ml ----\n     (owner, 2026-09-04: \"on desktop when I hover over the product on single\n     view, show the secondary but only for 50ml, if I clicked on the swatch the\n     auto show styled should not activate on hover then, this is only for\n     desktop\".)\n\n     The page has had two frames and a deliberate press for each of them since\n     the dots arrived; what it did not have is the card's gesture. A shopper\n     who has hovered forty cards on the way here arrives expecting the\n     photograph to answer the cursor, and on the one page that is entirely\n     about this bottle it did nothing at all.\n\n     IT IS THE CARD'S RULE, FOR THE CARD'S REASON. Only the 50ml was shot in a\n     scene (snippets/styled-shot), so a hover on any other rung would either\n     reveal nothing or - worse, and this is the exact bug the minis lane had on\n     2026-09-01 - fade the bottle you are looking at out from under a 50ml\n     arrangement. The gate is the picked rung's own data-kind, read at the\n     moment the cursor arrives, so it follows the size switch for free: click\n     the 10ml swatch and the photograph stops answering, click the 50ml back\n     and it answers again. data-kind is on every PDP [name=\"id\"], including the\n     visually-hidden one a single-size product renders, so there is no rung\n     this cannot ask about.\n\n     THE PRESS STILL WINS, because the peek is a LOAN and not a state: it\n     remembers the frame the stage was resting on and puts it back on the way\n     out, and any press on the dots ends the loan outright (see show above), so\n     whichever frame that press chose is simply where the stage now rests. That\n     keeps the 2026-09-01 contract - a click swaps the shot and HOLDS there -\n     true, with the mouse only ever borrowing what no press has claimed.\n\n     DESKTOP IS `(min-width: 1024px)` AND A REAL POINTER, which is exactly the\n     envelope the drag above refuses: .pdp__media claims touch-action: pan-y\n     below 1024px and the frames belong to the thumb there. The two gestures\n     cannot both own the stage, so they take a width apiece. pointerType is the\n     second half of that - a touchscreen laptop matches the media query for its\n     mouse and still emits a `touch` pointerenter under a finger.\n\n     No :focus-within twin, deliberately. The dots live INSIDE the stage, so\n     keyboard focus landing on one would reveal the very frame that press is\n     about and fight it. The card can afford that pairing; a surface carrying\n     its own pager cannot.\n\n     Nothing new in CSS. show() is the same call the dots make, so the peek\n     inherits every rule already keyed on .pdp__shot--styled.is-active - the\n     crossfade, the variant layer standing down, and the dark-scene inversion\n     of the switch itself. A second mechanism would have had to re-derive all\n     three and could only ever drift out of step with them. */\n  const styledAt = shots.findIndex((s) => s.classList.contains('pdp__shot--styled'));\n  const deskHover = matchMedia('(min-width: 1024px) and (hover: hover) and (pointer: fine)');\n  const fifty = () => {\n    const on = heroForm && heroForm.querySelector('input[name=\"id\"]:checked');\n    return !!on && on.dataset.kind === 'full';\n  };\n\n  /* ---- THE SCENE HAS TO BE ON THE MACHINE BEFORE THE CURSOR ASKS FOR IT ----\n     (measured on the staging preview, 2026-09-04: the first hover of a cold\n     visit showed a WHITE STAGE and the network entry for the scene appeared at\n     the moment of the reveal, not at load.)\n\n     That is `loading=\"lazy\"` doing exactly what it was asked to do on\n     2026-08-19, and the reason it was asked is still good: the scene is a\n     second 1600px photograph and it must never race the bottle, which is the\n     LCP. What has changed is who asks for it. A press on the dots is a\n     deliberate act and can afford to wait a beat; a hover is not, it happens by\n     accident on the way to the buy column, and an empty frame under the cursor\n     reads as a broken page rather than as a picture arriving.\n\n     So the file is fetched and DECODED on the idle after load - after the\n     bottle, after everything the page needs to be usable, and only where a\n     hover can actually happen: a desktop pointer, on the 50ml. decode() rather\n     than a bare load because the blank in that measurement outlasted the\n     transfer (12ms, straight out of cache) - it was the 1600px JPEG being\n     decoded on reveal, which is the half `loading` does not cover.\n\n     AND THE PEEK REFUSES A PICTURE THAT IS NOT THERE YET. On a connection slow\n     enough that the warm has not landed, doing nothing is the honest answer:\n     the shopper keeps the bottle they were looking at, the warm is nudged\n     again, and the next hover reveals. A stage that goes blank is never the\n     better half of that trade. */\n  const scene = styledAt > -1 ? shots[styledAt] : null;\n  const warm = () => {\n    if (!scene || (scene.complete && scene.naturalWidth)) return;\n    if (!deskHover.matches || !fifty()) return;\n    /* STOPPING THE DEFERRAL IS WHAT STARTS THE FETCH, and decode() alone is\n       not it: measured on the harness, decode() on an image whose lazy load\n       has never begun rejects on the spot (\"the source image cannot be\n       decoded\") and requests nothing, which a .catch swallows into a warm that\n       silently does nothing at all. Flipping the attribute to eager is the\n       documented way to un-defer a parsed image, and the decode after it is\n       the second half - the blank in that measurement outlasted a 12ms\n       cache-hit transfer, so it was the 1600px JPEG decoding, not arriving. */\n    if (scene.loading === 'lazy') scene.loading = 'eager';\n    if (scene.decode) scene.decode().catch(() => {});\n  };\n  const idle = window.requestIdleCallback || ((fn) => setTimeout(fn, 800));\n  if (document.readyState === 'complete') idle(warm);\n  else window.addEventListener('load', () => idle(warm), { once: true });\n\n  if (styledAt > -1) {\n    /* enter/leave, not over/out: the dots and both shots are children of the\n       stage, and over/out would fire a leave every time the cursor crossed one\n       of them. */\n    stage.addEventListener('pointerenter', (e) => {\n      if (e.pointerType !== 'mouse' || !deskHover.matches) return;\n      if (stage.classList.contains('is-swiping') || !fifty()) return;\n      if (!scene.complete || !scene.naturalWidth) { warm(); return; }\n      const from = shots.findIndex((s) => s.classList.contains('is-active'));\n      if (from < 0 || from === styledAt) return;   /* already there: nothing to lend */\n      peekFrom = from;\n      show(styledAt);\n    });\n    stage.addEventListener('pointerleave', () => {\n      if (peekFrom < 0) return;                    /* nothing on loan, or a press took it */\n      const back = peekFrom;\n      peekFrom = -1;\n      show(back);\n    });\n  }\n\n  show(0);\n}\n\n/* ---- initPdpSwipe is GONE (owner, 2026-09-03: \"Product page when swipe it\n   swipes to next product instead of image\") ----\n\n   It read a drag on .pdp__media and navigated to the rel=next/prev neighbour.\n   The gesture now belongs to the photograph and lives inside initPdpHero,\n   beside the dots and the show() it drives - one controller for one surface,\n   which is also the only way a drag and a pager can agree on which frame is\n   up. The neighbour links themselves stay in main-product.liquid: they were\n   always real rel=prev/next anchors, and a screen reader and a crawler still\n   walk the house by them.\n\n/* ---- initPdpNav is GONE (owner, 2026-08-19: \"Please don't make the top nav\n   sticky\") ----\n\n   It watched the PDP's pinned media and put .pdp-nav-release on <html> once the\n   photograph had scrolled past, which translated the header off the top so the\n   ritual lane and the footer had a clean run. Every line of it existed to undo\n   stickiness on one template. With .site-head static the header leaves by\n   itself, everywhere, and there is nothing left to release. The class, its two\n   CSS rules and the observer all went together. */\n\nfunction initDeck() {\n  document.querySelectorAll('[data-deck]').forEach((deck) => {\n    const cards = [...deck.children];\n    if (!cards.length) return;\n    let raf = 0;\n    const feed = () => {\n      raf = 0;\n      const mid = deck.getBoundingClientRect().left + deck.clientWidth / 2;\n      cards.forEach((card) => {\n        const r = card.getBoundingClientRect();\n        const f = Math.max(0, 1 - Math.abs(r.left + r.width / 2 - mid) / (r.width * 1.1));\n        card.style.setProperty('--f', f.toFixed(3));\n        card.classList.toggle('is-lit', f > 0.8);\n      });\n    };\n    deck.addEventListener('scroll', () => { if (!raf) raf = requestAnimationFrame(feed); },\n                          { passive: true });\n    deck.addEventListener('click', (e) => {\n      /* The `.dk-dock` guard that used to head this list is gone with the card's\n         commerce bar - there is no longer anything on a poster to click except\n         the poster and the photo swap. */\n      if (e.target.closest('[data-shot-swap]')) return; /* the photo swap passes through */\n      if (e.detail === 0) return;                     /* keyboard/SR activation navigates */\n      const card = e.target.closest('[data-card]');\n      if (!card || card.classList.contains('is-lit')) return;\n      e.preventDefault();\n      card.scrollIntoView({ behavior: reduceMotion.matches ? 'auto' : 'smooth',\n                            inline: 'center', block: 'nearest' });\n    });\n    /* wake centred on card 2 - scrollLeft only, never scroll the page */\n    const c = cards[1] || cards[0];\n    deck.scrollLeft = c.offsetLeft - (deck.clientWidth - c.offsetWidth) / 2;\n    feed();\n  });\n}\n\n/* ---- Island: the PDP Add pill mirrors the real form ---- */\nfunction initDockAdd() {\n  document.addEventListener('click', (e) => {\n    const trigger = e.target.closest('[data-dock-add]');\n    if (!trigger) return;\n    const form = document.querySelector('[data-pdp-form]');\n    if (!form) return;\n    /* the pill IS the point of action down here - hand it to the submit\n       handler so the confirmation and the parcel start from the thumb, not\n       from a button scrolled off the top of the page */\n    addTrigger = trigger;\n    submitForm(form);\n    addTrigger = null;                        /* nothing submitted: do not leave it armed */\n  });\n}\n\n/* ---- The island's commerce face: the product currently in view ----\n\n   The island was only ever \"dynamic\" in the sense that the SERVER picked a face\n   at render time - nav everywhere, add-to-cart on a PDP - and then it sat there.\n   On a phone the whole site is a column of poster cards, so for most of a\n   session the bar at the bottom of the screen was three generic pills sitting\n   under a specific, named, photographed product.\n\n   It follows the scroll: while a card holds the middle of the screen the\n   island promotes that card's price, variant and Add control into thumb reach.\n   Leave the card and it goes back to being navigation.\n\n   Two rules keep it from flickering. It only takes a card that is genuinely\n   dominant (the most-visible one, and only past ON), and it only lets go when\n   nothing is above OFF - a hysteresis gap, so a card hovering around a single\n   threshold cannot strobe the bar. It also never overrides a server-set face:\n   on a PDP the add-to-cart bar is the point of the page, and the ritual lane\n   further down is full of cards that would otherwise steal it. */\n/* ---- The island's mode handoff ----\n   Every face change goes through here so the bar never cuts. Two beats: the\n   face on screen takes .is-morphing and sinks out, then data-mode flips and the\n   new face rises in (see the .dock keyframes in components.css).\n\n   COLLAPSES, does not queue. Scrolling fast past three cards fires three mode\n   changes inside the exit window; without the pending-timer guard each would\n   start its own handoff and the bar would stutter through faces it was never\n   meant to show. The last mode asked for is the one that lands.\n\n   Reduced motion skips the beat entirely rather than shortening it - there is\n   no point paying 140ms of latency for an animation nobody will see. */\nlet dockMorph = 0;\n/* ---- THE HANDOFF IS INSTANT NOW ----\n   (owner, 2026-08-19: \"The dock swapping on and off product needs to be\n   instantly\".)\n\n   It was a two-beat handoff: .is-morphing sank the face on screen, and only\n   after a timer did data-mode flip and the new face rise. That beat was 140ms,\n   then 90, and it was always dead time - the bar shows NEITHER face while it\n   runs, so every number above zero is latency the shopper feels and nothing\n   they see. Asked for instant twice, the honest answer is to delete the beat\n   rather than shorten it again.\n\n   The change is still animated. `island-face` plays on the ARRIVING face\n   (components.css), so the new price and swatch land from slightly under at\n   reduced opacity - it reads as the bar re-forming, which is what the\n   two-beat version was reaching for, and it costs nothing because it happens\n   while the face is already showing.\n\n   dockMorph is kept as a name only so the pending-timer collapse below stays\n   readable; there is no timer left to clear. */\nfunction setDockMode(dock, mode) {\n  if (!dock || dock.getAttribute('data-mode') === mode) return;\n  clearTimeout(dockMorph);\n  dock.classList.remove('is-morphing');\n  dock.setAttribute('data-mode', mode);\n}\n\n/* ---- The compact views buy through a sheet, not through the island ----\n   (owner, 2026-08-14: \"On product all views when 2 colum and that other list\n   view, the dock add to cart needs to be away and then these cards need to ahve\n   a add to cart icon on it, when you click on it a little popup to ask you the\n   variant and the price\".)\n\n   The island's face works by adopting the ONE card that dominates the screen.\n   In duo two posters share a row at identical heights, and in list five or six\n   rows are visible at once - so there is no dominant card to adopt, and the bar\n   would either price one of a pair under both or flip between rows. The face\n   stands down on those views (see the view test in initDockCommerce) and this\n   takes over.\n\n   A MIRROR OF .dk-src, exactly like the island's face. Nothing here holds a\n   variant, a price or a cart path of its own: the sheet is filled from the\n   tapped card's real form, the size buttons press the real options, and Add\n   calls requestSubmit() on the real form so bundling, the R0 tester, the added\n   feedback and the cart section refresh all continue through initCart's single\n   path. Two surfaces, one source of truth, no third state.\n\n   One dialog for the whole grid, filled on open - see the snippet for why not\n   one popover per card. */\n/* The island opens this sheet too (the compact commerce face - see\n   initDockCommerce), and it is not inside a card, so the delegated\n   [data-card-add] listener below cannot reach it. Rather than give the dock a\n   second copy of the fill-and-open logic, initCardAdd publishes the one it\n   already has. Null until the sheet is mounted and <dialog> is supported, which\n   is exactly the condition the dock has to fall back on. */\nlet openCardSheet = null;\n/* The same sheet, filled from a PRODUCT PAGE instead of a card (owner,\n   2026-08-19: \"This one needs to follow the same schematic / style if possible\n   when selecting the variant\"). Assigned by initCardAdd next to its card\n   twin; the dock's size chip on a PDP is the only caller. */\nlet openPdpSheet = null;\n\nfunction initCardAdd() {\n  const sheet = document.querySelector('[data-cadd]');\n  if (!sheet || typeof sheet.showModal !== 'function') return;\n  const nameEl = sheet.querySelector('[data-cadd-name]');\n  const sizesEl = sheet.querySelector('[data-cadd-sizes]');\n  const priceEl = sheet.querySelector('[data-cadd-price]');\n  const addEl = sheet.querySelector('[data-cadd-add]');\n  const addLabel = sheet.querySelector('[data-cadd-add-label]');\n  if (!nameEl || !sizesEl || !priceEl || !addEl) return;\n\n  let form = null;          /* the real form, while the sheet is open         */\n  let opener = null;        /* the button that opened it - see the submit      */\n  let pdpMode = false;      /* which fill the sheet is showing - see openPdpSheet */\n\n  /* Re-read the card and repaint. Called on open and after every size tap, so\n     the price in the sheet is always the card's own answer rather than a copy\n     that was correct when it was made. */\n  const sync = () => {\n    if (!form) return;\n    const source = form.querySelector('[data-card-price]');\n    if (source) {\n      const sourceWas = priceWasOf(source);\n      paintPrice(priceEl, source.textContent.trim(),\n                 sourceWas && !sourceWas.hidden ? sourceWas.textContent.trim() : '');\n    }\n    const options = [...form.querySelectorAll('[data-size-opt]')];\n    sizesEl.querySelectorAll('[data-cadd-size]').forEach((button) => {\n      const match = options.find((o) => o.dataset.variantId === button.dataset.caddSize);\n      const on = !!match && (match.classList.contains('is-active') ||\n                             match.getAttribute('aria-pressed') === 'true');\n      button.classList.toggle('is-active', on);\n      button.setAttribute('aria-pressed', on ? 'true' : 'false');\n    });\n    placeSizeThumb(sizesEl.querySelector('.sizes'), false);\n  };\n\n  const fill = (card) => {\n    form = card.querySelector('[data-product-form]');\n    if (!form) return false;\n    const name = card.querySelector('.dk-id__name');\n    nameEl.textContent = name ? name.textContent.trim() : 'This scent';\n    sheet.setAttribute('aria-labelledby', nameEl.id);\n\n    /* Built here rather than cloned: a clone would bring data-size-opt with it,\n       and initSizes listens on the document - the copies would then drive the\n       card's variant input directly and the two swatches would race. These\n       carry their own hook and forward to the real option instead. */\n    const options = [...form.querySelectorAll('[data-size-opt]')];\n    if (options.length > 1) {\n      const group = document.createElement('span');\n      group.className = 'sizes';\n      group.setAttribute('role', 'group');\n      group.setAttribute('aria-label', 'Size');\n      options.forEach((option) => {\n        const button = document.createElement('button');\n        button.type = 'button';\n        button.className = 'size-opt' + ('bundle' in option.dataset ? ' size-opt--bundle' : '');\n        button.dataset.caddSize = option.dataset.variantId || '';\n        /* ---- EACH RUNG STATES ITS OWN PRICE (2026-08-19) ----\n\n           The sheet used to print one price under a row of bare size words, so\n           choosing a size meant tapping one to find out what it cost and\n           tapping back if it was not the one. That was tolerable while this was\n           a duo/list convenience; it is not now that the island's compact face\n           routes every phone Add through here - this IS the size decision, so\n           it has to show what the sizes cost.\n\n           data-price-label is already on every option (size-options.liquid\n           writes it for exactly this kind of mirror), so nothing is computed\n           here and the numbers cannot disagree with the card's own. */\n        const label = document.createElement('span');\n        label.className = 'size-opt__t';\n        label.textContent = option.textContent.trim();\n        button.appendChild(label);\n        if (option.dataset.priceLabel) {\n          const money = document.createElement('span');\n          money.className = 'size-opt__p';\n          money.textContent = option.dataset.priceLabel;\n          button.appendChild(money);\n        }\n        /* ---- THE BUNDLE RUNG SAYS WHAT IS IN IT, IN WORDS ON THE BUTTON ----\n           (owner, 2026-08-28: \"Mobile on add to cart popup show 10ml + 50ml\n           for bundle\".)\n\n           It already carried the fact - data-bundle-of is \"50ml + 10ml\", built\n           by size-options.liquid from the rungs this product actually has -\n           and it put it in a `title` attribute. A title is a hover tooltip,\n           and this sheet exists BECAUSE the shopper is on a phone: there is no\n           hover, so on the one surface where the sizes are the whole decision,\n           the most expensive rung was the one word that says least. The PDP\n           has printed it as .sizepick__of all along; this prints the same\n           string in the same place the price sits.\n\n           The title stays for the desktop path (the island routes through this\n           sheet too) and for the accessible name, which size-options already\n           writes as \"Bundle - 50ml + 10ml\". */\n        if (option.dataset.bundleOf) {\n          button.title = option.dataset.bundleOf;\n          const of = document.createElement('span');\n          of.className = 'size-opt__of';\n          of.textContent = option.dataset.bundleOf;\n          button.appendChild(of);\n        }\n        group.appendChild(button);\n      });\n      sizesEl.replaceChildren(group);\n    } else {\n      /* One size is not a choice. Print it as the fact it is - a disabled\n         option here would be a control with a single state, which reads as\n         broken (the same rule size-options.liquid follows). */\n      const only = form.querySelector('[data-variant-input]');\n      const label = document.createElement('span');\n      label.className = 'cadd__only';\n      label.textContent = (only && only.dataset.variantLabel) ||\n        (options[0] && options[0].textContent.trim()) || '';\n      sizesEl.replaceChildren(label);\n    }\n\n    const sourceAdd = form.querySelector('[data-add]');\n    const sold = !!(sourceAdd && sourceAdd.disabled);\n    addEl.disabled = sold;\n    addEl.setAttribute('aria-disabled', sold ? 'true' : 'false');\n    if (addLabel) addLabel.textContent = sold ? 'Sold out' : 'Add to cart';\n    sync();\n    return true;\n  };\n\n  const close = () => { if (sheet.open) sheet.close(); };\n\n  /* One way in, two callers. `from` is the element the flying-bottle animation\n     starts at and the working state hangs on - the card's own trigger for a\n     card tap, the island's disc when the dock opens it, because that is where\n     the shopper's finger already is in each case. */\n  openCardSheet = (card, from) => {\n    if (!card) return false;\n\n    /* ---- ONE SIZE IS NOT A QUESTION (owner, 2026-09-01: \"when I click add to\n       cart on mobile that the popup should not show if 1 variant and just\n       immediately adds it\") ----\n\n       The island's compact Add has always taken this shortcut (initDockCommerce\n       tests the same thing), but the CARD's own trigger - duo and list - opened\n       the sheet whatever it found. So a shopper on a one-size product (a scent\n       with a size retired through custom.hidden_sizes) got a modal that\n       named a product, printed one price, showed no choice, and asked them to\n       press Add a second time.\n\n       The rule lives HERE rather than at each caller so the two doors into this\n       sheet cannot drift apart. `<= 1` and not `=== 0` on purpose: size-options\n       renders NO [data-size-opt] at all for a single size (it prints a quiet\n       .size-note instead), and the dock's own guard is `> 1`, so this is the\n       same threshold read from the other side.\n\n       It submits the card's real form through initCart's single path, with the\n       tapped trigger as addTrigger, so the flying bottle, the added feedback,\n       the R0 tester and the cart drawer all behave exactly as they do when the\n       sheet's own Add is pressed. */\n    const lone = card.querySelector('[data-product-form]');\n    if (lone && lone.querySelectorAll('[data-size-opt]').length <= 1) {\n      const add = lone.querySelector('[data-add]');\n      if (add && add.disabled) return false;\n      addTrigger = from && from.isConnected ? from : (add || null);\n      submitForm(lone);\n      addTrigger = null;\n      return true;\n    }\n\n    if (!fill(card)) return false;\n    pdpMode = false;\n    opener = from || null;\n    sheet.showModal();\n    /* The swatch has no box until the dialog is in the top layer, so the\n       capsule can only be placed now - the same ordering the island's face\n       needs after setDockMode. */\n    placeSizeThumb(sizesEl.querySelector('.sizes'), true);\n    return true;\n  };\n\n  /* ---- THE PDP FILL ----\n     Same sheet, same rungs, same black pill - a different source of truth.\n     Everything below reads the PDP's own radios (size-options.liquid stamps\n     data-price-label / data-compare-label / data-kind / data-unavailable on\n     each one) and writes back through them, so initPdpVariant, initSeg, the\n     stage's variant swap and the dock's own mirrors all fire exactly as if the\n     on-page picker had been used. No second variant state, no second price. */\n  const pdpRungs = () => {\n    const pdp = document.querySelector('[data-pdp-form]');\n    return pdp ? [...pdp.querySelectorAll('input[name=\"id\"]')] : [];\n  };\n  /* The size word lives on the tile around the radio, not on the radio - and\n     the dock's swatch mirrors are the fallback for a template (or harness)\n     with no tiles. */\n  const pdpLabel = (radio) => {\n    const tile = radio.closest('label');\n    const name = tile && tile.querySelector('.sizepick__name');\n    if (name) return name.textContent.trim();\n    const mirror = document.querySelector('[data-dock-size=\"' + cssAttr(radio.value) + '\"]');\n    return mirror ? mirror.textContent.trim() : '';\n  };\n\n  const syncPdp = () => {\n    if (!form || !pdpMode) return;\n    const rungs = pdpRungs();\n    const picked = rungs.find((r) => r.checked) || null;\n    sizesEl.querySelectorAll('[data-cadd-size]').forEach((button) => {\n      const on = !!picked && picked.value === button.dataset.caddSize;\n      button.classList.toggle('is-active', on);\n      button.setAttribute('aria-pressed', on ? 'true' : 'false');\n    });\n    if (picked) {\n      paintPrice(priceEl, picked.dataset.priceLabel || '',\n                 picked.dataset.compareLabel || '');\n    }\n    /* A rung the shop cannot sell is stated on the button, exactly as the\n       card path states it - data-unavailable survives even when the radio is\n       not disabled (the whole-product-out case, where the ladder still has to\n       land somewhere). */\n    const sold = !picked || picked.hasAttribute('data-unavailable');\n    addEl.disabled = sold;\n    addEl.setAttribute('aria-disabled', sold ? 'true' : 'false');\n    if (addLabel) addLabel.textContent = sold ? 'Sold out' : 'Add to cart';\n    placeSizeThumb(sizesEl.querySelector('.sizes'), false);\n  };\n\n  openPdpSheet = (from) => {\n    const pdp = document.querySelector('[data-pdp-form]');\n    const rungs = pdpRungs();\n    if (!pdp || !rungs.length) return false;\n    form = pdp;\n    pdpMode = true;\n\n    /* \"and just the product name perhaps\" - the sheet leads with it, which is\n       the half of the owner's interrupted note this surface can already keep. */\n    const pageName = document.querySelector('.pdp__name');\n    nameEl.textContent = pageName ? pageName.textContent.trim() : 'This scent';\n    sheet.setAttribute('aria-labelledby', nameEl.id);\n\n    if (rungs.length > 1) {\n      const group = document.createElement('span');\n      group.className = 'sizes';\n      group.setAttribute('role', 'group');\n      group.setAttribute('aria-label', 'Size');\n      rungs.forEach((radio) => {\n        const button = document.createElement('button');\n        button.type = 'button';\n        button.className = 'size-opt' + (radio.dataset.kind === 'bundle' ? ' size-opt--bundle' : '');\n        button.dataset.caddSize = radio.value;\n        if (radio.disabled) { button.disabled = true; button.setAttribute('aria-disabled', 'true'); }\n        const label = document.createElement('span');\n        label.className = 'size-opt__t';\n        label.textContent = pdpLabel(radio);\n        button.appendChild(label);\n        const priceText = radio.hasAttribute('data-unavailable') ? 'Sold out' : (radio.dataset.priceLabel || '');\n        if (priceText) {\n          const money = document.createElement('span');\n          money.className = 'size-opt__p';\n          money.textContent = priceText;\n          button.appendChild(money);\n        }\n        /* The bundle's contents, same as the card path above. The PDP tile\n           already prints them as .sizepick__of - read from there rather than\n           adding a second attribute that could drift from what is on screen a\n           few hundred pixels away. */\n        const tile = radio.closest('label');\n        const of = tile && tile.querySelector('.sizepick__of');\n        if (of && of.textContent.trim()) {\n          const said = document.createElement('span');\n          said.className = 'size-opt__of';\n          said.textContent = of.textContent.trim();\n          button.title = said.textContent;\n          button.appendChild(said);\n        }\n        group.appendChild(button);\n      });\n      sizesEl.replaceChildren(group);\n    } else {\n      const only = document.createElement('span');\n      only.className = 'cadd__only';\n      only.textContent = pdpLabel(rungs[0]);\n      sizesEl.replaceChildren(only);\n    }\n\n    syncPdp();\n    opener = from && from.isConnected ? from : null;\n    sheet.showModal();\n    placeSizeThumb(sizesEl.querySelector('.sizes'), true);\n    return true;\n  };\n\n  document.addEventListener('click', (e) => {\n    const trigger = e.target.closest('[data-card-add]');\n    if (!trigger || trigger.disabled) return;\n    const card = trigger.closest('[data-card]');\n    if (!card) return;\n    e.preventDefault();\n    openCardSheet(card, trigger);\n  });\n\n  sheet.addEventListener('click', (e) => {\n    if (e.target.closest('[data-cadd-close]')) { close(); return; }\n    /* Tapping the backdrop closes. The dialog element IS the backdrop's event\n       target, so a click that did not land on the shell came from outside. */\n    if (!e.target.closest('.cadd__shell')) { close(); return; }\n\n    const size = e.target.closest('[data-cadd-size]');\n    if (size && form) {\n      if (pdpMode) {\n        /* Press the real radio, never a copy of it - the change event is what\n           runs initPdpVariant (price, stage, Payflex), initSeg and the dock's\n           own mirrors, so the page behind the sheet is already true when the\n           sheet closes. */\n        const radio = form.querySelector('input[name=\"id\"][value=\"' + cssAttr(size.dataset.caddSize) + '\"]');\n        if (radio && !radio.disabled && !radio.checked) {\n          radio.checked = true;\n          radio.dispatchEvent(new Event('change', { bubbles: true }));\n        }\n        syncPdp();\n      } else {\n        const source = form.querySelector('[data-size-opt][data-variant-id=\"' + cssAttr(size.dataset.caddSize) + '\"]');\n        if (source) { source.click(); sync(); }\n      }\n      return;\n    }\n\n    const add = e.target.closest('[data-cadd-add]');\n    if (!add || !form || add.disabled) return;\n    /* THE CARD'S BUTTON IS THE TRIGGER, NOT THIS ONE. addTrigger is what the\n       submit handler animates from and hangs its working state on, and it reads\n       the element's box AFTER the request comes back - by which time this sheet\n       has closed and every box inside it is zero, so the parcel would silently\n       never fly. The opener is still on screen and is where the shopper's eye\n       already is, so the bottle travels from the poster to the trolley. */\n    addTrigger = opener && opener.isConnected ? opener : add;\n    submitForm(form);\n    addTrigger = null;\n    /* Closed on the way out, not on the way back: initCart's feedback and the\n       cart drawer are the confirmation, and holding a sheet open over them\n       would put the shopper two layers deep on a phone. */\n    close();\n  });\n\n  sheet.addEventListener('close', () => { form = null; opener = null; pdpMode = false; });\n}\n\n/* ---- Picking the small size shows the small bottle ----\n   Every sellable 10ml variant now carries its own photograph on the store, so\n   this paints THE PRODUCT'S OWN mini - not, as it did while the shots were\n   being staged, a random file from a pool of six standing in for forty. The URL\n   is server-rendered onto data-mini-src (snippets/mini-shot.liquid resolves it\n   from the variant), so nothing here reaches for the catalogue index and there\n   is no list of filenames in the JS to fall out of step with the store.\n\n   Two surfaces, one rule: the PDP stage, and whichever card the island has\n   adopted - on a catalogue page the size switch lives in the island, so tapping\n   10ml there has to change something the shopper can actually see.\n\n   Lazy by construction: the mini <img> ships with no src at all and only gets\n   one the first time a shopper asks for the small size, so a PDP that is never\n   switched never downloads it. */\nfunction initMiniSwap() {\n  /* \"10ml\" yes, \"100ml\" no - hence the parse rather than a substring test. */\n  const isMini = (label) => {\n    const m = /(\\d+)\\s*ml/i.exec(label || '');\n    return !!m && parseInt(m[1], 10) <= 10;\n  };\n\n  /* ---- the PDP stage ----\n     Same rule as the card below: the layer carries the SELECTED variant's\n     photograph, not \"the small one\". The base shot underneath is already the\n     selected variant at load (sections/main-product.liquid runs size-options'\n     ladder), so the layer's job is only to cover it when the shopper moves to\n     a size with a different bottle - and to get out of the way when they come\n     back, or when they pick a rung with no photograph of its own.\n\n     `base` is read ONCE from the stage's first shot. Comparing against it is\n     what makes the Bundle work without naming it: no data-variant-src, so src\n     resolves to nothing, so the layer hides and the 50ml underneath shows. */\n  const stage = document.querySelector('[data-pdp-hero]');\n  const heroVariant = stage && stage.querySelector('[data-pdp-variant]');\n  const pdpForm = document.querySelector('[data-pdp-form]');\n  if (stage && heroVariant && pdpForm) {\n    /* The src ATTRIBUTE, not currentSrc: currentSrc is only populated once the\n       browser has run resource selection on the srcset, and this function runs\n       at boot. When it came back empty the filename compare below could not\n       match, so the layer painted the 50ml's 2000px copy OVER the 50ml already\n       on the stage - a second full-size download and a fade-in of the same\n       photograph a beat after the page had settled. That is a large part of\n       what the owner was seeing as the stage \"delay show\" (2026-08-18). The\n       attribute is in the markup, so it is there on the first line of script. */\n    const baseShot = stage.querySelector('.pdp__shot:not([data-pdp-variant])');\n    const baseSrc = baseShot ? (baseShot.getAttribute('src') || baseShot.currentSrc || '') : '';\n    const fileOf = (u) => (u || '').split('?')[0].split('/').pop();\n\n    const paintHero = () => {\n      const picked = pdpForm.querySelector('input[name=\"id\"]:checked');\n      const label = picked && picked.parentElement ? picked.parentElement.textContent : '';\n      let src = (picked && picked.dataset.variantSrc) || '';\n      if (!src && isMini(label)) src = heroVariant.dataset.miniSrc || '';\n\n      /* Compare by FILENAME, not by the whole url: the base shot's src carries\n         whatever width the srcset settled on, and data-variant-src is rendered\n         at a fixed width, so the two strings never match even for one image. */\n      if (!src || fileOf(src) === fileOf(baseSrc)) {\n        stage.classList.remove('is-variant');\n        return;\n      }\n      if (heroVariant.getAttribute('src') !== src) heroVariant.src = src;\n      stage.classList.add('is-variant');\n    };\n    pdpForm.addEventListener('change', paintHero);\n    paintHero();\n  }\n\n  /* ---- THE ADDRESS BAR IS PART OF THE PAGE ----\n     Picking a size changed the price, the posted id, the stage and the Add\n     button, and left the URL saying nothing. ?variant= was already honoured on\n     the way IN (size-options.liquid reads it and opens on that rung), so the\n     page could be linked to but never linked FROM: a shopper who chose the\n     10ml and then shared, bookmarked or reloaded landed back on the 50ml, and\n     every analytics event carried the default variant rather than the one on\n     screen. The card grid has done this since it learned to carry a size\n     (syncCardLink writes the same parameter onto every card link); this is the\n     product page catching up with its own cards.\n\n     replaceState, not pushState. A size picker is one decision being revised,\n     not four pages visited - pushState would make Back walk the rungs and turn\n     leaving the product into as many presses as the shopper had been curious.\n     The trade is that Back does not restore a size within the page, which is\n     the same trade Shopify's own themes make and the one that keeps Back\n     meaning \"the page before this one\".\n\n     Deliberately its own listener rather than a line inside paintHero: that\n     block only runs for a product with a variant hero, and a product without\n     one still has an address worth keeping true. Every other parameter on the\n     URL is preserved. */\n  if (pdpForm) {\n    pdpForm.addEventListener('change', (e) => {\n      if (!e.target || e.target.name !== 'id') return;\n      const picked = pdpForm.querySelector('input[name=\"id\"]:checked');\n      const id = picked && picked.value;\n      if (!id) return;\n      try {\n        const url = new URL(window.location.href);\n        if (url.searchParams.get('variant') === String(id)) return;\n        url.searchParams.set('variant', id);\n        history.replaceState(history.state, '', url);\n      } catch (err) { /* an exotic context with no history access is not a reason to break the picker */ }\n    });\n  }\n\n  /* ---- the adopted card, driven from the island ----\n     The island's buttons are mirrors that click the card's real option, so this\n     listens for the SOURCE click rather than the dock's and gets the card for\n     free as the button's own ancestor - one listener covers both paths.\n\n     THE POSTER IS THE SELECTED VARIANT'S BOTTLE (owner, 2026-08-17), which is\n     why this reads the rung's own data-variant-src rather than testing for\n     \"is it the small one\". It used to be a binary mini/not-mini flip, and that\n     stopped being enough the moment the Bundle rung arrived: three sizes cannot\n     be described by one boolean, and \"not mini\" was quietly restoring the\n     opening poster for a rung that is not the 50ml.\n\n     Order matters. The rung's own photograph wins; data-mini-src is kept as the\n     fallback for a small size whose variant has no picture yet (see mini-shot\n     .liquid - that fallback is not dead code); anything left over restores the\n     opening poster, which is both the right answer for the Bundle and the right\n     answer for coming back to the 50ml. */\n  document.addEventListener('click', (e) => {\n    const opt = e.target.closest('[data-size-opt]');\n    if (!opt) return;\n    const card = opt.closest('[data-card]');\n    const shot = card && card.querySelector('.shot');\n    if (!shot) return;\n\n    /* ---- THE REVEAL IS THIS SIZE'S OWN SCENE OR NOTHING ----\n       (owner, 2026-08-24, then 2026-08-26: \"No 10ml styled shots... on the 10ml\n       variant we will only so the 10ml single shot and no longer the styled\n       shot we can remove that\".)\n\n       Each size was shot in its own arrangement, so a card set to the 10ml\n       cannot reveal the 50ml's. What this used to do when a rung had no scene\n       was restore the OPENING one - the 50ml's - which was the honest answer\n       while the only rung without a scene was the Bundle and the card had\n       nowhere else to go. It is not the honest answer now: the 10ml has no\n       scene either (snippets/styled-shot no longer reads custom.10ml_styled,\n       and the pictures are still in the store), so that fallback would put a\n       50ml arrangement behind a 10ml bottle on every card in the grid.\n\n       So a rung with no scene drops the card to ONE FRAME instead, which is the\n       same thing product-card does at render time when the size it opens on was\n       never shot. .is-oneframe hides the reveal, hides the swap dots, releases\n       the horizontal axis back to the browser and stops any leftover .show-alt\n       blanking the poster; see the block in components.css.\n\n       Coming BACK to a size that has a scene is where the mode has to be asked\n       again: in leaf mode the card was resting on its scene before the shopper\n       tried the 10ml, and it has to be resting on it afterwards too. Nothing is\n       touched on a scene-to-scene change, so a shopper who chose a frame with\n       the dots keeps it. */\n    const alt = card.querySelector('.shot--alt');\n    if (alt) {\n      /* The snapshot below is what a size change restores the card TO, so it\n         has to be taken off a hydrated element: a deferred one would record\n         an empty string as the frame to come back to (2026-09-12). A size\n         change is a shopper acting on this card, so the fetch is wanted. */\n      hydrateShot(alt);\n      if (!card.dataset.altWas) {\n        card.dataset.altWas = alt.currentSrc || alt.src;\n        if (alt.srcset) card.dataset.altSet = alt.srcset;\n      }\n      const styled = opt.dataset.variantStyled || '';\n      const wasOne = card.classList.contains('is-oneframe');\n      if (styled) {\n        if (styled !== alt.getAttribute('src')) {\n          alt.removeAttribute('srcset');   /* or the browser keeps a candidate from the old set */\n          alt.src = styled;\n        }\n        if (wasOne) {\n          card.classList.remove('is-oneframe');\n          showShot(card, cardRestsOnScene(card));\n        }\n      } else if (!wasOne) {\n        card.classList.add('is-oneframe');\n        showShot(card, false);\n      }\n    }\n\n    /* Remember the opening poster ONCE, so the card can always get it back\n       however many times the shopper flips between the sizes. Captured on the\n       first click rather than at load, because that is the first moment the\n       browser has settled on a srcset candidate. */\n    if (!card.dataset.shotWas) {\n      card.dataset.shotWas = shot.currentSrc || shot.src;\n      if (shot.srcset) card.dataset.shotSet = shot.srcset;\n    }\n\n    let src = opt.dataset.variantSrc || '';\n    if (!src && isMini(opt.textContent)) src = card.dataset.miniSrc || '';\n\n    if (!src || src === card.dataset.shotWas) {\n      shot.src = card.dataset.shotWas;\n      if (card.dataset.shotSet) shot.srcset = card.dataset.shotSet;\n      return;\n    }\n    shot.removeAttribute('srcset');   /* or the browser keeps serving a candidate from the old set */\n    shot.src = src;\n  });\n}\n\n/* ---- The size bubble: one capsule that slides between the labels ----\n   Owner asked for \"a liquid bubble\" over 50ml / 10ml. What it is NOT is a\n   metaball: a real blob morph needs filter: blur() + contrast() on a container,\n   and that container would be a fixed bar sitting over a scrolling page - the\n   exact shape of the render-surface problem this theme spent a release removing\n   (see the notes on refract.liquid and the .dk-dock backdrop). It would come\n   straight back, on the one control a shopper taps repeatedly.\n\n   So the liquid is in the MOTION, not in a filter. One capsule, positioned from\n   the active label's own box, moving on transform + width with the --spring\n   curve, which overshoots and settles. Nothing composites, nothing blurs, and\n   the labels stay crisp while the page scrolls underneath.\n\n   Measured rather than declared because the labels are different widths (\"50ml\"\n   is wider than \"10ml\") and the copy comes from the variant titles, so there is\n   no width to hard-code. offsetLeft/offsetWidth read against the group, and the\n   group is the offsetParent, so a scroll position can never leak into the sum.\n\n   `instant` is for the first placement after the group is (re)built: set the\n   position with transitions off, force the frame, then arm them - otherwise the\n   bubble flies in from the left edge every time the island adopts a new card. */\nfunction placeSizeThumb(group, instant) {\n  if (!group) return;\n  const on = group.querySelector('.size-opt.is-active');\n  if (!on) { group.classList.remove('thumb-on'); return; }\n  /* Nothing to measure yet. The commerce face is display:none until the island\n     has actually switched modes, and syncCommerce builds the swatch BEFORE that\n     switch - so the first call after an adoption reads a zero box. Bail rather\n     than write width:0 into the custom property, which would leave the capsule\n     collapsed with its transition armed and animate it open on the next tap. */\n  if (!on.offsetWidth) { group.classList.remove('thumb-on'); return; }\n  if (instant) group.classList.remove('thumb-on');\n  group.style.setProperty('--thumb-x', on.offsetLeft + 'px');\n  group.style.setProperty('--thumb-w', on.offsetWidth + 'px');\n  if (instant) { void group.offsetWidth; }\n  group.classList.add('thumb-on');\n}\n\nfunction initDockCommerce() {\n  const dock = document.querySelector('[data-system=\"dock\"]');\n  if (!dock || !('IntersectionObserver' in window)) return;\n  const commerce = dock.querySelector('[data-dock-commerce]');\n  const priceEl = dock.querySelector('[data-dock-commerce-price]');\n  const variantsEl = dock.querySelector('[data-dock-commerce-variants]');\n  const addEl = dock.querySelector('[data-dock-card-add]');\n  if (!commerce || !priceEl || !variantsEl || !addEl) return;\n\n  /* The SERVER-rendered mode, read before anything can change it. On a PDP it\n     is 'atc' and this controller stands down entirely - the ritual lane at the\n     foot of a product page is full of cards, and letting them steal the bar\n     from the buy face (which initPdpBuyBar holds for the whole page) would put a\n     third thing in the rotation. Boot order matters: initDockCommerce runs\n     BEFORE initPdpBuyBar, so this read is always the server's. */\n  const base = dock.getAttribute('data-mode') || 'nav';\n  if (base !== 'nav') return;                  /* the PDP owns this face itself */\n\n  const narrow = matchMedia('(max-width: 1023px)');\n  /* ---- FULLY VISIBLE, OR NOT AT ALL (owner, 2026-08-19: \"that pill should\n     ONLY show if the product card is fully visible tho\") ----\n\n     It was 0.7, which was a compromise from when the card carried its own price\n     and the island was a convenience. It is not one now: the island is the only\n     place a price appears on a catalogue page, so a bar quoting a poster that\n     is 70% on screen is quoting one the shopper may be reading the bottom half\n     of while the top half of the next one is already in frame. The owner is\n     asking for the strict version and the strict version is also the correct\n     one - the answer belongs to the poster you are actually looking at.\n\n     0.99, not 1: an IntersectionObserver ratio is computed from\n     sub-pixel rects, and a card sized in dvh against a fractional device pixel\n     ratio lands on 0.9987 rather than 1 more often than not. Asking for exactly\n     1 is asking for a threshold that a correct layout misses at random.\n\n     OFF stays generous and is now further away, which matters more than it did:\n     the band between the two is what stops a card parked on the line from\n     strobing the bar, and a take-point this high needs a wide one under it. */\n  const ON = 0.99, OFF = 0.6;                  /* take at ON, let go under OFF */\n  /* Two cards can BOTH clear 70% - a 2-up grid mid-scroll, or a lane with a\n     wide viewport. Ratio alone then picks by a rounding difference, and the\n     island ends up speaking for whichever poster happens to be a pixel taller\n     on screen rather than the one being looked at. Anything inside this band of\n     the leader is treated as a tie and broken by distance to the middle of the\n     screen instead. */\n  const TIE = 0.1;\n  const ratio = new Map();\n  let io = null, count = 0, current = null, currentForm = null, sweep = 0;\n\n  /* THE FACE DOES NOT LET GO THE INSTANT A CARD LEAVES (owner, 2026-08-13:\n     \"this view should show the normal menu if you dont have actual product in\n     view for like 2 seconds or something like that\" + \"the transition between\n     products needs to be seamless on the dock\").\n\n     Those are one bug, not two. Dropping to nav the moment the top card fell\n     under OFF meant the GAP between two cards flipped the bar - commerce, nav,\n     commerce - in the few hundred milliseconds it takes to scroll from one\n     poster to the next. That flash is what stopped the swap feeling seamless,\n     and it also meant the bar's resting state on a catalogue was whichever\n     control happened to be under the thumb at the time.\n\n     So the release is deferred instead: nothing in view starts a 2s clock, and\n     any card taking over cancels it. Scrolling between posters now never\n     reaches the clock, so the face morphs A -> B directly; genuinely leaving\n     the products - a filter panel, the footer, a long editorial block - runs it\n     out and the menu comes back. Taking the bar is still instant; only letting\n     it go waits.\n\n     400ms, AND THE OFF BAND IS GONE (owner, 2026-08-19: \"if my screen idle on a\n     poston like this where nether product is fully in view that it will return\n     to normal until the full product is in view\").\n\n     The band was the thing keeping the face up in exactly the state the owner\n     screenshotted: parked between two posters with each about 60% visible, the\n     bar still quoting one of them. It existed to bridge the gap between two\n     cards during a scroll, but the drop CLOCK already does that job - and the\n     clock can tell idling from scrolling, which a visibility band cannot. So\n     the only question now is \"is a card fully in view\", and if the answer is no\n     for 400ms the island goes back to being navigation.\n\n     400 is the whole margin: a flick from one poster to the next reaches the\n     next card's 0.99 well inside it, so scrolling never sees the menu; stopping\n     between two does, which is the request. It went 2000 -> 500 -> 400 across\n     three notes, all in the same direction. */\n  const DROP_DELAY = 400;\n  let dropTimer = 0;\n  const cancelDrop = () => { if (dropTimer) { clearTimeout(dropTimer); dropTimer = 0; } };\n\n  /* The card already holds every fact and every real input the face needs, so\n     nothing here reaches for the catalogue index and no second cart state can\n     disagree with the product in front of the shopper. */\n  const syncCommerce = () => {\n    if (!currentForm) return;\n    const sourcePrice = currentForm.querySelector('[data-card-price]');\n    const sourceAdd = currentForm.querySelector('[data-add]');\n    const picked = currentForm.querySelector('[data-variant-input]');\n    const options = [...currentForm.querySelectorAll('[data-size-opt]')];\n    if (sourcePrice) {\n      const sourceWas = priceWasOf(sourcePrice);\n      paintPrice(priceEl, sourcePrice.textContent.trim(),\n                 sourceWas && !sourceWas.hidden ? sourceWas.textContent.trim() : '');\n      priceEl.removeAttribute('aria-hidden');\n    }\n\n    /* REBUILD ONLY WHEN THE SET ACTUALLY CHANGES. This used to replaceChildren()\n       on every sync, which was fine while the active size was nothing but an ink\n       colour - but the bubble is a moving element, and an element that is thrown\n       away and recreated cannot animate anywhere. Tapping 10ml rebuilt the group\n       and the capsule simply reappeared under the other label.\n       So the key is the list of variant ids: same product, same sizes, same\n       DOM - only the state and the bubble move. A different card rebuilds. */\n    /* The rung count drives the container query that chooses between the swatch\n       and the chip - two rungs fit in a narrower zone than three. See\n       .dock-commerce__pick in components.css. */\n    commerce.dataset.opts = String(options.length);\n\n    const key = options.map((o) => o.dataset.variantId || '').join(',');\n    const existing = variantsEl.querySelector('.sizes');\n    /* The label on the compact chip. Read from the card's own active option, so\n       it cannot disagree with the swatch beside it or the price above it. */\n    const activeLabel = () => {\n      const on = options.find((o) => o.classList.contains('is-active') ||\n                                     o.getAttribute('aria-pressed') === 'true');\n      return (on && on.textContent.trim()) ||\n             (picked && picked.dataset.variantLabel) ||\n             (options[0] && options[0].textContent.trim()) || '';\n    };\n    if (options.length > 1 && existing && existing.dataset.key === key) {\n      [...existing.querySelectorAll('.size-opt')].forEach((button, i) => {\n        const source = options[i];\n        if (!source) return;\n        const on = source.classList.contains('is-active') || source.getAttribute('aria-pressed') === 'true';\n        button.classList.toggle('is-active', on);\n        button.setAttribute('aria-pressed', on ? 'true' : 'false');\n      });\n      const chip = variantsEl.querySelector('[data-dock-pick] > span');\n      if (chip) chip.textContent = activeLabel();\n      placeSizeThumb(existing, false);\n    } else if (options.length > 1) {\n      /* BOTH READINGS ARE BUILT, and CSS shows one. Deciding here instead would\n         mean measuring the zone on every adoption and re-deciding on every\n         resize - and getting it wrong the one time it matters, because the\n         capsule has no box at all until setDockMode has run. The container\n         query has no such ordering problem and costs nothing. */\n      const pick = document.createElement('button');\n      pick.type = 'button';\n      pick.className = 'dock-commerce__pick';\n      pick.dataset.dockPick = '';\n      pick.setAttribute('aria-haspopup', 'dialog');\n      pick.innerHTML = '<span></span><svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" ' +\n        'stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n        '<path d=\"M4 7.5l6 6 6-6\"/></svg>';\n      pick.firstChild.textContent = activeLabel();\n\n      const group = document.createElement('span');\n      group.className = 'sizes';\n      group.dataset.key = key;\n      group.setAttribute('role', 'group');\n      group.setAttribute('aria-label', 'Size');\n      options.forEach((option) => {\n        const button = document.createElement('button');\n        button.type = 'button';\n        button.className = 'size-opt' + ('bundle' in option.dataset ? ' size-opt--bundle' : '');\n        button.dataset.dockCardSize = option.dataset.variantId || '';\n        button.textContent = option.textContent.trim();\n        if (option.dataset.bundleOf) button.title = option.dataset.bundleOf;\n        const on = option.classList.contains('is-active') || option.getAttribute('aria-pressed') === 'true';\n        button.classList.toggle('is-active', on);\n        button.setAttribute('aria-pressed', on ? 'true' : 'false');\n        group.appendChild(button);\n      });\n      variantsEl.replaceChildren(pick, group);\n      placeSizeThumb(group, true);\n    } else {\n      const variant = document.createElement('span');\n      variant.className = 'dock-commerce__variant';\n      variant.textContent = (picked && picked.dataset.variantLabel) ||\n        (options[0] && options[0].textContent.trim()) || '';\n      variantsEl.replaceChildren(variant);\n    }\n\n    const name = current && current.querySelector('.dk-id__name');\n    const title = name ? name.textContent.trim() : 'product';\n    addEl.setAttribute('aria-label', 'Add ' + title + ' to cart');\n    /* The magic button names the product twice over. Its label is the one\n       control in the bar whose whole meaning is \"this one\" - an unlabelled\n       sparkle would be the only thing down here a screen-reader user could not\n       identify - and its PROMPT is the question the tap actually asks.\n\n       The prompt matters more than it used to: as of 2026-08-19 the sparkle\n       sends rather than prefills (owner: \"it must initiate a chat with Jennie\n       and then Jennie must tell about the product\"), so a stale one would ask\n       Jennie about the poster before last. It is rewritten on every adoption,\n       right here, from the same card the price came from - the PDP's own copy\n       is server-rendered in dock.liquid and never changes, because a product\n       page is about one product.\n\n       Queried off `commerce` now, not off the dock: the pearl came inside the\n       capsule in the same pass. */\n    const magic = commerce.querySelector('[data-magic]');\n    if (magic) {\n      magic.setAttribute('aria-label', 'Ask Jennie about this scent - ' + title);\n      magic.setAttribute('data-chat-prompt',\n        'Tell me about ' + title + ' - how does it smell, and who is it for?');\n    }\n    addEl.disabled = !!(sourceAdd && sourceAdd.disabled);\n    addEl.setAttribute('aria-disabled', addEl.disabled ? 'true' : 'false');\n  };\n\n  /* .is-adopted is the visible tie between the bar and the poster it is buying\n     for. It is only meaningful now: while the card printed its own price and\n     Add there was nothing to point at, because the answer was already on the\n     card. Released in every exit path below - a stale adoption would leave two\n     cards claiming the island. */\n  const release = () => {\n    if (current) current.classList.remove('is-adopted');\n    current = null;\n    currentForm = null;\n    commerce.classList.remove('is-swapping');\n  };\n\n  const paint = (card, swapping) => {\n    const form = card.querySelector('[data-product-form]');\n    const name = card.querySelector('.dk-id__name');\n    if (!form || !name || !name.textContent.trim()) return false;\n    release();\n    current = card;\n    currentForm = form;\n    card.classList.add('is-adopted');\n    syncCommerce();\n    /* The new facts arrive in the same frame - this is not a sequenced\n       cross-fade, which would need the old price held on screen while the new\n       one is already the truth. It is a settle: the face lands from slightly\n       under, at reduced opacity, so a swap reads as the bar re-forming around\n       the next poster rather than as text being swapped out under the thumb.\n       Toggled off and reflowed first so it re-runs on every swap. */\n    if (swapping) {\n      commerce.classList.remove('is-swapping');\n      void commerce.offsetWidth;\n      commerce.classList.add('is-swapping');\n    }\n    return true;\n  };\n\n  /* The promoted controls are mirrors. Variant taps press the real card\n     option; Add submits the real form so bundling, feedback and cart sections\n     continue through initCart's single path. */\n  /* IS THE FACE WEARING THE CHIP RIGHT NOW? The container query owns that\n     answer, and this is the one honest way to read it back - one computed-style\n     read on a tap, which is not a scroll-frequency event. Deriving it here from\n     the zone's width instead would be a second copy of the thresholds in\n     components.css, and the two would drift the first time either moved. */\n  const isCompact = () => {\n    const pick = variantsEl.querySelector('[data-dock-pick]');\n    return !!pick && getComputedStyle(pick).display !== 'none';\n  };\n\n  commerce.addEventListener('click', (e) => {\n    const size = e.target.closest('[data-dock-card-size]');\n    if (size && currentForm) {\n      e.preventDefault();\n      const source = currentForm.querySelector('[data-size-opt][data-variant-id=\"' + cssAttr(size.dataset.dockCardSize) + '\"]');\n      if (source) { source.click(); syncCommerce(); }\n      return;\n    }\n    /* The chip is the other door to the same sheet - a shopper who wants to\n       change the size should not have to press a button labelled Add to do it. */\n    const pick = e.target.closest('[data-dock-pick]');\n    if (pick && current && openCardSheet) {\n      e.preventDefault();\n      openCardSheet(current, addEl);\n      return;\n    }\n    const trigger = e.target.closest('[data-dock-card-add]');\n    if (!trigger || !currentForm || trigger.disabled) return;\n    e.preventDefault();\n    /* ---- THE COMPACT FACE ASKS FIRST (owner, 2026-08-19: \"if you click on the\n       ad to cart that it gives your the options of above\") ----\n\n       Not a preference: when the swatch is hidden, the size the Add would post\n       is not on screen anywhere except a 40px chip, so a straight submit would\n       be buying a variant the shopper was never shown. The sheet is where the\n       choice lives on every other compact surface (duo, list), so this is the\n       same sheet, not a third one - and it fills from this same form, so the\n       cart path is still initCart's single one.\n\n       Only when there is a choice to make. A one-size product has nothing to\n       ask about and goes straight in, as it always did. */\n    if (isCompact() && openCardSheet && current &&\n        currentForm.querySelectorAll('[data-size-opt]').length > 1) {\n      openCardSheet(current, trigger);\n      return;\n    }\n    addTrigger = trigger;\n    submitForm(currentForm);\n    addTrigger = null;\n  });\n  document.addEventListener('click', (e) => {\n    const source = e.target.closest('[data-size-opt]');\n    if (source && currentForm && currentForm.contains(source)) syncCommerce();\n  });\n\n  const settle = () => {\n    let best = null, top = 0;\n    ratio.forEach((r, card) => { if (r > top) { top = r; best = card; } });\n    /* Break a near-tie by which card owns the MIDDLE of the screen. A candidate\n       must clear ON on its OWN - the band only decides between cards that are\n       each already dominant enough to be adopted, so this can never hand the\n       island a poster the 70% rule would have refused. Measured only when there\n       IS more than one, so the common case (a single dominant poster) costs no\n       geometry at all and a 2-up row costs two rect reads on cards the observer\n       has already confirmed are on screen. */\n    if (best && top >= ON) {\n      const tied = [];\n      ratio.forEach((r, card) => { if (r >= ON && r >= top - TIE) tied.push(card); });\n      if (tied.length > 1) {\n        const mid = window.innerHeight / 2;\n        let nearest = null, closest = Infinity;\n        tied.forEach((card) => {\n          const box = card.getBoundingClientRect();\n          const d = Math.abs(box.top + box.height / 2 - mid);\n          if (d < closest) { closest = d; nearest = card; }\n        });\n        if (nearest) best = nearest;\n      }\n    }\n    if (best && top >= ON && best !== current) {\n      cancelDrop();\n      /* A swap is an adoption that REPLACES one - the very first take on a page\n         is not one, and animating that would flash the face the moment the bar\n         appeared. */\n      if (paint(best, !!current)) {\n        setDockMode(dock, 'card');\n        /* Now, not inside paint(). The face is display:none until this line\n           runs, so the size bubble had no box to measure while the swatch was\n           being built - this is the first moment it can be placed. */\n        placeSizeThumb(variantsEl.querySelector('.sizes'), true);\n      }\n    } else if (best && top >= ON) {\n      /* Already showing this one and it is still fully in view. */\n      cancelDrop();\n    } else if (current && !dropTimer) {\n      dropTimer = setTimeout(() => {\n        dropTimer = 0;\n        release();\n        setDockMode(dock, base);\n      }, DROP_DELAY);\n    }\n  };\n\n  const observe = () => {\n    const cards = document.querySelectorAll('[data-card]');\n    count = cards.length;\n    cards.forEach((c) => { if (!ratio.has(c)) { ratio.set(c, 0); io.observe(c); } });\n  };\n  const start = () => {\n    if (io) return;\n    io = new IntersectionObserver((entries) => {\n      entries.forEach((en) => {\n        if (!en.isIntersecting) { ratio.set(en.target, 0); return; }\n        /* \"Fully visible\" has to mean something for a card TALLER than the\n           screen, or the strict take-point above would simply never fire on\n           one and the island would go quiet. A poster that is bigger than the\n           viewport is as visible as it can be when it fills the viewport, so\n           the denominator is whichever is smaller. For a card that fits - every\n           one-up collection card, which is sized in dvh precisely so it does -\n           this is the plain ratio, measured on height because these cards are\n           full-bleed and the width term cancels. */\n        const own = en.boundingClientRect.height;\n        const room = Math.min(own, window.innerHeight) || 1;\n        ratio.set(en.target, Math.min(1, en.intersectionRect.height / room));\n      });\n      settle();\n    }, {\n      /* A ladder rather than the four points the old thresholds gave, because\n         the number above is no longer the browser's own ratio: on a card taller\n         than the screen the browser's ratio tops out well below ON, so a\n         threshold list built from ON and OFF would stop delivering updates\n         exactly where this needs them. The rungs are cheap - the callback is\n         off the render path and only fires as a card crosses one. */\n      threshold: [0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1]\n    });\n    observe();\n  };\n  const stop = () => {\n    if (io) { io.disconnect(); io = null; }\n    cancelDrop();          /* a pending release must not fire into a torn-down face */\n    release();\n    ratio.clear(); count = 0;\n    setDockMode(dock, base);\n  };\n\n  /* ---- THE COMPACT COLLECTION VIEWS TAKE THE FACE AWAY ----\n     (owner, 2026-08-14: \"On product all views when 2 colum and that other list\n     view, the dock add to cart needs to be away and then these cards need to\n     ahve a add to cart icon on it\".)\n\n     Not a preference - the mechanism genuinely does not work there. This\n     controller sells for whichever card DOMINATES the screen, and duo puts two\n     posters side by side at identical heights while list puts five or six rows\n     on screen at once. Neither has a dominant card, so the bar would price one\n     of a pair under both of them, or flip between rows as they cross 70%.\n     Raising the threshold cannot fix that; the question itself is wrong at\n     those layouts. The cards carry their own trigger instead (initCardAdd).\n\n     Single view is untouched: one poster is one screenful there, which is the\n     shape this face was designed around.\n\n     Read from the DOM rather than from initCollectionFilter's state, so the\n     boot order of the two does not matter and a page with no view switcher at\n     all (a lane, the home page) simply reports 'single'. */\n  const grid = document.querySelector('.cx[data-view]');\n  const dockableView = () => !grid || grid.dataset.view === 'single';\n\n  const syncViewport = () => ((narrow.matches && dockableView()) ? start() : stop());\n  syncViewport();\n  narrow.addEventListener('change', syncViewport);\n  /* attributeFilter, so this fires on a view change and on nothing else - the\n     grid mutates constantly as filters hide and show cards. */\n  if (grid && 'MutationObserver' in window) {\n    new MutationObserver(syncViewport)\n      .observe(grid, { attributes: true, attributeFilter: ['data-view'] });\n  }\n\n  /* Same re-measure the PDP swatch does, for the same two reasons - a webfont\n     landing after the first adoption, and a resize moving the elastic middle. */\n  if (document.fonts && document.fonts.ready) {\n    document.fonts.ready.then(() => placeSizeThumb(variantsEl.querySelector('.sizes'), true));\n  }\n  onResizeSettled(() => placeSizeThumb(variantsEl.querySelector('.sizes'), true));\n\n  /* Lanes and grids grow: the ritual lane fills itself from the index, the\n     collection loads more pages. Rather than watch the whole document for\n     mutations, notice on scroll that the count moved - at most once a second,\n     and only a length read. */\n  document.addEventListener('scroll', () => {\n    if (!io || sweep) return;\n    sweep = setTimeout(() => {\n      sweep = 0;\n      if (document.querySelectorAll('[data-card]').length !== count) observe();\n    }, 1000);\n  }, { passive: true });\n}\n\n/* ---- PDP: the buy bar that follows the column down (>=1024px) ----\n   The island retires at 1024px and the media is pinned, so past the fold the\n   desktop PDP had no Add to Cart at all - the shopper read the notes, the trust\n   rows and the ingredients and then had to scroll back up to buy. This watches\n   the real button and hands over the moment it leaves the screen; the bar is a\n   remote control for that same form (data-dock-add), never a second one.\n\n   When the middle of the page carries a lot of copy the bar drops its name and\n   price and becomes the button alone - by then it has been on screen for most\n   of the read and the name is just repeating the <h1> above it.\n\n   ONE OBSERVER, TWO SURFACES (owner, 2026-08-12: \"when the add to cart is on\n   the screen like in the middle, the dock can just show its normal things\").\n   The question both surfaces are asking is identical - \"can the shopper reach\n   the real button right now?\" - so they share the answer rather than each\n   running their own IntersectionObserver on the same element. While the button\n   is on screen the desktop bar stays hidden AND the island goes back to being\n   navigation; the moment it scrolls away the bar arrives and the island becomes\n   the buy face. Which of the two is doing the work is purely a width question,\n   so the observer runs at every width and each surface takes its own half. */\nfunction initPdpBuyBar() {\n  /* THERE IS NO DESKTOP BUY BAR ANY MORE (owner, 2026-08-18: \"when scrolling\n     down remove that fixed sticky add to cart thing when not on the top\").\n\n     This used to run two things: the fixed .pdp__buybar pill at >=1024px, and\n     the mobile island's buy face. The bar is gone with its markup and its CSS -\n     it was a patch for the pinned media (photograph holds still, long buy\n     column scrolls past it, the only control that matters ends up two screens\n     up), and nothing pins now. 04 \u00A710 always said it: the dock is the sticky\n     buy bar and there is no second one.\n\n     What is left is that one line - a product page's dock stays in buy mode.\n     The function keeps its name because the boot order below depends on it\n     (initDockCommerce runs first and reads the face this sets). */\n  const dock = document.querySelector('[data-system=\"dock\"]');\n  /* Only a dock the SERVER put in buy mode may be toggled - on any other\n     template 'atc' is not a face it has. */\n  if (!dock || dock.getAttribute('data-mode') !== 'atc') return;\n  if (!document.querySelector('[data-pdp-form]')) return;\n  setDockMode(dock, 'atc');\n}\n\n/* ---- The island's size swatch: a MIRROR of the PDP's radios ----\n   Two controls, one source of truth. Tapping a dock size checks the matching\n   radio in [data-pdp-form] and fires `change`, so initPdpVariant (price, the\n   Payflex quarter, the buy-bar price) and initSeg (the sliding thumb) all run\n   exactly as if the on-page seg had been used - and the reverse direction is\n   wired too, so using the on-page seg moves the island. There is never a second\n   variant value to keep in step, only a second way to set the one that exists. */\nfunction initDockSize() {\n  const wrap = document.querySelector('[data-dock-sizes]');\n  const form = document.querySelector('[data-pdp-form]');\n  if (!wrap || !form) return;\n  const buttons = [...wrap.querySelectorAll('[data-dock-size]')];\n  let first = true;\n\n  /* Same bubble as the catalogue face. The PDP's swatch is server-rendered and\n     never rebuilt, so the capsule only has to be placed once and then moved -\n     `first` is what tells it to arrive without sliding in from the edge. */\n  /* The compact face's label. Server-rendered with the opening rung and moved\n     by this, so the chip and the swatch are two readings of one value and a\n     narrow PDP never reports a size the Add is not about to post. */\n  const pickLabel = document.querySelector('[data-dock-pick-label]');\n\n  const sync = () => {\n    const picked = form.querySelector('input[name=\"id\"]:checked');\n    const id = picked ? picked.value : '';\n    let label = '';\n    buttons.forEach((b) => {\n      const on = b.dataset.dockSize === id;\n      if (on) label = b.textContent.trim();\n      b.classList.toggle('is-active', on);\n      b.setAttribute('aria-pressed', on ? 'true' : 'false');\n    });\n    if (pickLabel && label) pickLabel.textContent = label;\n    placeSizeThumb(wrap, first);\n    first = false;\n  };\n\n  /* ---- The chip opens THE SHEET (owner, 2026-08-19: \"This one needs to\n     follow the same schematic / style if possible when selecting the variant\",\n     against a screenshot of the card add sheet - and, from the note before it,\n     \"can the dock expand tho and show better nd just the product name\").\n\n     Both notes describe the same object. The sheet IS the dock expanding: it\n     rises from the bar's own edge, it leads with the product name, and it is\n     the exact surface the duo and list cards already select sizes in - name,\n     the rungs with their prices, the black active pill, price + Add. The\n     floating swatch popover that briefly lived here was a second, poorer\n     answer to the same question and is deleted.\n\n     openPdpSheet is initCardAdd's PDP path: the sheet fills from this page's\n     own radios and submits this page's own form, so there is still exactly one\n     variant state and one cart path. */\n  const pick = document.querySelector('[data-dock-pick-pdp]');\n  if (pick) {\n    pick.addEventListener('click', (e) => {\n      e.preventDefault();\n      if (openPdpSheet) openPdpSheet(document.querySelector('[data-dock-add]') || pick);\n    });\n  }\n\n  wrap.addEventListener('click', (e) => {\n    const btn = e.target.closest('[data-dock-size]');\n    if (!btn) return;\n    const radio = form.querySelector('input[name=\"id\"][value=\"' + cssAttr(btn.dataset.dockSize) + '\"]');\n    if (!radio || radio.checked) return;\n    radio.checked = true;\n    radio.dispatchEvent(new Event('change', { bubbles: true }));\n  });\n\n\n  form.addEventListener('change', sync);\n  sync();\n\n  /* The capsule is measured from rendered text, so anything that changes text\n     metrics after the first placement has to move it. Two things do: a webfont\n     arriving (this swatch is placed at boot, which can be before Inter has\n     swapped in) and a resize, since the type scale is fluid and the middle zone\n     is elastic. Neither is a scroll-frequency event, so a straight re-measure\n     is cheaper than trying to be clever about it. */\n  if (document.fonts && document.fonts.ready) {\n    document.fonts.ready.then(() => placeSizeThumb(wrap, true));\n  }\n  onResizeSettled(() => placeSizeThumb(wrap, true));\n}\n\n/* ---- <dialog> on a browser without one: Safari before 15.4 ----\n   There <dialog> parses as an unknown element with neither showModal() nor\n   close(), so an unguarded call is a TypeError that also kills whatever the\n   same click handler was going to do next - on those phones the burger did\n   nothing at all. The cart drawer, the search sheet and the card sheet\n   already guard themselves; these two are the search sheet's pattern\n   (initSearch open/close) lifted out so every other sheet shares it: open\n   as a plain in-flow element via the open attribute, close by taking the\n   attribute away. Non-modal, no focus trap, but reachable. */\nfunction dialogShow(el) {\n  if (!el) return;\n  if (typeof el.showModal === 'function') { if (!el.open) el.showModal(); }\n  else el.setAttribute('open', '');\n}\nfunction dialogShut(el) {\n  if (!el) return;\n  if (el.open) el.close(); else el.removeAttribute('open');\n}\n\n/* ---- html.in-room: the rooms' :has() fallback ----\n   duet.css, finder.css and components.css hide the island (.dock) and\n   Jennie's hail behind html:has(.dt), html:has(.sf) and html:has(.cx.is-open).\n   Safari before 15.4 has no :has(), so on those phones the island floated over\n   the rooms and the filter sheet. This class says the same thing in a\n   selector every browser reads, and theme.css carries the one fallback rule\n   for it, scoped to browsers WITHOUT :has() so the two can never disagree\n   (the finder's is-flat and landscape reverts stay the :has rules' own).\n   Mirrors the CSS conditions exactly: a flat finder has given the chrome\n   back, so it is not a room. The rooms are server-rendered, so one call at\n   boot sees them; the finder's flattening and the filter sheet's open/close\n   are the only two things that change the answer afterwards. */\nfunction syncInRoom() {\n  const inRoom = !!document.querySelector('.dt, .sf:not(.is-flat), .cx.is-open');\n  document.documentElement.classList.toggle('in-room', inRoom);\n}\n\n/* ---- Top-layer overlays: menu sheet + cart drawer (<dialog>) ---- */\nfunction initOverlays() {\n  const menu = document.querySelector('[data-menu]');\n  const drawer = document.querySelector('[data-drawer]');\n  document.addEventListener('click', (e) => {\n    if (e.target.closest('[data-menu-open]') && menu) { e.preventDefault(); dialogShow(menu); }\n    if (e.target.closest('[data-menu-close]') && menu) dialogShut(menu);\n    if (e.target.closest('[data-drawer-open]') && drawer) { e.preventDefault(); openDrawer(); }\n    /* Scoped to the dialog on purpose. /cart renders the same bag snippet, and\n       anything in it that ever gains a close control must not reach in here and\n       shut a sheet the shopper is not looking at. */\n    const closer = e.target.closest('[data-drawer-close]');\n    if (closer && drawer && drawer.contains(closer)) dialogShut(drawer);\n    /* click on the backdrop closes (dialog itself is the target) */\n    if (e.target === menu) dialogShut(menu);\n    if (e.target === drawer) dialogShut(drawer);\n  });\n}\n\n/* ---- Mobile menu sheet: drill-down panes, drag-to-dismiss, inertness ----\n   The sheet is a native modal <dialog>, so the focus trap, the scroll lock\n   and the top-layer stacking all come from the platform - nothing in here\n   touches scrollTop or body position, which is why opening it can never\n   jump the page. Open/close stays in initOverlays; this controller owns\n   the state INSIDE the sheet and mirrors it back onto the page. */\nfunction initMenuSheet() {\n  const sheet = document.querySelector('[data-menu]');\n  if (!sheet) return;\n  const panes = sheet.querySelector('[data-menu-panes]');\n  if (!panes) return;\n  const scroller = sheet.querySelector('[data-menu-scroll]');\n  const grip = sheet.querySelector('[data-menu-grip]');\n  const back = sheet.querySelector('[data-menu-back]');\n  const backLabel = sheet.querySelector('[data-menu-back-label]');\n  const home = sheet.querySelector('[data-menu-home]');\n  const title = sheet.querySelector('[data-menu-title]');\n  const supportsInert = typeof HTMLElement !== 'undefined' &&\n    Object.prototype.hasOwnProperty.call(HTMLElement.prototype, 'inert');\n  /* one entry per level: the pane name, plus the row that opened it so\n     Back can put focus exactly where the shopper left it */\n  const stack = [{ name: 'root', from: null }];\n\n  const paneOf = (name) => panes.querySelector('[data-menu-pane=\"' + cssAttr(name) + '\"]');\n  const labelOf = (name) => { const p = paneOf(name); return (p && p.dataset.menuLabel) || 'Menu'; };\n\n  function show(name, isBack, moveFocus) {\n    panes.querySelectorAll('[data-menu-pane]').forEach((p) => {\n      p.toggleAttribute('hidden', p.dataset.menuPane !== name);\n    });\n    panes.classList.toggle('is-back', !!isBack);\n    if (title) title.textContent = labelOf(name);\n    if (back) back.toggleAttribute('hidden', name === 'root');\n    /* Home and Back share the bar's leading slot (owner, 2026-08-19: \"On main\n       menu we require a way to go back to the home page\"). At the root there is\n       nothing to go back TO, so the wordmark holds the corner; one level in,\n       Back takes it - up is what the shopper means there, and Home is still\n       reachable through it. Toggled here so the two can never both be showing\n       or both be gone. */\n    if (home) home.toggleAttribute('hidden', name !== 'root');\n    if (backLabel) {\n      const prev = stack[stack.length - 2];\n      backLabel.textContent = labelOf(prev ? prev.name : 'root');\n    }\n    panes.querySelectorAll('[data-menu-drill]').forEach((b) => {\n      b.setAttribute('aria-expanded', b.dataset.menuDrill === name ? 'true' : 'false');\n    });\n    if (scroller) scroller.scrollTop = 0;\n    /* a drill hides the button that was focused, so hand focus to the pane */\n    if (moveFocus) {\n      const target = paneOf(name);\n      const first = target && target.querySelector('a[href], button:not([hidden])');\n      if (first) first.focus({ preventScroll: true });\n    }\n  }\n  function drill(name, from) {\n    if (!paneOf(name)) return;\n    stack.push({ name: name, from: from || null });\n    show(name, false, true);\n  }\n  function goBack() {\n    if (stack.length < 2) return;\n    const leaving = stack.pop();\n    show(stack[stack.length - 1].name, true, false);\n    /* land the shopper back on the row they opened, not the top of the list */\n    if (leaving.from && leaving.from.isConnected) leaving.from.focus({ preventScroll: true });\n  }\n\n  sheet.addEventListener('click', (e) => {\n    const open = e.target.closest('[data-menu-drill]');\n    if (open && sheet.contains(open)) { e.preventDefault(); drill(open.dataset.menuDrill, open); return; }\n    if (e.target.closest('[data-menu-back]')) { e.preventDefault(); goBack(); return; }\n    /* the cart and search actions hand over to their own dialog - the sheet\n       steps aside so two modals are never stacked on top of each other */\n    if (e.target.closest('[data-drawer-open], [data-search-open]')) dialogShut(sheet);\n  });\n\n  /* Escape inside a sub-pane walks back one level before it closes. */\n  sheet.addEventListener('cancel', (e) => {\n    if (stack.length > 1) { e.preventDefault(); goBack(); }\n  });\n\n  /* ---- the rest of the page: inert while the sheet is up. The platform\n     already blocks it, this is the belt for browsers that do not. Anything\n     holding its own <dialog> is skipped - the cart drawer and the search\n     popup have to stay operable when the sheet hands over to them. ---- */\n  function outside() {\n    return Array.from(document.body.children).filter((n) =>\n      n !== sheet && n.tagName !== 'SCRIPT' && n.tagName !== 'DIALOG' &&\n      !n.querySelector('dialog') && !n.hasAttribute('data-toast-region'));\n  }\n  function setAside(on) {\n    outside().forEach((n) => {\n      if (supportsInert) n.inert = on;\n      else if (on) n.setAttribute('aria-hidden', 'true');\n      else n.removeAttribute('aria-hidden');\n    });\n  }\n\n  function resetDrag() {\n    sheet.style.setProperty('--menu-drag', '0px');\n    sheet.classList.remove('is-dragging', 'is-settling');\n  }\n  function syncTriggers(open) {\n    document.querySelectorAll('[data-menu-open][aria-expanded]').forEach((t) => {\n      t.setAttribute('aria-expanded', open ? 'true' : 'false');\n    });\n  }\n\n  /* showModal() fires no event, so the open attribute is the signal. */\n  new MutationObserver(() => {\n    if (sheet.hasAttribute('open')) {\n      stack.length = 0; stack.push({ name: 'root', from: null });\n      show('root', false, false);\n      resetDrag();\n      setAside(true);\n      syncTriggers(true);\n    } else {\n      resetDrag();\n      setAside(false);\n      syncTriggers(false);\n    }\n  }).observe(sheet, { attributes: true, attributeFilter: ['open'] });\n\n  /* ---- swipe down to dismiss. Only from the grip, which is touch-action:\n     none, so a drag can never fight the pane scroller - that is what keeps\n     it free of jank. Reduced motion opts out entirely. ---- */\n  if (grip && !reduceMotion.matches) {\n    let startY = 0, dragY = 0, dragging = false, pid = null;\n    const end = () => {\n      if (!dragging) return;\n      dragging = false;\n      if (pid != null && grip.hasPointerCapture(pid)) grip.releasePointerCapture(pid);\n      sheet.classList.remove('is-dragging');\n      sheet.classList.add('is-settling');\n      if (dragY > 110) {\n        sheet.style.setProperty('--menu-drag', (sheet.offsetHeight || 600) + 'px');\n        setTimeout(() => dialogShut(sheet), 220);\n      } else {\n        sheet.style.setProperty('--menu-drag', '0px');\n      }\n    };\n    grip.addEventListener('pointerdown', (e) => {\n      if (e.button != null && e.button > 0) return;\n      dragging = true; startY = e.clientY; dragY = 0; pid = e.pointerId;\n      try { grip.setPointerCapture(pid); } catch (_) { /* pen/mouse edge cases */ }\n      sheet.classList.add('is-dragging');\n      sheet.classList.remove('is-settling');\n    });\n    grip.addEventListener('pointermove', (e) => {\n      if (!dragging) return;\n      dragY = Math.max(0, e.clientY - startY);\n      sheet.style.setProperty('--menu-drag', dragY + 'px');\n    });\n    grip.addEventListener('pointerup', end);\n    grip.addEventListener('pointercancel', end);\n  }\n}\n\n/* ---- F2: a <details> that a link can open ----\n\n   The account pages put the preferences block and the \"Add a new address\"\n   form behind disclosures (owner: \"The preferences block must not be shown\n   directly - put it behind a CTA that opens it. Same for the address book\").\n   Everything about that is free except one thing: the addresses page has\n   linked to `#new-address` from its own header since it was written, and a\n   fragment that lands on a CLOSED <details> scrolls to a shut box and looks\n   like a broken link.\n\n   THE ID IS ON THE <details> ITSELF, which is what makes this three lines\n   instead of a map of anchors to panels: anything the page can point at is\n   either a disclosure or it is not, and only the first case is ours.\n\n   CRITICAL TIER, not idle. The rule in the boot block is \"does a tap on this\n   do nothing if it has not run yet\" - \"Add an address\" is a control a shopper\n   arrives intending to press, and a second of dead link reads as a broken\n   page. (This used to be the whole reason it was split out of initAcctAuth,\n   which sat in idle. initAcctAuth is critical too now, for the same rule -\n   it owns the recover dialog, the preference pills and the register form's\n   consent - so the split is only about keeping a whole-theme hash handler\n   separate from an account-only controller.) Without JS entirely the browser still\n   scrolls to the right section and the summary is one press away, which is a\n   fallback rather than a dead end. */\nfunction initAcctDisclose() {\n  const openHash = () => {\n    const id = (location.hash || '').slice(1);\n    if (!id) return;\n    let el = null;\n    try { el = document.getElementById(decodeURIComponent(id)); } catch (e) { return; }\n    if (!el || el.tagName !== 'DETAILS') return;\n    el.open = true;\n    /* The section carries tabindex=\"-1\" so the keyboard lands inside what it\n       just opened rather than back at the top of the document. preventScroll\n       because the browser's own fragment jump is already doing that, and two\n       scrolls in one frame fight. */\n    try { el.focus({ preventScroll: true }); } catch (e) { /* older browsers */ }\n  };\n\n  /* Same-document links only. `a[href^=\"#\"]` rather than a href containing a\n     hash: `/somewhere-else#new-address` must not open this page's panel. */\n  document.addEventListener('click', (e) => {\n    const a = e.target.closest('a[href^=\"#\"]');\n    if (!a) return;\n    const id = (a.getAttribute('href') || '').slice(1);\n    if (!id) return;\n    const el = document.getElementById(id);\n    /* Not prevented: the browser still does the fragment jump, which is the\n       part that gets the panel on screen. This only makes it open first. */\n    if (el && el.tagName === 'DETAILS') el.open = true;\n  });\n\n  window.addEventListener('hashchange', openHash);\n  openHash();\n}\n\n/* ---- The fragrance-notes system (PDP) - ports the behaviour of\n   old_reference/fragrance-notes-widget.html onto server-rendered\n   markup: hover popover on desktop (top layer), bottom sheet on\n   touch, click-through deep-links into the filtered shop. ---- */\nfunction initNotes() {\n  const root = document.querySelector('[data-notes]');\n  if (!root) return;\n  const supportsPopover = typeof HTMLElement !== 'undefined' &&\n    Object.prototype.hasOwnProperty.call(HTMLElement.prototype, 'popover');\n  if (supportsPopover) {\n    root.querySelectorAll('[data-note-detail]').forEach((d) => d.setAttribute('popover', 'manual'));\n  }\n  const scrim = document.createElement('div');\n  scrim.className = 'note-scrim';\n  document.body.appendChild(scrim);\n  let openNote = null;\n  let hoverTimer = null;\n\n  const isTouch = () => matchMedia('(hover: none)').matches;\n  const detailOf = (note) => note && note.querySelector('[data-note-detail]');\n\n  /* Desktop placement: top-layer popovers position against the viewport,\n     so compute fixed coords from the note's rect; flip below when there\n     is no room above; clamp inside the viewport. */\n  function position(note, detail) {\n    if (!supportsPopover || isTouch()) return;\n    detail.style.position = 'fixed';\n    detail.style.right = 'auto';\n    const r = note.getBoundingClientRect();\n    const dw = detail.offsetWidth, dh = detail.offsetHeight;\n    const vw = window.innerWidth, vh = window.innerHeight, gap = 12, m = 8;\n    let below = false;\n    let top = r.top - dh - gap;\n    if (top < m) { top = r.bottom + gap; below = true; }\n    if (top + dh > vh - m) top = Math.max(m, vh - dh - m);\n    let left = r.left;\n    if (left + dw > vw - m) left = r.right - dw;\n    if (left < m) left = m;\n    if (left + dw > vw - m) left = vw - dw - m;\n    detail.style.top = top + 'px';\n    detail.style.left = left + 'px';\n    detail.style.transformOrigin = below ? '24px top' : '24px bottom';\n  }\n\n  function open(note) {\n    const detail = detailOf(note);\n    if (!detail) return;\n    if (openNote && openNote !== note) close(openNote);\n    openNote = note;\n    note.classList.add('is-open');\n    if (isTouch()) {\n      detail.style.position = detail.style.top = detail.style.left =\n        detail.style.right = detail.style.transformOrigin = '';\n      scrim.classList.add('is-visible');\n      document.body.classList.add('no-scroll');\n    }\n    if (supportsPopover) { try { if (!detail.matches(':popover-open')) detail.showPopover(); } catch (e) {} }\n    position(note, detail);\n  }\n  function close(note) {\n    if (!note) return;\n    const detail = detailOf(note);\n    if (detail && supportsPopover) { try { if (detail.matches(':popover-open')) detail.hidePopover(); } catch (e) {} }\n    note.classList.remove('is-open');\n    if (openNote === note) openNote = null;\n  }\n  function closeAll() {\n    root.querySelectorAll('[data-note].is-open').forEach(close);\n    scrim.classList.remove('is-visible');\n    document.body.classList.remove('no-scroll');\n  }\n\n  scrim.addEventListener('click', closeAll);\n  document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeAll(); });\n\n  /* desktop hover - intent timer bridges the gap to the popover */\n  const scheduleClose = () => {\n    clearTimeout(hoverTimer);\n    hoverTimer = setTimeout(() => { if (openNote) close(openNote); }, 130);\n  };\n  root.addEventListener('mouseover', (e) => {\n    if (isTouch()) return;\n    const note = e.target.closest('[data-note]');\n    if (!note || !root.contains(note)) return;\n    clearTimeout(hoverTimer);\n    if (openNote !== note) open(note);\n  });\n  root.addEventListener('mouseout', (e) => {\n    if (isTouch()) return;\n    const note = e.target.closest('[data-note]');\n    if (!note) return;\n    if (e.relatedTarget && note.contains(e.relatedTarget)) return;\n    scheduleClose();\n  });\n\n  /* keyboard focus */\n  root.addEventListener('focusin', (e) => {\n    if (isTouch()) return;\n    const note = e.target.closest('[data-note]');\n    if (note) open(note);\n  });\n  root.addEventListener('focusout', (e) => {\n    if (isTouch()) return;\n    const note = e.target.closest('[data-note]');\n    if (!note) return;\n    if (e.relatedTarget && note.contains(e.relatedTarget)) return;\n    close(note);\n  });\n\n  /* touch: tap toggles the bottom sheet (the note link is a real\n     anchor, so its default navigation is intercepted here - the\n     sheet's \"Browse this note\" CTA is the touch click-through) */\n  root.addEventListener('click', (e) => {\n    if (!isTouch()) return;\n    if (e.target.closest('[data-note-detail]')) return;\n    const note = e.target.closest('[data-note]');\n    if (!note) return;\n    e.preventDefault();\n    const willOpen = note !== openNote;\n    closeAll();\n    if (willOpen) open(note);\n  });\n  /* desktop: the anchor navigates natively; clicking the row around\n     it follows the same deep-link */\n  root.addEventListener('click', (e) => {\n    if (isTouch()) return;\n    if (e.target.closest('[data-note-detail]')) return;\n    if (e.target.closest('[data-note-link]')) return;   /* native nav */\n    const note = e.target.closest('[data-note]');\n    if (!note) return;\n    const url = note.getAttribute('data-shop-url');\n    if (url) window.location.href = url;\n  });\n\n  /* keep an open desktop popover glued to its note on scroll/resize */\n  const reflow = () => {\n    if (openNote && !isTouch()) {\n      const d = detailOf(openNote);\n      if (d) position(openNote, d);\n    }\n  };\n  window.addEventListener('scroll', reflow, true);\n  onResize(reflow);\n}\n\n/* ---- The sort control, in the store's own material (owner, 2026-08-19:\n   \"Align this to the right ... and improve the dropdown\") ----\n\n   A <select> paints its open list in the operating system's chrome - the blue\n   band in the owner's screenshot - and a stylesheet cannot reach inside it.\n   This puts a real listbox over the top of it.\n\n   THE SELECT IS STILL THE VALUE. Nothing here holds a sort of its own: a choice\n   sets `select.value` and fires `change`, which is the one listener\n   initCollectionFilter binds, the same event the URL restore replays, and the\n   same element a no-JS visit uses directly. So there is one source of truth and\n   this is a second way to set it - the same contract the island has with a\n   card's form.\n\n   It also listens back: restoring ?sort= from the URL sets the select\n   programmatically, which fires no event, so the trigger reads its label from\n   the select on every open as well as on every change.\n\n   Bound at boot rather than inside initCollectionFilter, so it does not wait on\n   the 156KB engine to arrive before the control looks like ours. */\nfunction initSortMenu() {\n  document.querySelectorAll('[data-sortmenu]').forEach((wrap) => {\n    const sel = wrap.querySelector('select');\n    if (!sel || !sel.options.length) return;\n\n    const btn = document.createElement('button');\n    btn.type = 'button';\n    btn.className = 'cx-sort__btn';\n    btn.setAttribute('aria-haspopup', 'listbox');\n    btn.setAttribute('aria-expanded', 'false');\n    const val = document.createElement('span');\n    val.className = 'cx-sort__val';\n    btn.appendChild(val);\n    btn.insertAdjacentHTML('beforeend',\n      '<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" ' +\n      'stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 7.5l6 6 6-6\"/></svg>');\n\n    const menu = document.createElement('div');\n    menu.className = 'cx-sort__menu';\n    menu.setAttribute('role', 'listbox');\n    menu.setAttribute('aria-label', 'Sort scents');\n    menu.hidden = true;\n    const opts = [...sel.options].map((o) => {\n      const row = document.createElement('button');\n      row.type = 'button';\n      row.className = 'cx-sort__opt';\n      row.setAttribute('role', 'option');\n      row.dataset.value = o.value;\n      row.innerHTML = '<span>' + o.textContent.trim() + '</span>' +\n        '<span class=\"cx-sort__tick\" aria-hidden=\"true\"><svg viewBox=\"0 0 20 20\" fill=\"none\" ' +\n        'stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' +\n        '<path d=\"M4.4 10.4l3.6 3.6 7.6-7.6\"/></svg></span>';\n      menu.appendChild(row);\n      return row;\n    });\n\n    /* ---- THE TRIGGER IS THE ONLY THING THAT GETS THE SHORT NAME ----\n       (owner, 2026-08-27: the pill takes a lot of space on a phone.)\n\n       main-collection puts a `data-short` on every option - \"Price up\" for\n       \"Price, low to high\", and so on - and it is printed HERE and nowhere\n       else. The menu rows are built from textContent above, so they keep the\n       full sentence, and the aria-label below keeps it too: the trigger reads\n       as \"Sort scents: Price, low to high\" however narrow the screen is.\n\n       An option with no data-short simply keeps its full label, so a sixth\n       sort added to that select renders correctly before anybody remembers\n       this function exists. */\n    const terse = matchMedia('(max-width: 559.98px)');\n\n    /* The accessible name of the trigger has to say what it IS as well as what\n       it is set to - \"Featured\" alone is a word with no job attached. */\n    const paint = () => {\n      const picked = sel.options[sel.selectedIndex] || sel.options[0];\n      const full = picked.textContent.trim();\n      val.textContent = (terse.matches && picked.dataset.short) ? picked.dataset.short : full;\n      btn.setAttribute('aria-label', 'Sort scents: ' + full);\n      opts.forEach((o) => o.setAttribute('aria-selected', o.dataset.value === sel.value ? 'true' : 'false'));\n    };\n\n    /* Rotating a phone crosses this boundary, and the label has to follow. */\n    terse.addEventListener('change', paint);\n\n    let cursor = -1;\n    const point = (i) => {\n      cursor = Math.max(0, Math.min(opts.length - 1, i));\n      opts.forEach((o, n) => o.classList.toggle('is-cursor', n === cursor));\n      opts[cursor].focus();\n    };\n    const open = () => {\n      if (!menu.hidden) return;\n      paint();\n      menu.hidden = false;\n      btn.setAttribute('aria-expanded', 'true');\n      point(Math.max(0, sel.selectedIndex));\n    };\n    const close = (refocus) => {\n      if (menu.hidden) return;\n      menu.hidden = true;\n      btn.setAttribute('aria-expanded', 'false');\n      opts.forEach((o) => o.classList.remove('is-cursor'));\n      if (refocus) btn.focus();\n    };\n    const choose = (value) => {\n      if (sel.value !== value) {\n        sel.value = value;\n        sel.dispatchEvent(new Event('change', { bubbles: true }));\n      }\n      paint();\n      close(true);\n    };\n\n    btn.addEventListener('click', () => (menu.hidden ? open() : close(true)));\n    btn.addEventListener('keydown', (e) => {\n      if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {\n        e.preventDefault(); open();\n      }\n    });\n    menu.addEventListener('click', (e) => {\n      const row = e.target.closest('[data-value]');\n      if (row) choose(row.dataset.value);\n    });\n    menu.addEventListener('keydown', (e) => {\n      if (e.key === 'Escape') { e.preventDefault(); close(true); return; }\n      if (e.key === 'ArrowDown') { e.preventDefault(); point(cursor + 1); return; }\n      if (e.key === 'ArrowUp') { e.preventDefault(); point(cursor - 1); return; }\n      if (e.key === 'Home') { e.preventDefault(); point(0); return; }\n      if (e.key === 'End') { e.preventDefault(); point(opts.length - 1); return; }\n      if (e.key === 'Tab') close(false);\n    });\n    /* Outside-click closes. Capture, so a click on another control in the bar\n       closes this before that control's own handler runs and the toolbar never\n       has two things open at once. */\n    document.addEventListener('pointerdown', (e) => {\n      if (!menu.hidden && !wrap.contains(e.target)) close(false);\n    }, true);\n    sel.addEventListener('change', paint);\n\n    wrap.append(btn, menu);\n    wrap.classList.add('is-upgraded');\n    /* ---- AND THE SELECT STEPS OUT OF THE KEYBOARD'S WAY (2026-09-10, audit T9) ----\n       .is-upgraded shrinks the native control to a clipped pixel, but a\n       clipped <select> is still a tab stop: a keyboard shopper pressed Tab\n       onto an invisible control, then again onto the one they could see, and\n       a screen reader announced \"Sort scents\" twice. It stays the value -\n       choose() writes it and fires `change` from script, which a tab stop has\n       nothing to do with - so it only leaves the tab order and the\n       accessibility tree, the \"Sort scents\" label with it. The trigger above\n       already carries the full name. */\n    sel.tabIndex = -1;\n    const nativeLabel = sel.closest('.cx-sort__native');\n    (nativeLabel || sel).setAttribute('aria-hidden', 'true');\n    paint();\n  });\n}\n\n/* ---- Read-more (PDP About) - the full copy renders once and the clip\n   collapses it with a mask fade. We measure the clip against its clamp: if\n   the copy already fits, the toggle is removed (no dead \"Read more\"); if it\n   overflows, the button lifts the clamp. Only the text swaps - the chevron\n   stays put and rotates via CSS. ---- */\nfunction initReadMore() {\n  const wrap = document.querySelector('[data-readmore-wrap]');\n  if (!wrap) return;\n  const clip = wrap.querySelector('[data-readmore-clip]');\n  const btn = wrap.querySelector('[data-readmore]');\n  const txt = wrap.querySelector('[data-readmore-text]') || btn;\n  if (!clip || !btn) return;\n\n  /* Hide the toggle when the copy doesn't overflow the collapsed clamp. */\n  const measure = () => {\n    if (wrap.getAttribute('data-expanded') === 'true') return;\n    if (clip.scrollHeight <= clip.clientHeight + 4) {\n      wrap.setAttribute('data-expanded', 'true');   /* lifts clamp + mask */\n      btn.hidden = true;\n    }\n  };\n  requestAnimationFrame(measure);\n  onResizeSettled(measure);\n\n  btn.addEventListener('click', () => {\n    const open = wrap.getAttribute('data-expanded') === 'true';\n    wrap.setAttribute('data-expanded', open ? 'false' : 'true');\n    txt.textContent = open ? (btn.dataset.moreLabel || 'Read more')\n                           : (btn.dataset.lessLabel || 'Read less');\n  });\n}\n\n/* ---- PDP detail modals: Ingredients / Returns as top-layer <dialog>s.\n   Openers carry data-pdp-modal-open=\"<key>\"; each sheet is data-pdp-modal.\n   showModal() gives free focus-trap + Escape + inert background; a click on\n   the backdrop (target IS the dialog) light-dismisses. ---- */\nfunction initPdpModals() {\n  const modals = [...document.querySelectorAll('[data-pdp-modal]')];\n  if (!modals.length) return;\n  const byKey = {};\n  modals.forEach((m) => { byKey[m.dataset.pdpModal] = m; });\n  document.addEventListener('click', (e) => {\n    const opener = e.target.closest('[data-pdp-modal-open]');\n    if (opener) {\n      const m = byKey[opener.getAttribute('data-pdp-modal-open')];\n      if (m && typeof m.showModal === 'function') {\n        e.preventDefault();\n        if (!m.open) m.showModal();\n      }\n      return;\n    }\n    if (e.target.closest('[data-pdp-modal-close]')) {\n      const d = e.target.closest('[data-pdp-modal]');\n      if (d) d.close();\n      return;\n    }\n    /* backdrop click - the dialog element itself is the event target */\n    if (e.target.matches && e.target.matches('[data-pdp-modal]')) e.target.close();\n  });\n}\n\n/* ---- Info bubbles: the \"free sample included\" tooltip in the cart.\n   Popover top-layer so it escapes the drawer's clip; the button's\n   popovertarget owns click/Escape/light-dismiss, hover previews it on\n   fine pointers, and title=\"\" is the no-popover fallback. ---- */\nfunction initTips() {\n  const supportsPopover = typeof HTMLElement !== 'undefined' &&\n    Object.prototype.hasOwnProperty.call(HTMLElement.prototype, 'popover');\n  if (!supportsPopover) return;\n\n  function position(pop) {\n    const btn = document.querySelector('[popovertarget=\"' + cssAttr(pop.id) + '\"]');\n    if (!btn) return;\n    pop.style.position = 'fixed';\n    pop.style.margin = '0';\n    pop.style.right = 'auto';\n    const r = btn.getBoundingClientRect();\n    const pw = pop.offsetWidth, ph = pop.offsetHeight;\n    const vw = window.innerWidth, vh = window.innerHeight, gap = 10, m = 8;\n    let below = false;\n    let top = r.top - ph - gap;\n    if (top < m) { top = r.bottom + gap; below = true; }\n    if (top + ph > vh - m) top = Math.max(m, vh - ph - m);\n    let left = r.left + r.width / 2 - pw / 2;\n    if (left + pw > vw - m) left = vw - pw - m;\n    if (left < m) left = m;\n    pop.style.top = top + 'px';\n    pop.style.left = left + 'px';\n    pop.classList.toggle('is-below', below);\n    const arrow = r.left + r.width / 2 - left;\n    pop.style.setProperty('--tip-arrow', Math.max(12, Math.min(pw - 12, arrow)) + 'px');\n    pop.classList.add('is-placed');\n  }\n\n  /* Which bubbles are open right now. Maintained here so the scroll\n     reflow below never has to ask the document. */\n  const openTips = new Set();\n\n  document.addEventListener('toggle', (e) => {\n    const pop = e.target;\n    if (!pop.matches || !pop.matches('[data-tip-bubble]')) return;\n    if (e.newState === 'open') { openTips.add(pop); position(pop); }\n    else { openTips.delete(pop); pop.classList.remove('is-placed'); }\n  }, true);\n\n  let hoverTimer = 0;\n  if (finePointer.matches) {\n    document.addEventListener('pointerover', (e) => {\n      const btn = e.target.closest('[data-tip-btn]');\n      if (!btn) return;\n      clearTimeout(hoverTimer);\n      /* Drop the native title - the bubble is saying the same thing, and the\n         browser would stack its own tooltip on top of it. Done here rather\n         than once on init because the drawer re-renders its lines from the\n         server, which brings the attribute back. The no-popover path returned\n         above, so title survives exactly where it is still the only fallback. */\n      if (btn.hasAttribute('title')) btn.removeAttribute('title');\n      const pop = document.getElementById(btn.getAttribute('popovertarget'));\n      if (pop && pop.showPopover && !pop.matches(':popover-open')) { try { pop.showPopover(); } catch (_) {} }\n    });\n    document.addEventListener('pointerout', (e) => {\n      /* [data-tip-host] is the generic name for \"a thing wearing an info dot\";\n         [data-sample-tag] is the cart's tester mark, which was the only one\n         when this was written. Everything below already works off whichever\n         it matched - the guard is tag.contains and the bubble is found inside\n         the tag - so a second host costs one selector. */\n      const tag = e.target.closest('[data-sample-tag], [data-tip-host]');\n      if (!tag) return;\n      if (e.relatedTarget && tag.contains(e.relatedTarget)) return;\n      clearTimeout(hoverTimer);\n      hoverTimer = setTimeout(() => {\n        const pop = tag.querySelector('[data-tip-bubble]');\n        if (pop && pop.hidePopover && pop.matches(':popover-open')) { try { pop.hidePopover(); } catch (_) {} }\n      }, 120);\n    });\n  }\n\n  /* Keep an open bubble glued to its button. This listens on window in the\n     CAPTURE phase, so it sees the scroll of every lane, rail and drawer on\n     the page as well as the document - it fires on essentially every scroll\n     frame. It used to answer that with a document-wide\n     querySelectorAll('[data-tip-bubble]:popover-open') every single time, on\n     every page, whether or not a bubble had ever been opened. The set above\n     makes the common case one integer test. Disconnected nodes are pruned on\n     the way past: the drawer re-renders its lines from the server, so a bubble\n     can leave the DOM while open without ever firing a close toggle. */\n  const reflow = () => {\n    openTips.forEach((pop) => {\n      if (!pop.isConnected) { openTips.delete(pop); return; }\n      position(pop);\n    });\n  };\n  window.addEventListener('scroll', () => { if (openTips.size) reflow(); }, true);\n  onResize(() => { if (openTips.size) reflow(); });\n}\n\n/* ============================================================\n   Search + layering - the storefront wiring.\n\n   None of the thinking happens in this file. It lives in\n   assets/juce-engine.js, which is loaded just before this one (see\n   layout/theme.liquid) and is deliberately DOM-free, fetch-free and\n   Shopify-free, so the exact same brain runs in Node, behind an API, or\n   on another storefront. See steering/11-search-and-layering.md.\n\n   What stays here: fetch the index once through the Section Rendering\n   API, cache it for the session, and paint. The trigger is still a real\n   /search link, so with JS off the shopper still lands on the results page.\n   ============================================================ */\n/* The brain is 156KB of source and NOTHING above the fold needs it, so it is\n   no longer a deferred <script> in the head. It used to be, purely to\n   guarantee it landed on window.JuceEngine before this file ran - and that\n   guarantee cost every navigation the parse and execute of the whole engine\n   before a single control became interactive.\n\n   Now it is fetched on demand and every consumer goes through loadEngine().\n   JE is a `let` that the loader fills in; the 40-odd `JE.x` call sites below\n   are unchanged, because all of them sit inside functions that only run once\n   the promise has resolved. See steering/12-performance-playbook.md \u00A73. */\nlet JE = null;\nlet enginePromise = null;\n\nfunction loadEngine() {\n  if (enginePromise) return enginePromise;\n  enginePromise = new Promise((resolve) => {\n    if (window.JuceEngine) { JE = window.JuceEngine; resolve(JE); return; }\n    const src = (window.theme && window.theme.engine) || null;\n    if (!src) {\n      console.warn('[juce] no engine URL on window.theme; search and layering fall back to /search');\n      resolve(null);\n      return;\n    }\n    const s = document.createElement('script');\n    s.src = src;\n    s.async = true;\n    s.onload = () => {\n      JE = window.JuceEngine || null;\n      if (!JE) console.warn('[juce] juce-engine.js loaded but exported nothing');\n      resolve(JE);\n    };\n    s.onerror = () => {\n      console.warn('[juce] juce-engine.js did not load; search and layering fall back to /search');\n      resolve(null);\n    };\n    document.head.appendChild(s);\n  });\n  return enginePromise;\n}\n\n/* thin aliases so the rest of the file reads the way it always has */\nfunction srchNorm(s) { return JE ? JE.norm(s) : String(s == null ? '' : s).toLowerCase().trim(); }\nfunction srchMoney(c) { return JE ? JE.money(c) : 'R ' + Math.round((parseInt(c, 10) || 0) / 100); }\n\n/* ---- the catalogue index, fetched ONCE per session and shared by every\n   surface that needs it: the search popup, the PDP ritual lane and the\n   layering explorer. v3 of the payload carries notes per tier and note\n   categories, both of which the layering model needs, so the cache key is\n   bumped to retire any v2 payload still sitting in a shopper's session. ---- */\n/* v6: rows grew mid/mpc (the sellable 10ml) for the discovery builder\n   v7: and mimg, the 10ml's OWN photograph, for the bag's cross-sell. The bump\n       is not optional: a v6 payload cached in a live session has no mimg on any\n       row, so the suggestion would keep showing 50ml bottles until that session\n       ended - which is exactly the shape of bug that reads as \"the change did\n       not ship\". */\n/* v8: rows gained `pimg`, the variant packshot kept back from the\n   main_product_image override (see snippets/search-row.liquid). A cached v7\n   payload has no such field, and the scent trail would silently keep falling\n   back to the landscape lifestyle shot for the life of the cache - the exact\n   bug, still on screen, after the fix shipped. The key is the only thing that\n   invalidates it; the payload `version` must stay in lockstep with the\n   engine's VERSION and is not a cache buster. */\n/* v9: NOT a shape change. The 2026-08-24 tier reprice moved every 50ml and\n   every bundle, and `pc`/`mpc` are plain numbers baked into the payload at\n   render time, so a session opened before the change kept quoting R460 and\n   R530 from its own cache for up to JUCE_INDEX_MAX_AGE. The row shape was\n   identical, the version matched, and nothing else invalidates. Bump this key\n   on any CATALOGUE-WIDE PRICE CHANGE too, not only when a field is added: the\n   payload is a snapshot, and the key is the only thing that retires it. */\n/* v10: the 2026-08-24 reshoot. `pimg` and `mimg` are absolute image URLs baked\n   into the payload at render time, and every 50ml and 10ml plate was replaced\n   that day, so a session holding a v9 payload kept pointing the search rows,\n   the cart's cross-sell and the layering rail at photographs that had been\n   superseded. Same class of staleness as the v9 price bump, same cure: the key\n   is the only thing that retires a snapshot. Bump on any catalogue-wide IMAGE\n   change as well as a price one. */\n/* v11: rows gained `png` and `mpng`, the transparent cut-outs. A v10 payload has\n   neither, so every surface that now prefers them would silently keep drawing\n   bedded plates for the life of the cache - the exact shape of bug that reads as\n   \"the change did not ship\". */\n/* v12: TWO changes at once, and either one alone would have needed this key.\n   Rows gained `rt` and `rc` - the score and how many people gave it - and the\n   Top rated sort reads nothing else, so a session holding a v11 payload would\n   sort a whole shelf by a field that is undefined on every row and land on the\n   source order while the control said \"Top rated\".\n   And rows STOPPED CARRYING SOME VARIANT IDS: a size retired on one product\n   (snippets/size-options.liquid, custom.hidden_sizes) is now absent from\n   mid/mpc/mimg/mpng, which is the difference between \"the 10ml is gone\" and\n   \"the 10ml is gone except for the cart's cross-sell, the discovery builder\n   and the layering rails, for the next six hours\". A cached snapshot is the\n   only thing that can put a retired size back on the page. */\n/* v13: all 41 50ml cut-outs were rebuilt from the supplied per-file PNGs and\n   custom.50ml_png now points at a new Shopify File, so every `png` in a v12\n   payload is last week's URL. This is the catalogue-wide IMAGE change the v10\n   note said to bump on, and the reason it matters even though the old files\n   still resolve: a cached session would go on drawing the derived-mask\n   generation on the layering band, the duet and the finder for six hours, and\n   desert-nomad - which had no cut-out at all until today - would keep falling\n   back to its bedded plate on every one of them. */\nconst JUCE_INDEX_KEY = 'juce:search-index:v15';\n/* THIRTY MINUTES, NOT SIX HOURS (2026-08-30), and the change rides with the\n   compatibility fix below. The six-hour window was chosen while the cache was\n   silently never being read: the payload emits \"version\":\"5.0.0\" and the\n   engine bumped to 5.1.0, so the equality test under it had been false on\n   every page load since, and every navigation refetched and reparsed the whole\n   catalogue. Repairing that test turns a six-hour snapshot ON for the first\n   time - and the v9 to v13 notes above are five separate incidents where a\n   stale snapshot quoted a retired size or last week's price, each fixed by a\n   human remembering to bump the key. Thirty minutes keeps almost all of the\n   saving (a shopper's whole session is usually inside it) and cuts the blast\n   radius of a forgotten bump by an order of magnitude. */\nconst JUCE_INDEX_MAX_AGE = 30 * 60 * 1000;\nlet juceIndexPrep = null, juceIndexP = null;\nfunction juceIngest(data) {\n  const rows = Array.isArray(data) ? data : (data && (data.p || data.products)) || [];\n  const notes = Array.isArray(data) ? [] : (data && data.notes) || [];\n  juceIndexPrep = JE.prepare(rows, notes);\n  if (juceIndexPrep.diagnostics && juceIndexPrep.diagnostics.warnings.length) {\n    console.warn('[juce] catalogue prepared with warnings', juceIndexPrep.diagnostics);\n  }\n  return juceIndexPrep;\n}\nfunction juceIndex(force = false) {\n  if (force) {\n    juceIndexPrep = null; juceIndexP = null;\n    try { sessionStorage.removeItem(JUCE_INDEX_KEY); } catch (e) { /* storage is optional */ }\n  }\n  if (juceIndexPrep) return Promise.resolve(juceIndexPrep);\n  if (juceIndexP) return juceIndexP;\n\n  /* This is the ONE gate every engine-backed surface passes through, which is\n     why the engine fetch hangs off it: nothing below can prepare a payload or\n     read JE.VERSION until the brain is in hand. Callers see the same promise\n     contract they always did. */\n  juceIndexP = loadEngine()\n    .then((engine) => {\n      if (!engine) return null;\n      try {\n        const cached = JSON.parse(sessionStorage.getItem(JUCE_INDEX_KEY) || 'null');\n        const fresh = cached && cached.savedAt && Date.now() - cached.savedAt < JUCE_INDEX_MAX_AGE;\n        /* SCHEMA, NOT BUILD (2026-08-30). This compared the payload's version\n           against engine.VERSION - the engine's own BUILD number. The payload\n           says \"5.0.0\" and ENGINE_VERSION is \"5.1.0\", so `compatible` was\n           false on every single page load and the branch below threw the cache\n           away and refetched. Measured against the live theme: the served\n           payload is \"version\":\"5.0.0\", juce-engine.js:1658 is '5.1.0'.\n           Every navigation in the store paid a full-catalogue fetch, a\n           DOMParser pass over the HTML, a JSON.parse, a full JE.prepare and a\n           ~90KB stringify back into sessionStorage - for a cache that could\n           never be read.\n\n           They are different facts. A build number moves whenever the engine\n           changes at all; what decides whether a stored payload can still be\n           ingested is its SHAPE, and the engine already publishes that as\n           SCHEMA_VERSION (5). A missing `schema` is a MISS, deliberately: the\n           old `!cached.payload.version ||` escape hatch made a payload with no\n           version compatible with every engine, which is the opposite of what\n           a version gate is for, and it is also what lets a pre-fix cache fall\n           through cleanly exactly once. */\n        const compatible = cached && cached.payload\n          && String(cached.payload.schema || '') === String(engine.SCHEMA_VERSION);\n        if (fresh && compatible) return juceIngest(cached.payload);\n        if (cached) sessionStorage.removeItem(JUCE_INDEX_KEY);\n      } catch (e) { /* private mode / corrupt cache - fall through to network */ }\n      const root = (window.Shopify && Shopify.routes && Shopify.routes.root) || '/';\n      return fetch(root + '?section_id=search-index', { headers: { Accept: 'text/html' } })\n        .then((r) => (r.ok ? r.text() : Promise.reject(new Error('index returned ' + r.status))))\n        .then((html) => {\n          const doc = new DOMParser().parseFromString(html, 'text/html');\n          const node = doc.querySelector('[data-search-index]');\n          if (!node) throw new Error('no search index in response');\n          const payload = JSON.parse(node.textContent);\n          const prepared = juceIngest(payload);\n          if (!prepared.list.length) throw new Error('search index contained no products');\n          try {\n            sessionStorage.setItem(JUCE_INDEX_KEY, JSON.stringify({ savedAt: Date.now(), payload }));\n          } catch (e) { /* storage is optional */ }\n          return prepared;\n        });\n    })\n    .catch((error) => {\n      console.warn('[juce] catalogue index unavailable', error);\n      juceIndexP = null; juceIndexPrep = null;\n      return null;\n    });\n  return juceIndexP;\n}\n/* PUBLISHED ON PURPOSE. theme.js is a module, so everything above is private\n   to it - and assets/layer-plan.js needs the very same prepared catalogue on\n   /pages/your-layers. Handing it this function rather than letting it fetch\n   its own copy is what makes the two share one sessionStorage snapshot, one\n   schema gate and one set of ingest warnings. A second loader would be a\n   second place to forget the JUCE_INDEX_KEY bump.\n   The contract is the function's own: call it, get a promise of the prepared\n   index, or of null when the catalogue is unreachable. */\nwindow.juceIndex = juceIndex;\n\n/* juce.sniff() (2026-10-06). The banner at the top of view-source\n   (snippets/source-banner.liquid) ends by offering it to whoever reads that\n   far. It picks one scent from the same prepared catalogue the search uses,\n   so it costs nothing until it is typed, and prints it in the house's own\n   colours. It never prints the row's br/dp - the house a scent was made\n   after - because JUCE never names another house. */\nwindow.juce = window.juce || {};\nwindow.juce.sniff = function sniff() {\n  const ink = 'color:#1B1814', muted = 'color:#6E6E6E';\n  juceIndex().then((prep) => {\n    const pool = ((prep && prep.list) || []).filter((s) => s.raw && s.raw.a !== false && s.u\n      && ((s.raw.nt || []).length || (s.raw.nh || []).length || (s.raw.nb || []).length));\n    if (!pool.length) {\n      console.log('%cNothing in the air just now. Try again in a moment.', muted);\n      return;\n    }\n    const s = pool[Math.floor(Math.random() * pool.length)];\n    const r = s.raw;\n    const list = (a) => (a || []).join(', ');\n    const seasons = (r.se || []).length >= 4 ? 'all year' : list(r.se);\n    const when = (r.ti || []).length === 1 ? 'by ' + r.ti[0] : (r.ti || []).length ? 'day or night' : '';\n    const wear = [seasons, when].filter(Boolean).join(', ');\n    const family = [r.c, r.sc && r.sc !== r.c ? r.sc.toLowerCase() : ''].filter(Boolean).join(', ');\n    const rows = [\n      ['%cJUCE', 'font-family:\"Fredoka\",sans-serif;font-size:32px;line-height:1.1;letter-spacing:0;' + ink],\n      ['%cpicked for you', muted],\n      ['%c' + s.t, 'font-size:18px;font-weight:600;' + ink],\n    ];\n    if (family) rows.push(['%c' + family, muted]);\n    rows.push(['%c ', '']);\n    if ((r.nt || []).length) rows.push(['%ctop     %c' + list(r.nt), muted, ink]);\n    if ((r.nh || []).length) rows.push(['%cheart   %c' + list(r.nh), muted, ink]);\n    if ((r.nb || []).length) rows.push(['%cbase    %c' + list(r.nb), muted, ink]);\n    if (wear) rows.push(['%cwear it %c' + wear, muted, ink]);\n    rows.push(['%c ', '']);\n    rows.push(['%c' + location.origin + s.u, ink]);\n    rows.push(['%cnot this one? juce.sniff() again.', muted]);\n    rows.forEach((args) => console.log(...args));\n  });\n  return '\u00B7  \u02DA  \u00B7';\n};\n\n/* ---- Complete the ritual: re-rank the PDP lane with the real layering\n   model. The section ships a wide, mixed candidate pool (see\n   sections/ritual-lane.liquid); this picks the best of it, orders it, and\n   replaces each caption with the actual reason the two scents layer. With\n   JS off the Liquid ordering and wording stand on their own. ---- */\nfunction initRitualLane() {\n  const lane = document.querySelector('[data-ritual][data-anchor]');\n  if (!lane || !JE) return;\n  const anchorHandle = lane.dataset.anchor;\n  const show = parseInt(lane.dataset.show, 10) || 6;\n  const cards = Array.from(lane.querySelectorAll('[data-card][data-handle]'));\n  if (cards.length < 2) return;\n\n  juceIndex().then((prep) => {\n    if (!prep || !prep.byHandle[anchorHandle]) return;\n    const pool = cards.map((c) => c.dataset.handle).filter((h) => prep.byHandle[h]);\n    if (pool.length < 2) return;\n\n    /* Rank the whole pool so the lane always fills, then let the verdict\n       decide the wording: a merely workable pair should not claim to be a\n       great one. The audience rule the lane has always followed still\n       applies - it is enforced upstream, in the Liquid pool. */\n    /* Positive surfaces never label a risky or avoid pair as a recommendation.\n       If the compatible pool is small, accuracy wins over filling every slot. */\n    const ranked = JE.layerPairs(prep, anchorHandle, {\n      pool, limit: pool.length, min: 'good', availableOnly: true,\n      preferAudience: prep.byHandle[anchorHandle].g\n    });\n    if (!ranked.length) return;\n\n    const byHandle = {};\n    cards.forEach((c) => { byHandle[c.dataset.handle] = c; });\n    const frag = document.createDocumentFragment();\n    ranked.slice(0, show).forEach((item) => {\n      const card = byHandle[item.p.h];\n      if (!card) return;\n      card.hidden = false;\n      const why = card.querySelector('[data-card-why]');\n      if (why && item.reason) why.textContent = item.reason;\n      frag.appendChild(card);                       /* appending moves it, so this reorders */\n    });\n    const keep = new Set(ranked.slice(0, show).map((x) => x.p.h));\n    cards.forEach((c) => { if (!keep.has(c.dataset.handle)) c.hidden = true; });\n    lane.appendChild(frag);\n  });\n}\n\n/* ---- The layering study (sections/pdp-layering.liquid) --------------------\n   The editorial band under the PDP: ONE best partner presented as a pair,\n   with the wear order, then three runners-up as an index. The section's\n   Liquid is the candidate generator and the no-JS answer (shared-note tiers,\n   gender rule); this re-ranks with the real layering model and rewrites the\n   spread - hero, order line, why, buttons, index - once the catalogue is in\n   hand. Liquid answer standing = fine; model finding nothing over 'good' =\n   the band hides, because the honest no beats a weak yes.\n\n   Packshots come from [data-layer-pool], a handle-to-image map the section\n   emits for every compatible candidate: the index's own `img` field is the\n   landscape lifestyle file on 40 of 42 products, which is the wrong crop for\n   a portrait plate. Pool first, index rewrite as the fallback.\n\n   The add buttons ride the [data-pair-add] delegate (initDrawerPairs) with\n   data-pair-open, so the drawer opens as the confirmation - the same path\n   the finder's answer and the bag suggestion use. No second add code. ---- */\n/* THE FLOAT LOOP SLEEPS WHEN NOBODY IS LOOKING. The two bottles carry an\n   infinite transform animation (theme.css, pdp-layer-float) and this is a\n   product page - the band spends most of its life off-screen, and an\n   animation under a drop-shadow filter that never stops is the kind of\n   standing cost this theme has already paid for once. Parking it is a class,\n   so the CSS keeps owning the motion and this only says when.\n\n   .is-still PAUSES, it does not start: with no IntersectionObserver, or with\n   this never running at all, the bottles animate rather than freeze. The\n   class goes on the duo, which paintLayerHero never replaces - it swaps the\n   mate's src, not the node - so the gate survives every repaint. */\nfunction armLayerFloat(root) {\n  const duo = root.querySelector('.pdp-layer__duo');\n  if (!duo || typeof IntersectionObserver !== 'function') return;\n  new IntersectionObserver((entries) => {\n    entries.forEach((entry) => duo.classList.toggle('is-still', !entry.isIntersecting));\n  }, { rootMargin: '120px' }).observe(duo);\n}\n\nfunction initPdpLayer() {\n  const root = document.querySelector('[data-pdp-layer][data-anchor]');\n  if (!root || !JE) return;\n  armLayerFloat(root);\n\n  let pool = {};\n  const poolNode = root.querySelector('[data-layer-pool]');\n  if (poolNode) {\n    try { pool = JSON.parse(poolNode.textContent) || {}; } catch (e) { pool = {}; }\n  }\n\n  /* ---- THE BAG DECIDES WHAT THE BUTTONS OFFER (2026-08-22) ----\n     Liquid answered this first off `cart.items` and stamped data-layer-own,\n     but a page served from cache can carry a bag that is not this shopper's,\n     and a both-button aimed at a bottle already in the bag buys it twice. So\n     the bag is re-read here and the DANGEROUS direction is corrected on its\n     own, before and regardless of the catalogue index - which may never\n     arrive, and the correction must not wait on it. The safe direction\n     (Liquid said owned, the bag says not) costs only a narrower offer and is\n     left to the repaint below. */\n  const bagOwn = fetch(routes.cart + '.js', { headers: { Accept: 'application/json' } })\n    .then((r) => (r.ok ? r.json() : null))\n    .then((c) => (c && c.items ? new Set(c.items.map((it) => it.handle)) : null))\n    .catch(() => null);\n  bagOwn.then((own) => {\n    if (layerBagMoved) return;                 /* a real cart change already spoke */\n    if (!own || !own.has(root.dataset.anchor)) return;\n    root.dataset.layerOwn = '1';\n    root.querySelectorAll('[data-layer-acts] [data-both-id]').forEach(layerSolo);\n  });\n\n  Promise.all([juceIndex(), bagOwn]).then((answers) => {\n    const prep = answers[0];\n    const own = answers[1];\n    if (!prep) return;                                   /* the Liquid answer stands */\n    const anchor = prep.byHandle[root.dataset.anchor];\n    if (!anchor) return;\n    /* ...but never over a cart change that has happened since (refreshLayerOffer).\n       This `own` is the page-load photograph, and the buy box is right above. */\n    if (own && !layerBagMoved) root.dataset.layerOwn = own.has(anchor.h) ? '1' : '0';\n    /* `audience: true` is the HARD gate (juce-engine.js audienceOk) and\n       preferAudience is only a ranking bias. The section's own header comment\n       says the gender rule is absolute in BOTH passes, and the Liquid pass has\n       always enforced it - this pass never did, because the bias was being\n       passed where the gate was meant. A partner the Liquid pass excluded on\n       gender is also absent from [data-layer-pool], so promoting one here also\n       cost it its packshot. Both halves of that bug close here. */\n    const ranked = JE.layerPairs(prep, anchor, {\n      limit: 4, min: 'good', availableOnly: true,\n      audience: true, preferAudience: anchor.g\n    });\n    if (!ranked.length) { root.hidden = true; return; }  /* the honest no */\n    /* ---- DOES THE PARTNER PICK US BACK? ----\n       \"A perfect pair\" is a claim about a PAIR, and a pair is symmetric - but\n       the list above is one-directional, so the band was reading the top of\n       the anchor's own ranking and printing it as if both bottles had agreed.\n       On 15 of 41 scents they had not: Desert Leather's page called Coastal\n       Daze its perfect pair, and Coastal Daze's page called Saltwater Dreams\n       its perfect pair. Two pages, one shopper, and the second one contradicts\n       the first.\n\n       Forcing agreement is the wrong fix - measured, matching every scent to a\n       partner that names it back costs a mean 7 points and drops six pairs out\n       of `great` altogether (Desert Leather would fall from Coastal Daze at\n       75.6 to a merely `good` 59.3). The recommendation is right; the SENTENCE\n       was overclaiming. So the ranking stands and the eyebrow tells the truth:\n       the strong claim where the two bottles genuinely choose each other, and\n       the directional one - which is true on both pages at once - where they\n       do not. One extra ranking call, off an index already in hand. */\n    const mate = ranked[0].p;\n    const back = JE.layerPairs(prep, mate, {\n      limit: 1, min: 'good', availableOnly: true,\n      audience: true, preferAudience: mate.g\n    })[0];\n    const mutual = !!(back && back.p.h === anchor.h);\n    paintLayerHero(root, anchor, ranked[0], pool, mutual);\n    paintLayerAlts(root, ranked.slice(1), pool);\n    /* Kept so refreshLayerOffer can DRAW the pair offer back when the anchor\n       leaves the bag. Flipping a flag only works while the pair buttons are\n       still on the page, and they are not when this pass ran with the anchor\n       already owned: paintLayerHero drew the partner-only ladder, which\n       carries no anchor ids to turn back on. Cheap to re-run - it compares the\n       image src before touching it, and the acts row is four nodes. */\n    layerRepaint = () => paintLayerHero(root, anchor, ranked[0], pool, mutual);\n  });\n}\n\n/* Same drawing as snippets/icon.liquid chevron-right - this side is built\n   client-side from the catalogue index. If one changes, change both. */\nfunction layerChevron() {\n  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n  svg.setAttribute('viewBox', '0 0 20 20');\n  svg.setAttribute('fill', 'none');\n  svg.setAttribute('stroke', 'currentColor');\n  svg.setAttribute('stroke-width', '1.5');\n  svg.setAttribute('stroke-linecap', 'round');\n  svg.setAttribute('stroke-linejoin', 'round');\n  svg.setAttribute('aria-hidden', 'true');\n  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n  path.setAttribute('d', 'M7.5 4l6 6-6 6');\n  svg.appendChild(path);\n  return svg;\n}\n\n/* THE PLATE ONLY EVER TAKES A PACKSHOT. Three sources, best first:\n   the section's own pool (a real variant packshot on the F2F2F2 bed), then\n   the index's `pimg` - which snippets/search-row.liquid preserves precisely\n   because `img` is overridden by custom.main_product_image, a 3413x2160\n   LANDSCAPE lifestyle file on 40 of 42 products. `img` is the last resort and\n   it is the picture that made the band look broken: cropped into a portrait\n   frame under mix-blend darken it paints as a dark rectangle, not a bottle. */\nfunction layerShotSrc(rec, pool, kind) {\n  const art = pool[rec.h];\n  if (art && art.pack) return kind === 'thumb' && art.thumb ? art.thumb : art.pack;\n  /* png first, same order as shot(): this rail draws bottles over the band's\n     ground, so a bedded plate would bring its own plate with it. The pool's\n     `pack` above is already a cutout where one exists; this is the path for a\n     record the server never rendered into the pool. */\n  const src = String(rec.png || rec.pimg || rec.img || '');\n  if (!src) return '';\n  return src.replace(/width=\\d+/, 'width=' + (kind === 'thumb' ? 240 : 900));\n}\n\nfunction layerAddBtn(rec, size, cents, variantId, sampleId, cls) {\n  const btn = document.createElement('button');\n  btn.type = 'button';\n  btn.className = cls;\n  btn.setAttribute('data-pair-add', rec.h);\n  btn.setAttribute('data-pair-size', size);\n  btn.setAttribute('data-pair-open', '');\n  if (variantId) btn.setAttribute('data-variant-id', variantId);\n  if (sampleId) btn.setAttribute('data-sample-id', sampleId);\n  btn.textContent = 'Add the ' + size + (cents > 0 ? ' \u00B7 ' + srchMoney(cents) : '');\n  return btn;\n}\n\n/* THE PAIR BUTTON - one press, both bottles (owner, 2026-08-22: \"if you do not\n   have the main product yet in cart, that there is a button to add both 50ml\n   and 10ml\"). Priced as the sum, labelled as what it does, and carrying the\n   anchor's own ids so pairAddItems can build the other half of the payload and\n   LOOM can credit this band for BOTH scents rather than only the one it named.\n   data-solo-label is what it falls back to if the bag turns out to hold the\n   anchor after all (layerSolo). Mirrored in sections/pdp-layering.liquid. */\nfunction layerBothBtn(anchor, rec, size, cls) {\n  const mini = size === '10ml';\n  const mateCents = mini ? rec.mpc : rec.pc;\n  const total = (mini ? anchor.mpc : anchor.pc) + mateCents;\n  const btn = layerAddBtn(rec, size, mateCents,\n    mini ? rec.mid : rec.vid, mini ? '' : rec.sid, cls);\n  btn.setAttribute('data-pair-both', anchor.h);\n  btn.setAttribute('data-both-id', mini ? anchor.mid : anchor.vid);\n  if (!mini && anchor.sid) btn.setAttribute('data-both-sample-id', anchor.sid);\n  btn.setAttribute('data-layer-set', 'Layer set');\n  btn.setAttribute('data-solo-label', btn.textContent);\n  btn.textContent = 'Add both in ' + size + (total > 0 ? ' \u00B7 ' + srchMoney(total) : '');\n  btn.setAttribute('data-pair-label', btn.textContent);\n  return btn;\n}\n\n/* ---- ONE BUTTON, TWO OFFERS, AND THE BAG PICKS (2026-09-02) ----\n   Back to the partner-only button it would have been had the bag been read a\n   second earlier. Called when the anchor turns out to be in the bag already -\n   because the cached page lied, because the shopper just bought it with the\n   OTHER both-button, or because they added it from the buy box above - and\n   nothing here may put it in there twice.\n\n   THIS USED TO BE A DEMOLITION: it stripped data-both-id and friends off the\n   button, so the flip only ever went one way and a shopper who took the anchor\n   back out of the bag was left with a band offering the partner alone, quietly\n   selling half a pair. The both-offer now STAYS on the button and wears a\n   data-both-off flag instead; pairAddItems reads the flag, so the payload is\n   the same as it was while the attributes were gone, and layerPair can put the\n   pair offer back on when the bag empties out again. Both labels ride along\n   (data-solo-label / data-pair-label) so neither has to be rebuilt. */\nfunction layerSolo(btn) {\n  if (!btn.hasAttribute('data-both-id')) return;\n  if (!btn.hasAttribute('data-pair-label')) {\n    btn.setAttribute('data-pair-label', btn.textContent.trim());\n  }\n  btn.setAttribute('data-both-off', '');\n  const solo = btn.getAttribute('data-solo-label');\n  if (solo) btn.textContent = solo;\n}\n\n/* The other direction: the anchor has left the bag, so the band goes back to\n   offering the pair. Only ever undoes a layerSolo - a button that never had a\n   both-offer (the partner-only ladder paintLayerHero draws when the anchor is\n   owned, or a size only one of the two bottles comes in) has nothing to put\n   back and must not grow a pair claim it was never given the ids for. */\nfunction layerPair(btn) {\n  if (!btn.hasAttribute('data-both-off')) return;\n  btn.removeAttribute('data-both-off');\n  const pair = btn.getAttribute('data-pair-label');\n  if (pair) btn.textContent = pair;\n}\n\n/* Is this button, right now, offering both bottles? Carrying the ids is no\n   longer the same question as making the claim - layerSolo leaves them on. */\nfunction layerPaired(btn) {\n  return !!btn && btn.hasAttribute('data-both-id') && !btn.hasAttribute('data-both-off');\n}\n\n/* An add-both has landed, so the anchor is in the bag and the band's OTHER\n   size must stop offering to put it there. Without this, pressing both pair\n   buttons in a row bought the anchor twice and the shopper found out in the\n   drawer. */\nfunction layerBothSpent(btn) {\n  if (!layerPaired(btn)) return;              /* a solo press bought no anchor */\n  const acts = btn.closest('[data-layer-acts]');\n  if (!acts) return;\n  const root = acts.closest('[data-pdp-layer]');\n  if (root) root.dataset.layerOwn = '1';\n  acts.querySelectorAll('[data-both-id]').forEach((other) => {\n    if (other !== btn) layerSolo(other);\n  });\n}\n\n/* Has a real cart change spoken since the page loaded? The page-load bag read\n   in initPdpLayer is a fetch that may land AFTER the shopper has already put\n   the anchor in the bag from the buy box, and the answer it carries is then a\n   photograph of a bag that no longer exists. Whoever spoke last wins, and a\n   swapDrawer is always later than the load. */\nlet layerBagMoved = false;\n/* Redraw the band's hero and its action row from the model's own answer, set\n   once initPdpLayer has one. Null until then (or forever, if the index never\n   lands) - in which case the Liquid buttons are still on the page and the flag\n   flip below is the whole story. */\nlet layerRepaint = null;\n\n/* ---- THE OFFER FOLLOWS THE BAG, ALL SESSION (owner, 2026-09-02: \"when I have\n   the product in my cart or added it on that page to my cart, this btn needs\n   to automatically update to add the partner and not add both\") ----\n   The bag was read once, at init, and the band argued from that photograph for\n   the rest of the visit: add the anchor from the buy box twelve inches above\n   and the pair button underneath still offered to buy it a second time.\n   swapDrawer is the one place that knows the cart changed, so the band re-reads\n   it there - and it re-reads it in BOTH directions, so taking the anchor back\n   out puts the pair offer back rather than leaving the band selling half of it.\n\n   No second fetch. swapDrawer has just painted the server's own answer into the\n   cart markup and every line row carries data-handle (snippets/cart-lines.liquid),\n   so this is a querySelector over bytes a millisecond old. Line rows only:\n   the drawer also holds suggestion rows, which point at scents that are\n   precisely NOT in the bag. A bundle line answers yes, correctly - the handle\n   is the product's, whichever variant of it the shopper took. */\nfunction refreshLayerOffer() {\n  const root = document.querySelector('[data-pdp-layer][data-anchor]');\n  if (!root || !root.dataset.anchor) return;\n  const bag = document.querySelector('[data-drawer-inner]')\n    || document.querySelector('[data-cart-page] [data-cart-inner]');\n  if (!bag) return;\n  const own = !!bag.querySelector('[data-line-key][data-handle=\"'\n    + cssAttr(root.dataset.anchor) + '\"]');\n  const was = root.dataset.layerOwn;\n  layerBagMoved = true;\n  root.dataset.layerOwn = own ? '1' : '0';\n\n  /* The flag flip first, and in place: it leaves the pressed button's own node\n     alone, so the tick the shopper just earned survives the swap that follows\n     their add. */\n  const pairs = root.querySelectorAll('[data-layer-acts] [data-both-id]');\n  pairs.forEach((btn) => (own ? layerSolo(btn) : layerPair(btn)));\n\n  /* Nothing to flip and the offer just widened: the model repainted while the\n     anchor was in the bag, so the acts row is the partner-only ladder and the\n     pair offer has to be DRAWN rather than switched back on. */\n  if (!own && was === '1' && !pairs.length && layerRepaint) layerRepaint();\n}\n\nfunction paintLayerHero(root, anchor, lead, pool, mutual) {\n  const rec = lead.p;\n  const url = (pool[rec.h] && pool[rec.h].u) || rec.u || '#';\n\n  const img = root.querySelector('[data-layer-img]');\n  const src = layerShotSrc(rec, pool, 'pack');\n  /* compare the attribute, not .src - the property comes back absolutised */\n  if (img && src && img.getAttribute('src') !== src) {\n    img.srcset = [480, 720, 900, 1200]\n      .map((w) => src.replace(/width=\\d+/, 'width=' + w) + ' ' + w + 'w').join(', ');\n    img.src = src;\n    img.alt = rec.t;\n  }\n  /* the partner's caption is its figure's link (pdp-layering.liquid), so it\n     follows the partner exactly as the names line does */\n  const cap = root.querySelector('[data-layer-cap]');\n  if (cap) { cap.textContent = rec.t; cap.href = url; }\n\n  const name = root.querySelector('[data-layer-name]');\n  if (name) { name.textContent = rec.t; name.href = url; }\n\n  /* Exclusivity only where it is earned (see the note in initPdpLayer). Both\n     bottles name each other -> the pair claim. Otherwise the directional one,\n     which stays true no matter which of the two pages the shopper is standing\n     on. Liquid's own stamp never says \"perfect\", so the pre-JS band cannot\n     overclaim either. */\n  const stamp = root.querySelector('[data-layer-stamp]');\n  if (stamp) {\n    stamp.textContent = mutual\n      ? (lead.verdict === 'great' ? 'A perfect pair' : 'A natural pair')\n      : 'Best with ' + anchor.t;\n  }\n\n  const why = root.querySelector('[data-layer-why]');\n  if (why) why.textContent = (lead.why && (lead.why.long || lead.why.short)) || lead.reason || '';\n\n  /* the one instruction worth printing: which bottle goes on first, in the\n     model's own words (deterministicDirection) */\n  const dir = lead.directions || (lead.report && lead.report.directions) || null;\n  const order = root.querySelector('[data-layer-order]');\n  if (order && dir && dir.text) { order.textContent = dir.text; order.hidden = false; }\n  const roleSelf = root.querySelector('[data-layer-role-self]');\n  const roleMate = root.querySelector('[data-layer-role-mate]');\n  if (dir && roleSelf && roleMate) {\n    const selfFirst = dir.first === anchor.h;\n    roleSelf.textContent = dir.flexible ? 'Either order' : (selfFirst ? 'Goes on first' : 'Over it, lightly');\n    roleMate.textContent = dir.flexible ? 'Either order' : (selfFirst ? 'Over it, lightly' : 'Goes on first');\n    roleSelf.hidden = false;\n    roleMate.hidden = false;\n  }\n\n  const acts = root.querySelector('[data-layer-acts]');\n  if (acts) {\n    acts.replaceChildren();\n    /* ONE primary. The band used to offer three actions of equal apparent\n       weight (owner, pass 3: \"no clear primary\"): a solid 10ml pill, a 50ml in\n       .btn--ghost - which has no ring at all, so the MORE expensive purchase\n       rendered as bare black text - and a text link. Three tiers now, three\n       surfaces: solid ink for the 10ml (trying the layer is the cheap honest\n       first step), a hairline ring for the 50ml, quiet text for Discover.\n       Mirrored in sections/pdp-layering.liquid's no-JS answer. */\n    const hasMini = !!(rec.mid && rec.mpc > 0);\n    /* ---- THE PAIR OR THE PARTNER, and the bag decides. Same rule as the\n       Liquid pass: with the anchor missing, one press buys both bottles,\n       because selling somebody the accompaniment to a scent they have not\n       taken leaves them holding half a pair. A size is offered as a pair only\n       where BOTH bottles come in it; where only one does, that one size is the\n       whole offer rather than a mixed pair of claims. ---- */\n    const own = root.dataset.layerOwn === '1';\n    const bothTen = !own && hasMini && !!(anchor.mid && anchor.mpc > 0);\n    const bothFifty = !own && !!(anchor.vid && rec.vid);\n    if (bothTen || bothFifty) {\n      if (bothTen) acts.appendChild(layerBothBtn(anchor, rec, '10ml', 'btn'));\n      if (bothFifty) acts.appendChild(layerBothBtn(anchor, rec, '50ml', bothTen ? 'btn btn--secondary' : 'btn'));\n    } else {\n      if (hasMini) acts.appendChild(layerAddBtn(rec, '10ml', rec.mpc, rec.mid, '', 'btn'));\n      acts.appendChild(layerAddBtn(rec, '50ml', rec.pc, rec.vid, rec.sid, hasMini ? 'btn btn--secondary' : 'btn'));\n    }\n    const visit = document.createElement('a');\n    visit.className = 'text-action pdp-layer__visit';\n    visit.href = url;\n    visit.textContent = 'Discover ' + rec.t;\n    visit.appendChild(layerChevron());\n    acts.appendChild(visit);\n  }\n}\n\nfunction paintLayerAlts(root, items, pool) {\n  const list = root.querySelector('[data-layer-alts]');\n  const wrap = list && list.closest('.pdp-layer__alts');\n  if (!list) return;\n  if (!items.length) { if (wrap) wrap.hidden = true; return; }\n  list.replaceChildren();\n  items.slice(0, 3).forEach((item) => {\n    const rec = item.p;\n    const li = document.createElement('li');\n    li.className = 'pdp-layer__alt';\n    const a = document.createElement('a');\n    a.className = 'pdp-layer__alt-link';\n    a.href = (pool[rec.h] && pool[rec.h].u) || rec.u || '#';\n    const thumb = document.createElement('span');\n    thumb.className = 'pdp-layer__alt-thumb';\n    const src = layerShotSrc(rec, pool, 'thumb');\n    if (src) {\n      const img = document.createElement('img');\n      img.src = src;\n      img.alt = '';\n      img.loading = 'lazy';\n      img.decoding = 'async';\n      /* matches .pdp-layer__alt-thumb's own box - at 56px a whole packshot\n         rendered as a speck (owner, pass 3: the rows are \"thin and\n         weightless\"). CSS still owns the drawn size; these are the CLS\n         placeholder only. */\n      img.width = 88;\n      img.height = 88;\n      thumb.appendChild(img);\n    }\n    const body = document.createElement('span');\n    body.className = 'pdp-layer__alt-body';\n    const nm = document.createElement('span');\n    nm.className = 'pdp-layer__alt-name';\n    nm.textContent = rec.t;\n    const reason = document.createElement('span');\n    reason.className = 'pdp-layer__alt-why';\n    reason.textContent = item.reason || (item.why && item.why.short) || '';\n    body.append(nm, reason);\n    const price = document.createElement('span');\n    price.className = 'pdp-layer__alt-price';\n    const hasMini = !!(rec.mid && rec.mpc > 0);\n    price.textContent = hasMini ? 'from ' + srchMoney(rec.mpc) : (rec.pc > 0 ? srchMoney(rec.pc) : '');\n    a.append(thumb, body, price, layerChevron());\n    li.appendChild(a);\n    list.appendChild(li);\n  });\n  if (wrap) wrap.hidden = false;\n}\n\n/* ---- The drawer's one suggestion ------------------------------------------\n   The site already knows which scents wear well together, and the cart is the\n   one place that knowledge is worth money. Rather than a grid of \"you may also\n   like\", the drawer offers the SINGLE best partner for what is already in the\n   bag, with the model's own reason under it - the same layerPairs the PDP\n   ritual lane and the layering lab run.\n\n   Cheap by construction: the catalogue index is the one already cached for the\n   session by search, it is only ever requested when the drawer actually opens,\n   and the whole thing is one row. If the model finds nothing that clears\n   \"good\", nothing is shown - a weak suggestion is worse than none.\n   The section ships the block hidden, so with JS off the drawer is unchanged.\n---------------------------------------------------------------------------- */\nlet drawerPairsToken = 0;\n/* ---- WHAT THE SHOPPER HAS ALREADY SAID NO TO ----\n   The suggestion can be dismissed, and a dismissal that is forgotten the\n   moment the sheet re-renders is not a dismissal - the same bottle would be\n   back before the drawer finished closing. Session-scoped on purpose: it is a\n   \"not this trip\", not a preference worth keeping forever, and it costs the\n   shopper nothing to see it again tomorrow. sessionStorage can throw (private\n   mode, storage disabled), so every touch of it is guarded and the feature\n   degrades to in-memory. */\nconst BAG_SNUB_KEY = 'juce:bag-snubbed';\n/* AND AFTER TWO NOs, STOP ASKING (owner, 2026-08-18: \"if you click on it 2\n   times it would disappear\"). Dismissing retires that scent and offers the next\n   perfect match; dismissing again says the shopper is not shopping for a second\n   bottle, and the block stands down for the rest of the session. Without this\n   the sheet would keep producing partners - a bag of two has five or six the\n   model rates as great - and a suggestion you cannot get rid of is an advert. */\nconst BAG_SNUB_LIMIT = 2;\nlet bagSnubbed = null;\n/* ---- THE SLOT'S OTHER STATE: A PAIR THE SHOPPER JUST BROKE ----\n   Set the moment one half of a layer set is removed, read by\n   refreshDrawerPairs before it looks for a suggestion at all, and cleared the\n   moment the pair stops being true. Module-level rather than parked on the\n   host node because [data-drawer-pairs] is destroyed and rebuilt by every\n   Section Rendering swap - including the very swap that removes the row. */\nlet bagUnpair = null;\nfunction snubbedPairs() {\n  if (bagSnubbed) return bagSnubbed;\n  bagSnubbed = new Set();\n  try {\n    const raw = sessionStorage.getItem(BAG_SNUB_KEY);\n    if (raw) JSON.parse(raw).forEach((h) => bagSnubbed.add(h));\n  } catch (e) { /* no storage: the set still works, it just does not persist */ }\n  return bagSnubbed;\n}\nfunction snubPair(handle) {\n  if (!handle) return;\n  const set = snubbedPairs();\n  set.add(handle);\n  try { sessionStorage.setItem(BAG_SNUB_KEY, JSON.stringify([...set])); } catch (e) {}\n}\n\n/* ============================================================================\n   A COMBO THE BAG HAS ALREADY BEEN SHOWN STAYS A COMBO\n   (owner, 2026-08-31: \"When there is layered and work well combos on cart they\n   should not be overwritten and be presistant based on what the user inital\n   selected\".)\n\n   THE PROBLEM. Every auto block in the bag is DERIVED - bagChemistry rescores\n   the whole bag and bagClusters rebuilds the blocks from scratch on every\n   render, every quantity tap and every Section Rendering swap. Nothing wrote\n   down that a pair had ever been drawn, so the grouping only existed for as\n   long as the model kept producing it, and anything that moved the reading\n   moved the shopper's combo with it: an index that has not landed yet, a size\n   swap that lands mid-fetch, a re-score after a catalogue change. A shopper\n   who was told two of their bottles are a set, and then watched the set come\n   apart under them, has been told the shop is guessing.\n\n   THE ANSWER IS A LEDGER, NOT A LONGER RULE. Every pair the bag actually draws\n   as a combo is written down here, and bagChemistry treats a written-down pair\n   as bonded from then on - alongside, never instead of, the model's own\n   verdict. So the model can only ever ADD combos to a bag; it can never take\n   one back off a shopper who has already seen it.\n\n   SESSION-SCOPED, and deliberately the same scope as the snub list beside it.\n   This is \"the pairs THIS bag has been shown\", not a preference: a new visit\n   starts from the model's own reading again, which is right, because by then\n   the bag itself is usually different too.\n\n   THE KEY IS THE TWO HANDLES, SORTED. A pair is a pair whichever order the\n   bottles went into the bag, and sorting is what makes adding the same combo\n   from the other direction land on the row that is already there.\n\n   IT DOES NOT MANUFACTURE A REASON. A kept pair is only ever promoted out of\n   `pairs`, which holds a real layerScore report for every distinct pair in the\n   bag - so the caption and the ritual card are the engine's own words about\n   these two bottles, exactly as they were the first time. What is remembered\n   is the DECISION to group them, never the sentence. */\nconst BAG_KEPT_KEY = 'juce:bag-combos';\nlet bagKept = null;\nfunction comboKey(a, b) { return a < b ? a + '|' + b : b + '|' + a; }\nfunction keptCombos() {\n  if (bagKept) return bagKept;\n  bagKept = new Set();\n  try {\n    const raw = sessionStorage.getItem(BAG_KEPT_KEY);\n    if (raw) JSON.parse(raw).forEach((k) => bagKept.add(k));\n  } catch (e) { /* no storage: the ledger still works, it just does not persist */ }\n  return bagKept;\n}\nfunction keepCombo(a, b) {\n  if (!a || !b || a === b) return;\n  const set = keptCombos();\n  const k = comboKey(a, b);\n  if (set.has(k)) return;                 /* only write when it moves */\n  set.add(k);\n  try { sessionStorage.setItem(BAG_KEPT_KEY, JSON.stringify([...set])); } catch (e) {}\n}\n/* Showing or hiding the suggestion changes what the undo pill has to clear\n   (measureBagFoot), so the two are never set apart. */\nfunction showBagPairs(host, on) {\n  if (!host) return;\n  host.hidden = !on;\n  measureBagFoot();\n}\n/* ============================================================================\n   WHAT THE BAG ALREADY IS\n   (owner, 2026-08-22: \"if there is already a well selection of layering that it\n   wont always try to be upsell, more thoughtful ... if there is already a good\n   combo not to try upsell again in the cart ... automatically group things,\n   that says Works well together (Magic star) ... to get them to keep the\n   product in the cart\".)\n\n   Until now the sheet only ever asked ONE question - what is the best partner\n   for something in this bag - and it asked it on every render, whatever was\n   already in there. A shopper who had just taken a duet out of the layering\n   studio got a third bottle offered at the foot, underneath the block that had\n   literally just titled the two of them a set. That is the moment an assistant\n   turns into an advert, and it is the screenshot this work came from.\n\n   So the bag is READ FIRST, once, and the reading drives two things:\n\n     1. THE LIST GROUPS ITSELF. Two scents the model bonds are drawn as one\n        block - a sparkle, \"Works well together\", and the reason - whether they\n        arrived together carrying `_layer_set` or a shopper assembled them a\n        bottle at a time over three visits. Nothing about a pair should depend\n        on which door it came through, and a shopper who can SEE that two of\n        their bottles are a pair has a reason to keep both.\n     2. THE OFFER STANDS DOWN. A bag already holding a pair is not short of\n        anything, so the slot stops selling and says the one thing left that is\n        worth saying: which of the two goes on first.\n\n   Cost: one layerScore per pair of distinct scents in the bag, memoised on the\n   bag's own handle list, so quantity taps, size swaps and re-renders re-use the\n   reading rather than re-deriving it. A five-bottle bag is ten calls, once.\n   ========================================================================= */\n/* A bag of ten distinct scents is a stock take, not a bag, and an all-pairs\n   read of it buys nothing a shopper will read. The reading stops there. */\nconst BAG_CHEM_MAX = 10;\nlet bagChem = null;\n\n/* ---- A 3ml IS NOT A BOTTLE ----\n   (owner, 2026-08-31: \"It brings up 3ml's that should not show at all even in\n   cart\".)\n\n   JUCE stopped selling 3ml bottles on 2026-08-12 and snippets/size-options is\n   the one filter that says so - every card, every picker, every index row and\n   every size panel in the theme skips a variant whose title carries \"3ml\".\n   The VARIANT still exists in Shopify and is still in stock on all 41 scents\n   (R55 to R85), so a legacy cart, a saved link or a /cart/add from anywhere\n   outside this theme can still put one in the bag. It renders as a row,\n   because it is a line somebody is being charged for and a charged line that\n   does not appear is a worse bug than this one.\n\n   What it must NOT do is count as a BOTTLE. Everything below this function\n   reads the bag through it, so before this filter a paid 3ml was a full\n   participant in the chemistry: two 3ml samples were unioned into a \"Works\n   well together\" block, counted as a completed combo, and given a \"How to wear\n   it\" card - illustrated, because ritualTile takes its picture off the cart\n   row, with two photographs of sample vials. A shopper was being told how to\n   layer two testers, and a third bottle was being offered off the back of one.\n\n   The test is the same substring the rest of the theme uses. The Bundle is\n   called exactly \"Bundle\" and no variant title may carry a size it is not\n   (size-options states that law), so `3ml` cannot match anything else. The\n   free R0 tester never reaches here at all - cart-lines folds it into the\n   bottle that earned it and draws no row.\n\n   THE FREE TESTER AND THE PAID 3ml ARE THE SAME OBJECT TO THIS FUNCTION and\n   that is fine: neither is a bottle. They are separated further up, where it\n   matters, by price. */\nfunction bagRowIsBottle(row) {\n  return (row.getAttribute('data-size') || '').toLowerCase().indexOf('3ml') === -1;\n}\n\n/* Every line in one list, in list order, minus the rows on their way out - a\n   pair whose other half is mid-removal is not a pair any more - and minus the\n   3ml, which is not a bottle. */\nfunction bagLineRows(root) {\n  if (!root) return [];\n  return Array.from(root.querySelectorAll('[data-line-key][data-handle]'))\n    .filter((row) => !row.classList.contains('is-leaving'))\n    .filter(bagRowIsBottle);\n}\n\n/* THE BAR TWO SCENTS HAVE TO CLEAR before the bag calls them a pair. `great`\n   is the model's TOP verdict and it is the same bar bestPairForBag already\n   uses to offer one, so the sheet can never group a pairing it would not have\n   suggested. A pairing a human actually smelled (udesly.layers-well-with)\n   passes on the endorsement alone - the model is not the authority there. */\nfunction bondedPair(item) {\n  return item.verdict === 'great' || item.curated === 2;\n}\n\n/* One read of the whole bag: every pair of distinct scents scored, the bonded\n   ones kept, and those joined into blocks. Memoised on (index, handles) - the\n   only two things it depends on. Quantity is deliberately not in the key: two\n   of a bottle layer exactly as one of it does. */\nfunction bagChemistry(prep, list) {\n  if (!prep || !JE || !list) return null;\n  const handles = [];\n  const seen = new Set();\n  bagLineRows(list).forEach((row) => {\n    const h = row.getAttribute('data-handle');\n    if (!h || seen.has(h) || !prep.byHandle[h]) return;\n    seen.add(h);\n    handles.push(h);\n  });\n  const key = handles.join('|');\n  if (bagChem && bagChem.prep === prep && bagChem.key === key) return bagChem;\n\n  const pairs = [];\n  if (handles.length > 1 && handles.length <= BAG_CHEM_MAX) {\n    for (let i = 0; i < handles.length; i++) {\n      for (let j = i + 1; j < handles.length; j++) {\n        const report = JE.layerScore(prep, prep.byHandle[handles[i]], prep.byHandle[handles[j]]);\n        if (!report) continue;\n        const item = {\n          a: handles[i], b: handles[j], report: report,\n          score: report.score, verdict: report.verdict\n        };\n        item.curated = pairCurated(item);\n        pairs.push(item);\n      }\n    }\n    /* the merchant's own call first, then the model's - the same order\n       betterPair settles a tie in, for the same reason */\n    pairs.sort((x, y) => (y.curated - x.curated) || (y.score - x.score));\n  }\n  /* THE MODEL'S BONDS, PLUS THE ONES THIS BAG HAS ALREADY BEEN SHOWN. Both\n     halves have to be in the bag for a kept pair to count, which `pairs`\n     already guarantees - it is built from the handles on screen. */\n  const kept = keptCombos();\n  const bonded = pairs.filter((p) => bondedPair(p) || kept.has(comboKey(p.a, p.b)));\n  bagChem = {\n    prep: prep, key: key, handles: handles, pairs: pairs,\n    bonded: bonded, clusters: bagClusters(handles, bonded)\n  };\n  return bagChem;\n}\n\n/* Bonded pairs joined into blocks. Three scents that each layer with the other\n   two are ONE block and not three overlapping ones, so no bottle is ever drawn\n   inside two groups at once. Union-find over a handful of handles; the members\n   come out in list order because `handles` is in list order. */\nfunction bagClusters(handles, bonded) {\n  if (!bonded.length) return [];\n  const parent = {};\n  handles.forEach((h) => { parent[h] = h; });\n  const find = (h) => {\n    while (parent[h] !== h) { parent[h] = parent[parent[h]]; h = parent[h]; }\n    return h;\n  };\n  bonded.forEach((item) => {\n    const ra = find(item.a), rb = find(item.b);\n    if (ra !== rb) parent[ra] = rb;\n  });\n  const blocks = new Map();\n  handles.forEach((h) => {\n    const root = find(h);\n    if (!blocks.has(root)) blocks.set(root, []);\n    blocks.get(root).push(h);\n  });\n  return Array.from(blocks.values()).filter((block) => block.length > 1);\n}\n\n/* The combos the shopper can actually SEE - every block in the list holding two\n   or more live rows, however it got there. Counted off the DOM rather than off\n   the chemistry because groupBagLines has just finished drawing exactly this,\n   and a card that disagreed with the blocks above it would be arguing with the\n   page a shopper is looking at. */\nfunction bagBlocks(list) {\n  return Array.from(list.querySelectorAll('[data-cart-group]'))\n    .filter((section) => bagLineRows(section).length > 1);\n}\n\n/* ---- THE BOTTLES THAT ARE ALREADY LAYERED WITH SOMETHING IN THE BAG ----\n   (owner, 2026-08-26: \"it should not recommend a product that is already\n   layered nicely with another product in the cart\".)\n\n   A block on the screen holding two or more DIFFERENT scents is the sheet\n   saying, in as many words, that those bottles belong to each other. Offering a\n   third partner FOR one of them directly underneath that block is the advert\n   the grouping work was meant to end: the shopper is told they already have a\n   pair, and is then sold a way to make the pair they already have.\n\n   So every scent inside a drawn block is retired as an ANCHOR. It stays in the\n   bag, it still keeps itself from being offered back, and its block still counts\n   towards the two-combo stop; it simply stops being a bottle the sheet is\n   allowed to shop for. A loose bottle standing beside that block is still fair\n   game, which is what keeps owner rule (d) alive - one pair held is an\n   opportunity for a second, just never a second one built on a bottle that is\n   already spoken for.\n\n   DISTINCT handles, not rows. A block holding a 50ml and a 10ml of one scent is\n   one scent in two sizes with no partner in it, so it retires nothing.\n\n   Read off the DOM for the same reason bagCombos is: groupBagLines has just\n   finished drawing these blocks, and a slot that disagreed with the blocks above\n   it would be arguing with the page a shopper is looking at. */\nfunction bagPairedHandles(list) {\n  const paired = new Set();\n  bagBlocks(list).forEach((section) => {\n    const inside = bagLineRows(section)\n      .map((row) => row.getAttribute('data-handle'))\n      .filter(Boolean);\n    const members = inside.filter((h, i) => inside.indexOf(h) === i);\n    if (members.length > 1) members.forEach((h) => paired.add(h));\n  });\n  return paired;\n}\n\n/* ============================================================================\n   WHAT COUNTS AS A COMPLETED LAYERING COMBO\n   ============================================================================\n   Two things, and only two:\n\n     1. A BUNDLE LINE. A Bundle is one Shopify variant holding a 50ml and a\n        10ml of the same scent at 90% of the pair. It is a layering combo by\n        construction - there is nothing left to complete about it.\n     2. TWO PRODUCTS IN THE BAG THAT ARE A REAL PAIR. \"Real\" means the pair\n        matrix rates them `great` (its TOP verdict, the same bar bestPairForBag\n        has to clear before it may offer one) or a human put them on the\n        merchant's own layers-well-with list - see bondedPair. It ALSO means a\n        pair the shopper themselves declared by adding both together out of the\n        layering studio, which arrives carrying `_layer_set`.\n\n   All three of those are already drawn in the list as blocks by the time this\n   runs - groupBagLines has just finished - so the count is read off the blocks\n   rather than re-derived. That is deliberate: a number that disagreed with the\n   blocks a shopper is looking at would be arguing with the page.\n\n   ---- WHY floor(rows / 2), AND WHAT \"MAXIMAL MATCHING\" MEANS HERE ----\n   A bottle can be a great partner for two others: three scents that each bond\n   to the middle one are ONE block, not three pairs, and counting three would\n   have the bag \"hold three combos\" on the strength of three bottles. So the\n   members of a block are MATCHED OFF against each other - each bottle spends\n   itself on one pairing and is then unavailable - which for a block of n live\n   rows is floor(n / 2) combos. Blocks never share a row, so the matching is\n   non-overlapping across the whole bag by construction.\n\n   This is a MAXIMAL matching (nothing can be added to it), not a proven MAXIMUM\n   one. Getting from one to the other means running a real matching algorithm\n   over a graph that has at most five bottles in it and can only ever differ by\n   one - which would buy a rounder number and cost a reader the ability to see\n   the rule at a glance. floor(n / 2) per block is the rule, stated.\n\n   A BUNDLE INSIDE A BLOCK COUNTS TWICE, once for the pair it is and once for\n   the pair it makes with the scent beside it, and that is right: they are two\n   different pairings using two different bottles out of that line. A bag\n   holding a Bundle and a scent that layers with it genuinely is \"a well\n   selection of layering\", which is exactly the state the stop rule is for. */\nfunction countBagCombos(blockSizes, bundles) {\n  let n = Math.max(0, bundles || 0);\n  (blockSizes || []).forEach((rows) => { n += Math.floor(rows / 2); });\n  return n;\n}\nfunction bagCombos(list) {\n  const blockSizes = bagBlocks(list).map((section) => bagLineRows(section).length);\n  const bundles = bagLineRows(list)\n    .filter((row) => row.hasAttribute('data-bundle'))\n    .reduce((n, row) => n + Math.max(1, bagQty(row)), 0);\n  return countBagCombos(blockSizes, bundles);\n}\n\n/* THE PAIRS THE RITUAL CARDS ARE ABOUT: one per block the list actually drew,\n   not whichever pairs in the bag happen to score highest. The two differ the\n   moment a curated set sits beside a loose bottle the model likes more, and a\n   card would then be explaining a pairing the list never drew. A block whose\n   scents are not in the catalogue index simply produces none.\n\n   ONE PER BLOCK, NOT ONE IN TOTAL (owner, 2026-08-31: \"allow to show multiple\n   layer wells\"). This used to answer null unless the bag held exactly one\n   block, which meant a shopper holding TWO combos - the state the bag works\n   hardest to produce - got no card at all: the offer ladder had gone quiet at\n   two combos and the ritual card had gone quiet at two blocks, so the slot\n   said nothing about either of them. They are pages of one deck now.\n\n   ---- AND A BLOCK OF THREE IS NOT ONE PAIR (owner, 2026-09-01) ----\n   \"On the cart 'work well together' it sometimes groups 3 items that works\n   together well, the how to wear info card should indicate that correctly ***\n   oky group the two best, or the inital one or 3 if the equal, but have\n   separate cards, we want the most robust method.\"\n\n   THE OLD `[0]` WAS THE BUG, and it was invisible on a bag of two. A block is\n   a CLUSTER (bagClusters, union-find over bonded edges), so it holds three\n   scents whenever one bottle bonds the other two - and this took the single\n   best-scoring pair inside it and threw the rest away. The heading above the\n   rows said three bottles belong together; the card underneath explained two\n   of them and named no others, so the third bottle was in a block with no\n   instructions anywhere on the page. A shopper cannot tell from that whether\n   it goes on first, last, or with either.\n\n   SEPARATE CARDS, ONE PER REAL BOND, and that is the owner's \"most robust\n   method\" read literally: a card can only ever describe two bottles, because\n   applying a fragrance is a two-hand act and `directions` is a statement about\n   an ordered pair. So the block contributes as many cards as it has bonds -\n   two for the common chain (a bonds b, b bonds c), three when all three bond\n   each other, which is the \"or 3 if the equal\" case.\n\n   BONDS, NOT COMBINATIONS. It reads chem.bonded rather than chem.pairs, so a\n   cluster of three yields at most the edges that actually built it. Taking\n   every internal pair instead would invent a card for a combination the model\n   never rated - exactly the fault rememberBlock exists to prevent one function\n   further down, and for the same reason.\n\n   BEST FIRST. chem.bonded is filtered out of chem.pairs, which bagChemistry\n   has already sorted merchant-curated first and then by score, and Array\n   filter preserves order - so the strongest pairing in a block opens its run\n   of cards and the deck's arrow walks down from there.\n\n   THE FALLBACK IS THE OLD BEHAVIOUR AND IT IS FOR SERVER BLOCKS. A block the\n   shopper caused carries `_layer_set` and is never re-grouped here, so its\n   scents need not be bonded at all - the model may rate them merely good. It\n   would otherwise produce no card. One card off the best pair inside it is\n   what it has always had.\n\n   CAPPED AT THREE PER BLOCK. Bonds inside a cluster grow with the square of\n   its size and a bag of five could otherwise fill the deck with nothing else.\n   Three is the whole triangle, which is every case anybody has actually met. */\nconst BAG_RITUAL_PER_BLOCK = 3;\nfunction bagBlockPairs(chem, list) {\n  if (!chem) return [];\n  const out = [];\n  bagBlocks(list).forEach((block) => {\n    const inside = bagLineRows(block).map((row) => row.getAttribute('data-handle'));\n    const held = (p) => inside.indexOf(p.a) !== -1 && inside.indexOf(p.b) !== -1;\n    const bonds = chem.bonded.filter(held).slice(0, BAG_RITUAL_PER_BLOCK);\n    if (bonds.length) { out.push(...bonds); return; }\n    const best = chem.pairs.filter(held)[0];\n    if (best) out.push(best);\n  });\n  return out;\n}\n\n/* IS THIS BAG A SINGLE 50ml AND NOTHING ELSE?\n   Rule 3 of the ladder is written for exactly that shopper and no other, so the\n   test is strict: one row, quantity one, not a Bundle, and a 50ml. The size is\n   read off data-size, which cart-lines stamps on the row from the variant title\n   it already downcased - the same string the rungs inside the row carry, so a\n   substring match holds whatever the variant is actually called. */\nfunction bagLoneFifty(list) {\n  const rows = bagLineRows(list);\n  if (rows.length !== 1) return null;\n  const row = rows[0];\n  if (row.hasAttribute('data-bundle')) return null;\n  if (bagQty(row) !== 1) return null;\n  const size = (row.getAttribute('data-size') || '').toLowerCase();\n  if (size.indexOf('50ml') === -1) return null;\n  return row.getAttribute('data-handle') || null;\n}\n\n/* ============================================================================\n   THE OFFER LADDER - what the slot is allowed to say, in order\n   (owner, 2026-08-26)\n\n     (c) \"Cart upsell free shipping single 50ml, so if you only have a 50ml to\n          try and upsell the 10ml with + free shipping on layering combo tho.\"\n     (d) \"If there is another opportunity to upsell a layering combo the cart\n          upsell should suggest that instead, but the thing is we dont want a\n          endless loop, if there is 2 layering combos it should stop.\"\n\n   Every rule the bag follows is in this one function so a human can read them\n   off the screen. Nothing here touches the DOM, asks the engine or reads\n   storage: it is handed a description of the bag and returns a verdict, which\n   is also what makes it testable without a browser.\n\n     offers[]   0-2 cards asking for a bottle:\n                  kind 'combo'       a great partner missing from the bag\n                  kind 'lone-fifty'  the 10ml of a lone 50ml, framed by delivery\n     rituals    true when the combos already held get their own cards\n\n   THE ORDER IS THE POINT.\n\n   0. AN EMPTY BAG HAS NOTHING TO PAIR WITH.\n\n   1. TWO COMPLETED COMBOS AND THE BAG STOPS ASKING. This is the owner's\n      anti-endless-loop rule and it is a HARD stop: not a lower bar, not a\n      different card - nothing. A shopper who has assembled two pairs is the\n      last one who needs a fifth bottle suggested, and a bag of two has five or\n      six partners the model rates highly, so without this the slot would keep\n      producing them for as long as the shopper kept saying yes.\n\n   2. A PERFECT PARTNER OUTRANKS EVERYTHING ELSE. If any bottle in the bag has\n      a partner the matrix rates `great` and that partner is not already in the\n      cart, that is the offer - owner (d), verbatim: \"the cart upsell should\n      suggest that instead\". It outranks the free-delivery upsell below it\n      because a reason to buy is stronger than a reason to spend.\n\n      This is a CHANGE of behaviour from 2026-08-22, on purpose. A bag holding\n      one completed combo used to stand the offer down entirely; owner (d) says\n      one combo is an opportunity for a second, and only two is enough.\n\n      AND THE BOTTLE IT IS ANCHORED ON MUST BE UNSPOKEN FOR (owner, 2026-08-26:\n      \"it should not recommend a product that is already layered nicely with\n      another product in the cart\"). A scent the list has drawn inside a block is\n      not shopped for - see bagPairedHandles - so an offer is always about a\n      bottle that genuinely has nobody. A bag whose every bottle sits in a block\n      therefore produces no candidate at all, and rule 4 catches it: nothing\n      sold, and the pair they are holding explained instead.\n\n   3. A LONE 50ml GETS ITS 10ml, AND THE DELIVERY CONSEQUENCE WITH IT. Rule 2\n      only fires on the model's TOP verdict, so a scent with no `great` partner\n      leaves a one-bottle bag with nothing said to it at all. Owner (c) is about\n      exactly that shopper: offer the cheap yes, and say what it does to the\n      free-delivery bar. What it must NEVER do is promise a threshold it does\n      not cross - see bagShipConsequence, which states the shortfall when there\n      is one and says nothing when free delivery is already unlocked.\n\n   4. EVERY COMBO HELD GETS A CARD SAYING HOW TO WEAR IT. The ritual card is\n      not an offer - it has no button - which is why the two-nos memory below\n      does not silence it and why rule 1 does not either.\n\n      IT USED TO BE \"ONE COMBO\", and the reason it stopped is worth stating.\n      Above one combo the slot could not pick which pair it meant, so it said\n      nothing (owner, 2026-08-22: \"if there is multiple combos not to show HOW\n      TO WEAR IT tho\") - and rule 1 had already silenced the offer at two. The\n      two silences met, so the shopper who had done exactly what the bag was\n      built to encourage, and was holding two real pairs, was the one shopper\n      the slot had nothing at all to say to. It does not have to pick any more:\n      the cards are pages of a deck with an arrow between them, one per block\n      the list drew, in list order (owner, 2026-08-31: \"allow to show multiple\n      layer wells, uppsells and things and > arrow to view other combos\").\n\n      Since rule 2 stopped shopping for bottles that already sit in a block,\n      this is also where a bag that is nothing BUT a pair lands, and that is the\n      whole point of it (owner, 2026-08-26: \"if there is no layering\n      recomendation then it should not show, and instead the thingy below that\n      says WHY they layer nicely\").\n\n   AND THE TWO-NOS MEMORY, which is older than this ladder and survives it\n   (owner, 2026-08-18: \"if you click on it 2 times it would disappear\"). It\n   gates the two SELLING rules and nothing else: dismissing twice says the\n   shopper is not shopping for another bottle, not that they no longer want to\n   know which of the two they have goes on first.\n\n   ---- THE DECK, AND WHAT IS ALLOWED IN IT ----\n   The slot holds one card at a time and an arrow moves between them. Offers\n   lead, because a shopper reading a bag is deciding whether to add, and the\n   held combos follow, because they are a fact rather than a question. The\n   count is capped where the owner capped it: at most BAG_OFFER_MAX offers,\n   however many the engine found. Ritual cards are not capped - the bag itself\n   is the cap, since bagChemistry stops reading past BAG_CHEM_MAX bottles.\n   ========================================================================= */\nconst BAG_COMBO_STOP = 2;\n/* ---- AND NEVER MORE THAN TWO OFFERS AT ONCE ----\n   (owner, 2026-08-31: \"for upsells we do not want to spam, when there is 2\n   upsells already it is enough\".)\n\n   The same number as the stop above, and the same instinct behind it: two is\n   as much shopping as a bag is allowed to do on a shopper's behalf. The deck\n   can hold more cards than this - the ritual cards are not offers and are not\n   counted here - but only two of them are ever asking for money.\n\n   The engine usually finds four or five great partners for a two-bottle bag,\n   so this is a real cut rather than a ceiling nothing reaches. */\nconst BAG_OFFER_MAX = 2;\n\n/* Returns the DECK, not a single verdict:\n\n     offers   0-2 cards asking for a bottle    ('combo' | 'lone-fifty')\n     rituals  true when the held combos get their own cards\n     why      one line, for a human reading this in a console\n\n   The two are gated differently and that is the whole shape of it. An OFFER is\n   the shop asking for money and is silenced by the stop, by the two-nos memory\n   and by the cap. A RITUAL card asks for nothing - it says which of two\n   bottles the shopper already owns goes on first - so it is silenced by none\n   of them, which is what \"if there is no layering recomendation then it should\n   not show, and instead the thingy below that says WHY they layer nicely\"\n   (owner, 2026-08-26) asked for, and what \"allow to show multiple layer wells\"\n   (2026-08-31) extends to every combo in the bag rather than the first. */\nfunction bagOfferPlan(facts) {\n  const f = facts || {};\n  if (!f.bottles) return { offers: [], rituals: false, why: 'the bag is empty' };\n  const held = f.combos || 0;\n  const quiet = (f.snubs || 0) >= (f.snubLimit || BAG_SNUB_LIMIT);\n  const stopped = held >= BAG_COMBO_STOP;\n  const offers = [];\n  if (!quiet && !stopped) {\n    (f.greatPartners || []).slice(0, BAG_OFFER_MAX).forEach((pick) => {\n      offers.push({ kind: 'combo', pick: pick });\n    });\n    /* The lone-50ml mini is a DIFFERENT offer, not a third partner, and it is\n       only reachable when the bag has no great partner to name - a shopper\n       being shown a perfect match does not also need to be sold a small\n       version of something else. */\n    if (!offers.length && f.lonePartner) offers.push({ kind: 'lone-fifty', pick: f.lonePartner });\n  }\n  const why = offers.length\n    ? offers.length + ' offer(s) - ' + (offers[0].kind === 'combo' ? 'great partners missing from the bag' : 'one 50ml, and its 10ml partner')\n    : stopped ? 'two combos already - the bag stops asking'\n    : quiet ? 'dismissed twice this session'\n    : 'nothing good enough to offer';\n  return { offers: offers, rituals: held > 0, why: why };\n}\n\n/* ---- WHAT ADDING THIS DOES TO FREE DELIVERY, SAID HONESTLY ----\n   (owner, 2026-08-26: \"upsell the 10ml with + free shipping on layering combo\n   tho\".)\n\n   Both numbers are already on the page and neither is recomputed here: the\n   threshold is on the delivery bar the bag renders at its head, and the total\n   is on the bag's own wrapper - both server-rendered on every cart render, and\n   both moved by the optimistic pass, so they cannot drift from the figures the\n   shopper is reading.\n\n   THREE ANSWERS, AND ONLY ONE OF THEM IS A PROMISE:\n     already free    say nothing. The rail above has said it.\n     this crosses    \"This takes you over free delivery.\"\n     it does not     the shortfall, in money, and no claim at all.\n   The third case is the one this function exists for. Adding an R 260 mini to\n   an R 300 bag does not unlock anything, and a row that said it did would be\n   selling a rate the checkout is going to refuse. */\nfunction bagShipConsequence(cents) {\n  const inner = bagScope();\n  const bar = bagPick('[data-ship-bar]');\n  if (!inner || !bar) return '';\n  if (bar.getAttribute('data-ship-unlocked') === '1') return '';\n  const threshold = parseInt(bar.getAttribute('data-ship-threshold'), 10);\n  const total = parseInt(inner.getAttribute('data-total-cents'), 10);\n  if (!threshold || !Number.isFinite(total)) return '';\n  const short = threshold - total - Math.max(0, cents || 0);\n  if (short <= 0) return 'This takes you over free delivery.';\n  return bagMoney(short) + ' still to go for free delivery.';\n}\n\n/* WHICH GOES ON FIRST, if the matrix has an opinion. The engine's own\n   directions, taken verbatim rather than reworded, so the bag, the PDP and the\n   layering studio give one instruction. Empty when the report carries none -\n   an invented order is worse than no order. */\nfunction pairFirstSay(item) {\n  const dir = (item && item.report && item.report.directions) || {};\n  const partner = item && item.p;\n  if (!dir.firstTitle || !partner) return '';\n  return dir.first === partner.h\n    ? dir.firstTitle + ' goes on first.'\n    : dir.firstTitle + ' first, then this over it.';\n}\n\n/* THE CAPTION ON A BLOCK, and deliberately not the sentence the slot beneath it\n   says. This one answers WHY these bottles belong to each other - the notes\n   they share, or the merchant's own call - and the ritual card answers HOW they\n   go on. One fact each, so the sheet never says the same thing twice.\n\n   A BLOCK CAN BE BIGGER THAN A PAIR. Three scents that each bond to the middle\n   one are one block, and they share the block without necessarily sharing the\n   notes: \"both carry bergamot\" printed over three bottles is not loose, it is\n   false. So above two members the notes are INTERSECTED across every pair\n   inside the block and only what survives all of them is claimed. When nothing\n   survives, the block says its heading and nothing more - \"works well together\"\n   is still true of it, and a caption that is not true of every row in the block\n   it captions has no business being there. */\nconst GROUP_ALL = { 3: 'all three', 4: 'all four', 5: 'all five' };\nfunction groupWhy(item, members, chem) {\n  if (!item) return '';\n  const count = members ? members.length : 2;\n  const report = item.report || {};\n  let shared = (report.shared || []).map((n) => String(n).toLowerCase()).filter(Boolean);\n  if (count > 2) {\n    const inside = new Set(members);\n    ((chem && chem.pairs) || []).forEach((p) => {\n      if (!inside.has(p.a) || !inside.has(p.b)) return;\n      const theirs = new Set(((p.report && p.report.shared) || [])\n        .map((n) => String(n).toLowerCase()));\n      shared = shared.filter((n) => theirs.has(n));\n    });\n  }\n  shared = shared.slice(0, 2);\n  const notes = shared.length > 1 ? shared[0] + ' and ' + shared[1] : shared[0];\n  const all = count > 2 ? (GROUP_ALL[count] || 'all of them') : 'both';\n  if (notes) return all + ' carry ' + notes;\n  /* the merchant's endorsement and the model's own short reason are both\n     statements about ONE pair, so neither may caption a bigger block */\n  if (count > 2) return '';\n  if (item.curated) return item.curated === 2 ? 'picked by JUCE to wear together' : 'a JUCE pairing';\n  const short = report.why && report.why.short;\n  return short ? short.charAt(0).toLowerCase() + short.slice(1) : '';\n}\n\n/* The AI mark (the drop with its spark, 2026-10-27), built in script because\n   these blocks are assembled client-side from the catalogue index. Same\n   drawing as `sparkle` in snippets/icon.liquid, which is what the\n   server-rendered block renders - if one changes, change both. */\nfunction sparkGlyph() {\n  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n  svg.setAttribute('viewBox', '0 0 24 24');\n  svg.setAttribute('fill', 'none');\n  svg.setAttribute('stroke', 'currentColor');\n  svg.setAttribute('stroke-width', '2');\n  svg.setAttribute('stroke-linecap', 'round');\n  svg.setAttribute('stroke-linejoin', 'round');\n  svg.setAttribute('aria-hidden', 'true');\n  [['M12 2.8C9.6 6.1 5.6 10.4 5.6 14.4a6.4 6.4 0 0 0 12.8 0C18.4 10.4 14.4 6.1 12 2.8Z', false],\n    ['M12 10.6Q12.52 13.78 15.7 14.3Q12.52 14.82 12 18Q11.48 14.82 8.3 14.3Q11.48 13.78 12 10.6Z', true]]\n    .forEach(([d, solid]) => {\n      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n      path.setAttribute('d', d);\n      if (solid) { path.setAttribute('fill', 'currentColor'); path.setAttribute('stroke', 'none'); }\n      svg.appendChild(path);\n    });\n  return svg;\n}\n\nfunction setGroupWhy(section, text) {\n  let el = section.querySelector('[data-group-why]');\n  if (!el) {\n    if (!text) return;\n    const head = section.querySelector('.cart-group__head');\n    if (!head) return;\n    el = document.createElement('span');\n    el.className = 'cart-group__why';\n    el.setAttribute('data-group-why', '');\n    head.appendChild(el);\n  }\n  el.textContent = text;\n  el.hidden = !text;\n}\n\nfunction makeAutoGroup(key, why) {\n  const section = document.createElement('section');\n  section.className = 'cart-group cart-group--auto';\n  section.setAttribute('data-cart-group', key);\n  const head = document.createElement('p');\n  head.className = 'cart-group__head';\n  const kind = document.createElement('span');\n  kind.className = 'cart-group__kind';\n  /* no tag: this block is the model's own reading of a bag a shopper filled a\n     bottle at a time, and groupKind says so in those words */\n  kind.textContent = groupKind('');\n  head.append(sparkGlyph(), kind);\n  const body = document.createElement('div');\n  body.className = 'cart-group__body';\n  section.append(head, body);\n  setGroupWhy(section, why);\n  return section;\n}\n\n/* ---- THE LIST GROUPS ITSELF ----\n   A block the SERVER drew (the `_layer_set` property, written at the moment two\n   scents were added together) already says the right words; it just does not\n   know the chemistry, so it gains the reason. Scents a shopper assembled one at\n   a time gain the whole block. Both end up as the same object, because a pair\n   is a pair however it got here.\n\n   The rows are MOVED, never redrawn: they carry line keys, an open size panel,\n   focus and half-finished animations, and rebuilding them would throw all four\n   away on every quantity tap. A block lands at the position of its FIRST\n   member, so the bag keeps the order it was built in and the later half comes\n   up to meet the earlier one rather than the pair jumping somewhere new.\n\n   A server block is never restructured. Its rows carry a property saying they\n   belong to each other, the shopper caused that, and re-grouping them here\n   would be the model overruling a decision it did not make.\n\n   IDEMPOTENT BY SIGNATURE. refreshDrawerPairs calls this twice on the first\n   open of a session - once off the cached index so the list never re-arranges\n   under a thumb, once more when the fetch lands - and the second call must not\n   tear down and rebuild what the first one drew. */\n/* Every pair inside a drawn block goes into the ledger, so a block of three\n   comes back as a block of three rather than as whichever two the model still\n   likes best. It is only ever additive - see the note over keptCombos. */\nfunction rememberBlock(members, chem, declared) {\n  const list = (members || []).filter(Boolean);\n  if (list.length < 2) return;\n  /* ---- A BLOCK OF FOUR IS NOT SIX COMBOS ----\n     This wrote EVERY pair inside a block, which is wrong for an auto one and\n     was caught the day it shipped. A block is a CLUSTER held together by a\n     chain of real bonds, not a clique: {elegant-iris, floral-goddess,\n     pink-cashmere, sunbeam} is drawn as one because floral-goddess bonds all\n     three, and writing all six internal pairs promoted elegant-iris|sunbeam -\n     which the engine calls RISKY - to a permanent bond for the rest of the\n     session. Measured in the bag harness: three of the seven \"bonded\" pairs in\n     that bag were the ledger's own inventions, one good and two risky.\n\n     That breaks the promise in the note over keptCombos: what is remembered is\n     the DECISION to group them, never a claim about them. So an auto block\n     remembers only the edges the model actually bonded. The cluster still\n     survives a re-read - every edge that built it is in the ledger - it simply\n     cannot grow bonds it never had. */\n  if (declared) {\n    /* A SERVER BLOCK IS THE DECISION ITSELF. `_layer_set` was written when the\n       shopper added these together out of the layering studio, so every pair\n       inside it is a pairing a person chose. There is nothing to infer. */\n    for (let i = 0; i < list.length; i++) {\n      for (let j = i + 1; j < list.length; j++) keepCombo(list[i], list[j]);\n    }\n    return;\n  }\n  const inside = new Set(list);\n  ((chem && chem.bonded) || []).forEach((p) => {\n    if (inside.has(p.a) && inside.has(p.b)) keepCombo(p.a, p.b);\n  });\n}\n\nfunction groupBagLines(prep, list) {\n  const chem = bagChemistry(prep, list);\n  if (!chem) return;\n\n  Array.from(list.querySelectorAll('[data-cart-group]')).forEach((section) => {\n    if (section.classList.contains('cart-group--auto')) return;\n    /* the heading says what the shopper chose, off the tag the add wrote.\n       The server prints the same thing from the same rule, so this only ever\n       matters when a block reaches the DOM without a server render behind it -\n       but the tag is the authority either way, and one function owns it. */\n    const kindEl = section.querySelector('.cart-group__kind');\n    if (kindEl) {\n      const want = groupKind(section.dataset.groupTag);\n      if (kindEl.textContent !== want) kindEl.textContent = want;\n    }\n    const inside = bagLineRows(section).map((row) => row.getAttribute('data-handle'));\n    const members = inside.filter((h, i) => h && inside.indexOf(h) === i);\n    const pair = chem.pairs.filter((p) => inside.indexOf(p.a) !== -1 && inside.indexOf(p.b) !== -1)[0];\n    setGroupWhy(section, groupWhy(pair, members, chem));\n    /* A SERVER BLOCK IS THE STRONGEST FORM OF \"the user selected this\" - it\n       carries `_layer_set`, written at the moment the shopper added the two\n       together out of the layering studio. Writing it to the ledger is what\n       keeps the two grouped after the property has done its job, and what\n       keeps the pair drawn if the studio's own ordering ever stops matching\n       the model's - which is why it, and only it, remembers every pair inside\n       itself rather than only the model's own edges. */\n    rememberBlock(members, chem, true);\n  });\n\n  /* A row the server grouped is not available to a block drawn here. A row this\n     function already moved IS - otherwise the signature would read empty on the\n     second call and rebuild the very blocks it is meant to leave alone. */\n  const free = new Set();\n  bagLineRows(list).forEach((row) => {\n    const owner = row.closest('[data-cart-group]');\n    if (owner && !owner.classList.contains('cart-group--auto')) return;\n    free.add(row.getAttribute('data-handle'));\n  });\n  const clusters = chem.clusters\n    .map((block) => block.filter((h) => free.has(h)))\n    .filter((block) => block.length > 1);\n  const signature = clusters.map((block) => block.join('+')).join(',');\n  if (list.getAttribute('data-auto-groups') === signature) return;\n\n  /* Flat first. A block whose members have changed is not edited in place,\n     because the row that has to leave it may belong to the block built next. */\n  Array.from(list.querySelectorAll('.cart-group--auto')).forEach((section) => {\n    const body = section.querySelector('.cart-group__body');\n    if (body) while (body.firstChild) list.insertBefore(body.firstChild, section);\n    section.remove();\n  });\n\n  clusters.forEach((block) => {\n    const members = new Set(block);\n    const rows = Array.from(list.children).filter((node) =>\n      node.getAttribute && members.has(node.getAttribute('data-handle')));\n    if (rows.length < 2) return;\n    const pair = chem.bonded.filter((p) => members.has(p.a) && members.has(p.b))[0];\n    const section = makeAutoGroup('auto:' + block.join('+'), groupWhy(pair, block, chem));\n    list.insertBefore(section, rows[0]);\n    const body = section.querySelector('.cart-group__body');\n    rows.forEach((row) => body.appendChild(row));\n    /* The model's own edges inside this cluster, never the whole clique. */\n    rememberBlock(block, chem, false);\n  });\n  list.setAttribute('data-auto-groups', signature);\n}\n\n/* ---- THE SHEET HAS NOTHING LEFT TO SELL, SO IT SAYS SOMETHING USEFUL ----\n\n            [ tile | tile ]\n        MYSTIC HAZE + COASTAL DAZE\n     Apply Mystic Haze first as the anchor, then\n        add Coastal Daze lightly on top.\n             [ In season now ]\n\n   Same card grammar as the broken-duet state that shares this slot - two tiles\n   on one bed, the two names, one sentence - because both are about a pair the\n   shopper already has, and the sheet keeps one shape for that. What it does NOT\n   have is a button: there is nothing to buy here, which is the entire point.\n\n   The sentence is the engine's own `directions.text`, used verbatim rather than\n   reworded, so the bag, the PDP and the layering studio all give one\n   instruction. The block heading up in the list says WHY the two belong\n   together; this says HOW they go on, and neither repeats the other. */\nfunction ritualTile(list, rec) {\n  const box = document.createElement('span');\n  box.className = 'bag-ritual__tile';\n  /* THE BOTTLE'S OWN ROW, not whichever row for this scent comes first. A bag\n     holding both a 50ml and a paid 3ml of the same scent has two rows under\n     one handle, and querySelector takes the earlier of them - which drew the\n     pair with a photograph of a sample vial. */\n  const rowFor = Array.from(list.querySelectorAll('[data-handle=\"' + cssAttr(rec.h) + '\"]'))\n    .filter(bagRowIsBottle)[0];\n  const art = rowFor && rowFor.querySelector('.line-item__media img');\n  const src = (art && (art.currentSrc || art.src)) || rec.pimg || rec.img || '';\n  if (src) {\n    const img = document.createElement('img');\n    img.src = String(src).replace(/width=\\d+/, 'width=160');\n    img.alt = ''; img.loading = 'lazy'; img.decoding = 'async';\n    box.appendChild(img);\n  } else {\n    const ph = document.createElement('span');\n    ph.className = 'pair-row__ph';\n    ph.textContent = 'Placeholder';\n    box.appendChild(ph);\n  }\n  return box;\n}\n/* Only the chips that say something the heading and the instruction cannot:\n   the half of the day the pair gains, and whether it suits the season the\n   shopper is standing in. The chemistry chips are dropped on purpose - the\n   block heading already names the notes the two share. */\nfunction ritualChips(prep, a, b, item, seq) {\n  if (!JE.pairContext) return null;\n  let ctx = null;\n  try { ctx = JE.pairContext(prep, a, b, { report: item.report }); } catch (e) { return null; }\n  const keep = ((ctx && ctx.chips) || [])\n    .filter((chip) => chip.k === 'season' || chip.k.indexOf('extend-') === 0)\n    .slice(0, 2);\n  if (!keep.length) return null;\n  /* ---- A PILL THAT CAN EXPLAIN ITSELF (2026-08-27) ----\n     A shopper looked at \"Adds the daytime\" over their bag and asked what it\n     meant. The engine had always built the sentence that answers it and this\n     renderer had always dropped it on the floor. It borrows the free tester's\n     info dot rather than inventing a second explain pattern: same button, same\n     popover, same initTips delegation, and the same title= fallback for a\n     browser with no popover support.\n\n     A <div> and not a <p>, to match the rule cart-lines.liquid states about\n     .line-item__meta: a <div popover> inside a <p> is invalid nesting. This\n     row is built with createElement so no parser ever runs on it and nothing\n     would actually break, but one shape for one component beats two. */\n  const wrap = document.createElement('div');\n  wrap.className = 'bag-ritual__chips';\n  /* Ids are scoped by SURFACE **and by card**, because initTips resolves a\n     bubble's button by its popovertarget and two bubbles holding one id point\n     both dots at the first one.\n\n     The surface half is /cart, which renders the drawer and the page at once -\n     exactly how cart-lines.liquid scopes its tips by context. The CARD half\n     arrived with the deck (2026-08-31): the slot can hold a ritual card per\n     combo in the bag now, so \"the ritual card\" stopped being a single thing\n     and `seq` is which one this is. Without it a two-combo bag put two dots on\n     one bubble and the second card's chip explained the first card's pair. */\n  const where = bagIsPage() ? 'page' : 'drawer';\n  keep.forEach((chip, i) => {\n    const el = document.createElement('span');\n    el.className = 'bag-ritual__chip';\n    el.appendChild(document.createTextNode(chip.t));\n    if (chip.long) {\n      el.setAttribute('data-tip-host', '');\n      const id = 'tip-ritual-' + where + '-' + (seq || 0) + '-' + i;\n      const dot = document.createElement('button');\n      dot.className = 'info-dot';\n      dot.type = 'button';\n      dot.setAttribute('data-tip-btn', '');\n      dot.setAttribute('popovertarget', id);\n      dot.setAttribute('aria-label', 'What \"' + chip.t + '\" means');\n      dot.setAttribute('title', chip.long);\n      dot.textContent = 'i';\n      const bubble = document.createElement('div');\n      bubble.className = 'info-bubble';\n      bubble.id = id;\n      bubble.setAttribute('popover', '');\n      bubble.setAttribute('data-tip-bubble', '');\n      bubble.textContent = chip.long;\n      el.appendChild(dot);\n      el.appendChild(bubble);\n    }\n    wrap.appendChild(el);\n  });\n  return wrap;\n}\nfunction ritualCard(prep, item, list, seq) {\n  const a = prep.byHandle[item.a], b = prep.byHandle[item.b];\n  if (!a || !b) return null;\n  const report = item.report || {};\n  const dir = report.directions || {};\n  const how = dir.text || (report.why && report.why.long) || '';\n  if (!how) return null;\n\n  const card = document.createElement('div');\n  card.className = 'bag-ritual';\n  const pics = document.createElement('span');\n  pics.className = 'bag-ritual__pics';\n  pics.setAttribute('aria-hidden', 'true');\n  /* the heavier of the two on the left, because that is the one that goes on\n     first and the sentence under it reads left to right */\n  const lead = dir.first === b.h ? b : a;\n  const over = lead === a ? b : a;\n  pics.append(ritualTile(list, lead), ritualTile(list, over));\n  card.appendChild(pics);\n\n  const names = document.createElement('p');\n  names.className = 'bag-ritual__names';\n  const first = document.createElement('span');\n  first.className = 'bag-ritual__name';\n  first.textContent = lead.t;\n  const plus = document.createElement('span');\n  plus.className = 'bag-ritual__plus';\n  plus.textContent = '+';\n  const second = document.createElement('span');\n  second.className = 'bag-ritual__name';\n  second.textContent = over.t;\n  names.append(first, plus, second);\n  card.appendChild(names);\n\n  const why = document.createElement('p');\n  why.className = 'bag-ritual__why';\n  why.textContent = how;\n  card.appendChild(why);\n\n  const chips = ritualChips(prep, lead, over, item, seq);\n  if (chips) card.appendChild(chips);\n  return card;\n}\n\n/* ---- RULE 3'S CANDIDATE: THE 10ml THAT LAYERS WITH THE ONE BOTTLE HELD ----\n   Deliberately a different search from bestPairForBag, and the two differences\n   are the whole reason this exists:\n\n     THE FLOOR IS `good`, NOT `great`. Rule 2 has already looked for a perfect\n     match and not found one; refusing to say anything to a shopper holding one\n     bottle because the catalogue has no 99-scoring partner for it is the bag\n     being precious rather than useful. `good` is the floor the layering studio\n     itself uses, so nothing offered here is below what the studio would show.\n\n     IT MUST HAVE A SELLABLE 10ml. The offer IS the mini - \"upsell the 10ml\"\n     (owner) - so a partner with no mid/mpc in the index is not a candidate at\n     all rather than a candidate quietly sold at 50ml. */\nfunction bestMiniForLone(prep, handle, bag) {\n  const anchor = prep.byHandle[handle];\n  if (!anchor || !JE) return null;\n  const snubbed = snubbedPairs();\n  let top = null;\n  JE.layerPairs(prep, anchor, {\n    limit: 8, min: 'good', availableOnly: true,\n    exclude: bag, preferAudience: anchor.g\n  }).forEach((item) => {\n    if (!item.p || !item.p.mid || !(item.p.mpc > 0)) return;\n    if (snubbed.has(item.p.h)) return;\n    item.anchor = anchor;\n    item.curated = pairCurated(item);\n    if (!top || betterPair(item, top)) top = item;\n  });\n  return top;\n}\n\n/* One pass over the bag, driving both halves of the answer: the list groups\n   itself, and the slot asks the ladder what it is allowed to say. Called\n   synchronously off the cached index on every render, and again if a\n   first-of-session fetch has to land first.\n\n   Everything below the ladder call is presentation. The DECISION is entirely\n   in bagOfferPlan, and it is handed plain numbers - which is what lets the\n   rules be exercised against fixtures with no browser in the room. */\nfunction paintBagChemistry(prep, list, host) {\n  groupBagLines(prep, list);\n  const slot = host.querySelector('[data-pairs-slot]');\n  if (!slot) return;\n  const label = host.querySelector('[data-pairs-label]');\n  const chem = bagChemistry(prep, list);\n\n  /* Every handle in the list, INCLUDING a row on its way out - a suggestion\n     that offers back the bottle the shopper is in the middle of removing is\n     worse than no suggestion at all, and the server render that settles it is\n     still in flight. */\n  const bag = Array.from(list.querySelectorAll('[data-handle]'))\n    /* Deliberately NOT bagLineRows - that drops the row on its way out and this\n       list must keep it. It shares the 3ml test and nothing else. */\n    .filter(bagRowIsBottle)\n    .map((node) => node.getAttribute('data-handle')).filter(Boolean);\n  const combos = bagCombos(list);\n  /* The bottles the list has already paired off, read straight off the blocks\n     groupBagLines drew a moment ago. Rule 2 will not shop for any of them. */\n  const paired = bagPairedHandles(list);\n  /* The two candidates the ladder chooses between, resolved before it is\n     asked so the rules themselves stay free of the engine. The second is only\n     searched for when the bag is the one shape rule 3 is written for, so the\n     ordinary case still costs exactly the one ranking it always did. */\n  const lone = bag.length && combos < BAG_COMBO_STOP ? bagLoneFifty(list) : null;\n  const greatPartners = bag.length && combos < BAG_COMBO_STOP ? bestPairForBag(prep, bag, paired) : [];\n  const lonePartner = (!greatPartners.length && lone) ? bestMiniForLone(prep, lone, bag) : null;\n\n  const plan = bagOfferPlan({\n    bottles: bag.length,\n    combos: combos,\n    snubs: snubbedPairs().size,\n    snubLimit: BAG_SNUB_LIMIT,\n    greatPartners: greatPartners,\n    lonePartner: lonePartner\n  });\n\n  /* ---- THE DECK ----\n     One entry per card, each carrying its own heading, because the heading is\n     what says which KIND of card this is. A lone 50ml is being sold a mini and\n     a reason about delivery; a bag with a perfect match is being sold the\n     match; a pair already held is being told which bottle goes on first. Same\n     slot, same arrow, honest heading on every page of it. */\n  const deck = [];\n  plan.offers.forEach((offer) => {\n    deck.push({\n      label: offer.kind === 'lone-fifty' ? 'Make it a layering combo' : 'Layers with your cart',\n      node: pairRow(offer.pick, offer.kind)\n    });\n  });\n  if (plan.rituals) {\n    /* ---- A PAIR ALREADY HELD ----\n       No button on these cards: there is nothing to buy here, which is the\n       entire point. They answer HOW the two go on; the block heading up in the\n       list answers WHY they belong together, and neither repeats the other. */\n    bagBlockPairs(chem, list).forEach((pair, i) => {\n      /* `i` is what keeps the info-dot popover ids apart between two ritual\n         cards in one slot - see the note in ritualChips. */\n      const card = ritualCard(prep, pair, list, i);\n      if (card) deck.push({ label: 'How to wear it', node: card, ritual: true });\n    });\n  }\n\n  if (!deck.length) {\n    /* EMPTIED, NOT JUST HIDDEN. Most renders reach this function on a slot the\n       Section Rendering swap has just rebuilt, so it is already empty - but the\n       in-place path (the second call, when a first-of-session index fetch\n       lands) has a slot full of the last deck. Leaving those cards under a\n       hidden host keeps a scroll handler bound to nodes nothing can reach, and\n       makes the next showBagPairs(host, true) flash a deck the bag no longer\n       has any reason to offer. */\n    slot.replaceChildren();\n    const nav = host.querySelector('[data-pairs-nav]');\n    if (nav) nav.hidden = true;\n    host.classList.remove('is-ritual');\n    showBagPairs(host, false);\n    return;\n  }\n  paintBagDeck(host, slot, label, deck);\n  showBagPairs(host, true);\n}\n\n/* ---- ONE SLOT, SEVERAL CARDS, AN ARROW BETWEEN THEM ----\n   (owner, 2026-08-31: \"allow to show multiple layer wells, uppsells and things\n   and > arrow to view other combos etc\".)\n\n   EVERY CARD IS IN THE DOM AND THE SLOT IS A SNAP SCROLLER. Rebuilding the\n   card on each turn would throw away the size rungs, a half-finished add and\n   the popover state inside a pair row every time the shopper looked at the\n   next one, which is the same reason groupBagLines MOVES rows rather than\n   redrawing them.\n\n   THE SWIPE IS THE BROWSER'S, NOT OURS (owner, 2026-08-31: \"Also allow to\n   slide the how to wear for mobile slide\"). The cards were shown one at a time\n   with `hidden` until then, which no gesture can reach. They sit in a\n   horizontal scroll-snap track now, so the platform arbitrates the axis - a\n   vertical drag still scrolls the sheet this slot lives at the foot of - and\n   the momentum, the snap and the rubber-banding come free. There is no pointer\n   handler here to fight the drawer with. components.css carries the track and\n   the reasoning for it.\n\n   SO THE INDEX IS READ, NOT HELD. scrollLeft is the truth once a thumb can\n   move it, and the arrows drive the same scroller rather than a second piece\n   of state a swipe would immediately contradict.\n\n   IT STILL RESETS ON EVERY REPAINT. A repaint means the bag changed - a bottle\n   added, a quantity tapped, an offer dismissed - and after that the shopper is\n   looking at a different set of answers; leaving them on page three of the old\n   deck would be the slot remembering something the bag no longer holds. The\n   one thing it keeps is the OFFERS-FIRST order, so the card that used to be\n   the whole slot is still the card that opens it.\n\n   THE NAV IS ABSENT AT ONE CARD rather than disabled. A greyed-out arrow pair\n   over a single suggestion is two controls that exist to say there is nothing\n   to press - and with one card there is nothing to scroll either, so the track\n   is inert and the slot behaves exactly as it did before the deck existed. */\nfunction paintBagDeck(host, slot, label, deck) {\n  slot.replaceChildren(...deck.map((card) => card.node));\n  /* Back to the first card. A repainted slot keeps its old scrollLeft\n     otherwise, which would open the deck part-way through card two. */\n  slot.scrollLeft = 0;\n\n  const paint = (n) => {\n    const card = deck[n] || deck[0];\n    if (label) label.textContent = card.label;\n    host.classList.toggle('is-ritual', !!card.ritual);\n  };\n  paint(0);\n\n  const nav = host.querySelector('[data-pairs-nav]');\n  const at = nav && nav.querySelector('[data-pairs-at]');\n  if (nav) nav.hidden = deck.length < 2;\n  if (deck.length < 2) {\n    /* Cleared rather than left standing. It is inside a hidden element so\n       nothing paints it, but it is also aria-live, and a stale \"3 / 3\" is one\n       CSS change away from being announced over a slot holding one card. */\n    if (at) at.textContent = '';\n    return;\n  }\n  if (at) at.textContent = '1 / ' + deck.length;\n\n  /* WHICH CARD THE TRACK IS SHOWING, from the only thing that knows: where it\n     has been scrolled to. Rounded on the card pitch rather than compared\n     against each child's offset, because the snap lands within a pixel of the\n     boundary and a strict comparison flickers between two answers there. */\n  let seat = 0;\n  /* WHERE AN ARROW SENT IT, and -1 the rest of the time. See below. */\n  let bound = -1;\n  /* ---- THE PITCH IS THE DISTANCE BETWEEN CARDS, NOT THE WIDTH OF ONE ----\n     The track is a flex row with `gap: var(--space-3)`, so card k starts at\n     k * (width + gap) and dividing scrollLeft by the WIDTH drifts a whole gap\n     further out of step with every card. It also made `go` scroll to a point\n     `to * gap` short of the card it was aiming at and leave the snap to make\n     up the difference.\n\n     Measured off two real cards rather than by reading the gap token back,\n     so a change to --space-3 - or a card that ever stops being full width -\n     cannot put the two answers out of step again. offsetLeft is the layout\n     position, so it is unaffected by both the scroll and the transforms a\n     dismissing row carries. */\n  const pitch = () => {\n    const one = slot.firstElementChild;\n    if (!one) return slot.clientWidth || 1;\n    const two = one.nextElementSibling;\n    return (two ? two.offsetLeft - one.offsetLeft : one.offsetWidth) || 1;\n  };\n  const where = () => Math.max(0, Math.min(deck.length - 1, Math.round(slot.scrollLeft / pitch())));\n  const show = (n) => {\n    seat = n;\n    paint(n);\n    if (at) at.textContent = (n + 1) + ' / ' + deck.length;\n    /* The cards are different heights, so the undo pill's clearance is\n       re-measured on every turn, exactly as it is on show and hide. */\n    measureBagFoot();\n  };\n  /* ---- THE ARROWS OWN THE COUNTER UNTIL THE GLIDE LANDS ----\n     (owner, 2026-09-02: \"on this section on desktop that numbers sometimes\n     glitches\".)\n\n     A smooth scrollTo fires `scroll` for every frame it travels, and `read`\n     answered every one of them. Between two neighbouring cards that costs\n     nothing - there is no third card in between - but the arrows WRAP, so\n     pressing > on the last card of a four-card deck glides the whole width of\n     the track back to the first, and the counter counted itself down on the\n     way: 4/4, 3/4, 2/4, 1/4, with the heading and the undo pill's clearance\n     changing under each one. That is the glitch, and it is exactly why it\n     only happened \"sometimes\" - only the wrap is a long journey.\n\n     So an arrow names its destination, and until the track arrives there the\n     scroll handler says nothing.\n\n     THE LOCK IS TIME-BOUNDED AS WELL, and that half is what makes it safe. A\n     thumb or a trackpad can wrestle a glide off course and land it somewhere\n     the arrow was not aiming at - the track would then never pass through\n     `bound`, and a counter that has stopped answering at all is worse than\n     the spin this exists to prevent. 900ms is well past the longest smooth\n     scroll a track this wide runs, and after it the handler simply goes back\n     to reading the truth. */\n  let till = 0;\n  const read = () => {\n    const n = where();\n    if (bound !== -1) {\n      if (n === bound || Date.now() > till) bound = -1;\n      else return;\n    }\n    if (n === seat) return;\n    show(n);\n  };\n  slot.onscroll = read;\n\n  const go = (n) => {\n    const to = ((n % deck.length) + deck.length) % deck.length;   /* the arrows never dead-end */\n    bound = to; till = Date.now() + 900;\n    slot.scrollTo({ left: to * pitch(), behavior: 'smooth' });\n    /* scrollTo is asynchronous, so the heading is not made to wait for it -\n       saying it here is what makes the press feel answered. */\n    show(to);\n  };\n  /* Rebound rather than delegated: this nav is destroyed and rebuilt by every\n     Section Rendering swap, so a document-level listener would have to look the\n     deck up again on each press and the deck is a closure over this render. */\n  const prev = nav.querySelector('[data-pairs-prev]');\n  const next = nav.querySelector('[data-pairs-next]');\n  if (prev) prev.onclick = () => go(seat - 1);\n  if (next) next.onclick = () => go(seat + 1);\n}\n\nfunction refreshDrawerPairs() {\n  const host = bagPick('[data-drawer-pairs]');\n  if (!host) return;\n  /* ---- A BROKEN PAIR OUTRANKS EVERYTHING ----\n     This is the whole trick behind \"it replaces the layers with your bag\n     section\": refreshDrawerPairs is the TAIL of swapDrawer, so putting the\n     branch here means the card is re-injected into the brand-new slot by the\n     very render that removed the row, with no second host and no node that has\n     to survive the swap. It also runs before the index is consulted, because\n     the card has something true to say without one. */\n  /* Taking the slot RETIRES any suggestion still in flight. The index fetch\n     armed when the sheet opened resolves hundreds of ms later, and its only\n     guard is this token - which this branch used to return past without ever\n     bumping, so a late index landed pairRow() straight over the unpair card\n     and took the \"Put it back\" button with it, under a heading still reading\n     \"Why those two held\". Bumping here is what makes the token cover the one\n     path that can steal the slot. */\n  /* ---- THE LIST GROUPS ITSELF IN THE SAME FRAME AS THE SWAP ----\n     The catalogue index is already in hand on every render but the first of a\n     session, and a block that arrives a frame late re-arranges the bag under\n     the shopper's thumb. Synchronous when it can be, awaited below when it\n     cannot; groupBagLines is idempotent by signature, so running it twice\n     costs one string comparison and changes nothing.\n\n     ABOVE THE UNPAIR BRANCH ON PURPOSE. Grouping is a FACT about the bag; the\n     slot is an OFFER about it, and the two are not the same concern. While a\n     broken duet held the slot this ran after a `return`, so every render\n     landing during that state - a quantity tap, a size swap - handed back a\n     brand-new subtree with no blocks in it at all, and the bag silently\n     un-grouped itself until the card was resolved. */\n  const list = bagPick('.drawer__lines');\n  if (list && JE && juceIndexPrep) groupBagLines(juceIndexPrep, list);\n  if (bagUnpair) { drawerPairsToken++; renderUnpairCard(host); return; }\n  /* back to the resting state - only reachable in place, since a server render\n     ships the label and the class at rest already */\n  const label = host.querySelector('[data-pairs-label]');\n  if (label) label.textContent = 'Layers with your cart';\n  host.classList.remove('is-unpair', 'is-ritual');\n  if (!JE) return;\n  if (!list) { showBagPairs(host, false); return; }\n  if (juceIndexPrep) paintBagChemistry(juceIndexPrep, list, host);\n  const token = ++drawerPairsToken;\n  juceIndex().then((prep) => {\n    /* the sheet may have re-rendered while the index was in flight */\n    if (!prep || token !== drawerPairsToken) return;\n    const mount = bagPick('[data-drawer-pairs]');\n    const rows = bagPick('.drawer__lines');\n    if (!mount || !rows || !mount.querySelector('[data-pairs-slot]')) return;\n    paintBagChemistry(prep, rows, mount);\n  });\n}\n/* Best partner across the WHOLE bag, not just the last thing added: every\n   item anchors a ranking, the winner is whichever candidate scores highest\n   against any of them, and anything already in the cart is excluded twice\n   over (the engine's own exclude list, and again on the way out).\n\n   The winning ANCHOR is carried out with the winner. A shopper holding three\n   bottles could read \"Both carry cardamom and amber\" and have no way to tell\n   which of the three it meant, so the row has to be able to name it. */\nfunction bestPairForBag(prep, bag, paired) {\n  const inBag = new Set(bag);\n  const snubbed = snubbedPairs();\n  const seen = new Map();\n  bag.forEach((handle) => {\n    const anchor = prep.byHandle[handle];\n    if (!anchor) return;\n    /* THIS BOTTLE ALREADY HAS A PARTNER IN THE BAG, so the sheet is not\n       shopping for it (owner, 2026-08-26). It is still excluded from the\n       results below as a member of `bag` - it just stops being an anchor.\n       bagPairedHandles carries the reasoning. */\n    if (paired && paired.has(handle)) return;\n    /* PERFECT MATCHES ONLY (owner, 2026-08-18). `great` is the model's TOP\n       verdict, not its passing one - `good` is the floor the layering studio\n       uses because a shopper who came there to explore wants options. The bag\n       is not that surface: it interrupts a shopper who has already decided, so\n       it has to be right or absent. On a bag with no great partner this now\n       shows nothing at all, which is the intended outcome and not a bug. */\n    /* `audience: true` is the HARD shelf gate; preferAudience is only a\n       ranking bias. This call passed the bias where the gate was meant - the\n       identical bug initPdpLayer carried until it was fixed, and the reason\n       the bag could answer a masculine anchor with a partner the PDP had\n       already ruled out. Both surfaces ask the engine the same question now. */\n    JE.layerPairs(prep, anchor, {\n      limit: 6, min: 'great', availableOnly: true,\n      exclude: bag, audience: true, preferAudience: anchor.g\n    }).forEach((item) => {\n      if (inBag.has(item.p.h)) return;\n      /* already said no to this one, this session */\n      if (snubbed.has(item.p.h)) return;\n      /* belt and braces on the floor above: the row says \"made to layer\", so\n         nothing below the top verdict may ever reach it */\n      if (item.verdict !== 'great') return;\n      /* the bag is \"right or absent\": a cross-shelf partner needs a\n         disclosure this row has no room to carry, so it stands down here -\n         the labeled fallback belongs to the rooms that can explain it */\n      if (item.cross) return;\n      item.anchor = anchor;                       /* the bag item this score came from */\n      item.curated = pairCurated(item);\n      const prev = seen.get(item.p.h);\n      if (!prev || betterPair(item, prev)) seen.set(item.p.h, item);\n    });\n  });\n  /* RANKED, AND ALL OF THEM. The caller takes as many as the offer cap allows -\n     it used to take the single best, which is why a bag with four great\n     partners could only ever be shown one of them and had to be re-rendered to\n     see the next. Sorted with the same comparator that used to pick the\n     winner, so the head of this list is exactly the row that shipped before. */\n  return Array.from(seen.values()).sort((a, b) => (betterPair(a, b) ? -1 : betterPair(b, a) ? 1 : 0));\n}\n/* 2 = on the merchant's layers-well-with list, 1 = on complimenting, 0 = the\n   model's own read. Taken back off the report rather than re-derived, so the\n   row can never claim a curated pairing the score was not built from. */\nfunction pairCurated(item) {\n  const f = ((item.report && item.report.factors) || []).filter((x) => x.key === 'curated' && x.value > 0)[0];\n  if (!f) return 0;\n  return f.short === 'Made to layer' ? 2 : 1;\n}\n/* THE ENGINE'S OWN ORDER, NOT A SECOND ONE ON TOP OF IT.\n\n   This used to compare the curated tier FIRST and the score only as a\n   tie-break, which is not a tie-break at all - it is a lexicographic\n   override, and it made the bag disagree with the product page about the\n   same anchor on 8 of 41 scents. Bold Ambition's page led with Summer\n   Escape (85.3) while the bag offered Bold Soul (75.1), purely because the\n   merchant's list happened to name the weaker pair. The engine ALREADY pays\n   curated 20-26 points inside the score (layerScore, factor `curated`), so\n   ranking on it again double-counts the one thing it weights hardest.\n\n   baseRank is what layerPairs itself sorts on, so taking the maximum of it\n   reproduces the PDP's hero exactly for a one-bottle bag. Curated survives\n   as the genuine tie-break it was written to be: it settles pairs the engine\n   scores level, and nothing wider than that. */\nfunction pairRank(item) {\n  return Number.isFinite(item.baseRank) ? item.baseRank : item.score;\n}\nfunction betterPair(a, b) {\n  const ra = pairRank(a), rb = pairRank(b);\n  /* STRICTLY, and not within a tolerance band. A band is just a smaller\n     second ranking: at 0.5 it still flipped Desert Rose and Golden Babe away\n     from the partner their own product page leads with, which is the whole\n     defect this function is being fixed for. Curated settles a genuine dead\n     heat and nothing wider. */\n  if (ra !== rb) return ra > rb;\n  if (a.curated !== b.curated) return a.curated > b.curated;\n  return a.score > b.score;\n}\n/* One line doing two jobs: WHY this scent, and WHICH bottle in the bag it was\n   picked against. The engine supplies the why (its own reason text, not a\n   reworded copy of it) and the anchor supplies the what; they are said as one\n   clause so the row stays a row. Ordered by how much each says: the merchant's\n   own pairing first, then the notes the two genuinely share, then the shape of\n   the layer, and only then the model's next-best sentence. */\n/* The chemistry clause, in the engine's own words, for a pair that shares no\n   NOTE - two scents can bridge on family or on weight and still be a real\n   pairing. Same string the product page prints under the same pair, so the\n   two surfaces explain a pairing identically.\n\n   Contrast is taken last on purpose: the note line directly under this row\n   (pairFirstSay) already says which bottle goes on first, and 'one anchors,\n   one lifts' is that same fact wearing a different coat. */\nfunction pairChem(item) {\n  const report = item.report || {};\n  const alts = (report.why && report.why.alts) || [];\n  const pick = alts.filter((x) => x.key !== 'contrast')[0] || alts[0];\n  const say = (pick && pick.short) || item.reason || (report.why && report.why.short) || '';\n  return say ? say.charAt(0).toLowerCase() + say.slice(1) : '';\n}\nfunction pairWhy(item) {\n  const name = item.anchor && item.anchor.t;\n  if (!name) return item.reason || 'Wears well with your cart';\n  const report = item.report || {};\n  const factors = report.factors || [];\n  const has = (key) => factors.filter((f) => f.key === key && f.value > 0)[0];\n  const curated = item.curated == null ? pairCurated(item) : item.curated;\n  const shared = (report.shared || []).slice(0, 2)\n    .map((n) => String(n).toLowerCase()).filter(Boolean);\n  const notes = shared.length > 1 ? shared[0] + ' and ' + shared[1] : shared[0];\n\n  /* 40 of 42 products carry curated pairs, so \"Made to layer with X\" alone\n     would be nearly the only sentence the drawer ever says, and the chemistry\n     the model found would be thrown away. Endorsement first (it is the\n     strongest claim, and it holds the anchor name), chemistry as the tail -\n     which is exactly what the 2-line clamp gives up first on a narrow phone. */\n  if (curated) {\n    const lead = (curated === 2 ? 'Made to layer with ' : 'A JUCE pairing with ') + name;\n    /* THE ENDORSEMENT IS NOT A REASON. This branch used to stop at `lead`\n       whenever the two shared no notes, and 'Made to layer with Desert\n       Leather' on its own is the shop asserting a pairing and declining to\n       say why - the owner's report, and the exact case Desert Leather x\n       Coastal Daze hits (shared: [], because they bridge on FAMILY, not on a\n       note). The chemistry the model did find is right there on the report\n       and the product page already prints it; the bag threw it away. */\n    const tail = notes ? 'both carry ' + notes : pairChem(item);\n    return tail ? lead + ' \u00B7 ' + tail : lead;\n  }\n  if (notes) return 'Shares ' + notes + ' with ' + name;\n  /* report.anchor is the heavier of the two - which one it is decides whether\n     this bottle goes under the bag's or over it */\n  if (has('contrast')) return (report.anchor === item.p.h ? 'Anchors ' : 'Lifts ') + name;\n\n  const rest = item.reason || (report.why && report.why.short);\n  if (rest) return 'With ' + name + ', ' + rest.charAt(0).toLowerCase() + rest.slice(1);\n  return 'Layers well with ' + name;\n}\n/* A glyph built in script rather than rendered by snippets/icon.liquid, because\n   this row is assembled client-side from the catalogue index. Same drawing as\n   the Liquid set - if one changes, change both. */\nfunction pairGlyph(kind) {\n  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n  svg.setAttribute('viewBox', '0 0 20 20');\n  svg.setAttribute('fill', 'none');\n  svg.setAttribute('stroke', 'currentColor');\n  svg.setAttribute('stroke-width', kind === 'tick' ? '2' : '1.5');\n  svg.setAttribute('stroke-linecap', 'round');\n  svg.setAttribute('stroke-linejoin', 'round');\n  svg.setAttribute('aria-hidden', 'true');\n  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n  path.setAttribute('d', kind === 'tick' ? 'M4.5 10.5l3.6 3.6L15.5 6.7' : 'M5 5l10 10M15 5L5 15');\n  svg.appendChild(path);\n  return svg;\n}\n\n/* ---- THE SUGGESTION ----\n   One partner, one reason, one note, one control, and a way out.\n\n     [ tile ]  MISS JASMINE                                    x\n               Made to layer with Floral Frenzy - both carry\n               jasmine and amber\n               Floral Frenzy goes on first. R 120 still to go\n               for free delivery.\n     [   10ml   R 260   |   50ml   R 530   ]\n\n   ---- TIGHTENED, 2026-08-26 (owner: \"can we improve that widget also abit on\n   the cart\") ----\n   The row says four things and stops:\n\n     THE NAME.\n     ONE REASON, drawn from the matrix's own words - the merchant's endorsement\n       if there is one, else the notes the two genuinely share, else the model's\n       own sentence. Never a reworded copy: pairWhy picks between the engine's\n       strings, it does not write new ones.\n     ONE NOTE, carrying the two facts a reason cannot: which of the two goes on\n       FIRST (the engine's own directions, verbatim), and - on a lone 50ml,\n       where the offer is about delivery - exactly what adding this does to the\n       free-delivery bar, including when the answer is \"not enough\".\n     ONE CONTROL, the two sizes joined into a single split button.\n\n   Nothing is stacked. The delivery fact appears in the note OR on the button\n   (pairAddButton's \"+ free shipping\"), never in both, because a fact printed\n   twice on one card reads as two facts.\n\n   THE 10ml LEADS (owner, 2026-08-19: \"Try to upsell 10ml with 50ml being second\n   option\"). It is not a smaller version of the offer, it is a different offer:\n   how a shopper says yes to a scent they have not smelled without spending R530\n   to do it. Both are real buttons with real prices - the choice is not hidden\n   behind a tap, only ordered. Only drawn when the product actually has a\n   sellable 10ml (index field mid).\n\n   `kind` is the ladder's verdict - 'combo' or 'lone-fifty' - and it decides one\n   thing only: whether the delivery consequence is spoken in the note. */\nfunction pairRow(item, kind) {\n  const p = item.p;\n  const row = document.createElement('div');\n  row.className = 'pair-row';\n  row.setAttribute('data-pair-handle', p.h);\n\n  const media = document.createElement('a');\n  media.className = 'pair-row__media';\n  media.href = p.u || '#';\n  media.tabIndex = -1;\n  media.setAttribute('aria-hidden', 'true');\n  /* THE TILE ALWAYS HAS SOMETHING IN IT (owner, 2026-08-19: \"also show the\n     product variant image here tho\"). It used to render an <img> only when the\n     index record carried one and leave an empty grey box when it did not -\n     which is the broken-looking tile in the owner's screenshot. Same answer as\n     the cart line beside it: the picture if there is one, and a placeholder\n     that says so if there is not. */\n  /* ---- THE 10ml's BOTTLE, BECAUSE THE 10ml IS THE OFFER ----\n     (owner, 2026-08-19: \"that product recommened please use the 10ml variant as\n     the primary image also zoomed in\".)\n\n     The row leads with the mini - that ruling is a few lines down and it is the\n     whole point of this block: an unsmelled scent is a cheap yes or it is no\n     sale. The picture was still the 50ml, so the tile showed one bottle while\n     the button under it sold a different one.\n\n     mimg is the 10ml variant's own photograph, emitted by search-row only when\n     that variant exists AND has been shot (see the note there). It falls back\n     to the resting packshot - a product whose mini has no photograph yet still\n     needs a tile, and the 50ml at least shows the right label. The tile is\n     zoomed harder than a cart line's in CSS (.pair-row__media img) because a\n     10ml stands about two thirds the height of a 50ml in the same frame. */\n  /* ---- AND WHEN IT IS NOT THE MINI, IT IS THE 50ml's OWN BOTTLE ----\n     (owner, 2026-09-01: \"it is using the wrong variant image here and it\n     doesn't look right\".)\n\n     The fallback was `img`, which is not a packshot at all: search-row's `_img`\n     is overwritten at the end by custom.main_product_image, the EDITORIAL\n     picture. Measured on coastal-daze - img is 320x203 landscape, pimg is\n     320x320 - so `object-fit: contain` put a wide scene inside a square 3.5rem\n     tile and left a small bottle adrift in it, which is exactly what the\n     screenshot shows.\n\n     `pimg` is the field built for this: search-row picks the 50ml variant's own\n     photograph BY NAME and its comment already says \"for pimg, cropped like a\n     cart line\". So the ladder is the offer's own picture, then the 50ml's, then\n     the editorial one as a last resort. 40 of the 42 products are affected and\n     every one of them carries a pimg, so nothing falls back to nothing.\n\n     This was always slightly wrong - any scent whose 10ml had not been shot hit\n     the same fallback - and the launch switches made it the normal case by\n     retiring the 10ml, which is how it surfaced. */\n  const tile = p.mimg || p.pimg || p.img;\n  if (tile) {\n    const img = document.createElement('img');\n    /* the index carries a 320px url for card use; a 56px tile does not need\n       to pull that down a phone connection */\n    img.src = String(tile).replace(/width=\\d+/, 'width=160');\n    img.width = 56; img.height = 56;\n    img.loading = 'lazy'; img.decoding = 'async'; img.alt = '';\n    if (p.mimg) media.classList.add('pair-row__media--mini');\n    media.appendChild(img);\n  } else {\n    const ph = document.createElement('span');\n    ph.className = 'pair-row__ph';\n    ph.textContent = 'Placeholder';\n    media.appendChild(ph);\n  }\n  row.appendChild(media);\n\n  const body = document.createElement('div');\n  body.className = 'pair-row__body';\n  const name = document.createElement('a');\n  name.className = 'pair-row__name';\n  name.href = p.u || '#';\n  name.textContent = p.t;\n  const why = document.createElement('span');\n  why.className = 'pair-row__why';\n  why.textContent = pairWhy(item);\n  why.title = why.textContent;\n  body.append(name, why);\n\n  /* ---- THE NOTE: HOW IT GOES ON, AND WHAT IT COSTS TO GET IT HERE ----\n     One line, assembled from at most two clauses, and both of them are facts\n     the reason above cannot carry. Rendered only when there is something in it;\n     an empty node under every suggestion is a row gap pretending to be\n     information. */\n  const said = [];\n  const first = pairFirstSay(item);\n  if (first) said.push(first);\n  if (kind === 'lone-fifty') {\n    /* The mini is the offer on this plan, so the consequence is quoted for the\n       mini's price - which is also the one the button under it leads with. */\n    const shipSay = bagShipConsequence(p.mpc);\n    if (shipSay) said.push(shipSay);\n  }\n  if (said.length) {\n    const note = document.createElement('span');\n    note.className = 'pair-row__note';\n    note.textContent = said.join(' ');\n    body.appendChild(note);\n  }\n  row.appendChild(body);\n\n  /* Not this one. The only control in the sheet that changes what the model is\n     allowed to say next. */\n  const snub = document.createElement('button');\n  snub.type = 'button';\n  snub.className = 'pair-row__x';\n  snub.setAttribute('data-pair-dismiss', p.h);\n  snub.setAttribute('aria-label', 'Not interested in ' + p.t);\n  snub.appendChild(pairGlyph('close'));\n  row.appendChild(snub);\n\n  /* ---- THE 10ml LEADS (owner, 2026-08-19: \"Try to upsell 10ml with 50ml\n     being second option\") ----\n\n     The row used to open with the 50ml at R530 and offer the 10ml after it. As\n     a suggestion at the bottom of a cart that is the wrong way round: this is\n     an unplanned scent the shopper has not smelled, and the cheap yes is the\n     one they might actually say. Leading with the 10ml also makes the 50ml\n     read as the upgrade rather than as the ask.\n\n     Both are still real buttons with real prices - the choice is not hidden\n     behind a tap, only re-ordered. */\n  const sizes = document.createElement('div');\n  sizes.className = 'pair-row__sizes';\n  const hasMini = p.mid && p.mpc > 0;\n  /* SAID ONCE. On the lone-50ml plan the note above has already stated what\n     this does to free delivery, in a full sentence and in both directions, so\n     the button must not repeat the half of it that is good news. */\n  const mayFlag = kind !== 'lone-fifty';\n  if (hasMini) sizes.appendChild(pairAddButton(p, '10ml', p.mpc, p.mid, '', mayFlag));\n  sizes.appendChild(pairAddButton(p, '50ml', p.pc, p.vid, p.sid, mayFlag && !hasMini));\n  row.appendChild(sizes);\n  return row;\n}\n/* The button carries WHAT TO POST, stamped on it at the moment it is built\n   from the record that already had it. Reaching back into the shared prepared\n   index at click time looks equivalent and is not: it silently falls through\n   to a product round trip whenever that reference is not what it was, and the\n   fallback resolves against a variant list that has no 10ml in it. */\n/* ---- \"+ FREE SHIPPING\" WHEN THIS SIZE IS THE ONE THAT CROSSES ----\n   (owner, 2026-08-19: \"if under shipping discount ad a thingy that says + free\n   shipping for 10ml should they pass it\".)\n\n   Both numbers are already on the page and neither is recomputed here: the\n   threshold is on the delivery bar the sheet renders at its head, and the cart\n   total is on the drawer's own inner wrapper (both server-rendered every time\n   the cart re-renders, so they cannot drift from the totals the shopper is\n   reading). If the bag is short of free delivery and this size's price closes\n   the gap, the button says so.\n\n   Only ever on ONE button - the cheapest size that gets there. \"+ free\n   shipping\" on both rungs would be true and useless: it is a reason to pick\n   this one, and a reason that applies to everything is not a reason. */\nfunction pairShipsFree(cents) {\n  const inner = bagScope();\n  const bar = bagPick('[data-ship-bar]');\n  if (!inner || !bar) return false;\n  if (bar.getAttribute('data-ship-unlocked') === '1') return false;   /* already there */\n  const threshold = parseInt(bar.getAttribute('data-ship-threshold'), 10);\n  const total = parseInt(inner.getAttribute('data-total-cents'), 10);\n  if (!threshold || !Number.isFinite(total)) return false;\n  return total + cents >= threshold;\n}\n\nfunction pairAddButton(p, size, cents, variantId, sampleId, mayFlagShipping) {\n  const btn = document.createElement('button');\n  btn.type = 'button';\n  btn.className = 'pair-add';\n  btn.setAttribute('data-pair-add', p.h);\n  btn.setAttribute('data-pair-size', size);\n  if (variantId) btn.setAttribute('data-variant-id', variantId);\n  if (sampleId) btn.setAttribute('data-sample-id', sampleId);\n  const label = document.createElement('span');\n  label.className = 'pair-add__size';\n  label.textContent = size;\n  const price = document.createElement('span');\n  price.className = 'pair-add__price';\n  price.textContent = srchMoney(cents);\n  const tick = pairGlyph('tick');\n  tick.classList.add('pair-add__tick');\n  btn.append(label, price, tick);\n  let name = 'Add the ' + size + ' of ' + p.t + ', ' + srchMoney(cents);\n  if (mayFlagShipping && pairShipsFree(cents)) {\n    const ship = document.createElement('span');\n    ship.className = 'pair-add__ship';\n    ship.textContent = '+ free shipping';\n    btn.appendChild(ship);\n    btn.classList.add('has-ship');\n    name += ', and unlocks free delivery';\n  }\n  btn.setAttribute('aria-label', name);\n  return btn;\n}\n\n/* The 50ml plus its free R0 tester, exactly the pairing main-product and\n   product-card compute in Liquid - a scent added from the drawer has to carry\n   the same promise as one added from its own page. Kept as the FALLBACK path:\n   an index cached before vid/sid/mid existed has no ids to add from, and a\n   suggestion that cannot be bought is worse than one round trip. */\nfunction bundleVariants(product, size) {\n  let full = null, mini = null, sample = null, firstPaid = null;\n  ((product && product.variants) || []).forEach((v) => {\n    const t = String(v.title || '').toLowerCase();\n    if (v.price === 0 || t.indexOf('sample') !== -1) {\n      if (v.available !== false && !sample) sample = v;\n      return;\n    }\n    if (v.available === false || t.indexOf('3ml') !== -1) return;\n    if (!firstPaid) firstPaid = v;\n    if (t.indexOf('50ml') !== -1 && !full) full = v;\n    if (t.indexOf('10ml') !== -1 && !mini) mini = v;\n  });\n  if (size === '10ml') return { variant: mini, sample: null };\n  const variant = full || firstPaid;\n  return { variant: variant, sample: variant && variant === full ? sample : null };\n}\n/* What to post for this button. Straight off the catalogue index when it\n   carries the ids (no round trip at all, which is most of why the add feels\n   immediate), and off the product JSON when it does not. */\nfunction pairAddItems(btn) {\n  const handle = btn.getAttribute('data-pair-add');\n  const size = btn.getAttribute('data-pair-size') || '50ml';\n  /* ---- A BOTTLE ADDED INTO A SET HAS TO CARRY THE SET (2026-08-21) ----\n     swapLineSize does this at the size switch (:1274) and undoPayloadFor does\n     it at the undo, because a bottle that loses `_layer_set` silently drops\n     out of its own block and the duet quietly stops being a duet. This path\n     attached no properties at all, so the first caller to offer an add inside\n     a set context would have lost it with nothing failing and nothing warning.\n     Opt-in via data-layer-set on the button, and never on the R0 tester: a\n     tester belongs to its bottle and tagSet (:39) is explicit about that. */\n  /* data-both-off = the pair offer is stood down because the bag already holds\n     the anchor (layerSolo), so this post is one bottle joining a bag rather\n     than a set being bought - and a one-bottle block titled \"Layer set\" is not\n     a set. Read here rather than at the flip so the attributes can stay put. */\n  const paired = layerPaired(btn);\n  const layer = paired ? btn.getAttribute('data-layer-set') : '';\n  const tag = (items) => (layer ? tagSet(items, layer) : items);\n\n  /* ---- THE OTHER HALF OF THE PAIR (2026-08-22) ----\n     data-both-id is the ANCHOR's variant - the scent the page is about - and\n     the PDP band stamps it only while that scent is missing from the bag\n     (owner: \"if you do not have the main product yet in cart, that there is a\n     button to add both 50ml and 10ml\"). One post, two bottles, anchor first so\n     the drawer's set block reads in the same order the plate does, and its own\n     free tester rides along on the 50ml exactly as the partner's does.\n     Still no second add path: this is the same button and the same delegate. */\n  const bothId = paired ? btn.getAttribute('data-both-id') : '';\n  const bothItems = () => {\n    if (!bothId) return [];\n    const items = [{ id: bothId, quantity: 1 }];\n    const bothSample = btn.getAttribute('data-both-sample-id');\n    if (bothSample) items.push({ id: bothSample, quantity: 1, properties: SAMPLE_PROPS });\n    return items;\n  };\n\n  const variantId = btn.getAttribute('data-variant-id');\n  if (variantId) {\n    const items = bothItems();\n    items.push({ id: variantId, quantity: 1 });\n    const sampleId = btn.getAttribute('data-sample-id');\n    if (sampleId) items.push({ id: sampleId, quantity: 1, properties: SAMPLE_PROPS });\n    return Promise.resolve(tag(items));\n  }\n  const root = (window.Shopify && Shopify.routes && Shopify.routes.root) || '/';\n  return fetch(root + 'products/' + encodeURIComponent(handle) + '.js', { headers: { Accept: 'application/json' } })\n    .then((r) => (r.ok ? r.json() : Promise.reject(new Error('product ' + r.status))))\n    .then((product) => {\n      const pick = bundleVariants(product, size);\n      if (!pick.variant) return Promise.reject(new Error('no sellable ' + size));\n      const items = bothItems();\n      items.push({ id: pick.variant.id, quantity: 1 });\n      if (pick.sample) items.push({ id: pick.sample.id, quantity: 1, properties: SAMPLE_PROPS });\n      return tag(items);\n    });\n}\n\n/* ---- THE HAND-OFF ----\n   The suggestion gives its bottle to the list. It is the one moment in the bag\n   where something genuinely moves from one place to another, so it is the one\n   place worth animating properly: the packshot leaves the suggestion's tile,\n   arcs, and lands in the new row's tile at exactly its size.\n\n   The parcel MUST be a child of the <dialog>. The sheet is in the top layer\n   and the site's ordinary .fly-parcel is merely z-indexed, so it would be\n   painted behind the very sheet it is supposed to be crossing. */\nfunction flyInsideBag(from, handle, src) {\n  if (reduceMotion.matches || !from || !src) { spotlightLine(handle); return; }\n  const drawer = bagSurface();\n  const line = bagPick('.drawer__lines [data-handle=\"' + cssAttr(handle) + '\"]');\n  const tile = line && line.querySelector('.line-item__media');\n  if (!drawer || !tile || typeof drawer.animate !== 'function') { spotlightLine(handle); return; }\n  const to = tile.getBoundingClientRect();\n  if (!to.width || !from.width) { spotlightLine(handle); return; }\n\n  const fly = document.createElement('span');\n  fly.className = 'bag-fly';\n  fly.style.left = from.left + 'px';\n  fly.style.top = from.top + 'px';\n  fly.style.width = from.width + 'px';\n  fly.style.height = from.height + 'px';\n  const img = document.createElement('img');\n  img.src = src; img.alt = '';\n  fly.appendChild(img);\n  drawer.appendChild(fly);\n\n  const dx = (to.left + to.width / 2) - (from.left + from.width / 2);\n  const dy = (to.top + to.height / 2) - (from.top + from.height / 2);\n  const scale = to.width / from.width;\n  /* the lift at the halfway point is what makes it read as handed over rather\n     than dragged; transform and opacity only, so it never leaves the compositor */\n  const anim = fly.animate([\n    { transform: 'translate(0px, 0px) scale(1)', opacity: 1 },\n    { transform: 'translate(' + (dx * 0.5) + 'px, ' + (dy * 0.5 - 22) + 'px) scale(' + ((1 + scale) / 2).toFixed(3) + ')', opacity: 1, offset: 0.55 },\n    { transform: 'translate(' + dx + 'px, ' + dy + 'px) scale(' + scale.toFixed(3) + ')', opacity: 0 }\n  ], { duration: 540, easing: 'cubic-bezier(.22, 1, .36, 1)', fill: 'forwards' });\n  /* Belt and braces: a parcel whose animation never reports finishing (a\n     backgrounded tab, a frozen frame clock) would sit over the sheet forever.\n     It is inert either way, but a bottle stuck mid-air reads as a fault. */\n  let landed = false;\n  const land = () => {\n    if (landed) return;\n    landed = true;\n    fly.remove();\n    spotlightLine(handle);\n  };\n  anim.onfinish = land;\n  anim.oncancel = land;\n  setTimeout(land, 900);\n}\n\nfunction addPairToCart(btn) {\n  const row = btn.closest('.pair-row');\n  const handle = btn.getAttribute('data-pair-add');\n  if (!handle || btn.disabled) return;\n  if (row && row.classList.contains('is-working')) return;   /* one at a time */\n  const size = btn.getAttribute('data-pair-size') || '50ml';\n  const art = row && row.querySelector('.pair-row__media img');\n  const src = art ? art.src : '';\n  /* The finder's answer and the layering finale fire this handler too, from\n     ordinary .btn pills that have no size/price spans to hide and no check to\n     draw. They keep the label swap they always had; only the bag's own split\n     control gets the fill-and-tick treatment. */\n  const split = btn.classList.contains('pair-add');\n  const label = split ? '' : btn.textContent;\n  if (!split) {\n    /* lock the width first: Adding / Added / Add R 690 are three different\n       lengths and the row must not jump three times */\n    btn.style.minWidth = btn.offsetWidth + 'px';\n    btn.disabled = true;\n    btn.textContent = 'Adding';\n  }\n\n  /* a hold, never a spinner (04 \u00A73) - and the other half dims so the row reads\n     as busy rather than as half-pressed */\n  if (row) row.classList.add('is-working');\n  btn.classList.add('is-working');\n\n  pairAddItems(btn)\n    .then((items) => {\n      const ticket = ++cartTicket;\n      return cartAdd(items)\n        .then((res) => {\n          /* if only the freebie failed, the bottle still stands */\n          if (res && res.status && res.status !== 200 && items.length > 1) return cartAdd([items[0]]);\n          return res;\n        })\n        .then((res) => {\n          if (!res || (res.status && res.status !== 200)) {\n            return Promise.reject(new Error((res && res.description) || 'add failed'));\n          }\n          btn.classList.remove('is-working');\n          btn.classList.add('is-added');           /* fills green, the check draws itself */\n          btn.classList.add('is-committed');       /* one short press-in under the yes */\n          if (!split) btn.textContent = 'Added';\n          layerBothSpent(btn);                     /* the anchor is in the bag now */\n\n          /* Added from a PAGE rather than from inside the open drawer (the\n             finder's answer, for one): the drawer IS the confirmation, so it\n             has to be opened rather than quietly re-rendered behind the page\n             the shopper is still looking at. */\n          if (btn.hasAttribute('data-pair-open')) {\n            swapDrawer(res.sections, ticket);\n            openDrawerConfirm();\n            spotlightLine(handle);\n            return;\n          }\n          /* THE BEAT. The swap replaces the whole inner subtree, this row\n             included, and a check destroyed in the tick it was drawn is not a\n             confirmation. Then the suggestion stands down and hands its bottle\n             over: rect measured BEFORE the fade, because a scaling row reports\n             a box it is no longer in. */\n          setTimeout(() => {\n            const media = row && row.querySelector('.pair-row__media');\n            const from = media ? media.getBoundingClientRect() : null;\n            if (row) row.classList.add('is-taken');\n            setTimeout(() => {\n              swapDrawer(res.sections, ticket);\n              if (typeof res.item_count === 'number') setCartCount(cartBottles(res));\n              flyInsideBag(from, handle, src);\n            }, reduceMotion.matches ? 0 : 150);\n          }, reduceMotion.matches ? 0 : 480);\n        });\n    })\n    .catch(() => {\n      if (row) row.classList.remove('is-working');\n      btn.classList.remove('is-working');\n      btn.classList.remove('is-added');\n      btn.classList.remove('is-committed');\n      if (!split) {\n        btn.disabled = false;\n        btn.textContent = label;\n        btn.style.minWidth = '';\n      }\n      cartFail('We could not add the ' + size + ' just now. Please try again.');\n    });\n}\n\n/* Not this one. The row leaves on the tap, the handle is remembered for the\n   session, and the model is asked again immediately - so a dismissal either\n   produces the next perfect match or, when there is not one, nothing at all.\n   Two taps and the suggestions are gone; that is the intent. */\nfunction dismissPair(btn) {\n  const handle = btn.getAttribute('data-pair-dismiss');\n  const row = btn.closest('.pair-row');\n  const host = bagPick('[data-drawer-pairs]');\n  snubPair(handle);\n  announceDrawer('Suggestion dismissed');\n  const next = () => {\n    /* hide first: refreshDrawerPairs unhides it again only if it found one,\n       so the block can never flash empty between the two */\n    showBagPairs(host, false);\n    refreshDrawerPairs();\n  };\n  if (!row || reduceMotion.matches) { next(); return; }\n  row.classList.add('is-dismissing');\n  setTimeout(next, 180);\n}\n\n/* ============================================================================\n   THE BROKEN DUET - the same slot, saying the other thing\n   (owner, 2026-08-21: \"if you remove the partner we want to make a Jennie API\n   message perhaps to explain abit more why they are good together and a re-add\n   option this will replace the layers with your bag section, like the card\n   needs to be quite re-active\".)\n\n   Two bottles that arrived in the bag TOGETHER carry `_layer_set`, and that\n   property is the only place their pairing is written down. Remove one and the\n   fact evaporates: the survivor is just a bottle, the block that titled them\n   collapses (cart-lines counts its members now), and nothing in the sheet ever\n   says what was undone. This is the thing that says it, in the one position\n   the sheet has for an offer.\n\n   WHAT MAKES IT REACTIVE. It mounts on the TAP, inside the 240ms the row takes\n   to close, not on the answer: the reason comes from JE.layerScore against the\n   catalogue index already cached for the session, so there is nothing to wait\n   for and nothing to spin. Jennie's sentence is an UPGRADE that fades in over\n   the local one when her server answers - the same shape the layering page\n   uses (jennieNote), so there is one pattern on the site and not two. With her\n   off, slow or unreachable the card is unchanged and nobody notices.\n\n   ONE OFFER, NOT TWO. The undo snackbar stands down for this removal (setLine).\n   Two \"put it back\" offers stacked over the foot is worse than one, and this\n   one carries a reason and does not expire after seven seconds.\n\n   IT IS ONLY UP WHILE IT IS TRUE. The pair stops being news when the bottle\n   comes back, when the shopper dismisses it, when the other half leaves too,\n   when the bag empties, when the sheet closes - and when the survivor stops\n   being alone, because a set that still holds a pair without the removed\n   bottle, or a survivor the list draws into a new block, is not a broken\n   duet (owner, 2026-08-27). takeUnpair and unpairKept each hold half of that\n   last rule.\n   ========================================================================= */\nlet unpairSeq = 0;\nlet unpairAbort = null;\n\n/* Everything the card needs, read off the row BEFORE it goes. Returns false\n   when this removal did not break anything - a lone bottle, the last member\n   of a set, or a set that still holds a pair without this bottle - and the\n   caller then offers the ordinary undo instead. */\nfunction takeUnpair(line, payload) {\n  const layer = line.getAttribute('data-layer-set');\n  if (!layer || !payload || !payload.handle) return false;\n  const mates = bagPickAll('.drawer__lines [data-layer-set=\"' + cssAttr(layer) + '\"]')\n    .filter((row) => row !== line && !row.classList.contains('is-leaving'))\n    .map((row) => ({ h: row.getAttribute('data-handle'), t: lineName(row) }))\n    .filter((m) => m.h);\n  if (!mates.length) return false;\n  /* AND NOT WHEN THE SET KEEPS A PAIR WITHOUT THIS BOTTLE (owner, 2026-08-27:\n     a Chosen-together block of Nightcap + The Tropics stood whole in the bag,\n     and this card argued Coastal Daze back for Nightcap underneath it). Sets\n     merge by label, so a bottle can leave a set of three - and the two that\n     remain are still exactly the combo the block above says they are. Nothing\n     broke; there is no lonely survivor to argue for; a shopper who kept a\n     pair is not owed an advert for the bottle they let go. The mis-tap case\n     is covered by the ordinary undo this false falls back to. */\n  if (mates.length > 1) return false;\n  const art = line.querySelector('.line-item__media img');\n  const size = line.querySelector('.line-item__size');\n  bagUnpair = {\n    seq: ++unpairSeq,\n    label: layer,\n    mates: mates,\n    gone: {\n      h: payload.handle,\n      t: payload.name,\n      /* the line total, not the unit - a shopper putting back two bottles is\n         being quoted for two */\n      cents: (parseInt(line.getAttribute('data-unit-cents'), 10) || 0) * (payload.quantity || 1),\n      size: size ? (size.textContent || '').trim() : '',\n      img: art ? (art.currentSrc || art.src || '') : ''\n    },\n    payload: payload,\n    mini: miniWayBack(line, payload),\n    note: '',\n    asked: false\n  };\n  return true;\n}\n\n/* ---- THE CHEAPER WAY TO KEEP THE PAIR ----\n   (owner, 2026-08-22: \"when they remove a 50ml like that on a combo we need to\n   give them the option to opt for the 10ml as another option\".)\n\n   A shopper who has just taken a R875 bottle out of a duet has usually said\n   something about the price, not about the scent. Offering only that same\n   bottle back at that same money answers a question they did not ask. The 10ml\n   keeps the pair whole for a third of it, and it is the one alternative this\n   card can carry without turning a way back into a shop.\n\n   READ OFF THE ROW, BEFORE IT GOES, like everything else on this card. The size\n   rungs are server truth - real variant, real price, and availability already\n   decided, since a sold-out rung ships disabled. The catalogue index's `mid` is\n   not gated that way, so an offer built from it can be for a bottle the\n   warehouse cannot pick. Nothing here is re-derived at click time.\n\n   THE SET RIDES ALONG. properties carries `_layer_set` off the undo payload,\n   which is the only thing that puts the bottle back INTO its block rather than\n   beside it - and putting the pair back together is the entire point.\n\n   Never offered when the bottle that left was ALREADY the 10ml: there is no\n   smaller way back, and \"or the 10ml\" under a 10ml is nonsense. */\nfunction miniWayBack(line, payload) {\n  const rung = Array.from(line.querySelectorAll('[data-line-size][data-size*=\"10ml\"]'))\n    .filter((b) => !b.disabled)[0];\n  if (!rung) return null;\n  const id = rung.getAttribute('data-variant-id');\n  const unit = parseInt(rung.getAttribute('data-cents'), 10);\n  if (!id || !unit || unit <= 0) return null;\n  if (String(id) === String(payload.variantId)) return null;\n  const quantity = payload.quantity || 1;\n  return {\n    variantId: id,\n    /* a 10ml earns no free 3ml - the promise is attached to the 50ml, and\n       cart-lines folds a tester onto a 50ml or a Bundle and nothing else */\n    testerId: '',\n    quantity: quantity,\n    handle: payload.handle,\n    name: payload.name,\n    properties: payload.properties,\n    /* the LINE total, the same arithmetic the 50ml half of the control is\n       quoted with, so the two prices are comparable */\n    cents: unit * quantity\n  };\n}\n\n/* The pair, retired. `redraw` asks for the slot to go straight back to being a\n   suggestion; the removal path passes false because the render it has already\n   set in motion will do that on its own a beat later. */\nfunction clearBagUnpair(redraw) {\n  if (!bagUnpair) return;\n  bagUnpair = null;\n  if (unpairAbort) { unpairAbort.abort(); unpairAbort = null; }\n  const host = bagPick('[data-drawer-pairs]');\n  /* only ever hide OUR card - a suggestion sitting there is not ours to close */\n  if (host && host.classList.contains('is-unpair')) showBagPairs(host, false);\n  if (redraw) refreshDrawerPairs();\n}\n\n/* WHICH BOTTLE IS THE CARD ABOUT? takeUnpair only captures a removal that left\n   exactly one set-mate behind, so the survivor is known - but it is re-checked\n   against the bag on every draw, because both of the card's premises can stop\n   being true while it is on screen: the survivor can leave too, and the\n   survivor can stop being ALONE. The scoring loop below still picks the best\n   of whatever remains, so an older multi-mate state degrades rather than\n   breaks. */\nfunction unpairKept(prep) {\n  const st = bagUnpair;\n  if (!st) return null;\n  const live = new Set(bagPickAll('.drawer__lines [data-handle]:not(.is-leaving)')\n    .map((n) => n.getAttribute('data-handle')));\n  let alive = st.mates.filter((m) => live.has(m.h));\n  /* AND THE SURVIVOR MUST STILL BE UNSPOKEN FOR. A mate the list has since\n     drawn into a block with someone else has a partner again - the sheet does\n     not shop for a bottle in that position (bagPairedHandles, owner\n     2026-08-26), and an offer to restore its old pair directly under a block\n     naming its new one is the same advert the grouping work exists to end.\n     Read off the drawn blocks on every draw, not off the capture, because\n     this is the premise that changes mid-card: a suggestion taken while the\n     offer is up can hand the survivor a new partner (2026-08-27). */\n  const list = bagPick('.drawer__lines');\n  if (list) {\n    const paired = bagPairedHandles(list);\n    alive = alive.filter((m) => !paired.has(m.h));\n  }\n  if (!alive.length) return null;\n  const gone = prep && prep.byHandle ? prep.byHandle[st.gone.h] : null;\n  if (!JE || !gone) return { mate: alive[0], report: null };\n  let best = null;\n  alive.forEach((m) => {\n    const other = prep.byHandle[m.h];\n    if (!other) return;\n    const rep = JE.layerScore(prep, other, gone);\n    if (rep && (!best || rep.score > best.report.score)) best = { mate: m, report: rep };\n  });\n  return best || { mate: alive[0], report: null };\n}\n\nfunction unpairCap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; }\n\n/* THE REASON, LOCALLY AND IMMEDIATELY. Same report the layering page draws its\n   verdict from, said in the past tense because the pair is now history. Two\n   clauses at most: what held them, and which way round they went on. If the\n   index is not in hand the card still says the one thing that is true without\n   it - they came into the bag together. */\nfunction unpairWhy(kept, st) {\n  const rep = kept && kept.report;\n  const name = kept && kept.mate ? kept.mate.t : '';\n  const together = name\n    ? st.gone.t + ' and ' + name + ' came into your cart as a pair.'\n    : st.gone.t + ' came into your cart as half of a pair.';\n  if (!rep) return together;\n  const shared = (rep.shared || []).slice(0, 2)\n    .map((n) => String(n).toLowerCase()).filter(Boolean);\n  const notes = shared.length > 1 ? shared[0] + ' and ' + shared[1] : shared[0];\n  const why = rep.why || {};\n  const dir = rep.directions || {};\n  const bits = [];\n  if (notes) {\n    bits.push(unpairCap(notes) + (shared.length > 1 ? ' sit' : ' sits')\n      + ' in both, which is what held them together.');\n  } else if (why.long) bits.push(unpairCap(String(why.long).replace(/\\.?$/, '.')));\n  else if (why.short) bits.push(unpairCap(String(why.short).replace(/\\.?$/, '.')));\n  if (dir.firstTitle && dir.secondTitle) {\n    bits.push(dir.flexible\n      ? 'They carry the same weight, so either one goes on first.'\n      : dir.firstTitle + ' went on first, ' + dir.secondTitle + ' over it.');\n  }\n  return bits.length ? bits.join(' ') : together;\n}\n\n/* Jennie's own sentence, over the top of the local one. Aborted after seven\n   seconds and guarded by the sequence, so an answer about a pair the shopper\n   has already put back can never land. Asked ONCE per broken pair: the card is\n   redrawn by every cart render, and a fetch per render is a fetch per tap. */\nfunction askJennieUnpair(st, kept) {\n  if (st.asked || !kept || !kept.mate) return;\n  st.asked = true;\n  const ep = ((window.theme && window.theme.jennie && window.theme.jennie.endpoint) || '')\n    .trim().replace(/\\/+$/, '');\n  if (!ep) return;\n  const seq = st.seq;\n  if (unpairAbort) unpairAbort.abort();\n  unpairAbort = new AbortController();\n  const ctl = unpairAbort;\n  const timer = window.setTimeout(() => ctl.abort(), 7000);\n  fetch(ep + '/api/client/unpair-note?kept=' + encodeURIComponent(kept.mate.h)\n      + '&removed=' + encodeURIComponent(st.gone.h),\n    { headers: { Accept: 'application/json' }, signal: ctl.signal })\n    .then((r) => (r.ok ? r.json() : null))\n    .then((d) => {\n      window.clearTimeout(timer);\n      if (!bagUnpair || bagUnpair.seq !== seq || !d || !d.note) return;\n      st.note = String(d.note);\n      paintUnpairWhy(st);\n    })\n    .catch(() => { window.clearTimeout(timer); });\n}\n/* The sentence swapped in place rather than the card rebuilt: the shopper may\n   already be reading it, and re-rendering the whole block under their thumb\n   would move the button they were reaching for. */\nfunction paintUnpairWhy(st) {\n  const el = document.querySelector('[data-drawer-pairs] [data-unpair-why]');\n  if (!el) return;\n  el.textContent = st.note;\n  el.classList.remove('is-in');\n  void el.offsetWidth;                         /* re-trigger the keyframe */\n  el.classList.add('is-in');\n}\n\n/* A tile, or an honest placeholder. Same rule as the cart line beside it and\n   the suggestion under it: never an empty grey box. */\nfunction unpairTile(src, cls) {\n  const box = document.createElement('span');\n  box.className = 'bag-unpair__tile' + (cls ? ' ' + cls : '');\n  if (src) {\n    const img = document.createElement('img');\n    img.src = String(src).replace(/width=\\d+/, 'width=160');\n    img.alt = ''; img.loading = 'lazy'; img.decoding = 'async';\n    box.appendChild(img);\n  } else {\n    const ph = document.createElement('span');\n    ph.className = 'pair-row__ph';\n    ph.textContent = 'Placeholder';\n    box.appendChild(ph);\n  }\n  return box;\n}\n\n/* ---- THE CARD ----\n     [ kept ][ gone ]                                            x\n     DRIFTWOOD + DARK REBELLION\n     Amber and vetiver sit in both, which is what held them\n     together. Dark Rebellion went on first, Driftwood over it.\n     [           Put it back              R 530           ]\n\n   The bottle that left is drawn faded and outlined rather than removed from\n   the picture: the point of the card is the pair, and a pair with one half\n   missing is the fact being reported. */\n/* Half of the way-back control. Wears .pair-add so it inherits the\n   fill-and-tick the suggestion's controls already have, and carries WHICH way\n   back it is - stamped at build time, never worked out again at click time. */\nfunction unpairWay(handle, which, text, cents, label) {\n  const btn = document.createElement('button');\n  btn.type = 'button';\n  btn.className = 'pair-add bag-unpair__back';\n  btn.setAttribute('data-unpair-readd', handle);\n  btn.setAttribute('data-unpair-size', which);\n  const size = document.createElement('span');\n  size.className = 'pair-add__size';\n  size.textContent = text;\n  btn.appendChild(size);\n  if (cents > 0) {\n    const price = document.createElement('span');\n    price.className = 'pair-add__price';\n    price.textContent = srchMoney(cents);\n    btn.appendChild(price);\n  }\n  const tick = pairGlyph('tick');\n  tick.classList.add('pair-add__tick');\n  btn.appendChild(tick);\n  btn.setAttribute('aria-label', label);\n  return btn;\n}\n\nfunction unpairCard(st, kept) {\n  const card = document.createElement('div');\n  card.className = 'bag-unpair';\n\n  const pics = document.createElement('div');\n  pics.className = 'bag-unpair__pics';\n  pics.setAttribute('aria-hidden', 'true');\n  pics.append(unpairTile(kept.keptImg, 'is-kept'), unpairTile(st.gone.img, 'is-gone'));\n  card.appendChild(pics);\n\n  const names = document.createElement('p');\n  names.className = 'bag-unpair__names';\n  const a = document.createElement('span');\n  a.className = 'bag-unpair__name';\n  a.textContent = kept.mate.t;\n  const plus = document.createElement('span');\n  plus.className = 'bag-unpair__plus';\n  plus.setAttribute('aria-hidden', 'true');\n  plus.textContent = '+';\n  const b = document.createElement('span');\n  b.className = 'bag-unpair__name is-gone';\n  b.textContent = st.gone.t;\n  names.append(a, plus, b);\n  card.appendChild(names);\n\n  const why = document.createElement('p');\n  why.className = 'bag-unpair__why is-in';\n  why.setAttribute('data-unpair-why', '');\n  why.textContent = st.note;\n  card.appendChild(why);\n\n  /* THE WAY(S) BACK. One control, two halves - the same grammar as the size\n     switch and the suggestion's two doors, so the sheet keeps one visual\n     language and the 10ml does not arrive as a second kind of button. With no\n     mini to offer the control is a single full-width way back, exactly as it\n     has always been: `grid-auto-flow: column` gives one column for one child\n     and two for two, so nothing has to be told how many there are.\n\n     THE UNDO LEADS. This card is an offer to reverse a tap, and the bottle that\n     left is the true answer to it; the 10ml is the alternative, and \"Or the\n     10ml\" needs something in front of it to be a sentence. That is the one\n     place this sheet does NOT put the mini first (the suggestion does, and for\n     the opposite reason - there, nothing has been chosen yet). */\n  const ways = document.createElement('div');\n  ways.className = 'bag-unpair__ways';\n  ways.appendChild(unpairWay(st.gone.h, 'full', 'Put it back', st.gone.cents,\n    'Put ' + st.gone.t + (st.gone.size ? ' ' + st.gone.size : '') + ' back in your cart'\n      + (st.gone.cents > 0 ? ', ' + srchMoney(st.gone.cents) : '')));\n  if (st.mini) {\n    ways.appendChild(unpairWay(st.gone.h, 'mini', 'Or the 10ml', st.mini.cents,\n      'Keep the pair with the 10ml of ' + st.gone.t + ' instead, '\n        + srchMoney(st.mini.cents)));\n  }\n  card.appendChild(ways);\n\n  /* Not interested. The pair was still worth saying once; saying it twice\n     would be an advert. */\n  const drop = document.createElement('button');\n  drop.type = 'button';\n  drop.className = 'pair-row__x bag-unpair__x';\n  drop.setAttribute('data-unpair-dismiss', '');\n  drop.setAttribute('aria-label', 'Dismiss');\n  drop.appendChild(pairGlyph('close'));\n  card.appendChild(drop);\n  return card;\n}\n\nfunction renderUnpairCard(host) {\n  const st = bagUnpair;\n  if (!host) return;\n  if (!st) { showBagPairs(host, false); return; }\n  const prep = juceIndexPrep;\n  const kept = unpairKept(prep);\n  /* the half the card was about has left too, the bag is empty, or the\n     survivor is drawn into a block with a new partner - either way there is\n     no lonely half left to talk about, so the slot goes back to what it was */\n  if (!kept) { clearBagUnpair(true); return; }\n  const slot = host.querySelector('[data-pairs-slot]');\n  if (!slot) return;\n  /* the kept bottle's own picture, off the row that is still in the bag */\n  const row = bagPick('.drawer__lines [data-handle=\"' + cssAttr(kept.mate.h) + '\"]');\n  const art = row && row.querySelector('.line-item__media img');\n  kept.keptImg = art ? (art.currentSrc || art.src || '') : '';\n  const label = host.querySelector('[data-pairs-label]');\n  if (label) label.textContent = 'Why those two held';\n  host.classList.add('is-unpair');\n  /* the local line is written once and then owned by the state, so a redraw\n     never throws Jennie's sentence away and asks for it again */\n  if (!st.note) st.note = unpairWhy(kept, st);\n  slot.replaceChildren(unpairCard(st, kept));\n  showBagPairs(host, true);\n  askJennieUnpair(st, kept);\n  /* THE INDEX MAY NOT BE IN HAND YET - the drawer's first open, or a shopper\n     who has not touched search. The card is already up and already true; when\n     the catalogue lands, draw it once more so it gains its reason. */\n  if (!prep && JE) {\n    const seq = st.seq;\n    juceIndex().then((ready) => {\n      if (!ready || !bagUnpair || bagUnpair.seq !== seq || bagUnpair.note !== st.note) return;\n      /* nothing has overwritten the local line, so replace it with the better\n         one the index can now produce */\n      const better = unpairWhy(unpairKept(ready), st);\n      if (!better || better === st.note) return;\n      st.note = better;\n      paintUnpairWhy(st);\n    });\n  }\n}\n\n/* THE WAY BACK, TAKEN. Optimistic like every other answer in this sheet: the\n   pair is retired before the round trip, because readdLine performs the\n   Section Rendering swap itself and that swap ends in refreshDrawerPairs - a\n   card still holding the pair would redraw itself over the very bottle it had\n   just restored. Reversible, so a failure puts the card back rather than\n   leaving the shopper with neither the bottle nor the offer. */\nfunction readdUnpair(btn) {\n  const st = bagUnpair;\n  if (!st || btn.classList.contains('is-working')) return;\n  /* WHICH way back. The half the finger landed on says so itself, and the\n     payload behind it was built off the row's own rungs before the row went -\n     so a card redrawn by a later render can never post the 50ml's variant for\n     the 10ml's button, and a mini that has since stopped being offered falls\n     back to the bottle that actually left rather than to nothing. */\n  const payload = (btn.getAttribute('data-unpair-size') === 'mini' && st.mini)\n    ? st.mini : st.payload;\n  const card = btn.closest('.bag-unpair');\n  if (card) card.classList.add('is-working');\n  btn.classList.add('is-working');              /* a hold, never a spinner */\n  bagUnpair = null;\n  if (unpairAbort) { unpairAbort.abort(); unpairAbort = null; }\n  readdLine(payload)\n    .catch(() => {\n      st.asked = true;                          /* do not pay for her line twice */\n      bagUnpair = st;\n      cartFail('We could not put ' + st.gone.t + ' back. Please add it again.');\n      refreshDrawerPairs();\n    });\n}\n\n/* Not interested. The card leaves on the tap and the slot goes straight back\n   to offering a partner, rather than waiting for the next cart render. */\nfunction dismissUnpair() {\n  const card = document.querySelector('[data-drawer-pairs] .bag-unpair');\n  announceDrawer('Dismissed');\n  const done = () => clearBagUnpair(true);\n  if (!card || reduceMotion.matches) { done(); return; }\n  card.classList.add('is-dismissing');\n  setTimeout(done, 180);\n}\n\nfunction initDrawerPairs() {\n  document.addEventListener('click', (e) => {\n    const back = e.target.closest('[data-unpair-readd]');\n    if (back) { e.preventDefault(); readdUnpair(back); return; }\n    const drop = e.target.closest('[data-unpair-dismiss]');\n    if (drop) { e.preventDefault(); dismissUnpair(); return; }\n    const snub = e.target.closest('[data-pair-dismiss]');\n    if (snub) { e.preventDefault(); dismissPair(snub); return; }\n    const btn = e.target.closest('[data-pair-add]');\n    if (!btn) return;\n    e.preventDefault();\n    addPairToCart(btn);\n  });\n}\n\nfunction initSearch() {\n  const dialog = document.querySelector('[data-search]');\n  /* NOT `|| !JE`, and that clause is why the popup stopped opening at all.\n\n     It was written when juce-engine.js was a deferred script in the head,\n     purely so it landed on window.JuceEngine before theme.js ran - so at this\n     line JE was already there and the guard only ever meant \"this build has\n     no brain\". The performance pass made the engine demand-loaded\n     (loadEngine, and the note where the script tag used to be in\n     layout/theme.liquid), which means JE is null at boot BY DESIGN and this\n     guard started firing on every page load. Everything below it - including\n     the one line that matters, the click handler that preventDefaults\n     [data-search-open] - was never bound, so the trigger degraded to the\n     real /search link it is in the markup and the shopper got a page reload\n     where a panel should have opened.\n\n     Nothing below needs the engine at bind time. Every JE call site in here\n     sits behind loadIndex(), the open path renders `renderUnavailable()` when\n     the index resolves to null, and warm-on-intent pulls the brain when a\n     thumb comes near the trigger. A missing brain is already handled INSIDE;\n     it was never a reason not to bind. */\n  if (!dialog) return;\n  const input = dialog.querySelector('[data-search-input]');\n  const panel = dialog.querySelector('[data-search-panel]');\n  const live = dialog.querySelector('[data-search-live]');\n  if (!input || !panel) return;\n  const SEARCH_URL = (cfg.routes && cfg.routes.search) || '/search';\n  const REC_KEY = 'juce:recent:v1';\n  const QRY_KEY = 'juce:queries:v1';\n  let PREP = null, active = -1, lastRaw = '';\n\n  /* ---- history: what this shopper has looked at, and what they asked for ----\n     Both lists live in localStorage on this device only - nothing is sent\n     anywhere, and private mode simply means an empty history and the old\n     bestseller seed. Newest first, so position IS recency. */\n  function readList(key) {\n    try { const a = JSON.parse(localStorage.getItem(key) || '[]'); return Array.isArray(a) ? a : []; }\n    catch (e) { return []; }\n  }\n  function pushList(key, value, cap) {\n    try {\n      const arr = readList(key).filter((x) => x !== value);\n      arr.unshift(value);\n      localStorage.setItem(key, JSON.stringify(arr.slice(0, cap)));\n    } catch (e) { /* private mode - history just stays empty */ }\n  }\n  function recordView() {\n    const m = location.pathname.match(/\\/products\\/([^/?#]+)/);\n    if (m) pushList(REC_KEY, m[1], 12);\n  }\n  /* Only queries the shopper ACTED on - opened a result, or pushed through to\n     the full search page. Recording every debounced keystroke would fill the\n     history with the prefixes of one word (\"v\", \"ve\", \"vet\", \"vetiver\") and\n     let a typo weigh as much as an answer. */\n  function recordQuery(raw) {\n    const q = (raw || '').trim();\n    if (q.length >= 2) pushList(QRY_KEY, q.toLowerCase(), 8);\n  }\n  function recentProducts(n) {\n    if (!PREP) return [];\n    const curr = (location.pathname.match(/\\/products\\/([^/?#]+)/) || [])[1];\n    const out = [];\n    for (const h of readList(REC_KEY)) {\n      if (h === curr) continue;\n      const p = PREP.byHandle[h];\n      if (p) out.push(p);\n      if (out.length >= (n || 4)) break;\n    }\n    return out;\n  }\n\n  /* ---- \"Picked for you\" - the second half of the seed ----\n     Two signals, both already understood by the engine, so this adds no model\n     of its own:\n\n       \u00B7 the scents looked at   -> JE.kinship, the same family/note/body\n                                   similarity the finder uses for \"more like\n                                   this\". Weighted by recency.\n       \u00B7 the things searched for -> the query re-run through JE.search. What the\n                                   engine would have shown for \"warm vanilla\" is\n                                   the best possible reading of that phrase; we\n                                   just remember it was asked.\n\n     Everything already ON the recently-viewed shelf is excluded - showing a\n     shopper the four scents they just opened, twice, under two headings, is not\n     a recommendation. With no history at all this returns nothing and the seed\n     falls back to bestsellers. */\n  function recommend(n) {\n    if (!PREP || !PREP.list.length) return [];\n    const want = n || 4;\n    const viewed = readList(REC_KEY);\n    const queries = readList(QRY_KEY);\n    if (!viewed.length && !queries.length) return [];\n\n    const score = new Map();\n    const bump = (p, by) => { if (p && by > 0) score.set(p, (score.get(p) || 0) + by); };\n    const seen = new Set(viewed);\n    const curr = (location.pathname.match(/\\/products\\/([^/?#]+)/) || [])[1];\n    if (curr) seen.add(curr);\n\n    viewed.slice(0, 5).forEach((handle, i) => {\n      const anchor = PREP.byHandle[handle];\n      if (!anchor) return;\n      const decay = 1 / (1 + i);                 /* the last one looked at leads */\n      PREP.list.forEach((p) => {\n        if (p === anchor || seen.has(p.h)) return;\n        bump(p, JE.kinship(anchor, p) * decay);\n      });\n    });\n\n    queries.slice(0, 4).forEach((q, i) => {\n      let res = null;\n      try { res = JE.search(PREP, q, { limit: 5 }); } catch (e) { return; }\n      if (!res || !res.results.length) return;\n      const decay = 0.9 / (1 + i);\n      const top = res.results[0].score || 1;\n      res.results.forEach((hit, rank) => {\n        if (seen.has(hit.p.h)) return;\n        /* normalised against the query's own best hit, so a broad query and a\n           precise one contribute on the same scale */\n        bump(hit.p, (top ? hit.score / top : 1) * decay / (1 + rank * 0.3));\n      });\n    });\n\n    return [...score.entries()]\n      .filter(([p]) => p.a !== false)            /* never recommend a sold-out bottle */\n      .sort((a, b) => b[1] - a[1])\n      .slice(0, want)\n      .map(([p]) => p);\n  }\n\n  /* the index is shared with the ritual lane and the layering explorer, so\n     it is fetched by juceIndex() at module level - one request per session */\n  function loadIndex(force = false) { return juceIndex(force).then((p) => { PREP = p; return p; }); }\n\n  /* ---- tiny DOM builders (all text via textContent - never innerHTML) ---- */\n  function el(tag, cls, text) {\n    const n = document.createElement(tag);\n    if (cls) n.className = cls;\n    if (text != null) n.textContent = text;\n    return n;\n  }\n  /* THE 50ml PACKSHOT, NOT THE LIFESTYLE FILE (owner, 2026-08-22: \"our quick\n     search does not show the 50ml image that is also zoomed in like the cart\n     one\").\n\n     `img` on an index row is the merchant's custom.main_product_image\n     override, and on this catalogue that is a LANDSCAPE file on 40 of 42\n     products - measured off the live index, 320x203 against 320x320 for the\n     bottles - so a square 52px result tile and a 120px pairs tile were both\n     showing a centre slice of a wide scene. `pimg` is the sellable 50ml's own\n     square photograph on the F2F2F2 bed, which snippets/search-row.liquid\n     preserves precisely for this. Same reading as layerShotSrc, the account\n     scent trail and the scent finder, so the popup can no longer disagree with\n     the rest of the shop about which bottle a shopper is looking at.\n\n     BOTH the result row and the pairs rail come through here, and that is why\n     they change together: one product, one photograph, wherever the popup\n     draws it. `|| p.img` is the same honest fallback the other consumers keep -\n     all 42 rows carry a pimg today, and a product added tomorrow without a\n     variant photograph still shows something rather than an empty frame. */\n  function thumb(p, wrapCls, imgCls) {\n    const span = el('span', wrapCls);\n    const src = p.pimg || p.img;\n    if (src) {\n      const img = el('img', imgCls);\n      img.src = src; img.loading = 'lazy'; img.decoding = 'async'; img.alt = p.t;\n      img.width = 100; img.height = 100;\n      span.appendChild(img);\n    }\n    return span;\n  }\n  /* the sub-line quietly states WHY a product surfaced: the matched note\n     (highlighted), else its own top notes.\n\n     IT NEVER NAMES ANOTHER HOUSE (owner, 2026-08-28). The engine stopped\n     returning a 'brand' why then; this kept the branch that drew it and,\n     one line down, a `p.raw.br` fallback that printed the designer's name\n     on its own whenever a row had no notes and no category. Both are gone.\n     `br`/`dp` still SCORE, so typing \"creed\" still finds the bottle - it\n     just surfaces with what the row can say for itself, or with nothing. */\n  function subLine(p, why) {\n    const wrap = el('span', 'sr-row__notes');\n    if (why && why.type === 'note') {\n      const rest = p.tnames.filter((x) => srchNorm(x) !== srchNorm(why.text)).slice(0, 2);\n      wrap.appendChild(el('b', 'sr-hi', why.text));\n      if (rest.length) wrap.appendChild(document.createTextNode(' \u00B7 ' + rest.join(' \u00B7 ')));\n      return wrap;\n    }\n    const txt = p.tnames.length ? p.tnames.slice(0, 3).join(' \u00B7 ') : (p.raw.c || '');\n    if (!txt) return null;\n    wrap.textContent = txt;\n    return wrap;\n  }\n  /* Every option gets an id so the field can point at it with\n     aria-activedescendant (2026-09-10, audit T10) - the one way a screen reader\n     learns which row the arrow keys are on while focus stays in the field. A\n     counter rather than the handle: the same scent can be in two lists at once\n     (\"Recently viewed\" and \"Picked for you\"), and an id must be unique. */\n  let optSeq = 0;\n  function row(p, why) {\n    const a = el('a', 'sr-row');\n    a.href = p.u; a.setAttribute('role', 'option');\n    a.id = 'sr-opt-' + (++optSeq);\n    a.appendChild(thumb(p, 'sr-row__media', 'sr-row__img'));\n    const id = el('span', 'sr-row__id');\n    id.appendChild(el('span', 'sr-row__name', p.t));\n    const sub = subLine(p, why);\n    if (sub) id.appendChild(sub);\n    a.appendChild(id);\n    a.appendChild(el('span', 'sr-row__price price', srchMoney(p.pc)));\n    return a;\n  }\n  function tile(item) {\n    const p = item.p || item;\n    const a = el('a', 'sr-tile');\n    a.href = p.u;\n    a.appendChild(thumb(p, 'sr-tile__media', 'sr-tile__img'));\n    a.appendChild(el('span', 'sr-tile__name', p.t));\n    if (item.reason) a.appendChild(el('span', 'sr-tile__why', item.reason));\n    a.appendChild(el('span', 'sr-tile__price price', srchMoney(p.pc)));\n    return a;\n  }\n  /* note card: the matched note, described exactly as the PDP does */\n  function noteCard(note) {\n    const card = el('div', 'sr-note');\n    const icon = el('span', 'sr-note__icon');\n    if (note.img) { const im = el('img', 'sr-note__img'); im.src = note.img; im.loading = 'lazy'; im.decoding = 'async'; im.alt = ''; icon.appendChild(im); }\n    else icon.classList.add('sr-note__icon--blank');\n    card.appendChild(icon);\n    const body = el('div', 'sr-note__body');\n    const name = el('p', 'sr-note__name', note.name);\n    if (note.c) name.appendChild(el('span', 'sr-note__cat', note.c));\n    body.appendChild(name);\n    const desc = note.d || note.sl || note.v;\n    if (desc) body.appendChild(el('p', 'sr-note__desc', desc));\n    card.appendChild(body);\n    if (note.count) card.appendChild(el('span', 'sr-note__count', note.count + (note.count === 1 ? ' scent' : ' scents')));\n    return card;\n  }\n  function chip(label, query) {\n    const b = el('button', 'chip sr-chip', label);\n    b.type = 'button'; b.dataset.srchFill = query;\n    return b;\n  }\n  function group(title) {\n    const g = el('div', 'sr-group');\n    if (title) g.appendChild(el('p', 'eyebrow sr-group__title', title));\n    return g;\n  }\n  /* ---- ASK JENNIE, UNDER THE PANEL (owner, 2026-09-12: \"on the search need\n     something ask jennie about this or something, small nice and clean and\n     suttle\") ----\n\n     A search box matches words against a catalogue. It cannot answer \"like\n     Baccarat but lighter for a Joburg summer\", and the shopper who types\n     something close to that gets the no-match state and a row of favourites\n     - at the exact moment they have described what they want better than any\n     facet could. The one surface that CAN answer it had no door on this\n     screen: the popup covers the header seams and the dock, and the menu is a\n     separate sheet.\n\n     So one line, in every state of the panel, saying the same thing in the\n     shopper's own words: what they typed travels as the prompt, so she opens\n     already holding the question rather than asking them to type it twice.\n     Empty box, no query - the open invitation instead.\n\n     A REAL SEAM, not a second chat implementation: <a href> to the help\n     centre with data-chat-open, and initJennie's delegated handler does the\n     rest - warm the module, close whatever chrome is up, open the panel,\n     honour the href if the module never loads. Nothing here knows about\n     Jennie beyond the attribute.\n\n     THE POPUP HAS TO CLOSE FIRST, and that is not done here either: it is a\n     showModal() dialog exactly like the menu sheet, so a panel opened over it\n     lands UNDERNEATH and reads as nothing having happened. closeChrome now\n     shuts [data-search] for every seam at once - see its note. */\n  function askRow(raw) {\n    const q = (raw || '').trim();\n    const a = el('a', 'sr-ask');\n    a.href = '/pages/help';\n    a.setAttribute('data-chat-open', '');\n    a.setAttribute('data-chat-prompt', q\n      ? 'Hi Jennie, I searched for \"' + q + '\" - what would you suggest?'\n      : 'Hi Jennie, I could use a hand finding a scent.');\n    const dot = el('span', 'jennie-dot');\n    dot.setAttribute('aria-hidden', 'true');\n    a.appendChild(dot);\n    const t = el('span', 'sr-ask__text');\n    t.appendChild(el('span', 'sr-ask__lead', q ? 'Ask Jennie about \u201C' + q + '\u201D' : 'Ask Jennie'));\n    t.appendChild(el('span', 'sr-ask__note', q\n      ? 'She knows the notes, the wear and what layers with it'\n      : 'Describe it in your own words and she will find it'));\n    a.appendChild(t);\n    return a;\n  }\n  function bestsellers(n) {\n    if (!PREP) return [];\n    const tagged = PREP.list.filter((p) => p.tag && p.tag.indexOf('best') !== -1);\n    const pool = tagged.length ? tagged : PREP.list;\n    return pool.slice(0, n || 6);\n  }\n  function announce(msg) { if (live) { live.textContent = ''; requestAnimationFrame(() => { live.textContent = msg; }); } }\n\n  /* ---- render states ---- */\n  /* A repaint throws the old rows away, so the field must stop pointing at one\n     of them - an activedescendant naming a node that no longer exists reads as\n     nothing at all, or worse, as the last thing it named. */\n  function paint(node) { panel.replaceChildren(node); active = -1; input.removeAttribute('aria-activedescendant'); }\n\n  function tryChips() {\n    const wrap = el('div', 'sr-tries');\n    /* The house vocabulary is Feminine / Masculine / Gender Neutral, never\n       \"for him\" or \"for her\" (owner, 2026-08-25). Both halves matter: the\n       label is read on the chip and the fill is typed into the search box in\n       front of the shopper. Both tokens are already first-class facets. */\n    [['Fresh', 'fresh'], ['Woody', 'woody'], ['Sweet', 'sweet'], ['Summer', 'summer masculine'],\n     ['Date night', 'date night'], ['Feminine floral', 'floral feminine']].forEach((c) => wrap.appendChild(chip(c[0], c[1])));\n    return wrap;\n  }\n\n  /* The empty state, in the order a returning shopper actually thinks (owner,\n     2026-08-12): first the thing they were just looking at, then what that\n     history suggests, and only then the generic prompts.\n\n     It used to open on five bestsellers with \"Recently viewed\" underneath -\n     the shop's opinion above the shopper's own. On a returning visit the top\n     of the panel was the same five products every time, and the scent they had\n     opened ten minutes ago was below the fold of a 44px-row list. */\n  function seedList(title, items) {\n    const g = group(title);\n    const list = el('div', 'sr-list'); list.setAttribute('role', 'listbox');\n    list.setAttribute('aria-label', title);\n    items.forEach((p) => list.appendChild(row(p)));\n    g.appendChild(list);\n    return g;\n  }\n\n  function renderSeed() {\n    const frag = document.createDocumentFragment();\n    const recent = recentProducts(4);\n    if (recent.length) frag.appendChild(seedList('Recently viewed', recent));\n\n    const picked = recommend(4);\n    if (picked.length) frag.appendChild(seedList('Picked for you', picked));\n    else {\n      /* no history worth reading yet - the shop's own answer, as before */\n      const best = bestsellers(recent.length ? 4 : 5);\n      if (best.length) frag.appendChild(seedList(recent.length ? 'Popular right now' : 'Recommended', best));\n    }\n\n    const t = group('Try a mood');\n    t.appendChild(tryChips());\n    frag.appendChild(t);\n    frag.appendChild(askRow(''));\n    paint(frag);\n  }\n\n  function renderResults(raw, res) {\n    const results = res.results;\n    const frag = document.createDocumentFragment();\n    /* a matched note leads with its own card, described like the PDP */\n    if (res.focus && res.focus.note) frag.appendChild(noteCard(res.focus.note));\n    if (res.read.length && !(res.focus && res.focus.whole)) {\n      const r = el('p', 'sr-read');\n      r.appendChild(el('span', 'sr-read__k', 'Matching'));\n      r.appendChild(document.createTextNode(' ' + res.read.join(' \u00B7 ')));\n      frag.appendChild(r);\n    }\n    const list = el('div', 'sr-list'); list.setAttribute('role', 'listbox');\n    list.setAttribute('aria-label', 'Search results');\n    results.forEach((x) => list.appendChild(row(x.p, x.why)));\n    frag.appendChild(list);\n\n    /* Layering only earns its place when the shopper clearly has ONE scent\n       in mind. Typing a note (\"star\") or a mood (\"fresh\") is browsing, and a\n       pairing rail there is noise about a product they never chose. The\n       engine makes that call; we just honour it. */\n    if (JE.wantsPairs(res)) {\n      const anchor = res.intent.anchor || results[0].p;\n      /* no exclusions: what layers with this scent is the same answer here,\n         on the PDP and in the layering lab. See the note in the collection\n         filter's layer() for why filtering by what is on screen was wrong. */\n      const pairs = JE.layerPairs(PREP, anchor, { limit: 4, min: 'good', availableOnly: true });\n      if (pairs.length >= 2) {\n        const g = group('Layers well with ' + anchor.t);\n        g.classList.add('sr-group--pairs');\n        const rail = el('div', 'sr-rail');\n        pairs.forEach((pr) => rail.appendChild(tile(pr)));\n        g.appendChild(rail);\n        frag.appendChild(g);\n      }\n    }\n\n    const more = el('a', 'sr-all');\n    more.href = SEARCH_URL + '?q=' + encodeURIComponent(raw) + '&type=product';\n    more.textContent = 'See all results for \u201C' + raw.trim() + '\u201D';\n    frag.appendChild(more);\n    /* under the way out to the full results page, not above it: the list on\n       screen is still the answer to what was typed, and this is the door for\n       the shopper it did not answer. */\n    frag.appendChild(askRow(raw));\n    paint(frag);\n    announce(results.length + (results.length === 1 ? ' result' : ' results'));\n  }\n\n  function renderEmpty(raw) {\n    const frag = document.createDocumentFragment();\n    const miss = el('div', 'sr-miss');\n    miss.appendChild(el('p', 'sr-miss__lead', 'No match for \u201C' + raw.trim() + '\u201D'));\n    miss.appendChild(el('p', 'sr-miss__hint', 'Try a note (vetiver, fig), a mood (fresh, woody) or the name of a scent you love.'));\n    miss.appendChild(tryChips());\n    frag.appendChild(miss);\n    /* ABOVE the favourites here, unlike the results state: nothing matched,\n       so the shop's four bestsellers are the consolation and the person who\n       can actually read the sentence they typed is the answer. */\n    frag.appendChild(askRow(raw));\n    const g = group('Reach for a favourite');\n    const list = el('div', 'sr-list'); list.setAttribute('role', 'listbox');\n    bestsellers(4).forEach((p) => list.appendChild(row(p)));\n    g.appendChild(list);\n    frag.appendChild(g);\n    paint(frag);\n    announce('No results. Showing favourites.');\n  }\n\n  function loading() { paint(el('div', 'sr-loading', 'Searching\u2026')); }\n  function renderUnavailable() {\n    const miss = el('div', 'sr-miss');\n    miss.appendChild(el('p', 'sr-miss__lead', 'Search is taking a moment.'));\n    miss.appendChild(el('p', 'sr-miss__hint', 'Retry the catalogue connection or continue to the full search page.'));\n    const retry = el('button', 'chip sr-chip', 'Try again');\n    retry.type = 'button'; retry.dataset.srchRetry = '';\n    miss.appendChild(retry);\n    const full = el('a', 'sr-all', 'Open full search');\n    full.href = SEARCH_URL + (input.value.trim() ? '?q=' + encodeURIComponent(input.value.trim()) + '&type=product' : '');\n    const frag = document.createDocumentFragment();\n    frag.append(miss, full); paint(frag);\n    announce('Search catalogue unavailable. Retry or open full search.');\n  }\n\n  /* ---- run (debounced) ---- */\n  let t = 0;\n  function run() {\n    const raw = input.value;\n    lastRaw = raw;\n    input.setAttribute('aria-expanded', 'true');\n    if (!raw.trim()) {\n      loadIndex().then((p) => {\n        if (input.value.trim()) return;\n        p ? renderSeed() : renderUnavailable();\n      });\n      return;\n    }\n    loadIndex().then((p) => {\n      if (input.value !== raw) return;                 /* superseded by a newer keystroke */\n      if (!p) { renderUnavailable(); return; }\n      const res = JE.search(p, raw, { limit: 7 });\n      res.results.length ? renderResults(raw, res) : renderEmpty(raw);\n    });\n  }\n  input.addEventListener('input', () => { clearTimeout(t); t = setTimeout(run, 110); });\n\n  /* fill-from-chip and retry (event-delegated) */\n  panel.addEventListener('click', (e) => {\n    /* an opened result is the shopper confirming that this query meant what\n       the engine thought it meant - that is the version worth remembering */\n    if (e.target.closest('.sr-row, .sr-tile')) { recordQuery(lastRaw); return; }\n    const retry = e.target.closest('[data-srch-retry]');\n    if (retry) {\n      loading();\n      loadIndex(true).then((p) => { p ? run() : renderUnavailable(); });\n      return;\n    }\n    const c = e.target.closest('[data-srch-fill]');\n    if (!c) return;\n    input.value = c.dataset.srchFill;\n    input.focus();\n    clearTimeout(t); run();\n  });\n\n  /* ---- keyboard: roving highlight over result rows ---- */\n  function rows() { return Array.from(panel.querySelectorAll('.sr-row')); }\n  function setActive(next) {\n    const items = rows();\n    if (!items.length) { active = -1; return; }\n    active = (next + items.length) % items.length;\n    items.forEach((it, i) => it.classList.toggle('is-active', i === active));\n    const cur = items[active];\n    if (cur) {\n      cur.scrollIntoView({ block: 'nearest' }); cur.setAttribute('aria-selected', 'true');\n      if (cur.id) input.setAttribute('aria-activedescendant', cur.id);\n    }\n    items.forEach((it, i) => { if (i !== active) it.removeAttribute('aria-selected'); });\n  }\n  input.addEventListener('keydown', (e) => {\n    if (e.key === 'ArrowDown') { e.preventDefault(); setActive(active + 1); }\n    else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(active - 1); }\n    else if (e.key === 'Enter') {\n      const items = rows();\n      recordQuery(input.value);\n      if (active >= 0 && items[active]) { e.preventDefault(); window.location.href = items[active].href; }\n      else if (input.value.trim()) { window.location.href = SEARCH_URL + '?q=' + encodeURIComponent(input.value) + '&type=product'; e.preventDefault(); }\n    }\n  });\n\n  /* ---- open / close ---- */\n  function open() {\n    if (typeof dialog.showModal === 'function') { if (!dialog.open) dialog.showModal(); }\n    else dialog.setAttribute('open', '');\n    input.setAttribute('aria-expanded', 'true');\n    loadIndex().then((p) => {\n      if (!dialog.open && dialog.hasAttribute('open') === false) return;\n      if (input.value.trim()) run();\n      else if (p) renderSeed();\n      else renderUnavailable();\n    });\n    requestAnimationFrame(() => { input.focus(); input.select(); });\n  }\n  function close() { if (dialog.open) dialog.close(); else dialog.removeAttribute('open'); input.setAttribute('aria-expanded', 'false'); }\n  /* ESCAPE CLOSES THE DIALOG WITHOUT CALLING close() (2026-09-10, audit T10).\n     A modal <dialog> answers Escape natively and fires only its own `close`,\n     so the field went on saying aria-expanded=\"true\" - and pointing at a row -\n     over a list that had gone. The event is the one thing every way out of\n     the dialog shares: Escape, the X, the backdrop and a close() from code. */\n  dialog.addEventListener('close', () => {\n    input.setAttribute('aria-expanded', 'false');\n    input.removeAttribute('aria-activedescendant');\n    active = -1;\n  });\n\n  /* warm the index on intent (hover/focus the trigger) so first open is instant */\n  let warmed = false;\n  const warm = () => { if (!warmed) { warmed = true; loadIndex(); } };\n  document.addEventListener('pointerover', (e) => { if (e.target.closest('[data-search-open]')) warm(); }, { passive: true });\n  document.addEventListener('focusin', (e) => { if (e.target.closest('[data-search-open]')) warm(); });\n\n  document.addEventListener('click', (e) => {\n    if (e.target.closest('[data-search-open]')) { e.preventDefault(); open(); return; }\n    if (e.target.closest('[data-search-close]')) { e.preventDefault(); close(); return; }\n    if (e.target === dialog) close();                  /* backdrop */\n  });\n  /* the dialog's own submit (no-JS path stays intact) -> full search page */\n  const form = dialog.querySelector('form');\n  if (form) form.addEventListener('submit', (e) => {\n    if (!input.value.trim()) { e.preventDefault(); return; }\n    recordQuery(input.value);\n    /* let it navigate to /search?q= natively - that's the \"see all\" surface */\n  });\n  /* \"See all results for ...\" is the same commitment as pressing Enter */\n  panel.addEventListener('click', (e) => {\n    if (e.target.closest('.sr-all')) recordQuery(lastRaw || input.value);\n  });\n\n  /* remember this product for \"recently viewed\", and idle-prewarm the index\n     so the very first open never waits on the network.\n\n     HELD BEHIND whenActive (2026-08-30). initSearch is in the CRITICAL tier,\n     so this line ran on a page the browser merely PRERENDERED because a thumb\n     rested on a card - and loadIndex goes through juceIndex, which injects the\n     193KB engine and fetches the whole catalogue. That is the exact cost\n     bootEngineSurfaces is wrapped in whenActive to avoid (see its note at the\n     boot tier, and playbook 12 section 3.2), and it made the page the shopper\n     is actually looking at slower in order to prewarm one they may never open.\n     It is also why the speculationrules eagerness in theme.liquid was dropped\n     to `conservative`; this line was quietly paying the moderate price anyway.\n\n     The warm-on-intent handlers above are untouched: a real hover or focus on\n     the search trigger still loads the index ahead of the first open, which is\n     where the perceived speed actually comes from. */\n  /* ---- AND ONLY WHERE THE BRAIN IS ALREADY COMING (2026-09-10, audit T13) ----\n     Behind whenActive it still ran on EVERY page a shopper actually opened -\n     home, cart, an article, a policy page - and on each one it fetched the\n     whole catalogue, ran it through DOMParser, JSON.parse and a full\n     JE.prepare and wrote ~90KB back into sessionStorage, for a popup most\n     visits never open. Now it only warms on a page that mounts an engine\n     surface (a shelf, a product page, the finder, the duet), where the brain\n     is loading anyway and the catalogue is one more fetch on a warm\n     connection. Everywhere else the pointerover / focusin warmers above do it\n     on intent, a moment before the tap that needs it.\n     The test sits INSIDE the idle callback on purpose: ENGINE_SURFACE is a\n     const declared further down this module, and initSearch runs in the boot\n     sequence before that line - reading it here synchronously would throw\n     (temporal dead zone) and take the rest of the critical tier with it. */\n  recordView();\n  whenActive(() => {\n    if (!('requestIdleCallback' in window)) return;\n    requestIdleCallback(() => {\n      if (document.querySelector(ENGINE_SURFACE)) loadIndex();\n    }, { timeout: 4000 });\n  });\n}\n\n/* ---- Review carousel - ONE surface, two data paths (sections/reviews-ugc).\n\n   The curated Review blocks are rendered server-side, so they are what a\n   no-JS visitor sees and what holds the band's shape while anything else\n   loads. When the section is on the Fera path the live wall renders\n   off-canvas in [data-rev-src] and we READ it back out - rating, headline,\n   body, author, product, photos - and rebuild every review as one of OUR\n   cards. Fera owns the data; the theme owns every pixel.\n\n   The fallback is not optional: if the wall never arrives, or Fera changes\n   its DOM shape and we read fewer than [data-rev-min] reviews, the curated\n   cards simply stay and the theme's own aggregate is revealed instead of\n   Fera's. The band is never empty and never shows two different scores.\n\n   The carousel is a real one: paged snap scrolling, prev/next, dots, arrow\n   keys, drag on desktop (the track carries data-lane so initLaneDrag adopts\n   it) and native swipe on touch. It NEVER auto-advances (owner decision).\n\n   Fera selectors verified against the live rendered wall on 2026-08-03\n   (display 606526 -> .fera-allReviews--grid); the .fera-productReviews-*\n   twins are accepted too so a product-page display would work unchanged. */\nconst REV_STAR = 'M10 1.7l2.47 5.01 5.53.8-4 3.9.94 5.51L10 14.31l-4.94 2.6.94-5.51-4-3.9 5.53-.8z';\nconst REV_WAIT = 12000;\nconst REV_SVG = 'http://www.w3.org/2000/svg';\n\nfunction revText(n) { return n ? n.textContent.replace(/\\s+/g, ' ').trim() : ''; }\n\nfunction revEl(tag, cls, text) {\n  const n = document.createElement(tag);\n  if (cls) n.className = cls;\n  if (text != null) n.textContent = text;\n  return n;\n}\n\nfunction revStars(rating) {\n  const wrap = revEl('span', 'revc__stars');\n  wrap.setAttribute('role', 'img');\n  wrap.setAttribute('aria-label', rating + ' out of 5 stars');\n  for (let i = 1; i <= 5; i++) {\n    const cell = revEl('span', 'revc__star' + (i > rating ? ' is-off' : ''));\n    const svg = document.createElementNS(REV_SVG, 'svg');\n    svg.setAttribute('width', '15'); svg.setAttribute('height', '15');\n    svg.setAttribute('viewBox', '0 0 20 20'); svg.setAttribute('fill', 'currentColor');\n    svg.setAttribute('aria-hidden', 'true'); svg.setAttribute('focusable', 'false');\n    const path = document.createElementNS(REV_SVG, 'path');\n    path.setAttribute('d', REV_STAR);\n    svg.appendChild(path); cell.appendChild(svg); wrap.appendChild(cell);\n  }\n  return wrap;\n}\n\n/* The verified mark. It rides the reviewer's NAME rather than standing on the\n   byline as a third tracked-caps chip beside the product and the date - it is\n   a fact about the person, it can never wrap away from them, and at 13px it\n   costs the row almost no width. <title> inside role=\"img\" is what names it to\n   a screen reader; there is no visible word to read. */\nfunction revMark() {\n  const svg = document.createElementNS(REV_SVG, 'svg');\n  svg.setAttribute('class', 'revc__vf');\n  svg.setAttribute('width', '13'); svg.setAttribute('height', '13');\n  svg.setAttribute('viewBox', '0 0 20 20');\n  svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'currentColor');\n  svg.setAttribute('stroke-width', '1.9');\n  svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round');\n  svg.setAttribute('role', 'img'); svg.setAttribute('focusable', 'false');\n  const t = document.createElementNS(REV_SVG, 'title');\n  t.textContent = 'Verified buyer';\n  const p = document.createElementNS(REV_SVG, 'path');\n  p.setAttribute('d', 'M3.6 10.6l4 4 8.8-9.2');\n  svg.appendChild(t); svg.appendChild(p);\n  return svg;\n}\n\n/* The first usable thumbnail a review carries, for the wall's footer mark.\n   Both harvests are handled: revRead hands over plain strings, the API path\n   hands over { thumb, full }. */\nfunction revFace(media) {\n  if (!media || !media.length) return '';\n  for (let i = 0; i < media.length; i++) {\n    const m = media[i];\n    const thumb = typeof m === 'string' ? m : (m && m.thumb);\n    if (thumb) return thumb;\n  }\n  return '';\n}\n\n/* ---- SIX DIFFERENT PEOPLE, NOT SIX REVIEWS ----\n   (owner, 2026-08-26: the band has to be \"cleaner ... and structured\".)\n\n   Fera's `highest_quality` order is per REVIEW, and the people who write good\n   reviews write several. Measured against this store on 2026-08-26, the top 24\n   were 24 reviews by 15 people: Dimpho M. held five of them, four others held\n   two each, and the six the wall was showing included \"Botlhare M.\" twice -\n   the same name, beside the same photo, in two neighbouring cells. That reads\n   as a duplicated card, which is to say as a bug, whatever the two reviews\n   actually say.\n\n   So the wall takes ONE review per person, and at most two per scent, off a\n   deeper page of candidates. The scent cap is the smaller half of the same\n   idea: this band is the store's social proof, not one bottle's, and three\n   ENCHANTED cards in a six-cell grid make it look like a one-product shop.\n\n   THE RULE NEVER SHRINKS THE BAND. A young store, or one enthusiast holding\n   every good review, would otherwise empty cells to satisfy a preference - so\n   anything rejected is kept in order and backfills to `want`. A wall of six\n   with a repeat beats a wall of three. */\nfunction revSpread(rows, want) {\n  if (!rows || rows.length <= 1) return rows || [];\n  const n = want || rows.length;\n  const person = Object.create(null);\n  const scent = Object.create(null);\n  const keep = [];\n  const spare = [];\n  rows.forEach(function (r) {\n    const who = (r.name || '').trim().toLowerCase();\n    const what = (r.product || r.place || '').trim().toLowerCase();\n    if ((who && person[who]) || (what && scent[what] >= 2)) { spare.push(r); return; }\n    if (who) person[who] = 1;\n    if (what) scent[what] = (scent[what] || 0) + 1;\n    keep.push(r);\n  });\n  for (let i = 0; keep.length < n && i < spare.length; i++) keep.push(spare[i]);\n  return keep;\n}\n\n/* Remote strings go in via textContent only, remote images must be https,\n   and a product link is rebuilt from its PATH on THIS host - so a\n   compromised widget can neither inject markup nor send a shopper off-site. */\nfunction revPath(href) {\n  if (!href) return '';\n  try {\n    const u = new URL(href, window.location.href);\n    return u.pathname.indexOf('/products/') === 0 ? u.pathname : '';\n  } catch (e) { return ''; }\n}\n\nfunction revRead(node) {\n  const stars = node.querySelector('[data-rating]');\n  let rating = stars ? Math.round(parseFloat(stars.getAttribute('data-rating'))) : 0;\n  if (!(rating >= 0 && rating <= 5)) rating = 0;\n\n  let body = revText(node.querySelector('.fera-truncation-fullText'));\n  if (!body) {\n    body = revText(node.querySelector('.fera-allReviews-review-body, .fera-productReviews-review-body'))\n      .replace(/\\s*Show More$/i, '');\n  }\n  const head = revText(node.querySelector('.fera-allReviews-review-heading, .fera-productReviews-review-heading'));\n  if (!head && !body) return null;\n\n  const link = node.querySelector('.fera-allReviews-review-product-link, .fera-productReviews-review-product-link');\n  /* WHEN and WHETHER IT IS VERIFIED. Both were on the page all along and\n     neither was being read. Captured off a live product page on 2026-08-21:\n     the date is `.fera-*-review-date-val` carrying a relative label\n     (\"5 days ago\") in its text and the ISO instant in data-value, and a\n     verified review carries the class `verified` on its root as well as a\n     `[data-fera-verified=\"true\"]` mark in its customer block. Both are\n     additive - revCard only draws them when it is asked to. */\n  const when = node.querySelector('.fera-allReviews-review-date-val, .fera-productReviews-review-date-val');\n  const iso = when ? String(when.getAttribute('data-value') || '') : '';\n  return {\n    rating: rating,\n    head: head,\n    body: body,\n    date: revText(when),\n    dateIso: /^\\d{4}-\\d{2}-\\d{2}/.test(iso) ? iso : '',\n    verified: node.classList.contains('verified')\n      || !!node.querySelector('[data-fera-verified=\"true\"]'),\n    name: revText(node.querySelector('.fera-allReviews-review-customer-name, .fera-productReviews-review-customer-name')),\n    place: revText(node.querySelector('.fera-allReviews-review-customer-location, .fera-productReviews-review-customer-location')),\n    product: revText(link),\n    href: revPath(link && link.href),\n    media: Array.prototype.slice\n      .call(node.querySelectorAll('.fera-review-media-item-thumbnail, .fera-allReviews-photo, .fera-productReviews-photo'))\n      .slice(0, 3)\n      .map(function (img) { return img.getAttribute('src') || ''; })\n      .filter(function (src) { return /^https:\\/\\//i.test(src); })\n  };\n}\n\n/* TWO SHAPES OF THE SAME CARD.\n\n   opts.meta     adds the date and the verified mark - who left this and when,\n                 which is what a review is worth on a page asking to be\n                 believed. Recency was on the page all along and read by\n                 nobody.\n   opts.compact  renders ONE statement instead of a headline AND a body.\n\n   Why compact exists (owner, pass 3: reviews \"ALOT cleaner and nicer not so\n   messy ... also MOBILE\"). A real Fera row always carries both a heading and a\n   body, so every home card was stars + a 2-line bold headline + a 4-line\n   paragraph + a byline: four stacked type blocks in a column 340px wide, which\n   is the stack the owner was reading as mess. A card being SCANNED in a\n   scroller gets the review itself and nothing else; the PDP sheet, which is\n   being READ, still gets both. The headline is the fallback, not the loss:\n   revRead only returns a row when at least one of the two is there.\n\n   Neither option changes what is HARVESTED, so both surfaces keep the same\n   data and only the presentation differs. */\nfunction revCard(d, opts) {\n  const meta = !!(opts && opts.meta);\n  const compact = !!(opts && opts.compact);\n  const zoom = !!(opts && opts.zoom);\n  const li = revEl('li', 'revc');\n  if (d.rating > 0) li.appendChild(revStars(d.rating));\n  if (d.head && !compact) li.appendChild(revEl('p', 'revc__head', d.head));\n  const say = compact ? (d.body || d.head) : d.body;\n  if (say) li.appendChild(revEl('blockquote', 'revc__quote', say));\n\n  /* TWO SHAPES OF MEDIA, because two harvests feed this. The DOM path\n     (revRead) can only see the thumbnail the widget rendered, so it hands over\n     strings; the API path knows the full-size file too and hands over\n     { thumb, full }. opts.zoom then makes each thumbnail a real button that\n     swaps the big file in - a shopper photo is evidence, and evidence you\n     cannot look at is decoration.\n\n     THE STRIP IS THE SHEET'S NOW (owner, 2026-08-26: the band has to be\n     \"cleaner ... and structured\"). On the wall it floated between the quote\n     and the byline, so a card with a photo and a card without were different\n     objects: in a stretched row the photoless one grew a ~100px hole above a\n     byline pinned to the floor, and one ringed 40px square alone in a column\n     of type read as an image that had failed to load. The compact shape puts\n     the FIRST photo in the footer instead, at 44px, as the attribution's mark\n     - see below. The sheet is the reading surface and still shows every photo,\n     bigger and zoomable. */\n  if (!compact && d.media.length) {\n    const strip = revEl('span', 'revc__media');\n    d.media.forEach(function (m) {\n      const thumb = typeof m === 'string' ? m : m.thumb;\n      const full = typeof m === 'string' ? m : (m.full || m.thumb);\n      if (!thumb) return;\n      const img = revEl('img', 'revc__thumb');\n      img.src = thumb; img.alt = ''; img.loading = 'lazy'; img.decoding = 'async';\n      img.width = 52; img.height = 52;\n      if (!zoom) { strip.appendChild(img); return; }\n      const shot = revEl('button', 'revc__shot');\n      shot.type = 'button';\n      shot.setAttribute('data-thumb', thumb);\n      shot.setAttribute('data-full', full);\n      shot.setAttribute('aria-expanded', 'false');\n      shot.setAttribute('aria-label', 'Enlarge customer photo');\n      shot.appendChild(img);\n      strip.appendChild(shot);\n    });\n    if (strip.childNodes.length) li.appendChild(strip);\n  }\n\n  /* THE BYLINE, IN TWO SHAPES.\n\n     The SHEET keeps the flat wrapping row it always had: it is a reading\n     surface, the column is wide, and a name followed by a scent and a date on\n     one baseline is right there.\n\n     The WALL stacks them - name on top, scent and date as its caption - inside\n     a fixed-height footer. That is what makes every cell's floor the same\n     height whatever the name's length or whether there is a photo, which a\n     wrapping flex row could never promise in a 400px column, and it is the\n     difference between six cards and a grid. */\n  const by = revEl('p', 'revc__by');\n  if (d.name) {\n    const who = revEl('span', 'revc__author', d.name);\n    if (meta && d.verified) who.appendChild(revMark());\n    by.appendChild(who);\n  }\n  const sub = compact ? revEl('span', 'revc__sub') : by;\n  if (d.product) {\n    if (d.href) {\n      const a = revEl('a', 'revc__meta', d.product);\n      a.href = d.href;\n      sub.appendChild(a);\n    } else {\n      sub.appendChild(revEl('span', 'revc__meta', d.product));\n    }\n  } else if (d.place) {\n    sub.appendChild(revEl('span', 'revc__meta', d.place));\n  }\n  /* The date takes .revc__date, not .revc__meta. .revc__meta is the byline's\n     tracked-caps voice and that voice belongs to the PRODUCT - a name being\n     stated. \"4 weeks ago\" set as \"4 W E E K S  A G O\" beside \"ELYSIUM\" and a\n     reviewer's name gave the row three shouting labels and one quiet one, and\n     three tracked-caps chips in a 300px column is exactly the noise this pass\n     is here to remove. .revc__date was already written and wired to nothing. */\n  if (meta && d.date) {\n    const when = revEl('time', 'revc__date', d.date);\n    if (d.dateIso) when.setAttribute('datetime', d.dateIso);\n    sub.appendChild(when);\n  }\n  if (compact && sub.childNodes.length) by.appendChild(sub);\n\n  if (compact) {\n    /* The footer is the card's floor and it is ALWAYS emitted when there is\n       anything to attribute, photo or no photo - that uniform height is the\n       whole point of it. The photo is the first one the review carries; a\n       review with four is still one credential on a card being glanced at, and\n       the other three are a tap away in the sheet. */\n    const face = revFace(d.media);\n    if (by.childNodes.length || face) {\n      const foot = revEl('div', 'revc__foot');\n      if (face) {\n        const img = revEl('img', 'revc__face');\n        img.src = face; img.alt = ''; img.loading = 'lazy'; img.decoding = 'async';\n        img.width = 44; img.height = 44;\n        foot.appendChild(img);\n      }\n      foot.appendChild(by);\n      li.appendChild(foot);\n    }\n  } else if (by.childNodes.length) {\n    li.appendChild(by);\n  }\n  /* THE STORE'S ANSWER, when there is one. Fera stores a reply against the\n     review and the badge display never showed it, so eight years of Jennie\n     replying to people has been invisible on this site. It renders UNDER the\n     byline and indented behind a rule, so it reads as a response to that\n     review rather than as a second review. Only the API path sets d.reply. */\n  if (d.reply && d.reply.body) {\n    const rep = revEl('div', 'revc__reply');\n    if (d.reply.name) rep.appendChild(revEl('p', 'revc__reply-who', d.reply.name));\n    rep.appendChild(revEl('p', 'revc__reply-say', d.reply.body));\n    li.appendChild(rep);\n  }\n  return li;\n}\n\n/* Fera's own live aggregate, scraped out of a rendered display. THE HOME BAND\n   IS THE ONLY CALLER NOW - the PDP reads api.fera.ai instead (initPdpReviews),\n   which is both more than this can give and immune to Fera renaming a class.\n   This stays because the home wall genuinely is an embed: it shows Fera's own\n   display, so reading Fera's own DOM is the honest source there.\n\n   THREE display shapes, one reader: the all-reviews wall, the product-reviews\n   wall, and the product-reviews SUMMARY (the rating badge display 606576\n   renders - verified against its live DOM 2026-08-20:\n   `.fera-productReviewsSummary-avgRating[data-value=\"4.9\"]` +\n   `.fera-productReviewsSummary-reviewCount[data-value=\"9\"]`). data-value is\n   read before textContent either way. */\nfunction revAgg(src) {\n  const a = src.querySelector('.fera-allReviews-avgRating-val, .fera-productReviews-avgRating-val, .fera-productReviewsSummary-avgRating');\n  const c = src.querySelector('.fera-allReviews-count-total, .fera-allReviews-reviewCount, .fera-productReviews-reviewCount, .fera-productReviewsSummary-reviewCount');\n  if (!a) return null;\n  const avg = parseFloat(a.getAttribute('data-value') || a.textContent);\n  const raw = c ? (c.getAttribute('data-value') || c.textContent || '') : '';\n  const count = parseInt(String(raw).replace(/[^\\d]/g, ''), 10);\n  if (!(avg >= 0 && avg <= 5)) return null;\n  return { avg: avg, count: count };\n}\n\nfunction revFillStat(stat, agg) {\n  if (!stat) return;\n  if (agg) {\n    /* REVEAL, because the plate now ships hidden under a live source\n       (2026-08-30). sections/reviews-ugc.liquid stops asserting a typed rating\n       on the first frame: while Fera is the source the plate is `hidden` and\n       this is the only thing that takes it off, so the score a shopper sees\n       has always come from the review system. If the aggregate never arrives\n       the plate simply never appears, which is the honest state. */\n    stat.removeAttribute('hidden');\n    const v = Math.round(agg.avg * 10) / 10;\n    const num = stat.querySelector('[data-rev-avg]');\n    const bar = stat.querySelector('[data-rev-stars]');\n    const cnt = stat.querySelector('[data-rev-count]');\n    if (num) num.textContent = String(v);\n    if (bar) {\n      bar.style.setProperty('--fill', (v * 20) + '%');\n      bar.setAttribute('aria-label', v + ' out of 5 stars');\n    }\n    if (cnt && isFinite(agg.count) && agg.count > 0) {\n      /* revNum, not a local toLocaleString: en-ZA groups with a NO-BREAK\n         space, and this replace only ever matched that one - so a locale that\n         groups with an ordinary space would leave the head reading \"1 214\"\n         while the button beside it read \"1,214\". One formatter, one answer. */\n      cnt.textContent = revNum(agg.count) + ' reviews';\n    }\n  }\n  stat.classList.remove('is-pending');       /* class, not [hidden] - base.css */\n}\n\n/* ---- The PDP's Reviews row + sheet ----\n   (owner, 2026-08-19: reviews \"needs to be part of those pills\" - and\n   2026-08-22: \"reviews is not nice tho, I want to be able to view the fera\n   reviews of that product hto\".)\n\n   ---- WHY THIS STOPPED READING FERA'S WIDGET ----\n\n   Every version before 2026-08-22 harvested the DOM: the store's Fera display\n   rendered off-canvas in a .rev-src layer and this read the numbers back out\n   of it. It could never answer the owner, because the store's display (606576)\n   is a rating BADGE - it publishes an average and a count and carries no\n   review bodies at all. The sheet showed a score and then a line admitting the\n   reviews were not published here, which is exactly as broken as it sounds.\n\n   The reviews were one HTTP call away the whole time. api.fera.ai's PUBLIC\n   reviews endpoint takes the same public store_pk the widget already ships,\n   filters by external_product_id, and returns everything the page wants:\n\n     rating, heading, body, is_verified, created_at\n     customer.display_name / .display_location\n     media[] (photos, thumbnail_url + full url)\n     store_reply { name, body }\n     product.rating { average, count, counts[1..5] }   <- the histogram\n     meta { total_count, page, page_count, page_size }\n\n   Probed against this store on 2026-08-22: Cityscape 8 reviews, Mystic Haze\n   16, product-scoped correctly, `access-control-allow-origin: *`. Nothing has\n   to be configured in the Fera dashboard, which was the last blocker and was\n   never ours to unblock.\n\n   What that buys beyond the reviews themselves: no vendor class names to break\n   on, no MutationObserver racing a third-party render, no off-canvas layer,\n   and the sheet can show the star BREAKDOWN, the shopper photos and the\n   store's own replies - none of which the badge ever put on the page.\n\n   fera.js still loads on the PDP (snippets/fera-loader), for Fera's product\n   view tracking only. This function does not wait for it, need it, or touch\n   it.\n\n   THE ROW SHIPS HIDDEN and only appears once Fera has answered with a real\n   average - the page never prints a rating it cannot stand behind, and a scent\n   nobody has reviewed simply has three doors instead of four. Same rule as\n   before; only the source changed. */\nconst FERA_API = 'https://api.fera.ai/v3/public/reviews';\nconst FERA_PAGE = 10;\n\n/* When a review was left, in the fewest words that are still true. Recent ones\n   want the relative phrase - \"3 weeks ago\" is how a shopper judges whether a\n   scent is still what it was - and anything past a year wants the actual date,\n   because \"31 months ago\" is arithmetic homework. Fera hands over an ISO\n   instant and no label of its own (the widget rendered its own); this is the\n   label. */\nfunction revAgo(iso) {\n  const t = Date.parse(iso);\n  if (!isFinite(t)) return '';\n  const days = Math.floor((Date.now() - t) / 86400000);\n  if (days < 0) return '';\n  if (days < 1) return 'Today';\n  if (days === 1) return 'Yesterday';\n  if (days < 14) return days + ' days ago';\n  if (days < 61) {\n    const w = Math.round(days / 7);\n    return w + (w === 1 ? ' week ago' : ' weeks ago');\n  }\n  if (days < 365) {\n    const m = Math.round(days / 30);\n    return m + (m === 1 ? ' month ago' : ' months ago');\n  }\n  return new Date(t).toLocaleDateString('en-ZA', { month: 'short', year: 'numeric' });\n}\n\n/* One Fera API record -> the shape revCard already speaks.\n\n   THE PRODUCT NAME IS A CHOICE, not a default. On the PDP sheet it is dropped:\n   that sheet is opened FROM the product, so printing it on every card is the\n   page telling you where you are eight times over. On the HOME band it is the\n   most useful word on the card, because it is what turns an opinion into an\n   opinion about a bottle you can go and buy - so initHomeReviews asks for it\n   with { product: true }. The town stays either way; it is the one thing about\n   the reviewer that makes them a person rather than a name.\n\n   revCard prints .product when it is there and falls back to .place when it is\n   not, so one flag is the whole difference. */\nfunction feraCard(r, opts) {\n  const c = r.customer || {};\n  const reply = r.store_reply && r.store_reply.body ? r.store_reply : null;\n  const prod = (opts && opts.product && r.product) ? r.product : null;\n  return {\n    rating: Math.round(r.rating || 0),\n    head: r.heading || '',\n    body: r.body || '',\n    date: revAgo(r.created_at),\n    dateIso: r.created_at || '',\n    verified: !!r.is_verified,\n    name: c.display_name || '',\n    place: c.display_location || '',\n    product: prod ? (prod.name || '') : '',\n    /* rebuilt from the PATH on THIS host, the same guard the harvest path\n       applies - a compromised feed can never send a shopper off-site. */\n    href: prod ? revPath(prod.url) : '',\n    /* thumb for the strip, full for the tap - both straight from Fera's CDN\n       and both https-gated, the same rule the harvest path applied. */\n    media: (r.media || [])\n      .filter(function (m) { return m && m.type === 'photo'; })\n      .map(function (m) {\n        return { thumb: m.thumbnail_url || m.url || '', full: m.url || m.thumbnail_url || '' };\n      })\n      .filter(function (m) { return /^https:\\/\\//i.test(m.thumb); })\n      .slice(0, 4),\n    reply: reply ? { name: reply.name || '', body: reply.body } : null\n  };\n}\n\n/* ============================================================\n   THE HOME REVIEW BAND - the wall, the score and the sheet.\n\n   (owner, 2026-08-25: the reviews are \"messy on desktop and on mobile ...\n   look at how the PRODUCT PAGE loads reviews and do something similar on the\n   home page, plus the ability to view more reviews\".)\n\n   This replaces initRevCarousel, which is deleted along with the whole\n   .revcar* skin. There is no paged scroller any more, so there is nothing here\n   that measures a card, counts pages, builds dots, disables arrows or listens\n   to scroll. The wall is a CSS grid; how many cards a width can hold is a CSS\n   rule (theme.css .revwall__list). What is left for script is the part only a\n   network can answer: which reviews, what the score is, and the reader.\n\n   ---- WHERE THE REVIEWS COME FROM, IN THREE TIERS ----\n\n   1. api.fera.ai, store-wide. Exactly the mechanism the PDP sheet uses\n      (initPdpReviews) with product_id left off, which is the whole reason A2\n      says to copy the product page. Probed against this store on 2026-08-25:\n\n        /v3/public/reviews?public_key=..&sort=highest_quality\n            1,214 reviews, and highest_quality returns the same order the Fera\n            wall itself renders - the cards in the owner's screenshot are the\n            first four of it. Paging via meta{page,page_count,total_count}.\n        /v3/public/ratings?public_key=..&subject=store\n            one row: { average: 4.7, count: 1214, counts: [12,17,63,199,923] }.\n            The store aggregate AND its histogram, which the DOM harvest could\n            never give and which the sheet needs.\n\n      Both answer `access-control-allow-origin: *`. No vendor class names, no\n      MutationObserver racing a third-party render, and no twelve-second wait.\n\n   2. The rendered wall, harvested out of [data-rev-src]. This is what shipped\n      before and it stays, unchanged, as the backstop for tier 1 being\n      unreachable. Fera still renders off-canvas in a real, laid-out,\n      zero-height container for exactly this reason.\n\n   3. The section's curated Review blocks, which are already in the markup.\n      Nothing has to happen for these to be correct - they are what a no-JS\n      shopper sees and what stays if neither call above ever answers.\n\n   ---- WHAT NEVER HAPPENS ----\n   The score is never a hole and the button never has no label. Both are\n   server-rendered from the section's own settings on the first frame and\n   CORRECTED in place when Fera answers. A failed fetch leaves a true, if\n   slightly stale, number on screen rather than a gap.\n\n   ---- BOOT TIER ----\n   CRITICAL, not idle, because this binds the handler behind \"Read all N\n   reviews\" (12 SS3: anything that binds a handler a shopper can fire runs\n   now). It costs nothing to run early - the two fetches are held behind\n   onIdle + whenActive inside, so a prerendered page still asks Fera nothing.\n   ============================================================ */\nconst FERA_RATINGS = 'https://api.fera.ai/v3/public/ratings';\nconst HOME_REV_CARDS = 6;                    /* what the 3-up grid holds */\n\n/* South African thousands, the same shape revFillStat writes, so the count in\n   the section head and the count on the button can never disagree. */\nfunction revNum(n) {\n  return Number(n).toLocaleString('en-ZA').replace(/\\s/g, ',');\n}\n\nfunction initHomeReviews() {\n  const wall = document.querySelector('[data-rev-wall]');\n  if (!wall) return;\n  const track = wall.querySelector('[data-rev-track]');\n  if (!track) return;\n\n  const band = wall.closest('.rev-band') || document;\n  const stat = band.querySelector('[data-rev-stat]');\n  const allBtn = band.querySelector('[data-rev-all]');\n  const allLbl = band.querySelector('[data-rev-all-label]');\n  const sheet = band.querySelector('[data-home-rev-sheet]');\n  const sheetBars = sheet ? sheet.querySelector('[data-hrev-bars]') : null;\n  /* The plate's distribution bar. Unlike the sheet's histogram this ships\n     HIDDEN and is revealed only once real per-star counts exist - it has no\n     skeleton worth holding, so an absence has to read as an absence. */\n  const ribbon = stat ? stat.querySelector('[data-rev-ribbon]') : null;\n  const key = (wall.getAttribute('data-fera-key') || '').trim();\n\n  wall.classList.toggle('revwall--empty', track.children.length === 0);\n\n  /* Curated source, or no key: the blocks in the markup ARE the band. */\n  if (!wall.hasAttribute('data-rev-live') || !key) return;\n\n  let aggDone = false;\n  /* Tier 1 answered with neither cards nor a score. The sheet then has to be\n     fed from the page rather than from the call that just failed. */\n  let apiDead = false;\n  /* apiDead for the WRONG reason is the theme editor, where no tier is meant to\n     run and there is nothing to retry. This is the other reason - the network -\n     and it is the only one the sheet offers a way out of. */\n  let apiDeadNet = false;\n  /* What the harvest read out of Fera's own rendered wall, kept so the sheet\n     can show the SAME reviews the cards came from. */\n  let harvestRows = null;\n  /* Set by mountSheet: re-render a fallback sheet when a better tier lands. */\n  let sheetRefill = null;\n\n  /* ---- painting ---- */\n\n  function paintCards(rows) {\n    if (!rows.length) return false;\n    const frag = document.createDocumentFragment();\n    /* compact + meta: one statement, then who said it, whether they bought it,\n       which scent and when. See revCard - the wall is scanned, the sheet is\n       read. */\n    rows.slice(0, HOME_REV_CARDS).forEach(function (d) {\n      frag.appendChild(revCard(d, { meta: true, compact: true }));\n    });\n    track.textContent = '';\n    track.appendChild(frag);\n    wall.classList.remove('revwall--empty');\n    return true;\n  }\n\n  /* The button says how many reviews are behind it, and that number has to be\n     the same one the head is showing however it was arrived at - the API row,\n     or the harvested wall. One writer, called from both. */\n  function paintCount(count) {\n    if (!allLbl || !isFinite(count) || count <= 0) return;\n    allLbl.textContent = 'Read all ' + revNum(count) + ' reviews';\n  }\n\n  /* NEVER FIVE EMPTY BARS. The histogram is the one part of the score that\n     only tier 1 can produce: the rendered wall publishes an average and a\n     total, never a per-star breakdown, and six curated cards are not 1,082\n     reviews so counting them would be a lie drawn as a chart. So the bars are\n     drawn when there are real counts and DROPPED when there are not - which is\n     exactly what the PDP sheet does (initPdpReviews.revEmpty). An absence\n     reads as an absence; a row of empty tracks reads as a broken widget. */\n  function noBars() {\n    if (sheetBars) sheetBars.hidden = true;\n    if (ribbon) ribbon.hidden = true;\n  }\n\n  /* THE ONE NUMBER, in all three places it appears: the section head, the\n     button's label and the sheet's own score. Whichever tier arrived at it, it\n     is written HERE and only here - so \"Read all 1,179 reviews\" can never open\n     a sheet headed 1,082, which is what happened while the sheet's count was\n     written inside the API branch alone.\n     Shape is revFillStat's: { avg, count }, or null to just clear is-pending. */\n  function paintScore(agg) {\n    revFillStat(stat, agg);\n    if (!agg) return;\n    const avg = Math.round((agg.avg || 0) * 10) / 10;\n    const count = agg.count || 0;\n    if (count > 0) paintCount(count);\n    if (!sheet) return;\n\n    const n = sheet.querySelector('[data-hrev-avg]');\n    const bar = sheet.querySelector('[data-hrev-stars]');\n    const cnt = sheet.querySelector('[data-hrev-count]');\n    if (avg > 0) {\n      if (n) n.textContent = String(avg);\n      if (bar) {\n        bar.style.setProperty('--fill', (avg * 20) + '%');\n        bar.setAttribute('aria-label', avg + ' out of 5 stars');\n      }\n    }\n    if (cnt && count > 0) cnt.textContent = count === 1 ? '1 review' : revNum(count) + ' reviews';\n  }\n\n  /* The store aggregate off the API, which is the only source that carries the\n     per-star histogram with it. */\n  function paintAgg(agg) {\n    if (!agg) return false;\n    const avg = Math.round((agg.average || 0) * 10) / 10;\n    const count = agg.count || 0;\n    if (!(avg > 0) || !count) return false;\n    aggDone = true;\n\n    paintScore({ avg: avg, count: count });\n    if (!sheet) return true;\n\n    /* counts[] arrives lowest star first (index 0 = one star). Each bar is a\n       share of the BIGGEST band rather than of the total, so a 4.7 does not\n       render as one full bar and four invisible slivers - the shape is the\n       point of drawing it. The number beside it is still the true count. */\n    const counts = Array.isArray(agg.counts) ? agg.counts : null;\n    if (!counts) { noBars(); return true; }\n\n    /* THE RIBBON, in the plate: each band as a SHARE OF THE TOTAL, so the five\n       segments tile one bar and the shape of the store's opinion is the thing\n       you read. That is deliberately NOT the sheet's maths below, where each\n       bar is a share of the BIGGEST band - a histogram is five measurements\n       being compared to each other, a ribbon is one whole being divided up,\n       and sharing a denominator between them would make one of the two lie. */\n    if (ribbon) {\n      let whole = 0;\n      for (let i = 0; i < counts.length; i++) whole += counts[i] || 0;\n      if (whole > 0) {\n        for (let s = 1; s <= 5; s++) {\n          const seg = ribbon.querySelector('[data-rev-seg=\"' + s + '\"]');\n          if (seg) seg.style.setProperty('--w', (((counts[s - 1] || 0) / whole) * 100).toFixed(2) + '%');\n        }\n        ribbon.hidden = false;\n      }\n    }\n    const top = Math.max.apply(null, counts.concat([1]));\n    for (let s = 1; s <= 5; s++) {\n      const hits = counts[s - 1] || 0;\n      const fill = sheet.querySelector('[data-hrev-bar=\"' + s + '\"]');\n      const num = sheet.querySelector('[data-hrev-bar-n=\"' + s + '\"]');\n      if (fill) fill.style.setProperty('--fill', Math.round((hits / top) * 100) + '%');\n      if (num) num.textContent = revNum(hits);\n      const li = fill && fill.closest('.revsheet__bar');\n      if (li) {\n        li.setAttribute('aria-label', s + (s === 1 ? ' star: ' : ' stars: ')\n          + hits + (hits === 1 ? ' review' : ' reviews'));\n      }\n    }\n    if (sheetBars) sheetBars.hidden = false;\n    return true;\n  }\n\n  /* ---- tier 1: the public API ---- */\n\n  const feraUrl = (params) => FERA_API + '?public_key=' + encodeURIComponent(key) + params;\n\n  /* Four pages' worth of candidates for six cells, because revSpread throws\n     most of them away: at HOME_REV_CARDS the top six were six reviews by four\n     people. One request either way - page_size is not a second round trip. */\n  function apiWall() {\n    return fetch(feraUrl('&sort=highest_quality&page_size=' + (HOME_REV_CARDS * 4) + '&page=1'),\n                 { headers: { Accept: 'application/json' } })\n      .then((r) => (r.ok ? r.json() : null))\n      .then((d) => {\n        if (!d || !Array.isArray(d.data) || !d.data.length) return false;\n        /* product: true - the PDP drops the scent name because the sheet was\n           opened FROM that scent. Here it is the most useful word on the card:\n           it turns an opinion into an opinion about a bottle you can buy. */\n        return paintCards(revSpread(\n          d.data.map(function (r) { return feraCard(r, { product: true }); }),\n          HOME_REV_CARDS\n        ));\n      });\n  }\n\n  function apiAgg() {\n    return fetch(FERA_RATINGS + '?public_key=' + encodeURIComponent(key) + '&subject=store',\n                 { headers: { Accept: 'application/json' } })\n      .then((r) => (r.ok ? r.json() : null))\n      .then((d) => {\n        const rows = d && Array.isArray(d.data) ? d.data : [];\n        for (let i = 0; i < rows.length; i++) {\n          if (rows[i] && rows[i].subject === 'store') return paintAgg(rows[i]);\n        }\n        return false;\n      });\n  }\n\n  /* ---- tier 2: harvest the off-canvas wall ----\n     Unchanged from what shipped before, and only reached when tier 1 could not\n     be made at all. revRead reads vendor class names, so a Fera rename lands\n     here as \"no reviews found\" and the curated blocks simply stay. */\n  function harvest(needCards) {\n    const src = wall.querySelector('[data-rev-src]');\n    if (!src) { if (!aggDone) { paintScore(null); noBars(); } return; }\n\n    /* THE BUNDLE ARRIVES HERE, NOT ON EVERY VISIT (owner, 2026-08-27: the home\n       page \"shows but the page loads\"). snippets/fera-loader is rendered with\n       lazy:true by this section, so the window.fera queue is configured inline\n       at parse time but cdn.fera.ai/js/fera.js is never fetched - which took\n       roughly 2.7s of third-party requests off a page that only wants this\n       tier when the API tier has already failed. This is that moment, so this\n       is where it is asked for.\n\n       Guarded on the src attribute rather than a flag of our own: the product\n       page renders the same snippet WITHOUT lazy, so on any page that already\n       has the bundle this finds it and adds nothing. Fera reads the queue when\n       it boots, so configuration having happened long before it lands is the\n       normal path and not a race. */\n    if (!document.querySelector('script[src*=\"cdn.fera.ai/js/fera.js\"]')) {\n      const boot = document.createElement('script');\n      boot.async = true;\n      boot.src = 'https://cdn.fera.ai/js/fera.js';\n      document.head.appendChild(boot);\n    }\n\n    const min = parseInt(wall.getAttribute('data-rev-min'), 10) || 3;\n    let settled = false;\n    let obs = null;\n    let bail = 0;\n\n    const settle = function (agg) {\n      if (settled) return;\n      settled = true;\n      if (obs) obs.disconnect();\n      clearTimeout(bail);\n      if (aggDone) return;\n      /* head, button AND sheet off one object - null just clears is-pending */\n      paintScore(agg);\n      /* the wall publishes no per-star breakdown, so the sheet drops its\n         histogram rather than showing five empty tracks */\n      noBars();\n    };\n\n    const attempt = function () {\n      if (settled) return;\n      const list = src.querySelector('.fera-allReviews-reviews, .fera-productReviews-reviews');\n      if (!list) return;\n      const found = [];\n      list.querySelectorAll('.fera-allReviews-review, .fera-productReviews-review').forEach(function (node) {\n        const d = revRead(node);\n        if (d) found.push(d);\n      });\n      if (found.length < min) return;              /* not ready, or shape changed */\n      /* Kept whether or not the cards were needed: if tier 1 is unreachable the\n         SHEET is fed from these, and they are the reviews the wall is showing. */\n      harvestRows = found;\n      /* The SHEET keeps every harvested review - it is the reading surface and\n         a second review by the same person is a second review there. Only the\n         wall, which is six cells being glanced at, wants one voice each. */\n      if (needCards) paintCards(revSpread(found, HOME_REV_CARDS));\n      if (sheetRefill) sheetRefill();\n      settle(revAgg(src));\n    };\n\n    obs = new MutationObserver(attempt);\n    obs.observe(src, { childList: true, subtree: true });\n    bail = setTimeout(function () { settle(null); }, REV_WAIT);   /* curated stays */\n    attempt();\n  }\n\n  /* ---- THE SHEET: the PDP's reader, over the whole store ----\n     Default order, which is newest first - the same order the PDP sheet reads\n     in. The wall above is sorted by quality because it is an argument; a\n     reader who opened \"all reviews\" wants the recent ones.\n\n     Bound NOW, fetched on INTENT: hover or keyboard focus of the button warms\n     the first page, and the click runs it again. load() is idempotent\n     (busy / page >= pages), so warm, click and a second click are all safe, and\n     a failed first call leaves it retryable.\n\n     The dialog itself is opened by initPdpModals, which is delegated on the\n     document and also critical - so the sheet opens on the first tap whether or\n     not this has finished.\n\n     AND IT IS NEVER A DOOR ONTO NOTHING. Tier 1 is the source when it answers,\n     but when it is the very thing that failed the reviews behind the door are\n     the ones already ON the page: the harvested wall, or the curated blocks the\n     section always renders. The sheet shows those and says so. A button\n     promising 1,179 reviews that opens an empty panel is worse than no button. */\n  function mountSheet() {\n    if (!sheet || !allBtn) return;\n    const list = sheet.querySelector('[data-hrev-list]');\n    const more = sheet.querySelector('[data-hrev-more]');\n    const moreLbl = sheet.querySelector('[data-hrev-more-label]');\n    const fine = sheet.querySelector('[data-hrev-fine]');\n    const note = sheet.querySelector('[data-hrev-note]');\n    const empty = sheet.querySelector('[data-hrev-empty]');\n\n    let page = 0, pages = 1, total = 0, shown = 0, busy = false;\n    let from = '';                 /* which tier filled it: api / harvest / page */\n    let canRetry = false;          /* the fallback ran because a fetch FAILED, not because the editor is open */\n\n    function nothing() {\n      if (!aggDone) noBars();\n      if (empty) empty.hidden = false;\n    }\n\n    /* THE FALLBACK READER. Fills the sheet from whatever tier actually produced\n       the cards, and prints how many that is so the count in the head is never\n       silently contradicted by the length of the list under it.\n       Preference order is the same as the band's: the harvested rows (full\n       cards - heading, body, photos, replies), else a clone of exactly what the\n       wall is showing, which on the curated tier is the section's own blocks. */\n    function localList() {\n      if (!list || shown) return;\n      let n = 0;\n      if (harvestRows && harvestRows.length) {\n        from = 'harvest';\n        harvestRows.forEach(function (d) {\n          list.appendChild(revCard(d, { meta: true, zoom: true }));\n          n++;\n        });\n      } else {\n        from = 'page';\n        Array.prototype.forEach.call(track.children, function (li) {\n          const copy = li.cloneNode(true);\n          /* A curated block carries shopify_attributes. Two elements claiming\n             one block id is how the theme editor starts selecting the wrong\n             thing, so the copy is a copy of the CONTENT only. */\n          copy.removeAttribute('data-shopify-editor-block');\n          copy.removeAttribute('id');\n          list.appendChild(copy);\n          n++;\n        });\n      }\n      if (!n) { nothing(); return; }\n      shown = n; total = n; page = 1; pages = 1;\n      list.hidden = false;\n      if (fine) fine.hidden = true;\n      /* A DEAD END IS NOT AN ANSWER (owner, 2026-08-27: \"when I click on view\n         more reviews it only shows up to 6, and nothing more\"). Six is exactly\n         what this branch produces - the wall's own cards, cloned - and until\n         now it also hid the button, so a shopper who hit one bad minute of\n         Fera got six reviews and no route back to the 1,216. The button stays\n         when the reason was the network, and becomes the retry: one tap clears\n         apiDead and asks the API again. It is still hidden when the fallback is\n         legitimate (the theme editor), where there is nothing to retry. */\n      if (more && canRetry) {\n        if (moreLbl) moreLbl.textContent = 'Try again';\n        more.hidden = false;\n      } else if (more) {\n        more.hidden = true;\n      }\n      if (note) {\n        note.textContent = 'Showing the ' + n + ' review' + (n === 1 ? '' : 's')\n          + ' on this page. The rest are not reachable right now'\n          + (canRetry ? ' - tap Try again in a moment.' : '.');\n        note.hidden = false;\n      }\n    }\n\n    /* The way back. Everything localList wrote is thrown away and the API is\n       asked from page one again, so a retry that works leaves the sheet exactly\n       as a first successful open would have. */\n    function retry() {\n      canRetry = false;\n      apiDead = false;\n      if (list) list.textContent = '';\n      shown = 0; total = 0; page = 0; pages = 1; from = '';\n      if (note) { note.hidden = true; note.textContent = ''; }\n      if (empty) empty.hidden = true;\n      if (moreLbl) moreLbl.textContent = 'Loading...';\n      load();\n    }\n\n    function paintMore() {\n      if (!more) return;\n      const left = Math.max(0, total - shown);\n      if (page >= pages || !left) { more.hidden = true; return; }\n      if (moreLbl) {\n        moreLbl.textContent = 'Show ' + Math.min(left, FERA_PAGE) + ' more'\n          + (left === 1 ? ' review' : ' reviews');\n      }\n      more.hidden = false;\n    }\n\n    function load() {\n      if (busy || page >= pages) return;\n      /* Already known to be unreachable: do not ask again, read the page. */\n      if (apiDead) { canRetry = apiDeadNet; localList(); return; }\n      busy = true;\n      if (more && !more.hidden && moreLbl) moreLbl.textContent = 'Loading...';\n      fetch(feraUrl('&page_size=' + FERA_PAGE + '&page=' + (page + 1)),\n            { headers: { Accept: 'application/json' } })\n        .then((r) => (r.ok ? r.json() : null))\n        .then((d) => {\n          busy = false;\n          if (!d || !Array.isArray(d.data) || !d.data.length) {\n            if (more) more.hidden = true;\n            if (!shown) localList();\n            return;\n          }\n          const meta = d.meta || {};\n          from = 'api';\n          page = meta.page || page + 1;\n          pages = meta.page_count || page;\n          total = meta.total_count || d.data.length;\n          d.data.forEach((r) => {\n            if (!list) return;\n            list.appendChild(revCard(feraCard(r, { product: true }), { meta: true, zoom: true }));\n            shown++;\n          });\n          if (list && shown) list.hidden = false;\n          if (fine && shown) fine.hidden = false;\n          paintMore();\n        })\n        .catch(() => {\n          busy = false;\n          /* Nothing yet: fall back to the reviews the band itself is showing,\n             which is the whole point of there being three tiers. Mid-list: put\n             the button back, because the reviews it would have fetched still\n             exist and a second tap is the retry. */\n          if (!shown) { canRetry = true; if (more) more.hidden = true; localList(); return; }\n          paintMore();\n        });\n    }\n\n    if (more) more.addEventListener('click', function () {\n      if (canRetry) { retry(); return; }\n      load();\n    });\n\n    /* Tap a shopper photo, see it. The strip is four thumbnails; clicking one\n       swaps in the full-size file and lets the figure span the card, clicking\n       again puts it back. No lightbox - the sheet is already the overlay. */\n    if (list) {\n      list.addEventListener('click', (e) => {\n        const shot = e.target.closest('.revc__shot');\n        if (!shot) return;\n        const img = shot.querySelector('img');\n        const big = shot.classList.toggle('is-big');\n        shot.setAttribute('aria-expanded', big ? 'true' : 'false');\n        if (img) {\n          const swap = big ? shot.getAttribute('data-full') : shot.getAttribute('data-thumb');\n          if (swap) img.src = swap;\n        }\n      });\n    }\n\n    allBtn.addEventListener('pointerover', () => load(), { passive: true, once: true });\n    allBtn.addEventListener('focus', () => load(), { once: true });\n    allBtn.addEventListener('click', () => load());\n\n    /* A reader can beat the harvest to the button: the sheet then fills from\n       the curated blocks, and the harvest lands a moment later with the same\n       reviews the wall is now showing. Swap them in, so the sheet is never\n       showing a different tier from the cards behind it. */\n    sheetRefill = function () {\n      if (from !== 'page' || !list) return;\n      list.textContent = '';\n      shown = 0; total = 0; page = 0; pages = 1;\n      if (note) { note.hidden = true; note.textContent = ''; }\n      localList();\n    };\n  }\n\n  mountSheet();\n\n  /* In the theme editor the curated blocks must stay selectable, so no tier\n     runs - but the sheet is bound above and reads the page, so the merchant\n     sees their own blocks behind their own button rather than an empty panel. */\n  if (window.Shopify && window.Shopify.designMode) {\n    apiDead = true;\n    paintScore(null);\n    noBars();\n    return;\n  }\n\n  /* The two band fetches are the only network here and nothing is waiting on\n     a tap for them, so they wait for idle - and for the document to actually\n     be the shopper's, so a prerender never counts as a visit. */\n  onIdle(function () {\n    whenActive(function () {\n      /* BOTH answers before deciding, or the harvest races the aggregate it\n         was only started because of and the score gets written twice. */\n      Promise.all([\n        apiAgg().catch(function () { return false; }),\n        apiWall().catch(function () { return false; })\n      ]).then(function (r) {\n        /* Tier 1 said nothing at all: the sheet must read the page too, or the\n           door opens onto the same call that just failed. If EITHER answered,\n           the reviews endpoint is reachable and the sheet keeps paging it. */\n        if (!r[0] && !r[1]) { apiDead = true; apiDeadNet = true; }\n        /* Fall back to reading Fera's own rendered wall, and let it fill the\n           cards too when that is what is missing. */\n        if (!r[1]) harvest(true);\n        else if (!r[0]) harvest(false);\n      });\n    });\n  });\n}\n\n/* ============================================================\n   Cross-document view transitions - the moving parts.\n\n   base.css owns everything static: which elements persist and how the body\n   travels. One thing cannot be expressed in CSS: WHICH WAY the navigation is\n   going, so back can feel like back.\n\n   THE hero-shot PAINTER IS GONE (owner, 2026-08-19: the PDP image \"shifts\n   during page load\" - it was the card shot morphing into the stage, the one\n   element still moving after the root fade had finished). The PDP photograph\n   now rides the root cross-fade like everything else; see base.css \u00A73.\n\n   Firefox has no cross-document transitions yet and simply ignores all of\n   it - real navigation, no fallback code required.\n\n   THE SAFETY HALF IS NOT HERE. The in-app-browser skip and the 800ms stall\n   watchdog live in an inline head script in layout/theme.liquid, because\n   `pagereveal` can fire before this deferred module has run and the thing\n   they exist to survive is exactly this module not running. Both listeners\n   below and there are additive, so a transition can carry a `back` type and\n   still be watched. Do not move them here (2026-09-08).\n   ============================================================ */\nfunction initViewTransitions() {\n  const nav = typeof navigation !== 'undefined' ? navigation : null;\n\n  /* navigation.activation is Chromium-only; without it every navigation\n     simply reads as forward, which is the sane default. */\n  window.addEventListener('pagereveal', (e) => {\n    if (!e.viewTransition) return;\n    const act = nav && nav.activation;\n    const back = act && act.from && nav.currentEntry\n      && typeof act.from.index === 'number'\n      && act.from.index > nav.currentEntry.index;\n    if (back && e.viewTransition.types) e.viewTransition.types.add('back');\n  });\n}\n\n/* ---- Jennie: the chat panel's opener ----\n   The binder only. It owns the tap and nothing else: the conversation client,\n   the streaming parser and the card renderers are in assets/jennie.js and\n   arrive on first intent, so a shopper who never opens the chat never pays\n   for it. Same trade as the search engine.\n\n   This is critical tier because it BINDS A HANDLER a shopper can fire. If it\n   ran at idle, the first tap on \"Chat now\" would fall through to the href and\n   navigate away to the Help Centre instead of opening the panel - which is\n   exactly the failure the tier rule above exists to prevent.\n\n   PROGRESSIVE BY CONSTRUCTION. Every seam in the theme is an <a href> with a\n   real fallback, and preventDefault only happens once the panel is genuinely\n   opening. No JS, no endpoint, no module - the link still works. */\nfunction initJennie() {\n  const panel = document.querySelector('[data-jennie]');\n  const conf = (window.theme && window.theme.jennie) || null;\n  if (!panel || !conf || !conf.module) return;\n\n  const SEAM = '[data-chat-open], [data-magic]';\n  let ready = null;   /* the module promise, started on first intent */\n  let api = null;\n\n  const warm = () => {\n    if (ready) return ready;\n    ready = import(conf.module)\n      .then((m) => { api = m.default(conf); return api; })\n      .catch((err) => { console.warn('[juce] Jennie failed to load', err); ready = null; return null; });\n    return ready;\n  };\n\n  /* Hover and keyboard focus are intent; the module is usually parsed before\n     the click lands. Mirrors initSearch's warm-on-intent. */\n  document.addEventListener('pointerover', (e) => {\n    if (e.target.closest(SEAM)) warm();\n  }, { passive: true });\n  document.addEventListener('focusin', (e) => {\n    if (e.target.closest(SEAM)) warm();\n  });\n\n  /* ---- missed messages, without loading the client ----\n     The badge has to be able to appear for someone who has NOT opened the\n     chat this visit - otherwise the one case it exists for (an agent replied\n     while they were away) is the one case it cannot report. So the check\n     lives here rather than in jennie.js: a single small fetch, only when a\n     previous conversation is on record, and only once the page is idle.\n     Anyone who has never chatted does nothing at all. */\n  const hail = document.querySelector('[data-jn-hail]');\n  const hailCount = document.querySelector('[data-jn-hail-count]');\n\n  function checkMissed() {\n    if (!hail || !hailCount || !conf.endpoint) return;\n    let saved;\n    try { saved = JSON.parse(localStorage.getItem('juce:jennie:v1') || '{}'); }\n    catch (err) { return; }\n    if (!saved.session) return;\n\n    fetch(conf.endpoint.replace(/\\/+$/, '') + '/api/poll?session='\n      + encodeURIComponent(saved.session) + '&after=' + (saved.seen || 0),\n      { headers: { Accept: 'application/json' } })\n      .then((r) => (r.ok ? r.json() : null))\n      .then((d) => {\n        const n = d && d.messages ? d.messages.length : 0;\n        hailCount.textContent = n > 9 ? '9+' : String(n);\n        hailCount.hidden = n === 0;\n        hail.classList.toggle('is-waiting', n > 0);\n      })\n      .catch(() => { /* offline - try again next visit */ });\n  }\n  onIdle(() => whenActive(checkMissed));\n\n  /* THE CHAT SURVIVES THE PAGE. This is an MPA - every navigation tears the\n     panel down - so \"the chat stays open while they browse\" means: the panel\n     writes a session flag while it is open, and every page that boots with\n     the flag set reopens it quietly (no focus steal, no re-greeting; the\n     conversation itself was already persisted). Session-scoped on purpose:\n     a fresh tab starts closed. */\n  try {\n    if (sessionStorage.getItem('juce:jennie:open:v1') === '1') {\n      whenActive(() => warm().then((jn) => {\n        if (jn && !jn.isOpen()) jn.open({ quiet: true, source: 'resume' });\n      }));\n    }\n  } catch (err) { /* private mode - the flag never existed */ }\n\n  /* ONE DOOR, TWO KNOCKS. Every seam in the theme goes through open(); so does\n     anything that is not a seam - the trade application, which has to hand\n     over a whole brief rather than a link's worth of prompt. Giving it an\n     event rather than a global keeps the module private and keeps the panel\n     the only thing that knows how to open itself. */\n  /* WHICH BOTTLE THE SPARKLE IS POINTING AT, resolved at the moment of the tap\n     and never cached. The dock is a moving target - it adopts whichever poster\n     owns the screen, and on a product page the shopper can change the size\n     under it - so an id written into an attribute at boot, or even at adoption,\n     is one flick of the thumb away from being the scent before last.\n\n     Both surfaces answer the same question in the same words: the adopted\n     card's own form, or the product page's own picker. Only the ID is used;\n     the title is for the line the panel draws locally, and the server looks\n     the scent up again from its own catalogue either way. */\n  function magicScent(seam) {\n    const adopted = document.querySelector('.dk-card.is-adopted');\n    const form = (adopted && adopted.querySelector('form')) || document.querySelector('[data-pdp-form]');\n    if (!form) return null;\n    const picked = form.querySelector('input[name=\"id\"]:checked')\n      || form.querySelector('[data-variant-input]');\n    const id = picked ? Number(picked.value) : 0;\n    if (!id) return null;\n    const size = (picked.getAttribute('data-variant-label')\n      || (form.querySelector('[data-size-opt].is-active, [data-size-opt][aria-pressed=\"true\"]') || {}).textContent\n      || '').trim();\n    /* The name is for the line the panel draws, nothing else - the server\n       never trusts it. Taken from the adopted poster when there is one, and\n       otherwise off the sparkle's own accessible name, which dock.liquid\n       renders server-side on a product page and syncCommerce rewrites on\n       every adoption: \"Ask Jennie about this scent - Bold Soul\". */\n    const name = adopted && adopted.querySelector('.dk-id__name');\n    const label = (seam && seam.getAttribute('aria-label')) || '';\n    const title = (name ? name.textContent : label.split(' - ').slice(1).join(' - ')).trim();\n    return { variant_id: id, size: size || null, title: title || null };\n  }\n\n  /* `handoff` is any guided room sending everything it holds rather than a\n     sentence: the scent finder, the layering duet, and whatever joins them.\n     One slot, one shape ({ surface, line, answer }) - see askHandoff in\n     assets/jennie.js, which is where it becomes a module line and an event.\n\n     RESOLVES WITH WHETHER THE BRIEF ACTUALLY TRAVELLED. The room raises the\n     bubble that says it did, and the guards that can stop it (she is already\n     mid-answer, the module never loaded, the shopper cleared context) all sit\n     on the far side of this promise. Rooms toast in the .then, never at click\n     time - the same rule initCart keeps for the drawer confirmation. */\n  function open(prompt, ask, source, fallback, about, handoff) {\n    /* THE MENU CLOSES FIRST (owner, 2026-08-19: \"When click on Chat now the\n       menu must close and jennie opens\"). On a phone the menu sheet is a\n       showModal() dialog, so a panel opened while it is up lands underneath\n       it and reads as nothing having happened. On a desktop the mega panel is\n       hover state and would sit over the chat it just launched. See\n       closeChrome. */\n    closeChrome();\n    if (hailCount) { hailCount.hidden = true; }\n    if (hail) hail.classList.remove('is-waiting');\n    return warm().then((jn) => {\n      if (!jn) {\n        if (fallback) location.href = fallback;   /* module died - honour the fallback */\n        return false;\n      }\n      /* `=== true` on purpose: an older cached jennie.js returns undefined\n         from open(), and the honest reading of \"the widget did not say\" is\n         \"do not claim it landed\". Understating is survivable; the bubble\n         claiming a hand-off that never left is what this fixes. */\n      return jn.open({ prompt: prompt, ask: ask, source: source, about: about || null, handoff: handoff || null }) === true;\n    });\n  }\n\n  document.addEventListener('click', (e) => {\n    const seam = e.target.closest(SEAM);\n    if (!seam) return;\n    /* The sparkle is a separate switch: it can be turned off without taking\n       the eight text seams down with it. */\n    if (seam.hasAttribute('data-magic') && !conf.dockMagic) return;\n    if (seam.getAttribute('aria-disabled') === 'true') return;\n\n    e.preventDefault();\n    /* READ THE PROMPT NOW, NOT AT BOOT. paintJennie rewrites\n       data-chat-prompt on the scent finder's hand-off every time the brief\n       changes, so a value cached at bind time would be the wrong question. */\n    const prompt = seam.getAttribute('data-chat-prompt') || '';\n    const magic = seam.dataset.magic != null;\n    /* THE SPARKLE POSTS AN ACTION, NOT A SENTENCE (owner, 2026-08-19: \"it must\n       initiate a chat with Jennie and then Jennie must tell about the product\",\n       then: \"dont show 'Tell me about Bold Soul...' as a message rather a\n       module or something triggered\").\n\n       Sending the question as though the shopper typed it was a lie in three\n       places at once: the panel drew it in their bubble, the transcript kept it\n       as their words, and the model read it as something they had said. So the\n       tap now sends what it IS - a variant id and the fact that a button was\n       pressed - and the panel draws a module line instead of a bubble. The\n       server resolves the scent from its own catalogue and steers her to talk\n       about it; see chat.ts, about_product.\n\n       The prompt stays as the fallback for the case the id cannot be resolved\n       (no form on the page, a card mid-swap), because a sparkle that does\n       nothing is worse than a sparkle that asks in words. Every text seam\n       keeps the prefill it always had. */\n    const about = magic ? magicScent(seam) : null;\n    if (about) { open('', false, 'dock', seam.href, about); return; }\n    open(prompt, magic && !!prompt, magic ? 'dock' : 'seam', seam.href);\n  });\n\n  document.addEventListener('juce:chat', (e) => {\n    const d = (e && e.detail) || {};\n    /* d.handoff is a guided room handing over everything it holds (D12) - the\n       scent finder or the layering duet, both through this one slot: not a\n       sentence but the brief itself, which the panel draws as a module line\n       and posts as an action. The brief's own STRUCTURE travels separately,\n       on the section root, where jennie.js reads it into page.<surface> on\n       every message. */\n    /* THE ANSWER GOES BACK ON THE DETAIL. dispatchEvent is synchronous, so a\n       room that dispatched this can read d.done the line after and confirm in\n       the .then rather than at click time. A page with no controller listening\n       leaves d.done undefined, which is the room's cue that it cannot claim\n       anything - see openDuetJennie and the finder's openJennie. */\n    d.done = open(d.prompt || '', !!d.ask, d.source || 'seam', d.fallback, d.about || null, d.handoff || null);\n  });\n}\n\n/* Shared by the trade dialog and every chat seam (owner, 2026-08-19: \"When\n   click on Chat now the menu must close and jennie opens\").\n\n   A navigation surface still standing when something else takes the screen is\n   two modals deep, and on a phone the second one opens UNDERNEATH the first -\n   the menu sheet is a top-layer <dialog> shown with showModal(), so Jennie\n   opening behind it is invisible and the shopper is looking at the menu they\n   thought they had just left.\n\n   The desktop mega panel is not a dialog - it is CSS :hover / :focus-within -\n   so it cannot be \"closed\", only out-voted. `.is-closed` is the lock initNav\n   already stamps for the Escape key, and it beats both states; initNav clears\n   it again on the next pointer entry, so the panel is not dead, just dismissed.\n   Blurring matters too: without it focus stays inside the group and\n   :focus-within holds the panel open the moment the lock lifts. */\nfunction closeChrome() {\n  const sheet = document.querySelector('[data-menu]');\n  if (sheet && sheet.open) sheet.close();\n\n  /* THE SEARCH POPUP IS THE SAME KIND OF THING (2026-09-12). It is a\n     showModal() dialog too, and since the panel grew an Ask Jennie line\n     (askRow in initSearch) there is a seam INSIDE it - so the chat would\n     have opened in the top layer beneath the popup and the shopper would\n     still be looking at their search results. close() rather than a\n     classname: initSearch listens for the dialog's own `close` event and\n     puts the combobox state back from there, so every way out agrees. */\n  const search = document.querySelector('[data-search]');\n  if (search && search.open) search.close();\n\n  const active = document.activeElement;\n  const inGroup = active && active.closest ? active.closest('[data-nav-group]') : null;\n  document.querySelectorAll('[data-nav-group]').forEach((group) => {\n    if (group !== inGroup && !group.matches(':hover')) return;\n    group.classList.add('is-closed');\n    const link = group.querySelector('.site-nav__link');\n    if (link) link.setAttribute('aria-expanded', 'false');\n  });\n  if (inGroup && active.blur) active.blur();\n}\n\n/* ============================================================\n   STORE CREDIT (H1, H3).\n\n   The balance lives in Flits, and Flits' API is opened by one shop-wide token\n   that reads and moves ANY customer's money. That token can never be in this\n   file, and the Shopify app proxy does not stand in for it - measured on\n   2026-08-26, /apps/flits/api/1/25130/{id}/credit/get_credit answers 401 with\n   no token and 401 with only Flits' own customer_hash. So every call here\n   goes to Jennie, which holds the token, and identity is proved rather than\n   claimed: window.theme.credit.proof is an hmac Liquid computed over the\n   customer id with the Flits shop-metafield token as the key. There is no\n   window.theme.credit at all for a guest, so this whole block no-ops.\n\n   TWO SURFACES, ONE MECHANISM.\n     initAcctCredit  the account page: the balance tile, the figure, and the\n                     history. One GET, passive, idle tier.\n     initCartCredit  the bag: how much comes off, and the code that makes it\n                     come off. Owns the Checkout button, so critical tier.\n\n   THE FIGURES ARE NEVER OURS. Flits decides what a cart can absorb, because\n   it owns the spend rules; this file asks and prints. The only arithmetic in\n   here is \"available minus chosen\", which is subtraction of two numbers Flits\n   sent in the same breath.\n   ============================================================ */\n\n/* Cents, signed, in the house money shape. bagMoney clamps at zero because a\n   subtotal cannot be negative - a credit row can, so the sign is put back on\n   the outside rather than by changing that function under the bag. */\nfunction creditMoney(cents) {\n  const n = Math.round(Number(cents) || 0);\n  return (n < 0 ? '-' : '') + bagMoney(Math.abs(n));\n}\n\nfunction creditCfg() {\n  const c = window.theme && window.theme.credit;\n  if (!c || !c.endpoint || !c.id || !c.proof) return null;\n  return c;\n}\n\n/* One door. The proof rides a HEADER and never a query string: it is close\n   enough to a credential that it must not land in an access log, a Referer or\n   a history entry, and a header is the only place a value can ride a GET\n   without reaching all three. */\nfunction creditCall(path, body) {\n  const c = creditCfg();\n  if (!c) return Promise.reject(new Error('no credit'));\n  const base = String(c.endpoint).replace(/\\/+$/, '');\n  const url = base + path + (body ? '' : '?customer_id=' + encodeURIComponent(c.id));\n  const init = { headers: { 'x-credit-proof': c.proof } };\n  /* Never longer than eight seconds: a JENNIE that hangs must not hold a\n     shopper at checkout for nginx's three minutes. A timeout rejects like\n     any failed call, so checkout goes on without credit (steering/house/29). */\n  if (typeof AbortSignal !== 'undefined' && AbortSignal.timeout) init.signal = AbortSignal.timeout(8000);\n  if (body) {\n    init.method = 'POST';\n    init.headers['Content-Type'] = 'application/json';\n    init.body = JSON.stringify(Object.assign({ customer_id: c.id }, body));\n  }\n  return fetch(url, init).then((r) => r.json());\n}\n\n/* ---- the bag ---- */\n\n/* THE SELECTION LIVES HERE, NOT IN THE DOM, because swapDrawer replaces every\n   child of [data-drawer-inner] on every quantity tap and the control is\n   inside it. sessionStorage carries it across a real navigation as well - the\n   drawer is on every page and /cart is a different document. */\nlet creditOffer = null;\nlet creditSig = '';\nlet creditSeq = 0;\nlet creditQuoteAt = 0;\nlet creditQuoting = false;\nlet creditPaying = false;\n\n/* THE CONTROL, BUILT TO FIT THE OFFER.\n\n   One amount is a switch and several are pills. The shape is decided by what\n   Flits actually returns rather than by a setting, because the question the\n   control has to ask genuinely differs: with one spend rule the only question\n   is whether to use it, and a menu wrapped around a yes is not a choice.\n\n   Both shapes are <button>s carrying [data-credit-pick] with the rule id, so\n   one delegated click handler drives either. Neither is a form control, which\n   is deliberate: the bag posts to Shopify and a stray named input inside that\n   form would ride to checkout as cart-attribute noise. */\nfunction creditControl(options, available) {\n  if (!options.length) return [];\n\n  if (options.length === 1) {\n    const o = options[0];\n    const b = document.createElement('button');\n    b.type = 'button';\n    b.className = 'bag-credit__switch';\n    b.dataset.creditPick = String(o.rule_id);\n    b.setAttribute('role', 'switch');\n    b.setAttribute('aria-checked', 'false');\n\n    const t = document.createElement('span');\n    t.className = 'bag-credit__switch-t';\n    /* \"Use R 2073.26\" - and NOT \"of your R 2073.26\", which was the old label.\n       The amount available is printed in the head one line above, so saying it\n       twice in one block made the control read as a riddle. When the offer is\n       smaller than the balance the difference is the interesting part, and the\n       effect line below already carries it as \"stays on your account\". */\n    t.textContent = 'Use ' + bagMoney(o.cents);\n    b.appendChild(t);\n\n    const track = document.createElement('span');\n    track.className = 'bag-credit__track';\n    track.setAttribute('aria-hidden', 'true');\n    track.appendChild(document.createElement('i'));\n    b.appendChild(track);\n    return [b];\n  }\n\n  return options.map((o) => {\n    const b = document.createElement('button');\n    b.type = 'button';\n    b.className = 'chip bag-credit__pill';\n    b.dataset.creditPick = String(o.rule_id);\n    b.setAttribute('aria-pressed', 'false');\n    b.textContent = bagMoney(o.cents);\n    return b;\n  });\n}\n\n/* Paint the chosen state onto whichever shape is there. Kept apart from the\n   builder so a repaint after a quantity change moves the state without\n   rebuilding the control under the shopper's finger. */\nfunction creditControlState(choice, rule) {\n  choice.querySelectorAll('[data-credit-pick]').forEach((b) => {\n    const on = rule > 0 && Number(b.dataset.creditPick) === rule;\n    b.classList.toggle('is-on', on);\n    if (b.getAttribute('role') === 'switch') b.setAttribute('aria-checked', on ? 'true' : 'false');\n    else b.setAttribute('aria-pressed', on ? 'true' : 'false');\n  });\n}\n\nfunction creditRule() {\n  try {\n    const v = parseInt(sessionStorage.getItem('juce:credit:rule') || '', 10);\n    return Number.isFinite(v) && v > 0 ? v : -1;\n  } catch { return -1; }\n}\nfunction setCreditRule(id) {\n  try {\n    if (id > 0) sessionStorage.setItem('juce:credit:rule', String(id));\n    else sessionStorage.removeItem('juce:credit:rule');\n  } catch { /* private mode: the choice lasts the page, which is enough */ }\n}\n\nfunction initCartCredit() {\n  if (!creditCfg()) return;\n\n  /* Delegated, so the drawer swap never needs re-binding. A click rather than\n     a change, because the control is now buttons rather than a <select>. */\n  document.addEventListener('click', (ev) => {\n    const hit = ev.target.closest && ev.target.closest('[data-credit-pick]');\n    if (!hit) return;\n    ev.preventDefault();\n    const want = parseInt(hit.dataset.creditPick, 10);\n    /* A switch toggles; a pill picks. Pressing the pill that is already on\n       turns it off, which is the only way back to \"none\" when there is no\n       \"Do not use\" row to press. */\n    const now = creditRule();\n    const id = (want > 0 && want === now) ? -1 : (Number.isFinite(want) ? want : -1);\n    setCreditRule(id);\n    if (!(id > 0)) {\n      /* Un-choosing has to reach Flits, not just this page: a code minted on\n         an earlier visit is still attached to the account and would ride the\n         next checkout. Fire and forget - it must never delay anybody. */\n      creditCall('/api/credit/clear', {}).catch(() => {});\n    }\n    paintCartCredit();\n  });\n\n  /* Space and Enter already press a <button>. The switch additionally answers\n     the arrow keys a switch is expected to answer. */\n  document.addEventListener('keydown', (ev) => {\n    const hit = ev.target.closest && ev.target.closest('[data-credit-pick][role=\"switch\"]');\n    if (!hit) return;\n    if (ev.key !== 'ArrowLeft' && ev.key !== 'ArrowRight') return;\n    ev.preventDefault();\n    const want = parseInt(hit.dataset.creditPick, 10);\n    const on = ev.key === 'ArrowRight';\n    const id = on && Number.isFinite(want) ? want : -1;\n    if (id === creditRule()) return;\n    setCreditRule(id);\n    if (!(id > 0)) creditCall('/api/credit/clear', {}).catch(() => {});\n    paintCartCredit();\n  });\n\n  /* CAPTURE, so this runs before anything else bound to the bag forms. */\n  document.addEventListener('submit', creditCheckout, true);\n\n  refreshCartCredit();\n}\n\n/* Called on boot and from the tail of swapDrawer. Repaints the brand-new\n   shell from what is already known, then re-asks - the amount Flits offers\n   depends on the cart total, so a swap is exactly when it changes. */\nfunction refreshCartCredit() {\n  if (!creditCfg()) return;\n  if (!document.querySelector('[data-cart-credit]')) return;\n  paintCartCredit();\n  quoteCredit();\n}\n\nfunction quoteCredit() {\n  /* One quote per 400ms of quiet. A quantity stepper fires a swap per tap and\n     a shopper taps three times in a second; the old app debounced this at\n     2500ms, which is long enough for the figure to be visibly stale. */\n  const at = Date.now();\n  creditQuoteAt = at;\n  setTimeout(() => {\n    if (creditQuoteAt !== at) return;\n    if (!document.querySelector('[data-cart-credit]')) return;\n    const seq = ++creditSeq;\n    creditQuoting = true;\n    cartJson()\n      .then((cart) => creditCall('/api/credit/offer', { cart }))\n      .then((res) => {\n        if (seq !== creditSeq) return;                 /* a stale answer */\n        creditOffer = (res && res.ok === true) ? res : null;\n        paintCartCredit();\n      })\n      .catch(() => {})\n      .then(() => { creditQuoting = false; });\n  }, 400);\n}\n\n/* Shopify's own cart, verbatim, because Flits' spend rules are evaluated\n   against it and this file must not have a second opinion about what is in\n   the bag. */\nfunction cartJson() {\n  const url = ((window.theme && window.theme.routes && window.theme.routes.cart) || '/cart') + '.js';\n  return fetch(url, { headers: { Accept: 'application/json' } }).then((r) => r.json());\n}\n\nfunction paintCartCredit() {\n  const boxes = [...document.querySelectorAll('[data-cart-credit]')];\n  if (!boxes.length) return;\n  const offer = creditOffer;\n  const options = (offer && Array.isArray(offer.options)) ? offer.options : [];\n  const available = offer ? Math.max(0, Math.round(Number(offer.available) || 0)) : 0;\n\n  /* NO CREDIT, NO CONTROL. Zero balance, nothing this cart can absorb, the\n     dial off, Flits not answering and a rotated token all land here, and all\n     of them leave the bag exactly as it was. */\n  if (!offer || available <= 0 || !options.length) {\n    boxes.forEach((b) => { b.hidden = true; });\n    return;\n  }\n\n  let rule = creditRule();\n  if (rule > 0 && !options.some((o) => o.rule_id === rule)) {\n    /* The chosen rule stopped applying, most likely because the cart shrank.\n       Forget it rather than carrying a choice that no longer exists. */\n    rule = -1;\n    setCreditRule(-1);\n  }\n  const chosen = options.find((o) => o.rule_id === rule) || null;\n\n  /* Rebuilding the <select> throws focus away, so it only happens when the\n     offer itself changed - a repaint after a quantity tap that returned the\n     same amounts leaves the control alone. */\n  const sig = available + '|' + options.map((o) => o.rule_id + ':' + o.cents).join(',');\n\n  boxes.forEach((box) => {\n    const have = box.querySelector('[data-credit-have]');\n    if (have) have.textContent = bagMoney(available) + ' available';\n\n    /* THE CONTROL. One amount is a switch, several are pills - see the note in\n       snippets/cart-credit.liquid for why the native <select> went.\n\n       Rebuilt only when the OFFER changed, never on a plain repaint: a\n       quantity tap that returns the same amounts must not tear down the\n       control the shopper's finger or focus is on. */\n    const choice = box.querySelector('[data-credit-choice]');\n    if (choice && sig !== creditSig) {\n      choice.replaceChildren(...creditControl(options, available));\n    }\n    if (choice) creditControlState(choice, rule);\n\n    const eff = box.querySelector('[data-credit-effect]');\n    const off = box.querySelector('[data-credit-off]');\n    const left = box.querySelector('[data-credit-left]');\n    if (eff) {\n      if (chosen) {\n        if (off) off.textContent = creditMoney(-chosen.cents);\n        if (left) left.textContent = bagMoney(Math.max(0, available - chosen.cents));\n        eff.hidden = false;\n      } else {\n        eff.hidden = true;\n      }\n    }\n    /* THE RULE (owner, 2026-08-26: \"make it animate with like a line or\n       something\"). .is-on drives a scaleX hairline in nav-account.css, so\n       choosing an amount draws a line under the block and clearing it\n       retracts the same line.\n\n       It is set AFTER eff.hidden so the line and the sentence arrive on the\n       same frame; setting it first drew a line under nothing for one frame on\n       a slow phone. There is no rAF and no timer here on purpose - the class\n       is the whole animation, and CSS owns the timing so a reduced-motion\n       preference can turn it off in one place rather than in JS. */\n    box.classList.toggle('is-on', !!chosen);\n    box.hidden = false;\n  });\n  creditSig = sig;\n}\n\n/* [data-credit-tell] is the BAG's line and nothing else writes it. It used to\n   be named apart from a [data-credit-say] on the account panel, because on\n   /account both surfaces are in one document and a shared hook would have a\n   Checkout press wipe the sentence under the balance. That panel is gone now\n   (the ledger moved into .acct-pop--credit), so the collision it was avoiding\n   cannot happen - but the name stays: this hook means \"the bag has something\n   to tell you\", and widening it to every credit surface is how the collision\n   would come back. */\nfunction creditSay(words) {\n  document.querySelectorAll('[data-credit-tell]').forEach((el) => { el.textContent = words || ''; });\n}\n\n/* THE ONE MOMENT MONEY MOVES. Checkout is pressed, a rule is chosen, so ask\n   Jennie for the discount code and go to checkout carrying it.\n\n   IT CANNOT COST ANYBODY THEIR BAG. Every failure - a refusal, a timeout, a\n   dial turned off, a server in rehearsal mode - ends at the same place the\n   button was always going to end: /checkout, without the credit, with one\n   sentence saying so. The credit is untouched and is still there next time. */\nfunction creditCheckout(ev) {\n  const form = ev.target;\n  if (!form || form.tagName !== 'FORM') return;\n  /* Both bags post to the cart route: the drawer's .drawer__pay sits INSIDE\n     .drawer__foot, and the cart page's form WRAPS .cart-page__foot. Matching\n     on the action rather than on an ancestor is the one test that is true for\n     both, and it was a real bug the other way round. */\n  if (!/\\/cart(\\?|$)/.test(form.getAttribute('action') || '')) return;\n  const box = document.querySelector('[data-cart-credit]:not([hidden])');\n  if (!box) return;\n  /* \"Update cart\" is a submit on the same form and must pass straight through. */\n  const by = ev.submitter || document.activeElement;\n  if (!by || by.name !== 'checkout') return;\n\n  const rule = creditRule();\n  if (!(rule > 0)) {\n    /* Not using it this time. Clear any code minted earlier so it cannot ride\n       along, and let the ordinary checkout happen - not awaited, because a\n       housekeeping call must never stand between somebody and paying. */\n    creditCall('/api/credit/clear', {}).catch(() => {});\n    return;\n  }\n  if (creditPaying) { ev.preventDefault(); return; }\n\n  ev.preventDefault();\n  creditPaying = true;\n  creditSay('Putting your credit on the order.');\n  const btn = by;\n  btn.disabled = true;\n\n  /* The button STAYS disabled through this. The shopper is already on their\n     way out; re-enabling it would offer a second press into a navigation\n     that is a second away, and a double mint is the one thing worth avoiding\n     even when Flits is the side that failed. */\n  const give = (words) => {\n    creditSay(words);\n    /* A real navigation either way, which is what this sheet has always done.\n       form.submit() is deliberately NOT used: it would drop name=\"checkout\"\n       and update the cart instead of leaving it. */\n    setTimeout(() => { window.location.assign('/checkout'); }, 1200);\n  };\n\n  cartJson()\n    .then((cart) => creditCall('/api/credit/apply', { cart, rule_id: rule }))\n    .then((res) => {\n      if (res && res.ok === true && res.code) {\n        window.location.assign('/checkout?discount=' + encodeURIComponent(res.code));\n        return;\n      }\n      give('Your credit could not be attached this time, so we are taking you to checkout without it. Nothing has been used.');\n    })\n    .catch(() => {\n      give('We could not reach your credit just now, so we are taking you to checkout without it. Nothing has been used.');\n    });\n}\n\n/* ============================================================\n   Boot.\n\n   An MPA pays for its JavaScript on EVERY navigation, so the order here is\n   not cosmetic - it is the difference between a page that answers the first\n   tap and one that looks ready before it is. Three tiers:\n\n     critical  - anything that BINDS A HANDLER a shopper can fire, plus\n                 anything that would otherwise leave content hidden or\n                 visibly reflow. Runs now.\n     engine    - the search/layering surfaces, behind loadEngine().\n     idle      - passive: below the fold, or talking to a network. Nothing\n                 here owns a control.\n\n   The line between critical and idle is NOT \"above the fold\". It is \"does a\n   tap on this do nothing if it has not run yet\". Deferring a controller that\n   owns a button does not make the page faster, it makes the button dead for\n   up to a second, which reads as a slow site - and reads worse than the\n   milliseconds it saved. initPdpModals, initTips, initReadMore and initNotes\n   were briefly in `idle` for exactly that wrong reason. See\n   steering/12-performance-playbook.md \u00A73.\n   ============================================================ */\nconst onIdle = (fn) => (window.requestIdleCallback\n  ? requestIdleCallback(fn, { timeout: 1000 })\n  : setTimeout(fn, 200));\n\n/* Work with side effects - network calls, feeds, geo lookups - must not run\n   while a document is only being PRERENDERED, or a hover the shopper never\n   acted on shows up as a visit. Held until the page is genuinely theirs. */\nconst whenActive = (fn) => (document.prerendering\n  ? document.addEventListener('prerenderingchange', fn, { once: true })\n  : fn());\n\n/* DOMContentLoaded, or now if it has already fired. readyState cannot tell\n   the two apart - it reads 'interactive' from before the event until load -\n   so code that arrives in a chunk after the event and waits on it would wait\n   forever. This file is a module and always runs before the event, so it is\n   the one place that can know. */\nlet domReadyFired = document.readyState === 'complete';\nif (!domReadyFired) document.addEventListener('DOMContentLoaded', () => { domReadyFired = true; }, { once: true });\nfunction afterDomReady(fn) {\n  if (domReadyFired) fn();\n  else document.addEventListener('DOMContentLoaded', fn, { once: true });\n}\n\n/* ============================================================\n   The chunks (2026-09-11).\n\n   Every page used to download, parse and compile every controller in the\n   theme. Measured on the home page, about four fifths of this file was code\n   for a page the shopper was not on, and Lighthouse named it first under\n   unused JavaScript. The controllers that only one kind of page can use now\n   live in files of their own, and this file imports one only when that\n   page's hooks are in the document:\n\n     theme-rooms.js    the Duet and the Consultation   [data-duet], [data-finder]\n     theme-shelf.js    the collection filter + shelf   [data-cfilter]\n     theme-service.js  tracking, returns, stockists    [data-track] ...\n     theme-gift.js     the gift card composer          [data-giftcard]\n     theme-account.js  sign-in, register, dashboard    customers/* hooks\n     theme-later.js    delivery, reviews, order story  idle readers\n\n   Each boots in the tier it always did (the calls below say which), and the\n   layout modulepreloads the file on the templates that need it, so a control\n   binds as the page finishes parsing, not a round trip later.\n\n   THE URLS COME FROM THE LAYOUT (window.theme.chunks) because asset_url is\n   the only thing that knows the ?v= digest, and an asset fetched without it\n   is a cache entry nobody can bust. The sibling URL below is a fallback for\n   a page rendered by a layout that predates the list - the GitHub sync lands\n   files one at a time, so for a minute that page exists.\n\n   A chunk never imports this file: a URL without the digest would be a\n   second copy of it, booted twice. It exports mount(kit) instead, and\n   chunkKit() is everything the moved code still reads from here. JE is a\n   getter because loadEngine() fills it in; the engine-backed chunks are\n   mounted only after that promise settles.\n   ============================================================ */\nconst CHUNKS = cfg.chunks || {};\nconst chunkFetches = {};\nconst chunkMounts = {};\nlet chunkKitObj = null;\n\nfunction chunkKit() {\n  if (chunkKitObj) return chunkKitObj;\n  chunkKitObj = {\n    get JE() { return JE; },\n    CF_VIEW_KEY, FERA_API, FERA_PAGE, SAMPLE_PROPS, afterDomReady, armPairLight,\n    bagMoney, cartAdd, cartBottles, cartFail, closeChrome, creditCall, creditCfg,\n    creditMoney, cssAttr, dialogShow, dialogShut, feraCard, finePointer, juceIndex,\n    laneStep, nextCartTicket, onIdle, onResizeSettled, openDrawer, openDrawerConfirm,\n    reduceMotion, revCard, routes, setCartCount, setMoney, spotlightLine, srchMoney,\n    srchNorm, swapDrawer, syncInRoom, tagSet, toast, whenActive\n  };\n  return chunkKitObj;\n}\n\nfunction chunkUrl(name) {\n  if (CHUNKS[name]) return CHUNKS[name];\n  const self = document.querySelector('script[type=\"module\"][src*=\"/theme.js\"]');\n  return self ? self.src.replace(/\\/theme\\.js(\\?.*)?$/, '/theme-' + name + '.js') : '/assets/theme-' + name + '.js';\n}\n\n/* Starts the download; safe to call early and more than once. */\nfunction fetchChunk(name) {\n  if (!chunkFetches[name]) chunkFetches[name] = import(chunkUrl(name));\n  return chunkFetches[name];\n}\n\n/* Resolves to the chunk's controllers, or to null when it could not be had -\n   a chunk that fails costs its own page's controls, never the rest of boot. */\nfunction mountChunk(name) {\n  if (!chunkMounts[name]) {\n    chunkMounts[name] = fetchChunk(name)\n      .then((m) => m.mount(chunkKit()))\n      .catch((err) => {\n        console.warn('[juce] theme-' + name + '.js did not load', err);\n        return null;\n      });\n  }\n  return chunkMounts[name];\n}\n\n/* Import a chunk only when one of its hooks is on the page, then boot it. */\nfunction withChunk(name, hooks, boot) {\n  if (!document.querySelector(hooks)) return;\n  mountChunk(name).then((c) => { if (c) boot(c); });\n}\n\ndocument.documentElement.classList.replace('no-js', 'js');\n\n/* ---- critical ---- */\ninitHeadHeight();\ninitGlassSwitch(); initShine(); initReveal(); initSizes(); initQty(); initCart(); publishCartBridge();\ninitCartPage();\ninitLanes(); initLaneLoop(); initLaneAuto(); initLaneDrag(); initNav();\ninitDeck(); initShotDefer(); initShotSwap(); initShotSwipe(); initCardAdd();\n/* CRITICAL on both of this tier's grounds at once. It owns a control the\n   shopper presses (the bottle/leaf switch), and it decides which of a card's\n   two frames is the resting one - so held to idle it would let the whole wall\n   paint as bottles and then dissolve into scenes a beat later, which is the\n   visible reflow this tier exists to prevent. It reads one localStorage key,\n   talks to no network, and returns on its first line on every page that is not\n   a collection. */\ninitShotMode();\ninitPdpVariant(); initPdpHero(); initDockAdd();\ninitDockCommerce(); initDockSize(); initPdpBuyBar(); initMiniSwap(); initPairLight();\ninitOverlays(); syncInRoom(); initMenuSheet(); initSearch(); initJennie();\n/* The service pages and the gift card composer own controls, so they keep\n   this tier; they are imported first, and only where their hooks are. */\nwithChunk('service', '[data-track], [data-returns], [data-stockists], [data-trade]', (c) => {\n  c.initTrade(); c.initTrack(); c.initReturns(); c.initStockists();\n});\nwithChunk('gift', '[data-giftcard]', (c) => { c.initGiftCard(); });\ninitNotes(); initReadMore(); initPdpModals(); initTips(); initSortMenu();\n/* CRITICAL because it owns a link the shopper presses (\"Add an address\"\n   -> #new-address), not because it is above the fold. It no-ops instantly\n   on every page that has no <details> with an id. */\ninitAcctDisclose();\n/* CRITICAL, and it was wrong in idle. It owns three controls: the \"Forgot\n   your password\" dialog, the preference and shelf pills, and - the one that\n   made this a defect rather than a slow button - the register form's SUBMIT\n   handler, which is what carries the cookie consent and the Jennie profile\n   claim. At the idle tier a shopper who typed fast and pressed \"Create\n   account\" before requestIdleCallback fired got an account with no profile\n   and no cookie record, silently, for a window measured in hundreds of\n   milliseconds. It failed closed, which is the right direction and the only\n   reason this was not worse, but \"undisclosed and sometimes\" is not a\n   consent design. Same rule as initHomeReviews: the ONE network read it does\n   (the account page reading the profile back) is held behind onIdle +\n   whenActive inside the function, so this tier costs the page no network.\n   initAcctReturns is CRITICAL for the same reason, and not because it is\n   above the fold: it owns Withdraw and Change what happens, two controls a\n   shopper presses, and a control that does nothing for the first second is a\n   control that is broken. Its one network read is held behind onIdle +\n   whenActive inside the function, so this tier costs the page no network.\n   Both live in theme-account.js, imported only where their hooks are - the\n   sign-in, register and account templates, which modulepreload it. */\nwithChunk('account', '[data-recover-pop], [data-acct-prefs], [data-acct-returns]', (c) => {\n  c.initAcctAuth(); c.initAcctReturns();\n});\n/* CRITICAL, and for the one reason that matters in this tier: it binds the\n   Checkout button. A shopper who chose an amount of store credit and pressed\n   Checkout before an idle callback fired would go to checkout with none of it\n   applied, silently, and only find out on the payment screen. The select is\n   the other control it owns. Its network calls are inside refreshCartCredit,\n   which is a debounced quote, not a boot fetch, and it no-ops instantly on\n   every page with no bag control - which is every page for a guest. */\ninitCartCredit();\n/* initHomeReviews is CRITICAL because it binds \"Read all N reviews\" - it must\n   not be dead for the first second on the one band that band is for. Its two\n   fetches are held behind onIdle + whenActive inside the function, so being in\n   this tier costs the page no network at all. */\ninitHomeReviews();\ninitViewTransitions();\n\n/* ---- engine-backed ----\n   One call site, so the fetch cannot start by accident: a page that mounts a\n   search or layering surface asks for the brain immediately, every other page\n   waits for idle. Either way the inits run behind the same promise and no-op\n   when they find no DOM. initSearch is NOT in here - it binds the popup's\n   open control eagerly above and pulls the engine itself on first intent. */\nconst ENGINE_SURFACE = '[data-cfilter], [data-duet], [data-finder], [data-ritual][data-anchor], [data-pdp-layer][data-anchor]';\n/* The shelf and the rooms are chunks: fetched beside the engine, mounted after\n   it (they read JE at mount), and run in the same order as before. */\nconst bootEngineSurfaces = () => {\n  const shelf = document.querySelector('[data-cfilter]');\n  const rooms = document.querySelector('[data-duet], [data-finder]');\n  if (shelf) fetchChunk('shelf').catch(() => {});\n  if (rooms) fetchChunk('rooms').catch(() => {});\n  return loadEngine()\n    .then(() => Promise.all([shelf && mountChunk('shelf'), rooms && mountChunk('rooms')]))\n    .then(([s, r]) => {\n      if (s) { s.initCollectionFilter(); s.initShelfHead(); }\n      initRitualLane(); initPdpLayer();\n      if (r) { r.initDuet(); r.initScentFinder(); }\n      initDrawerPairs();\n    });\n};\n/* whenActive matters MORE here than anywhere else in this file. A PDP mounts\n   the ritual lane, so it counts as an engine surface - which means a\n   prerendered product page would otherwise fetch the 156KB brain AND the full\n   catalogue index while the shopper is still looking at the collection they\n   have not left yet. The speculation rule is deliberately conservative for the\n   same reason (layout/theme.liquid), but this is the belt to that braces. */\nwhenActive(() => {\n  if (document.querySelector(ENGINE_SURFACE)) bootEngineSurfaces();\n  else onIdle(bootEngineSurfaces);\n});\n\n/* ---- idle ----\n   Passive only. Nothing in here owns a control the shopper can press. */\nonIdle(() => {\n  initSeg();\n  withChunk('account', '[data-acct-recent], [data-credit-tile], [data-credit-pop], [data-acct-credit]', (c) => {\n    c.initAcctRecent(); c.initAcctCredit();\n  });\n  whenActive(() => {\n    withChunk('later', '[data-deliver], [data-rev-row], [data-layer-story]', (c) => {\n      c.initDeliver(); c.initPdpReviews(); c.initLayerStory();\n    });\n  });\n});\n"],
  "mappings": "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",
  "names": ["line", "pair", "s", "t"]
}
