diff --git a/assets/js/components.js b/assets/js/components.js
index 74b346b..6564c60 100644
--- a/assets/js/components.js
+++ b/assets/js/components.js
@@ -16,7 +16,7 @@
import { h, Fragment } from '../vendor/preact.js';
import htm from '../vendor/htm.js';
import {
- skyFillForElev, grassFillForElev,
+ skyGradientForElev, skyFillForElev, grassFillForElev,
moonPhaseFraction,
} from './utils.js';
@@ -42,20 +42,41 @@ const html = htm.bind(h);
export function SkyScope({ elev, dt, size = 26 }) {
const r = size / 2;
const innerR = r - 1.2;
- const skyFill = skyFillForElev(elev);
- const isDay = elev > -3;
- const elevClamped = Math.max(-30, Math.min(90, elev));
+
+ // Rising if local hour < 12 (dt.getUTCHours() == local hour after timezone fix)
+ const localHour = dt ? dt.getUTCHours() : 12;
+ const isRising = localHour < 12;
+
+ const skyGrad = skyGradientForElev(elev, isRising);
+ // Flat horizon colour used for moon shadow fill
+ const skyHorizon = skyGrad.bot;
+
+ const isDay = elev > 0; // sun only drawn when actually above horizon
+ const isTwil = elev > -8 && !isDay; // moon shown in twilight too
+ const isNight = elev <= -8;
+
+ // Sun position — clamped so it never goes below the horizon line (y = r)
+ const elevClamped = Math.max(0, Math.min(90, elev));
const sunY = r - Math.sin((elevClamped * Math.PI) / 180) * (innerR - 2.5);
const sunR = Math.max(2.2, innerR * 0.32);
+
+ // Near-horizon sun gets a warm glow halo
+ const sunGlowOpacity = elev < 10 ? Math.max(0, 1 - elev / 10) * 0.65 : 0.25;
+ const sunGlowR = sunR + (elev < 6 ? 3.5 : 1.8);
+ const sunGlowColor = isRising ? '#ff8c3a' : '#ff9a6a';
+
const phase = moonPhaseFraction(dt);
const moonR = innerR * 0.55;
const phaseOffset = Math.cos(2 * Math.PI * phase) * moonR;
const moonLitFromRight = phase < 0.5;
const shadowCx = r + (moonLitFromRight ? -phaseOffset : phaseOffset);
const grass = grassFillForElev(elev);
- const uid = `${size}-${Math.round(elev * 10)}-${Math.round(phase * 1000)}`;
- const clipId = `scope-clip-${uid}`;
- const grassId = `scope-grass-${uid}`;
+
+ const uid = `ss-${size}-${Math.round(elev * 10)}-${Math.round(phase * 1000)}-${isRising ? 'r' : 's'}`;
+ const clipId = `clip-${uid}`;
+ const skyId = `sky-${uid}`;
+ const grassId = `grass-${uid}`;
+
return html`
`;
}
@@ -254,16 +300,24 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date() })
// grass on the bottom, sun positioned by elevation (or moon at night
// with its phase shadow). Mirrors the little hourly SkyScopes.
const cx = 100, cy = 100, R = 86;
- const isDay = elev > -3;
- const skyFill = skyFillForElev(elev);
+ const localHour = dt ? dt.getUTCHours() : 12;
+ const isRising = localHour < 12;
+ const isDay = elev > 0; // sun only above the actual horizon
+ const isTwil = elev > -8 && !isDay;
+ const skyGrad = skyGradientForElev(elev, isRising);
+ const skyHorizon = skyGrad.bot;
const grass = grassFillForElev(elev);
const phase = moonPhaseFraction(dt);
- const lensR = 95; // full back of the dial
- const elevClamped = Math.max(-30, Math.min(90, elev));
+ const lensR = 95;
+ const elevClamped = Math.max(0, Math.min(90, elev)); // clamp to above horizon
const sunY = cy - Math.sin((elevClamped * Math.PI) / 180) * (lensR - 18);
const sunDrawR = 14;
+ // Near-horizon glow for the big dial
+ const sunGlowR = sunDrawR + (elev < 10 ? 7 : 3);
+ const sunGlowOp = elev < 10 ? Math.max(0, 1 - elev / 10) * 0.55 : 0.2;
+ const sunGlowCol = isRising ? '#ff8c3a' : '#ff9a6a';
const moonDrawR = 15;
- const moonY = cy - 45; // float in the upper sky, away from the readout
+ const moonY = cy - 45;
const phaseOffset = Math.cos(2 * Math.PI * phase) * moonDrawR;
const moonLitFromRight = phase < 0.5;
const moonShadowCx = cx + (moonLitFromRight ? -phaseOffset : phaseOffset);
@@ -327,28 +381,39 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date() })
+
+
+
+
+
+
+
+
+
-
+
- ${isDay
- ? html`<${Fragment}>
-
-
- >`
- : html`<${Fragment}>
-
-
-
- >`}
+ ${isDay && html`<${Fragment}>
+
+
+ >`}
+ ${(isTwil || (!isDay && !isTwil)) && html`<${Fragment}>
+
+
+
+
+ >`}
${stressBands.map((b, i) => html`
30° high noon (bright blue)
-// > 10° mid-sky (pale blue)
-// > 3° morning/afternoon (warm gold)
-// > -3° golden hour / sunset (deep amber)
-// > -8° civil twilight (dusky lilac)
-// > -14° nautical twilight (deep blue-violet)
-// else night (indigo)
-// To shift "when sunset starts" visually, tweak the elevation
-// thresholds. To recolour: replace the hex codes.
+// isRising: true when sun is climbing (local hour < 12).
+//
+// Sunrise palette → reds, oranges, yellows at the horizon.
+// Sunset palette → pinks, purples, mauves at the horizon.
+// Both share the same deep blue zenith at high elevations.
// ═══════════════════════════════════════════════════════════════════
-export function skyFillForElev(e) {
- if (e > 30) return '#9fd3ef';
- if (e > 10) return '#c8e3ee';
- if (e > 3) return '#ffd596';
- if (e > -3) return '#f59b6e';
- if (e > -8) return '#9279b0';
- if (e > -14) return '#3a2f60';
- return '#1a1538';
+export function skyGradientForElev(e, isRising) {
+ // Full daytime — blue sky, paler toward horizon
+ if (e > 30) return { top: '#5bb8e8', bot: '#9fd3ef' };
+ if (e > 10) return { top: '#7cc5ec', bot: '#c8e3ee' };
+
+ // Low sun — golden hour / near-horizon
+ if (e > 3) {
+ return isRising
+ ? { top: '#6aaddb', bot: '#ffb347' } // sunrise: blue → warm amber/orange
+ : { top: '#7b8fc4', bot: '#ffb877' }; // sunset: blue-violet → golden
+ }
+
+ // Sun at/just below horizon — the dramatic band
+ if (e > -3) {
+ return isRising
+ ? { top: '#4a7aaa', bot: '#e8622a' } // sunrise: deep blue → vivid red-orange
+ : { top: '#7a5090', bot: '#e8826a' }; // sunset: violet → coral/pink
+ }
+
+ // Civil twilight
+ if (e > -8) {
+ return isRising
+ ? { top: '#2a3a6a', bot: '#a04828' } // pre-dawn: indigo → burnt sienna
+ : { top: '#5a3572', bot: '#c07080' }; // dusk: purple → dusty rose/mauve
+ }
+
+ // Nautical twilight
+ if (e > -14) return { top: '#1e1a50', bot: '#3a2f60' };
+
+ // Night
+ return { top: '#0e0c28', bot: '#1a1538' };
+}
+
+// Convenience flat colour — returns the bot (horizon) colour for contexts
+// that still need a single fill (e.g. moon shadow fill).
+export function skyFillForElev(e, isRising = false) {
+ return skyGradientForElev(e, isRising).bot;
}
// Grass palette — mirrors skyFillForElev but for the ground.