diff --git a/docs/img/dashboard-dark-forecast.png b/docs/img/dashboard-dark-forecast.png new file mode 100644 index 0000000..76707db Binary files /dev/null and b/docs/img/dashboard-dark-forecast.png differ diff --git a/docs/img/dashboard-dark.png b/docs/img/dashboard-dark.png new file mode 100644 index 0000000..941b031 Binary files /dev/null and b/docs/img/dashboard-dark.png differ diff --git a/src/static/dashboard.html b/src/static/dashboard.html index c5f4ae8..3c07e1e 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -33,6 +33,57 @@ --red: #cc4b37; --border: #dce7e3; --shadow: 0 16px 40px rgba(23, 57, 67, .10); + --surface: #ffffff; /* buttons, inputs, overlays */ + --surface-2: #f1f7f5; /* hover rows, popup metrics */ + --surface-3: #f7fbfa; /* outlook box */ + --overlay: rgba(255,255,255,.93); + --overlay-border: rgba(207,224,218,.9); + --map-bg: #dcebea; + --glow: rgba(56, 180, 213, .12); + --map-filter: none; + --chart-grid: rgba(19,43,53,.08); + --mint-ink: #146644; + --mint-border: #b6dfc9; + --ok-bg: #e2f3ea; --ok-ink: #0c5138; --ok-border: #bfe3d2; + --watch-bg: #fdf1dc; --watch-ink: #6b4a05; --watch-border: #f0d9a8; + --danger-bg: #fbe3de; --danger-ink: #7c1d10; --danger-border: #f2c0b6; + --demo-bg: #fdeeda; --demo-ink: #9a6200; + color-scheme: light; + } + /* Dark theme: same hues, inverted lightness. Flow/rain/marker colours + on the map are data encodings and stay identical in both themes. */ + [data-theme="dark"] { + --ink: #e6eef1; + --muted: #97a9b0; + --paper: #0f1a1f; + --card: #16252c; + --river: #3fb3d8; + --river-light: #5ccbe8; + --mint: #143b2c; + --green: #3fb07f; + --amber: #e6a83a; + --red: #e46a55; + --border: #27393f; + --shadow: 0 16px 40px rgba(0, 0, 0, .45); + --surface: #1c2c33; + --surface-2: #213238; + --surface-3: #1a2a30; + --overlay: rgba(22,37,44,.92); + --overlay-border: rgba(64,86,94,.9); + --map-bg: #1a262b; + --glow: rgba(63, 179, 216, .10); + /* OSM tiles are light; invert + rotate keeps roads/labels legible + while the water/river overlays (drawn in SVG, unfiltered) keep + their real colours. */ + --map-filter: invert(1) hue-rotate(180deg) brightness(.92) contrast(.9) saturate(.75); + --chart-grid: rgba(230,238,241,.10); + --mint-ink: #7fd7ac; + --mint-border: #2a5c45; + --ok-bg: #143b2c; --ok-ink: #9fe3c3; --ok-border: #2a6b4d; + --watch-bg: #3d2f0d; --watch-ink: #f3d489; --watch-border: #6b4f14; + --danger-bg: #421c15; --danger-ink: #ffb3a6; --danger-border: #7a2f22; + --demo-bg: #3d2f0d; --demo-ink: #f3d489; + color-scheme: dark; } * { box-sizing: border-box; } html, body { margin: 0; min-height: 100%; } @@ -43,7 +94,7 @@ color: var(--ink); overflow-x: hidden; background: - radial-gradient(circle at 8% 0%, rgba(56, 180, 213, .12), transparent 25rem), + radial-gradient(circle at 8% 0%, var(--glow), transparent 25rem), var(--paper); /* Thai families first so Thai text uses a proper Thai face where one is installed (Android/iOS/Windows all ship one); Latin falls @@ -60,7 +111,13 @@ html[lang="th"] body { line-break: normal; overflow-wrap: anywhere; } html[lang="th"] .stat-value, html[lang="th"] .flow-value { overflow-wrap: normal; } .lang-toggle { padding: 9px 12px; font-size: .78rem; font-weight: 800; white-space: nowrap; } - .lang-toggle[data-active-lang="th"] { background: var(--mint); border-color: #b6dfc9; color: #146644; } + .lang-toggle[data-active-lang="th"] { background: var(--mint); border-color: var(--mint-border); color: var(--mint-ink); } + .theme-toggle { padding: 9px 11px; font-size: .95rem; line-height: 1; } + .leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--card); color: var(--ink); } + .leaflet-container a.leaflet-popup-close-button { color: var(--muted); } + .leaflet-bar a, .leaflet-control-attribution { background: var(--surface); color: var(--ink); border-color: var(--border); } + .leaflet-control-attribution a { color: var(--river); } + [data-theme="dark"] .leaflet-control-attribution { background: var(--overlay); } .shell { max-width: 1500px; margin: 0 auto; padding: 24px; } header { display: flex; align-items: center; justify-content: space-between; gap: 20px; @@ -78,16 +135,16 @@ .header-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .live-pill { display: flex; gap: 8px; align-items: center; padding: 9px 13px; border-radius: 999px; - background: var(--mint); color: #146644; font-weight: 750; font-size: .8rem; + background: var(--mint); color: var(--mint-ink); font-weight: 750; font-size: .8rem; } .live-dot { width: 8px; height: 8px; border-radius: 50%; background: #22a66c; box-shadow: 0 0 0 5px rgba(34,166,108,.12); } - .live-pill.demo { background: #fdeeda; color: #9a6200; } + .live-pill.demo { background: var(--demo-bg); color: var(--demo-ink); } .live-pill.demo .live-dot { background: #e6a23c; box-shadow: 0 0 0 5px rgba(230,162,60,.15); } button { - border: 1px solid var(--border); border-radius: 11px; background: white; color: var(--ink); + border: 1px solid var(--border); border-radius: 11px; background: var(--surface); color: var(--ink); padding: 10px 14px; cursor: pointer; font-weight: 700; box-shadow: 0 3px 10px rgba(22,52,62,.05); } - button:hover { border-color: #a9c4bb; transform: translateY(-1px); } + button:hover { border-color: var(--river-light); transform: translateY(-1px); } button:disabled { opacity: .55; cursor: wait; transform: none; } .stats { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 14px; margin-bottom: 14px; } .stat { @@ -102,13 +159,14 @@ .workspace { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 14px; min-height: 640px; } .map-card, .side-card { background: var(--card); border: 1px solid var(--border); border-radius: 19px; box-shadow: var(--shadow); overflow: hidden; } .map-card { position: relative; } - #station-map { height: 640px; width: 100%; background: #dcebea; } + #station-map { height: 640px; width: 100%; background: var(--map-bg); } + .leaflet-tile-pane { filter: var(--map-filter); } .map-overlay { position: absolute; z-index: 500; top: 16px; left: 52px; right: 16px; display: flex; justify-content: space-between; align-items: flex-start; pointer-events: none; } .map-heading, .legend { - background: rgba(255,255,255,.93); backdrop-filter: blur(9px); border: 1px solid rgba(207,224,218,.9); + background: var(--overlay); backdrop-filter: blur(9px); border: 1px solid var(--overlay-border); border-radius: 13px; padding: 11px 13px; box-shadow: 0 7px 20px rgba(22,58,68,.12); pointer-events: auto; /* .map-overlay disables events; re-enable for the legend's rain toggle */ } @@ -130,7 +188,7 @@ width: 100%; display: grid; grid-template-columns: 40px minmax(0,1fr) auto; gap: 10px; align-items: center; padding: 11px; border: 0; border-radius: 12px; box-shadow: none; text-align: left; background: transparent; } - .station-row:hover { background: #f1f7f5; transform: none; } + .station-row:hover { background: var(--surface-2); transform: none; } .station-code { width: 40px; height: 40px; display: grid; place-items: center; border-radius: 11px; color: white; font-size: .68rem; font-weight: 850; } .station-code.long { font-size: .55rem; word-break: break-all; line-height: 1.15; padding: 2px; text-align: center; } .station-name { overflow: hidden; } @@ -139,9 +197,9 @@ .station-name span { color: var(--muted); font-size: .68rem; margin-top: 3px; } .flow-value { text-align: right; font-size: .82rem; font-weight: 800; } .flow-value span { display: block; color: var(--muted); font-size: .61rem; font-weight: 650; margin-top: 2px; } - .loading-panel, .error-panel { position: absolute; z-index: 600; inset: 0; display: grid; place-items: center; background: rgba(243,247,245,.88); } - .loading-card { background: white; padding: 18px 22px; border-radius: 14px; box-shadow: var(--shadow); font-weight: 750; } - .error-panel { display: none; color: #8d2f22; text-align: center; padding: 25px; } + .loading-panel, .error-panel { position: absolute; z-index: 600; inset: 0; display: grid; place-items: center; background: var(--overlay); } + .loading-card { background: var(--card); padding: 18px 22px; border-radius: 14px; box-shadow: var(--shadow); font-weight: 750; } + .error-panel { display: none; color: var(--red); text-align: center; padding: 25px; } .marker-wrap { background: none; border: 0; } .flow-marker { --marker-color: #087da5; --marker-size: 26px; @@ -181,7 +239,7 @@ .risk-chips { display: flex; gap: 6px; } .risk-chip { flex: 1; text-align: center; border-radius: 8px; padding: 5px 4px; font-size: .64rem; font-weight: 800; color: white; } .risk-chip span { display: block; font-weight: 650; font-size: .58rem; opacity: .85; } - .p1-outlook { border: 1px solid var(--border); border-left: 4px solid var(--river); border-radius: 12px; padding: 12px 14px; margin-top: 14px; background: #f7fbfa; } + .p1-outlook { border: 1px solid var(--border); border-left: 4px solid var(--river); border-radius: 12px; padding: 12px 14px; margin-top: 14px; background: var(--surface-3); } .p1-outlook-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; } .p1-outlook-head strong { font-size: .88rem; } .p1-peak { color: var(--muted); font-size: .76rem; } @@ -195,16 +253,16 @@ .popup h3 { margin: 4px 0 2px; font-size: 1rem; } .popup-th { color: var(--muted); font-size: .74rem; } .popup-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; } - .popup-metric { background: #f1f7f5; padding: 8px; border-radius: 9px; } + .popup-metric { background: var(--surface-2); padding: 8px; border-radius: 9px; } .popup-metric span { display: block; color: var(--muted); font-size: .62rem; } .popup-metric strong { display: block; margin-top: 2px; font-size: .86rem; } .popup-time { margin-top: 9px; color: var(--muted); font-size: .64rem; } /* Verdict banner: light tinted backgrounds with dark ink (contrast-safe) */ - #flood-verdict.ok { background: #e2f3ea; color: #0c5138; border-color: #bfe3d2; } - #flood-verdict.watch { background: #fdf1dc; color: #6b4a05; border-color: #f0d9a8; } - #flood-verdict.danger { background: #fbe3de; color: #7c1d10; border-color: #f2c0b6; } + #flood-verdict.ok { background: var(--ok-bg); color: var(--ok-ink); border-color: var(--ok-border); } + #flood-verdict.watch { background: var(--watch-bg); color: var(--watch-ink); border-color: var(--watch-border); } + #flood-verdict.danger { background: var(--danger-bg); color: var(--danger-ink); border-color: var(--danger-border); } #legend-toggle { - display: none; pointer-events: auto; background: rgba(255,255,255,.93); border: 1px solid rgba(207,224,218,.9); + display: none; pointer-events: auto; background: var(--overlay); border: 1px solid var(--overlay-border); border-radius: 11px; padding: 8px 12px; font-size: .72rem; font-weight: 800; color: var(--ink); box-shadow: 0 7px 20px rgba(22,58,68,.12); } @@ -277,6 +335,7 @@