Using products in other languages is especially valuable — it makes it easier to play and share meaningful experiences with international friends, helps you better understand each other’s cultures, and naturally supports language development.
, which is BEFORE
// window.Shopify.currency exists — an eager HUF gate here silently exits on
// every page load (live-verified 2026-08-16: the preview param never stuck).
// So: arm resolution and the settings flip happen now (they need no
// currency), and the HUF gate runs at DOMContentLoaded, reverting the flip
// for non-HUF visitors before the component initialises.
// ── Preview plumbing (sticky per session, excluded from bookkeeping) ──────
var preview = null;
try {
var m = window.location.search.match(/[?&]consentexp=([a-z]+)/);
if (m) {
if (m[1] === 'off') sessionStorage.removeItem('kvce_prev');
else sessionStorage.setItem('kvce_prev', m[1] === 'control' ? 'control' : 'treatment');
}
preview = sessionStorage.getItem('kvce_prev');
} catch (e) { /* storage blocked: preview simply unavailable */ }
if (MODE === 'preview' && !preview) return;
// ── Arm ───────────────────────────────────────────────────────────────────
var arm;
var bookkeeping = MODE === 'ab' && !preview;
if (preview) arm = preview;
else if (MODE === 'on') arm = 'treatment';
else {
try {
arm = localStorage.getItem('kv_consent_arm');
if (arm !== 'control' && arm !== 'treatment') {
arm = Math.random() < 0.5 ? 'control' : 'treatment';
localStorage.setItem('kv_consent_arm', arm);
}
} catch (e) {
// Storage blocked: unassignable and unmeasurable, so leave the default
// banner and stay out of the books rather than pollute an arm.
return;
}
}
function visitorId() {
try {
var v = localStorage.getItem('otto_visitor_id');
if (!v) {
v = window.crypto && crypto.randomUUID ? crypto.randomUUID() : 'v-' + Date.now() + '-' + Math.random().toString(16).slice(2);
localStorage.setItem('otto_visitor_id', v);
}
return v;
} catch (e) { return null; }
}
function track(name, idSuffix, extra) {
if (!bookkeeping) return;
var vid = visitorId();
if (!vid) return;
var data = { experimentKey: KEY, variant: arm };
if (extra) for (var k in extra) data[k] = extra[k];
try {
fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
keepalive: true,
body: JSON.stringify({ events: [{
id: KEY + ':' + vid + (idSuffix ? ':' + idSuffix : ''),
eventName: name, clientId: vid,
occurredAt: new Date().toISOString(), data: data
}] })
}).catch(function () {});
} catch (e) { /* bookkeeping must never break the page */ }
}
function dayKey() { return new Date().toISOString().slice(0, 10); }
// ── Treatment step 1: flip Pandectes' own modality settings pre-init ──────
// Their component snapshots window.pandectesBannerSettings at init; this
// snippet renders before
, which in practice runs first. If it does
// not, step 3's fallback covers the gap.
var flipped = false;
if (arm === 'treatment') {
try {
var s = window.pandectesBannerSettings;
if (s && s.theme) { s.theme.isModal = false; s.theme.scrollLock = false; flipped = true; }
} catch (e) { /* fall through to the CSS fallback */ }
}
function revertFlip() {
if (!flipped) return;
try {
var s2 = window.pandectesBannerSettings;
if (s2 && s2.theme) { s2.theme.isModal = true; s2.theme.scrollLock = true; }
} catch (e) {}
}
var COMPACT_CSS =
// Scoped to the FIRST-VISIT view only. Live-verified 2026-08-16 at
// 375x812: this store's first-visit view renders `.dialog.notice`
// (231px tall by default; the button row is `.actions`, the text is
// `.intro`/#message inside `.body`). This CSS measures 182px on the
// real banner with both buttons fully visible. `.banner` is kept as an
// alias in case a Pandectes config change renames the view. The
// preferences dialog (a different view class) keeps its full layout,
// so the granular consent choices render exactly as Pandectes designed
// them.
//
// DELIBERATELY NO max-height/overflow cap: a cap that guesses low clips
// the consent BUTTONS (caught live at 148px), and a banner whose accept
// and decline are unreachable is a compliance bug, not a compact layout.
// Height comes from shrinking the parts instead.
'.dialog.notice,.dialog.banner{padding:10px 14px 12px;}' +
'.dialog.notice .header,.dialog.banner .header{margin:0;padding:0;min-height:0;}' +
'.dialog.notice .header .title,.dialog.banner .header .title{font-size:14px;margin:0;}' +
'.dialog.notice #message,.dialog.banner #message{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:12px;line-height:1.35;}' +
'.dialog.notice .intro ul,.dialog.notice .details,.dialog.banner .intro ul,.dialog.banner .details{display:none;}' +
'.dialog.notice .actions,.dialog.banner .actions{display:flex;flex-wrap:nowrap;gap:8px;margin:8px 0 0;padding:0;}' +
'.dialog.notice .actions button,.dialog.banner .actions button{flex:1;padding:9px 8px;font-size:13px;min-height:0;margin:0;}' +
'.overlay{display:none!important;}';
var seenFired = false;
function onBannerAppears(host) {
// Treatment step 2: inject the compact stylesheet into the open shadow
// root. A separate node, so Pandectes' own #pandectes-cmp-custom-styles
// (their dashboard CSS) is never touched.
if (arm === 'treatment') {
try {
var root = host.shadowRoot;
if (root && !root.getElementById('kv-consent-compact')) {
var st = document.createElement('style');
st.id = 'kv-consent-compact';
st.textContent = COMPACT_CSS;
root.appendChild(st);
}
} catch (e) { /* selectors missed: default banner, fail open */ }
// Treatment step 3: if the settings flip lost the init race, the
// component may have locked body scroll before we got here. One
// restore + one delayed recheck — not an observer war.
var unlock = function () {
try {
if (getComputedStyle(document.body).overflow === 'hidden') document.body.style.overflow = 'visible';
if (getComputedStyle(document.documentElement).overflow === 'hidden') document.documentElement.style.overflow = 'visible';
} catch (e) {}
};
unlock();
setTimeout(unlock, 600);
}
if (!seenFired) {
seenFired = true;
track('experiment_exposure', '', null); // once per visitor (upsert on stable id)
track('consent_seen', 'seen-' + dayKey(), null); // daily denominator
}
}
// The component mounts asynchronously; watch for it rather than racing it.
// A visitor with stored consent never mounts a banner → observer sees
// nothing → no exposure, which is exactly the right denominator.
function watch() {
var existing = document.querySelector('pandectes-cmp');
if (existing) { onBannerAppears(existing); return; }
var mo = new MutationObserver(function () {
var el = document.querySelector('pandectes-cmp');
if (el) { mo.disconnect(); onBannerAppears(el); }
});
mo.observe(document.documentElement, { childList: true, subtree: true });
// A banner that has not mounted within a minute is not mounting on this
// page view. Stop observing rather than watching forever.
setTimeout(function () { mo.disconnect(); }, 60000);
}
// The deferred HUF gate: non-HU visitors get the flip reverted (before the
// component initialises, which also happens after DOMContentLoaded) and are
// never bucketed, watched, or stamped.
function boot() {
if (!(window.Shopify && window.Shopify.currency && window.Shopify.currency.active === 'HUF')) {
revertFlip();
return;
}
watch();
stampCart();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
else boot();
// ── Choice — Shopify's documented event, not Pandectes internals ──────────
document.addEventListener('visitorConsentCollected', function (e) {
var d = (e && e.detail && e.detail.customerPrivacy) || {};
track('consent_choice', 'choice-' + dayKey(), {
analytics: d.analyticsProcessingAllowed === true,
marketing: d.marketingAllowed === true
});
});
// ── Cart attribute stamp (both arms — a one-armed experiment cannot be read)
// Fired once per session when a cart exists. A cart created later on this
// same page view gets stamped on the next navigation, which every checkout
// path includes.
function stampCart() {
if (!bookkeeping) return;
try {
if (/(^|;\s*)cart=/.test(document.cookie) && !sessionStorage.getItem('kvce_stamped')) {
sessionStorage.setItem('kvce_stamped', '1');
fetch('/cart/update.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ attributes: { _experiment_consent: KEY + ':' + arm } })
}).catch(function () {
try { sessionStorage.removeItem('kvce_stamped'); } catch (e) {}
});
}
} catch (e) { /* never block the page on bookkeeping */ }
}
})();