{"company":{"model":{"version":3,"companyId":"6411fe8c6df9f10f76c9df53","imageViewedEventsEnabled":false,"analyticsEnabled":false,"matomoTrackerEnabled":false,"matomoTrackerFullEnabled":false,"ecommerceTrackerEnabled":false,"zsiteVanityUrlsEnabled":false,"heatmapsEnabled":false,"isUsingUuids":false,"isImageCroppingEnabled":true,"enableCNCReflowPreventionOutput":false,"enableImageLQIP":false,"textWidgetEnabled":false,"enhancedTextWidgetEnabled":true,"dynamicProductWidgetEnabled":false,"cloudinaryDefaultImageProvider":true,"adaTabOrderEnabled":true,"experiencePreRenderEnabled":false,"userPermissionsEnabled":false,"salesforceRecommendationsEnabled":false,"combinedGroupsSpeed":true,"imageErrorSampleRate":0,"imageCompressionQuality":85,"matomoTrackingUrl":"http://","shareWithAllByDefault":true,"cloudinaryImageAndDprOverride":"q_auto","state":"Active"}},"viewer":{"experiences":{"C3_75fdddc8-9242-8132-8003-4da81b964311_a0b080d0-9fcb-804e-8003-4c2287f5bfee":{"currentSceneNumber":1,"transitionDirection":0,"model":{"companyId":"6411fe8c6df9f10f76c9df53","createdAt":1704927957958,"modifiedAt":1704927957958,"autoPlayDelay":0,"type":"Experience","language":"en-US","id":"C3_75fdddc8-9242-8132-8003-4da81b964311_a0b080d0-9fcb-804e-8003-4c2287f5bfee","name":"Desktop","width":1440,"height":570,"transition":"none","scenes":[{"id":"2bcb0d15-cef4-8031-8003-4c688dc0c6ce","sceneIndex":0,"sceneNumber":"1","widgets":[{"uuid":"2bcb0d15-cef4-8031-8003-4c6c614b9ce1","title":"Copy","left":11.11111111111112,"top":75.78947368421052,"width":77.77777777777779,"rotation":0,"layerIndex":0,"customClasses":[null],"editor":{"type":"text"},"type":"text-enhanced","effects":[],"animations":[],"fontFamilies":[{"fontProvider":"custom","fontFamily":"VolkoGrot Light","fontStyle":"normal","fontWeight":"300","cssFamilyNames":"VolkoGrot Light","fontMetadata":{"customFontId":"custom-ac60ec2b-e895-805a-8002-c2434ecdb64e_normal-300","format":"woff","url":"https://images.getfastr.com/22/44/d69c107540c7b89f9b2b0a119805.woff"}}],"paragraphRuns":[{"styles":["text-align: center","line-height: 1.2"],"textRuns":[{"dataAttributes":["data-font-family=\"VolkoGrot Light\"","data-font-style=\"normal\"","data-font-weight=\"300\"","data-font-provider=\"custom\"","data-css-family-names=\"VolkoGrot Light\"","style=\"\""],"styles":["font-size: 18px","line-height: 1.2","font-weight: 300","font-family: &quot;VolkoGrot Light&quot;","letter-spacing: 0px","color: #375481","opacity: 1"],"text":"Say hello to SPF with no pilling, no heaviness and no white cast. The award-winning formula is completely invisible, weightless and scentless, providing oil-free, broad spectrum protection and supporting visibly healthier skin. Plus, it’s formulated without oxybenzone and parabens, and it’s cruelty-free, non-irritating and vegan. That’s how Supergoop! makes the magic happen. "}],"headerLevel":null}],"text":"<div style=\"text-align: center;line-height: 1.2\">\n                    <span data-font-family=\"VolkoGrot Light\" data-font-style=\"normal\" data-font-weight=\"300\" data-font-provider=\"custom\" data-css-family-names=\"VolkoGrot Light\" style=\"\"><span style=\"opacity: 1\"><span style=\"color: #375481\"><span style=\"letter-spacing: 0px\"><span style=\"font-family: &quot;VolkoGrot Light&quot;\"><span style=\"font-weight: 300\"><span style=\"line-height: 1.2\"><span style=\"font-size: 18px\">Say hello to SPF with no pilling, no heaviness and no white cast. The award-winning formula is completely invisible, weightless and scentless, providing oil-free, broad spectrum protection and supporting visibly healthier skin. Plus, it’s formulated without oxybenzone and parabens, and it’s cruelty-free, non-irritating and vegan. That’s how Supergoop! makes the magic happen. </span></span></span></span></span></span></span></span></div>"},{"uuid":"2bcb0d15-cef4-8031-8003-4c6c614b9ce0","title":"Title","left":11.11111111111111,"top":66.91772534785238,"width":77.77777777777779,"rotation":0,"layerIndex":1,"customClasses":[null],"editor":{"type":"text"},"type":"text-enhanced","effects":[],"animations":[],"fontFamilies":[{"fontProvider":"custom","fontFamily":"VolkoGrot Regular","fontStyle":"normal","fontWeight":"400","cssFamilyNames":"VolkoGrot Regular","fontMetadata":{"customFontId":"custom-ac60ec2b-e895-805a-8002-c243bdfbdf20_normal-400","format":"woff","url":"https://images.getfastr.com/4f/6d/807b899444398e56e9739427d7ef.woff"}}],"paragraphRuns":[{"styles":["text-align: center","line-height: 1.2"],"textRuns":[{"dataAttributes":["data-font-family=\"VolkoGrot Regular\"","data-font-style=\"normal\"","data-font-weight=\"400\"","data-font-provider=\"custom\"","data-css-family-names=\"VolkoGrot Regular\"","style=\"\""],"styles":["font-size: 30px","line-height: 1.2","font-weight: 400","font-family: &quot;VolkoGrot Regular&quot;","letter-spacing: 0px","color: #142745","opacity: 1"],"text":"The key to healthy skin all year long"}]}],"text":"<div style=\"text-align: center;line-height: 1.2\">\n                    <span data-font-family=\"VolkoGrot Regular\" data-font-style=\"normal\" data-font-weight=\"400\" data-font-provider=\"custom\" data-css-family-names=\"VolkoGrot Regular\" style=\"\"><span style=\"opacity: 1\"><span style=\"color: #142745\"><span style=\"letter-spacing: 0px\"><span style=\"font-family: &quot;VolkoGrot Regular&quot;\"><span style=\"font-weight: 400\"><span style=\"line-height: 1.2\"><span style=\"font-size: 30px\">The key to healthy skin all year long</span></span></span></span></span></span></span></span></div>"},{"uuid":"d3b2debf-0c1a-8001-8003-4da90adc8be3","title":"Group 29770.png","left":11.11111111111111,"top":8.771929824561402,"width":77.77777777777779,"height":51.92982456140351,"rotation":0,"layerIndex":2,"customClasses":[null],"editor":{"type":"image"},"imageId":"6bb5d8c7-39e9-8153-8003-4da90ad1af74","src":"https://images.getfastr.com/c9/03/357f46bc4978a243c03aaea3cb6f.png","sourceType":"Static","originalImageWidth":2270,"originalImageHeight":600,"type":"image","animations":[],"altText":"Group 29770.png","effects":[]}],"widgetTabOrder":["2bcb0d15-cef4-8031-8003-4c6c614b9ce1","2bcb0d15-cef4-8031-8003-4c6c614b9ce0","d3b2debf-0c1a-8001-8003-4da90adc8be3"],"removedWidgetTabOrder":[],"customClasses":[null]}],"threshold":993,"backgroundScene":{"sceneNumber":"background","widgets":[],"widgetTabOrder":[],"removedWidgetTabOrder":[]},"overlayScene":{"sceneNumber":"overlay","widgets":[],"widgetTabOrder":[],"removedWidgetTabOrder":[]},"backdrop":{"type":"color","fit":"Experience","color":"#f5f5f5","image":null},"behaviors":[{"steps":[{"id":"","steps":[],"data":{"loadType":"executeJs","details":{"jsSnippet":"\n(function() {\n    window.FASTR_FRONTEND.experiences[_experienceId].tamperLibrary = {\n        setImageUrl: async function setImage(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    const sourceData = integrationResults.get(source);\n\n    if (!sourceData) {\n        debug.error(`No data found for source ${source}`);\n    }\n\n    // Replace image-widget in SSR body\n    let widgetEl = domRef.querySelector(`div[id$='${widget.uuid}'] img`);\n\n    if (widgetEl) {\n        widgetEl.removeAttribute('srcset');\n        widgetEl.style.setProperty('object-fit', 'contain');\n        widgetEl.src = sourceData;\n\n        // Replace in the model\n        if (widget['src']) {\n            widget.src = sourceData;\n            widget.changeSource = \"behavior\";\n        }\n    } else {\n\n        // Replace image in svg SSR body\n        widgetEl = domRef.querySelector(`div[id$='${widget.uuid}'] svg`);\n\n        if (widgetEl) {\n            let imageEl = widgetEl.querySelector('image');\n            imageEl.setAttribute('href', sourceData);\n            imageEl.setAttribute('preserveAspectRatio', 'xMidYMid meet'); // This will ensure proportional scaling of the image\n\n            // Replace in the model\n            widget.svgContent = widgetEl.innerHTML;\n        }\n    }\n}\n,\n        setHref: async function setOnclick(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    const url = integrationResults.get(source);\n\n    // Replace in SSR body\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}'] a`);\n    if (widgetEl) {\n        widgetEl.setAttribute('href', url);\n    }\n\n    // Replace in the model\n    widget[\"action\"] = widget[\"action\"] || [];\n    widget[\"action\"].type = \"link\";\n    widget[\"action\"].url = url;\n}\n,\n        setSceneTarget: async function setOnclick(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    // Replace in the model\n\n    // TODO: This sets the scene click target\n}\n,\n        setText: async function setText(integrationResults, widget, args) {\n\n    const getInnerHTML = function(node) {\n        // The nodes we're creating are actually nested 2 deep\n        if (node.children[0] && node.children[0].children[0]) {\n            return node.children[0].children[0].innerHTML;\n        }\n\n        debug.error('Failed to find generated replacement text in node', node);\n        return \"\";\n    }\n\n    const replaceInTextNodes = function(node, searchText, replacementText, modifyOriginal = false) {\n        let tempNode;\n        if (typeof node === 'string') {\n            // We're working with the widget model\n            let tempDiv = document.createElement('div');\n            tempDiv.innerHTML = node;\n            tempNode = tempDiv;\n        } else {\n            // We're working with an actual DOM node\n            tempNode = modifyOriginal ? node : node.cloneNode(true);\n        }\n    \n        let nodeList = [];\n        let walker = document.createTreeWalker(tempNode, NodeFilter.SHOW_TEXT);\n        while (walker.nextNode()) nodeList.push(walker.currentNode);\n    \n        for (let textNode of nodeList) {\n            textNode.nodeValue = textNode.nodeValue.replaceAll(searchText, replacementText);\n        }\n    \n        return (typeof node === 'string') ? getInnerHTML(tempNode) : tempNode;\n    }\n\n    debug.breakpoint();\n\n    const { source, placeholder } = args;\n\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}']`);\n\n    const sourceValue = integrationResults.get(source);\n\n    if (!sourceValue) {\n        debug.log(`No match found in integration results for ${source} for widget ${widget.uuid}`);\n        return;\n    }\n\n    if (placeholder) {\n        // Replace in the model\n        if (widget[\"text\"]) {\n            widget[\"text\"] = replaceInTextNodes(widget[\"text\"], placeholder, sourceValue);\n        }\n\n        // Replace in SSR body\n        replaceInTextNodes(widgetEl, placeholder, sourceValue, true);\n        \n    } else {\n        // Replace in the model\n        if (widget[\"text\"]) {\n            widget[\"text\"] = replaceInTextNodes(widget[\"text\"], widgetEl.textContent.trim(), sourceValue);\n        }\n\n        // Replace in SSR body\n        replaceInTextNodes(widgetEl, widgetEl.textContent.trim(), sourceValue, true);\n    }\n\n    widget.text = getInnerHTML(widgetEl);\n},\n        hideGroup: \nasync function hideGroup(_, __, args) {\n\n    debug.breakpoint();\n\n    const { placeholder } = args;\n\n    const widgets = placeholder.split(',');\n\n    for (const widgetUuid of widgets) {\n        const { widget } = findWidgetByUuid(_model, widgetUuid);\n        this.hideWidget(_, widget);\n    }\n    \n}\n,\n        hideWidget: \nasync function hideWidget(_, widget) {\n\n    debug.breakpoint();\n\n    const widgetUuuid = widget.uuid;\n    // Replace in SSR body\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}']`);\n    widgetEl.style.opacity = 0\n    widgetEl.style.display = 'none';\n\n    // Replace in the model\n    widget['opacity'] = \"0\";\n    \n}\n,\n        setAltText: \nasync function setAltText(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    const txt = integrationResults.get(source);\n\n    // Replace in SSR body\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}']`);\n    widgetEl.setAttribute('aria-label', txt);\n\n    // If this is an image widget, also set the alt text\n    const img = widgetEl.querySelector('img');\n    if (img) {\n        img.setAttribute('alt', txt);\n    }\n\n    // Replace in the model\n    widget['alt'] = txt;\n    \n}\n,\n    };\n    window.FASTR_FRONTEND.experiences[_experienceId].conditionFunction = async function evaluateConditions(integrationResults, conditions) {\n    debug.breakpoint();\n\n    try {\n        for (let i = 0; i < conditions.length; i++) {\n            const condition = conditions[i];\n            const source = integrationResults.get(condition.lefthandPrefix);\n\n            const isBooleanComparison = condition.comparator === \"isNot\" || condition.comparator === \"is\";\n\n            let comparison;\n            if (condition.righthandType === \"static\") {\n\n                // If this is a static boolean comparison, we are comparing to \"true\".  It's only is or is-not that dictates the result.\n                if (isBooleanComparison) {\n                    comparison = true;\n                } else {\n                    comparison = condition.righthandValue;\n                }\n\n            } else {\n                comparison = integrationResults.get(condition.righthandPrefix);\n            }\n\n            let comparisonNumber = Number(comparison);\n\n            if (!isBooleanComparison) {\n                if (!isNaN(comparisonNumber)) {\n                    comparison = comparisonNumber;\n                } else if ((comparison.startsWith(`\"`) && comparison.endsWith(`\"`)) || (comparison.startsWith(`'`) && comparison.endsWith(`'`))) {\n                    comparison = comparison.slice(1, -1);\n\n                    // Try converting to a number, if not possible use it as a string\n                    const comparisonNumber = Number(comparison);\n                    if (!isNaN(comparisonNumber)) {\n                        comparison = comparisonNumber;\n                    }\n                }\n            }\n\n            const comparator = condition.comparator;\n\n            const comparators = {\n                \"<\": (a, b) => a < b,\n                \"<=\": (a, b) => a <= b,\n                \"==\": (a, b) => a == b,\n                \">=\": (a, b) => a >= b,\n                \">\": (a, b) => a > b,\n                \"!=\": (a, b) => a != b,\n                equals: (a, b) => a === b,\n                doesNotEqual: (a, b) => a !== b,\n                includes: (a, b) => a.includes(b),\n                doesNotContain: (a, b) => !a.includes(b),\n                startsWith: (a, b) => a.startsWith(b),\n                endsWith: (a, b) => a.endsWith(b),\n                is: (a, b) => a == b,\n                isNot: (a, b) => a != b,\n            };\n\n            const comparisonFunction = comparators[comparator];\n            if (!comparisonFunction) throw new Error(`Invalid comparator: ${comparator}`);\n\n            debug.log(`Comparing: ${source} ${comparator} ${comparison}`);\n\n            const result = comparisonFunction(source, comparison);\n\n            debug.log(`The result of the comparison is ${result}`);\n\n            if (!result) return false;\n        }\n\n        return true;\n    } catch (error) {\n        console.log(`Failed to evaluate conditions: ${error}`);\n    }\n\n    return false;\n}\n;\n\n    modelModificationConfigs.forEach((config) => {\n        try {\n            if (config.gatherInputs) {\n                config.gatherInputs = eval(\"(\" + config.gatherInputs + \")()\");\n            }\n            config.integrations.forEach((integration) => {\n                integration.processInputs = eval(\"(\" + integration.processInputs + \")()\");\n                integration.processFunction = eval(\"(\" + integrations[integration.processFunction] + \")()\");\n                integration.processOutputs = eval(\"(\" + integration.processOutputs + \")()\");\n            });\n        } catch (e) {\n            debug.error(\"Error setting up integration functions\");\n            debug.error(e);\n        }\n\n        window.FASTR_FRONTEND.experiences[_experienceId].registerModelTamperFunction(config);\n    });\n\n})();\n","jsSnippetMetaData":"\nconst _experienceId = \"BEHAVIOR_EXPERIENCEID_PLACEHOLDER\";\n\nconst lastUnderscoreIndex = _experienceId.lastIndexOf(\"_\");\n\nconst experienceTypeId = _experienceId.substring(0, lastUnderscoreIndex);\nconst experienceOrdinal = _experienceId.substring(lastUnderscoreIndex + 1);\n\nlet _model = undefined;\n\nwindow.ZMAGS_API = window.ZMAGS_API || { experiences: {} };\nwindow.ZMAGS_API.experiences[_experienceId] = window.ZMAGS_API.experiences[_experienceId] || {};\n\ntry {\n    window.FASTR_FRONTEND = window.FASTR_FRONTEND || {};\n    window.FASTR_FRONTEND.experiences = window.FASTR_FRONTEND.experiences || {};\n    window.FASTR_FRONTEND.experiences[_experienceId] = window.FASTR_FRONTEND.experiences[_experienceId] || {};\n\n    // Pull properties from ZMAGS_API to FASTR_FRONTEND.\n    // NOTE: this sets us up to deprecate ZMAGS_API from viewer in the future\n    for (const prop in window.ZMAGS_API.experiences[_experienceId]) {\n        if (!window.FASTR_FRONTEND.experiences[_experienceId].hasOwnProperty(prop)) {\n            window.FASTR_FRONTEND.experiences[_experienceId][prop] = window.ZMAGS_API.experiences[_experienceId][prop];\n        }\n    }\n} catch (error) {\n    console.error(error);\n}\n\nconst domRef = document.querySelector(`div[data-experience='${experienceTypeId}'][data-ordinal='${experienceOrdinal}']`);\n\nconst getFASTR_FRONTEND_DEBUG = () => {\n    try {\n        return JSON.parse(localStorage.getItem(\"FASTR_FRONTEND_DEBUG\"));\n    } catch (error) {\n        console.warn(\"Error parsing FASTR_FRONTEND_DEBUG from localStorage:\", error);\n        return {};\n    }\n};\n\nconst FASTR_FRONTEND_DEBUG = getFASTR_FRONTEND_DEBUG();\n\nconst debug = {\n    output: (type, message, ...optionalParams) => {\n        if (FASTR_FRONTEND_DEBUG && FASTR_FRONTEND_DEBUG.trace) {\n            console[type](`[FASTR_FRONTEND_DEBUG] ${message}`, ...optionalParams);\n        }\n    },\n    log: (message, ...optionalParams) => {\n        debug.output(\"log\", message, ...optionalParams);\n    },\n    error: (message, ...optionalParams) => {\n        debug.output(\"error\", message, ...optionalParams);\n    },\n    breakpoint: () => {\n        if (FASTR_FRONTEND_DEBUG && FASTR_FRONTEND_DEBUG.break) {\n            debugger;\n        }\n    },\n};\n\nwindow.FASTR_FRONTEND.experiences[_experienceId].dumpState = () => {\n    const experienceIdentifier = _experienceId.split(\"_\").slice(0, -1).join(\"_\");\n    const model = window[_experienceId + \"_STATE\"].viewer.experiences[experienceIdentifier].model;\n    const modelSizeInKB = JSON.stringify(model).length / 1024;\n    console.log(`Model size for experience ${_experienceId}: ${modelSizeInKB.toFixed(2)} KB`);\n};\n\n\nconst modelModificationConfigs = [];\n\nconst backgroundIntegrations = [\n   \"// Create a style element\\nvar style = document.createElement('style');\\n\\n// Set the CSS content\\nstyle.textContent = '@media (min-width: 992px) {\\\\n' +\\n    '.col-lg-9 {\\\\n' +\\n    '    flex: 0 0 100%;\\\\n' +\\n    '    max-width: 100%;\\\\n' +\\n    '}\\\\n' +\\n    '}';\\n\\nif(document.querySelector('.product-breadcrumb.c-product-breadcrumb')){\\n    // Append the style element to the document head\\n    document.head.appendChild(style);\\n}\\n\\n\"\n];\n\nconst functionIntegrations = {};\n\nconst integrations = {};\n\nwindow.FASTR_FRONTEND = window.FASTR_FRONTEND || {};\nwindow.FASTR_FRONTEND.experiences = window.FASTR_FRONTEND.experiences || {};\n\nwindow.FASTR_FRONTEND.experiences[_experienceId] = {\n    initialTimestamp: performance.now(),\n    memoCache: new Map(),\n    pendingCache: new Map(),\n    performanceCounters: {\n        memoizedFunctions: 0,\n        cacheHits: 0,\n        cacheMisses: 0,\n        callDurations: [],\n        keyMapping: {},\n    },\n    hashFnv32a: function (str, seed = 0x811c9dc5) {\n        let h = seed;\n        for (let i = 0; i < str.length; i++) {\n            h ^= str.charCodeAt(i);\n            h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);\n        }\n        return h >>> 0;\n    },\n    memoize: function (fn) {\n        this.performanceCounters.memoizedFunctions++;\n        debug.log(`Memoized function created. Total: ${this.performanceCounters.memoizedFunctions}`);\n\n        return async (...args) => {\n            const fnString = fn.toString();\n            const key = JSON.stringify({ fnString, args });\n            const hashedKey = this.hashFnv32a(key);\n            const start = performance.now();\n\n            this.performanceCounters.keyMapping[hashedKey] = key;\n\n            if (this.memoCache.has(hashedKey)) {\n                this.performanceCounters.cacheHits++;\n                const end = performance.now();\n                debug.log(`Cache hit for key ${hashedKey}. Total hits: ${this.performanceCounters.cacheHits}`);\n                this.performanceCounters.callDurations.push({\n                    key: hashedKey,\n                    type: \"hit\",\n                    duration: end - start,\n                    start,\n                });\n                return this.memoCache.get(hashedKey);\n            }\n\n            if (this.pendingCache.has(hashedKey)) {\n                this.performanceCounters.waitingCalls++;\n                const result = await this.pendingCache.get(hashedKey);\n                this.performanceCounters.cacheHits++;\n                debug.log(`Pending cache hit for key ${hashedKey}. Total hits: ${this.performanceCounters.cacheHits}`);\n                const end = performance.now();\n                this.performanceCounters.callDurations.push({\n                    key: hashedKey,\n                    type: \"awaiting cache\",\n                    duration: end - start,\n                    start,\n                });\n                return result;\n            }\n\n            this.performanceCounters.cacheMisses++;\n            debug.log(`Cache miss for key ${hashedKey}. Total misses: ${this.performanceCounters.cacheMisses}`);\n            const promise = fn(...args);\n            this.pendingCache.set(hashedKey, promise);\n            const result = await promise;\n            this.memoCache.set(hashedKey, result);\n            this.pendingCache.delete(hashedKey);\n\n            const end = performance.now();\n            this.performanceCounters.totalDuration += end - start;\n            debug.log(`Memoized function ${fn.name} took ${end - start} milliseconds to execute. Total duration: ${this.performanceCounters.totalDuration}`);\n            this.performanceCounters.callDurations.push({\n                key: hashedKey,\n                type: \"miss\",\n                duration: end - start,\n                start,\n            });\n            return result;\n        };\n    },\n    runPerformanceTrace: function () {\n        const { callDurations, keyMapping } = this.performanceCounters;\n\n        const sortedDurations = callDurations.sort((a, b) => a.start - b.start);\n\n        sortedDurations.forEach(({ key, type, duration, start }) => {\n            const { fnString, args } = JSON.parse(keyMapping[key]);\n\n            let strippedFnString = fnString.replace(/async\\s+function\\s*\\(\\s*input\\s*\\)\\s*\\{\\s*/, \"\").replace(/\\n/g, \"\");\n\n            const shortenedFnString = strippedFnString.length > 90 ? strippedFnString.substring(0, 87) + \"...\" : strippedFnString;\n            const argsString = JSON.stringify(args).replace(/\\n/g, \"\");\n            const shortenedArgsString = argsString.length > 90 ? argsString.substring(0, 87) + \"...\" : argsString;\n\n            const fullKey = `Body: [${shortenedFnString}] Args: [${shortenedArgsString}]`;\n\n            const elapsedTime = start - this.initialTimestamp;\n            const minutes = Math.floor(elapsedTime / 60000);\n            const seconds = ((elapsedTime % 60000) / 1000).toFixed(3);\n            console.log(`[${minutes}:${seconds}] [${type}] [${key}] ${fullKey}: ${Math.round(duration)}ms`);\n        });\n    },\n    processGivenInputs: async function (inputFn) {\n        const memoizedProcessInputs = this.memoize(inputFn);\n        debug.log(\"Gathering inputs with :\", inputFn);\n        const generatedInput = await memoizedProcessInputs();\n        return generatedInput;\n    },\n    processIntegrationConfig: async function (inputJSON, processInputsFn, processFunctionFn, processOutputsFn) {\n        const memoizedProcessInputs = this.memoize(processInputsFn);\n        const memoizedProcessFunction = this.memoize(processFunctionFn);\n        const memoizedProcessOutputs = this.memoize(processOutputsFn);\n\n        debug.log(\"Processing inputs with inputJSON:\", inputJSON);\n        const processedInput = await memoizedProcessInputs(inputJSON);\n\n        debug.log(\"Executing process function with processedInput:\", processedInput);\n        const output = await memoizedProcessFunction(processedInput);\n\n        debug.log(\"Processing outputs with output:\", output);\n        const outputJSON = await memoizedProcessOutputs(output);\n\n        debug.log(\"Returning outputJSON:\", outputJSON);\n        return outputJSON;\n    },\n    runBackgroundIntegrations: async function () {\n        debug.breakpoint();\n\n        for (const integration of backgroundIntegrations) {\n            const body = integration;\n            try {\n                const func = new Function(\"model\", body);\n                await func(_model);\n            } catch (error) {\n                debug.error(\"Failed to run background integration\");\n                debug.error(JSON.stringify(body));\n                debug.error(error);\n            }\n        }\n    },\n    customActionHandler: async function (integrationId, args) {\n        debug.breakpoint();\n\n        const fn = functionIntegrations[integrationId];\n\n        const wrappedFn =\n`(async function customAction(inputs, model) {\n    ${fn}\n})(inputs, model);`;\n\n        // Flatten our inputs down to a simple object\n        const inputs = args ? Object.fromEntries(args.map(({name, value}) => [name, value])) : {};\n\n        try {\n            const func = new Function(\"inputs\", \"model\", wrappedFn);\n            await func(inputs, _model);\n        } catch (error) {\n            debug.error(\"Failed to run custom action integration\");\n            debug.error(JSON.stringify(fn));\n            debug.error(error);\n        }\n    },\n};\n\nwindow.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions = window.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions || [];\n\n// Called once _model is guaranteed to be set\nwindow.FASTR_FRONTEND.experiences[_experienceId].experienceInit = async () => {\n    if (window.FASTR_FRONTEND.experiences[_experienceId].experienceInitCalled) return;\n\n    window.FASTR_FRONTEND.experiences[_experienceId].experienceInitCalled = true;\n\n    window.FASTR_FRONTEND.experiences[_experienceId].runBackgroundIntegrations();\n\n    window.FASTR_FRONTEND.experiences[_experienceId].getCurrentScene = () => {\n        // If we're in pre-init, return scene 1\n        return window[_experienceId + \"_STATE\"] ? window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].currentSceneNumber : 1;\n    };\n\n    window.FASTR_FRONTEND.experiences[_experienceId].setCurrentScene = (sceneNumber) => {\n        window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].currentSceneNumber = sceneNumber;\n    };\n\n    window.FASTR_FRONTEND.experiences[_experienceId].reassertModel = () => {\n        window.ZMAGS_API.experiences[_experienceId].updateStateCallback(async () => {\n            return modelTamperPipeline(_model);\n        });\n    };\n\n    // Walk the model and find any custom actions or overlays that need to be updated\n    function updateWidgetActions(widgets) {\n        for (let widget of widgets) {\n            if (widget.action) {\n                if (widget.action.type === \"js\" && widget.action.code.startsWith(\"window.FASTR_FRONTEND.experiences[_experienceId].customActionHandler\")) {\n                    widget.action.code = widget.action.code.replace(\"_experienceId\", `\"${_experienceId}\"`);\n                } else if (widget.action.type === \"toggleOverlay\") {\n                    debugger;\n                    const widgetDomElement = domRef.querySelector(`div[id$='${widget.uuid}']`);\n\n                    let { parent, index } = findWidgetByUuid(_model, widget.uuid);\n\n                    const overlayResults = findWidgetByUuid(_model, widget.action.target);\n\n                    const overlayTarget = overlayResults.widget;\n\n                    function traverseAndUpdateWidgets(widgetList, originalWidget, parent, index) {\n                        for (const widget of widgetList) {\n                            // If widget type is composite, traverse its group\n                            if (widget.type === \"composite\" && widget.group) {\n                                traverseAndUpdateWidgets(widget.group, originalWidget, parent, index);\n                            } else {\n                                // Adjust the top and left properties of each widget\n                                widget.top += originalWidget.top;\n                                widget.left += originalWidget.left;\n\n                                // Add the widget to the parent\n                                parent.children.splice(index, 0, widget);\n                                index++;\n                            }\n                        }\n                    }\n\n                    function traverseAndAssignHandlers(widgetList, handler) {\n                        const observer = new MutationObserver((mutationsList) => {\n                            for (let mutation of mutationsList) {\n                                // Check the addedNodes property\n                                if (mutation.addedNodes) {\n                                    mutation.addedNodes.forEach((node) => {\n                                        if (node.id) {\n                                            widgetList.forEach((widget) => {\n                                                if (node.id.endsWith(widget.uuid)) {\n                                                    node.onmouseleave = handler;\n                                                }\n                                            });\n                                        }\n                                    });\n                                }\n                            }\n                        });\n\n                        // Start observing the document with the configured parameters\n                        observer.observe(domRef, { childList: true, subtree: true });\n\n                        for (const widget of widgetList) {\n                            // If widget type is composite, traverse its group\n                            if (widget.type === \"composite\" && widget.group) {\n                                traverseAndAssignHandlers(widget.group, handler);\n                            }\n                        }\n                    }\n\n                    if (widgetDomElement) {\n                        if (widget.action.triggerType === \"onHover\") {\n                            const targetLeave = () => {\n                                // Retrieve the original widget from the saved data\n                                const originalWidget = JSON.parse(widgetDomElement.dataset.originalWidget);\n\n                                // Remove overlay widgets from the parent\n                                if (parent && index !== null) {\n                                    parent.children.splice(index - overlayTarget.widgets.length, overlayTarget.widgets.length);\n                                }\n\n                                // Restore the original widget to its original position\n                                if (parent) {\n                                    parent.children.splice(index - overlayTarget.widgets.length, 0, originalWidget);\n                                }\n\n                                // Adjust the top and left properties of each child in overlayTarget's widgets array\n                                if (overlayTarget && overlayTarget.widgets) {\n                                    overlayTarget.widgets.forEach((overlayWidget) => {\n                                        overlayWidget.top += originalWidget.top;\n                                        overlayWidget.left += originalWidget.left;\n                                    });\n                                }\n\n                                // Reassert\n                                window.FASTR_FRONTEND.experiences[_experienceId].reassertModel();\n                            };\n\n                            widgetDomElement.onmouseenter = () => {\n                                // Keep a copy of the original widget\n                                const originalWidget = { ...widget };\n\n                                // Remove the widget from its parent\n                                if (parent && index !== null) {\n                                    parent.children.splice(index, 1);\n                                }\n\n                                // Swap all the elements of the widgets array in overlayTarget\n                                // as children of the original widget's parent\n                                if (parent && overlayTarget && overlayTarget.widgets) {\n                                    traverseAndUpdateWidgets(overlayTarget.widgets, originalWidget, parent, index);\n                                }\n\n                                // Save the original widget somewhere so we can restore it later\n                                widgetDomElement.dataset.originalWidget = JSON.stringify(originalWidget);\n\n                                // Reassert\n                                window.FASTR_FRONTEND.experiences[_experienceId].reassertModel();\n\n                                // Loop over widgets again to assign onmouseleave event handlers\n                                if (overlayTarget && overlayTarget.widgets) {\n                                    traverseAndAssignHandlers(overlayTarget.widgets, targetLeave);\n                                }\n                            };\n                        }\n                    }\n                }\n            }\n\n            if (widget.type === \"composite\") {\n                updateWidgetActions(widget.group);\n            }\n        }\n    }\n\n    // Start by iterating over scenes and calling the function on each scene's widgets\n    _model.scenes.forEach((scene) => updateWidgetActions(scene.widgets));\n};\n\nconst modelTamperPipeline = async (model) => {\n    if (model.id !== experienceId) return model;\n\n    // Set global _model for this experience\n    _model = model;\n\n    debug.breakpoint();\n\n    await window.FASTR_FRONTEND.experiences[_experienceId].experienceInit();\n\n    debug.log(\"Running model tamper pipeline for experience:\", experienceId);\n\n    for (const tamperFunction of window.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions) {\n        // NOTE: tamperFunction must be run async to allow for async integrations.  The tamperFunctions are themselves responsible for\n        // updating both the model and SSR, and tamperFunctions must also be assumed orderless.\n        tamperFunction(model);\n    }\n\n    return model;\n};\n\n// If the init.js hasn't loaded the experience state yet, set this callback which will be run before the initial render\nconst modelCallbacks = window.FASTR_FRONTEND.experiences[_experienceId].modelCallbacks || window.ZMAGS_API.experiences[_experienceId].modelCallbacks || [];\nmodelCallbacks.push(modelTamperPipeline);\n\n// For compat, assert that state object on both FF and ZMAGS_API\nwindow.FASTR_FRONTEND.experiences[_experienceId].modelCallbacks = modelCallbacks;\nwindow.ZMAGS_API.experiences[_experienceId].modelCallbacks = modelCallbacks;\n\n// If the init.js has loaded the experience state, update the model on the window for use in rendering\nif (window[_experienceId + \"_STATE\"]) {\n    window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].model = await modelTamperPipeline(window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].model);\n    debug.log(\"Experience state loaded, model updated for experience:\", experienceId);\n}\n\nconst updateStateCallback = window.FASTR_FRONTEND.experiences[_experienceId].updateStateCallback || window.ZMAGS_API.experiences[_experienceId].updateStateCallback;\n\n// For compat, assert that state update fn on both FF and ZMAGS_API\nwindow.FASTR_FRONTEND.experiences[_experienceId].updateStateCallback = updateStateCallback;\nwindow.ZMAGS_API.experiences[_experienceId].updateStateCallback = updateStateCallback;\n\nwindow.FASTR_FRONTEND.experiences[_experienceId].registerModelTamperFunction = async (modelModificationConfig) => {\n    debug.log(`Registering model tamper function for experience: ${experienceId} and widget ${modelModificationConfig.widgetUuid}`);\n\n    const tamperFunctionWrapper = async (model) => {\n        try {\n            const { widget } = findWidgetByUuid(model, modelModificationConfig.widgetUuid);\n            if (!widget) return model;\n\n            const visibleScene = window.FASTR_FRONTEND.experiences[_experienceId].getCurrentScene();\n            if (visibleScene !== modelModificationConfig.sceneId) return model;\n\n            debug.log(\"Running integrations for experience:\", experienceId, \"and widget:\", modelModificationConfig.widgetUuid);\n\n            let integrationResult = {\n                get: function (path) {\n                    const parts = path.match(/\"(.*?)\"|[\\w\\s]+|\\d+/g);\n\n                    return parts.reduce((obj, key) => {\n                        if (isNaN(key)) key = key.replace(/\"/g, \"\");\n                        else key = Number(key);\n\n                        return obj[key];\n                    }, this);\n                },\n            };\n\n            const initialInput = await window.FASTR_FRONTEND.experiences[_experienceId].processGivenInputs(modelModificationConfig.gatherInputs);\n            for (const integration of modelModificationConfig.integrations) {\n                const inputJSON = { ...integrationResult, ...integration.input, ...initialInput };\n                const processedInput = await window.FASTR_FRONTEND.experiences[_experienceId].processIntegrationConfig(\n                    inputJSON,\n                    integration.processInputs,\n                    integration.processFunction,\n                    integration.processOutputs\n                );\n                integrationResult = { ...integrationResult, ...processedInput };\n            }\n\n            const tamperDescriptor = modelModificationConfig.tamperFunction;\n            debug.log(\"Calling tamper function \" + tamperDescriptor.type + \" for experience:\", experienceId, \"and widget:\", modelModificationConfig.widgetUuid);\n\n            if (!tamperDescriptor) {\n                debug.log(\"Ignoring undefined tamper function!\");\n                return model;\n            }\n\n            if (tamperDescriptor.conditions) {\n                debug.log(\"Calling condition function for experience:\", experienceId, \"and widget:\", modelModificationConfig.widgetUuid);\n\n                const conditionsPassed = await window.FASTR_FRONTEND.experiences[_experienceId].conditionFunction(integrationResult, modelModificationConfig.tamperFunction.conditions);\n                if (!conditionsPassed) return model;\n            }\n\n            await window.FASTR_FRONTEND.experiences[_experienceId].tamperLibrary[tamperDescriptor.type](integrationResult, widget, tamperDescriptor);\n        } catch (e) {\n            debug.error(\"Failed to process tamper function\");\n            debug.error(JSON.stringify(modelModificationConfig));\n            debug.error(e);\n        }\n        return model;\n    };\n\n    window.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions.push(tamperFunctionWrapper);\n};\n\nfunction findWidgetByUuid(item, widgetUuid, parent = null) {\n    if (item.uuid === widgetUuid || item.id === widgetUuid) {\n        return { widget: item, parent, index: parent ? parent.children.indexOf(item) : null };\n    } else if (item.svgContent && item.svgContent.includes(widgetUuid)) {\n        return { widget: item, parent, index: parent ? parent.children.indexOf(item) : null };\n    }\n\n    const children = item.widgets || item.scenes || item.group;\n    if (children) {\n        item.children = children;\n        for (const child of children) {\n            const result = findWidgetByUuid(child, widgetUuid, item);\n            if (result) return result;\n        }\n    }\n\n    return null;\n}\n\nfunction findSceneByWidgetUuid(widgetUuid) {\n    for (let scene of _model.scenes) {\n        if (sceneContainsWidget(scene, widgetUuid)) {\n            return scene;\n        }\n    }\n    return null;\n}\n\nfunction sceneContainsWidget(scene, widgetUuid) {\n    const children = scene.widgets || scene.group;\n    if (children) {\n        for (const child of children) {\n            if (child.uuid === widgetUuid) {\n                return true;\n            }\n\n            if (child.type === \"composite\" && sceneContainsWidget(child, widgetUuid)) {\n                return true;\n            }\n        }\n    }\n\n    return false;\n}\n\n// Function to run when a mutation is observed\nasync function handleVisibilityChange(mutationsList, observer) {\n    for (let mutation of mutationsList) {\n        if (mutation.type === \"attributes\" && mutation.attributeName === \"style\") {\n            const visibility = getComputedStyle(mutation.target).visibility;\n            if (visibility === \"visible\") {\n                const sceneNumber = parseInt(mutation.target.className.split(\"-\")[1]);\n\n                if (window.FASTR_FRONTEND.experiences[_experienceId].getCurrentScene() !== sceneNumber) {\n                    debug.log(`Scene ${sceneNumber} is now visible.`);\n                    window.FASTR_FRONTEND.experiences[_experienceId].setCurrentScene(sceneNumber);\n                    window.FASTR_FRONTEND.experiences[_experienceId].reassertModel();\n                }\n            }\n        }\n    }\n}\n\n// Create an observer instance linked to the callback function\nconst observer = new MutationObserver(handleVisibilityChange);\n\n// Options for the observer (which mutations to observe)\nconst config = { attributes: true, attributeFilter: [\"style\"] };\n\n// Find scene divs and start observing them\nconst sceneDivs = Array.from(domRef.querySelectorAll('div[class^=\"scene-\"]')).filter((el) => {\n    const name = el.className.split(\"-\")[1];\n    return !isNaN(parseInt(name));\n});\nsceneDivs.forEach((sceneDiv) => observer.observe(sceneDiv, config));\n\n"}},"stateKey":"newTrigger"}]}]}}},"breakpointGroups":[],"queuedAnalyticsEvents":[{"companyId":"6411fe8c6df9f10f76c9df53","experienceId":"C3_75fdddc8-9242-8132-8003-4da81b964311_a0b080d0-9fcb-804e-8003-4c2287f5bfee","experienceName":"Desktop"}],"queuedGroupEvents":[],"initialLoad":false,"experienceFit":"width","isBehaviorExecuting":false,"activeExperienceId":"C3_75fdddc8-9242-8132-8003-4da81b964311_a0b080d0-9fcb-804e-8003-4c2287f5bfee","activeExperience":{"currentSceneNumber":1,"transitionDirection":0,"model":{"companyId":"6411fe8c6df9f10f76c9df53","createdAt":1704927957958,"modifiedAt":1704927957958,"autoPlayDelay":0,"type":"Experience","language":"en-US","id":"C3_75fdddc8-9242-8132-8003-4da81b964311_a0b080d0-9fcb-804e-8003-4c2287f5bfee","name":"Desktop","width":1440,"height":570,"transition":"none","scenes":[{"id":"2bcb0d15-cef4-8031-8003-4c688dc0c6ce","sceneIndex":0,"sceneNumber":"1","widgets":[{"uuid":"2bcb0d15-cef4-8031-8003-4c6c614b9ce1","title":"Copy","left":11.11111111111112,"top":75.78947368421052,"width":77.77777777777779,"rotation":0,"layerIndex":0,"customClasses":[null],"editor":{"type":"text"},"type":"text-enhanced","effects":[],"animations":[],"fontFamilies":[{"fontProvider":"custom","fontFamily":"VolkoGrot Light","fontStyle":"normal","fontWeight":"300","cssFamilyNames":"VolkoGrot Light","fontMetadata":{"customFontId":"custom-ac60ec2b-e895-805a-8002-c2434ecdb64e_normal-300","format":"woff","url":"https://images.getfastr.com/22/44/d69c107540c7b89f9b2b0a119805.woff"}}],"paragraphRuns":[{"styles":["text-align: center","line-height: 1.2"],"textRuns":[{"dataAttributes":["data-font-family=\"VolkoGrot Light\"","data-font-style=\"normal\"","data-font-weight=\"300\"","data-font-provider=\"custom\"","data-css-family-names=\"VolkoGrot Light\"","style=\"\""],"styles":["font-size: 18px","line-height: 1.2","font-weight: 300","font-family: &quot;VolkoGrot Light&quot;","letter-spacing: 0px","color: #375481","opacity: 1"],"text":"Say hello to SPF with no pilling, no heaviness and no white cast. The award-winning formula is completely invisible, weightless and scentless, providing oil-free, broad spectrum protection and supporting visibly healthier skin. Plus, it’s formulated without oxybenzone and parabens, and it’s cruelty-free, non-irritating and vegan. That’s how Supergoop! makes the magic happen. "}],"headerLevel":null}],"text":"<div style=\"text-align: center;line-height: 1.2\">\n                    <span data-font-family=\"VolkoGrot Light\" data-font-style=\"normal\" data-font-weight=\"300\" data-font-provider=\"custom\" data-css-family-names=\"VolkoGrot Light\" style=\"\"><span style=\"opacity: 1\"><span style=\"color: #375481\"><span style=\"letter-spacing: 0px\"><span style=\"font-family: &quot;VolkoGrot Light&quot;\"><span style=\"font-weight: 300\"><span style=\"line-height: 1.2\"><span style=\"font-size: 18px\">Say hello to SPF with no pilling, no heaviness and no white cast. The award-winning formula is completely invisible, weightless and scentless, providing oil-free, broad spectrum protection and supporting visibly healthier skin. Plus, it’s formulated without oxybenzone and parabens, and it’s cruelty-free, non-irritating and vegan. That’s how Supergoop! makes the magic happen. </span></span></span></span></span></span></span></span></div>"},{"uuid":"2bcb0d15-cef4-8031-8003-4c6c614b9ce0","title":"Title","left":11.11111111111111,"top":66.91772534785238,"width":77.77777777777779,"rotation":0,"layerIndex":1,"customClasses":[null],"editor":{"type":"text"},"type":"text-enhanced","effects":[],"animations":[],"fontFamilies":[{"fontProvider":"custom","fontFamily":"VolkoGrot Regular","fontStyle":"normal","fontWeight":"400","cssFamilyNames":"VolkoGrot Regular","fontMetadata":{"customFontId":"custom-ac60ec2b-e895-805a-8002-c243bdfbdf20_normal-400","format":"woff","url":"https://images.getfastr.com/4f/6d/807b899444398e56e9739427d7ef.woff"}}],"paragraphRuns":[{"styles":["text-align: center","line-height: 1.2"],"textRuns":[{"dataAttributes":["data-font-family=\"VolkoGrot Regular\"","data-font-style=\"normal\"","data-font-weight=\"400\"","data-font-provider=\"custom\"","data-css-family-names=\"VolkoGrot Regular\"","style=\"\""],"styles":["font-size: 30px","line-height: 1.2","font-weight: 400","font-family: &quot;VolkoGrot Regular&quot;","letter-spacing: 0px","color: #142745","opacity: 1"],"text":"The key to healthy skin all year long"}]}],"text":"<div style=\"text-align: center;line-height: 1.2\">\n                    <span data-font-family=\"VolkoGrot Regular\" data-font-style=\"normal\" data-font-weight=\"400\" data-font-provider=\"custom\" data-css-family-names=\"VolkoGrot Regular\" style=\"\"><span style=\"opacity: 1\"><span style=\"color: #142745\"><span style=\"letter-spacing: 0px\"><span style=\"font-family: &quot;VolkoGrot Regular&quot;\"><span style=\"font-weight: 400\"><span style=\"line-height: 1.2\"><span style=\"font-size: 30px\">The key to healthy skin all year long</span></span></span></span></span></span></span></span></div>"},{"uuid":"d3b2debf-0c1a-8001-8003-4da90adc8be3","title":"Group 29770.png","left":11.11111111111111,"top":8.771929824561402,"width":77.77777777777779,"height":51.92982456140351,"rotation":0,"layerIndex":2,"customClasses":[null],"editor":{"type":"image"},"imageId":"6bb5d8c7-39e9-8153-8003-4da90ad1af74","src":"https://images.getfastr.com/c9/03/357f46bc4978a243c03aaea3cb6f.png","sourceType":"Static","originalImageWidth":2270,"originalImageHeight":600,"type":"image","animations":[],"altText":"Group 29770.png","effects":[]}],"widgetTabOrder":["2bcb0d15-cef4-8031-8003-4c6c614b9ce1","2bcb0d15-cef4-8031-8003-4c6c614b9ce0","d3b2debf-0c1a-8001-8003-4da90adc8be3"],"removedWidgetTabOrder":[],"customClasses":[null]}],"threshold":993,"backgroundScene":{"sceneNumber":"background","widgets":[],"widgetTabOrder":[],"removedWidgetTabOrder":[]},"overlayScene":{"sceneNumber":"overlay","widgets":[],"widgetTabOrder":[],"removedWidgetTabOrder":[]},"backdrop":{"type":"color","fit":"Experience","color":"#f5f5f5","image":null},"behaviors":[{"steps":[{"id":"","steps":[],"data":{"loadType":"executeJs","details":{"jsSnippet":"\n(function() {\n    window.FASTR_FRONTEND.experiences[_experienceId].tamperLibrary = {\n        setImageUrl: async function setImage(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    const sourceData = integrationResults.get(source);\n\n    if (!sourceData) {\n        debug.error(`No data found for source ${source}`);\n    }\n\n    // Replace image-widget in SSR body\n    let widgetEl = domRef.querySelector(`div[id$='${widget.uuid}'] img`);\n\n    if (widgetEl) {\n        widgetEl.removeAttribute('srcset');\n        widgetEl.style.setProperty('object-fit', 'contain');\n        widgetEl.src = sourceData;\n\n        // Replace in the model\n        if (widget['src']) {\n            widget.src = sourceData;\n            widget.changeSource = \"behavior\";\n        }\n    } else {\n\n        // Replace image in svg SSR body\n        widgetEl = domRef.querySelector(`div[id$='${widget.uuid}'] svg`);\n\n        if (widgetEl) {\n            let imageEl = widgetEl.querySelector('image');\n            imageEl.setAttribute('href', sourceData);\n            imageEl.setAttribute('preserveAspectRatio', 'xMidYMid meet'); // This will ensure proportional scaling of the image\n\n            // Replace in the model\n            widget.svgContent = widgetEl.innerHTML;\n        }\n    }\n}\n,\n        setHref: async function setOnclick(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    const url = integrationResults.get(source);\n\n    // Replace in SSR body\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}'] a`);\n    if (widgetEl) {\n        widgetEl.setAttribute('href', url);\n    }\n\n    // Replace in the model\n    widget[\"action\"] = widget[\"action\"] || [];\n    widget[\"action\"].type = \"link\";\n    widget[\"action\"].url = url;\n}\n,\n        setSceneTarget: async function setOnclick(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    // Replace in the model\n\n    // TODO: This sets the scene click target\n}\n,\n        setText: async function setText(integrationResults, widget, args) {\n\n    const getInnerHTML = function(node) {\n        // The nodes we're creating are actually nested 2 deep\n        if (node.children[0] && node.children[0].children[0]) {\n            return node.children[0].children[0].innerHTML;\n        }\n\n        debug.error('Failed to find generated replacement text in node', node);\n        return \"\";\n    }\n\n    const replaceInTextNodes = function(node, searchText, replacementText, modifyOriginal = false) {\n        let tempNode;\n        if (typeof node === 'string') {\n            // We're working with the widget model\n            let tempDiv = document.createElement('div');\n            tempDiv.innerHTML = node;\n            tempNode = tempDiv;\n        } else {\n            // We're working with an actual DOM node\n            tempNode = modifyOriginal ? node : node.cloneNode(true);\n        }\n    \n        let nodeList = [];\n        let walker = document.createTreeWalker(tempNode, NodeFilter.SHOW_TEXT);\n        while (walker.nextNode()) nodeList.push(walker.currentNode);\n    \n        for (let textNode of nodeList) {\n            textNode.nodeValue = textNode.nodeValue.replaceAll(searchText, replacementText);\n        }\n    \n        return (typeof node === 'string') ? getInnerHTML(tempNode) : tempNode;\n    }\n\n    debug.breakpoint();\n\n    const { source, placeholder } = args;\n\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}']`);\n\n    const sourceValue = integrationResults.get(source);\n\n    if (!sourceValue) {\n        debug.log(`No match found in integration results for ${source} for widget ${widget.uuid}`);\n        return;\n    }\n\n    if (placeholder) {\n        // Replace in the model\n        if (widget[\"text\"]) {\n            widget[\"text\"] = replaceInTextNodes(widget[\"text\"], placeholder, sourceValue);\n        }\n\n        // Replace in SSR body\n        replaceInTextNodes(widgetEl, placeholder, sourceValue, true);\n        \n    } else {\n        // Replace in the model\n        if (widget[\"text\"]) {\n            widget[\"text\"] = replaceInTextNodes(widget[\"text\"], widgetEl.textContent.trim(), sourceValue);\n        }\n\n        // Replace in SSR body\n        replaceInTextNodes(widgetEl, widgetEl.textContent.trim(), sourceValue, true);\n    }\n\n    widget.text = getInnerHTML(widgetEl);\n},\n        hideGroup: \nasync function hideGroup(_, __, args) {\n\n    debug.breakpoint();\n\n    const { placeholder } = args;\n\n    const widgets = placeholder.split(',');\n\n    for (const widgetUuid of widgets) {\n        const { widget } = findWidgetByUuid(_model, widgetUuid);\n        this.hideWidget(_, widget);\n    }\n    \n}\n,\n        hideWidget: \nasync function hideWidget(_, widget) {\n\n    debug.breakpoint();\n\n    const widgetUuuid = widget.uuid;\n    // Replace in SSR body\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}']`);\n    widgetEl.style.opacity = 0\n    widgetEl.style.display = 'none';\n\n    // Replace in the model\n    widget['opacity'] = \"0\";\n    \n}\n,\n        setAltText: \nasync function setAltText(integrationResults, widget, args) {\n\n    debug.breakpoint();\n\n    const { source } = args;\n\n    const txt = integrationResults.get(source);\n\n    // Replace in SSR body\n    const widgetEl = domRef.querySelector(`div[id$='${widget.uuid}']`);\n    widgetEl.setAttribute('aria-label', txt);\n\n    // If this is an image widget, also set the alt text\n    const img = widgetEl.querySelector('img');\n    if (img) {\n        img.setAttribute('alt', txt);\n    }\n\n    // Replace in the model\n    widget['alt'] = txt;\n    \n}\n,\n    };\n    window.FASTR_FRONTEND.experiences[_experienceId].conditionFunction = async function evaluateConditions(integrationResults, conditions) {\n    debug.breakpoint();\n\n    try {\n        for (let i = 0; i < conditions.length; i++) {\n            const condition = conditions[i];\n            const source = integrationResults.get(condition.lefthandPrefix);\n\n            const isBooleanComparison = condition.comparator === \"isNot\" || condition.comparator === \"is\";\n\n            let comparison;\n            if (condition.righthandType === \"static\") {\n\n                // If this is a static boolean comparison, we are comparing to \"true\".  It's only is or is-not that dictates the result.\n                if (isBooleanComparison) {\n                    comparison = true;\n                } else {\n                    comparison = condition.righthandValue;\n                }\n\n            } else {\n                comparison = integrationResults.get(condition.righthandPrefix);\n            }\n\n            let comparisonNumber = Number(comparison);\n\n            if (!isBooleanComparison) {\n                if (!isNaN(comparisonNumber)) {\n                    comparison = comparisonNumber;\n                } else if ((comparison.startsWith(`\"`) && comparison.endsWith(`\"`)) || (comparison.startsWith(`'`) && comparison.endsWith(`'`))) {\n                    comparison = comparison.slice(1, -1);\n\n                    // Try converting to a number, if not possible use it as a string\n                    const comparisonNumber = Number(comparison);\n                    if (!isNaN(comparisonNumber)) {\n                        comparison = comparisonNumber;\n                    }\n                }\n            }\n\n            const comparator = condition.comparator;\n\n            const comparators = {\n                \"<\": (a, b) => a < b,\n                \"<=\": (a, b) => a <= b,\n                \"==\": (a, b) => a == b,\n                \">=\": (a, b) => a >= b,\n                \">\": (a, b) => a > b,\n                \"!=\": (a, b) => a != b,\n                equals: (a, b) => a === b,\n                doesNotEqual: (a, b) => a !== b,\n                includes: (a, b) => a.includes(b),\n                doesNotContain: (a, b) => !a.includes(b),\n                startsWith: (a, b) => a.startsWith(b),\n                endsWith: (a, b) => a.endsWith(b),\n                is: (a, b) => a == b,\n                isNot: (a, b) => a != b,\n            };\n\n            const comparisonFunction = comparators[comparator];\n            if (!comparisonFunction) throw new Error(`Invalid comparator: ${comparator}`);\n\n            debug.log(`Comparing: ${source} ${comparator} ${comparison}`);\n\n            const result = comparisonFunction(source, comparison);\n\n            debug.log(`The result of the comparison is ${result}`);\n\n            if (!result) return false;\n        }\n\n        return true;\n    } catch (error) {\n        console.log(`Failed to evaluate conditions: ${error}`);\n    }\n\n    return false;\n}\n;\n\n    modelModificationConfigs.forEach((config) => {\n        try {\n            if (config.gatherInputs) {\n                config.gatherInputs = eval(\"(\" + config.gatherInputs + \")()\");\n            }\n            config.integrations.forEach((integration) => {\n                integration.processInputs = eval(\"(\" + integration.processInputs + \")()\");\n                integration.processFunction = eval(\"(\" + integrations[integration.processFunction] + \")()\");\n                integration.processOutputs = eval(\"(\" + integration.processOutputs + \")()\");\n            });\n        } catch (e) {\n            debug.error(\"Error setting up integration functions\");\n            debug.error(e);\n        }\n\n        window.FASTR_FRONTEND.experiences[_experienceId].registerModelTamperFunction(config);\n    });\n\n})();\n","jsSnippetMetaData":"\nconst _experienceId = \"BEHAVIOR_EXPERIENCEID_PLACEHOLDER\";\n\nconst lastUnderscoreIndex = _experienceId.lastIndexOf(\"_\");\n\nconst experienceTypeId = _experienceId.substring(0, lastUnderscoreIndex);\nconst experienceOrdinal = _experienceId.substring(lastUnderscoreIndex + 1);\n\nlet _model = undefined;\n\nwindow.ZMAGS_API = window.ZMAGS_API || { experiences: {} };\nwindow.ZMAGS_API.experiences[_experienceId] = window.ZMAGS_API.experiences[_experienceId] || {};\n\ntry {\n    window.FASTR_FRONTEND = window.FASTR_FRONTEND || {};\n    window.FASTR_FRONTEND.experiences = window.FASTR_FRONTEND.experiences || {};\n    window.FASTR_FRONTEND.experiences[_experienceId] = window.FASTR_FRONTEND.experiences[_experienceId] || {};\n\n    // Pull properties from ZMAGS_API to FASTR_FRONTEND.\n    // NOTE: this sets us up to deprecate ZMAGS_API from viewer in the future\n    for (const prop in window.ZMAGS_API.experiences[_experienceId]) {\n        if (!window.FASTR_FRONTEND.experiences[_experienceId].hasOwnProperty(prop)) {\n            window.FASTR_FRONTEND.experiences[_experienceId][prop] = window.ZMAGS_API.experiences[_experienceId][prop];\n        }\n    }\n} catch (error) {\n    console.error(error);\n}\n\nconst domRef = document.querySelector(`div[data-experience='${experienceTypeId}'][data-ordinal='${experienceOrdinal}']`);\n\nconst getFASTR_FRONTEND_DEBUG = () => {\n    try {\n        return JSON.parse(localStorage.getItem(\"FASTR_FRONTEND_DEBUG\"));\n    } catch (error) {\n        console.warn(\"Error parsing FASTR_FRONTEND_DEBUG from localStorage:\", error);\n        return {};\n    }\n};\n\nconst FASTR_FRONTEND_DEBUG = getFASTR_FRONTEND_DEBUG();\n\nconst debug = {\n    output: (type, message, ...optionalParams) => {\n        if (FASTR_FRONTEND_DEBUG && FASTR_FRONTEND_DEBUG.trace) {\n            console[type](`[FASTR_FRONTEND_DEBUG] ${message}`, ...optionalParams);\n        }\n    },\n    log: (message, ...optionalParams) => {\n        debug.output(\"log\", message, ...optionalParams);\n    },\n    error: (message, ...optionalParams) => {\n        debug.output(\"error\", message, ...optionalParams);\n    },\n    breakpoint: () => {\n        if (FASTR_FRONTEND_DEBUG && FASTR_FRONTEND_DEBUG.break) {\n            debugger;\n        }\n    },\n};\n\nwindow.FASTR_FRONTEND.experiences[_experienceId].dumpState = () => {\n    const experienceIdentifier = _experienceId.split(\"_\").slice(0, -1).join(\"_\");\n    const model = window[_experienceId + \"_STATE\"].viewer.experiences[experienceIdentifier].model;\n    const modelSizeInKB = JSON.stringify(model).length / 1024;\n    console.log(`Model size for experience ${_experienceId}: ${modelSizeInKB.toFixed(2)} KB`);\n};\n\n\nconst modelModificationConfigs = [];\n\nconst backgroundIntegrations = [\n   \"// Create a style element\\nvar style = document.createElement('style');\\n\\n// Set the CSS content\\nstyle.textContent = '@media (min-width: 992px) {\\\\n' +\\n    '.col-lg-9 {\\\\n' +\\n    '    flex: 0 0 100%;\\\\n' +\\n    '    max-width: 100%;\\\\n' +\\n    '}\\\\n' +\\n    '}';\\n\\nif(document.querySelector('.product-breadcrumb.c-product-breadcrumb')){\\n    // Append the style element to the document head\\n    document.head.appendChild(style);\\n}\\n\\n\"\n];\n\nconst functionIntegrations = {};\n\nconst integrations = {};\n\nwindow.FASTR_FRONTEND = window.FASTR_FRONTEND || {};\nwindow.FASTR_FRONTEND.experiences = window.FASTR_FRONTEND.experiences || {};\n\nwindow.FASTR_FRONTEND.experiences[_experienceId] = {\n    initialTimestamp: performance.now(),\n    memoCache: new Map(),\n    pendingCache: new Map(),\n    performanceCounters: {\n        memoizedFunctions: 0,\n        cacheHits: 0,\n        cacheMisses: 0,\n        callDurations: [],\n        keyMapping: {},\n    },\n    hashFnv32a: function (str, seed = 0x811c9dc5) {\n        let h = seed;\n        for (let i = 0; i < str.length; i++) {\n            h ^= str.charCodeAt(i);\n            h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);\n        }\n        return h >>> 0;\n    },\n    memoize: function (fn) {\n        this.performanceCounters.memoizedFunctions++;\n        debug.log(`Memoized function created. Total: ${this.performanceCounters.memoizedFunctions}`);\n\n        return async (...args) => {\n            const fnString = fn.toString();\n            const key = JSON.stringify({ fnString, args });\n            const hashedKey = this.hashFnv32a(key);\n            const start = performance.now();\n\n            this.performanceCounters.keyMapping[hashedKey] = key;\n\n            if (this.memoCache.has(hashedKey)) {\n                this.performanceCounters.cacheHits++;\n                const end = performance.now();\n                debug.log(`Cache hit for key ${hashedKey}. Total hits: ${this.performanceCounters.cacheHits}`);\n                this.performanceCounters.callDurations.push({\n                    key: hashedKey,\n                    type: \"hit\",\n                    duration: end - start,\n                    start,\n                });\n                return this.memoCache.get(hashedKey);\n            }\n\n            if (this.pendingCache.has(hashedKey)) {\n                this.performanceCounters.waitingCalls++;\n                const result = await this.pendingCache.get(hashedKey);\n                this.performanceCounters.cacheHits++;\n                debug.log(`Pending cache hit for key ${hashedKey}. Total hits: ${this.performanceCounters.cacheHits}`);\n                const end = performance.now();\n                this.performanceCounters.callDurations.push({\n                    key: hashedKey,\n                    type: \"awaiting cache\",\n                    duration: end - start,\n                    start,\n                });\n                return result;\n            }\n\n            this.performanceCounters.cacheMisses++;\n            debug.log(`Cache miss for key ${hashedKey}. Total misses: ${this.performanceCounters.cacheMisses}`);\n            const promise = fn(...args);\n            this.pendingCache.set(hashedKey, promise);\n            const result = await promise;\n            this.memoCache.set(hashedKey, result);\n            this.pendingCache.delete(hashedKey);\n\n            const end = performance.now();\n            this.performanceCounters.totalDuration += end - start;\n            debug.log(`Memoized function ${fn.name} took ${end - start} milliseconds to execute. Total duration: ${this.performanceCounters.totalDuration}`);\n            this.performanceCounters.callDurations.push({\n                key: hashedKey,\n                type: \"miss\",\n                duration: end - start,\n                start,\n            });\n            return result;\n        };\n    },\n    runPerformanceTrace: function () {\n        const { callDurations, keyMapping } = this.performanceCounters;\n\n        const sortedDurations = callDurations.sort((a, b) => a.start - b.start);\n\n        sortedDurations.forEach(({ key, type, duration, start }) => {\n            const { fnString, args } = JSON.parse(keyMapping[key]);\n\n            let strippedFnString = fnString.replace(/async\\s+function\\s*\\(\\s*input\\s*\\)\\s*\\{\\s*/, \"\").replace(/\\n/g, \"\");\n\n            const shortenedFnString = strippedFnString.length > 90 ? strippedFnString.substring(0, 87) + \"...\" : strippedFnString;\n            const argsString = JSON.stringify(args).replace(/\\n/g, \"\");\n            const shortenedArgsString = argsString.length > 90 ? argsString.substring(0, 87) + \"...\" : argsString;\n\n            const fullKey = `Body: [${shortenedFnString}] Args: [${shortenedArgsString}]`;\n\n            const elapsedTime = start - this.initialTimestamp;\n            const minutes = Math.floor(elapsedTime / 60000);\n            const seconds = ((elapsedTime % 60000) / 1000).toFixed(3);\n            console.log(`[${minutes}:${seconds}] [${type}] [${key}] ${fullKey}: ${Math.round(duration)}ms`);\n        });\n    },\n    processGivenInputs: async function (inputFn) {\n        const memoizedProcessInputs = this.memoize(inputFn);\n        debug.log(\"Gathering inputs with :\", inputFn);\n        const generatedInput = await memoizedProcessInputs();\n        return generatedInput;\n    },\n    processIntegrationConfig: async function (inputJSON, processInputsFn, processFunctionFn, processOutputsFn) {\n        const memoizedProcessInputs = this.memoize(processInputsFn);\n        const memoizedProcessFunction = this.memoize(processFunctionFn);\n        const memoizedProcessOutputs = this.memoize(processOutputsFn);\n\n        debug.log(\"Processing inputs with inputJSON:\", inputJSON);\n        const processedInput = await memoizedProcessInputs(inputJSON);\n\n        debug.log(\"Executing process function with processedInput:\", processedInput);\n        const output = await memoizedProcessFunction(processedInput);\n\n        debug.log(\"Processing outputs with output:\", output);\n        const outputJSON = await memoizedProcessOutputs(output);\n\n        debug.log(\"Returning outputJSON:\", outputJSON);\n        return outputJSON;\n    },\n    runBackgroundIntegrations: async function () {\n        debug.breakpoint();\n\n        for (const integration of backgroundIntegrations) {\n            const body = integration;\n            try {\n                const func = new Function(\"model\", body);\n                await func(_model);\n            } catch (error) {\n                debug.error(\"Failed to run background integration\");\n                debug.error(JSON.stringify(body));\n                debug.error(error);\n            }\n        }\n    },\n    customActionHandler: async function (integrationId, args) {\n        debug.breakpoint();\n\n        const fn = functionIntegrations[integrationId];\n\n        const wrappedFn =\n`(async function customAction(inputs, model) {\n    ${fn}\n})(inputs, model);`;\n\n        // Flatten our inputs down to a simple object\n        const inputs = args ? Object.fromEntries(args.map(({name, value}) => [name, value])) : {};\n\n        try {\n            const func = new Function(\"inputs\", \"model\", wrappedFn);\n            await func(inputs, _model);\n        } catch (error) {\n            debug.error(\"Failed to run custom action integration\");\n            debug.error(JSON.stringify(fn));\n            debug.error(error);\n        }\n    },\n};\n\nwindow.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions = window.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions || [];\n\n// Called once _model is guaranteed to be set\nwindow.FASTR_FRONTEND.experiences[_experienceId].experienceInit = async () => {\n    if (window.FASTR_FRONTEND.experiences[_experienceId].experienceInitCalled) return;\n\n    window.FASTR_FRONTEND.experiences[_experienceId].experienceInitCalled = true;\n\n    window.FASTR_FRONTEND.experiences[_experienceId].runBackgroundIntegrations();\n\n    window.FASTR_FRONTEND.experiences[_experienceId].getCurrentScene = () => {\n        // If we're in pre-init, return scene 1\n        return window[_experienceId + \"_STATE\"] ? window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].currentSceneNumber : 1;\n    };\n\n    window.FASTR_FRONTEND.experiences[_experienceId].setCurrentScene = (sceneNumber) => {\n        window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].currentSceneNumber = sceneNumber;\n    };\n\n    window.FASTR_FRONTEND.experiences[_experienceId].reassertModel = () => {\n        window.ZMAGS_API.experiences[_experienceId].updateStateCallback(async () => {\n            return modelTamperPipeline(_model);\n        });\n    };\n\n    // Walk the model and find any custom actions or overlays that need to be updated\n    function updateWidgetActions(widgets) {\n        for (let widget of widgets) {\n            if (widget.action) {\n                if (widget.action.type === \"js\" && widget.action.code.startsWith(\"window.FASTR_FRONTEND.experiences[_experienceId].customActionHandler\")) {\n                    widget.action.code = widget.action.code.replace(\"_experienceId\", `\"${_experienceId}\"`);\n                } else if (widget.action.type === \"toggleOverlay\") {\n                    debugger;\n                    const widgetDomElement = domRef.querySelector(`div[id$='${widget.uuid}']`);\n\n                    let { parent, index } = findWidgetByUuid(_model, widget.uuid);\n\n                    const overlayResults = findWidgetByUuid(_model, widget.action.target);\n\n                    const overlayTarget = overlayResults.widget;\n\n                    function traverseAndUpdateWidgets(widgetList, originalWidget, parent, index) {\n                        for (const widget of widgetList) {\n                            // If widget type is composite, traverse its group\n                            if (widget.type === \"composite\" && widget.group) {\n                                traverseAndUpdateWidgets(widget.group, originalWidget, parent, index);\n                            } else {\n                                // Adjust the top and left properties of each widget\n                                widget.top += originalWidget.top;\n                                widget.left += originalWidget.left;\n\n                                // Add the widget to the parent\n                                parent.children.splice(index, 0, widget);\n                                index++;\n                            }\n                        }\n                    }\n\n                    function traverseAndAssignHandlers(widgetList, handler) {\n                        const observer = new MutationObserver((mutationsList) => {\n                            for (let mutation of mutationsList) {\n                                // Check the addedNodes property\n                                if (mutation.addedNodes) {\n                                    mutation.addedNodes.forEach((node) => {\n                                        if (node.id) {\n                                            widgetList.forEach((widget) => {\n                                                if (node.id.endsWith(widget.uuid)) {\n                                                    node.onmouseleave = handler;\n                                                }\n                                            });\n                                        }\n                                    });\n                                }\n                            }\n                        });\n\n                        // Start observing the document with the configured parameters\n                        observer.observe(domRef, { childList: true, subtree: true });\n\n                        for (const widget of widgetList) {\n                            // If widget type is composite, traverse its group\n                            if (widget.type === \"composite\" && widget.group) {\n                                traverseAndAssignHandlers(widget.group, handler);\n                            }\n                        }\n                    }\n\n                    if (widgetDomElement) {\n                        if (widget.action.triggerType === \"onHover\") {\n                            const targetLeave = () => {\n                                // Retrieve the original widget from the saved data\n                                const originalWidget = JSON.parse(widgetDomElement.dataset.originalWidget);\n\n                                // Remove overlay widgets from the parent\n                                if (parent && index !== null) {\n                                    parent.children.splice(index - overlayTarget.widgets.length, overlayTarget.widgets.length);\n                                }\n\n                                // Restore the original widget to its original position\n                                if (parent) {\n                                    parent.children.splice(index - overlayTarget.widgets.length, 0, originalWidget);\n                                }\n\n                                // Adjust the top and left properties of each child in overlayTarget's widgets array\n                                if (overlayTarget && overlayTarget.widgets) {\n                                    overlayTarget.widgets.forEach((overlayWidget) => {\n                                        overlayWidget.top += originalWidget.top;\n                                        overlayWidget.left += originalWidget.left;\n                                    });\n                                }\n\n                                // Reassert\n                                window.FASTR_FRONTEND.experiences[_experienceId].reassertModel();\n                            };\n\n                            widgetDomElement.onmouseenter = () => {\n                                // Keep a copy of the original widget\n                                const originalWidget = { ...widget };\n\n                                // Remove the widget from its parent\n                                if (parent && index !== null) {\n                                    parent.children.splice(index, 1);\n                                }\n\n                                // Swap all the elements of the widgets array in overlayTarget\n                                // as children of the original widget's parent\n                                if (parent && overlayTarget && overlayTarget.widgets) {\n                                    traverseAndUpdateWidgets(overlayTarget.widgets, originalWidget, parent, index);\n                                }\n\n                                // Save the original widget somewhere so we can restore it later\n                                widgetDomElement.dataset.originalWidget = JSON.stringify(originalWidget);\n\n                                // Reassert\n                                window.FASTR_FRONTEND.experiences[_experienceId].reassertModel();\n\n                                // Loop over widgets again to assign onmouseleave event handlers\n                                if (overlayTarget && overlayTarget.widgets) {\n                                    traverseAndAssignHandlers(overlayTarget.widgets, targetLeave);\n                                }\n                            };\n                        }\n                    }\n                }\n            }\n\n            if (widget.type === \"composite\") {\n                updateWidgetActions(widget.group);\n            }\n        }\n    }\n\n    // Start by iterating over scenes and calling the function on each scene's widgets\n    _model.scenes.forEach((scene) => updateWidgetActions(scene.widgets));\n};\n\nconst modelTamperPipeline = async (model) => {\n    if (model.id !== experienceId) return model;\n\n    // Set global _model for this experience\n    _model = model;\n\n    debug.breakpoint();\n\n    await window.FASTR_FRONTEND.experiences[_experienceId].experienceInit();\n\n    debug.log(\"Running model tamper pipeline for experience:\", experienceId);\n\n    for (const tamperFunction of window.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions) {\n        // NOTE: tamperFunction must be run async to allow for async integrations.  The tamperFunctions are themselves responsible for\n        // updating both the model and SSR, and tamperFunctions must also be assumed orderless.\n        tamperFunction(model);\n    }\n\n    return model;\n};\n\n// If the init.js hasn't loaded the experience state yet, set this callback which will be run before the initial render\nconst modelCallbacks = window.FASTR_FRONTEND.experiences[_experienceId].modelCallbacks || window.ZMAGS_API.experiences[_experienceId].modelCallbacks || [];\nmodelCallbacks.push(modelTamperPipeline);\n\n// For compat, assert that state object on both FF and ZMAGS_API\nwindow.FASTR_FRONTEND.experiences[_experienceId].modelCallbacks = modelCallbacks;\nwindow.ZMAGS_API.experiences[_experienceId].modelCallbacks = modelCallbacks;\n\n// If the init.js has loaded the experience state, update the model on the window for use in rendering\nif (window[_experienceId + \"_STATE\"]) {\n    window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].model = await modelTamperPipeline(window[_experienceId + \"_STATE\"].viewer.experiences[experienceTypeId].model);\n    debug.log(\"Experience state loaded, model updated for experience:\", experienceId);\n}\n\nconst updateStateCallback = window.FASTR_FRONTEND.experiences[_experienceId].updateStateCallback || window.ZMAGS_API.experiences[_experienceId].updateStateCallback;\n\n// For compat, assert that state update fn on both FF and ZMAGS_API\nwindow.FASTR_FRONTEND.experiences[_experienceId].updateStateCallback = updateStateCallback;\nwindow.ZMAGS_API.experiences[_experienceId].updateStateCallback = updateStateCallback;\n\nwindow.FASTR_FRONTEND.experiences[_experienceId].registerModelTamperFunction = async (modelModificationConfig) => {\n    debug.log(`Registering model tamper function for experience: ${experienceId} and widget ${modelModificationConfig.widgetUuid}`);\n\n    const tamperFunctionWrapper = async (model) => {\n        try {\n            const { widget } = findWidgetByUuid(model, modelModificationConfig.widgetUuid);\n            if (!widget) return model;\n\n            const visibleScene = window.FASTR_FRONTEND.experiences[_experienceId].getCurrentScene();\n            if (visibleScene !== modelModificationConfig.sceneId) return model;\n\n            debug.log(\"Running integrations for experience:\", experienceId, \"and widget:\", modelModificationConfig.widgetUuid);\n\n            let integrationResult = {\n                get: function (path) {\n                    const parts = path.match(/\"(.*?)\"|[\\w\\s]+|\\d+/g);\n\n                    return parts.reduce((obj, key) => {\n                        if (isNaN(key)) key = key.replace(/\"/g, \"\");\n                        else key = Number(key);\n\n                        return obj[key];\n                    }, this);\n                },\n            };\n\n            const initialInput = await window.FASTR_FRONTEND.experiences[_experienceId].processGivenInputs(modelModificationConfig.gatherInputs);\n            for (const integration of modelModificationConfig.integrations) {\n                const inputJSON = { ...integrationResult, ...integration.input, ...initialInput };\n                const processedInput = await window.FASTR_FRONTEND.experiences[_experienceId].processIntegrationConfig(\n                    inputJSON,\n                    integration.processInputs,\n                    integration.processFunction,\n                    integration.processOutputs\n                );\n                integrationResult = { ...integrationResult, ...processedInput };\n            }\n\n            const tamperDescriptor = modelModificationConfig.tamperFunction;\n            debug.log(\"Calling tamper function \" + tamperDescriptor.type + \" for experience:\", experienceId, \"and widget:\", modelModificationConfig.widgetUuid);\n\n            if (!tamperDescriptor) {\n                debug.log(\"Ignoring undefined tamper function!\");\n                return model;\n            }\n\n            if (tamperDescriptor.conditions) {\n                debug.log(\"Calling condition function for experience:\", experienceId, \"and widget:\", modelModificationConfig.widgetUuid);\n\n                const conditionsPassed = await window.FASTR_FRONTEND.experiences[_experienceId].conditionFunction(integrationResult, modelModificationConfig.tamperFunction.conditions);\n                if (!conditionsPassed) return model;\n            }\n\n            await window.FASTR_FRONTEND.experiences[_experienceId].tamperLibrary[tamperDescriptor.type](integrationResult, widget, tamperDescriptor);\n        } catch (e) {\n            debug.error(\"Failed to process tamper function\");\n            debug.error(JSON.stringify(modelModificationConfig));\n            debug.error(e);\n        }\n        return model;\n    };\n\n    window.FASTR_FRONTEND.experiences[_experienceId].tamperFunctions.push(tamperFunctionWrapper);\n};\n\nfunction findWidgetByUuid(item, widgetUuid, parent = null) {\n    if (item.uuid === widgetUuid || item.id === widgetUuid) {\n        return { widget: item, parent, index: parent ? parent.children.indexOf(item) : null };\n    } else if (item.svgContent && item.svgContent.includes(widgetUuid)) {\n        return { widget: item, parent, index: parent ? parent.children.indexOf(item) : null };\n    }\n\n    const children = item.widgets || item.scenes || item.group;\n    if (children) {\n        item.children = children;\n        for (const child of children) {\n            const result = findWidgetByUuid(child, widgetUuid, item);\n            if (result) return result;\n        }\n    }\n\n    return null;\n}\n\nfunction findSceneByWidgetUuid(widgetUuid) {\n    for (let scene of _model.scenes) {\n        if (sceneContainsWidget(scene, widgetUuid)) {\n            return scene;\n        }\n    }\n    return null;\n}\n\nfunction sceneContainsWidget(scene, widgetUuid) {\n    const children = scene.widgets || scene.group;\n    if (children) {\n        for (const child of children) {\n            if (child.uuid === widgetUuid) {\n                return true;\n            }\n\n            if (child.type === \"composite\" && sceneContainsWidget(child, widgetUuid)) {\n                return true;\n            }\n        }\n    }\n\n    return false;\n}\n\n// Function to run when a mutation is observed\nasync function handleVisibilityChange(mutationsList, observer) {\n    for (let mutation of mutationsList) {\n        if (mutation.type === \"attributes\" && mutation.attributeName === \"style\") {\n            const visibility = getComputedStyle(mutation.target).visibility;\n            if (visibility === \"visible\") {\n                const sceneNumber = parseInt(mutation.target.className.split(\"-\")[1]);\n\n                if (window.FASTR_FRONTEND.experiences[_experienceId].getCurrentScene() !== sceneNumber) {\n                    debug.log(`Scene ${sceneNumber} is now visible.`);\n                    window.FASTR_FRONTEND.experiences[_experienceId].setCurrentScene(sceneNumber);\n                    window.FASTR_FRONTEND.experiences[_experienceId].reassertModel();\n                }\n            }\n        }\n    }\n}\n\n// Create an observer instance linked to the callback function\nconst observer = new MutationObserver(handleVisibilityChange);\n\n// Options for the observer (which mutations to observe)\nconst config = { attributes: true, attributeFilter: [\"style\"] };\n\n// Find scene divs and start observing them\nconst sceneDivs = Array.from(domRef.querySelectorAll('div[class^=\"scene-\"]')).filter((el) => {\n    const name = el.className.split(\"-\")[1];\n    return !isNaN(parseInt(name));\n});\nsceneDivs.forEach((sceneDiv) => observer.observe(sceneDiv, config));\n\n"}},"stateKey":"newTrigger"}]}]}},"loadedImages":{"6bb5d8c7-39e9-8153-8003-4da90ad1af74":1280}},"page":{"viewportHeight":570,"viewportWidth":1440,"containerHeight":570,"containerWidth":1440}}