diff --git a/assets/css/ui.css b/assets/css/ui.css
index ff34ce3..35d8a3d 100644
--- a/assets/css/ui.css
+++ b/assets/css/ui.css
@@ -1726,6 +1726,19 @@
padding-bottom: 0;
}
+/* Group-tinted rows remove the divider border since the background pill
+ provides the visual separation. */
+.insight-row--felt,
+.insight-row--surface,
+.insight-row--ambient,
+.insight-row--precip,
+.insight-row--sky,
+.insight-row--wind,
+.insight-row--airqual,
+.insight-row--solar {
+ border-bottom: none;
+}
+
.insight-icon {
width: 20px;
text-align: center;
@@ -1807,6 +1820,17 @@
font-weight: 700;
}
+/* Group-tinted backgrounds for insight rows — very light shade matching
+ the column group colour so items feel categorised at a glance. */
+.insight-row--felt { background: rgba(223,243,236,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+.insight-row--surface { background: rgba(221,234,248,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+.insight-row--ambient { background: rgba(236,234,227,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+.insight-row--precip { background: rgba(247,228,238,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+.insight-row--sky { background: rgba(234,232,252,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+.insight-row--wind { background: rgba(250,230,224,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+.insight-row--airqual { background: rgba(229,240,216,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+.insight-row--solar { background: rgba(250,234,204,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
+
/* ── Wide screens - the rail is narrow (240px), so stack each value
beneath its label instead of letting long values overflow sideways.
Mobile keeps the inline label/value layout (plenty of width there). */
diff --git a/assets/images/profiles/pets.png b/assets/images/profiles/pets.png
new file mode 100644
index 0000000..fb38455
Binary files /dev/null and b/assets/images/profiles/pets.png differ
diff --git a/assets/js/app.js b/assets/js/app.js
index d5b9cb9..a5a9bbf 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1694,8 +1694,8 @@ 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 }, keyPrefix = '') => html`
-
+ const renderRow = ({ icon, label, value, alert, grp }, keyPrefix = '') => html`
+
${icon}
${label}
${value}
@@ -1705,8 +1705,8 @@ export function UTCIForecast() {
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 = hasFeltAnchor ? glanceSummary.filter(i => i.label === '1991-2020 High' || i.label === '1991-2020 Average') : [];
- const restSummary = glanceSummary.filter(i => !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time' || i.label === '1991-2020 High' || i.label === '1991-2020 Average')));
+ 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')));
return [
...restSummary.flatMap(item => [
renderRow(item),
@@ -1715,11 +1715,11 @@ export function UTCIForecast() {
? [
...heatEvents.map(e => renderRow(e, 'ev-')),
...(drivingItem ? [renderRow(drivingItem)] : []),
- ...climateRows.map(item => renderRow(item)),
...(comfortItem ? [renderRow(comfortItem)] : []),
]
: []),
]),
+ ...climateRows.map(item => renderRow(item)),
...otherEvents.map(e => renderRow(e, 'ev-')),
];
})()}
diff --git a/assets/js/compute.js b/assets/js/compute.js
index fd28ea6..316c2bb 100644
--- a/assets/js/compute.js
+++ b/assets/js/compute.js
@@ -479,6 +479,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Lightning risk',
value: `${lightningRisk} at ${hhmm(peakLightningRow.iso)}`,
alert: peakLightning >= 5,
+ grp: 'precip',
}] : [];
const moistureLabel = (v) => {
@@ -513,22 +514,20 @@ 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 fmt = (anom) => {
- const mag = Math.abs(anom);
- return mag < 0.5
- ? 'About average'
- : `${anom >= 0 ? '+' : '−'}${mag.toFixed(1)}° ${anom >= 0 ? 'warmer' : 'cooler'}`;
- };
const out = [];
const nHigh = normals.high?.[doy];
if (nHigh != null) {
const a = dayHigh - nHigh;
- out.push({ icon: '🌡', label: '1991-2020 High', value: fmt(a), alert: a >= 5 });
+ 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;
- out.push({ icon: '🌡', label: '1991-2020 Average', value: fmt(a), alert: a >= 5 });
+ 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' });
+ }
}
return out;
})();
@@ -557,6 +556,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Good driving time',
value: formatWindows(drivingWins) ?? 'Drive with care',
alert: drivingWins.length === 0,
+ grp: 'felt',
};
// ── Farming ────────────────────────────────────────────────────────────
@@ -575,12 +575,14 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Best field work',
value: formatWindows(fieldWins) ?? 'No suitable window',
alert: fieldWins.length === 0,
+ grp: 'surface',
},
...(show('soilT') ? [{
icon: '🌱',
label: 'Soil warms to 10°C by',
value: soilWarmRow ? (hhmm(soilWarmRow.iso) === '00:00' ? 'All Day' : hhmm(soilWarmRow.iso)) : 'Not today',
alert: !soilWarmRow,
+ grp: 'surface',
}] : []),
...(show('precipProb') ? [rainItem] : []),
...lightningItem,
@@ -589,6 +591,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Soil moisture',
value: moistureLabel(soilM) ?? '-',
alert: soilM != null && (soilM < 0.10 || soilM > 0.50),
+ grp: 'surface',
}] : []),
...(weekDays ? computeCropAdvice(weekDays, lat) : []),
...(show('pollen') && maxPollen >= 10 ? [{
@@ -596,6 +599,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Grass pollen',
value: pollenLabel(maxPollen),
alert: maxPollen >= 50,
+ grp: 'airqual',
}] : []),
];
}
@@ -612,11 +616,13 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Peak cabin temp',
value: peakCabin ? `${Math.round(peakCabin.vehicleT)}° at ${hhmm(peakCabin.iso)}` : '-',
alert: !!(peakCabin && peakCabin.vehicleT > 31.5),
+ grp: 'felt',
}, {
icon: '🧒',
label: 'Children/pets in car',
value: dangerFrom ? `Unsafe from ${hhmm(dangerFrom.iso)}` : 'Safe all day',
alert: !!dangerFrom,
+ grp: 'felt',
}] : []),
...(show('precipProb') ? [rainItem] : []),
...lightningItem,
@@ -630,6 +636,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
return best ? `${hhmm(best.iso)} (${Math.round(best.utciAdj)}° felt)` : '-';
})(),
alert: false,
+ grp: 'felt',
},
];
}
@@ -651,18 +658,21 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Peak indoor (unmanaged)',
value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-',
alert: !!(peakIndoor && peakIndoor.indoorT >= 28),
+ grp: 'felt',
}] : []),
...(show('managedT') ? [{
icon: '🌡',
label: 'Peak indoor (managed)',
value: peakManaged ? `${Math.round(peakManaged.managedT)}°` : '-',
alert: !!(peakManaged && peakManaged.managedT >= 28),
+ grp: 'felt',
}] : []),
...(show('indoorT') ? [{
icon: '🪟',
label: 'Open windows',
value: formatWindows(ventWins) ?? 'Keep closed',
alert: false,
+ grp: 'felt',
}] : []),
...lightningItem,
...(show('aqi') ? [{
@@ -676,6 +686,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Pollen',
value: pollenLabel(maxPollen),
alert: maxPollen >= 50,
+ grp: 'airqual',
}] : []),
];
}
@@ -695,24 +706,28 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Sunsoak for pet',
value: peakFur ? `${Math.round(peakFur.furSurfaceT)}° at ${hhmm(peakFur.iso)}` : '-',
alert: !!(peakFur && peakFur.furSurfaceT >= 45),
+ grp: 'surface',
}] : []),
...(show('petShadeT') ? [{
icon: '🌳',
label: 'Shade for pet',
value: peakShade ? `${Math.round(peakShade.shadeT)}° at ${hhmm(peakShade.iso)}` : '-',
alert: !!(peakShade && peakShade.shadeT >= 26),
+ grp: 'ambient',
}] : []),
...(show('petHomeT') ? [{
icon: '🏠',
label: 'Pet at home',
value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-',
alert: !!(peakIndoor && peakIndoor.indoorT >= 26),
+ grp: 'felt',
}] : []),
...(show('vehicleT') ? [{
icon: '🧒',
label: 'Kids/pets in car',
value: carDanger ? `Unsafe from ${hhmm(carDanger.iso)}` : 'Safe all day',
alert: !!carDanger,
+ grp: 'felt',
}] : []),
...lightningItem,
...(show('aqi') ? [{
@@ -720,12 +735,14 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Air quality',
value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
alert: peakAqi >= 60,
+ grp: 'airqual',
}] : []),
...(show('pollen') && maxPollen >= 10 ? [{
icon: '🌼',
label: 'Pollen',
value: pollenLabel(maxPollen),
alert: maxPollen >= 50,
+ grp: 'airqual',
}] : []),
];
}
@@ -749,6 +766,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: `Best ${variant} window`,
value: formatWindows(coolWins) ?? 'No cool window today',
alert: coolWins.length === 0,
+ grp: 'felt',
},
...(show('precipProb') ? [rainItem] : []),
...lightningItem,
@@ -757,6 +775,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'UV burn time (peak)',
value: burnMins,
alert: !!(peakUvRow && peakUvRow.uv >= 6),
+ grp: 'solar',
}] : []),
...(show('aqi') ? [{
icon: '💨',
@@ -793,18 +812,21 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Comfortable window',
value: formatWindows(comfortWins) ?? 'No comfortable window',
alert: comfortWins.length === 0,
+ grp: 'felt',
},
{
icon: '🌡',
label: 'Peak felt temp',
value: peakFelt ? `${Math.round(peakFelt.utciAdj)}° at ${hhmm(peakFelt.iso)}` : '-',
alert: !!(peakFelt && (peakFelt.utciAdj >= 32 || peakFelt.utciAdj < 0)),
+ grp: 'felt',
},
...((show('burn') || show('uvA')) && burnMins ? [{
icon: '☀',
label: 'UV burn time (peak)',
value: burnMins,
alert: !!(peakUvRow && peakUvRow.uv >= 6),
+ grp: 'solar',
}] : []),
...(show('precipProb') ? [rainItem] : []),
...lightningItem,
@@ -812,13 +834,15 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
icon: '💨',
label: 'Air quality',
value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
- alert: peakAqi >= 60,
- }] : []),
- ...(show('pollen') && maxPollen >= 10 ? [{
+alert: peakAqi >= 60,
+ grp: 'airqual',
+ }] : []),
+ ...(show('pollen') && maxPollen >= 10 ? [{
icon: '🌿',
label: 'Pollen',
value: pollenLabel(maxPollen),
alert: maxPollen >= 50,
+ grp: 'airqual',
}] : []),
];
}
@@ -894,11 +918,11 @@ export function computeCropAdvice(weekDays, lat) {
const ready = sowSeason.filter(c =>
weekSoilT != null && weekSoilT >= c.sow.minSoilT && !groundWet);
out.push(ready.length ? {
- icon: '🌱', label: 'Good to sow', value: listCrops(ready), alert: false,
+ icon: '🌱', label: 'Good to sow', value: listCrops(ready), alert: false, grp: 'surface',
} : {
icon: '🌱', label: 'Good to sow',
value: groundWet ? 'Hold off — ground too wet' : 'Hold off — soil still cold',
- alert: true,
+ alert: true, grp: 'surface',
});
}
@@ -911,10 +935,10 @@ export function computeCropAdvice(weekDays, lat) {
out.push(ready.length ? {
icon: '🌾', label: 'Good to harvest',
value: dryDriven ? `${listCrops(ready)} (${dryRange()})` : listCrops(ready),
- alert: false,
+ alert: false, grp: 'surface',
} : {
icon: '🌾', label: 'Good to harvest',
- value: 'Hold — too wet to harvest grain', alert: true,
+ value: 'Hold — too wet to harvest grain', alert: true, grp: 'surface',
});
}