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