From b49433f5ff1a9a0d84aadb146b0f1b2ae72251cf Mon Sep 17 00:00:00 2001 From: Eddy G Date: Sat, 28 Jun 2025 13:05:04 -0400 Subject: [PATCH] Add responsive scaling; improve scanlines and Mobile Safari support - Replace CSS zoom with CSS transform scaling for better mobile compatibility - Implement wrapper-based scaling approach that includes both content and navigation bar - Replace Almanac layout with CSS Grid for better cross-browser layout - Greatly improve scanline algorithm to handle a wide variety of displays - Add setting to override automatic scanlines to user-specified scale factor - Remove scanline scaling debug functions - Refactor settings module: initialize settings upfront and improve change handler declarations - Enhance scanline SCSS with repeating-linear-gradient for better performance - Add app icon for iOS/iPadOS - Add 'fullscreen' event listener - De-bounce 'resize' event listener - Add 'orientationchange' event listener - Implement three resize scaling algorithms: - Baseline (when no scaling is needed, like on the index page) - Mobile scaling (except Mobile Safari kiosk mode) - Mobile Safari kiosk mode (using manual offset calculations) - Standard fullscreen/kiosk mode (using CSS centering) --- server/images/logos/app-icon-180.png | Bin 0 -> 35029 bytes server/scripts/index.mjs | 77 +- server/scripts/modules/almanac.mjs | 40 +- server/scripts/modules/navigation.mjs | 1080 ++++++++------------- server/scripts/modules/settings.mjs | 148 ++- server/scripts/modules/utils/setting.mjs | 5 +- server/styles/main.css | 2 +- server/styles/main.css.map | 2 +- server/styles/scss/_almanac.scss | 71 +- server/styles/scss/_current-weather.scss | 27 +- server/styles/scss/_page.scss | 53 +- server/styles/scss/_progress.scss | 7 +- server/styles/scss/_travel.scss | 13 +- server/styles/scss/_weather-display.scss | 2 + server/styles/scss/shared/_scanlines.scss | 66 +- views/index.ejs | 186 ++-- views/partials/almanac.ejs | 26 +- 17 files changed, 797 insertions(+), 1008 deletions(-) create mode 100644 server/images/logos/app-icon-180.png diff --git a/server/images/logos/app-icon-180.png b/server/images/logos/app-icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..8f4232161fb0e881c30d69287bc90fe1f1fb7a20 GIT binary patch literal 35029 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 Bi#Y%Q literal 0 HcmV?d00001 diff --git a/server/scripts/index.mjs b/server/scripts/index.mjs index 4b86dc6..c4dc551 100644 --- a/server/scripts/index.mjs +++ b/server/scripts/index.mjs @@ -1,13 +1,14 @@ import { json } from './modules/utils/fetch.mjs'; import noSleep from './modules/utils/nosleep.mjs'; import { - message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, + message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, isIOS, } from './modules/navigation.mjs'; import { round2 } from './modules/utils/units.mjs'; import { parseQueryString } from './modules/share.mjs'; import settings from './modules/settings.mjs'; import AutoComplete from './modules/autocomplete.mjs'; import { loadAllData } from './modules/utils/data-loader.mjs'; +import { debugFlag } from './modules/utils/debug.mjs'; document.addEventListener('DOMContentLoaded', () => { init(); @@ -56,7 +57,15 @@ const init = async () => { document.querySelector('#NavigatePrevious').addEventListener('click', btnNavigatePreviousClick); document.querySelector('#NavigatePlay').addEventListener('click', btnNavigatePlayClick); document.querySelector('#ToggleScanlines').addEventListener('click', btnNavigateToggleScanlines); - document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR).addEventListener('click', btnFullScreenClick); + + // Hide fullscreen button on iOS since it doesn't support true fullscreen + const fullscreenButton = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR); + if (isIOS()) { + fullscreenButton.style.display = 'none'; + } else { + fullscreenButton.addEventListener('click', btnFullScreenClick); + } + const btnGetGps = document.querySelector(BNT_GET_GPS_SELECTOR); btnGetGps.addEventListener('click', btnGetGpsClick); if (!navigator.geolocation) btnGetGps.style.display = 'none'; @@ -64,9 +73,6 @@ const init = async () => { document.querySelector('#divTwc').addEventListener('mousemove', () => { if (document.fullscreenElement) updateFullScreenNavigate(); }); - // local change detection when exiting full screen via ESC key (or other non button click methods) - window.addEventListener('resize', fullScreenResizeCheck); - fullScreenResizeCheck.wasFull = false; document.querySelector('#btnGetLatLng').addEventListener('click', () => autoComplete.directFormSubmit()); @@ -116,9 +122,21 @@ const init = async () => { btnGetGpsClick(); } - // if kiosk mode was set via the query string, also play immediately - settings.kiosk.value = parsedParameters['settings-kiosk-checkbox'] === 'true'; - const play = parsedParameters['settings-kiosk-checkbox'] ?? localStorage.getItem('play'); + // Handle kiosk mode initialization + const urlKioskCheckbox = parsedParameters['settings-kiosk-checkbox']; + + // If kiosk=false is specified, disable kiosk mode and clear any stored value + if (urlKioskCheckbox === 'false') { + settings.kiosk.value = false; + // Clear stored value by using conditional storage with false + settings.kiosk.conditionalStoreToLocalStorage(false, false); + } else if (urlKioskCheckbox === 'true') { + // if kiosk mode was set via the query string, enable it + settings.kiosk.value = true; + } + + // Auto-play logic: also play immediately if kiosk mode is enabled + const play = urlKioskCheckbox ?? localStorage.getItem('play'); if (play === null || play === 'true') postMessage('navButton', 'play'); document.querySelector('#btnClearQuery').addEventListener('click', () => { @@ -195,8 +213,7 @@ const enterFullScreen = async () => { const element = document.querySelector('#divTwc'); // Supports most browsers and their versions. - const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen - || element.mozRequestFullscreen || element.msRequestFullscreen; + const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen || element.mozRequestFullscreen || element.msRequestFullscreen; if (requestMethod) { try { @@ -206,24 +223,27 @@ const enterFullScreen = async () => { allowsInlineMediaPlayback: true, }); - // Allow a moment for fullscreen to engage, then optimize - setTimeout(() => { - resize(); - }, 100); + if (debugFlag('fullscreen')) { + setTimeout(() => { + console.log(`🖥️ Fullscreen engaged. window=${window.innerWidth}x${window.innerHeight} fullscreenElement=${!!document.fullscreenElement}`); + }, 150); + } } catch (error) { console.error('❌ Fullscreen request failed:', error); } } else { // iOS doesn't support FullScreen API. window.scrollTo(0, 0); + resize(true); // Force resize for iOS } - resize(); updateFullScreenNavigate(); // change hover text and image const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR); - img.src = 'images/nav/ic_fullscreen_exit_white_24dp_2x.png'; - img.title = 'Exit fullscreen'; + if (img && img.style.display !== 'none') { + img.src = 'images/nav/ic_fullscreen_exit_white_24dp_2x.png'; + img.title = 'Exit fullscreen'; + } }; const exitFullscreen = () => { @@ -239,15 +259,17 @@ const exitFullscreen = () => { } else if (document.msExitFullscreen) { document.msExitFullscreen(); } - resize(); + // Note: resize will be called by fullscreenchange event listener exitFullScreenVisibilityChanges(); }; const exitFullScreenVisibilityChanges = () => { // change hover text and image const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR); - img.src = 'images/nav/ic_fullscreen_white_24dp_2x.png'; - img.title = 'Enter fullscreen'; + if (img && img.style.display !== 'none') { + img.src = 'images/nav/ic_fullscreen_white_24dp_2x.png'; + img.title = 'Enter fullscreen'; + } document.querySelector('#divTwc').classList.remove('no-cursor'); const divTwcBottom = document.querySelector('#divTwcBottom'); divTwcBottom.classList.remove('hidden'); @@ -429,21 +451,6 @@ const getForecastFromLatLon = (latitude, longitude, fromGps = false) => { }); }; -// check for change in full screen triggered by browser and run local functions -const fullScreenResizeCheck = () => { - if (fullScreenResizeCheck.wasFull && !document.fullscreenElement) { - // leaving full screen - exitFullScreenVisibilityChanges(); - } - if (!fullScreenResizeCheck.wasFull && document.fullscreenElement) { - // entering full screen - // can't do much here because a UI interaction is required to change the full screen div element - } - - // store state of fullscreen element for next change detection - fullScreenResizeCheck.wasFull = !!document.fullscreenElement; -}; - const getCustomCode = async () => { // fetch the custom file and see if it returns a 200 status const response = await fetch('scripts/custom.js', { method: 'HEAD' }); diff --git a/server/scripts/modules/almanac.mjs b/server/scripts/modules/almanac.mjs index 2b75fa9..7d9190f 100644 --- a/server/scripts/modules/almanac.mjs +++ b/server/scripts/modules/almanac.mjs @@ -113,17 +113,28 @@ class Almanac extends WeatherDisplay { async drawCanvas() { super.drawCanvas(); const info = this.data; + + // Generate sun data grid in reading order (left-to-right, top-to-bottom) + + // Set day names const Today = DateTime.local(); const Tomorrow = Today.plus({ days: 1 }); + this.elem.querySelector('.day-1').textContent = Today.toLocaleString({ weekday: 'long' }); + this.elem.querySelector('.day-2').textContent = Tomorrow.toLocaleString({ weekday: 'long' }); - // sun and moon data - this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' }); - this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' }); - this.elem.querySelector('.rise-1').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[0].sunrise)); - this.elem.querySelector('.rise-2').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[1].sunrise)); - this.elem.querySelector('.set-1').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[0].sunset)); - this.elem.querySelector('.set-2').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[1].sunset)); + const todaySunrise = DateTime.fromJSDate(info.sun[0].sunrise); + const todaySunset = DateTime.fromJSDate(info.sun[0].sunset); + const [todaySunriseFormatted, todaySunsetFormatted] = formatTimesForColumn([todaySunrise, todaySunset]); + this.elem.querySelector('.rise-1').textContent = todaySunriseFormatted; + this.elem.querySelector('.set-1').textContent = todaySunsetFormatted; + const tomorrowSunrise = DateTime.fromJSDate(info.sun[1].sunrise); + const tomorrowSunset = DateTime.fromJSDate(info.sun[1].sunset); + const [tomorrowSunriseFormatted, tomorrowSunsetformatted] = formatTimesForColumn([tomorrowSunrise, tomorrowSunset]); + this.elem.querySelector('.rise-2').textContent = tomorrowSunriseFormatted; + this.elem.querySelector('.set-2').textContent = tomorrowSunsetformatted; + + // Moon data const days = info.moon.map((MoonPhase) => { const fill = {}; @@ -168,7 +179,20 @@ const imageName = (type) => { } }; -const timeFormat = (dt) => dt.setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(); +const formatTimesForColumn = (times) => { + const formatted = times.map((dt) => dt.setZone(timeZone()).toFormat('h:mm a').toUpperCase()); + + // Check if any time has a 2-digit hour (starts with '1') + const hasTwoDigitHour = formatted.some((time) => time.startsWith('1')); + + // If mixed digit lengths, pad single-digit hours with non-breaking space + if (hasTwoDigitHour) { + return formatted.map((time) => (time.startsWith('1') ? time : `\u00A0${time}`)); + } + + // Otherwise, no padding needed + return formatted; +}; // register display const display = new Almanac(9, 'almanac'); diff --git a/server/scripts/modules/navigation.mjs b/server/scripts/modules/navigation.mjs index 20e26c2..39016b4 100644 --- a/server/scripts/modules/navigation.mjs +++ b/server/scripts/modules/navigation.mjs @@ -4,6 +4,7 @@ import STATUS from './status.mjs'; import { wrap } from './utils/calc.mjs'; import { safeJson } from './utils/fetch.mjs'; import { getPoint } from './utils/weather.mjs'; +import { debugFlag } from './utils/debug.mjs'; import settings from './settings.mjs'; document.addEventListener('DOMContentLoaded', () => { @@ -16,8 +17,36 @@ let progress; const weatherParameters = {}; const init = async () => { - // set up resize handler - window.addEventListener('resize', resize); + // set up the resize handler with debounce logic to prevent rapid-fire calls + let resizeTimeout; + + // Handle fullscreen change events and trigger an immediate resize calculation + const fullscreenEvents = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']; + fullscreenEvents.forEach((eventName) => { + document.addEventListener(eventName, () => { + if (debugFlag('fullscreen')) { + console.log(`🖥️ ${eventName} event fired. fullscreenElement=${!!document.fullscreenElement}`); + } + resize(true); + }); + }); + + // De-bounced resize handler to prevent rapid-fire resize calls + window.addEventListener('resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => resize(), 100); + }); + + // Handle orientation changes (Mobile Safari doesn't always fire resize events on orientation change) + window.addEventListener('orientationchange', () => { + if (debugFlag('resize')) { + console.log('📱 Orientation change detected, forcing resize after short delay'); + } + clearTimeout(resizeTimeout); + // Use a slightly longer delay for orientation changes to allow the browser to settle + resizeTimeout = setTimeout(() => resize(true), 200); + }); + resize(); generateCheckboxes(); @@ -114,7 +143,7 @@ const updateStatus = (value) => { if (progress) progress.drawCanvas(displays, countLoadedDisplays()); // first display is hazards and it must load before evaluating the first display - if (displays[0].status === STATUS.loading) return; + if (!displays[0] || displays[0].status === STATUS.loading) return; // calculate first enabled display const firstDisplayIndex = displays.findIndex((display) => display?.enabled && display?.timing?.totalScreens > 0); @@ -127,7 +156,7 @@ const updateStatus = (value) => { } // if hazards data arrives after the firstDisplayIndex loads, then we need to hot wire this to the first display - if (value.id === 0 && value.status === STATUS.loaded && displays[0].timing.totalScreens === 0) { + if (value.id === 0 && value.status === STATUS.loaded && displays[0] && displays[0].timing && displays[0].timing.totalScreens === 0) { value.id = firstDisplayIndex; value.status = displays[firstDisplayIndex].status; } @@ -185,7 +214,13 @@ const navTo = (direction) => { let firstDisplay; let displayCount = 0; do { - if (displays[displayCount].status === STATUS.loaded && displays[displayCount].timing.totalScreens > 0) firstDisplay = displays[displayCount]; + // Check if displayCount is within bounds and the display exists + if (displayCount < displays.length && displays[displayCount]) { + const display = displays[displayCount]; + if (display.status === STATUS.loaded && display.timing?.totalScreens > 0) { + firstDisplay = display; + } + } displayCount += 1; } while (!firstDisplay && displayCount < displays.length); @@ -215,15 +250,18 @@ const loadDisplay = (direction) => { // convert form simple 0-10 to start at current display index +/-1 and wrap idx = wrap(curIdx + (i + 1) * direction, totalDisplays); if (displays[idx].status === STATUS.loaded && displays[idx].timing.totalScreens > 0) { - // Prevent infinite recursion by ensuring we don't select the same display - if (idx !== curIdx) { - foundSuitableDisplay = true; - break; - } + foundSuitableDisplay = true; + break; } } - // if no suitable display was found, do NOT proceed to avoid infinite recursion + // If no other suitable display was found, but current display is still suitable (e.g. user only enabled one display), stay on it + if (!foundSuitableDisplay && displays[curIdx] && displays[curIdx].status === STATUS.loaded && displays[curIdx].timing.totalScreens > 0) { + idx = curIdx; + foundSuitableDisplay = true; + } + + // if no suitable display was found at all, do NOT proceed to avoid infinite recursion if (!foundSuitableDisplay) { console.warn('No suitable display found for navigation'); return; @@ -305,27 +343,222 @@ const handleNavButton = (button) => { // return the specificed display const getDisplay = (index) => displays[index]; +// Helper function to detect iOS (using technique from nosleep.js) +const isIOS = () => { + const { userAgent } = navigator; + const iOSRegex = /CPU.*OS ([0-9_]{1,})[0-9_]{0,}|(CPU like).*AppleWebKit.*Mobile/i; + return iOSRegex.test(userAgent) && !window.MSStream; +}; + +// Track the last applied scale to avoid redundant operations +let lastAppliedScale = null; +let lastAppliedKioskMode = null; + // resize the container on a page resize -const resize = () => { - // Check for display optimization opportunities before applying zoom - const displayInfo = getDisplayInfo(); - - const targetWidth = settings.wide.value ? 640 + 107 + 107 : 640; - const widthZoomPercent = (document.querySelector('#divTwcBottom').getBoundingClientRect().width) / targetWidth; - const heightZoomPercent = (window.innerHeight) / 480; - - const scale = Math.min(widthZoomPercent, heightZoomPercent); - const { isKioskLike } = displayInfo; - - if (scale < 1.0 || isKioskLike) { - document.querySelector('#container').style.zoom = scale; - // Apply scanline scaling for low-resolution displays and kiosk mode - applyScanlineScaling(scale); - } else { - document.querySelector('#container').style.zoom = 'unset'; - // Reset scanline scaling - applyScanlineScaling(1.0); +const resize = (force = false) => { + // Ignore resize events caused by pinch-to-zoom on mobile + if (window.visualViewport && Math.abs(window.visualViewport.scale - 1) > 0.01) { + return; } + + const isFullscreen = !!document.fullscreenElement; + const isKioskMode = settings.kiosk?.value || false; + const isMobileSafariKiosk = isIOS() && isKioskMode; // Detect Mobile Safari in kiosk mode (regardless of standalone status) + const targetWidth = settings.wide.value ? 640 + 107 + 107 : 640; + + // Use window width instead of bottom container width to avoid zero-dimension issues + const widthZoomPercent = window.innerWidth / targetWidth; + const heightZoomPercent = window.innerHeight / 480; + + // Standard scaling: fit within both dimensions + const scale = Math.min(widthZoomPercent, heightZoomPercent); + + // For Mobile Safari in kiosk mode, always use centering behavior regardless of scale + // For other platforms, only use fullscreen/centering behavior for actual fullscreen or kiosk mode where content fits naturally + const isKioskLike = isFullscreen || (isKioskMode && scale >= 1.0) || isMobileSafariKiosk; + + if (debugFlag('resize') || debugFlag('fullscreen')) { + console.log(`🖥️ Resize: force=${force} isKioskLike=${isKioskLike} window=${window.innerWidth}x${window.innerHeight} targetWidth=${targetWidth} widthZoom=${widthZoomPercent.toFixed(3)} heightZoom=${heightZoomPercent.toFixed(3)} finalScale=${scale.toFixed(3)} fullscreenElement=${!!document.fullscreenElement} isIOS=${isIOS()} standalone=${window.navigator.standalone} isMobileSafariKiosk=${isMobileSafariKiosk} kioskMode=${settings.kiosk?.value} wideMode=${settings.wide.value}`); + } + + // Prevent zero or negative scale values + if (scale <= 0) { + console.warn('Invalid scale calculated, skipping resize'); + return; + } + + // Skip redundant resize operations if scale and mode haven't changed (unless forced) + const scaleChanged = Math.abs((lastAppliedScale || 0) - scale) > 0.001; + const modeChanged = lastAppliedKioskMode !== isKioskLike; + + if (!force && !scaleChanged && !modeChanged) { + return; // No meaningful change, skip resize operation + } + + // Update tracking variables + lastAppliedScale = scale; + lastAppliedKioskMode = isKioskLike; + window.currentScale = scale; // Make scale available to settings module + + const wrapper = document.querySelector('#divTwc'); + const mainContainer = document.querySelector('#divTwcMain'); + + // BASELINE: content fits naturally, no scaling needed + if (!isKioskLike && scale >= 1.0 && !isKioskMode) { + if (debugFlag('fullscreen')) { + console.log('🖥️ Resetting fullscreen/kiosk styles to normal'); + } + + // Reset wrapper styles (only properties that are actually set in fullscreen/scaling modes) + wrapper.style.removeProperty('width'); + wrapper.style.removeProperty('height'); + wrapper.style.removeProperty('overflow'); + wrapper.style.removeProperty('transform'); + wrapper.style.removeProperty('transform-origin'); + + // Reset container styles that might have been applied during fullscreen + mainContainer.style.removeProperty('transform'); + mainContainer.style.removeProperty('transform-origin'); + mainContainer.style.removeProperty('width'); + mainContainer.style.removeProperty('height'); + mainContainer.style.removeProperty('position'); + mainContainer.style.removeProperty('left'); + mainContainer.style.removeProperty('top'); + mainContainer.style.removeProperty('margin-left'); + mainContainer.style.removeProperty('margin-top'); + + applyScanlineScaling(1.0); + return; + } + + // MOBILE SCALING: Use wrapper scaling for mobile devices (but not Mobile Safari kiosk mode) + if ((scale < 1.0 || (isKioskMode && !isKioskLike)) && !isMobileSafariKiosk) { + /* + * MOBILE SCALING (Wrapper Scaling) + * + * Why scale the wrapper instead of mainContainer? + * - For mobile devices where content is larger than viewport, we need to scale the entire layout + * - The wrapper (#divTwc) contains both the main content AND the bottom navigation bar + * - Scaling the wrapper ensures both elements are scaled together as a unit + * - No centering is applied - content aligns to top-left for typical mobile behavior + * - Uses explicit dimensions to prevent layout issues and eliminate gaps after scaling + */ + wrapper.style.setProperty('transform', `scale(${scale})`); + wrapper.style.setProperty('transform-origin', 'top left'); // Scale from top-left corner + + // Set explicit dimensions to prevent layout issues on mobile + const wrapperWidth = settings.wide.value ? 854 : 640; + // Calculate total height: main content (480px) + bottom navigation bar + const bottomBar = document.querySelector('#divTwcBottom'); + const bottomBarHeight = bottomBar ? bottomBar.offsetHeight : 40; // fallback to ~40px + const totalHeight = 480 + bottomBarHeight; + const scaledHeight = totalHeight * scale; // Height after scaling + + wrapper.style.setProperty('width', `${wrapperWidth}px`); + wrapper.style.setProperty('height', `${scaledHeight}px`); // Use scaled height to eliminate gap + applyScanlineScaling(scale); + return; + } + + // KIOSK/FULLSCREEN SCALING: Two different positioning approaches for different platforms + const wrapperWidth = settings.wide.value ? 854 : 640; + const wrapperHeight = 480; + + // Reset wrapper styles to avoid double scaling (wrapper remains unstyled) + wrapper.style.removeProperty('width'); + wrapper.style.removeProperty('height'); + wrapper.style.removeProperty('transform'); + wrapper.style.removeProperty('transform-origin'); + + // Platform-specific positioning logic + let transformOrigin; + let leftPosition; + let topPosition; + let marginLeft; + let marginTop; + + if (isMobileSafariKiosk) { + /* + * MOBILE SAFARI KIOSK MODE (Manual offset calculation) + * + * Why this approach? + * - Mobile Safari in kiosk mode has unique viewport behaviors that don't work well with standard CSS centering + * - We want orientation-specific centering: vertical in portrait, horizontal in landscape + * - The standard CSS centering method can cause layout issues in Mobile Safari's constrained environment + */ + const scaledWidth = wrapperWidth * scale; + const scaledHeight = wrapperHeight * scale; + + // Determine if we're in portrait or landscape + const isPortrait = window.innerHeight > window.innerWidth; + + let offsetX = 0; + let offsetY = 0; + + if (isPortrait) { + offsetY = (window.innerHeight - scaledHeight) / 2; // center vertically, align to left edge + } else { + offsetX = (window.innerWidth - scaledWidth) / 2; // center horizontally, align to top edge + } + + if (debugFlag('fullscreen')) { + console.log(`📱 Mobile Safari kiosk centering: ${isPortrait ? 'portrait' : 'landscape'} wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)}`); + } + + // Set positioning values for manual offset calculation + transformOrigin = 'top left'; // Scale from top-left corner + leftPosition = `${offsetX}px`; // Exact pixel positioning + topPosition = `${offsetY}px`; // Exact pixel positioning + marginLeft = null; // Clear any previous centering margins + marginTop = null; // Clear any previous centering margins + } else { + /* + * STANDARD FULLSCREEN/KIOSK MODE (CSS-based Centering) + * + * Why this approach? + * - Should work reliably across all other browsers and scenarios (desktop, non-Safari mobile, etc.) + * - Uses standard CSS centering techniques that browsers handle efficiently + * - Always centers both horizontally and vertically + */ + const scaledWidth = wrapperWidth * scale; + const scaledHeight = wrapperHeight * scale; + const offsetX = (window.innerWidth - scaledWidth) / 2; + const offsetY = (window.innerHeight - scaledHeight) / 2; + + if (debugFlag('fullscreen')) { + console.log(`🖥️ Applying fullscreen/kiosk scaling: wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)} transform: scale(${scale}) translate(${offsetX / scale}px, ${offsetY / scale}px)`); + } + + // Set positioning values for CSS-based centering + transformOrigin = 'center center'; // Scale from center point + leftPosition = '50%'; // Position at 50% from left + topPosition = '50%'; // Position at 50% from top + marginLeft = `-${wrapperWidth / 2}px`; // Pull back by half width + marginTop = `-${wrapperHeight / 2}px`; // Pull back by half height + } + + // Apply shared mainContainer properties (same for both kiosk modes) + mainContainer.style.setProperty('transform', `scale(${scale})`, 'important'); + mainContainer.style.setProperty('transform-origin', transformOrigin, 'important'); + mainContainer.style.setProperty('width', `${wrapperWidth}px`, 'important'); + mainContainer.style.setProperty('height', `${wrapperHeight}px`, 'important'); + mainContainer.style.setProperty('position', 'absolute', 'important'); + mainContainer.style.setProperty('left', leftPosition, 'important'); + mainContainer.style.setProperty('top', topPosition, 'important'); + + // Apply or clear margin properties based on positioning method + if (marginLeft !== null) { + mainContainer.style.setProperty('margin-left', marginLeft, 'important'); + } else { + mainContainer.style.removeProperty('margin-left'); + } + if (marginTop !== null) { + mainContainer.style.setProperty('margin-top', marginTop, 'important'); + } else { + mainContainer.style.removeProperty('margin-top'); + } + + applyScanlineScaling(scale); }; // reset all statuses to loading on all displays, used to keep the progress bar accurate during refresh @@ -333,677 +566,163 @@ const resetStatuses = () => { displays.forEach((display) => { display.status = STATUS.loading; }); }; -// Enhanced kiosk detection with automatic fullscreen optimization -const getDisplayInfo = () => { - const isKiosk = settings.kiosk?.value || false; - const isFullscreen = !!document.fullscreenElement; - const isKioskLike = isKiosk || isFullscreen || (window.innerHeight >= window.screen.height - 10); - - return { isKiosk, isFullscreen, isKioskLike }; -}; - -// Make function globally available for debugging -window.getDisplayInfo = getDisplayInfo; - -// Apply dynamic scanline scaling based on zoom level -const applyScanlineScaling = (zoomScale) => { - // Only apply if scanlines are enabled +// Apply scanline scaling to try and prevent banding by avoiding fractional scaling +const applyScanlineScaling = (scale) => { const container = document.querySelector('#container'); if (!container || !container.classList.contains('scanlines')) { return; } - // Get display and viewport information - const displayWidth = window.screen.width; - const displayHeight = window.screen.height; - const devicePixelRatio = window.devicePixelRatio || 1; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; - const isFullscreen = !!document.fullscreenElement; - const isKiosk = settings.kiosk?.value || false; - const isKioskLike = isKiosk || isFullscreen || (window.innerHeight >= window.screen.height - 10); - - // Check for sub-pixel rendering issues - const effectiveScanlineHeight = 1 * zoomScale * devicePixelRatio; - const willCauseAliasing = effectiveScanlineHeight < 1.0 || (effectiveScanlineHeight % 1 !== 0); - - // Calculate optimal scanline thickness - let scanlineScale = 1; - let scalingReason = 'default'; - - // Primary strategy: Ensure scanlines render as whole pixels - if (willCauseAliasing) { - if (zoomScale > 1.0) { - // Upscaling scenario (like 1024x768 → 1.6x zoom) - const targetThickness = Math.ceil(1 / zoomScale); - scanlineScale = Math.max(1, targetThickness); - scalingReason = 'upscaling aliasing prevention'; - } else { - // Downscaling scenario - scanlineScale = Math.ceil(1 / zoomScale); - scalingReason = 'downscaling aliasing prevention'; - } - } - - // Specific display-based adjustments - if (displayWidth <= 1024 && displayHeight <= 768 && devicePixelRatio < 2) { - if (zoomScale > 1.4) { - scanlineScale = Math.max(scanlineScale, Math.round(1 / zoomScale * 2)); - scalingReason = '1024x768 high upscaling compensation'; - } else { - scanlineScale = Math.max(scanlineScale, 1); - scalingReason = '1024x768 display optimization'; - } - } - - // Override for kiosk/fullscreen mode with specific viewport dimensions - if (isKioskLike && ( - Math.abs(zoomScale - 1.598) < 0.05 // More flexible zoom detection for 1024x768 scenarios - || (viewportWidth === 1023 && viewportHeight === 767) // Exact Chrome kiosk viewport - || (viewportWidth === 1024 && viewportHeight === 768) // Perfect viewport - )) { - // Kiosk mode optimization for 1024x768 displays - // Use optimal scanlines that render as exactly 2px with no banding - if (viewportWidth === 1023 && viewportHeight === 767) { - // For the exact 1023x767 Chrome kiosk viewport - // Calculate precise thickness for exactly 2px rendering - const targetRendered = 2.0; - scanlineScale = targetRendered / zoomScale; // This gives us exactly 2px - scalingReason = 'Chrome kiosk 1023x767 - optimal 2px scanlines'; - } else { - // For 1024x768 or similar zoomed scenarios - scanlineScale = 1.25; // Standard 2px optimization - scalingReason = 'Kiosk/fullscreen 1024x768 - optimal 2px scanlines'; - } - } - - // Calculate precise thickness to avoid sub-pixel rendering - let preciseThickness = scanlineScale; - let backgroundSize = scanlineScale * 2; - - // For upscaling scenarios, try to make the final rendered size a whole number - // BUT skip this if we already have a specific override for the zoom level - if (zoomScale > 1.0 && willCauseAliasing && !scalingReason.includes('optimal') && !scalingReason.includes('Kiosk')) { - const targetRenderedHeight = Math.round(effectiveScanlineHeight); - preciseThickness = targetRenderedHeight / zoomScale / devicePixelRatio; - backgroundSize = preciseThickness * 2; - } - - // Apply dynamic styles with fractional pixel compensation - let styleElement = document.getElementById('dynamic-scanlines'); - if (!styleElement) { - styleElement = document.createElement('style'); - styleElement.id = 'dynamic-scanlines'; - document.head.appendChild(styleElement); - } - - const cssRules = ` - .scanlines:before { - height: ${preciseThickness}px !important; - image-rendering: pixelated !important; - image-rendering: crisp-edges !important; - } - .scanlines:after { - background-size: 100% ${backgroundSize}px !important; - image-rendering: pixelated !important; - image-rendering: crisp-edges !important; - } - `; - - styleElement.textContent = cssRules; - - // Only log when optimal kiosk mode is applied (minimize debug output) - if (scalingReason.includes('optimal') && !window.scanlineLoggedOnce) { - console.log(`Scanlines: ${preciseThickness}px (${scalingReason})`); - window.scanlineLoggedOnce = true; - } -}; - -// Debug function for scanlines -// All these can be called from browser console. -// Leaving them here for now, but they can potentially be removed later. -// Function to request perfect fullscreen for optimal display -const requestPerfectFullscreen = async () => { - const element = document.querySelector('#divTwc'); - - try { - // Use the Fullscreen API to get perfect viewport control - const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen - || element.mozRequestFullScreen || element.msRequestFullscreen; - - if (requestMethod) { - // Request fullscreen with minimal logging - await requestMethod.call(element, { - navigationUI: 'hide', - // Request specific fullscreen options if supported - allowsInlineMediaPlayback: true, - }); - - // Allow a moment for fullscreen to engage - setTimeout(() => { - // Re-trigger resize to apply optimal scaling - resize(); - - // Apply scanline scaling based on new dimensions - const container = document.querySelector('#container'); - const zoomScale = parseFloat(container.style.zoom) || 1; - applyScanlineScaling(zoomScale); - }, 100); - - return true; - } - console.warn('Fullscreen API not supported'); - return false; - } catch (error) { - console.error('Failed to request fullscreen:', error); - return false; - } -}; - -// Make function globally available for debugging -window.requestPerfectFullscreen = requestPerfectFullscreen; - -const debugScanlines = () => { - console.group('Manual Scanlines Debug'); - - const container = document.querySelector('#container'); - if (!container) { - console.error('Container element not found'); - console.groupEnd(); - return { error: 'Container element not found' }; - } - - const hasScanlinesClass = container.classList.contains('scanlines'); - const containerRect = container.getBoundingClientRect(); - const currentZoom = parseFloat(container.style.zoom) || 1; - - console.log(`Scanlines class present: ${hasScanlinesClass}`); - console.log(`Container dimensions: ${containerRect.width.toFixed(2)}x${containerRect.height.toFixed(2)}`); - console.log(`Current zoom: ${currentZoom}`); - - const debugInfo = { - hasScanlinesClass, - containerDimensions: { - width: containerRect.width, - height: containerRect.height, - left: containerRect.left, - top: containerRect.top, - }, - currentZoom, - viewport: { - width: window.innerWidth, - height: window.innerHeight, - }, - screen: { - width: window.screen.width, - height: window.screen.height, - }, - devicePixelRatio: window.devicePixelRatio || 1, - isFullscreen: !!document.fullscreenElement, - }; - - if (hasScanlinesClass) { - console.log(`Triggering applyScanlineScaling with zoom: ${currentZoom}`); - applyScanlineScaling(currentZoom); - - // Check if dynamic styles exist - const dynamicStyle = document.getElementById('dynamic-scanlines'); - if (dynamicStyle) { - console.log('Current dynamic CSS:', dynamicStyle.textContent); - debugInfo.dynamicCSS = dynamicStyle.textContent; - } else { - console.log('No dynamic scanlines styles found'); - debugInfo.dynamicCSS = null; - } - - // Get computed styles for scanlines - const beforeStyle = window.getComputedStyle(container, ':before'); - const afterStyle = window.getComputedStyle(container, ':after'); - - const computedStyles = { - before: { - height: beforeStyle.height, - background: beforeStyle.background, - opacity: beforeStyle.opacity, - imageRendering: beforeStyle.imageRendering, - }, - after: { - backgroundSize: afterStyle.backgroundSize, - backgroundImage: afterStyle.backgroundImage, - opacity: afterStyle.opacity, - imageRendering: afterStyle.imageRendering, - }, - }; - - console.log('Computed :before styles:'); - console.log(' height:', computedStyles.before.height); - console.log(' background:', computedStyles.before.background); - console.log(' opacity:', computedStyles.before.opacity); - console.log(' image-rendering:', computedStyles.before.imageRendering); - - console.log('Computed :after styles:'); - console.log(' background-size:', computedStyles.after.backgroundSize); - console.log(' background-image:', computedStyles.after.backgroundImage); - console.log(' opacity:', computedStyles.after.opacity); - console.log(' image-rendering:', computedStyles.after.imageRendering); - - debugInfo.computedStyles = computedStyles; - } - - console.groupEnd(); - return debugInfo; -}; - -// Make debug function globally available -window.debugScanlines = debugScanlines; - -// Test function to manually set scanline scale - can be called from browser console -const testScanlineScale = (scale) => { - console.log(`Testing scanline scale: ${scale}x`); - - let styleElement = document.getElementById('dynamic-scanlines'); - if (!styleElement) { - styleElement = document.createElement('style'); - styleElement.id = 'dynamic-scanlines'; - document.head.appendChild(styleElement); - } - - const cssRules = ` - .scanlines:before { - height: ${scale}px !important; - image-rendering: pixelated !important; - image-rendering: crisp-edges !important; - } - .scanlines:after { - background-size: 100% ${scale * 2}px !important; - image-rendering: pixelated !important; - image-rendering: crisp-edges !important; - } - `; - - styleElement.textContent = cssRules; - - // Calculate what this will look like when rendered - const container = document.querySelector('#container'); - const zoom = parseFloat(container?.style.zoom) || 1; - const expectedRendered = scale * zoom; - const isWholePixel = Math.abs(expectedRendered % 1) < 0.01; - - const result = { - appliedScale: scale, - backgroundSize: scale * 2, - currentZoom: zoom, - expectedRendered, - isWholePixel, - cssRules: cssRules.trim(), - }; - - console.log(`Applied ${scale}px scanline height with ${scale * 2}px background-size`); - console.log(`Expected rendered height: ${expectedRendered.toFixed(4)}px`); - console.log(`Will render as whole pixels: ${isWholePixel}`); - - return result; -}; - -// Make test function globally available -window.testScanlineScale = testScanlineScale; - -// Test function for precise fractional values to eliminate banding -const testPreciseScanlines = () => { - const container = document.querySelector('#container'); - const zoom = parseFloat(container?.style.zoom) || 1; - - console.group('Testing Precise Scanline Values'); - console.log(`Current zoom: ${zoom.toFixed(4)}`); - - // Test values that should result in whole pixel rendering - const testValues = [ - 0.625, // Should render as 1px (0.625 * 1.598 ≈ 1.0) - 1.25, // Should render as 2px (1.25 * 1.598 ≈ 2.0) - 1.875, // Should render as 3px (1.875 * 1.598 ≈ 3.0) - 2.5, // Should render as 4px (2.5 * 1.598 ≈ 4.0) - ]; - - const results = testValues.map((value) => { - const rendered = value * zoom; - const isWholePixel = Math.abs(rendered % 1) < 0.01; - const result = { - inputValue: value, - renderedValue: rendered, - isWholePixel, - fractionalPart: rendered % 1, - }; - console.log(`Test ${value}px → ${rendered.toFixed(4)}px rendered (${isWholePixel ? '✅ whole' : '❌ fractional'})`); - return result; - }); - - console.log('Use testScanlineScale(value) to try these values'); - console.groupEnd(); - - return { - currentZoom: zoom, - testResults: results, - recommendation: 'Use testScanlineScale(value) to apply a specific value', - }; -}; - -// Make precise test function globally available -window.testPreciseScanlines = testPreciseScanlines; - -// Function to analyze container dimension issues -const analyzeContainerDimensions = () => { - const container = document.querySelector('#container'); - if (!container) { - return { error: 'Container not found' }; - } - - const containerRect = container.getBoundingClientRect(); - const containerStyle = window.getComputedStyle(container); - const { parentElement } = container; - const parentRect = parentElement ? parentElement.getBoundingClientRect() : null; - const parentStyle = parentElement ? window.getComputedStyle(parentElement) : null; - - const analysis = { - container: { - rect: { - width: containerRect.width, - height: containerRect.height, - left: containerRect.left, - top: containerRect.top, - }, - computedStyle: { - width: containerStyle.width, - height: containerStyle.height, - padding: containerStyle.padding, - margin: containerStyle.margin, - border: containerStyle.border, - boxSizing: containerStyle.boxSizing, - zoom: containerStyle.zoom, - transform: containerStyle.transform, - }, - }, - parent: parentRect ? { - rect: { - width: parentRect.width, - height: parentRect.height, - left: parentRect.left, - top: parentRect.top, - }, - computedStyle: { - width: parentStyle.width, - height: parentStyle.height, - padding: parentStyle.padding, - margin: parentStyle.margin, - border: parentStyle.border, - boxSizing: parentStyle.boxSizing, - }, - } : null, - viewport: { - width: window.innerWidth, - height: window.innerHeight, - }, - screen: { - width: window.screen.width, - height: window.screen.height, - }, - devicePixelRatio: window.devicePixelRatio || 1, - isFullscreen: !!document.fullscreenElement, - }; - - console.group('Container Dimension Analysis'); - console.log('Container Rect:', analysis.container.rect); - console.log('Container Computed Style:', analysis.container.computedStyle); - if (analysis.parent) { - console.log('Parent Rect:', analysis.parent.rect); - console.log('Parent Computed Style:', analysis.parent.computedStyle); - } - console.log('Viewport:', analysis.viewport); - console.log('Screen:', analysis.screen); - - // Check for fractional dimension causes - const expectedTargetWidth = 640; // Base width - const expectedTargetHeight = 480; // Base height - const actualScale = Math.min(analysis.viewport.width / expectedTargetWidth, analysis.viewport.height / expectedTargetHeight); - const fractionalWidth = analysis.container.rect.width % 1; - const fractionalHeight = analysis.container.rect.height % 1; - - console.log(`Expected scale: ${actualScale.toFixed(4)}`); - console.log(`Fractional width: ${fractionalWidth.toFixed(4)}px`); - console.log(`Fractional height: ${fractionalHeight.toFixed(4)}px`); - console.log(`Width is fractional: ${fractionalWidth > 0.01}`); - console.log(`Height is fractional: ${fractionalHeight > 0.01}`); - - analysis.scaling = { - expectedScale: actualScale, - fractionalWidth, - fractionalHeight, - hasFractionalDimensions: fractionalWidth > 0.01 || fractionalHeight > 0.01, - }; - - console.groupEnd(); - return analysis; -}; - -// Make container analysis function globally available -window.analyzeContainerDimensions = analyzeContainerDimensions; - -// Function to calculate optimal scanline thickness that eliminates fractional rendering -const calculateOptimalScanlineThickness = (targetZoom = null) => { - const container = document.querySelector('#container'); - if (!container) { - return { error: 'Container not found' }; - } - - const currentZoom = targetZoom || parseFloat(container.style.zoom) || 1; const devicePixelRatio = window.devicePixelRatio || 1; + const currentMode = settings?.scanLineMode?.value || 'auto'; + let cssThickness; + let scanlineDebugInfo = null; - console.group('Calculating Optimal Scanline Thickness'); - console.log(`Current zoom: ${currentZoom.toFixed(4)}`); - console.log(`Device pixel ratio: ${devicePixelRatio}`); - - // Calculate possible thickness values that result in whole pixel rendering - const candidates = []; - - // Test thickness values from 0.1 to 3.0 in 0.001 increments - for (let thickness = 0.1; thickness <= 3.0; thickness += 0.001) { - const renderedHeight = thickness * currentZoom * devicePixelRatio; - const fractionalPart = renderedHeight % 1; - - // If the rendered height is very close to a whole number - if (fractionalPart < 0.001 || fractionalPart > 0.999) { - const wholePixelHeight = Math.round(renderedHeight); - candidates.push({ - thickness: Math.round(thickness * 1000) / 1000, // Round to 3 decimal places - renderedHeight: wholePixelHeight, - actualRendered: renderedHeight, - error: Math.abs(renderedHeight - wholePixelHeight), - }); + // Helper function to round CSS values intelligently based on scale and DPR + // At high scales, precise fractional pixels render fine; at low scales, alignment matters more + const roundCSSValue = (value) => { + // On 1x DPI displays, use exact calculated values + if (devicePixelRatio === 1) { + return value; } - } - // Sort by error (closest to whole pixel) and prefer reasonable thickness values - candidates.sort((a, b) => { - if (Math.abs(a.error - b.error) < 0.0001) { - // If errors are similar, prefer thickness closer to 1 - return Math.abs(a.thickness - 1) - Math.abs(b.thickness - 1); + // At high scales (>2x), the browser scaling dominates and fractional pixels render well + // Prioritize nice fractions for better visual consistency + if (scale > 2.0) { + // Try quarter-pixel boundaries first (0.25, 0.5, 0.75, 1.0, etc.) + const quarterRounded = Math.round(value * 4) / 4; + if (Math.abs(quarterRounded - value) <= 0.125) { // Within 0.125px tolerance + return quarterRounded; + } + // Fall through to half-pixel boundaries for high scale fallback } - return a.error - b.error; - }); - // Take the best candidates for different pixel heights - const recommendations = []; - const seenHeights = new Set(); - - candidates.some((candidate) => { - if (!seenHeights.has(candidate.renderedHeight) && recommendations.length < 5) { - seenHeights.add(candidate.renderedHeight); - recommendations.push(candidate); - } - return recommendations.length >= 5; // Stop when we have 5 recommendations - }); - - console.log('Recommendations:'); - recommendations.forEach((rec, index) => { - console.log(`${index + 1}. ${rec.thickness}px → ${rec.renderedHeight}px (error: ${rec.error.toFixed(6)})`); - }); - - const result = { - currentZoom, - devicePixelRatio, - recommendations, - bestRecommendation: recommendations[0] || null, + // At lower scales (and high scale fallback), pixel alignment matters more for crisp rendering + // Round UP to the next half-pixel to ensure scanlines are never thinner than intended + const halfPixelRounded = Math.ceil(value * 2) / 2; + return halfPixelRounded; }; - if (result.bestRecommendation) { - console.log(`Best recommendation: ${result.bestRecommendation.thickness}px`); - console.log(` Will render as: ${result.bestRecommendation.renderedHeight}px`); - console.log(` Use: testScanlineScale(${result.bestRecommendation.thickness})`); - } - - console.groupEnd(); - return result; -}; - -// Make optimal calculation function globally available -window.calculateOptimalScanlineThickness = calculateOptimalScanlineThickness; - -// Function to analyze viewport and provide fullscreen optimization recommendations -const analyzeViewportOptimization = () => { - const viewport = { - width: window.innerWidth, - height: window.innerHeight, - screen: { - width: window.screen.width, - height: window.screen.height, - }, - devicePixelRatio: window.devicePixelRatio || 1, - isFullscreen: !!document.fullscreenElement, - isKiosk: settings.kiosk?.value || false, - }; - - // Check for fractional viewport dimensions - const hasFractionalViewport = (viewport.width % 1 !== 0) || (viewport.height % 1 !== 0); - - // Check for common kiosk viewport sizes - const isKnownKioskSize = ( - (viewport.width === 1023 && viewport.height === 767) // Common Chrome kiosk issue - || (viewport.width === 1024 && viewport.height === 768) // Perfect kiosk size - ); - - // Minimize debug output for production use - if (window.debugMode) { - console.group('Viewport Optimization Analysis'); - console.log('Current viewport:', `${viewport.width}x${viewport.height}`); - console.log('Screen resolution:', `${viewport.screen.width}x${viewport.screen.height}`); - console.log('Device pixel ratio:', viewport.devicePixelRatio); - console.log('Has fractional viewport:', hasFractionalViewport); - console.log('Is known kiosk size:', isKnownKioskSize); - console.log('Is fullscreen:', viewport.isFullscreen); - console.log('Is kiosk mode:', viewport.isKiosk); - } - - // Kiosk-specific analysis - const recommendations = []; - - if (viewport.isKiosk && isKnownKioskSize) { - if (viewport.width === 1023 && viewport.height === 767) { - recommendations.push('Detected 1023x767 kiosk viewport - using calculated optimal scanlines for perfect 2px rendering'); - } else if (viewport.width === 1024 && viewport.height === 768) { - recommendations.push('Perfect 1024x768 kiosk viewport detected - optimal scanlines will be applied'); - } - } else if (viewport.isKiosk && hasFractionalViewport) { - recommendations.push('Custom kiosk viewport detected - scanlines will be optimized for exact dimensions'); - } - - // Calculate what the zoom scale would be with current dimensions - const targetWidth = settings.wide?.value ? 640 + 107 + 107 : 640; - const targetHeight = 480; - - const currentWidthRatio = viewport.width / targetWidth; - const currentHeightRatio = viewport.height / targetHeight; - const currentScale = Math.min(currentWidthRatio, currentHeightRatio); - - // Calculate scanline rendering for current setup - const currentScanlineHeight = 1 * currentScale * viewport.devicePixelRatio; - const willCauseAliasing = currentScanlineHeight < 1.0 || (currentScanlineHeight % 1 !== 0); - - if (window.debugMode) { - console.log('Scaling Analysis:'); - console.log(` Current scale: ${currentScale.toFixed(6)}`); - console.log(` Base scanline rendering: ${currentScanlineHeight.toFixed(6)}px`); - console.log(` Will cause aliasing: ${willCauseAliasing}`); - - if (viewport.isKiosk && isKnownKioskSize) { - // Calculate what our optimal scanline thickness would be - const targetRendered = 2.0; // We want 2px scanlines - const optimalThickness = targetRendered / (currentScale * viewport.devicePixelRatio); - console.log(`Optimal scanline thickness: ${optimalThickness.toFixed(6)}px`); - console.log(`Expected rendered height: ${(optimalThickness * currentScale * viewport.devicePixelRatio).toFixed(6)}px`); - } - - if (recommendations.length > 0) { - console.log('Kiosk Optimization Status:'); - recommendations.forEach((rec) => console.log(` • ${rec}`)); - } else if (viewport.isKiosk) { - console.log('Custom kiosk configuration - using automatic optimization'); - } else { - console.log('Not in kiosk mode - standard scaling applies'); - } - - console.groupEnd(); - } - - return { - viewport, - hasFractionalViewport, - isKnownKioskSize, - recommendations, - scaling: { - current: currentScale, - scanlineRendering: currentScanlineHeight, - willCauseAliasing, - }, - }; -}; - -// Make function globally available for debugging -window.analyzeViewportOptimization = analyzeViewportOptimization; - -// Function to test fullscreen API capabilities -const testFullscreenCapabilities = () => { - const element = document.querySelector('#divTwc'); - - console.group('Fullscreen API Test'); - - const capabilities = { - requestFullscreen: !!element.requestFullscreen, - webkitRequestFullscreen: !!element.webkitRequestFullscreen, - mozRequestFullScreen: !!element.mozRequestFullScreen, - msRequestFullscreen: !!element.msRequestFullscreen, - fullscreenEnabled: !!document.fullscreenEnabled, - currentlyFullscreen: !!document.fullscreenElement, - }; - - console.log('API Support:', capabilities); - - // Determine the best method - const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen - || element.mozRequestFullScreen || element.msRequestFullscreen; - - if (requestMethod) { - console.log('Fullscreen API available'); - console.log('Can attempt programmatic fullscreen for viewport optimization'); + // Manual modes: use smart rounding in scaled scenarios to avoid banding + if (currentMode === 'thin') { + const rawValue = 1 / scale; + const cssValue = scale === 1.0 ? rawValue : roundCSSValue(rawValue); + cssThickness = `${cssValue}px`; + scanlineDebugInfo = { + css: cssValue, + visual: 1, + target: '1px visual thickness', + reason: scale === 1.0 ? 'Thin: 1px visual user override (exact)' : 'Thin: 1px visual user override (rounded)', + isManual: true, + }; + } else if (currentMode === 'medium') { + const rawValue = 2 / scale; + const cssValue = scale === 1.0 ? rawValue : roundCSSValue(rawValue); + cssThickness = `${cssValue}px`; + scanlineDebugInfo = { + css: cssValue, + visual: 2, + target: '2px visual thickness', + reason: scale === 1.0 ? 'Medium: 2px visual user override (exact)' : 'Medium: 2px visual user override (rounded)', + isManual: true, + }; + } else if (currentMode === 'thick') { + const rawValue = 3 / scale; + const cssValue = scale === 1.0 ? rawValue : roundCSSValue(rawValue); + cssThickness = `${cssValue}px`; + scanlineDebugInfo = { + css: cssValue, + visual: 3, + target: '3px visual thickness', + reason: scale === 1.0 ? 'Thick: 3px visual user override (exact)' : 'Thick: 3px visual user override (rounded)', + isManual: true, + }; } else { - console.log('Fullscreen API not supported'); + // Auto mode: choose thickness based on scaling behavior + + let visualThickness; + let reason; + + if (scale === 1.0) { + // Unscaled mode: use reasonable thickness based on device characteristics + const isHighDPIMobile = devicePixelRatio >= 2 && viewportWidth <= 768 && viewportHeight <= 768; + const isHighDPITablet = devicePixelRatio >= 2 && viewportWidth <= 1024 && viewportHeight <= 1024; + + if (isHighDPIMobile) { + // High-DPI mobile: use thin scanlines but not too thin + const cssValue = roundCSSValue(1.5 / devicePixelRatio); + cssThickness = `${cssValue}px`; + reason = `Auto: ${cssValue}px unscaled (high-DPI mobile, DPR=${devicePixelRatio})`; + } else if (isHighDPITablet) { + // High-DPI tablets: use slightly thicker scanlines for better visibility + const cssValue = roundCSSValue(1.5 / devicePixelRatio); + cssThickness = `${cssValue}px`; + reason = `Auto: ${cssValue}px unscaled (high-DPI tablet, DPR=${devicePixelRatio})`; + } else if (devicePixelRatio >= 2) { + // High-DPI desktop: use scanlines that look similar to scaled mode + const cssValue = roundCSSValue(1.5 / devicePixelRatio); + cssThickness = `${cssValue}px`; + reason = `Auto: ${cssValue}px unscaled (high-DPI desktop, DPR=${devicePixelRatio})`; + } else { + // Standard DPI desktop: use 2px for better visibility + cssThickness = '2px'; + reason = 'Auto: 2px unscaled (standard DPI desktop)'; + } + } else if (scale < 1.0) { + // Mobile scaling: use thinner scanlines for small displays + visualThickness = 1; + const cssValue = roundCSSValue(visualThickness / scale); + cssThickness = `${cssValue}px`; + reason = `Auto: ${cssValue}px scaled (mobile, scale=${scale})`; + } else if (scale >= 3.0) { + // Very high scale (large displays/high DPI): use thick scanlines for visibility + visualThickness = 3; + const cssValue = roundCSSValue(visualThickness / scale); + cssThickness = `${cssValue}px`; + reason = `Auto: ${cssValue}px scaled (large display/high scale, scale=${scale})`; + } else { + // Medium scale kiosk/fullscreen: use medium scanlines with smart rounding + visualThickness = 2; + const rawValue = visualThickness / scale; + const cssValue = roundCSSValue(rawValue); + cssThickness = `${cssValue}px`; + reason = `Auto: ${cssValue}px scaled (kiosk/fullscreen, scale=${scale})`; + + if (debugFlag('scanlines')) { + console.log(`↕️ Kiosk/fullscreen rounding: raw=${rawValue}, rounded=${cssValue}, DPR=${devicePixelRatio}, scale=${scale}`); + } + } + + // Extract numeric value from cssThickness for debug info + const cssNumericValue = parseFloat(cssThickness); + + scanlineDebugInfo = { + css: cssNumericValue, + visual: scale === 1.0 ? cssNumericValue : visualThickness, // For unscaled mode, visual thickness equals CSS thickness + target: scale === 1.0 ? `${cssNumericValue}px CSS (unscaled)` : `${visualThickness}px visual thickness`, + reason, + isManual: false, + }; } - console.groupEnd(); + container.style.setProperty('--scanline-thickness', cssThickness); - return capabilities; + // Output debug information if enabled + if (debugFlag('scanlines')) { + const actualRendered = scanlineDebugInfo.css * scale; + const physicalRendered = actualRendered * devicePixelRatio; + const visualThickness = scanlineDebugInfo.visual || actualRendered; // Use visual thickness if available + + console.log(`↕️ Scanline optimization: ${cssThickness} CSS × ${scale.toFixed(3)} scale = ${actualRendered.toFixed(3)}px rendered (${visualThickness}px visual target) × ${devicePixelRatio}x DPI = ${physicalRendered.toFixed(3)}px physical - ${scanlineDebugInfo.reason}`); + console.log(`↕️ Display: ${viewportWidth}×${viewportHeight}, Scale factors: width=${(window.innerWidth / (settings.wide.value ? 854 : 640)).toFixed(3)}, height=${(window.innerHeight / 480).toFixed(3)}, DPR=${devicePixelRatio}`); + console.log(`↕️ Thickness: CSS=${cssThickness}, Visual=${visualThickness.toFixed(1)}px, Rendered=${actualRendered.toFixed(3)}px, Physical=${physicalRendered.toFixed(3)}px`); + } }; -// Make function globally available for debugging -window.testFullscreenCapabilities = testFullscreenCapabilities; +// Make applyScanlineScaling available for direct calls from Settings +window.applyScanlineScaling = applyScanlineScaling; // allow displays to register themselves const registerDisplay = (display) => { @@ -1059,4 +778,5 @@ export { message, latLonReceived, timeZone, + isIOS, }; diff --git a/server/scripts/modules/settings.mjs b/server/scripts/modules/settings.mjs index 0e38d53..2011f30 100644 --- a/server/scripts/modules/settings.mjs +++ b/server/scripts/modules/settings.mjs @@ -1,12 +1,87 @@ import Setting from './utils/setting.mjs'; -document.addEventListener('DOMContentLoaded', () => { - init(); -}); - -// default speed +// Initialize settings immediately so other modules can access them const settings = { speed: { value: 1.0 } }; +// Declare change functions first, before they're referenced in init() to avoid the Temporal Dead Zone (TDZ) +const wideScreenChange = (value) => { + const container = document.querySelector('#divTwc'); + if (!container) return; // DOM not ready + + if (value) { + container.classList.add('wide'); + } else { + container.classList.remove('wide'); + } + // Trigger resize to recalculate scaling for new width + window.dispatchEvent(new Event('resize')); +}; + +const kioskChange = (value) => { + const body = document.querySelector('body'); + if (!body) return; // DOM not ready + + if (value) { + body.classList.add('kiosk'); + window.dispatchEvent(new Event('resize')); + } else { + body.classList.remove('kiosk'); + } +}; + +const scanLineChange = (value) => { + const container = document.getElementById('container'); + const navIcons = document.getElementById('ToggleScanlines'); + + if (!container || !navIcons) return; // DOM elements not ready + + if (value) { + container.classList.add('scanlines'); + navIcons.classList.add('on'); + } else { + // Remove all scanline classes + container.classList.remove('scanlines', 'scanlines-auto', 'scanlines-fine', 'scanlines-normal', 'scanlines-thick', 'scanlines-classic', 'scanlines-retro'); + navIcons.classList.remove('on'); + } +}; + +const scanLineModeChange = (_value) => { + // Only apply if scanlines are currently enabled + if (settings.scanLines?.value) { + // Call the scanline update function directly with current scale + if (typeof window.applyScanlineScaling === 'function') { + // Get current scale from navigation module or use 1.0 as fallback + const scale = window.currentScale || 1.0; + window.applyScanlineScaling(scale); + } + } +}; + +// Simple global helper to change scanline mode when remote debugging or in kiosk mode +window.changeScanlineMode = (mode) => { + if (typeof settings === 'undefined' || !settings.scanLineMode) { + console.error('Settings system not available'); + return false; + } + + const validModes = ['auto', 'thin', 'medium', 'thick']; + if (!validModes.includes(mode)) { + return false; + } + + settings.scanLineMode.value = mode; + return true; +}; + +const unitChange = () => { + // reload the data at the top level to refresh units + // after the initial load + if (unitChange.firstRunDone) { + window.location.reload(); + } + unitChange.firstRunDone = true; +}; + const init = () => { // create settings see setting.mjs for defaults settings.wide = new Setting('wide', { @@ -39,6 +114,19 @@ const init = () => { changeAction: scanLineChange, sticky: true, }); + settings.scanLineMode = new Setting('scanLineMode', { + name: 'Scan Line Style', + type: 'select', + defaultValue: 'auto', + changeAction: scanLineModeChange, + sticky: true, + values: [ + ['auto', 'Auto (Adaptive)'], + ['thin', 'Thin (1p)'], + ['medium', 'Medium (2x)'], + ['thick', 'Thick (3x)'], + ], + }); settings.units = new Setting('units', { name: 'Units', type: 'select', @@ -62,54 +150,16 @@ const init = () => { ], visible: false, }); +}; - // generate html objects +init(); + +// generate html objects +document.addEventListener('DOMContentLoaded', () => { const settingHtml = Object.values(settings).map((d) => d.generate()); - - // write to page const settingsSection = document.querySelector('#settings'); settingsSection.innerHTML = ''; settingsSection.append(...settingHtml); -}; - -const wideScreenChange = (value) => { - const container = document.querySelector('#divTwc'); - if (value) { - container.classList.add('wide'); - } else { - container.classList.remove('wide'); - } -}; - -const kioskChange = (value) => { - const body = document.querySelector('body'); - if (value) { - body.classList.add('kiosk'); - window.dispatchEvent(new Event('resize')); - } else { - body.classList.remove('kiosk'); - } -}; - -const scanLineChange = (value) => { - const container = document.getElementById('container'); - const navIcons = document.getElementById('ToggleScanlines'); - if (value) { - container.classList.add('scanlines'); - navIcons.classList.add('on'); - } else { - container.classList.remove('scanlines'); - navIcons.classList.remove('on'); - } -}; - -const unitChange = () => { - // reload the data at the top level to refresh units - // after the initial load - if (unitChange.firstRunDone) { - window.location.reload(); - } - unitChange.firstRunDone = true; -}; +}); export default settings; diff --git a/server/scripts/modules/utils/setting.mjs b/server/scripts/modules/utils/setting.mjs index 2bfe853..4ac0862 100644 --- a/server/scripts/modules/utils/setting.mjs +++ b/server/scripts/modules/utils/setting.mjs @@ -171,8 +171,9 @@ class Setting { } } } - } catch { - return null; + } catch (error) { + console.warn(`Failed to parse settings from localStorage: ${error} - allSettings=${allSettings}`); + localStorage?.removeItem(SETTINGS_KEY); } return null; } diff --git a/server/styles/main.css b/server/styles/main.css index 7a599c7..3c9bbd5 100644 --- a/server/styles/main.css +++ b/server/styles/main.css @@ -1 +1 @@ -@font-face{font-family:"Star4000";src:url("../fonts/Star4000.woff") format("woff");font-display:swap}body{font-family:"Star4000"}@media(prefers-color-scheme: dark){body{background-color:#000;color:#fff}}@media(prefers-color-scheme: dark){body a{color:#add8e6}}body.kiosk{margin:0px;overflow:hidden;width:100vw}#divQuery{max-width:640px}#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 #txtAddress{width:calc(100% - 170px);max-width:490px;font-size:16pt;min-width:200px;display:inline-block}@media(prefers-color-scheme: dark){#divQuery #txtAddress{background-color:#000;color:#fff;border:1px solid #a9a9a9}}.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}#divTwc.wide{max-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:100%}@media(prefers-color-scheme: dark){#divTwcBottom{background-color:#303030}}#divTwcBottom>div{padding-left:6px;padding-right:6px}@media(max-width: 550px){#divTwcBottom>div{zoom:.9}}@media(max-width: 500px){#divTwcBottom>div{zoom:.8}}@media(max-width: 450px){#divTwcBottom>div{zoom:.7}}@media(max-width: 400px){#divTwcBottom>div{zoom:.6}}@media(max-width: 350px){#divTwcBottom>div{zoom:.5}}#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}#display{font-family:"Star4000";margin:0 0 0 0;width:100%}#container{position:relative;width:640px;height:480px;background-image:url(../images/backgrounds/1.png);transform-origin:0 0}.wide #container{padding-left:107px;padding-right:107px;background-repeat:no-repeat;background:url(../images/backgrounds/1-wide.png)}#divTwc:fullscreen #container,.kiosk #divTwc #container{width:unset;height:unset;transform-origin: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:36px;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:300px}#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)}#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>div{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}.weather-display{width:640px;height:480px;overflow:hidden;position:relative;background-image:url(../images/backgrounds/1.png);height:0px}.weather-display.show{height:480px}.weather-display .template{display:none}.weather-display .header{width:640px;height:60px;padding-top:30px}.weather-display .header .title{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;font-family:"Star4000";font-size:24pt;position:absolute;width:250px}.weather-display .header .title.single{left:170px;top:25px}.weather-display .header .title.dual{left:170px}.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 .title.single{top:40px}.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:415px;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;height:310px;overflow:hidden}.weather-display .main.has-scroll.no-header{height:400px}.weather-display .main.has-box{margin-left:64px;margin-right:64px;width:calc(100% - 128px)}.weather-display .scroll{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:70px;overflow:hidden;margin-top:3px}.weather-display .scroll.hazard{background-color:#702323}.weather-display .scroll .fixed,.weather-display .scroll .scroll-header{margin-left:55px;margin-right:55px;overflow:hidden}.weather-display .scroll.hazard .fixed{margin-left:0;margin-right:0}.weather-display .scroll .scroll-header{height:26px;font-family:"Star4000 Small";font-size:20pt;margin-top:-10px}.weather-display .scroll .fixed{font-family:"Star4000";font-size:24pt}.weather-display .scroll .fixed .scroll-area{text-wrap:nowrap;position:relative}.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:24pt}.weather-display .main.current-weather.main .col.right{right:0px;font-family:"Star4000 Large";font-size:20px;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{height:100px}.weather-display .main.current-weather.main .icon img{max-width:126px}.weather-display .main.current-weather.main .wind-container{margin-bottom:10px}.weather-display .main.current-weather.main .wind-container>div{width:45%;display:inline-block;margin:0px}.weather-display .main.current-weather.main .wind-container .wind-label{margin-left:5px}.weather-display .main.current-weather.main .wind-container .wind{text-align:right}.weather-display .main.current-weather.main .wind-gusts{margin-left:5px}.weather-display .main.current-weather.main .location{color:#ff0;max-height:32px;margin-bottom:10px;padding-top:4px;overflow:hidden;text-wrap:nowrap}#extended-forecast-html.weather-display{background-image:url("../images/backgrounds/2.png")}.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:repeating-linear-gradient(0deg, #001040 0px, #102080 136px, #102080 202px, #001040 338px)}.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:url(../images/backgrounds/1-chart.png)}#hourly-graph-html .header .right{position:absolute;top:35px;right:60px;width:360px;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;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 .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:0px;width:640px;height:20px}.weather-display .main.hourly-graph.main .x-axis .label{text-align:center;width:50px}.weather-display .main.hourly-graph.main .x-axis .label.l-1{left:25px}.weather-display .main.hourly-graph.main .x-axis .label.l-2{left:158px}.weather-display .main.hourly-graph.main .x-axis .label.l-3{left:291px}.weather-display .main.hourly-graph.main .x-axis .label.l-4{left:424px}.weather-display .main.hourly-graph.main .x-axis .label.l-5{left:557px}.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:140px}.weather-display .main.hourly-graph.main .y-axis .label.l-3{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:absolute;width:100%}.weather-display .main.travel.main .column-headers{position:sticky;top:0px;z-index:5}.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:repeating-linear-gradient(0deg, #001040 0px, #102080 136px, #102080 202px, #001040 338px)}.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 .latest-observations.main{overflow-y:hidden}.weather-display .latest-observations.main .column-headers{height:20px;position:absolute;width:100%}.weather-display .latest-observations.main .column-headers{top:0px}.weather-display .latest-observations.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;position:absolute;top:-14px;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 .latest-observations.main .column-headers .temp{display:none}.weather-display .latest-observations.main .column-headers .temp.show{display:inline-block}.weather-display .latest-observations.main .temp{left:230px}.weather-display .latest-observations.main .weather{left:280px}.weather-display .latest-observations.main .wind{left:430px}.weather-display .latest-observations.main .observation-lines{min-height:338px;padding-top:10px}.weather-display .latest-observations.main .observation-lines .observation-row{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;position:relative;height:40px}.weather-display .latest-observations.main .observation-lines .observation-row>div{position:absolute;top:8px}.weather-display .latest-observations.main .observation-lines .observation-row .wind{white-space:pre;text-align:right}.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}.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:19pt}.weather-display .progress .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:310px;overflow:hidden}.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:url("../images/backgrounds/4.png")}#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>div{display:inline-block}#radar-html.weather-display .header .right .scale-table{display:table-row;border-collapse:collapse}#radar-html.weather-display .header .right .scale-table .box{display:table-cell;border:2px solid #000;width:17px;height:24px;padding:0}#radar-html.weather-display .header .right .scale-table .box-1{background-color:#31d216}#radar-html.weather-display .header .right .scale-table .box-2{background-color:#1c8a12}#radar-html.weather-display .header .right .scale-table .box-3{background-color:#145a0f}#radar-html.weather-display .header .right .scale-table .box-4{background-color:#0a280a}#radar-html.weather-display .header .right .scale-table .box-5{background-color:#c4b346}#radar-html.weather-display .header .right .scale-table .box-6{background-color:#be4813}#radar-html.weather-display .header .right .scale-table .box-7{background-color:#ab0e0e}#radar-html.weather-display .header .right .scale-table .box-8{background-color:#731f04}#radar-html.weather-display .header .right .scale .text{position:relative;top:-5px}#radar-html.weather-display .header .right .time{position:relative;font-weight:normal;top:-14px;font-family:"Star4000 Small";font-size:24pt}.weather-display .main.radar{overflow:hidden;height:367px}.weather-display .main.radar .container .tiles{position:absolute;width:1400px}.weather-display .main.radar .container .tiles img{vertical-align:middle}.weather-display .main.radar .container .scroll-area{position:relative}.wide.radar #container{background:url(../images/backgrounds/4-wide.png)}#regional-forecast-html.weather-display{background-image:url("../images/backgrounds/5.png")}.weather-display .main.regional-forecast{position:relative}.weather-display .main.regional-forecast .map{position:absolute;transform-origin:0 0}.weather-display .main.regional-forecast .location{position:absolute;width:140px;margin-left:-40px;margin-top:-35px}.weather-display .main.regional-forecast .location>div{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.regional-forecast .location .icon{top:26px;left:44px}.weather-display .main.regional-forecast .location .icon img{max-height:32px}.weather-display .main.regional-forecast .location .temp{font-family:"Star4000 Large";font-size:28px;padding-top:2px;color:#ff0;top:28px;text-align:right;width:40px}.weather-display .main.regional-forecast .location .city{font-family:Star4000;font-size:20px}#almanac-html.weather-display{background-image:url("../images/backgrounds/3.png")}.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:table;margin-left:50px;height:100px}.weather-display .main.almanac .sun>div{display:table-row;position:relative}.weather-display .main.almanac .sun>div>div{display:table-cell}.weather-display .main.almanac .sun .days{color:#ff0;text-align:right;top:-5px}.weather-display .main.almanac .sun .days .day{padding-right:10px}.weather-display .main.almanac .sun .times{text-align:right}.weather-display .main.almanac .sun .times .sun-time{width:200px}.weather-display .main.almanac .sun .times.times-1{top:-10px}.weather-display .main.almanac .sun .times.times-2{top:-15px}.weather-display .main.almanac .moon{position:relative;top:-10px;padding:0px 60px}.weather-display .main.almanac .moon .title{color:#ff0}.weather-display .main.almanac .moon .day{display:inline-block;text-align:center;width:130px}.weather-display .main.almanac .moon .day .icon{padding-left:10px}.weather-display .main.almanac .moon .day .date{position:relative;top:-10px}.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}.media{display:none}#ToggleMedia{display:none}#ToggleMedia.available{display:inline-block}#ToggleMedia.available img.on{display:none}#ToggleMedia.available img.off{display:block}#ToggleMedia.available.playing img.on{display:block}#ToggleMedia.available.playing img.off{display:none}#spc-outlook-html.weather-display{background-image:url("../images/backgrounds/6.png")}.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%)}.scanlines{position:relative;overflow:hidden}.scanlines:before,.scanlines:after{display:block;pointer-events:none;content:"";position:absolute}.scanlines:before{width:100%;height:1px;z-index:2147483649;background:rgba(0,0,0,.3);opacity:.75;animation:scanline 6s linear infinite}.scanlines:after{top:0;right:0;bottom:0;left:0;z-index:2147483648;background:linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.3) 51%);background-size:100% 2px;animation:none;image-rendering:crisp-edges;image-rendering:pixelated}@media(-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.scanlines:before{height:1px}.scanlines:after{background-size:100% 2px}}@media(max-width: 1200px)and (max-height: 900px)and (-webkit-max-device-pixel-ratio: 1.5){.scanlines:before{height:1.5px}.scanlines:after{background-size:100% 3px}}@media(max-width: 1024px)and (max-height: 768px){.scanlines:before{height:2px}.scanlines:after{background-size:100% 4px}}@media(max-width: 800px)and (max-height: 600px){.scanlines:before{height:3px}.scanlines:after{background-size:100% 6px}}@keyframes scanline{0%{transform:translate3d(0, 200000%, 0)}}@keyframes scanlines{0%{background-position:0 50%}}/*# sourceMappingURL=main.css.map */ +@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}#divQuery{max-width:640px;padding-left: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 #txtAddress{width:calc(100% - 170px);max-width:490px;font-size:16pt;min-width:200px;display:inline-block}@media(prefers-color-scheme: dark){#divQuery #txtAddress{background-color:#000;color:#fff;border:1px solid #a9a9a9}}.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}.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{zoom:.9}}@media(max-width: 500px){#divTwcBottom>div{zoom:.8}}@media(max-width: 450px){#divTwcBottom>div{zoom:.7}}@media(max-width: 400px){#divTwcBottom>div{zoom:.6}}@media(max-width: 350px){#divTwcBottom>div{zoom:.5}}#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}#display{font-family:"Star4000";margin:0 0 0 0;width:100%}#container{position:relative;width:640px;height:480px;background-image:url(../images/backgrounds/1.png);transform-origin:0 0}.wide #container{padding-left:107px;padding-right:107px;background-repeat:no-repeat;background:url(../images/backgrounds/1-wide.png)}#divTwc:fullscreen #container,.kiosk #divTwc #container{width:unset;height:unset;transform-origin: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:36px;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:300px}#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)}#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}.weather-display{width:640px;height:480px;overflow:hidden;position:relative;background-image:url(../images/backgrounds/1.png);height:0px}.weather-display.show{height:480px}.weather-display .template{display:none}.weather-display .header{width:640px;height:60px;padding-top:30px}.weather-display .header .title{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;font-family:"Star4000";font-size:24pt;position:absolute;width:250px}.weather-display .header .title.single{left:170px;top:25px}.weather-display .header .title.dual{left:170px}.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 .title.single{top:40px}.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:415px;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;height:310px;overflow:hidden}.weather-display .main.has-scroll.no-header{height:400px}.weather-display .main.has-box{margin-left:64px;margin-right:64px;width:calc(100% - 128px)}.weather-display .scroll{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:70px;overflow:hidden;margin-top:3px}.weather-display .scroll.hazard{background-color:#702323}.weather-display .scroll .fixed,.weather-display .scroll .scroll-header{margin-left:55px;margin-right:55px;overflow:hidden}.weather-display .scroll.hazard .fixed{margin-left:0;margin-right:0}.weather-display .scroll .scroll-header{height:26px;font-family:"Star4000 Small";font-size:20pt;margin-top:-10px}.weather-display .scroll .fixed{font-family:"Star4000";font-size:24pt}.weather-display .scroll .fixed .scroll-area{text-wrap:nowrap;position:relative}.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:24pt}.weather-display .main.current-weather.main .col.right{right:0px;font-family:"Star4000 Large";font-size:20px;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{height:100px}.weather-display .main.current-weather.main .icon img{max-width:126px}.weather-display .main.current-weather.main .wind-container{margin-bottom:10px}.weather-display .main.current-weather.main .wind-container>div{width:45%;display:inline-block;margin:0px}.weather-display .main.current-weather.main .wind-container .wind-label{margin-left:5px}.weather-display .main.current-weather.main .wind-container .wind{text-align:right}.weather-display .main.current-weather.main .wind-gusts{margin-left:5px}.weather-display .main.current-weather.main .location{color:#ff0;max-height:32px;margin-bottom:10px;padding-top:4px;overflow:hidden;text-wrap:nowrap}#extended-forecast-html.weather-display{background-image:url("../images/backgrounds/2.png")}.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:repeating-linear-gradient(0deg, #001040 0px, #102080 136px, #102080 202px, #001040 338px)}.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:url(../images/backgrounds/1-chart.png)}#hourly-graph-html .header .right{position:absolute;top:35px;right:60px;width:360px;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;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 .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:0px;width:640px;height:20px}.weather-display .main.hourly-graph.main .x-axis .label{text-align:center;width:50px}.weather-display .main.hourly-graph.main .x-axis .label.l-1{left:25px}.weather-display .main.hourly-graph.main .x-axis .label.l-2{left:158px}.weather-display .main.hourly-graph.main .x-axis .label.l-3{left:291px}.weather-display .main.hourly-graph.main .x-axis .label.l-4{left:424px}.weather-display .main.hourly-graph.main .x-axis .label.l-5{left:557px}.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:140px}.weather-display .main.hourly-graph.main .y-axis .label.l-3{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:absolute;width:100%}.weather-display .main.travel.main .column-headers{position:sticky;top:0px;z-index:5}.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:repeating-linear-gradient(0deg, #001040 0px, #102080 136px, #102080 202px, #001040 338px)}.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 .latest-observations.main{overflow-y:hidden}.weather-display .latest-observations.main .column-headers{height:20px;position:absolute;width:100%}.weather-display .latest-observations.main .column-headers{top:0px}.weather-display .latest-observations.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;position:absolute;top:-14px;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 .latest-observations.main .column-headers .temp{display:none}.weather-display .latest-observations.main .column-headers .temp.show{display:inline-block}.weather-display .latest-observations.main .temp{left:230px}.weather-display .latest-observations.main .weather{left:280px}.weather-display .latest-observations.main .wind{left:430px}.weather-display .latest-observations.main .observation-lines{min-height:338px;padding-top:10px}.weather-display .latest-observations.main .observation-lines .observation-row{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;position:relative;height:40px}.weather-display .latest-observations.main .observation-lines .observation-row>div{position:absolute;top:8px}.weather-display .latest-observations.main .observation-lines .observation-row .wind{white-space:pre;text-align:right}.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}.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:19pt}.weather-display .progress .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:310px;overflow:hidden;line-height:28px}.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:url("../images/backgrounds/4.png")}#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>div{display:inline-block}#radar-html.weather-display .header .right .scale-table{display:table-row;border-collapse:collapse}#radar-html.weather-display .header .right .scale-table .box{display:table-cell;border:2px solid #000;width:17px;height:24px;padding:0}#radar-html.weather-display .header .right .scale-table .box-1{background-color:#31d216}#radar-html.weather-display .header .right .scale-table .box-2{background-color:#1c8a12}#radar-html.weather-display .header .right .scale-table .box-3{background-color:#145a0f}#radar-html.weather-display .header .right .scale-table .box-4{background-color:#0a280a}#radar-html.weather-display .header .right .scale-table .box-5{background-color:#c4b346}#radar-html.weather-display .header .right .scale-table .box-6{background-color:#be4813}#radar-html.weather-display .header .right .scale-table .box-7{background-color:#ab0e0e}#radar-html.weather-display .header .right .scale-table .box-8{background-color:#731f04}#radar-html.weather-display .header .right .scale .text{position:relative;top:-5px}#radar-html.weather-display .header .right .time{position:relative;font-weight:normal;top:-14px;font-family:"Star4000 Small";font-size:24pt}.weather-display .main.radar{overflow:hidden;height:367px}.weather-display .main.radar .container .tiles{position:absolute;width:1400px}.weather-display .main.radar .container .tiles img{vertical-align:middle}.weather-display .main.radar .container .scroll-area{position:relative}.wide.radar #container{background:url(../images/backgrounds/4-wide.png)}#regional-forecast-html.weather-display{background-image:url("../images/backgrounds/5.png")}.weather-display .main.regional-forecast{position:relative}.weather-display .main.regional-forecast .map{position:absolute;transform-origin:0 0}.weather-display .main.regional-forecast .location{position:absolute;width:140px;margin-left:-40px;margin-top:-35px}.weather-display .main.regional-forecast .location>div{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.regional-forecast .location .icon{top:26px;left:44px}.weather-display .main.regional-forecast .location .icon img{max-height:32px}.weather-display .main.regional-forecast .location .temp{font-family:"Star4000 Large";font-size:28px;padding-top:2px;color:#ff0;top:28px;text-align:right;width:40px}.weather-display .main.regional-forecast .location .city{font-family:Star4000;font-size:20px}#almanac-html.weather-display{background-image:url("../images/backgrounds/3.png")}.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:table;margin-left:50px;height:100px}.weather-display .main.almanac .sun>div{display:table-row;position:relative}.weather-display .main.almanac .sun>div>div{display:table-cell}.weather-display .main.almanac .sun .days{color:#ff0;text-align:right;top:-5px}.weather-display .main.almanac .sun .days .day{padding-right:10px}.weather-display .main.almanac .sun .times{text-align:right}.weather-display .main.almanac .sun .times .sun-time{width:200px}.weather-display .main.almanac .sun .times.times-1{top:-10px}.weather-display .main.almanac .sun .times.times-2{top:-15px}.weather-display .main.almanac .moon{position:relative;top:-10px;padding:0px 60px}.weather-display .main.almanac .moon .title{color:#ff0}.weather-display .main.almanac .moon .day{display:inline-block;text-align:center;width:130px}.weather-display .main.almanac .moon .day .icon{padding-left:10px}.weather-display .main.almanac .moon .day .date{position:relative;top:-10px}.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}.media{display:none}#ToggleMedia{display:none}#ToggleMedia.available{display:inline-block}#ToggleMedia.available img.on{display:none}#ToggleMedia.available img.off{display:block}#ToggleMedia.available.playing img.on{display:block}#ToggleMedia.available.playing img.off{display:none}#spc-outlook-html.weather-display{background-image:url("../images/backgrounds/6.png")}.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%)}.scanlines{position:relative;overflow:hidden}.scanlines:before,.scanlines:after{display:block;pointer-events:none;content:"";position:absolute}.scanlines:before{width:100%;height:1px;z-index:2147483649;background:rgba(0,0,0,.3);opacity:.75;animation:scanline 6s linear infinite}.scanlines:after{top:0;right:0;bottom:0;left:0;z-index:2147483648;background:linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.3) 51%);background-size:100% 2px;animation:none;image-rendering:crisp-edges;image-rendering:pixelated}@media(-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.scanlines:before{height:1px}.scanlines:after{background-size:100% 2px}}@media(max-width: 1200px)and (max-height: 900px)and (-webkit-max-device-pixel-ratio: 1.5){.scanlines:before{height:1.5px}.scanlines:after{background-size:100% 3px}}@media(max-width: 1024px)and (max-height: 768px){.scanlines:before{height:2px}.scanlines:after{background-size:100% 4px}}@media(max-width: 800px)and (max-height: 600px){.scanlines:before{height:3px}.scanlines:after{background-size:100% 6px}}@keyframes scanline{0%{transform:translate3d(0, 200000%, 0)}}@keyframes scanlines{0%{background-position:0 50%}}/*# sourceMappingURL=main.css.map */ diff --git a/server/styles/main.css.map b/server/styles/main.css.map index 757a055..4360c5a 100644 --- a/server/styles/main.css.map +++ b/server/styles/main.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["scss/_page.scss","scss/shared/_utils.scss","scss/_weather-display.scss","scss/shared/_colors.scss","scss/_current-weather.scss","scss/_extended-forecast.scss","scss/_hourly.scss","scss/_hourly-graph.scss","scss/_travel.scss","scss/_latest-observations.scss","scss/_local-forecast.scss","scss/_progress.scss","scss/_radar.scss","scss/_regional-forecast.scss","scss/_almanac.scss","scss/_hazards.scss","scss/_media.scss","scss/_spc-outlook.scss","scss/shared/_scanlines.scss"],"names":[],"mappings":"AAGA,WACC,uBACA,iDACA,kBAGD,KACC,uBAEA,mCAHD,KAIE,sBACA,YAIA,mCADD,OAEE,eAIF,WACC,WACA,gBACA,YAIF,UACC,gBAEA,mBACC,qBACA,YACA,iBAEA,8BACC,YACA,sBAGD,0BACC,eACA,yBAEA,mCAJD,0BAKE,sBACA,YAQA,uCACC,aAEA,mCAHD,uCAIE,sBAKD,mCADD,wCAEE,cAKH,qCACC,sBAEA,mCAHD,qCAIE,uBAGD,yCACC,iBAMJ,iCAEC,uBAGD,sBACC,yBACA,gBACA,eACA,gBACA,qBAEA,mCAPD,sBAQE,sBACA,WACA,0BAOH,0BACC,sBACA,sBACA,kBACA,aAEA,mCAND,0BAOE,uBAGD,8BAEC,mBACA,gBACA,uBACA,eAEA,uCACC,sBACA,WAMH,QACC,cACA,sBACA,WACA,WACA,gBAEA,aACC,gBAIF,eACC,gBAGD,YACC,aACA,iBACA,sBACA,sBAGD,gBACC,OACA,mBACA,aACA,sBACA,uBAGD,aACC,gBACA,aACA,sBACA,sBAGD,iBACC,OACA,kBACA,aACA,sBACA,uBAGD,cAEC,aACA,mBACA,sBAEA,WACA,WAEA,mCATD,cAUE,0BAKF,kBACC,iBACA,kBAGA,yBALD,kBAME,SAGD,yBATD,kBAUE,SAGD,yBAbD,kBAcE,SAGD,yBAjBD,kBAkBE,SAGD,yBArBD,kBAsBE,SAIF,kBACC,OACA,gBAID,oBACC,OACA,kBAGD,mBACC,OACA,iBAGD,oBACC,aAGD,WACC,WACA,aACA,mBACA,sBACA,WACA,gBAGD,eACC,iBACA,kBAGD,eACC,OACA,gBAGD,iBACC,OACA,kBAGD,gBACC,OACA,iBAGD,YACC,kBACA,kBAGD,YACC,uBAGD,eACC,YAGD,WACC,gCACA,0DACA,kBAGD,WACC,6BACA,uDACA,kBAGD,WACC,6BACA,uDACA,kBAGD,SACC,uBACA,eACA,WAGD,WACC,kBACA,YACA,aAEA,kDACA,qBAGD,iBACC,mBACA,oBACA,4BACA,iDAGD,wDAGC,YACA,aACA,uBAGD,SACC,YACA,aACA,eACA,yBACA,aACA,mBACA,kBACA,uBAEA,gBACC,2BACA,eACA,WACA,kBAGD,kBACC,mBAGD,uBACC,eAIF,SACC,iBACA,gBAGD,UACC,mBAGD,2BAEC,mBC3VA,4FAEC,WAGD,mDACC,WACA,eAGD,2CACC,UAGD,6CACC,aAGD,+CACC,aD2UD,mDACC,WAGD,oCAEC,4FAEC,WAGD,mDACC,WACA,eAGD,2CACC,WAGD,6CACC,oBAGD,+CACC,qBAIF,uCACC,cACA,gBAEA,qDACC,aAEA,+DACC,eACA,UAMJ,kBACC,sBAGD,kCAEC,aACA,mBACA,uBACA,qBAEA,sDACC,YAIF,oDAEC,kBAGD,8DAEC,aACA,mBACA,gCACA,WACA,WACA,kBACA,WAKC,iCACC,aAKH,WACC,eAGD,iBACC,qBAEA,qBACC,aAGD,sBACC,qBAKA,wBACC,qBAGD,yBACC,aAMH,SACC,mBACA,UACA,8BAGD,2BACC,kBACA,UACA,8CAGD,cACC,YACA,6BACA,aACA,6BACA,eAGC,qBACC,qBACA,UAGD,2BACC,qBACA,gBACA,iFACA,YACA,cACA,mBAGD,yDAEC,kBACA,qBACA,oBACA,YACA,gBACA,eACA,gBACA,iBACA,sBACA,eACA,yBACA,sBACA,qBACA,iBACA,2BACA,8BACA,0BACA,iBAGD,wBACC,oBAGD,yCACC,8BAGD,iCACC,cACA,8BAGD,+EAEC,YACA,iBACA,eACA,iBAGD,4BACC,qBACA,wBACA,kBACA,iBAGD,qFAEC,0BACA,oBAGD,wBACC,cACA,yBACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,8BACC,YAGD,4DAEC,yBACA,6BACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,wEAEC,YAGD,+BACC,yBACA,qBACA,gCACA,iDACA,sBACA,YAGD,iCACC,cACA,sBACA,qBACA,gCAGD,8EAEC,cAGD,kCACC,cAGD,oCAEC,qFAEC,0BACA,oBAGD,wBACC,cACA,yBACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,8BACC,YAGD,4DAEC,yBACA,6BACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,wEAEC,YAGD,+BACC,yBACA,qBACA,gCACA,iDACA,sBACA,YAGD,iCACC,cACA,sBACA,qBACA,gCAGD,8EAEC,cAGD,kCACC,eAIF,mCAEC,qFAEC,0BACA,oBAGD,wBACC,cACA,yBACA,qBACA,kCACA,8SACA,iEACA,+DACA,sGAGD,8BACC,YAGD,4DAEC,yBACA,6BACA,qBACA,8SACA,iEACA,+DACA,sGAGD,wEAEC,YAGD,+BACC,yBACA,qBACA,8CACA,sBACA,YAGD,iCACC,cACA,yBACA,qBACA,kCAGD,8EAEC,cAGD,kCACC,eAMJ,mBACC,WACA,aAGD,yBACC,aAID,kCACC,wBAMA,sBACC,wBExwBF,iBACC,YACA,aACA,gBACA,kBACA,kDAGA,WAEA,sBACC,aAGD,2BACC,aAGD,yBACC,YACA,YACA,iBAEA,gCACC,MC3BW,KFMb,YACC,6JCsBC,uBACA,eACA,kBACA,YAEA,uCACC,WACA,SAGD,qCACC,WAEA,yCACC,kBAGD,0CACC,SAGD,6CACC,SAMH,+BACC,SACA,UACA,kBACA,WAGD,oCACC,kBACA,SACA,WAGD,uCACC,SAGD,oCACC,gBACA,MC3ES,KD4ET,6BACA,eDxEF,YACC,6JCyEC,WACA,YACA,iBACA,kBAEA,yCACC,iBAKH,uBACC,kBAEA,kCACC,YACA,aACA,gBAEA,4CACC,aAIF,+BACC,iBACA,kBACA,yBAMF,yBD3GA,YACC,6JC4GA,YACA,YACA,gBACA,eAEA,gCACC,yBAGD,wEAEC,iBACA,kBACA,gBAID,uCACC,cACA,eAGD,wCACC,YACA,6BACA,eACA,iBAGD,gCACC,uBACA,eAEA,6CACC,iBACA,kBEhJF,iDACC,YACA,YACA,qBACA,gBACA,iBACA,kBHNF,YACC,6JGSC,sDACC,gCACA,eAID,uDACC,UACA,6BACA,eACA,iBACA,iBAEA,4DACC,mBAEA,sIAEC,qBAGD,mEACC,iBAGD,mEACC,YACA,kBAQJ,oDACC,kBAGD,kDACC,6BACA,eAKD,kDACC,aAEA,sDACC,gBAIF,4DACC,mBAEA,gEACC,UACA,qBACA,WAGD,wEACC,gBAGD,kEACC,iBAIF,wDACC,gBAGD,sDACC,MD7FW,KC8FX,gBACA,mBACA,gBACA,gBACA,iBC/FH,wCACC,oDAIA,wDACC,gBACA,iBAGD,8CJPA,YACC,6JIQA,YACA,aACA,YACA,qBACA,gBACA,uBACA,eAEA,oDACC,yBACA,kBACA,MF1BW,KE6BZ,yDACC,kBACA,YACA,eAGD,oDACC,kBACA,YAEA,wDACC,gBAIF,4DACC,WAEA,+EACC,qBACA,UACA,mBAEA,mFACC,kBAGD,sFACC,6BACA,eAGD,yFACC,MFhDU,QEmDX,yFACC,MFlES,KGIb,mCACC,kBAEA,mDACC,iBHJa,QGKb,YACA,kBACA,WAGD,mDACC,gBACA,QACA,UAEA,uDACC,qBACA,6BACA,eACA,MHpBiB,KGqBjB,kBACA,UACA,ULpBH,YACC,6JKuBC,yDACC,WAGD,yDACC,WAGD,yDACC,WAIF,iDACC,iBACA,iBAEA,qGAMA,6DACC,6BACA,eACA,YACA,MHzDU,KFMb,YACC,6JKoDE,kBAEA,iEACC,kBACA,gBACA,QAGD,mEACC,UAGD,mEACC,WACA,WACA,kBACA,UAGD,mEACC,WAGD,mEACC,WAEA,8EACC,WAGD,8EACC,MH5ES,QGgFX,mEACC,WACA,YACA,iBC9FL,mBACC,wDAGC,kCACC,kBACA,SACA,WACA,YACA,6BACA,eNPF,YACC,6JMQC,iBAEA,sCACC,iBAGD,+CACC,UAGD,yCACC,cAGD,wCACC,WASF,6CACC,kBAGD,gDACC,6BACA,eACA,MJ3CkB,KFGpB,YACC,6JMyCC,iBACA,kBAGD,iDACC,WACA,SACA,YACA,YAEA,wDACC,kBACA,WAEA,4DACC,UAGD,4DACC,WAGD,4DACC,WAGD,4DACC,WAGD,4DACC,WAQH,gDACC,QACA,UAEA,oDACC,YACA,aAIF,iDACC,QACA,SACA,WACA,aAEA,wDACC,iBACA,UAEA,4DACC,QAGD,4DACC,UAGD,4DACC,WAKH,yDACC,iBJtHa,QIuHb,YACA,kBACA,WAGD,yDACC,gBACA,QACA,UAGA,+DACC,WAGD,+DACC,WAGD,+DACC,WC3IH,mCACC,kBAEA,mDACC,iBLJa,QKKb,YACA,kBACA,WAGD,mDACC,gBACA,QACA,UAEA,uDACC,qBACA,6BACA,eACA,MLpBiB,KKqBjB,kBACA,UACA,UPpBH,YACC,6JOuBC,yDACC,WACA,kBAEA,6DACC,WAID,8DACC,WACA,WAKH,iDACC,iBACA,iBAEA,qGAMA,6DACC,6BACA,eACA,YACA,ML5DU,KFMb,YACC,6JOuDE,kBAEA,iEACC,kBACA,gBACA,QAGD,mEACC,UAGD,mEACC,WACA,WACA,kBACA,UAEA,uEACC,eAIF,mEACC,WACA,kBAEA,uEACC,WAGD,wEACC,WACA,WC1FL,2CACC,kBAEA,2DACC,YACA,kBACA,WAGD,2DACC,QAEA,+DACC,qBACA,6BACA,eACA,kBACA,URhBH,YACC,6JQmBC,iEAEC,aAEA,sEACC,qBAKH,iDACC,WAGD,oDACC,WAGD,iDACC,WAGD,8DACC,iBACA,iBAEA,+EACC,uBACA,eRhDH,YACC,6JQiDE,kBACA,YAEA,mFACC,kBACA,QAGD,qFACC,gBACA,iBC9DJ,4CACC,kBACA,SACA,gBACA,sBACA,aACA,gBAGD,4CACC,kBAGD,2CACC,uBACA,eACA,yBTdD,YACC,6JSeA,iBACA,iBCpBF,2BVGC,YACC,6JUFD,gCACA,eAEA,sCACC,kBACA,SACA,gBACA,sBACA,aACA,gBAEA,4CACC,kBAEA,kDACC,mBAEA,yDACC,mFAIF,mDACC,kBACA,iBACA,UACA,QAEA,uDACC,iBRlBM,QQmBN,aACA,iBVhBJ,yHAEC,WAGD,+DACC,WACA,eAGD,2DACC,UAGD,4DACC,aAGD,6DACC,aUEE,gaAMC,cAYJ,2BACC,GACC,4BAGD,KACC,4BAIF,+DACC,sBACA,sBACA,iBACA,YACA,kBACA,aAEA,oEACC,cAGD,6EACC,YACA,WACA,YACA,6OAiBA,sBACA,6BACA,mCACA,+BACA,wCAGD,sEACC,kBACA,QACA,UACA,sBACA,WACA,YACA,6BCjHH,4BACC,oDAEA,oCACC,YAEA,gDACC,WACA,+BACA,iBACA,eACA,WAEA,qDACC,SAGD,wDACC,SAIF,2CACC,kBACA,UACA,YACA,eACA,uBACA,eACA,iBX1BF,YACC,6JW2BC,kBAEA,sDACC,qBAGD,wDACC,kBACA,yBAEA,6DACC,mBACA,sBACA,WACA,YACA,UAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAKD,wDACC,kBACA,SAIF,iDACC,kBACA,mBACA,UACA,6BACA,eAMJ,6BACC,gBACA,aAIC,+CACC,kBACA,aAEA,mDACC,sBAIF,qDACC,kBAKH,uBACC,iDC1HD,wCACC,oDAGD,yCAGC,kBAEA,8CACC,kBACA,qBAGD,mDACC,kBACA,YACA,kBACA,iBAEA,uDACC,kBZlBF,YACC,6JYqBA,yDACC,SACA,UAEA,6DACC,gBAIF,yDACC,6BACA,eACA,gBACA,MVzCW,KU0CX,SACA,iBACA,WAGD,yDACC,qBACA,eC9CH,8BACC,oDAGD,+BACC,uBACA,ebHA,YACC,6JaKD,oCACC,cACA,iBACA,aAGA,wCACC,kBACA,kBAEA,4CACC,mBAIF,0CACC,MXzBkB,KW0BlB,iBACA,SAEA,+CACC,mBAKF,2CACC,iBAEA,qDACC,YAGD,mDACC,UAGD,mDACC,UAKH,qCACC,kBACA,UAEA,iBAEA,4CACC,MX3DkB,KW8DnB,0CACC,qBACA,kBACA,YAEA,gDAEC,kBAGD,gDACC,kBACA,UCzEH,oCACC,kBACA,aACA,yBAEA,kDACC,iBACA,iBAEA,0DACC,uBACA,eACA,WdVH,YACC,6JcWE,kBACA,yBACA,gBACA,iBACA,kBACA,oBCvBJ,OACC,aAGD,aACC,aAEA,uBACC,qBAEA,8BACC,aAGD,+BACC,cAKA,sCACC,cAGD,uCACC,aCtBJ,kCACC,oDAKA,yCACC,kBACA,QACA,gBACA,sBACA,aACA,gBAGD,2CACC,kBACA,WACA,6BACA,ehBhBD,YACC,6JgBmBA,uDACC,kBACA,UACA,YAEA,oEACC,WAGD,oEACC,UAGD,oEACC,UAGD,oEACC,UAGD,oEACC,UAGD,oEACC,SAKH,oCACC,kBACA,UAEA,yCACC,YAEA,mDACC,kBACA,uBACA,eACA,YACA,iBhB/DH,YACC,6JgBgEE,iBAGD,mDACC,kBACA,YACA,YACA,WACA,gBACA,gCACA,gGC5BJ,WACC,kBACA,gBAEA,mCAEC,cACA,oBACA,WACA,kBAID,kBAGC,WACA,WACA,mBACA,WA3DW,eA4DX,QAhDa,IAkBb,sCAoCD,iBACC,MACA,QACA,SACA,OACA,QAnEa,WAoEb,+EAGA,yBApDA,eAwDA,4BACA,0BAMD,mEAEC,kBACC,OAnGU,IAsGX,iBACC,0BAKF,0FACC,kBACC,aAGD,iBACC,0BAKF,iDACC,kBACC,WAGD,iBACC,0BAKF,gDACC,kBACC,WAGD,iBACC,0BAMH,oBACC,GACC,sCAKF,qBACC,GACC","file":"main.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["scss/_page.scss","scss/shared/_utils.scss","scss/_weather-display.scss","scss/shared/_colors.scss","scss/_current-weather.scss","scss/_extended-forecast.scss","scss/_hourly.scss","scss/_hourly-graph.scss","scss/_travel.scss","scss/_latest-observations.scss","scss/_local-forecast.scss","scss/_progress.scss","scss/_radar.scss","scss/_regional-forecast.scss","scss/_almanac.scss","scss/_hazards.scss","scss/_media.scss","scss/_spc-outlook.scss","scss/shared/_scanlines.scss"],"names":[],"mappings":"AAGA,WACC,uBACA,iDACA,kBAGD,KACC,uBACA,SAGA,mCALD,KAME,sBACA,YAIA,mCADD,OAEE,eAIF,WACC,WACA,YACA,gBACA,YAMF,UACC,gBACA,iBAEA,mBACC,qBACA,YACA,iBAEA,8BACC,YACA,sBAGD,0BACC,eACA,yBAEA,mCAJD,0BAKE,sBACA,YAQA,uCACC,aAEA,mCAHD,uCAIE,sBAKD,mCADD,wCAEE,cAKH,qCACC,sBAEA,mCAHD,qCAIE,uBAGD,yCACC,iBAMJ,iCAEC,uBAGD,sBACC,yBACA,gBACA,eACA,gBACA,qBAEA,mCAPD,sBAQE,sBACA,WACA,0BAOH,0BACC,sBACA,sBACA,kBACA,aAEA,mCAND,0BAOE,uBAGD,8BAEC,mBACA,gBACA,uBACA,eAEA,uCACC,sBACA,WAMH,QACC,cACA,sBACA,WACA,WACA,gBACA,SAEA,aACC,gBAIF,iBACC,YAGD,YACC,YACA,aAEA,kBACC,YAIF,eACC,gBAGD,YACC,aACA,iBACA,sBACA,sBAGD,gBACC,OACA,mBACA,aACA,sBACA,uBAGD,aACC,gBACA,aACA,sBACA,sBAGD,iBACC,OACA,kBACA,aACA,sBACA,uBAGD,cAEC,aACA,mBACA,sBAEA,WACA,YAEA,oBACC,YAGD,mCAbD,cAcE,0BAKF,kBACC,iBACA,kBAGA,yBALD,kBAME,SAGD,yBATD,kBAUE,SAGD,yBAbD,kBAcE,SAGD,yBAjBD,kBAkBE,SAGD,yBArBD,kBAsBE,SAIF,kBACC,OACA,gBAID,oBACC,OACA,kBAGD,mBACC,OACA,iBAGD,oBACC,aAGD,WACC,WACA,aACA,mBACA,sBACA,WACA,gBAGD,eACC,iBACA,kBAGD,eACC,OACA,gBAGD,iBACC,OACA,kBAGD,gBACC,OACA,iBAGD,YACC,kBACA,kBAGD,YACC,uBAGD,eACC,YAGD,WACC,gCACA,0DACA,kBAGD,WACC,6BACA,uDACA,kBAGD,WACC,6BACA,uDACA,kBAGD,SACC,uBACA,eACA,WAGD,WACC,kBACA,YACA,aAEA,kDACA,qBAGD,iBACC,mBACA,oBACA,4BACA,iDAGD,wDAGC,YACA,aACA,uBAGD,SACC,YACA,aACA,eACA,yBACA,aACA,mBACA,kBACA,uBAEA,gBACC,2BACA,eACA,WACA,kBAGD,kBACC,mBAGD,uBACC,eAIF,SACC,iBACA,gBAGD,UACC,mBAGD,2BAEC,mBCnXA,4FAEC,WAGD,mDACC,WACA,eAGD,2CACC,UAGD,6CACC,aAGD,+CACC,aDmWD,mDACC,WAGD,oCAEC,4FAEC,WAGD,mDACC,WACA,eAGD,2CACC,WAGD,6CACC,oBAGD,+CACC,qBAIF,uCACC,cACA,gBAEA,qDACC,aAEA,+DACC,eACA,UAMJ,kBACC,sBAGD,kCAEC,aACA,mBACA,uBACA,qBAEA,sDACC,YAIF,oDAEC,kBAGD,8DAEC,aACA,mBACA,gCACA,WACA,WACA,kBACA,WAIA,6BACC,aAIF,WACC,eAGD,iBACC,qBAEA,qBACC,aAGD,sBACC,qBAKA,wBACC,qBAGD,yBACC,aAMH,SACC,mBACA,UACA,8BAGD,2BACC,kBACA,UACA,8CAGD,cACC,YACA,6BACA,aACA,6BACA,eAGC,qBACC,qBACA,UAGD,2BACC,qBACA,gBACA,iFACA,YACA,cACA,mBAGD,yDAEC,kBACA,qBACA,oBACA,YACA,gBACA,eACA,gBACA,iBACA,sBACA,eACA,yBACA,sBACA,qBACA,iBACA,2BACA,8BACA,0BACA,iBAGD,wBACC,oBAGD,yCACC,8BAGD,iCACC,cACA,8BAGD,+EAEC,YACA,iBACA,eACA,iBAGD,4BACC,qBACA,wBACA,kBACA,iBAGD,qFAEC,0BACA,oBAGD,wBACC,cACA,yBACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,8BACC,YAGD,4DAEC,yBACA,6BACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,wEAEC,YAGD,+BACC,yBACA,qBACA,gCACA,iDACA,sBACA,YAGD,iCACC,cACA,sBACA,qBACA,gCAGD,8EAEC,cAGD,kCACC,cAGD,oCAEC,qFAEC,0BACA,oBAGD,wBACC,cACA,yBACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,8BACC,YAGD,4DAEC,yBACA,6BACA,qBACA,gCACA,8SACA,iEACA,+DACA,sGAGD,wEAEC,YAGD,+BACC,yBACA,qBACA,gCACA,iDACA,sBACA,YAGD,iCACC,cACA,sBACA,qBACA,gCAGD,8EAEC,cAGD,kCACC,eAIF,mCAEC,qFAEC,0BACA,oBAGD,wBACC,cACA,yBACA,qBACA,kCACA,8SACA,iEACA,+DACA,sGAGD,8BACC,YAGD,4DAEC,yBACA,6BACA,qBACA,8SACA,iEACA,+DACA,sGAGD,wEAEC,YAGD,+BACC,yBACA,qBACA,8CACA,sBACA,YAGD,iCACC,cACA,yBACA,qBACA,kCAGD,8EAEC,cAGD,kCACC,eAMJ,mBACC,WACA,aAGD,yBACC,aAID,kCACC,wBAMA,sBACC,wBE9xBF,iBACC,YACA,aACA,gBACA,kBACA,kDAGA,WAEA,sBACC,aAGD,2BACC,aAGD,yBACC,YACA,YACA,iBAEA,gCACC,MC3BW,KFMb,YACC,6JCsBC,uBACA,eACA,kBACA,YAEA,uCACC,WACA,SAGD,qCACC,WAEA,yCACC,kBAGD,0CACC,SAGD,6CACC,SAMH,+BACC,SACA,UACA,kBACA,WAGD,oCACC,kBACA,SACA,WAGD,uCACC,SAGD,oCACC,gBACA,MC3ES,KD4ET,6BACA,eDxEF,YACC,6JCyEC,WACA,YACA,iBACA,kBAEA,yCACC,iBAKH,uBACC,kBAEA,kCACC,YACA,aACA,gBAEA,4CACC,aAIF,+BACC,iBACA,kBACA,yBAMF,yBD3GA,YACC,6JC4GA,YACA,YACA,gBACA,eAEA,gCACC,yBAGD,wEAEC,iBACA,kBACA,gBAID,uCACC,cACA,eAGD,wCACC,YACA,6BACA,eACA,iBAGD,gCACC,uBACA,eAEA,6CACC,iBACA,kBEhJF,iDACC,YACA,YACA,qBACA,gBACA,iBACA,kBHNF,YACC,6JGSC,sDACC,gCACA,eAID,uDACC,UACA,6BACA,eACA,iBACA,iBAEA,4DACC,mBAEA,sIAEC,qBAGD,mEACC,iBAGD,mEACC,YACA,kBAQJ,oDACC,kBAGD,kDACC,6BACA,eAKD,kDACC,aAEA,sDACC,gBAIF,4DACC,mBAEA,gEACC,UACA,qBACA,WAGD,wEACC,gBAGD,kEACC,iBAIF,wDACC,gBAGD,sDACC,MD7FW,KC8FX,gBACA,mBACA,gBACA,gBACA,iBC/FH,wCACC,oDAIA,wDACC,gBACA,iBAGD,8CJPA,YACC,6JIQA,YACA,aACA,YACA,qBACA,gBACA,uBACA,eAEA,oDACC,yBACA,kBACA,MF1BW,KE6BZ,yDACC,kBACA,YACA,eAGD,oDACC,kBACA,YAEA,wDACC,gBAIF,4DACC,WAEA,+EACC,qBACA,UACA,mBAEA,mFACC,kBAGD,sFACC,6BACA,eAGD,yFACC,MFhDU,QEmDX,yFACC,MFlES,KGIb,mCACC,kBAEA,mDACC,iBHJa,QGKb,YACA,kBACA,WAGD,mDACC,gBACA,QACA,UAEA,uDACC,qBACA,6BACA,eACA,MHpBiB,KGqBjB,kBACA,UACA,ULpBH,YACC,6JKuBC,yDACC,WAGD,yDACC,WAGD,yDACC,WAIF,iDACC,iBACA,iBAEA,qGAMA,6DACC,6BACA,eACA,YACA,MHzDU,KFMb,YACC,6JKoDE,kBAEA,iEACC,kBACA,gBACA,QAGD,mEACC,UAGD,mEACC,WACA,WACA,kBACA,UAGD,mEACC,WAGD,mEACC,WAEA,8EACC,WAGD,8EACC,MH5ES,QGgFX,mEACC,WACA,YACA,iBC9FL,mBACC,wDAGC,kCACC,kBACA,SACA,WACA,YACA,6BACA,eNPF,YACC,6JMQC,iBAEA,sCACC,iBAGD,+CACC,UAGD,yCACC,cAGD,wCACC,WASF,6CACC,kBAGD,gDACC,6BACA,eACA,MJ3CkB,KFGpB,YACC,6JMyCC,iBACA,kBAGD,iDACC,WACA,SACA,YACA,YAEA,wDACC,kBACA,WAEA,4DACC,UAGD,4DACC,WAGD,4DACC,WAGD,4DACC,WAGD,4DACC,WAQH,gDACC,QACA,UAEA,oDACC,YACA,aAIF,iDACC,QACA,SACA,WACA,aAEA,wDACC,iBACA,UAEA,4DACC,QAGD,4DACC,UAGD,4DACC,WAKH,yDACC,iBJtHa,QIuHb,YACA,kBACA,WAGD,yDACC,gBACA,QACA,UAGA,+DACC,WAGD,+DACC,WAGD,+DACC,WC3IH,mCACC,kBAEA,mDACC,iBLJa,QKKb,YACA,kBACA,WAGD,mDACC,gBACA,QACA,UAEA,uDACC,qBACA,6BACA,eACA,MLpBiB,KKqBjB,kBACA,UACA,UPpBH,YACC,6JOuBC,yDACC,WACA,kBAEA,6DACC,WAID,8DACC,WACA,WAKH,iDACC,iBACA,iBAEA,qGAMA,6DACC,6BACA,eACA,YACA,ML5DU,KFMb,YACC,6JOuDE,kBAEA,iEACC,kBACA,gBACA,QAGD,mEACC,UAGD,mEACC,WACA,WACA,kBACA,UAEA,uEACC,eAIF,mEACC,WACA,kBAEA,uEACC,WAGD,wEACC,WACA,WC1FL,2CACC,kBAEA,2DACC,YACA,kBACA,WAGD,2DACC,QAEA,+DACC,qBACA,6BACA,eACA,kBACA,URhBH,YACC,6JQmBC,iEAEC,aAEA,sEACC,qBAKH,iDACC,WAGD,oDACC,WAGD,iDACC,WAGD,8DACC,iBACA,iBAEA,+EACC,uBACA,eRhDH,YACC,6JQiDE,kBACA,YAEA,mFACC,kBACA,QAGD,qFACC,gBACA,iBC9DJ,4CACC,kBACA,SACA,gBACA,sBACA,aACA,gBAGD,4CACC,kBAGD,2CACC,uBACA,eACA,yBTdD,YACC,6JSeA,iBACA,iBCpBF,2BVGC,YACC,6JUFD,gCACA,eAEA,sCACC,kBACA,SACA,gBACA,sBACA,aACA,gBACA,iBAEA,4CACC,kBAEA,kDACC,mBAEA,yDACC,mFAIF,mDACC,kBACA,iBACA,UACA,QAEA,uDACC,iBRnBM,QQoBN,aACA,iBVjBJ,yHAEC,WAGD,+DACC,WACA,eAGD,2DACC,UAGD,4DACC,aAGD,6DACC,aUGE,gaAMC,cAYJ,2BACC,GACC,4BAGD,KACC,4BAIF,+DACC,sBACA,sBACA,iBACA,YACA,kBACA,aAEA,oEACC,cAGD,6EACC,YACA,WACA,YACA,6OAiBA,sBACA,6BACA,mCACA,+BACA,wCAGD,sEACC,kBACA,QACA,UACA,sBACA,WACA,YACA,6BClHH,4BACC,oDAEA,oCACC,YAEA,gDACC,WACA,+BACA,iBACA,eACA,WAEA,qDACC,SAGD,wDACC,SAIF,2CACC,kBACA,UACA,YACA,eACA,uBACA,eACA,iBX1BF,YACC,6JW2BC,kBAEA,sDACC,qBAGD,wDACC,kBACA,yBAEA,6DACC,mBACA,sBACA,WACA,YACA,UAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAGD,+DACC,yBAKD,wDACC,kBACA,SAIF,iDACC,kBACA,mBACA,UACA,6BACA,eAMJ,6BACC,gBACA,aAIC,+CACC,kBACA,aAEA,mDACC,sBAIF,qDACC,kBAKH,uBACC,iDC1HD,wCACC,oDAGD,yCAGC,kBAEA,8CACC,kBACA,qBAGD,mDACC,kBACA,YACA,kBACA,iBAEA,uDACC,kBZlBF,YACC,6JYqBA,yDACC,SACA,UAEA,6DACC,gBAIF,yDACC,6BACA,eACA,gBACA,MVzCW,KU0CX,SACA,iBACA,WAGD,yDACC,qBACA,eC9CH,8BACC,oDAGD,+BACC,uBACA,ebHA,YACC,6JaKD,oCACC,cACA,iBACA,aAGA,wCACC,kBACA,kBAEA,4CACC,mBAIF,0CACC,MXzBkB,KW0BlB,iBACA,SAEA,+CACC,mBAKF,2CACC,iBAEA,qDACC,YAGD,mDACC,UAGD,mDACC,UAKH,qCACC,kBACA,UAEA,iBAEA,4CACC,MX3DkB,KW8DnB,0CACC,qBACA,kBACA,YAEA,gDAEC,kBAGD,gDACC,kBACA,UCzEH,oCACC,kBACA,aACA,yBAEA,kDACC,iBACA,iBAEA,0DACC,uBACA,eACA,WdVH,YACC,6JcWE,kBACA,yBACA,gBACA,iBACA,kBACA,oBCvBJ,OACC,aAGD,aACC,aAEA,uBACC,qBAEA,8BACC,aAGD,+BACC,cAKA,sCACC,cAGD,uCACC,aCtBJ,kCACC,oDAKA,yCACC,kBACA,QACA,gBACA,sBACA,aACA,gBAGD,2CACC,kBACA,WACA,6BACA,ehBhBD,YACC,6JgBmBA,uDACC,kBACA,UACA,YAEA,oEACC,WAGD,oEACC,UAGD,oEACC,UAGD,oEACC,UAGD,oEACC,UAGD,oEACC,SAKH,oCACC,kBACA,UAEA,yCACC,YAEA,mDACC,kBACA,uBACA,eACA,YACA,iBhB/DH,YACC,6JgBgEE,iBAGD,mDACC,kBACA,YACA,YACA,WACA,gBACA,gCACA,gGC5BJ,WACC,kBACA,gBAEA,mCAEC,cACA,oBACA,WACA,kBAID,kBAGC,WACA,WACA,mBACA,WA3DW,eA4DX,QAhDa,IAkBb,sCAoCD,iBACC,MACA,QACA,SACA,OACA,QAnEa,WAoEb,+EAGA,yBApDA,eAwDA,4BACA,0BAMD,mEAEC,kBACC,OAnGU,IAsGX,iBACC,0BAKF,0FACC,kBACC,aAGD,iBACC,0BAKF,iDACC,kBACC,WAGD,iBACC,0BAKF,gDACC,kBACC,WAGD,iBACC,0BAMH,oBACC,GACC,sCAKF,qBACC,GACC","file":"main.css"} \ No newline at end of file diff --git a/server/styles/scss/_almanac.scss b/server/styles/scss/_almanac.scss index 54356b1..ffe943d 100644 --- a/server/styles/scss/_almanac.scss +++ b/server/styles/scss/_almanac.scss @@ -1,5 +1,5 @@ -@use 'shared/_colors'as c; -@use 'shared/_utils'as u; +@use 'shared/_colors' as c; +@use 'shared/_utils' as u; #almanac-html.weather-display { background-image: url('../images/backgrounds/3.png'); @@ -11,62 +11,57 @@ @include u.text-shadow(); .sun { - display: table; - margin-left: 50px; - height: 100px; + // Use CSS Grid for cross-browser consistency + // Grid is populated in reading order (left-to-right, top-to-bottom): + display: grid; + grid-template-columns: auto auto auto; + grid-template-rows: auto auto auto; + gap: 0px 90px; + margin: 3px auto 5px auto; // align the bottom of the div with the background + width: fit-content; + line-height: 30px; - - &>div { - display: table-row; + .grid-item { + // Reset inherited styles that interfere with grid layout + width: auto; + height: auto; + padding: 0; + margin: 0; position: relative; - &>div { - display: table-cell; - } - } - - .days { - color: c.$column-header-text; - text-align: right; - top: -5px; - - .day { - padding-right: 10px; + // Column headers (day names) + &.header { + color: c.$column-header-text; + text-align: center; } - } - - .times { - text-align: right; - - .sun-time { - width: 200px; + // Row labels (Sunrise:, Sunset:) + &.row-label { + // color: c.$column-header-text; // screenshots show labels were white + text-align: right; } - &.times-1 { - top: -10px; - } - - &.times-2 { - top: -15px; + // Time values (sunrise/sunset) + &.time { + text-align: center; } } } .moon { position: relative; - top: -10px; - - padding: 0px 60px; + padding: 7px 50px; + line-height: 36px; .title { color: c.$column-header-text; + padding-left: 13px; } .day { display: inline-block; text-align: center; - width: 130px; + width: 132px; .icon { // shadow in image make it look off center @@ -82,4 +77,4 @@ -} \ No newline at end of file +} diff --git a/server/styles/scss/_current-weather.scss b/server/styles/scss/_current-weather.scss index eca3954..4606de3 100644 --- a/server/styles/scss/_current-weather.scss +++ b/server/styles/scss/_current-weather.scss @@ -1,5 +1,5 @@ -@use 'shared/_colors'as c; -@use 'shared/_utils'as u; +@use 'shared/_colors' as c; +@use 'shared/_utils' as u; .weather-display .main.current-weather { &.main { @@ -58,27 +58,19 @@ font-size: 24pt; } - .condition {} - .icon { - height: 100px; - img { - max-width: 126px; + margin: 0 auto; + display: block; } } .wind-container { - margin-bottom: 10px; + margin-left: 10px; + display: flex; &>div { - width: 45%; - display: inline-block; - margin: 0px; - } - - .wind-label { - margin-left: 5px; + width: 50%; } .wind { @@ -87,7 +79,8 @@ } .wind-gusts { - margin-left: 5px; + text-align: right; + font-size: 28px; } .location { @@ -99,4 +92,4 @@ text-wrap: nowrap; } } -} \ No newline at end of file +} diff --git a/server/styles/scss/_page.scss b/server/styles/scss/_page.scss index 806b171..747b4bd 100644 --- a/server/styles/scss/_page.scss +++ b/server/styles/scss/_page.scss @@ -9,6 +9,7 @@ body { font-family: "Star4000"; + margin: 0; @media (prefers-color-scheme: dark) { background-color: #000000; @@ -23,13 +24,17 @@ body { &.kiosk { margin: 0px; + padding: 0px; overflow: hidden; width: 100vw; + // Always use black background in kiosk mode, regardless of light/dark preference + background-color: #000000 !important; } } #divQuery { max-width: 640px; + padding: 8px; .buttons { display: inline-block; @@ -137,12 +142,26 @@ body { color: #ffffff; width: 100%; max-width: 640px; + margin: 0; // Ensure edge-to-edge display &.wide { max-width: 854px; } } +.content-wrapper { + padding: 8px; +} + +#divTwcMain { + width: 640px; + height: 480px; + + .wide & { + width: 854px; + } +} + .kiosk #divTwc { max-width: unset; } @@ -184,7 +203,11 @@ body { background-color: #000000; color: #ffffff; - width: 100%; + width: 640px; + + .wide & { + width: 854px; + } @media (prefers-color-scheme: dark) { background-color: rgb(48, 48, 48); @@ -196,25 +219,26 @@ body { padding-left: 6px; padding-right: 6px; - // scale down the buttons on narrower screens + // Use font-size scaling instead of zoom/transform to avoid layout gaps and preserve icon tap targets. + // While not semantically ideal, it works well for our fixed-layout design. @media (max-width: 550px) { - zoom: 0.90; + font-size: 0.90em; } @media (max-width: 500px) { - zoom: 0.80; + font-size: 0.80em; } @media (max-width: 450px) { - zoom: 0.70; + font-size: 0.70em; } @media (max-width: 400px) { - zoom: 0.60; + font-size: 0.60em; } @media (max-width: 350px) { - zoom: 0.50; + font-size: 0.50em; } } @@ -325,7 +349,6 @@ body { // background-image: none; width: unset; height: unset; - transform-origin: unset; } #loading { @@ -399,7 +422,8 @@ body { label { display: block; - max-width: 300px; + max-width: fit-content; + cursor: pointer; .alert { display: none; @@ -414,6 +438,13 @@ body { #divTwcBottom img { transform: scale(0.75); + + // Make icons larger in widescreen mode on mobile + @media (max-width: 550px) { + .wide & { + transform: scale(1.0); // Larger icons in widescreen + } + } } #divTwc:fullscreen, @@ -446,9 +477,7 @@ body { .kiosk { #divTwc #divTwcBottom { - >div { - display: none; - } + display: none; } } diff --git a/server/styles/scss/_progress.scss b/server/styles/scss/_progress.scss index 2bfcea0..98a3c6c 100644 --- a/server/styles/scss/_progress.scss +++ b/server/styles/scss/_progress.scss @@ -1,5 +1,5 @@ -@use 'shared/_colors'as c; -@use 'shared/_utils'as u; +@use 'shared/_colors' as c; +@use 'shared/_utils' as u; .weather-display .progress { @include u.text-shadow(); @@ -13,6 +13,7 @@ box-sizing: border-box; height: 310px; overflow: hidden; + line-height: 28px; .item { position: relative; @@ -117,4 +118,4 @@ transition: width 1s steps(6); } } -} \ No newline at end of file +} diff --git a/server/styles/scss/_travel.scss b/server/styles/scss/_travel.scss index 08f8f0e..add8642 100644 --- a/server/styles/scss/_travel.scss +++ b/server/styles/scss/_travel.scss @@ -1,5 +1,5 @@ -@use 'shared/_colors'as c; -@use 'shared/_utils'as u; +@use 'shared/_colors' as c; +@use 'shared/_utils' as u; .weather-display .main.travel { &.main { @@ -8,14 +8,11 @@ .column-headers { background-color: c.$column-header; height: 20px; - position: absolute; - width: 100%; - } - - .column-headers { position: sticky; top: 0px; + width: 100%; z-index: 5; + overflow: hidden; // prevent thin gaps between header and content div { display: inline-block; @@ -100,4 +97,4 @@ } } } -} \ No newline at end of file +} diff --git a/server/styles/scss/_weather-display.scss b/server/styles/scss/_weather-display.scss index 2af2af9..fd7ed22 100644 --- a/server/styles/scss/_weather-display.scss +++ b/server/styles/scss/_weather-display.scss @@ -94,11 +94,13 @@ &.has-scroll { width: 640px; + margin-top: 0; height: 310px; overflow: hidden; &.no-header { height: 400px; + margin-top: 0; // Reset for no-header case since the gap issue is header-related } } diff --git a/server/styles/scss/shared/_scanlines.scss b/server/styles/scss/shared/_scanlines.scss index afed6ba..66636f1 100644 --- a/server/styles/scss/shared/_scanlines.scss +++ b/server/styles/scss/shared/_scanlines.scss @@ -83,10 +83,12 @@ $scan-opacity: .75; bottom: 0; left: 0; z-index: $scan-z-index; - background: linear-gradient(to bottom, - transparent 50%, - $scan-color 51%); - background-size: 100% $scan-width*2; + // repeating-linear-gradient is more efficient than linear-gradient+background-size because it doesn't require the browser to calculate tiling + background: repeating-linear-gradient(to bottom, + transparent 0, + transparent $scan-width, + $scan-color $scan-width, + $scan-color calc($scan-width * 2)); @include scan-crt($scan-crt); // Prevent sub-pixel aliasing on scaled displays @@ -94,51 +96,21 @@ $scan-opacity: .75; image-rendering: pixelated; } - // Responsive scanlines for different display scenarios - - // High DPI displays - use original sizing - @media (-webkit-min-device-pixel-ratio: 2), - (min-resolution: 192dpi) { - &:before { - height: $scan-width; - } - - &:after { - background-size: 100% calc($scan-width * 2); - } + // Scanlines use dynamic thickness calculated by JavaScript + // JavaScript calculates optimal thickness to prevent banding at any scale factor + // The --scanline-thickness custom property is set by applyScanlineScaling() + // The modes (hairline, thin, medium, thick) force the base thickness selection + // Some modes may appear the same (e.g. hairline and thin) depending on the display + &:before { + height: var(--scanline-thickness, $scan-width); } - // Medium resolution displays (1024x768 and similar) - @media (max-width: 1200px) and (max-height: 900px) and (-webkit-max-device-pixel-ratio: 1.5) { - &:before { - height: calc($scan-width * 1.5); - } - - &:after { - background-size: 100% calc($scan-width * 3); - } - } - - // Low resolution displays - increase thickness to prevent banding - @media (max-width: 1024px) and (max-height: 768px) { - &:before { - height: calc($scan-width * 2); - } - - &:after { - background-size: 100% calc($scan-width * 4); - } - } - - // Very low resolution displays - @media (max-width: 800px) and (max-height: 600px) { - &:before { - height: calc($scan-width * 3); - } - - &:after { - background-size: 100% calc($scan-width * 6); - } + &:after { + background: repeating-linear-gradient(to bottom, + transparent 0, + transparent var(--scanline-thickness, $scan-width), + $scan-color var(--scanline-thickness, $scan-width), + $scan-color calc(var(--scanline-thickness, $scan-width) * 2)); } } diff --git a/views/index.ejs b/views/index.ejs index 43bc0d0..992c0e8 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -14,6 +14,7 @@ + @@ -80,64 +81,66 @@
-
-
-
-
WeatherStar 4000+
-
v<%- version %>
-
Enter your location above to continue
-
-
-
- <%- include('partials/progress.ejs') %> -
-
- <%- include('partials/hourly.ejs') %> -
-
- <%- include('partials/hourly-graph.ejs') %> -
-
- <%- include('partials/travel.ejs') %> -
-
- <%- include('partials/current-weather.ejs') %> -
-
- <%- include('partials/local-forecast.ejs') %> -
-
- <%- include('partials/latest-observations.ejs') %> -
-
- <%- include('partials/regional-forecast.ejs') %> -
-
- <%- include('partials/almanac.ejs') %> -
-
- <%- include('partials/spc-outlook.ejs') %> -
-
- <%- include('partials/extended-forecast.ejs') %> -
-
- <%- include('partials/radar.ejs') %> -
-
- <%- include('partials/hazards.ejs') %> -
-
-
-
- - - - -
-
- -
+
+
+
+
+
WeatherStar 4000+
+
v<%- version %>
+
Enter your location above to continue
+
+
+
+ <%- include('partials/progress.ejs') %> +
+
+ <%- include('partials/hourly.ejs') %> +
+
+ <%- include('partials/hourly-graph.ejs') %> +
+
+ <%- include('partials/travel.ejs') %> +
+
+ <%- include('partials/current-weather.ejs') %> +
+
+ <%- include('partials/local-forecast.ejs') %> +
+
+ <%- include('partials/latest-observations.ejs') %> +
+
+ <%- include('partials/regional-forecast.ejs') %> +
+
+ <%- include('partials/almanac.ejs') %> +
+
+ <%- include('partials/spc-outlook.ejs') %> +
+
+ <%- include('partials/extended-forecast.ejs') %> +
+
+ <%- include('partials/radar.ejs') %> +
+
+ <%- include('partials/hazards.ejs') %> +
+
+
+
+
+ + + + +
+
+ +
@@ -152,41 +155,42 @@
-
+
+
-
- More information + +
+ +
Selected displays
+
+ +
+ +
Settings
+
+
+ +
Sharing
+
+ Copy Permalink Link copied to clipboard! + +
+ +
Forecast Information
+
+ Location:
+ Station Id:
+ Radar Id:
+ Zone Id:
+ Music: Not playing
+ Ws4kp Version: <%- version %> +
-
- -
Selected displays
-
- -
- -
Settings
-
-
- -
Sharing
-
- Copy Permalink Link copied to clipboard! - -
-
- -
Forecast Information
-
- Location:
- Station Id:
- Radar Id:
- Zone Id:
- Music: Not playing
- Ws4kp Version: <%- version %> -
diff --git a/views/partials/almanac.ejs b/views/partials/almanac.ejs index fa8f81a..6bf0a09 100644 --- a/views/partials/almanac.ejs +++ b/views/partials/almanac.ejs @@ -1,21 +1,15 @@ <%- include('header.ejs', {title:'Almanac', hasTime: true}) %>
-
-
-
Monday
-
Tuesday
-
-
-
Sunrise:
-
6:24 am
-
6:25 am
-
-
-
Sunset:
-
6:24 am
-
6:25 am
-
+
+
+
+
Sunrise:
+
+
+
Sunset:
+
+
Moon Data:
@@ -28,4 +22,4 @@
-<%- include('scroll.ejs') %> \ No newline at end of file +<%- include('scroll.ejs') %>