+
+
+
+ ${error && html`
+
+ ⚠ ${error}
+
`}
+ ${loading && !error && html`
+
Acquiring forecast data…
`}
+
+ ${forecast && days.length > 0 && html`
+ <${Fragment}>
+
+
+
+
+ ${proPromptDay !== null && days[proPromptDay] && (() => {
+ const promptDate = days[proPromptDay].date;
+ const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long' });
+ const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short' });
+ return html`
+
+
+
+ 🔒 ${proPromptSource === 'custom'
+ ? 'Custom columns are part of SunScope Extra'
+ : `${dayName}'s forecast is part of SunScope Extra`}
+
+
+ ${proPromptSource === 'custom'
+ ? 'Pro lets you choose exactly which columns appear — mix and match Air, Dew, Soil, UV, UTCI and more to build your perfect view.'
+ : 'Pro unlocks the full 14-day forecast, customisable columns, and an ad-free view.'}
+
+
+ £2 / month · launching soon
+
+
+
+
+ Notify me at launch
+
+
+
+
`;
+ })()}
+
+ ${(() => {
+ const band = confidenceBand(selectedDay);
+ const isOutlook = selectedDay >= 7;
+ return html`
+
+
+ Day ${selectedDay + 1} of 14 · Forecast clarity: ${band.label}
+
+ ${isOutlook && html`
+
+ forecast skill is reduced — treat hourly detail as trend, not precision
+ `}
+
`;
+ })()}
+
+
+
+ Profile:
+ ${Object.entries(FILTER_PROFILES).map(([key, profile]) => {
+ const isCustom = key === 'custom';
+ const locked = isCustom && !isPro;
+ return html`
+ `;
+ })}
+
+
+
+ ${isPro && html`
+
+ Columns:
+ ${[
+ // Hour and UTCI+P are always-on — no toggle button for them.
+ { key: 'air', label: 'Air' },
+ { key: 'rh', label: 'RH' },
+ { key: 'dew', label: 'Dew' },
+ { key: 'soilT', label: 'Soil °C' },
+ { key: 'soilT6', label: 'Soil 6cm' },
+ { key: 'soilM', label: 'Soil moist' },
+ { key: 'wind', label: 'Wind' },
+ { key: 'dir', label: 'Dir' },
+ { key: 'cloud', label: 'Cloud' },
+ { key: 'sun', label: 'Sun' },
+ { key: 'direct', label: 'Direct' },
+ { key: 'diffuse', label: 'Diffuse' },
+ { key: 'tmrt', label: 'Tmrt' },
+ { key: 'delta', label: 'Δ' },
+ { key: 'utci', label: 'UTCI' },
+ { key: 'uvA', label: 'UV-A' },
+ { key: 'uvB', label: 'UV-B' },
+ ].filter(c =>
+ activeProfile === 'custom' || !!FILTER_PROFILES[activeProfile].cols[c.key]
+ ).map(c => html`
+ `)}
+
+ ${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['burn']) && html`
+ `}
+
+ ${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['precip']) && html`
+ `}
+
`}
+
+
+
+
+
+
+
+
+
+ | Hour |
+ ${visibleCols.air && html`Air °C | `}
+ ${visibleCols.rh && html`RH % | `}
+ ${visibleCols.dew && html`Dew °C | `}
+ ${visibleCols.soilT && html`Soil °C surface | `}
+ ${visibleCols.soilT6 && html`Soil 6cm °C root | `}
+ ${visibleCols.soilM && html`Soil moist m³/m³ | `}
+ ${visibleCols.wind && html`Wind m/s (gust) | `}
+ ${visibleCols.dir && html`Dir - | `}
+ ${visibleCols.cloud && html`Cloud % | `}
+ ${visibleCols.sun && html`Sun elev° | `}
+ ${visibleCols.direct && html`Direct W/m² | `}
+ ${visibleCols.diffuse && html`Diffuse W/m² | `}
+ ${visibleCols.tmrt && html`Tmrt °C | `}
+ ${visibleCols.delta && html`Δ UTCI−Air | `}
+ ${visibleCols.utci && html`UTCI °C felt | `}
+ ${visibleCols.uvA && html`UV-A est. idx | `}
+ ${visibleCols.uvB && html`UV-B est. idx | `}
+ ${visibleCols.burn && html`Burn to MED | `}
+ ${visibleCols.precip && html`Pcpt mm/h | `}
+ UTCI+P °C adj. |
+
+
+
+
+
+
+
+
+
+ ${visible.map((r) => {
+ const cat = utciCategory(r.utci);
+ const isNight = r.elev < 0;
+ const isNow =
+ now.toDateString() === r.dt.toDateString() &&
+ now.getHours() === r.dt.getHours();
+ const delta = r.utci - r.Ta;
+ const adjCat = utciCategory(r.utciAdj);
+ return html`
+
+ |
+
+ <${SkyScope} elev=${r.elev} dt=${r.dt} size=${30} />
+ ${r.dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}
+
+ |
+ ${visibleCols.air && html`${r.Ta.toFixed(1)} | `}
+ ${visibleCols.rh && html`${Math.round(r.RH)} | `}
+ ${visibleCols.dew && html`${r.dew != null ? r.dew.toFixed(1) : '—'} | `}
+ ${visibleCols.soilT && html`
+ ${r.soilT0 != null ? r.soilT0.toFixed(1) : '—'} | `}
+ ${visibleCols.soilT6 && html`
+ ${r.soilT6 != null ? r.soilT6.toFixed(1) : '—'} | `}
+ ${visibleCols.soilM && html`
+ ${r.soilM != null ? r.soilM.toFixed(3) : '—'} | `}
+ ${visibleCols.wind && html`
+ ${r.va.toFixed(1)}${r.gust != null && r.gust > r.va + 0.5
+ ? html`(${r.gust.toFixed(1)})`
+ : ''}
+ | `}
+ ${visibleCols.dir && html`
+
+ <${WindVane} bearing=${r.wd} size=${28} />
+ ${r.compass.label}
+
+ | `}
+ ${visibleCols.cloud && html`
+
+ <${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${28} />
+ ${Math.round(r.cc)}
+
+ | `}
+ ${visibleCols.sun && html`${r.elev > 0 ? r.elev.toFixed(1) : '—'} | `}
+ ${visibleCols.direct && html`${Math.round(r.dir)} | `}
+ ${visibleCols.diffuse && html`${Math.round(r.dif)} | `}
+ ${visibleCols.tmrt && html`${r.Tmrt.toFixed(1)} | `}
+ ${visibleCols.delta && html`
+ 3 ? '#c8601a' : delta < -3 ? '#3f73c4' : '#9a7d5a',
+ fontWeight: 600,
+ }}>
+ ${delta > 0 ? '+' : ''}${delta.toFixed(1)}
+ | `}
+ ${visibleCols.utci && html`
+
+
+ ${r.utci.toFixed(1)}
+
+ | `}
+ ${visibleCols.uvA && html`
+ 0 ? '#c8922a' : '#5a4228' }}>
+ ${r.uvA > 0 ? r.uvA.toFixed(1) : '—'}
+ | `}
+ ${visibleCols.uvB && html`
+ 0 ? '#c44a3a' : '#5a4228', fontWeight: 600 }}>
+ ${r.uvB > 0 ? r.uvB.toFixed(2) : '—'}
+ | `}
+ ${visibleCols.burn && html`
+ 0 ? (sunburnMinutes(r.uv, skinType) < 30 ? '#c44a3a' : '#c8601a') : '#5a4228' }}>
+ ${burnLabel(sunburnMinutes(r.uv, skinType))}
+ | `}
+ ${visibleCols.precip && html`
+
+
+ <${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} />
+ 0 ? '#6090c8' : r.precip > 0 ? '#5090b0' : '#c0a880' }}>
+ ${r.snow > 0 ? r.snow.toFixed(1) + 'cm' : r.precip > 0 ? r.precip.toFixed(1) : '—'}
+
+
+ | `}
+
+
+ ${r.utciAdj.toFixed(1)}
+
+ |
+
`;
+ })}
+
+
+
+
+
+
+
Thermal stress bands
+
+ ${[
+ { l: '-27 to -13 Arctic', bg: '#3f73c4', fg: '#fff' },
+ { l: '-13 to 0 Freezing', bg: '#7eb0e0', fg: '#111' },
+ { l: '0 to 9 Cold', bg: '#bcd9ec', fg: '#111' },
+ { l: '9 to 18 Chilled', bg: '#c8dcc0', fg: '#111' },
+ { l: '18 to 26 Comfortable', bg: '#6ab05a', fg: '#fff' },
+ { l: '26 to 32 Mod heat', bg: '#e8c547', fg: '#111' },
+ { l: '32 to 38 Strong heat', bg: '#dc8a3a', fg: '#111' },
+ { l: '38 to 46 V. strong', bg: '#c44a3a', fg: '#fff' },
+ { l: '> 46 Extreme heat', bg: '#7a1a1a', fg: '#fff' },
+ ].map((b, i) => html`
+
+ ${b.l}
+ `)}
+
+
+ >`}
+
+
+
What is SunScope?
+
+ SunScope shows how the weather will actually feel on your body — not just the air
+ temperature. It uses the Universal Thermal Climate Index (UTCI), a
+ peer-reviewed biometeorological standard developed by Bröde et al. (2012) that combines
+ air temperature, humidity, wind speed, and solar radiation into a single felt
+ temperature. On a calm, sunny winter day UTCI can read several degrees warmer than
+ the thermometer; on a grey, blustery day it can read far colder. Forecast data is
+ sourced in real time from Open-Meteo, a free and open-source weather API,
+ and solar radiation is used to calculate Mean Radiant Temperature — the heat your skin
+ absorbs from the sun — making SunScope one of the most complete outdoor comfort forecasts
+ available for free.
+
+
+ The UTCI+P column adds the SunScope soak-factor: an
+ original precipitation penalty that accounts for the extra chill of rain and snow on
+ exposed skin and wet clothing. Light drizzle reduces the felt temperature by around
+ 1–2 °C; heavy rain combined with wind can push it down by 7–8 °C. Snow carries an
+ additional penalty on top. The result is an honest, real-world comfort score for any
+ location worldwide — simply search for your town or city and compare the hourly
+ forecast across the next 3 days (and up to 14 days with SunScope Extra).
+
+
+
+
+
+