(function () { "use strict"; var VERSION = 70; if (window.__homeImmersiveVersion === VERSION) return; window.__homeImmersiveVersion = VERSION; var CFG = window.HOME_IMMERSIVE_CONFIG || {}; var DESKTOP_MQ = CFG.desktopMq || "(min-width: 992px) and (hover: hover) and (pointer: fine)"; var WIDTH_MQ = CFG.widthMq || "(min-width: 992px)"; var SCOPE = CFG.scope || ".hero"; var CMS_SCOPE = CFG.cmsScope || ".features_hero"; var MODE_STORAGE_KEY = CFG.modeStorageKey || "home-hero-view-mode"; var MODE_CAROUSEL = "carousel"; var MODE_FULLSCREEN = "fullscreen"; var THREE_CDN = CFG.threeCdn || "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"; var GSAP_CDN = CFG.gsapCdn || "https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js"; var TILE_GAP = CFG.tileGap != null ? CFG.tileGap : 0.1; var TILE_HEIGHT = CFG.tileHeight != null ? CFG.tileHeight : 2.25; var TILE_WIDTH = CFG.tileWidth != null ? CFG.tileWidth : TILE_HEIGHT * (16 / 9); var TILE_UNIT = TILE_WIDTH + TILE_GAP; var CAM_FOV = CFG.cameraFov != null ? CFG.cameraFov : 50; var CAM_Z = CFG.cameraZ != null ? CFG.cameraZ : 8; var SCROLL_LERP = CFG.scrollLerp != null ? CFG.scrollLerp : 0.12; var DRAG_SENS = CFG.dragSens != null ? CFG.dragSens : 0.012; var INTRO_DUR = CFG.introDur != null ? CFG.introDur : 2.0; var FEATURES_HIDE_DUR = CFG.featuresHideDur != null ? CFG.featuresHideDur : INTRO_DUR * 0.5; var FEATURES_REVEAL_DUR = CFG.featuresRevealDur != null ? CFG.featuresRevealDur : INTRO_DUR * 0.52; var SIDES_REVEAL_DUR = CFG.sidesRevealDur != null ? CFG.sidesRevealDur : INTRO_DUR * 0.38; var ENTER_MORPH_END = CFG.enterMorphEnd != null ? CFG.enterMorphEnd : 0.78; var ENTER_SIDES_START = CFG.enterSidesStart != null ? CFG.enterSidesStart : 0.12; var ENTER_SIDES_END = CFG.enterSidesEnd != null ? CFG.enterSidesEnd : 0.62; var ENTER_SPREAD_START = CFG.enterSpreadStart != null ? CFG.enterSpreadStart : 0.4; var ENTER_HANDOFF_VIDEO_AT = CFG.enterHandoffVideoAt != null ? CFG.enterHandoffVideoAt : 0.58; var EXIT_SIDES_END = CFG.exitSidesEnd != null ? CFG.exitSidesEnd : 0.48; var EXIT_HANDOFF_AT = CFG.exitHandoffAt != null ? CFG.exitHandoffAt : 0.38; var EXIT_MORPH_START = CFG.exitMorphStart != null ? CFG.exitMorphStart : 0.36; var EXIT_SIDES_FADE_END = CFG.exitSidesFadeEnd != null ? CFG.exitSidesFadeEnd : 0.62; var BG_COLOR = CFG.bgColor != null ? CFG.bgColor : 0x050505; var DRAG_LERP = CFG.dragLerp != null ? CFG.dragLerp : 0.1; var WARP_LERP = CFG.warpLerp != null ? CFG.warpLerp : 0.07; var WARP_VEL_LERP = CFG.warpVelLerp != null ? CFG.warpVelLerp : 0.16; var HOLD_MS = CFG.holdMs != null ? CFG.holdMs : 650; var HOLD_MOVE_PX = CFG.holdMovePx != null ? CFG.holdMovePx : 14; var HOLD_RING_LEN = 175.929; var CAPTION_POS_LERP = CFG.captionPosLerp != null ? CFG.captionPosLerp : 0.16; var CAPTION_POS_LERP_DRAG = CFG.captionPosLerpDrag != null ? CFG.captionPosLerpDrag : 0.28; var CAPTION_POS_LERP_INTRO = CFG.captionPosLerpIntro != null ? CFG.captionPosLerpIntro : 0.24; var CAPTION_TEXT_LERP = CFG.captionTextLerp != null ? CFG.captionTextLerp : 0.16; var CAPTION_TEXT_TRAVEL = CFG.captionTextTravel != null ? CFG.captionTextTravel : 14; var TILE_VERT = [ "uniform float uWarpIntensity;", "uniform float uViewportWidth;", "uniform float uTileHalfH;", "varying vec2 vUv;", "void main() {", " vUv = uv;", " vec4 worldPos = modelMatrix * vec4(position, 1.0);", " float screenX = worldPos.x / (uViewportWidth * 0.5);", " float screenXClamped = clamp(screenX, -1.0, 1.0);", " float rollCurve = exp(-screenXClamped * screenXClamped * 6.0);", " float edgeCurve = screenXClamped * screenXClamped;", " float edgeBend = edgeCurve * uWarpIntensity * -0.4;", " float zRoll = rollCurve * uWarpIntensity * 0.8 + edgeBend;", " float scaleY = 1.0 + rollCurve * uWarpIntensity * 0.15;", " float yNorm = position.y / max(uTileHalfH, 0.001);", " float bowCurve = 1.0 - yNorm * yNorm;", " float bow = bowCurve * rollCurve * uWarpIntensity * 0.15;", " vec3 p = position;", " p.y *= scaleY;", " p.x += bow;", " p.z += zRoll;", " gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);", "}", ].join("\n"); var TILE_FRAG = [ "uniform sampler2D uVideo;", "uniform vec2 uCoverScale;", "uniform vec2 uCoverOffset;", "uniform float uGray;", "uniform float uIntroZoom;", "uniform float uOpacity;", "uniform float uBright;", "varying vec2 vUv;", "void main() {", " vec2 centeredUv = vUv - 0.5;", " centeredUv /= max(uIntroZoom, 0.001);", " centeredUv += 0.5;", " vec2 uv = (centeredUv - 0.5) * uCoverScale + 0.5 + uCoverOffset;", " vec4 col = texture2D(uVideo, uv);", " float luma = dot(col.rgb, vec3(0.299, 0.587, 0.114));", " col.rgb = mix(col.rgb, vec3(luma), clamp(uGray, 0.0, 1.0));", " col.rgb *= uBright;", " col.a *= clamp(uOpacity, 0.0, 1.0);", " gl_FragColor = col;", "}", ].join("\n"); var desktopMq = window.matchMedia(DESKTOP_MQ); var widthMq = window.matchMedia(WIDTH_MQ); var reduceMq = window.matchMedia("(prefers-reduced-motion: reduce)"); function isHomePage() { var path = (window.location.pathname || "/").replace(/\/+$/, "") || "/"; return path === "/" || path === "/index.html"; } function canShowHoldUi() { if (CFG.enabled === false) return false; if (!isHomePage()) return false; if (!widthMq.matches) return false; return true; } function canRunImmersive() { return canShowHoldUi(); } function readSavedMode() { try { var mode = localStorage.getItem(MODE_STORAGE_KEY); return mode === MODE_CAROUSEL ? MODE_CAROUSEL : MODE_FULLSCREEN; } catch (err) { return MODE_FULLSCREEN; } } function saveMode(mode) { try { localStorage.setItem(MODE_STORAGE_KEY, mode === MODE_CAROUSEL ? MODE_CAROUSEL : MODE_FULLSCREEN); } catch (err) {} } function canRestoreCarouselMode() { if (!canRunImmersive()) return false; if (reduceMq.matches) return false; return readSavedMode() === MODE_CAROUSEL; } function applyCarouselRestorePending() { if (!canRestoreCarouselMode()) return; document.documentElement.classList.add("is-home-carousel-restore"); document.documentElement.style.setProperty("--hero-list-reveal", "0"); document.documentElement.style.setProperty("--hero-overlay-reveal", "0"); } function clearCarouselRestorePending() { document.documentElement.classList.remove("is-home-carousel-restore"); } function updatePageModeFlags() { if (canShowHoldUi()) { document.documentElement.classList.remove("home-immersive-off"); } else { document.documentElement.classList.add("home-immersive-off"); } } function injectHoldUiStyles() { if (document.getElementById("home-immersive-hold-inline")) return; var style = document.createElement("style"); style.id = "home-immersive-hold-inline"; style.textContent = ".home-mode-hold{--hold-p:0;--glyph-morph:0;position:fixed!important;right:0.75rem!important;bottom:0.75rem!important;z-index:10050!important;display:inline-flex!important;flex-direction:row;align-items:center;gap:0.75rem;padding:0;margin:0;border:0;background:transparent;cursor:pointer;visibility:visible!important;opacity:1!important;pointer-events:auto}" + ".home-mode-hold.is-busy{pointer-events:none}" + '.home-mode-hold-label{font:400 11px/1.2 "Fragment Mono","SFMono-Regular",ui-monospace,monospace;letter-spacing:0.04em;text-transform:uppercase;color:rgba(255,255,255,0.92);white-space:nowrap;user-select:none;text-shadow:0 1px 8px rgba(0,0,0,0.55)}' + ".home-mode-hold-dot{position:relative;flex:0 0 auto;width:56px;height:56px}" + ".home-mode-hold-ring{display:block;width:56px;height:56px;transform:rotate(-90deg)}" + ".home-mode-hold-ring circle{fill:none;stroke:rgba(255,255,255,0.28);stroke-width:1}" + ".home-mode-hold-ring .home-mode-hold-progress{stroke:rgba(255,255,255,0.92);stroke-width:1;stroke-linecap:round;stroke-dasharray:175.929;stroke-dashoffset:175.929}" + ".home-mode-hold-glyphs{--m:var(--glyph-morph,0);--spread:11px;--tight:calc(var(--spread)*(1 - var(--m)*0.52));position:absolute;left:50%;top:50%;width:22px;height:22px;margin:-11px 0 0 -11px;transform:rotate(calc(var(--m)*90deg));pointer-events:none}" + ".home-mode-hold-glyph{position:absolute;left:50%;top:50%;width:4px;height:4px;margin:-2px 0 0 -2px;border-radius:50%;background:rgba(255,255,255,0.9)}" + ".home-mode-hold-glyph--a{transform:translate(calc(-1*var(--tight)),0)}" + ".home-mode-hold-glyph--b{opacity:calc(1 - var(--m));transform:translate(0,0)}" + ".home-mode-hold-glyph--c{transform:translate(var(--tight),0)}" + "@media(max-width:991px){.home-mode-hold{display:none!important}}"; (document.head || document.documentElement).appendChild(style); } updatePageModeFlags(); applyCarouselRestorePending(); function norm(s) { return (s || "").replace(/\s+/g, " ").trim().toLowerCase(); } function slugFromName(name) { return norm(name) .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function getLineName(el) { var n = el && el.querySelector(".name"); return norm(n ? n.textContent : el ? el.textContent : ""); } function getItemName(item) { var n = item && item.querySelector(".name"); if (n) return norm(n.textContent); var videos = item ? item.querySelectorAll("video") : []; for (var i = 0; i < videos.length; i++) { var label = norm(videos[i].textContent); if (label) return label; } return ""; } function getVideo(item) { if (!item) return null; var desktop = item.querySelector("video.desktop"); var mobile = item.querySelector("video.mobile"); if (desktop && mobile) { return getComputedStyle(desktop).display !== "none" ? desktop : mobile; } return item.querySelector("video"); } function playVideoEl(video) { if (!video) return; video.muted = true; video.playsInline = true; if (!video.paused && video.readyState >= 2) return; if (typeof window.playPortfolioHeroVideo === "function" && video.closest(".cms_background .cms_item")) { window.playPortfolioHeroVideo(video); return; } if (typeof window.startPortfolioVideo === "function") { window.startPortfolioVideo(video); return; } video.play().catch(function () {}); } function pauseVideoEl(video) { if (video && !video.paused) video.pause(); } function buildProjects() { if (window.__portfolioPairs && window.__portfolioPairs.length) { return window.__portfolioPairs .map(function (pair, index) { return projectFromPair(pair, index); }) .filter(Boolean); } var scope = document.querySelector(SCOPE) || document.querySelector(CMS_SCOPE) || document; var lineEls = Array.prototype.slice.call( scope.querySelectorAll(".list_projects .line_features, .cms_features .line_features, .line_features"), ); var items = Array.prototype.slice.call(document.querySelectorAll(".background_cases .cms_background .cms_item")); var map = {}; var pairs = []; items.forEach(function (item) { var name = getItemName(item); if (name) map[name] = item; }); lineEls.forEach(function (line) { var row = line.closest(".cms_line") || line; var item = map[getLineName(line)]; if (!item) return; pairs.push({ line: line, item: item, row: row }); }); if (!pairs.length) { var count = Math.min(lineEls.length, items.length); for (var i = 0; i < count; i++) { var ln = lineEls[i]; pairs.push({ line: ln, item: items[i], row: ln.closest(".cms_line") || ln }); } } return pairs .map(function (pair, index) { return projectFromPair(pair, index); }) .filter(Boolean); } function queryProjectMeta(row, line, selectors) { var scopes = []; if (row) scopes.push(row); if (line && line !== row) scopes.push(line); for (var si = 0; si < scopes.length; si++) { var scope = scopes[si]; for (var ai = 0; ai < selectors.length; ai++) { var sel = selectors[ai]; var el = scope.querySelector(sel) || scope.querySelector(".lf-layer " + sel) || scope.querySelector(".lf-layer--white " + sel); if (el) return el; } } return null; } function projectFromPair(pair, index) { var row = pair.row || pair.line; var line = pair.line || row; var item = pair.item; if (!row || !item) return null; var nameEl = queryProjectMeta(row, line, [".name"]); var industryEl = queryProjectMeta(row, line, [".industry", ".description", ".tag", ".category"]); var yearEl = queryProjectMeta(row, line, [".year"]); var name = nameEl ? nameEl.textContent.replace(/\s+/g, " ").trim() : ""; var industry = industryEl ? industryEl.textContent.replace(/\s+/g, " ").trim() : ""; var year = yearEl ? yearEl.textContent.replace(/\s+/g, " ").trim() : ""; var slug = slugFromName(name); return { index: index, name: name, industry: industry, year: year, slug: slug, href: slug ? "/work/" + slug : "", item: item, row: row, line: line, video: getVideo(item), }; } function getActiveProjectIndex(projects) { var active = document.querySelector(".background_cases .cms_item.is-active"); if (!active) return 0; for (var i = 0; i < projects.length; i++) { if (projects[i].item === active) return i; } return 0; } function isBlockedTarget(el) { if (!el || !el.closest) return true; return !!el.closest( "a, button, input, textarea, select, .fixed_menu, .bottom_fixed, .menu_expanded, .expand_btn_primary, .home-mode-hold", ); } // jsDelivr is a single point of failure for GSAP / three.js; fall back to unpkg, share one load per page. function loadScriptWithFallback(urls, isReady) { return new Promise(function (resolve, reject) { var i = 0; (function next() { if (isReady()) return resolve(); if (i >= urls.length) return reject(new Error("script failed: " + urls[0])); var s = document.createElement("script"); s.src = urls[i++]; s.async = true; s.onload = function () { if (isReady()) resolve(); else next(); }; s.onerror = function () { s.remove(); next(); }; document.head.appendChild(s); })(); }); } function fallbackUrl(url) { return url.replace("https://cdn.jsdelivr.net/npm/", "https://unpkg.com/"); } function sharedGsap(url) { if (window.gsap) return Promise.resolve(window.gsap); if (!window.__siteGsapPromise) { window.__siteGsapPromise = loadScriptWithFallback([url, fallbackUrl(url)], function () { return !!window.gsap; }); window.__siteGsapPromise.catch(function () { window.__siteGsapPromise = null; }); } return window.__siteGsapPromise.then(function () { return window.gsap; }); } function loadThree() { if (typeof THREE !== "undefined") return Promise.resolve(); if (loadThree.promise) return loadThree.promise; loadThree.promise = loadScriptWithFallback([THREE_CDN, fallbackUrl(THREE_CDN)], function () { return typeof THREE !== "undefined"; }); loadThree.promise.catch(function () { loadThree.promise = null; }); return loadThree.promise; } function loadGsap() { if (typeof window.gsap !== "undefined") return Promise.resolve(window.gsap); if (loadGsap.promise) return loadGsap.promise; loadGsap.promise = sharedGsap(GSAP_CDN); loadGsap.promise.catch(function () { loadGsap.promise = null; }); return loadGsap.promise; } var projects = []; var scopeEl = null; var immersiveRoot = null; var modeHoldEl = null; var holdProgressEl = null; var metaNameEl = null; var metaYearEl = null; var metaSubEl = null; var metaCaptionEl = null; var metaNameLayers = null; var metaYearLayers = null; var metaSubLayers = null; var featuresHeroEl = null; var captionVecA = null; var captionVecB = null; var captionVecC = null; var hold = { active: false, startedAt: 0, originX: 0, originY: 0, raf: 0, glyphLocked: null, introGlyphDone: false }; var modeHoldBound = false; var immersiveBooted = false; var immersive = { active: false, entering: false, exiting: false, renderer: null, scene: null, camera: null, track: null, tiles: [], tileW: TILE_WIDTH, tileH: TILE_HEIGHT, viewportWidth: 20, raf: 0, selectedIndex: 0, lastSyncedCmsIndex: -1, playbackIndex: -1, videoTextures: null, scrollX: 0, targetScrollX: 0, lastScrollX: 0, scrollVel: 0, warpIntensity: 0, dragging: false, dragX: 0, dragScrollStart: 0, dragMoved: false, dragVel: 0, dragLastX: 0, dragLastT: 0, intro: null, skipEnterIntro: false, restoreEnter: false, introZoom: 1, shellProgress: 0, heroRect: null, loopWidth: 0, eventsBound: false, casesEl: null, casesMorphOrigin: null, casesFromRect: null, casesToRect: null, casesHandoffDone: false, casesExitHandoff: false, casesExitTileRect: null, casesPin: null, casesHandoffFinalized: false, handoffVideoSynced: false, domFilterVideo: null, domFilterSaved: "", enterMorphLocked: false, enterMorphFrom: null, enterMorphTo: null, enterMorphAspect: 0, featuresTween: null, featuresRevealStarted: false, featuresPin: null, caption: { left: 50, top: 85, width: 40, textLayer: 0, textBlend: 1, textIndex: -1, swapFrom: null, swapTo: null, swapping: false, swapFreezeLeft: 50, swapFreezeTop: 85, swapFreezeWidth: 40, anchorLeft: 50, anchorTop: 85, anchorWidth: 40, layoutDirty: true, captionTween: null, enterRevealed: false, gapPx: 11, }, }; function getFeaturesHeroEl() { if (featuresHeroEl && featuresHeroEl.isConnected) return featuresHeroEl; featuresHeroEl = document.querySelector(CMS_SCOPE); return featuresHeroEl; } function captureFeaturesHeroRect() { var el = getFeaturesHeroEl(); if (!el) return null; var r = el.getBoundingClientRect(); if (r.width < 1 || r.height < 1) return null; return { left: r.left, top: r.top, width: r.width, height: r.height }; } function applyFeaturesPinnedLayout(rect) { var el = getFeaturesHeroEl(); if (!el || !rect) return; el.style.position = "fixed"; el.style.left = rect.left + "px"; el.style.top = rect.top + "px"; el.style.width = rect.width + "px"; el.style.height = rect.height + "px"; el.style.right = "auto"; el.style.bottom = "auto"; el.style.margin = "0"; el.style.boxSizing = "border-box"; el.style.zIndex = "170"; el.classList.add("is-home-features-pinned"); document.documentElement.classList.add("is-home-features-pinned"); } function pinFeaturesHeroForTransition() { var el = getFeaturesHeroEl(); if (!el) return false; if (el.classList.contains("is-home-features-pinned") && el.parentNode === document.body) { if (immersive.featuresPin && immersive.featuresPin.rect) { applyFeaturesPinnedLayout(immersive.featuresPin.rect); } return true; } var rect = captureFeaturesHeroRect(); if (!rect) return false; if (!immersive.featuresPin) { immersive.featuresPin = { placeholder: document.createComment("home-features-pin"), parent: null, rect: null }; } var pin = immersive.featuresPin; pin.parent = el.parentNode; pin.parent.insertBefore(pin.placeholder, el); pin.rect = rect; document.body.appendChild(el); applyFeaturesPinnedLayout(rect); return true; } function restoreFeaturesHeroFromPin() { var el = getFeaturesHeroEl(); if (!el || !immersive.featuresPin || !immersive.featuresPin.parent) return; if (el.parentNode !== document.body) return; var pin = immersive.featuresPin; pin.parent.insertBefore(el, pin.placeholder); pin.placeholder.remove(); pin.parent = null; pin.rect = null; el.classList.remove("is-home-features-pinned"); document.documentElement.classList.remove("is-home-features-pinned"); el.style.removeProperty("position"); el.style.removeProperty("left"); el.style.removeProperty("top"); el.style.removeProperty("width"); el.style.removeProperty("height"); el.style.removeProperty("right"); el.style.removeProperty("bottom"); el.style.removeProperty("margin"); el.style.removeProperty("box-sizing"); el.style.removeProperty("z-index"); } function killFeaturesTween() { if (!immersive.featuresTween) return; if (typeof window.gsap !== "undefined" && immersive.featuresTween.kill) { immersive.featuresTween.kill(); } immersive.featuresTween = null; } function setFeaturesHeroHidden(instant) { var el = getFeaturesHeroEl(); if (!el) return; document.documentElement.classList.add("is-home-features-hidden"); if (immersive.featuresTween && !instant) return; killFeaturesTween(); if (typeof window.gsap !== "undefined") { window.gsap.killTweensOf(el); window.gsap.set(el, { autoAlpha: 0, y: instant ? 12 : 0, pointerEvents: "none" }); return; } el.style.opacity = "0"; el.style.visibility = "hidden"; el.style.pointerEvents = "none"; } function resetFeaturesHeroVisible() { var el = getFeaturesHeroEl(); if (!el) return; killFeaturesTween(); document.documentElement.classList.remove("is-home-features-hidden"); if (typeof window.gsap !== "undefined") { window.gsap.killTweensOf(el); window.gsap.set(el, { clearProps: "transform,opacity,visibility,pointerEvents" }); return; } el.style.removeProperty("opacity"); el.style.removeProperty("transform"); el.style.removeProperty("visibility"); el.style.removeProperty("pointer-events"); } function animateFeaturesHeroHide(introStartTime) { var el = getFeaturesHeroEl(); if (!el) return Promise.resolve(); document.documentElement.classList.add("is-home-features-hidden"); immersive.featuresRevealStarted = false; return loadGsap() .then(function (gsap) { killFeaturesTween(); gsap.killTweensOf(el); var elapsed = introStartTime ? (performance.now() - introStartTime) / 1000 : 0; var duration = Math.max(0.15, FEATURES_HIDE_DUR - elapsed); immersive.featuresTween = gsap.to(el, { autoAlpha: 0, duration: duration, ease: "power2.inOut", overwrite: "auto", onComplete: function () { gsap.set(el, { pointerEvents: "none" }); immersive.featuresTween = null; }, }); }) .catch(function () { el.style.opacity = "0"; el.style.visibility = "hidden"; el.style.pointerEvents = "none"; }); } function animateFeaturesHeroReveal() { var el = getFeaturesHeroEl(); if (!el || immersive.featuresRevealStarted) return Promise.resolve(); immersive.featuresRevealStarted = true; return loadGsap() .then(function (gsap) { killFeaturesTween(); gsap.killTweensOf(el); immersive.featuresTween = gsap.fromTo( el, { autoAlpha: 0 }, { autoAlpha: 1, duration: FEATURES_REVEAL_DUR, ease: "power2.out", overwrite: "auto", onComplete: function () { gsap.set(el, { clearProps: "transform,opacity,visibility,pointerEvents" }); document.documentElement.classList.remove("is-home-features-hidden"); immersive.featuresTween = null; }, }, ); }) .catch(function () { el.style.opacity = "1"; el.style.visibility = "visible"; el.style.transform = ""; el.style.pointerEvents = ""; document.documentElement.classList.remove("is-home-features-hidden"); }); } function makeCubicBezier(x1, y1, x2, y2) { var ax = 3 * x1 - 3 * x2 + 1; var bx = 3 * x2 - 6 * x1; var cx = 3 * x1; var ay = 3 * y1 - 3 * y2 + 1; var by = 3 * y2 - 6 * y1; var cy = 3 * y1; function sampleCurveX(t) { return ((ax * t + bx) * t + cx) * t; } function sampleCurveY(t) { return ((ay * t + by) * t + cy) * t; } function sampleCurveDX(t) { return (3 * ax * t + 2 * bx) * t + cx; } function solveCurveX(x) { var t2 = x; var i; for (i = 0; i < 8; i++) { var dx = sampleCurveX(t2) - x; if (Math.abs(dx) < 1e-7) return t2; var d = sampleCurveDX(t2); if (Math.abs(d) < 1e-6) break; t2 -= dx / d; } var t0 = 0; var t1 = 1; t2 = x; while (t0 < t1) { var sx = sampleCurveX(t2); if (Math.abs(sx - x) < 1e-7) return t2; if (x > sx) t0 = t2; else t1 = t2; t2 = (t0 + t1) * 0.5; } return t2; } return function (t) { if (t <= 0) return 0; if (t >= 1) return 1; return sampleCurveY(solveCurveX(t)); }; } var EASE_LOCO = makeCubicBezier(0.22, 1, 0.36, 1); function smoothRange(t, start, end) { return EASE_LOCO(clamp((t - start) / Math.max(0.0001, end - start), 0, 1)); } function applyFeaturesHeroEnterProgress(rawP) { var el = getFeaturesHeroEl(); if (!el) return; killFeaturesTween(); pinFeaturesHeroForTransition(); var hideP = immersive.restoreEnter ? 1 : smoothRange(rawP, 0, 0.34); var op = 1 - hideP; el.style.opacity = String(op); el.style.visibility = op < 0.02 ? "hidden" : "visible"; el.style.transform = ""; el.style.pointerEvents = "none"; if (hideP > 0.02) { document.documentElement.classList.add("is-home-features-hidden"); } } function applyFeaturesHeroExitProgress(rawP) { var el = getFeaturesHeroEl(); if (!el) return; killFeaturesTween(); pinFeaturesHeroForTransition(); var revealP = smoothRange(rawP, 0.48, 0.98); el.style.opacity = String(revealP); el.style.visibility = revealP < 0.02 ? "hidden" : "visible"; el.style.transform = ""; el.style.pointerEvents = revealP > 0.55 ? "auto" : "none"; if (revealP > 0.98) { document.documentElement.classList.remove("is-home-features-hidden"); } } function clearFeaturesHeroInlineStyles() { restoreFeaturesHeroFromPin(); var el = getFeaturesHeroEl(); if (!el) return; el.style.removeProperty("opacity"); el.style.removeProperty("visibility"); el.style.removeProperty("transform"); el.style.removeProperty("pointer-events"); document.documentElement.classList.remove("is-home-features-hidden"); } function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } function lerp(a, b, t) { return a + (b - a) * t; } function cubicEaseInOut(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function visibleWorldHeight(fovDeg, z) { return 2 * Math.tan((fovDeg * Math.PI) / 180 / 2) * z; } function updateHoldGlyphMorph(holdP) { if (!modeHoldEl) return; if (hold.glyphLocked != null && !hold.active) { modeHoldEl.style.setProperty("--glyph-morph", String(hold.glyphLocked)); return; } var carousel = !!immersive.active; var morph; if (hold.active && holdP != null) { morph = carousel ? 1 - holdP : holdP; } else { morph = carousel ? 1 : 0; } modeHoldEl.style.setProperty("--glyph-morph", String(clamp(morph, 0, 1))); } function updateHoldGlyphFromIntro(rawP, closing) { if (!modeHoldEl || hold.active) return; if (hold.glyphLocked != null) { modeHoldEl.style.setProperty("--glyph-morph", String(hold.glyphLocked)); return; } if (hold.introGlyphDone) return; if (immersive.restoreEnter && !closing) { modeHoldEl.style.setProperty("--glyph-morph", "1"); return; } var morph = closing ? 1 - rawP : rawP; modeHoldEl.style.setProperty("--glyph-morph", String(clamp(morph, 0, 1))); } function updateModeHoldState() { if (!modeHoldEl) return; var busy = immersive.entering || immersive.exiting || (immersive.intro && immersive.intro.active); modeHoldEl.classList.toggle("is-busy", !!busy); modeHoldEl.classList.toggle("is-carousel-mode", !!immersive.active); updateHoldGlyphMorph(null); } function ensureModeHoldUi() { if (!canShowHoldUi()) return null; injectHoldUiStyles(); if ( modeHoldEl && (!modeHoldEl.querySelector(".home-mode-hold-glyphs") || !modeHoldEl.querySelector(".home-mode-hold-label")) ) { if (modeHoldEl.parentNode) modeHoldEl.parentNode.removeChild(modeHoldEl); modeHoldEl = null; holdProgressEl = null; } if (modeHoldEl) return modeHoldEl; modeHoldEl = document.createElement("button"); modeHoldEl.type = "button"; modeHoldEl.className = "home-mode-hold"; modeHoldEl.setAttribute("aria-label", "Click and hold to switch view mode"); modeHoldEl.innerHTML = 'Click and Hold' + '' + '" + '" + ""; holdProgressEl = modeHoldEl.querySelector(".home-mode-hold-progress"); var mount = document.body || document.documentElement; if (mount && !modeHoldEl.parentNode) mount.appendChild(modeHoldEl); bindModeHold(); updateModeHoldState(); return modeHoldEl; } function setHoldProgress(p) { if (!holdProgressEl) return; var clamped = clamp(p, 0, 1); holdProgressEl.style.strokeDashoffset = String(HOLD_RING_LEN * (1 - clamped)); modeHoldEl.style.setProperty("--hold-p", String(clamped)); updateHoldGlyphMorph(clamped); } function cancelHold() { if (!hold.active) return; hold.active = false; hold.startedAt = 0; hold.glyphLocked = null; if (hold.raf) { cancelAnimationFrame(hold.raf); hold.raf = 0; } if (modeHoldEl) modeHoldEl.classList.remove("is-holding"); setHoldProgress(0); } function finishHoldSwitch() { var targetMorph = immersive.active ? 0 : 1; hold.glyphLocked = targetMorph; hold.introGlyphDone = true; hold.active = false; hold.startedAt = 0; if (hold.raf) { cancelAnimationFrame(hold.raf); hold.raf = 0; } if (modeHoldEl) { modeHoldEl.classList.remove("is-holding"); modeHoldEl.style.setProperty("--glyph-morph", String(targetMorph)); } if (holdProgressEl) holdProgressEl.style.strokeDashoffset = String(HOLD_RING_LEN); if (modeHoldEl) modeHoldEl.style.setProperty("--hold-p", "0"); } function completeHoldSwitch() { finishHoldSwitch(); if (immersive.active) { saveMode(MODE_FULLSCREEN); exitImmersive(); return; } saveMode(MODE_CAROUSEL); enterImmersive(); } function tickHold() { if (!hold.active) { hold.raf = 0; return; } var p = (performance.now() - hold.startedAt) / HOLD_MS; setHoldProgress(p); if (p >= 1) { hold.active = false; hold.raf = 0; completeHoldSwitch(); return; } hold.raf = requestAnimationFrame(tickHold); } function startHold(e) { if (immersive.entering || immersive.exiting) return; if (immersive.intro && immersive.intro.active) return; ensureModeHoldUi(); hold.active = true; hold.startedAt = performance.now(); hold.originX = e.clientX; hold.originY = e.clientY; modeHoldEl.classList.add("is-holding"); if (!hold.raf) hold.raf = requestAnimationFrame(tickHold); if (!immersive.active && !sceneCoreMatchesProjects()) { requestAnimationFrame(function () { if (!hold.active) return; var t = performance.now(); warmImmersiveScene(true); // The ring pauses for the build instead of jumping ahead. if (hold.active) hold.startedAt += performance.now() - t; }); } } function onModeHoldMove(e) { if (!hold.active) return; var dx = e.clientX - hold.originX; var dy = e.clientY - hold.originY; if (dx * dx + dy * dy > HOLD_MOVE_PX * HOLD_MOVE_PX) cancelHold(); } function bindModeHold() { if (modeHoldBound || !modeHoldEl) return; modeHoldBound = true; modeHoldEl.addEventListener("pointerdown", function (e) { e.preventDefault(); startHold(e); }); modeHoldEl.addEventListener("pointerenter", function (e) { if (e.pointerType === "mouse" && !immersive.active) warmImmersiveScene(); }); modeHoldEl.addEventListener("pointermove", onModeHoldMove); modeHoldEl.addEventListener("pointerup", cancelHold); modeHoldEl.addEventListener("pointercancel", cancelHold); modeHoldEl.addEventListener("pointerleave", cancelHold); } function scrollForRawIndex(rawIndex) { return (-rawIndex + (projects.length - 1) / 2) * TILE_UNIT; } function rawIndexFromScroll(scrollX) { return Math.round(-scrollX / TILE_UNIT + (projects.length - 1) / 2); } function logicalIndexFromRaw(rawIndex) { var count = projects.length; if (!count) return 0; return ((rawIndex % count) + count) % count; } function scrollBounds() { var count = projects.length; if (count <= 1) { var s = count ? scrollForRawIndex(0) : 0; return { min: s, max: s }; } return { min: scrollForRawIndex(count - 1), max: scrollForRawIndex(0) }; } function clampScrollValue(scrollX) { var b = scrollBounds(); return clamp(scrollX, b.min, b.max); } function indexFromScroll(scrollX) { var count = projects.length; if (!count) return 0; return clamp(rawIndexFromScroll(scrollX), 0, count - 1); } function setSelectedIndex(index) { var count = projects.length; if (!count) return; var next = clamp(index, 0, count - 1); if (next === immersive.selectedIndex && Math.abs(immersive.targetScrollX - scrollForRawIndex(next)) < 0.001) { return; } immersive.selectedIndex = next; immersive.targetScrollX = scrollForRawIndex(next); syncCmsIfCarouselSettled(); } function syncCmsIfCarouselSettled() { if (!immersive.active || (immersive.intro && immersive.intro.active)) return; if (!immersive.casesHandoffDone) return; if (immersive.lastSyncedCmsIndex === immersive.selectedIndex) return; immersive.lastSyncedCmsIndex = immersive.selectedIndex; syncCmsToProject(projects[immersive.selectedIndex], { skipAutoplayUi: true }); } function setHeroListReveal(p) { document.documentElement.style.setProperty("--hero-list-reveal", String(clamp(p, 0, 1))); } function setHeroOverlayReveal(p) { document.documentElement.style.setProperty("--hero-overlay-reveal", String(clamp(p, 0, 1))); } function setHeroUiReveal(listP, overlayP) { setHeroListReveal(listP); setHeroOverlayReveal(overlayP != null ? overlayP : listP); } function captureHeroRect() { var hero = scopeEl || document.querySelector(SCOPE); if (hero) { var r = hero.getBoundingClientRect(); immersive.heroRect = { left: r.left, top: r.top, width: r.width, height: r.height }; return; } immersive.heroRect = { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }; } function captureCasesFromRect() { var bg = document.querySelector(".background_cases"); if (!bg) { captureHeroRect(); immersive.casesFromRect = immersive.heroRect; return; } var r = bg.getBoundingClientRect(); immersive.casesFromRect = { left: r.left, top: r.top, width: r.width, height: r.height }; } function copyRect(r) { return { left: r.left, top: r.top, width: r.width, height: r.height }; } function tileAspect() { var w = immersive.tileW || TILE_WIDTH; var h = immersive.tileH || TILE_HEIGHT; return w / Math.max(h, 0.001); } function cropRectToAspect(rect, aspect) { if (!rect || !aspect || aspect <= 0) return rect; var w = rect.width; var h = rect.height; if (w <= 0 || h <= 0) return rect; var rectAspect = w / h; var outW; var outH; if (rectAspect > aspect) { outH = h; outW = h * aspect; } else { outW = w; outH = w / aspect; } return { left: rect.left + (w - outW) * 0.5, top: rect.top + (h - outH) * 0.5, width: outW, height: outH }; } function snapRectToAspect(rect, aspect) { if (!rect || !aspect || aspect <= 0) return rect; var cx = rect.left + rect.width * 0.5; var cy = rect.top + rect.height * 0.5; var w = Math.max(8, rect.width); var h = w / aspect; return { left: cx - w * 0.5, top: cy - h * 0.5, width: w, height: h }; } function clearEnterMorphRects() { immersive.enterMorphLocked = false; immersive.enterMorphFrom = null; immersive.enterMorphTo = null; immersive.enterMorphAspect = 0; } function lockEnterMorphRects(activeIdx, count) { if (immersive.enterMorphLocked) return; prepareActiveTileForProjection(activeIdx, count); var to = projectActiveTileScreenRect(); if (!to) { prepareCasesTargetRect(); to = immersive.casesToRect || computeCarouselCenterTileRect(); } if (!to) return; var fromSource = immersive.casesMorphOrigin || immersive.casesFromRect; if (!fromSource) return; immersive.enterMorphFrom = copyRect(fromSource); immersive.enterMorphTo = copyRect(to); immersive.enterMorphAspect = to.width / Math.max(to.height, 1); immersive.enterMorphLocked = true; } function morphEnterRects(activeIdx, count) { if (activeIdx != null && count != null) { lockEnterMorphRects(activeIdx, count); } if (immersive.enterMorphLocked && immersive.enterMorphFrom && immersive.enterMorphTo) { return { from: immersive.enterMorphFrom, to: immersive.enterMorphTo }; } var fromSource = immersive.casesMorphOrigin || immersive.casesFromRect; var to = projectActiveTileScreenRect(); if (!to) { prepareCasesTargetRect(); to = immersive.casesToRect || computeCarouselCenterTileRect(); } if (!to) return { from: null, to: null }; var from = fromSource ? copyRect(fromSource) : null; return { from: from, to: to }; } function applyCasesMorphLayout(rect) { var el = immersive.casesEl; if (!el || !rect) return; el.style.position = "fixed"; el.style.left = rect.left + "px"; el.style.top = rect.top + "px"; el.style.width = rect.width + "px"; el.style.height = rect.height + "px"; el.style.right = "auto"; el.style.bottom = "auto"; el.style.margin = "0"; el.style.zIndex = "160"; el.style.overflow = "hidden"; el.style.contain = "strict"; el.style.transformOrigin = "center center"; el.style.transform = "none"; el.style.visibility = "visible"; el.style.pointerEvents = "none"; } function applyCasesMorphTransform(from, to, t) { var el = immersive.casesEl; if (!el || !from || !to) return; if (t <= 0) { applyCasesMorphLayout(from); return; } if (t >= 1) { applyCasesMorphLayout(to); return; } applyCasesMorphLayout({ left: lerp(from.left, to.left, t), top: lerp(from.top, to.top, t), width: lerp(from.width, to.width, t), height: lerp(from.height, to.height, t), }); } function applyCasesMorph(from, to, t) { applyCasesMorphTransform(from, to, t); } function projectWorldTileRect(cx, cy, cz, tileW, tileH) { if (!immersive.renderer || !immersive.camera || typeof THREE === "undefined") return null; if (!captionVecA) { captionVecA = new THREE.Vector3(); captionVecB = new THREE.Vector3(); captionVecC = new THREE.Vector3(); } var pts = [ [cx - tileW / 2, cy + tileH / 2, cz], [cx + tileW / 2, cy + tileH / 2, cz], [cx - tileW / 2, cy - tileH / 2, cz], [cx + tileW / 2, cy - tileH / 2, cz], ]; var canvas = immersive.renderer.domElement; var cr = canvas.getBoundingClientRect(); if (!cr.width || !cr.height) { cr = { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }; } var minX = Infinity; var minY = Infinity; var maxX = -Infinity; var maxY = -Infinity; pts.forEach(function (p) { captionVecA.set(p[0], p[1], p[2]).project(immersive.camera); var px = (captionVecA.x * 0.5 + 0.5) * cr.width + cr.left; var py = (-captionVecA.y * 0.5 + 0.5) * cr.height + cr.top; minX = Math.min(minX, px); minY = Math.min(minY, py); maxX = Math.max(maxX, px); maxY = Math.max(maxY, py); }); return { left: minX, top: minY, width: Math.max(8, maxX - minX), height: Math.max(8, maxY - minY) }; } function computeCarouselCenterTileRect() { return projectWorldTileRect(0, 0, 0, immersive.tileW, immersive.tileH); } function prepareCasesTargetRect() { if (!immersive.track) return null; var idx = immersive.selectedIndex; var scrollTarget = scrollForRawIndex(idx); immersive.track.position.x = scrollTarget; immersive.scrollX = scrollTarget; immersive.targetScrollX = scrollTarget; updateCamera(); immersive.casesToRect = computeCarouselCenterTileRect(); return immersive.casesToRect; } function setCanvasMorphOpacity(alpha) { if (!immersive.renderer || !immersive.renderer.domElement) return; var a = clamp(alpha, 0, 1); immersive.renderer.domElement.style.opacity = String(a); immersive.renderer.domElement.style.visibility = a > 0.001 ? "visible" : "hidden"; } function setCanvasDuringMorph(visible) { setCanvasMorphOpacity(visible ? 1 : 0); } function setIntroTilesVisible(visible) { immersive.tiles.forEach(function (tile) { setTileOpacity(tile, visible ? 1 : 0); }); } function detachCasesForMorph(el) { if (!el || el.parentNode === document.body) return; if (!immersive.casesPin) { immersive.casesPin = { placeholder: document.createComment("home-cases-pin"), parent: null }; } var pin = immersive.casesPin; pin.parent = el.parentNode; pin.parent.insertBefore(pin.placeholder, el); document.body.appendChild(el); } function restoreCasesFromMorph(el) { if (!el || !immersive.casesPin || !immersive.casesPin.parent) return; if (el.parentNode !== document.body) return; var pin = immersive.casesPin; pin.parent.insertBefore(el, pin.placeholder); pin.placeholder.remove(); pin.parent = null; } function pinBackgroundCases(atRect) { var el = document.querySelector(".background_cases"); if (!el) return false; immersive.casesEl = el; var r = atRect ? { left: atRect.left, top: atRect.top, width: atRect.width, height: atRect.height } : (function () { var box = el.getBoundingClientRect(); return { left: box.left, top: box.top, width: box.width, height: box.height }; })(); immersive.casesMorphOrigin = r; if (!atRect) immersive.casesFromRect = r; detachCasesForMorph(el); document.documentElement.classList.add("is-home-cases-morph"); el.classList.add("is-home-cases-morphing"); el.classList.remove("is-home-cases-in-carousel"); applyCasesMorphLayout(r); return true; } function markCasesEnterHandoff() { if (immersive.casesHandoffDone) return; immersive.casesHandoffDone = true; syncHandoffVideo(); immersive.handoffVideoSynced = true; document.documentElement.classList.add("is-home-carousel-view"); pauseNonSelectedCmsVideos(); } function finalizeCasesHandoffDom() { if (immersive.casesHandoffFinalized || !immersive.casesEl) return; immersive.casesHandoffFinalized = true; immersive.casesEl.classList.remove("is-home-cases-morphing"); immersive.casesEl.classList.add("is-home-cases-in-carousel"); immersive.casesEl.style.cssText = ""; parkCasesOnScreen(immersive.casesEl); restoreDomVideoFilter(); restoreCasesFromMorph(immersive.casesEl); document.documentElement.classList.remove("is-home-cases-morph"); setCanvasMorphOpacity(1); } // Chrome stops presenting frames of a