diff --git a/assets/css/ui.css b/assets/css/ui.css
index b1697a1..f59124b 100644
--- a/assets/css/ui.css
+++ b/assets/css/ui.css
@@ -1787,6 +1787,17 @@
white-space: pre-line; /* honour \n so multi-range windows stack one per line */
}
+/* Secondary figure inside a value — currently the day's peak alongside the
+ climate anomaly. Inline where there is width; on its own line in the 240px
+ desktop rail, so the two numbers break deliberately rather than wherever
+ the text happens to run out of room. Nested inside .insight-value rather
+ than given its own grid area so rows without a sub keep their layout. */
+.insight-sub {
+ font-weight: 500;
+ opacity: 0.72;
+}
+.insight-sub::before { content: ' '; }
+
/* Alert state - draws attention without being aggressive */
.insight-row--alert .insight-value {
color: #b84020;
@@ -1853,6 +1864,11 @@
grid-area: value;
text-align: left; /* value sits left-aligned under its label */
}
+ /* Rail is only 240px — the secondary figure takes its own line. */
+ .insight-panel--glance .insight-sub {
+ display: block;
+ }
+ .insight-panel--glance .insight-sub::before { content: none; }
}
/* ── Narrow screens - drop the rail below the table ─────────────────── */
diff --git a/assets/js/app.js b/assets/js/app.js
index b80d058..8afc488 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1579,19 +1579,19 @@ export function UTCIForecast() {
${(() => {
const heatEvents = eventGlanceItems.filter(e => /heat caution|heat warning|extreme heat/i.test(e.label)).map(e => ({ ...e, alert: true }));
const otherEvents = eventGlanceItems.filter(e => !/heat caution|heat warning|extreme heat/i.test(e.label));
- const renderRow = ({ icon, label, value, alert, grp }, keyPrefix = '') => html`
+ const renderRow = ({ icon, label, value, sub, alert, grp }, keyPrefix = '') => html`
${icon}
${titleCaseText(label)}
- ${titleCaseText(value)}
+ ${titleCaseText(value)}${sub ? html`${titleCaseText(sub)}` : ''}
`;
// Only the outdoors profile reorders items around the heat block
// (it has a 'Peak felt temp' anchor). Other profiles render in order.
const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp');
const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : null;
const drivingItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Good driving time') : null;
- const climateRows = glanceSummary.filter(i => i.label === '1991-2020 High' || i.label === '1991-2020 Average');
- const restSummary = glanceSummary.filter(i => i.label !== '1991-2020 High' && i.label !== '1991-2020 Average' && !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time')));
+ const climateRows = glanceSummary.filter(i => i.label === '1991-2020 Average');
+ const restSummary = glanceSummary.filter(i => i.label !== '1991-2020 Average' && !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time')));
return [
...restSummary.flatMap(item => [
renderRow(item),
diff --git a/assets/js/compute.js b/assets/js/compute.js
index f52cefd..7059dc8 100644
--- a/assets/js/compute.js
+++ b/assets/js/compute.js
@@ -548,10 +548,12 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
};
// ── Vs seasonal norms (climate anomalies) ──────────────────────────────
- // Shown across every profile: two rows comparing this day against the
- // 1991-2020 norm for the date — the day's HIGH vs the normal high, and the
- // day's 24h AVERAGE vs the normal mean. High tracks the daytime peak people
- // notice; the average is the standard climate anomaly. An honest climate cue.
+ // One row comparing this day against the 1991-2020 norm for the date. The
+ // headline is the standard climate anomaly — the day's 24h AVERAGE vs the
+ // normal mean — with the day's HIGH vs the normal high carried alongside as
+ // the peak people actually notice. Kept in a single row because two rows
+ // sharing the icon and "1991-2020 … warmer" phrasing read as duplicates
+ // giving contradictory answers. An honest climate cue.
const climateItem = (() => {
if (!normals) return [];
const key = dayKey || todayRows[0]?.iso?.slice(0, 10);
@@ -562,22 +564,36 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
if (taVals.length === 0) return [];
const dayHigh = Math.max(...taVals);
const dayMean = taVals.reduce((a, b) => a + b, 0) / taVals.length;
- const out = [];
+ // Either day-of-year table can have null slots — reduceNormals keeps the
+ // { high, mean } shape as long as one series survived — so treat each as
+ // independently optional.
const nHigh = normals.high?.[doy];
- if (nHigh != null) {
- const a = dayHigh - nHigh;
- if (Math.abs(a) >= 3) {
- out.push({ icon: '🌡', label: '1991-2020 High', value: `${a >= 0 ? '+' : '−'}${Math.abs(a).toFixed(1)}° ${a >= 0 ? 'warmer' : 'cooler'}`, alert: a >= 5, grp: 'ambient' });
- }
- }
const nMean = normals.mean?.[doy];
- if (nMean != null) {
- const a = dayMean - nMean;
- if (Math.abs(a) >= 3) {
- out.push({ icon: '🌡', label: '1991-2020 Average', value: `${a >= 0 ? '+' : '−'}${Math.abs(a).toFixed(1)}° ${a >= 0 ? 'warmer' : 'cooler'}`, alert: a >= 5, grp: 'ambient' });
- }
+ const aHigh = nHigh != null ? dayHigh - nHigh : null;
+ const aMean = nMean != null ? dayMean - nMean : null;
+ // Show the row when EITHER anomaly is notable. The mean anomaly is usually
+ // the smaller of the two (a clear night pulls it back towards normal), so
+ // gating on it alone would hide days with a dramatic peak.
+ const notable = v => v != null && Math.abs(v) >= 3;
+ if (!notable(aHigh) && !notable(aMean)) return [];
+ const fmt = a => `${a >= 0 ? '+' : '−'}${Math.abs(a).toFixed(1)}°`;
+ const word = a => (a >= 0 ? 'warmer' : 'cooler');
+ // Each half keeps its own sign and word, so a day whose peak and mean fall
+ // on opposite sides of the norm still reads correctly.
+ // `sub` is the secondary figure — rendered inline on mobile, on its own
+ // line in the narrow desktop rail (see .insight-sub in ui.css).
+ let value, sub = null;
+ if (aMean != null && aHigh != null) {
+ value = `${fmt(aMean)} ${word(aMean)}`;
+ sub = `(${fmt(aHigh)} peak)`;
+ } else if (aMean != null) {
+ value = `${fmt(aMean)} ${word(aMean)}`;
+ } else {
+ value = `${fmt(aHigh)} peak ${word(aHigh)}`;
}
- return out;
+ // Flagged off the high, matching the "5 degrees against the normal high" rule.
+ const alert = (aHigh != null ? aHigh : aMean) >= 5;
+ return [{ icon: '🌡', label: '1991-2020 Average', value, sub, alert, grp: 'ambient' }];
})();
// ── Good Driving Time ──────────────────────────────────────────────────
diff --git a/data/tracking.json b/data/tracking.json
index dd67127..c66e8b0 100644
--- a/data/tracking.json
+++ b/data/tracking.json
@@ -199,5 +199,11 @@
"profiles": {
"farming": 2
}
+ },
+ "2026-08-09": {
+ "visits": 9,
+ "profiles": {
+ "vehicle": 1
+ }
}
}
\ No newline at end of file
diff --git a/faq.html b/faq.html
index ef599f7..bd48dbf 100644
--- a/faq.html
+++ b/faq.html
@@ -56,7 +56,7 @@
{"@type":"Question","name":"Does SunScope show air quality and pollen forecasts?","acceptedAnswer":{"@type":"Answer","text":"Yes. Three air environment columns sit after Cloud cover in the table: Visibility (horizontal visibility in km), AQI (European Air Quality Index combining PM2.5, PM10, ozone, nitrogen dioxide, and sulphur dioxide), and Pollen (grains per cubic metre with a type picker for grass, birch, alder, mugwort, olive, ragweed, or total). All three are sourced from Copernicus CAMS via Open-Meteo. Toggle them using the column buttons above the table."}},
{"@type":"Question","name":"What does the weather icon on each day tab mean?","acceptedAnswer":{"@type":"Answer","text":"Each day tab shows a weather icon for dominant daytime sky conditions - a sun disc for clear days, cloud variants for cloudy days, or a precipitation icon for rainy or snowy days. Below the icon are the day high and low SunSoak felt temperatures in warm and cool colours. The icon is derived from actual solar elevation and cloud data, not a fixed set of generic symbols."}},
{"@type":"Question","name":"What are the weather event alerts at the top of the page?","acceptedAnswer":{"@type":"Answer","text":"When notable conditions are forecast - ground frost, strong wind warning, fog, or a heat event - a note appears across the top of the page, above the navigation, with the full message and the dates it applies to. When several events are active it cycles between them. Tapping or clicking the event icon in the hour column opens a popup with the same detail for that hour."}},
- {"@type":"Question","name":"What is the Today at a Glance panel and what does vs seasonal average mean?","acceptedAnswer":{"@type":"Answer","text":"Today at a Glance is a summary panel above the table that pulls together the 4-5 numbers that matter most for the day and your active profile, so you don't have to scan the full hourly table. It also shows a vs seasonal average comparison on every profile: the day's forecast high and 24-hour mean against the 1991-2020 climate normal for that date and location, sourced from Open-Meteo's historical archive. A reading of plus 5 degrees or more against the normal high is flagged - an honest anomaly check on top of the raw forecast."}},
+ {"@type":"Question","name":"What is the Today at a Glance panel and what does vs seasonal average mean?","acceptedAnswer":{"@type":"Answer","text":"Today at a Glance is a summary panel above the table that pulls together the 4-5 numbers that matter most for the day and your active profile, so you don't have to scan the full hourly table. It also shows a vs seasonal average comparison on every profile: the day's 24-hour mean against the 1991-2020 climate normal for that date and location, with the day's peak shown alongside it, sourced from Open-Meteo's historical archive. It appears whenever the day runs 3 degrees or more off the normal, and a reading of plus 5 degrees or more against the normal high is flagged - an honest anomaly check on top of the raw forecast."}},
{"@type":"Question","name":"Can I see how the sky and weather will look later today?","acceptedAnswer":{"@type":"Answer","text":"Yes. By default the sky scope shows the current moment, with sun position, sky colour, glow, cloud, rain, snow and haze all calculated from real forecast data. You can also travel through the day: press the Day cycle play button to run a 24-hour time-lapse that animates the sky and weather through the next day in about a minute and a half, or drag the thumb along the timeline bar beneath the scope to scrub to any instant. The timeline shows the day's changing sky colours with sunrise and sunset markers at their real times."}},
{"@type":"Question","name":"How far ahead does the forecast go?","acceptedAnswer":{"@type":"Answer","text":"All 14 days of hourly data are free for everyone. The forecast auto-refreshes while the page is open so the current hour always reflects the latest data from Open-Meteo. No manual reload needed."}},
{"@type":"Question","name":"How accurate is SunScope?","acceptedAnswer":{"@type":"Answer","text":"SunScope uses Open-Meteo data from ECMWF and national meteorological services - the same models used by professional forecasters. The UTCI calculation follows the peer-reviewed Bröde 2012 polynomial exactly. Derived columns such as vehicle temperature, indoor temperature, and concrete surface are physics-based estimates that vary based on local factors like building construction, shading, and surface colour. Treat them as well-informed estimates rather than precise measurements."}},
@@ -161,7 +161,7 @@
What is the Today at a Glance panel and what does "vs seasonal average" mean?
- Today at a Glance is a summary panel above the table that pulls together the 4-5 numbers that matter most for the day and your active profile - peak heat window, rain risk, wind, UV, and similar - so you don't have to scan the full hourly table to get the headline picture. Content adapts to what you're doing: the Farming profile adds sow and harvest advice from a seasonal crop calendar, for example.
Every profile also shows a vs seasonal average comparison: the day's forecast high and 24-hour mean set against the 1991-2020 climate normal for that date and location, calculated from Open-Meteo's historical archive. A gap of 5 degrees or more against the normal high is flagged - an honest anomaly check on top of the raw forecast numbers.
+ Today at a Glance is a summary panel above the table that pulls together the 4-5 numbers that matter most for the day and your active profile - peak heat window, rain risk, wind, UV, and similar - so you don't have to scan the full hourly table to get the headline picture. Content adapts to what you're doing: the Farming profile adds sow and harvest advice from a seasonal crop calendar, for example.
Every profile also shows a vs seasonal average comparison: the day's 24-hour mean set against the 1991-2020 climate normal for that date and location, with the day's peak shown alongside it, calculated from Open-Meteo's historical archive. It appears whenever the day runs 3 degrees or more off the normal, and a gap of 5 degrees or more against the normal high is flagged - an honest anomaly check on top of the raw forecast numbers.
diff --git a/features.html b/features.html
index 4504c8b..c8b5202 100644
--- a/features.html
+++ b/features.html
@@ -130,8 +130,9 @@
scan the full hourly table to get the headline picture. Content adapts to what you're actually
doing: the Farming profile adds sow and harvest advice from a seasonal crop calendar, for example.
Every profile also gets a vs seasonal average comparison, showing how the day's
- high and 24-hour mean stack up against the 1991–2020 climate normal for that date and location —
- an honest anomaly check on top of the raw forecast numbers.
+ 24-hour mean stacks up against the 1991–2020 climate normal for that date and location, with the
+ day's peak alongside it — an honest anomaly check on top of the raw forecast numbers. It appears
+ whenever the day runs 3 degrees or more off the normal.
Weather event alerts and popups