diff --git a/assets/css/table.css b/assets/css/table.css
index b7400ee..0abd179 100644
--- a/assets/css/table.css
+++ b/assets/css/table.css
@@ -103,33 +103,15 @@
border-left: none;
}
-/* The selected-tab background is a different gradient (radial vs linear), and
- CSS can't tween between gradients — so we paint the active gradient as an
- overlay pseudo-element behind the tab content and fade its opacity on hover.
- z-index:-1 keeps it above the tab's own background but below the labels. */
-.utci-day-tab::before {
- content: '';
- position: absolute;
- inset: 0;
- background: var(--w-bg-active);
- opacity: 0;
- transition: opacity 0.2s ease;
- pointer-events: none;
- z-index: -1;
-}
-
-/* On hover, a non-active tab fades in the selected gradient + filter so it
- previews the selected shade (!important to beat the inline filter). */
-.utci-day-tab:hover:not(.active)::before {
- opacity: 1;
-}
-.utci-day-tab:hover:not(.active) {
- filter: saturate(1.1) brightness(1.04) !important;
+.utci-day-tab:hover:not(.active):not(.locked) {
+ filter: brightness(1.14) !important;
}
.utci-day-tab.active {
color: #c8922a;
- border-bottom-color: #c8922a;
+ outline: 3px solid #c8922a;
+ outline-offset: -2px;
+ z-index: 1;
}
/* The "Mon 12 May" date underneath each tab name */
diff --git a/assets/js/app.js b/assets/js/app.js
index f3ca7f0..ddb14b1 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1514,20 +1514,23 @@ export function UTCIForecast() {
${((glanceSummary && glanceSummary.length > 0) || eventGlanceItems.length > 0) && html`
At a glance${glanceDate ? ` · ${glanceDate}` : ''}
- ${glanceSummary.map(({ icon, label, value, alert }) => html`
-
- ${icon}
- ${label}
- ${value}
-
- `)}
- ${eventGlanceItems.map(({ icon, label, value, alert }) => html`
-
- ${icon}
- ${label}
- ${value}
-
- `)}
+ ${(() => {
+ const heatEvents = eventGlanceItems.filter(e => /heat spike|heat warning|extreme heat/i.test(e.label)).map(e => ({ ...e, alert: true }));
+ const otherEvents = eventGlanceItems.filter(e => !/heat spike|heat warning|extreme heat/i.test(e.label));
+ const renderRow = ({ icon, label, value, alert }, keyPrefix = '') => html`
+
+ ${icon}
+ ${label}
+ ${value}
+
`;
+ return [
+ ...glanceSummary.flatMap(item => [
+ renderRow(item),
+ ...(item.label === 'Peak felt temp' ? heatEvents.map(e => renderRow(e, 'ev-')) : []),
+ ]),
+ ...otherEvents.map(e => renderRow(e, 'ev-')),
+ ];
+ })()}
`}
${visible.length > 0 && html`
diff --git a/assets/js/components/DayTabs.js b/assets/js/components/DayTabs.js
index 0ec0d92..e056171 100644
--- a/assets/js/components/DayTabs.js
+++ b/assets/js/components/DayTabs.js
@@ -69,6 +69,15 @@ function weatherGradient(r, g, b) {
// colour, drawn as a radial gradient whose strong colour sits at the outer
// edge and softens toward a lighter centre — so the hue reads as radiating
// inward from the outside of the tab.
+function weatherGradientNeutral(r, g, b) {
+ const blend = (c) => Math.round(c + (255 - c) * 0.58);
+ const [mr, mg, mb] = [blend(r), blend(g), blend(b)];
+ const lighten = (c) => Math.min(255, Math.round(c + (255 - c) * 0.38));
+ const center = `rgb(${lighten(mr)},${lighten(mg)},${lighten(mb)})`;
+ const edge = `rgb(${mr},${mg},${mb})`;
+ return `radial-gradient(circle at 50% 50%, ${center} 0%, ${center} 28%, ${edge} 100%)`;
+}
+
function weatherGradientActive(r, g, b) {
const blend = (c) => Math.round(c + (255 - c) * 0.42);
const [mr, mg, mb] = [blend(r), blend(g), blend(b)];
@@ -295,17 +304,20 @@ export function DayTabs({
const dayHi = utcipVals.length ? Math.round(Math.max(...utcipVals)) : null;
const dayLo = utcipVals.length ? Math.round(Math.min(...utcipVals)) : null;
- // Day-tab weather icon - use daytime rows where available
- const dayRows = d.rows.filter(r => r.elev > 0);
- const repRows = dayRows.length > 0 ? dayRows : d.rows;
+ // Day-tab weather icon - use core daylight rows (elev > 10°) where
+ // available, falling back to any above-horizon rows, then all rows.
+ const coreRows = d.rows.filter(r => r.elev > 10);
+ const aboveRows = d.rows.filter(r => r.elev > 0);
+ const repRows = coreRows.length > 0 ? coreRows : aboveRows.length > 0 ? aboveRows : d.rows;
const dayPrecip = repRows.reduce((s, r) => s + (r.precip || 0), 0);
const daySnow = repRows.reduce((s, r) => s + (r.snow || 0), 0);
const catCounts = {};
repRows.forEach(r => { if (r.cloudCat) catCounts[r.cloudCat] = (catCounts[r.cloudCat] || 0) + 1; });
const modalCloudCat = Object.keys(catCounts).sort((a, b) => catCounts[b] - catCounts[a])[0] || 'clear';
- const midRow = repRows[Math.floor(repRows.length / 2)];
- const repElev = midRow ? midRow.elev : 45;
- const repDt = midRow ? midRow.dt : dDate;
+ // Use solar-noon row (highest elevation) as the representative row
+ const noonRow = repRows.reduce((best, r) => (r.elev > (best?.elev ?? -Infinity) ? r : best), null);
+ const repElev = noonRow ? noonRow.elev : 45;
+ const repDt = noonRow ? noonRow.dt : dDate;
const showPrecip = dayPrecip >= 0.3 || daySnow >= 0.1;
// Base RGB for each weather condition — pastelised by weatherGradient()
@@ -320,6 +332,7 @@ export function DayTabs({
: modalCloudCat === 'wispy' ? [200, 180, 130]
: [220, 190, 50];
const wBg = weatherGradient(wR, wG, wB);
+ const wBgNeutral = weatherGradientNeutral(wR, wG, wB);
const wBgActive = weatherGradientActive(wR, wG, wB);
const wText = '#2a1d10';
@@ -340,8 +353,7 @@ export function DayTabs({
? `${band.label} · SunScope Extra unlocks day ${i + 1}`
: `${band.label} · day ${i + 1} of 14`}
style=${{
- background: isActive ? wBgActive : wBg,
- '--w-bg-active': wBgActive,
+ background: isActive ? wBgActive : wBgNeutral,
color: wText,
borderTop: '1px solid #c9b08a',
borderRight: 'none',
@@ -350,7 +362,6 @@ export function DayTabs({
opacity: locked ? 0.5 : 1,
cursor: locked ? 'not-allowed' : 'pointer',
position: 'relative',
- filter: isActive ? 'saturate(1.1) brightness(1.04)' : 'none',
}}
>
${locked && html`
diff --git a/assets/js/compute.js b/assets/js/compute.js
index 435a4b3..3bf9279 100644
--- a/assets/js/compute.js
+++ b/assets/js/compute.js
@@ -385,7 +385,9 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
const peakRow = (field) => todayRows.reduce((best, r) =>
(r[field] != null && (best == null || r[field] > best[field])) ? r : best, null);
- const maxRainProb = Math.max(0, ...todayRows.map(r => r.precipProb ?? 0));
+ const peakRainRow = todayRows.reduce((best, r) =>
+ (r.precipProb != null && (best == null || r.precipProb > best.precipProb)) ? r : best, null);
+ const maxRainProb = peakRainRow ? (peakRainRow.precipProb ?? 0) : 0;
// Longest run of rows that satisfy a predicate.
const longestWindow = (rows, cond) => {
@@ -428,7 +430,9 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
const rainItem = {
icon: '🌧',
label: 'Rain risk',
- value: `${Math.round(maxRainProb)}%`,
+ value: maxRainProb > 0 && peakRainRow?.iso
+ ? `${Math.round(maxRainProb)}% at ${hhmm(peakRainRow.iso)}`
+ : `${Math.round(maxRainProb)}%`,
alert: maxRainProb >= 60,
};
diff --git a/data/tracking.json b/data/tracking.json
index 06a5538..b1fae76 100644
--- a/data/tracking.json
+++ b/data/tracking.json
@@ -92,5 +92,9 @@
"vehicle": 2,
"farming": 5
}
+ },
+ "2026-06-23": {
+ "visits": 7,
+ "profiles": []
}
}
\ No newline at end of file