1<!DOCTYPE html><!-- Last Published: Thu Sep 10 2026 21:57:33 GMT+0000 (Coordinated Universal Time) --><html data-wf-domain="elinakustlyvy.webflow.io" data-wf-page="67dabf419d6912f7227e7d9a" data-wf-site="653c048c7bdcdc4c8f434684" lang="en"><head><meta charset="utf-8"/><link href="https://cdn.prod.website-files.com" rel="preconnect" crossorigin="anonymous"/><title>Vancouver Product Photographer | 10+ Years Beauty & Luxury Brands</title><meta content="Meet Elina Kustlyvy, a Vancouver commercial photographer specializing in beauty, skincare, cosmetics, and product photography for premium brands." name="description"/><meta content="Vancouver Product Photographer | 10+ Years Beauty & Luxury Brands" property="og:title"/><meta content="Meet Elina Kustlyvy, a Vancouver commercial photographer specializing in beauty, skincare, cosmetics, and product photography for premium brands." property="og:description"/><meta content="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/688d4b57a8feabde56f3f9b3_Frame%202085662577.png" property="og:image"/><meta content="Vancouver Product Photographer | 10+ Years Beauty & Luxury Brands" name="twitter:title"/><meta content="Meet Elina Kustlyvy, a Vancouver commercial photographer specializing in beauty, skincare, cosmetics, and product photography for premium brands." name="twitter:description"/><meta property="og:type" content="website"/><meta content="summary_large_image" name="twitter:card"/><meta content="width=device-width, initial-scale=1" name="viewport"/><meta content="LCoAeevlnY7YUT1TDZKo0hC9DjuTAxweBtZyVPXsUiY" name="google-site-verification"/><link href="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/css/elinakustlyvy.webflow.shared.2656f86d7.css" rel="stylesheet" type="text/css" integrity="sha384-Jlb4bXCoKLFlOlGq8JpK7UEfhCP52ZJEebgdQwmMGzTy9zwqDUTsmaxQ+4qn7+wD" crossorigin="anonymous"/><link href="https://fonts.googleapis.com" rel="preconnect"/><link href="https://fonts.gstatic.com" rel="preconnect" crossorigin="anonymous"/>
1<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js" type="text/javascript"></script>
1<script type="text/javascript">WebFont.load({ google: { families: ["Oswald:200,300,400,500,600,700","Inconsolata:400,700","Inter:300,400,500,600,700"] }});</script>
1<script type="text/javascript">!function(o,c){var n=c.documentElement,t=" w-mod-";n.className+=t+"js",("ontouchstart"in o||o.DocumentTouch&&c instanceof DocumentTouch)&&(n.className+=t+"touch")}(window,document);</script>
1<link href="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6552c3a0ca2347feb6abe25f_ico.png" rel="shortcut icon" type="image/x-icon"/><link href="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6552c3bd4925011fa3f58ad2_webflow-icon.png" rel="apple-touch-icon"/><link href="https://www.elinakustlyvy.com/about" rel="canonical"/><!-- Creative Work Series - Portfolio/ratings context -->
2<script type="application/ld+json"> 3{ 4 "@context": "https://schema.org/", 5 "@type": "CreativeWorkSeries", 6 "name": "Elina Kustlyvy Photography", 7 "aggregateRating": { 8 "@type": "AggregateRating", 9 "ratingValue": "4.9", 10 "bestRating": "5", 11 "worstRating": "1", 12 "ratingCount": "179" 13 } 14} 15</script>
15 16 17<!-- Main Business Schema -->
18<script type="application/ld+json"> 19{ 20 "@context": "https://schema.org", 21 "@type": "ProfessionalService", 22 "name": "Elina Kustlyvy Photography", 23 "description": "Commercial product photographer based in Vancouver, B.C., working with brands in Los Angeles, Seattle, and Toronto. Specializing in clean, impactful imagery for beauty, skincare, cosmetics, e-commerce, and food photography.", 24 "url": "https://www.elinakustlyvy.com", 25 "image": "https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/688d4b57a8feabde56f3f9b3_Frame%202085662577.png", 26 "logo": "https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6552c2882b7b50618bbc1eca_ElinaKustlyvy2%20(1).png", 27 "telephone": "+16729999499", 28 "email": "[email protected]", 29 "priceRange": "$1200-$5000+", 30 "aggregateRating": { 31 "@type": "AggregateRating", 32 "ratingValue": "4.9", 33 "bestRating": "5", 34 "worstRating": "1", 35 "ratingCount": "179" 36 }, 37 "founder": { 38 "@type": "Person", 39 "name": "Elina Kustlyvy", 40 "jobTitle": "Commercial Product Photographer", 41 "email": "[email protected]", 42 "telephone": "+16729999499", 43 "sameAs": [ 44 "https://instagram.com/elina.kustlyvy", 45 "https://www.linkedin.com/in/elinakustlyvy/" 46 ] 47 }, 48 "address": { 49 "@type": "PostalAddress", 50 "streetAddress": "5629 Birney Ave", 51 "addressLocality": "Vancouver", 52 "addressRegion": "BC", 53 "postalCode": "V6S 0L5", 54 "addressCountry": "CA" 55 }, 56 "geo": { 57 "@type": "GeoCoordinates", 58 "latitude": "49.2327", 59 "longitude": "-123.1813" 60 }, 61 "areaServed": [ 62 { 63 "@type": "City", 64 "name": "Vancouver", 65 "containedInPlace": { 66 "@type": "State", 67 "name": "British Columbia" 68 } 69 }, 70 { 71 "@type": "City", 72 "name": "Toronto", 73 "containedInPlace": { 74 "@type": "State", 75 "name": "Ontario" 76 } 77 }, 78 { 79 "@type": "City", 80 "name": "Los Angeles", 81 "containedInPlace": { 82 "@type": "State", 83 "name": "California" 84 } 85 }, 86 { 87 "@type": "City", 88 "name": "Seattle", 89 "containedInPlace": { 90 "@type": "State", 91 "name": "Washington" 92 } 93 } 94 ], 95 "sameAs": [ 96 "https://www.instagram.com/elina.kustlyvy", 97 "https://www.linkedin.com/company/kustlyvy-photo", 98 "https://www.facebook.com/elinakustlyvy" 99 ], 100 "hasOfferCatalog": { 101 "@type": "OfferCatalog", 102 "name": "Photography Services", 103 "itemListElement": [ 104 { 105 "@type": "Offer", 106 "itemOffered": { 107 "@type": "Service", 108 "name": "Beauty Photography", 109 "description": "Beauty photography that captures authentic product performance and builds customer confidence.", 110 "url": "https://www.elinakustlyvy.com/services/beauty-photography" 111 } 112 }, 113 { 114 "@type": "Offer", 115 "itemOffered": { 116 "@type": "Service", 117 "name": "Skincare Photography", 118 "description": "Tactile skincare photography that showcases transformative textures and makes your formulas irresistible.", 119 "url": "https://www.elinakustlyvy.com/services/skincare-photography" 120 } 121 }, 122 { 123 "@type": "Offer", 124 "itemOffered": { 125 "@type": "Service", 126 "name": "Cosmetics Photography", 127 "description": "Bold, brand-aligned cosmetics photography that showcases true colors and textures across diverse skin tones.", 128 "url": "https://www.elinakustlyvy.com/services/cosmetics-photography" 129 } 130 }, 131 { 132 "@type": "Offer", 133 "itemOffered": { 134 "@type": "Service", 135 "name": "E-commerce Photography", 136 "description": "Technical e-commerce photography that captures every surface clearly so customers know exactly what they're buying.", 137 "url": "https://www.elinakustlyvy.com/services/ecommerce-photography" 138 } 139 }, 140 { 141 "@type": "Offer", 142 "itemOffered": { 143 "@type": "Service", 144 "name": "Commercial Photography", 145 "description": "Advanced commercial photography for complex products requiring technical lighting expertise and category-specific knowledge.", 146 "url": "https://www.elinakustlyvy.com/services/other" 147 } 148 } 149 ] 150 }, 151 "review": [ 152 { 153 "@type": "Review", 154 "author": { 155 "@type": "Person", 156 "name": "Ankita Sanawar" 157 }, 158 "reviewBody": "I had the pleasure of working with Elina, and she truly has an incredible eye for detail. She understands how to translate a vision into stunning, high-quality visuals. I would highly recommend her to anyone looking for a talented and professional product photographer." 159 }, 160 { 161 "@type": "Review", 162 "author": { 163 "@type": "Person", 164 "name": "Ibrahim Muradov" 165 }, 166 "reviewBody": "I can't thank Elina enough for the incredible headsh
166ots she took of me â these are by far the best photos I've ever had. From the moment I saw them, I was speechless â truly magnificent! Elina has a rare gift for capturing not just an image, but a person's personality, energy, and presence. Her professionalism, creativity, and warm approach made the whole experience so enjoyable and relaxed. She made me feel completely at ease, and the results speak for themselves â absolutely stunning!" 167 }, 168 { 169 "@type": "Review", 170 "author": { 171 "@type": "Person", 172 "name": "YoungSin Choi" 173 }, 174 "reviewBody": "Elina recently did a wedding invitation/engagement photoshoot for my fiancé and me. We are not photo takers at all to begin with and feel uncomfortable and awkward and very shy being taken photos. Before the shoot, She said her style is not to pose us with detailed exact manuals but to let us be natural authentic ourselves. During the shoot, it was amazing how comfortable I was doing a photoshoot outdoors where strangers are passing by. Even my extremely introvert fiancé was so comfortable. Everything was just happening and flowing and we were being ourselves and being in the moment. As a result, every single picture turned out absolutely amazing and had us screaming and tearing up. On top of her technical skillful work done, all the pictures look so real and us. It's like we became a part of Elina's art work. I could not have found anyone better than her for this photoshoot. She is a brilliant photographer with extraordinary eyes to see through and beyond, and an artist with a beautiful caring heart." 175 }, 176 { 177 "@type": "Review", 178 "author": { 179 "@type": "Person", 180 "name": "Alix Ligier de Laprade" 181 }, 182 "reviewBody": "Working with Elina was awesome! She is a very talented and experienced photographer, with an open mind and great attention to detail. The visuals are amazing and very close to what we envisioned." 183 }, 184 { 185 "@type": "Review", 186 "author": { 187 "@type": "Person", 188 "name": "Winnie Chu" 189 }, 190 "reviewBody": "I had the pleasure to work with her on both product and model shoots for my brands. All the sessions that we've done so far have been amazing and the results are always better than expected. She always goes above and beyond to get the perfect shot! Also, she is extremely personable, friendly, and professional. Super easy to work with! From planning the shoot to finishing the shoot she has been very helpful throughout the whole process. She patiently listened to my ideas and helped me bring my ideas to life!" 191 }, 192 { 193 "@type": "Review", 194 "author": { 195 "@type": "Organization", 196 "name": "Frankly Agency" 197 }, 198 "reviewBody": "We approached Elina for two skin care and a wellness photo shoot to enhance our social media marketing campaigns. We needed high-quality images to promote our client's products effectively and to capture the essence of wellness for our campaign. We were highly satisfied with Elina's work. The results exceeded our expectations, particularly in how she captured the product details and conveyed the wellness theme. Her attention to detail and ability to bring our concepts to life were impressive. Without hesitation, we would recommend Elina's services to others seeking professional photography." 199 }, 200 { 201 "@type": "Review", 202 "author": { 203 "@type": "Person", 204 "name": "Alisha Hopps" 205 }, 206 "reviewBody": "I hired Elina to shoot my masterclass BTS and close up beauty portraits. She did a beautiful job and was so supportive on set! Thank you, Elina!" 207 }, 208 { 209 "@type": "Review", 210 "author": { 211 "@type": "Person", 212 "name": "Maggie Pham" 213 }, 214 "reviewBody": "I loved working with Elina!! She's always so creative and passionate about getting the right photo and is extremely prompt when it comes to getting the pictures done thanks Elina!! Can't wait for our next project." 215 }, 216 { 217 "@type": "Review", 218 "author": { 219 "@type": "Person", 220 "name": "AJ Gill" 221 }, 222 "reviewBody": "Elina was nothing short of spectacular. Very easy to deal with and was responsive to all the needs of my content. I would recommend her to anyone who is looking to create content for their company." 223 }, 224 { 225 "@type": "Review", 226 "author": { 227 "@type": "Person", 228 "name": "Ksenia Penkina" 229 }, 230 "reviewBody": "Had a great time with Elina doing photoshoot for my book, will definitely come back for more:) thank you!!" 231 } 232 ] 233} 234</script>
234<!-- Google Tag Manager -->
235<script>
vendor: 327 bytes, lines 235-239
235(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 236new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 237j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 238'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 239})(window,document,'script','dataLayer','
239GTM-P99G3FB
vendor: 3 bytes, line 239
239');
239</script>
239 240<!-- End Google Tag Manager --> 241 242<style> 243/* Start of custom CSS for Webflow */ 244@media (hover: none) {.cursor {display:none !important;}} 245 246/* Hide default cursor globally */ 247body, html, div, span, p, a, h1, h2, h3, h4, h5, h6, ul, li, nav, header, footer, section, article { 248 cursor: none !important; 249} 250 251/* Custom cursor styles */ 252.cursor { 253 position: fixed; 254 width: 20px; 255 height: 20px; 256 background-color: white; 257 border-radius: 50%; 258 pointer-events: none; 259 z-index: 9999; 260 mix-blend-mode: difference; 261 transition: transform 0.2s ease-out, width 0.2s ease-out, height 0.2s ease-out; 262 will-change: transform; 263 top: 0; 264 left: 0; 265} 266 267/* Cursor hover state for links */ 268.cursor.active { 269 width: 40px; 270 height: 40px; 271} 272 273/* Prevent cursor blend mode from affecting hover images */ 274.hover-img { 275 isolation: isolate; 276} 277 278/* Hide body initially to prevent flash - only on first visit */ 279body:not(.animation-complete) { 280 visibility: hidden !important; 281 background-color: #fafafa !important; 282} 283 284/* Barba.js page transition overlay */ 285.page-transition { 286 position: fixed; 287 z-index: 10000; 288 width: 100%; 289 height: 100vh; 290 top: 0; 291 left: 0; 292 pointer-events: none; 293 background: #ffffff; 294 transform: scaleY(0); 295 transform-origin: bottom; 296} 297 298/* Konpo Loading Animation Styles */ 299.konpo-loader { 300 position: fixed; 301 top: 0; 302 left: 0; 303 width: 100vw; 304 height: 100vh; 305 background-color: #fafafa; 306 z-index: 10000; 307 overflow: hidden; 308 display: block !important; 309 visibility: visible !important; 310} 311 312.konpo-loader-bg { 313 position: absolute; 314 top: 0; 315 left: 0; 316 width: 100%; 317 height: 100%; 318 background: #f8f8f8; 319 transform-origin: top; 320 transform: scaleY(0); 321 display: block; 322 visibility: visible; 323} 324 325.konpo-counter { 326 position: fixed; 327 right: 3rem; 328 bottom: 2rem; 329 display: flex; 330 height: 120px; 331 font-size: 120px; 332 line-height: 150px; 333 font-family: Hatton, Georgia, sans-serif; 334 font-weight: 200; 335 color: #000000; 336 clip-path: polygon(0 0, 100% 0, 100% 120px, 0 120px); 337} 338 339.konpo-counter-1, 340.konpo-counter-2, 341.konpo-counter-3 { 342 position: relative; 343 top: -15px; 344} 345 346.konpo-num { 347 display: block; 348} 349 350.konpo-num1offset1 { 351 position: relative; 352 right: -30px; 353} 354 355.konpo-num1offset2 { 356 position: relative; 357 right: -15px; 358} 359 360.konpo-images-container { 361 position: absolute; 362 top: 0; 363 left: 0; 364 width: 100%; 365 height: 100%; 366} 367 368.konpo-img { 369 position: absolute; 370 top: 1.5rem; 371 left: 1.5rem; 372 width: 15%; 373 aspect-ratio: 3/4; 374 border-radius: 0; 375 overflow: hidden; 376 opacity: 0; 377 transform: scale(0); 378} 379 380.konpo-img img { 381 width: 100%; 382 height: 100%; 383 object-fit: cover; 384} 385 386.konpo-img.animate-out { 387 top: unset; 388 left: unset; 389 bottom: unset; 390 right: unset; 391} 392 393@media (max-width: 768px) { 394 .konpo-counter { 395 font-size: 80px; 396 height: 80px; 397 line-height: 100px; 398 right: 1.5rem; 399 bottom: 1.5rem; 400 } 401 402 .konpo-img { 403 width: 30%; 404 } 405} 406 407/* Media query for desktop - show carousels properly */ 408@media screen and (min-width: 768px) { 409 .infinite-scroll-track,
410 .contact.infinite-scroll-track { 411 display: block; 412 visibility: visible; 413 width: 100%; 414 } 415} 416 417/* Landscape orientation specific styles */ 418.hover-wrapper.is-landscape { 419 padding-bottom: 0; /* Remove padding from parent */ 420} 421 422/* Target hover-frame inside landscape containers */ 423.hover-wrapper.is-landscape .hover-frame { 424 width: 100%; 425 padding-bottom: 67%; /* 67% padding for landscape orientation */ 426} 427 428/* Mobile image and swipe handling */ 429@media (max-width: 991px) { 430 /* Disable desktop hover effects on mobile */ 431 .hover-wrapper:hover .hover-img { 432 opacity: 0; 433 } 434 435 .hover-wrapper:hover .hover-img.top { 436 opacity: 1; 437 } 438 439 /* Mobile image transitions */ 440 .hover-img { 441 transition: opacity 0.5s ease-in-out !important; /* Increased from 0.3s to 0.5s */ 442 will-change: opacity; 443 -webkit-backface-visibility: hidden; 444 backface-visibility: hidden; 445 transform: translateZ(0); 446 pointer-events: none; /* Prevent image from capturing touch events */ 447 opacity: 0; /* Default state to prevent white flash */ 448 } 449 450 /* Universal rules for ALL containers */ 451 .hover-wrapper .hover-img.top { 452 opacity: 1 !important; 453 visibility: visible !important; 454 z-index: 2; 455 display: block !important; 456 transition: opacity 0.5s ease-in-out !important; 457 } 458 459 .hover-wrapper .hover-img:not(.top) { 460 opacity: 0 !important; 461 visibility: hidden !important; 462 z-index: 1; 463 transition: opacity 0.5s ease-in-out !important; 464 } 465 466 /* Specific rules for landscape containers */ 467 .hover-wrapper.is-landscape .hover-img.top { 468 position: absolute !important; 469 top: 0 !important; 470 left: 0 !important; 471 width: 100% !important; 472 height: 100% !important; 473 object-fit: cover !important; 474 } 475 476 /* Ensure images in landscape containers are visible on mobile */ 477 .hover-wrapper.is-landscape .hover-img.top { 478 opacity: 1 !important; 479 visibility: visible !important; 480 position: relative !important; 481 z-index: 2; 482 display: block !important; 483 height: auto !important; 484 width: 100% !important; 485 transition: transform 0.3s ease, opacity 0.5s ease !important; /* Updated opacity transition */ 486 } 487 488 /* Fix white screen issue when swiping in landscape containers */ 489 .hover-wrapper.is-landscape .hover-img { 490 position: absolute; 491 top: 0; 492 left: 0; 493 width: 100%; 494 height: 100%; 495 opacity: 0; 496 transition: transform 0.3s ease-out, opacity 0.5s ease-out;
496 /* Updated opacity transition */ 497 } 498 499 /* iOS-style slide transitions */ 500 .hover-wrapper .hover-img { 501 position: absolute; 502 top: 0; 503 left: 0; 504 width: 100%; 505 height: 100%; 506 transition: transform 0.3s ease-out, opacity 0.5s ease-out; /* Updated opacity transition */ 507 will-change: transform, opacity; 508 } 509 510 /* Slide directions */ 511 .hover-img.slide-in-right { 512 transform: translateX(100%); 513 opacity: 1; 514 } 515 516 .hover-img.slide-in-left { 517 transform: translateX(-100%); 518 opacity: 1; 519 } 520 521 .hover-img.slide-out-right { 522 transform: translateX(100%); 523 opacity: 1; 524 z-index: 1; 525 } 526 527 .hover-img.slide-out-left { 528 transform: translateX(-100%); 529 opacity: 1; 530 z-index: 1; 531 } 532 533 .hover-img.top { 534 transform: translateX(0); 535 opacity: 1; 536 z-index: 2; 537 } 538 539 /* Swipe overlay for better touch detection */ 540 .swipe-overlay { 541 position: absolute; 542 top: 0; 543 left: 0; 544 width: 100%; 545 height: 100%; 546 z-index: 4; /* Lower z-index to ensure dots are above */ 547 pointer-events: auto !important; 548 } 549 550 /* Make sure the hover wrapper has position relative for overlay positioning */ 551 .hover-wrapper { 552 position: relative !important; 553 overflow: hidden; 554 } 555} 556 557/* Main carousel container */ 558.carousel { 559 width: 100%; 560 overflow: hidden; 561 height: 300px; 562 position: relative; 563} 564 565/* Hide original CMS structure when flattened */ 566.carousel[data-flattened="true"] .w-dyn-list { 567 display: none !important; 568} 569 570/* New flattened scroll track */ 571.infinite-scroll-track { 572 display: flex; 573 height: 100%; 574 will-change: transform; 575 transform: translate3d(0, 0, 0); 576 backface-visibility: hidden; 577} 578 579/* Individual image items */ 580.scroll-image-item { 581 flex: 0 0 auto; 582 height: 100%; 583 display: flex; 584 align-items: center; 585} 586 587/* Images */ 588.scroll-image { 589 height: 100% !important; 590 width: auto !important; 591 max-width: none !important; 592 object-fit: cover; 593 display: block; 594 transform: translate3d(0, 0, 0); 595 backface-visibility: hidden; 596} 597 598/* Pause on hover */ 599.carousel:hover .infinite-scroll-track { 600 animation-play-state: paused; 601} 602 603/* Mobile only */ 604@media (max-width: 991px) { 605 .infinite-scroll-track { 606 display: flex; 607 } 608 609 /* Disable desktop hover effects on mobile */ 610 .hover-wrapper:hover .hover-img { 611 opacity: 0; 612 } 613 614 .hover-wrapper:hover .hover-img.top { 615 opacity: 1; 616 } 617 618 /* Mobile image transitions */ 619 .hover-img { 620 transition: opacity 0.5s ease-in-out !important; /* Increased from 0.3s to 0.5s */ 621 will-change: opacity; 622 -webkit-backface-visibility: hidden; 623 backface-visibility: hidden; 624 transform: translateZ(0); 625 pointer-events: none; /* Prevent image from capturing touch events */ 626 opacity: 0; /* Default state to prevent white flash */ 627 } 628 629 .hover-img.top { 630 opacity: 1 !important; 631 visibility: visible !important; /* Added visibility property */ 632 z-index: 2; 633 transition: opacity 0.5s ease-in-out !important; /* Added transition */ 634 } 635 636 .hover-img:not(.top) { 637 opacity: 0 !important; 638 visibility: hidden !important; /* Added visibility property */ 639 z-index: 1; 640 transition: opacity 0.5s ease-in-out !important; /* Added transition */ 641 } 642} 643 644@media (min-width: 992px) { 645 /* Show infinite scroll track for all carousels */ 646 .infinite-scroll-track { 647 display: flex !important; 648 } 649 650 /* Special handling for contact subclass */ 651 .contact .infinite-scroll-track { 652 display: flex !important; 653 } 654 655 .carousel[data-flattened="true"] .w-dyn-list { 656 display: block !important; 657 } 658} 659 660/* Mobile dots */ 661@media (max-width: 991px) { 662 /* Mobile dots styling */ 663 .mobile-dots { 664 display: flex !important; 665 justify-content: center; 666 gap: 8px; 667 position: absolute; 668 bottom: 8px; 669 left: 50%; 670 transform: translateX(-50%); 671 z-index: 10; 672 /* Liquid glass effect */ 673 background: rgba(255, 255, 255, 0.15); 674 backdrop-filter: blur(2px) saturate(180%); 675 border: 0.3px solid rgba(255, 255, 255, 0.5); 676 border-radius: 2rem; 677 padding: 4px 8px; 678 box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2), 679 inset 0 4px 20px rgba(255, 255, 255, 0.3); 680 opacity: 1 !important; /* Ensure dots are visible */ 681 visibility: visible !important; /* Ensure dots are visible */ 682 pointer-events: auto !important; /* Ensure dots are clickable */ 683 } 684 685 /* Liquid shine effect */ 686 .mobile-dots::after { 687 content: ''; 688 position: absolute; 689 top: 0; 690 left: 0; 691 width: 100%; 692 height: 100%; 693 background: rgba(255, 255, 255, 0.1); 694 border-radius: 2rem; 695 backdrop-filter: blur(1px); 696 box-shadow: inset -10px -8px 0px -11px rgba(255, 255, 255, 1), 697 inset 0px -9px 0px -8px rgba(255, 255, 255, 1); 698 opacity: 0.6; 699 z-index: -1; 700 filter: blur(1px) drop-shadow(10px 4px 6px black) brightness(115%); 701 pointer-events: none; 702 } 703 704 /* Force display of dots in ALL hover-wrapper containers */ 705 .hover-wrapper .mobile-dots, 706 .hover-wrapper.is-landscape .mobile-dots, 707 .contact.hover-wrapper .mobile-dots { 708 display: flex !important; 709 opacity: 1 !important; 710 visibility: visible !important; 711 /* Test position - more prominent */ 712 bottom: 20px; 713 /* Enhanced liquid glass for hover wrappers */ 714 background: rgba(255, 255, 255, 0.2); 715 backdrop-filter: blur(3px) saturate(200%); 716 border: 0.5px solid rgba(255, 255, 255, 0.6); 717 padding: 4px 10px; 718 box-shadow: 0 12px 40px rgba(31, 38, 135, 0.3), 719 inset 0 6px 25px rgba(255, 255, 255, 0.4); 720 z-index: 999 !important; 721 } 722 723 .mobile-dot { 724 width: 8px; 725 height: 8px; 726 border-radius: 50%; 727 /* Simple single dot - no border at all */ 728 background: rgba(255, 255, 255, 0.8); 729 cursor: pointer; 730 transition: all 0.3s ease; 731 padding: 0; 732 margin: 0; 733 pointer-events: auto !important; /* Ensure dots are clickable */ 734 } 735 736 .mobile-dot.active { 737 background: white; 738 transform: scale(1.2); 739 /* Simple active state - no border */ 740 } 741 742 /* Swipe overlay for better touch detection */ 743 .swipe-overlay { 744 position: absolute; 745 top: 0; 746 left: 0; 747 width: 100%; 748 height: 100%; 749 z-index: 4; /* Lower z-index to ensure dots are above */ 750 pointer-events: auto !important; 751 } 752 753 754 /* Make sure the hover wrapper has position relative for overlay positioning */ 755 .hover-wrapper { 756 position: relative !important; 757 overflow: hidden; 758 } 759} 760 761/* Hide dots on desktop */ 762@media (min-width: 992px) { 763 .mobile-dots { 764 display: none; 765 } 766} 767 768 769/* End of custom CSS for Webflow */ 770</style></head><body data-barba="wrapper"><div class="u-main-wrapper"><div data-animation="default" data-collapse="medium" data-duration="400" data-easing="ease" data-easing2="ease" role="banner" class="u-navbar w-nav"><div class="u-container u-flex u-space-center-stretch"><div class="u-flex"><a href="/" class="w-nav-brand"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc779897506cafceac8c5e_Logo.avif" loading="lazy" width="33" alt=""/></a></div><nav role="navigation" class="u-nav-menu w-nav-menu"><a href="/services" class="u-nav-link w-nav-link">Services</a><a href="/about" aria-current="page" class="u-nav-link w-nav-link w--current">About</a><a href="/contact" class="u-nav-link flex end w-inline-block"><div>Contact</div><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc7797983c0b49e7dbecb3_arrow-white.png" loading="lazy" width="28" alt="" class="image-45"/></a><a href="/services" data-w-id="14eff895-225b-0d16-a2bb-73cddbb5da6d" class="nav-menu-link _2 w-inline-block"><h2 class="nav-menu-link-text">Services</h2><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc7797983c0b49e7dbecb3_arrow-white.png" loading="lazy" width="28" alt="" class="image-45"/><div class="w-layout-vflex mobile-menu-image-wrapper"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/653eef9de9b5cf57e47e6f1b_Projects.png" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/653eef9de9b5cf57e47e6f1b_Projects-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/653eef9de9b5cf57e47e6f1b_Projects.png 827w" alt="" class="mobile-menu right"/></div></a><a href="/about" aria-current="page" class="nav-menu-link _2 w-inline-block w--current"><h2 class="nav-menu-link-text">About</h2><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc7797983c0b49e7dbecb3_arrow-white.png" loading="lazy" width="28" alt="" class="image-45"/><div class="w-layout-vflex mobile-menu-image-wrapper"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/65506715de2293e054c09deb_Elina-Kustlyvy.jpeg" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/65506715de2293e054c09deb_Elina-Kustlyvy-p-500.jpeg 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/65506715de2293e054c09deb_Elina-Kustlyvy-p-800.jpeg 800w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/65506715de2293e054c09deb_Elina-Kustlyvy-p-1080.jpeg 1080w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/65506715de2293e054c09deb_Elina-Kustlyvy-p-1600.jpeg 1600w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/65506715de2293e054c09deb_Elina-Kustlyvy-p-2000.jpeg 2000w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/65506715de2293e054c09deb_Elina-Kustlyvy.jpeg 2500w" alt="" class="mobile-menu"/></div></a><a href="/contact" class="nav-menu-link _2 w-inline-block"><h2 class="nav-menu-link-text">Contact</h2><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc7797983c0b49e7dbecb3_arrow-white.png" loading="lazy" width="28" alt="" class="image-45"/><div class="w-layout-vflex mobile-menu-image-wrapper"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc779a7b9df7f3c9ff129
770c_Pierre-HermeEdidt%201.avif" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc779a7b9df7f3c9ff129c_Pierre-HermeEdidt%201-p-500.avif 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc779a7b9df7f3c9ff129c_Pierre-HermeEdidt%201.avif 832w" alt="" class="mobile-menu left"/></div></a></nav><div class="menu-button-4 w-nav-button"><div class="w-icon-nav-menu"></div></div></div><div class="u-container"><div class="separator"></div></div></div><div class="u-content"><div class="u-section services_grid invert"><div id="w-node-dd6234a0-f504-0b82-29a7-3690fe0c7133-227e7d9a" class="u-container gap services left-line"><div class="u-block u-service-gap"><svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 959 306" fill="none" class="svg top-32"><path d="M0 301.027C6.11758 291.083 19.4998 266.603 34.0291 237.915L137.646 27.9225C141.469 19.89 144.528 9.18002 144.91 2.67751H165.557L188.498 249.39C190.41 270.045 191.939 284.58 195.38 301.027H165.557C165.939 286.875 166.704 272.34 164.792 250.92L159.822 192.78H63.4699L47.4113 225.675C31.735 257.04 21.0292 287.258 17.2057 301.027H0ZM66.9111 185.895H159.057L145.293 25.245L66.9111 185.895Z" fill="currentColor"></path><path d="M295.472 306C261.825 306 254.561 286.875 230.473 280.755L279.796 49.725C283.619 31.365 285.531 16.065 286.296 0H311.531L270.619 191.633C296.237 131.963 337.531 100.597 369.648 100.597C399.471 100.597 412.853 125.078 412.853 157.208C412.853 218.025 364.677 306 295.472 306ZM253.414 272.723C263.355 290.318 278.266 298.733 294.325 298.733C348.236 298.733 390.295 211.14 390.295 156.825C390.295 131.963 381.501 113.22 360.471 113.22C326.06 113.22 276.737 163.71 261.061 236.003L253.414 272.723Z" fill="currentColor"></path><path d="M496.417 306C449.771 306 429.889 273.105 429.889 233.708C429.889 174.42 474.624 100.597 543.829 100.597C590.475 100.597 610.74 133.493 610.74 172.89C610.74 231.795 565.623 306 496.417 306ZM452.065 241.74C452.065 274.635 465.447 300.645 496.8 300.645C549.182 300.645 588.564 221.468 588.564 164.475C588.564 131.963 574.799 105.57 543.064 105.57C490.682 105.57 452.065 184.747 452.065 241.74Z" fill="currentColor"></path><path d="M691.209 100.597C699.239 100.597 702.297 106.718 699.239 119.34L672.092 231.03C662.151 271.575 667.121 292.995 688.151 292.995C724.091 292.995 771.503 229.883 789.855 143.438L797.885 105.57H822.355C816.237 125.46 809.355 151.853 802.473 183.6L779.914 289.17C794.061 283.815 810.12 268.515 820.826 248.625L825.031 250.155C808.591 280.755 782.591 306 767.297 306C759.267 306 756.591 299.88 759.267 287.258L778.003 198.518C755.826 256.658 718.356 306 676.298 306C646.857 306 638.445 282.285 649.533 235.237L678.592 117.428C664.445 122.783 648.386 138.465 637.68 158.355L633.475 156.443C649.533 126.225 675.915 100.597 691.209 100.597Z" fill="currentColor"></path><path d="M863.413 306C853.089 306 850.795 299.115 853.472 287.258L890.56 112.455H858.06L859.589 105.57H864.942C901.648 105.57 906.236 75.735 915.03 31.365H935.677C928.794 64.6425 918.853 95.2425 887.883 105.57H959L957.471 112.455H911.589L873.736 290.318C899.354 286.493 925.353 268.897 936.059 248.242L939.883 250.155C921.53 285.345 881.001 306 863.413 306Z" fill="currentColor"></path></svg></div><div class="u-block u-service-gap u-flex u-horizontal space-betweenn overflow"><div class="u-block u-service-gap u-max-w"><div class="w-layout-vflex"><h1 class="label_footer color-black u-subtitle">- Elina Kustlyvy</h1><h2 class="label_footer color-black u-subtitle gray">commercial photographer</h2></div><p class="u-p u-large h-1-8">Vancouver, BC<br/>Toronto, ON<br/>Seattle, WA<br/>Los Angeles, CA</p><div class="u-block u-flex u-gap"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/686f467cf0ecdecdba623d1c_elina.png" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/686f467cf0ecdecdba623d1c_elina-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/686f467cf0ecdecdba623d1c_elina-p-800.png 800w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/686f467cf0ecdecdba623d1c_elina.png 1050w" alt="Elina Kustlyvy" class="ab-img"/><div class="u-block u-flex u-vertical u-a-bottom"><a href="https://instagram.com/elina.kustlyvy" target="_blank" class="u-nav-link link">Instagram</a><a href="https://www.linkedin.com/in/elinakustlyvy/" target="_blank" class="u-nav-link link">Linkedin</a></div></div></div><div class="text-info"><p class="u-p u-small">Elina Kustlyvy is a commercial photographer specializing in beauty, cosmetics, and skincare. She works with beauty and lifestyle brands on launches, campaigns, rebrands, and ongoing content â building cohesive image libraries for brands whose visual
770s no longer match the quality of the product or the direction they're moving in.<br/><br/>Her work lives in the details that trip most shoots up. She handles the surfaces that are hardest to photograph â metallic finishes, iridescent packaging, reflective glass â and shoots at the macro scale where beauty is won or lost: the movement of a liquid, the weight of a cream, the fine texture of a cosmetic. That control, paired with a clear read on each brand's commercial goals, is what turns inconsistent visuals into a polished, coherent identity â and the same eye carries into the broader commercial work she takes on beyond beauty.</p><a href="/contact" class="u-wrapper u-flex u-space-center-stretch w-inline-block"><div class="label_footer u-font-family-hatton u-text-color-black">let's work together</div><img width="28" loading="lazy" alt="" src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc779797506cafceac8a30_arrow-link.png" class="image-44 invert"/></a></div></div><div class="u-section"><div class="u-container"></div></div></div><div id="w-node-dd6234a0-f504-0b82-29a7-3690fe0c715d-227e7d9a" class="u-container u-flex v"><div class="carousel"></div><div data-w-id="cc19f053-5e1a-3d9c-6384-a66a163c540f" style="-webkit-transform:translate3d(0, 0px, 0) scale3d(1, 1, 1) rotateX(0) rotateY(0) rotateZ(0) skew(0, 0);-moz-transform:translate3d(0, 0px, 0) scale3d(1, 1, 1) rotateX(0) rotateY(0) rotateZ(0) skew(0, 0);-ms-transform:translate3d(0, 0px, 0) scale3d(1, 1, 1) rotateX(0) rotateY(0) rotateZ(0) skew(0, 0);transform:translate3d(0, 0px, 0) scale3d(1, 1, 1) rotateX(0) rotateY(0) rotateZ(0) skew(0, 0)" class="flex-v"><div class="carousel-wrapper-top vertical w-dyn-list"><div role="list" class="carousel-collection-wrapper vertical w-dyn-items"><div role="listitem" class="collection-item-9 w-dyn-item"><div class="collection-list-wrapper-7 vertical w-dyn-list">
770<script type="text/x-wf-template" id="wf-template-d9c98db8-9334-8b31-84b8-0f8e43ff1bc1">%3Cdiv%20role%3D%22listitem%22%20class%3D%22carousel-image-block%20vertical%20w-dyn-item%20w-dyn-repeater-item%22%3E%3Cimg%20src%3D%22https%3A%2F%2Fcdn.prod.website-files.com%2F653c048c7bdcdc4c8f4346aa%2F680ff4878d572c6cb7e2b666_67ee276addfdf869c2f281c3_BaborXElina.webp%22%20loading%3D%22lazy%22%20alt%3D%22A%20white%20bottle%20of%20Doctor%20Babor%20moisture%20cream%22%20class%3D%22carousel-image%20vertical%22%2F%3E%3C%2Fdiv%3E</script>
770<div role="list" class="carousel-wrapper vertical w-dyn-items"><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b666_67ee276addfdf869c2f281c3_BaborXElina.webp" loading="lazy" alt="A white bottle of Doctor Babor moisture cream" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b640_67ee276a91f3cbee54d366fb_3.motion.jpeg" loading="lazy" alt="A bottle of Rhode sits on a table next to a container" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b688_67ee276a1d7871835df390cf_205.jpeg" loading="lazy" alt="A stack of Rhode makeup products on a table" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b669_67ee276a735362331dfd796e_chanelweb.webp" loading="lazy" alt="A bottle of Chanel Le Vernis nail polish" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/695c4e958d9bc4efbaed7ba5_79.webp" loading="lazy" alt="Lifted Movement resistance bands" sizes="100vw" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/695c4e958d9bc4efbaed7ba5_79-p-500.webp 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/695c4e958d9bc4efbaed7ba5_79-p-800.webp 800w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/695c4e958d9bc4efbaed7ba5_79-p-1080.webp 1080w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/695c4e958d9bc4efbaed7ba5_79-p-1600.webp 1600w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/695c4e958d9bc4efbaed7ba5_79.webp 1638w" class="carousel-image vertical"/></div></div><div class="w-dyn-hide w-dyn-empty"><div>No items found.</div></div></div></div><div role="listitem" class="collection-item-9 w-dyn-item"><div class="collection-list-wrapper-7 vertical w-dyn-list">
770<script type="text/x-wf-template" id="wf-template-d9c98db8-9334-8b31-84b8-0f8e43ff1bc1">%3Cdiv%20role%3D%22listitem%22%20class%3D%22carousel-image-block%20vertical%20w-dyn-item%20w-dyn-repeater-item%22%3E%3Cimg%20src%3D%22https%3A%2F%2Fcdn.prod.website-files.com%2F653c048c7bdcdc4c8f4346aa%2F688e75cf66c46fb22de60c3e_68733b9fa843468a93d33a36_Salt%252526Stone%252520%252520copy.jpeg%22%20loading%3D%22lazy%22%20alt%3D%22Salt%20%26amp%3B%20Stone%20body%20wash%22%20class%3D%22carousel-image%20vertical%22%2F%3E%3C%2Fdiv%3E</script>
770<div role="list" class="carousel-wrapper vertical w-dyn-items"><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/688e75cf66c46fb22de60c3e_68733b9fa843468a93d33a36_Salt%2526Stone%2520%2520copy.jpeg" loading="lazy" alt="Salt & Stone body wash" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff482509c03baa864c222_67ee27575c26c96298d43222_EVERLUCID-texture.jpeg" loading="lazy" alt="Two blue glass balls on a white background" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/6941e3f6ea7ba88b104e25e8_13%201(1).webp" loading="lazy" alt="The image features products from Lifted Movement, a wellness and fitness brand known for functional, design-forward recovery tools" sizes="100vw" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/6941e3f6ea7ba88b104e25e8_13%201(1)-p-500.webp 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/6941e3f6ea7ba88b104e25e8_13%201(1)-p-800.webp 800w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/6941e3f6ea7ba88b104e25e8_13%201(1)-p-1080.webp 1080w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/6941e3f6ea7ba88b104e25e8_13%201(1)-p-1600.webp 1600w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/6941e3f6ea7ba88b104e25e8_13%201(1).webp 1638w" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff482509c03baa864c205_67ee25c3887db36be43c17ac_byredo.webp" loading="lazy" alt="Byredo lipstick served on gold tray with red gradient background" class="carousel-image vertical"/></div><div role="listitem" class="carousel-image-block vertical w-dyn-item w-dyn-repeater-item"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff482509c03baa864c210_67ee2757c018886d0400eb26_Sauces.jpeg" loading="lazy" alt="Two bottles of liquid sit on a red background" class="carousel-image vertical"/></div></div><div class="w-dyn-hide w-dyn-empty"><div>No items found.</div></div></div></div></div></div></div></div></div><div class="u-section gap hide"><div class="u-container"><div class="u-grid _3clm none-gap m-grid"><div class="u-block u-brand-wrapper"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper last"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%202
7700.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper last"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper bottom-last"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper bottom-last"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div><div class="u-block u-brand-wrapper last bottom-last"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png" loading="lazy" sizes="(max-width: 550px) 100vw, 550px" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/6550691de863f11001fbbacf_image%2020.png 550w" alt="" class="u-img-brand"/></div></div></div><div class="u-container"><div class="separator"></div></div></div></div><div class="u-footer u-bg-black u-flex u-vertical gap large white"><div class="u-container"></div><div class="footer-wide">Elina Kustlyvy</div><div class="u-container"><div class="u-grid pd"><div class="u-block u-flex pd none-pd"><div class="u-wrapper"><div class="u-flex gap m-flex"><a href="https://instagram.com/elina.kustlyvy" class="footer-link u-text-color-black">Instagram</a><a href="https://www.linkedin.com/in/elinakustlyvy/" class="footer-link u-text-color-black">Linkedin</a></div></div></div><div class="u-block u-flex u-vertical gap gap-half pd no-pd"><a href="/about" aria-current="page" class="u-footer-link text-color-white large u-text-color-black w--current">About me</a><a href="/services" class="u-footer-link text-color-white large u-text-color-black">Services</a></div><div class="u-block u-flex u-vertical gap pd none-border m-gap"><div class="u-wrapper u-flex u-vertical u-gap"><div class="label_footer u-text-color-black">Email</div><div class="footer-link u-text-color-black">[email protected]</div></div><div class="u-wrapper u-flex u-vertical u-gap"><div class="label_footer u-text-color-black">Phone</div><div class="footer-link u-text-color-black">+1.672.999.9499</div></div><a href="/contact" class="contact-link w-inline-block"><div class="u-wrapper u-flex u-space-center-stretch"><div class="label_footer u-font-family-hatton u-text-color-black">Contact </div><img width="28" loading="lazy" alt="" src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/67dc779797506cafceac8a30_arrow-link.png" class="image-44 invert"/></div></a></div></div></div><div class="u-container"><div class="u-grid m-flex"><div class="u-block u-flex"><div class="u-wrapper"><div class="u-flex gap"><a href="/llm-info" target="_blank" class="footer-link u-text-color-black sm">Hey AI, learn about me</a><div class="footer-link u-text-color-white small u-text-color-black m-smal">2017-2026 © Elina Kustlyvy</div></div></div></div><div class="u-block u-flex"></div><div class="u-block u-flex align-right none-m"><div class="u-wrapper"><div class="u-flex gap"><a href="https://www.awwwards.com/sites/product-photographer" target="_blank" class="w-inline-block"><img src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/68cc4d6bdd4fe7f3f4d7
770da7e_Awwwards%20Logo%202018.png" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/68cc4d6bdd4fe7f3f4d7da7e_Awwwards%20Logo%202018-p-500.png 500w, https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/68cc4d6bdd4fe7f3f4d7da7e_Awwwards%20Logo%202018.png 1024w" alt="" class="awwwards"/></a><a href="/cookies" class="footer-link u-text-color-white small u-text-color-black m-smal">Privacy</a></div></div></div></div></div></div></div>
770<script src="https://d3e54v103j8qbb.cloudfront.net/js/jquery-3.5.1.min.dc5e7f18c8.js?site=653c048c7bdcdc4c8f434684" type="text/javascript" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
770<script src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/js/webflow.schunk.e69d4436a4f69c2f.js" type="text/javascript" integrity="sha384-PDNVQvj+Xy8whLCTSdTl8gGeXfnW/cY1ENTqNfq1ZRBJSvo9GbofXXkc1Ma7EoIp" crossorigin="anonymous"></script>
770<script src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/js/webflow.schunk.c461935f6cc32d4a.js" type="text/javascript" integrity="sha384-WVdsS4wxpqwNXkamY0FVmS3J2tfICuevydVPsHIQ9ziuVIziMPzM4VNk0DUZP25R" crossorigin="anonymous"></script>
770<script src="https://cdn.prod.website-files.com/653c048c7bdcdc4c8f434684/js/webflow.93fc42eb.3e7e5d520b739541.js" type="text/javascript" integrity="sha384-khMnpfeP0MuiiV4pK/XS46vPaSyw8hZHSe42VlxGePMmFKcTb3lWxZ+vLn17nDte" crossorigin="anonymous"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/gsap.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/ScrollTrigger.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/SplitText.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/Flip.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/ScrollSmoother.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/ScrollToPlugin.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/TextPlugin.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/CustomEase.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/CustomWiggle.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/CustomBounce.min.js" type="text/javascript"></script>
770<script src="https://cdn.prod.website-files.com/gsap/3.15.0/EasePack.min.js" type="text/javascript"></script>
770<script type="text/javascript">gsap.registerPlugin(ScrollTrigger,SplitText,Flip,ScrollSmoother,ScrollToPlugin,TextPlugin,CustomEase,CustomWiggle,CustomBounce,EasePack);</script>
770<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 770-771
770 771<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
771GTM-P99G3FB
vendor: 83 bytes, lines 771-772
771" 772height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
773<!-- End Google Tag Manager (noscript) --> 774 775<!-- Google tag (gtag.js) event - delayed navigation helper -->
776<script> 777 // Helper function to delay opening a URL until a gtag event is sent. 778 // Call it in response to an action that should navigate to a URL. 779 function gtagSendEvent(url) { 780 var callback = function () { 781 if (typeof url === 'string') { 782 window.location = url; 783 } 784 }; 785 gtag('event', 'conversion_event_submit_lead_form', { 786 'event_callback': callback, 787 'event_timeout': 2000, 788 // <event_parameters> 789 }); 790 return false; 791 } 792</script>
792 793<!-- Finsweet Attributes v2 (enables fs-list / fs-list-nest) -->
794<script async type="module" 795 src="https://cdn.jsdelivr.net/npm/@finsweet/attributes@2/attributes.js" 796 fs-list></script>
796 797 798<!-- Finsweet scroll-disable helper -->
799<script defer 800 src="https://cdn.jsdelivr.net/npm/@finsweet/attributes-scrolldisable@1/scrolldisable.js"></script>
800 801
802<script async type="module" 803 src="https://cdn.jsdelivr.net/npm/@finsweet/attributes-cmsslider@1/cmsslider.js" 804 fs-list></script>
vendor: 1 bytes, line 804
804
805<script> 806 807/* configuration -------------------------------------------------------- */ 808const DISTANCE_THRESHOLD = 25; 809const SWITCH_COOLDOWN = 120; 810/* ---------------------------------------------------------------------- */ 811 812// Load BetterScroll from CDN 813document.addEventListener('DOMContentLoaded', function() { 814 // Initialize all carousels 815 initCarousels(); 816 817 // Initialize vertical carousels 818 initVerticalCarousels(); 819 820 // Initialize scroll tracks 821 calculateScrollDistance(); 822 823 // Initialize BetterScroll 824 const bsScript = document.createElement('script'); 825 bsScript.src = 'https://unpkg.com/[email protected]/dist/better-scroll.js'; 826 bsScript.onload = function() { 827 // Initialize wheel after a delay to ensure DOM is ready 828 setTimeout(initWheel, 1000); 829 }; 830 document.head.appendChild(bsScript); 831}); 832 833window.Webflow ||= []; 834window.fsAttributes ||= []; 835 836window.Webflow.push(() => { 837 setTimeout(() => initResponsiveGalleries(), 200); 838}); 839 840window.fsAttributes.push(['list', () => { 841 setTimeout(() => initResponsiveGalleries(), 300); 842}]); 843 844function initResponsiveGalleries() { 845 // Aggressively reset all galleries to show first image 846 // Include both standard hover-wrapper and those with contact subclass 847 const allWrappers = document.querySelectorAll('.hover-wrapper, .contact.hover-wrapper'); 848 allWrappers.forEach((wrapper) => { 849 const images = wrapper.querySelectorAll('.hover-img'); 850 if (images.length > 0) { 851 images.forEach(img => { 852 img.classList.remove('top'); 853 }); 854 images[0].classList.add('top'); 855 } 856 }); 857 858 // Update all counter elements with image counts (works for both desktop and mobile) 859 updateImageCounters(); 860 861 // Then add the appropriate interaction based on screen size 862 // Make sure both standard and contact subclass wrappers are processed 863 if (window.innerWidth > 991) { 864 initHoverWrappers(); 865 } else { 866 initMobileSwipeCarousels(); 867 } 868} 869 870/* Function to update counter elements with formatted image counts */ 871function updateImageCounters() { 872 // Wait for images to load, then count them 873 setTimeout(() => { 874 // Get all counter.split elements 875 const counterElements = document.querySelectorAll('.counter.split'); 876 877 // Process each counter individually to find its associated gallery 878 counterElements.forEach((counter, index) => { 879 // Find the item container that contains this counter 880 let itemContainer = counter.closest('.item-collection, .w-dyn-item'); 881 882 if (!itemContainer) { 883 return; 884 } 885 886 // Look for hover-wrapper within this item container 887 const hoverWrapper = itemContainer.querySelector('.hover-wrapper, .contact.hover-wrapper'); 888 889 if (hoverWrapper) { 890 // Count images in this specific hover wrapper 891 const images = hoverWrapper.querySelectorAll('.hover-img, .carousel-image, img'); 892 const imageCount = images.length; 893 894 // Format the count with leading zeros (001, 007, etc.) 895 const formattedCount = `(${String(imageCount).padStart(3, '0')})`; 896 897 // Update this counter with its specific image count 898 counter.textContent = formattedCount; 899 } else { 900 // Try to find images in the entire item container 901 const itemImages = itemContainer.querySelectorAll('.hover-img, .carousel-image, img'); 902 const imageCount = itemImages.length; 903 904 // Format the count with leading zeros 905 const formattedCount = `(${String(imageCount).padStart(3, '0')})`; 906 907 // Update this counter 908 counter.textContent = formattedCount; 909 } 910 }); 911 }, 1000); // Wait 1 second for images to load 912} 913 914/* Desktop hover functionality */ 915function initHoverWrappers() { 916 // Include both standard hover-wrapper and those with contact subclass 917 const wrappers = document.querySelectorAll('.hover-wrapper, .contact.hover-wrapper'); 918 919 // Update all counter elements with image counts 920 updateImageCounters(); 921 922 wrappers.forEach(wrap => { 923 const isContact = wrap.classList.contains('contact'); 924 925 if (wrap.dataset.hoverInitialized) { 926 return; 927 } 928 wrap.dataset.hoverInitialized = 1; 929 930 const observer = new MutationObserver(() => { 931 const imgs = wrap.querySelectorAll('.hover-img'); 932 if (imgs.length >= 2){ 933 observer.disconnect(); 934 setTimeout(() => {
935 addHoverEffect(wrap); 936 }, 100); 937 } 938 }); 939 observer.observe(wrap, {childList:true, subtree:true}); 940 941 const imgCount = wrap.querySelectorAll('.hover-img').length; 942 if (imgCount >= 2){ 943 observer.disconnect(); 944 setTimeout(() => { 945 addHoverEffect(wrap); 946 }, 100); 947 } else if (imgCount === 1) { 948 // Handle single image case 949 observer.disconnect(); 950 const singleImg = wrap.querySelector('.hover-img'); 951 if (singleImg) { 952 singleImg.classList.add('top'); 953 } 954 } 955 }); 956} 957 958function addHoverEffect(item) { 959 if (item.dataset.hoverBound) { 960 return; 961 } 962 item.dataset.hoverBound = 1; 963 964 const images = Array.from(item.querySelectorAll('.hover-img')); 965 if (images.length < 2) { 966 return; 967 } 968 969 let currentIndex = 0; 970 let canSwitch = true; 971 let lastMousePos = { x: 0, y: 0 }; 972 let accumulatedDistance = 0; 973 974 // Track timeouts to prevent race conditions 975 let switchTimeout = null; 976 let cooldownTimeout = null; 977 978 // Force all images to load eagerly 979 images.forEach(img => { 980 img.loading = 'eager'; 981 if (img.dataset.src && !img.src) { 982 img.src = img.dataset.src; 983 } 984 }); 985 986 // Initialize - ensure first image is visible 987 function initialize() { 988 images.forEach((img) => { 989 img.classList.remove('top'); 990 }); 991 images[0].classList.add('top'); 992 } 993 994 // Wait for images to load 995 let loadCount = 0; 996 const checkLoad = () => { 997 loadCount++; 998 if (loadCount >= images.length) { 999 initialize(); 1000 } 1001 }; 1002 1003 images.forEach(img => { 1004 if (img.complete) { 1005 checkLoad(); 1006 } else { 1007 img.addEventListener('load', checkLoad); 1008 img.addEventListener('error', checkLoad); 1009 } 1010 }); 1011 1012 // Fallback initialization 1013 setTimeout(() => { 1014 if (loadCount < images.length) { 1015 initialize(); 1016 } 1017 }, 1000); 1018 1019 // Switch to next image 1020 function switchImage() { 1021 if (!canSwitch || !images || images.length < 2) return; 1022 1023 const nextIndex = (currentIndex + 1) % images.length; 1024 1025 // Switch to next image 1026 1027 // Add defensive checks before accessing classList 1028 if (images[nextIndex] && images[nextIndex].classList) { 1029 // Add top class to next image first 1030 images[nextIndex].classList.add('top'); 1031 } 1032 1033 // Remove top class from current image after a delay with defensive checks 1034 const prevIndex = currentIndex; 1035 clearTimeout(switchTimeout); // Clear any pending timeout 1036 switchTimeout = setTimeout(() => { 1037 if (images && images[prevIndex] && images[prevIndex].classList) { 1038 images[prevIndex].classList.remove('top'); 1039 } 1040 }, 50); 1041 1042 currentIndex = nextIndex; 1043 1044 // Cooldown 1045 canSwitch = false; 1046 clearTimeout(cooldownTimeout); // Clear any pending timeout 1047 cooldownTimeout = setTimeout(() => { 1048 canSwitch = true; 1049 }, SWITCH_COOLDOWN); 1050 } 1051 1052 // Reset to first image 1053 function reset() { 1054 1055 // Add defensive checks to prevent errors 1056 if (!images || images.length === 0) return; 1057 1058 // Check if first image exists before accessing it 1059 if (images[0] && images[0].classList) { 1060 images[0].classList.add('top'); 1061 } 1062 1063 // Remove top from others after delay with defensive checks 1064 setTimeout(() => { 1065 if (!images) return; 1066 1067 images.forEach((img, i) => { 1068 if (i !== 0 && img && img.classList) { 1069 img.classList.remove('top'); 1070 } 1071 }); 1072 }, 50); 1073 1074 currentIndex = 0; 1075 accumulatedDistance = 0; 1076 canSwitch = true; 1077 } 1078 1079 // Distance calculation 1080 function getDistance(x1, y1, x2, y2) { 1081 return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 1082 } 1083 1084 // Mouse events 1085 item.addEventListener('mouseenter', function() { 1086 }); 1087 1088 item.addEventListener('mousemove', function(e) { 1089 if (lastMousePos.x !== 0 || lastMousePos.y !== 0) { 1090 const distance = getDistance(lastMousePos.x, lastMousePos.y, e.clientX, e.clientY); 1091 accumulatedDistance += distance; 1092 1093 if (accumulatedDistance >
1093= DISTANCE_THRESHOLD && canSwitch) { 1094 switchImage(); 1095 accumulatedDistance = 0; 1096 } 1097 } 1098 1099 lastMousePos = { x: e.clientX, y: e.clientY }; 1100 }); 1101 1102 item.addEventListener('mouseleave', function() { 1103 // Clear any pending timeouts to prevent race conditions 1104 clearTimeout(switchTimeout); 1105 clearTimeout(cooldownTimeout); 1106 reset(); 1107 lastMousePos = { x: 0, y: 0 }; 1108 }); 1109} // Close addHoverEffect function 1110 1111/* Main carousel initialization function */ 1112function initCarousels() { 1113 // Initialize responsive galleries first 1114 initResponsiveGalleries(); 1115 1116 // Initialize flattened carousels (infinite scroll) 1117 flattenCarousel(); 1118} 1119 1120/* Mobile swipe functionality - FIXED VERSION */ 1121function initMobileSwipeCarousels() { 1122 console.log('ð± initMobileSwipeCarousels called - window width:', window.innerWidth); 1123 1124 // Only run on mobile devices (â¤991px) 1125 if (window.innerWidth > 991) { 1126 console.log('ð± Screen too wide for mobile swipe, skipping'); 1127 return; 1128 } 1129 1130 // Include both standard hover-wrapper and those with contact subclass 1131 const wrappers = document.querySelectorAll('.hover-wrapper, .contact.hover-wrapper'); 1132 console.log('ð± Found', wrappers.length, 'hover-wrapper elements'); 1133 wrappers.forEach((wrapper, index) => { 1134 console.log(`ð± Processing wrapper ${index + 1}:`, wrapper); 1135 const isContact = wrapper.classList.contains('contact'); 1136 1137 // Skip if already bound or if it's inside a carousel (except hero carousels) 1138 const carouselParent = wrapper.closest('.carousel'); 1139 if (wrapper.dataset.swipeBound || (carouselParent && !carouselParent.classList.contains('hero-carousel'))) { 1140 return; 1141 } 1142 1143 // Wait longer for images to be created by Konpo animation 1144 setTimeout(() => { 1145 const images = wrapper.querySelectorAll('.hover-img'); 1146 console.log('â° After longer timeout, found', images.length, 'images in wrapper'); 1147 1148 // Add mobile swipe to ALL containers regardless of image count 1149 // This ensures dots are added to every container for testing 1150 addMobileSwipe(wrapper); 1151 }, 2000); // Increased from 300ms to 2000ms to wait for Konpo 1152 }); 1153} 1154 1155function addMobileSwipe(wrapper) { 1156 console.log('ð¯ addMobileSwipe called for:', wrapper); 1157 if (wrapper.dataset.swipeBound) { 1158 console.log('â ï¸ Wrapper already bound, skipping'); 1159 return; 1160 } 1161 wrapper.dataset.swipeBound = 1; 1162 1163 const images = Array.from(wrapper.querySelectorAll('.hover-img')); 1164 console.log('ð¼ï¸ Found', images.length, 'images in wrapper'); 1165 1166 // TESTING: Always add dots even for single image containers 1167 if (images.length === 0) { 1168 console.log('â No images found, returning'); 1169 return; 1170 } 1171 1172 // For single image, still make it visible 1173 if (images.length === 1) { 1174 const singleImg = images[0]; 1175 singleImg.classList.add('top'); 1176 // Continue to add dots for testing - create a dummy array for dots 1177 // This will create at least one dot for single-image containers 1178 images = [images[0], images[0]]; // Duplicate the image for dot creation 1179 } 1180 1181 // Reset all images first - remove any existing 'top' classes 1182 images.forEach(img => img.classList.remove('top')); 1183 1184 // Always set the first image as active initially 1185 images[0].classList.add('top'); 1186 let currentIndex = 0; 1187 1188 // Create dots container 1189 console.log('ð Creating dots container with', images.length, 'dots'); 1190 const dotsContainer = document.createElement('div'); 1191 dotsContainer.className = 'mobile-dots'; 1192 dotsContainer.style.display = 'flex'; 1193 dotsContainer.style.opacity = '1'; 1194 dotsContainer.style.visibility = 'visible'; 1195 dotsContainer.style.zIndex = '999'; 1196 console.log('ð Dots container styles applied'); 1197 1198 // Create dot elements 1199 images.forEach((_, i) => { 1200 const dot = document.createElement('button'); 1201 dot.className = 'mobile-dot' + (i === currentIndex ? ' active' : ''); 1202 dot.dataset.index = i; 1203 dot.addEventListener('click', function() { 1204 showImage(i); 1205 }); 1206 dotsContainer.appendChild(dot); 1207 }); 1208 1209 // Add dots to wrapper 1210 wrapper.appendChild(dotsContainer); 1211 1212 const dots = wrapper.querySelectorAll('.mobile-dot'); 1213 1214 // Track if we're in the middle of a transition 1215 let isTransitioning = false;
1216 1217 // Preload all images to prevent template background from showing 1218 function preloadImages() { 1219 images.forEach(img => { 1220 // Force eager loading for all images 1221 img.loading = 'eager'; 1222 1223 // If image has data-src but no src, set the src 1224 if (img.dataset.src && !img.src) { 1225 img.src = img.dataset.src; 1226 } 1227 1228 // Ensure image is loaded in background 1229 if (img.complete) { 1230 // Image already loaded 1231 } else { 1232 // Set up load event 1233 img.addEventListener('load', () => { 1234 // Image loaded successfully 1235 }); 1236 img.addEventListener('error', () => { 1237 // Handle image load error silently 1238 }); 1239 } 1240 1241 // Hide all images initially except the current one 1242 if (images.indexOf(img) !== currentIndex) { 1243 img.style.opacity = '0'; 1244 img.style.visibility = 'hidden'; 1245 } 1246 }); 1247 } 1248 1249 // Call preload immediately 1250 preloadImages(); 1251 1252 function showImage(index) { 1253 if (isTransitioning || index === currentIndex) return; 1254 isTransitioning = true; 1255 1256 // Get container type 1257 const isLandscape = wrapper.classList.contains('is-landscape'); 1258 1259 // Determine swipe direction 1260 const isNext = index > currentIndex || (currentIndex === images.length - 1 && index === 0); 1261 const isPrev = index < currentIndex || (currentIndex === 0 && index === images.length - 1); 1262 1263 // Ensure the next image is ready before transition 1264 if (images[index]) { 1265 // Make sure the next image is loaded and ready 1266 images[index].style.opacity = '0'; 1267 images[index].style.visibility = 'visible'; 1268 images[index].style.zIndex = '2'; // Place above current image but below top 1269 } 1270 1271 // Remove all transition classes from all images 1272 images.forEach(img => { 1273 img.classList.remove('top', 'slide-in-right', 'slide-in-left', 'slide-out-right', 'slide-out-left'); 1274 }); 1275 1276 // Set initial positions for animation 1277 if (images[currentIndex]) { 1278 // Current image slides out 1279 if (isNext) { 1280 images[currentIndex].classList.add('slide-out-left'); 1281 } else if (isPrev) { 1282 images[currentIndex].classList.add('slide-out-right'); 1283 } 1284 } 1285 1286 if (images[index]) { 1287 // Position the new image off-screen initially 1288 if (isNext) { 1289 images[index].classList.add('slide-in-right'); 1290 } else if (isPrev) { 1291 images[index].classList.add('slide-in-left'); 1292 } 1293 1294 // Force a reflow to ensure styles are applied before transition 1295 void images[index].offsetWidth; 1296 1297 // Now move the new image to center position and make visible 1298 setTimeout(() => { 1299 images[index].style.opacity = '1'; 1300 images[index].classList.remove('slide-in-right', 'slide-in-left'); 1301 images[index].classList.add('top'); 1302 }, 20); 1303 } 1304 1305 // Update dots 1306 dots.forEach((dot, i) => { 1307 if (i === index) { 1308 dot.classList.add('active'); 1309 } else { 1310 dot.classList.remove('active'); 1311 } 1312 }); 1313 1314 // Update current index 1315 currentIndex = index; 1316 1317 // Allow next transition after animation completes 1318 setTimeout(function() { 1319 // Clean up all transition classes 1320 images.forEach(img => { 1321 img.classList.remove('slide-out-left', 'slide-out-right'); 1322 if (!img.classList.contains('top')) { 1323 img.style.opacity = '0'; 1324 img.style.visibility = 'hidden'; 1325 } 1326 }); 1327 isTransitioning = false; 1328 }, 350); // Slightly longer than CSS transition 1329 } 1330 1331 // Create a transparent swipe overlay for better touch detection 1332 const swipeOverlay = document.createElement('div'); 1333 swipeOverlay.className = 'swipe-overlay'; 1334 swipeOverlay.style.position = 'absolute'; 1335 swipeOverlay.style.top = '0'; 1336 swipeOverlay.style.left = '0'; 1337 swipeOverlay.style.width = '100%'; 1338 swipeOverlay.style.height = '100%'; 1339 swipeOverlay.style.zIndex = '2'; // Lower z-index to ensure it doesn't block dots 1340 wrapper.insertBefore(swipeOverlay, wrapper.firstChild); // Insert at the beginning to ensure it's below dots but above images 1341 1342 // Enhanced swipe detection variables 1343 let touchStartX = 0; 1344 let touchStartY = 0; 1345 let touchEndX = 0; 1346 let touchEndY = 0; 1347 let minSwipeDistance = 30; 1348 1349 // Add touch event listeners to the overlay 1350 swipeOverlay.addEventListener('touchstart', function(e) { 1351 console.log('ð Touch start detected'); 1352 if (isTransitioning) { 1353 console.log('â ï¸ Transition in progress, ignoring'); 1354 return; 1355 } 1356 touchStartX = e.changedTouches[0].screenX; 1357 touchStartY = e.changedTouches[0].screenY; 1358 console.log('ð Touch start position:', touchStartX, touchStartY); 1359 }, { passive: false }); 1360 1361 swipeOverlay.addEventListener('touchmove', function(e) { 1362 if (isTransitioning) return; 1363 1364 // Get current touch position 1365 touchEndX = e.changedTouches[0].screenX; 1366 touchEndY = e.changedTouches[0].screenY; 1367 1368 // Calculate horizontal and vertical differences 1369 const diffX = Math.abs(touchStartX - touchEndX); 1370 const diffY = Math.abs(touchStartY - touchEndY); 1371 1372 // If horizontal swipe is dominant and significant, prevent default scrolling 1373 if (diffX > diffY && diffX > 10) { 1374 e.preventDefault(); 1375 } 1376 }, { passive: false }); 1377 1378 swipeOverlay.addEventListener('touchend', function(e) { 1379 if (isTransitioning) return; 1380 touchEndX = e.changedTouches[0].screenX; 1381 touchEndY = e.changedTouches[0].screenY; 1382 1383 handleSwipe(); 1384 }, { passive: true }); 1385 1386 function handleSwipe() { 1387 // Calculate horizontal and vertical differences 1388 const diffX = touchStartX - touchEndX; 1389 const diffY = Math.abs(touchStartY - touchEndY); 1390 1391 // Ensure it's a horizontal swipe (more horizontal than vertical) 1392 if (Math.abs(diffX) > diffY && Math.abs(diffX) > minSwipeDistance) { 1393 if (diffX > 0) {
1394 // Swipe left - go to next image or loop to first 1395 if (currentIndex < images.length - 1) { 1396 showImage(currentIndex + 1); 1397 } else { 1398 // Loop back to the first image 1399 showImage(0); 1400 } 1401 } else if (diffX < 0) { 1402 // Swipe right - go to previous image or loop to last 1403 if (currentIndex > 0) { 1404 showImage(currentIndex - 1); 1405 } else { 1406 // Loop back to the last image 1407 showImage(images.length - 1); 1408 } 1409 } 1410 } 1411 } 1412} 1413 1414/* Carousel functionality */ 1415document.addEventListener('DOMContentLoaded', function() { 1416 setTimeout(() => { 1417 flattenCarousel(); 1418 }, 1000); 1419}); 1420 1421window.fsAttributes = window.fsAttributes || []; 1422window.fsAttributes.push(['cmslist', () => { 1423 setTimeout(flattenCarousel, 500); 1424}]); 1425 1426function flattenCarousel() { 1427 // Process ALL carousels, including those with subclasses like "contact" 1428 const carousels = document.querySelectorAll('.carousel'); 1429 1430 1431 carousels.forEach((carousel, index) => { 1432 if (carousel.dataset.flattened) return; 1433 1434 1435 carousel.dataset.flattened = 'true'; 1436 1437 const allImages = []; 1438 const images = carousel.querySelectorAll('.carousel-image, .hover-img'); 1439 1440 1441 images.forEach(img => { 1442 if (img.src && img.src !== '') { 1443 allImages.push({ 1444 src: img.src, 1445 alt: img.alt || '', 1446 loading: img.loading || 'lazy' 1447 }); 1448 } 1449 }); 1450 1451 if (allImages.length === 0) return; 1452 1453 const tripleImages = [...allImages, ...allImages, ...allImages]; 1454 1455 const newHTML = ` 1456 <div class="infinite-scroll-track"> 1457 ${tripleImages.map(img => ` 1458 <div class="scroll-image-item"> 1459 <img src="${img.src}" alt="${img.alt}" loading="${img.loading}" class="scroll-image"> 1460 </div> 1461 `).join('')} 1462 </div> 1463 `; 1464 1465 carousel.innerHTML = newHTML; 1466 }); 1467 1468 setTimeout(() => { 1469 calculateScrollDistance(); 1470 }, 500); 1471} 1472 1473function initVerticalCarousels() { 1474 1475 1476 // Find all potential vertical carousel elements 1477 const verticalElements = [ 1478 ...document.querySelectorAll('.vertical-carousel-track'), 1479 ...document.querySelectorAll('.vertical-scroll'), 1480 ...document.querySelectorAll('.vertical-carousel'), 1481 ...document.querySelectorAll('.vertical-scroll-container') 1482 ]; 1483 1484 if (verticalElements.length > 0) { 1485 verticalElements.forEach((element, index) => { 1486 // Add vertical-scroll-track class to the element if it doesn't have it 1487 if (!element.classList.contains('vertical-scroll-track')) { 1488 element.classList.add('vertical-scroll-track'); 1489 1490 } 1491 1492 // Ensure parent containers have proper styling 1493 const parent = element.parentElement; 1494 if (parent) { 1495 parent.style.overflow = 'hidden'; 1496 parent.style.position = 'relative'; 1497 parent.style.height = '100%'; 1498 1499 // Also check grandparent if it exists 1500 const grandparent = parent.parentElement; 1501 if (grandparent) { 1502 grandparent.style.overflow = 'hidden'; 1503 grandparent.style.position = 'relative'; 1504 grandparent.style.height = '100%'; 1505 } 1506 } 1507 1508 // Set initial styles for the vertical track 1509 element.style.position = 'relative'; 1510 element.style.willChange = 'transform'; 1511 element.style.height = 'auto'; 1512 1513 // Check if we need to duplicate items for smooth scrolling 1514 const items = element.children; 1515 if (items.length > 0) { 1516 const totalHeight = element.scrollHeight; 1517 const containerHeight = parent ? parent.offsetHeight : element.offsetHeight; 1518 1519 // If content is not tall enough for smooth infinite scrolling 1520 if (totalHeight < containerHeight * 2) { 1521 1522 1523 // Clone items until we have enough height 1524 const originalItems = Array.from(items); 1525 let currentHeight = totalHeight; 1526 1527 while (currentHeight < containerHeight * 3) { 1528 originalItems.forEach(item => { 1529 const clone = item.cloneNode(true); 1530 element.appendChild(clone); 1531 }); 1532 currentHeight = element.scrollHeight; 1533 } 1534 1535 1536 } 1537 } 1538 }); 1539 } else { 1540 1541 } 1542} 1543 1544function calculateScrollDistance() { 1545 1546 1547 // Find all scroll tracks - both horizontal and vertical 1548 const horizontalTracks = document.querySelectorAll('.scroll-track, .infinite-scroll-track'); 1549 const verticalTrackSelectors = ['.vertical-carousel-track', '.vertical-scroll', '.vertical-carousel', '.vertical-scroll-container']; 1550 1551 // Find vertical tracks using various selectors 1552 let verticalTracks = [];
1553 verticalTrackSelectors.forEach(selector => { 1554 const elements = document.querySelectorAll(selector); 1555 elements.forEach(el => { 1556 // Add the vertical-scroll-track class to identify it for animation 1557 if (!el.classList.contains('vertical-scroll-track')) { 1558 el.classList.add('vertical-scroll-track'); 1559 } 1560 verticalTracks.push(el); 1561 }); 1562 }); 1563 1564 // Combine horizontal and vertical tracks 1565 const allTracks = [...horizontalTracks, ...verticalTracks]; 1566 1567 if (allTracks.length === 0) { 1568 1569 return; 1570 } 1571 1572 allTracks.forEach((track, index) => { 1573 const isContact = track.closest('.contact') !== null; 1574 const isVertical = track.classList.contains('vertical-scroll-track'); 1575 1576 1577 const images = track.querySelectorAll('.scroll-image'); 1578 let imagesLoaded = 0; 1579 1580 function checkAllLoaded() { 1581 imagesLoaded++; 1582 if (imagesLoaded === images.length || imagesLoaded > 10) { 1583 setAnimation(track, index); 1584 } 1585 } 1586 1587 images.forEach(img => { 1588 if (img.complete) { 1589 checkAllLoaded(); 1590 } else { 1591 img.addEventListener('load', checkAllLoaded); 1592 img.addEventListener('error', checkAllLoaded); 1593 } 1594 }); 1595 1596 // Fallback in case images don't load properly 1597 setTimeout(() => setAnimation(track, index), 2000); 1598 }); 1599 1600 function setAnimation(track, index) { 1601 // Determine if this is a vertical carousel 1602 const isVertical = track.classList.contains('vertical-scroll-track'); 1603 1604 if (isVertical) { 1605 // Handle vertical scrolling 1606 const totalHeight = track.scrollHeight; 1607 const containerHeight = track.parentElement ? track.parentElement.offsetHeight : 500; 1608 const moveDistance = totalHeight; 1609 1610 // Ensure the track has enough height for scrolling 1611 if (totalHeight <= containerHeight) { 1612 // If content isn't tall enough, duplicate items to make it scrollable 1613 const items = track.children; 1614 if (items.length > 0) { 1615 // Clone all items and append them to create more scrolling content 1616 for (let i = 0; i < 10; i++) { 1617 Array.from(items).forEach(item => { 1618 const clone = item.cloneNode(true); 1619 track.appendChild(clone); 1620 }); 1621 } 1622 } 1623 } 1624 1625 // Create unique animation name for vertical track 1626 const animationName = `dynamicScrollVertical${index}`; 1627 1628 const keyframes = ` 1629 @keyframes ${animationName} { 1630 0% { transform: translate3d(0, 0, 0); } 1631 100% { transform: translate3d(0, -${moveDistance}px, 0); } 1632 } 1633 `; 1634 1635 // Check if style already exists 1636 const existingStyle = document.getElementById(`scroll-style-vertical-${index}`); 1637 if (existingStyle) { 1638 existingStyle.textContent = keyframes; 1639 } else { 1640 const style = document.createElement('style'); 1641 style.id = `scroll-style-vertical-${index}`; 1642 style.textContent = keyframes; 1643 document.head.appendChild(style); 1644 } 1645 1646 // Set animation with slower speed for vertical scrolling 1647 track.style.animation = `${animationName} 40s linear infinite`; 1648 1649 // Ensure the track stays visible 1650 track.style.display = 'block'; 1651 track.style.visibility = 'visible'; 1652 track.style.opacity = '1'; 1653 track.style.willChange = 'transform'; 1654 1655 // Make sure parent container has proper styling 1656 const parentContainer = track.parentElement; 1657 if (parentContainer) { 1658 parentContainer.style.overflow = 'hidden'; 1659 parentContainer.style.position = 'relative'; 1660 parentContainer.style.height = '100%'; 1661 1662 // Ensure grandparent container also has proper styling 1663 const grandparentContainer = parentContainer.parentElement; 1664 if (grandparentContainer) { 1665 grandparentContainer.style.overflow = 'hidden'; 1666 grandparentContainer.style.position = 'relative'; 1667 } 1668 } 1669 } else { 1670 // Handle horizontal scrolling (original code) 1671 const totalWidth = track.scrollWidth; 1672 const containerWidth = track.parentElement.offsetWidth; 1673 const moveDistance = totalWidth / 3; 1674 1675 // Create unique animation name for each track 1676 const animationName = `dynamicScroll${index}`; 1677
1678 const keyframes = ` 1679 @keyframes ${animationName} { 1680 0% { transform: translate3d(0, 0, 0); } 1681 100% { transform: translate3d(-${moveDistance}px, 0, 0); } 1682 } 1683 `; 1684 1685 // Check if style already exists 1686 const existingStyle = document.getElementById(`scroll-style-${index}`); 1687 if (existingStyle) { 1688 existingStyle.textContent = keyframes; 1689 } else { 1690 const style = document.createElement('style'); 1691 style.id = `scroll-style-${index}`; 1692 style.textContent = keyframes; 1693 document.head.appendChild(style); 1694 } 1695 1696 track.style.animation = `${animationName} 30s linear infinite`; 1697 } 1698 1699 } 1700} 1701 1702// ============================================================================ 1703// KONPO LOADING ANIMATION 1704// ============================================================================ 1705// Recreates the Konpo-style loading animation targeting #pre-loader 1706const initKonpoAnimation = () => { 1707 // Check if this is the first visit and we're on the home page 1708 if (sessionStorage.getItem('konpo-animation-seen') || 1709 (!window.location.pathname.includes('/') && window.location.pathname !== '/index.html')) { 1710 // If not running animation, mark as complete and restore visibility 1711 document.body.classList.add('animation-complete'); 1712 document.body.style.visibility = 'visible'; 1713 document.body.style.overflow = ''; 1714 return; 1715 } 1716 1717 // Hide body content for animation 1718 document.body.style.visibility = 'hidden'; 1719 document.body.style.overflow = 'hidden'; 1720 1721 // Mark animation as seen 1722 sessionStorage.setItem('konpo-animation-seen', 'true'); 1723 1724 // Image URLs from the #pre-loader gallery - BaborXElina first for top position 1725 const imageUrls = [ 1726 'https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b666_67ee276addfdf869c2f281c3_BaborXElina.webp', 1727 'https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b640_67ee276a91f3cbee54d366fb_3.motion.jpeg', 1728 'https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b688_67ee276a1d7871835df390cf_205.jpeg', 1729 'https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b669_67ee276a735362331dfd796e_chanelweb.webp', 1730 'https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b6a0_67ee276a5e8e2b6b4b2b5b6b_image5.webp', 1731 'https://cdn.prod.website-files.com/653c048c7bdcdc4c8f4346aa/680ff4878d572c6cb7e2b6a1_67ee276a5e8e2b6b4b2b5b6c_image6.webp' 1732 ]; 1733 1734 // Create loader HTML 1735 const createLoader = () => { 1736 const loader = document.createElement('div'); 1737 loader.className = 'konpo-loader'; 1738 loader.innerHTML = ` 1739 <div class="konpo-loader-bg"></div> 1740 1741 <div class="konpo-counter"> 1742 <div class="konpo-counter-1 digit"></div> 1743 <div class="konpo-counter-2 digit"></div> 1744 <div class="konpo-counter-3 digit"></div> 1745 </div> 1746 1747 <div class="konpo-images-container"></div> 1748 `; 1749 1750 return loader; 1751 }; 1752 1753 // Create counter digits 1754 const createCounterDigits = (loader) => { 1755 const counter1 = loader.querySelector('.konpo-counter-1'); 1756 const num0 = document.createElement('div'); 1757 num0.className = 'konpo-num'; 1758 num0.textContent = '0'; 1759 counter1.appendChild(num0); 1760 1761 const num1 = document.createElement('div'); 1762 num1.className = 'konpo-num konpo-num1offset1'; 1763 num1.textContent = '1'; 1764 counter1.appendChild(num1); 1765 1766 const counter2 = loader.querySelector('.konpo-counter-2'); 1767 for (let i = 0; i <= 10; i++) { 1768 const numDiv = document.createElement('div'); 1769 numDiv.className = i === 1 ? 'konpo-num konpo-num1offset2' : 'konpo-num'; 1770 numDiv.textContent = i === 10 ? '0' : i; 1771 counter2.appendChild(numDiv); 1772 } 1773 1774 const counter3 = loader.querySelector('.konpo-counter-3'); 1775 for (let i = 0; i < 30; i++) { 1776 const numDiv = document.createElement('div'); 1777 numDiv.className = 'konpo-num'; 1778 numDiv.textContent = i % 10; 1779 counter3.appendChild(numDiv); 1780 } 1781 const finalNum = document.createElement('div'); 1782 finalNum.className = 'konpo-num'; 1783 finalNum.textContent = '0'; 1784 counter3.appendChild(finalNum); 1785 }; 1786 1787 // Create image elements 1788 const createImages = (loader) => {
1789 imageUrls.forEach((url, index) => { 1790 const img = document.createElement('img'); 1791 img.src = url; 1792 img.className = 'konpo-img'; 1793 img.alt = `Animation image ${index + 1}`; 1794 img.loading = 'eager'; 1795 // Position BaborXElina (index 0) at top, others staggered below 1796 if (index === 0) { 1797 img.style.left = '50px'; 1798 img.style.top = '20px'; 1799 img.style.zIndex = '10001'; // Ensure it's on top 1800 } else { 1801 img.style.left = `${20 + (index * 40)}px`; 1802 img.style.top = `${20 + (index * 30)}px`; 1803 } 1804 // Add error handling for broken images 1805 img.onerror = function() { 1806 this.style.display = 'none'; 1807 console.warn('Failed to load image:', url); 1808 }; 1809 loader.appendChild(img); 1810 }); 1811 }; 1812 1813 // Animate counter 1814 const animateCounter = (counter, duration, delay = 0) => { 1815 const numHeight = counter.querySelector('.konpo-num').clientHeight; 1816 const totalDistance = (counter.querySelectorAll('.konpo-num').length - 1) * numHeight; 1817 1818 gsap.to(counter, { 1819 y: -totalDistance, 1820 duration: duration, 1821 delay: delay, 1822 ease: 'power2.inOut' 1823 }); 1824 }; 1825 1826 // Animate images to target gallery 1827 const animateImages = (loader) => { 1828 const images = loader.querySelectorAll('.konpo-img'); 1829 const targetGallery = document.querySelector('#pre-loader'); 1830 1831 console.log('ð¯ KONPO DEBUG: Starting image animation'); 1832 console.log('ð Images found:', images.length); 1833 console.log('ð¯ Target gallery element:', targetGallery); 1834 1835 if (!targetGallery) { 1836 console.warn('â Target gallery #pre-loader not found'); 1837 return; 1838 } 1839 1840 // Find individual hover-img elements within the gallery 1841 const hoverImages = targetGallery.querySelectorAll('.hover-img'); 1842 console.log('ð¼ï¸ Hover images found:', hoverImages.length); 1843 1844 // Use GSAP timeline for smooth animation 1845 const mainTimeline = gsap.timeline(); 1846 1847 // Animate images to individual hover-img positions 1848 images.forEach((img, index) => { 1849 img.classList.remove('animate-out'); 1850 1851 // Get target hover-img element (cycle through available ones) 1852 const targetHoverImg = hoverImages[index % hoverImages.length]; 1853 1854 if (!targetHoverImg) { 1855 console.warn(`â No target hover-img found for image ${index + 1}`); 1856 return; 1857 } 1858 1859 const timeline = gsap.timeline(); 1860 1861 // Get initial image position 1862 const imgRect = img.getBoundingClientRect(); 1863 const targetRect = targetHoverImg.getBoundingClientRect(); 1864 1865 // Calculate target center 1866 const targetX = targetRect.left + (targetRect.width / 2); 1867 const targetY = targetRect.top + (targetRect.height / 2); 1868 1869 // Calculate movement delta 1870 const deltaX = targetX - (imgRect.left + imgRect.width / 2); 1871 const deltaY = targetY - (imgRect.top + imgRect.height / 2); 1872 1873 // Calculate scale to match target size 1874 const scaleX = targetRect.width / imgRect.width; 1875 const scaleY = targetRect.height / imgRect.height; 1876 const targetScale = Math.min(scaleX, scaleY); // Use smaller scale to fit within target 1877 1878 console.log(`ð¯ Image ${index + 1} targeting hover-img ${(index % hoverImages.length) + 1}:`, { 1879 targetRect: { 1880 left: targetRect.left, 1881 top: targetRect.top, 1882 width: targetRect.width, 1883 height: targetRect.height 1884 }, 1885 targetCenter: { targetX, targetY }, 1886 delta: { deltaX, deltaY }, 1887 targetScale: targetScale 1888 }); 1889 1890 // Scale and move to target 1891 timeline 1892 .to(img, { 1893 x: deltaX, 1894 y: deltaY, 1895 scale: targetScale, 1896 duration: 0.45, 1897 ease: 'power3.in', 1898 onStart: () => console.log(`â¶ï¸ Image ${index + 1} animation started`), 1899 onComplete: () => console.log(`â Image ${index + 1} reached hover-img target`) 1900 }, 0.025) 1901 .to(img, { 1902 scale: targetScale * 1.05, 1903 duration: 0.2, 1904 ease: 'power2.out' 1905 }, 0.5) 1906 .to(img, { 1907 scale: targetScale, 1908 duration: 0.25, 1909 ease: 'power2.out' 1910 }, 0.7) 1911 .to(img, { 1912 opacity: 0, 1913 duration: 0.2, 1914 ease: 'power2.out', 1915 onComplete: () => console.log(`ð» Image ${index + 1} faded out`) 1916 }, 0.8); 1917 1918 mainTimeline.add(timeline, index * 0.1); 1919 }); 1920 1921 return mainTimeline; 1922 }; 1923 1924 // Initialize animation 1925 const initKonpoAnimation = () => { 1926 // Wait for DOM to be ready 1927 if (document.readyState === 'loading') { 1928 document.addEventListener('DOMContentLoaded', initKonpoAnimation); 1929 return; 1930 } 1931 1932 // Check if GSAP is available 1933 if (typeof gsap === 'undefined') { 1934 console.warn('GSAP not loaded, loading from CDN...'); 1935 const script = document.createElement('script'); 1936 script.src = 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js'; 1937 script.onload = () => { 1938 setTimeout(startAnimation, 100); 1939 }; 1940 document.head.appendChild(script); 1941 return; 1942 } 1943 1944 startAnimation(); 1945 }; 1946 1947 const startAnimation = () => { 1948 const loader = createLoader(); 1949 document.body.appendChild(loader); 1950 1951 createCounterDigits(loader); 1952 createImages(loader); 1953 1954 // Start animation sequence 1955 const tl = gsap.timeline(); 1956 1957 // Set initial states 1958 gsap.set('.konpo-img', { scale: 0, opacity: 1 }); 1959 1960 // Show loader background immediately 1961 gsap.set('.konpo-loader-bg', { scaleY: 1 }); 1962
1963 // Background scale up (already visible) 1964 tl.to('.konpo-loader-bg', { 1965 scaleY: 1, 1966 duration: 0.1, 1967 ease: 'power2.inOut', 1968 delay: 0 1969 }); 1970 1971 // Images scale in 1972 tl.to('.konpo-img', { 1973 scale: 1, 1974 duration: 1, 1975 stagger: 0.125, 1976 ease: 'power3.out' 1977 }, '<'); 1978 1979 // Start counters 1980 tl.add(() => { 1981 animateCounter(loader.querySelector('.konpo-counter-3'), 2.5); 1982 animateCounter(loader.querySelector('.konpo-counter-2'), 3); 1983 animateCounter(loader.querySelector('.konpo-counter-1'), 2, 1.5); 1984 }, '<'); 1985 1986 // Hide counter and animate images to target 1987 tl.to('.konpo-counter', { 1988 opacity: 0, 1989 duration: 0.3, 1990 ease: 'power3.out', 1991 delay: 0.3, 1992 onStart: () => { 1993 animateImages(loader); 1994 } 1995 }); 1996 1997 // Remove loader and reveal site simultaneously 1998 tl.to(loader, { 1999 opacity: 0, 2000 duration: 0.2, 2001 ease: 'power2.out', 2002 delay: 1.0, 2003 onStart: () => { 2004 // Start revealing site content before loader fully fades 2005 document.body.classList.add('animation-complete'); 2006 document.body.style.visibility = 'visible'; 2007 document.body.style.overflow = ''; 2008 }, 2009 onComplete: () => { 2010 loader.remove(); 2011 } 2012 }); 2013 }; 2014 2015 // Start the animation 2016 startAnimation(); 2017}; 2018 2019// Call the animation function 2020initKonpoAnimation(); 2021 2022// ============================================================================ 2023// HIGH-RESOLUTION IMAGE UPGRADE (Mobile-Friendly) 2024// ============================================================================ 2025// Upgrades img.hover-img thumbnails to highest resolution from srcset 2026// Optimized to prevent interference with mobile swipe gestures 2027 2028(() => { 2029 const SEL = 'img.hover-img'; // thumbnails 2030 let isInteracting = false; 2031 let debounceTimer = null; 2032 2033 /* ---------- pick the largest srcset candidate ---------- */ 2034 const largest = set => set.split(',').pop().trim().split(' ')[0]; 2035 2036 /* ---------- upgrade one <img> -------------------------- */ 2037 const upgrade = img => { 2038 const hi = img.hasAttribute('srcset') // choose full-res URL 2039 ? largest(img.getAttribute('srcset')) 2040 : img.src; 2041 img.src = hi; 2042 img.removeAttribute('srcset'); 2043 img.removeAttribute('sizes'); 2044 }; 2045 2046 /* ---------- scan & log --------------------------------- */ 2047 const scan = tag => { 2048 if (isInteracting) return; // Skip during touch interactions 2049 const imgs = (tag || document).querySelectorAll(SEL); 2050 if (!imgs.length) return; 2051 2052 imgs.forEach(upgrade); 2053 }; 2054 2055 /* ---------- debounced scan function ------------------- */ 2056 const debouncedScan = (node) => { 2057 clearTimeout(debounceTimer); 2058 debounceTimer = setTimeout(() => scan(node), 100); 2059 }; 2060 2061 /* ---------- track touch interactions ------------------- */ 2062 document.addEventListener('touchstart', () => isInteracting = true); 2063 document.addEventListener('touchend', () => { 2064 setTimeout(() => isInteracting = false, 200); 2065 }); 2066 2067 /* 1 ) first try â DOM parsed */ 2068 document.addEventListener('DOMContentLoaded', () => scan()); 2069 2070 /* 2 ) Webflow finishes hydration (fires on window load) */ 2071 window.addEventListener('load', () => scan()); 2072 2073 /* 3 ) anything added later - mobile-friendly observer */ 2074 new MutationObserver(mutations => { 2075 if (isInteracting) return; // Skip during touch interactions 2076 2077 const hasRelevantChanges = mutations.some(m => 2078 Array.from(m.addedNodes).some(n => 2079 n.nodeType === 1 && ( 2080 n.matches?.(SEL) || 2081 n.querySelectorAll?.(SEL).length 2082 ) 2083 ) 2084 ); 2085 2086 if (hasRelevantChanges) { 2087 debouncedScan(); 2088 } 2089 }).observe(document.body, {childList: true, subtree: true}); 2090})(); 2091 2092// Simple filled dot cursor implementation 2093$(document).ready(function() { 2094 // Add cursor element to body 2095 $('body').append('<div class="cursor"></div>'); 2096 2097 const cursor = $('.cursor')[0]; 2098 2099 // Track mouse movement and update cursor position with transform for better performance 2100 $(window).mousemove(function(e) { 2101 if (cursor) { 2102 // Adjust offset based on cursor size (20px default, 40px when active)
2103 const isActive = cursor.classList.contains('active'); 2104 const offset = isActive ? 20 : 10; // Half of cursor size for centering 2105 cursor.style.transform = `translate(${e.clientX - offset}px, ${e.clientY - offset}px)`; 2106 } 2107 }); 2108 2109 // Add hover effects on links 2110 $("a").on("mouseenter", function() { 2111 $('.cursor').addClass("active"); 2112 }); 2113 2114 $("a").on("mouseleave", function() { 2115 $('.cursor').removeClass("active"); 2116 }); 2117}); 2118 2119// Barba.js Page Transitions 2120function pageTransition() { 2121 var tl = gsap.timeline(); 2122 2123 tl.to(".page-transition", { 2124 duration: 1, 2125 scaleY: 1, 2126 transformOrigin: "bottom", 2127 ease: "power4.inOut", 2128 }); 2129 2130 tl.to(".page-transition", { 2131 duration: 1, 2132 scaleY: 0, 2133 transformOrigin: "top", 2134 ease: "power4.inOut", 2135 delay: 0.2, 2136 }); 2137} 2138 2139function contentAnimation() { 2140 var tl = gsap.timeline(); 2141 2142 // Animate headings - slide up from below 2143 tl.fromTo(".u-heading", { 2144 y: 60, 2145 opacity: 0 2146 }, { 2147 y: 0, 2148 opacity: 1, 2149 duration: 1, 2150 ease: "power3.out", 2151 stagger: 0.1, 2152 delay: 0.75 2153 }); 2154 2155 // Animate image containers - fade up 2156 tl.fromTo(".item.cms", { 2157 y: 40, 2158 opacity: 0 2159 }, { 2160 y: 0, 2161 opacity: 1, 2162 duration: 0.8, 2163 ease: "power2.out", 2164 stagger: 0.15 2165 }, "-=0.5"); 2166 2167 // Animate paragraphs - staggered fade up with delay 2168 tl.fromTo(".u-p", { 2169 y: 30, 2170 opacity: 0 2171 }, { 2172 y: 0, 2173 opacity: 1, 2174 duration: 0.6, 2175 ease: "power2.out", 2176 stagger: 0.1 2177 }, "-=0.3"); 2178} 2179 2180function delay(n) { 2181 n = n || 0; 2182 return new Promise((done) => { 2183 setTimeout(() => { 2184 done(); 2185 }, n); 2186 }); 2187} 2188 2189// Initialize Barba.js when DOM is ready 2190$(document).ready(function() { 2191 // Add transition overlay to body 2192 $('body').prepend('<div class="page-transition"></div>'); 2193 2194 // Dynamically add data-barba="container" to all .u-section elements 2195 $('.u-section').attr('data-barba', 'container'); 2196 2197 // Initialize Barba.js 2198 if (typeof barba !== 'undefined') { 2199 barba.init({ 2200 sync: true, 2201 transitions: [ 2202 { 2203 async leave(data) { 2204 const done = this.async(); 2205 pageTransition(); 2206 await delay(1000); 2207 done(); 2208 }, 2209 2210 async enter(data) { 2211 contentAnimation(); 2212 }, 2213 2214 async once(data) { 2215 contentAnimation(); 2216 }, 2217 }, 2218 ], 2219 }); 2220 } 2221}); 2222 2223</script>
2223 2224<!-- Cloudflare Turnstile -->
2225<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
vendor: 1 bytes, line 2225
2225
2226<script> 2227(function () { 2228 function addTurnstile() { 2229 var forms = document.querySelectorAll('form[data-name]'); 2230 if (!forms.length) return; 2231 2232 forms.forEach(function (form) { 2233 if (form.querySelector('.cf-turnstile')) return; 2234 2235 var widget = document.createElement('div'); 2236 widget.className = 'cf-turnstile'; 2237 widget.setAttribute('data-sitekey', '0x4AAAAAABeM177zuWxpi87W'); 2238 widget.setAttribute('data-theme', 'light'); 2239 widget.style.marginBottom = '16px'; 2240 2241 var submitBtn = form.querySelector('[type="submit"]'); 2242 if (submitBtn) { 2243 form.insertBefore(widget, submitBtn); 2244 } else { 2245 form.appendChild(widget); 2246 } 2247 2248 form.addEventListener('submit', function (e) { 2249 var token = form.querySelector('[name="cf-turnstile-response"]'); 2250 if (!token || !token.value) { 2251 e.preventDefault(); 2252 e.stopImmediatePropagation(); 2253 return false; 2254 } 2255 }, true); 2256 }); 2257 } 2258 2259 if (document.readyState === 'loading') { 2260 document.addEventListener('DOMContentLoaded', addTurnstile); 2261 } else { 2262 addTurnstile(); 2263 } 2264})(); 2265</script>
2265 2266<div id="awwwards" style="position: fixed; z-index: 999; transform: translateY(-50%); top: 50%; right: 0"><a href="https://www.awwwards.com/sites/product-photographer" target="_blank"><svg width="53.08" height="171.358"><path class="js-color-bg" fill="black" d="M0 0h53.08v171.358H0z"></path><g class="js-color-text" fill="white"><path d="M20.048 153.585v-2.002l6.752-3.757h-6.752v-1.9h10.23v2.002l-6.752 3.757h6.752v1.9zM29.899 142.382a3.317 3.317 0 0 1-1.359 1.293c-.575.297-1.223.446-1.944.446-.721 0-1.369-.149-1.944-.446a3.317 3.317 0 0 1-1.359-1.293c-.331-.564-.497-1.232-.497-2.003 0-.769.166-1.437.497-2.002a3.332 3.332 0 0 1 1.359-1.294c.575-.297 1.224-.445 1.944-.445.722 0 1.369.148 1.944.445a3.326 3.326 0 0 1 1.359 1.294c.33.565.496 1.233.496 2.002.001.77-.166 1.438-.496 2.003m-1.703-3.348c-.435-.331-.967-.497-1.601-.497s-1.167.166-1.601.497c-.434.332-.65.78-.65 1.345s.217 1.014.65 1.346c.434.33.967.496 1.601.496s1.166-.166 1.601-.496c.434-.332.649-.78.649-1.346.001-.565-.215-1.013-.649-1.345M22.912 134.996v-1.812h1.185c-.43-.283-.752-.593-.973-.929-.219-.336-.329-.732-.329-1.19 0-.479.127-.902.38-1.272.254-.37.635-.633 1.141-.79-.478-.262-.851-.591-1.118-.985a2.221 2.221 0 0 1-.402-1.265c0-.682.2-1.218.599-1.607.4-.391.957-.585 1.668-.585h5.218v1.812H25.37c-.682 0-1.023.303-1.023.907 0 .467.264.85.789 1.146.527.299 1.286.446 2.28.446h2.865v1.813H25.37c-.682 0-1.023.303-1.023.906 0 .468.275.851.826 1.147.551.298 1.352.446 2.404.446h2.704v1.812h-7.369zM21.626 122.457c-.225.224-.502.336-.833.336s-.608-.112-.833-.336a1.128 1.128 0 0 1-.336-.833c0-.331.111-.609.336-.833.225-.225.502-.336.833-.336s.608.111.833.336c.225.224.337.502.337.833 0 .332-.112.608-.337.833m1.286-1.739h7.366v1.813h-7.366v-1.813zM22.912 118.668v-1.812h1.185a3.348 3.348 0 0 1-.951-1.009 2.434 2.434 0 0 1-.351-1.272c0-.681.19-1.229.57-1.644.38-.414.931-.621 1.651-.621h5.263v1.812h-4.722c-.418 0-.727.096-.92.285-.195.19-.293.447-.293.769 0 .302.116.58.351.833.233.254.577.458 1.03.613.453.156.992.234 1.615.234h2.938v1.812h-7.366zM29.833 109.129a3.33 3.33 0 0 1-1.432 1.169 4.535 4.535 0 0 1-1.805.373 4.537 4.537 0 0 1-1.807-.373c-.579-.248-1.057-.638-1.432-1.169s-.563-1.196-.563-1.995c0-.771.183-1.413.549-1.93a3.28 3.28 0 0 1 1.382-1.141 4.221 4.221 0 0 1 1.709-.364h.746v5.071c.447-.02.838-.183 1.168-.49.332-.307.498-.724.498-1.248 0-.41-.093-.754-.277-1.031-.186-.278-.473-.529-.863-.753l.542-1.462c.69.303 1.224.724 1.592 1.265.371.541.556 1.235.556 2.083 0 .799-.188 1.464-.563 1.995m-4.085-3.574c-.41.088-.746.261-1.009.52-.262.258-.395.61-.395 1.06 0 .428.137.784.409 1.067.272.282.604.458.994.525v-3.172zM29.833 100.878c-.375.531-.852.921-1.432 1.169a4.552 4.552 0 0 1-3.612 0c-.579-.248-1.057-.638-1.432-1.169s-.563-1.196-.563-1.995c0-.77.183-1.412.549-1.93a3.278 3.278 0 0 1 1.382-1.14 4.222 4.222 0 0 1 1.709-.365h.746v5.072a1.794 1.794 0 0 0 1.168-.49c.332-.307.498-.724.498-1.249 0-.41-.093-.753-.277-1.031-.186-.277-.473-.528-.863-.753l.542-1.462c.69.302 1.224.724 1.592 1.265.371.541.556 1.234.556 2.083 0 .799-.188 1.464-.563 1.995m-4.085-3.573c-.41.088-.746.261-1.009.519-.262.258-.395.611-.395 1.06 0 .429.137.784.409 1.067.272.282.604.458.994.526v-3.172zM35.481 16.926l-4.782 14.969h-3.266l-2.584-9.682-2.584 9.682h-3.268l-4.781-14.969h3.713l2.673 10.276 2.524-10.276h3.445l2.524 10.276 2.674-10.276zM37.979 27.083c1.426 0 2.495 1.068 2.495 2.495 0 1.425-1.069 2.495-2.495 2.495-1.425 0-2.495-1.07-2.495-2.495-.001-1.427 1.07-2.495 2.495-2.495"></path></g></svg></a></div>
2266<script src="https://hubspotonwebflow.com/assets/js/form-124.js" type="text/javascript" integrity="sha384-bjyNIOqAKScdeQ3THsDZLGagNN56B4X2Auu9YZIGu+tA/PlggMk4jbWruG/P6zYj" crossorigin="anonymous"></script>
2266<script src="https://hubspotv2.use1-marketplace-1p-apps-prod-red.if.webflow.services/static/loader.js" type="text/javascript" async="" defer=""></script>
2266<script src="https://hubspotv2.use1-marketplace-1p-apps-prod-red.if.webflow.services/static/disable-hubspot-chatbot.js" type="text/javascript" async="" defer=""></script>
2266<script src="https://js.hs-scripts.com/20245411.js" type="text/javascript" async="" defer=""></script>
2266<script src="https://cdn.schema-flow.com/script.min.js" type="text/javascript" integrity="sha384-lkeaHi6CUqjssPG+DrkxfgqSYKpysN7Rgo/uRqMbpVgt0Kpeqx9lBgjZ+g7HBfbt" crossorigin="anonymous"></script>
2266<script src="https://hubspotv2.use1-marketplace-1p-apps-prod-red.if.webflow.services/static/disable-hubspot-chatbot.js" type="text/javascript" async="" defer=""></script>
2266<script src="https://js.hs-scripts.com/20245411.js" type="text/javascript" async="" defer=""></script>
2266</body></html>
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.