diff --git a/server/images/logos/app-icon-180.png b/server/images/logos/app-icon-180.png new file mode 100644 index 0000000..8f42321 Binary files /dev/null and b/server/images/logos/app-icon-180.png differ 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') %>