4sp-dv 1.0.26 → 1.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  <title>4SP - VERSION 5 CLIENT</title>
7
7
  <link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo.png">
8
8
 
9
- <base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.25/logged-in/">
9
+ <base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.28/logged-in/">
10
10
  <script src="https://cdn.tailwindcss.com"></script>
11
11
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
12
12
 
@@ -106,7 +106,7 @@
106
106
  font-size: 1rem;
107
107
  cursor: pointer;
108
108
  opacity: 1;
109
- transition: opacity 0.3s, color 0.3s ease;
109
+ transition: opacity 0.3s, color 0.3s ease, background-color 0.3s ease;
110
110
  z-index: 55;
111
111
  pointer-events: auto;
112
112
  background: transparent;
@@ -115,13 +115,17 @@
115
115
 
116
116
  #glide-left {
117
117
  left: 0;
118
- background: linear-gradient(to right, var(--navbar-bg, #000000) 30%, transparent);
118
+ background-color: var(--navbar-bg, #000000);
119
+ -webkit-mask-image: linear-gradient(to right, black 30%, transparent);
120
+ mask-image: linear-gradient(to right, black 30%, transparent);
119
121
  justify-content: flex-start;
120
122
  padding-left: 8px;
121
123
  }
122
124
  #glide-right {
123
125
  right: 0;
124
- background: linear-gradient(to left, var(--navbar-bg, #000000) 30%, transparent);
126
+ background-color: var(--navbar-bg, #000000);
127
+ -webkit-mask-image: linear-gradient(to left, black 30%, transparent);
128
+ mask-image: linear-gradient(to left, black 30%, transparent);
125
129
  justify-content: flex-end;
126
130
  padding-right: 8px;
127
131
  }
@@ -140,7 +144,7 @@
140
144
  .nav-tab:hover {
141
145
  color: var(--tab-hover-text, #ffffff);
142
146
  background-color: var(--tab-hover-bg, rgba(79, 70, 229, 0.05));
143
- border-color: var(--tab-hover-border, transparent);
147
+ border-color: var(--tab-active-border, #4f46e5);
144
148
  transform: translateY(-1px);
145
149
  z-index: 50;
146
150
  }
@@ -424,7 +428,7 @@
424
428
  }
425
429
  .nav-tab:hover {
426
430
  color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05);
427
- border-color: #4f46e5; transform: scale(1.02);
431
+ border-color: var(--tab-active-border, #4f46e5); transform: scale(1.02);
428
432
  z-index: 50;
429
433
  }
430
434
 
@@ -611,7 +615,7 @@
611
615
  </div>
612
616
 
613
617
  <label class="block text-gray-400 text-xs mb-2 font-light">Background Color</label>
614
- <div class="grid grid-cols-5 gap-2 mb-6" id="pfp-color-grid">
618
+ <div class="flex flex-wrap gap-2 mb-6" id="pfp-color-grid">
615
619
  <!-- Colors generated by JS -->
616
620
  </div>
617
621
 
@@ -774,7 +778,16 @@
774
778
  const displayUsername = document.getElementById('display-username');
775
779
  const displayEmail = document.getElementById('display-email');
776
780
 
777
- const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.25/logged-in/';
781
+ const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.28/logged-in/';
782
+
783
+ // Preload Logos
784
+ const preloadImgs = [
785
+ 'https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo-dark.png',
786
+ 'https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo-christmas.png',
787
+ 'https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png'
788
+ ];
789
+ preloadImgs.forEach(src => new Image().src = src);
790
+
778
791
  const STORAGE_KEY = 'local_access_code';
779
792
  const USER_DATA_KEY = 'local_user_data';
780
793
  const LAST_PAGE_KEY = 'local_last_page';
@@ -1109,7 +1122,26 @@
1109
1122
  }
1110
1123
 
1111
1124
  // Initial check
1112
- setTimeout(updateScrollGilders, 50);
1125
+ setTimeout(() => {
1126
+ updateScrollGilders();
1127
+
1128
+ // Ensure Active Tab Visibility (Scroll if under gradient)
1129
+ const activeTab = tabsContainerEl.querySelector('.nav-tab.active');
1130
+ if (activeTab) {
1131
+ const containerRect = tabsContainerEl.getBoundingClientRect();
1132
+ const tabRect = activeTab.getBoundingClientRect();
1133
+ const gradientWidth = 70; // Gradient is ~60px + padding
1134
+
1135
+ // Check Left Edge
1136
+ if (tabRect.left < containerRect.left + gradientWidth) {
1137
+ tabsContainerEl.scrollBy({ left: tabRect.left - containerRect.left - gradientWidth - 20, behavior: 'smooth' });
1138
+ }
1139
+ // Check Right Edge
1140
+ else if (tabRect.right > containerRect.right - gradientWidth) {
1141
+ tabsContainerEl.scrollBy({ left: tabRect.right - containerRect.right + gradientWidth + 20, behavior: 'smooth' });
1142
+ }
1143
+ }
1144
+ }, 50);
1113
1145
  }
1114
1146
 
1115
1147
  // Auth Menu
@@ -1614,6 +1646,9 @@
1614
1646
  if (sec.id === `tab-${tabId}`) sec.classList.remove('hidden');
1615
1647
  else sec.classList.add('hidden');
1616
1648
  });
1649
+
1650
+ // Save State
1651
+ localStorage.setItem('last_settings_tab', tabId);
1617
1652
  });
1618
1653
  });
1619
1654
 
@@ -1684,7 +1719,18 @@
1684
1719
  // --- 2. Personalization ---
1685
1720
  // Letter Avatar Setup
1686
1721
  // Removed last color (Grey) to make 10 colors (5x2 grid)
1687
- const letterColors = ['EF4444', 'F97316', 'F59E0B', '84CC16', '10B981', '06B6D4', '3B82F6', '6366F1', '8B5CF6', 'EC4899'];
1722
+ const letterColors = [
1723
+ 'EF4444', // Red
1724
+ 'F97316', 'FDBA74', // Orange
1725
+ 'EAB308', 'FDE047', // Yellow
1726
+ '22C55E', '86EFAC', // Green
1727
+ '06B6D4', '67E8F9', // Cyan
1728
+ '3B82F6', '93C5FD', // Blue
1729
+ '6366F1', 'A5B4FC', // Indigo
1730
+ 'A855F7', 'D8B4FE', // Purple
1731
+ 'EC4899', 'F9A8D4', // Pink
1732
+ '6B7280', '000000' // Grey & Black
1733
+ ];
1688
1734
  const pfpModeBtns = document.querySelectorAll('.pfp-mode-btn');
1689
1735
  const pfpLetterOptions = document.getElementById('pfp-letter-options');
1690
1736
  const pfpUploadOptions = document.getElementById('pfp-upload-options');
@@ -1726,6 +1772,11 @@
1726
1772
  const div = document.createElement('div');
1727
1773
  div.className = 'w-10 h-10 rounded-lg cursor-pointer hover:scale-110 transition border-2 border-transparent';
1728
1774
  div.style.backgroundColor = '#' + color;
1775
+
1776
+ if (color === '000000') {
1777
+ div.style.borderColor = '#ffffff';
1778
+ }
1779
+
1729
1780
  div.onclick = () => {
1730
1781
  pfpState.letterColor = '#' + color;
1731
1782
  updatePfpPreview();
@@ -2096,24 +2147,36 @@
2096
2147
  // Apply Logo & Tinting
2097
2148
  const logoImg = document.getElementById('navbar-logo');
2098
2149
  if (logoImg) {
2099
- // Update Source (Use absolute path comparison or specific logic)
2100
- // We just set it. Browser handles cache.
2101
- // Note: The original navigation.js checked src endsWith to avoid reload flicker, but simple assignment is okay.
2102
2150
  if (!logoImg.src.includes(logoSrc)) {
2103
2151
  logoImg.src = logoSrc;
2104
2152
  }
2105
2153
 
2106
- // Apply Tinting
2107
2154
  const noFilterThemes = ['Dark', 'Light', 'Christmas'];
2108
- if (noFilterThemes.includes(theme.name)) {
2155
+ const isNoFilter = noFilterThemes.includes(theme.name);
2156
+
2157
+ // Check if mode is changing (Tinted <-> Standard)
2158
+ // We check existing transform state
2159
+ const wasNoFilter = logoImg.style.transform === '' || logoImg.style.transform === 'none';
2160
+ const modeChanged = isNoFilter !== wasNoFilter;
2161
+
2162
+ if (modeChanged) {
2163
+ logoImg.style.transition = 'none';
2164
+ }
2165
+
2166
+ if (isNoFilter) {
2109
2167
  logoImg.style.filter = '';
2110
2168
  logoImg.style.transform = '';
2111
2169
  } else {
2112
2170
  const tintColor = theme['tab-active-text'] || '#ffffff';
2113
- // The original navigation.js used this trick for SVG coloring via filter
2114
2171
  logoImg.style.filter = `drop-shadow(100px 0 0 ${tintColor})`;
2115
2172
  logoImg.style.transform = 'translateX(-100px)';
2116
2173
  }
2174
+
2175
+ if (modeChanged) {
2176
+ // Force Reflow
2177
+ void logoImg.offsetWidth;
2178
+ logoImg.style.transition = ''; // Restore
2179
+ }
2117
2180
  }
2118
2181
 
2119
2182
  // Save to local storage for persistence on reload
@@ -2371,6 +2434,14 @@
2371
2434
 
2372
2435
  updatePfpPreview();
2373
2436
  }
2437
+
2438
+ // Restore Tab
2439
+ const lastTab = localStorage.getItem('last_settings_tab');
2440
+ if (lastTab) {
2441
+ const tabBtn = document.querySelector(`.settings-tab[data-tab="${lastTab}"]`);
2442
+ if(tabBtn) tabBtn.click();
2443
+ }
2444
+
2374
2445
  loadThemes();
2375
2446
  loadPanicKeys();
2376
2447
  }
@@ -291,7 +291,7 @@
291
291
  .nav-tab:hover {
292
292
  color: var(--tab-hover-text);
293
293
  background-color: rgba(79, 70, 229, 0.05);
294
- border-color: #4f46e5;
294
+ border-color: var(--tab-active-border);
295
295
  transform: scale(1.05); /* Tab bubbles up */
296
296
  }
297
297
  .nav-tab.active {
@@ -525,7 +525,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
525
525
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
526
526
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
527
527
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
528
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
528
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
529
529
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
530
530
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
531
531
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -368,7 +368,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
368
368
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
369
369
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
370
370
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
371
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
371
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
372
372
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
373
373
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
374
374
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -74,6 +74,7 @@
74
74
 
75
75
  /* Enable absolute positioning for spinner */
76
76
  position: relative;
77
+ min-height: 200px; /* Ensure height stability */
77
78
  }
78
79
 
79
80
  .dashboard-widget:hover {
@@ -125,6 +126,12 @@
125
126
  width: 1px;
126
127
  height: 100px;
127
128
  background-color: #262626;
129
+ transition: opacity 0.5s ease, width 0.5s ease;
130
+ }
131
+ .vertical-divider.hidden-anim {
132
+ opacity: 0;
133
+ width: 0;
134
+ margin: 0;
128
135
  }
129
136
 
130
137
  /* --- WEATHER STYLES --- */
@@ -134,6 +141,15 @@
134
141
  flex-direction: column;
135
142
  justify-content: center;
136
143
  gap: 0.5rem;
144
+ transition: opacity 0.5s ease, transform 0.5s ease;
145
+ opacity: 1;
146
+ transform: translateY(0);
147
+ }
148
+ .weather-section.hidden-anim {
149
+ opacity: 0;
150
+ transform: translateY(10px);
151
+ pointer-events: none;
152
+ position: absolute; /* Take out of flow when hidden */
137
153
  }
138
154
 
139
155
  .weather-main {
@@ -213,6 +229,38 @@
213
229
  font-size: 1.2rem;
214
230
  }
215
231
 
232
+ /* Retry Button */
233
+ #retry-weather-btn {
234
+ position: fixed;
235
+ bottom: 2rem;
236
+ right: 2rem;
237
+ width: 40px;
238
+ height: 40px;
239
+ border-radius: 14px;
240
+ border: 1px solid #4b5563;
241
+ background: transparent;
242
+ color: #d1d5db;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ cursor: pointer;
247
+ z-index: 100;
248
+ opacity: 0;
249
+ pointer-events: none;
250
+ transition: opacity 0.5s ease, transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), background-color 0.2s;
251
+ transform: scale(0.8) translateY(20px);
252
+ }
253
+ #retry-weather-btn:hover {
254
+ background-color: #374151;
255
+ color: white;
256
+ transform: scale(1.1) translateY(0);
257
+ }
258
+ #retry-weather-btn.visible {
259
+ opacity: 1;
260
+ pointer-events: auto;
261
+ transform: scale(1) translateY(0);
262
+ }
263
+
216
264
  /* Font Awesome's base spin animation (needed for custom speed) */
217
265
  @keyframes fa-spin {
218
266
  0% {
@@ -250,7 +298,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
250
298
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
251
299
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
252
300
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
253
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
301
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
254
302
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
255
303
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
256
304
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -276,9 +324,9 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
276
324
  <div id="clock-date" class="date-display">--------</div>
277
325
  </div>
278
326
 
279
- <div class="vertical-divider"></div>
327
+ <div class="vertical-divider" id="weather-divider"></div>
280
328
 
281
- <div class="weather-section">
329
+ <div class="weather-section" id="weather-content">
282
330
  <div class="weather-main">
283
331
  <div id="w-icon" class="weather-icon"><i class="fa-solid fa-cloud"></i></div>
284
332
  <div id="w-temp" class="weather-temp">--°</div>
@@ -301,6 +349,10 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
301
349
  </div>
302
350
  </div>
303
351
 
352
+ <button id="retry-weather-btn" onclick="retryWeather()" title="Retry Weather">
353
+ <i class="fa-solid fa-cloud-sun"></i>
354
+ </button>
355
+
304
356
  <div id="weather-loader">
305
357
  <i class="fa-solid fa-circle-notch fa-spin-slow"></i>
306
358
  </div>
@@ -329,6 +381,10 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
329
381
  const wWindArrow = document.getElementById('w-wind-arrow');
330
382
  const wLocLink = document.getElementById('w-location-link');
331
383
  const weatherLoader = document.getElementById('weather-loader');
384
+
385
+ const weatherContent = document.getElementById('weather-content');
386
+ const weatherDivider = document.getElementById('weather-divider');
387
+ const retryBtn = document.getElementById('retry-weather-btn');
332
388
 
333
389
  // --- LOADER FUNCTIONS ---
334
390
  function showLoader() {
@@ -339,6 +395,35 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
339
395
  weatherLoader.classList.remove('loading');
340
396
  }
341
397
 
398
+ // --- ERROR STATE UI ---
399
+ function showRetryState() {
400
+ hideLoader();
401
+ weatherContent.classList.add('hidden-anim');
402
+ weatherDivider.classList.add('hidden-anim');
403
+ retryBtn.classList.add('visible');
404
+ }
405
+
406
+ function hideRetryState() {
407
+ retryBtn.classList.remove('visible');
408
+ // We don't immediately show weatherContent, wait for data or loader
409
+ // But we can prep them
410
+ weatherContent.classList.remove('hidden-anim');
411
+ weatherDivider.classList.remove('hidden-anim');
412
+ }
413
+
414
+ // --- RETRY LOGIC ---
415
+ window.retryWeather = function() {
416
+ hideRetryState();
417
+ showLoader();
418
+
419
+ // If we have coords, use them. If not, re-init.
420
+ if (CURRENT_LAT && CURRENT_LON) {
421
+ loadWeatherPrimary(CURRENT_LAT, CURRENT_LON);
422
+ } else {
423
+ initWeather();
424
+ }
425
+ };
426
+
342
427
  // --- FIREBASE INIT WAITER ---
343
428
  function waitForFirebase(callback) {
344
429
  const check = setInterval(() => {
@@ -416,30 +501,42 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
416
501
  // Cache is fresh, use it
417
502
  console.log("Using cached weather data.");
418
503
  updateWeatherUI(cache.weatherData);
419
- hideLoader(); // FIX: Added hideLoader for cache hits
504
+ hideLoader();
505
+ // Ensure UI is visible
506
+ weatherContent.classList.remove('hidden-anim');
507
+ weatherDivider.classList.remove('hidden-anim');
508
+ retryBtn.classList.remove('visible');
420
509
  } else {
421
510
  // Cache is stale or non-existent, fetch new data
422
511
  console.log("Weather cache stale or missing. Fetching new data.");
423
512
  resetWeatherDisplay();
424
- loadWeatherWithFallback(lat, lon);
513
+ loadWeatherPrimary(lat, lon);
425
514
  }
426
515
  }
427
516
 
428
517
  // --- LOCATION FETCH (IP Fallback) ---
429
518
  async function initWeather() {
430
- showLoader(); // Show loader at start of location determination
431
- try {
432
- const ipRes = await fetch('https://ipapi.co/json/');
433
- if (!ipRes.ok) throw new Error('IP API Error');
434
- const ipData = await ipRes.json();
435
- lat = ipData.latitude;
436
- lon = ipData.longitude;
437
- } catch (e) {
438
- console.warn("IP location fetch failed:", e);
439
- // Fallback to New York or handle gracefully
440
- lat = 40.7128;
441
- lon = -74.0060;
442
- }
519
+ showLoader();
520
+ // Hide previous state to avoid clutter during load
521
+ weatherContent.classList.add('hidden-anim');
522
+ weatherDivider.classList.add('hidden-anim');
523
+ retryBtn.classList.remove('visible');
524
+
525
+ try {
526
+ const ipRes = await fetch('https://ipapi.co/json/');
527
+ if (!ipRes.ok) throw new Error('IP API Error');
528
+ const ipData = await ipRes.json();
529
+
530
+ // Store globally so retry can use it
531
+ CURRENT_LAT = ipData.latitude;
532
+ CURRENT_LON = ipData.longitude;
533
+
534
+ loadWeatherPrimary(CURRENT_LAT, CURRENT_LON);
535
+ } catch (e) {
536
+ console.warn("IP location fetch failed:", e);
537
+ // Even if location fails, show retry button
538
+ showRetryState();
539
+ }
443
540
  }
444
541
 
445
542
  // --- LOCATION FETCH (Browser Geolocation) ---
@@ -449,8 +546,13 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
449
546
  return;
450
547
  }
451
548
 
452
- showLoader(); // Show loader before geolocating
549
+ showLoader();
453
550
  resetWeatherDisplay();
551
+
552
+ // Hide content while locating
553
+ weatherContent.classList.add('hidden-anim');
554
+ weatherDivider.classList.add('hidden-anim');
555
+ retryBtn.classList.remove('visible');
454
556
 
455
557
  try {
456
558
  const pos = await new Promise((resolve, reject) => {
@@ -463,131 +565,33 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
463
565
  CURRENT_LAT = pos.coords.latitude;
464
566
  CURRENT_LON = pos.coords.longitude;
465
567
 
466
- // When explicitly asked for new location, bypass cache and force a fetch
467
- loadWeatherWithFallback(CURRENT_LAT, CURRENT_LON);
568
+ loadWeatherPrimary(CURRENT_LAT, CURRENT_LON);
468
569
 
469
570
  } catch(e) {
470
571
  console.error("Geolocation failed:", e);
471
- // Fallback to IP data if browser location fails (which is checked via initWeather)
472
- initWeather();
572
+ // On geo fail, try init (IP) again? Or just error state.
573
+ // Let's go to error state to let user retry or see issue
574
+ showRetryState();
473
575
  }
474
576
  }
475
577
 
476
- // --- FALLBACK LOGIC ---
477
- async function loadWeatherWithFallback(lat, lon) {
478
- // Loader is already shown by calling functions (initWeather or getUserBrowserLocation)
578
+ // --- PRIMARY FETCH (OPEN METEO ONLY) ---
579
+ async function loadWeatherPrimary(lat, lon) {
479
580
  try {
480
- // 1. Google Weather (using Firebase API Key) - PRIMARY
481
- try {
482
- await fetchGoogleWeather(lat, lon);
483
- return;
484
- } catch(e) {
485
- console.warn("Google Weather failed, attempting NWS:", e);
486
- }
487
-
488
- // 2. NWS - SECONDARY
489
- try {
490
- await fetchNWS(lat, lon);
491
- return;
492
- } catch(e) {
493
- console.warn("NWS failed, attempting Open-Meteo:", e);
494
- }
495
-
496
- // 3. Open-Meteo - TERTIARY
497
- try {
498
- await fetchOpenMeteo(lat, lon);
499
- } catch(e) {
500
- console.error("All weather sources failed", e);
501
- hideLoader(); // Hide on absolute failure
502
- }
503
- } catch (finalError) {
504
- // Catches errors during the fallback chain setup
505
- hideLoader();
581
+ await fetchOpenMeteo(lat, lon);
582
+ } catch (error) {
583
+ console.error("Weather fetch failed:", error);
584
+ showRetryState();
506
585
  }
507
586
  }
508
587
 
509
- // --- GOOGLE WEATHER FETCH ---
510
- async function fetchGoogleWeather(lat, lon) {
511
- const apiKey = firebaseConfig.apiKey;
512
- const url = `https://weather.googleapis.com/v1/currentConditions:lookup?key=${apiKey}&location.latitude=${lat}&location.longitude=${lon}`;
513
-
514
- const res = await fetch(url);
515
- if (!res.ok) {
516
- const errText = await res.text();
517
- console.error("Google Weather API Failed. Details:", errText);
518
- throw new Error(`Google Weather Error: ${res.status} - ${errText}`);
519
- }
520
-
521
- const data = await res.json();
522
- if (!data.currentConditions) throw new Error("Invalid Google Weather Data");
523
-
524
- const curr = data.currentConditions;
525
-
526
- let tempF = curr.temperature?.value;
527
- if (curr.temperature?.unit?.toLowerCase() === 'celsius' || curr.temperature?.unit?.toLowerCase() === 'c') {
528
- tempF = (tempF * 9/5) + 32;
529
- }
530
-
531
- const weatherCode = curr.weatherCondition || "CLOUDY";
532
-
533
- const weatherData = {
534
- temp: Math.round(tempF || 0),
535
- iconCode: weatherCode,
536
- isDay: true,
537
- high: "--",
538
- low: "--",
539
- windSpeed: (curr.windSpeed?.value || 0) + " mph",
540
- windDir: curr.windDirection || 0,
541
- isNWS: false,
542
- isGoogle: true
543
- };
544
-
545
- setWeatherCache(weatherData);
546
- updateWeatherUI(weatherData);
547
- hideLoader(); // Hide loader on success
548
- }
549
-
550
- // --- NWS FETCH ---
551
- async function fetchNWS(lat, lon) {
552
- const pointsRes = await fetch(`https://api.weather.gov/points/${lat},${lon}`);
553
- if (!pointsRes.ok) throw new Error("NWS Point Fail");
554
- const pointsData = await pointsRes.json();
555
-
556
- const [forecastRes, hourlyRes] = await Promise.all([
557
- fetch(pointsData.properties.forecast),
558
- fetch(pointsData.properties.forecastHourly)
559
- ]);
560
-
561
- const dailyData = await forecastRes.json();
562
- const hourlyData = await hourlyRes.json();
563
-
564
- const current = hourlyData.properties.periods[0];
565
-
566
- const first24 = hourlyData.properties.periods.slice(0, 24);
567
- const temps = first24.map(p => p.temperature);
568
- const high = Math.max(...temps);
569
- const low = Math.min(...temps);
570
-
571
- const weatherData = {
572
- temp: current.temperature,
573
- iconCode: current.shortForecast,
574
- isDay: current.isDaytime,
575
- high: high,
576
- low: low,
577
- windSpeed: current.windSpeed,
578
- windDir: current.windDirection,
579
- isNWS: true
580
- };
581
-
582
- setWeatherCache(weatherData);
583
- updateWeatherUI(weatherData);
584
- hideLoader(); // Hide loader on success
585
- }
586
-
587
588
  // --- OPEN METEO FETCH ---
588
589
  async function fetchOpenMeteo(lat, lon) {
589
590
  const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,is_day,weather_code,wind_speed_10m,wind_direction_10m&daily=temperature_2m_max,temperature_2m_min&temperature_unit=fahrenheit&wind_speed_unit=mph&timezone=auto`;
591
+
590
592
  const res = await fetch(url);
593
+ if (!res.ok) throw new Error("Open-Meteo API response not OK");
594
+
591
595
  const data = await res.json();
592
596
 
593
597
  const weatherData = {
@@ -603,7 +607,12 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
603
607
 
604
608
  setWeatherCache(weatherData);
605
609
  updateWeatherUI(weatherData);
606
- hideLoader(); // Hide loader on success
610
+
611
+ hideLoader();
612
+ // Success: Reveal UI
613
+ weatherContent.classList.remove('hidden-anim');
614
+ weatherDivider.classList.remove('hidden-anim');
615
+ retryBtn.classList.remove('visible');
607
616
  }
608
617
 
609
618
  // --- UI UPDATE (Unchanged) ---
@@ -248,7 +248,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
248
248
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
249
249
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
250
250
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
251
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
251
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
252
252
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
253
253
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
254
254
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -265,7 +265,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
265
265
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
266
266
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
267
267
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
268
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
268
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
269
269
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
270
270
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
271
271
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -395,7 +395,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
395
395
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
396
396
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
397
397
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
398
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
398
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
399
399
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
400
400
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
401
401
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -211,7 +211,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
211
211
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
212
212
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
213
213
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
214
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
214
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
215
215
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
216
216
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
217
217
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -287,7 +287,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
287
287
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
288
288
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
289
289
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
290
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
290
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
291
291
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
292
292
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
293
293
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -271,7 +271,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
271
271
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
272
272
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
273
273
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
274
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
274
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
275
275
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
276
276
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
277
277
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -270,7 +270,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
270
270
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
271
271
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
272
272
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
273
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
273
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
274
274
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
275
275
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
276
276
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -216,7 +216,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
216
216
  .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
217
217
  .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
218
218
  .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
219
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
219
+ .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: var(--tab-active-border); transform: scale(1.05); }
220
220
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
221
221
  .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
222
222
  .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@@ -18,7 +18,7 @@
18
18
  <script src="../injector.js"></script>
19
19
 
20
20
  <style>
21
- /* --- 4SP BASE STYLING (MATCHING notes.html) --- */
21
+ /* --- 4SP BASE STYLING --- */
22
22
  :root {
23
23
  --bg-page: #040404;
24
24
  --bg-container: #000000;
@@ -46,55 +46,70 @@
46
46
  font-weight: 400 !important;
47
47
  }
48
48
 
49
- /* --- UI COMPONENTS (MATCHING notes.html) --- */
49
+ /* --- UI COMPONENTS --- */
50
50
  .btn-toolbar-style {
51
51
  background: var(--menu-bg, #000000);
52
52
  border: 1px solid var(--menu-border, #333);
53
53
  border-radius: 0.75rem;
54
54
  color: #d1d5db;
55
- padding: 0.5rem 1rem;
55
+ padding: 0.6rem 1.2rem;
56
56
  font-weight: 500;
57
57
  cursor: pointer;
58
58
  transition: all 0.2s;
59
59
  display: inline-flex;
60
60
  align-items: center;
61
61
  justify-content: center;
62
- gap: 0.5rem;
62
+ gap: 0.75rem;
63
+ font-size: 0.95rem;
64
+ white-space: nowrap;
63
65
  }
64
66
 
65
67
  .btn-toolbar-style:hover {
66
68
  background-color: #000000;
67
69
  border-color: #fff;
68
70
  color: #ffffff;
71
+ transform: translateY(-1px);
69
72
  }
70
73
 
71
- .btn-toolbar-style.btn-primary-override {
72
- background-color: rgba(79, 70, 229, 0.1);
73
- border: 1px solid #4f46e5;
74
- color: #4f46e5;
75
- }
76
- .btn-toolbar-style.btn-primary-override:hover {
77
- background-color: rgba(79, 70, 229, 0.15);
78
- border-color: #6366f1;
79
- color: #6366f1;
74
+ .provider-select-wrapper {
75
+ position: relative;
76
+ display: inline-block;
77
+ min-width: 180px; /* Minimum width */
80
78
  }
81
-
82
- /* Input Styling */
83
- .input-text-style {
84
- width: 100%;
85
- padding: 0.6rem 1rem;
86
- border: 1px solid #333;
87
- background-color: #000000;
88
- border-radius: 0.75rem;
89
- color: #c0c0c0;
90
- transition: all 0.2s;
79
+
80
+ .provider-select {
81
+ appearance: none;
82
+ background: #0d0d0d;
83
+ border: 1px solid #333;
84
+ color: #fff;
85
+ padding: 0.6rem 2.5rem 0.6rem 1rem;
86
+ border-radius: 0.75rem;
87
+ font-family: 'Geist', sans-serif;
91
88
  font-size: 0.95rem;
92
- font-weight: 300;
89
+ cursor: pointer;
90
+ width: auto; /* Allow width to change based on content */
91
+ min-width: 100%;
92
+ transition: border-color 0.2s;
93
+ }
94
+
95
+ .provider-select:hover {
96
+ border-color: #6366f1;
93
97
  }
94
- .input-text-style:focus {
95
- border-color: #505050;
98
+
99
+ .provider-select:focus {
96
100
  outline: none;
97
- box-shadow: none;
101
+ border-color: #6366f1;
102
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
103
+ }
104
+
105
+ .select-icon {
106
+ position: absolute;
107
+ right: 1rem;
108
+ top: 50%;
109
+ transform: translateY(-50%);
110
+ pointer-events: none;
111
+ color: #6b7280;
112
+ font-size: 0.75rem;
98
113
  }
99
114
 
100
115
  /* --- LAYOUT UTILS --- */
@@ -107,7 +122,7 @@
107
122
  header {
108
123
  display: flex;
109
124
  flex-direction: column;
110
- gap: 1rem;
125
+ gap: 1.5rem;
111
126
  padding: 1.5rem;
112
127
  border-bottom: 1px solid #1a1a1a;
113
128
  background-color: var(--bg-page);
@@ -120,61 +135,6 @@
120
135
  }
121
136
  }
122
137
 
123
- /* --- SEARCH DROPDOWN --- */
124
- .search-container {
125
- position: relative;
126
- width: 100%;
127
- max-width: 350px;
128
- }
129
-
130
- .search-dropdown {
131
- position: absolute;
132
- top: calc(100% + 0.5rem);
133
- left: 0;
134
- right: 0;
135
- background: #000000;
136
- border: 1px solid #333;
137
- border-radius: 1rem;
138
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
139
- z-index: 100;
140
- max-height: 300px;
141
- overflow-y: auto;
142
- display: none;
143
- padding: 0.5rem;
144
- }
145
- .search-dropdown.active {
146
- display: block;
147
- animation: fadeIn 0.2s ease-out;
148
- }
149
- @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }
150
-
151
- .search-item {
152
- padding: 0.6rem 0.8rem;
153
- cursor: pointer;
154
- border-radius: 0.5rem;
155
- display: flex;
156
- flex-direction: column;
157
- gap: 2px;
158
- transition: background 0.2s;
159
- border-bottom: 1px solid transparent;
160
- }
161
- .search-item:hover { background: #1a1a1a; color: #fff; }
162
- .search-item-main { font-size: 0.95rem; color: #e5e7eb; }
163
- .search-item-sub { color: #6b7280; font-size: 0.8rem; }
164
-
165
- .geo-btn-absolute {
166
- position: absolute;
167
- right: 0.75rem;
168
- top: 50%;
169
- transform: translateY(-50%);
170
- color: #6b7280;
171
- cursor: pointer;
172
- transition: color 0.2s;
173
- background: none;
174
- border: none;
175
- }
176
- .geo-btn-absolute:hover { color: #4f46e5; }
177
-
178
138
  /* --- WEATHER CARDS --- */
179
139
  .weather-card {
180
140
  background-color: #0d0d0d;
@@ -200,11 +160,10 @@
200
160
  color: var(--color-indigo);
201
161
  }
202
162
 
203
- /* Hourly Scroll with Fading Edges */
163
+ /* Hourly Scroll */
204
164
  .scroll-mask-container {
205
165
  position: relative;
206
166
  width: 100%;
207
- /* CSS Mask for fading edges */
208
167
  -webkit-mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
209
168
  mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
210
169
  }
@@ -213,12 +172,12 @@
213
172
  display: flex;
214
173
  overflow-x: auto;
215
174
  gap: 0.75rem;
216
- padding: 1rem 1.5rem; /* Extra padding for the mask to not cut off content immediately */
175
+ padding: 1rem 1.5rem;
217
176
  margin-top: 0.5rem;
218
- scrollbar-width: none; /* Hide scrollbar Firefox */
219
- -ms-overflow-style: none; /* Hide scrollbar IE/Edge */
177
+ scrollbar-width: none;
178
+ -ms-overflow-style: none;
220
179
  }
221
- .hourly-container::-webkit-scrollbar { display: none; } /* Hide scrollbar Chrome */
180
+ .hourly-container::-webkit-scrollbar { display: none; }
222
181
 
223
182
  .hourly-item {
224
183
  min-width: 80px;
@@ -309,55 +268,16 @@
309
268
  .detail-label { color: #707070; font-size: 0.9rem; }
310
269
  .detail-value { color: #fff; font-weight: 500; }
311
270
 
312
- /* Provider Select styling fix */
313
- select option {
314
- background: #000;
315
- color: #fff;
316
- padding: 10px;
317
- }
318
-
319
- /* INJECTED STYLES */
320
- :root { --bg-page: #040404; --bg-container: #000000; --border-color: #333; --text-primary: #c0c0c0; --text-light-grey: #d1d5db; --accent-color: #6366f1; --navbar-bg: #000000; --navbar-border: #1f2937; --tab-text: #9ca3af; --tab-hover-text: #ffffff; --tab-active-text: #4f46e5; --tab-active-bg: rgba(79, 70, 229, 0.1); --tab-active-border: #4f46e5; }
321
- @keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-5px) rotate(-5deg); } 50% { transform: translateX(5px) rotate(5deg); } 75% { transform: translateX(-5px) rotate(-5deg); } 100% { transform: translateX(0); } }
322
- .shake-anim { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
323
- body.no-animations * { transition: none !important; animation: none !important; transform: none !important; }
324
- body:not(.no-animations) .btn-toolbar-style, body:not(.no-animations) .btn-lock-screen, body:not(.no-animations) .icon-btn, body:not(.no-animations) .nav-tab, body:not(.no-animations) .btn-card-action, body:not(.no-animations) .input-text-style, body:not(.no-animations) .input-lock-screen, body:not(.no-animations) .input-key-style { transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; }
325
- body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .btn-lock-screen:active, body:not(.no-animations) .icon-btn:active, body:not(.no-animations) .nav-tab:active, body:not(.no-animations) .btn-card-action:active { transform: scale(0.96); transition: transform 0.05s ease-out !important; }
326
- .btn-toolbar-style { background: #0a0a0a; border: 1px solid #333; border-radius: 14px; color: #d1d5db; padding: 0.75rem 1.25rem; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6); }
327
- .btn-toolbar-style:hover { transform: scale(1.05) translateY(-2px); box-shadow: 0 6px 20px rgba(255, 255, 255, 0.1); border-color: #fff; color: #ffffff; }
328
- .btn-toolbar-style.btn-primary-override { background-color: rgba(79, 70, 229, 0.1); border: 1px solid #4f46e5; color: #4f46e5; }
329
- .btn-toolbar-style.btn-primary-override:hover { background-color: rgba(79, 70, 229, 0.2); border-color: #6366f1; color: #6366f1; box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4); }
330
- .btn-toolbar-style.btn-primary-override-danger { background-color: rgba(220, 38, 38, 0.1); border: 1px solid #dc2626; color: #dc2626; }
331
- .btn-toolbar-style.btn-primary-override-danger:hover { background-color: rgba(220, 38, 38, 0.2); border-color: #ef4444; color: #ef4444; box-shadow: 0 6px 20px rgba(220, 38, 38, 0.4); }
332
- .btn-lock-screen { padding: 0.75rem 1.25rem; font-size: 0.875rem; font-weight: 500; border-radius: 14px; color: rgb(99 102 241); background-color: rgba(99, 102, 241, 0.1); border: 1px solid rgb(99 102 241); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6); }
333
- .btn-lock-screen:hover { transform: scale(1.05) translateY(-2px); box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4); background-color: rgba(99, 102, 241, 0.2); }
334
- .icon-btn { width: 40px; height: 40px; border-radius: 14px; border: 1px solid #4b5563; display: flex; align-items: center; justify-content: center; color: #d1d5db; cursor: pointer; background: transparent; }
335
- .icon-btn:hover { background-color: #374151; color: white; transform: scale(1.1); }
336
- .btn-card-action { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border-radius: 14px; background-color: transparent; border: 1px solid transparent; color: #9ca3af; cursor: pointer; }
337
- .btn-card-action:hover { background-color: rgba(79, 70, 229, 0.1); color: #4f46e5; border-color: #4f46e5; transform: scale(1.15); }
338
- .input-text-style, .input-select-style { width: 100%; padding: 0.75rem; border: 1px solid #252525; background-color: #111111; border-radius: 14px; color: #c0c0c0; }
339
- .input-text-style:focus, .input-select-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.02); }
340
- .input-lock-screen { background-color: #111; border: 1px solid #252525; color: white; border-radius: 14px; padding: 0.75rem; text-align: center; letter-spacing: 0.2em; outline: none; }
341
- .input-lock-screen:focus { border-color: rgb(99 102 241); transform: scale(1.02); }
342
- .input-key-style { width: 4rem; padding: 0.75rem; border: 1px solid #252525; background-color: #111111; border-radius: 14px; color: #c0c0c0; font-size: 1.1rem; text-align: center; font-weight: 500; text-transform: uppercase; }
343
- .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
344
- .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
345
- .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
346
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
347
- .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
348
- .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
349
- .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
350
- .eagler-dropdown-link:hover { background-color: rgba(255, 255, 255, 0.1); color: white; transform: translateX(4px); }
351
- #notification-container { position: fixed; bottom: 2rem; right: 2rem; display: flex; flex-direction: column; gap: 0.75rem; z-index: 1000; pointer-events: none; }
352
- .notification-toast { background-color: #0a0a0a; border: 1px solid #333; border-radius: 14px; padding: 0.75rem 1.25rem; color: #fff; box-shadow: 0 4px 15px rgba(0,0,0,0.5); display: flex; align-items: center; gap: 0.75rem; font-size: 0.9rem; min-width: 200px; transform: translateX(120%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease, background-color 0.2s; opacity: 0; pointer-events: auto; cursor: default; }
353
- .notification-toast.show { transform: translateX(0); opacity: 1; }
354
- .notification-toast.show:hover { transform: scale(1.05) translateX(-5px); background-color: #151515; border-color: #555; box-shadow: 0 8px 25px rgba(0,0,0,0.7); }
355
- .notification-icon { font-size: 1.1rem; }
356
- .notification-icon.success { color: #4ade80; }
357
- .notification-icon.info { color: #60a5fa; }
358
- .notification-icon.warning { color: #fbbf24; }
359
-
360
- </style>
271
+ /* Option Styling (Limited support) */
272
+ option { background-color: #000; color: #fff; padding: 10px; }
273
+
274
+ /* INJECTED STYLES (Animations) */
275
+ :root { --bg-page: #040404; --bg-container: #000000; --border-color: #333; --text-primary: #c0c0c0; --text-light-grey: #d1d5db; --accent-color: #6366f1; }
276
+ @keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-5px) rotate(-5deg); } 50% { transform: translateX(5px) rotate(5deg); } 75% { transform: translateX(-5px) rotate(-5deg); } 100% { transform: translateX(0); } }
277
+ .shake-anim { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
278
+ body:not(.no-animations) .btn-toolbar-style, body:not(.no-animations) .hourly-item, body:not(.no-animations) .daily-row { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
279
+ body:not(.no-animations) .btn-toolbar-style:active { transform: scale(0.96); }
280
+ </style>
361
281
 
362
282
  </head>
363
283
  <body class="min-h-screen">
@@ -378,7 +298,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
378
298
  </div>
379
299
  <div id="modal-details-list" class="flex flex-col gap-3 pt-2">
380
300
  </div>
381
- <button class="btn-toolbar-style w-full mt-4" onclick="closeHourlyModal()">Close</button>
301
+ <button class="btn-toolbar-style w-full mt-4 justify-center" onclick="closeHourlyModal()">Close</button>
382
302
  </div>
383
303
  </div>
384
304
 
@@ -391,22 +311,17 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
391
311
  </div>
392
312
 
393
313
  <div class="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
394
- <div class="search-container">
395
- <input type="text" id="loc-search-input" placeholder="Change Location..." class="input-text-style pr-10">
396
- <button id="btn-geo" class="geo-btn-absolute" title="Use Current Location">
397
- <i class="fa-solid fa-location-crosshairs"></i>
398
- </button>
399
-
400
- <div id="search-results" class="search-dropdown">
401
- </div>
402
- </div>
403
-
404
- <div class="relative w-full md:w-auto">
405
- <select id="provider-select" class="btn-toolbar-style w-full appearance-none pr-8 outline-none">
406
- <option value="nws">🇺🇸 NWS (USA)</option>
407
- <option value="open-meteo">🌍 Open-Meteo</option>
314
+ <button id="btn-geo" class="btn-toolbar-style w-full md:w-auto">
315
+ <i class="fa-solid fa-location-crosshairs text-[#6366f1]"></i>
316
+ <span>Use Current Location</span>
317
+ </button>
318
+
319
+ <div class="provider-select-wrapper w-full md:w-auto">
320
+ <select id="provider-select" class="provider-select">
321
+ <option value="nws">🇺🇸 NWS (Official)</option>
322
+ <option value="open-meteo">🌍 Open-Meteo (Global)</option>
408
323
  </select>
409
- <i class="fa-solid fa-chevron-down absolute right-3 top-1/2 -translate-y-1/2 text-xs pointer-events-none text-gray-400"></i>
324
+ <i class="fa-solid fa-chevron-down select-icon"></i>
410
325
  </div>
411
326
  </div>
412
327
  </header>
@@ -479,10 +394,6 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
479
394
  const mainContent = document.getElementById('main-content');
480
395
  const locNameEl = document.getElementById('loc-name');
481
396
  const providerSelect = document.getElementById('provider-select');
482
-
483
- // Search Elements
484
- const searchInput = document.getElementById('loc-search-input');
485
- const searchResults = document.getElementById('search-results');
486
397
  const btnGeo = document.getElementById('btn-geo');
487
398
 
488
399
  // Current Els
@@ -570,7 +481,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
570
481
  loadWeather();
571
482
  } catch (err) {
572
483
  loadingText.innerText = "Location Failed.";
573
- alert("Could not determine location. Please search manually.");
484
+ alert("Could not determine location. Check browser permissions.");
574
485
  loadingOverlay.style.opacity = '0';
575
486
  loadingOverlay.style.pointerEvents = 'none';
576
487
  }
@@ -607,131 +518,10 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
607
518
  }
608
519
  }
609
520
 
610
- // --- SEARCH LOGIC ---
611
- let debounceTimer;
612
- searchInput.addEventListener('input', (e) => {
613
- clearTimeout(debounceTimer);
614
- const query = e.target.value.trim();
615
- if(query.length < 3) {
616
- searchResults.classList.remove('active');
617
- return;
618
- }
619
- // Show loading state in dropdown
620
- searchResults.innerHTML = '<div class="search-item text-gray-500"><i class="fa-solid fa-spinner fa-spin mr-2"></i> Searching...</div>';
621
- searchResults.classList.add('active');
622
-
623
- debounceTimer = setTimeout(() => fetchSearchResults(query), 500);
624
- });
625
-
626
- document.addEventListener('click', (e) => {
627
- if(!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
628
- searchResults.classList.remove('active');
629
- }
630
- });
631
-
632
521
  btnGeo.addEventListener('click', () => {
633
- searchInput.value = '';
634
522
  triggerAutoLocation();
635
523
  });
636
524
 
637
- async function fetchSearchResults(query) {
638
- let viewbox = '';
639
- if (STATE.lat && STATE.lon) {
640
- const b = 1;
641
- viewbox = `&viewbox=${STATE.lon-b},${STATE.lat+b},${STATE.lon+b},${STATE.lat-b}`;
642
- }
643
-
644
- try {
645
- const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=10&addressdetails=1${viewbox}`;
646
- const res = await fetch(url);
647
- const data = await res.json();
648
-
649
- const filtered = data.filter(item => {
650
- const type = item.type;
651
- const category = item.class;
652
- const validClasses = ['place', 'boundary'];
653
- const validTypes = ['city', 'town', 'village', 'hamlet', 'administrative'];
654
-
655
- if (!validClasses.includes(category)) return false;
656
- if (category === 'boundary' && type !== 'administrative') return false;
657
- return true;
658
- });
659
-
660
- renderSearchResults(filtered.slice(0, 5));
661
- } catch (e) {
662
- console.error("Search failed", e);
663
- searchResults.innerHTML = '<div class="search-item text-red-500">Error searching.</div>';
664
- }
665
- }
666
-
667
- function renderSearchResults(results) {
668
- searchResults.innerHTML = '';
669
-
670
- if (results.length === 0) {
671
- const empty = document.createElement('div');
672
- empty.className = 'search-item text-gray-500 cursor-default';
673
- empty.innerText = 'No places found';
674
- searchResults.appendChild(empty);
675
- } else {
676
- results.forEach(loc => {
677
- const div = document.createElement('div');
678
- div.className = 'search-item';
679
-
680
- const address = loc.address || {};
681
- const city = address.city || address.town || address.village || address.hamlet || loc.name;
682
- const state = address.state || address.country;
683
-
684
- div.innerHTML = `
685
- <div class="search-item-main">${city}</div>
686
- <div class="search-item-sub">${state}</div>
687
- `;
688
-
689
- div.addEventListener('click', () => {
690
- selectLocation(parseFloat(loc.lat), parseFloat(loc.lon), city, state, address.country_code);
691
- });
692
-
693
- searchResults.appendChild(div);
694
- });
695
- }
696
-
697
- // Add "Use Current Location" at the bottom
698
- const currDiv = document.createElement('div');
699
- currDiv.className = 'search-item border-t border-[#333] mt-1 pt-2';
700
- if (STATE.locationAllowed) {
701
- currDiv.innerHTML = `<div class="search-item-main text-[#6366f1]"><i class="fa-solid fa-location-crosshairs mr-2"></i> Use Current Location</div>`;
702
- currDiv.addEventListener('click', () => {
703
- searchInput.value = '';
704
- searchResults.classList.remove('active');
705
- triggerAutoLocation();
706
- });
707
- } else {
708
- currDiv.innerHTML = `<div class="search-item-main text-gray-500 cursor-not-allowed"><i class="fa-solid fa-location-slash mr-2"></i> Current Location Unavailable</div>`;
709
- }
710
- searchResults.appendChild(currDiv);
711
-
712
- searchResults.classList.add('active');
713
- }
714
-
715
- function selectLocation(lat, lon, city, region, countryCode) {
716
- STATE.lat = lat;
717
- STATE.lon = lon;
718
- STATE.city = city;
719
- STATE.region = region;
720
-
721
- searchInput.value = '';
722
- searchResults.classList.remove('active');
723
-
724
- const isUS = (countryCode && countryCode === 'us');
725
-
726
- if (!isUS) {
727
- STATE.provider = 'open-meteo';
728
- providerSelect.value = 'open-meteo';
729
- }
730
-
731
- updateLocationDisplay();
732
- loadWeather();
733
- }
734
-
735
525
  // --- WEATHER FETCHING LOGIC ---
736
526
 
737
527
  async function loadWeather() {
package/navigation.js CHANGED
@@ -1138,7 +1138,7 @@ let db;
1138
1138
  text-decoration: none; line-height: 1.5; display: flex; align-items: center; margin-right: 8px;
1139
1139
  border: 1px solid transparent;
1140
1140
  }
1141
- .nav-tab:not(.active):hover { color: var(--tab-hover-text); border-color: var(--tab-hover-border); background-color: var(--tab-hover-bg); }
1141
+ .nav-tab:not(.active):hover { color: var(--tab-hover-text); border-color: var(--tab-active-border); background-color: var(--tab-hover-bg); }
1142
1142
  .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); }
1143
1143
  .nav-tab.active:hover { color: var(--tab-active-hover-text); border-color: var(--tab-active-hover-border); background-color: var(--tab-active-hover-bg); }
1144
1144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4sp-dv",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/v5-4simpleproblems/v5-4simpleproblems-dv#readme",
package/test.html CHANGED
@@ -53,7 +53,7 @@
53
53
  .nav-tab:hover {
54
54
  color: var(--tab-hover-text, #ffffff);
55
55
  background-color: var(--tab-hover-bg, rgba(79, 70, 229, 0.05));
56
- border-color: var(--tab-hover-border, transparent);
56
+ border-color: var(--tab-active-border, #4f46e5);
57
57
  }
58
58
  .nav-tab.active {
59
59
  color: var(--tab-active-text, #4f46e5);