PageSourceSearch

https://elinakustlyvy.webflow.io/about

html elinakustlyvy.webflow.io collected 2026-10-03 13:00:55 UTC 102,878 bytes, 2,266 lines download raw bytes

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 &amp; 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 &amp; 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 &amp; 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&#x27;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&#x27;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&#x27;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 &amp; 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.