1/* ============================================================ 2 Influeri â Layout C · "Spotlight" home orchestrator (classic IIFE / var) 3 Composes the shared components into the spotlight landing page: 4 · HERO: the hv3 Spotlight Marquee (cursor wash + dual marquees, driven by 5 hero-variants.js which guards on .hv3) with a heart-mark CENTREPIECE in 6 the `spotlight` interaction â the cursor-following light is the signature 7 of this layout (distinct from Layout B's `pulse`). 8 · bg-fx: the interaction-activated ambient field, armed on load. 9 · SECTIONS (canonical set): logo-marquee, the heart-mark band, the process 10 guided WALKTHROUGH ([data-proc-walkthrough]), the flow-blocks live demo 11 ([data-flow-blocks] + [data-fb-match]/[data-fb-review]/[data-fb-launch]), 12 the features grid, the case-cards CAROUSEL ([data-cc-carousel]), and the 13 testimonials carousel ([data-quotes]) â all of those component scripts 14 self-mount by their data-* hooks; this file only owns the hero heart-mark 15 band + page chrome. 16 · NAV: Pages dropdown + mobile sheet · scroll-reveal · footer year. 17 Reduced-motion: heart-mark + bg-fx receive reduce=true (no cursor follow, 18 no ambient â static crossfade); the hv3 wash/marquees freeze via CSS; the 19 self-mounting components each own their own reduce path; reveal shows 20 everything immediately. 21 Load order (page): gsap â heart-config.js â heart-mark.js â bg-fx.js â 22 studio-deck.js â logo-marquee.js â process.js â flow-blocks.js â 23 case-cards.js â testimonials.js â hero-variants.js â 24 home-spotlight.js (this, last). 25 ============================================================ */ 26(function () { 27 "use strict"; 28 29 var gsap = window.gsap; 30 var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 31 var doc = document; 32 33 doc.documentElement.classList.add("reveal-ready"); 34 35 /* ---- footer year ---- */ 36 var yearEl = doc.querySelector("[data-year]"); 37 if (yearEl) { 38 var y = new Date().getFullYear(); 39 if (y >= 2024) yearEl.textContent = "Copyright © " + y + " Influeri AB"; 40 } 41 42 /* ======================= HERO heart-mark (spotlight) ======================= */ 43 /* The decorative avatar-heart, mounted in the `spotlight` interaction so a 44 soft pinkâgold light follows the cursor and tiles under it brighten/lift â 45 the signature visual of Layout C. Composes heart-mark.js (which composes 46 the heart engine); never forks it. */ 47 var markEl = doc.querySelector("[data-mark]"); 48 if (gsap && markEl && typeof createHeartMark === "function") { 49 var stage = markEl.querySelector("[data-stage]"); 50 var tilesBox = markEl.querySelector("[data-tiles]"); 51 var cfg = {}; 52 if (markEl.dataset.config) { 53 try { cfg = JSON.parse(markEl.dataset.config); } catch (e) { cfg = {}; } 54 } 55 /* Spotlight-ONLY creator set: authentic Instagram/TikTok-style lifestyle 56 portraits (assets/creators-spotlight/s001..s216.jpg) â full-frame, diverse, 57 real outfits/settings â instead of the shared face-only c01..c60 default. 58 There are MORE images than heart tiles (~203) so every tile gets a distinct 59 photo â zero repetition (paired with featCount:0 in the mark's data-config, 60 which removes the feature tiles that would otherwise reuse an image). 61 Scoped to this page by passing `creators` to createHeartMark; the shared 62 heart-mark.js / heart-config.js and the other home pages are untouched. 63 64 Real creator photos now lead this set (creatorMosaicSet in 65 components/data-creators.js hands back N DISTINCT images â every fetched 66 Instagram avatar first, interim portraits filling the tail), so the mark 67 gains real faces on each fetch while the zero-repetition rule above 68 still holds. Falls back to the plain interim run if the resolver is 69 absent. */ 70 var spotCreators = window.creatorMosaicSet 71 ? window.creatorMosaicSet(216) 72 : Array.from({ length: 216 }, function (_, i) { 73 return { img: "assets/creators-spotlight/s" + String(i + 1).padStart(3, "0") + ".jpg" }; 74 }); 75 var mark = createHeartMark({ 76 stage: stage, 77 tilesBox: tilesBox, 78 gsap: gsap, 79 reduce: reduce, 80 interaction: markEl.dataset.mark || "spotlight", 81 config: cfg, 82 creators: spotCreators 83 }); 84 mark.enter(); 85 markEl._mark = mark; 86 } 87 88 /* ======================= bg-fx â NOT MOUNTED (directed 2026-09-16) ======================= 89 The background stays pure white. bg-fx draws its blobs in "11,11,11" and 90 "120,120,120" â near-black and mid-grey, not the pinkâgold its own header 91 claims â so once they were actually on screen they read as dark shapes 92 drifting over the white page, which is against white-mode discipline. 93 94 Why they were not on screen before: bg-fx sized its canvas to 95 document.scrollHeight (~20 500px here) while bg-fx.css pins the element 96 position:fixed to one viewport, so the blobs were seeded down a field ~22x 97 taller than the band anyone could see and almost none of them ever showed. 98 That sizing bug is fixed in components/bg-fx.js â which is exactly what 99 made them visible, and why this mount is now off rather than left to a bug 100 to suppress it. 101 102 index.html armed bg-fx TWICE, here and in flow/flow-director.js â two
103 full-viewport canvases, each with its own rAF loop. Both are off. 104 105 To bring it back: restore the two lines below AND give createBgFx a light 106 tint via its `opacity` / a colour pass in the component, or it will be grey 107 shapes on white again. 108 var bg = createBgFx({ root: doc.body, gsap: gsap, reduce: reduce }); 109 bg.arm(); 110 ========================================================================= */ 111 112 /* ======================= NAV: Pages dropdown ======================= */ 113 var drop = doc.querySelector("[data-drop]"); 114 /* Bound once: some pages load two page scripts (about.html pulls about.js 115 *and* features.js), which would otherwise stack duplicate nav listeners. */ 116 if (drop && !drop.hasAttribute("data-drop-bound")) { 117 var dToggle = drop.querySelector("[data-drop-toggle]"); 118 drop.setAttribute("data-drop-bound", ""); 119 var dropTimer = null; 120 var dropOpen = function () { return drop.classList.contains("is-open"); }; 121 var setDrop = function (state) { 122 if (dropTimer) { clearTimeout(dropTimer); dropTimer = null; } 123 drop.classList.toggle("is-open", state); 124 if (dToggle) dToggle.setAttribute("aria-expanded", state ? "true" : "false"); 125 }; 126 /* Deferred close: a cursor that clips the panel's corner, or crosses the 127 trigger->panel seam, must not snap the menu shut mid-reach. CSS :hover / 128 :focus-within own the visible state; this keeps .is-open and 129 aria-expanded in step with it. */ 130 var closeDropSoon = function () { 131 if (dropTimer) clearTimeout(dropTimer); 132 dropTimer = setTimeout(function () { dropTimer = null; setDrop(false); }, 260); 133 }; 134 var popLinks = function () { return [].slice.call(drop.querySelectorAll(".nav__pop a")); }; 135 /* Escape returns focus to the trigger, which is itself inside .nav__drop -- 136 without this latch that programmatic focus fires focusin and reopens the 137 panel the user just dismissed. Cleared as soon as focus or the pointer 138 genuinely re-engages. */ 139 var escHold = false; 140 drop.addEventListener("mouseenter", function () { escHold = false; setDrop(true); }); 141 drop.addEventListener("mouseleave", closeDropSoon); 142 drop.addEventListener("focusin", function () { if (!escHold) setDrop(true); }); 143 drop.addEventListener("focusout", function (e) { if (!drop.contains(e.relatedTarget)) { escHold = false; setDrop(false); } }); 144 if (dToggle) dToggle.addEventListener("click", function (e) { e.preventDefault(); escHold = false; setDrop(!dropOpen()); }); 145 /* The markup declares role="menu", so honour it: arrows, Home/End, Escape. */ 146 drop.addEventListener("keydown", function (e) { 147 var links = popLinks(), i = links.indexOf(doc.activeElement), next = -1; 148 if (e.key === "Escape") { 149 if (!dropOpen()) return; 150 e.stopPropagation(); 151 escHold = true; 152 setDrop(false); 153 if (dToggle && drop.contains(doc.activeElement)) dToggle.focus(); 154 return; 155 } 156 if (e.key === "ArrowDown") next = i < 0 ? 0 : Math.min(i + 1, links.length - 1); 157 else if (e.key === "ArrowUp") { 158 e.preventDefault(); e.stopPropagation(); 159 if (i <= 0) { if (dToggle) dToggle.focus(); return; } 160 next = i - 1; 161 } 162 else if (e.key === "Home" && i >= 0) next = 0; 163 else if (e.key === "End" && i >= 0) next = links.length - 1; 164 else return; 165 e.preventDefault(); e.stopPropagation(); 166 escHold = false; 167 setDrop(true); 168 if (links[next]) links[next].focus(); 169 }); 170 doc.addEventListener("click", function (e) { if (!drop.contains(e.target)) setDrop(false); }); 171 /* Esc also closes a panel opened by hover, where focus never entered it. */ 172 doc.addEventListener("keydown", function (e) { 173 if (e.key === "Escape" && dropOpen() && !drop.contains(doc.activeElement)) setDrop(false); 174 }); 175 } 176 177 /* ======================= NAV: mobile sheet ======================= */ 178 var burger = doc.querySelector("[data-burger]"); 179 var sheet = doc.querySelector("[data-sheet]"); 180 if (burger && sheet) { 181 var sheetClose = sheet.querySelector("[data-sheet-close]"); 182 var sheetScrollY = 0; 183 var setSheet = function (state) { 184 sheet.classList.toggle("is-open", state); 185 sheet.setAttribute("aria-hidden", state ? "false" : "true"); 186 burger.setAttribute("aria-expanded", state ? "true" : "false"); 187 /* iOS Safari still rubber-bands / scrolls the page behind a 188 body{overflow:hidden} sheet -- pin body to its scroll position 189 with position:fixed instead, which iOS actually honors. */ 190 if (state) { 191 sheetScrollY = window.scrollY || doc.documentElement.scrollTop; 192 doc.body.style.position = "fixed"; 193 doc.body.style.top = -sheetScrollY + "px"; 194 doc.body.style.left = "0"; 195 doc.body.style.right = "0"; 196 doc.body.style.overflow = "hidden"; 197 } else { 198 doc.body.style.position = ""; 199 doc.body.style.top = ""; 200 doc.body.style.left = ""; 201 doc.body.style.right = ""; 202 doc.body.style.overflow = ""; 203 window.scrollTo(0, sheetScrollY); 204 } 205 if (state) { var f = sheet.querySelector("a, button"); if (f) f.focus({ preventScroll: true }); } 206 }; 207 burger.addEventListener("click", function () { setSheet(true); }); 208 if (sheetClose) sheetClose.addEventListener("click", function () { setSheet(false); burger.focus({ preventScroll: true }); }); 209 sheet.querySelectorAll("a").forEach(function (a) { a.addEventListener("click", function () { setSheet(false); }); }); 210 window.__influeriCloseSheet = function () { 211 if (sheet.classList.contains("is-open")) { setSheet(false); burger.focus({ preventScroll: true }); return true; } 212 return false;
213 }; 214 } 215 216 /* ======================= Global Esc (close the mobile sheet) ======================= */ 217 doc.addEventListener("keydown", function (e) { 218 if (e.key !== "Escape") return; 219 if (window.__influeriCloseSheet) window.__influeriCloseSheet(); 220 }); 221 222 /* ======================= Explore · Block 1 override (spotlight only) ======================= 223 Feedback round (PDF #27): Block 1 should show creator PROFILE CARDS appearing 224 from the prompt â NOT the Estimated Results dashboard. flow-blocks.js (shared) 225 still builds Block 2 (review deck) + Block 3 (launch); the page hands Block 1's 226 host the spotlight-only hook [data-spot-match] so flow-blocks skips it and we own 227 it here. We COMPOSE existing CSS only: the .fb-match / .prompt-card chrome and the 228 .ru__creator card (both already on the page) â no shared JS/CSS is forked. 229 Sequence: type brief -> "matchingâ¦" beat -> prompt slides up & out -> a single 230 column of creator cards rises in (staggered) -> hold -> loop. Hover pauses; 231 reduced-motion shows the static end-state (prompt hidden, column shown). */ 232 (function () { 233 var host = doc.querySelector("[data-spot-match]"); 234 if (!host || !gsap) return; 235 236 var BRIEF = "Find fashion creators in the US"; 237 // Fashion-focused, diverse mix (styles / genders / backgrounds). Photos 238 // resolve through components/data-creators.js, so each of these five shows 239 // its real Instagram avatar as soon as that handle has been fetched and 240 // keeps an interim portrait until then. 241 var CREATORS = [ 242 { h: "@jackiegustafssonn", f: "184k", e: "6.4%" }, 243 { h: "@alicejoythomas", f: "212k", e: "5.1%" }, 244 { h: "@erickshots", f: "98k", e: "7.8%" }, 245 { h: "@chandsgarms", f: "76k", e: "8.6%" }, 246 { h: "@emmachamberlain", f: "1.2M", e: "4.9%" } 247 ]; 248 CREATORS.forEach(function (c, i) { 249 var u = c.h.replace(/^@/, ""); 250 c.img = window.creatorPhoto 251 ? window.creatorPhoto(u, i) 252 : "assets/creators-spotlight/s" + String(((i * 7) % 216) + 1).padStart(3, "0") + ".jpg"; 253 }); 254 255 var cardsHTML = CREATORS.map(function (c) { 256 return '<article class="ru__creator spot-col__card">' + 257 '<span class="ru__avatar ru__avatar--photo"><img src="' + c.img + '" alt="' + c.h + '" loading="lazy"></span>' + 258 '<div class="ru__cmeta"><p class="ru__cname">' + c.h + "</p>" + 259 '<p class="ru__cstat">Followers: ' + c.f + "</p>" + 260 '<p class="ru__cstat">Engagement: ' + c.e + "</p></div></article>"; 261 }).join(""); 262 263 host.innerHTML = 264 '<div class="fb-stage fb-match">' + 265 '<div class="fb-match__panel">' + 266 '<div class="prompt-card fb-match__prompt" data-spot-promptcard>' + 267 '<textarea class="prompt-card__input" data-spot-input rows="3" readonly tabindex="-1" ' + 268 'placeholder="Find creators with a single prompt"></textarea>' + 269 '<button class="btn btn--gradient prompt-card__cta" type="button" tabindex="-1" aria-hidden="true">' + 270 'Generate shortlist' + 271 '<svg class="arrow" viewBox="0 0 15 15" width="15" height="15" aria-hidden="true"><path d="M14.7 8.07a1 1 0 0 0 0-1.41L8.34.29A1 1 0 0 0 6.93 1.7l5.66 5.66-5.66 5.66a1 1 0 0 0 1.41 1.41L14.7 8.07ZM0 7.36v1h14v-2H0v1Z" fill="currentColor"/></svg>' + 272 '</button>' + 273 '<span class="fb-match__matching" data-spot-matching aria-hidden="true"><i></i><i></i><i></i> Matching from 300M+ creatorsâ¦</span>' + 274 '</div>' + 275 '</div>' + 276 '<div class="fb-match__scaler">' + 277 '<div class="spot-col" data-spot-col aria-live="polite">' + cardsHTML + '</div>' + 278 '</div>' + 279 '</div>'; 280 281 var panel = host.querySelector(".fb-match__panel"); 282 var input = host.querySelector("[data-spot-input]"); 283 var caretEl = host.querySelector("[data-spot-caret]"); 284 var matchingEl = host.querySelector("[data-spot-matching]"); 285 var col = host.querySelector("[data-spot-col]"); 286 var cards = Array.prototype.slice.call(col.querySelectorAll(".spot-col__card")); 287 288 var loopId = null, paused = false, killed = false, started = false, tl = null; 289 290 function setMatching(on) { if (matchingEl) matchingEl.classList.toggle("is-on", on); } 291 function setCaret(on) { if (caretEl) caretEl.style.opacity = on ? "" : "0"; } 292 293 function armColHidden() { 294 gsap.set(col, { opacity: 0 }); 295 gsap.set(cards, { opacity: 0, y: 18 }); 296 } 297 298 function typeBrief(done) { 299 input.value = ""; 300 if (reduce) { input.value = BRIEF; if (done) done(); return; } 301 setCaret(true); 302 var i = 0; 303 (function step() { 304 if (killed) return; 305 i++; input.value = BRIEF.slice(0, i); 306 if (i < BRIEF.length) loopId = setTimeout(step, 26); 307 else { setCaret(false);
307 if (done) loopId = setTimeout(done, 420); } 308 })(); 309 } 310 311 function closeAndLoop() { 312 if (killed) return; 313 if (paused) { loopId = setTimeout(closeAndLoop, 500); return; } 314 // fade the column out, slide the prompt panel back in, then loop 315 tl = gsap.timeline({ onComplete: function () { loopId = setTimeout(cycle, 700); } }); 316 tl.to(col, { opacity: 0, duration: 0.4, ease: "power2.in" }, 0); 317 tl.to(panel, { opacity: 1, y: 0, duration: 0.5, ease: "power3.out" }, 0.1); 318 tl.add(function () { armColHidden(); }); 319 } 320 321 function cycle() { 322 if (killed) return; 323 setMatching(false); 324 gsap.set(panel, { opacity: 1, y: 0 }); 325 armColHidden(); 326 typeBrief(function () { 327 if (killed) return; 328 setMatching(true); 329 loopId = setTimeout(function () { 330 if (killed) return; 331 setMatching(false); 332 // prompt slides up & out; the creator column rises in its place 333 tl = gsap.timeline(); 334 tl.to(panel, { opacity: 0, y: -24, duration: 0.4, ease: "power3.out" }, 0); 335 tl.to(col, { opacity: 1, duration: 0.4, ease: "power2.out" }, 0.25); 336 tl.to(cards, { opacity: 1, y: 0, duration: 0.5, stagger: 0.08, ease: "power3.out" }, 0.3); 337 loopId = setTimeout(closeAndLoop, 3600); 338 }, 760); 339 }); 340 } 341 342 function start() { 343 if (started) return; 344 started = true; 345 // flow-blocks.css hides .fb-copy/.fb-visual until .fb-block.is-in â the shared 346 // armReveal no longer manages this block (its host was renamed), so reveal it here. 347 if (blockEl) blockEl.classList.add("is-in"); 348 if (reduce) { // static end-state: hide prompt, show column 349 input.value = BRIEF; 350 setCaret(false); 351 setMatching(false); 352 gsap.set(panel, { opacity: 0 }); 353 gsap.set(col, { opacity: 1 }); 354 gsap.set(cards, { opacity: 1, y: 0 }); 355 return; 356 } 357 armColHidden(); 358 loopId = setTimeout(cycle, 700); // let the block reveal read first 359 } 360 361 host.addEventListener("pointerenter", function () { if (!reduce) { paused = true; if (tl && tl.isActive && tl.isActive()) tl.pause(); } }); 362 host.addEventListener("pointerleave", function () { if (!reduce) { paused = false; if (tl && tl.paused && tl.paused()) tl.resume(); } }); 363 364 armColHidden(); 365 366 // reveal-gated start: begin when the block scrolls into view (or is already in view) 367 var blockEl = host.closest(".fb-block") || host; 368 function inView(el) { 369 var r = el.getBoundingClientRect(); 370 return r.top < (window.innerHeight || document.documentElement.clientHeight) && r.bottom > 0; 371 } 372 if (reduce || inView(blockEl) || !("IntersectionObserver" in window)) { 373 start(); 374 } else { 375 var io2 = new IntersectionObserver(function (entries) { 376 entries.forEach(function (en) { if (en.isIntersecting) { start(); io2.unobserve(en.target); } }); 377 }, { threshold: 0.18, rootMargin: "0px 0px -8% 0px" }); 378 io2.observe(blockEl); 379 } 380 })(); 381 382 /* ======================= Scroll reveal ======================= */ 383 var revealEls = Array.prototype.slice.call(doc.querySelectorAll(".reveal")); 384 if (revealEls.length) { 385 if (reduce || !("IntersectionObserver" in window)) { 386 revealEls.forEach(function (el) { el.classList.add("is-in"); }); 387 } else { 388 var io = new IntersectionObserver(function (entries) { 389 entries.forEach(function (en) { 390 if (en.isIntersecting) { en.target.classList.add("is-in"); io.unobserve(en.target); } 391 }); 392 }, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" }); 393 revealEls.forEach(function (el) { io.observe(el); }); 394 // failsafe: never leave anything hidden 395 setTimeout(function () { revealEls.forEach(function (el) { el.classList.add("is-in"); }); }, 2600); 396 } 397 } 398})(); 399 400/* ======================= Enterprise Workflow (page-local) ======================= 401 Reuses the process.js "connected-flow" CSS classes (.proc-flow, .proc-num, 402 .proc-flow__card) with different content â six org functions instead of the 403 five matching steps â rather than forking process.js's buildFlow(), which 404 only supports one hardcoded STEPS dataset. */ 405(function () { 406 "use strict"; 407 var el = document.querySelector("[data-workflow-flow]"); 408 if (!el) return; 409 var WORKFLOW_ICONS = { 410 marketing: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>', 411 legal: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
411<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/></svg>', 412 procurement: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>', 413 finance: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="12" x="2" y="6" rx="2"/><circle cx="12" cy="12" r="2"/><path d="M6 12h.01M18 12h.01"/></svg>', 414 creators: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>', 415 management: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>' 416 }; 417 var WORKFLOW = [ 418 { icon: WORKFLOW_ICONS.marketing, title: "Marketing", body: "Builds the brief and owns the creative direction." }, 419 { icon: WORKFLOW_ICONS.legal, title: "Legal", body: "Reviews contracts and disclosure compliance automatically." }, 420 { icon: WORKFLOW_ICONS.procurement, title: "Procurement", body: "Approves vendor terms and creator rates in one workflow." }, 421 { icon: WORKFLOW_ICONS.finance, title: "Finance", body: "Tracks budget, automates payments, and reconciles spend." }, 422 { icon: WORKFLOW_ICONS.creators, title: "Creators", body: "Deliver content inside one governed, trackable workspace." }, 423 { icon: WORKFLOW_ICONS.management, title: "Data", body: "Sees performance and ROI across every team in real time." } 424 ]; 425 el.innerHTML = WORKFLOW.map(function (s, i) { 426 return '<div class="proc-flow__node" style="--i:' + i + '"><span class="proc-num" aria-hidden="true">' + s.icon + '</span>' + 427 '<div class="proc-flow__card"><p class="proc-title">' + s.title + '</p><p class="proc-body">' + s.body + '</p></div></div>'; 428 }).join(""); 429})(); 430 431/* ROI Calculator: replaced by the shared components/savings-calc.js â this 432 used to run its own separate formula, since unified with pricing.html's 433 calculator (see LP-12). */ 434 435/* ======================= SPOTLIGHT CREATOR DATA (page-scoped override) ======================= 436 Feedback round: replace the placeholder creators in "Creators ready to match" 437 (data-cc-rows) and the Trending marquee (data-track a/b) with the real team 438 list â real names + handles + niches. Photos are INTERIM lifestyle portraits 439 (assets/creators-spotlight/*) until the real profile images are fetched; add a 440 handleâfile entry to REAL below to swap any creator's face in (one line each). 441 Match % is intentionally omitted (removed per feedback). We overwrite the shared 442 creator-cards.js / trending-creators.js output page-scoped (those components and 443 the other home pages stay untouched). Runs after they fill the DOM (this file 444 loads last) and before the trendingâstudio wiring below, so clickâpopup binds 445 to these cards. */ 446(function () { 447 "use strict"; 448 var doc = document; 449 // Photo resolution now lives in components/data-creators.js (generated) so a 450 // newly fetched Instagram avatar shows up in every component at once instead 451 // of only here. Run tools/fetch-ig-avatars.mjs, then re-run 452 // tools/build-creator-manifest.mjs, and the faces below swap themselves. 453 // The inline fallback keeps this file working if data-creators.js is absent. 454 function photo(u, i) { 455 if (window.creatorPhoto) return window.creatorPhoto(u, i); 456 return "assets/creators-spotlight/s" + String(((i * 7) % 216) + 1).padStart(3, "0") + ".jpg"; 457 } 458 // name = the label the team used (real name for public figures, the niche for 459 // the rest); u = handle without @; niche = shown as the card tagline. 460 var C = [ 461 { u:"zaralarsson", name:"Zara Larsson", niche:"Music & Fashion", f:"5.8M", e:"3.1%" }, 462 { u:"anitta", name:"Anitta", niche:"Music", f:"3.2M", e:"3.6%" }, 463 { u:"pewdiepie", name:"PewDiePie", niche:"Gaming", f:"21M", e:"2.4%" }, 464 { u:"emmachamberlain", name:"Emma Chamberlain", niche:"Lifestyle & Style", f:"1.2M", e:"4.9%" }, 465 { u:"mrbeast", name:"MrBeast", niche:"Entertainment", f:"18M", e:"5.7%" }, 466 { u:"jackiegustafssonn", name:"Streetwear Fashion", niche:"Fashion", f:"184k", e:"6.4%" },
467 { u:"alicejoythomas", name:"Luxury Fashion", niche:"Fashion", f:"212k", e:"5.1%" }, 468 { u:"erickshots", name:"Sustainable Fashion", niche:"Fashion", f:"98k", e:"7.8%" }, 469 { u:"lariearaujoo", name:"Haircare", niche:"Beauty", f:"143k", e:"6.9%" }, 470 { u:"juliannahoffman4", name:"Skincare", niche:"Beauty", f:"126k", e:"7.2%" }, 471 { u:"aaalmgren", name:"Running", niche:"Fitness", f:"88k", e:"8.1%" }, 472 { u:"danifitnesss_", name:"Fitness", niche:"Fitness", f:"167k", e:"7.5%" }, 473 { u:"jennalovisa", name:"Fitness", niche:"Fitness", f:"204k", e:"5.9%" }, 474 { u:"karlarcosta", name:"Yoga & Light Living", niche:"Wellness", f:"79k", e:"9.0%" }, 475 { u:"huungrysoul", name:"Restaurants & Food", niche:"Food", f:"112k", e:"6.2%" }, 476 { u:"foodandcoffeegbg", name:"Restaurants & Food", niche:"Food", f:"64k", e:"8.8%" }, 477 { u:"zeinaskitchen", name:"Cooking", niche:"Food", f:"389k", e:"6.6%" }, 478 { u:"sampev2", name:"Fitness", niche:"Fitness", f:"151k", e:"7.0%" }, 479 { u:"n.lowry", name:"Productivity", niche:"Lifestyle", f:"93k", e:"7.7%" }, 480 { u:"hormozi", name:"Entrepreneurship", niche:"Business", f:"2.4M", e:"4.2%" }, 481 { u:"nannacebner", name:"Family", niche:"Family", f:"71k", e:"8.3%" }, 482 { u:"zoetastetravel", name:"Travel", niche:"Travel", f:"128k", e:"6.1%" }, 483 { u:"thia.interior", name:"Interior", niche:"Interior", f:"96k", e:"7.4%" }, 484 { u:"love_this_old_house", name:"Interior", niche:"Interior", f:"58k", e:"9.3%" }, 485 { u:"lknsardari", name:"Tech", niche:"Tech", f:"134k", e:"5.8%" }, 486 { u:"martimsilvai", name:"AI", niche:"Tech", f:"87k", e:"8.0%" }, 487 { u:"helenaruddstudio", name:"DIY", niche:"Maker & Craft", f:"69k", e:"8.9%" }, 488 { u:"chandsgarms", name:"Vintage Fashion", niche:"Fashion", f:"76k", e:"8.6%" }, 489 { u:"sophiasinot", name:"Beauty", niche:"Beauty", f:"158k", e:"6.7%" }, 490 { u:"makeupstina", name:"Beauty", niche:"Beauty", f:"119k", e:"7.1%" }, 491 { u:"sanaamanda", name:"Beauty", niche:"Beauty", f:"102k", e:"7.9%" } 492 ]; 493 C.forEach(function (c, i) { c.i = i; c.img = photo(c.u, i); c.handle = "@" + c.u; }); 494 495 /* ---- "Creators ready to match" · 8 rows, NO match % ---- */ 496 var pick = ["zaralarsson", "emmachamberlain", "mrbeast", "pewdiepie", "anitta", "jackiegustafssonn", "juliannahoffman4", "danifitnesss_"]; 497 var byU = {}; C.forEach(function (c) { byU[c.u] = c; }); 498 var rowsHost = doc.querySelector("[data-cc-rows]"); 499 if (rowsHost) { 500 rowsHost.innerHTML = pick.map(function (u, i) { 501 var c = byU[u]; if (!c) return ""; 502 return '<article class="cc-row" data-cc-row>' + 503 '<span class="cc-row__rank">' + (i + 1) + '</span>' + 504 '<span class="creator__avatar cc-row__avatar"><img src="' + c.img + '" alt="' + c.handle + '" loading="lazy"></span>' + 505 '<span class="cc-row__id"><b class="cc-row__name">' + c.name + '</b>' + 506 '<span class="cc-row__handle">' + c.handle + ' · ' + c.niche + '</span></span>' + 507 '<span class="cc-row__stat"><b>' + c.f + '</b><i>followers</i></span>' + 508 '<span class="cc-row__stat cc-row__stat--hide"><b>' + c.e + '</b><i>eng.</i></span>' + 509 '</article>'; 510 }).join(""); 511 } 512 513 /* ---- Trending marquee · both rails, set + aria-hidden loop duplicate ---- */ 514 function tCard(c, dup) { 515 return '<article class="creator"' + (dup ? ' aria-hidden="true"' : "") + ">" + 516 '<span class="creator__avatar"><img src="' + c.img + '" alt="' + c.handle + '" loading="lazy"></span>' + 517 '<div class="creator__meta">' + 518 '<p class="creator__name">' + c.handle + "</p>" + 519 '<p class="creator__stat">Followers: ' + c.f + "</p>" + 520 '<p class="creator__stat">Engagement: ' + c.e + "</p>" + 521 "</div></article>"; 522 } 523 function loopHTML(list) { 524 return list.map(function (c) { return tCard(c, false); }).join("") + 525 list.map(function (c) { return tCard(c, true); }).join(""); 526 } 527 var rowA = C.slice(0, 16), rowB = C.slice(16); 528 var tA = doc.querySelector("[data-track='a']"), tB = doc.querySelector("[data-track='b']"); 529 if (tA) tA.innerHTML = loopHTML(rowA); 530 if (tB) tB.innerHTML = loopHTML(rowB); 531})(); 532 533/* TRENDING â STUDIO POPUP removed (directed 2026-09-02): every creator card 534 is a pure animation now â no click/Enter/Space handoff to a profile 535 popup, on this page or any other. See page/home.js for the matching 536 removal of the duplicate wiring that lived there. */
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.