PageSourceSearch

https://tagtaville.ca/

html tagtaville.ca collected 2026-10-07 00:35:13 UTC 247,458 bytes, 7,759 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8">
5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6  <title>Tag ta Ville</title>
7  <link rel="icon" type="image/svg+xml" href="assets/pin_3d_glossy_blue.svg">
8  
8<script src='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js'></script>
8
9  <link href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css' rel='stylesheet' />
10  
10<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
10
11  
11<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
11
12
13  <!-- GoatCounter Analytics -->
14  
14<script data-goatcounter="https://tagtaville.goatcounter.com/count"
15          async src="//gc.zgo.at/count.js"></script>
15
16
17  <!-- Google Fonts -->
18  <link rel="preconnect" href="https://fonts.googleapis.com">
19  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
20  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
21
22  <style>
23    * { margin: 0; padding: 0; box-sizing: border-box; }
24    body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
25    
26    #map { width: 100vw; height: 100vh; }
27    
28    .panel {
29      position: fixed;
30      background: white;
31      border-radius: 8px;
32      box-shadow: 0 2px 10px rgba(0,0,0,0.3);
33    }
34    
35    /* Main Control Panel - Top Left */
36    .main-panel {
37      top: 20px;
38      left: 20px;
39      width: 420px;
40      max-height: calc(100vh - 40px);
41      overflow-y: auto;
42      padding: 20px;
43    }
44    
45    .main-panel h2 {
46      font-size: 22px;
47      margin-bottom: 20px;
48      color: #1a73e8;
49      font-weight: 700;
50    }
51    
52    .step-section {
53      margin-bottom: 8px;
54    }
55    
56    .step-header {
57      font-size: 17px;
58      font-weight: 600;
59      color: #1a73e8;
60      margin-bottom: 24px;
61      padding: 8px 12px;
62      background: #e8f0fe;
63      border-radius: 4px;
64    }
65    
66    .step-label {
67      display: block;
68      font-size: 16px;
69      font-weight: 500;
70      color: #555;
71      margin-top: 8px;
72      margin-bottom: 8px;
73    }
74    
75    .button-group {
76      display: inline-flex;
77      background: #e8f0fe;
78      border-radius: 8px;
79      padding: 4px;
80      margin-bottom: 8px;
81    }
82
83    .btn {
84      padding: 10px 24px;
85      border: none;
86      background: transparent;
87      color: #4285f4;
88      border-radius: 6px;
89      font-size: 16px;
90      font-weight: 500;
91      cursor: pointer;
92      transition: all 0.2s;
93    }
94
95    .btn:hover:not(.active) { background: rgba(255,255,255,0.5); }
96    .btn.active {
97      background: white;
98      color: #1a73e8;
99      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
100    }
101    
102    .address-input {
103      width: 100%;
104      padding: 10px;
105      border: 2px solid #ddd;
106      border-radius: 4px;
107      font-size: 16px;
108      margin-bottom: 8px;
109    }
110    
111    .address-input:focus { outline: none; border-color: #4285f4; }
112    
113    .address-suggestions {
114      background: white;
115      border: 2px solid #ddd;
116      border-top: none;
117      border-radius: 0 0 4px 4px;
118      max-height: 200px;
119      overflow-y: auto;
120      margin-bottom: 10px;
121    }
122    
123    .address-suggestion {
124      padding: 10px;
125      cursor: pointer;
126      font-size: 15px;
127      color: #333;
128      border-bottom: 1px solid #f0f0f0;
129    }
130    
131    .address-suggestion:hover { background: #f5f5f5; }
132    .address-suggestion:last-child { border-bottom: none; }
133    
134    .confirm-btn, .next-btn, .submit-btn {
135      width: 100%;
136      padding: 12px;
137      background: #4285f4;
138      color: white;
139      border: none;
140      border-radius: 12px;
141      font-size: 16px;
142      font-weight: 600;
143      cursor: pointer;
144      transition: background 0.2s;
145    }
146    
147    .confirm-btn:hover, .next-btn:hover { background: #357ae8; }
148
149    .confirm-btn.pulsate {
150      animation: pulsateButton 2s ease-in-out 3;
151    }
152
153    @keyframes pulsateButton {
154      0%, 100% {
155        transform: scale(1);
156        box-shadow: 0 2px 4px rgba(0,0,0,0.2);
157      }
158      50% {
159        transform: scale(1.05);
160        box-shadow: 0 4px 12px rgba(66, 133, 244, 0.6);
161      }
162    }
163
164    .next-btn {
165      background: #4285f4;
166      margin-top: 15px;
167    }
168
169    .next-btn:hover { background: #357ae8; }
170    
171    .submit-btn {
172      background: #34a853;
173      font-size: 16px;
174      padding: 14px;
175    }
176    
177    .submit-btn:hover { background: #2d9348; }
178    
179    .clear-btn {
180      width: 100%;
181      padding: 12px;
182      background: white;
183      color: #d32f2f;
184      border: 2px solid #d32f2f;
185      border-radius: 12px;
186      font-size: 15px;
187      font-weight: 600;
188      cursor: pointer;
189      margin-top: 8px;
190      transition: all 0.2s;
191    }
192
193    .clear-btn:hover {
194      background: #fff5f5;
195      color: #d32f2f;
196      border-color: #d32f2f;
197    }
198    
199    .instruction {
200      font-size: 15px;
201      color: #666;
202      padding: 10px;
203      background: #f5f5f5;
204      border-radius: 4px;
205      margin: 8px 0;
206    }
207    
208    .selected-info {
209      background: #e8f0fe;
210      padding: 12px;
211      border-radius: 4px;
212      margin-bottom: 10px;
213      border-left: 4px solid #4285f4;
214    }
215    
216    .selected-info div {
217      font-size: 16px;
218      color: #333;
219      margin-bottom: 4px;
220    }
221    
222    .selected-info div:last-child { margin-bottom: 0; }
223    
224    /* Tag Mode Selector (Red/Green) */
225    .tag-mode-selector {
226      display: flex;
227      gap: 8px;
228      margin-bottom: 15px;
229    }
230    
231    .tag-mode-btn {
232      flex: 1;
233      padding: 12px;
234      border: 2px solid #ddd;
235      border-radius: 8px;
236      font-size: 16px;
237      font-weight: 600;
238      cursor: pointer;
239      transition: all 0.2s;
240      background: white;
241    }
242
243    .tag-mode-btn.danger-mode {
244      color: #ea4335;
245      border-color: #ea4335;
246    }
247    
248    .tag-mode-btn.danger-mode.active {
249      background: #ea4335;
250      color: white;
251    }
252    
253    .tag-mode-btn.safe-mode {
254      color: #34a853;
255      border-color: #34a853;
256    }
257    
258    .tag-mode-btn.safe-mode.active {
259      background: #34a853;
260      color: white;
261    }
262
263    /* Pulse animations for zone type buttons when not active */
264    @keyframes greenPulse {
265      0%, 100% {
266        transform: scale(1);
267        box-shadow: 0 0 0 0 rgba(52, 168, 83, 0.7);
268      }
269      50% {
270        transform: scale(1.05);
271        box-shadow: 0 0 0 8px rgba(52, 168, 83, 0);
272      }
273    }
274
275    @keyframes redPulse {
276      0%, 100% {
277        transform: scale(1);
278        box-shadow: 0 0 0 0 rgba(234, 67, 53, 0.7);
279      }
280      50% {
281        transform: scale(1.05);
282        box-shadow: 0 0 0 8px rgba(234, 67, 53, 0);
283      }
284    }
285
286    .tag-mode-btn.safe-mode.active {
287      animation: greenPulse 2s ease-in-out infinite;
288    }
289
290    .tag-mode-btn.danger-mode.active {
291      animation: redPulse 2s ease-in-out infinite;
292    }
293    
294    /* Tool Selector (Cursor/Tag Can/Pressure Washer) */
295    .tool-selector-row {
296      display: flex;
297      gap: 8px;
298      margin-bottom: 32px;
299    }
300    
301    .tool-btn {
302      flex: 1;
303      padding: 14px 8px;
304      border: 2px solid #ddd;
305      border-radius: 8px;
306      font-size: 15px;
307      font-weight: 600;
308      cursor: pointer;
309      transition: all 0.2s;
310      background: white;
311      color: #666;
312      text-align: center;
313      line-height: 1.3;
314    }
315
316    #tool-cursor {
317      flex: 0.6;
318      font-size: 11px;
319    }
320
321    .tool-btn .cursor-icon {
322      font-size: 32px;
323      display: block;
324    }
325    
326    .tool-btn:hover { background: #f5f5f5; }
327
328    .tool-btn.active {
329      background: #4285f4;
330      color: white;
331      border-color: #4285f4;
332    }
333
334    /* Pulse animation for paint button when not active */
335    @keyframes paintButtonPulse {
336      0%, 100% {
337        transform: scale(1);
338        box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.7);
339      }
340      50% {
341        transform: scale(1.05);
342        box-shadow: 0 0 0 8px rgba(66, 133, 244, 0);
343      }
344    }
345
346    #tool-paint:not(.active) {
347      border-color: #4285f4;
348      color: #4285f4;
349      animation: paintButtonPulse 2s ease-in-out infinite;
350    }
351    
352    .tool-emoji {
353      font-size: 20px;
354      display: block;
355      margin-bottom: 4px;
356    }
357    
358    .spray-size-control {
359      margin-bottom: 20px;
360    }
361    
362    .slider {
363      width: 100%;
364      height: 4px;
365      border-radius: 2px;
366      background: #ddd;
367      outline: none;
368    }
369    
370    .toggle-label {
371      display: block;
372      font-size: 15px;
373      color: #555;
374      margin-bottom: 8px;
375      cursor: pointer;
376    }
377    
378    .toggle-label input { margin-right: 8px; }
379    
380    .paint-target-selector {
381      padding: 10px;
382      background: #f8f9fa;
383      border-radius: 4px;
384      margin-bottom: 12px;
385    }
386    
387    .submit-info {
388      font-size: 16px;
389      color: #666;
390      padding: 12px;
391      background: #f8f9fa;
392      border-radius: 4px;
393      margin-bottom: 12px;
394      line-height: 1.5;
395    }
396    
397    .reset-btn {
398      width: 100%;
399      padding: 10px;
400      background: #666;
401      color: white;
402      border: none;
403      border-radius: 4px;
404      font-size: 15px;
405      font-weight: 500;
406      cursor: pointer;
407      margin-top: 20px;
408    }
409    
410    .reset-btn:hover { background: #555; }
411    
412    .hidden { display: none !important; }
413    
414    /* Info Panel - BOTTOM RIGHT */
415    .info-panel {
416      bottom: 20px;
417      right: 20px;
418      padding: 15px;
419      font-size: 15px;
420    }
421    
422    .info-row { margin-bottom: 5px; }
423    .info-label { font-weight: 600; color: #555; margin-right: 5px; }
424    .status { color: #4285f4; font-weight: 500; }
425    .status.loading { color: #fbbc04; }
426    
427    /* <span data-en="View Controls" data-fr="Contrôles de vue">Contrôles de vue</span> - Top Right */
428    .view-controls {
429      top: 20px;
430      right: 20px;
431      padding: 15px;
432      width: 200px;
433    }
434    
435    .view-controls h3 {
436      font-size: 16px;
437      margin-bottom: 12px;
438      color: #333;
439      font-weight: 600;
440    }
441    
442    .control-group { margin-bottom: 12px; }
443    
444    .control-label {
445      display: block;
446      font-size: 17px;
447      font-weight: 500;
448      color: #555;
449      margin-bottom: 6px;
450    }
451    
452    /* Cluster Items */
453    .cluster-item {
454      background: #f8f9fa;
455      padding: 12px;
456      border-radius: 4px;
457      margin-bottom: 10px;
458      border-left: 4px solid;
459      cursor: pointer;
460      transition: all 0.2s;
461    }
462    
463    .cluster-item:hover {
464      background: #e8f0fe;
465    }
466    
467    .cluster-item.selected {
468      background: #e8f0fe;
469    }
470    
471    .cluster-item.disliked.selected {
472      background: #fce8e6;
473      box-shadow: 0 0 0 3px #ea4335;
474    }
475    
476    .cluster-item.liked.selected {
477      background: #e6f4ea;
478      box-shadow: 0 0 0 3px #34a853;
479    }
480    
481    .cluster-item.disliked { border-color: #ea4335; }
482    .cluster-item.liked { border-color: #34a853; }
483    
484    .cluster-header {
485      font-size: 15px;
486      font-weight: 600;
487      margin-bottom: 8px;
488      color: #333;
489    }
490    
491    .cluster-input {
492      width: 100%;
493      padding: 8px;
494      border: 2px solid #ddd;
495      border-radius: 4px;
496      font-size: 15px;
497      margin-bottom: 8px;
498    }
499    
500    .cluster-input:focus {
501      outline: none;
502      border-color: #4285f4;
503    }
504    
505    .cluster-save-btn {
506      padding: 6px 12px;
507      background: #4285f4;
508      color: white;
509      border: none;
510      border-radius: 4px;
511      font-size: 12px;
512      font-weight: 500;
513      cursor: pointer;
514    }
515    
516    .cluster-save-btn:hover { background: #357ae8; }
517    
518    .cluster-save-btn.saved {
519      background: #34a853;
520      pointer-events: none;
521    }
522    
523    .cluster-save-btn.saved:after {
524      content: " ✓";
525    }
526    
527    /* Welcome popup */
528    .welcome-overlay {
529      position: fixed;
530      top: 0;
531      left: 0;
532      width: 100vw;
533      height: 100vh;
534      background: rgba(0, 0, 0, 0.5);
535      display: flex;
536      align-items: center;
537      justify-content: center;
538      z-index: 10000;
539    }
540    
541    .welcome-popup {
542      position: relative;
543      background: white;
544      border-radius: 16px;
545      padding: 50px 58px;
546      max-width: 680px;
547      box-shadow: 0 20px 60px rgba(0,0,0,0.3);
548      text-align: center;
549    }
550    
551    .welcome-logo {
552      width: 120px;
553      height: 120px;
554      margin: 0 auto 20px;
555    }
556    
557    .welcome-title {
558      font-family: 'Inter', sans-serif;
559      font-size: 36px;
560      font-weight: 300;
561      color: #1a73e8;
562      margin-bottom: 38px;
563      letter-spacing: 1px;
564    }
565    
566    .welcome-text {
567      font-size: 18px;
568      font-family: 'Inter', sans-serif;
569      color: #333;
570      line-height: 1.72;
571      margin-bottom: 21px;
572      font-weight: 400;
573    }
574
575    .welcome-text:first-of-type {
576      font-size: 19px;
577      font-weight: 600;
578      color: #1a73e8;
579      margin-bottom: 26px;
580      text-align: center;
581    }
582
583    .welcome-text:first-of-type strong {
584      color: #1a73e8;
585    }
586
587    .welcome-text:nth-of-type(4), .welcome-text:nth-of-type(5) {
588      font-size: 17px;
589      color: #555;
590      line-height: 1.68;
591    }
592
593    .welcome-text strong {
594      font-weight: 600;
595      color: #333;
596    }
597
598    .data-collapsible {
599      margin-top: 24px;
600      margin-bottom: 24px;
601      border: 1px solid #e8e8e8;
602      border-radius: 8px;
603      overflow: hidden;
604    }
605
606    .data-collapsible-header {
607      background: #f8f9fa;
608      padding: 14px 18px;
609      cursor: pointer;
610      font-size: 16px;
611      font-weight: 600;
612      color: #1a73e8;
613      display: flex;
614      align-items: center;
615      justify-content: space-between;
616      transition: background 0.2s;
617      font-family: 'Inter', sans-serif;
618    }
619
620    .data-collapsible-header:hover {
621      background: #f0f2f5;
622    }
623
624    .data-collapsible-arrow {
625      font-size: 16px;
626      transition: transform 0.3s;
627    }
628
629    .data-collapsible-header.open .data-collapsible-arrow {
630      transform: rotate(180deg);
631    }
632
633    .data-collapsible-content {
634      max-height: 0;
635      overflow: hidden;
636      transition: max-height 0.3s ease-out;
637      padding: 0 18px;
638    }
639
640    .data-collapsible-content.open {
641      max-height: 500px;
642      padding: 18px;
643    }
644
645    .data-collapsible-content p {
646      font-size: 17px;
647      color: #555;
648      line-height: 1.68;
649      margin-bottom: 16px;
650      font-family: 'Inter', sans-serif;
651    }
652
653    .data-collapsible-content p:last-child {
654      margin-bottom: 0;
655    }
656
657    .welcome-buttons {
658      display: flex;
659      gap: 12px;
660      justify-content: center;
661      margin-top: 34px;
662    }
663    
664    .welcome-btn {
665      padding: 14px 32px;
666      border-radius: 8px;
667      font-size: 17px;
668      font-weight: 500;
669      cursor: pointer;
670      transition: all 0.2s;
671      border: none;
672    }
673
674    .welcome-btn.primary {
675      background: #4285f4;
676      color: white;
677      border: 2px solid #4285f4;
678    }
679
680    .welcome-btn.primary:hover {
681      background: #3367d6;
682    }
683
684    .welcome-btn.secondary {
685      background: white;
686      color: #4285f4;
687      border: 2px solid #4285f4;
688    }
689
690    .welcome-btn.secondary:hover {
691      background: #f0f7ff;
692    }
693    
694    /* Tag checkboxes */
695    .tag-checkboxes {
696      display: grid;
697      grid-template-columns: 1fr 1fr;
698      gap: 8px;
699      margin-bottom: 10px;
700    }
701    
702    .tag-checkbox-label {
703      display: flex;
704      align-items: center;
705      padding: 8px;
706      background: #f8f9fa;
707      border-radius: 4px;
708      cursor: pointer;
709      font-size: 15px;
710      transition: all 0.2s;
711    }
712    
713    .tag-checkbox-label:hover {
714      background: #e8f0fe;
715    }
716    
717    .tag-checkbox-label input {
718      margin-right: 8px;
719    }
720    
721    .cluster-comment {
722      width: 100%;
723      padding: 8px;
724      border: 2px solid #ddd;
725      border-radius: 4px;
726      font-size: 12px;
727      margin-bottom: 8px;
728      resize: vertical;
729      min-height: 50px;
730      font-family: inherit;
731    }
732    
733    .cluster-comment:focus {
734      outline: none;
735      border-color: #4285f4;
736    }
737    
738    /* On-screen navigation arrows (Step 1 & 2) */
739    .map-nav-controls {
740      position: fixed;
741      bottom: 20px;
742      right: 20px;
743      display: none;
744      background: white;
745      padding: 10px;
746      border-radius: 8px;
747      box-shadow: 0 2px 8px rgba(0,0,0,0.15);
748      z-index: 100;
749    }
750    
751    .map-nav-controls.active { display: block; }
752    
753    .map-nav-grid {
754      display: grid;
755      grid-template-columns: repeat(3, 45px);
756      grid-template-rows: repeat(3, 45px);
757      gap: 6px;
758      margin-bottom: 8px;
759    }
760    
761    .map-nav-btn {
762      background: #f5f5f5;
763      border: 2px solid #ddd;
764      border-radius: 6px;
765      cursor: pointer;
766      font-size: 18px;
767      font-weight: bold;
768      color: #555;
769      transition: all 0.2s;
770      display: flex;
771      align-items: center;
772      justify-content: center;
773    }
774    
775    .map-nav-btn:hover {
776      background: #e0e0e0;
777      border-color: #999;
778      transform: scale(1.05);
779    }
780    
781    .map-nav-btn:active {
782      transform: scale(0.95);
783    }
784    
785    .map-nav-btn.active-press {
786      background: #4285f4;
787      color: white;
788      border-color: #4285f4;
789      transform: scale(0.95);
790    }
791    
792    .map-nav-btn.rotate {
793      background: #4285f4;
794      color: white;
795      border-color: #4285f4;
796      font-size: 18px;
797    }
798    
799    .map-nav-btn.rotate:hover {
800
801.map-nav-btn.zoom {
802  background: #34a853;
803  color: white;
804  border-color: #34a853;
805  font-size: 24px;
806}
807
808.map-nav-btn.zoom:hover {
809  background: #2d9348;
810}
811      background: #357ae8;
812    }
813    
814    .map-nav-label {
815      font-size: 10px;
816      color: #666;
817      text-align: center;
818      font-weight: 500;
819    }
820    
821    /* Back button */
822    .back-btn {
823      width: 100%;
824      padding: 10px;
825      background: transparent;
826      color: #888;
827      border: none;
828      border-radius: 0;
829      font-size: 15px;
830      font-weight: 500;
831      cursor: pointer;
832      margin-top: 12px;
833      margin-bottom: 0;
834      display: flex;
835      align-items: center;
836      justify-content: center;
837      gap: 5px;
838      text-decoration: underline;
839    }
840
841    .back-btn:hover {
842      background: transparent;
843      color: #555;
844    }
845
846    /* Info button at bottom left */
847    .info-button {
848      position: fixed;
849      bottom: 20px;
850      left: 20px;
851      width: 52px;
852      height: 52px;
853      background: white;
854      border: none;
855      border-radius: 50%;
856      color: #1a73e8;
857      font-size: 26px;
858      font-weight: 600;
859      font-style: italic;
860      cursor: pointer;
861      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
862      z-index: 1000;
863      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
864      display: flex;
865      align-items: center;
866      justify-content: center;
867      font-family: 'Georgia', 'Times New Roman', serif;
868    }
869
870    .info-button:hover {
871      background: #f8f9fa;
872      box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
873      transform: scale(1.08);
874    }
875
876    .info-button:active {
877      transform: scale(0.95);
878    }
879
880    .info-tooltip {
881      position: fixed;
882      bottom: 80px;
883      left: 20px;
884      max-width: 350px;
885      background: white;
886      border-radius: 12px;
887      padding: 20px;
888      box-shadow: 0 4px 20px rgba(0,0,0,0.2);
889      z-index: 999;
890      display: none;
891      font-family: 'Inter', sans-serif;
892    }
893
894    .info-tooltip.visible {
895      display: block;
896      animation: fadeIn 0.2s;
897    }
898
899    @keyframes fadeIn {
900      from { opacity: 0; transform: translateY(10px); }
901      to { opacity: 1; transform: translateY(0); }
902    }
903
904    .info-tooltip-header {
905      font-size: 17px;
906      font-weight: 600;
907      color: #1a73e8;
908      margin-bottom: 14px;
909      padding-bottom: 12px;
910      border-bottom: 2px solid #e8e8e8;
911    }
912
913    .info-tooltip-content {
914      font-size: 15px;
915      color: #444;
916      line-height: 1.7;
917    }
918
919    .info-tooltip-content p {
920      margin: 0;
921      text-align: left;
922    }
923
924    .info-tooltip-close {
925      position: absolute;
926      top: 12px;
927      right: 12px;
928      background: none;
929      border: none;
930      font-size: 20px;
931      color: #999;
932      cursor: pointer;
933      width: 24px;
934      height: 24px;
935      display: flex;
936      align-items: center;
937      justify-content: center;
938      border-radius: 50%;
939      transition: all 0.2s;
940    }
941
942    .info-tooltip-close:hover {
943      background: #f0f0f0;
944      color: #666;
945    }
946
947    /* Step 3 detailed tagging */
948    .cluster-type-badge {
949      display: inline-block;
950      padding: 8px 16px;
951      border-radius: 20px;
952      font-size: 15px;
953      font-weight: 600;
954      margin-bottom: 12px;
955    }
956    
957    .cluster-type-badge.disliked {
958      background: #fce8e6;
959      color: #ea4335;
960      border: 2px solid #ea4335;
961    }
962    
963    .cluster-type-badge.liked {
964      background: #e6f4ea;
965      color: #34a853;
966      border: 2px solid #34a853;
967    }
968    
969    .cluster-size {
970      font-size: 15px;
971      color: #666;
972      margin-bottom: 15px;
973      font-weight: 500;
974    }
975
976    .split-section {
977      margin-bottom: 12px;
978      border: 1px solid #e8e8e8;
979      border-radius: 6px;
980      overflow: hidden;
981    }
982
983    .split-section-header {
984      background: #fff8f0;
985      padding: 8px 12px;
986      cursor: pointer;
987      font-size: 14px;
988      font-weight: 600;
989      color: #ff9800;
990      display: flex;
991      align-items: center;
992      justify-content: space-between;
993      transition: background 0.2s;
994    }
995
996    .split-info-icon {
997      display: inline-flex;
998      align-items: center;
999      justify-content: center;
1000      width: 18px;
1001      height: 18px;
1002      border-radius: 50%;
1003      border: 1.5px solid #ff9800;
1004      color: #ff9800;
1005      font-size: 12px;
1006      font-weight: bold;
1007      margin-left: 8px;
1008      cursor: help;
1009      transition: all 0.2s;
1010    }
1011
1012    .split-info-icon:hover {
1013      background: #ff9800;
1014      color: white;
1015      transform: scale(1.1);
1016    }
1017
1018    .split-section-header:hover {
1019      background: #ffedd4;
1020    }
1021
1022    .split-section-arrow {
1023      font-size: 10px;
1024      transition: transform 0.3s;
1025    }
1026
1027    .split-section-header.open .split-section-arrow {
1028      transform: rotate(180deg);
1029    }
1030
1031    .split-section-content {
1032      max-height: 0;
1033      overflow: hidden;
1034      transition: max-height 0.3s ease-out;
1035      padding: 0 12px;
1036    }
1037
1038    .split-section-content.open {
1039      max-height: 200px;
1040      padding: 10px 12px;
1041    }
1042
1043    .split-cluster-btn {
1044      width: 100%;
1045      padding: 8px;
1046      background: white;
1047      border: 2px dashed #ff9800;
1048      border-radius: 6px;
1049      color: #ff9800;
1050      font-size: 14px;
1051      font-weight: 600;
1052      cursor: pointer;
1053      transition: all 0.2s;
1054    }
1055
1056    .split-cluster-btn:hover {
1057      background: #fff3e0;
1058      border-style: solid;
1059      transform: translateY(-1px);
1060    }
1061
1062    @keyframes fadeInOut {
1063      0% { opacity: 0; transform: translateY(-10px); }
1064      15% { opacity: 1; transform: translateY(0); }
1065      85% { opacity: 1; transform: translateY(0); }
1066      100% { opacity: 0; transform: translateY(-10px); }
1067    }
1068
1069    @keyframes pulse {
1070      0%, 100% { transform: scale(1); }
1071      50% { transform: scale(1.05); }
1072    }
1073
1074    /* Loading Spinner */
1075    .loading-overlay {
1076      position: fixed;
1077      top: 0;
1078      left: 0;
1079      right: 0;
1080      bottom: 0;
1081      background: rgba(0, 0, 0, 0.5);
1082      display: flex;
1083      flex-direction: column;
1084      align-items: center;
1085      justify-content: center;
1086      z-index: 9999;
1087    }
1088
1089    .loading-overlay.hidden {
1090      display: none;
1091    }
1092
1093    .loading-spinner {
1094      width: 60px;
1095      height: 60px;
1096      border: 6px solid #f3f3f3;
1097      border-top: 6px solid #4285f4;
1098      border-radius: 50%;
1099      animation: spin 1s linear infinite;
1100    }
1101
1102    @keyframes spin {
1103      0% { transform: rotate(0deg); }
1104      100% { transform: rotate(360deg); }
1105    }
1106
1107    .loading-text {
1108      margin-top: 20px;
1109      color: white;
1110      font-size: 18px;
1111      font-weight: 600;
1112    }
1113
1114    /* Zoom Prompt */
1115    .zoom-prompt {
1116      position: fixed;
1117      bottom: 30px;
1118      left: 50%;
1119      transform: translateX(-50%);
1120      background: rgba(66, 133, 244, 0.95);
1121      color: white;
1122      padding: 16px 32px;
1123      border-radius: 30px;
1124      font-size: 16px;
1125      font-weight: 600;
1126      box-shadow: 0 4px 20px rgba(0,0,0,0.3);
1127      z-index: 10001;
1128      animation: pulsatePrompt 2s ease-in-out infinite;
1129      display: none;
1130      backdrop-filter: blur(10px);
1131    }
1132
1133    .zoom-prompt.visible {
1134      display: block;
1135    }
1136
1137    @keyframes pulsatePrompt {
1138      0%, 100% {
1139        transform: translateX(-50%) scale(1);
1140        box-shadow: 0 4px 20px rgba(0,0,0,0.3);
1141      }
1142      50% {
1143        transform: translateX(-50%) scale(1.05);
1144        box-shadow: 0 6px 30px rgba(66, 133, 244, 0.5);
1145      }
1146    }
1147
1148    /* Progress Tracker */
1149    .progress-tracker {
1150      position: absolute;
1151      top: 75px;
1152      right: 20px;
1153      background: white;
1154      padding: 16px;
1155      border-radius: 12px;
1156      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1157      min-width: 200px;
1158      z-index: 5;
1159    }
1160
1161    .progress-title {
1162      font-size: 12px;
1163      font-weight: 700;
1164      color: #666;
1165      text-transform: uppercase;
1166      letter-spacing: 0.5px;
1167      margin-bottom: 12px;
1168    }
1169
1170    .progress-step {
1171      display: flex;
1172      align-items: center;
1173      gap: 10px;
1174      margin-bottom: 10px;
1175      font-size: 15px;
1176    }
1177
1178    .progress-circle {
1179      width: 24px;
1180      height: 24px;
1181      border-radius: 50%;
1182      background: #e0e0e0;
1183      color: #999;
1184      display: flex;
1185      align-items: center;
1186      justify-content: center;
1187      font-size: 11px;
1188      font-weight: 700;
1189      flex-shrink: 0;
1190    }
1191
1192    .progress-step.active .progress-circle {
1193      background: #4285f4;
1194      color: white;
1195    }
1196
1197    .progress-step.completed .progress-circle {
1198      background: #34a853;
1199      color: white;
1200    }
1201
1202    .progress-step.completed .progress-circle::after {
1203      content: '✓';
1204    }
1205
1206    .progress-step-text {
1207      color: #999;
1208    }
1209
1210    .progress-step.active .progress-step-text {
1211      color: #4285f4;
1212      font-weight: 600;
1213    }
1214
1215    .progress-step.completed .progress-step-text {
1216      color: #666;
1217    }
1218
1219    /* Tutorial Overlay */
1220    .tutorial-overlay {
1221      position: fixed;
1222      top: 0;
1223      left: 0;
1224      right: 0;
1225      bottom: 0;
1226      background: rgba(0, 0, 0, 0.8);
1227      z-index: 10000;
1228      display: flex;
1229      align-items: center;
1230      justify-content: center;
1231    }
1232
1233    .tutorial-overlay.hidden {
1234      display: none;
1235    }
1236
1237    .tutorial-card {
1238      background: white;
1239      border-radius: 16px;
1240      padding: 32px 40px;
1241      max-width: 900px;
1242      box-shadow: 0 20px 60px rgba(0,0,0,0.3);
1243      animation: slideUp 0.3s ease-out;
1244    }
1245
1246    @keyframes slideUp {
1247      from {
1248        opacity: 0;
1249        transform: translateY(30px);
1250      }
1251      to {
1252        opacity: 1;
1253        transform: translateY(0);
1254      }
1255    }
1256
1257    .tutorial-step-content {
1258      text-align: center;
1259    }
1260
1261    .tutorial-icon {
1262      font-size: 64px;
1263      margin-bottom: 12px;
1264    }
1265
1266    .tutorial-icon:empty {
1267      display: none;
1268    }
1269
1270    .tutorial-title {
1271      font-size: 28px;
1272      font-weight: 700;
1273      color: #333;
1274      margin-bottom: 24px;
1275    }
1276
1277    .tutorial-description {
1278      font-size: 16px;
1279      color: #666;
1280      line-height: 1.6;
1281      margin-bottom: 32px;
1282    }
1283
1284    .tutorial-description p {
1285      margin: 12px 0;
1286    }
1287
1288    .tutorial-progress {
1289      display: flex;
1290      justify-content: center;
1291      gap: 8px;
1292      margin-bottom: 32px;
1293    }
1294
1295    .tutorial-progress-dot {
1296      width: 10px;
1297      height: 10px;
1298      border-radius: 50%;
1299      background: #ddd;
1300    }
1301
1302    .tutorial-progress-dot.active {
1303      background: #4285f4;
1304      width: 24px;
1305      border-radius: 5px;
1306    }
1307
1308    .tutorial-actions {
1309      display: flex;
1310      gap: 12px;
1311      justify-content: center;
1312    }
1313
1314    .tutorial-btn-skip {
1315      padding: 12px 24px;
1316      border: 2px solid #ddd;
1317      background: white;
1318      color: #666;
1319      border-radius: 8px;
1320      font-size: 15px;
1321      font-weight: 600;
1322      cursor: pointer;
1323    }
1324
1325    .tutorial-btn-next {
1326      padding: 12px 32px;
1327      border: none;
1328      background: #4285f4;
1329      color: white;
1330      border-radius: 8px;
1331      font-size: 15px;
1332      font-weight: 600;
1333      cursor: pointer;
1334    }
1335
1336    .tutorial-btn-skip:hover {
1337      background: #f5f5f5;
1338    }
1339
1340    .tutorial-btn-next:hover {
1341      background: #3367d6;
1342    }
1343    
1344    .tag-category {
1345      margin-bottom: 15px;
1346      background: white;
1347      border-radius: 8px;
1348      border: 2px solid #e0e0e0;
1349      overflow: hidden;
1350    }
1351    
1352    .tag-category-header {
1353      background: linear-gradient(to bottom, #f8f9fa, #f0f0f0);
1354      padding: 10px 15px;
1355      font-size: 16px;
1356      font-weight: 600;
1357      color: #333;
1358      border-bottom: 2px solid #e0e0e0;
1359      display: flex;
1360      align-items: center;
1361      justify-content: space-between;
1362      cursor: pointer;
1363      user-select: none;
1364    }
1365    
1366    .tag-category-header:hover {
1367      background: linear-gradient(to bottom, #f0f0f0, #e8e8e8);
1368    }
1369    
1370    .tag-category-title {
1371      display: flex;
1372      align-items: center;
1373      gap: 8px;
1374    }
1375    
1376    .tag-category-icon {
1377      font-size: 16px;
1378      display: none; /* REMOVE EMOJIS */
1379    }
1380    
1381    .tag-category-arrow {
1382      font-size: 12px;
1383      transition: transform 0.3s;
1384    }
1385    
1386    .tag-category.collapsed .tag-category-arrow {
1387      transform: rotate(-90deg);
1388    }
1389    
1390    .tag-options-group {
1391      padding: 10px;
1392      display: block;
1393    }
1394    
1395    .tag-category.collapsed .tag-options-group {
1396      display: none;
1397    }
1398    
1399    .tag-section-label {
1400      font-size: 10px;
1401      font-weight: 600;
1402      color: #666;
1403      text-transform: uppercase;
1404      letter-spacing: 0.5px;
1405      margin: 8px 0 6px 4px;
1406    }
1407    
1408    .tag-section-label.positive { color: #34a853; }
1409    .tag-section-label.negative { color: #ea4335; }
1410    
1411    .tag-option-item {
1412      display: flex;
1413      align-items: center;
1414      padding: 10px 12px;
1415      margin-bottom: 5px;
1416      background: #fafafa;
1417      border: 2px solid #e8e8e8;
1418      border-radius: 6px;
1419      cursor: pointer;
1420      transition: all 0.2s;
1421      font-size: 16px;
1422      user-select: none;
1423    }
1424    
1425    /* FIX 5: Color all positive tags green */
1426    .tag-option-item.positive {
1427      background: rgba(52, 168, 83, 0.08);
1428      border-color: rgba(52, 168, 83, 0.25);
1429    }
1430    
1431    /* FIX 5: Color all negative tags red */
1432    .tag-option-item.negative {
1433      background: rgba(234, 67, 53, 0.08);
1434      border-color: rgba(234, 67, 53, 0.25);
1435    }
1436    
1437    .tag-option-item:hover {
1438      background: #f0f7ff;
1439      border-color: #b3d9ff;
1440      transform: translateX(2px);
1441    }
1442    
1443    .tag-option-item.selected {
1444      background: #e8f0fe;
1445      border-color: #4285f4;
1446      font-weight: 500;
1447    }
1448    
1449    .tag-option-item.positive.selected {
1450      background: #e6f4ea;
1451      border-color: #34a853;
1452      color: #1e7e34;
1453    }
1454    
1455    .tag-option-item.negative.selected {
1456      background: #fce8e6;
1457      border-color: #ea4335;
1458      color: #c5221f;
1459    }
1460    
1461    .tag-option-checkbox {
1462      width: 18px;
1463      height: 18px;
1464      margin-right: 10px;
1465      cursor: pointer;
1466      flex-shrink: 0;
1467    }
1468    
1469    .tag-option-label {
1470      flex: 1;
1471    }
1472    
1473    .season-badge {
1474      display: inline-flex;
1475      align-items: center;
1476      justify-content: center;
1477      min-width: 60px;
1478      padding: 2px 8px;
1479      margin-right: 8px;
1480      background: #4285f4;
1481      color: white;
1482      border-radius: 12px;
1483      font-size: 10px;
1484      font-weight: 600;
1485      text-transform: uppercase;
1486      letter-spacing: 0.5px;
1487    }
1488    
1489    .season-badge.spring { background: #34a853; }
1490    .season-badge.summer { background: #fbbc04; color: #333; }
1491    .season-badge.fall { background: #ea8600; }
1492    .season-badge.winter { background: #4285f4; }
1493    
1494    .cluster-comment-full {
1495      width: 100%;
1496      padding: 10px;
1497      border: 2px solid #ddd;
1498      border-radius: 6px;
1499      font-size: 15px;
1500      margin: 12px 0;
1501      resize: vertical;
1502      min-height: 70px;
1503      font-family: inherit;
1504    }
1505    
1506    .cluster-comment-full:focus {
1507      outline: none;
1508      border-color: #4285f4;
1509      background: #f9fbff;
1510    }
1511    
1512    .cluster-nav-buttons {
1513      display: grid;
1514      grid-template-columns: 1fr 1fr;
1515      gap: 8px;
1516      margin-bottom: 12px;
1517    }
1518    
1519    .nav-cluster-btn {
1520      padding: 10px;
1521      border: 2px solid #ddd;
1522      background: white;
1523      color: #666;
1524      border-radius: 6px;
1525      font-size: 15px;
1526      font-weight: 600;
1527      cursor: pointer;
1528      transition: all 0.2s;
1529    }
1530    
1531    .nav-cluster-btn:hover:not(:disabled) {
1532      background: #f5f5f5;
1533      border-color: #999;
1534      transform: translateY(-1px);
1535      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1536    }
1537    
1538    .nav-cluster-btn:disabled {
1539      opacity: 0.3;
1540      cursor: not-allowed;
1541    }
1542    
1543    .nav-cluster-btn.primary {
1544      background: #4285f4;
1545      color: white;
1546      border-color: #4285f4;
1547    }
1548    
1549    .nav-cluster-btn.primary:hover {
1550      background: #357ae8;
1551    }
1552    
1553    .nav-cluster-btn.skip {
1554      color: #999;
1555      border-style: dashed;
1556    }
1557    
1558    .nav-cluster-btn.skip:hover {
1559      color: #666;
1560      border-color: #666;
1561    }
1562
1563
1564    /* Custom Alert Popup */
1565    .custom-alert-overlay {
1566      position: fixed;
1567      top: 0;
1568      left: 0;
1569      width: 100%;
1570      height: 100%;
1571      background: rgba(0, 0, 0, 0.5);
1572      display: flex;
1573      align-items: center;
1574      justify-content: center;
1575      z-index: 10000;
1576      animation: fadeIn 0.2s ease-in;
1577    }
1578    
1579    @keyframes fadeIn {
1580      from { opacity: 0; }
1581      to { opacity: 1; }
1582    }
1583    
1584    .custom-alert {
1585      background: white;
1586      border-radius: 12px;
1587      padding: 24px;
1588      max-width: 500px;
1589      width: 90%;
1590      max-height: 80vh;
1591      overflow-y: auto;
1592      box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
1593      animation: slideIn 0.3s ease-out;
1594    }
1595    
1596    @keyframes slideIn {
1597      from {
1598        transform: translateY(-50px);
1599        opacity: 0;
1600      }
1601      to {
1602        transform: translateY(0);
1603        opacity: 1;
1604      }
1605    }
1606    
1607    .custom-alert h3 {
1608      margin: 0 0 16px 0;
1609      font-size: 20px;
1610      color: #333;
1611    }
1612    
1613    .custom-alert-content {
1614      margin-bottom: 20px;
1615      font-size: 16px;
1616      line-height: 1.6;
1617      color: #555;
1618    }
1619
1620    .custom-alert-content p {
1621      margin-bottom: 15px;
1622    }
1623
1624    .custom-alert-content p:last-child {
1625      margin-bottom: 0;
1626    }
1627    
1628    .custom-alert-button {
1629      width: 100%;
1630      padding: 12px;
1631      background: #4285f4;
1632      color: white;
1633      border: none;
1634      border-radius: 6px;
1635      font-size: 15px;
1636      font-weight: 500;
1637      cursor: pointer;
1638      transition: background 0.2s;
1639    }
1640    
1641    .custom-alert-button:hover {
1642      background: #3367d6;
1643    }
1644
1645    /* Mosaic Visualization Styles */
1646    .visualization-section {
1647      margin: 25px 0;
1648      text-align: center;
1649    }
1650
1651    .visualization-section h4 {
1652      margin-bottom: 15px;
1653      color: #333;
1654      font-size: 18px;
1655      font-weight: 600;
1656    }
1657
1658    .visualization-section canvas {
1659      border-radius: 8px;
1660      box-shadow: 0 4px 16px rgba(0,0,0,0.1);
1661      max-width: 100%;
1662      height: auto;
1663    }
1664
1665    .download-viz-btn {
1666      flex: 1;
1667      padding: 14px;
1668      background: white;
1669      color: #4285f4;
1670      border: 2px solid #4285f4;
1671      border-radius: 8px;
1672      font-size: 16px;
1673      font-weight: 600;
1674      cursor: pointer;
1675      transition: background 0.2s, transform 0.2s;
1676    }
1677
1678    .download-viz-btn:hover {
1679      background: #f0f7ff;
1680    }
1681
1682    .download-viz-btn.pulsate-btn {
1683      animation: pulsate 2s ease-in-out infinite;
1684    }
1685
1686    @keyframes pulsate {
1687      0%, 100% {
1688        transform: scale(1);
1689        box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.7);
1690      }
1691      50% {
1692        transform: scale(1.05);
1693        box-shadow: 0 0 20px 5px rgba(66, 133, 244, 0.3);
1694      }
1695    }
1696
1697    .buttons-row {
1698      display: flex;
1699      gap: 15px;
1700      margin-top: 15px;
1701      max-width: 600px;
1702      margin-left: auto;
1703      margin-right: auto;
1704    }
1705
1706    .submit-comment-btn {
1707      width: 100%;
1708      padding: 14px;
1709      background: #34a853;
1710      color: white;
1711      border: none;
1712      border-radius: 8px;
1713      font-size: 15px;
1714      font-weight: 600;
1715      cursor: pointer;
1716      transition: all 0.2s;
1717      margin-top: 12px;
1718      box-shadow: 0 2px 4px rgba(52, 168, 83, 0.2);
1719    }
1720
1721    .submit-comment-btn:hover {
1722      background: #2d9548;
1723      box-shadow: 0 4px 8px rgba(52, 168, 83, 0.3);
1724      transform: translateY(-1px);
1725    }
1726
1727    .stats-row {
1728      margin-bottom: 10px;
1729      font-size: 14px;
1730    }
1731
1732    .comment-section {
1733      margin-top: 30px;
1734      padding: 20px;
1735      background: #f8f9fa;
1736      border-radius: 12px;
1737      border: 1px solid #e0e0e0;
1738    }
1739
1740    .comment-section label {
1741      display: block;
1742      margin-bottom: 12px;
1743      font-weight: 600;
1744      font-size: 16px;
1745      color: #1a73e8;
1746    }
1747
1748    .comment-section textarea:focus {
1749      outline: none;
1750      border-color: #1a73e8;
1751      box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
1752    }
1753
1754    /* Language Toggle Slider */
1755    .lang-toggle-container {
1756      position: absolute;
1757      top: 20px;
1758      right: 20px;
1759      z-index: 10;
1760    }
1761    
1762    .lang-toggle {
1763      position: relative;
1764      display: inline-block;
1765      width: 80px;
1766      height: 36px;
1767      background: #e0e0e0;
1768      border-radius: 18px;
1769      cursor: pointer;
1770      transition: background 0.3s;
1771    }
1772    
1773    .lang-toggle:hover {
1774      background: #d0d0d0;
1775    }
1776    
1777    .lang-toggle-slider {
1778      position: absolute;
1779      top: 3px;
1780      left: 3px;
1781      width: 36px;
1782      height: 30px;
1783      background: white;
1784      border-radius: 15px;
1785      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
1786      transition: transform 0.3s;
1787      display: flex;
1788      align-items: center;
1789      justify-content: center;
1790      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
1791      font-weight: 700;
1792      font-size: 15px;
1793      color: #0d47a1;
1794      letter-spacing: 0.5px;
1795      -webkit-font-smoothing: subpixel-antialiased;
1796      text-rendering: geometricPrecision;
1797    }
1798    
1799    .lang-toggle.en .lang-toggle-slider {
1800      transform: translateX(44px);
1801    }
1802    
1803    .lang-toggle-labels {
1804      position: absolute;
1805      top: 0;
1806      left: 0;
1807      right: 0;
1808      bottom: 0;
1809      display: flex;
1810      align-items: center;
1811      justify-content: space-between;
1812      padding: 0 10px;
1813      pointer-events: none;
1814    }
1815    
1816    .lang-toggle-label {
1817      font-size: 15px;
1818      font-weight: 600;
1819      color: #666;
1820      transition: color 0.3s, opacity 0.3s;
1821      -webkit-font-smoothing: antialiased;
1822      -moz-osx-font-smoothing: grayscale;
1823    }
1824    
1825    /* Hide FR label when slider is on it (default state) */
1826    .lang-toggle:not(.en) .lang-toggle-label.fr {
1827      opacity: 0;
1828    }
1829    
1830    /* Hide EN label when slider is on it */
1831    .lang-toggle.en .lang-toggle-label.en {
1832      opacity: 0;
1833    }
1834    
1835    /* Show FR label when slider is NOT on it (EN selected) */
1836    .lang-toggle.en .lang-toggle-label.fr {
1837      color: #999;
1838      opacity: 1;
1839    }
1840    
1841    /* Show EN label when slider is NOT on it (FR selected) */
1842    .lang-toggle:not(.en) .lang-toggle-label.en {
1843      color: #999;
1844      opacity: 1;
1845    }
1846
1847    /* Paint hint tooltip */
1848    .paint-hint {
1849      display: none;
1850      position: absolute;
1851      top: -8px;
1852      right: 6px;
1853      background: #4285f4;
1854      color: white;
1855      padding: 6px 12px;
1856      border-radius: 6px;
1857      font-size: 11px;
1858      font-weight: 600;
1859      white-space: nowrap;
1860      z-index: 10;
1861    }
1862
1863
1864    @keyframes hintPulse {
1865      0%, 100% { opacity: 1; transform: scale(1); }
1866      50% { opacity: 0.85; transform: scale(1.05); }
1867    }
1868
1869    /* ========== MOBILE RESPONSIVE ========== */
1870    /* Mobile-only segmented control */
1871    .mobile-segmented-control {
1872      display: none;
1873      background: #e0e0e0;
1874      border-radius: 10px;
1875      padding: 4px;
1876      gap: 4px;
1877      margin-bottom: 12px;
1878    }
1879
1880    .mobile-segment {
1881      flex: 1;
1882      padding: 12px 8px;
1883      border-radius: 8px;
1884      font-size: 12px;
1885      font-weight: 600;
1886      text-align: center;
1887      cursor: pointer;
1888      transition: all 0.2s;
1889      background: transparent;
1890      color: #666;
1891      border: none;
1892      position: relative;
1893    }
1894
1895    .mobile-segment .segment-check {
1896      display: none;
1897      position: absolute;
1898      top: 4px;
1899      right: 6px;
1900      width: 18px;
1901      height: 18px;
1902      background: rgba(0,0,0,0.2);
1903      border-radius: 50%;
1904      font-size: 11px;
1905      line-height: 18px;
1906      text-align: center;
1907    }
1908
1909    .mobile-segment.active-positive {
1910      background: #34a853;
1911      color: white;
1912      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
1913    }
1914
1915    .mobile-segment.active-positive .segment-check {
1916      display: block;
1917      background: rgba(0,0,0,0.15);
1918    }
1919
1920    .mobile-segment.active-negative {
1921      background: #ea4335;
1922      color: white;
1923      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
1924    }
1925
1926    .mobile-segment.active-negative .segment-check {
1927      display: block;
1928      background: rgba(0,0,0,0.15);
1929    }
1930
1931    .mobile-segment.active-navigate {
1932      background: white;
1933      color: #1a73e8;
1934      box-shadow: 0 2px 4px rgba(0,0,0,0.15);
1935    }
1936
1937    .mobile-segment.active-navigate .segment-check {
1938      display: block;
1939      background: #1a73e8;
1940      color: white;
1941    }
1942
1943    /* Mobile-only paint toolbar (hidden on desktop) */
1944    .mobile-paint-toolbar {
1945      display: none;
1946      flex-direction: column;
1947      margin-bottom: 10px;
1948    }
1949
1950    .mobile-paint-btn {
1951      flex: 1;
1952      padding: 10px 6px;
1953      border: 2px solid #ddd;
1954      border-radius: 8px;
1955      font-size: 13px;
1956      font-weight: 600;
1957      cursor: pointer;
1958      background: #f5f5f5;
1959      color: #333;
1960      text-align: center;
1961    }
1962
1963    .mobile-paint-btn.mobile-green.active {
1964      background: #34a853;
1965      color: white;
1966      border-color: #2d9348;
1967    }
1968
1969    .mobile-paint-btn.mobile-red.active {
1970      background: #ea4335;
1971      color: white;
1972      border-color: #d33426;
1973    }
1974
1975    .mobile-paint-btn.mobile-toggle {
1976      background: #4285f4;
1977      color: white;
1978      border-color: #3367d6;
1979      font-weight: 700;
1980    }
1981
1982    .mobile-paint-btn.mobile-toggle.mobile-pulse {
1983      animation: mobilePulse 1.5s ease-in-out infinite;
1984    }
1985
1986    @keyframes mobilePulse {
1987      0%, 100% { box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.5); }
1988      50% { box-shadow: 0 0 0 8px rgba(66, 133, 244, 0); }
1989    }
1990
1991    .mobile-paint-btn.mobile-toggle.painting {
1992      background: #ea4335;
1993      border-color: #d33426;
1994      animation: none;
1995    }
1996
1997    /* Toggle Switch for Mobile Paint */
1998    .mobile-toggle-switch {
1999      position: relative;
2000      display: inline-block;
2001      width: 56px;
2002      height: 32px;
2003      flex-shrink: 0;
2004    }
2005
2006    .mobile-toggle-switch input {
2007      opacity: 0;
2008      width: 0;
2009      height: 0;
2010    }
2011
2012    .toggle-slider {
2013      position: absolute;
2014      cursor: pointer;
2015      top: 0;
2016      left: 0;
2017      right: 0;
2018      bottom: 0;
2019      background-color: #ccc;
2020      transition: .3s;
2021      border-radius: 32px;
2022      border: 2px solid #999;
2023    }
2024
2025    .toggle-slider:before {
2026      position: absolute;
2027      content: "";
2028      height: 24px;
2029      width: 24px;
2030      left: 2px;
2031      bottom: 2px;
2032      background-color: white;
2033      transition: .3s;
2034      border-radius: 50%;
2035    }
2036
2037    .mobile-toggle-switch input:checked + .toggle-slider {
2038      background-color: #4285f4;
2039      border-color: #3367d6;
2040    }
2041
2042    .mobile-toggle-switch input:checked + .toggle-slider:before {
2043      transform: translateX(24px);
2044    }
2045
2046    .mobile-toggle-switch.mobile-pulse .toggle-slider {
2047      animation: mobilePulse 1.5s ease-in-out infinite;
2048    }
2049
2050    .mobile-paint-btn.mobile-erase {
2051      background: #f5f5f5;
2052      border-color: #ddd;
2053    }
2054
2055    .mobile-paint-btn.mobile-erase.active {
2056      background: #ff9800;
2057      color: white;
2058      border-color: #f57c00;
2059    }
2060
2061    .mobile-paint-btn.mobile-clear-all {
2062      background: #fff5f5;
2063      color: #d32f2f;
2064      border-color: #d32f2f;
2065      font-weight: 700;
2066    }
2067
2068    .mobile-paint-btn.mobile-clear-all:active {
2069      background: #d32f2f;
2070      color: white;
2071    }
2072
2073    /* Mobile Option F styles */
2074    .mobile-option-f {
2075      display: none;
2076    }
2077
2078    .mobile-zone-btn {
2079      flex: 1;
2080      padding: 12px;
2081      border: 2px solid #ddd;
2082      border-radius: 8px;
2083      font-size: 15px;
2084      font-weight: 600;
2085      cursor: pointer;
2086      background: white;
2087      color: #666;
2088      transition: all 0.2s;
2089    }
2090
2091    .mobile-zone-btn.mobile-zone-positive {
2092      border-color: #34a853;
2093      color: #34a853;
2094    }
2095
2096    .mobile-zone-btn.mobile-zone-positive.active {
2097      background: #34a853;
2098      color: white;
2099    }
2100
2101    .mobile-zone-btn.mobile-zone-negative {
2102      border-color: #ea4335;
2103      color: #ea4335;
2104    }
2105
2106    .mobile-zone-btn.mobile-zone-negative.active {
2107      background: #ea4335;
2108      color: white;
2109    }
2110
2111    .mobile-peinturer-btn {
2112      width: 100%;
2113      padding: 14px;
2114      background: white;
2115      color: #1a73e8;
2116      border: 3px solid #1a73e8;
2117      border-radius: 12px;
2118      font-size: 16px;
2119      font-weight: 600;
2120      cursor: pointer;
2121      transition: all 0.2s;
2122      position: relative;
2123    }
2124
2125    .mobile-peinturer-btn.active {
2126      background: #1a73e8;
2127      color: white;
2128    }
2129
2130    .mobile-action-btn {
2131      flex: 1;
2132      padding: 10px 6px;
2133      background: white;
2134      color: #666;
2135      border: 2px solid #ddd;
2136      border-radius: 6px;
2137      font-size: 12px;
2138      font-weight: 600;
2139      cursor: pointer;
2140      transition: all 0.2s;
2141      position: relative;
2142    }
2143
2144    .mobile-action-btn:active {
2145      background: #f5f5f5;
2146    }
2147
2148    /* Mobile tool buttons (Naviguer, Peinturer, Effacer) */
2149    .mobile-tool-btn {
2150      flex: 1;
2151      padding: 12px 8px;
2152      background: white;
2153      color: #666;
2154      border: 2px solid #ddd;
2155      border-radius: 8px;
2156      font-size: 14px;
2157      font-weight: 600;
2158      cursor: pointer;
2159      transition: all 0.2s;
2160      position: relative;
2161    }
2162
2163    .mobile-tool-btn.active {
2164      border-color: #1a73e8;
2165      background: #1a73e8;
2166      color: white;
2167    }
2168
2169    .mobile-tool-btn.mobile-peinturer {
2170      border-color: #1a73e8;
2171      color: #1a73e8;
2172    }
2173
2174    .mobile-tool-btn.mobile-peinturer.active {
2175      color: white;
2176    }
2177
2178    /* Mobile paint hint (on Peinturer button) */
2179    .mobile-paint-hint {
2180      display: block;
2181      position: absolute;
2182      top: -8px;
2183      right: 6px;
2184      background: #4285f4;
2185      color: white;
2186      padding: 4px 8px;
2187      border-radius: 6px;
2188      font-size: 10px;
2189      font-weight: 600;
2190      white-space: nowrap;
2191      z-index: 10;
2192      animation: hintPulse 2s ease-in-out infinite;
2193    }
2194
2195    /* Mobile tool button checkmark */
2196    .mobile-tool-checkmark {
2197      position: absolute;
2198      top: -8px;
2199      right: -8px;
2200      width: 22px;
2201      height: 22px;
2202      background: white;
2203      border: 2px solid #1a73e8;
2204      border-radius: 50%;
2205      display: none;
2206      align-items: center;
2207      justify-content: center;
2208      color: #1a73e8;
2209      font-size: 13px;
2210      font-weight: 700;
2211      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
2212    }
2213
2214    .mobile-tool-btn.active .mobile-tool-checkmark {
2215      display: flex;
2216    }
2217
2218    /* Panel minimize button (mobile) */
2219    .panel-minimize-btn {
2220      display: none;
2221      position: absolute;
2222      top: 12px;
2223      right: 16px;
2224      width: 32px;
2225      height: 32px;
2226      background: white;
2227      border: 2px solid #ddd;
2228      border-radius: 50%;
2229      cursor: pointer;
2230      align-items: center;
2231      justify-content: center;
2232      font-size: 18px;
2233      color: #666;
2234      z-index: 10;
2235      transition: all 0.2s;
2236    }
2237
2238    .panel-minimize-btn:hover {
2239      background: #f5f5f5;
2240      border-color: #1a73e8;
2241      color: #1a73e8;
2242    }
2243
2244    .mobile-clear-all-btn {
2245      width: 100%;
2246      padding: 8px;
2247      background: white;
2248      color: #ea4335;
2249      border: 2px solid #ea4335;
2250      border-radius: 8px;
2251      font-size: 12px;
2252      font-weight: 600;
2253      cursor: pointer;
2254      transition: all 0.2s;
2255      margin-top: 8px;
2256    }
2257
2258    /* Checkmark badge for mobile buttons */
2259    .mobile-checkmark {
2260      position: absolute;
2261      top: -8px;
2262      right: -8px;
2263      width: 24px;
2264      height: 24px;
2265      background: white;
2266      border: 2px solid #1a73e8;
2267      border-radius: 50%;
2268      display: none;
2269      align-items: center;
2270      justify-content: center;
2271      color: #1a73e8;
2272      font-size: 14px;
2273      font-weight: 700;
2274      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
2275    }
2276
2277    .mobile-peinturer-btn.active .mobile-checkmark,
2278    .mobile-action-btn.active .mobile-checkmark {
2279      display: flex;
2280    }
2281
2282    /* Bottom sheet handle */
2283    .sheet-handle {
2284      display: none;
2285      width: 48px;
2286      height: 5px;
2287      background: #ddd;
2288      border-radius: 3px;
2289      margin: 8px auto 12px;
2290    }
2291
2292    @media (max-width: 768px) {
2293      /* Hide language toggle on mobile */
2294      #lang-toggle {
2295        display: none !important;
2296      }
2297
2298      /* Hide desktop clear all button on mobile */
2299      #clear-all-paint-btn {
2300        display: none !important;
2301      }
2302
2303      /* Bottom sheet handle visible on mobile */
2304      .sheet-handle {
2305        display: block;
2306      }
2307
2308      /* Panel minimize button hidden on mobile */
2309      .panel-minimize-btn {
2310        display: none !important;
2311      }
2312
2313      /* Main panel as compact bottom sheet */
2314      .main-panel {
2315        top: auto;
2316        bottom: 0;
2317        left: 0;
2318        right: 0;
2319        width: 100%;
2320        height: 60vh;
2321        max-height: 60vh;
2322        padding: 0 12px 12px;
2323        border-radius: 20px 20px 0 0;
2324        overflow-y: auto;
2325        box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
2326        transition: transform 0.3s ease;
2327      }
2328
2329      /* Step 1 specific: panel at 35vh (no scrolling) */
2330      .main-panel:has(#step1:not(.hidden)) {
2331        height: 35vh;
2332        max-height: 35vh;
2333        overflow: hidden !important;
2334      }
2335
2336      /* Step 2 specific: panel at 44vh (no scrolling) */
2337      .main-panel:has(#step2:not(.hidden)) {
2338        height: 44vh;
2339        max-height: 44vh;
2340        overflow: hidden !important;
2341      }
2342
2343      /* Reduce spacing in Step 2 on mobile */
2344      #step2-paint .step-header {
2345        margin-bottom: 12px;
2346        padding: 6px 10px;
2347        font-size: 15px;
2348      }
2349
2350      .mobile-option-f >
2350 div:first-child {
2351        margin-bottom: 4px;
2352      }
2353
2354      /* Hide cluster badge and size on mobile Step 3 to save space */
2355      .cluster-type-badge,
2356      .cluster-size {
2357        display: none !important;
2358      }
2359
2360      /* Panel minimized state */
2361      .main-panel.minimized {
2362        transform: translateY(calc(100% - 60px));
2363      }
2364
2365      .main-panel h2 {
2366        font-size: 15px;
2367        margin-bottom: 8px;
2368      }
2369
2370      .step-section {
2371        margin-bottom: 8px;
2372      }
2373
2374      /* Smaller buttons */
2375      .btn, .confirm-btn, .next-btn, .back-btn, .submit-btn {
2376        min-height: 34px;
2377        font-size: 13px;
2378        padding: 6px 12px;
2379      }
2380
2381      .tool-btn {
2382        min-height: 34px;
2383        font-size: 12px;
2384        padding: 6px 10px;
2385      }
2386
2387      /* Language toggle */
2388      .lang-toggle {
2389        top: 8px;
2390        right: 8px;
2391        font-size: 12px;
2392        padding: 4px 8px;
2393      }
2394
2395      /* Info button positioning for mobile - above bottom sheet */
2396      .info-button {
2397        bottom: calc(45vh + 10px);
2398        left: 16px;
2399        width: 44px;
2400        height: 44px;
2401        font-size: 22px;
2402      }
2403
2404      .info-tooltip {
2405        bottom: calc(45vh + 65px);
2406        left: 16px;
2407        max-width: calc(100vw - 32px);
2408      }
2409
2410      /* Progress tracker - compact horizontal */
2411      .progress-tracker {
2412        top: 8px;
2413        left: 8px;
2414        right: auto;
2415        padding: 6px 10px;
2416        font-size: 11px;
2417        display: flex !important;
2418        flex-direction: row;
2419        align-items: center;
2420        gap: 6px;
2421        min-width: auto;
2422      }
2423
2424      .progress-tracker .progress-title {
2425        display: none;
2426      }
2427
2428      .progress-tracker .progress-step {
2429        flex-direction: row;
2430        gap: 3px;
2431      }
2432
2433      .progress-step-text {
2434        display: none;
2435      }
2436
2437      .progress-circle {
2438        width: 20px;
2439        height: 20px;
2440        font-size: 10px;
2441      }
2442
2443      /* Hide map nav controls (arrow buttons) on mobile */
2444      .map-nav-controls {
2445        display: none !important;
2446      }
2447
2448      /* Tutorial overlay */
2449      .tutorial-card {
2450        margin: 10px;
2451        max-width: calc(100vw - 20px);
2452        padding: 16px;
2453      }
2454
2455      .tutorial-title { font-size: 18px; }
2456      .tutorial-body { font-size: 14px; }
2457
2458      /* Welcome popup */
2459      .welcome-overlay {
2460        align-items: stretch;
2461        padding: 0;
2462        background: white;
2463      }
2464
2465      .welcome-popup {
2466        padding: 20px 20px 80px;
2467        max-width: 100vw;
2468        width: 100vw;
2469        height: 100vh;
2470        margin: 0;
2471        border-radius: 0;
2472        overflow-y: scroll;
2473        display: flex;
2474        flex-direction: column;
2475        justify-content: space-between;
2476        position: relative;
2477        -webkit-overflow-scrolling: touch;
2478      }
2479
2480      /* Visible scrollbar on mobile */
2481      .welcome-popup::-webkit-scrollbar {
2482        width: 8px;
2483        display: block;
2484      }
2485
2486      .welcome-popup::-webkit-scrollbar-track {
2487        background: #f0f0f0;
2488        border-radius: 4px;
2489      }
2490
2491      .welcome-popup::-webkit-scrollbar-thumb {
2492        background: #1a73e8;
2493        border-radius: 4px;
2494      }
2495
2496      .welcome-popup::-webkit-scrollbar-thumb:hover {
2497        background: #1557b0;
2498      }
2499
2500      .welcome-popup .lang-toggle-container {
2501        position: static;
2502        display: flex;
2503        justify-content: center;
2504        margin-bottom: 20px;
2505      }
2506
2507      .welcome-overlay .popup {
2508        margin: 10px;
2509        max-width: calc(100vw - 20px);
2510        padding: 20px;
2511      }
2512
2513      .welcome-logo {
2514        width: 80px;
2515        height: 80px;
2516      }
2517
2518      .city-name {
2519        font-size: 30px !important;
2520        letter-spacing: 2px !important;
2521      }
2522
2523      .app-title { font-size: 24px !important; }
2524
2525      /* Tag options grid */
2526      .tag-options { grid-template-columns: 1fr; }
2527
2528      /* Zoom prompt */
2529      .zoom-prompt {
2530        bottom: calc(40vh + 10px);
2531        padding: 8px 16px;
2532        font-size: 12px;
2533      }
2534
2535      /* Smaller labels */
2536      .cluster-size, .cluster-type-badge { font-size: 12px; }
2537      .step-header { font-size: 13px; padding: 5px 8px; }
2538      .step-label { font-size: 12px; }
2539      .control-label { font-size: 13px; }
2540
2541      /* Smaller canvas on mobile */
2542      #mosaic-canvas {
2543        max-width: 90vw !important;
2544      }
2545
2546      /* Button groups */
2547      .button-group {
2548        flex-direction: column;
2549        gap: 6px;
2550      }
2551
2552      .button-group .btn { width: 100%; }
2553
2554      /* Custom alert */
2555      .custom-alert {
2556        width: 100vw;
2557        max-width: none;
2558        height: 100vh;
2559        max-height: 100vh;
2560        margin: 0;
2561        border-radius: 0;
2562        overflow-y: scroll !important;
2563        -webkit-overflow-scrolling: touch;
2564        padding: 80px 24px 60px !important;
2565        box-sizing: border-box;
2566      }
2567
2568      /* Visible scrollbar on mobile for submission screen */
2569      .custom-alert::-webkit-scrollbar {
2570        width: 12px !important;
2571        display: block !important;
2572      }
2573
2574      .custom-alert::-webkit-scrollbar-track {
2575        background: #f0f0f0 !important;
2576        border-radius: 6px;
2577      }
2578
2579      .custom-alert::-webkit-scrollbar-thumb {
2580        background: #1a73e8 !important;
2581        border-radius: 6px;
2582      }
2583
2584      .custom-alert::-webkit-scrollbar-thumb:hover {
2585        background: #1557b0 !important;
2586      }
2587
2588      /* Slider */
2589      input[type="range"] { width: 100%; }
2590
2591      /* === MOBILE SIMPLIFICATION === */
2592
2593      /* Step 1: Hide method choice, show address only */
2594      #step1 .step-label:first-of-type,
2595      #step1 .button-group {
2596        display: none !important;
2597      }
2598
2599      #address-section {
2600        display: block !important;
2601      }
2602
2603      #coords-display {
2604        display: none !important;
2605      }
2606
2607      /* Step 2: Hide size slider, paint targets, display options, original tool/color selectors */
2608      #size-controls,
2609      .paint-target-selector,
2610      #display-options-header,
2611      #display-options-content,
2612      .tool-selector-row,
2613      .tag-mode-selector,
2614      #step2-paint > .step-label {
2615        display: none !important;
2616      }
2617
2618      /* Show mobile-only controls */
2619      .mobile-option-f {
2620        display: block !important;
2621      }
2622
2623      /* Step 3: Hide split section on mobile */
2624      .split-section {
2625        display: none !important;
2626      }
2627    }
2628
2629    @media (max-width: 480px) {
2630      .main-panel {
2631        max-height: 38vh;
2632        padding: 8px 10px;
2633      }
2634
2635      .main-panel h2 { font-size: 14px; }
2636      .step-header { font-size: 12px; padding: 4px 6px; }
2637
2638      .city-name { font-size: 24px !important; }
2639      .app-title { font-size: 20px !important; }
2640      .welcome-overlay .popup p { font-size: 13px; }
2641    }
2642
2643  </style>
2644</head>
2645<body>
2646    <!-- Progress Tracker -->
2647    <div class="progress-tracker" id="progress-tracker" style="display: none;">
2648      <div class="progress-title">
2649        <span data-en="Progress" data-fr="Progression">Progression</span>
2650      </div>
2651      <div class="progress-step" id="progress-step-1">
2652        <div class="progress-circle">1</div>
2653        <div class="progress-step-text" data-en="Select neighbourhood" data-fr="Sélectionner son quartier">Sélectionner son quartier</div>
2654      </div>
2655      <div class="progress-step" id="progress-step-2">
2656        <div class="progress-circle">2</div>
2657        <div class="progress-step-text" data-en="Paint" data-fr="Taguer">Taguer</div>
2658      </div>
2659      <div class="progress-step" id="progress-step-3">
2660        <div class="progress-circle">3</div>
2661        <div class="progress-step-text" data-en="Label" data-fr="Étiqueter">Étiqueter</div>
2662      </div>
2663      <div class="progress-step" id="progress-step-4">
2664        <div class="progress-circle">4</div>
2665        <div class="progress-step-text" data-en="Submit" data-fr="Soumettre">Soumettre</div>
2666      </div>
2667    </div>
2668
2669    <!-- Tutorial Overlay -->
2670    <div class="tutorial-overlay hidden" id="tutorial-overlay">
2671      <div class="tutorial-card">
2672        <div class="tutorial-step-content" id="tutorial-content">
2673          <!-- Content will be dynamically inserted -->
2674        </div>
2675      </div>
2676    </div>
2677
2678    <!-- Loading Overlay -->
2679    <div class="loading-overlay hidden" id="loading-overlay">
2680      <div class="loading-spinner"></div>
2681      <div class="loading-text">
2682        <span data-en="Loading buildings..." data-fr="Chargement des bâtiments...">Chargement des bâtiments...</span>
2683      </div>
2684    </div>
2685
2686    <div style="position: fixed; top: 20px; right: 20px; z-index: 1000;">
2687      <button id="lang-toggle" style="padding: 8px 16px; background: white; border: 2px solid #4285f4; border-radius: 20px; font-size: 14px; font-weight: 500; cursor: pointer; color: #4285f4; box-shadow: 0 2px 8px rgba(0,0,0,0.15);">FR / EN</button>
2688    </div>
2689
2690
2691<!-- Password Protection Screen -->
2692<div class="welcome-overlay" id="password-overlay" style="display: none;">
2693  <div class="welcome-popup" style="max-width: 400px;">
2694    <!-- Language Toggle -->
2695    <div class="lang-toggle-container">
2696      <div class="lang-toggle" id="password-lang-toggle">
2697        <div class="lang-toggle-slider">FR</div>
2698        <div class="lang-toggle-labels">
2699          <span class="lang-toggle-label fr">FR</span>
2700          <span class="lang-toggle-label en">EN</span>
2701        </div>
2702      </div>
2703    </div>
2704    
2705    <div style="text-align: center; margin-bottom: 24px;">
2706      <div style="width: 80px; height: 80px; margin: 0 auto 16px; background: #003366; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40px;">
2707        🔒
2708      </div>
2709      <h2 style="margin: 0 0 12px 0; font-size: 24px; color: #333;" data-en="Access Required" data-fr="Accès requis">Accès requis</h2>
2710      <p style="margin: 0; color: #666; font-size: 14px;" data-en="Please enter the password to access the tool." data-fr="Veuillez entrer le mot de passe pour accéder à l'outil.">Veuillez entrer le mot de passe pour accéder à l'outil.</p>
2711    </div>
2712    <input type="password" id="password-input" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; margin-bottom: 16px; font-size: 15px; box-sizing: border-box;" placeholder="Mot de passe">
2713    <button class="welcome-btn primary" id="password-submit" style="width: 100%;" data-en="Enter" data-fr="Entrer">Entrer</button>
2714    <p id="password-error" style="color: #ea4335; margin-top: 12px; display: none; font-size: 13px; text-align: center;" data-en="Incorrect password" data-fr="Mot de passe incorrect">Mot de passe incorrect</p>
2715  </div>
2716</div>
2717
2718<!-- Welcome Popup -->
2719<div class="welcome-overlay" id="welcome-overlay" style="display: none;">
2720  <div class="welcome-popup">
2721    <!-- Language Toggle -->
2722    <div class="lang-toggle-container">
2723      <div class="lang-toggle" id="welcome-lang-toggle">
2724        <div class="lang-toggle-slider">FR</div>
2725        <div class="lang-toggle-labels">
2726          <span class="lang-toggle-label fr">FR</span>
2727          <span class="lang-toggle-label en">EN</span>
2728        </div>
2729      </div>
2730    </div>
2731    
2732    <div style="text-align: center; margin-bottom: 10px;">
2733      <div style="font-family: 'Inter', sans-serif; font-size: 50px; font-weight: 700; color: #4285f4; text-transform: uppercase; letter-spacing: 4px;">
2734        MONTRÉAL
2735      </div>
2736    </div>
2737    
2738    <h1 class="welcome-title">Tag ta Ville</h1>
2739    
2740    <p class="welcome-text" data-en="<strong>You know your neighbourhood better than anyone.</strong>" data-fr="<strong>Vous connaissez votre quartier mieux que quiconque.</strong>">
2741      <strong>Vous connaissez votre quartier mieux que quiconque.</strong>
2742    </p>
2743    <p class="welcome-text" data-en="This tool allows you to identify places that improve quality of life in your area, and those that harm it." data-fr="Cet outil vous permet d'identifier les endroits qui améliorent la qualité de vie dans votre secteur, et ceux qui y nuisent.">
2744      Cet outil vous permet d'identifier les endroits qui améliorent la qualité de vie dans votre secteur, et ceux qui y nuisent.
2745    </p>
2746    <p class="welcome-text" data-en="Your contribution helps researchers better understand how urban spaces are experienced in daily life, and how they can be improved. It's quick, anonymous, and based entirely on what you observe as you move through your neighbourhood." data-fr="Votre contribution aide les chercheurs à mieux comprendre comment les espaces urbains sont vécus au quotidien et comment ils peuvent être améliorés. C'est rapide, anonyme, et basé entièrement sur ce que vous observez en vous déplaçant dans votre quartier.">
2747      Votre contribution aide les chercheurs à mieux comprendre comment les espaces urbains sont vécus au quotidien et comment ils peuvent être améliorés. C'est rapide, anonyme, et basé entièrement sur ce que vous observez en vous déplaçant dans votre quartier.
2748    </p>
2749
2750    <div class="data-collapsible">
2751      <div class="data-collapsible-header" id="data-collapsible-toggle">
2752        <span data-en="What do we do with this data?" data-fr="Que faisons-nous avec ces données?">Que faisons-nous avec ces données?</span>
2753        <span class="data-collapsible-arrow">▼</span>
2754      </div>
2755      <div class="data-collapsible-content" id="data-collapsible-content">
2756        <p data-en="The aggregated data will be used for research purposes to better understand our urban spaces. The results will be shared with decision-makers to promote better-informed urban policies. The data will not be used for any commercial purpose." data-fr="Les données agrégées seront utilisées à des fins de recherche pour mieux comprendre nos espaces urbains. Les résultats de ces travaux seront partagés avec les décideurs afin de promouvoir des politiques urbaines mieux informées. Les données ne seront utilisées à aucune fin commerciale.">
2757          Les données agrégées seront utilisées à des fins de recherche pour mieux comprendre nos espaces urbains. Les résultats de ces travaux seront partagés avec les décideurs afin de promouvoir des politiques urbaines mieux informées. Les données ne seront utilisées à aucune fin commerciale.
2758        </p>
2759        <p data-en="This project is directed by Professor Evelyne Brie, from the Department of Political Science at the University of Montreal. To learn more, or to contact us, click on &quot;About this project.&quot;" data-fr="Ce projet est dirigé par la professeure Evelyne Brie, du département de science politique de l'Université de Montréal. Pour en savoir plus, ou pour communiquer avec nous, cliquez sur « À propos de ce projet ».">
2760          Ce projet est dirigé par la professeure Evelyne Brie, du département de science politique de l'Université de Montréal. Pour en savoir plus, ou pour communiquer avec nous, cliquez sur « À propos de ce projet ».
2761        </p>
2762      </div>
2763    </div>
2764    
2765    <div class="welcome-buttons">
2766      <button class="welcome-btn secondary" id="more-info-btn" data-en="About this project" data-fr="À propos de ce projet">À propos de ce projet</button>
2767      <button class="welcome-btn primary" id="start-btn" data-en="Get Started" data-fr="Commencer">Commencer</button>
2768    </div>
2769    
2770    <!-- Logos -->
2771    <div style="margin-top: 34px; padding-top: 26px; border-top: 1px solid #e8e8e8; display: flex; align-items: center; justify-content: center; gap: 32px; flex-wrap: wrap;">
2772      <div style="background: white; padding: 8px 16px; border-radius: 6px;">
2773        <img src="./assets/ivado-rgb_logo-full-degrade.png" alt="IVADO" style="height: 65px; object-fit: contain;">
2774      </div>
2775      <img src="./assets/udem-logo.svg" alt="Université de Montréal" style="height: 50px; object-fit: contain;">
2776    </div>
2777  </div>
2778</div>
2779
2780<div id="map"></div>
2781
2782<!-- Zoom Prompt -->
2783<div class="zoom-prompt" id="zoom-prompt">
2784  <span data-en="Zoom in closer to see 3D buildings" data-fr="Zoomez pour voir les bâtiments en 3D">Zoomez pour voir les bâtiments en 3D</span>
2785</div>
2786
2787<!-- Info Button -->
2788<button class="info-button" id="info-button">i</button>
2789<div class="info-tooltip" id="info-tooltip">
2790  <button class="info-tooltip-close" id="info-tooltip-close">×</button>
2791  <div class="info-tooltip-header" id="info-tooltip-header">Step 1</div>
2792  <div class="info-tooltip-content" id="info-tooltip-content">
2793    <!-- Content will be dynamically updated -->
2794  </div>
2795</div>
2796
2797<!-- Main Control Panel - Top Left -->
2798<div class="panel main-panel">
2799  <div class="sheet-handle"></div>
2800  <button class="panel-minimize-btn" id="panel-minimize-btn">▼</button>
2801
2802  <!-- STEP 1: Select & Load -->
2803  <div class="step-section" id="step1">
2804    <div class="step-header"><span data-en="Step 1: Select Your Neighbourhood" data-fr="Étape 1 : Sélectionnez votre quartier">Étape 1 : Sélectionnez votre quartier</span></div>
2805    
2806    <span class="step-label"><span data-en="Selection Method" data-fr="Méthode de sélection">Méthode de sélection</span></span>
2807    <div class="button-group">
2808      <button class="btn active" id="btn-click"><span data-en="Click Map" data-fr="Cliquer sur la carte">Cliquer sur la carte</span></button>
2809      <button class="btn" id="btn-address"><span data-en="Enter Address" data-fr="Entrer une adresse">Entrer une adresse</span></button>
2810    </div>
2811    
2812    <div class="hidden" id="address-section">
2813      <input
2814        type="text"
2815        class="address-input"
2816        id="address-input"
2817        placeholder="Entrez une adresse à Montréal"
2818        autocomplete="off"
2819      />
2820      <div class="address-suggestions hidden" id="address-suggestions"></div>
2821    </div>
2822    
2823    
2824    <div class="hidden" id="location-selected">
2825      <div class="selected-info">
2826        <div><strong data-fr="Quartier sélectionné (750 mètres)" data-en="Neighbourhood Selected (750 meters)">Quartier sélectionné (750 mètres)</strong></div>
2827        <div id="coords-display">Waiting...</div>
2828      </div>
2829      <button class="confirm-btn" id="confirm-btn" data-fr="Visualiser mon quartier" data-en="Visualize My Neighbourhood">Visualiser mon quartier</button>
2830    </div>
2831  </div>
2832  
2833  <!-- STEP 2: Paint Areas (Combined Red/Green) -->
2834  <div class="step-section hidden" id="step2-paint">
2835    <div class="step-header"><span data-en="Step 2: Tag Areas" data-fr="Étape 2 : Taguer les zones">Étape 2 : Taguer les zones</span></div>
2836
2837    <!-- Mobile-only segmented control -->
2838    <!-- Mobile: Zone selector + Tool buttons -->
2839    <div class="mobile-option-f" style="display: none;">
2840      <div style="font-size: 12px; color: #666; margin-bottom: 6px; font-weight: 600;">
2841        <span data-en="I want to tag:" data-fr="Je veux taguer:">Je veux taguer:</span>
2842      </div>
2843      <div style="display: flex; gap: 8px; margin-bottom: 12px;">
2844        <button class="mobile-zone-btn mobile-zone-positive active" id="mobile-zone-positive">
2845          <span data-en="Positive Zone" data-fr="Zone positive">Zone positive</span>
2846        </button>
2847        <button class="mobile-zone-btn mobile-zone-negative" id="mobile-zone-negative">
2848          <span data-en="Negative Zone" data-fr="Zone négative">Zone négative</span>
2849        </button>
2850      </div>
2851
2852      <div style="display: flex; gap: 6px;">
2853        <button class="mobile-tool-btn active" id="mobile-navigate-btn">
2854          <span class="mobile-tool-checkmark">✓</span>
2855          <span data-en="Navigate" data-fr="Naviguer">Naviguer</span>
2856        </button>
2857        <button class="mobile-tool-btn mobile-peinturer" id="mobile-peinturer-btn">
2858          <span class="mobile-paint-hint" data-en="Click here!" data-fr="Cliquez ici !">Cliquez ici !</span>
2859          <span class="mobile-tool-checkmark">✓</span>
2860          <span data-en="Paint" data-fr="Peinturer">Peinturer</span>
2861        </button>
2862        <button class="mobile-tool-btn" id="mobile-erase-btn">
2863          <span class="mobile-tool-checkmark">✓</span>
2864          <span data-en="Erase" data-fr="Effacer">Effacer</span>
2865        </button>
2866      </div>
2867    </div>
2868
2869    <span class="step-label"><span data-en="Select Tool" data-fr="Sélectionnez l'outil">
2869Sélectionnez l'outil</span></span>
2870    <div class="tool-selector-row">
2871      <button class="tool-btn active" id="tool-cursor">
2872        <span class="cursor-icon">
2873          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2874            <path d="M3 3L10.07 19.97L12.58 12.58L19.97 10.07L3 3Z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
2875          </svg>
2876        </span>
2877      </button>
2878      <button class="tool-btn" id="tool-paint" style="position: relative;">
2879        <div class="paint-hint" id="paint-hint-desktop" data-en="Click here!" data-fr="Cliquez ici !">Cliquez ici !</div>
2880        <span data-en="Tag Can" data-fr="Peinturer">Peinturer</span>
2881      </button>
2882      <button class="tool-btn" id="tool-erase">
2883        <span data-en="Eraser" data-fr="Effacer">Effacer</span>
2884      </button>
2885    </div>
2886
2887    <div id="size-controls">
2888      <div class="spray-size-control">
2889        <label class="control-label">
2890          <span id="tool-size-label" data-en="Tool Size" data-fr="Taille de l'outil">
2890Taille de l'outil</span>: <strong><span id="tool-size-value">10</span>m</strong>
2891        </label>
2892        <input type="range" min="2" max="20" value="10" class="slider" id="tool-size-slider" />
2893      </div>
2894    </div>
2895
2896    <span class="step-label"><span data-en="Select Zone Type" data-fr="Sélectionnez le type de zone">Sélectionnez le type de zone</span></span>
2897    <div class="tag-mode-selector">
2898      <button class="tag-mode-btn safe-mode active" id="paint-green">
2899        <span data-en="Positive Zone" data-fr="Zone positive">Zone positive</span>
2900      </button>
2901      <button class="tag-mode-btn danger-mode" id="paint-red">
2902        <span data-en="Negative Zone" data-fr="Zone négative">Zone négative</span>
2903      </button>
2904    </div>
2905
2906    <div class="paint-target-selector">
2907      <label class="toggle-label">
2908        <input type="checkbox" id="paint-buildings" checked />
2909        <span data-en="Paint Buildings" data-fr="Peindre les bâtiments">Peindre les bâtiments</span>
2910      </label>
2911      <label class="toggle-label">
2912        <input type="checkbox" id="paint-ground" checked />
2913        <span data-en="Paint Ground" data-fr="Peindre le sol">Peindre le sol</span>
2914      </label>
2915    </div>
2916
2917    <!-- View Controls in Step 2 - Collapsible -->
2918    <div class="step-label collapsible-header" id="display-options-header" style="margin-top: 15px; cursor: pointer; user-select: none;">
2919      <span data-en="Display Options" data-fr="Options d'affichage">Options d'affichage</span>
2920      <span style="float: right;">▼</span>
2921    </div>
2922    <div class="paint-target-selector" id="display-options-content">
2923      <label class="toggle-label">
2924        <input type="checkbox" id="toggle-buildings" checked />
2925        <span data-en="Show Buildings" data-fr="Afficher les bâtiments">Afficher les bâtiments</span>
2926      </label>
2927      <label class="toggle-label">
2928        <input type="checkbox" id="toggle-satellite" />
2929        <span data-en="Satellite View" data-fr="Vue satellite">Vue satellite</span>
2930      </label>
2931    </div>
2932    
2933    <button class="clear-btn" id="clear-all-paint-btn"><span data-en="Clear All" data-fr="Tout effacer">Tout effacer</span></button>
2934
2935    <button class="next-btn" id="next-to-tag-clusters-btn">
2936      <span data-en="Next: Label Tagged Areas →" data-fr="Suivant : Étiqueter les zones →">Suivant : Étiqueter les zones →</span>
2937    </button>
2938
2939    <button class="back-btn" id="back-from-step2">
2940      <span data-en="← Back to Neighbourhood Selection" data-fr="← Retour à la sélection de quartier">← Retour à la sélection de quartier</span>
2941    </button>
2942  </div>
2943  
2944  <!-- STEP 3: Tag Clusters (One at a time) -->
2945  <div class="step-section hidden" id="step3-tag-clusters">
2946    <div class="step-header">
2947      <span data-en="Step 3: Label area" data-fr="Étape 3: Étiqueter la zone">Étape 3: Étiqueter la zone</span> <span id="cluster-number">1</span> <span data-en="of" data-fr="de">de</span> <span id="cluster-total">5</span>
2948    </div>
2949    
2950    <div class="cluster-type-badge" id="cluster-type-badge">
2951      Disliked Area
2952    </div>
2953    
2954    <div class="cluster-size" id="cluster-size">
2955      97 voxels
2956    </div>
2957
2958    <div class="split-section">
2959      <div class="split-section-header" id="split-section-toggle">
2960        <div style="display: flex; align-items: center;">
2961          <span data-en="Split this area into 2 areas" data-fr="Diviser cette zone en 2 zones">Diviser cette zone en 2 zones</span>
2962          <span class="split-info-icon" id="split-info-icon" title="Plus d'informations">i</span>
2963        </div>
2964        <span class="split-section-arrow">▼</span>
2965      </div>
2966      <div class="split-section-content" id="split-section-content">
2967        <button class="split-cluster-btn" id="split-cluster-btn">
2968          <span data-en="Split Automatically" data-fr="Diviser automatiquement">Diviser automatiquement</span>
2969        </button>
2970      </div>
2971    </div>
2972
2973    <!-- Split success notification (hidden by default) -->
2974    <div class="split-success-msg hidden" id="split-success-msg" style="margin-bottom: 15px; padding: 12px; background: #4caf50; color: white; border-radius: 6px; font-weight: 600; text-align: center; animation: fadeInOut 2s;">
2975      <span data-en="✓ Automatically split!" data-fr="✓ Division automatique effectuée !">✓ Division automatique effectuée !</span>
2976    </div>
2977
2978    <div id="tag-instruction-text" style="margin-bottom: 20px; font-size: 14px; color: #333; font-weight: 400;">
2979      <span data-en="Select all applicable attributes:" data-fr="Sélectionnez tous les attributs applicables:">Sélectionnez tous les attributs applicables:</span>
2980    </div>
2981
2982    <div id="tag-categories-container"></div>
2983    
2984    <textarea
2985      class="cluster-comment-full"
2986      id="current-cluster-comment"
2987      data-placeholder-en="Additional comments..."
2988      data-placeholder-fr="Commentaires additionnels..."
2989      placeholder="Commentaires additionnels..."></textarea>
2990
2991    <div class="cluster-nav-buttons">
2992      <button class="nav-cluster-btn" id="prev-cluster-btn" disabled>
2993        <span data-en="← Previous" data-fr="← Précédent">← Précédent</span>
2994      </button>
2995      <button class="nav-cluster-btn primary" id="next-cluster-btn">
2996        <span data-en="Next →" data-fr="Suivant →">Suivant →</span>
2997      </button>
2998    </div>
2999
3000    <button class="back-btn" id="back-from-step3">
3001      <span data-en="← Back to Painting" data-fr="← Retour à la peinture">← Retour à la peinture</span>
3002    </button>
3003  </div>
3004  
3005  <!-- STEP 4: Submit -->
3006  <div class="step-section hidden" id="step4-submit">
3007    <div class="step-header"><span data-en="Step 4: Submit Your Tags" data-fr="Étape 4 : Soumettre vos étiquettes">Étape 4 : Soumettre vos étiquettes</span></div>
3008    
3009    <div class="submit-info">
3010      <span data-en="Your tagged areas and labels will be saved for safety analysis and urban planning." data-fr="Vos zones étiquetées seront enregistrées pour l'analyse de la sécurité et la planification urbaine.">Vos zones étiquetées seront enregistrées pour l'analyse de la sécurité et la planification urbaine.</span>
3011    </div>
3012    
3013    <button class="back-btn" id="back-from-step4" style="margin-bottom: 15px;">
3014      <span data-en="← Back to Tags" data-fr="← Retour aux étiquettes">← Retour aux étiquettes</span>
3015    </button>
3016    
3017    <button class="submit-btn" id="submit-btn">
3018      <span data-en="Submit" data-fr="Soumettre">Soumettre</span>
3019    </button>
3020  </div>
3021  
3022  <button class="reset-btn hidden" id="reset-btn"><span data-en="Reset & Start Over" data-fr="Réinitialiser">Réinitialiser</span></button>
3023</div>
3024
3025</div>
3026
3027<!-- On-Screen Map Navigation - Visible in Step 1 & 2 -->
3028<div class="map-nav-controls" id="map-nav-controls">
3029  <div style="margin-bottom: 12px;">
3030    <h3 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600;"><span data-en="View Controls" data-fr="Contrôle de vue">Contrôle de vue</span></h3>
3031    <div class="control-group">
3032      <label class="control-label" style="font-size: 12px;">
3033        <span data-en="Viewing Angle:" data-fr="Angle de vue :">Angle de vue :</span> <span id="pitch-value">45</span>°
3034      </label>
3035      <input type="range" min="0" max="85" value="45" class="slider" id="pitch-slider" style="width: 100%;" />
3036    </div>
3037  </div>
3038
3039  <div class="map-nav-grid">
3040    <div></div>
3041    <button class="map-nav-btn" id="map-nav-up" title="Pan Up">↑</button>
3042    <button class="map-nav-btn rotate" id="map-rotate-left" title="Rotate Left">↶</button>
3043    <button class="map-nav-btn" id="map-nav-left" title="Pan Left">←</button>
3044    <button class="map-nav-btn zoom" id="map-zoom-in" title="Zoom In">+</button>
3045    <button class="map-nav-btn" id="map-nav-right" title="Pan Right">→</button>
3046    <button class="map-nav-btn rotate" id="map-rotate-right" title="Rotate Right">↷</button>
3047    <button class="map-nav-btn" id="map-nav-down" title="Pan Down">↓</button>
3048    <button class="map-nav-btn zoom" id="map-zoom-out" title="Zoom Out">−</button>
3049  </div>
3050  <div class="map-nav-label"><span data-en="Navigate, rotate & zoom" data-fr="Naviguer, pivoter et zoomer">Naviguer, pivoter et zoomer</span></div>
3051</div>
3052
3053<script>
3054mapboxgl.accessToken = 'pk.eyJ1IjoiZXZlbHluZWJyaWUiLCJhIjoiY2themE5OGF2MDdxazJybG9oNzUyaXoxNSJ9.njPe2lcTp82DKjDeGkHaQA';
3055
3056const MONTREAL_CENTER = [-73.5673, 45.5017];
3057const MONTREAL_GRID_ROTATION = -33;
3058const RADIUS_KM = 0.75; // 750 meters
3059
3060let map, selectedPoint = null, selectionMode = 'click', centerMarker = null;
3061let allBuildingsData = null, dataLoaded = false;
3062let voxelsLoaded = false;
3063let currentStep = 1; // Track current step (1: select, 2: paint, 3: tag)
3064let currentPaintColor = 'liked'; // 'disliked' or 'liked' - DEFAULT TO LIKED
3065let currentTool = 'cursor';
3066let clickMapMode = false; // Only true when 'Click Map' button is clicked // Start with cursor for free exploration // 'cursor', 'paint', 'erase'
3067let toolSize = 10; // DEFAULT 10M
3068let paintBuildings = true;
3069let paintGround = true;
3070let groundPaintMarkers = [];
3071let groundPaintIdCounter = 0; // Unique ID counter for ground paint markers
3072let dislikedVoxels = new Set();
3073let likedVoxels = new Set();
3074let allVoxelFeatures = [];
3075let voxelSpatialIndex = null;
3076let isDragging = false;
3077let clusterLabels = {};
3078let clusterMarkers = [];
3079let splitDividerLines = []; // Store IDs of split divider line layers
3080let selectedClusterId = null;
3081let lastPaintTime = 0;
3082let paintQueue = [];
3083let updateScheduled = false;
3084let currentClusterIndex = 0;
3085let allClusters = [];
3086let selectedPaintHeight = 0; // Default to ground level
3087let isMapMoving = false;
3088let mouseMoveScheduled = false;
3089let lastMouseEvent = null;
3090let paintedOverlays = []; // Store painted overlay features
3091let overlayIdCounter = 0;
3092let paintedVoxelFeatures = []; // Single array of all painted voxel overlays
3093let paintedVoxelIndex = new Map(); // Map voxelIndex -> array index for fast lookup
3094let paintUpdatePending = false;
3095
3096let voxelOriginalHeights = new Map(); // Store original heights: voxelIndex -> {base, height}
3097
3098
3099
3100// Custom Alert System - Fix 6
3101function showCustomAlert(title, message) {
3102  const overlay = document.createElement('div');
3103  overlay.className = 'custom-alert-overlay';
3104  overlay.innerHTML = `
3105    <div class="custom-alert">
3106      <h3>${title}</h3>
3107      <div class="custom-alert-content">${message}</div>
3108      <button class="custom-alert-button" onclick="this.closest('.custom-alert-overlay').remove()">
3109        OK
3110      </button>
3111    </div>
3112  `;
3113  document.body.appendChild(overlay);
3114  overlay.addEventListener('click', (e) => {
3115    if (e.target === overlay) overlay.remove();
3116  });
3117}
3118
3119function showSubmissionErrorAlert(title, message) {
3120  const lang = (typeof currentLang !== 'undefined') ? currentLang : 'fr';
3121  const overlay = document.createElement('div');
3122  overlay.className = 'custom-alert-overlay';
3123  overlay.innerHTML = `
3124    <div class="custom-alert">
3125      <h3>${title}</h3>
3126      <div class="custom-alert-content">${message}</div>
3127
3128      <div style="margin-top: 24px; padding-top: 24px; border-top: 1px solid #e8e8e8;">
3129        <label style="display: block; font-weight: 600; margin-bottom: 8px; color: #333;">
3130          ${lang === 'fr' ? 'Commentaires ou suggestions?' : 'Any comments or suggestions?'}
3131        </label>
3132        <textarea id="error-feedback"
3133          style="width: 100%; min-height: 80px; padding: 10px; border: 2px solid #ddd; border-radius: 6px; font-family: inherit; font-size: 14px; resize: vertical;"
3134          placeholder="${lang === 'fr' ? 'Écrivez ici...' : 'Write here...'}"></textarea>
3135        <button id="submit-feedback-btn" class="custom-alert-button"
3136          style="margin-top: 10px; background: #34a853; width: auto; padding: 10px 24px; font-size: 14px;">
3137          ${lang === 'fr' ? 'Envoyer le commentaire' : 'Submit Feedback'}
3138        </button>
3139      </div>
3140
3141      <button class="custom-alert-button" id="error-finish-btn" style="margin-top: 20px;">
3142        ${lang === 'fr' ? 'Terminer' : 'Finish'}
3143      </button>
3144    </div>
3145  `;
3146  document.body.appendChild(overlay);
3147
3148  // Handle feedback submission
3149  overlay.querySelector('#submit-feedback-btn').addEventListener('click', async () => {
3150    const feedbackText = overlay.querySelector('#error-feedback').value.trim();
3151    if (!feedbackText) {
3152      alert(lang === 'fr' ? 'Veuillez entrer un commentaire.' : 'Please enter a comment.');
3153      return;
3154    }
3155
3156    const btn = overlay.querySelector('#submit-feedback-btn');
3157    btn.disabled = true;
3158    btn.textContent = lang === 'fr' ? 'Envoi...' : 'Sending...';
3159
3160    try {
3161      // Send feedback to backend
3162      await fetch('https://tag-ta-ville-production.up.railway.app/
3162api/feedback', {
3163        method: 'POST',
3164        headers: { 'Content-Type': 'application/json' },
3165        body: JSON.stringify({ feedback: feedbackText, type: 'submission_error' })
3166      });
3167
3168      btn.textContent = lang === 'fr' ? '✓ Envoyé' : '✓ Sent';
3169      btn.style.background = '#1e7e34';
3170      overlay.querySelector('#error-feedback').value = '';
3171    } catch (err) {
3172      console.error('Feedback submission error:', err);
3173      btn.textContent = lang === 'fr' ? 'Erreur - Réessayer' : 'Error - Retry';
3174      btn.disabled = false;
3175    }
3176  });
3177
3178  // Handle finish button - hard reset
3179  overlay.querySelector('#error-finish-btn').addEventListener('click', () => {
3180    overlay.remove();
3181    // Perform hard reset
3182    location.reload();
3183  });
3184
3185  overlay.addEventListener('click', (e) => {
3186    if (e.target === overlay) {
3187      overlay.remove();
3188      location.reload();
3189    }
3190  });
3191}
3192
3193function showCustomConfirm(title, message, onConfirm) {
3194  const overlay = document.createElement('div');
3195  overlay.className = 'custom-alert-overlay';
3196  const lang = (typeof currentLang !== 'undefined') ? currentLang : 'fr';
3197  overlay.innerHTML = `
3198    <div class="custom-alert">
3199      <h3>${title}</h3>
3200      <div class="custom-alert-content">${message}</div>
3201      <div style="display: flex; gap: 10px; margin-top: 20px;">
3202        <button class="custom-alert-button" style="background: #6c757d; flex: 1;" id="confirm-cancel">
3203          ${lang === 'fr' ? 'Annuler' : 'Cancel'}
3204        </button>
3205        <button class="custom-alert-button" style="background: #1a73e8; flex: 1;" id="confirm-ok">
3206          OK
3207        </button>
3208      </div>
3209    </div>
3210  `;
3211  document.body.appendChild(overlay);
3212
3213  overlay.querySelector('#confirm-ok').addEventListener('click', () => {
3214    overlay.remove();
3215    onConfirm(true);
3216  });
3217
3218  overlay.querySelector('#confirm-cancel').addEventListener('click', () => {
3219    overlay.remove();
3220    onConfirm(false);
3221  });
3222  
3223  overlay.addEventListener('click', (e) => {
3224    if (e.target === overlay) {
3225      overlay.remove();
3226      onConfirm(false);
3227    }
3228  });
3229}
3230
3231// Tag options for Step 3 — aligned with French version
3232const TAG_OPTIONS = {
3233  "Safety": {
3234    positive: [
3235      "Lighting & visibility",
3236      "Road signage",
3237      "Pedestrian & cycling amenities",
3238      "Motor vehicle speed"
3239    ],
3240    negative: [
3241      "Lighting & visibility",
3242      "Road signage",
3243      "Pedestrian & cycling amenities",
3244      "Motor vehicle speed"
3245    ]
3246  },
3247
3248  "Cleanliness & Maintenance": {
3249    positive: [
3250      "Cleanliness of public spaces",
3251      "Condition of roadways & sidewalks",
3252      "Condition of street furniture & building facades",
3253      "Vegetation management"
3254    ],
3255    negative: [
3256      "Cleanliness of public spaces",
3257      "Condition of roadways & sidewalks",
3258      "Condition of street furniture & building facades",
3259      "Vegetation management"
3260    ]
3261  },
3262
3263  "Atmosphere": {
3264    positive: [
3265      "Presence of green spaces",
3266      "Noise level",
3267      "Presence of art & culture",
3268      "Family suitability"
3269    ],
3270    negative: [
3271      "Lack of green spaces",
3272      "Noise level",
3273      "Absence of art & culture",
3274      "Family unsuitability"
3275    ]
3276  },
3277
3278  "Access & Services": {
3279    positive: [
3280      "Access to public transit",
3281      "Proximity to essential services",
3282      "Universal accessibility"
3283    ],
3284    negative: [
3285      "No access to public transit",
3286      "No proximity to essential services",
3287      "No universal accessibility"
3288    ]
3289  },
3290
3291  "Seasonal Conditions": {
3292    positive: [
3293      "SPRING No meltwater accumulation",
3294      "SUMMER Presence of shade",
3295      "SUMMER Access to water for cooling",
3296      "FALL Fallen leaves cleared",
3297      "WINTER Snow and ice cleared from public space",
3298      "WINTER Non-slippery surfaces"
3299    ],
3300    negative: [
3301      "SPRING Meltwater accumulation",
3302      "SUMMER Lack of shade",
3303      "SUMMER No accessible water points",
3304      "FALL Accumulation of fallen leaves",
3305      "WINTER Snow or ice obstructing public space",
3306      "WINTER Slippery surfaces"
3307    ]
3308  }
3309};
3310
3311
3312const TAG_OPTIONS_FR = {
3313  "Sécurité": {
3314    positive: [
3315      "Éclairage et visibilité",
3316      "Signalisation routière",
3317      "Aménagements pour piétons et cyclistes",
3318      "Vitesse des véhicules automobiles"
3319    ],
3320    negative: [
3321      "Éclairage et visibilité",
3322      "Signalisation routière",
3323      "Aménagements pour piétons et cyclistes",
3324      "Vitesse des véhicules automobiles"
3325    ]
3326  },
3327  "Propreté et entretien": {
3328    positive: [
3329      "Propreté des espaces publics",
3330      "État de la chaussée et des trottoirs",
3331      "État du mobilier urbain et des façades",
3332      "Gestion de la végétation"
3333    ],
3334    negative: [
3335      "Propreté des espaces publics",
3336      "État de la chaussée et des trottoirs",
3337      "État du mobilier urbain et des façades",
3338      "Gestion de la végétation"
3339    ]
3340  },
3341  "Atmosphère": {
3342    positive: [
3343      "Présence d'espaces verts",
3344      "Niveau sonore",
3345      "Présence d'art et de culture",
3346      "Adapté aux familles"
3347    ],
3348    negative: [
3349      "Absence d'espaces verts",
3350      "Niveau sonore",
3351      "Absence d'art et de culture",
3352      "Inadapté aux familles"
3353    ]
3354  },
3355  "Accès et services": {
3356    positive: [
3357      "Accès au transport collectif",
3358      "Proximité des services essentiels",
3359      "Accessibilité universelle"
3360    ],
3361    negative: [
3362      "Pas d'accès au transport collectif",
3363      "Pas de proximité des services essentiels",
3364      "Pas d'accessibilité universelle"
3365    ]
3366  },
3367  "Conditions saisonnières": {
3368    positive: [
3369      "PRINTEMPS Pas d'accumulation d’eau de fonte",
3370      "ÉTÉ Présence d'ombre",
3371      "ÉTÉ Accès à l'eau pour se rafraîchir",
3372      "AUTOMNE Feuilles mortes ramassées",
3373      "HIVER Neige et glace dégagées de l'espace public",
3374      "HIVER Surfaces non-glissantes"
3375    ],
3376    negative: [
3377      "PRINTEMPS Accumulation d’eau de fonte",
3378      "ÉTÉ Absence d'ombre",
3379      "ÉTÉ Absence de points d'eau accessibles",
3380      "AUTOMNE Accumulation de feuilles mortes",
3381      "HIVER Neige ou glace obstruant l'espace public",
3382      "HIVER Surfaces glissantes"
3383    ]
3384  }
3385};
3386
3387
3388// Performance Optimization - Fix 2
3389let updateBatchTimeout = null;
3390let lastUpdateHash = '';
3391let eraseUpdatePending = false;
3392
3393function batchUpdateVoxels() {
3394  if (updateBatchTimeout) return;
3395  updateBatchTimeout = setTimeout(() => {
3396    updateVoxelColors();
3397    updatePaintedVoxelsSource();
3398    updateBatchTimeout = null;
3399  }, 16); // ~60fps
3400}
3401
3402function batchEraseUpdate() {
3403  if (eraseUpdatePending) return;
3404  eraseUpdatePending = true;
3405  requestAnimationFrame(() => {
3406    updateVoxelColors();
3407    updatePaintedVoxelsSource();
3408    eraseUpdatePending = false;
3409  });
3410}
3411
3412// Welcome popup handlers
3413
3414// Wait for DOM to be fully loaded before adding event listeners
3415document.addEventListener('DOMContentLoaded', () => {
3416
3417// Show welcome overlay on page load (password protection removed)
3418document.getElementById('welcome-overlay').style.display = 'flex';
3419
3420// Language system
3421let currentLang = 'fr'; // Default French
3422
3423function switchLanguage(lang) {
3424  currentLang = lang;
3425  document.querySelectorAll('[data-en]').forEach(el => {
3426    // Use innerHTML for elements that contain HTML tags like <strong>
3427    if (el.getAttribute('data-' + lang).includes('<')) {
3428      el.innerHTML = el.getAttribute('data-' + lang);
3429    } else {
3430      el.textContent = el.getAttribute('data-' + lang);
3431    }
3432  });
3433
3434  // Update placeholders
3435  if (lang === 'fr') {
3436    document.getElementById('address-input').placeholder = 'Entrez une adresse à Montréal';
3437    // document.getElementById('password-input').placeholder = 'Mot de passe';
3438  } else {
3439    document.getElementById('address-input').placeholder = 'Enter an address in Montreal';
3440    // document.getElementById('password-input').placeholder = 'Password';
3441  }
3442
3443  // Update additional comments placeholder
3444  const commentTextarea = document.getElementById('current-cluster-comment');
3445  if (commentTextarea) {
3446    commentTextarea.placeholder = lang === 'fr'
3447      ? 'Commentaires additionnels...'
3448      : 'Additional comments...';
3449  }
3450
3451  // Re-render tag categories if in Step 3
3452  if (currentStep === 3 && allClusters && allClusters.length > 0 && typeof currentClusterIndex !== 'undefined') {
3453    const cluster = allClusters[currentClusterIndex];
3454    if (cluster) {
3455      renderTagCategories(cluster);
3456    }
3457  }
3458
3459  // Update info tooltip if it's visible
3460  if (typeof updateInfoTooltip === 'function') {
3461    updateInfoTooltip();
3462  }
3463}
3464
3465// Tutorial and Progress Tracker System
3466let currentTutorialStep = 0;
3467let tutorialCompleted = false;
3468
3469const tutorialSteps = [
3470  {
3471    step: 1,
3472    iconFr: '<img src="assets/pin_3d_glossy_blue.svg" style="width: 80px; height: 120px; object-fit: contain;">',
3473    iconEn: '<img src="assets/pin_3d_glossy_blue.svg" style="width: 80px; height: 120px; object-fit: contain;">',
3474    titleFr: 'Étape 1: Sélectionnez un emplacement',
3475    titleEn: 'Step 1: Select a Location',
3476    descriptionFr: 'Cliquez sur la carte pour choisir le centre de votre quartier.<br>Un cercle de 750 mètres apparaîtra autour de ce point.',
3477    descriptionEn: 'Click on the map to choose the center of your neighbourhood.<br>A 750 meter circle will appear around this point.'
3478  },
3479  {
3480    step: 2,
3481    iconFr: '',
3482    iconEn: '',
3483    titleFr: 'Étape 2: Taguez les zones de votre choix',
3484    titleEn: 'Step 2: Tag Areas of Your Choice',
3485    descriptionFr: 'Sélectionnez "Peinturer" pour taguer certaines zone
3485s dans votre quartier.<br><br>Indiquez en <strong style="color: #34a853;">vert</strong> celles qui sont positives, c\'est-à-dire qui <strong>améliorent votre qualité de vie</strong>.<br><br>Indiquez en <strong style="color: #ea4335;">rouge</strong> celles qui sont négatives, c\'est-à-dire qui <strong>diminuent votre qualité de vie</strong>.',
3486    descriptionEn: 'Use the paint tool to tag certain areas in your neighbourhood.<br><br>Indicate in <strong style="color: #34a853;">green</strong> those that are positive, i.e., that <strong>improve your quality of life</strong>.<br><br>Indicate in <strong style="color: #ea4335;">red</strong> those that are negative, i.e., that <strong>diminish your quality of life</strong>.'
3487  },
3488  {
3489    step: 3,
3490    iconFr: '',
3491    iconEn: '',
3492    titleFr: 'Étape 3: Étiquetez les zones',
3493    titleEn: 'Step 3: Label Areas',
3494    descriptionFr: 'Sélectionnez les <strong>attributs</strong> qui caractérisent chacune des zones.<br><br>Vous pouvez également inclure des <strong>commentaires individualisés</strong>.',
3495    descriptionEn: 'Select the <strong>attributes</strong> that characterize each of the areas.<br><br>You can also include <strong>individualized comments</strong>.'
3496  },
3497  {
3498    step: 4,
3499    iconFr: '',
3500    iconEn: '',
3501    titleFr: 'Étape 4: Confirmez et soumettez',
3502    titleEn: 'Step 4: Confirm and Submit',
3503    descriptionFr: 'En cliquant sur "soumettre", vous transmettrez votre évaluation.<br>Un numéro de confirmation vous sera donné.',
3504    descriptionEn: 'By clicking "submit", you will transmit your evaluation.<br>A confirmation number will be given to you.'
3505  }
3506];
3507
3508function showTutorial(stepNumber) {
3509  console.log(`showTutorial called with step ${stepNumber}`);
3510  const overlay = document.getElementById('tutorial-overlay');
3511  const content = document.getElementById('tutorial-content');
3512
3513  if (stepNumber < 0 || stepNumber >= tutorialSteps.length) {
3514    console.log('Invalid step number, returning');
3515    return;
3516  }
3517
3518  const step = tutorialSteps[stepNumber];
3519  const isLastStep = stepNumber === tutorialSteps.length - 1;
3520
3521  const icon = currentLang === 'fr' ? step.iconFr : step.iconEn;
3522  const title = currentLang === 'fr' ? step.titleFr : step.titleEn;
3523  const description = currentLang === 'fr' ? step.descriptionFr : step.descriptionEn;
3524
3525  console.log(`Tutorial step ${stepNumber}: ${title}`);
3526
3527  content.innerHTML = `
3528    <div class="tutorial-icon">${icon}</div>
3529    <div class="tutorial-title">${title}</div>
3530    <div class="tutorial-description">${description}</div>
3531
3532    <div class="tutorial-progress">
3533      ${tutorialSteps.map((_, i) => `
3534        <div class="tutorial-progress-dot ${i === stepNumber ? 'active' : ''}"></div>
3535      `).join('')}
3536    </div>
3537
3538    <div class="tutorial-actions">
3539      ${stepNumber === 3 ? `
3540        <button class="tutorial-btn-back" onclick="backToLabeling()" style="background: white; color: #666; border: 2px solid #ddd; padding: 12px 24px; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; margin-right: 12px;">
3541          ${currentLang === 'fr' ? '← Retour' : '← Back'}
3542        </button>
3543      ` : ''}
3544      <button class="tutorial-btn-next" onclick="nextTutorial()">
3545        ${stepNumber === 3 ? (currentLang === 'fr' ? 'Soumettre' : 'Submit') :
3546          isLastStep ? (currentLang === 'fr' ? 'Commencer' : 'Start') :
3547          (currentLang === 'fr' ? 'Suivant →' : 'Next →')}
3548      </button>
3549    </div>
3550  `;
3551
3552  console.log('Removing hidden class from tutorial overlay');
3553  overlay.classList.remove('hidden');
3554  currentTutorialStep = stepNumber;
3555  updateZoomPrompt();
3556}
3557
3558function nextTutorial() {
3559  closeTutorial();
3560
3561  // Step 3 tutorial (submit confirmation) - show step 4 submit page
3562  if (currentTutorialStep === 3) {
3563    document.getElementById('step3-tag-clusters').classList.add('hidden');
3564    document.getElementById('step4-submit').classList.remove('hidden');
3565
3566    // Show main panel for step 4
3567    const mainPanel = document.querySelector('.main-panel');
3568    if (mainPanel) mainPanel.style.display = 'block';
3569
3570    currentStep = 4;
3571    updateProgress(4);
3572    updateInfoTooltip();
3573  }
3574
3575  if (currentTutorialStep === tutorialSteps.length - 1) {
3576    tutorialCompleted = true;
3577  }
3578}
3579
3580function backToLabeling() {
3581  closeTutorial();
3582
3583  // Show main panel and step 3 UI
3584  const mainPanel = document.querySelector('.main-panel');
3585  if (mainPanel) mainPanel.style.display = 'block';
3586
3587  document.getElementById('step3-tag-clusters').classList.remove('hidden');
3588
3589  // Restore to last cluster
3590  if (allClusters.length > 0) {
3591    currentClusterIndex = allClusters.length - 1;
3592    showCluster(currentClusterIndex);
3593  }
3594
3595  // Reset progress to step 3
3596  updateProgress(3);
3597}
3598
3599function closeTutorial() {
3600  document.getElementById('tutorial-overlay').classList.add('hidden');
3601  updateZoomPrompt();
3602
3603  // Show paint hint after tutorial closes (only in step 2)
3604  if (currentStep === 2) {
3605    setTimeout(() => {
3606      const isMobile = window.innerWidth <= 768;
3607      const hint = document.getElementById(isMobile ? 'paint-hint-mobile' : 'paint-hint-desktop');
3608      if (hint) hint.style.display = 'block';
3609    }, 500);
3610  }
3611}
3612
3613function updateProgress(stepNumber) {
3614  // Update progress tracker (only 3 steps shown)
3615  for (let i = 1; i <= 3; i++) {
3616    const stepEl = document.getElementById(`progress-step-${i}`);
3617    if (stepEl) {
3618      stepEl.classList.remove('active', 'completed');
3619
3620      if (i < stepNumber) {
3621        stepEl.classList.add('completed');
3622      } else if (i === stepNumber) {
3623        stepEl.classList.add('active');
3624      }
3625    }
3626  }
3627}
3628
3629// Info Button System
3630const infoButton = document.getElementById('info-button');
3631const infoTooltip = document.getElementById('info-tooltip');
3632const infoTooltipClose = document.getElementById('info-tooltip-close');
3633const infoTooltipHeader = document.getElementById('info-tooltip-header');
3634const infoTooltipContent = document.getElementById('info-tooltip-content');
3635
3636const stepInstructions = {
3637  1: {
3638    titleFr: 'Étape 1 : Sélectionnez votre quartier',
3639    titleEn: 'Step 1: Select Your Neighbourhood',
3640    contentFr: '<p>Cliquez sur la carte pour choisir le centre de votre quartier, ou entrez une adresse dans la barre de recherche. Un cercle de 750 mètres apparaîtra autour de votre point. Cliquez ensuite sur "Visualiser mon quartier" pour continuer.</p>',
3641    contentEn: '<p>Click on the map to choose your neighbourhood center, or enter an address in the search bar. A 750 meter circle will appear around your point. Then click "Visualize My Neighbourhood" to continue.</p>'
3642  },
3643  2: {
3644    titleFr: 'Étape 2 : Taguer les zones',
3645    titleEn: 'Step 2: Tag Areas',
3646    contentFr: '<p>Cliquez sur "Peinturer" pour activer l\'outil de peinture. Sélectionnez <strong style="color: #34a853;">Zone positive</strong> pour les endroits qui améliorent votre qualité de vie, ou <strong style="color: #ea4335;">Zone négative</strong> pour ceux qui la diminuent. Ajustez la taille de l\'outil selon vos besoins, ou utilisez l\'effaceur pour corriger vos tags.</p>',
3647    contentEn: '<p>Click "Paint" to activate the paint tool. Select <strong style="color: #34a853;">Positive Zone</strong> for places that improve your quality of life, or <strong style="color: #ea4335;">Negative Zone</strong> for those that diminish it. Adjust the tool size as needed, or use the eraser to correct your tags.</p>'
3648  },
3649  3: {
3650    titleFr: 'Étape 3 : Étiqueter la zone',
3651    titleEn: 'Step 3: Label Area',
3652    contentFr: '<p>Sélectionnez les attributs qui caractérisent cette zone. Plusieurs attributs peuvent être sélectionnés. Ajoutez des commentaires additionnels si désiré, ou utilisez "Diviser" si la zone couvre plusieurs endroits distincts. Cliquez sur "Suivant" pour passer à la zone suivante.</p>',
3653    contentEn: '<p>Select the attributes that characterize this area. Multiple attributes can be selected. Add additional comments if desired, or use "Split" if the area covers multiple distinct locations. Click "Next" to move to the next area.</p>'
3654  },
3655  4: {
3656    titleFr: 'Étape 4 : Soumettre vos étiquettes',
3657    titleEn: 'Step 4: Submit Your Tags',
3658    contentFr: '<p>Vérifiez votre travail sur la carte. Cliquez sur "Soumettre" pour envoyer vos données. Un numéro de confirmation vous sera fourni et vos données seront utilisées pour la recherche urbaine.</p>',
3659    contentEn: '<p>Review your work on the map. Click "Submit" to send your data. A confirmation number will be provided and your data will be used for urban research.</p>'
3660  }
3661};
3662
3663function updateInfoTooltip() {
3664  const step = currentStep;
3665  const instructions = stepInstructions[step];
3666  if (instructions) {
3667    infoTooltipHeader.textContent = currentLang === 'fr' ? instructions.titleFr : instructions.titleEn;
3668    infoTooltipContent.innerHTML = currentLang === 'fr' ? instructions.contentFr : instructions.contentEn;
3669  }
3670}
3671
3672infoButton.addEventListener('click', () => {
3673  updateInfoTooltip();
3674  infoTooltip.classList.toggle('visible');
3675});
3676
3677infoTooltipClose.addEventListener('click', () => {
3678  infoTooltip.classList.remove('visible');
3679});
3680
3681// Close tooltip when clicking outside
3682document.addEventListener('click', (e) => {
3683  if (!infoButton.contains(e.target) && !infoTooltip.contains(e.target)) {
3684    infoTooltip.classList.remove('visible');
3685  }
3686});
3687
3688// Make functions global so they can be called from onclick
3689window.showTutorial = showTutorial;
3690window.nextTutorial = nextTutorial;
3691window.closeTutorial = closeTutorial;
3692window.backToLabeling = backToLabeling;
3693
3694
3695// Password popup language toggle
3696document.getElementById('password-lang-toggle').addEventListener('click', function() {
3697  const toggle = this;
3698  const slider = toggle.querySelector('.lang-toggle-slider');
3699  
3700  if (toggle.classList.contains('en')) {
3701    toggle.classList.remove('en');
3702    slider.textContent = 'FR';
3703    switchLanguage('fr');
3704  } else {
3705    toggle.classList.add('en');
3706    slider.textContent = 'EN';
3707    switchLanguage('en');
3708  }
3709});
3710
3711// Welcome popup language toggle
3712document.getElementById('welcome-lang-toggle').addEventListener('click', function() {
3713  const toggle = this;
3714  const slider = toggle.querySelector('.lang-toggle-slider');
3715  
3716  if (toggle.classList.contains('en')) {
3717    // Switch to French
3718    toggle.classList.remove('en');
3719    slider.textContent = 'FR';
3720    switchLanguage('fr');
3721  } else {
3722    // Switch to English
3723    toggle.classList.add('en');
3724    slider.textContent = 'EN';
3725    switchLanguage('en');
3726  }
3727});
3728
3729// Language toggle button
3730document.getElementById('lang-toggle').addEventListener('click', () => {
3731  currentLang = currentLang === 'fr' ? 'en' : 'fr';
3732  switchLanguage(currentLang);
3733});
3734
3735// Password protection - DISABLED
3736// const CORRECT_PASSWORD = 'ojiW6TK3rP*LiEf';
3737//
3738// document.getElementById('password-submit').addEventListener('click', () => {
3739//   const input = document.getElementById('password-input').value;
3740//   if (input === CORRECT_PASSWORD) {
3741//     document.getElementById('password-overlay').style.display = 'none';
3742//     document.getElementById('welcome-overlay').style.display = 'flex';
3743//   } else {
3744//     document.getElementById('password-error').style.display = 'block';
3745//   }
3746// });
3747//
3748// // Allow Enter key to submit password
3749// document.getElementById('password-input').addEventListener('keypress', (e) => {
3750//   if (e.key === 'Enter') {
3751//     document.getElementById('password-submit').click();
3752//   }
3753// });
3754
3755// Data collapsible toggle
3756document.getElementById('data-collapsible-toggle').addEventListener('click', () => {
3757  const header = document.getElementById('data-collapsible-toggle');
3758  const content = document.getElementById('data-collapsible-content');
3759
3760  header.classList.toggle('open');
3761  content.classList.toggle('open');
3762});
3763
3764document.getElementById('start-btn').addEventListener('click', () => {
3765  document.getElementById('welcome-overlay').style.display = 'none';
3766  // Show map navigation controls
3767  document.getElementById('map-nav-controls').classList.add('active');
3768
3769  // Show progress tracker
3770  document.getElementById('progress-tracker').style.display = 'block';
3771
3772  // Show first tutorial after a brief delay to ensure welcome overlay is hidden
3773  setTimeout(() => {
3774    console.log('Showing tutorial step 1');
3775    updateProgress(1);
3776    showTutorial(0);
3777  }, 300);
3778});
3779
3780document.getElementById('more-info-btn').addEventListener('click', () => {
3781  // Show research info popup
3782  const title = currentLang === 'fr' ? 'À propos de ce projet' : 'About this project';
3783  const message = currentLang === 'fr'
3784    ? 'Ce projet est dirigé par Evelyne Brie, professeure adjointe en science politique affiliée à IVADO à l\'Université de Montréal. Vous pouvez la contacter par courriel au <a href="mailto:[email protected]" style="color: #4285f4;">[email protected]</a>.<br><br>Ses recherches portent sur la cohésion sociale, les inégalités structurelles et la prise de décision en matière de politique urbaine. Tag ta Ville fait partie de ce travail et utilise la cartographie participative pour mieux comprendre comment les gens vivent la sécurité et le confort dans leurs environnements urbains quotidiens.<br><br>Ce projet a été autorisé par le bureau d\'éthique de la recherche de l\'Université de Montréal (#2026-8007).'
3785    : 'This project is directed by Evelyne Brie, Assistant Professor in Political Science affiliated with IVADO at the University of Montreal. You can reach her by email at <a href="mailto:[email protected]" style="color: #4285f4;">[email protected]</a>.<br><br>Her research focuses on social cohesion, inequalities, and decision-making in urban policy. Tag ta Ville is part of this work, using participatory mapping to better understand how people experience safety and comfort in their everyday urban environments.<br><br>This project has been approved by the University of Montreal\'s research ethics board (#2026-8007).';
3786  showCustomAlert(title, message);
3787});
3788
3789// Confirm button - load neighbourhood
3790document.getElementById('confirm-btn').addEventListener('click', async () => {
3791  console.log('=== CONFIRM BUTTON CLICKED ===');
3792  console.log('selectedPoint:', selectedPoint);
3793  console.log('dataLoaded:', dataLoaded);
3794  console.log('allBuildingsData length:', allBuildingsData?.length);
3795  
3796  if (!selectedPoint) {
3797    const title = currentLang === 'fr' ? 'Avis' : 'Notice';
3798    const message = currentLang === 'fr' 
3799      ? 'Veuillez d\'abord sélectionner un point sur la carte.' 
3800      : 'Please first select a point on the map.';
3801    showCustomAlert(title, message);
3802    return;
3803  }
3804  
3805  if (!dataLoaded || !tileManifest) {
3806    const title = currentLang === 'fr' ? 'Avis' : 'Notice';
3807    const message = currentLang === 'fr'
3808      ? 'Les données sont toujours en cours de chargement. Veuillez patienter un instant et réessayer.'
3809      : 'Data is still loading. Please wait a moment and try again.';
3810    showCustomAlert(title, message);
3811    return;
3812  }
3813  
3814  console.log('✓ Proceeding to Step 2...');
3815  console.log('✓ Loading voxels and trees...');
3816
3817  // Switch to Step 2
3818  const step1 = document.getElementById('step1');
3819  const step2 = document.getElementById('step2-paint');
3820
3821  if (step1) step1.classList.add('hidden');
3822  if (step2) step2.classList.remove('hidden');
3823  currentStep = 2;
3824
3825  // Update progress
3826  updateProgress(2);
3827
3828  // Update info tooltip for new step
3829  if (typeof updateInfoTooltip === 'function') updateInfoTooltip();
3830
3831  const isMobile = window.innerWidth <= 768;
3832
3833  // Zoom in to ensure 3D buildings are visible
3834  console.log('🔍 Auto-zooming to selected point for 3D buildings...');
3835  console.log('Current zoom:', map.getZoom(), 'Target zoom: 16.5');
3836  map.flyTo({
3837    center: selectedPoint,
3838    zoom: 16.5,
3839    pitch: isMobile ? 0 : 60,
3840    bearing: map.getBearing(),
3841    duration: 2000
3842  });
3843
3844  // Mobile: lock zoom level
3845  if (isMobile) {
3846    map.setMinZoom(15.5);
3847    map.setMaxZoom(17.5);
3848  }
3849
3850  // Show tutorial after zoom animation starts
3851  setTimeout(() => {
3852    showTutorial(1);
3853  }, 500);
3854
3855  // Load the voxels and trees
3856  try {
3857    await loadVoxelsAndTrees(selectedPoint);
3858    console.log('✓ Voxels and trees loaded successfully');
3859  } catch (error) {
3860    console.error('✗ Error loading voxels:', error);
3861    const title = currentLang === 'fr' ? 'Erreur' : 'Error';
3862    const message = currentLang === 'fr'
3863      ? 'Échec du chargement des données du bâtiment. Veuillez réessayer.'
3864      : 'Failed to load building data. Please try again.';
3865    showCustomAlert(title, message);
3866  }
3867});
3868
3869// Map navigation controls
3870const MAP_PAN_DISTANCE = 100;
3871const MAP_ROTATE_ANGLE = 15;
3872
3873document.getElementById('map-nav-up').addEventListener('click', () => {
3874  map.panBy([0, -MAP_PAN_DISTANCE]);
3875});
3876
3877document.getElementById('map-nav-down').addEventListener('click', () => {
3878  map.panBy([0, MAP_PAN_DISTANCE]);
3879});
3880
3881document.getElementById('map-nav-left').addEventListener('click', () => {
3882  map.panBy([-MAP_PAN_DISTANCE, 0]);
3883});
3884
3885document.getElementById('map-nav-right').addEventListener('click', () => {
3886  map.panBy([MAP_PAN_DISTANCE, 0]);
3887});
3888
3889document.getElementById('map-rotate-left').addEventListener('click', () => {
3890  const current = map.getBearing();
3891  map.easeTo({ bearing: current - MAP_ROTATE_ANGLE, duration: 300 });
3892});
3893
3894document.getElementById('map-rotate-right').addEventListener('click', () => {
3895  const current = map.getBearing();
3896  map.easeTo({ bearing: current + MAP_ROTATE_ANGLE, duration: 300 });
3897});
3898
3899// Zoom controls
3900document.getElementById('map-zoom-in').addEventListener('click', () => {
3901  map.zoomIn({ duration: 300 });
3902});
3903
3904document.getElementById('map-zoom-out').addEventListener('click', () => {
3905  map.zoomOut({ duration: 300 });
3906});
3907
3908// Keyboard navigation with live visual feedback
3909document.addEventListener('keydown', (e) => {
3910  // Only respond to arrow keys when not typing in an input
3911  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
3912  
3913  let buttonId = null;
3914  
3915  switch(e.key) {
3916    case 'ArrowUp':
3917      e.preventDefault();
3918      map.panBy([0, -MAP_PAN_DISTANCE]);
3919      buttonId = 'map-nav-up';
3920      break;
3921    case 'ArrowDown':
3922      e.preventDefault();
3923      map.panBy([0, MAP_PAN_DISTANCE]);
3924      buttonId = 'map-nav-down';
3925      break;
3926    case 'ArrowLeft':
3927      e.preventDefault();
3928      map.panBy([-MAP_PAN_DISTANCE, 0]);
3929      buttonId = 'map-nav-left';
3930      break;
3931    case 'ArrowRight':
3932      e.preventDefault();
3933      map.panBy([MAP_PAN_DISTANCE, 0]);
3934      buttonId = 'map-nav-right';
3935      break;
3936  }
3937  
3938  // Highlight the button
3939  if (buttonId) {
3940    const button = document.getElementById(buttonId);
3941    if (button) {
3942      button.classList.add('active-press');
3943    }
3944  }
3945});
3946
3947document.addEventListener('keyup', (e) => {
3948  // Remove highlight when key is released
3949  const keyMap = {
3950    'ArrowUp': 'map-nav-up',
3951    'ArrowDown': 'map-nav-down',
3952    'ArrowLeft': 'map-nav-left',
3953    'ArrowRight': 'map-nav-right'
3954  };
3955  
3956  const buttonId = keyMap[e.key];
3957  if (buttonId) {
3958    const button = document.getElementById(buttonId);
3959    if (button) {
3960      button.classList.remove('active-press');
3961    }
3962  }
3963});
3964
3965// Initialize map
3966const isMobile = window.innerWidth <= 768;
3967
3968map = new mapboxgl.Map({
3969  container: 'map',
3970  style: 'mapbox://styles/mapbox/streets-v12',
3971  center: MONTREAL_CENTER,
3972  zoom: 12,
3973  pitch: isMobile ? 0 : 45,
3974  bearing: -17,
3975  antialias: true
3976});
3977
3978map.on('load', () => {
3979  console.log('Map loaded');
3980
3981  // Hide ALL building-related layers in the map style
3982  modifyBuildingLayers();
3983
3984  loadAllData();
3985});
3986
3987// Modify building layers to show flat 2D at lower zoom, hide at higher zoom
3988function modifyBuildingLayers() {
3989  const style = map.getStyle();
3990  if (style && style.layers) {
3991    style.layers.forEach(layer => {
3992      if (layer.id.includes('building') ||
3993          (layer['source-layer'] && layer['source-layer'].includes('building')) ||
3994          (layer.type === 'fill-extrusion' && !layer.id.includes('voxel') && !layer.id.includes('tree') && !layer.id.includes('painted'))) {
3995        try {
3996          if (map.getLayer(layer.id)) {
3997            // Show at low zoom (flat), hide at high zoom (where voxels appear)
3998            if (layer.type === 'fill-extrusion') {
3999              // Make buildings completely flat - just footprints
4000              map.setPaintProperty(layer.id, 'fill-extrusion-height', [
4001                'interpolate', ['linear'], ['zoom'],
4002                12, 0.3,      // Almost completely flat
4003                15, 0.3,      // Still flat
4004                15.5, 0       // Invisible when voxels appear
4005              ]);
4006              map.setPaintProperty(layer.id, 'fill-extrusion-opacity', [
4007                'interpolate', ['linear'], ['zoom'],
4008                12, 0.4,      // Semi-transparent at low zoom
4009                15, 0.3,      // Fading
4010                15.5, 0       // Invisible when voxels appear
4011              ]);
4012              // Set base height to 0 to ensure they're flat on the ground
4013              map.setPaintProperty(layer.id, 'fill-extrusion-base', 0);
4014            } else if (layer.type === 'fill') {
4015              // For 2D fill layers, just fade them out
4016              map.setPaintProperty(layer.id, 'fill-opacity', [
4017                'interpolate', ['linear'], ['zoom'],
4018                12, 0.4,
4019                15, 0.2,
4020                15.5, 0
4021              ]);
4022            }
4023          }
4024        } catch(e) {
4025          // Layer doesn't support these properties or already modified
4026        }
4027      }
4028    });
4029  }
4030}
4031
4032// Keep building layers modified even when style updates
4033map.on('styledata', () => {
4034  modifyBuildingLayers();
4035});
4036
4037map.on('sourcedata', (e) => {
4038  if (e.isSourceLoaded) {
4039    modifyBuildingLayers();
4040  }
4041});
4042
4043// Handle WebGL context lost (prevents crashes)
4044map.on('webglcontextlost', (e) => {
4045  console.warn('WebGL context lost - preventing default to allow recovery');
4046  e.preventDefault();
4047});
4048
4049map.on('webglcontextrestored', () => {
4050  console.log('WebGL context restored');
4051  // Reload data if needed
4052  if (voxelsLoaded && selectedPoint) {
4053    console.log('Reloading voxels after context restore...');
4054    loadVoxelsAndTrees(selectedPoint);
4055  }
4056});
4057
4058// Update pitch value live
4059map.on('pitch', () => {
4060  const currentPitch = Math.round(map.getPitch());
4061  document.getElementById('pitch-value').textContent = currentPitch;
4062  document.getElementById('pitch-slider').value = currentPitch;
4063});
4064
4065// Show/hide zoom prompt based on zoom level
4066function updateZoomPrompt() {
4067  const zoomPrompt = document.getElementById('zoom-prompt');
4068  if (!zoomPrompt) return;
4069
4070  const currentZoom = map.getZoom();
4071  const step2Panel = document.getElementById('step2-paint');
4072  const step3Panel = document.getElementById('step3-tag-clusters');
4073  const isStep2Visible = step2Panel && !step2Panel.classList.contains('hidden');
4074  const isStep3Visible = step3Panel && !step3Panel.classList.contains('hidden');
4075  const tutorialOverlay = document.getElementById('tutorial-overlay');
4076  const isTutorialVisible = tutorialOverlay && !tutorialOverlay.classList.contains('hidden');
4077  const loadingOverlay = document.getElementById('loading-overlay');
4078  const isLoadingVisible = loadingOverlay && !loadingOverlay.classList.contains('hidden');
4079
4080  // Show prompt in Step 2 or Step 3 when tutorial is NOT visible, loading is NOT visible, and zoom is too far out
4081  if (typeof currentStep !== 'undefined' && (currentStep === 2 || currentStep === 3) && (isStep2Visible || isStep3Visible) && !isTutorialVisible && !isLoadingVisible && currentZoom < 15.5) {
4082    zoomPrompt.classList.add('visible');
4083  } else {
4084    zoomPrompt.classList.remove('visible');
4085  }
4086}
4087
4088map.on('zoom', updateZoomPrompt);
4089map.on('zoomend', updateZoomPrompt);
4090
4091// Store manifest for lazy loading
4092let tileManifest = null;
4093let loadedTileCache = {};
4094
4095// Load only manifest initially (lazy load tiles on demand)
4096async function loadAllData() {
4097  console.log('Loading tile manifest...');
4098
4099  try {
4100    const cacheBuster = '?v=' + Date.now();
4101    const response = await fetch('./data/tiles_merged/tile_manifest.json' + cacheBuster);
4102    tileManifest = await response.json();
4103
4104    // Initialize empty array - tiles loaded on demand
4105    allBuildingsData = [];
4106    console.log(`✓ Manifest loaded: ${tileManifest.tiles.length} tiles available`);
4107  } catch (error) {
4108    console.error('Error loading manifest:', error);
4109    await new Promise(resolve => setTimeout(resolve, 1000));
4110    try {
4111      const cacheBuster = '?v=' + Date.now();
4112      const response = await fetch('./data/tiles_merged/tile_manifest.json' + cacheBuster);
4113      tileManifest = await response.json();
4114      allBuildingsData = [];
4115      console.log(`✓ Manifest loaded on retry: ${tileManifest.tiles.length} tiles available`);
4116    } catch (retryError) {
4117      console.error('Failed to load manifest after retry:', retryError);
4118    }
4119  }
4120  
4121  dataLoaded = true;
4122  console.log('✓ All data loaded successfully');
4123}
4124
4125// Mode selection
4126document.getElementById('btn-click').addEventListener('click', () => {
4127  selectionMode = 'click';
4128  document.getElementById('btn-click').classList.add('active');
4129  document.getElementById('btn-address').classList.remove('active');
4130  document.getElementById('address-section').classList.add('hidden');
4131});
4132
4133document.getElementById('btn-address').addEventListener('click', () => {
4134  selectionMode = 'address';
4135  document.getElementById('btn-address').classList.add('active');
4136  document.getElementById('btn-click').classList.remove('active');
4137  document.getElementById('address-section').classList.remove('hidden');
4138  document.getElementById('address-input').focus();
4139});
4140
4141// Mobile: auto-select address mode and update step 1 header
4142if (window.innerWidth <= 768) {
4143  document.getElementById('btn-address').click();
4144  const step1Header = document.querySelector('#step1 .step-header span');
4145  if (step1Header) {
4146    step1Header.setAttribute('data-fr', 'Étape 1 : Cliquez sur la carte ou entrez une adresse');
4147    step1Header.setAttribute('data-en', 'Step 1: Click on map or enter an address');
4148    step1Header.textContent = (typeof currentLang !== 'undefined' && currentLang === 'en')
4149      ? 'Step 1: Click on map or enter an address'
4150      : 'Étape 1 : Cliquez sur la carte ou entrez une adresse';
4151  }
4152}
4153
4154// Address autocomplete
4155let addressTimeout;
4156document.getElementById('address-input').addEventListener('input', (e) => {
4157  const query = e.target.value.trim();
4158  
4159  if (query.length < 3) {
4160    document.getElementById('address-suggestions').classList.add('hidden');
4161    return;
4162  }
4163  
4164  clearTimeout(addressTimeout);
4165  addressTimeout = setTimeout(() => {
4166    searchAddresses(query);
4167  }, 300);
4168});
4169
4170async function searchAddresses(query) {
4171  const searchText = query.includes('Montreal') ? query : `${query}, Montreal, Quebec`;
4172  const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(searchText)}.json?access_token=${mapboxgl.accessToken}&bbox=-73.98,45.40,-73.48,45.70&limit=5`;
4173  
4174  try {
4175    const resp = await fetch(url);
4176    const data = await resp.json();
4177    
4178    if (data.features && data.features.length > 0) {
4179      const suggestionsDiv = document.getElementById('address-suggestions');
4180      suggestionsDiv.innerHTML = '';
4181      
4182      data.features.forEach(feature => {
4183        const div = document.createElement('div');
4184        div.className = 'address-suggestion';
4185        div.textContent = feature.place_name;
4186        div.addEventListener('click', () => {
4187          const [lng, lat] = feature.center;
4188          document.getElementById('address-input').value = feature.place_name;
4189          suggestionsDiv.classList.add('hidden');
4190          selectPoint({ lng, lat });
4191          map.flyTo({ center: [lng, lat], zoom: 16, duration: 1500 });
4192        });
4193        suggestionsDiv.appendChild(div);
4194      });
4195      
4196      suggestionsDiv.classList.remove('hidden');
4197    }
4198  } catch (error) {
4199    console.error('Geocoding error:', error);
4200  }
4201}
4202
4203// Mouse handlers
4204map.on('mousedown', (e) => {
4205  // Step 1: Click to select/move point
4206  if (!voxelsLoaded && selectionMode === 'click') {
4207    selectPoint(e.lngLat);
4208    return;
4209  }
4210  
4211  // Step 2: Paint or erase
4212  if (voxelsLoaded && currentTool !== 'cursor') {
4213    isDragging = true;
4214    performAction([e.lngLat.lng, e.lngLat.lat]);
4215  }
4216});
4217
4218map.on('mouseup', () => {
4219  isDragging = false;
4220});
4221
4222map.on('mouseleave', () => {
4223  isDragging = false;
4224});
4225
4226// Mobile: paint on touch (tap + drag)
4227if (window.innerWidth <= 768) {
4228  const canvas = map.getCanvas();
4229
4230  canvas.addEventListener('touchstart', (e) => {
4231    if (e.touches.length === 1) {
4232      const touch = e.touches[0];
4233      const rect = canvas.getBoundingClientRect();
4234      const x = touch.clientX - rect.left;
4235      const y = touch.clientY - rect.top;
4236      const point = new mapboxgl.Point(x, y);
4237      const lngLat = map.unproject(point);
4238
4239      // Step 1: Touch to select location (works in both click and address modes)
4240      if (!voxelsLoaded) {
4241        e.preventDefault();
4242        selectPoint(lngLat);
4243        return;
4244      }
4245
4246      // Step 2: Touch to paint or erase
4247      if (voxelsLoaded && currentTool !== 'cursor') {
4248        e.preventDefault();
4249        isDragging = true;
4250        performAction([lngLat.lng, lngLat.lat], point);
4251      }
4252    }
4253  }, { passive: false });
4254
4255  canvas.addEventListener('touchmove', (e) => {
4256    if (isDragging && voxelsLoaded && currentTool !== 'cursor' && e.touches.length === 1) {
4257      e.preventDefault();
4258      const touch = e.touches[0];
4259      const rect = canvas.getBoundingClientRect();
4260      // Use canvas-relative coordinates for accurate unprojection
4261      const x = touch.clientX - rect.left;
4262      const y = touch.clientY - rect.top;
4263      const point = new mapboxgl.Point(x, y);
4264      const lngLat = map.unproject(point);
4265      const now = Date.now();
4266      if (now - lastPaintTime > 150) {
4267        lastPaintTime = now;
4268        performAction([lngLat.lng, lngLat.lat], point);
4269      }
4270    }
4271  }, { passive: false });
4272
4273  canvas.addEventListener('touchend', () => {
4274    isDragging = false;
4275  });
4276}
4277
4278// Disable tool preview during map movement for better performance
4279map.on('movestart', () => {
4280  isMapMoving = true;
4281  // Hide tool preview during movement
4282  if (map.getSource('tool-preview')) {
4283    map.getSource('tool-preview').setData({ type: 'FeatureCollection', features: [] });
4284  }
4285});
4286
4287map.on('moveend', () => {
4288  isMapMoving = false;
4289  // Update preview once movement stops
4290  if (lastMouseEvent && currentTool !== 'cursor' && voxelsLoaded) {
4291    setTimeout(() => {
4292      updateToolPreview(lastMouseEvent);
4293    }, 50);
4294  }
4295});
4296
4297// Throttled mousemove with requestAnimationFrame
4298map.on('mousemove', (e) => {
4299  lastMouseEvent = e;
4300
4301  // Skip updates during map movement for performance
4302  if (isMapMoving) return;
4303
4304  if (currentTool !== 'cursor' && voxelsLoaded && map.getSource('tool-preview')) {
4305    // Use requestAnimationFrame to throttle updates
4306    if (!mouseMoveScheduled) {
4307      mouseMoveScheduled = true;
4308      requestAnimationFrame(() => {
4309        mouseMoveScheduled = false;
4310        if (lastMouseEvent) {
4311          updateToolPreview(lastMouseEvent);
4312        }
4313      });
4314    }
4315
4316    // Throttle painting during drag - only paint every 150ms for smoother performance
4317    if (isDragging && !isMapMoving) {
4318      const now = Date.now();
4319      if (now - lastPaintTime > 150) {
4320        lastPaintTime = now;
4321        performAction([e.lngLat.lng, e.lngLat.lat], e.point);
4322      }
4323    }
4324  }
4325});
4326
4327function updateToolPreview(e) {
4328  if (!map.getSource('tool-preview')) return;
4329
4330  const radiusKm = toolSize / 1000;
4331  const circle = turf.circle([e.lngLat.lng, e.lngLat.lat], radiusKm, {
4332    steps: 32,
4333    units: 'kilometers'
4334  });
4335
4336  map.getSource('tool-preview').setData(circle);
4337
4338  let color = currentTool === 'erase' ? '#666666' :
4339              currentPaintColor === 'disliked' ? '#ff0000' : '#00ff00';
4340
4341  map.setPaintProperty('tool-preview-fill', 'fill-color', color);
4342  map.setPaintProperty('tool-preview-line', 'line-color', color);
4343}
4344
4345function selectPoint(lngLat) {
4346  selectedPoint = lngLat;
4347
4348  if (centerMarker) centerMarker.remove();
4349
4350  // Create custom pin marker element
4351  const el = document.createElement('div');
4352  el.style.width = '60px';
4353  el.style.height = '90px';
4354  el.style.backgroundImage = 'url(assets/pin_3d_glossy_blue.svg)';
4355  el.style.backgroundSize = 'contain';
4356  el.style.backgroundRepeat = 'no-repeat';
4357  el.style.backgroundPosition = 'center bottom';
4358  el.style.cursor = 'pointer';
4359
4360  centerMarker = new mapboxgl.Marker({
4361    element: el,
4362    anchor: 'bottom'  // Anchor at bottom of pin (tip of pole)
4363  })
4364    .setLngLat([lngLat.lng, lngLat.lat])
4365    .addTo(map);
4366  
4367  drawRadiusCircle(lngLat);
4368  
4369  document.getElementById('coords-display').textContent =
4370    `Lat: ${lngLat.lat.toFixed(4)}, Lng: ${lngLat.lng.toFixed(4)}`;
4371
4372  document.getElementById('location-selected').classList.remove('hidden');
4373
4374  // Make confirm button pulsate when it appears
4375  const confirmBtn = document.getElementById('confirm-btn');
4376  confirmBtn.classList.add('pulsate');
4377  setTimeout(() => {
4378    confirmBtn.classList.remove('pulsate');
4379  }, 6000); // Remove after 3 cycles (2s * 3 = 6s)
4380
4381}
4382
4383function drawRadiusCircle(center) {
4384  const circle = turf.circle([center.lng, center.lat], RADIUS_KM, { steps: 64, units: 'kilometers' });
4385  
4386  if (map.getSource('radius-circle')) {
4387    map.getSource('radius-circle').setData(circle);
4388  } else {
4389    map.addSource('radius-circle', { type: 'geojson', data: circle });
4390
4391    // Only show the dashed line border, no fill
4392    map.addLayer({
4393      id: 'radius-circle-line',
4394      type: 'line',
4395      source: 'radius-circle',
4396      paint: { 'line-color': '#4285f4', 'line-width': 3, 'line-dasharray': [2, 2] }
4397    });
4398  }
4399}
4400
4401// Back button handlers
4402document.getElementById('back-from-step2').addEventListener('click', () => {
4403  const lang = (typeof currentLang !== 'undefined') ? currentLang : 'fr';
4404  const title = lang === 'fr' ? 'Confirmation' : 'Confirmation';
4405  const message = lang === 'fr' 
4406    ? 'Revenir en arrière ? Cela effacera toutes vos données et vous permettra de sélectionner une nouvelle zone.' 
4407    : 'Go back? This will clear all your data and let you select a new area.';
4408    
4409  showCustomConfirm(title, message, (confirmed) => {
4410    if (confirmed) {
4411      // Remove center marker
4412      if (centerMarker) { 
4413        centerMarker.remove(); 
4414        centerMarker = null; 
4415      }
4416      
4417      // Remove all map layers (voxels, trees, radius circle)
4418      if (map.getLayer('voxel-fill-2d')) map.removeLayer('voxel-fill-2d');
4419      if (map.getLayer('voxel-fill')) map.removeLayer('voxel-fill');
4420      if (map.getSource('voxel-cubes')) map.removeSource('voxel-cubes');
4421
4422      if (map.getLayer('radius-circle-line')) {
4423        map.removeLayer('radius-circle-line');
4424        map.removeSource('radius-circle');
4425      }
4426      
4427      // Clear paint
4428      dislikedVoxels.clear();
4429      likedVoxels.clear();
4430      
4431      // Remove ground paint markers
4432      groundPaintMarkers.forEach(paintId => {
4433        if (map.getLayer(paintId)) {
4434          map.removeLayer(paintId);
4435          map.removeSource(paintId);
4436        }
4437      });
4438      groundPaintMarkers = [];
4439      groundPaintIdCounter = 0; // Reset ground paint ID counter
4440
4441      // Remove overlays
4442      paintedOverlays.forEach(overlayId => {
4443        try {
4444          if (map.getLayer(overlayId)) map.removeLayer(overlayId);
4445          if (map.getSource(overlayId)) map.removeSource(overlayId);
4446        } catch(e) {}
4447      });
4448      paintedOverlays = [];
4449      
4450      // Remove cluster markers
4451      clusterMarkers.forEach(marker => marker.remove());
4452      clusterMarkers = [];
4453      
4454      // Remove tool preview
4455      removeToolPreview();
4456      
4457      // Reset voxel data
4458      voxelsLoaded = false;
4459      voxelSpatialIndex = null;
4460      allVoxelFeatures = [];
4461      
4462      // Reset state variables
4463      currentTool = 'cursor';
4464      currentPaintColor = 'liked';
4465      isDragging = false;
4466      clusterLabels = {};
4467      allClusters = [];
4468      selectedClusterId = null;
4469      paintedVoxelFeatures = [];
4470      paintedVoxelIndex = new Map();
4471      window.paintLimitNotified = false; // Reset paint limit notification
4472      
4473      // Remove selected point (wrapped in try-catch)
4474      try {
4475        if (typeof selectedPointMarker !== 'undefined' && selectedPointMarker) {
4476          selectedPointMarker.remove();
4477          selectedPointMarker = null;
4478        }
4479      } catch(e) {
4480        console.log('selectedPointMarker cleanup skipped:', e);
4481      }
4482      
4483      try {
4484        if (typeof selectedPoint !== 'undefined') {
4485          selectedPoint = null;
4486        }
4487      } catch(e) {
4488        console.log('selectedPoint cleanup skipped:', e);
4489      }
4490      
4491      // Re-enable map interactions
4492      map.dragPan.enable();
4493      map.scrollZoom.enable();
4494      
4495      // Hide Step 2 and other steps, Show Step 1
4496      document.getElementById('step2-paint').classList.add('hidden');
4497      document.getElementById('step3-tag-clusters').classList.add('hidden');
4498      document.getElementById('step4-submit').classList.add('hidden');
4499      document.getElementById('step1').classList.remove('hidden');
4500
4501      // Update progress when going back to step 1
4502      updateProgress(1);
4503
4504      // Reset Step 1 UI elements
4505      document.getElementById('location-selected').classList.add('hidden');
4506      document.getElementById('address-suggestions').classList.add('hidden');
4507      document.getElementById('address-input').value = '';
4508      
4509      // Clear clusters list
4510      document.getElementById('clusters-list').innerHTML = '';
4511      
4512      // Hide reset button
4513      document.getElementById('reset-btn').classList.add('hidden');
4514      
4515      // Reset tool selection
4516      document.getElementById('tool-cursor').classList.add('active');
4517      document.getElementById('tool-paint').classList.remove('active');
4518      document.getElementById('tool-erase').classList.remove('active');
4519      document.getElementById('paint-green').classList.add('active');
4520      document.getElementById('paint-red').classList.remove('active');
4521      
4522      // Hide map nav controls
4523      document.getElementById('map-nav-controls').classList.remove('active');
4524      
4525      // Fly back to Montreal center
4526      map.flyTo({
4527        center: MONTREAL_CENTER,
4528        zoom: 12,
4529        pitch: window.innerWidth <= 768 ? 0 : 45,
4530        bearing: -17,
4531        duration: 1500
4532      });
4533    }
4534  });
4535});
4536
4537document.getElementById('back-from-step3').addEventListener('click', () => {
4538  document.getElementById('step3-tag-clusters').classList.add('hidden');
4539  document.getElementById('step2-paint').classList.remove('hidden');
4540
4541  // Update step and progress when going back
4542  currentStep = 2;
4543  updateProgress(2);
4544  updateInfoTooltip();
4545
4546  // Remove cluster markers
4547  clusterMarkers.forEach(marker => marker.remove());
4548  clusterMarkers = [];
4549
4550  // Remove split divider lines
4551  splitDividerLines.forEach(lineId => {
4552    try {
4553      if (map.getLayer(lineId)) map.removeLayer(lineId);
4554      if (map.getSource(lineId)) map.removeSource(lineId);
4555    } catch(e) {}
4556  });
4557  splitDividerLines = [];
4558
4559  // FIX: Re-enable paint tool when going back to step 2
4560  // Explicitly reset all tool states to ensure paint tool works properly
4561
4562  // Set tool state
4563  currentTool = 'paint';
4564
4565  // Update desktop button states
4566  document.getElementById('tool-paint').classList.add('active');
4567  document.getElementById('tool-cursor').classList.remove('active');
4568  document.getElementById('tool-erase').classList.remove('active');
4569
4570  // Update mobile button states
4571  if (window.innerWidth <= 768) {
4572    document.getElementById('mobile-peinturer-btn').classList.add('active');
4573    document.getElementById('mobile-navigate-btn').classList.remove('active');
4574    document.getElementById('mobile-erase-btn').classList.remove('active');
4575
4576    // Reset camera pitch on mobile (overhead view for Step 2)
4577    map.setPitch(0);
4578  }
4579
4580  // Set map interaction states for paint tool
4581  map.getCanvas().style.cursor = 'crosshair';
4582  map.dragPan.disable();
4583  map.scrollZoom.enable();
4584  map.doubleClickZoom.enable();
4585
4586  // Create tool preview
4587  createToolPreview();
4588});
4589
4590document.getElementById('back-from-step4').addEventListener('click', () => {
4591  document.getElementById('step4-submit').classList.add('hidden');
4592  document.getElementById('step3-tag-clusters').classList.remove('hidden');
4593
4594  // Show main panel for step 3
4595  const mainPanel = document.querySelector('.main-panel');
4596  if (mainPanel) mainPanel.style.display = 'block';
4597
4598  // Update step and progress when going back to step 3
4599  currentStep = 3;
4600  updateProgress(3);
4601  updateInfoTooltip();
4602
4603  // Go back to last cluster
4604  if (allClusters.length > 0) {
4605    currentClusterIndex = allClusters.length - 1;
4606    showCluster(currentClusterIndex);
4607  }
4608});
4609
4610// Load tiles near a specific location (lazy loading for performance)
4611async function loadTilesNearLocation(center, radiusKm = 1.5) {
4612  if (!tileManifest) return [];
4613
4614  const TILE_SIZE = 0.02; // degrees
4615  const tilesToLoad = [];
4616
4617  // Calculate which tiles intersect with radius
4618  const minLon = center.lng - (radiusKm / 111);
4619  const maxLon = center.lng + (radiusKm / 111);
4620  const minLat = center.lat - (radiusKm / 111);
4621  const maxLat = center.lat + (radiusKm / 111);
4622
4623  // Find tiles in bounding box
4624  for (const tile of tileManifest.tiles) {
4625    const filename = tile.file;
4626    const match = filename.match(/tile_(-?\d+)_(-?\d+)\.geojson\.gz/);
4627    if (!match) continue;
4628
4629    const tileX = parseInt(match[1]);
4630    const tileY = parseInt(match[2]);
4631
4632    const tileLonMin = -73.98 + (tileX * TILE_SIZE);
4633    const tileLonMax = tileLonMin + TILE_SIZE;
4634    const tileLatMin = 45.40 + (tileY * TILE_SIZE);
4635    const tileLatMax = tileLatMin + TILE_SIZE;
4636
4637    // Check if tile intersects with search area
4638    if (tileLonMax >= minLon && tileLonMin <= maxLon &&
4639        tileLatMax >= minLat && tileLatMin <= maxLat) {
4640      tilesToLoad.push(filename);
4641    }
4642  }
4643
4644  // Use cached tiles or load new ones
4645  const cachedTiles = tilesToLoad.filter(f => loadedTileCache[f]);
4646  const uncachedTiles = tilesToLoad.filter(f => !loadedTileCache[f]);
4647
4648  console.log(`Loading ${uncachedTiles.length} tiles (${cachedTiles.length} cached)...`);
4649
4650  // Load uncached tiles in smaller batches (max 5 concurrent)
4651  const batchSize = 5;
4652  const allFeatures = [...cachedTiles.flatMap(f => loadedTileCache[f])];
4653
4654  for (let i = 0; i < uncachedTiles.length; i += batchSize) {
4655    const batch = uncachedTiles.slice(i, i + batchSize);
4656    const batchPromises = batch.map(async (filename) => {
4657      try {
4658        const resp = await fetch(`./data/tiles_merged/${filename}`);
4659        const blob = await resp.blob();
4660        const buffer = await blob.arrayBuffer();
4661        const decompressed = pako.ungzip(new Uint8Array(buffer), { to: 'string' });
4662        const features = JSON.parse(decompressed).features;
4663        loadedTileCache[filename] = features;
4664        return features;
4665      } catch (e) {
4666        console.error(`Failed to load ${filename}:`, e);
4667        return [];
4668      }
4669    });
4670
4671    const batchResults = await Promise.all(batchPromises);
4672    allFeatures.push(...batchResults.flat());
4673  }
4674
4675  return allFeatures;
4676}
4677
4678async function loadVoxelsAndTrees(center) {
4679  // Show loading indicator
4680  document.getElementById('loading-overlay').classList.remove('hidden');
4681  updateZoomPrompt();
4682  console.log('⏳ Loading data...');
4683
4684  // Load only nearby tiles (performance optimization)
4685  const buildingsData = await loadTilesNearLocation(center, RADIUS_KM + 0.3);
4686  console.log(`✓ Loaded ${buildingsData.length} buildings from nearby tiles`);
4687
4688  const buildingsInRadius = buildingsData.filter(b => {
4689    try {
4690      const c = turf.center(b);
4691      const d = turf.distance([center.lng, center.lat], c.geometry.coordinates, { units: 'kilometers' });
4692      return d <= RADIUS_KM;
4693    } catch (e) { return false; }
4694  });
4695
4696  console.log(`Buildings in radius: ${buildingsInRadius.length}`);
4697
4698  // Process voxels immediately
4699  requestAnimationFrame(() => {
4700    processVoxels(buildingsInRadius, center);
4701  });
4702
4703}
4704
4705function processVoxels(buildings, center) {
4706  const CUBE_SIZE = 5, centerLat = center.lat;
4707  const metersPerDegreeLat = 111320;
4708  const metersPerDegreeLon = 111320 * Math.cos(centerLat * Math.PI / 180);
4709  const cubeSizeDegLat = CUBE_SIZE / metersPerDegreeLat;
4710  const cubeSizeDegLon = CUBE_SIZE / metersPerDegreeLon;
4711  const rotationRad = (MONTREAL_GRID_ROTATION * Math.PI) / 180;
4712
4713  console.log(`⏳ Processing ${buildings.length} buildings into voxels...`);
4714
4715  const allCubes = [];
4716  let skippedLOD = 0;
4717  const startTime = performance.now();
4718
4719  buildings.forEach(building => {
4720    const rawHeight = building.properties?.height || 10;
4721    // Round height to nearest CUBE_SIZE to avoid thin partial layers
4722    const height = Math.round(rawHeight / CUBE_SIZE) * CUBE_SIZE;
4723    if (height < CUBE_SIZE) return; // Skip very short buildings
4724
4725    const geometry = building.geometry;
4726    if (!geometry) return;
4727    
4728    try {
4729      let poly = geometry.type === 'Polygon' ? turf.polygon(geometry.coordinates) : turf.polygon(geometry.coordinates[0]);
4730
4731      // Shrink building footprint to create gaps between buildings
4732      try {
4733        poly = turf.buffer(poly, -1, { units: 'meters' });
4734        if (!poly || poly.geometry.coordinates.length === 0) return; // Skip if buffer creates invalid geometry
4735      } catch (e) {
4736        return; // Skip buildings that can't be buffered
4737      }
4738
4739      const bbox = turf.bbox(poly);
4740      const [minLon, minLat, maxLon, maxLat] = bbox;
4741      const centerLon = (minLon + maxLon) / 2, centerLat = (minLat + maxLat) / 2;
4742      const numLayers = Math.ceil(height / CUBE_SIZE);
4743      
4744      for (let layer = 0; layer < numLayers; layer++) {
4745        const baseHeight = layer * CUBE_SIZE;
4746        const topHeight = Math.min((layer + 1) * CUBE_SIZE, height);
4747
4748        // Add vertical gap between layers (0.3m gap)
4749        const verticalGap = 0.3;
4750        const adjustedBase = baseHeight + verticalGap;
4751        const adjustedTop = topHeight - verticalGap;
4752
4753        if (adjustedTop <= adjustedBase) continue; // Skip if gap makes layer too thin
4754
4755        // Uniform color for all buildings - cold bright grey
4756        let color = '#dce0e3';
4757        
4758        for (let lat = minLat; lat <= maxLat; lat += cubeSizeDegLat) {
4759          for (let lon = minLon; lon <= maxLon; lon += cubeSizeDegLon) {
4760            const gap = 0.05, gapLat = cubeSizeDegLat * gap, gapLon = cubeSizeDegLon * gap;
4761            
4762            const localLon = lon - centerLon, localLat = lat - centerLat;
4763            const rotatedLon = localLon * Math.cos(rotationRad) - localLat * Math.sin(rotationRad);
4764            const rotatedLat = localLon * Math.sin(rotationRad) + localLat * Math.cos(rotationRad);
4765            const finalLon = rotatedLon + centerLon, finalLat = rotatedLat + centerLat;
4766            
4767            const cubeSquare = turf.polygon([[
4768              [finalLon + gapLon, finalLat + gapLat],
4769              [finalLon + cubeSizeDegLon - gapLon, finalLat + gapLat],
4770              [finalLon + cubeSizeDegLon - gapLon, finalLat + cubeSizeDegLat - gapLat],
4771              [finalLon + gapLon, finalLat + cubeSizeDegLat - gapLat],
4772              [finalLon + gapLon, finalLat + gapLat]
4773            ]]);
4774            
4775            try {
4776              if (turf.booleanIntersects(cubeSquare, poly)) {
4777                allCubes.push({
4778                  type: 'Feature',
4779                  properties: { height: adjustedTop, base: adjustedBase, color: color, originalColor: color },
4780                  geometry: cubeSquare.geometry
4781                });
4782              }
4783            } catch (e) {}
4784          }
4785        }
4786      }
4787    } catch (e) {}
4788  });
4789
4790  const processingTime = ((performance.now() - startTime) / 1000).toFixed(2);
4791  console.log(`✓ Generated ${allCubes.length} voxels in ${processingTime}s (${skippedLOD} distant buildings skipped)`);
4792
4793  // Warn if too many voxels (may cause performance issues)
4794  if (allCubes.length > 15000) {
4795    console.warn(`⚠️ High voxel count (${allCubes.length}). This may cause performance issues or crashes when zooming.`);
4796  }
4797
4798  allVoxelFeatures = allCubes;
4799  voxelsLoaded = true;
4800  buildSpatialIndex();
4801  
4802  if (map.getSource('voxel-cubes')) {
4803    map.getSource('voxel-cubes').setData({ type: 'FeatureCollection', features: allCubes });
4804  } else {
4805    map.addSource('voxel-cubes', {
4806      type: 'geojson',
4807      data: { type: 'FeatureCollection', features: allCubes },
4808      tolerance: 2.0,
4809      buffer: 64,
4810      generateId: true
4811    });
4812
4813    // Only 3D layer - no 2D layer to avoid disappearing buildings
4814    map.addLayer({
4815      id: 'voxel-fill',
4816      type: 'fill-extrusion',
4817      source: 'voxel-cubes',
4818      minzoom: 15,
4819      maxzoom: 24,
4820      paint: {
4821        'fill-extrusion-color': ['get', 'color'],
4822        'fill-extrusion-height': [
4823          'interpolate', ['linear'], ['zoom'],
4824          15, 0,
4825          15.5, ['get', 'height']
4826        ],
4827        'fill-extrusion-base': ['get', 'base'],
4828        'fill-extrusion-opacity': [
4829          'interpolate', ['linear'], ['zoom'],
4830          15, 0,
4831          15.5, 0.9
4832        ],
4833        'fill-extrusion-vertical-gradient': true
4834      }
4835    });
4836  }
4837
4838  // Hide ALL building-related layers to ensure only voxels show
4839  modifyBuildingLayers();
4840
4841  // Hide loading overlay
4842  document.getElementById('loading-overlay').classList.add('hidden');
4843  updateZoomPrompt();
4844}
4845
4846function buildSpatialIndex() {
4847  voxelSpatialIndex = new Map();
4848  const gridSize = 0.001;
4849  
4850  allVoxelFeatures.forEach((voxel, index) => {
4851    try {
4852      const center = turf.center(voxel);
4853      const [lon, lat] = center.geometry.coordinates;
4854      const gridX = Math.floor(lon / gridSize);
4855      const gridY = Math.floor(lat / gridSize);
4856      const key = `${gridX},${gridY}`;
4857      
4858      if (!voxelSpatialIndex.has(key)) {
4859        voxelSpatialIndex.set(key, []);
4860      }
4861      voxelSpatialIndex.get(key).push(index);
4862    } catch (e) {}
4863  });
4864  
4865  console.log(`Spatial index: ${voxelSpatialIndex.size} cells`);
4866}
4867
4868
4869// Paint color selection
4870document.getElementById('paint-red').addEventListener('click', () => {
4871  currentPaintColor = 'disliked';
4872  document.getElementById('paint-red').classList.add('active');
4873  document.getElementById('paint-green').classList.remove('active');
4874  
4875  // Update preview color if tool is active
4876  if (currentTool === 'paint' && map.getSource('tool-preview')) {
4877    // Force preview update
4878    map.getCanvas().dispatchEvent(new MouseEvent('mousemove'));
4879  }
4880});
4881
4882document.getElementById('paint-green').addEventListener('click', () => {
4883  currentPaintColor = 'liked';
4884  document.getElementById('paint-green').classList.add('active');
4885  document.getElementById('paint-red').classList.remove('active');
4886  
4887  // Update preview color if tool is active
4888  if (currentTool === 'paint' && map.getSource('tool-preview')) {
4889    // Force preview update
4890    map.getCanvas().dispatchEvent(new MouseEvent('mousemove'));
4891  }
4892});
4893
4894// Display options collapsible
4895const displayOptionsHeader = document.getElementById('display-options-header');
4896const displayOptionsContent = document.getElementById('display-options-content');
4897
4898// Initialize as collapsed
4899if (displayOptionsContent) {
4900  displayOptionsContent.style.display = 'none';
4901  const arrow = displayOptionsHeader.querySelector('span:last-child');
4902  if (arrow) arrow.textContent = '▶';
4903}
4904
4905displayOptionsHeader.addEventListener('click', () => {
4906  const arrow = displayOptionsHeader.querySelector('span:last-child');
4907
4908  if (displayOptionsContent.style.display === 'none') {
4909    displayOptionsContent.style.display = 'block';
4910    arrow.textContent = '▼';
4911  } else {
4912    displayOptionsContent.style.display = 'none';
4913    arrow.textContent = '▶';
4914  }
4915});
4916
4917// Tool selection
4918document.getElementById('tool-cursor').addEventListener('click', () => {
4919  currentTool = 'cursor';
4920  document.getElementById('tool-cursor').classList.add('active');
4921  document.getElementById('tool-paint').classList.remove('active');
4922  document.getElementById('tool-erase').classList.remove('active');
4923  map.getCanvas().style.cursor = '';
4924  map.dragPan.enable();
4925  map.scrollZoom.enable();
4926  map.doubleClickZoom.enable();
4927  removeToolPreview();
4928});
4929
4930document.getElementById('tool-paint').addEventListener('click', () => {
4931  // Hide paint hints
4932  const hintD = document.getElementById('paint-hint-desktop');
4933  const hintM = document.getElementById('paint-hint-mobile');
4934  if (hintD) hintD.style.display = 'none';
4935  if (hintM) hintM.style.display = 'none';
4936  currentTool = 'paint';
4937  document.getElementById('tool-paint').classList.add('active');
4938  document.getElementById('tool-cursor').classList.remove('active');
4939  document.getElementById('tool-erase').classList.remove('active');
4940  map.getCanvas().style.cursor = 'crosshair';
4941  map.dragPan.disable();
4942  map.scrollZoom.enable(); // KEEP ZOOM ENABLED
4943  map.doubleClickZoom.enable(); // KEEP DOUBLE-CLICK ZOOM
4944  createToolPreview();
4945});
4946
4947document.getElementById('tool-erase').addEventListener('click', () => {
4948  currentTool = 'erase';
4949  document.getElementById('tool-erase').classList.add('active');
4950  document.getElementById('tool-cursor').classList.remove('active');
4951  document.getElementById('tool-paint').classList.remove('active');
4952  map.getCanvas().style.cursor = 'crosshair';
4953  map.dragPan.disable();
4954  map.scrollZoom.enable(); // KEEP ZOOM ENABLED
4955  map.doubleClickZoom.enable(); // KEEP DOUBLE-CLICK ZOOM
4956  createToolPreview();
4957});
4958
4959// Tool size
4960document.getElementById('tool-size-slider').addEventListener('input', (e) => {
4961  toolSize = parseInt(e.target.value);
4962  document.getElementById('tool-size-value').textContent = toolSize;
4963});
4964
4965// Paint targets
4966document.getElementById('paint-buildings').addEventListener('change', (e) => {
4967  paintBuildings = e.target.checked;
4968});
4969
4970document.getElementById('paint-ground').addEventListener('change', (e) => {
4971  paintGround = e.target.checked;
4972});
4973
4974// Mobile Option F logic
4975if (window.innerWidth <= 768) {
4976  const zonePositiveBtn = document.getElementById('mobile-zone-positive');
4977  const zoneNegativeBtn = document.getElementById('mobile-zone-negative');
4978  const peinturerBtn = document.getElementById('mobile-peinturer-btn');
4979  const navigateBtn = document.getElementById('mobile-navigate-btn');
4980  const eraseBtn = document.getElementById('mobile-erase-btn');
4981
4982  let currentZoneType = 'positive'; // Default to positive zone
4983
4984  // Zone type selection
4985  zonePositiveBtn.addEventListener('click', () => {
4986    currentZoneType = 'positive';
4987    zonePositiveBtn.classList.add('active');
4988    zoneNegativeBtn.classList.remove('active');
4989    document.getElementById('paint-green').click();
4990  });
4991
4992  zoneNegativeBtn.addEventListener('click', () => {
4993    currentZoneType = 'negative';
4994    zoneNegativeBtn.classList.add('active');
4995    zonePositiveBtn.classList.remove('active');
4996    document.getElementById('paint-red').click();
4997  });
4998
4999  // Peinturer button
5000  peinturerBtn.addEventListener('click', () => {
5001    // Hide mobile paint hint
5002    const mobileHint = peinturerBtn.querySelector('.mobile-paint-hint');
5003    if (mobileHint) mobileHint.style.display = 'none';
5004
5005    // Set paint tool state directly
5006    currentTool = 'paint';
5007
5008    // Update mobile button states
5009    peinturerBtn.classList.add('active');
5010    navigateBtn.classList.remove('active');
5011    eraseBtn.classList.remove('active');
5012
5013    // Update desktop button states
5014    document.getElementById('tool-paint').classList.add('active');
5015    document.getElementById('tool-cursor').classList.remove('active');
5016    document.getElementById('tool-erase').classList.remove('active');
5017
5018    // Set map interaction states
5019    map.getCanvas().style.cursor = 'crosshair';
5020    map.dragPan.disable();
5021    map.scrollZoom.enable();
5022    map.doubleClickZoom.enable();
5023
5024    // Create tool preview
5025    createToolPreview();
5026  });
5027
5028  // Navigate button
5029  navigateBtn.addEventListener('click', () => {
5030    // Set cursor tool state directly
5031    currentTool = 'cursor';
5032
5033    // Update mobile button states
5034    navigateBtn.classList.add('active');
5035    peinturerBtn.classList.remove('active');
5036    eraseBtn.classList.remove('active');
5037
5038    // Update desktop button states
5039    document.getElementById('tool-cursor').classList.add('active');
5040    document.getElementById('tool-paint').classList.remove('active');
5041    document.getElementById('tool-erase').classList.remove('active');
5042
5043    // Set map interaction states
5044    map.getCanvas().style.cursor = '';
5045    map.dragPan.enable();
5046    map.scrollZoom.enable();
5047    map.doubleClickZoom.enable();
5048
5049    // Remove tool preview
5050    removeToolPreview();
5051  });
5052
5053  // Erase button
5054  eraseBtn.addEventListener('click', () => {
5055    // Set erase tool state directly
5056    currentTool = 'erase';
5057
5058    // Update mobile button states
5059    eraseBtn.classList.add('active');
5060    peinturerBtn.classList.remove('active');
5061    navigateBtn.classList.remove('active');
5062
5063    // Update desktop button states
5064    document.getElementById('tool-erase').classList.add('active');
5065    document.getElementById('tool-cursor').classList.remove('active');
5066    document.getElementById('tool-paint').classList.remove('active');
5067
5068    // Set map interaction states
5069    map.getCanvas().style.cursor = 'crosshair';
5070    map.dragPan.disable();
5071    map.scrollZoom.enable();
5072    map.doubleClickZoom.enable();
5073
5074    // Create tool preview
5075    createToolPreview();
5076  });
5077
5078}
5079
5080// Mobile panel minimize/expand - DISABLED (panel always fixed)
5081// if (window.innerWidth <= 768) {
5082//   const minimizeBtn = document.getElementById('panel-minimize-btn');
5083//   const mainPanel = document.querySelector('.main-panel');
5084//   let isPanelMinimized = false;
5085//
5086//   if (minimizeBtn && mainPanel) {
5087//     minimizeBtn.addEventListener('click', () => {
5088//       isPanelMinimized = !isPanelMinimized;
5089//       mainPanel.classList.toggle('minimized', isPanelMinimized);
5090//       minimizeBtn.textContent = isPanelMinimized ? '▲' : '▼';
5091//     });
5092//   }
5093// }
5094
5095function createToolPreview() {
5096  try {
5097    // Remove existing preview first to avoid conflicts
5098    removeToolPreview();
5099
5100    map.addSource('tool-preview', {
5101      type: 'geojson',
5102      data: { type: 'FeatureCollection', features: [] }
5103    });
5104
5105    map.addLayer({
5106      id: 'tool-preview-fill',
5107      type: 'fill',
5108      source: 'tool-preview',
5109      paint: { 'fill-color': '#ff0000', 'fill-opacity': 0.15 }
5110    });
5111
5112    map.addLayer({
5113      id: 'tool-preview-line',
5114      type: 'line',
5115      source: 'tool-preview',
5116      paint: { 'line-color': '#ff0000', 'line-width': 2, 'line-dasharray': [3, 3] }
5117    });
5118  } catch (error) {
5119    console.error('Error creating tool preview:', error);
5120  }
5121}
5122
5123function removeToolPreview() {
5124  try {
5125    if (map.getLayer('tool-preview-fill')) {
5126      map.removeLayer('tool-preview-fill');
5127    }
5128    if (map.getLayer('tool-preview-line')) {
5129      map.removeLayer('tool-preview-line');
5130    }
5131    if (map.getSource('tool-preview')) {
5132      map.removeSource('tool-preview');
5133    }
5134  } catch (error) {
5135    console.error('Error removing tool preview:', error);
5136  }
5137}
5138
5139
5140// OPTIMIZED: Paint buildings - batch updates to single source
5141function paintBuildingsWithOverlay(clickPoint, radiusMeters, color, type) {
5142  const gridSize = 0.001;
5143  const radiusDeg = radiusMeters / 111320;
5144  const [clickLon, clickLat] = clickPoint;
5145
5146  const gridRadius = Math.ceil(radiusDeg / gridSize);
5147  const centerGridX = Math.floor(clickLon / gridSize);
5148  const centerGridY = Math.floor(clickLat / gridSize);
5149
5150  let updatedCount = 0;
5151
5152  for (let dx = -gridRadius; dx <= gridRadius; dx++) {
5153    for (let dy = -gridRadius; dy <= gridRadius; dy++) {
5154      const key = `${centerGridX + dx},${centerGridY + dy}`;
5155      const indices = voxelSpatialIndex.get(key);
5156
5157      if (indices) {
5158        indices.forEach(index => {
5159          const voxel = allVoxelFeatures[index];
5160          try {
5161            if (!voxel._center) {
5162              const center = turf.center(voxel);
5163              voxel._center = center.geometry.coordinates;
5164            }
5165
5166            const dx = clickPoint[0] - voxel._center[0];
5167            const dy = clickPoint[1] - voxel._center[1];
5168            const distSquared = dx * dx + dy * dy;
5169            const radiusDegSquared = radiusDeg * radiusDeg;
5170
5171            if (distSquared < radiusDegSquared) {
5172              // Add to painted features
5173              addPaintedVoxel(voxel, color, type, index);
5174              updatedCount++;
5175            }
5176          } catch (e) {}
5177        });
5178      }
5179    }
5180  }
5181
5182  // Batch update the map source (throttled)
5183  if (updatedCount > 0) {
5184    schedulePaintUpdate();
5185  }
5186}
5187
5188// OPTIMIZED: Add voxel to painted features array (batched)
5189function addPaintedVoxel(voxel, color, type, voxelIndex) {
5190  const baseHeight = voxel.properties.base || 0;
5191  const topHeight = voxel.properties.height || 10;
5192
5193  if (topHeight <= baseHeight) return;
5194
5195  // Mark voxel as tagged (don't change color - use overlay only)
5196  voxel.properties.tagged = true;
5197  voxel.properties.tagType = type;
5198
5199  // Check if already painted using index map
5200  const existingIdx = paintedVoxelIndex.get(voxelIndex);
5201
5202  // Expand only slightly (1.1m) to cover the 1m gap, no more
5203  let expandedGeometry = voxel.geometry;
5204  try {
5205    const poly = turf.polygon(voxel.geometry.coordinates);
5206    const expanded = turf.buffer(poly, 1.1, { units: 'meters' });
5207    if (expanded && expanded.geometry) {
5208      expandedGeometry = expanded.geometry;
5209    }
5210  } catch (e) {
5211    // If buffer fails, use original
5212  }
5213
5214  const overlayFeature = {
5215    type: 'Feature',
5216    geometry: expandedGeometry,
5217    properties: {
5218      color: color,
5219      type: type,
5220      base: baseHeight, // Use actual voxel base height
5221      height: topHeight + 0.5, // Slightly higher - minimal overlap with neighbors
5222      voxelIndex: voxelIndex
5223    }
5224  };
5225
5226  if (existingIdx !== undefined) {
5227    // Update existing
5228    paintedVoxelFeatures[existingIdx] = overlayFeature;
5229  } else {
5230    // Add new - check mobile limit to prevent performance issues
5231    const isMobile = window.innerWidth <= 768;
5232    const maxFeatures = isMobile ? 1500 : 5000;
5233
5234    if (paintedVoxelFeatures.length >= maxFeatures) {
5235      // On mobile, limit total painted features to prevent memory/performance issues
5236      // Still allow updating existing voxels, just not adding new ones
5237
5238      // Show notification once when limit is reached
5239      if (!window.paintLimitNotified) {
5240        window.paintLimitNotified = true;
5241        const title = currentLang === 'fr' ? 'Limite atteinte' : 'Limit reached';
5242        const message = currentLang === 'fr'
5243          ? 'Vous avez atteint la limite de peinture. Vous pouvez continuer à modifier les zones déjà peintes ou passer à l\'étape suivante.'
5244          : 'You have reached the painting limit. You can continue to modify already painted areas or proceed to the next step.';
5245        setTimeout(() => showCustomAlert(title, message), 100);
5246      }
5247      return;
5248    }
5249
5250    const newIdx = paintedVoxelFeatures.length;
5251    paintedVoxelFeatures.push(overlayFeature);
5252    paintedVoxelIndex.set(voxelIndex, newIdx);
5253  }
5254
5255  // Track in appropriate set
5256  if (type === 'disliked') {
5257    dislikedVoxels.add(voxelIndex);
5258    likedVoxels.delete(voxelIndex);
5259  } else {
5260    likedVoxels.add(voxelIndex);
5261    dislikedVoxels.delete(voxelIndex);
5262  }
5263}
5264
5265// OPTIMIZED: Batch update painted voxels (throttled with RAF)
5266function schedulePaintUpdate() {
5267  if (paintUpdatePending) return;
5268
5269  paintUpdatePending = true;
5270  requestAnimationFrame(() => {
5271    paintUpdatePending = false;
5272    updatePaintedVoxelsSource();
5273  });
5274}
5275
5276function updatePaintedVoxelsSource() {
5277  // Create or update single source for all painted voxels
5278  if (!map.getSource('painted-voxels')) {
5279    map.addSource('painted-voxels', {
5280      type: 'geojson',
5281      data: {
5282        type: 'FeatureCollection',
5283        features: paintedVoxelFeatures
5284      }
5285    });
5286
5287    // 3D layer only - simplified rendering
5288    map.addLayer({
5289      id: 'painted-voxels-layer',
5290      type: 'fill-extrusion',
5291      source: 'painted-voxels',
5292      minzoom: 15,
5293      maxzoom: 24,
5294      paint: {
5295        'fill-extrusion-color': ['get', 'color'],
5296        'fill-extrusion-height': [
5297          'interpolate', ['linear'], ['zoom'],
5298          15, 0,
5299          15.5, ['get', 'height']
5300        ],
5301        'fill-extrusion-base': ['get', 'base'],
5302        'fill-extrusion-opacity': [
5303          'interpolate', ['linear'], ['zoom'],
5304          15, 0,
5305          15.5, 1.0
5306        ],
5307        'fill-extrusion-vertical-gradient': false  // Disable gradient for solid colors
5308      }
5309    });
5310
5311    // Move painted layers to the very top
5312    if (map.getLayer('voxel-fill')) {
5313      map.moveLayer('painted-voxels-layer-2d');
5314      map.moveLayer('painted-voxels-layer');
5315    }
5316  } else {
5317    map.getSource('painted-voxels').setData({
5318      type: 'FeatureCollection',
5319      features: paintedVoxelFeatures
5320    });
5321  }
5322}
5323
5324
5325function performAction(clickPoint, pixelPoint) {
5326  try {
5327    if (currentTool === 'erase') {
5328      eraseAtPoint(clickPoint, toolSize);
5329    } else if (currentTool === 'paint') {
5330      const currentColor = currentPaintColor === 'disliked' ? '#ff0000' : '#00ff00';
5331
5332      if (paintGround) {
5333        paintGroundCircle(clickPoint, toolSize, currentColor, currentPaintColor);
5334      }
5335
5336      if (paintBuildings && voxelSpatialIndex) {
5337        paintBuildingsWithOverlay(clickPoint, toolSize, currentColor, currentPaintColor);
5338      }
5339    }
5340  } catch (error) {
5341    console.error('Error in performAction:', error);
5342    console.log('Current tool:', currentTool, 'Paint ground:', paintGround, 'Paint buildings:', paintBuildings);
5343  }
5344}
5345
5346// OPTIMIZED: Update only specific voxels that were painted
5347function updatePaintedVoxels(indices) {
5348  if (!map.getSource('voxel-cubes')) return;
5349  
5350  const source = map.getSource('voxel-cubes');
5351  const currentData = source._data;
5352  
5353  if (currentData && currentData.features) {
5354    indices.forEach(idx => {
5355      if (currentData.features[idx] && allVoxelFeatures[idx]) {
5356        currentData.features[idx].properties.color = allVoxelFeatures[idx].properties.color;
5357        currentData.features[idx].properties.tagged = allVoxelFeatures[idx].properties.tagged;
5358        currentData.features[idx].properties.tagType = allVoxelFeatures[idx].properties.tagType;
5359      }
5360    });
5361    
5362    // Much faster update - only changes painted voxels
5363    source.setData(currentData);
5364  }
5365}
5366
5367function fastSprayPaint(clickPoint, radiusMeters, color, type, pixelPoint) {
5368  const gridSize = 0.001;
5369  const radiusDeg = radiusMeters / 111320;
5370  const [clickLon, clickLat] = clickPoint;
5371  
5372  // Use selected paint height from dropdown
5373  let targetHeight = selectedPaintHeight;
5374  
5375  const gridRadius = Math.ceil(radiusDeg / gridSize);
5376  const centerGridX = Math.floor(clickLon / gridSize);
5377  const centerGridY = Math.floor(clickLat / gridSize);
5378  
5379  const paintedIndices = []; // Return indices instead of count
5380  const targetSet = type === 'disliked' ? dislikedVoxels : likedVoxels;
5381  const radiusDegSquared = radiusDeg * radiusDeg;
5382  
5383  for (let dx = -gridRadius; dx <= gridRadius; dx++) {
5384    for (let dy = -gridRadius; dy <= gridRadius; dy++) {
5385      const key = `${centerGridX + dx},${centerGridY + dy}`;
5386      const indices = voxelSpatialIndex.get(key);
5387      
5388      if (indices) {
5389        indices.forEach(index => {
5390          const voxel = allVoxelFeatures[index];
5391          try {
5392            if (!voxel._center) {
5393              const center = turf.center(voxel);
5394              voxel._center = center.geometry.coordinates;
5395            }
5396            
5397            // FIX 3: 3D distance check to prevent stacking bug
5398            const clickHeight = clickPoint[2] || 0;
5399            const voxelHeight = voxel._center[2] || 0;
5400            
5401            // Horizontal distance in degrees
5402            const dx = clickPoint[0] - voxel._center[0];
5403            const dy = clickPoint[1] - voxel._center[1];
5404            const distSquared2D = dx * dx + dy * dy;
5405            
5406            // CRITICAL: Also check vertical distance (height)
5407            // Voxels are ~5-10m tall, so if height difference > toolSize, don't paint
5408            const heightDiff = Math.abs(clickHeight - voxelHeight);
5409            
5410            // Only paint if within horizontal range AND height difference is small
5411            if (distSquared2D < radiusDegSquared && heightDiff < 15) {
5412              voxel.properties.tagged = true;
5413              voxel.properties.tagType = type;
5414              targetSet.add(index);
5415              paintedIndices.push(index); // Collect painted indices
5416            }
5417          } catch (e) {}
5418        });
5419      }
5420    }
5421  }
5422  
5423  return paintedIndices;
5424}
5425
5426function paintGroundCircle(center, radiusMeters, color, type) {
5427  const radiusKm = radiusMeters / 1000;
5428  const circle = turf.circle(center, radiusKm, { steps: 16, units: 'kilometers' }); // Reduced steps from 32 to 16
5429
5430  // Use unique counter instead of array length to avoid ID collisions after erasing
5431  const paintId = `ground-paint-${groundPaintIdCounter++}`;
5432  circle.properties = { color: color, type: type };
5433
5434  map.addSource(paintId, {
5435    type: 'geojson',
5436    data: circle
5437  });
5438  
5439  // Insert ground paint BEFORE all voxel layers (at the bottom)
5440  const beforeLayer = map.getLayer('voxel-fill-2d') ? 'voxel-fill-2d' :
5441                      map.getLayer('voxel-fill') ? 'voxel-fill' : undefined;
5442
5443  map.addLayer({
5444    id: paintId,
5445    type: 'fill',
5446    source: paintId,
5447    paint: {
5448      'fill-color': color,
5449      'fill-opacity': 0.4
5450    }
5451  }, beforeLayer);
5452  
5453  // Force immediate high-quality render
5454  map.triggerRepaint();
5455  
5456  groundPaintMarkers.push(paintId);
5457  
5458  // Limit total ground markers to prevent performance issues
5459  if (groundPaintMarkers.length > 500) {
5460    const oldestId = groundPaintMarkers.shift();
5461    if (map.getLayer(oldestId)) {
5462      map.removeLayer(oldestId);
5463      map.removeSource(oldestId);
5464    }
5465  }
5466}
5467
5468function eraseAtPoint(clickPoint, radiusMeters) {
5469  const radiusKm = radiusMeters / 1000;
5470  
5471  if (paintGround) {
5472    groundPaintMarkers = groundPaintMarkers.filter(paintId => {
5473      try {
5474        const source = map.getSource(paintId);
5475        if (source && source._data && source._data.geometry) {
5476          const eraseCircle = turf.circle(clickPoint, radiusKm, { units: 'kilometers' });
5477          const overlaps = turf.booleanOverlap(source._data, eraseCircle) || 
5478                         turf.booleanContains(eraseCircle, source._data) ||
5479                         turf.booleanWithin(source._data, eraseCircle);
5480          
5481          if (overlaps) {
5482            if (map.getLayer(paintId)) {
5483              map.removeLayer(paintId);
5484              map.removeSource(paintId);
5485            }
5486            return false;
5487          }
5488        }
5489      } catch (e) {}
5490      return true;
5491    });
5492  }
5493
5494  if (paintBuildings && voxelSpatialIndex) {
5495    const gridSize = 0.001;
5496    const radiusDeg = radiusMeters / 111320;
5497    const [clickLon, clickLat] = clickPoint;
5498
5499    const gridRadius = Math.ceil(radiusDeg / gridSize) + 2; // Extend search radius to catch buffered overlays
5500    const centerGridX = Math.floor(clickLon / gridSize);
5501    const centerGridY = Math.floor(clickLat / gridSize);
5502
5503    let erasedCount = 0;
5504    const eraseCircle = turf.circle(clickPoint, radiusKm, { units: 'kilometers', steps: 32 });
5505    const toErase = []; // Collect indices to erase
5506
5507    for (let dx = -gridRadius; dx <= gridRadius; dx++) {
5508      for (let dy = -gridRadius; dy <= gridRadius; dy++) {
5509        const key = `${centerGridX + dx},${centerGridY + dy}`;
5510        const indices = voxelSpatialIndex.get(key);
5511
5512        if (indices) {
5513          indices.forEach(index => {
5514            const voxel = allVoxelFeatures[index];
5515
5516            if (voxel.properties.tagged) {
5517              try {
5518                // Check if the PAINTED feature (which is buffered) overlaps with erase circle using index map
5519                const paintIdx = paintedVoxelIndex.get(index);
5520                if (paintIdx !== undefined) {
5521                  const paintedFeature = paintedVoxelFeatures[paintIdx];
5522                  const paintedPoly = turf.polygon(paintedFeature.geometry.coordinates);
5523
5524                  // Check for any overlap between the painted (buffered) geometry and erase circle
5525                  const overlaps = turf.booleanOverlap(paintedPoly, eraseCircle) ||
5526                                 turf.booleanContains(eraseCircle, paintedPoly) ||
5527                                 turf.booleanWithin(paintedPoly, eraseCircle);
5528
5529                  if (overlaps) {
5530                    toErase.push(index);
5531                    // Restore original properties
5532                    voxel.properties.color = voxel.properties.originalColor;
5533                    delete voxel.properties.tagged;
5534                    delete voxel.properties.tagType;
5535                    dislikedVoxels.delete(index);
5536                    likedVoxels.delete(index);
5537                    erasedCount++;
5538                  }
5539                }
5540              } catch (e) {
5541                console.error('Error checking voxel for erase:', e);
5542              }
5543            }
5544          });
5545        }
5546      }
5547    }
5548
5549    if (erasedCount > 0) {
5550      // Remove erased voxels from painted features and rebuild index
5551      paintedVoxelFeatures = paintedVoxelFeatures.filter(f => !toErase.includes(f.properties.voxelIndex));
5552
5553      // Rebuild index map
5554      paintedVoxelIndex.clear();
5555      paintedVoxelFeatures.forEach((feature, idx) => {
5556        paintedVoxelIndex.set(feature.properties.voxelIndex, idx);
5557      });
5558
5559      // Force immediate update for erasing - only update painted layer
5560      updatePaintedVoxelsSource();
5561    }
5562  }
5563}
5564
5565function updateVoxelColors() {
5566  // OPTIMIZED: Update colors directly in the existing data without reloading
5567  // This prevents voxels from disappearing and is much faster
5568  if (map.getSource('voxel-cubes')) {
5569    const source = map.getSource('voxel-cubes');
5570    const currentData = source._data;
5571    
5572    // Update colors in place
5573    if (currentData && currentData.features) {
5574      currentData.features.forEach((feature, idx) => {
5575        if (allVoxelFeatures[idx]) {
5576          feature.properties.color = allVoxelFeatures[idx].properties.color;
5577          feature.properties.tagged = allVoxelFeatures[idx].properties.tagged;
5578          feature.properties.tagType = allVoxelFeatures[idx].properties.tagType;
5579        }
5580      });
5581    }
5582    
5583    // Trigger re-render without full data reload
5584    source.setData(currentData);
5585  }
5586}
5587
5588// Clear all paint
5589document.getElementById('clear-all-paint-btn').addEventListener('click', () => {
5590  const lang = (typeof currentLang !== 'undefined') ? currentLang : 'fr';
5591  const title = lang === 'fr' ? 'Confirmation' : 'Confirmation';
5592  const message = lang === 'fr' ? 'Effacer toutes les zones peintes ?' : 'Clear all paint?';
5593  
5594  showCustomConfirm(title, message, (confirmed) => {
5595    if (confirmed) {
5596      dislikedVoxels.clear();
5597      likedVoxels.clear();
5598
5599      // Clear painted voxel features array and index
5600      paintedVoxelFeatures = [];
5601      paintedVoxelIndex.clear();
5602
5603      allVoxelFeatures.forEach(voxel => {
5604        if (voxel.properties.tagged) {
5605          voxel.properties.color = voxel.properties.originalColor;
5606          delete voxel.properties.tagged;
5607          delete voxel.properties.tagType;
5608        }
5609      });
5610
5611      // Update both sources immediately
5612      updatePaintedVoxelsSource();
5613      updateVoxelColors();
5614
5615      groundPaintMarkers.forEach(paintId => {
5616        if (map.getLayer(paintId)) {
5617          map.removeLayer(paintId);
5618          map.removeSource(paintId);
5619        }
5620      });
5621      groundPaintMarkers = [];
5622      groundPaintIdCounter = 0; // Reset ground paint ID counter
5623      window.paintLimitNotified = false; // Reset paint limit notification
5624
5625      // Remove overlays
5626      paintedOverlays.forEach(overlayId => {
5627        try {
5628          if (map.getLayer(overlayId)) map.removeLayer(overlayId);
5629          if (map.getSource(overlayId)) map.removeSource(overlayId);
5630        } catch(e) {}
5631      });
5632      paintedOverlays = [];
5633
5634      // Remove split divider lines
5635      splitDividerLines.forEach(lineId => {
5636        try {
5637          if (map.getLayer(lineId)) map.removeLayer(lineId);
5638          if (map.getSource(lineId)) map.removeSource(lineId);
5639        } catch(e) {}
5640      });
5641      splitDividerLines = [];
5642    }
5643  });
5644});
5645
5646// Next to cluster tagging
5647document.getElementById('next-to-tag-clusters-btn').addEventListener('click', () => {
5648  // Check if there are any painted zones BEFORE transitioning to Step 3
5649  const hasPaintedVoxels = dislikedVoxels.size > 0 || likedVoxels.size > 0;
5650  const hasGroundPolygons = groundPaintMarkers.length > 0;
5651
5652  if (!hasPaintedVoxels && !hasGroundPolygons) {
5653    const title = currentLang === 'fr' ? 'Avis' : 'Notice';
5654    const message = currentLang === 'fr' ? 'Aucune zone peinte trouvée.' : 'No painted areas found.';
5655    showCustomAlert(title, message);
5656    return; // Stay on Step 2
5657  }
5658
5659  document.getElementById('step2-paint').classList.add('hidden');
5660  document.getElementById('step3-tag-clusters').classList.remove('hidden');
5661
5662  // Scroll panel to top on mobile
5663  const mainPanel = document.querySelector('.main-panel');
5664  if (mainPanel) {
5665    mainPanel.scrollTop = 0;
5666  }
5667
5668  // Update step
5669  currentStep = 3;
5670
5671  // Update progress and show tutorial
5672  updateProgress(3);
5673  showTutorial(2);
5674
5675  // Update info tooltip for new step
5676  if (typeof updateInfoTooltip === 'function') updateInfoTooltip();
5677
5678  // Reset tool to cursor
5679  if (currentTool !== 'cursor') {
5680    document.getElementById('tool-cursor').click();
5681  }
5682
5683  detectAndDisplayClusters();
5684});
5685
5686// Improved cluster detection with edge-to-edge distance for ground circles
5687function detectAndDisplayClusters() {
5688  const CLUSTER_DISTANCE_KM = 0.020; // 20m edge-to-edge distance for merging clusters
5689  
5690  console.log('Starting cluster detection...');
5691  
5692  const dislikedPoints = [];
5693  dislikedVoxels.forEach(idx => {
5694    const voxel = allVoxelFeatures[idx];
5695    if (!voxel._center) {
5696      voxel._center = turf.center(voxel).geometry.coordinates;
5697    }
5698    dislikedPoints.push({ coords: voxel._center, type: 'disliked', idx, isVoxel: true });
5699  });
5700  
5701  // FIX 1: Add ground polygons to clustering
5702  groundPaintMarkers.forEach(paintId => {
5703    const source = map.getSource(paintId);
5704    if (!source) return;
5705    const data = source._data;
5706    if (data.properties.type === 'disliked') {
5707      const center = turf.center(data).geometry.coordinates;
5708      const area = turf.area(data);
5709      const radius = Math.sqrt(area / Math.PI) / 1000; // radius in km
5710      dislikedPoints.push({ 
5711        coords: center, 
5712        type: 'disliked', 
5713        idx: paintId, 
5714        isVoxel: false,
5715        area: area,
5716        radius: radius,
5717        geometry: data.geometry
5718      });
5719    }
5720  });
5721  
5722  const likedPoints = [];
5723  likedVoxels.forEach(idx => {
5724    const voxel = allVoxelFeatures[idx];
5725    if (!voxel._center) {
5726      voxel._center = turf.center(voxel).geometry.coordinates;
5727    }
5728    likedPoints.push({ coords: voxel._center, type: 'liked', idx, isVoxel: true });
5729  });
5730  
5731  // FIX 1: Add ground polygons to clustering
5732  groundPaintMarkers.forEach(paintId => {
5733    const source = map.getSource(paintId);
5734    if (!source) return;
5735    const data = source._data;
5736    if (data.properties.type === 'liked') {
5737      const center = turf.center(data).geometry.coordinates;
5738      const area = turf.area(data);
5739      const radius = Math.sqrt(area / Math.PI) / 1000; // radius in km
5740      likedPoints.push({ 
5741        coords: center, 
5742        type: 'liked', 
5743        idx: paintId, 
5744        isVoxel: false,
5745        area: area,
5746        radius: radius,
5747        geometry: data.geometry
5748      });
5749    }
5750  });
5751  
5752  console.log(`Clustering ${dislikedPoints.length} disliked and ${likedPoints.length} liked points`);
5753  
5754  const dislikedClusters = clusterPointsDBSCAN(dislikedPoints, CLUSTER_DISTANCE_KM);
5755  const likedClusters = clusterPointsDBSCAN(likedPoints, CLUSTER_DISTANCE_KM);
5756  
5757  console.log(`Found ${dislikedClusters.length} disliked clusters and ${likedClusters.length} liked clusters`);
5758  
5759  // Build allClusters array
5760  allClusters = [];
5761  
5762  dislikedClusters.forEach((cluster, idx) => {
5763    const coords = cluster.map(p => p.coords);
5764    const avgLon = coords.reduce((sum, c) => sum + c[0], 0) / coords.length;
5765    const avgLat = coords.reduce((sum, c) => sum + c[1], 0) / coords.length;
5766
5767    allClusters.push({
5768      id: `disliked-${idx}`,
5769      type: 'disliked',
5770      label: `Disliked Area ${idx + 1}`,
5771      size: cluster.length,
5772      centroid: [avgLon, avgLat],
5773      points: cluster,
5774      splitCount: 0  // Track number of times this cluster has been split
5775    });
5776  });
5777
5778  likedClusters.forEach((cluster, idx) => {
5779    const coords = cluster.map(p => p.coords);
5780    const avgLon = coords.reduce((sum, c) => sum + c[0], 0) / coords.length;
5781    const avgLat = coords.reduce((sum, c) => sum + c[1], 0) / coords.length;
5782
5783    allClusters.push({
5784      id: `liked-${idx}`,
5785      type: 'liked',
5786      label: `Liked Area ${idx + 1}`,
5787      size: cluster.length,
5788      centroid: [avgLon, avgLat],
5789      points: cluster,
5790      splitCount: 0  // Track number of times this cluster has been split
5791    });
5792  });
5793  
5794  if (allClusters.length === 0) {
5795    const title = currentLang === 'fr' ? 'Avis' : 'Notice';
5796    const message = currentLang === 'fr' ? 'Aucune zone peinte trouvée.' : 'No painted areas found.';
5797    showCustomAlert(title, message);
5798    document.getElementById('step3-tag-clusters').classList.add('hidden');
5799    document.getElementById('step2-paint').classList.remove('hidden');
5800    currentStep = 2;
5801    updateProgress(2);
5802    if (typeof updateInfoTooltip === 'function') updateInfoTooltip();
5803    return;
5804  }
5805  
5806  // Initialize navigation handlers
5807  initStep3Navigation();
5808  
5809  // Start with first cluster
5810  currentClusterIndex = 0;
5811  showCluster(0);
5812}
5813
5814// Optimized DBSCAN-like clustering with spatial indexing
5815function clusterPointsDBSCAN(points, maxDistance) {
5816  if (points.length === 0) return [];
5817  
5818  // Build spatial index for fast neighbor lookup
5819  const spatialGrid = new Map();
5820  const gridSize = maxDistance; // Grid cell size = cluster distance
5821  
5822  points.forEach((point, idx) => {
5823    const [lon, lat] = point.coords;
5824    const gridX = Math.floor(lon / gridSize);
5825    const gridY = Math.floor(lat / gridSize);
5826    const key = `${gridX},${gridY}`;
5827    
5828    if (!spatialGrid.has(key)) {
5829      spatialGrid.set(key, []);
5830    }
5831    spatialGrid.get(key).push({ ...point, originalIdx: idx });
5832  });
5833  
5834  // DBSCAN clustering
5835  const clusters = [];
5836  const visited = new Set();
5837  const clustered = new Set();
5838  
5839  function getNeighbors(pointIdx) {
5840    const point = points[pointIdx];
5841    const [lon, lat] = point.coords;
5842    const gridX = Math.floor(lon / gridSize);
5843    const gridY = Math.floor(lat / gridSize);
5844    
5845    const neighbors = [];
5846    
5847    // Check surrounding cells (3x3 grid)
5848    for (let dx = -1; dx <= 1; dx++) {
5849      for (let dy = -1; dy <= 1; dy++) {
5850        const key = `${gridX + dx},${gridY + dy}`;
5851        const cellPoints = spatialGrid.get(key);
5852        
5853        if (cellPoints) {
5854          cellPoints.forEach(cellPoint => {
5855            const centerDist = turf.distance(point.coords, cellPoint.coords, { units: 'kilometers' });
5856            
5857            // Calculate edge-to-edge distance for ground paint circles
5858            let effectiveDistance = centerDist;
5859            
5860            // If both points are ground paint circles, subtract their radii
5861            if (point.isVoxel === false && point.radius && cellPoint.radius) {
5862              effectiveDistance = Math.max(0, centerDist - point.radius - cellPoint.radius);
5863            }
5864            // If one is a ground circle, subtract its radius
5865            else if (point.isVoxel === false && point.radius) {
5866              effectiveDistance = Math.max(0, centerDist - point.radius);
5867            }
5868            else if (cellPoint.radius && cellPoint.isVoxel === false) {
5869              effectiveDistance = Math.max(0, centerDist - cellPoint.radius);
5870            }
5871            
5872            if (effectiveDistance <= maxDistance && cellPoint.originalIdx !== pointIdx) {
5873              neighbors.push(cellPoint.originalIdx);
5874            }
5875          });
5876        }
5877      }
5878    }
5879    
5880    return neighbors;
5881  }
5882  
5883  function expandCluster(startIdx, neighbors, cluster) {
5884    cluster.push(points[startIdx]);
5885    clustered.add(startIdx);
5886    
5887    const queue = [...neighbors];
5888    const inQueue = new Set(neighbors);
5889    
5890    while (queue.length > 0) {
5891      const currentIdx = queue.shift();
5892      
5893      if (visited.has(currentIdx)) continue;
5894      visited.add(currentIdx);
5895      
5896      if (!clustered.has(currentIdx)) {
5897        cluster.push(points[currentIdx]);
5898        clustered.add(currentIdx);
5899      }
5900      
5901      const currentNeighbors = getNeighbors(currentIdx);
5902      
5903      currentNeighbors.forEach(neighborIdx => {
5904        if (!inQueue.has(neighborIdx) && !clustered.has(neighborIdx)) {
5905          queue.push(neighborIdx);
5906          inQueue.add(neighborIdx);
5907        }
5908      });
5909    }
5910  }
5911  
5912  // Process all points
5913  for (let i = 0; i < points.length; i++) {
5914    if (clustered.has(i)) continue;
5915    
5916    visited.add(i);
5917    const neighbors = getNeighbors(i);
5918    
5919    // Start new cluster
5920    const cluster = [];
5921    expandCluster(i, neighbors, cluster);
5922    
5923    if (cluster.length > 0) {
5924      clusters.push(cluster);
5925    }
5926  }
5927  
5928  return clusters;
5929}
5930
5931function showCluster(index) {
5932  const cluster = allClusters[index];
5933
5934  // Scroll panel to top when showing new cluster
5935  const mainPanel = document.querySelector('.main-panel');
5936  if (mainPanel) {
5937    mainPanel.scrollTop = 0;
5938  }
5939
5940  document.getElementById('cluster-number').textContent = index + 1;
5941  document.getElementById('cluster-total').textContent = allClusters.length;
5942  
5943  const badge = document.getElementById('cluster-type-badge');
5944  const badgeText = cluster.type === 'disliked' 
5945    ? (currentLang === 'fr' ? 'Zone négative' : 'Disliked Area')
5946    : (currentLang === 'fr' ? 'Zone positive' : 'Liked Area');
5947  badge.textContent = badgeText;
5948  badge.className = `cluster-type-badge ${cluster.type}`;
5949  
5950  document.getElementById('cluster-size').textContent = `${cluster.size} voxels`;
5951  
5952  // Calculate ground area from the cluster's ground paint points
5953  let groundArea = 0;
5954  let voxelCount = 0;
5955  const groundGeometries = [];
5956  
5957  if (cluster.points && cluster.points.length > 0) {
5958    cluster.points.forEach(point => {
5959      if (point.isVoxel === false && point.geometry) {
5960        // Collect geometries for union
5961        groundGeometries.push(point.geometry);
5962      } else if (point.isVoxel === true) {
5963        voxelCount++;
5964      }
5965    });
5966  }
5967  
5968  // Calculate union to get actual overlaid area
5969  if (groundGeometries.length > 0) {
5970    console.log(`=== AREA CALCULATION for cluster ${cluster.id} ===`);
5971    console.log(`Ground geometries count: ${groundGeometries.length}`);
5972    
5973    try {
5974      if (groundGeometries.length === 1) {
5975        // Single geometry
5976        groundArea = turf.area(groundGeometries[0]);
5977        console.log(`Single geometry area: ${Math.round(groundArea)} m²`);
5978      } else {
5979        // Properly union overlapping circles
5980        console.log('Starting union of overlapping circles...');
5981        
5982        let union = turf.feature(groundGeometries[0]);
5983        let successfulUnions = 1;
5984        
5985        for (let i = 1; i < groundGeometries.length; i++) {
5986          try {
5987            const nextFeature = turf.feature(groundGeometries[i]);
5988            const result = turf.union(union, nextFeature);
5989            
5990            if (result && result.geometry) {
5991              union = result;
5992              successfulUnions++;
5993            } else {
5994              console.warn(`Union ${i} returned null, skipping`);
5995            }
5996          } catch (unionError) {
5997            console.warn(`Union failed at geometry ${i}:`, unionError);
5998          }
5999        }
6000        
6001        if (union && union.geometry) {
6002          groundArea = turf.area(union.geometry);
6003          console.log(`Successfully unioned ${successfulUnions}/${groundGeometries.length} geometries`);
6004          console.log(`Final union area: ${Math.round(groundArea)} m²`);
6005          
6006          // Update cluster centroid to be center of the union
6007          const unionCentroid = turf.centroid(union);
6008          cluster.centroid = unionCentroid.geometry.coordinates;
6009        } else {
6010          // Fallback: use first circle only
6011          groundArea = turf.area(groundGeometries[0]);
6012          console.log(`Union failed, using first geometry: ${Math.round(groundArea)} m²`);
6013        }
6014      }
6015    } catch (error) {
6016      console.error('Error calculating ground area:', error);
6017      // Fallback to first geometry
6018      if (groundGeometries.length > 0) {
6019        groundArea = turf.area(groundGeometries[0]);
6020        console.log(`Error fallback area: ${Math.round(groundArea)} m²`);
6021      }
6022    }
6023    console.log(`=== END AREA CALCULATION ===`);
6024  }
6025  
6026  // Update display with ground area if available
6027  const sizeText = currentLang === 'fr' 
6028    ? `${voxelCount} voxels${groundArea > 0 ? ` • ${Math.round(groundArea)} m²` : ''}` 
6029    : `${voxelCount} voxels${groundArea > 0 ? ` • ${Math.round(groundArea)} m²` : ''}`;
6030  document.getElementById('cluster-size').textContent = sizeText;
6031  
6032  renderTagCategories(cluster);
6033  showClusterMarker(cluster.centroid, cluster.type);
6034  
6035  document.getElementById('prev-cluster-btn').disabled = (index === 0);
6036  const nextBtn = document.getElementById('next-cluster-btn');
6037  const nextText = currentLang === 'fr' ? 'Suivant →' : 'Next →';
6038  const finishText = currentLang === 'fr' ? 'Terminer →' : 'Finish →';
6039  nextBtn.innerHTML = `<span data-en="${index === allClusters.length - 1 ? 'Finish →' : 'Next →'}" data-fr="${index === allClusters.length - 1 ? 'Terminer →' : 'Suivant →'}">${index === allClusters.length - 1 ? finishText : nextText}</span>`;
6040
6041  // Update split section visibility based on split status
6042  const splitSection = document.querySelector('.split-section');
6043  const splitBtn = document.getElementById('split-cluster-btn');
6044
6045  if (cluster.splitCount >= 1) {
6046    // Already split - show cancel split button in orange
6047    splitSection.style.display = 'block';
6048    const cancelText = currentLang === 'fr' ? 'Annuler la division' : 'Cancel Split';
6049    splitBtn.innerHTML = `<span data-en="Cancel Split" data-fr="Annuler la division">${cancelText}</span>`;
6050    splitBtn.dataset.action = 'cancel';
6051    splitBtn.style.background = '#ff9800';
6052    splitBtn.style.borderColor = '#ff9800';
6053    splitBtn.style.color = 'white';
6054    splitBtn.style.borderStyle = 'solid';
6055  } else {
6056    // Not split - show split section
6057    splitSection.style.display = 'block';
6058    const splitText = currentLang === 'fr' ? 'Diviser automatiquement' : 'Split Automatically';
6059    splitBtn.innerHTML = `<span data-en="Split Automatically" data-fr="Diviser automatiquement">${splitText}</span>`;
6060    splitBtn.dataset.action = 'split';
6061    splitBtn.style.background = '#fff3e0';
6062    splitBtn.style.borderColor = '#ff9800';
6063    splitBtn.style.color = '#ff9800';
6064    splitBtn.style.borderStyle = 'dashed';
6065  }
6066}
6067
6068function renderTagCategories(cluster) {
6069  const container = document.getElementById('tag-categories-container');
6070  container.innerHTML = '';
6071
6072  const savedData = clusterLabels[cluster.id] || { tags: [], comment: '', openCategory: null };
6073
6074  // Show only positive for liked clusters, only negative for disliked
6075  const showPositive = (cluster.type === 'liked');
6076
6077  // Update instruction text based on cluster type
6078  const instructionDiv = document.getElementById('tag-instruction-text');
6079  if (showPositive) {
6080    const textFr = 'Sélectionnez tous les attributs applicables parmi la liste de <strong style="color: #34a853;">critères positifs</strong> suivants:';
6081    const textEn = 'Select all applicable attributes from the following list of <strong style="color: #34a853;">positive criteria</strong>:';
6082    instructionDiv.innerHTML = `<span>${currentLang === 'fr' ? textFr : textEn}</span>`;
6083  } else {
6084    const textFr = 'Sélectionnez tous les attributs applicables parmi la liste de <strong style="color: #ea4335;">critères négatifs</strong> suivants:';
6085    const textEn = 'Select all applicable attributes from the following list of <strong style="color: #ea4335;">negative criteria</strong>:';
6086    instructionDiv.innerHTML = `<span>${currentLang === 'fr' ? textFr : textEn}</span>`;
6087  }
6088
6089  // Use the correct language version
6090  const tagOptions = currentLang === 'fr' ? TAG_OPTIONS_FR : TAG_OPTIONS;
6091  
6092  Object.entries(tagOptions).forEach(([category, options], index) => {
6093    const categoryDiv = document.createElement('div');
6094    const isOpen = (savedData.openCategory === category);
6095    categoryDiv.className = isOpen ? 'tag-category' : 'tag-category collapsed';
6096    categoryDiv.dataset.category = category;
6097    
6098    let html = `
6099      <div class="tag-category-header">
6100        <div class="tag-category-title">
6101          <span>${category}</span>
6102        </div>
6103        <span class="tag-category-arrow">▼</span>
6104      </div>
6105      <div class="tag-options-group">
6106    `;
6107    
6108    // Show only relevant options based on cluster type
6109    const relevantOptions = showPositive ? options.positive : options.negative;
6110    const optionClass = showPositive ? 'positive' : 'negative';
6111    
6112    relevantOptions.forEach(option => {
6113      const isSelected = savedData.tags.includes(option);
6114      
6115      // Check if this is a seasonal tag and extract season
6116      let displayText = option;
6117      let seasonBadge = '';
6118      const seasonMatch = option.match(/^(WINTER|SPRING|SUMMER|FALL|HIVER|PRINTEMPS|ÉTÉ|AUTOMNE)\s+(.+)$/i);
6119      
6120      if (seasonMatch) {
6121        const season = seasonMatch[1].toUpperCase();
6122        displayText = seasonMatch[2];
6123        
6124        // Map season to badge class and reorder: Spring, Summer, Fall, Winter
6125        const seasonMap = {
6126          'SPRING': { class: 'spring', text: 'SPRING' },
6127          'PRINTEMPS': { class: 'spring', text: 'PRINTEMPS' },
6128          'SUMMER': { class: 'summer', text: 'SUMMER' },
6129          'ÉTÉ': { class: 'summer', text: 'ÉTÉ' },
6130          'FALL': { class: 'fall', text: 'FALL' },
6131          'AUTOMNE': { class: 'fall', text: 'AUTOMNE' },
6132          'WINTER': { class: 'winter', text: 'WINTER' },
6133          'HIVER': { class: 'winter', text: 'HIVER' }
6134        };
6135        
6136        const seasonInfo = seasonMap[season];
6137        if (seasonInfo) {
6138          seasonBadge = `<span class="season-badge ${seasonInfo.class}">${seasonInfo.text}</span>`;
6139        }
6140      }
6141      
6142      html += `
6143        <div class="tag-option-item ${optionClass} ${isSelected ? 'selected' : ''}" data-tag="${option}">
6144          <input type="checkbox" class="tag-option-checkbox" ${isSelected ? 'checked' : ''} />
6145          <span class="tag-option-label">${seasonBadge}${displayText}</span>
6146        </div>
6147      `;
6148    });
6149    
6150    html += '</div>';
6151    categoryDiv.innerHTML = html;
6152    container.appendChild(categoryDiv);
6153    
6154    // Add click handler for header to toggle
6155    const header = categoryDiv.querySelector('.tag-category-header');
6156    header.addEventListener('click', () => {
6157      // Close all other categories
6158      container.querySelectorAll('.tag-category').forEach(cat => {
6159        if (cat !== categoryDiv) {
6160          cat.classList.add('collapsed');
6161        }
6162      });
6163      
6164      // Toggle this one
6165      const wasCollapsed = categoryDiv.classList.contains('collapsed');
6166      categoryDiv.classList.toggle('collapsed');
6167      
6168      // Save open category
6169      if (!categoryDiv.classList.contains('collapsed')) {
6170        savedData.openCategory = category;
6171        clusterLabels[cluster.id] = savedData;
6172      }
6173    });
6174  });
6175  
6176  // Auto-save on change
6177  container.querySelectorAll('.tag-option-item').forEach(item => {
6178    item.addEventListener('click', (e) => {
6179      const checkbox = item.querySelector('input[type="checkbox"]');
6180      if (e.target !== checkbox) {
6181        checkbox.checked = !checkbox.checked;
6182      }
6183      item.classList.toggle('selected', checkbox.checked);
6184      autoSaveCurrentCluster();
6185    });
6186  });
6187  
6188  document.getElementById('current-cluster-comment').value = savedData.comment || '';
6189  
6190  // Auto-save comment (remove old listener first to avoid duplicates)
6191  const commentBox = document.getElementById('current-cluster-comment');
6192  const commentHandler = () => autoSaveCurrentCluster();
6193  commentBox.removeEventListener('input', commentHandler);
6194  commentBox.addEventListener('input', commentHandler);
6195}
6196
6197function autoSaveCurrentCluster() {
6198  const cluster = allClusters[currentClusterIndex];
6199  
6200  const selectedTags = [];
6201  document.querySelectorAll('.tag-option-item input:checked').forEach(checkbox => {
6202    const item = checkbox.closest('.tag-option-item');
6203    selectedTags.push(item.dataset.tag);
6204  });
6205  
6206  const comment = document.getElementById('current-cluster-comment').value.trim();
6207  
6208  // Find which category is open
6209  let openCategory = null;
6210  document.querySelectorAll('.tag-category').forEach(cat => {
6211    if (!cat.classList.contains('collapsed')) {
6212      openCategory = cat.dataset.category;
6213    }
6214  });
6215  
6216  clusterLabels[cluster.id] = {
6217    tags: selectedTags,
6218    comment: comment,
6219    openCategory: openCategory
6220  };
6221}
6222
6223// Initialize Step 3 navigation (call once when Step 3 is first shown)
6224let step3NavigationInitialized = false;
6225
6226function initStep3Navigation() {
6227  if (step3NavigationInitialized) return;
6228  
6229  document.getElementById('prev-cluster-btn').addEventListener('click', () => {
6230    currentClusterIndex--;
6231    showCluster(currentClusterIndex);
6232  });
6233
6234  document.getElementById('next-cluster-btn').addEventListener('click', () => {
6235    if (currentClusterIndex < allClusters.length - 1) {
6236      currentClusterIndex++;
6237      showCluster(currentClusterIndex);
6238    } else {
6239      // Show custom confirmation before submitting
6240      const confirmTitle = currentLang === 'fr'
6241        ? 'Êtes-vous prêt(e) à soumettre votre évaluation ?'
6242        : 'Are you ready to submit your assessment?';
6243
6244      const confirmMessage = currentLang === 'fr'
6245        ? 'Vous avez étiqueté tous les clusters. Une fois soumis, vous ne pourrez pas modifier vos réponses.'
6246        : 'You have labeled all clusters. Once submitted, you cannot modify your responses.';
6247
6248      showCustomConfirm(confirmTitle, confirmMessage, (confirmed) => {
6249        if (confirmed) {
6250          // Submit after confirmation
6251          updateProgress(4);
6252
6253          // Hide main panel
6254          const mainPanel = document.querySelector('.main-panel');
6255          if (mainPanel) mainPanel.style.display = 'none';
6256
6257          // Trigger submission directly
6258          document.getElementById('submit-btn').click();
6259        }
6260      });
6261    }
6262  });
6263
6264  // Skip button removed - users must label all clusters
6265  // document.getElementById('skip-cluster-btn').addEventListener('click', () => {
6266  //   const cluster = allClusters[currentClusterIndex];
6267  //   clusterLabels[cluster.id] = { tags: [], comment: '' };
6268  //
6269  //   if (currentClusterIndex < allClusters.length - 1) {
6270  //     currentClusterIndex++;
6271  //     showCluster(currentClusterIndex);
6272  //   } else {
6273  //     clusterMarkers.forEach(marker => marker.remove());
6274  //     clusterMarkers = [];
6275  //
6276  //     // Remove split divider lines
6277  //     splitDividerLines.forEach(lineId => {
6278  //       try {
6279  //         if (map.getLayer(lineId)) map.removeLayer(lineId);
6280  //         if (map.getSource(lineId)) map.removeSource(lineId);
6281  //       } catch(e) {}
6282  //     });
6283  //     splitDividerLines = [];
6284  //
6285  //     document.getElementById('step3-tag-clusters').classList.add('hidden');
6286  //     document.getElementById('step4-submit').classList.remove('hidden');
6287  //
6288  //     // Update progress and show tutorial
6289  //     updateProgress(4);
6290  //     showTutorial(3);
6291  //   }
6292  // });
6293  
6294  step3NavigationInitialized = true;
6295
6296  // Split cluster functionality - direct split into 2 groups
6297
6298  // K-means clustering for exactly 2 clusters
6299  function kMeansSplit(points) {
6300    if (points.length < 2) return [points];
6301
6302    // Find the two furthest points as initial centroids (poles)
6303    let maxDist = 0;
6304    let pole1Idx = 0;
6305    let pole2Idx = 1;
6306
6307    for (let i = 0; i < points.length; i++) {
6308      for (let j = i + 1; j < points.length; j++) {
6309        const dist = turf.distance(points[i].coords, points[j].coords);
6310        if (dist > maxDist) {
6311          maxDist = dist;
6312          pole1Idx = i;
6313          pole2Idx = j;
6314        }
6315      }
6316    }
6317
6318    let centroid1 = [...points[pole1Idx].coords];
6319    let centroid2 = [...points[pole2Idx].coords];
6320
6321    // Run k-means with fixed iterations
6322    const maxIterations = 10;
6323
6324    for (let iter = 0; iter < maxIterations; iter++) {
6325      // Assign points to nearest centroid
6326      const cluster1 = [];
6327      const cluster2 = [];
6328
6329      points.forEach(point => {
6330        const dist1 = turf.distance(point.coords, centroid1);
6331        const dist2 = turf.distance(point.coords, centroid2);
6332
6333        if (dist1 < dist2) {
6334          cluster1.push(point);
6335        } else {
6336          cluster2.push(point);
6337        }
6338      });
6339
6340      // If one cluster is empty, force a split
6341      if (cluster1.length === 0 || cluster2.length === 0) {
6342        const mid = Math.floor(points.length / 2);
6343        return [points.slice(0, mid), points.slice(mid)];
6344      }
6345
6346      // Recalculate centroids
6347      const newCentroid1 = [
6348        cluster1.reduce((sum, p) => sum + p.coords[0], 0) / cluster1.length,
6349        cluster1.reduce((sum, p) => sum + p.coords[1], 0) / cluster1.length
6350      ];
6351      const newCentroid2 = [
6352        cluster2.reduce((sum, p) => sum + p.coords[0], 0) / cluster2.length,
6353        cluster2.reduce((sum, p) => sum + p.coords[1], 0) / cluster2.length
6354      ];
6355
6356      // Check convergence
6357      const shift1 = turf.distance(centroid1, newCentroid1);
6358      const shift2 = turf.distance(centroid2, newCentroid2);
6359
6360      centroid1 = newCentroid1;
6361      centroid2 = newCentroid2;
6362
6363      // Early stop if converged (centroids barely moved)
6364      if (shift1 < 0.001 && shift2 < 0.001) {
6365        break;
6366      }
6367    }
6368
6369    // Final assignment
6370    const cluster1 = [];
6371    const cluster2 = [];
6372
6373    points.forEach(point => {
6374      const dist1 = turf.distance(point.coords, centroid1);
6375      const dist2 = turf.distance(point.coords, centroid2);
6376
6377      if (dist1 < dist2) {
6378        cluster1.push(point);
6379      } else {
6380        cluster2.push(point);
6381      }
6382    });
6383
6384    return [cluster1, cluster2].filter(c => c.length > 0);
6385  }
6386
6387  // Split section toggle
6388  document.getElementById('split-section-toggle').addEventListener('click', () => {
6389    const header = document.getElementById('split-section-toggle');
6390    const content = document.getElementById('split-section-content');
6391
6392    header.classList.toggle('open');
6393    content.classList.toggle('open');
6394  });
6395
6396  // Info icon for split section
6397  document.getElementById('split-info-icon').addEventListener('click', (e) => {
6398    e.stopPropagation(); // Prevent toggling the accordion
6399
6400    const lang = currentLang || 'fr';
6401    const title = lang === 'fr' ? 'Division automatique' : 'Automatic Division';
6402    const message = lang === 'fr'
6403      ? '<p><strong>Division automatique :</strong> Les zones sont automatiquement divisées en détectant les concentrations de marqueurs.</p><p><strong>Pour modifier la division :</strong> Retournez à l\'écran précédent et effacez les marqueurs entre les éléments que vous souhaitez séparer, ou ajoutez des marqueurs pour fusionner deux groupes en un seul.</p>'
6404      : '<p><strong>Automatic division:</strong>
6404 Areas are automatically divided by detecting marker concentrations.</p><p><strong>To modify the division:</strong> Go back to the previous screen and erase markers between items you want to separate, or add markers to merge two groups into one.</p>';
6405
6406    showCustomAlert(title, message);
6407  });
6408
6409  document.getElementById('split-cluster-btn').addEventListener('click', () => {
6410    const cluster = allClusters[currentClusterIndex];
6411    if (!cluster || !cluster.points) return;
6412
6413    const btn = document.getElementById('split-cluster-btn');
6414    const action = btn.dataset.action;
6415
6416    // Add pulse animation to button
6417    btn.style.animation = 'pulse 0.3s';
6418    setTimeout(() => btn.style.animation = '', 300);
6419
6420    // If action is cancel, revert the split
6421    if (action === 'cancel') {
6422      console.log('=== CANCEL SPLIT ===');
6423      console.log('Current cluster ID:', cluster.id);
6424      console.log('All clusters before:', allClusters.map(c => c.id));
6425
6426      // Extract parent ID from current cluster
6427      const parentId = cluster.id.split('-split-')[0];
6428      console.log('Parent ID:', parentId);
6429
6430      // Find ALL split siblings (including current cluster)
6431      const splitSiblings = allClusters.filter(c => c.id.startsWith(parentId + '-split-'));
6432      console.log('Split siblings found:', splitSiblings.map(s => s.id));
6433
6434      if (splitSiblings.length > 0) {
6435        // Find the position of the first split sibling
6436        const firstSplitIndex = allClusters.findIndex(c => c.id.startsWith(parentId + '-split-'));
6437        console.log('First split index:', firstSplitIndex);
6438
6439        // Merge all split clusters back into original
6440        const allPoints = [];
6441        splitSiblings.forEach(sibling => {
6442          allPoints.push(...sibling.points);
6443          // Remove cluster labels
6444          if (clusterLabels[sibling.id]) {
6445            delete clusterLabels[sibling.id];
6446          }
6447        });
6448
6449        // Remove ALL split siblings from allClusters
6450        allClusters = allClusters.filter(c => !c.id.startsWith(parentId + '-split-'));
6451        console.log('Clusters after removal:', allClusters.map(c => c.id));
6452
6453        // Remove split divider lines
6454        splitDividerLines.forEach(lineId => {
6455          if (map.getLayer(lineId)) map.removeLayer(lineId);
6456          if (map.getSource(lineId)) map.removeSource(lineId);
6457        });
6458        splitDividerLines = [];
6459
6460        // Recreate original merged cluster
6461        const coords = allPoints.map(p => p.coords);
6462        const avgLon = coords.reduce((sum, c) => sum + c[0], 0) / coords.length;
6463        const avgLat = coords.reduce((sum, c) => sum + c[1], 0) / coords.length;
6464
6465        const mergedCluster = {
6466          id: parentId,
6467          type: cluster.type,
6468          label: cluster.type === 'liked' ? 'Liked Area' : 'Disliked Area',
6469          size: allPoints.length,
6470          centroid: [avgLon, avgLat],
6471          points: allPoints,
6472          splitCount: 0
6473        };
6474
6475        // Insert merged cluster at the position where splits were
6476        const insertIndex = Math.min(firstSplitIndex, allClusters.length);
6477        allClusters.splice(insertIndex, 0, mergedCluster);
6478
6479        // Update current index
6480        currentClusterIndex = insertIndex;
6481
6482        console.log('Merged cluster created:', mergedCluster.id);
6483        console.log('All clusters after merge:', allClusters.map(c => c.id));
6484        console.log('Total clusters:', allClusters.length);
6485        console.log('Current index:', currentClusterIndex);
6486
6487        // Update display
6488        showCluster(currentClusterIndex);
6489      }
6490      return;
6491    }
6492
6493    // Split using k-means into 2 clusters
6494    const newClusters = kMeansSplit(cluster.points);
6495
6496    console.log(`Splitting cluster ${cluster.id} into ${newClusters.length} new clusters`);
6497
6498    // Draw dividing line between the two split clusters
6499    if (newClusters.length === 2) {
6500      const cluster1Points = newClusters[0];
6501      const cluster2Points = newClusters[1];
6502
6503      // Calculate centroids of each cluster
6504      const centroid1 = [
6505        cluster1Points.reduce((sum, p) => sum + p.coords[0], 0) / cluster1Points.length,
6506        cluster1Points.reduce((sum, p) => sum + p.coords[1], 0) / cluster1Points.length
6507      ];
6508      const centroid2 = [
6509        cluster2Points.reduce((sum, p) => sum + p.coords[0], 0) / cluster2Points.length,
6510        cluster2Points.reduce((sum, p) => sum + p.coords[1], 0) / cluster2Points.length
6511      ];
6512
6513      // Get all points from original cluster to find bounds
6514      const allPoints = [...cluster1Points, ...cluster2Points];
6515      const allCoords = allPoints.map(p => p.coords);
6516
6517      // Find min/max to determine cluster bounds
6518      const lons = allCoords.map(c => c[0]);
6519      const lats = allCoords.map(c => c[1]);
6520      const minLon = Math.min(...lons);
6521      const maxLon = Math.max(...lons);
6522      const minLat = Math.min(...lats);
6523      const maxLat = Math.max(...lats);
6524
6525      // Calculate midpoint between centroids
6526      const midpoint = [
6527        (centroid1[0] + centroid2[0]) / 2,
6528        (centroid1[1] + centroid2[1]) / 2
6529      ];
6530
6531      // Calculate perpendicular line to the line connecting centroids
6532      const dx = centroid2[0] - centroid1[0];
6533      const dy = centroid2[1] - centroid1[1];
6534
6535      // Perpendicular vector (rotate 90 degrees)
6536      const perpDx = -dy;
6537      const perpDy = dx;
6538
6539      // Normalize
6540      const length = Math.sqrt(perpDx * perpDx + perpDy * perpDy);
6541      const normPerpDx = perpDx / length;
6542      const normPerpDy = perpDy / length;
6543
6544      // Calculate line length to span the cluster bounds
6545      const clusterWidth = Math.max(maxLon - minLon, maxLat - minLat);
6546      const lineExtent = clusterWidth * 0.6; // 60% of cluster width
6547
6548      // Create line endpoints that stay within cluster area
6549      const lineStart = [
6550        midpoint[0] - normPerpDx * lineExtent,
6551        midpoint[1] - normPerpDy * lineExtent
6552      ];
6553      const lineEnd = [
6554        midpoint[0] + normPerpDx * lineExtent,
6555        midpoint[1] + normPerpDy * lineExtent
6556      ];
6557
6558      // Use orange color matching the split button
6559      const lineColor = '#ff9800';
6560
6561      // Create unique layer ID
6562      const lineLayerId = `split-divider-${Date.now()}`;
6563      splitDividerLines.push(lineLayerId);
6564
6565      // Add line to map
6566      map.addSource(lineLayerId, {
6567        type: 'geojson',
6568        data: {
6569          type: 'Feature',
6570          geometry: {
6571            type: 'LineString',
6572            coordinates: [lineStart, lineEnd]
6573          }
6574        }
6575      });
6576
6577      map.addLayer({
6578        id: lineLayerId,
6579        type: 'line',
6580        source: lineLayerId,
6581        paint: {
6582          'line-color': lineColor,
6583          'line-width': 8,
6584          'line-opacity': 1.0,
6585          'line-dasharray': [2, 2] // Dashed pattern
6586        }
6587      });
6588
6589      console.log(`Drew dashed split divider line between clusters`);
6590    }
6591
6592    // Create new cluster objects
6593    const newClusterObjects = [];
6594    newClusters.forEach((clusterPoints, idx) => {
6595      const coords = clusterPoints.map(p => p.coords);
6596      const avgLon = coords.reduce((sum, c) => sum + c[0], 0) / coords.length;
6597      const avgLat = coords.reduce((sum, c) => sum + c[1], 0) / coords.length;
6598
6599      newClusterObjects.push({
6600        id: `${cluster.id}-split-${idx}`,
6601        type: cluster.type,
6602        label: `${cluster.label} (Split ${idx + 1})`,
6603        size: clusterPoints.length,
6604        centroid: [avgLon, avgLat],
6605        points: clusterPoints,
6606        splitCount: (cluster.splitCount || 0) + 1  // Inherit and increment split count
6607      });
6608    });
6609
6610    // Remove original cluster from allClusters
6611    allClusters.splice(currentClusterIndex, 1);
6612
6613    // Insert new clusters at the same position
6614    allClusters.splice(currentClusterIndex, 0, ...newClusterObjects);
6615
6616    console.log(`New total clusters: ${allClusters.length}`);
6617
6618    // Show success message
6619    const successMsg = document.getElementById('split-success-msg');
6620    successMsg.classList.remove('hidden');
6621    setTimeout(() => {
6622      successMsg.classList.add('hidden');
6623    }, 2000);
6624
6625    // Update cluster counter in header
6626    document.getElementById('cluster-total').textContent = allClusters.length;
6627
6628    // Show the first of the new split clusters
6629    showCluster(currentClusterIndex);
6630  });
6631}
6632
6633function showClusterMarker(coords, type) {
6634  // Remove existing cluster markers
6635  clusterMarkers.forEach(marker => marker.remove());
6636  clusterMarkers = [];
6637
6638  // Create teardrop marker - MUCH BIGGER and positioned ABOVE cluster
6639  const el = document.createElement('div');
6640  el.style.width = '90px';
6641  el.style.height = '120px';
6642  el.style.position = 'relative';
6643  el.innerHTML = `
6644    <svg width="90" height="120" viewBox="0 0 30 40" style="filter: drop-shadow(0 4px 8px rgba(0,0,0,0.4));">
6645      <path d="M15,5 C10,5 5,10 5,15 C5,20 15,35 15,35 C15,35 25,20 25,15 C25,10 20,5 15,5 Z"
6646            fill="${type === 'disliked' ? '#ea4335' : '#34a853'}"
6647            stroke="white"
6648            stroke-width="2"/>
6649      <circle cx="15" cy="15" r="5" fill="white"/>
6650    </svg>
6651  `;
6652
6653  const marker = new mapboxgl.Marker({ element: el, anchor: 'bottom', offset: [0, -150] })
6654    .setLngLat(coords)
6655    .addTo(map);
6656
6657  clusterMarkers.push(marker);
6658
6659  // Fly to cluster with closer zoom
6660  // On mobile, zoom even closer to the cluster and account for panel at bottom
6661  const isMobile = window.innerWidth <= 768;
6662  const flyToOptions = {
6663    center: coords,
6664    zoom: isMobile ? 19.2 : 18.5, // Closer zoom on mobile
6665    pitch: 60, // Good 3D view
6666    duration: 1000
6667  };
6668
6669  // On mobile, add padding to shift center upward (panel is 60vh at bottom)
6670  if (isMobile) {
6671    flyToOptions.padding = { top: 0, bottom: window.innerHeight * 0.35, left: 0, right: 0 };
6672  }
6673
6674  map.flyTo(flyToOptions);
6675}
6676
6677// Next to submit (this button doesn't exist in the HTML, so commenting out)
6678// document.getElementById('next-to-submit-btn').addEventListener('click', () => {
6679//   clusterMarkers.forEach(marker => marker.remove());
6680//   clusterMarkers = [];
6681//   document.getElementById('step3-tag-clusters').classList.add('hidden');
6682//   document.getElementById('step4-submit').classList.remove('hidden');
6683// });
6684
6685// Submit to Backend
6686
6687// Step 4 back button
6688const step4BackBtn = document.getElementById('step4-back-btn');
6689if (step4BackBtn) {
6690  step4BackBtn.addEventListener('click', () => {
6691    // Go back to Step 3
6692    document.getElementById('step-4').classList.remove('active');
6693    document.getElementById('step-3').classList.add('active');
6694    currentStep = 3;
6695    if (typeof updateInfoTooltip === 'function') updateInfoTooltip();
6696  });
6697}
6698
6699document.getElementById('submit-btn').addEventListener('click', async () => {
6700  const submitBtn = document.getElementById('submit-btn');
6701  submitBtn.disabled = true;
6702  const submittingText = currentLang === 'fr' ? 'Envoi en cours...' : 'Submitting...';
6703  submitBtn.innerHTML = `<span>${submittingText}</span>`;
6704
6705  // Show loading overlay
6706  const loadingOverlay = document.getElementById('loading-overlay');
6707  const loadingText = loadingOverlay.querySelector('.loading-text span');
6708  loadingText.textContent = currentLang === 'fr' ? 'Envoi en cours...' : 'Submitting...';
6709  loadingOverlay.classList.remove('hidden');
6710
6711  try {
6712    // Collect ground polygons with calculated areas
6713    const groundPolygons = groundPaintMarkers.map(paintId => {
6714      const source = map.getSource(paintId);
6715      if (!source) return null;
6716      
6717      const data = source._data;
6718      
6719      // Calculate area in square meters using Turf
6720      let area = 0;
6721      try {
6722        area = turf.area(data.geometry);
6723      } catch (e) {
6724        console.error('Error calculating ground polygon area:', e);
6725      }
6726      
6727      return {
6728        type: data.properties.type,
6729        geometry: data.geometry, // GeoJSON polygon geometry
6730        center: data.geometry.coordinates[0][0], // First coordinate as approximate center
6731        color: data.properties.color,
6732        area: Math.round(area) // Area in square meters
6733      };
6734    }).filter(Boolean);
6735    
6736    // Prepare submission data in GeoJSON-compatible format
6737    console.log('🔍 DEBUG: selectedPoint at submission time:', selectedPoint);
6738    
6739    const submissionData = {
6740      sessionTimestamp: new Date().toISOString(),
6741      userLocation: selectedPoint ? {
6742        lng: selectedPoint.lng || selectedPoint[0],
6743        lat: selectedPoint.lat || selectedPoint[1]
6744      } : null,
6745      radiusMeters: 1000,
6746      dislikedVoxels: Array.from(dislikedVoxels),
6747      likedVoxels: Array.from(likedVoxels),
6748      groundPolygons: groundPolygons, // NEW: Include ground-painted areas
6749      clusters: allClusters.map(cluster => {
6750        const labels = clusterLabels[cluster.id] || { tags: [], comment: '' };
6751        
6752        // Calculate centroid from cluster points
6753        // Each point in cluster.points is an object: { coords: [lng, lat, height], type, idx }
6754        let avgLng = 0, avgLat = 0, avgHeight = 0;
6755        cluster.points.forEach(pt => {
6756          avgLng += pt.coords[0];
6757          avgLat += pt.coords[1];
6758          avgHeight += pt.coords[2] || 0;
6759        });
6760        const count = cluster.points.length;
6761        
6762        const centroid = {
6763          lng: avgLng / count,
6764          lat: avgLat / count,
6765          height: avgHeight / count
6766        };
6767        
6768        // Calculate total ground area for this cluster by finding overlapping ground polygons
6769        let totalGroundArea = 0;
6770        const centroidPoint = turf.point([centroid.lng, centroid.lat]);
6771        
6772        groundPolygons.forEach(polygon => {
6773          if (polygon && polygon.type === cluster.type) {
6774            try {
6775              // Check if the cluster centroid is within this ground polygon
6776              const isInside = turf.booleanPointInPolygon(centroidPoint, polygon.geometry);
6777              
6778              if (isInside) {
6779                // Calculate area of this polygon in square meters
6780                const area = turf.area(polygon.geometry);
6781                totalGroundArea += area;
6782              }
6783            } catch (e) {
6784              console.error('Error calculating ground area:', e);
6785            }
6786          }
6787        });
6788        
6789        return {
6790          id: cluster.id,
6791          type: cluster.type,
6792          voxels: cluster.points.map(pt => ({
6793            key: `${pt.coords[0].toFixed(6)}_${pt.coords[1].toFixed(6)}_${(pt.coords[2] || 0).toFixed(1)}`,
6794            lng: pt.coords[0],
6795            lat: pt.coords[1],
6796            height: pt.coords[2] || 0
6797          })),
6798          centroid: centroid,
6799          groundAreaM2: Math.round(totalGroundArea),
6800          tags: labels.tags,
6801          comment: labels.comment
6802        };
6803      })
6804    };
6805    
6806    // Send to backend with 30 second timeout
6807    const API_URL = 'https://tag-ta-ville-production.up.railway.app/
6807api/submissions';
6808
6809    const controller = new AbortController();
6810    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
6811
6812    const response = await fetch(API_URL, {
6813      method: 'POST',
6814      headers: {
6815        'Content-Type': 'application/json',
6816      },
6817      body: JSON.stringify(submissionData),
6818      signal: controller.signal
6819    });
6820
6821    clearTimeout(timeoutId);
6822    
6823    const result = await response.json();
6824    
6825    if (result.success) {
6826      // Calculate ground area in m² for liked and disliked areas
6827      // Union overlapping polygons first to avoid double-counting
6828      let dislikedAreaM2 = 0;
6829      let likedAreaM2 = 0;
6830
6831      // Separate polygons by type
6832      const dislikedPolygons = groundPolygons.filter(p => p && p.type === 'disliked' && p.geometry);
6833      const likedPolygons = groundPolygons.filter(p => p && p.type === 'liked' && p.geometry);
6834
6835      // Union disliked polygons
6836      if (dislikedPolygons.length > 0) {
6837        try {
6838          let unionedDisliked = dislikedPolygons[0].geometry;
6839          for (let i = 1; i < dislikedPolygons.length; i++) {
6840            try {
6841              unionedDisliked = turf.union(turf.featureCollection([
6842                turf.feature(unionedDisliked),
6843                turf.feature(dislikedPolygons[i].geometry)
6844              ])).geometry;
6845            } catch (e) {
6846              console.warn('Union failed, using individual area');
6847            }
6848          }
6849          dislikedAreaM2 = turf.area(unionedDisliked);
6850        } catch (e) {
6851          // Fallback to sum if union fails
6852          dislikedPolygons.forEach(p => dislikedAreaM2 += turf.area(p.geometry));
6853        }
6854      }
6855
6856      // Union liked polygons
6857      if (likedPolygons.length > 0) {
6858        try {
6859          let unionedLiked = likedPolygons[0].geometry;
6860          for (let i = 1; i < likedPolygons.length; i++) {
6861            try {
6862              unionedLiked = turf.union(turf.featureCollection([
6863                turf.feature(unionedLiked),
6864                turf.feature(likedPolygons[i].geometry)
6865              ])).geometry;
6866            } catch (e) {
6867              console.warn('Union failed, using individual area');
6868            }
6869          }
6870          likedAreaM2 = turf.area(unionedLiked);
6871        } catch (e) {
6872          // Fallback to sum if union fails
6873          likedPolygons.forEach(p => likedAreaM2 += turf.area(p.geometry));
6874        }
6875      }
6876
6877      // Round to whole numbers
6878      dislikedAreaM2 = Math.round(dislikedAreaM2);
6879      likedAreaM2 = Math.round(likedAreaM2);
6880      
6881      // Translations for success message
6882      const successTitle = currentLang === 'fr' ? 'Soumission réussie' : 'Submission Successful';
6883      const submissionIdLabel = currentLang === 'fr' ? 'ID de soumission :' : 'Submission ID:';
6884      const dislikedLabel = currentLang === 'fr' ? 'Zones non appréciées :' : 'Disliked areas:';
6885      const likedLabel = currentLang === 'fr' ? 'Zones appréciées :' : 'Liked areas:';
6886      const thankYouMsg = currentLang === 'fr' ? 'Merci pour votre contribution à ce projet de recherche!' : 'Thank you for your contribution to this research project!';
6887      const commentsLabel = currentLang === 'fr' ? 'Commentaires ou suggestions?' : 'Comments or suggestions?';
6888      const commentsPlaceholder = currentLang === 'fr' ? 'Écrivez ici...' : 'Write here...';
6889      const okText = currentLang === 'fr' ? 'Terminer' : 'Finish';
6890      const visualizationTitle = currentLang === 'fr' ? 'Votre évaluation' : 'Your Evaluation';
6891      const downloadBtnText = currentLang === 'fr' ? 'Télécharger mon évaluation' : 'Download My Evaluation';
6892      const shareBtnText = currentLang === 'fr' ? 'Partager avec des amis' : 'Share with Friends';
6893      const submitCommentText = currentLang === 'fr' ? 'Envoyer le commentaire' : 'Submit Comment';
6894
6895      // Hide loading overlay
6896      document.getElementById('loading-overlay').classList.add('hidden');
6897
6898      const overlay = document.createElement('div');
6899      overlay.className = 'custom-alert-overlay';
6900      overlay.innerHTML = `
6901        <div class="custom-alert" style="max-width: 900px;">
6902          <h3>${successTitle}</h3>
6903          <div class="custom-alert-content">
6904            <div style="margin-bottom: 15px; padding: 10px; background: #f0f7ff; border-radius: 6px;">
6905              <strong>${submissionIdLabel}</strong><br>
6906              <span style="font-size: 11px; color: #666; word-break: break-all;">${result.submissionId}</span>
6907            </div>
6908
6909            <p style="margin-top: 10px; margin-bottom: 15px; color: #333; font-size: 15px;">${thankYouMsg}</p>
6910
6911            <div class="visualization-section">
6912              <h4>${visualizationTitle}</h4>
6913              <canvas id="mosaic-canvas" width="1600" height="1600" style="max-width: 500px; width: 100%;"></canvas>
6914              <div class="buttons-row">
6915                <button class="download-viz-btn" onclick="downloadCanvas()">
6916                  ${downloadBtnText}
6917                </button>
6918                <button class="download-viz-btn pulsate-btn" onclick="shareWithFriends()">
6919                  ${shareBtnText}
6920                </button>
6921              </div>
6922            </div>
6923
6924            <div class="comment-section">
6925              <label>${commentsLabel}</label>
6926              <textarea
6927                id="submission-comments"
6928                placeholder="${commentsPlaceholder}"
6929                style="width: 100%; min-height: 100px; padding: 14px; border: 2px solid #e0e0e0; border-radius: 8px; font-family: inherit; font-size: 14px; resize: vertical; box-sizing: border-box; background: white; transition: border-color 0.2s;"
6930              ></textarea>
6931              <button id="submit-comment-btn" class="submit-comment-btn">
6932                ${submitCommentText}
6933              </button>
6934            </div>
6935          </div>
6936          <button class="custom-alert-button" id="submission-ok-btn" style="margin-top: 30px;">
6937            ${okText}
6938          </button>
6939        </div>
6940      `;
6941      document.body.appendChild(overlay);
6942
6943      // Draw mosaic visualization
6944      setTimeout(() => {
6945        const canvas = document.getElementById('mosaic-canvas');
6946        if (!canvas) return;
6947
6948        const ctx = canvas.getContext('2d');
6949        const size = 1600;
6950        const center = 800;
6951        const radius = 720;
6952
6953        function seededRandom(seed) {
6954          const x = Math.sin(seed++) * 10000;
6955          return x - Math.floor(x);
6956        }
6957
6958        // Helper to convert lng/lat to canvas pixels
6959        function lngLatToPixels(lng, lat) {
6960          // At Montreal (~45.5°N):
6961          // 1° latitude = 111.32km
6962          // 1° longitude = 111.32km * cos(45.5°) ≈ 78.0km
6963          // For 750m to fill 720px radius:
6964          const latMetersPerDegree = 111320;
6965          const lngMetersPerDegree = 111320 * Math.cos(selectedPoint.lat * Math.PI / 180);
6966          const radiusMeters = 750;
6967          const radiusPixels = 720;
6968
6969          const scaleX = radiusPixels / (radiusMeters / lngMetersPerDegree);
6970          const scaleY = radiusPixels / (radiusMeters / latMetersPerDegree);
6971
6972          const x = center + (lng - selectedPoint.lng) * scaleX;
6973          const y = center - (lat - selectedPoint.lat) * scaleY;
6974          return { x, y };
6975        }
6976
6977        // Deduplicate positions that are too close together
6978        function deduplicatePositions(positions, minDistance = 40) {
6979          const unique = [];
6980          positions.forEach(p => {
6981            const tooClose = unique.some(u => {
6982              const dist = Math.sqrt((p.x - u.x) ** 2 + (p.y - u.y) ** 2);
6983              return dist < minDistance;
6984            });
6985            if (!tooClose) {
6986              unique.push(p);
6987            }
6988          });
6989          return unique;
6990        }
6991
6992        // Convert voxel indices to coordinate arrays
6993        const canvasCenter = center;
6994        const redVoxelsRaw = Array.from(dislikedVoxels)
6995          .map(index => {
6996            const voxel = allVoxelFeatures[index];
6997            if (!voxel) return null;
6998            const voxelCenter = turf.center(voxel);
6999            return lngLatToPixels(voxelCenter.geometry.coordinates[0], voxelCenter.geometry.coordinates[1]);
7000          })
7001          .filter(p => {
7002            if (!p) return false;
7003            const dist = Math.sqrt((p.x - canvasCenter) ** 2 + (p.y - canvasCenter) ** 2);
7004            return dist < radius - 10;
7005          });
7006
7007        const greenVoxelsRaw = Array.from(likedVoxels)
7008          .map(index => {
7009            const voxel = allVoxelFeatures[index];
7010            if (!voxel) return null;
7011            const voxelCenter = turf.center(voxel);
7012            return lngLatToPixels(voxelCenter.geometry.coordinates[0], voxelCenter.geometry.coordinates[1]);
7013          })
7014          .filter(p => {
7015            if (!p) return false;
7016            const dist = Math.sqrt((p.x - canvasCenter) ** 2 + (p.y - canvasCenter) ** 2);
7017            return dist < radius - 10;
7018          });
7019
7020        // Add ground polygons to visualization
7021        const redGroundRaw = groundPolygons
7022          .filter(p => p && p.type === 'disliked' && p.geometry)
7023          .map(polygon => {
7024            const polyCenter = turf.center(polygon.geometry);
7025            return lngLatToPixels(polyCenter.geometry.coordinates[0], polyCenter.geometry.coordinates[1]);
7026          })
7027          .filter(p => {
7028            if (!p) return false;
7029            const dist = Math.sqrt((p.x - canvasCenter) ** 2 + (p.y - canvasCenter) ** 2);
7030            return dist < radius - 10;
7031          });
7032
7033        const greenGroundRaw = groundPolygons
7034          .filter(p => p && p.type === 'liked' && p.geometry)
7035          .map(polygon => {
7036            const polyCenter = turf.center(polygon.geometry);
7037            return lngLatToPixels(polyCenter.geometry.coordinates[0], polyCenter.geometry.coordinates[1]);
7038          })
7039          .filter(p => {
7040            if (!p) return false;
7041            const dist = Math.sqrt((p.x - canvasCenter) ** 2 + (p.y - canvasCenter) ** 2);
7042            return dist < radius - 10;
7043          });
7044
7045        // Combine voxels and ground polygons, then deduplicate
7046        const redVoxels = deduplicatePositions([...redVoxelsRaw, ...redGroundRaw]);
7047        const greenVoxels = deduplicatePositions([...greenVoxelsRaw, ...greenGroundRaw]);
7048
7049        // Background - blue
7050        ctx.fillStyle = '#4285f4';
7051        ctx.fillRect(0, 0, size, size);
7052
7053        // Draw real map background inside circle using actual voxel data
7054        ctx.save();
7055        ctx.beginPath();
7056        ctx.arc(center, center, radius, 0, Math.PI * 2);
7057        ctx.clip();
7058
7059        // Map base
7060        ctx.fillStyle = '#e8e8e8';
7061        ctx.fillRect(0, 0, size, size);
7062
7063        // Draw actual buildings from voxel data
7064        ctx.fillStyle = '#d0d0d0';
7065        ctx.strokeStyle = '#c0c0c0';
7066        ctx.lineWidth = 1;
7067
7068        allVoxelFeatures.forEach(voxel => {
7069          if (!voxel || !voxel.geometry || !voxel.geometry.coordinates) return;
7070
7071          try {
7072            const coords = voxel.geometry.coordinates[0];
7073            if (!coords || coords.length === 0) return;
7074
7075            ctx.beginPath();
7076            coords.forEach((coord, i) => {
7077              const p = lngLatToPixels(coord[0], coord[1]);
7078              if (i === 0) {
7079                ctx.moveTo(p.x, p.y);
7080              } else {
7081                ctx.lineTo(p.x, p.y);
7082              }
7083            });
7084            ctx.closePath();
7085            ctx.fill();
7086            ctx.stroke();
7087          } catch (e) {}
7088        });
7089
7090        ctx.restore();
7091
7092        // Draw MOSAIC tiles for red voxels
7093        redVoxels.forEach(v => {
7094          const tileSize = 32 + seededRandom(v.x * v.y) * 20;
7095          ctx.save();
7096          ctx.translate(v.x, v.y);
7097          ctx.rotate(seededRandom(v.x + v.y) * Math.PI * 2);
7098
7099          const shades = [
7100            'rgba(234, 67, 53, 0.5)',
7101            'rgba(211, 52, 38, 0.5)',
7102            'rgba(197, 34, 31, 0.5)',
7103            'rgba(179, 20, 18, 0.5)'
7104          ];
7105          const shade = shades[Math.floor(seededRandom(v.x * 2) * shades.length)];
7106
7107          ctx.fillStyle = shade;
7108          ctx.fillRect(-tileSize/2, -tileSize/2, tileSize, tileSize);
7109
7110          ctx.strokeStyle = '#ffffff';
7111          ctx.lineWidth = 2;
7112          ctx.strokeRect(-tileSize/2, -tileSize/2, tileSize, tileSize);
7113
7114          ctx.restore();
7115        });
7116
7117        // Draw MOSAIC tiles for green voxels
7118        greenVoxels.forEach(v => {
7119          const tileSize = 32 + seededRandom(v.x * v.y + 100) * 20;
7120          ctx.save();
7121          ctx.translate(v.x, v.y);
7122          ctx.rotate(seededRandom(v.x + v.y + 100) * Math.PI * 2);
7123
7124          const shades = [
7125            'rgba(52, 168, 83, 0.5)',
7126            'rgba(45, 149, 72, 0.5)',
7127            'rgba(15, 157, 88, 0.5)',
7128            'rgba(10, 128, 67, 0.5)'
7129          ];
7130          const shade = shades[Math.floor(seededRandom(v.y * 2) * shades.length)];
7131
7132          ctx.fillStyle = shade;
7133          ctx.fillRect(-tileSize/2, -tileSize/2, tileSize, tileSize);
7134
7135          ctx.strokeStyle = '#ffffff';
7136          ctx.lineWidth = 2;
7137          ctx.strokeRect(-tileSize/2, -tileSize/2, tileSize, tileSize);
7138
7139          ctx.restore();
7140        });
7141
7142        // Circle border - white
7143        ctx.strokeStyle = 'white';
7144        ctx.lineWidth = 20;
7145        ctx.beginPath();
7146        ctx.arc(center, center, radius, 0, Math.PI * 2);
7147        ctx.stroke();
7148
7149        // Draw pin icon in center
7150        drawPinIcon(center, center, 30);
7151
7152        // Labels - white and bold
7153        ctx.fillStyle = 'white';
7154        ctx.font = 'bold 56px Arial';
7155
7156        // "Tag Ta Ville" in top left
7157        ctx.textAlign = 'left';
7158        ctx.textBaseline = 'top';
7159        ctx.fillText('Tag Ta Ville', 40, 40);
7160
7161        // City name underneath
7162        ctx.font = 'bold 36px Arial';
7163        ctx.fillText('Montréal', 40, 104);
7164
7165        // "tagtaville.ca" in top right
7166        ctx.font = 'bold 56px Arial';
7167        ctx.textAlign = 'right';
7168        ctx.fillText('tagtaville.ca', size - 40, 40);
7169
7170        // Submission ID in bottom right - FULL ID
7171        ctx.font = 'bold 20px monospace';
7172        ctx.textAlign = 'right';
7173        ctx.textBaseline = 'bottom';
7174        ctx.fillText('ID: ' + result.submissionId, size - 40, size - 40);
7175
7176        // Draw both logos on bottom left (white inverted, side by side)
7177        const udemLogo = new Image();
7178        const ivadoLogo = new Image();
7179        let logosLoaded = 0;
7180
7181        udemLogo.onload = ivadoLogo.onload = function() {
7182          logosLoaded++;
7183          if (logosLoaded === 2) {
7184            ctx.save();
7185            ctx.filter = 'brightness(0) invert(1)';
7186
7187            // UdeM logo on left
7188            const udemHeight = 80;
7189            const udemWidth = udemLogo.width * (udemHeight / udemLogo.height);
7190            ctx.drawImage(udemLogo, 40, size - 50 - udemHeight, udemWidth, udemHeight);
7191
7192            // IVADO logo next to UdeM with 30px gap
7193            const ivadoHeight = 80;
7194            const ivadoWidth = ivadoLogo.width * (ivadoHeight / ivadoLogo.height);
7195            ctx.drawImage(ivadoLogo, 40 + udemWidth + 30, size - 50 - ivadoHeight, ivadoWidth, ivadoHeight);
7196
7197            ctx.restore();
7198          }
7199        };
7200
7201        udemLogo.src = 'assets/udem-logo.svg';
7202        ivadoLogo.src = 'assets/ivado-rgb_logo-full-degrade.png';
7203
7204        function drawSmallPin(x, y, scale) {
7205          ctx.save();
7206          const s = scale / 30;
7207          ctx.translate(x, y);
7208
7209          // Pole
7210          const poleGradient = ctx.createLinearGradient(0, 16 * s, 0, 59 * s);
7211          poleGradient.addColorStop(0, '#1976d2');
7212          poleGradient.addColorStop(1, '#0d47a1');
7213          ctx.fillStyle = poleGradient;
7214          ctx.fillRect(-3 * s, 16 * s, 6 * s, 43 * s);
7215
7216          // Sphere
7217          const sphereGradient = ctx.createRadialGradient(0, -4 * s, 0, 0, -4 * s, 20 * s);
7218          sphereGradient.addColorStop(0, '#64b5f6');
7219          sphereGradient.addColorStop(0.6, '#2196f3');
7220          sphereGradient.addColorStop(1, '#1565c0');
7221          ctx.fillStyle = sphereGradient;
7222          ctx.beginPath();
7223          ctx.arc(0, -4 * s, 20 * s, 0, Math.PI * 2);
7224          ctx.fill();
7225
7226          ctx.restore();
7227        }
7228
7229        function drawPinIcon(x, y, scale) {
7230          ctx.save();
7231
7232          const s = scale / 30;
7233          // Position so the BOTTOM of the pole is at (x, y)
7234          ctx.translate(x, y - 59 * s);
7235
7236          // Shadow
7237          ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
7238          ctx.beginPath();
7239          ctx.ellipse(0, 59 * s, 14 * s, 4 * s, 0, 0, Math.PI * 2);
7240          ctx.fill();
7241
7242          // Pole
7243          const poleGradient = ctx.createLinearGradient(0, 16 * s, 0, 59 * s);
7244          poleGradient.addColorStop(0, '#1976d2');
7245          poleGradient.addColorStop(1, '#0d47a1');
7246          ctx.fillStyle = poleGradient;
7247          ctx.beginPath();
7248          ctx.roundRect(-3 * s, 16 * s, 6 * s, 43 * s, 3 * s);
7249          ctx.fill();
7250
7251          // Main sphere
7252          const sphereGradient = ctx.createRadialGradient(0, -4 * s, 0, 0, -4 * s, 20 * s);
7253          sphereGradient.addColorStop(0, '#64b5f6');
7254          sphereGradient.addColorStop(0.6, '#2196f3');
7255          sphereGradient.addColorStop(1, '#1565c0');
7256          ctx.fillStyle = sphereGradient;
7257          ctx.beginPath();
7258          ctx.arc(0, -4 * s, 20 * s, 0, Math.PI * 2);
7259          ctx.fill();
7260
7261          // Highlight
7262          const highlightGradient = ctx.createRadialGradient(-6 * s, -10 * s, 0, -6 * s, -10 * s, 20 * s);
7263          highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
7264          highlightGradient.addColorStop(0.3, 'rgba(255, 255, 255, 0.5)');
7265          highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
7266          ctx.fillStyle = highlightGradient;
7267          ctx.beginPath();
7268          ctx.arc(0, -4 * s, 20 * s, 0, Math.PI * 2);
7269          ctx.fill();
7270
7271          // White reflection
7272          ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
7273          ctx.beginPath();
7274          ctx.arc(-8 * s, -12 * s, 7 * s, 0, Math.PI * 2);
7275          ctx.fill();
7276
7277          ctx.restore();
7278        }
7279      }, 100);
7280
7281      // Make canvas clickable on mobile to save to photo library
7282      if (window.innerWidth <= 768) {
7283        setTimeout(() => {
7284          const canvas = document.getElementById('mosaic-canvas');
7285          if (canvas) {
7286            canvas.style.cursor = 'pointer';
7287            canvas.addEventListener('click', () => {
7288              downloadCanvas();
7289            });
7290          }
7291        }, 150);
7292      }
7293
7294      // Download function for mosaic
7295      window.downloadCanvas = async function() {
7296        const canvas = document.getElementById('mosaic-canvas');
7297
7298        // On mobile, try to use Share API to save to photos
7299        if (window.innerWidth <= 768 && navigator.share && navigator.canShare) {
7300          try {
7301            const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
7302            const file = new File([blob], 'tagtaville_' + result.submissionId + '.png', { type: 'image/png' });
7303
7304            if (navigator.canShare({ files: [file] })) {
7305              await navigator.share({
7306                files: [file],
7307                title: 'Mon évaluation Tag ta Ville',
7308              });
7309              return;
7310            }
7311          } catch (err) {
7312            console.log('Share failed, falling back to download:', err);
7313          }
7314        }
7315
7316        // Fallback: regular download
7317        const link = document.createElement('a');
7318        link.download = 'tagtaville_' + result.submissionId + '.png';
7319        link.href = canvas.toDataURL('image/png');
7320        link.click();
7321      };
7322
7323      // Share function
7324      window.shareWithFriends = async function() {
7325        const canvas = document.getElementById('mosaic-canvas');
7326        const websiteUrl = 'https://tagtaville.ca';
7327
7328        canvas.toBlob(async (blob) => {
7329          const file = new File([blob], 'tagtaville_' + result.submissionId + '.png', { type: 'image/png' });
7330
7331          if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) {
7332            try {
7333              await navigator.share({
7334                title: 'Participe avec moi à Tag ta Ville!',
7335                text: "J'ai contribué à Tag ta Ville, un projet de recherche de l'Université de Montréal qui cartographie la qualité de vie dans nos quartiers. Découvre mon évaluation de mon secteur en pièce jointe.\n\nParticipe toi aussi: c'est rapide, gratuit et entièrement anonyme!",
7336                url: websiteUrl,
7337                files: [file]
7338              });
7339            } catch (err) {
7340              if (err.name !== 'AbortError') {
7341                fallbackShare();
7342              }
7343            }
7344          } else {
7345            fallbackShare();
7346          }
7347        }, 'image/png');
7348
7349        function fallbackShare() {
7350          downloadCanvas();
7351          const shareText = "J'ai contribué à Tag ta Ville, un projet de recherche de l'Université de Montréal qui cartographie la qualité de vie dans nos quartiers. Découvre mon évaluation de mon secteur en pièce jointe.\n\nParticipe toi aussi: c'est rapide, gratuit et entièrement anonyme!\n\n" + websiteUrl;
7352
7353          if (navigator.clipboard) {
7354            navigator.clipboard.writeText(shareText).then(() => {
7355              alert("Image téléchargée et message copié dans le presse-papier!\n\nPartage-le avec tes amis!");
7356            }).catch(() => {
7357              alert("Image téléchargée!\n\nPartage ce message avec tes amis:\n\n" + shareText);
7358            });
7359          } else {
7360            alert("Image téléchargée!\n\nPartage ce message avec tes amis:\n\n" + shareText);
7361          }
7362        }
7363      };
7364
7365      // Mark step 4 as completed in progress tracker
7366      const step4 = document.getElementById('progress-step-4');
7367      if (step4) {
7368        step4.classList.remove('active');
7369        step4.classList.add('completed');
7370      }
7371
7372      // Handle comment submission
7373      document.getElementById('submit-comment-btn').addEventListener('click', async () => {
7374        const commentText = document.getElementById('submission-comments').value.trim();
7375        if (!commentText) {
7376          alert(currentLang === 'fr' ? 'Veuillez entrer un commentaire.' : 'Please enter a comment.');
7377          return;
7378        }
7379
7380        const btn = document.getElementById('submit-comment-btn');
7381        btn.disabled = true;
7382        btn.textContent = currentLang === 'fr' ? 'Envoi...' : 'Sending...';
7383
7384        try {
7385          await fetch('https://tag-ta-ville-production.up.railway.app/
7385api/feedback', {
7386            method: 'POST',
7387            headers: { 'Content-Type': 'application/json' },
7388            body: JSON.stringify({
7389              feedback: commentText,
7390              type: 'submission_comment',
7391              submissionId: result.submissionId
7392            })
7393          });
7394
7395          btn.textContent = currentLang === 'fr' ? '✓ Envoyé' : '✓ Sent';
7396          btn.style.background = '#1e7e34';
7397          document.getElementById('submission-comments').value = '';
7398        } catch (err) {
7399          console.error('Comment submission error:', err);
7400          btn.textContent = currentLang === 'fr' ? 'Erreur - Réessayer' : 'Error - Retry';
7401          btn.disabled = false;
7402        }
7403      });
7404
7405      // Add event listener to finish button - hard reload
7406      document.getElementById('submission-ok-btn').addEventListener('click', () => {
7407        location.reload();
7408
7409        // Remove center marker
7410        if (centerMarker) {
7411          centerMarker.remove();
7412          centerMarker = null;
7413        }
7414
7415        // Remove all map layers (voxels, trees, radius circle)
7416        if (map.getLayer('voxel-fill-2d')) map.removeLayer('voxel-fill-2d');
7417        if (map.getLayer('voxel-fill')) map.removeLayer('voxel-fill');
7418        if (map.getSource('voxel-cubes')) map.removeSource('voxel-cubes');
7419
7420        if (map.getLayer('tree-cubes-2d')) map.removeLayer('tree-cubes-2d');
7421        if (map.getLayer('tree-cubes')) map.removeLayer('tree-cubes');
7422        if (map.getSource('tree-cubes')) map.removeSource('tree-cubes');
7423        if (map.getLayer('radius-circle-line')) {
7424          map.removeLayer('radius-circle-line');
7425          map.removeSource('radius-circle');
7426        }
7427
7428        // Remove painted voxels layers
7429        if (map.getLayer('painted-voxels-layer')) {
7430          map.removeLayer('painted-voxels-layer');
7431        }
7432        if (map.getSource('painted-voxels')) {
7433          map.removeSource('painted-voxels');
7434        }
7435
7436        // Remove ground paint markers
7437        groundPaintMarkers.forEach(paintId => {
7438          if (map.getLayer(paintId)) {
7439            map.removeLayer(paintId);
7440            map.removeSource(paintId);
7441          }
7442        });
7443        groundPaintMarkers = [];
7444
7445        // Remove painted overlays
7446        paintedOverlays.forEach(overlayId => {
7447          try {
7448            if (map.getLayer(overlayId)) map.removeLayer(overlayId);
7449            if (map.getSource(overlayId)) map.removeSource(overlayId);
7450          } catch(e) {}
7451        });
7452        paintedOverlays = [];
7453
7454        // Remove cluster markers
7455        clusterMarkers.forEach(marker => marker.remove());
7456        clusterMarkers = [];
7457
7458        // Remove split divider lines
7459        splitDividerLines.forEach(lineId => {
7460          try {
7461            if (map.getLayer(lineId)) map.removeLayer(lineId);
7462            if (map.getSource(lineId)) map.removeSource(lineId);
7463          } catch(e) {}
7464        });
7465        splitDividerLines = [];
7466
7467        // Remove tool preview
7468        removeToolPreview();
7469
7470        // Reset voxel data
7471        voxelsLoaded = false;
7472        voxelSpatialIndex = null;
7473        allVoxelFeatures = [];
7474        dislikedVoxels.clear();
7475        likedVoxels.clear();
7476        paintedVoxelFeatures = [];
7477        paintedVoxelIndex.clear();
7478
7479        // Reset state variables
7480        currentTool = 'cursor';
7481        currentPaintColor = 'liked';
7482        isDragging = false;
7483        clusterLabels = {};
7484        allClusters = [];
7485        selectedClusterId = null;
7486        currentClusterIndex = 0;
7487
7488        // Clear selected point (wrapped in try-catch)
7489        try {
7490          if (typeof selectedPointMarker !== 'undefined' && selectedPointMarker) {
7491            selectedPointMarker.remove();
7492            selectedPointMarker = null;
7493          }
7494        } catch(e) {
7495          console.log('selectedPointMarker cleanup skipped:', e);
7496        }
7497
7498        try {
7499          if (typeof selectedPoint !== 'undefined') {
7500            selectedPoint = null;
7501          }
7502        } catch(e) {
7503          console.log('selectedPoint cleanup skipped:', e);
7504        }
7505
7506        // Re-enable map interactions
7507        map.dragPan.enable();
7508        map.scrollZoom.enable();
7509
7510        // Hide all steps
7511        document.getElementById('step1').classList.add('hidden');
7512        document.getElementById('step2-paint').classList.add('hidden');
7513        document.getElementById('step3-tag-clusters').classList.add('hidden');
7514        document.getElementById('step4-submit').classList.add('hidden');
7515
7516        // Reset Step 1 UI elements
7517        document.getElementById('location-selected').classList.add('hidden');
7518        document.getElementById('address-suggestions').classList.add('hidden');
7519        document.getElementById('address-input').value = '';
7520
7521        // Clear clusters list
7522        document.getElementById('clusters-list').innerHTML = '';
7523
7524        // Hide reset button
7525        document.getElementById('reset-btn').classList.add('hidden');
7526
7527        // Reset tool selection
7528        document.getElementById('tool-cursor').classList.add('active');
7529        document.getElementById('tool-paint').classList.remove('active');
7530        document.getElementById('tool-erase').classList.remove('active');
7531        document.getElementById('paint-green').classList.add('active');
7532        document.getElementById('paint-red').classList.remove('active');
7533
7534        // Hide map nav controls and progress tracker
7535        document.getElementById('map-nav-controls').classList.remove('active');
7536        document.getElementById('progress-tracker').style.display = 'none';
7537
7538        // Show main panel again
7539        const mainPanel = document.querySelector('.main-panel');
7540        if (mainPanel) mainPanel.style.display = 'block';
7541
7542        // Reset progress
7543        updateProgress(1);
7544
7545        // Show welcome overlay for fresh start
7546        document.getElementById('welcome-overlay').style.display = 'flex';
7547
7548        // Fly back to Montreal center
7549        map.flyTo({
7550          center: MONTREAL_CENTER,
7551          zoom: 12,
7552          pitch: isMobile ? 0 : 45,
7553          bearing: -17,
7554          duration: 1500
7555        });
7556      });
7557    } else {
7558      throw new Error(result.error || 'Submission failed');
7559    }
7560    
7561  } catch (error) {
7562    // Hide loading overlay on error
7563    document.getElementById('loading-overlay').classList.add('hidden');
7564    console.error('Submission error:', error);
7565    const lang = (typeof currentLang !== 'undefined') ? currentLang : 'fr';
7566    const title = lang === 'fr' ? 'Erreur' : 'Error';
7567    const message = lang === 'fr'
7568      ? `Erreur lors de la soumission des données :<br><br>${error.message}<br><br>Assurez-vous que le serveur backend est en cours d'exécution.`
7569      : `Error submitting data:<br><br>${error.message}<br><br>Please make sure the backend server is running.`;
7570    showSubmissionErrorAlert(title, message);
7571  } finally {
7572    submitBtn.disabled = false;
7573    const submitText = currentLang === 'fr' ? 'Soumettre' : 'Submit';
7574    submitBtn.innerHTML = `<span data-en="Submit" data-fr="Soumettre">${submitText}</span>`;
7575  }
7576});
7577
7578// View controls
7579const pitchSlider = document.getElementById('pitch-slider');
7580if (pitchSlider) {
7581  pitchSlider.addEventListener('input', (e) => {
7582    const pitch = parseInt(e.target.value);
7583    document.getElementById('pitch-value').textContent = pitch;
7584    map.setPitch(pitch);
7585    console.log('Pitch set to:', pitch);
7586  });
7587} else {
7588  console.error('pitch-slider element not found');
7589}
7590
7591const toggleBuildings = document.getElementById('toggle-buildings');
7592if (toggleBuildings) {
7593  toggleBuildings.addEventListener('change', (e) => {
7594    const visibility = e.target.checked ? 'visible' : 'none';
7595    // Toggle both 2D and 3D layers
7596    if (map.getLayer('voxel-fill-2d')) {
7597      map.setLayoutProperty('voxel-fill-2d', 'visibility', visibility);
7598    }
7599    if (map.getLayer('voxel-fill')) {
7600      map.setLayoutProperty('voxel-fill', 'visibility', visibility);
7601    }
7602  });
7603} else {
7604  console.error('toggle-buildings element not found');
7605}
7606
7607
7608const toggleRadius = document.getElementById('toggle-radius');
7609if (toggleRadius) {
7610  toggleRadius.addEventListener('change', (e) => {
7611    if (map.getLayer('radius-circle-line')) {
7612      const v = e.target.checked ? 'visible' : 'none';
7613      map.setLayoutProperty('radius-circle-line', 'visibility', v);
7614    }
7615  });
7616}
7617
7618// Satellite toggle
7619let satelliteLayerAdded = false;
7620
7621const toggleSatellite = document.getElementById('toggle-satellite');
7622if (toggleSatellite) {
7623  toggleSatellite.addEventListener('change', (e) => {
7624    console.log('Toggle satellite:', e.target.checked);
7625    if (e.target.checked) {
7626      addSatelliteLayer();
7627    } else {
7628      removeSatelliteLayer();
7629    }
7630  });
7631} else {
7632  console.error('toggle-satellite element not found');
7633}
7634
7635function addSatelliteLayer() {
7636  if (!satelliteLayerAdded) {
7637    try {
7638      map.addSource('google-satellite', {
7639        type: 'raster',
7640        tiles: [
7641          'https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
7642          'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}'
7643        ],
7644        tileSize: 256
7645      });
7646      
7647      // Insert before voxels if they exist, otherwise just add
7648      const beforeLayer = map.getLayer('voxel-fill') ? 'voxel-fill' : undefined;
7649      
7650      map.addLayer({
7651        id: 'google-satellite-layer',
7652        type: 'raster',
7653        source: 'google-satellite',
7654        paint: { 'raster-opacity': 0.6 }
7655      }, beforeLayer);
7656      
7657      satelliteLayerAdded = true;
7658      console.log('✓ Satellite layer added');
7659    } catch (error) {
7660      console.error('Error adding satellite layer:', error);
7661    }
7662  }
7663}
7664
7665function removeSatelliteLayer() {
7666  if (satelliteLayerAdded) {
7667    if (map.getLayer('google-satellite-layer')) {
7668      map.removeLayer('google-satellite-layer');
7669    }
7670    if (map.getSource('google-satellite')) {
7671      map.removeSource('google-satellite');
7672    }
7673    satelliteLayerAdded = false;
7674  }
7675}
7676
7677// Reset
7678document.getElementById('reset-btn').addEventListener('click', () => {
7679  if (centerMarker) { centerMarker.remove(); centerMarker = null; }
7680  
7681  if (map.getLayer('voxel-fill-2d')) map.removeLayer('voxel-fill-2d');
7682  if (map.getLayer('voxel-fill')) map.removeLayer('voxel-fill');
7683  if (map.getSource('voxel-cubes')) map.removeSource('voxel-cubes');
7684
7685  if (map.getLayer('tree-cubes-2d')) map.removeLayer('tree-cubes-2d');
7686  if (map.getLayer('tree-cubes')) map.removeLayer('tree-cubes');
7687  if (map.getSource('tree-cubes')) map.removeSource('tree-cubes');
7688  if (map.getLayer('radius-circle-fill')) { 
7689    map.removeLayer('radius-circle-fill'); 
7690    map.removeLayer('radius-circle-line'); 
7691    map.removeSource('radius-circle'); 
7692  }
7693  
7694  groundPaintMarkers.forEach(paintId => {
7695    if (map.getLayer(paintId)) {
7696      map.removeLayer(paintId);
7697      map.removeSource(paintId);
7698    }
7699  });
7700  groundPaintMarkers = [];
7701  
7702  clusterMarkers.forEach(marker => marker.remove());
7703  clusterMarkers = [];
7704  
7705  removeToolPreview();
7706
7707  // Keep 2D buildings hidden - only show 3D voxels
7708  // if (map.getLayer('building')) map.setLayoutProperty('building', 'visibility', 'visible');
7709
7710  voxelsLoaded = false;
7711  voxelSpatialIndex = null;
7712  currentTool = 'cursor';
7713  currentPaintColor = 'liked';
7714  dislikedVoxels.clear();
7715  likedVoxels.clear();
7716  allVoxelFeatures = [];
7717  isDragging = false;
7718  clusterLabels = {};
7719  selectedClusterId = null;
7720  
7721  map.dragPan.enable();
7722  map.scrollZoom.enable();
7723  
7724  // Reset UI
7725  document.getElementById('step1').classList.remove('hidden');
7726  document.getElementById('step2-paint').classList.add('hidden');
7727  document.getElementById('step3-tag-clusters').classList.add('hidden');
7728  document.getElementById('step4-submit').classList.add('hidden');
7729  
7730  document.getElementById('location-selected').classList.add('hidden');
7731  document.getElementById('address-suggestions').classList.add('hidden');
7732  document.getElementById('address-input').value = '';
7733  
7734  document.getElementById('clusters-list').innerHTML = '';
7735  document.getElementById('reset-btn').classList.add('hidden');
7736  
7737  document.getElementById('tool-cursor').classList.add('active');
7738  document.getElementById('tool-paint').classList.remove('active');
7739  document.getElementById('tool-erase').classList.remove('active');
7740  document.getElementById('paint-green').classList.add('active');
7741  document.getElementById('paint-red').classList.remove('active');
7742  
7743  map.flyTo({
7744    center: MONTREAL_CENTER,
7745    zoom: 12,
7746    pitch: 45,
7747    bearing: -17,
7748    duration: 1500
7749  });
7750  
7751});
7752
7753
7754}); // End DOMContentLoaded
7755
7756</script>
7756
7757
7758</body>
7759</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.