Every score starts at 10.0 and subtracts penalties for the conditions. The result is clamped to 1–10. If a measurement is unavailable, its penalty is skipped rather than counted as zero — unknown is not the same as perfect.
Penalties
- Wind (0–4): ≤10 km/h (0), ≤20 (−1), ≤30 (−2), ≤40 (−3), >40 (−4). Headwind ×1.3, tailwind ×0.7, crosswind ×1.0.
- Gusts (0–1.5): based on the gap between gust and mean wind — that spread is what actually moves the bike.
- Temperature (0–6): free inside your comfort band (15–25°C by default), then growing with distance outside it. Uses feels-like when available.
- Wind chill on the bike (0–1.5): only below freezing. Computed from the airspeed you actually meet at riding speed, which is why a road pace chills harder than a trail pace.
- Precipitation (0–5): the worse of how likely rain is and how hard it is falling.
- Humidity (0–2): ≤60% (0), ≤80% (−1), >80% (−2). At ≥90% the total is capped at 4.
- Visibility (0–3): ≥10 km (0), ≥5 (−1), ≥2 (−2), <2 (−3).
- UV (0–1.5): ≤5 (0), ≤7 (−0.5), ≤9 (−1), above (−1.5).
- Surface (gravel and MTB only, 0–3): from the forecast model's soil moisture, which already accounts for drying by sun and wind. Both ends cost: saturated is mud, bone-dry is loose and blown out, and "tacky" in between is free. Where soil moisture is unavailable it falls back to rainfall over the last 48 h (gravel) or 72 h (MTB).
Per-discipline weighting
The same penalties are weighted differently per discipline, because the same weather is not the same ride:
| Factor |
Road |
Gravel |
MTB |
| Wind | ×1.0 | ×1.5 | ×0.7 |
| Gusts | ×1.0 | ×1.0 | ×0.6 |
| Precipitation | ×1.2 | ×1.0 | ×0.9 |
| Surface / mud | — | ×0.8 (48 h) | ×1.0 (72 h) |
Hard ceilings
Some conditions cap the score no matter how good everything else is: extreme heat (cap 2.0), thunderstorms (1.5), freezing rain (1.5), near-freezing with precipitation (2.5), and snow (2.0 road / 3.0 off-road).
Score bands
🟢 8–10 Excellent · 🟡 6–8 Good · 🟠 4–6 Fair · 🔴 2.5–4 Poor · ⛔ below 2.5 Skip it. The same band drives the badge, the colour, and the wording everywhere in the app.
Note: wind is treated as a crosswind by default, since your route direction is unknown.
🚴🏼 A bit more context: Blog post on the project.