diff --git a/.gitignore b/.gitignore index 5eb1dc9..e06e8cb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,3 @@ dist/* .env nohup.out windy-*.txt -data/ diff --git a/README.md b/README.md index 47da745..643127a 100644 --- a/README.md +++ b/README.md @@ -120,48 +120,6 @@ The `Ground View` screen requires a Windy Webcams API key. Create a file named ` You can obtain a free API key from [Windy Webcams API](https://api.windy.com/webcams). -## MySQL - -`Hazard List` is now backed by MySQL in server mode. The UI only shows the latest 7 hazards, but the database retains full history. - -Set these environment variables before starting the app: - -```text -WS4KP_MYSQL_HOST=127.0.0.1 -WS4KP_MYSQL_PORT=3306 -WS4KP_MYSQL_SOCKET_PATH=/var/run/mysql/mysql.sock -WS4KP_MYSQL_USER=root -WS4KP_MYSQL_PASSWORD=your-password -WS4KP_MYSQL_DATABASE=ws4kp_linhanced -``` - -If your local MariaDB/MySQL instance is socket-only, set `WS4KP_MYSQL_SOCKET_PATH` and omit `WS4KP_MYSQL_HOST` / `WS4KP_MYSQL_PORT`. - -Create the required table: - -```sql -CREATE TABLE hazard_history ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - location_label VARCHAR(255) NOT NULL, - location_key VARCHAR(128) NOT NULL, - hazard_type VARCHAR(128) NOT NULL, - source VARCHAR(64) NOT NULL, - severity VARCHAR(64) DEFAULT NULL, - latest_hazard_id VARCHAR(255) DEFAULT NULL, - encountered_at DATETIME NOT NULL, - last_seen_at DATETIME NOT NULL, - ongoing TINYINT(1) NOT NULL DEFAULT 1, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - PRIMARY KEY (id), - UNIQUE KEY uq_logical_hazard (location_key, hazard_type, source), - KEY idx_last_seen_at (last_seen_at), - KEY idx_location_key (location_key), - KEY idx_ongoing (ongoing) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - ## Running Modes This fork supports two main runtime styles. @@ -179,7 +137,6 @@ This mode includes: * Express server entry point * proxying and caching for weather/map requests * Fastfetch-backed Server Observations -* MySQL-backed Hazard List history * better shared performance when multiple clients use the same instance ### Static Mode @@ -197,9 +154,9 @@ Or upload the generated `dist/` directory to your web server after running: npm run build ``` -The static build has been adjusted so frontend-generated paths no longer assume deployment at `/`, which makes subdirectory hosting more practical. **Also, features that require a backend server like the on-disk cache, Fastfetch-backed Server Observations, LWN Linux News, `Ground View`, and `Hazard List` will not work when running the static build by itself.** +The static build has been adjusted so frontend-generated paths no longer assume deployment at `/`, which makes subdirectory hosting more practical. **Also, features that require a backend server like the on-disk cache, Fastfetch-backed Server Observations, LWN Linux News, and `Ground View` will not work when running the static build by itself.** -The public demo at [https://mentalnet.xyz/ws4kp-linhanced-demo/](https://mentalnet.xyz/ws4kp-linhanced-demo/) is intentionally served as a static build, so the `Linux News`, `Server Observations`, `Ground View`, and `Hazard List` screens will not work there. +The public demo at [https://mentalnet.xyz/ws4kp-linhanced-demo/](https://mentalnet.xyz/ws4kp-linhanced-demo/) is intentionally served as a static build, so the `Linux News`, `Server Observations`, and `Ground View` screens will not work there. ## International Support diff --git a/index.mjs b/index.mjs index 6f8b398..7f61e70 100644 --- a/index.mjs +++ b/index.mjs @@ -21,8 +21,6 @@ import cache from './proxy/cache.mjs'; import devTools from './src/com.chrome.devtools.mjs'; import { discoverThemes } from './src/theme-discovery.mjs'; import { findNearestWindyWebcam, loadWindyApiKey } from './src/windy-webcams.mjs'; -import { checkHazardHistoryTable } from './src/mysql.mjs'; -import { getHistory, updateHistory } from './src/hazard-history.mjs'; const execAsync = promisify(exec); @@ -99,9 +97,6 @@ const { themes, themeAssets } = await discoverThemes(); const app = express(); const port = process.env.WS4KP_PORT ?? 8080; -// Enable JSON parsing for API endpoints -app.use(express.json()); - // Set X-Weatherstar header globally for playlist fallback detection app.use((req, res, next) => { res.setHeader('X-Weatherstar', 'true'); @@ -204,12 +199,6 @@ const staticOptions = { // Weather.gov API proxy (catch-all for any Weather.gov API endpoint) // Skip setting up routes for the caching proxy server in static mode if (!process.env?.STATIC) { - try { - await checkHazardHistoryTable(); - } catch (error) { - console.error(error.message); - } - // Server info endpoint for fastfetch output (must be before /api/ weather proxy) app.get('/api/server-info', async (req, res) => { try { @@ -295,48 +284,6 @@ if (!process.env?.STATIC) { } }); - // Hazard History API endpoints - app.get('/api/hazard-history', async (req, res) => { - try { - const history = await getHistory(); - res.json({ - success: true, - history, - }); - } catch (error) { - res.status(500).json({ - success: false, - history: [], - error: error.message, - }); - } - }); - - app.post('/api/hazard-history', async (req, res) => { - try { - const { location, locationKey, hazards } = req.body; - - if (!location || !locationKey || !Array.isArray(hazards)) { - res.status(400).json({ - success: false, - error: 'Missing or invalid location/locationKey/hazards', - }); - return; - } - - const history = await updateHistory({ location, locationKey, hazards }); - res.json({ - success: true, - history, - }); - } catch (error) { - res.status(500).json({ - success: false, - error: error.message, - }); - } - }); - app.use('/api/', weatherProxy); // Cache management DELETE endpoint to allow "uncaching" specific URLs diff --git a/package-lock.json b/package-lock.json index a47a764..c39aa70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,17 @@ { "name": "ws4kp-linhanced", - "version": "0.2", + "version": "0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ws4kp-linhanced", - "version": "0.1.1", + "version": "0.1", "license": "MIT", "dependencies": { "dotenv": "^17.0.1", "ejs": "^5.0.1", - "express": "^5.1.0", - "mysql2": "^3.22.1" + "express": "^5.1.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -3072,15 +3071,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/b4a": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", @@ -3839,15 +3829,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5113,15 +5094,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -6449,12 +6421,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6895,12 +6861,6 @@ "dev": true, "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -6911,21 +6871,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -7092,40 +7037,6 @@ "node": ">= 10.13.0" } }, - "node_modules/mysql2": { - "version": "3.22.1", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.1.tgz", - "integrity": "sha512-48+9UXehKyxxiP2pqCxUq+MSFvX+v41jwsSpFDQO/jAoFuAELutBGJUhWJnDbe82/OBlIhSBMC82WeonmznT/Q==", - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.2", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.2", - "long": "^5.3.2", - "lru.min": "^1.1.4", - "named-placeholders": "^1.1.6", - "sql-escaper": "^1.3.3" - }, - "engines": { - "node": ">= 8.0" - }, - "peerDependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8468,21 +8379,6 @@ "node": ">= 10.13.0" } }, - "node_modules/sql-escaper": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", - "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=2.0.0", - "node": ">=12.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index 4a5e27e..1e201bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws4kp-linhanced", - "version": "0.2", + "version": "0.1", "description": "WeatherStar 4000+: Linhanced - A Linux-focused fork of the WeatherStar 4000+ project", "main": "index.mjs", "type": "module", @@ -56,7 +56,6 @@ "dependencies": { "dotenv": "^17.0.1", "ejs": "^5.0.1", - "express": "^5.1.0", - "mysql2": "^3.22.1" + "express": "^5.1.0" } } diff --git a/server/scripts/index.mjs b/server/scripts/index.mjs index 7b38406..8bb592c 100644 --- a/server/scripts/index.mjs +++ b/server/scripts/index.mjs @@ -9,7 +9,6 @@ import settings from './modules/settings.mjs'; import './modules/utils/theme.mjs'; import './modules/latestobservations.mjs'; import './modules/groundview.mjs'; -import './modules/hazard-list.mjs'; import AutoComplete from './modules/autocomplete.mjs'; import { loadAllData } from './modules/utils/data-loader.mjs'; import { debugFlag } from './modules/utils/debug.mjs'; diff --git a/server/scripts/modules/currentweather.mjs b/server/scripts/modules/currentweather.mjs index a37315c..eac88e4 100644 --- a/server/scripts/modules/currentweather.mjs +++ b/server/scripts/modules/currentweather.mjs @@ -63,7 +63,7 @@ class CurrentWeather extends WeatherDisplay { dewpoint: this.data.DewPoint + String.fromCharCode(176), ceiling: this.data.Ceiling === 0 ? 'Unlimited' : `${this.data.Ceiling}${this.data.CeilingUnit}`, visibility: `${this.data.Visibility}${this.data.VisibilityUnit}`, - pressure: this.data.PressureDirection ? `${this.data.Pressure} ${this.data.PressureDirection}` : this.data.Pressure, + pressure: `${this.data.Pressure} ${this.data.PressureDirection}`, icon: { type: 'img', src: this.data.Icon }, }; @@ -121,15 +121,10 @@ const getCurrentWeatherByHourFromTime = (data) => { return currDiff < prevDiff ? curr : prev; }, availableTimes[0]); - const currentPressure = closestTime.pressure_msl; - const previousPressure = previousHour.pressure_msl; - let pressureTrend = ''; - if (Number.isFinite(currentPressure) && Number.isFinite(previousPressure)) { - const diff = currentPressure - previousPressure; - pressureTrend = 'Steady'; - if (diff > 0.5) pressureTrend = 'Rising'; - if (diff < -0.5) pressureTrend = 'Falling'; - } + const diff = (closestTime.pressure_msl ?? 0) - (previousHour.pressure_msl ?? 0); + let pressureTrend = 'Steady'; + if (diff > 0.5) pressureTrend = 'Rising'; + if (diff < -0.5) pressureTrend = 'Falling'; closestTime.pressureTrend = pressureTrend; closestTime.uv_index_max = data.forecast[currentDateKey]?.uv_index_max ?? closestTime.uv_index ?? 0; return closestTime; @@ -149,7 +144,6 @@ const parseData = async (weatherParameters) => { const visibilityConverter = distanceKilometers(); const ceilingMeters = Math.max(0, ((observation.temperature ?? 0) - (observation.dewPoint ?? 0)) * 68); const pressureValue = observation.pressure ?? currentForecast.pressure_msl ?? null; - const resolvedWindGust = observation.windGust ?? currentForecast.wind_gusts_10m ?? 0; return { city: weatherParameters.city, timeZone: weatherParameters.timeZone, @@ -164,17 +158,17 @@ const parseData = async (weatherParameters) => { WindSpeedRaw: observation.windSpeed, WindDirection: directionToNSEW(observation.windDirection ?? 0), Pressure: pressureValue === null ? '-' : pressureConverter(pressureValue * 100), - PressureDirection: pressureValue === null ? '' : (currentForecast.pressureTrend ?? ''), + PressureDirection: currentForecast.pressureTrend ?? 'Steady', Humidity: Math.round(observation.relativeHumidity ?? currentForecast.relative_humidity_2m ?? 0), - WindGust: windConverter(resolvedWindGust), - WindGustRaw: resolvedWindGust, + WindGust: windConverter(observation.windGust), + WindGustRaw: observation.windGust, WindUnit: windConverter.units, TextConditions: Number(observation.weatherCode ?? 0), Icon: getLargeIconFromWmoCodeWithWind( observation.weatherCode, Boolean(observation.isDay), observation.windSpeed, - resolvedWindGust + observation.windGust ), }; } diff --git a/server/scripts/modules/hazard-list.mjs b/server/scripts/modules/hazard-list.mjs deleted file mode 100644 index 87ec98f..0000000 --- a/server/scripts/modules/hazard-list.mjs +++ /dev/null @@ -1,137 +0,0 @@ -// Hazard List display - shows the last 7 hazard alerts encountered by this server -import STATUS from './status.mjs'; -import WeatherDisplay from './weatherdisplay.mjs'; -import { registerDisplay } from './navigation.mjs'; - -class HazardList extends WeatherDisplay { - constructor(navId, elemId) { - super(navId, elemId, 'Hazard List', true); - this.history = []; - } - - async getData(weatherParameters, refresh) { - const superResult = super.getData(weatherParameters, refresh); - - try { - // Fetch hazard history from backend - const response = await fetch('/api/hazard-history'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - if (result.success) { - this.data = result.history || []; - this.setStatus(STATUS.loaded); - } else { - throw new Error(result.error || 'Failed to load hazard history'); - } - } catch (error) { - // In static mode or if backend is unavailable, show empty state - this.data = []; - if (error.message.includes('404') || error.message.includes('Failed to fetch')) { - // Backend not available (static mode) - disable this display - this.setStatus(STATUS.disabled); - } else { - this.setStatus(STATUS.failed); - } - } - - return superResult; - } - - async drawCanvas() { - super.drawCanvas(); - if (!this.data || this.data.length === 0) { - this.showEmptyState(); - this.finishDraw(); - return; - } - - // Templates are extracted by WeatherDisplay.loadTemplates() - const container = this.elem.querySelector('.hazard-list-rows'); - container.innerHTML = ''; - - // Add hazard rows - this.data.forEach((hazard) => { - const row = this.fillTemplate('hazard-list-row', { - location: hazard.location, - hazard: this.abbreviateHazardType(hazard.hazardType), - date: this.formatDate(hazard.encounteredAt), - ongoing: hazard.ongoing ? 'YES' : 'NO', - }); - if (row) container.appendChild(row); - }); - - this.finishDraw(); - } - - showEmptyState() { - const container = this.elem.querySelector('.hazard-list-rows'); - container.innerHTML = ''; - - const emptyRow = this.fillTemplate('hazard-list-row', { - location: 'No hazard history available', - hazard: '', - date: '', - ongoing: '', - }); - if (emptyRow) container.appendChild(emptyRow); - } - - // Format ISO date to MM/DD - formatDate(isoDate) { - if (!isoDate) return '--/--'; - try { - const date = new Date(isoDate); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const day = date.getDate().toString().padStart(2, '0'); - return `${month}/${day}`; - } catch { - return '--/--'; - } - } - - // Abbreviate hazard type to 8 characters max with weather-specific shortcuts - abbreviateHazardType(type) { - if (!type) return ''; - - // Apply weather-specific abbreviations - let abbreviated = type - .replace(/Thunderstorm/g, 'T-storm') - .replace(/Warning/g, 'Warn') - .replace(/Advisory/g, 'Adv') - .replace(/Visibility/g, 'Vis') - .replace(/Reduced/g, 'Red') - .replace(/Severe/g, 'Sev') - .replace(/Extreme/g, 'Ext') - .replace(/Weather/g, 'Wx') - .replace(/Condition/g, 'Cond') - .replace(/Temperature/g, 'Temp') - .replace(/Precipitation/g, 'Precip') - .replace(/Tornado/g, 'Torn') - .replace(/Hurricane/g, 'Hurr') - .replace(/Tropical/g, 'Trop') - .replace(/Storm/g, 'Stm') - .replace(/Wind/g, 'Wnd') - .replace(/Snow/g, 'Snw') - .replace(/Rain/g, 'Rn') - .replace(/Fog/g, 'Fg') - .replace(/Hail/g, 'Hl') - .replace(/Freezing/g, 'Frz') - .replace(/ Dense/g, 'Dns'); - - // Hard truncate to 8 characters if still too long - if (abbreviated.length > 8) { - abbreviated = abbreviated.substring(0, 8); - } - - return abbreviated; - } -} - -// register display -const display = new HazardList(15, 'hazard-list'); -registerDisplay(display); - -export default display; diff --git a/server/scripts/modules/hazards.mjs b/server/scripts/modules/hazards.mjs index 3fb5a38..2048e7e 100644 --- a/server/scripts/modules/hazards.mjs +++ b/server/scripts/modules/hazards.mjs @@ -129,9 +129,6 @@ class Hazards extends WeatherDisplay { if (this.isEnabled) { this.drawLongCanvas(); } - - // Sync hazards to backend history - await this.syncHazardHistory(); } catch (error) { console.error(`Unexpected Active Alerts error: ${error.message}`); stopAlertTone(); @@ -266,58 +263,6 @@ class Hazards extends WeatherDisplay { this.getDataCallbacks.push(() => resolve(this.data)); }); } - - // Sync current hazards to backend history - async syncHazardHistory() { - try { - // Skip if no weather parameters available - if (!this.weatherParameters) return; - - // Format location - const { city, state, country, countryCode, latitude, longitude } = this.weatherParameters; - const location = this.formatLocationLabel(city, state, country, countryCode); - - // Create stable location key from lat/lon (rounded to 3 decimal places ~111m precision) - const locationKey = `${parseFloat(latitude).toFixed(3)},${parseFloat(longitude).toFixed(3)}`; - - // Normalize hazards for the API - const hazards = (this.data || []).map((hazard) => ({ - id: hazard.id, - hazardType: hazard.properties?.event || 'Unknown', - severity: hazard.properties?.severity || 'Unknown', - source: String(hazard.id || '').startsWith('derived-') ? 'derived' : 'noaa', - })); - - // Send to backend - await fetch('/api/hazard-history', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ location, locationKey, hazards }), - }); - } catch (error) { - // Silently fail - hazard history is non-critical - if (debugFlag('verbose-failures')) { - console.warn('Failed to sync hazard history:', error.message); - } - } - } - - // Format location label for history - formatLocationLabel(city, state, country, countryCode) { - const cleanCity = city?.trim() || 'Unknown'; - - // US locations: "City, State" - if (countryCode === 'US' || countryCode === 'USA') { - const cleanState = state?.trim(); - return cleanState ? `${cleanCity}, ${cleanState}` : cleanCity; - } - - // Non-US locations: "City, Country" - const cleanCountry = country?.trim(); - return cleanCountry ? `${cleanCity}, ${cleanCountry}` : cleanCity; - } } const calcSeverity = (severity, event) => { diff --git a/server/scripts/modules/icons.mjs b/server/scripts/modules/icons.mjs index f0e5650..3a65bb2 100644 --- a/server/scripts/modules/icons.mjs +++ b/server/scripts/modules/icons.mjs @@ -55,9 +55,12 @@ const getSmallIconFromWmoCode = (code, isDaytime = true) => smallIcon(buildSynth const getWeatherGovTokenFromWmoCodeWithWind = (code, windSpeedKmh, windGustsKmh) => { const baseToken = getWeatherGovTokenFromWmoCode(code); const windDesc = getWindDescriptor(windSpeedKmh, windGustsKmh); - const windCapableTokens = new Set(['skc', 'few', 'sct', 'bkn', 'ovc']); - if (windDesc && windCapableTokens.has(baseToken)) { + // Only use wind icon for non-precipitation conditions + // Precipitation codes: drizzle, rain, freezing rain, snow, sleet, thunderstorms + const precipitationCodes = [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 71, 73, 75, 77, 80, 81, 82, 85, 86, 95, 96, 99]; + + if (windDesc && !precipitationCodes.includes(Number(code))) { return `wind_${baseToken}`; } return baseToken; diff --git a/server/scripts/modules/utils/derived-hazards.mjs b/server/scripts/modules/utils/derived-hazards.mjs index 5e93e55..0d8f607 100644 --- a/server/scripts/modules/utils/derived-hazards.mjs +++ b/server/scripts/modules/utils/derived-hazards.mjs @@ -10,9 +10,8 @@ const SEVERITY_RANK = { }; const RULE_PRIORITY = { - tropical: 7, - thunderstorm: 6, - fog: 5, + tropical: 6, + thunderstorm: 5, freezing: 4, snow: 3, rain: 2, @@ -24,13 +23,11 @@ const WEATHER_CODES = { snow: new Set([71, 73, 75, 77, 85, 86]), thunderstorm: new Set([95, 96, 99]), rain: new Set([51, 53, 55, 61, 63, 65, 80, 81, 82]), - fog: new Set([45, 48]), }; const thresholds = { lowVisibilitySevere: 5 * METERS_PER_MILE, lowVisibilityExtreme: 2 * METERS_PER_MILE, - denseFogVisibility: 1000, gustSevere: 20 * KPH_PER_MPH, gustExtreme: 35 * KPH_PER_MPH, highWindSevere: 40 * KPH_PER_MPH, @@ -88,22 +85,15 @@ const getWorstHour = (hours, evaluator) => hours.reduce((worst, hour) => { const evaluateThunderstorm = (hours) => getWorstHour(hours, (hour) => { const code = Number(hour.weather_code ?? 0); if (!WEATHER_CODES.thunderstorm.has(code)) return null; - const visibility = hour.visibility ?? Number.POSITIVE_INFINITY; - const lowVisibility = visibility < thresholds.lowVisibilitySevere; - if (code === 96 || code === 99) { return { severity: 'Extreme', - description: lowVisibility - ? 'Thunderstorms with hail and very low visibility are expected in the next several hours and may create dangerous outdoor and travel conditions.' - : 'Thunderstorms with hail are possible in the next several hours and may create dangerous outdoor conditions.', + description: 'Thunderstorms with hail are possible in the next several hours and may create dangerous outdoor conditions.', }; } return { severity: 'Severe', - description: lowVisibility - ? 'Thunderstorms with reduced visibility are expected in the next several hours and may create hazardous outdoor and travel conditions.' - : 'Thunderstorms are possible in the next several hours and may create hazardous outdoor conditions.', + description: 'Thunderstorms are possible in the next several hours and may create hazardous outdoor conditions.', }; }); @@ -214,34 +204,11 @@ const evaluateWind = (hours) => getWorstHour(hours, (hour) => { return null; }); -const evaluateFog = (hours) => getWorstHour(hours, (hour) => { - const code = Number(hour.weather_code ?? 0); - if (!WEATHER_CODES.fog.has(code)) return null; - const visibility = hour.visibility ?? Number.POSITIVE_INFINITY; - - if (visibility <= thresholds.denseFogVisibility) { - return { - severity: 'Extreme', - description: 'Dense fog with very low visibility is expected in the next several hours and may create dangerous travel conditions.', - event: 'Dense Fog Warning', - }; - } - if (visibility < thresholds.lowVisibilitySevere) { - return { - severity: 'Severe', - description: 'Reduced visibility with mist or low cloud is expected in the next several hours and may create hazardous travel conditions.', - event: 'Reduced Visibility Advisory', - }; - } - return null; -}); - const deriveHazards = (weatherParameters) => { const upcomingHours = getUpcomingHours(weatherParameters); if (upcomingHours.length === 0) return []; const tropicalCandidate = evaluateTropical(upcomingHours); const thunderstormCandidate = evaluateThunderstorm(upcomingHours); - const fogCandidate = evaluateFog(upcomingHours); const freezingCandidate = evaluateFreezing(upcomingHours); const snowCandidate = evaluateSnow(upcomingHours); const rainCandidate = evaluateRain(upcomingHours); @@ -259,11 +226,6 @@ const deriveHazards = (weatherParameters) => { priority: RULE_PRIORITY.thunderstorm, ...thunderstormCandidate, }), - fogCandidate && buildDerivedHazard({ - id: 'derived-severe-weather-alert-fog', - priority: RULE_PRIORITY.fog, - ...fogCandidate, - }), freezingCandidate && buildDerivedHazard({ id: 'derived-severe-weather-alert-freezing', priority: RULE_PRIORITY.freezing, diff --git a/server/scripts/modules/utils/fetch.mjs b/server/scripts/modules/utils/fetch.mjs index b087ae3..234b717 100644 --- a/server/scripts/modules/utils/fetch.mjs +++ b/server/scripts/modules/utils/fetch.mjs @@ -1,8 +1,6 @@ import { rewriteUrl } from './url-rewrite.mjs'; const DEFAULT_REQUEST_TIMEOUT = 15000; // For example, with 3 retries: 15s+1s+15s+2s+15s+5s+15s = 68s -const inflightRequests = new Map(); -const responseCache = new Map(); // Centralized utilities for handling errors in Promise contexts const safeJson = async (url, params) => { @@ -81,81 +79,8 @@ const USER_AGENT_EXCLUDED_HOSTS = [ 'services.arcgis.com', ]; -const classifyRequest = (_url) => { - const url = new URL(_url, window.location.origin); - if (url.hostname.includes('api.weather.gov')) { - if (url.pathname.includes('/alerts/active')) return 'weatherGovAlerts'; - return 'weatherGovGeneral'; - } - if (url.hostname.includes('api.open-meteo.com')) { - return 'openMeteo'; - } - return 'default'; -}; - -const getRequestPolicy = (requestClass, providedParams = {}) => { - const defaults = { - default: { - timeout: DEFAULT_REQUEST_TIMEOUT, - retryCount: 3, - cacheTtlMs: 0, - }, - weatherGovAlerts: { - timeout: 8000, - retryCount: 1, - cacheTtlMs: 30000, - }, - weatherGovGeneral: { - timeout: 10000, - retryCount: 2, - cacheTtlMs: 60000, - }, - openMeteo: { - timeout: 8000, - retryCount: 1, - cacheTtlMs: 60000, - }, - }; - - const policy = defaults[requestClass] ?? defaults.default; - return { - timeout: providedParams.timeout ?? policy.timeout, - retryCount: providedParams.retryCount ?? policy.retryCount, - cacheTtlMs: providedParams.cacheTtlMs ?? policy.cacheTtlMs, - }; -}; - -const buildRequestKey = (url, responseType, params) => `${(params.method ?? 'GET').toUpperCase()}:${responseType}:${url.toString()}`; - -const getCachedResponse = (key) => { - const cached = responseCache.get(key); - if (!cached) return null; - if (Date.now() >= cached.expiresAt) { - responseCache.delete(key); - return null; - } - return cached.data; -}; - -const setCachedResponse = (key, data, ttlMs) => { - if (!ttlMs || ttlMs <= 0) return; - responseCache.set(key, { - data, - expiresAt: Date.now() + ttlMs, - }); -}; - -const isTransientError = (error) => error?.name === 'TimeoutError' - || error?.message?.includes('429') - || error?.message?.includes('500') - || error?.message?.includes('502') - || error?.message?.includes('503') - || error?.message?.includes('504'); - const fetchAsync = async (_url, responseType, _params = {}) => { const headers = {}; - const requestClass = classifyRequest(_url); - const policy = getRequestPolicy(requestClass, _params); const checkUrl = new URL(_url, window.location.origin); const shouldExcludeUserAgent = USER_AGENT_EXCLUDED_HOSTS.some((host) => checkUrl.hostname.includes(host)); @@ -173,12 +98,10 @@ const fetchAsync = async (_url, responseType, _params = {}) => { method: 'GET', mode: 'cors', type: 'GET', - retryCount: policy.retryCount, - timeout: policy.timeout, - cacheTtlMs: policy.cacheTtlMs, + retryCount: 3, // Default to 3 retries for any failed requests (timeout or 5xx server errors) + timeout: DEFAULT_REQUEST_TIMEOUT, ..._params, headers, - requestClass, }; // rewrite URLs for various services to use the backend proxy server for proper caching (and request logging) @@ -195,91 +118,66 @@ const fetchAsync = async (_url, responseType, _params = {}) => { }); } - const shouldUseTransportCache = params.method.toUpperCase() === 'GET' && !params.returnUrl; - const requestKey = shouldUseTransportCache ? buildRequestKey(url, responseType, params) : null; - const cachedData = shouldUseTransportCache ? getCachedResponse(requestKey) : null; - if (cachedData !== null) return cachedData; - if (shouldUseTransportCache && inflightRequests.has(requestKey)) { - return inflightRequests.get(requestKey); - } + // make the request + try { + const response = await doFetch(url, params); - const executeFetch = async () => { - // make the request - try { - const response = await doFetch(url, params); - - // check for ok response - if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`); - // process the response based on type - let result; - switch (responseType) { - case 'json': - result = await response.json(); - break; - case 'text': - result = await response.text(); - break; - case 'blob': - result = await response.blob(); - break; - default: - result = response; - } - - if (shouldUseTransportCache) { - setCachedResponse(requestKey, result, params.cacheTtlMs); - } - - // Return both data and URL if requested - if (params.returnUrl) { - return { - data: result, - url: response.url, - }; - } - - return result; - } catch (error) { - if (shouldUseTransportCache && cachedData !== null && isTransientError(error)) { - return cachedData; - } - - // Enhanced error handling for different error types - if (error.name === 'AbortError') { - console.log(`🛑 Fetch aborted for ${_url} (background tab throttling?)`); - return null; - } if (error.name === 'TimeoutError') { - console.warn(`⏱️ Request timeout for ${_url} (${error.message})`); - } else if (error.message.includes('429')) { - console.warn(`🐢 Rate limited for ${_url}`); - } else if (error.message.includes('502')) { - console.warn(`🚪 Bad Gateway error for ${_url}`); - } else if (error.message.includes('503')) { - console.warn(`⌛ Temporarily unavailable for ${_url}`); - } else if (error.message.includes('504')) { - console.warn(`⏱️ Gateway Timeout for ${_url}`); - } else if (error.message.includes('500')) { - console.warn(`💥 Internal Server Error for ${_url}`); - } else if (error.message.includes('CORS') || error.message.includes('Access-Control')) { - console.warn(`🔒 CORS or Access Control error for ${_url}`); - } else { - console.warn(`❌ Fetch failed for ${_url} (${error.message})`); - } - - if (!error.status) error.status = 0; - if (!error.responseJSON) error.responseJSON = null; - - throw error; + // check for ok response + if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`); + // process the response based on type + let result; + switch (responseType) { + case 'json': + result = await response.json(); + break; + case 'text': + result = await response.text(); + break; + case 'blob': + result = await response.blob(); + break; + default: + result = response; } - }; - if (!shouldUseTransportCache) return executeFetch(); + // Return both data and URL if requested + if (params.returnUrl) { + return { + data: result, + url: response.url, + }; + } - const inflightPromise = executeFetch().finally(() => { - inflightRequests.delete(requestKey); - }); - inflightRequests.set(requestKey, inflightPromise); - return inflightPromise; + return result; + } catch (error) { + // Enhanced error handling for different error types + if (error.name === 'AbortError') { + // AbortError always happens in the browser, regardless of server vs static mode + // Most likely causes include background tab throttling, user navigation, or client timeout + console.log(`🛑 Fetch aborted for ${_url} (background tab throttling?)`); + return null; // Always return null for AbortError instead of throwing + } if (error.name === 'TimeoutError') { + console.warn(`⏱️ Request timeout for ${_url} (${error.message})`); + } else if (error.message.includes('502')) { + console.warn(`🚪 Bad Gateway error for ${_url}`); + } else if (error.message.includes('503')) { + console.warn(`⌛ Temporarily unavailable for ${_url}`); + } else if (error.message.includes('504')) { + console.warn(`⏱️ Gateway Timeout for ${_url}`); + } else if (error.message.includes('500')) { + console.warn(`💥 Internal Server Error for ${_url}`); + } else if (error.message.includes('CORS') || error.message.includes('Access-Control')) { + console.warn(`🔒 CORS or Access Control error for ${_url}`); + } else { + console.warn(`❌ Fetch failed for ${_url} (${error.message})`); + } + + // Add standard error properties that calling code expects + if (!error.status) error.status = 0; + if (!error.responseJSON) error.responseJSON = null; + + throw error; + } }; // fetch with retry and back-off @@ -301,7 +199,7 @@ const doFetch = (url, params, originalRetryCount = null) => new Promise((resolve }; // Shared retry logic to avoid duplication - const attemptRetry = (reason, retryAfterMs = null) => { + const attemptRetry = (reason) => { // Safety check for params if (!params || typeof params.retryCount !== 'number') { console.error(`❌ Invalid params for retry: ${url}`); @@ -310,7 +208,7 @@ const doFetch = (url, params, originalRetryCount = null) => new Promise((resolve const retryAttempt = initialRetryCount - params.retryCount + 1; const remainingRetries = params.retryCount - 1; - const delayMs = retryDelay(retryAttempt, params.requestClass, retryAfterMs); + const delayMs = retryDelay(retryAttempt); console.warn(`🔄 Retry ${retryAttempt}/${initialRetryCount} for ${url} - ${reason} (retrying in ${delayMs}ms, ${remainingRetries} retr${remainingRetries === 1 ? 'y' : 'ies'} left)`); @@ -337,13 +235,6 @@ const doFetch = (url, params, originalRetryCount = null) => new Promise((resolve fetch(url, fetchParams).then((response) => { clearTimeout(timeoutId); // Clear timeout on successful response - if (params && params.retryCount > 0 && response.status === 429) { - const retryAfterHeader = response.headers.get('Retry-After'); - const retryAfterSeconds = Number.parseInt(retryAfterHeader, 10); - const retryAfterMs = Number.isFinite(retryAfterSeconds) ? retryAfterSeconds * 1000 : null; - return attemptRetry(`Rate limited 429 ${response.statusText}`, retryAfterMs); - } - // Retry 500 status codes if we have retries left if (params && params.retryCount > 0 && response.status >= 500 && response.status <= 599) { let errorType = 'Server error'; @@ -398,21 +289,13 @@ const doFetch = (url, params, originalRetryCount = null) => new Promise((resolve }); }); -const retryDelay = (retryNumber, requestClass = 'default', retryAfterMs = null) => { - if (retryAfterMs !== null) { - return retryAfterMs + Math.floor(Math.random() * 400); - } - - if (requestClass === 'openMeteo') { - return 5000 + Math.floor(Math.random() * 400); - } - +const retryDelay = (retryNumber) => { switch (retryNumber) { - case 1: return 1000 + Math.floor(Math.random() * 400); - case 2: return 2000 + Math.floor(Math.random() * 400); - case 3: return 5000 + Math.floor(Math.random() * 600); - case 4: return 10_000 + Math.floor(Math.random() * 1000); - default: return 30_000 + Math.floor(Math.random() * 1000); + case 1: return 1000; + case 2: return 2000; + case 3: return 5000; + case 4: return 10_000; + default: return 30_000; } }; diff --git a/server/styles/scss/_hazard-list.scss b/server/styles/scss/_hazard-list.scss deleted file mode 100644 index 4212f8d..0000000 --- a/server/styles/scss/_hazard-list.scss +++ /dev/null @@ -1,89 +0,0 @@ -@use 'shared/_colors' as c; -@use 'shared/_utils' as u; - -.weather-display .main.hazard-list { - &.main { - padding-top: 18px; - - .column-headers { - display: flex; - font-family: 'Star4000'; - font-size: 14pt; - font-weight: bold; - color: #ff0; - width: 70%; - margin: 4px auto 2px; - padding-top: 20px; - text-shadow: 3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000; - - .location { - width: 34%; - } - - .hazard { - width: 30%; - text-align: center; - } - - .date { - width: 22%; - text-align: center; - } - - .ongoing { - width: 14%; - text-align: center; - padding-right: 4px; - } - } - - .hazard-list-rows { - width: 70%; - margin: 0 auto; - - .hazard-list-row { - display: flex; - font-family: 'Star4000'; - font-size: 14pt; - line-height: 1.4; - margin-bottom: 4px; - - @include u.text-shadow(); - - &.template { - display: none; - } - - .location { - width: 34%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: #fff; - } - - .hazard { - width: 30%; - text-align: center; - color: c.$title-color; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .date { - width: 22%; - text-align: center; - color: #fff; - } - - .ongoing { - width: 14%; - text-align: center; - padding-right: 4px; - color: #fff; - } - } - } - } -} diff --git a/server/styles/scss/_weather-display.scss b/server/styles/scss/_weather-display.scss index 5b10ab1..56dbb94 100644 --- a/server/styles/scss/_weather-display.scss +++ b/server/styles/scss/_weather-display.scss @@ -28,7 +28,7 @@ .title { color: #eebe4b; - text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000, 0px -2px 0 #000, 0px 2px 0 #000, -2px 0px 0 #000, 2px 0px 0 #000, 3px 3px 0px #000, 4px 4px 0px #000, 5px 5px 2px rgba(0, 0, 0, 0.8); + text-shadow: 3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000; font-family: 'Star4000'; font-size: 24pt; position: absolute; diff --git a/server/styles/scss/ws.scss b/server/styles/scss/ws.scss index 3f8464b..76c0867 100644 --- a/server/styles/scss/ws.scss +++ b/server/styles/scss/ws.scss @@ -17,5 +17,4 @@ @use 'spc-outlook'; @use 'server-observations'; @use 'linux-news'; -@use 'hazard-list'; @use 'shared/scanlines'; diff --git a/server/styles/ws.min.css b/server/styles/ws.min.css index 6300c25..6db6c25 100644 --- a/server/styles/ws.min.css +++ b/server/styles/ws.min.css @@ -1 +1 @@ -@font-face{font-family:"Star4000";src:url("../fonts/Star4000.woff") format("woff");font-display:swap}body{font-family:"Star4000";margin:0}@media(prefers-color-scheme: dark){body{background-color:#000;color:#fff}}@media(prefers-color-scheme: dark){body a{color:#add8e6}}body.kiosk{margin:0px;padding:0px;overflow:hidden;width:100vw;background-color:#000 !important}#divQuery{max-width:640px;padding:8px}#divQuery .buttons{display:inline-block;width:150px;text-align:right}#divQuery .buttons #imgGetGps{height:13px;vertical-align:middle}#divQuery .buttons button{font-size:16pt;border:1px solid #a9a9a9}@media(prefers-color-scheme: dark){#divQuery .buttons button{background-color:#000;color:#fff}}#divQuery .buttons #btnGetGps img.dark{display:none}@media(prefers-color-scheme: dark){#divQuery .buttons #btnGetGps img.dark{display:inline-block}}@media(prefers-color-scheme: dark){#divQuery .buttons #btnGetGps img.light{display:none}}#divQuery .buttons #btnGetGps.active{background-color:#000}@media(prefers-color-scheme: dark){#divQuery .buttons #btnGetGps.active{background-color:#fff}}#divQuery .buttons #btnGetGps.active img{filter:invert(1)}#divQuery input,#divQuery button{font-family:"Star4000"}#divQuery #txtLocation{width:calc(100% - 170px);max-width:490px;font-size:16pt;min-width:200px;display:inline-block;background-color:#fff;color:#000;border:2px inset gray}@media(prefers-color-scheme: dark){#divQuery #txtLocation{background-color:#000;color:#fff;border:2px inset gray}}.autocomplete-suggestions{background-color:#fff;border:1px solid #000;position:absolute;z-index:9999}@media(prefers-color-scheme: dark){.autocomplete-suggestions{background-color:#000}}.autocomplete-suggestions div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:16pt}.autocomplete-suggestions div.selected{background-color:blue;color:#fff}#divTwc{display:block;background-color:#000;color:#fff;width:100%;max-width:640px;margin:0}#divTwc.wide{max-width:854px}.content-wrapper{padding:8px}#divTwcMain{width:640px;height:480px;position:relative}.wide #divTwcMain{width:854px}.kiosk #divTwc{max-width:unset}#divTwcLeft{display:none;text-align:right;flex-direction:column;vertical-align:middle}#divTwcLeft>div{flex:1;padding-right:12px;display:flex;flex-direction:column;justify-content:center}#divTwcRight{text-align:left;display:none;flex-direction:column;vertical-align:middle}#divTwcRight>div{flex:1;padding-left:12px;display:flex;flex-direction:column;justify-content:center}#divTwcBottom{display:flex;flex-direction:row;background-color:#000;color:#fff;width:640px}.wide #divTwcBottom{width:854px}@media(prefers-color-scheme: dark){#divTwcBottom{background-color:#303030}}#divTwcBottom>div{padding-left:6px;padding-right:6px}@media(max-width: 550px){#divTwcBottom>div{font-size:.9em}}@media(max-width: 500px){#divTwcBottom>div{font-size:.8em}}@media(max-width: 450px){#divTwcBottom>div{font-size:.7em}}@media(max-width: 400px){#divTwcBottom>div{font-size:.6em}}@media(max-width: 350px){#divTwcBottom>div{font-size:.5em}}#divTwcBottomLeft{flex:1;text-align:left}#divTwcBottomMiddle{flex:0;text-align:center}#divTwcBottomRight{flex:1;text-align:right}#divTwcNavContainer{display:none}#divTwcNav{width:100%;display:flex;flex-direction:row;background-color:#000;color:#fff;max-width:640px}#divTwcNav>div{padding-left:6px;padding-right:6px}#divTwcNavLeft{flex:1;text-align:left}#divTwcNavMiddle{flex:0;text-align:center}#divTwcNavRight{flex:1;text-align:right}#imgPause1x{visibility:hidden;position:absolute}.HideCursor{cursor:none !important}#txtScrollText{width:475px}@font-face{font-family:"Star4000 Extended";src:url("../fonts/Star4000 Extended.woff") format("woff");font-display:swap}@font-face{font-family:"Star4000 Large";src:url("../fonts/Star4000 Large.woff") format("woff");font-display:swap}@font-face{font-family:"Star4000 Small";src:url("../fonts/Star4000 Small.woff") format("woff");font-display:swap}:root{--theme-background-1: url('../images/backgrounds/1.png');--theme-background-1-chart: url('../images/backgrounds/1-chart.png');--theme-background-2: url('../images/backgrounds/2.png');--theme-background-3: url('../images/backgrounds/3.png');--theme-background-4: url('../images/backgrounds/4.png');--theme-background-5: url('../images/backgrounds/5.png');--theme-background-6: url('../images/backgrounds/6.png')}#display{font-family:"Star4000";margin:0 0 0 0;width:100%}#container{position:relative;width:640px;height:480px;background-image:var(--theme-background-1);transform-origin:0 0;background-repeat:no-repeat}.wide #container{padding-left:107px;padding-right:107px;background:url(../images/backgrounds/1-wide.png);background-repeat:no-repeat}#divTwc:fullscreen #container,.kiosk #divTwc #container{width:unset;height:unset}#loading{width:640px;height:480px;max-width:100%;text-shadow:4px 4px #000;display:flex;align-items:center;text-align:center;justify-content:center}#loading .title{font-family:Star4000 Large;font-size:26px;color:#ff0;margin-bottom:0px}#loading .version{margin-bottom:35px}#loading .instructions{font-size:18pt}.heading{font-weight:bold;margin-top:15px}#settings{margin-bottom:15px}#enabledDisplays,#settings{margin-bottom:15px}#enabledDisplays .loading,#enabledDisplays .retrying,#settings .loading,#settings .retrying{color:#ff0}#enabledDisplays .press-here,#settings .press-here{color:lime;cursor:pointer}#enabledDisplays .failed,#settings .failed{color:red}#enabledDisplays .no-data,#settings .no-data{color:silver}#enabledDisplays .disabled,#settings .disabled{color:silver}#enabledDisplays .press-here,#settings .press-here{color:#fff}@media(prefers-color-scheme: light){#enabledDisplays .loading,#enabledDisplays .retrying,#settings .loading,#settings .retrying{color:#990}#enabledDisplays .press-here,#settings .press-here{color:#000;cursor:pointer}#enabledDisplays .failed,#settings .failed{color:#900}#enabledDisplays .no-data,#settings .no-data{color:hsl(0,0%,30%)}#enabledDisplays .disabled,#settings .disabled{color:hsl(0,0%,30%)}}#enabledDisplays label,#settings label{display:block;max-width:fit-content;cursor:pointer}#enabledDisplays label .alert,#settings label .alert{display:none}#enabledDisplays label .alert.show,#settings label .alert.show{display:inline;color:red}#divTwcBottom img{transform:scale(0.75)}@media(max-width: 550px){.wide #divTwcBottom img{transform:scale(1)}}#divTwc:fullscreen,.kiosk #divTwc{display:flex;align-items:center;justify-content:center;align-content:center}#divTwc:fullscreen.no-cursor,.kiosk #divTwc.no-cursor{cursor:none}#divTwc:fullscreen #display,.kiosk #divTwc #display{position:relative}#divTwc:fullscreen #divTwcBottom,.kiosk #divTwc #divTwcBottom{display:flex;flex-direction:row;background-color:rgba(0,0,0,.5);color:#fff;width:100%;position:absolute;bottom:0px}.kiosk #divTwc #divTwcBottom{display:none}.navButton{cursor:pointer}#ToggleScanlines{display:inline-block}#ToggleScanlines .on{display:none}#ToggleScanlines .off{display:inline-block}#ToggleScanlines.on .on{display:inline-block}#ToggleScanlines.on .off{display:none}.visible{visibility:visible;opacity:1;transition:opacity .1s linear}#divTwc:fullscreen .hidden{visibility:hidden;opacity:0;transition:visibility 0s 1s,opacity 1s linear}.github-links{width:610px;max-width:calc(100vw - 30px);display:flex;justify-content:space-evenly;flex-wrap:wrap}.github-links span a{text-decoration:none;outline:0}.github-links span .widget{display:inline-block;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;font-size:0;line-height:0;white-space:nowrap}.github-links span .btn,.github-links span .social-count{position:relative;display:inline-block;display:inline-flex;height:14px;padding:2px 5px;font-size:11px;font-weight:600;line-height:14px;vertical-align:bottom;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-repeat:repeat-x;background-position:-1px -1px;background-size:110% 110%;border:1px solid}.github-links span .btn{border-radius:.25em}.github-links span .btn:not(:last-child){border-radius:.25em 0 0 .25em}.github-links span .social-count{border-left:0;border-radius:0 .25em .25em 0}.github-links span .widget-lg .btn,.github-links span .widget-lg .social-count{height:16px;padding:5px 10px;font-size:12px;line-height:16px}.github-links span .octicon{display:inline-block;vertical-align:text-top;fill:currentColor;overflow:visible}.github-links span .btn:focus-visible,.github-links span .social-count:focus-visible{outline:2px solid #0969da;outline-offset:-2px}.github-links span .btn{color:#24292f;background-color:#ebf0f4;border-color:#ccd1d5;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f6f8fa'/%3e%3cstop offset='90%25' stop-color='%23ebf0f4'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f6f8fa, #ebf0f4 90%);background-image:linear-gradient(180deg, #f6f8fa, #ebf0f4 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF6F8FA', endColorstr='#FFEAEFF3')}.github-links span :root .btn{filter:none}.github-links span .btn:hover,.github-links span .btn:focus{background-color:#e9ebef;background-position:0 -0.5em;border-color:#caccd1;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f3f4f6'/%3e%3cstop offset='90%25' stop-color='%23e9ebef'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f3f4f6, #e9ebef 90%);background-image:linear-gradient(180deg, #f3f4f6, #e9ebef 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF3F4F6', endColorstr='#FFE8EAEE')}.github-links span :root .btn:hover,.github-links span :root .btn:focus{filter:none}.github-links span .btn:active{background-color:#e5e9ed;border-color:#c7cbcf;border-color:rgba(27,31,36,.15);box-shadow:inset 0 .15em .3em rgba(27,31,36,.15);background-image:none;filter:none}.github-links span .social-count{color:#24292f;background-color:#fff;border-color:#ddddde;border-color:rgba(27,31,36,.15)}.github-links span .social-count:hover,.github-links span .social-count:focus{color:#0969da}.github-links span .octicon-heart{color:#bf3989}@media(prefers-color-scheme: light){.github-links span .btn:focus-visible,.github-links span .social-count:focus-visible{outline:2px solid #0969da;outline-offset:-2px}.github-links span .btn{color:#24292f;background-color:#ebf0f4;border-color:#ccd1d5;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f6f8fa'/%3e%3cstop offset='90%25' stop-color='%23ebf0f4'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f6f8fa, #ebf0f4 90%);background-image:linear-gradient(180deg, #f6f8fa, #ebf0f4 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF6F8FA', endColorstr='#FFEAEFF3')}.github-links span :root .btn{filter:none}.github-links span .btn:hover,.github-links span .btn:focus{background-color:#e9ebef;background-position:0 -0.5em;border-color:#caccd1;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f3f4f6'/%3e%3cstop offset='90%25' stop-color='%23e9ebef'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f3f4f6, #e9ebef 90%);background-image:linear-gradient(180deg, #f3f4f6, #e9ebef 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF3F4F6', endColorstr='#FFE8EAEE')}.github-links span :root .btn:hover,.github-links span :root .btn:focus{filter:none}.github-links span .btn:active{background-color:#e5e9ed;border-color:#c7cbcf;border-color:rgba(27,31,36,.15);box-shadow:inset 0 .15em .3em rgba(27,31,36,.15);background-image:none;filter:none}.github-links span .social-count{color:#24292f;background-color:#fff;border-color:#ddddde;border-color:rgba(27,31,36,.15)}.github-links span .social-count:hover,.github-links span .social-count:focus{color:#0969da}.github-links span .octicon-heart{color:#bf3989}}@media(prefers-color-scheme: dark){.github-links span .btn:focus-visible,.github-links span .social-count:focus-visible{outline:2px solid #58a6ff;outline-offset:-2px}.github-links span .btn{color:#c9d1d9;background-color:#1a1e23;border-color:#2f3439;border-color:rgba(240,246,252,.1);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%2321262d'/%3e%3cstop offset='90%25' stop-color='%231a1e23'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #21262d, #1a1e23 90%);background-image:linear-gradient(180deg, #21262d, #1a1e23 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF21262D', endColorstr='#FF191D22')}.github-links span :root .btn{filter:none}.github-links span .btn:hover,.github-links span .btn:focus{background-color:#292e33;background-position:0 -0.5em;border-color:#8b949e;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%2330363d'/%3e%3cstop offset='90%25' stop-color='%23292e33'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #30363d, #292e33 90%);background-image:linear-gradient(180deg, #30363d, #292e33 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF30363D', endColorstr='#FF282D32')}.github-links span :root .btn:hover,.github-links span :root .btn:focus{filter:none}.github-links span .btn:active{background-color:#161719;border-color:#8b949e;box-shadow:inset 0 .15em .3em rgba(1,4,9,.15);background-image:none;filter:none}.github-links span .social-count{color:#c9d1d9;background-color:#0d1117;border-color:#24282e;border-color:rgba(240,246,252,.1)}.github-links span .social-count:hover,.github-links span .social-count:focus{color:#58a6ff}.github-links span .octicon-heart{color:#db61a2}}#share-link-copied{color:#990;display:none}#share-link-instructions{display:none}body.kiosk #loading .instructions{display:none !important}.kiosk>*:not(#divTwc){display:none !important}#divInfo{display:grid;grid-template-columns:1fr 1fr;max-width:250px}.weather-display{width:640px;height:480px;overflow:hidden;position:relative;background-image:var(--theme-background-1);height:0px}.weather-display.show{height:480px}.weather-display .template{display:none}.weather-display .header{width:640px;height:60px;padding-top:30px;position:relative;z-index:20}.weather-display .header .title{color:#eebe4b;text-shadow:-2px -2px 0 #000,2px -2px 0 #000,-2px 2px 0 #000,2px 2px 0 #000,0px -2px 0 #000,0px 2px 0 #000,-2px 0px 0 #000,2px 0px 0 #000,3px 3px 0px #000,4px 4px 0px #000,5px 5px 2px rgba(0,0,0,.8);font-family:"Star4000";font-size:24pt;position:absolute;width:250px}.weather-display .header .title.single{left:138px;top:40px}.weather-display .header .title.dual{left:144px}.weather-display .header .title.dual>div{position:absolute}.weather-display .header .title.dual .top{top:-3px}.weather-display .header .title.dual .bottom{top:26px}.weather-display .header .logo{top:30px;left:50px;position:absolute;z-index:10}.weather-display .header .noaa-logo{position:absolute;top:39px;left:356px}.weather-display .header .date-time{white-space:pre;color:#fff;font-family:"Star4000 Small";font-size:24pt;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;left:436px;width:170px;text-align:right;position:absolute}.weather-display .header .date-time.date{padding-top:22px}.weather-display .main{position:relative}.weather-display .main.has-scroll{width:640px;margin-top:0;height:320px;overflow:hidden}.weather-display .main.has-scroll.no-header{height:400px;margin-top:0}.weather-display .main.has-box{margin-left:64px;margin-right:64px;width:calc(100% - 128px)}#container>.scroll{display:none;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;width:640px;height:77px;overflow:hidden;margin-top:3px;position:absolute;bottom:0px;z-index:1}#container>.scroll.hazard{background-color:#702323}#container>.scroll .scroll-container{width:640px}#container>.scroll .scroll-container .fixed,#container>.scroll .scroll-container .scroll-header{margin-left:55px;margin-right:55px;overflow:hidden;white-space:nowrap}#container>.scroll .scroll-container .scroll-header{height:26px;font-family:"Star4000 Small";font-size:20pt;margin-top:-10px}#container>.scroll .scroll-container .fixed{font-family:"Star4000";font-size:24pt}#container>.scroll .scroll-container .fixed .scroll-area{text-wrap:nowrap;position:relative}.radar #container>.scroll{z-index:100}.radar #container>.scroll.hazard{z-index:1000 !important}.wide #container>.scroll{width:854px;margin-left:-107px}.wide #container>.scroll .scroll-container{margin-left:107px}.weather-display .main.current-weather.main .col{height:50px;width:255px;display:inline-block;margin-top:10px;padding-top:10px;position:absolute;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.current-weather.main .col.left{font-family:"Star4000 Extended";font-size:18pt}.weather-display .main.current-weather.main .col.right{right:0px;font-family:"Star4000 Large";font-size:18px;font-weight:bold;line-height:24px}.weather-display .main.current-weather.main .col.right .row{margin-bottom:12px}.weather-display .main.current-weather.main .col.right .row .label,.weather-display .main.current-weather.main .col.right .row .value{display:inline-block}.weather-display .main.current-weather.main .col.right .row .label{margin-left:20px}.weather-display .main.current-weather.main .col.right .row .value{float:right;margin-right:10px}.weather-display .main.current-weather.main .center{text-align:center}.weather-display .main.current-weather.main .temp{font-family:"Star4000 Large";font-size:24pt}.weather-display .main.current-weather.main .icon img{margin:0 auto;display:block;width:108px}.weather-display .main.current-weather.main .wind-container{margin-left:10px;display:flex}.weather-display .main.current-weather.main .wind-container>div{width:50%}.weather-display .main.current-weather.main .wind-container .wind{text-align:right}.weather-display .main.current-weather.main .wind-gusts{text-align:right;font-size:28px}.weather-display .main.current-weather.main .location{color:#ff0;max-height:32px;margin-bottom:10px;padding-top:4px;overflow:hidden;text-wrap:nowrap;padding-left:15px}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .col.left{margin-top:35px}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .col.right{margin-top:36px;padding:21px}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .wind-container{display:block}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .wind-container>div{width:50%}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .wind-container .wind{text-align:right;font-size:22px;display:contents}.weather-display .main.latest-observations.main{padding-top:18px}.weather-display .main.latest-observations.main .column-headers{display:flex;font-family:"Star4000";font-size:14pt;font-weight:bold;color:#ff0;width:70%;margin:8px auto 10px;padding-top:20px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.latest-observations.main .column-headers .city{width:30%}.weather-display .main.latest-observations.main .column-headers .temp{width:15%;text-align:center}.weather-display .main.latest-observations.main .column-headers .conditions{width:30%;text-align:center}.weather-display .main.latest-observations.main .column-headers .wind{width:25%;text-align:right;padding-right:4px}.weather-display .main.latest-observations.main .observation-lines{width:70%;margin:0 auto}.weather-display .main.latest-observations.main .observation-lines .observation-row{display:flex;font-family:"Star4000";font-size:14pt;line-height:1.4;margin-bottom:4px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.latest-observations.main .observation-lines .observation-row.template{display:none}.weather-display .main.latest-observations.main .observation-lines .observation-row .city{width:30%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#fff}.weather-display .main.latest-observations.main .observation-lines .observation-row .temp{width:15%;text-align:center;color:#ff0}.weather-display .main.latest-observations.main .observation-lines .observation-row .conditions{width:30%;text-align:center;color:#fff}.weather-display .main.latest-observations.main .observation-lines .observation-row .wind{width:25%;text-align:right;padding-right:4px;color:#fff}#extended-forecast-html.weather-display{background-image:var(--theme-background-2)}.weather-display .main.extended-forecast .day-container{margin-top:16px;margin-left:27px}.weather-display .main.extended-forecast .day{text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;padding:5px;height:285px;width:155px;display:inline-block;margin:0px 15px;font-family:"Star4000";font-size:24pt}.weather-display .main.extended-forecast .day .date{text-transform:uppercase;text-align:center;color:#ff0}.weather-display .main.extended-forecast .day .condition{text-align:center;height:74px;margin-top:5px}.weather-display .main.extended-forecast .day .icon{text-align:center;height:75px}.weather-display .main.extended-forecast .day .icon img{max-height:75px}.weather-display .main.extended-forecast .day .temperatures{width:100%}.weather-display .main.extended-forecast .day .temperatures .temperature-block{display:inline-block;width:44%;vertical-align:top}.weather-display .main.extended-forecast .day .temperatures .temperature-block>div{text-align:center}.weather-display .main.extended-forecast .day .temperatures .temperature-block .value{font-family:"Star4000 Large";margin-top:4px}.weather-display .main.extended-forecast .day .temperatures .temperature-block.lo .label{color:#8080ff}.weather-display .main.extended-forecast .day .temperatures .temperature-block.hi .label{color:#ff0}.weather-display .main.hourly.main{overflow-y:hidden}.weather-display .main.hourly.main .column-headers{background-color:#200057;height:20px;position:absolute;width:100%}.weather-display .main.hourly.main .column-headers{position:sticky;top:0px;z-index:5}.weather-display .main.hourly.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;color:#ff0;position:absolute;top:-14px;z-index:5;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.hourly.main .column-headers .temp{left:355px}.weather-display .main.hourly.main .column-headers .like{left:435px}.weather-display .main.hourly.main .column-headers .wind{left:535px}.weather-display .main.hourly.main .hourly-lines{min-height:338px;padding-top:10px;background:#0b0b39}.weather-display .main.hourly.main .hourly-lines .hourly-row{font-family:"Star4000 Large";font-size:24pt;height:72px;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative}.weather-display .main.hourly.main .hourly-lines .hourly-row>div{position:absolute;white-space:pre;top:8px}.weather-display .main.hourly.main .hourly-lines .hourly-row .hour{left:25px}.weather-display .main.hourly.main .hourly-lines .hourly-row .icon{left:255px;width:70px;text-align:center;top:unset}.weather-display .main.hourly.main .hourly-lines .hourly-row .temp{left:355px}.weather-display .main.hourly.main .hourly-lines .hourly-row .like{left:425px}.weather-display .main.hourly.main .hourly-lines .hourly-row .like.heat-index{color:#e00}.weather-display .main.hourly.main .hourly-lines .hourly-row .like.wind-chill{color:#8080ff}.weather-display .main.hourly.main .hourly-lines .hourly-row .wind{left:505px;width:100px;text-align:right}#hourly-graph-html{background-image:var(--theme-background-1-chart)}#hourly-graph-html .header .right{position:absolute;top:35px;right:60px;width:360px;font-family:"Star4000 Small";font-size:28px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;text-align:right}#hourly-graph-html .header .right div{margin-top:-18px}#hourly-graph-html .header .right .temperature{color:red}#hourly-graph-html .header .right .dewpoint{color:green}#hourly-graph-html .header .right .cloud{color:#d3d3d3}#hourly-graph-html .header .right .rain{color:aqua}.weather-display .main.hourly-graph.main>div{position:absolute}.weather-display .main.hourly-graph.main .label{font-family:"Star4000 Small";font-size:24pt;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;margin-top:-15px;position:absolute}.weather-display .main.hourly-graph.main .x-axis{bottom:0px;left:54px;width:532px;height:20px}.weather-display .main.hourly-graph.main .x-axis .label{text-align:center;transform:translateX(-50%);white-space:nowrap}.weather-display .main.hourly-graph.main .x-axis .label.l-1{left:0px}.weather-display .main.hourly-graph.main .x-axis .label.l-2{left:133px}.weather-display .main.hourly-graph.main .x-axis .label.l-3{left:266px}.weather-display .main.hourly-graph.main .x-axis .label.l-4{left:399px}.weather-display .main.hourly-graph.main .x-axis .label.l-5{left:532px}.weather-display .main.hourly-graph.main .chart{top:0px;left:50px}.weather-display .main.hourly-graph.main .chart img{width:532px;height:285px}.weather-display .main.hourly-graph.main .y-axis{top:0px;left:0px;width:50px;height:285px}.weather-display .main.hourly-graph.main .y-axis .label{text-align:right;right:0px}.weather-display .main.hourly-graph.main .y-axis .label.l-1{top:0px}.weather-display .main.hourly-graph.main .y-axis .label.l-2{top:93.3333333333px}.weather-display .main.hourly-graph.main .y-axis .label.l-3{bottom:82.3333333333px}.weather-display .main.hourly-graph.main .y-axis .label.l-4{bottom:0px}.weather-display .main.hourly-graph.main .column-headers{background-color:#200057;height:20px;position:absolute;width:100%}.weather-display .main.hourly-graph.main .column-headers{position:sticky;top:0px;z-index:5}.weather-display .main.hourly-graph.main .column-headers .temp{left:355px}.weather-display .main.hourly-graph.main .column-headers .like{left:435px}.weather-display .main.hourly-graph.main .column-headers .wind{left:535px}.weather-display .main.travel.main{overflow-y:hidden}.weather-display .main.travel.main .column-headers{background-color:#200057;height:20px;position:sticky;top:0px;width:100%;z-index:5;overflow:hidden}.weather-display .main.travel.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;color:#ff0;position:absolute;top:-14px;z-index:5;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.travel.main .column-headers .temp{width:50px;text-align:center}.weather-display .main.travel.main .column-headers .temp.low{left:455px}.weather-display .main.travel.main .column-headers .temp.high{left:510px;width:60px}.weather-display .main.travel.main .travel-lines{min-height:338px;padding-top:10px;background:#0b0b39}.weather-display .main.travel.main .travel-lines .travel-row{font-family:"Star4000 Large";font-size:24pt;height:72px;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative}.weather-display .main.travel.main .travel-lines .travel-row>div{position:absolute;white-space:pre;top:8px}.weather-display .main.travel.main .travel-lines .travel-row .city{left:80px}.weather-display .main.travel.main .travel-lines .travel-row .icon{left:330px;width:70px;text-align:center;top:unset}.weather-display .main.travel.main .travel-lines .travel-row .icon img{max-width:47px}.weather-display .main.travel.main .travel-lines .travel-row .temp{width:50px;text-align:center}.weather-display .main.travel.main .travel-lines .travel-row .temp.low{left:455px}.weather-display .main.travel.main .travel-lines .travel-row .temp.high{left:510px;width:60px}.weather-display .local-forecast .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:280px;overflow:hidden}.weather-display .local-forecast .forecasts{position:relative}.weather-display .local-forecast .forecast{font-family:"Star4000";font-size:24pt;text-transform:uppercase;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;min-height:280px;line-height:40px}:root[data-theme]:not([data-theme=default]) .weather-display .local-forecast .forecasts{margin-top:16px}:root[data-theme]:not([data-theme=default]) .weather-display .local-forecast .forecast{font-size:24px;padding-right:20px}.weather-display .progress{text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;font-family:"Star4000 Extended";font-size:18pt}.weather-display .progress .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:310px;overflow:hidden;line-height:20px;width:90%;margin-top:25px}.weather-display .progress .container .item{position:relative}.weather-display .progress .container .item .name{white-space:nowrap}.weather-display .progress .container .item .name::after{content:"........................................................................"}.weather-display .progress .container .item .links{position:absolute;text-align:right;right:0px;top:0px}.weather-display .progress .container .item .links>div{background-color:#26235a;display:none;padding-left:4px}.weather-display .progress .container .item .links .loading,.weather-display .progress .container .item .links .retrying{color:#ff0}.weather-display .progress .container .item .links .press-here{color:lime;cursor:pointer}.weather-display .progress .container .item .links .failed{color:red}.weather-display .progress .container .item .links .no-data{color:silver}.weather-display .progress .container .item .links .disabled{color:silver}.weather-display .progress .container .item .links.loading .loading,.weather-display .progress .container .item .links.press-here .press-here,.weather-display .progress .container .item .links.failed .failed,.weather-display .progress .container .item .links.no-data .no-data,.weather-display .progress .container .item .links.disabled .disabled,.weather-display .progress .container .item .links.retrying .retrying{display:block}@keyframes progress-scroll{0%{background-position:-40px 0}100%{background-position:40px 0}}#progress-html.weather-display .scroll .progress-bar-container{border:2px solid #000;background-color:#fff;margin:20px auto;width:524px;position:relative;display:none}#progress-html.weather-display .scroll .progress-bar-container.show{display:block}#progress-html.weather-display .scroll .progress-bar-container .progress-bar{height:20px;margin:2px;width:520px;background:repeating-linear-gradient(90deg, #09246f 0px, #09246f 5px, #364ac0 5px, #364ac0 10px, #4f99f9 10px, #4f99f9 15px, #8ffdfa 15px, #8ffdfa 20px, #4f99f9 20px, #4f99f9 25px, #364ac0 25px, #364ac0 30px, #09246f 30px, #09246f 40px);animation-duration:2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:progress-scroll;animation-timing-function:steps(8, end)}#progress-html.weather-display .scroll .progress-bar-container .cover{position:absolute;top:0px;right:0px;background-color:#fff;width:100%;height:24px;transition:width 1s steps(6)}#radar-html.weather-display{background-image:var(--theme-background-4)}#radar-html.weather-display .header{height:83px}#radar-html.weather-display .header .title.dual{color:#fff;font-family:"Arial",sans-serif;font-weight:bold;font-size:28pt;left:155px}#radar-html.weather-display .header .title.dual .top{top:-4px}#radar-html.weather-display .header .title.dual .bottom{top:31px}#radar-html.weather-display .header .right{position:absolute;right:0px;width:360px;margin-top:2px;font-family:"Star4000";font-size:18pt;font-weight:bold;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;text-align:center}#radar-html.weather-display .header .right .scale-table{display:flex;justify-content:center;gap:4px}#radar-html.weather-display .header .right .scale-table .item{display:flex;flex-direction:column;align-items:center;width:25px;gap:2px}#radar-html.weather-display .header .right .scale-table .box{display:block;border:2.7px solid #000;width:90%;height:12px;margin-top:4px;padding:0}#radar-html.weather-display .header .right .scale-table .box-1{background-color:#49bef6}#radar-html.weather-display .header .right .scale-table .box-2{background-color:#31d216}#radar-html.weather-display .header .right .scale-table .box-3{background-color:#f1e458}#radar-html.weather-display .header .right .scale-table .box-4{background-color:#e08e2f}#radar-html.weather-display .header .right .scale-table .box-5{background-color:#c42a2a}#radar-html.weather-display .header .right .scale-table .box-6{background-color:#913bb8}#radar-html.weather-display .header .right .scale-table .label{font-family:"Star4000 Small";font-size:10pt;line-height:1;white-space:nowrap}#radar-html.weather-display .header .right .scale{margin-top:-2px}#radar-html.weather-display .header .right .time{position:relative;font-weight:normal;top:-20px;font-family:"Star4000 Small";font-size:18pt;left:132px}.weather-display .main.radar{overflow:hidden;height:367px}.weather-display .main.radar .container{position:relative;height:100%}.weather-display .main.radar .container .scroll-area{position:relative;height:100%}.weather-display .main.radar .container .frame{height:100%}.weather-display .main.radar .container .map{height:100%;width:100%}.weather-display .main.radar .container .leaflet-map{height:100%;width:100%;background:#061f3e}.weather-display .main.radar .container .leaflet-container{background:#061f3e;font-family:inherit}.weather-display .main.radar .container .radar-base-layer,.weather-display .main.radar .container .radar-base-layer .leaflet-tile{filter:grayscale(0.35) brightness(0.58) contrast(1.1) saturate(0.2)}.weather-display .main.radar .container .radar-boundary-layer,.weather-display .main.radar .container .radar-boundary-layer .leaflet-tile{filter:grayscale(0.8) brightness(0.7) contrast(1.3) saturate(0.1)}.weather-display .main.radar .container .leaflet-control-container,.weather-display .main.radar .container .leaflet-control-attribution,.weather-display .main.radar .container .leaflet-control-zoom{display:none}.weather-display .main.radar .container .location-marker{background:#ff0;border:2px solid #000;border-radius:50%}.weather-display .main.radar .container .nearby-weather-marker{display:none;background:rgba(0,0,0,0);border:0}.weather-display .main.radar .container .nearby-weather-marker .nearby-weather-marker-inner{display:inline-flex;flex-direction:column;align-items:center;min-width:72px;padding:2px 4px;background:rgba(18,34,61,0);color:#fff;text-align:center;opacity:.35}.weather-display .main.radar .container .nearby-weather-marker .city{font-family:"Star4000 Small";font-size:11pt;line-height:1;white-space:nowrap;margin-bottom:1px;text-shadow:1px 1px 0 #000;display:none}.weather-display .main.radar .container .nearby-weather-marker .details{display:flex;align-items:center;gap:2px}.weather-display .main.radar .container .nearby-weather-marker .temp{font-family:"Star4000";font-size:18pt;line-height:1;color:#ff0;text-shadow:1px 1px 0 #000}.weather-display .main.radar .container .nearby-weather-marker img{width:auto;height:20px}.wide.radar #container{background:url(../images/backgrounds/4-wide.png)}.weather-display .main.ground-view.main{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:20px 24px 18px}.weather-display .main.ground-view.main .hidden{display:none}.weather-display .main.ground-view.main .ground-view-media{width:100%;height:100%;display:flex;align-items:center;justify-content:center;flex:1 1 auto;min-height:0;margin-right:60px;margin-top:30px}.weather-display .main.ground-view.main .ground-view-image{display:block;max-width:300px;max-height:150px;object-fit:contain;border:2px solid hsla(0,0%,100%,.35);background:#000}.weather-display .main.ground-view.main .ground-view-label,.weather-display .main.ground-view.main .ground-view-empty{text-align:center;font-family:"Star4000";font-size:15pt;color:#ff0;padding:10px;margin-right:80px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.ground-view.main .ground-view-empty{flex:1 1 auto;display:flex;align-items:center;justify-content:center}#regional-forecast-html.weather-display{background-image:var(--theme-background-5)}.weather-display .main.regional-forecast{position:relative;overflow:hidden;z-index:0}.weather-display .main.regional-forecast .map{position:absolute;inset:0}.weather-display .main.regional-forecast .leaflet-map{height:100%;width:100%;background:#061f3e}.weather-display .main.regional-forecast .leaflet-container{background:#061f3e;font-family:inherit}.weather-display .main.regional-forecast .radar-base-layer,.weather-display .main.regional-forecast .radar-base-layer .leaflet-tile{filter:grayscale(0.35) brightness(0.58) contrast(1.1) saturate(0.2)}.weather-display .main.regional-forecast .radar-boundary-layer,.weather-display .main.regional-forecast .radar-boundary-layer .leaflet-tile{filter:grayscale(0.8) brightness(0.7) contrast(1.3) saturate(0.1)}.weather-display .main.regional-forecast .leaflet-control-container,.weather-display .main.regional-forecast .leaflet-control-attribution,.weather-display .main.regional-forecast .leaflet-control-zoom{display:none}.weather-display .main.regional-forecast .location-marker{background:#ff0;border:2px solid #000;border-radius:50%}.weather-display .main.regional-forecast .nearby-weather-marker{background:rgba(0,0,0,0);border:0}.weather-display .main.regional-forecast .nearby-weather-marker .nearby-weather-marker-inner{display:inline-flex;flex-direction:column;align-items:center;min-width:72px;color:#fff;text-align:center;opacity:.75}.weather-display .main.regional-forecast .nearby-weather-marker .city{display:none}.weather-display .main.regional-forecast .nearby-weather-marker .details{display:flex;align-items:center;gap:2px}.weather-display .main.regional-forecast .nearby-weather-marker .temp{font-family:"Star4000";font-size:21pt;line-height:1;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.regional-forecast .nearby-weather-marker img{width:auto;height:32px}#almanac-html.weather-display{background-image:var(--theme-background-3)}.weather-display .main.almanac{font-family:"Star4000";font-size:24pt;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.almanac .sun{display:grid;grid-template-columns:auto auto auto;grid-template-rows:auto auto auto;gap:0px 90px;margin:3px auto 5px auto;width:fit-content;line-height:30px}.weather-display .main.almanac .sun .grid-item{width:auto;height:auto;padding:0;margin:0;position:relative}.weather-display .main.almanac .sun .grid-item.header{color:#ff0;text-align:center}.weather-display .main.almanac .sun .grid-item.row-label{text-align:right}.weather-display .main.almanac .sun .grid-item.time{text-align:center}.weather-display .main.almanac .moon{position:relative;padding:7px 50px;line-height:36px}.weather-display .main.almanac .moon .title{color:#ff0;padding-left:13px}.weather-display .main.almanac .moon .day{display:inline-block;text-align:center;width:132px}.weather-display .main.almanac .moon .day .icon{padding-left:10px}.weather-display .main.almanac .moon .day .date{position:relative;top:-10px}#hazards-html.weather-display{background-image:url("../images/backgrounds/7.png")}.weather-display .main.hazards.main{overflow-y:hidden;height:480px;background-color:#702323}.weather-display .main.hazards.main .hazard-lines{min-height:400px;padding-top:10px}.weather-display .main.hazards.main .hazard-lines .hazard{font-family:"Star4000";font-size:24pt;color:#fff;text-shadow:0px 0px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative;text-transform:uppercase;margin-top:10px;margin-left:80px;margin-right:80px;padding-bottom:10px}.wide.hazards #container{background:url(../images/backgrounds/7-wide.png)}.media{display:none}#ToggleMediaContainer{display:none;position:relative}#ToggleMediaContainer.available{display:inline-block}#ToggleMediaContainer.available img.on{display:none}#ToggleMediaContainer.available img.off{display:block}#ToggleMediaContainer.available.playing img.on{display:block}#ToggleMediaContainer.available.playing img.off{display:none}#ToggleMediaContainer .volume-slider{display:none;position:absolute;top:0px;transform:translateY(-100%);width:100%;background-color:#000;text-align:center;z-index:100}@media(prefers-color-scheme: dark){#ToggleMediaContainer .volume-slider{background-color:#303030}}#ToggleMediaContainer .volume-slider input[type=range]{writing-mode:vertical-lr;direction:rtl;margin-top:20px;margin-bottom:20px}#ToggleMediaContainer .volume-slider.show{display:block}#spc-outlook-html.weather-display{background-image:var(--theme-background-6)}.weather-display .spc-outlook .container{position:relative;top:0px;margin:0px 10px;box-sizing:border-box;height:300px;overflow:hidden}.weather-display .spc-outlook .risk-levels{position:absolute;left:206px;font-family:"Star4000 Small";font-size:32px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .spc-outlook .risk-levels .risk-level{position:relative;top:-14px;height:20px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(1){left:100px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(2){left:80px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(3){left:60px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(4){left:40px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(5){left:20px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(6){left:0px}.weather-display .spc-outlook .days{position:absolute;top:120px}.weather-display .spc-outlook .days .day{height:60px}.weather-display .spc-outlook .days .day .day-name{position:absolute;font-family:"Star4000";font-size:24pt;width:200px;text-align:right;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;padding-top:20px}.weather-display .spc-outlook .days .day .risk-bar{position:absolute;width:150px;height:40px;left:210px;margin-top:20px;border:3px outset hsl(0,0%,70%);background:linear-gradient(0deg, hsl(0, 0%, 40%) 0%, hsl(0, 0%, 60%) 50%, hsl(0, 0%, 40%) 100%)}#server-observations-html.weather-display .header .title.single{font-size:20pt}.weather-display .server-observations.main{height:auto !important;min-height:250px}.weather-display .server-observations .container{position:relative;top:15px;box-sizing:border-box;height:250px;overflow:hidden}.weather-display .server-observations .server-output{position:relative;font-family:"Star4000";font-size:20pt;line-height:32px;color:#fff;text-transform:uppercase;text-align:center;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .server-observations .server-output .server-page{height:250px;padding:0 8px;box-sizing:border-box;margin-top:16px;padding-right:30px}.weather-display .server-observations .server-output .server-line{white-space:normal;overflow-wrap:anywhere;word-break:break-word;margin-bottom:6px}.weather-display .linux-news.main{height:auto !important;min-height:250px}.weather-display .linux-news .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:250px;overflow:hidden}.weather-display .linux-news .news-output{position:relative}.weather-display .linux-news .news-output .news-page{height:250px;box-sizing:border-box;padding:0 8px;display:flex;flex-direction:column;justify-content:space-between}.weather-display .linux-news .news-output .story{height:116px;overflow:hidden;margin-top:20px}.weather-display .linux-news .news-output .headline{font-family:"Star4000";font-size:17pt;line-height:22px;color:#ff0;text-transform:uppercase;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;margin-bottom:4px}.weather-display .linux-news .news-output .blurb{font-family:"Star4000";font-size:12pt;line-height:10pt;color:#fff;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;overflow:hidden;padding-right:30px}.weather-display .main.hazard-list.main{padding-top:18px}.weather-display .main.hazard-list.main .column-headers{display:flex;font-family:"Star4000";font-size:14pt;font-weight:bold;color:#ff0;width:70%;margin:4px auto 2px;padding-top:20px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.hazard-list.main .column-headers .location{width:34%}.weather-display .main.hazard-list.main .column-headers .hazard{width:30%;text-align:center}.weather-display .main.hazard-list.main .column-headers .date{width:22%;text-align:center}.weather-display .main.hazard-list.main .column-headers .ongoing{width:14%;text-align:center;padding-right:4px}.weather-display .main.hazard-list.main .hazard-list-rows{width:70%;margin:0 auto}.weather-display .main.hazard-list.main .hazard-list-rows .hazard-list-row{display:flex;font-family:"Star4000";font-size:14pt;line-height:1.4;margin-bottom:4px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.hazard-list.main .hazard-list-rows .hazard-list-row.template{display:none}.weather-display .main.hazard-list.main .hazard-list-rows .hazard-list-row .location{width:34%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#fff}.weather-display .main.hazard-list.main .hazard-list-rows .hazard-list-row .hazard{width:30%;text-align:center;color:#ff0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.weather-display .main.hazard-list.main .hazard-list-rows .hazard-list-row .date{width:22%;text-align:center;color:#fff}.weather-display .main.hazard-list.main .hazard-list-rows .hazard-list-row .ongoing{width:14%;text-align:center;padding-right:4px;color:#fff}.scanlines{position:relative;overflow:hidden;isolation:isolate}.scanlines #container{position:relative;z-index:1;transform:translateZ(0);will-change:filter;filter:blur(0.6px) saturate(1.12) contrast(1.02) brightness(1)}.scanlines #container::before,.scanlines #container::after{content:"";position:absolute;inset:0;pointer-events:none;z-index:3}.scanlines #container::before{background:linear-gradient(to right, rgba(255, 0, 0, 0.05) 0%, rgba(255, 0, 0, 0.01) 15%, rgba(255, 0, 0, 0) 50%, rgba(255, 0, 0, 0.01) 85%, rgba(255, 0, 0, 0.05) 100%);transform:translateX(-1px);filter:blur(1.8px);mix-blend-mode:screen}.scanlines #container::after{background:linear-gradient(to right, rgba(0, 140, 255, 0.05) 0%, rgba(0, 140, 255, 0.01) 15%, rgba(0, 140, 255, 0) 50%, rgba(0, 140, 255, 0.01) 85%, rgba(0, 140, 255, 0.05) 100%);transform:translateX(1px);filter:blur(1.8px);mix-blend-mode:screen}.scanlines:before,.scanlines:after{display:block;pointer-events:none;content:"";position:absolute;left:0;right:0}.scanlines:before{height:var(--scanline-thickness, 1px);z-index:2147483650;background:rgba(0,0,0,.3);opacity:.75;animation:scanline 6s linear infinite}.scanlines:after{top:0;bottom:0;z-index:2147483648;background:repeating-linear-gradient(to bottom, transparent 0, transparent var(--scanline-thickness, 1px), rgba(0, 0, 0, 0.3) var(--scanline-thickness, 1px), rgba(0, 0, 0, 0.3) calc(var(--scanline-thickness, 1px) * 2));animation:none}.scanlines{box-shadow:inset 0 0 80px rgba(0,0,0,.16),inset 0 0 18px hsla(0,0%,100%,.08)}.scanlines.crt-panels-only #container{filter:none}.scanlines.crt-panels-only .weather-display{filter:blur(0.6px) saturate(1.12) contrast(1.02) brightness(1);transform:translateZ(0)}.scanlines .header,.scanlines .main,.scanlines .scroll,.scanlines .date-time,.scanlines .city,.scanlines .temp,.scanlines .condition,.scanlines .location,.scanlines .label,.scanlines .value,.scanlines .title{text-shadow:0 0 1px hsla(0,0%,100%,.18),0 0 2px hsla(0,0%,100%,.06)}@keyframes scanline{0%{transform:translate3d(0, 200000%, 0)}}@keyframes scanlines{0%{background-position:0 50%}} \ No newline at end of file +@font-face{font-family:"Star4000";src:url("../fonts/Star4000.woff") format("woff");font-display:swap}body{font-family:"Star4000";margin:0}@media(prefers-color-scheme: dark){body{background-color:#000;color:#fff}}@media(prefers-color-scheme: dark){body a{color:#add8e6}}body.kiosk{margin:0px;padding:0px;overflow:hidden;width:100vw;background-color:#000 !important}#divQuery{max-width:640px;padding:8px}#divQuery .buttons{display:inline-block;width:150px;text-align:right}#divQuery .buttons #imgGetGps{height:13px;vertical-align:middle}#divQuery .buttons button{font-size:16pt;border:1px solid #a9a9a9}@media(prefers-color-scheme: dark){#divQuery .buttons button{background-color:#000;color:#fff}}#divQuery .buttons #btnGetGps img.dark{display:none}@media(prefers-color-scheme: dark){#divQuery .buttons #btnGetGps img.dark{display:inline-block}}@media(prefers-color-scheme: dark){#divQuery .buttons #btnGetGps img.light{display:none}}#divQuery .buttons #btnGetGps.active{background-color:#000}@media(prefers-color-scheme: dark){#divQuery .buttons #btnGetGps.active{background-color:#fff}}#divQuery .buttons #btnGetGps.active img{filter:invert(1)}#divQuery input,#divQuery button{font-family:"Star4000"}#divQuery #txtLocation{width:calc(100% - 170px);max-width:490px;font-size:16pt;min-width:200px;display:inline-block;background-color:#fff;color:#000;border:2px inset gray}@media(prefers-color-scheme: dark){#divQuery #txtLocation{background-color:#000;color:#fff;border:2px inset gray}}.autocomplete-suggestions{background-color:#fff;border:1px solid #000;position:absolute;z-index:9999}@media(prefers-color-scheme: dark){.autocomplete-suggestions{background-color:#000}}.autocomplete-suggestions div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:16pt}.autocomplete-suggestions div.selected{background-color:blue;color:#fff}#divTwc{display:block;background-color:#000;color:#fff;width:100%;max-width:640px;margin:0}#divTwc.wide{max-width:854px}.content-wrapper{padding:8px}#divTwcMain{width:640px;height:480px;position:relative}.wide #divTwcMain{width:854px}.kiosk #divTwc{max-width:unset}#divTwcLeft{display:none;text-align:right;flex-direction:column;vertical-align:middle}#divTwcLeft>div{flex:1;padding-right:12px;display:flex;flex-direction:column;justify-content:center}#divTwcRight{text-align:left;display:none;flex-direction:column;vertical-align:middle}#divTwcRight>div{flex:1;padding-left:12px;display:flex;flex-direction:column;justify-content:center}#divTwcBottom{display:flex;flex-direction:row;background-color:#000;color:#fff;width:640px}.wide #divTwcBottom{width:854px}@media(prefers-color-scheme: dark){#divTwcBottom{background-color:#303030}}#divTwcBottom>div{padding-left:6px;padding-right:6px}@media(max-width: 550px){#divTwcBottom>div{font-size:.9em}}@media(max-width: 500px){#divTwcBottom>div{font-size:.8em}}@media(max-width: 450px){#divTwcBottom>div{font-size:.7em}}@media(max-width: 400px){#divTwcBottom>div{font-size:.6em}}@media(max-width: 350px){#divTwcBottom>div{font-size:.5em}}#divTwcBottomLeft{flex:1;text-align:left}#divTwcBottomMiddle{flex:0;text-align:center}#divTwcBottomRight{flex:1;text-align:right}#divTwcNavContainer{display:none}#divTwcNav{width:100%;display:flex;flex-direction:row;background-color:#000;color:#fff;max-width:640px}#divTwcNav>div{padding-left:6px;padding-right:6px}#divTwcNavLeft{flex:1;text-align:left}#divTwcNavMiddle{flex:0;text-align:center}#divTwcNavRight{flex:1;text-align:right}#imgPause1x{visibility:hidden;position:absolute}.HideCursor{cursor:none !important}#txtScrollText{width:475px}@font-face{font-family:"Star4000 Extended";src:url("../fonts/Star4000 Extended.woff") format("woff");font-display:swap}@font-face{font-family:"Star4000 Large";src:url("../fonts/Star4000 Large.woff") format("woff");font-display:swap}@font-face{font-family:"Star4000 Small";src:url("../fonts/Star4000 Small.woff") format("woff");font-display:swap}:root{--theme-background-1: url('../images/backgrounds/1.png');--theme-background-1-chart: url('../images/backgrounds/1-chart.png');--theme-background-2: url('../images/backgrounds/2.png');--theme-background-3: url('../images/backgrounds/3.png');--theme-background-4: url('../images/backgrounds/4.png');--theme-background-5: url('../images/backgrounds/5.png');--theme-background-6: url('../images/backgrounds/6.png')}#display{font-family:"Star4000";margin:0 0 0 0;width:100%}#container{position:relative;width:640px;height:480px;background-image:var(--theme-background-1);transform-origin:0 0;background-repeat:no-repeat}.wide #container{padding-left:107px;padding-right:107px;background:url(../images/backgrounds/1-wide.png);background-repeat:no-repeat}#divTwc:fullscreen #container,.kiosk #divTwc #container{width:unset;height:unset}#loading{width:640px;height:480px;max-width:100%;text-shadow:4px 4px #000;display:flex;align-items:center;text-align:center;justify-content:center}#loading .title{font-family:Star4000 Large;font-size:26px;color:#ff0;margin-bottom:0px}#loading .version{margin-bottom:35px}#loading .instructions{font-size:18pt}.heading{font-weight:bold;margin-top:15px}#settings{margin-bottom:15px}#enabledDisplays,#settings{margin-bottom:15px}#enabledDisplays .loading,#enabledDisplays .retrying,#settings .loading,#settings .retrying{color:#ff0}#enabledDisplays .press-here,#settings .press-here{color:lime;cursor:pointer}#enabledDisplays .failed,#settings .failed{color:red}#enabledDisplays .no-data,#settings .no-data{color:silver}#enabledDisplays .disabled,#settings .disabled{color:silver}#enabledDisplays .press-here,#settings .press-here{color:#fff}@media(prefers-color-scheme: light){#enabledDisplays .loading,#enabledDisplays .retrying,#settings .loading,#settings .retrying{color:#990}#enabledDisplays .press-here,#settings .press-here{color:#000;cursor:pointer}#enabledDisplays .failed,#settings .failed{color:#900}#enabledDisplays .no-data,#settings .no-data{color:hsl(0,0%,30%)}#enabledDisplays .disabled,#settings .disabled{color:hsl(0,0%,30%)}}#enabledDisplays label,#settings label{display:block;max-width:fit-content;cursor:pointer}#enabledDisplays label .alert,#settings label .alert{display:none}#enabledDisplays label .alert.show,#settings label .alert.show{display:inline;color:red}#divTwcBottom img{transform:scale(0.75)}@media(max-width: 550px){.wide #divTwcBottom img{transform:scale(1)}}#divTwc:fullscreen,.kiosk #divTwc{display:flex;align-items:center;justify-content:center;align-content:center}#divTwc:fullscreen.no-cursor,.kiosk #divTwc.no-cursor{cursor:none}#divTwc:fullscreen #display,.kiosk #divTwc #display{position:relative}#divTwc:fullscreen #divTwcBottom,.kiosk #divTwc #divTwcBottom{display:flex;flex-direction:row;background-color:rgba(0,0,0,.5);color:#fff;width:100%;position:absolute;bottom:0px}.kiosk #divTwc #divTwcBottom{display:none}.navButton{cursor:pointer}#ToggleScanlines{display:inline-block}#ToggleScanlines .on{display:none}#ToggleScanlines .off{display:inline-block}#ToggleScanlines.on .on{display:inline-block}#ToggleScanlines.on .off{display:none}.visible{visibility:visible;opacity:1;transition:opacity .1s linear}#divTwc:fullscreen .hidden{visibility:hidden;opacity:0;transition:visibility 0s 1s,opacity 1s linear}.github-links{width:610px;max-width:calc(100vw - 30px);display:flex;justify-content:space-evenly;flex-wrap:wrap}.github-links span a{text-decoration:none;outline:0}.github-links span .widget{display:inline-block;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;font-size:0;line-height:0;white-space:nowrap}.github-links span .btn,.github-links span .social-count{position:relative;display:inline-block;display:inline-flex;height:14px;padding:2px 5px;font-size:11px;font-weight:600;line-height:14px;vertical-align:bottom;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-repeat:repeat-x;background-position:-1px -1px;background-size:110% 110%;border:1px solid}.github-links span .btn{border-radius:.25em}.github-links span .btn:not(:last-child){border-radius:.25em 0 0 .25em}.github-links span .social-count{border-left:0;border-radius:0 .25em .25em 0}.github-links span .widget-lg .btn,.github-links span .widget-lg .social-count{height:16px;padding:5px 10px;font-size:12px;line-height:16px}.github-links span .octicon{display:inline-block;vertical-align:text-top;fill:currentColor;overflow:visible}.github-links span .btn:focus-visible,.github-links span .social-count:focus-visible{outline:2px solid #0969da;outline-offset:-2px}.github-links span .btn{color:#24292f;background-color:#ebf0f4;border-color:#ccd1d5;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f6f8fa'/%3e%3cstop offset='90%25' stop-color='%23ebf0f4'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f6f8fa, #ebf0f4 90%);background-image:linear-gradient(180deg, #f6f8fa, #ebf0f4 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF6F8FA', endColorstr='#FFEAEFF3')}.github-links span :root .btn{filter:none}.github-links span .btn:hover,.github-links span .btn:focus{background-color:#e9ebef;background-position:0 -0.5em;border-color:#caccd1;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f3f4f6'/%3e%3cstop offset='90%25' stop-color='%23e9ebef'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f3f4f6, #e9ebef 90%);background-image:linear-gradient(180deg, #f3f4f6, #e9ebef 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF3F4F6', endColorstr='#FFE8EAEE')}.github-links span :root .btn:hover,.github-links span :root .btn:focus{filter:none}.github-links span .btn:active{background-color:#e5e9ed;border-color:#c7cbcf;border-color:rgba(27,31,36,.15);box-shadow:inset 0 .15em .3em rgba(27,31,36,.15);background-image:none;filter:none}.github-links span .social-count{color:#24292f;background-color:#fff;border-color:#ddddde;border-color:rgba(27,31,36,.15)}.github-links span .social-count:hover,.github-links span .social-count:focus{color:#0969da}.github-links span .octicon-heart{color:#bf3989}@media(prefers-color-scheme: light){.github-links span .btn:focus-visible,.github-links span .social-count:focus-visible{outline:2px solid #0969da;outline-offset:-2px}.github-links span .btn{color:#24292f;background-color:#ebf0f4;border-color:#ccd1d5;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f6f8fa'/%3e%3cstop offset='90%25' stop-color='%23ebf0f4'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f6f8fa, #ebf0f4 90%);background-image:linear-gradient(180deg, #f6f8fa, #ebf0f4 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF6F8FA', endColorstr='#FFEAEFF3')}.github-links span :root .btn{filter:none}.github-links span .btn:hover,.github-links span .btn:focus{background-color:#e9ebef;background-position:0 -0.5em;border-color:#caccd1;border-color:rgba(27,31,36,.15);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f3f4f6'/%3e%3cstop offset='90%25' stop-color='%23e9ebef'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #f3f4f6, #e9ebef 90%);background-image:linear-gradient(180deg, #f3f4f6, #e9ebef 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF3F4F6', endColorstr='#FFE8EAEE')}.github-links span :root .btn:hover,.github-links span :root .btn:focus{filter:none}.github-links span .btn:active{background-color:#e5e9ed;border-color:#c7cbcf;border-color:rgba(27,31,36,.15);box-shadow:inset 0 .15em .3em rgba(27,31,36,.15);background-image:none;filter:none}.github-links span .social-count{color:#24292f;background-color:#fff;border-color:#ddddde;border-color:rgba(27,31,36,.15)}.github-links span .social-count:hover,.github-links span .social-count:focus{color:#0969da}.github-links span .octicon-heart{color:#bf3989}}@media(prefers-color-scheme: dark){.github-links span .btn:focus-visible,.github-links span .social-count:focus-visible{outline:2px solid #58a6ff;outline-offset:-2px}.github-links span .btn{color:#c9d1d9;background-color:#1a1e23;border-color:#2f3439;border-color:rgba(240,246,252,.1);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%2321262d'/%3e%3cstop offset='90%25' stop-color='%231a1e23'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #21262d, #1a1e23 90%);background-image:linear-gradient(180deg, #21262d, #1a1e23 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF21262D', endColorstr='#FF191D22')}.github-links span :root .btn{filter:none}.github-links span .btn:hover,.github-links span .btn:focus{background-color:#292e33;background-position:0 -0.5em;border-color:#8b949e;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%2330363d'/%3e%3cstop offset='90%25' stop-color='%23292e33'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e");background-image:-moz-linear-gradient(top, #30363d, #292e33 90%);background-image:linear-gradient(180deg, #30363d, #292e33 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF30363D', endColorstr='#FF282D32')}.github-links span :root .btn:hover,.github-links span :root .btn:focus{filter:none}.github-links span .btn:active{background-color:#161719;border-color:#8b949e;box-shadow:inset 0 .15em .3em rgba(1,4,9,.15);background-image:none;filter:none}.github-links span .social-count{color:#c9d1d9;background-color:#0d1117;border-color:#24282e;border-color:rgba(240,246,252,.1)}.github-links span .social-count:hover,.github-links span .social-count:focus{color:#58a6ff}.github-links span .octicon-heart{color:#db61a2}}#share-link-copied{color:#990;display:none}#share-link-instructions{display:none}body.kiosk #loading .instructions{display:none !important}.kiosk>*:not(#divTwc){display:none !important}#divInfo{display:grid;grid-template-columns:1fr 1fr;max-width:250px}.weather-display{width:640px;height:480px;overflow:hidden;position:relative;background-image:var(--theme-background-1);height:0px}.weather-display.show{height:480px}.weather-display .template{display:none}.weather-display .header{width:640px;height:60px;padding-top:30px;position:relative;z-index:20}.weather-display .header .title{color:#eebe4b;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;font-family:"Star4000";font-size:24pt;position:absolute;width:250px}.weather-display .header .title.single{left:138px;top:40px}.weather-display .header .title.dual{left:144px}.weather-display .header .title.dual>div{position:absolute}.weather-display .header .title.dual .top{top:-3px}.weather-display .header .title.dual .bottom{top:26px}.weather-display .header .logo{top:30px;left:50px;position:absolute;z-index:10}.weather-display .header .noaa-logo{position:absolute;top:39px;left:356px}.weather-display .header .date-time{white-space:pre;color:#fff;font-family:"Star4000 Small";font-size:24pt;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;left:436px;width:170px;text-align:right;position:absolute}.weather-display .header .date-time.date{padding-top:22px}.weather-display .main{position:relative}.weather-display .main.has-scroll{width:640px;margin-top:0;height:320px;overflow:hidden}.weather-display .main.has-scroll.no-header{height:400px;margin-top:0}.weather-display .main.has-box{margin-left:64px;margin-right:64px;width:calc(100% - 128px)}#container>.scroll{display:none;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;width:640px;height:77px;overflow:hidden;margin-top:3px;position:absolute;bottom:0px;z-index:1}#container>.scroll.hazard{background-color:#702323}#container>.scroll .scroll-container{width:640px}#container>.scroll .scroll-container .fixed,#container>.scroll .scroll-container .scroll-header{margin-left:55px;margin-right:55px;overflow:hidden;white-space:nowrap}#container>.scroll .scroll-container .scroll-header{height:26px;font-family:"Star4000 Small";font-size:20pt;margin-top:-10px}#container>.scroll .scroll-container .fixed{font-family:"Star4000";font-size:24pt}#container>.scroll .scroll-container .fixed .scroll-area{text-wrap:nowrap;position:relative}.radar #container>.scroll{z-index:100}.radar #container>.scroll.hazard{z-index:1000 !important}.wide #container>.scroll{width:854px;margin-left:-107px}.wide #container>.scroll .scroll-container{margin-left:107px}.weather-display .main.current-weather.main .col{height:50px;width:255px;display:inline-block;margin-top:10px;padding-top:10px;position:absolute;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.current-weather.main .col.left{font-family:"Star4000 Extended";font-size:18pt}.weather-display .main.current-weather.main .col.right{right:0px;font-family:"Star4000 Large";font-size:18px;font-weight:bold;line-height:24px}.weather-display .main.current-weather.main .col.right .row{margin-bottom:12px}.weather-display .main.current-weather.main .col.right .row .label,.weather-display .main.current-weather.main .col.right .row .value{display:inline-block}.weather-display .main.current-weather.main .col.right .row .label{margin-left:20px}.weather-display .main.current-weather.main .col.right .row .value{float:right;margin-right:10px}.weather-display .main.current-weather.main .center{text-align:center}.weather-display .main.current-weather.main .temp{font-family:"Star4000 Large";font-size:24pt}.weather-display .main.current-weather.main .icon img{margin:0 auto;display:block;width:108px}.weather-display .main.current-weather.main .wind-container{margin-left:10px;display:flex}.weather-display .main.current-weather.main .wind-container>div{width:50%}.weather-display .main.current-weather.main .wind-container .wind{text-align:right}.weather-display .main.current-weather.main .wind-gusts{text-align:right;font-size:28px}.weather-display .main.current-weather.main .location{color:#ff0;max-height:32px;margin-bottom:10px;padding-top:4px;overflow:hidden;text-wrap:nowrap;padding-left:15px}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .col.left{margin-top:35px}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .col.right{margin-top:36px;padding:21px}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .wind-container{display:block}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .wind-container>div{width:50%}:root[data-theme]:not([data-theme=default]) .weather-display .main.current-weather.main .wind-container .wind{text-align:right;font-size:22px;display:contents}.weather-display .main.latest-observations.main{padding-top:18px}.weather-display .main.latest-observations.main .column-headers{display:flex;font-family:"Star4000";font-size:14pt;font-weight:bold;color:#ff0;width:70%;margin:8px auto 10px;padding-top:20px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.latest-observations.main .column-headers .city{width:30%}.weather-display .main.latest-observations.main .column-headers .temp{width:15%;text-align:center}.weather-display .main.latest-observations.main .column-headers .conditions{width:30%;text-align:center}.weather-display .main.latest-observations.main .column-headers .wind{width:25%;text-align:right;padding-right:4px}.weather-display .main.latest-observations.main .observation-lines{width:70%;margin:0 auto}.weather-display .main.latest-observations.main .observation-lines .observation-row{display:flex;font-family:"Star4000";font-size:14pt;line-height:1.4;margin-bottom:4px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.latest-observations.main .observation-lines .observation-row.template{display:none}.weather-display .main.latest-observations.main .observation-lines .observation-row .city{width:30%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#fff}.weather-display .main.latest-observations.main .observation-lines .observation-row .temp{width:15%;text-align:center;color:#ff0}.weather-display .main.latest-observations.main .observation-lines .observation-row .conditions{width:30%;text-align:center;color:#fff}.weather-display .main.latest-observations.main .observation-lines .observation-row .wind{width:25%;text-align:right;padding-right:4px;color:#fff}#extended-forecast-html.weather-display{background-image:var(--theme-background-2)}.weather-display .main.extended-forecast .day-container{margin-top:16px;margin-left:27px}.weather-display .main.extended-forecast .day{text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;padding:5px;height:285px;width:155px;display:inline-block;margin:0px 15px;font-family:"Star4000";font-size:24pt}.weather-display .main.extended-forecast .day .date{text-transform:uppercase;text-align:center;color:#ff0}.weather-display .main.extended-forecast .day .condition{text-align:center;height:74px;margin-top:5px}.weather-display .main.extended-forecast .day .icon{text-align:center;height:75px}.weather-display .main.extended-forecast .day .icon img{max-height:75px}.weather-display .main.extended-forecast .day .temperatures{width:100%}.weather-display .main.extended-forecast .day .temperatures .temperature-block{display:inline-block;width:44%;vertical-align:top}.weather-display .main.extended-forecast .day .temperatures .temperature-block>div{text-align:center}.weather-display .main.extended-forecast .day .temperatures .temperature-block .value{font-family:"Star4000 Large";margin-top:4px}.weather-display .main.extended-forecast .day .temperatures .temperature-block.lo .label{color:#8080ff}.weather-display .main.extended-forecast .day .temperatures .temperature-block.hi .label{color:#ff0}.weather-display .main.hourly.main{overflow-y:hidden}.weather-display .main.hourly.main .column-headers{background-color:#200057;height:20px;position:absolute;width:100%}.weather-display .main.hourly.main .column-headers{position:sticky;top:0px;z-index:5}.weather-display .main.hourly.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;color:#ff0;position:absolute;top:-14px;z-index:5;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.hourly.main .column-headers .temp{left:355px}.weather-display .main.hourly.main .column-headers .like{left:435px}.weather-display .main.hourly.main .column-headers .wind{left:535px}.weather-display .main.hourly.main .hourly-lines{min-height:338px;padding-top:10px;background:#0b0b39}.weather-display .main.hourly.main .hourly-lines .hourly-row{font-family:"Star4000 Large";font-size:24pt;height:72px;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative}.weather-display .main.hourly.main .hourly-lines .hourly-row>div{position:absolute;white-space:pre;top:8px}.weather-display .main.hourly.main .hourly-lines .hourly-row .hour{left:25px}.weather-display .main.hourly.main .hourly-lines .hourly-row .icon{left:255px;width:70px;text-align:center;top:unset}.weather-display .main.hourly.main .hourly-lines .hourly-row .temp{left:355px}.weather-display .main.hourly.main .hourly-lines .hourly-row .like{left:425px}.weather-display .main.hourly.main .hourly-lines .hourly-row .like.heat-index{color:#e00}.weather-display .main.hourly.main .hourly-lines .hourly-row .like.wind-chill{color:#8080ff}.weather-display .main.hourly.main .hourly-lines .hourly-row .wind{left:505px;width:100px;text-align:right}#hourly-graph-html{background-image:var(--theme-background-1-chart)}#hourly-graph-html .header .right{position:absolute;top:35px;right:60px;width:360px;font-family:"Star4000 Small";font-size:28px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;text-align:right}#hourly-graph-html .header .right div{margin-top:-18px}#hourly-graph-html .header .right .temperature{color:red}#hourly-graph-html .header .right .dewpoint{color:green}#hourly-graph-html .header .right .cloud{color:#d3d3d3}#hourly-graph-html .header .right .rain{color:aqua}.weather-display .main.hourly-graph.main>div{position:absolute}.weather-display .main.hourly-graph.main .label{font-family:"Star4000 Small";font-size:24pt;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;margin-top:-15px;position:absolute}.weather-display .main.hourly-graph.main .x-axis{bottom:0px;left:54px;width:532px;height:20px}.weather-display .main.hourly-graph.main .x-axis .label{text-align:center;transform:translateX(-50%);white-space:nowrap}.weather-display .main.hourly-graph.main .x-axis .label.l-1{left:0px}.weather-display .main.hourly-graph.main .x-axis .label.l-2{left:133px}.weather-display .main.hourly-graph.main .x-axis .label.l-3{left:266px}.weather-display .main.hourly-graph.main .x-axis .label.l-4{left:399px}.weather-display .main.hourly-graph.main .x-axis .label.l-5{left:532px}.weather-display .main.hourly-graph.main .chart{top:0px;left:50px}.weather-display .main.hourly-graph.main .chart img{width:532px;height:285px}.weather-display .main.hourly-graph.main .y-axis{top:0px;left:0px;width:50px;height:285px}.weather-display .main.hourly-graph.main .y-axis .label{text-align:right;right:0px}.weather-display .main.hourly-graph.main .y-axis .label.l-1{top:0px}.weather-display .main.hourly-graph.main .y-axis .label.l-2{top:93.3333333333px}.weather-display .main.hourly-graph.main .y-axis .label.l-3{bottom:82.3333333333px}.weather-display .main.hourly-graph.main .y-axis .label.l-4{bottom:0px}.weather-display .main.hourly-graph.main .column-headers{background-color:#200057;height:20px;position:absolute;width:100%}.weather-display .main.hourly-graph.main .column-headers{position:sticky;top:0px;z-index:5}.weather-display .main.hourly-graph.main .column-headers .temp{left:355px}.weather-display .main.hourly-graph.main .column-headers .like{left:435px}.weather-display .main.hourly-graph.main .column-headers .wind{left:535px}.weather-display .main.travel.main{overflow-y:hidden}.weather-display .main.travel.main .column-headers{background-color:#200057;height:20px;position:sticky;top:0px;width:100%;z-index:5;overflow:hidden}.weather-display .main.travel.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;color:#ff0;position:absolute;top:-14px;z-index:5;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.travel.main .column-headers .temp{width:50px;text-align:center}.weather-display .main.travel.main .column-headers .temp.low{left:455px}.weather-display .main.travel.main .column-headers .temp.high{left:510px;width:60px}.weather-display .main.travel.main .travel-lines{min-height:338px;padding-top:10px;background:#0b0b39}.weather-display .main.travel.main .travel-lines .travel-row{font-family:"Star4000 Large";font-size:24pt;height:72px;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative}.weather-display .main.travel.main .travel-lines .travel-row>div{position:absolute;white-space:pre;top:8px}.weather-display .main.travel.main .travel-lines .travel-row .city{left:80px}.weather-display .main.travel.main .travel-lines .travel-row .icon{left:330px;width:70px;text-align:center;top:unset}.weather-display .main.travel.main .travel-lines .travel-row .icon img{max-width:47px}.weather-display .main.travel.main .travel-lines .travel-row .temp{width:50px;text-align:center}.weather-display .main.travel.main .travel-lines .travel-row .temp.low{left:455px}.weather-display .main.travel.main .travel-lines .travel-row .temp.high{left:510px;width:60px}.weather-display .local-forecast .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:280px;overflow:hidden}.weather-display .local-forecast .forecasts{position:relative}.weather-display .local-forecast .forecast{font-family:"Star4000";font-size:24pt;text-transform:uppercase;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;min-height:280px;line-height:40px}:root[data-theme]:not([data-theme=default]) .weather-display .local-forecast .forecasts{margin-top:16px}:root[data-theme]:not([data-theme=default]) .weather-display .local-forecast .forecast{font-size:24px;padding-right:20px}.weather-display .progress{text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;font-family:"Star4000 Extended";font-size:18pt}.weather-display .progress .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:310px;overflow:hidden;line-height:20px;width:90%;margin-top:25px}.weather-display .progress .container .item{position:relative}.weather-display .progress .container .item .name{white-space:nowrap}.weather-display .progress .container .item .name::after{content:"........................................................................"}.weather-display .progress .container .item .links{position:absolute;text-align:right;right:0px;top:0px}.weather-display .progress .container .item .links>div{background-color:#26235a;display:none;padding-left:4px}.weather-display .progress .container .item .links .loading,.weather-display .progress .container .item .links .retrying{color:#ff0}.weather-display .progress .container .item .links .press-here{color:lime;cursor:pointer}.weather-display .progress .container .item .links .failed{color:red}.weather-display .progress .container .item .links .no-data{color:silver}.weather-display .progress .container .item .links .disabled{color:silver}.weather-display .progress .container .item .links.loading .loading,.weather-display .progress .container .item .links.press-here .press-here,.weather-display .progress .container .item .links.failed .failed,.weather-display .progress .container .item .links.no-data .no-data,.weather-display .progress .container .item .links.disabled .disabled,.weather-display .progress .container .item .links.retrying .retrying{display:block}@keyframes progress-scroll{0%{background-position:-40px 0}100%{background-position:40px 0}}#progress-html.weather-display .scroll .progress-bar-container{border:2px solid #000;background-color:#fff;margin:20px auto;width:524px;position:relative;display:none}#progress-html.weather-display .scroll .progress-bar-container.show{display:block}#progress-html.weather-display .scroll .progress-bar-container .progress-bar{height:20px;margin:2px;width:520px;background:repeating-linear-gradient(90deg, #09246f 0px, #09246f 5px, #364ac0 5px, #364ac0 10px, #4f99f9 10px, #4f99f9 15px, #8ffdfa 15px, #8ffdfa 20px, #4f99f9 20px, #4f99f9 25px, #364ac0 25px, #364ac0 30px, #09246f 30px, #09246f 40px);animation-duration:2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:progress-scroll;animation-timing-function:steps(8, end)}#progress-html.weather-display .scroll .progress-bar-container .cover{position:absolute;top:0px;right:0px;background-color:#fff;width:100%;height:24px;transition:width 1s steps(6)}#radar-html.weather-display{background-image:var(--theme-background-4)}#radar-html.weather-display .header{height:83px}#radar-html.weather-display .header .title.dual{color:#fff;font-family:"Arial",sans-serif;font-weight:bold;font-size:28pt;left:155px}#radar-html.weather-display .header .title.dual .top{top:-4px}#radar-html.weather-display .header .title.dual .bottom{top:31px}#radar-html.weather-display .header .right{position:absolute;right:0px;width:360px;margin-top:2px;font-family:"Star4000";font-size:18pt;font-weight:bold;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;text-align:center}#radar-html.weather-display .header .right .scale-table{display:flex;justify-content:center;gap:4px}#radar-html.weather-display .header .right .scale-table .item{display:flex;flex-direction:column;align-items:center;width:25px;gap:2px}#radar-html.weather-display .header .right .scale-table .box{display:block;border:2.7px solid #000;width:90%;height:12px;margin-top:4px;padding:0}#radar-html.weather-display .header .right .scale-table .box-1{background-color:#49bef6}#radar-html.weather-display .header .right .scale-table .box-2{background-color:#31d216}#radar-html.weather-display .header .right .scale-table .box-3{background-color:#f1e458}#radar-html.weather-display .header .right .scale-table .box-4{background-color:#e08e2f}#radar-html.weather-display .header .right .scale-table .box-5{background-color:#c42a2a}#radar-html.weather-display .header .right .scale-table .box-6{background-color:#913bb8}#radar-html.weather-display .header .right .scale-table .label{font-family:"Star4000 Small";font-size:10pt;line-height:1;white-space:nowrap}#radar-html.weather-display .header .right .scale{margin-top:-2px}#radar-html.weather-display .header .right .time{position:relative;font-weight:normal;top:-20px;font-family:"Star4000 Small";font-size:18pt;left:132px}.weather-display .main.radar{overflow:hidden;height:367px}.weather-display .main.radar .container{position:relative;height:100%}.weather-display .main.radar .container .scroll-area{position:relative;height:100%}.weather-display .main.radar .container .frame{height:100%}.weather-display .main.radar .container .map{height:100%;width:100%}.weather-display .main.radar .container .leaflet-map{height:100%;width:100%;background:#061f3e}.weather-display .main.radar .container .leaflet-container{background:#061f3e;font-family:inherit}.weather-display .main.radar .container .radar-base-layer,.weather-display .main.radar .container .radar-base-layer .leaflet-tile{filter:grayscale(0.35) brightness(0.58) contrast(1.1) saturate(0.2)}.weather-display .main.radar .container .radar-boundary-layer,.weather-display .main.radar .container .radar-boundary-layer .leaflet-tile{filter:grayscale(0.8) brightness(0.7) contrast(1.3) saturate(0.1)}.weather-display .main.radar .container .leaflet-control-container,.weather-display .main.radar .container .leaflet-control-attribution,.weather-display .main.radar .container .leaflet-control-zoom{display:none}.weather-display .main.radar .container .location-marker{background:#ff0;border:2px solid #000;border-radius:50%}.weather-display .main.radar .container .nearby-weather-marker{display:none;background:rgba(0,0,0,0);border:0}.weather-display .main.radar .container .nearby-weather-marker .nearby-weather-marker-inner{display:inline-flex;flex-direction:column;align-items:center;min-width:72px;padding:2px 4px;background:rgba(18,34,61,0);color:#fff;text-align:center;opacity:.35}.weather-display .main.radar .container .nearby-weather-marker .city{font-family:"Star4000 Small";font-size:11pt;line-height:1;white-space:nowrap;margin-bottom:1px;text-shadow:1px 1px 0 #000;display:none}.weather-display .main.radar .container .nearby-weather-marker .details{display:flex;align-items:center;gap:2px}.weather-display .main.radar .container .nearby-weather-marker .temp{font-family:"Star4000";font-size:18pt;line-height:1;color:#ff0;text-shadow:1px 1px 0 #000}.weather-display .main.radar .container .nearby-weather-marker img{width:auto;height:20px}.wide.radar #container{background:url(../images/backgrounds/4-wide.png)}.weather-display .main.ground-view.main{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:20px 24px 18px}.weather-display .main.ground-view.main .hidden{display:none}.weather-display .main.ground-view.main .ground-view-media{width:100%;height:100%;display:flex;align-items:center;justify-content:center;flex:1 1 auto;min-height:0;margin-right:60px;margin-top:30px}.weather-display .main.ground-view.main .ground-view-image{display:block;max-width:300px;max-height:150px;object-fit:contain;border:2px solid hsla(0,0%,100%,.35);background:#000}.weather-display .main.ground-view.main .ground-view-label,.weather-display .main.ground-view.main .ground-view-empty{text-align:center;font-family:"Star4000";font-size:15pt;color:#ff0;padding:10px;margin-right:80px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.ground-view.main .ground-view-empty{flex:1 1 auto;display:flex;align-items:center;justify-content:center}#regional-forecast-html.weather-display{background-image:var(--theme-background-5)}.weather-display .main.regional-forecast{position:relative;overflow:hidden;z-index:0}.weather-display .main.regional-forecast .map{position:absolute;inset:0}.weather-display .main.regional-forecast .leaflet-map{height:100%;width:100%;background:#061f3e}.weather-display .main.regional-forecast .leaflet-container{background:#061f3e;font-family:inherit}.weather-display .main.regional-forecast .radar-base-layer,.weather-display .main.regional-forecast .radar-base-layer .leaflet-tile{filter:grayscale(0.35) brightness(0.58) contrast(1.1) saturate(0.2)}.weather-display .main.regional-forecast .radar-boundary-layer,.weather-display .main.regional-forecast .radar-boundary-layer .leaflet-tile{filter:grayscale(0.8) brightness(0.7) contrast(1.3) saturate(0.1)}.weather-display .main.regional-forecast .leaflet-control-container,.weather-display .main.regional-forecast .leaflet-control-attribution,.weather-display .main.regional-forecast .leaflet-control-zoom{display:none}.weather-display .main.regional-forecast .location-marker{background:#ff0;border:2px solid #000;border-radius:50%}.weather-display .main.regional-forecast .nearby-weather-marker{background:rgba(0,0,0,0);border:0}.weather-display .main.regional-forecast .nearby-weather-marker .nearby-weather-marker-inner{display:inline-flex;flex-direction:column;align-items:center;min-width:72px;color:#fff;text-align:center;opacity:.75}.weather-display .main.regional-forecast .nearby-weather-marker .city{display:none}.weather-display .main.regional-forecast .nearby-weather-marker .details{display:flex;align-items:center;gap:2px}.weather-display .main.regional-forecast .nearby-weather-marker .temp{font-family:"Star4000";font-size:21pt;line-height:1;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.regional-forecast .nearby-weather-marker img{width:auto;height:32px}#almanac-html.weather-display{background-image:var(--theme-background-3)}.weather-display .main.almanac{font-family:"Star4000";font-size:24pt;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.almanac .sun{display:grid;grid-template-columns:auto auto auto;grid-template-rows:auto auto auto;gap:0px 90px;margin:3px auto 5px auto;width:fit-content;line-height:30px}.weather-display .main.almanac .sun .grid-item{width:auto;height:auto;padding:0;margin:0;position:relative}.weather-display .main.almanac .sun .grid-item.header{color:#ff0;text-align:center}.weather-display .main.almanac .sun .grid-item.row-label{text-align:right}.weather-display .main.almanac .sun .grid-item.time{text-align:center}.weather-display .main.almanac .moon{position:relative;padding:7px 50px;line-height:36px}.weather-display .main.almanac .moon .title{color:#ff0;padding-left:13px}.weather-display .main.almanac .moon .day{display:inline-block;text-align:center;width:132px}.weather-display .main.almanac .moon .day .icon{padding-left:10px}.weather-display .main.almanac .moon .day .date{position:relative;top:-10px}#hazards-html.weather-display{background-image:url("../images/backgrounds/7.png")}.weather-display .main.hazards.main{overflow-y:hidden;height:480px;background-color:#702323}.weather-display .main.hazards.main .hazard-lines{min-height:400px;padding-top:10px}.weather-display .main.hazards.main .hazard-lines .hazard{font-family:"Star4000";font-size:24pt;color:#fff;text-shadow:0px 0px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative;text-transform:uppercase;margin-top:10px;margin-left:80px;margin-right:80px;padding-bottom:10px}.wide.hazards #container{background:url(../images/backgrounds/7-wide.png)}.media{display:none}#ToggleMediaContainer{display:none;position:relative}#ToggleMediaContainer.available{display:inline-block}#ToggleMediaContainer.available img.on{display:none}#ToggleMediaContainer.available img.off{display:block}#ToggleMediaContainer.available.playing img.on{display:block}#ToggleMediaContainer.available.playing img.off{display:none}#ToggleMediaContainer .volume-slider{display:none;position:absolute;top:0px;transform:translateY(-100%);width:100%;background-color:#000;text-align:center;z-index:100}@media(prefers-color-scheme: dark){#ToggleMediaContainer .volume-slider{background-color:#303030}}#ToggleMediaContainer .volume-slider input[type=range]{writing-mode:vertical-lr;direction:rtl;margin-top:20px;margin-bottom:20px}#ToggleMediaContainer .volume-slider.show{display:block}#spc-outlook-html.weather-display{background-image:var(--theme-background-6)}.weather-display .spc-outlook .container{position:relative;top:0px;margin:0px 10px;box-sizing:border-box;height:300px;overflow:hidden}.weather-display .spc-outlook .risk-levels{position:absolute;left:206px;font-family:"Star4000 Small";font-size:32px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .spc-outlook .risk-levels .risk-level{position:relative;top:-14px;height:20px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(1){left:100px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(2){left:80px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(3){left:60px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(4){left:40px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(5){left:20px}.weather-display .spc-outlook .risk-levels .risk-level:nth-child(6){left:0px}.weather-display .spc-outlook .days{position:absolute;top:120px}.weather-display .spc-outlook .days .day{height:60px}.weather-display .spc-outlook .days .day .day-name{position:absolute;font-family:"Star4000";font-size:24pt;width:200px;text-align:right;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;padding-top:20px}.weather-display .spc-outlook .days .day .risk-bar{position:absolute;width:150px;height:40px;left:210px;margin-top:20px;border:3px outset hsl(0,0%,70%);background:linear-gradient(0deg, hsl(0, 0%, 40%) 0%, hsl(0, 0%, 60%) 50%, hsl(0, 0%, 40%) 100%)}#server-observations-html.weather-display .header .title.single{font-size:20pt}.weather-display .server-observations.main{height:auto !important;min-height:250px}.weather-display .server-observations .container{position:relative;top:15px;box-sizing:border-box;height:250px;overflow:hidden}.weather-display .server-observations .server-output{position:relative;font-family:"Star4000";font-size:20pt;line-height:32px;color:#fff;text-transform:uppercase;text-align:center;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .server-observations .server-output .server-page{height:250px;padding:0 8px;box-sizing:border-box;margin-top:16px;padding-right:30px}.weather-display .server-observations .server-output .server-line{white-space:normal;overflow-wrap:anywhere;word-break:break-word;margin-bottom:6px}.weather-display .linux-news.main{height:auto !important;min-height:250px}.weather-display .linux-news .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:250px;overflow:hidden}.weather-display .linux-news .news-output{position:relative}.weather-display .linux-news .news-output .news-page{height:250px;box-sizing:border-box;padding:0 8px;display:flex;flex-direction:column;justify-content:space-between}.weather-display .linux-news .news-output .story{height:116px;overflow:hidden;margin-top:20px}.weather-display .linux-news .news-output .headline{font-family:"Star4000";font-size:17pt;line-height:22px;color:#ff0;text-transform:uppercase;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;margin-bottom:4px}.weather-display .linux-news .news-output .blurb{font-family:"Star4000";font-size:12pt;line-height:10pt;color:#fff;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;overflow:hidden;padding-right:30px}.scanlines{position:relative;overflow:hidden;isolation:isolate}.scanlines #container{position:relative;z-index:1;transform:translateZ(0);will-change:filter;filter:blur(0.6px) saturate(1.12) contrast(1.02) brightness(1)}.scanlines #container::before,.scanlines #container::after{content:"";position:absolute;inset:0;pointer-events:none;z-index:3}.scanlines #container::before{background:linear-gradient(to right, rgba(255, 0, 0, 0.05) 0%, rgba(255, 0, 0, 0.01) 15%, rgba(255, 0, 0, 0) 50%, rgba(255, 0, 0, 0.01) 85%, rgba(255, 0, 0, 0.05) 100%);transform:translateX(-1px);filter:blur(1.8px);mix-blend-mode:screen}.scanlines #container::after{background:linear-gradient(to right, rgba(0, 140, 255, 0.05) 0%, rgba(0, 140, 255, 0.01) 15%, rgba(0, 140, 255, 0) 50%, rgba(0, 140, 255, 0.01) 85%, rgba(0, 140, 255, 0.05) 100%);transform:translateX(1px);filter:blur(1.8px);mix-blend-mode:screen}.scanlines:before,.scanlines:after{display:block;pointer-events:none;content:"";position:absolute;left:0;right:0}.scanlines:before{height:var(--scanline-thickness, 1px);z-index:2147483650;background:rgba(0,0,0,.3);opacity:.75;animation:scanline 6s linear infinite}.scanlines:after{top:0;bottom:0;z-index:2147483648;background:repeating-linear-gradient(to bottom, transparent 0, transparent var(--scanline-thickness, 1px), rgba(0, 0, 0, 0.3) var(--scanline-thickness, 1px), rgba(0, 0, 0, 0.3) calc(var(--scanline-thickness, 1px) * 2));animation:none}.scanlines{box-shadow:inset 0 0 80px rgba(0,0,0,.16),inset 0 0 18px hsla(0,0%,100%,.08)}.scanlines.crt-panels-only #container{filter:none}.scanlines.crt-panels-only .weather-display{filter:blur(0.6px) saturate(1.12) contrast(1.02) brightness(1);transform:translateZ(0)}.scanlines .header,.scanlines .main,.scanlines .scroll,.scanlines .date-time,.scanlines .city,.scanlines .temp,.scanlines .condition,.scanlines .location,.scanlines .label,.scanlines .value,.scanlines .title{text-shadow:0 0 1px hsla(0,0%,100%,.18),0 0 2px hsla(0,0%,100%,.06)}@keyframes scanline{0%{transform:translate3d(0, 200000%, 0)}}@keyframes scanlines{0%{background-position:0 50%}} \ No newline at end of file diff --git a/src/hazard-history.mjs b/src/hazard-history.mjs deleted file mode 100644 index 9545d17..0000000 --- a/src/hazard-history.mjs +++ /dev/null @@ -1,143 +0,0 @@ -import { getPool } from './mysql.mjs'; - -const MAX_HISTORY_ENTRIES = 7; - -const toIsoString = (value) => { - if (!value) return null; - const date = value instanceof Date ? value : new Date(value); - return Number.isNaN(date.getTime()) ? null : date.toISOString(); -}; - -const mapRowToHistoryEntry = (row) => ({ - location: row.location_label, - locationKey: row.location_key, - hazardType: row.hazard_type, - source: row.source, - severity: row.severity, - latestHazardId: row.latest_hazard_id, - encounteredAt: toIsoString(row.encountered_at), - lastSeenAt: toIsoString(row.last_seen_at), - ongoing: Boolean(row.ongoing), -}); - -/** - * Format location label from weather parameters - * @param {string} city - City name - * @param {string} state - State name - * @param {string} country - Country name - * @param {string} countryCode - Country code - * @returns {string} Formatted location label - */ -const formatLocation = (city, state, country, countryCode) => { - const cleanCity = city?.trim() || 'Unknown'; - - if (countryCode === 'US' || countryCode === 'USA') { - const cleanState = state?.trim(); - return cleanState ? `${cleanCity}, ${cleanState}` : cleanCity; - } - - const cleanCountry = country?.trim(); - return cleanCountry ? `${cleanCity}, ${cleanCountry}` : cleanCity; -}; - -const getHistory = async () => { - const [rows] = await getPool().query( - `SELECT - location_label, - location_key, - hazard_type, - source, - severity, - latest_hazard_id, - encountered_at, - last_seen_at, - ongoing - FROM hazard_history - ORDER BY last_seen_at DESC - LIMIT ?`, - [MAX_HISTORY_ENTRIES], - ); - - return rows.map(mapRowToHistoryEntry); -}; - -const updateHistory = async (payload) => { - const { location, locationKey, hazards = [] } = payload; - const validHazards = hazards.filter((hazard) => hazard?.hazardType && hazard?.source); - const pool = getPool(); - const connection = await pool.getConnection(); - - try { - await connection.beginTransaction(); - - if (validHazards.length === 0) { - await connection.execute( - `UPDATE hazard_history - SET ongoing = 0, - last_seen_at = UTC_TIMESTAMP() - WHERE location_key = ? - AND ongoing = 1`, - [locationKey], - ); - } else { - const keepClauses = validHazards.map(() => '(hazard_type = ? AND source = ?)').join(' OR '); - const keepParams = validHazards.flatMap((hazard) => [hazard.hazardType, hazard.source]); - - await connection.execute( - `UPDATE hazard_history - SET ongoing = 0, - last_seen_at = UTC_TIMESTAMP() - WHERE location_key = ? - AND ongoing = 1 - AND NOT (${keepClauses})`, - [locationKey, ...keepParams], - ); - } - - for (const hazard of validHazards) { - await connection.execute( - `INSERT INTO hazard_history ( - location_label, - location_key, - hazard_type, - source, - severity, - latest_hazard_id, - encountered_at, - last_seen_at, - ongoing - ) VALUES (?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP(), 1) - ON DUPLICATE KEY UPDATE - location_label = VALUES(location_label), - severity = VALUES(severity), - latest_hazard_id = VALUES(latest_hazard_id), - last_seen_at = UTC_TIMESTAMP(), - ongoing = 1`, - [ - location, - locationKey, - hazard.hazardType, - hazard.source, - hazard.severity ?? null, - hazard.id ?? null, - ], - ); - } - - await connection.commit(); - } catch (error) { - await connection.rollback(); - throw error; - } finally { - connection.release(); - } - - return getHistory(); -}; - -export { - formatLocation, - getHistory, - MAX_HISTORY_ENTRIES, - updateHistory, -}; diff --git a/src/mysql.mjs b/src/mysql.mjs deleted file mode 100644 index 0110164..0000000 --- a/src/mysql.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import mysql from 'mysql2/promise'; - -let pool; - -const getConfig = () => { - const { - WS4KP_MYSQL_HOST = '127.0.0.1', - WS4KP_MYSQL_PORT = '3306', - WS4KP_MYSQL_USER, - WS4KP_MYSQL_PASSWORD, - WS4KP_MYSQL_DATABASE, - WS4KP_MYSQL_SOCKET_PATH, - } = process.env; - - if (!WS4KP_MYSQL_USER || !WS4KP_MYSQL_PASSWORD || !WS4KP_MYSQL_DATABASE) { - throw new Error('Missing MySQL configuration. Set WS4KP_MYSQL_USER, WS4KP_MYSQL_PASSWORD, and WS4KP_MYSQL_DATABASE.'); - } - - const config = { - user: WS4KP_MYSQL_USER, - password: WS4KP_MYSQL_PASSWORD, - database: WS4KP_MYSQL_DATABASE, - waitForConnections: true, - connectionLimit: 10, - queueLimit: 0, - }; - - if (WS4KP_MYSQL_SOCKET_PATH) { - config.socketPath = WS4KP_MYSQL_SOCKET_PATH; - } else { - config.host = WS4KP_MYSQL_HOST; - config.port = Number(WS4KP_MYSQL_PORT); - } - - return config; -}; - -const getPool = () => { - if (!pool) { - pool = mysql.createPool(getConfig()); - } - return pool; -}; - -const checkHazardHistoryTable = async () => { - const config = getConfig(); - const [rows] = await getPool().query( - `SELECT 1 - FROM information_schema.tables - WHERE table_schema = ? - AND table_name = 'hazard_history' - LIMIT 1`, - [config.database], - ); - - if (rows.length === 0) { - throw new Error(`Hazard history database table 'hazard_history' is missing in database '${config.database}'. Run the documented CREATE TABLE statement before using Hazard List.`); - } - - return true; -}; - -export { - checkHazardHistoryTable, - getPool, -}; diff --git a/views/index.ejs b/views/index.ejs index 8126429..9e8ce83 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -151,9 +151,6 @@
<%- include('partials/linux-news.ejs') %>
-
- <%- include('partials/hazard-list.ejs') %> -
<%- include('partials/scroll.ejs') %> diff --git a/views/partials/hazard-list.ejs b/views/partials/hazard-list.ejs deleted file mode 100644 index 5b4acbd..0000000 --- a/views/partials/hazard-list.ejs +++ /dev/null @@ -1,27 +0,0 @@ -
- -
-
Hazard
-
List
-
-
-
-
-
-
-
LOCATION
-
HAZARD
-
DATE
-
ONGOING
-
-
-
-
-
-
-
-
-
-