diff --git a/index.html b/index.html index ca20e5e..0a07c27 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,23 @@ --faint: #d9d9d9; --paper: #fbfbfa; --panel: rgba(255, 255, 255, 0.72); + --panel-hover: #fff; --accent: #111; + --steam: #c9c9c7; + --skyline: rgba(21, 21, 21, 0.07); + } + + body.night { + color-scheme: dark; + --ink: #f2f5ff; + --muted: #aeb8cf; + --faint: rgba(255, 255, 255, 0.22); + --paper: #07101f; + --panel: rgba(10, 21, 39, 0.74); + --panel-hover: rgba(18, 34, 61, 0.92); + --accent: #dce8ff; + --steam: #6f7e99; + --skyline: rgba(0, 0, 0, 0.28); } * { @@ -48,6 +64,9 @@ Arial, sans-serif; font-feature-settings: "kern"; + transition: + background 280ms ease, + color 280ms ease; } main { @@ -77,6 +96,125 @@ opacity: 0; } + .night-details { + position: fixed; + inset: 0; + z-index: 0; + overflow: hidden; + opacity: 0; + pointer-events: none; + transition: opacity 280ms ease; + } + + body.night .night-details { + opacity: 1; + } + + .moon { + position: absolute; + top: clamp(34px, 8vh, 82px); + right: clamp(70px, 14vw, 180px); + width: clamp(42px, 7vw, 72px); + aspect-ratio: 1; + border-radius: 50%; + background: #f3e9b3; + box-shadow: 0 0 28px rgba(243, 233, 179, 0.36); + } + + .moon::after { + content: ""; + position: absolute; + top: -6%; + left: 24%; + width: 100%; + height: 100%; + border-radius: inherit; + background: var(--paper); + } + + .star { + position: absolute; + width: 3px; + height: 3px; + border-radius: 50%; + background: rgba(235, 241, 255, 0.92); + box-shadow: 0 0 8px rgba(235, 241, 255, 0.58); + } + + .star:nth-child(2) { + top: 18%; + left: 12%; + } + + .star:nth-child(3) { + top: 11%; + left: 34%; + width: 2px; + height: 2px; + } + + .star:nth-child(4) { + top: 28%; + left: 74%; + } + + .star:nth-child(5) { + top: 42%; + left: 22%; + width: 2px; + height: 2px; + } + + .star:nth-child(6) { + top: 36%; + left: 88%; + } + + .star:nth-child(7) { + top: 15%; + left: 58%; + width: 2px; + height: 2px; + } + + .skyline { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 18vh; + min-height: 116px; + background: + linear-gradient(90deg, transparent 12px, rgba(255, 214, 118, 0.65) 12px 17px, transparent 17px 40px) 0 38% / 72px 36px repeat-x, + linear-gradient(to top, var(--skyline), var(--skyline)); + clip-path: polygon( + 0 46%, + 7% 46%, + 7% 24%, + 15% 24%, + 15% 38%, + 24% 38%, + 24% 14%, + 34% 14%, + 34% 48%, + 44% 48%, + 44% 30%, + 54% 30%, + 54% 42%, + 64% 42%, + 64% 20%, + 74% 20%, + 74% 48%, + 84% 48%, + 84% 28%, + 94% 28%, + 94% 42%, + 100% 42%, + 100% 100%, + 0 100% + ); + } + .hero-inner { display: grid; gap: 18px; @@ -127,18 +265,24 @@ .steam { position: fixed; z-index: 0; - color: #c9c9c7; + color: var(--steam); font-size: 18px; pointer-events: none; animation: puff 1400ms ease-out forwards; will-change: transform, opacity; } - .mute-toggle { + .controls { position: fixed; top: 16px; right: 16px; z-index: 10; + display: flex; + gap: 8px; + } + + .theme-toggle, + .mute-toggle { display: inline-flex; width: 38px; height: 38px; @@ -157,12 +301,14 @@ transform 150ms ease; } + .theme-toggle:hover, .mute-toggle:hover { - background: #fff; + background: var(--panel-hover); border-color: #b8b8b8; transform: translateY(-1px); } + .theme-toggle:focus-visible, .mute-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; @@ -266,6 +412,17 @@
+ +
🚂 @@ -273,15 +430,27 @@
- +
+ + + +
0 trains dispatched
@@ -297,8 +466,11 @@ const dispatcher = document.querySelector("#dispatcher"); const hero = document.querySelector("#hero"); + const themeButton = document.querySelector("#theme"); const muteButton = document.querySelector("#mute"); const counter = document.querySelector("#counter"); + const themeMeta = document.querySelector('meta[name="theme-color"]'); + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); const choo = new Audio("./public/choo-choo.mp3"); choo.preload = "auto"; @@ -308,6 +480,10 @@ let lastLane = -1; let lastDirection = "rtl"; let muted = localStorage.getItem("wtc:muted") === "1"; + let themePreference = localStorage.getItem("wtc:theme"); + let nightMode = + themePreference === "night" || + (themePreference === null && systemTheme.matches); function pick(items) { return items[Math.floor(Math.random() * items.length)]; @@ -320,6 +496,30 @@ localStorage.setItem("wtc:muted", muted ? "1" : "0"); } + function syncThemeButton() { + document.body.classList.toggle("night", nightMode); + themeButton.textContent = nightMode ? "☀️" : "🌙"; + themeButton.setAttribute( + "aria-label", + nightMode ? "Turn off night mode" : "Turn on night mode", + ); + themeButton.setAttribute("aria-pressed", String(nightMode)); + themeMeta.setAttribute("content", nightMode ? "#07101f" : "#ffffff"); + } + + function setThemePreference(value) { + themePreference = value; + nightMode = value === "night"; + localStorage.setItem("wtc:theme", value); + syncThemeButton(); + } + + function syncSystemTheme() { + if (themePreference !== null) return; + nightMode = systemTheme.matches; + syncThemeButton(); + } + function updateCounter() { const noun = count === 1 ? "train" : "trains"; counter.textContent = `${count} ${noun} dispatched`; @@ -398,6 +598,11 @@ } dispatcher.addEventListener("click", dispatchTrain); + themeButton.addEventListener("click", (event) => { + event.stopPropagation(); + setThemePreference(nightMode ? "day" : "night"); + }); + muteButton.addEventListener("click", (event) => { event.stopPropagation(); muted = !muted; @@ -408,12 +613,16 @@ if (event.code === "Space" || event.code === "Enter") { event.preventDefault(); dispatchTrain(); + } else if (event.key === "n" || event.key === "N") { + setThemePreference(nightMode ? "day" : "night"); } else if (event.key === "m" || event.key === "M") { muted = !muted; syncMuteButton(); } }); + systemTheme.addEventListener("change", syncSystemTheme); + syncThemeButton(); syncMuteButton(); updateCounter();