PageSourceSearch

https://dreamy-truffle-77db6e.netlify.app/uccelli_cittadini%20definifivo/script.js?v=2026-04-24_1

js dreamy-truffle-77db6e.netlify.app collected 2026-10-03 10:40:13 UTC 90,349 bytes, 2,186 lines download raw bytes

1(function(){
2
3  // ========= VARIABILI GLOBALI FILE =========
4  let selectedFiles = [];
5
6const SPECIES_LIST = [
7  "Specie non identificata",
8  "Airone bianco maggiore (Ardea alba)",
9  "Airone cenerino (Ardea cinerea)",
10  "Airone guardabuoi (Bubulcus ibis)",
11  "Airone rosso (Ardea purpurea)",
12  "Allocco (Strix aluco)",
13  "Allodola (Alauda arvensis)",
14  "Anatra germanata (Anas platyrhynchos)",
15  "Anatra mandarina (Aix galericulata)",
16  "Anatra muta (Cairina moschata)",
17  "Assiolo (Otus scops)",
18  "Astore (Accipiter gentilis)",
19  "Averla maggiore (Lanius excubitor)",
20  "Averla piccola (Lanius collurio)",
21  "Balestruccio (Delichon urbicum)",
22  "Balia nera (Ficedula hypoleuca)",
23  "Ballerina bianca (Motacilla alba)",
24  "Ballerina gialla (Motacilla cinerea)",
25  "Barbagianni (Tyto alba)",
26  "Beccaccia (Scolopax rusticola)",
27  "Beccaccino (Gallinago gallinago)",
28  "Beccafico (Sylvia borin)",
29  "Canapino (Hippolais polyglotta)",
30  "Capinera (Sylvia atricapilla)",
31  "Cardellino (Carduelis carduelis)",
32  "Cavaliere d’Italia (Himantopus himantopus)",
33  "Cesena (Turdus pilaris)",
34  "Cicogna bianca (Ciconia ciconia)",
35  "Cicogna nera (Ciconia nigra)",
36  "Cigno reale (Cygnus olor)",
37  "Cincia bigia (Poecile palustris)",
38  "Cincia dal ciuffo (Lophophanes cristatus)",
39  "Cincia mora (Periparus ater)",
40  "Cinciallegra (Parus major)",
41  "Cinciarella (Cyanistes caeruleus)",
42  "Civetta (Athene noctua)",
43  "Codibugnolo (Aegithalos caudatus)",
44  "Codirosso (Phoenicurus phoenicurus)",
45  "Codirosso spazzacamino (Phoenicurus ochruros)",
46  "Colombaccio (Columba palumbus)",
47  "Cormorano (Phalacrocorax carbo)",
48  "Cornacchia grigia (Corvus cornix)",
49  "Cornacchia nera (Corvus corone)",
50  "Corriere grosso (Charadrius hiaticula)",
51  "Corriere piccolo (Charadrius dubius)",
52  "Cuculo (Cuculus canorus)",
53  "Cutrettola (Motacilla flava)",
54  "Fagiano (Phasianus colchicus)",
55  "Falco di palude (Circus aeruginosus)",
56  "Falco pellegrino (Falco peregrinus)",
57  "Falco pescatore (Pandion haliaetus)",
58  "Fenicottero (Phoenicopterus roseus)",
59  "Fiorrancino (Regulus ignicapilla)",
60  "Fischione (Mareca penelope)",
61  "Folaga (Fulica atra)",
62  "Fringuello (Fringilla coelebs)",
63  "Frosone (Coccothraustes coccothraustes)",
64  "Gabbiano comune (Chroicocephalus ridibundus)",
65  "Gabbiano reale (Larus michahellis)",
66  "Gallinella d’acqua (Gallinula chloropus)",
67  "Garzetta (Egretta garzetta)",
68  "Gavina (Larus canus)",
69  "Gazza (Pica pica)",
70  "Germano reale (Anas platyrhynchos)",
71  "Gheppio (Falco tinnunculus)",
72  "Ghiandaia (Garrulus glandarius)",
73  "Gru (Grus grus)",
74  "Gruccione (Merops apiaster)",
75  "Gufo comune (Asio otus)",
76  "Ibis sacro (Threskiornis aethiopicus)",
77  "Lodolaio (Falco subbuteo)",
78  "Lucherino (Spinus spinus)",
79  "Luì bianco (Phylloscopus bonelli)",
80  "Luì grosso (Phylloscopus trochilus)",
81  "Luì piccolo (Phylloscopus collybita)",
82  "Luì verde (Phylloscopus sibilatrix)",
83  "Marangone minore (Microcarbo pygmaeus)",
84  "Martin pescatore (Alcedo atthis)",
85  "Marzaiola (Spatula querquedula)",
86  "Merlo (Turdus merula)",
87  "Mestolone (Spatula clypeata)",
88  "Migliarino di palude (Emberiza schoeniclus)",
89  "Mignattaio (Plegadis falcinellus)",
90  "Moretta (Aythya fuligula)",
91  "Moretta tabaccata (Aythya nyroca)",
92  "Moriglione (Aythya ferina)",
93  "Nibbio bruno (Milvus migrans)",
94  "Nitticora (Nycticorax nycticorax)",
95  "Oca selvatica (Anser anser)",
96  "Parrocchetto dal collare (Psittacula krameri)",
97  "Parrocchetto monaco (Myiopsitta monachus)",
98  "Passera d’Italia (Passer italiae)",
99  "Passera mattugia (Passer montanus)",
100  "Passera scopaiola (Prunella modularis)",
101  "Pavoncella (Vanellus vanellus)",
102  "Pendolino (Remiz pendulinus)",
103  "Pernice rossa (Alectoris rufa)",
104  "Pettirosso (Erithacus rubecula)",
105  "Piccione di città (Columba livia)",
106  "Picchio muratore (Sitta europaea)",
107  "Picchio rosso maggiore (Dendrocopos major)",
108  "Picchio rosso minore (Dryobates minor)",
109  "Picchio verde (Picus viridis)",
110  "Pigliamosche (Muscicapa striata)",
111  "Piro piro piccolo (Actitis hypoleucos)",
112  "Pispola (Anthus pratensis)",
113  "Poiana (Buteo buteo)",
114  "Quaglia (Coturnix coturnix)",
115  "Regolo (Regulus regulus)",
116  "Rigogolo (Oriolus oriolus)",
117  "Rondine (Hirundo rustica)",
118  "Rondine montana (Ptyonoprogne rupestris)",
119  "Rondone comune (Apus apus)",
120  "Rondone maggiore (Tachymarptis melba)",
121  "Rondone pallido (Apus pallidus)",
122  "Saltimpalo (Saxicola rubicola)",
123  "Scricciolo (Troglodytes troglodytes)",
124  "Sgarza ciuffetto (Ardeola ralloides)",
125  "Smergo maggiore (Mergus merganser)",
126  "Sparviere (Accipiter nisus)",
127  "Spioncello (Anthus spinoletta)",
128  "Sterna comune (Sterna hirundo)",
129  "Stiaccino (Saxicola rubetra)",
130  "Storno (Sturnus vulgaris)",
131  "Succiacapre (Caprimulgus europaeus)",
132  "Svasso maggiore (Podiceps cristatus)",
133  "Taccola (Coloeus monedula)",
134  "Tarabuso (Botaurus stellaris)",
135  "Tarabusino (Ixobrychus minutus)",
136  "Topino (Riparia riparia)",
137  "Torcicollo (Jynx torquilla)",
138  "Tordo bottaccio (Turdus philomelos)",
139  "Tortora dal collare (Streptopelia decaocto)",
140  "Tortora selvatica (Streptopelia turtur)",
141  "Tuffetto (Tachybaptus ruficollis)",
142  "Upupa (Upupa epops)",
143  "Usignolo (Luscinia megarhynchos)",
144  "Usignolo di fiume (Cettia cetti)",
145  "Verdone (Chloris chloris)",
146  "Verzellino (Serinus serinus)"
147];
148
149
150function setupAutocomplete(inp, arr) {
151  let currentFocus;
152  inp.addEventListener("input", function(e) {
153      let a, b, i, val = this.value;
154      closeAllLists();
155      if (!val) { return false;}
156      currentFocus = -1;
157      a = document.createElement("DIV");
158      a.setAttribute("id", this.id + "autocomplete-list");
159      a.setAttribute("class", "autocomplete-items");
160      this.parentNode.appendChild(a);
161      
162      let matches = 0;
163      for (i = 0; i < arr.length; i++) {
164        if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
165          matches++;
166          if(matches > 10) break;
167          b = document.createElement("DIV");
168          b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>";
169          b.innerHTML += arr[i].substr(val.length);
170          b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";
171          b.addEventListener("click", function(e) {
172              inp.value = this.getElementsByTagName("input")[0].value;
173              closeAllLists();
174          });
175          a.appendChild(b);
176        }
177      }
178  });
179  
180  inp.addEventListener("keydown", function(e) {
181      let x = document.getElementById(this.id + "autocomplete-list");
182      if (x) x = x.getElementsByTagName("div");
183      if (e.keyCode == 40) {
184        currentFocus++;
185        addActive(x);
186      } else if (e.keyCode == 38) {
187        currentFocus--;
188        addActive(x);
189      } else if (e.keyCode == 13) {
190        e.preventDefault();
191        if (currentFocus > -1) {
192          if (x) x[currentFocus].click();
193        }
194      }
195  });
196  
197  function addActive(x) {
198    if (!x) return false;
199    removeActive(x);
200    if (currentFocus >= x.length) currentFocus = 0;
201    if (currentFocus < 0) currentFocus = (x.length - 1);
202    x[currentFocus].classList.add("autocomplete-active");
203  }
204  
205  function removeActive(x) {
206    for (let i = 0; i < x.length; i++) {
207      x[i].classList.remove("autocomplete-active");
208    }
209  }
210  
211  function closeAllLists(elmnt) {
212    const x = document.getElementsByClassName("autocomplete-items");
213    for (let i = 0; i < x.length; i++) {
214      if (elmnt != x[i] && elmnt != inp) {
215        x[i].parentNode.removeChild(x[i]);
216      }
217    }
218  }
219  
220  document.addEventListener("click", function (e) {
221      closeAllLists(e.target);
222  });
223}
224
225 // Conterrà oggetti { file, id }
226  const MAX_FILES = 2;
227  const MAX_TOTAL_BYTES = 4 * 1024 * 1024; // 4MB per stare nei limiti Netlify (6MB payload)
228
229  const STORAGE_KEY_NOME = "guardaMI_uccelli_cittadini_nome";
230  const STORAGE_KEY_EMAIL = "guardaMI_uccelli_cittadini_email";
231  const AGE_GATE_KEY = "guardaMI_age_gate_14_or_parent";
232  const SHARED_CITIZEN_PROFILE_KEY = "guardaMI_cittadini_profile_v1";
233
234  function getSharedCitizenProfile() {
235    try {
236      const raw = localStorage.getItem(SHARED_CITIZEN_PROFILE_KEY);
237      if (!raw) return null;
238      const data = JSON.parse(raw);
239      if (!data || typeof data !== "object") return null;
240      const nome = typeof data.nome === "string" ? data.nome : "";
241      const mail = typeof data.mail === "string" ? data.mail : "";
242      return { nome, mail };
243    } catch (e) {
244      return null;
245    }
246  }
247
248  function setSharedCitizenProfile(nome, mail) {
249    try {
250      const payload = {
251        nome: String(nome || ""),
252        mail: String(mail || ""),
253        updatedAt: Date.now()
254      };
255      localStorage.setItem(SHARED_CITIZEN_PROFILE_KEY, JSON.stringify(payload));
256      localStorage.setItem("userData", JSON.stringify({ nome: payload.nome, mail: payload.mail }));
257    } catch (e) {}
258  }
259
260  function clearSharedCitizenProfile() {
261    try { localStorage.removeItem(SHARED_CITIZEN_PROFILE_KEY); } catch (e) {}
262    try { localStorage.removeItem("userData"); } catch (e) {}
263  }
264
265  function loadUserDataFromStorage() {
266    const nomeInput = document.getElementById('nomeRilevatore');
267    const emailInput = document.getElementById('email');
268
269    if (typeof(Storage) !== "undefined") {
270      const shared = getSharedCitizenProfile();
271      if (shared) {
272        if (nomeInput && !nomeInput.value && shared.nome) nomeInput.value = shared.nome;
273        if (emailInput && !emailInput.value && shared.mail) emailInput.value = shared.mail;
274      }
275
276      const legacyNameKeys = ["uccelli_cittadini_def_userName", "cittadini_user_name"];
277      const legacyEmailKeys = ["uccelli_cittadini_def_userEmail", "cittadini_user_email"];
278
279      if (!localStorage.getItem(STORAGE_KEY_NOME)) {
280        for (let i = 0; i < legacyNameKeys.length; i++) {
281          const v = localStorage.getItem(legacyNameKeys[i]);
282          if (v) {
283            localStorage.setItem(STORAGE_KEY_NOME, v);
284            break;
285          }
286        }
287      }
288      if (!localStorage.getItem(STORAGE_KEY_EMAIL)) {
289        for (let i = 0; i < legacyEmailKeys.length; i++) {
290          const v = localStorage.getItem(legacyEmailKeys[i]);
291          if (v) {
292            localStorage.setItem(STORAGE_KEY_EMAIL, v);
293            break;
294          }
295        }
296      }
297
298      if (localStorage.getItem(STORAGE_KEY_NOME) && nomeInput) {
299        if (!nomeInput.value) nomeInput.value = localStorage.getItem(STORAGE_KEY_NOME);
300      }
301      if (localStorage.getItem(STORAGE_KEY_EMAIL) && emailInput) {
302        if (!emailInput.value) emailInput.value = localStorage.getItem(STORAGE_KEY_EMAIL);
303      }
304
305      const currentShared = getSharedCitizenProfile();
306      if (!currentShared) {
307        const n = nomeInput ? (nomeInput.value || "") : "";
308        const m = emailInput ? (emailInput.value || "") : "";
309        if (n || m) setSharedCitizenProfile(n, m);
310      }
311    }
312  }
313
314  function saveUserDataToStorage() {
315    const nomeInput = document.getElementById('nomeRilevatore');
316    const emailInput = document.getElementById('email');
317
318    if (typeof(Storage) !== "undefined") {
319      if (nomeInput) localStorage.setItem(STORAGE_KEY_NOME, nomeInput.value);
320      if (emailInput) localStorage.setItem(STORAGE_KEY_EMAIL, emailInput.value);
321    }
322    setSharedCitizenProfile(nomeInput ? nomeInput.value : "", emailInput ? emailInput.value : "");
323  }
324
325  function clearUserDataFromStorageAndForm() {
326    const nomeInput = document.getElementById('nomeRilevatore');
327    const emailInput = document.getElementById('email');
328
329    if (confirm("Sei sicuro di voler cancellare i dati salvati (Nome, E-mail) da questo dispositivo?")) {
330      if (typeof(Storage) !== "undefined") {
331        localStorage.removeItem(STORAGE_KEY_NOME);
332        localStorage.removeItem(STORAGE_KEY_EMAIL);
333        localStorage.removeItem("uccelli_cittadini_def_userName");
334        localStorage.removeItem("uccelli_cittadini_def_userEmail");
335        localStorage.removeItem("cittadini_user_name");
336        localStorage.removeItem("cittadini_user_email");
337      }
338      clearSharedCitizenProfile();
339      if (nomeInput) nomeInput.value = "";
340      if (emailInput) emailInput.value = "";
341      alert("I tuoi dati sono stati rimossi da questo dispositivo.");
342    }
343  }
344
345  // Init al caricamento
346  window.addEventListener('DOMContentLoaded', () => {
347
348      const nomeSpecieInput = document.getElementById("nomeSpecie");
349      if (nomeSpecieInput) {
350        setupAutocomplete(nomeSpecieInput, SPECIES_LIST);
351      }
352    
353      loadUserDataFromStorage();
354      
355      const nomeInput = document.getElementById('nomeRilevatore');
356      const emailInput = document.getElementById('email');
357      const clearBtn = document.getElementById('clearSavedUserData');
358      const ageGate = document.getElementById('ageGate14');
359
360      if (nomeInput) {
361        nomeInput.addEventListener('input', saveUserDataToStorage);
362        nomeInput.addEventListener('change', saveUserDataToStorage);
363      }
364      if (emailInput) {
365        emailInput.addEventListener('input', saveUserDataToStorage);
366        emailInput.addEventListener('change', saveUserDataToStorage);
367      }
368      if (clearBtn) {
369        clearBtn.addEventListener('click', clearUserDataFromStorageAndForm);
370      }
371      if (ageGate) {
372        if (localStorage.getItem(AGE_GATE_KEY) === "true") ageGate.checked = true;
373        ageGate.addEventListener('change', function() {
374          localStorage.setItem(AGE_GATE_KEY, ageGate.checked ? "true" : "false");
375          if (typeof updateSubmitState === "function") updateSubmitState();
376        });
377      }
378
379      // Initialize Date to Today
380      const dateField = document.getElementById('dataAvvistamento');
381      if (dateField && !dateField.value) {
382          const today = new Date();
383          const yyyy = today.getFullYear();
384          const mm = String(today.getMonth() + 1).padStart(2, '0');
385          const dd = String(today.getDate()).padStart(2, '0');
386          dateField.value = `${yyyy}-${mm}-${dd}`;
387      }
388  });
389
390
391  // ========= GOOGLE MAPS + GEOLOCALIZZAZIONE =========
392  let map;
393  let marker;
394  window.manualEditMade = false;
395
396  // ========= PERIMETRO CITTÀ METROPOLITANA DI MILANO =========
397  let milanoMetroPolygons = null;
398  let milanoBoundaryPromise = null;
399
400  function loadMilanoBoundary() {
401    if (milanoBoundaryPromise) return milanoBoundaryPromise;
402    // Assumiamo che il file GeoJSON sia presente nella root del sito (come in ricercatori)
403    milanoBoundaryPromise = fetch("/Comuni_zone_omogenee_20260117.geojson")
404      .then(function(response) {
405        if (!response.ok) throw new Error("HTTP " + response.status);
406        return response.json();
407      })
408      .then(function(data) {
409        const features = data && data.features ? data.features : [];
410        const polygons = [];
411        for (let i = 0; i < features.length; i++) {
412          const geom = features[i] && features[i].geometry;
413          if (!geom || !geom.type || !geom.coordinates) continue;
414          if (geom.type === "MultiPolygon") {
415            const mp = geom.coordinates;
416            for (let j = 0; j < mp.length; j++) {
417              polygons.push(mp[j]);
418            }
419          } else if (geom.type === "Polygon") {
420            polygons.push(geom.coordinates);
421          }
422        }
423        milanoMetroPolygons = polygons;
424        console.log("Confine Milano caricato:", milanoMetroPolygons.length, "poligoni");
425      })
426      .catch(function(err) {
427        console.error("Errore nel caricamento del confine di Milano:", err);
428        milanoMetroPolygons = null;
429      });
430    return milanoBoundaryPromise;
431  }
432
433  function ensureMilanoBoundaryLoaded() {
434    if (milanoMetroPolygons && Array.isArray(milanoMetroPolygons) && milanoMetroPolygons.length > 0) {
435      return Promise.resolve();
436    }
437    return loadMilanoBoundary();
438  }
439
440  function pointInRing(lat, lng, ring) {
441    let inside = false;
442    const y = lat;
443    const x = lng;
444    for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
445      const xi = ring[i][0];
446      const yi = ring[i][1];
447      const xj = ring[j][0];
448      const yj = ring[j][1];
449      const intersect = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
450      if (intersect) inside = !inside;
451    }
452    return inside;
453  }
454
455  function pointInPolygon(lat, lng, polygon) {
456    if (!polygon || polygon.length === 0) return false;
457    if (!pointInRing(lat, lng, polygon[0])) return false;
458    for (let i = 1; i < polygon.length; i++) {
459      if (pointInRing(lat, lng, polygon[i])) return false;
460    }
461    return true;
462  }
463
464  function isInsideMilanoMetro(lat, lng) {
465    if (typeof lat !== "number" || typeof lng !== "number") return false;
466    if (!isFinite(lat) || !isFinite(lng)) return false;
467
468    // Bounding box di sicurezza (include San Colombano al Lambro)
469    const minLat = 45.10, maxLat = 45.70;
470    const minLng = 8.80, maxLng = 9.70;
471    const inBoundingBox = (lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng);
472
473    if (milanoMetroPolygons && Array.isArray(milanoMetroPolygons) && milanoMetroPolygons.length > 0) {
474      for (let i = 0; i < milanoMetroPolygons.length; i++) {
475        if (pointInPolygon(lat, lng, milanoMetroPolygons[i])) {
476          return true;
477        }
478      }
479      // Se siamo fuori dai poligoni ma dentro la bbox, è fuori (perché i poligoni sono precisi)
480      // Se non abbiamo poligoni caricati, usiamo la bbox come fallback
481      return false;
482    }
483    return inBoundingBox;
484  }
485
486  // Handler globale per errori di autenticazione Google Maps
487  window.gm_authFailure = function() {
488    const mapDiv = document.getElementById('map');
489    if (mapDiv) {
490      mapDiv.innerHTML = '<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; padding:20px; text-align:center; color:#b91c1c; background:#fef2f2; border:1px solid #ef4444; border-radius:8px;"><div>⚠️ <strong>Errore caricamento mappa</strong></div><div style="font-size:13px; margin-top:5px;">Verifica la chiave API o le impostazioni del dominio (Referrer) nella Google Cloud Console.</div></div>';
491      mapDiv.style.height = '320px'; // Assicura altezza
492    }
493    console.error("Google Maps API Authentication Failure");
494  };
495
496  window.initMap = function() {
497    console.log("initMap called. Checking Google Maps API...");
498    if (window.google && window.google.maps) {
499        console.log("Google Maps API loaded. Version:", google.maps.version);
500    } else {
501        console.error("Google Maps API NOT loaded.");
502    }
503
504    const mapDiv = document.getElementById('map');
505    if (!mapDiv || !window.google || !google.maps) return;
506
507    // Centro di default: Milano
508    const defaultPos = { lat: 45.4642, lng: 9.19 };
509
510    map = new google.maps.Map(mapDiv, {
511      center: defaultPos,
512      zoom: 12,
513      streetViewControl: false,
514      mapTypeControl: false
515    });
516
517    marker = new google.maps.Marker({
518      position: defaultPos,
519      map: map,
520      draggable: true
521    });
522
523    // ========= SEARCH BOX (Autocomplete) =========
524    const searchInput = document.getElementById('mapSearch');
525    const jotformGeoInput = document.getElementById('jotformGeoloc');
526
527    if (searchInput && google.maps.places) {
528      const autocomplete = new google.maps.places.Autocomplete(searchInput, {
529        fields: ['geometry', 'formatted_address'],
530        types: ['geocode']
531      });
532
533      autocomplete.bindTo('bounds', map);
534
535      autocomplete.addListener('place_changed', () => {
536        window.manualEditMade = true; // Utente ha cercato un indirizzo
537        const place = autocomplete.getPlace();
538        if (!place.geometry || !place.geometry.location) {
539          return;
540        }
541
542        const location = place.geometry.location;
543
544        if (place.geometry.viewport) {
545          map.fitBounds(place.geometry.viewport);
546        } else {
547          map.setCenter(location);
548          map.setZoom(17);
549        }
550
551        marker.setPosition(location);
552
553        const lat = location.lat();
554        const lng = location.lng();
555        const latInput = document.getElementById('latitudine');
556        const lngInput = document.getElementById('longitudine');
557        
558        if (latInput && lngInput) {
559          latInput.value = lat.toFixed(6);
560          lngInput.value = lng.toFixed(6);
561        }
562
563        if (jotformGeoInput) {
564          jotformGeoInput.value = lat.toFixed(6) + ', ' + lng.toFixed(6);
565        }
566      });
567
568      searchInput.addEventListener('keydown', (e) => {
569        if (e.key === 'Enter') {
570          e.preventDefault();
571        }
572      });
573    }
574
575    window.map = map;
576    window.marker = marker;
577
578    const latInput = document.getElementById('latitudine');
579    const lngInput = document.getElementById('longitudine');
580
581    try {
582      const pending = window.__guardamiPendingExifCoords;
583      if (pending && typeof pending.lat === "number" && typeof pending.lng === "number") {
584        map.setCenter(pending);
585        map.setZoom(15);
586        marker.setPosition(pending);
587        if (latInput && lngInput) {
588          latInput.value = pending.lat.toFixed(6);
589          lngInput.value = pending.lng.toFixed(6);
590        }
591        syncGeoToJotform();
592        window.manualEditMade = true;
593        window.__guardamiPendingExifCoords = null;
594      }
595    } catch (e) {}
596
597    // Inizializza i campi con la posizione di default se vuoti
598    syncGeoToJotform();
599
600    // Sync marker -> inputs
601    marker.addListener('dragend', (e) => {
602      window.manualEditMade = true;
603      const pos = e.latLng;
604      if (latInput && lngInput) {
605        latInput.value = pos.lat().toFixed(6);
606        lngInput.value = pos.lng().toFixed(6);
607      }
608      syncGeoToJotform();
609
610      const lat = pos.lat();
611      const lng = pos.lng();
612      const minLat = 45.10, maxLat = 45.70;
613      const minLng = 8.80, maxLng = 9.70;
614      const inBoundingBox = (lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng);
615      if (!inBoundingBox) {
616        alert("Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo è riservato alle osservazioni in questo territorio.");
617      }
618    });
619
620    // Tentativo recupero sessione
621    /* DISABILITATO: Nessun dato salvato in locale.
622    try {
623      const last = sessionStorage.getItem('geoLast');
624      if (last) {
625        const obj = JSON.parse(last);
626        if (obj && typeof obj.lat === 'number') {
627          const p = { lat: obj.lat, lng: obj.lng };
628          map.setCenter(p);
629          map.setZoom(15);
630          marker.setPosition(p);
631          if (latInput) latInput.value = obj.lat.toFixed(6);
632          if (lngInput) lngInput.value = obj.lng.toFixed(6);
633        }
634      }
635    } catch(e) {}
636    */
637    console.log("Recupero sessione geo disabilitato per privacy.");
638
639    // Avvio geolocalizzazione
640    chiediGeolocalizzazione();
641  };
642
643  window.chiediGeolocalizzazione = function() {
644    if (!navigator.geolocation || !map || !marker) return;
645
646    navigator.geolocation.getCurrentPosition(
647      (pos) => {
648        if (window.manualEditMade) return;
649        const lat = pos.coords.latitude;
650        const lng = pos.coords.longitude;
651        const nuovaPos = { lat, lng };
652
653        map.setCenter(nuovaPos);
654        map.setZoom(15);
655        marker.setPosition(nuovaPos);
656
657        const latInput = document.getElementById('latitudine');
658        const lngInput = document.getElementById('longitudine');
659        if (latInput && lngInput) {
660          latInput.value = lat.toFixed(6);
661          lngInput.value = lng.toFixed(6);
662        }
663        syncGeoToJotform();
664        /* DISABILITATO: Nessun dato salvato in locale.
665        try { sessionStorage.setItem('geoConsentSession','granted'); } catch(e) {}
666        */
667        console.log("Salvataggio consenso geo disabilitato per privacy.");
668      },
669      (err) => {
670        console.warn('Errore geolocalizzazione:', err);
671      },
672      { enableHighAccuracy: true, timeout: 10000 }
673    );
674  }
675
676  function syncGeoToJotform() {
677    const latInput = document.getElementById('latitudine');
678    const lngInput = document.getElementById('longitudine');
679    const jotformGeo = document.getElementById('jotformGeoloc');
680    if (latInput && lngInput && jotformGeo) {
681      if (latInput.value && lngInput.value) {
682        jotformGeo.value = latInput.value + ', ' + lngInput.value;
683      } else {
684        jotformGeo.value = '';
685      }
686    }
687  }
688
689  // ========= LOGICA UI (Conosci specie / Dimensione / Colori) =========
690  const radios = document.querySelectorAll('input[name="conosciSpecieLocal"]');
691  const species = document.getElementById('speciesFields');
692  const colorPanel = document.getElementById('colorPanel');
693  const dimensionePanel = document.getElementById('dimensionePanel');
694  const dimRadios = document.querySelectorAll('input[name="dimensione"]');
695  const sizeRefImage = document.getElementById('sizeRefImage');
696  const colorBtns = document.querySelectorAll('.color-btn');
697
698  // MAPPATURA SPECIE PER DIMENSIONE E COLORE
699  const speciesMappings = [
700      // PICCOLO
701      { size: 'PICCOLO', color: 'BIANCO', species: ['balestruccio', 'codibugnolo', 'corriere-piccolo'] }, 
702      { size: 'PICCOLO', color: 'BIANCO/NERO', species: ['balestruccio', 'codibugnolo', 'picchio-rosso-minore'] },
703      { size: 'PICCOLO', color: 'NERO/GRIGIO', species: ['capinera', 'codirosso-spazzacamino'] }, 
704      { size: 'PICCOLO', color: 'GRIGIO', species: ['capinera', 'pigliamosche', 'codirosso-spazzacamino'] }, 
705      { size: 'PICCOLO', color: 'GIALLO', species: ['cinciarella', 'cinciallegra', 'verzellino', 'verdone', 'cardellino', 'lucherino', 'fiorrancino', 'regolo'] }, 
706      { size: 'PICCOLO', color: 'GIALLO/BLU', species: ['cinciarella'] }, 
707      { size: 'PICCOLO', color: 'BLU', species: ['cinciarella', 'martin-pescatore'] }, 
708      { size: 'PICCOLO', color: 'ROSSO', species: ['picchio-rosso-minore', 'pettirosso', 'cardellino', 'codirosso-comune', 'codirosso-spazzacamino'] }, 
709      { size: 'PICCOLO', color: 'ROSSO/GIALLO', species: ['cardellino'] },
710      { size: 'PICCOLO', color: 'MARRONE/ROSSO', species: ['pettirosso', 'fringuello', 'codirosso-comune', 'peppola'] },
711      { size: 'PICCOLO', color: 'VERDE', species: ['verdone', 'regolo'] },
712      { size: 'PICCOLO', color: 'BEIGE', species: ['torcicollo', 'passera-scopaiola', 'usignolo', 'corriere-piccolo', 'passera-d-italia', 'fiorrancino', 'rondine-montana'] },
713      { size: 'PICCOLO', color: 'MARRONE', species: ['passera-d-italia', 'passera-mattugia', 'torcicollo', 'passera-scopaiola', 'corriere-piccolo'] },
714
715
716      // MEDIO
717    { size: 'MEDIO', color: 'BIANCO', species: ['ballerina-bianca'] },
718    { size: 'MEDIO', color: 'BIANCO/NERO', species: ['ballerina-bianca', 'rondine', 'pavoncella'] },
719    { size: 'MEDIO', color: 'NERO/GRIGIO', species: [] },
720    { size: 'MEDIO', color: 'GRIGIO', species: ['assiolo'] },
721    { size: 'MEDIO', color: 'GIALLO', species: ['ballerina-gialla', 'gruccione', 'rigogolo'] },
722    { size: 'MEDIO', color: 'GIALLO/BLU', species: [] },
723    { size: 'MEDIO', color: 'BLU', species: ['gruccione'] },
724    { size: 'MEDIO', color: 'ROSSO', species: ['picchio-rosso-maggiore'] },
725    { size: 'MEDIO', color: 'ROSSO/GIALLO', species: [] },
726    { size: 'MEDIO', color: 'MARRONE/ROSSO', species: ['tuffetto'] },
727    { size: 'MEDIO', color: 'VERDE', species: ['storno'] },
728    { size: 'MEDIO', color: 'BEIGE', species: ['civetta', 'rondone-pallido', 'rondone-maggiore', 'torcicollo', 'averla-piccola'] },
729    { size: 'MEDIO', color: 'MARRONE', species: ['civetta', 'rondone-pallido', 'rondone-maggiore', 'merlo-femmina', 'assiolo', 'porciglione', 'tuffetto', 'averla-piccola'] },
730    { size: 'MEDIO', color: 'NERO', species: ['merlo', 'storno', 'rondone-comune'] },
731
732      // GRANDE
733      { size: 'GRANDE', color: 'NERO', species: ['cornacchia', 'gallinella-d-acqua', 'folaga', 'taccola', 'gazza'] }, 
734      { size: 'GRANDE', color: 'BIANCO', species: ['svasso-maggiore', 'gabbiano-comune', 'gabbiano-comune-inverno'] }, 
735      { size: 'GRANDE', color: 'BIANCO/NERO', species: ['folaga', 'gazza', 'cavaliere-ditalia'] },
736      { size: 'GRANDE', color: 'NERO/GRIGIO', species: ['cornacchia', 'taccola'] }, 
737      { size: 'GRANDE', color: 'BEIGE', species: ['sparviere', 'ghiandaia', 'gheppio', 'tarabusino', 'mestolone-maschio', 'mestolone-femmina', 'lodolaio', 'allocco', 'gufo-comune', 'tortora-selvatica', 'germano-reale'] },
738      { size: 'GRANDE', color: 'MARRONE', species: ['sparviere', 'ghiandaia', 'gheppio', 'tarabusino', 'mestolone-maschio', 'mestolone-femmina', 'lodolaio', 'allocco', 'gufo-comune', 'tortora-selvatica', 'germano-reale'] },
739      { size: 'GRANDE', color: 'MARRONE/ROSSO', species: ['sparviere', 'ghiandaia', 'gheppio', 'tarabusino', 'lodolaio', 'allocco', 'gufo-comune', 'tortora-selvatica'] },
740      { size: 'GRANDE', color: 'GRIGIO', species: ['piccione', 'colombaccio', 'colombella', 'falco-pellegrino', 'tortora-dal-collare', 'gabbiano-comune', 'gabbiano-comune-inverno', 'cuculo', 'tortora-selvatica'] },
741      { size: 'GRANDE', color: 'VERDE', species: ['parrocchetto-collare', 'parrocchetto-monaco', 'picchio-verde', 'germano-reale'] },
742      { size: 'GRANDE', color: 'BLU', species: ['gazza'] },
743
744      // MOLTO GRANDE
745      { size: 'MOLTO GRANDE', color: 'NERO', species: ['cormorano'] },
746      { size: 'MOLTO GRANDE', color: 'BIANCO', species: ['airone-guardabuoi', 'garzetta', 'ibis-sacro', 'cicogna', 'svasso-maggiore', 'gabbiano-reale', 'cigno-reale', 'smergo-maggiore-maschio'] },
747      { size: 'MOLTO GRANDE', color: 'BIANCO/NERO', species: ['ibis-sacro', 'cicogna'] },
748      { size: 'MOLTO GRANDE', color: 'NERO/GRIGIO', species: ['airone-cenerino'] },
749      { size: 'MOLTO GRANDE', color: 'GRIGIO', species: ['airone-cenerino', 'nitticora', 'gabbiano-reale', 'airone-rosso'] },
750      { size: 'MOLTO GRANDE', color: 'BEIGE', species: ['fagiano-femmina', 'svasso-maggiore', 'poiana'] },
751      { size: 'MOLTO GRANDE', color: 'MARRONE', species: ['fagiano-maschio', 'svasso-maggiore', 'poiana', 'smergo-maggiore-femmina'] },
752      { size: 'MOLTO GRANDE', color: 'ROSSO', species: ['fagiano-maschio', 'airone-rosso'] },
753      { size: 'MOLTO GRANDE', color: 'VERDE', species: ['smergo-maggiore-maschio'] },
754      { size: 'MOLTO GRANDE', color: 'GRIGIO/ARANCIO', species: ['smergo-maggiore-femmina'] },
755  ];
756
757  function updateBlocks(){
758    const val = (document.querySelector('input[name="conosciSpecieLocal"]:checked')||{}).value;
759    const sizeVal = (document.querySelector('input[name="dimensione"]:checked')||{}).value;
760    
761    if (species) species.style.display = (val === 'si') ? 'block' : 'none';
762    
763    if (val === 'no') {
764      if(dimensionePanel) dimensionePanel.style.display = 'block';
765      if (sizeVal) {
766        if(colorPanel) colorPanel.style.display = 'block';
767        updateSizeImage(sizeVal);
768        filterColors(sizeVal);
769        
770        // Aggiorna schede specie se c'è un colore selezionato
771        const colorVal = (document.querySelector('.color-btn.selected') || {}).getAttribute('data-color');
772        if (colorVal) {
773           updateSpeciesCards(sizeVal, colorVal);
774        }
775      } else {
776        if(colorPanel) colorPanel.style.display = 'none';
777        if(sizeRefImage) sizeRefImage.style.display = 'none';
778        clearSpeciesCards();
779      }
780    } else {
781      if(colorPanel) colorPanel.style.display = 'none';
782      if(dimensionePanel) dimensionePanel.style.display = 'none';
783      if(sizeRefImage) sizeRefImage.style.display = 'none';
784      clearSpeciesCards();
785    }
786  }
787
788  function clearSpeciesCards() {
789      const container = document.getElementById('speciesCardsContainer');
790      if(container) {
791          container.innerHTML = '';
792          container.style.display = 'none';
793      }
794      const hiddenColor = document.getElementById('colorePredominante');
795      if(hiddenColor) hiddenColor.value = '';
796      document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('selected'));
797  }
798
799  function resolveSpeciesImagePath(src) {
800      const s = String(src || '').trim();
801      if (!s) return s;
802      if (/^(https?:)?\/\//i.test(s) || s.startsWith('data:') || s.startsWith('blob:')) return s;
803      if (s.startsWith('/assets/species/')) return s;
804      const file = s.split('/').pop();
805      const remap = {
806          'Carcharodus alceae ali aperte.jpg': 'Carcharodus alceae ali aperte__uccelli.jpg',
807          'Gonepteryx rhamni_ali chiuse.jpg': 'Gonepteryx rhamni_ali chiuse__uccelli.jpg',
808          'airone rosso.jpg': 'airone rosso__uccelli.jpg',
809          'capriolo.jpg': 'capriolo__uccelli.jpg',
810          'picchio rosso maggiore.jpg': 'picchio rosso maggiore__uccelli.jpg',
811          'raganella.jpg': 'raganella__uccelli.jpg',
812          'rondine comune.jpg': 'rondine comune__uccelli.jpg',
813      };
814      const finalName = remap[file] || file;
815      return '/assets/species/' + encodeURIComponent(finalName);
816  }
817
818  function resolveSpeciesCardImagePath(species, colorVal) {
819      if (!species) return '';
820      if (species.id === 'germano-reale') {
821          const c = String(colorVal || '').trim().toUpperCase();
822          if (c === 'BEIGE' || c === 'MARRONE') {
823              return 'https://commons.wikimedia.org/wiki/Special:FilePath/Female%20Mallard%20Duck%20Rest%203.jpg?width=1200';
824          }
825      }
826      return resolveSpeciesImagePath(species.image);
827  }
828
829  function updateSpeciesCards(sizeVal, colorVal) {
830      const container = document.getElementById('speciesCardsContainer');
831      if(!container) return;
832
833      container.innerHTML = '';
834      
835      if(!sizeVal || !colorVal) {
836          container.style.display = 'none';
837          return;
838      }
839
840      // Trova mapping
841      let sizeKey = '';
842      if(sizeVal.includes('PICCOLO')) sizeKey = 'PICCOLO';
843      else if(sizeVal.includes('MEDIO')) sizeKey = 'MEDIO';
844      else if(sizeVal.includes('GRANDE') && !sizeVal.includes('MOLTO')) sizeKey = 'GRANDE';
845      else if(sizeVal.includes('MOLTO GRANDE')) sizeKey = 'MOLTO GRANDE';
846
847      const mapping = speciesMappings.find(m => m.size === sizeKey && m.color === colorVal);
848      
849      if(!mapping || !mapping.species || mapping.species.length === 0) {
850          container.innerHTML = '<p style="grid-column:1/-1; text-align:center; color:#666;">Nessuna specie trovata con queste caratteristiche.</p>';
851          container.style.display = 'grid'; // Grid layout defined in CSS
852          return;
853      }
854
855      // Filtra database
856      let speciesToShow = speciesDatabase.filter(s => mapping.species.includes(s.id));
857      
858      // Ordina alfabeticamente per nome
859      speciesToShow.sort((a, b) => a.name.localeCompare(b.name));
860
861      if(speciesToShow.length === 0) {
862           container.innerHTML = '<p style="grid-column:1/-1; text-align:center; color:#666;">Nessuna specie trovata nel database.</p>';
863           container.style.display = 'grid';
864           return;
865      }
866
867      // Render cards
868      speciesToShow.forEach(s => {
869          const card = document.createElement('div');
870          const imgSrc = resolveSpeciesCardImagePath(s, colorVal);
871          card.className = 'species-card';
872          // Click sulla card seleziona la specie
873          card.onclick = (e) => {
874              // Se il click non è avvenuto sull'immagine o sul pulsante, seleziona la specie
875              if(!e.target.closest('.species-card-img-container') && !e.target.closest('.species-select-btn')) {
876                  selectSpeciesFromCard(s);
877              }
878          };
879          
880          // Helper for phenology
881          const generatePhenology = (species) => {
882              // SE SPECIE E' GABBIANO COMUNE -> NIENTE FENOLOGIA
883              if(species.id === 'gabbiano-comune' || species.id === 'gabbiano-comune-inverno') return '';
884
885              const months = species.months || [];
886              const nesting = species.nesting || [];
887              const phenology = species.phenology || {};
888
889              let html = '<div class="phenology-container">';
890              const headers = ['G','F','M','A','M','G','L','A','S','O','N','D'];
891              
892              for(let i=0; i<12; i++) {
893                  let decades = ['phen-white', 'phen-white', 'phen-white'];
894                  
895                  if (phenology[i]) {
896                      if (Array.isArray(phenology[i]) && phenology[i].length === 3) {
897                          decades = phenology[i];
898                      } else {
899                          // Legacy string mapping
900                          const cls = phenology[i];
901                          if (cls === 'phen-green') decades = ['phen-green', 'phen-green', 'phen-green'];
902                          else if (cls === 'phen-yellow') decades = ['phen-yellow', 'phen-yellow', 'phen-yellow'];
903                          else if (cls === 'phen-white') decades = ['phen-white', 'phen-white', 'phen-white'];
904                          
905                          // Complex gradients (approximate to decades)
906                          else if (cls === 'phen-green-yellow') decades = ['phen-green', 'phen-green-yellow', 'phen-yellow'];
907                          else if (cls === 'phen-green-yellow-25') decades = ['phen-green', 'phen-yellow', 'phen-yellow'];
908                          else if (cls === 'phen-green-yellow-75') decades = ['phen-green', 'phen-green', 'phen-yellow'];
909                          
910                          else if (cls === 'phen-yellow-green') decades = ['phen-yellow', 'phen-yellow-green', 'phen-green'];
911                          else if (cls === 'phen-yellow-green-25') decades = ['phen-yellow', 'phen-green', 'phen-green'];
912                          else if (cls === 'phen-yellow-green-75') decades = ['phen-yellow', 'phen-yellow', 'phen-green'];
913
914                          else if (cls === 'phen-white-green') decades = ['phen-white', 'phen-white-green', 'phen-green'];
915                          else if (cls === 'phen-white-green-25') decades = ['phen-white', 'phen-green', 'phen-green'];
916                          else if (cls === 'phen-white-green-75') decades = ['phen-white', 'phen-white', 'phen-green'];
917                          
918                          else if (cls === 'phen-green-white') decades = ['phen-green', 'phen-green-white', 'phen-white'];
919                          else if (cls === 'phen-green-white-25') decades = ['phen-green', 'phen-white', 'phen-white'];
920                          else if (cls === 'phen-green-white-75') decades = ['phen-green', 'phen-green', 'phen-white'];
921                          
922                          else if (cls === 'phen-mix-aug-pigliamosche') decades = ['phen-yellow', 'phen-white', 'phen-green'];
923                          
924                          else decades = [cls, cls, cls];
925                      }
926                  } else {
927                      let base = 'phen-white';
928                      if(nesting.includes(i)) base = 'phen-yellow';
929                      else if(months.includes(i)) base = 'phen-green';
930                      decades = [base, base, base];
931                  }
932                  
933                  // Render Month Box
934                  html += `<div class="phen-month-box">`;
935                  // Decades
936                  decades.forEach(d => {
937                      html += `<div class="phen-decade ${d}"></div>`;
938                  });
939                  // Letter Overlay
940                  html += `<span class="phen-letter">${headers[i]}</span>`;
941                  html += `</div>`;
942              }
943
944              html += '</div>';
945
946              // Legenda
947          html += '<div style="margin-top:4px; font-size:11px; color:#555; display:flex; align-items:center; gap:12px; padding-left: 2%;">';
948          html += '<div style="display:flex; align-items:center;"><span style="width:10px; height:10px; background:#8cc63f; display:inline-block; margin-right:4px; border-radius:2px;"></span>Presenza</div>';
949          html += '<div style="display:flex; align-items:center;"><span style="width:10px; height:10px; background:#fff200; display:inline-block; margin-right:4px; border-radius:2px;"></span>Nidificazione</div>';
950          html += '</div>';
951
952              return html;
953          };
954
955          card.innerHTML = `
956            <div class="species-card-img-container" style="position: relative; overflow: hidden; background-color: #f1f5f9;">
957                <div class="blur-bg" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; 
958                            background-image: url('${imgSrc}'); background-size: cover; background-position: center; 
959                            filter: blur(10px) brightness(0.8); transform: scale(1.1); opacity: 0.8; z-index: 0;"></div>
960                <img src="${imgSrc}" alt="${s.name}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; z-index: 1;" onclick="event.stopPropagation(); openSpeciesLightbox('${imgSrc}')">
961            </div>
962            ${generatePhenology(s)}
963            <div class="species-card-header">
964                <span class="species-common-name">${s.name}</span>
965                <span class="species-sci-name">(${s.scientificName})</span>
966            </div>
967            <div class="species-card-body">
968                <div class="species-recognition">
969                    <strong>Come riconoscerlo:</strong> ${s.recognition || 'Descrizione non disponibile.'}
970                </div>
971                <div class="species-habitat">
972                    <strong>Habitat:</strong> ${s.habitat || 'Informazioni sull\'habitat non disponibili.'}
973                </div>
974                <button type="button" class="btn species-select-btn">
975                    Seleziona specie
976                </button>
977            </div>
978          `;
979
980          // Attach event listener to button
981          const btn = card.querySelector('.species-select-btn');
982          if(btn) {
983              btn.addEventListener('click', (e) => {
984                  e.stopPropagation();
985                  selectSpeciesFromCard(s);
986              });
987          }
988
989          container.appendChild(card);
990      });
991      
992      container.style.display = 'grid';
993  }
994
995  function selectSpeciesFromCard(species) {
996      // Popola il campo nomeSpecie
997      const nomeSpecieInput = document.getElementById('nomeSpecie');
998      if(nomeSpecieInput) {
999          const fullName = species.scientificName ? `${species.name} (${species.scientificName})` : species.name;
1000          nomeSpecieInput.value = fullName;
1001          
1002          // Trigger Rondoni check
1003          checkRondoni(nomeSpecieInput);
1004          
1005          // Imposta 'Conosci specie' a 'Si' per mostrare i campi
1006          const conosciSi = document.querySelector('input[name="conosciSpecieLocal"][value="si"]');
1007          if(conosciSi) {
1008              conosciSi.checked = true;
1009              conosciSi.dispatchEvent(new Event('change'));
1010          }
1011          
1012          // Scorri verso i campi specie
1013          document.getElementById('speciesFields').scrollIntoView({behavior: 'smooth'});
1014      }
1015  }
1016
1017  function checkRondoni(input) {
1018      const val = input.value.toLowerCase();
1019      const cavitaRow = document.querySelector('.cavita-row');
1020      if(cavitaRow) {
1021          if(val.includes('rondone')) {
1022              cavitaRow.style.display = 'block';
1023          } else {
1024              cavitaRow.style.display = 'none';
1025          }
1026      }
1027  }
1028
1029  function updateSizeImage(sizeVal) {
1030      if(!sizeRefImage) return;
1031      
1032      let imgSrc = '';
1033      if(sizeVal.includes('PICCOLO')) imgSrc = 'grafica/mano_pettirosso.png';
1034      else if(sizeVal.includes('MEDIO')) imgSrc = 'grafica/mano_media_merlo.png';
1035      else if(sizeVal.includes('GRANDE') && !sizeVal.includes('MOLTO')) imgSrc = 'grafica/mano_grande_cornacchia.png';
1036      else if(sizeVal.includes('MOLTO GRANDE')) imgSrc = 'grafica/molto_grande_airone_cenerino.png';
1037
1038      if(imgSrc) {
1039          const img = sizeRefImage.querySelector('img');
1040          if(img) img.src = imgSrc;
1041          sizeRefImage.style.display = 'block';
1042      }
1043  }
1044
1045  function filterColors(sizeVal) {
1046      // Reset all colors
1047      colorBtns.forEach(btn => btn.style.display = 'flex');
1048      
1049      // In base alla dimensione, mostra solo i colori disponibili
1050      // Usiamo mappings per determinare quali colori sono validi per la size
1051      let sizeKey = '';
1052      if(sizeVal.includes('PICCOLO')) sizeKey = 'PICCOLO';
1053      else if(sizeVal.includes('MEDIO')) sizeKey = 'MEDIO';
1054      else if(sizeVal.includes('GRANDE') && !sizeVal.includes('MOLTO')) sizeKey = 'GRANDE';
1055      else if(sizeVal.includes('MOLTO GRANDE')) sizeKey = 'MOLTO GRANDE';
1056
1057      // Ottieni colori validi
1058      const validColors = speciesMappings
1059          .filter(m => m.size === sizeKey)
1060          .map(m => m.color);
1061      
1062      // Nascondi colori non validi
1063      colorBtns.forEach(btn => {
1064          const color = btn.getAttribute('data-color');
1065          // Rimuovi specifici colori se size è MEDIO
1066          if(sizeKey === 'MEDIO' && (color === 'NERO/GRIGIO' || color === 'GIALLO/BLU' || color === 'ROSSO/GIALLO')) {
1067             btn.style.display = 'none';
1068             return;
1069          }
1070          if(!validColors.includes(color)) {
1071              btn.style.display = 'none';
1072          }
1073      });
1074  }
1075
1076  // Event Listeners
1077  radios.forEach(r => r.addEventListener('change', updateBlocks));
1078  dimRadios.forEach(r => r.addEventListener('change', updateBlocks));
1079  
1080  // Color Buttons
1081  if(colorPanel) {
1082      colorPanel.addEventListener('click', (e) => {
1083          const btn = e.target.closest('.color-btn');
1084          if(!btn) return;
1085          
1086          // Toggle selection
1087          document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('selected'));
1088          btn.classList.add('selected');
1089          
1090          const hidden = document.getElementById('colorePredominante');
1091          if(hidden) hidden.value = btn.getAttribute('data-color');
1092          
1093          // Update cards
1094          const sizeVal = (document.querySelector('input[name="dimensione"]:checked')||{}).value;
1095          updateSpeciesCards(sizeVal, btn.getAttribute('data-color'));
1096      });
1097  }
1098
1099  // Input nomeSpecie change
1100  const nomeSpecieInput = document.getElementById('nomeSpecie');
1101  if(nomeSpecieInput) {
1102      nomeSpecieInput.addEventListener('input', () => checkRondoni(nomeSpecieInput));
1103  }
1104
1105  // Cavità
1106  const cavitaRadios = document.querySelectorAll('input[name="q78_haiOsservato[]"]');
1107  cavitaRadios.forEach(r => {
1108      r.addEventListener('change', () => {
1109          const details = document.querySelector('.cavita-detail
1109s-row');
1110          if(details) {
1111              details.style.display = (r.value === 'Si') ? 'block' : 'none';
1112          }
1113      });
1114  });
1115
1116  // ========= FILE UPLOAD & PREVIEW =========
1117  const fileInput = document.getElementById('birdImage');
1118  const previewContainer = document.getElementById('previewContainer');
1119  const imagePreviewBox = document.getElementById('imagePreviewBox');
1120
1121  if(fileInput) {
1122      fileInput.addEventListener('change', handleFileSelect);
1123  }
1124
1125  // NUOVA FUNZIONE: Converte il file HEIC/HEIF in Blob JPEG
1126  async function convertFile(file) {
1127    if (
1128      file.type === "image/heic" ||
1129      file.type === "image/heif" ||
1130      (file.name && file.name.toLowerCase().endsWith(".heic")) ||
1131      (file.name && file.name.toLowerCase().endsWith(".heif"))
1132    ) {
1133      console.log(`Tentativo di conversione del file HEIC/HEIF: ${file.name}`);
1134      if (typeof heic2any === "undefined") {
1135        console.error("Libreria heic2any non caricata.");
1136        alert("Errore: la libreria di conversione HEIC non è disponibile.");
1137        return null;
1138      }
1139      try {
1140        const jpegBlob = await heic2any({
1141          blob: file,
1142          toType: "image/jpeg",
1143          quality: 0.9,
1144        });
1145        const convertedBlob = Array.isArray(jpegBlob) ? jpegBlob[0] : jpegBlob;
1146        const newFileName = (file.name || "immagine").replace(/\.heic|\.heif/i, "") + ".jpg";
1147        return new File([convertedBlob], newFileName, {
1148          type: "image/jpeg",
1149          lastModified: file.lastModified,
1150        });
1151      } catch (e) {
1152        console.error("Errore durante la conversione HEIC:", e);
1153        alert(`Impossibile convertire il file ${file.name} (formato HEIC/HEIF). Caricare una foto JPG/PNG.`);
1154        return null;
1155      }
1156    }
1157    return file;
1158  }
1159
1160  async function compressImageToTarget(file, targetBytes) {
1161    try {
1162      if (!file || !file.type || !file.type.startsWith("image/")) return file;
1163      if (file._guardamiProcessed === "1") return file;
1164      const target = Number(targetBytes) || 0;
1165      if (target > 0 && file.size && file.size <= target) return file;
1166
1167      const img = await new Promise((resolve, reject) => {
1168        const url = URL.createObjectURL(file);
1169        const i = new Image();
1170        i.onload = () => {
1171          URL.revokeObjectURL(url);
1172          resolve(i);
1173        };
1174        i.onerror = () => {
1175          URL.revokeObjectURL(url);
1176          reject(new Error("img-load"));
1177        };
1178        i.src = url;
1179      });
1180
1181      const w0 = img.naturalWidth || img.width;
1182      const h0 = img.naturalHeight || img.height;
1183      if (!w0 || !h0) return file;
1184
1185      const maxSides = [1600, 1280, 1024];
1186      const qualities = [0.82, 0.72, 0.62, 0.52];
1187
1188      for (const maxSide of maxSides) {
1189        const scale = Math.min(1, maxSide / Math.max(w0, h0));
1190        const tw = Math.max(1, Math.round(w0 * scale));
1191        const th = Math.max(1, Math.round(h0 * scale));
1192        const canvas = document.createElement("canvas");
1193        canvas.width = tw;
1194        canvas.height = th;
1195        const ctx = canvas.getContext("2d");
1196        if (!ctx) break;
1197        ctx.drawImage(img, 0, 0, tw, th);
1198
1199        for (const q of qualities) {
1200          const blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b), "image/jpeg", q));
1201          if (!blob) continue;
1202          const newName = (file.name || "immagine").replace(/\.(jpe?g|png|webp|heic|heif)$/i, "") + ".jpg";
1203          const out = new File([blob], newName, { type: "image/jpeg" });
1204          out._guardamiProcessed = "1";
1205          if (target > 0 && out.size <= target) return out;
1206          if (target <= 0 && out.size < file.size) return out;
1207          if (target > 0 && out.size <= Math.max(target * 1.05, target + 40 * 1024)) return out;
1208        }
1209      }
1210
1211      return file;
1212    } catch (e) {
1213      return file;
1214    }
1215  }
1216
1217  async function handleFileSelect(e) {
1218      const rawFiles = Array.from(e.target.files);
1219      if(rawFiles.length === 0) return;
1220
1221      // Reset input value to allow re-selecting same file
1222      e.target.value = '';
1223
1224      for(const rawFile of rawFiles) {
1225          if(selectedFiles.length >= MAX_FILES) {
1226              alert(`Puoi caricare massimo ${MAX_FILES} foto.`);
1227              break;
1228          }
1229          
1230          // CONVERSIONE HEIC
1231          let file = await convertFile(rawFile);
1232          if(!file) continue;
1233          const currentTotalBefore = selectedFiles.reduce((acc, item) => acc + item.file.size, 0);
1234          const remaining = Math.max(0, MAX_TOTAL_BYTES - currentTotalBefore);
1235          const target = remaining > 0 ? remaining : MAX_TOTAL_BYTES;
1236          file = await compressImageToTarget(file, target);
1237
1238          // Check Total Size
1239          const currentTotal = selectedFiles.reduce((acc, item) => acc + item.file.size, 0);
1240          if(currentTotal + file.size > MAX_TOTAL_BYTES) {
1241              const maxMb = (MAX_TOTAL_BYTES / (1024 * 1024)).toFixed(0);
1242              alert(`Dimensione totale file superata (Max ${maxMb}MB).`);
1243              break;
1244          }
1245
1246          selectedFiles.push({
1247              id: Date.now() + Math.random().toString(),
1248              file: file
1249          });
1250
1251          // --- EXIF CHECK ---
1252          try {
1253             if (window.exifr) {
1254                 const toDecimal = (dms, ref) => {
1255                     if (!Array.isArray(dms) || dms.length < 3) return null;
1256                     const toNum = (v) => {
1257                         if (typeof v === "number") return v;
1258                         if (v && typeof v === "object") {
1259                             if (typeof v.numerator === "number" && typeof v.denominator === "number" && v.denominator) return v.numerator / v.denominator;
1260                             if (typeof v.num === "number" && typeof v.den === "number" && v.den) return v.num / v.den;
1261                             if (typeof v.valueOf === "function") {
1262                                 const n = Number(v.valueOf());
1263                                 if (Number.isFinite(n)) return n;
1264                             }
1265                         }
1266                         const n = Number(v);
1267                         return Number.isFinite(n) ? n : null;
1268                     };
1269                     const deg = toNum(dms[0]);
1270                     const min = toNum(dms[1]);
1271                     const sec = toNum(dms[2]);
1272                     if (!Number.isFinite(deg) || !Number.isFinite(min) || !Number.isFinite(sec)) return null;
1273                     let dec = deg + min / 60 + sec / 3600;
1274                     const r = String(ref || "").toUpperCase();
1275                     if (r === "S" || r === "W") dec = -dec;
1276                     return dec;
1277                 };
1278
1279                 const extractCoords = (meta) => {
1280                     if (!meta || typeof meta !== "object") return null;
1281                     if (typeof meta.latitude === "number" && typeof meta.longitude === "number") return { lat: meta.latitude, lng: meta.longitude };
1282                     const lat = toDecimal(meta.GPSLatitude, meta.GPSLatitudeRef);
1283                     const lng = toDecimal(meta.GPSLongitude, meta.GPSLongitudeRef);
1284                     if (typeof lat === "number" && typeof lng === "number") return { lat, lng };
1285                     if (typeof meta.gpsLatitude === "number" && typeof meta.gpsLongitude === "number") return { lat: meta.gpsLatitude, lng: meta.gpsLongitude };
1286                     return null;
1287                 };
1288
1289                 const pickDate = (meta) =>
1290                     (meta && (meta.DateTimeOriginal || meta.CreateDate || meta.ModifyDate || meta.DateTimeDigitized || meta.dateTimeOriginal || meta.createDate || meta.modifyDate)) || null;
1291                 const parseExifDate = (value) => {
1292                     if (!value) return null;
1293                     if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
1294                     const d = new Date(value);
1295                     return isNaN(d.getTime()) ? null : d;
1296                 };
1297
1298                 const output = await exifr.parse(file).catch(() => null);
1299                 if (output) {
1300                     const dateVal = pickDate(output);
1301                     const d = parseExifDate(dateVal);
1302                     if (d) {
1303                         const yyyy = d.getFullYear();
1304                         const mm = String(d.getMonth() + 1).padStart(2, '0');
1305                         const dd = String(d.getDate()).padStart(2, '0');
1306                         const dateField = document.getElementById('dataAvvistamento');
1307                         if (dateField) dateField.value = `${yyyy}-${mm}-${dd}`;
1308                     }
1309
1310                     let coords = extractCoords(output);
1311                     if (!coords && typeof exifr.gps === "function") {
1312                         try {
1313                             const gps = await exifr.gps(file);
1314                             if (gps && typeof gps.latitude === "number" && typeof gps.longitude === "number") coords = { lat: gps.latitude, lng: gps.longitude };
1315                         } catch (e) {}
1316                     }
1317
1318                     if (coords) {
1319                         const freezeKey = "__guardami_freeze_geo_uccelli_citizen_v1";
1320                         if (window[freezeKey]) return;
1321                         const pos = { lat: coords.lat, lng: coords.lng };
1322                         window.__guardamiPendingExifCoords = pos;
1323                         window.manualEditMade = true;
1324                         const latInput = document.getElementById('latitudine');
1325                         const lngInput = document.getElementById('longitudine');
1326                         if (latInput && lngInput) {
1327                             latInput.value = coords.lat.toFixed(6);
1328                             lngInput.value = coords.lng.toFixed(6);
1329                         }
1330                         syncGeoToJotform();
1331                         const w = document.getElementById('exifWarningBox');
1332                         if(w) w.style.display = 'none';
1333                         if (window.map && window.marker) {
1334                             window.map.setCenter(pos);
1335                             window.map.setZoom(15);
1336                             window.marker.setPosition(pos);
1337                             window.__guardamiPendingExifCoords = null;
1338                         }
1339                     } else {
1340                         const latInput = document.getElementById('latitudine');
1341                         if (!window.manualEditMade && latInput && !latInput.value) {
1342                             const w = document.getElementById('exifWarningBox');
1343                             if(w) w.style.display = 'block';
1344                         }
1345                     }
1346                 }
1347             }
1348          } catch(e) {
1349              console.warn("EXIF error", e);
1350          }
1351      }
1352
1353      renderPreview();
1354  }
1355
1356  function syncInputFiles() {
1357      if(!fileInput) return;
1358      const dt = new DataTransfer();
1359      selectedFiles.forEach(item => {
1360          // item.file is the File object
1361          dt.items.add(item.file);
1362      });
1363      fileInput.files = dt.files;
1364  }
1365
1366  function renderPreview() {
1367      if(!previewContainer) return;
1368      previewContainer.innerHTML = '';
1369      
1370      syncInputFiles(); // Sync input with selectedFiles
1371
1372      if(selectedFiles.length > 0) {
1373          imagePreviewBox.style.display = 'block';
1374      } else {
1375          imagePreviewBox.style.display = 'none';
1376      }
1377
1378      selectedFiles.forEach((item, index) => {
1379          const div = document.createElement('div');
1380          div.className = 'preview-item';
1381          
1382          const img = document.createElement('img');
1383          const reader = new FileReader();
1384          reader.onload = (e) => img.src = e.target.result;
1385          reader.readAsDataURL(item.file);
1386          
1387          if(item.file.type.startsWith('image/')) {
1388              img.style.cursor = 'pointer';
1389              img.onclick = () => window.openLightbox(index);
1390          }
1391          
1392          const delBtn = document.createElement('button');
1393          delBtn.className = 'thumbnail-delete';
1394          delBtn.textContent = '×';
1395          delBtn.onclick = () => removeFile(index);
1396          
1397          div.appendChild(img);
1398          div.appendChild(delBtn);
1399          previewContainer.appendChild(div);
1400      });
1401  }
1402
1403  window.removeFile = function(index) {
1404      selectedFiles.splice(index, 1);
1405      renderPreview();
1406
1407      // Se non ci sono più file, resetta data e posizione
1408      if (selectedFiles.length === 0) {
1409          // Reset Data to Today
1410          const dateField = document.getElementById('dataAvvistamento');
1411          if (dateField) {
1412              const today = new Date();
1413              const yyyy = today.getFullYear();
1414              const mm = String(today.getMonth() + 1).padStart(2, '0');
1415              const dd = String(today.getDate()).padStart(2, '0');
1416              dateField.value = `${yyyy}-${mm}-${dd}`;
1417          }
1418          
1419          // Reset Geolocation
1420          if (window.chiediGeolocalizzazione) {
1421              window.manualEditMade = false; // Reset manual edit flag
1422              window.chiediGeolocalizzazione();
1423          }
1424          
1425          // Reset EXIF warning
1426          const w = document.getElementById('exifWarningBox');
1427          if(w) w.style.display = 'none';
1428      }
1429  }
1430
1431
1432  function readFileAsDataURL(file) {
1433      return new Promise((resolve, reject) => {
1434          const reader = new FileReader();
1435          reader.onload = (e) => resolve(e.target.result);
1436          reader.onerror = (e) => reject(e);
1437          reader.readAsDataURL(file);
1438      });
1439  }
1440
1441
1442  // ========= SUBMISSION =========
1443  const form = document.getElementById('bird-form');
1444  const submitBtn = document.getElementById('submitButton'); // Assicurarsi che esista nell'HTML o aggiungerlo se manca (Jotform usa button class="form-submit-button")
1445  
1446  // Se non c'è ID, cerchiamo il bottone submit generico
1447  const realSubmitBtn = submitBtn || form.querySelector('button[type="submit"]');
1448
1449  if(form) {
1450    form.noValidate = true; // Disabilita validazione browser per gestire tutto via JS
1451
1452    form.addEventListener('submit', async (e) => {
1453      e.preventDefault(); // Blocca sempre l'invio standard per usare la nostra logica
1454
1455      const freezeKey = "__guardami_freeze_geo_uccelli_citizen_v1";
1456      window[freezeKey] = true;
1457      let hasCoords = false;
1458      try {
1459        const latInput = document.getElementById('latitudine');
1460        const lngInput = document.getElementById('longitudine');
1461        const latV = latInput ? parseFloat(latInput.value) : NaN;
1462        const lngV = lngInput ? parseFloat(lngInput.value) : NaN;
1463        if (Number.isFinite(latV) && Number.isFinite(lngV)) hasCoords = true;
1464      } catch (e) {}
1465      try {
1466        if (!hasCoords && window.marker && typeof window.marker.getPosition === 'function') {
1467          const pos = window.marker.getPosition();
1468          if (pos && typeof pos.lat === 'function' && typeof pos.lng === 'function') {
1469            const latV = pos.lat();
1470            const lngV = pos.lng();
1471            if (Number.isFinite(latV) && Number.isFinite(lngV)) hasCoords = true;
1472          }
1473        }
1474      } catch (e) {}
1475      const tryFreezeGpsNow = async () => {
1476        try {
1477          if (hasCoords) return true;
1478          if (window.manualEditMade) return true;
1479          if (!navigator.geolocation) return false;
1480          const ok = await new Promise((resolve) => {
1481            navigator.geolocation.getCurrentPosition(
1482              (pos) => {
1483                try {
1484                  const la = pos && pos.coords ? pos.coords.latitude : null;
1485                  const lo = pos && pos.coords ? pos.coords.longitude : null;
1486                  if (typeof la !== "number" || typeof lo !== "number" || !isFinite(la) || !isFinite(lo)) return resolve(false);
1487                  const p = { lat: la, lng: lo };
1488                  if (window.map && window.marker) {
1489                    window.map.setCenter(p);
1490                    window.map.setZoom(15);
1491                    window.marker.setPosition(p);
1492                  }
1493                  const latInput = document.getElementById('latitudine');
1494                  const lngInput = document.getElementById('longitudine');
1495                  if (latInput) latInput.value = la.toFixed(6);
1496                  if (lngInput) lngInput.value = lo.toFixed(6);
1497                  try { if (typeof syncGeoToJotform === "function") syncGeoToJotform(); } catch (e) {}
1498                  resolve(true);
1499                } catch (e) {
1500                  resolve(false);
1501                }
1502              },
1503              () => resolve(false),
1504              { enableHighAccuracy: true, maximumAge: 0, timeout: 8000 }
1505            );
1506          });
1507          return ok;
1508        } catch (e) {
1509          return false;
1510        }
1511      };
1512
1513      await tryFreezeGpsNow();
1514
1515      // === 0. CONTROLLO PERIMETRO (CITTÀ METROPOLITANA DI MILANO) ===
1516      const latInput = document.getElementById('latitudine');
1517      const lngInput = document.getElementById('longitudine');
1518      let lat = latInput ? parseFloat(latInput.value) : NaN;
1519      let lng = lngInput ? parseFloat(lngInput.value) : NaN;
1520
1521      // Fallback se input non trovati o vuoti
1522      if (isNaN(lat) || isNaN(lng)) {
1523           const jg = document.getElementById('jotformGeoloc')?.value;
1524           if (jg && jg.includes(',')) {
1525               const parts = jg.split(',');
1526               lat = parseFloat(parts[0]);
1527               lng = parseFloat(parts[1]);
1528           }
1529      }
1530
1531      const safeOriginalBtnText = (realSubmitBtn && typeof realSubmitBtn.textContent === "string" && realSubmitBtn.textContent.trim()) ? realSubmitBtn.textContent : "Invia";
1532      if (realSubmitBtn && !realSubmitBtn.dataset.originalText) realSubmitBtn.dataset.originalText = safeOriginalBtnText;
1533      const restoreSubmitButton = () => {
1534        if (!realSubmitBtn) return;
1535        realSubmitBtn.disabled = false;
1536        realSubmitBtn.textContent = (realSubmitBtn.dataset && realSubmitBtn.dataset.originalText) ? realSubmitBtn.dataset.originalText : safeOriginalBtnText;
1537      };
1538
1539      if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
1540        alert("Posizione mancante: consenti l'accesso al GPS oppure imposta la posizione sulla mappa prima di inviare.");
1541        return;
1542      }
1543
1544      const minLat = 45.10, maxLat = 45.70;
1545      const minLng = 8.80, maxLng = 9.70;
1546      const inBoundingBox = (lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng);
1547      if (!inBoundingBox) {
1548        const modal = document.getElementById('outOfBoundsModal');
1549        if (modal) {
1550          modal.style.display = 'flex';
1551          const closeBtn = document.getElementById('closeOutOfBoundsBtn');
1552          if (closeBtn) closeBtn.onclick = () => modal.style.display = 'none';
1553          modal.onclick = (ev) => {
1554            if (ev.target === modal) modal.style.display = 'none';
1555          };
1556        } else {
1557          alert("⚠️ Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo non può essere inviato.");
1558        }
1559        return;
1560      }
1561
1562      if (!window.google || !window.google.maps || !window.google.maps.Geocoder) {
1563        alert("Impossibile verificare la posizione (servizio mappe non disponibile). Riprova tra poco.");
1564        return;
1565      }
1566
1567      if (realSubmitBtn) {
1568        realSubmitBtn.disabled = true;
1569        realSubmitBtn.textContent = "Verifica posizione...";
1570      }
1571
1572      let geoVerified = false;
1573      let isMilano = false;
1574      try {
1575        const geoResult = await new Promise((resolve) => {
1576          let done = false;
1577          const timeoutId = setTimeout(() => {
1578            done = true;
1579            resolve({ verified: false, inMilano: false });
1580          }, 6000);
1581
1582          new google.maps.Geocoder().geocode({ location: { lat, lng } }, (results, status) => {
1583            if (done) return;
1584            clearTimeout(timeoutId);
1585            if (status === "OK" && results && results.length > 0) {
1586              const inMilano = results.some((r) =>
1587                (r.address_components || []).some((c) => {
1588                  const types = c && c.types ? c.types : [];
1589                  const ln = (c && c.long_name ? String(c.long_name) : "").toLowerCase();
1590                  const sn = (c && c.short_name ? String(c.short_name) : "").toUpperCase();
1591                  return types.includes("administrative_area_level_2") && (ln.includes("milano") || sn === "MI");
1592                })
1593              );
1594              resolve({ verified: true, inMilano });
1595              return;
1596            }
1597            resolve({ verified: false, inMilano: false });
1598          });
1599        });
1600        geoVerified = !!(geoResult && geoResult.verified);
1601        isMilano = !!(geoResult && geoResult.inMilano);
1602      } catch (e2) {
1603        geoVerified = false;
1604        isMilano = false;
1605      }
1606
1607      restoreSubmitButton();
1608
1609      if (!geoVerified) {
1610        alert("Impossibile verificare la posizione in questo momento. Riprova tra poco.");
1611        return;
1612      }
1613
1614      if (!isMilano) {
1615        const modal = document.getElementById('outOfBoundsModal');
1616        if (modal) {
1617          modal.style.display = 'flex';
1618          const closeBtn = document.getElementById('closeOutOfBoundsBtn');
1619          if (closeBtn) closeBtn.onclick = () => modal.style.display = 'none';
1620          modal.onclick = (ev) => {
1621            if (ev.target === modal) modal.style.display = 'none';
1622          };
1623        } else {
1624          alert("⚠️ Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo non può essere inviato.");
1625        }
1626        return;
1627      }
1628
1629      // === VALIDAZIONE COMPLETA ===
1630      
1631      // 1. Foto
1632      if(selectedFiles.length === 0) {
1633        alert("Carica almeno una foto.");
1634        return;
1635      }
1636      
1637      // 2. Email
1638      const email = document.getElementById('email');
1639      if(!email || !email.value || !email.value.includes('@')) {
1640         alert("Inserisci un'email valida.");
1641         return;
1642      }
1643
1644      // 3. Cosa hai osservato (Radio)
1645      const cosa = document.querySelector('input[name="q167_cosaHai"]:checked');
1646      if(!cosa) {
1647          alert("Seleziona 'Cosa hai osservato' (Uccello vivo/morto).");
1648          return;
1649      }
1650
1651      // 4. Conosci la specie (Radio)
1652      const known = document.querySelector('input[name="conosciSpecieLocal"]:checked');
1653      if(!known) {
1654          alert("Indica se conosci la specie (Sì/No).");
1655          return;
1656      }
1657      
1658      const nomeSpecie = document.getElementById('nomeSpecie');
1659      if(known.value === 'si' && (!nomeSpecie || !nomeSpecie.value.trim())) {
1660        alert("Hai indicato di conoscere la specie, per favore inserisci il nome.");
1661        return;
1662      }
1663      
1664      // 5. Numero individui (Select)
1665      const numInd = document.getElementById('numeroIndividui');
1666      if(!numInd || !numInd.value) {
1667          alert("Seleziona il numero di individui.");
1668          return;
1669      }
1670
1671      // 6. Sesso (Checkbox group)
1672      const sesso = document.querySelectorAll('input[name="q20_sesso[]"]:checked');
1673      if(sesso.length === 0) {
1674          alert("Seleziona il sesso (o 'Non riconosciuto').");
1675          return;
1676      }
1677
1678      if(realSubmitBtn) {
1679        realSubmitBtn.disabled = true;
1680        realSubmitBtn.textContent = 'Invio in corso...';
1681      }
1682
1683      try {
1684        // --- PREPARAZIONE FILE (CONVERSIONE BASE64) ---
1685        const filesPayload = [];
1686
1687        // Riduzione lato client SOLO per immagini (per video/audio inviamo l'originale)
1688        async function compressImageIfNeeded(file) {
1689          try {
1690            if (!file || !file.type || !file.type.startsWith("image/")) return file;
1691            if (file._guardamiProcessed === "1") return file;
1692            if (file.size && file.size < 400 * 1024) return file; // <400KB ok
1693
1694            // Canvas compress: max 1600px lato lungo, JPEG quality 0.8
1695            const dataUrl = await readFileAsDataURL(file);
1696            const img = await new Promise((resolve, reject) => {
1697              const i = new Image();
1698              i.onload = () => resolve(i);
1699              i.onerror = () => reject(new Error("Impossibile caricare l'immagine per la compressione."));
1700              i.src = dataUrl;
1701            });
1702
1703            const maxSide = 1600;
1704            let w = img.naturalWidth || img.width;
1705            let h = img.naturalHeight || img.height;
1706            if (!w || !h) return file;
1707
1708            const scale = Math.min(1, maxSide / Math.max(w, h));
1709            const tw = Math.round(w * scale);
1710            const th = Math.round(h * scale);
1711
1712            const canvas = document.createElement("canvas");
1713            canvas.width = tw;
1714            canvas.height = th;
1715            const ctx = canvas.getContext("2d");
1716            if (!ctx) return file;
1717            ctx.drawImage(img, 0, 0, tw, th);
1718
1719            const blob = await new Promise((resolve) => {
1720              canvas.toBlob((b) => resolve(b), "image/jpeg", 0.8);
1721            });
1722            if (!blob) return file;
1723
1724            const newName = (file.name || "immagine").replace(/\.(jpe?g|png|webp|heic|heif)$/i, "") + ".jpg";
1725            return new File([blob], newName, { type: "image/jpeg" });
1726          } catch (e) {
1727            console.warn("Compressione immagine fallita, invio originale:", e);
1728            return file;
1729          }
1730        }
1731
1732        for (const item of selectedFiles) {
1733             const f = item.file;
1734             
1735             // Converte HEIC/HEIF in JPEG se necessario (funzione già presente nello script)
1736             let fileToSend = (f && f._guardamiProcessed === "1") ? f : await convertFile(f);
1737             if (!fileToSend) fileToSend = f;
1738
1739             // Comprimi solo se immagine
1740             if (fileToSend.type && fileToSend.type.startsWith("image/")) {
1741               fileToSend = await compressImageIfNeeded(fileToSend);
1742             }
1743
1744             // Lettura come Data URL
1745             const dataUrl = await readFileAsDataURL(fileToSend);
1746             const base64 = (dataUrl || "").toString().split(",")[1] || "";
1747             
1748             filesPayload.push({
1749                fileBase64: base64,
1750                fileName: fileToSend.name || "file",
1751                fileType: fileToSend.type || "application/octet-stream"
1752             });
1753        }
1754
1755        // --- RACCOLTA DATI FORM ---
1756        const sessoValues = Array.from(sesso).map(el => el.value);
1757        const ambienteValues = Array.from(document.querySelectorAll('input[name="q13_ambienteCircostante[]"]:checked')).map(el => el.value);
1758        
1759        // Raccolta dati extra (Cavità, Atlante)
1760        const haiOsservatoCavita = (document.querySelector('input[name="q78_haiOsservato[]"]:checked') || {}).value || "";
1761        const numeroCavita = document.getElementById('numeroCavita')?.value || "";
1762        const numeroCivico = document.getElementById('numeroCivico')?.value || "";
1763        const codiciAtlante = document.getElementById('codiciAtlanteInput')?.value || ""; // CSV string
1764        const noteAtlante = document.getElementById('altreOsservazioniAtlante')?.value || "";
1765
1766        // Recupero UID (da querystring o da Firebase Auth)
1767        const urlParams = new URLSearchParams(window.location.search || "");
1768        const uidFromQuery = urlParams.get('uid') || "";
1769        const uidFromAuth = (window.firebaseAuth && window.firebaseAuth.currentUser) ? window.firebaseAuth.currentUser.uid : "";
1770        const uid = uidFromQuery || uidFromAuth || "";
1771
1772        const clientRequestKey = "guardami_pending_request_uccelli_cittadini";
1773        let clientRequestId = "";
1774        try {
1775            const raw = sessionStorage.getItem(clientRequestKey);
1776            if (raw) {
1777                const parsed = JSON.parse(raw);
1778                if (parsed && parsed.id && parsed.ts && (Date.now() - Number(parsed.ts) < 120000)) {
1779                    clientRequestId = String(parsed.id);
1780                }
1781            }
1782        } catch (e) {}
1783        if (!clientRequestId) {
1784            clientRequestId =
1785                (window.crypto && typeof window.crypto.randomUUID === "function")
1786                    ? window.crypto.randomUUID()
1787                    : `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1788            try {
1789                sessionStorage.setItem(clientRequestKey, JSON.stringify({ id: clientRequestId, ts: Date.now() }));
1790            } catch (e) {}
1791        }
1792
1793        // Costruzione payload per Netlify Function
1794        const payload = {
1795            formID: "260210470291344",
1796            nome: document.getElementById('nomeRilevatore')?.value || "",
1797            email: email.value,
1798            istitutoScolastico: document.getElementById('istitutoScolastico')?.value || "",
1799            dataAvvistamento: document.getElementById('dataAvvistamento')?.value || "",
1800            cosaOsservato: cosa.value,
1801            conosciSpecieLocal: known.value,
1802            nomeSpecie: nomeSpecie?.value || "",
1803            otherSpecies: document.getElementById('otherSpecies')?.value || "",
1804            numeroIndividui: numInd.value,
1805            sesso: sessoValues,
1806            ambienteCircostante: ambienteValues,
1807            ulterioriOsservazioni: document.getElementById('ulterioriOsservazioni')?.value || "",
1808            geoloc: document.getElementById('jotformGeoloc')?.value || "",
1809            consenso: (document.querySelector('input[name="q266_ilSottoscritto"]:checked') || {}).value || "",
1810            dimensione: (document.querySelector('input[name="dimensione"]:checked') || {}).value || "",
1811            colorePredominante: document.getElementById('colorePredominante')?.value || "",
1812            
1813            // Campi Extra
1814            haiOsservatoCavita,
1815            numeroCavita,
1816            numeroCivico,
1817            codiciAtlante: codiciAtlante ? codiciAtlante.split(',') : [],
1818            noteAtlante,
1819            
1820            folder: "uccelli_cittadini_uploads",
1821            
1822            files: filesPayload,
1823            uid,
1824            clientRequestId: clientRequestId
1825        };
1826
1827        // --- INVIO ALLA NETLIFY FUNCTION (URL ASSOLUTO per supporto WebApp locale/cross-domain) ---
1828        const res = await fetch("/.netlify/functions/submit-jotform-cittadini", {
1829            method: "POST",
1830            headers: { "Content-Type": "application/json" },
1831            body: JSON.stringify(payload),
1832        });
1833
1834        if (res.ok) {
1835            
1836        try { sessionStorage.removeItem(clientRequestKey); } catch (e) {}
1837            
1838        // --- HISTORY SAVE (SERVER SIDE ONLY) ---
1839        // Disabilitiamo il salvataggio lato client per evitare duplicati, dato che la Netlify Function se ne occupa.
1840        // Se si volesse riattivare per feedback immediato, bisognerebbe gestire la duplicazione ID.
1841        /*
1842        try {
1843            const historyPayload = {
1844                id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
1845                timestamp: new Date().toISOString(),
1846                module: "Uccelli",
1847                species: nomeSpecie?.value || "Non specificata",
1848                date: payload.dataAvvistamento || new Date().toISOString().split('T')[0],
1849                location: payload.geoloc || "Non specificata",
1850                email: payload.email || "",
1851                observer: payload.nome || "",
1852                rawPayload: payload
1853            };
1854            
1855            if (window.parent && window.parent.saveSubmissionToHistoryFirestore) {
1856                await window.parent.saveSubmissionToHistoryFirestore("Uccelli", historyPayload);
1857            } else if (window.saveSubmissionToHistoryFirestore) {
1858                await window.saveSubmissionToHistoryFirestore("Uccelli", historyPayload);
1859            }
1860        } catch(e) {
1861            console.error("Error saving history:", e);
1862        }
1863        */
1864        // -----------------------------------
1865
1866        // Update score (Citizens)
1867        // DISABILITATO: La Netlify Function incrementa già il punteggio.
1868        // Evitiamo il doppio incremento (+2 invece di +1).
1869        /*
1870        if (typeof incrementUserScore === 'function') {
1871            await incrementUserScore();
1872        }
1873        */
1874
1875        // Mostra la modale di conferma, poi ricarica la pagina per una nuova segnalazione
1876        // Funzione helper per il redirect
1877        const doRedirect = () => {
1878             const hashParams = [];
1879             hashParams.push("segnalazione_inviata=true");
1880             hashParams.push("mapModule=" + encodeURIComponent("Uccelli (Cittadini)"));
1881             
1882             if (!isNaN(lat) && !isNaN(lng)) {
1883                 hashParams.push("mapGeoloc=" + encodeURIComponent(lat + "," + lng));
1884             }
1885             
1886             if (nomeSpecie && nomeSpecie.value) {
1887                 hashParams.push("mapSpecies=" + encodeURIComponent(nomeSpecie.value));
1888             }
1889
1890             const dateField = document.getElementById('dataAvvistamento');
1891             if (dateField && dateField.value) {
1892                 hashParams.push("mapDate=" + encodeURIComponent(dateField.value));
1893             }
1894             
1895             const finalHash = "#" + hashParams.join("&");
1896             const isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
1897             const homeBaseUrl = isLocal ? 
1897"../index.html" : (window.location.origin + "/index.html")
1897;
1898             window.location.href = homeBaseUrl + finalHash;
1899        };
1900
1901        const overlay = document.getElementById("successOverlay");
1902        if (overlay) {
1903          overlay.style.display = "flex";
1904          setTimeout(() => {
1905            doRedirect();
1906          }, 2200);
1907        } else {
1908          alert("Grazie per la segnalazione! Il file è stato correttamente trasmesso. Il Team di GuardaMI");
1909          doRedirect();
1910        }
1911      } else {
1912            // Errore server
1913            let errorMessage = "Errore sconosciuto";
1914            try {
1915                const errorData = await res.json();
1916                errorMessage = errorData.error || errorData.message || JSON.stringify(errorData);
1917            } catch(e) {}
1918            console.error("Errore invio:", errorMessage);
1919            if (res.status === 403) {
1920              const modal = document.getElementById('outOfBoundsModal');
1921              if (modal) {
1922                modal.style.display = 'flex';
1923                const closeBtn = document.getElementById('closeOutOfBoundsBtn');
1924                if (closeBtn) closeBtn.onclick = () => modal.style.display = 'none';
1925                modal.onclick = (ev) => {
1926                  if (ev.target === modal) modal.style.display = 'none';
1927                };
1928              } else {
1929                alert("⚠️ Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo non può essere inviato.");
1930              }
1931              return;
1932            }
1933            alert("Errore nell'invio: " + errorMessage);
1934        }
1935
1936      } catch (err) {
1937        console.error("Errore imprevisto:", err);
1938        alert("Si è verificato un errore di connessione. Riprova.");
1939      } finally {
1940        if(realSubmitBtn) {
1941            realSubmitBtn.disabled = false;
1942            realSubmitBtn.textContent = 'Invia';
1943        }
1944      }
1945    });
1946  }
1947  
1948  // ========= GESTIONE MODAL CODICI ATLANTE (Helper globale) =========
1949  window.openAtlanteModal = function() {
1950    const m = document.getElementById('atlanteModal');
1951    if(m) m.style.display = 'flex';
1952  };
1953
1954  window.closeAtlanteModal = function() {
1955    const m = document.getElementById('atlanteModal');
1956    if(m) m.style.display = 'none';
1957    
1958    const checks = m.querySelectorAll('input[type="checkbox"]:checked');
1959    const values = Array.from(checks).map(c => c.value);
1960    
1961    const hiddenInput = document.getElementById('codiciAtlanteInput');
1962    const displayDiv = document.getElementById('codiciAtlanteDisplay');
1963
1964    if(hiddenInput) hiddenInput.value = values.join(',');
1965    
1966    // Sync Note
1967    const notesArea = document.getElementById('atlanteNotes');
1968    const notesHidden = document.getElementById('altreOsservazioniAtlante');
1969    if(notesArea && notesHidden) {
1970        notesHidden.value = notesArea.value;
1971    }
1972    
1973    if(displayDiv) {
1974      if (values.length > 0) {
1975        displayDiv.textContent = values.join(', ');
1976        displayDiv.style.display = 'block';
1977      } else {
1978        displayDiv.textContent = '';
1979        displayDiv.style.display = 'none';
1980      }
1981    }
1982  };
1983
1984  // ========= LIGHTBOX =========
1985  let currentLightboxIndex = -1;
1986
1987  window.openLightbox = function(index) {
1988    if (typeof index !== 'number' || index < 0 || index >= selectedFiles.length) return;
1989    const item = selectedFiles[index];
1990    if (!item || !item.file || !item.file.type.startsWith('image/')) return;
1991
1992    currentLightboxIndex = index;
1993    const lightbox = document.getElementById('lightbox');
1994    const lightboxImage = document.getElementById('lightboxImage');
1995    
1996    // Update arrows visibility
1997    const prevBtn = document.querySelector('.lightbox-prev');
1998    const nextBtn = document.querySelector('.lightbox-next');
1999    const deleteBtn = document.querySelector('.lightbox-delete');
2000    if(deleteBtn) deleteBtn.style.display = 'block'; // Ensure delete button is visible for uploads
2001    
2002    let prevIdx = index - 1;
2003    while(prevIdx >= 0 && (!selectedFiles[prevIdx] || !selectedFiles[prevIdx].file.type.startsWith('image/'))) prevIdx--;
2004    
2005    let nextIdx = index + 1;
2006    while(nextIdx < selectedFiles.length && (!selectedFiles[nextIdx] || !selectedFiles[nextIdx].file.type.startsWith('image/'))) nextIdx++;
2007
2008    if(prevBtn) prevBtn.style.display = (prevIdx >= 0) ? 'flex' : 'none';
2009    if(nextBtn) nextBtn.style.display = (nextIdx < selectedFiles.length) ? 'flex' : 'none';
2010
2011    if(lightbox && lightboxImage) {
2012      const reader = new FileReader();
2013      reader.onload = (e) => {
2014          lightboxImage.src = e.target.result;
2015          lightbox.classList.add('visible');
2016      };
2017      reader.readAsDataURL(item.file);
2018    }
2019  };
2020
2021  window.openSpeciesLightbox = function(src) {
2022    const lightbox = document.getElementById('lightbox');
2023    const lightboxImage = document.getElementById('lightboxImage');
2024    const prevBtn = document.querySelector('.lightbox-prev');
2025    const nextBtn = document.querySelector('.lightbox-next');
2026    const deleteBtn = document.querySelector('.lightbox-delete');
2027
2028    if(prevBtn) prevBtn.style.display = 'none';
2029    if(nextBtn) nextBtn.style.display = 'none';
2030    if(deleteBtn) deleteBtn.style.display = 'none';
2031
2032    if(lightbox && lightboxImage) {
2033        lightboxImage.src = resolveSpeciesImagePath(src);
2034        lightbox.classList.add('visible');
2035    }
2036  };
2037
2038  window.closeLightbox = function() {
2039    const lightbox = document.getElementById('lightbox');
2040    if(lightbox) {
2041      lightbox.classList.remove('visible');
2042    }
2043    currentLightboxIndex = -1;
2044  };
2045
2046  window.prevImage = function() {
2047      let newIndex = currentLightboxIndex - 1;
2048      while(newIndex >= 0 && (!selectedFiles[newIndex] || !selectedFiles[newIndex].file.type.startsWith('image/'))){
2049          newIndex--;
2050      }
2051      if(newIndex >= 0) window.openLightbox(newIndex);
2052  };
2053
2054  window.nextImage = function() {
2055      let newIndex = currentLightboxIndex + 1;
2056      while(newIndex < selectedFiles.length && (!selectedFiles[newIndex] || !selectedFiles[newIndex].file.type.startsWith('image/'))){
2057          newIndex++;
2058      }
2059      if(newIndex < selectedFiles.length) window.openLightbox(newIndex);
2060  };
2061
2062  window.deleteCurrentLightboxImage = function() {
2063      if(currentLightboxIndex > -1) {
2064           window.removeFile(currentLightboxIndex);
2065           window.closeLightbox();
2066      }
2067  };
2068
2069  // Close lightbox when clicking outside the image
2070  const lightbox = document.getElementById('lightbox');
2071  if(lightbox) {
2072    lightbox.addEventListener('click', function(e) {
2073      if(e.target === lightbox) {
2074        closeLightbox();
2075      }
2076    });
2077  }
2078
2079  // Initialize blocks
2080  updateBlocks();
2081
2082  // Funzione per aprire/chiudere i box info
2083  window.toggleInfo = function(id) {
2084    const box = document.getElementById(id);
2085    if (box) {
2086      box.classList.toggle('visible');
2087    }
2088  };
2089
2090  // ========= SUBMIT CONSENT LOGIC =========
2091  const consentSubmitBtn = document.getElementById('submitButton');
2092  const consentRadiosGroup = document.querySelectorAll('input[name="q266_ilSottoscritto"]');
2093
2094  function updateSubmitState() {
2095      if(!consentSubmitBtn) return;
2096      
2097      const consentValue = (document.querySelector('input[name="q266_ilSottoscritto"]:checked') || {}).value;
2098      const ageGate = document.getElementById('ageGate14');
2099      const ageOk = (ageGate && ageGate.checked) || localStorage.getItem(AGE_GATE_KEY) === "true";
2100      
2101      if (consentValue === 'Acconsento' && ageOk) {
2102          consentSubmitBtn.disabled = false;
2103          consentSubmitBtn.style.background = '#1d4ed8'; // Blue
2104          consentSubmitBtn.style.cursor = 'pointer';
2105      } else {
2106          consentSubmitBtn.disabled = true;
2107          consentSubmitBtn.style.background = '#9ca3af'; // Gray
2108          consentSubmitBtn.style.cursor = 'not-allowed';
2109      }
2110  }
2111
2112  // Init immediately
2113  updateSubmitState();
2114
2115  // Listeners
2116  consentRadiosGroup.forEach(r => {
2117      r.addEventListener('change', updateSubmitState);
2118  });
2119
2120})();
2121
2122async function incrementUserScore() {
2123  // 1. Try Modular SDK (window.fs) - PREFERRED
2124  if (window.firebaseAuth && window.firebaseDb && window.fs && window.fs.updateDoc && window.fs.increment) {
2125      const user = window.firebaseAuth.currentUser;
2126      if (!user) {
2127          console.warn("User not logged in (Modular), score not updated.");
2128          return;
2129      }
2130      
2131      const userRef = window.fs.doc(window.firebaseDb, "users", user.uid);
2132      try {
2133          await window.fs.updateDoc(userRef, {
2134              points: window.fs.increment(1)
2135          });
2136          console.log("Score updated successfully +1 (Modular)");
2137          return;
2138      } catch (e) {
2139          console.error("Score update failed (Modular):", e);
2140          // Retry with setDoc if doc missing
2141          if (e.code === 'not-found' || e.message.includes("No document to update")) {
2142               try {
2143                   await window.fs.setDoc(userRef, { points: 1 }, { merge: true });
2144                   console.log("Score set successfully (Modular)");
2145               } catch (err) {
2146                   console.error("Retry set points failed (Modular):", err);
2147               }
2148          }
2149          return; 
2150      }
2151  }
2152}
2153
2154
2155  const istitutoScolastico = document.getElementById('istitutoScolastico');
2156  if (istitutoScolastico) {
2157    const scuoleList = document.getElementById('scuoleList');
2158    const normalize = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim();
2159    const updateSuggestions = () => {
2160      if (!scuoleList || typeof SCUOLE_MILANO === 'undefined') return;
2161      const q = normalize(istitutoScolastico.value);
2162      scuoleList.innerHTML = '';
2163      if (!q) return;
2164      let added = 0;
2165      for (const scuola of SCUOLE_MILANO) {
2166        if (normalize(scuola).includes(q)) {
2167          const option = document.createElement('option');
2168          option.value = scuola;
2169          scuoleList.appendChild(option);
2170          added++;
2171          if (added >= 30) break;
2172        }
2173      }
2174    };
2175
2176    const savedIstituto = localStorage.getItem('guardami_istituto');
2177    if (savedIstituto) {
2178      istitutoScolastico.value = savedIstituto;
2179    }
2180
2181    istitutoScolastico.addEventListener('input', () => {
2182      localStorage.setItem('guardami_istituto', istitutoScolastico.value);
2183      updateSuggestions();
2184    });
2185    istitutoScolastico.addEventListener('focus', updateSuggestions);
2186  }

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.