diff --git a/city_events.py b/city_events.py
index 23cca91..9fec566 100644
--- a/city_events.py
+++ b/city_events.py
@@ -13,7 +13,7 @@ EVENTS: dict[str, dict] = {
"text": "In August 2005, Hurricane Katrina overwhelmed the city's levees and flooded roughly 80% of New Orleans, one of the deadliest and costliest disasters in U.S. history.",
"url": "https://en.wikipedia.org/wiki/Effects_of_Hurricane_Katrina_in_New_Orleans"},
"houston-texas-us": {
- "text": "In August 2017, Hurricane Harvey stalled over the region and dropped more than 50 inches of rain — a record for a U.S. tropical cyclone — causing catastrophic flooding across Houston.",
+ "text": "In August 2017, Hurricane Harvey stalled over the region and dropped more than 50 inches of rain, a record for a U.S. tropical cyclone, causing catastrophic flooding across Houston.",
"url": "https://en.wikipedia.org/wiki/Hurricane_Harvey"},
"chicago-illinois-us": {
"text": "A brutal heat wave in July 1995 pushed the heat index past 120°F and caused an estimated 700 heat-related deaths in Chicago, reshaping how the city prepares for extreme heat.",
@@ -28,7 +28,7 @@ EVENTS: dict[str, dict] = {
"text": "The Blizzard of 1978 paralyzed Boston for a week, dropping about 27 inches of snow, stranding thousands of motorists, and flooding coastal neighborhoods.",
"url": "https://en.wikipedia.org/wiki/Northeastern_United_States_blizzard_of_1978"},
"seattle-washington-us": {
- "text": "During the June 2021 Western North America heat wave, Seattle hit an all-time record 108°F — a temperature the normally mild city had never before approached.",
+ "text": "During the June 2021 Western North America heat wave, Seattle hit an all-time record 108°F, a temperature the normally mild city had never before approached.",
"url": "https://en.wikipedia.org/wiki/2021_Western_North_America_heat_wave"},
"portland-oregon-us": {
"text": "In the June 2021 heat dome, Portland reached an all-time record 116°F across three consecutive record-shattering days.",
@@ -40,7 +40,7 @@ EVENTS: dict[str, dict] = {
"text": "Deep in Tornado Alley, the Oklahoma City area was struck by the May 3, 1999 tornado, which produced the highest wind speed ever measured on Earth, around 302 mph.",
"url": "https://en.wikipedia.org/wiki/1999_Bridge_Creek%E2%80%93Moore_tornado"},
"los-angeles-california-us": {
- "text": "In January 2025, wind-driven wildfires — chiefly the Palisades and Eaton fires — swept through Los Angeles, destroying thousands of structures in one of the most destructive fire disasters in California's history.",
+ "text": "In January 2025, wind-driven wildfires, chiefly the Palisades and Eaton fires, swept through Los Angeles, destroying thousands of structures in one of the most destructive fire disasters in California's history.",
"url": "https://en.wikipedia.org/wiki/January_2025_Southern_California_wildfires"},
# --- Canada ---
@@ -65,7 +65,7 @@ EVENTS: dict[str, dict] = {
"text": "In the summer of 2010, a record heat wave and surrounding peat and forest fires smothered Moscow in smog for weeks, with temperatures near 38°C and thousands of excess deaths.",
"url": "https://en.wikipedia.org/wiki/2010_Russian_wildfires"},
"madrid-es": {
- "text": "In January 2021, Storm Filomena buried Madrid under its heaviest snowfall in decades — around 50 cm — paralyzing the city for days.",
+ "text": "In January 2021, Storm Filomena buried Madrid under its heaviest snowfall in decades, around 50 cm, paralyzing the city for days.",
"url": "https://en.wikipedia.org/wiki/Storm_Filomena"},
"athens-attica-gr": {
"text": "In July 2018, wind-driven wildfires swept through Attica near Athens during extreme heat, killing more than 100 people in Greece's deadliest fire disaster.",
@@ -76,7 +76,7 @@ EVENTS: dict[str, dict] = {
"text": "During the 2019–20 'Black Summer' bushfires, hazardous smoke repeatedly blanketed Sydney, pushing its air quality to some of the worst of any major city.",
"url": "https://en.wikipedia.org/wiki/2019%E2%80%9320_Australian_bushfire_season"},
"melbourne-victoria-au": {
- "text": "On 'Black Saturday' in February 2009, extreme heat and wind drove bushfires across Victoria, around Melbourne, that killed 173 people — Australia's deadliest bushfires.",
+ "text": "On 'Black Saturday' in February 2009, extreme heat and wind drove bushfires across Victoria, around Melbourne, that killed 173 people in Australia's deadliest bushfires.",
"url": "https://en.wikipedia.org/wiki/Black_Saturday_bushfires"},
"brisbane-queensland-au": {
"text": "The 2010–11 Queensland floods peaked in January 2011 when the Brisbane River burst its banks, flooding thousands of homes and businesses across the city.",
@@ -90,7 +90,7 @@ EVENTS: dict[str, dict] = {
"text": "The December 2015 South Indian floods submerged much of Chennai after its heaviest rains in over a century, displacing hundreds of thousands.",
"url": "https://en.wikipedia.org/wiki/2015_South_India_floods"},
"kolkata-west-bengal-in": {
- "text": "In May 2020, Cyclone Amphan — the most powerful storm recorded in the Bay of Bengal — struck Kolkata directly, uprooting trees and cutting power across the city.",
+ "text": "In May 2020, Cyclone Amphan, the most powerful storm recorded in the Bay of Bengal, struck Kolkata directly, uprooting trees and cutting power across the city.",
"url": "https://en.wikipedia.org/wiki/Cyclone_Amphan"},
"karachi-sindh-pk": {
"text": "A severe heat wave in June 2015 killed an estimated 1,200 people in Karachi as temperatures reached about 45°C during Ramadan.",
@@ -114,7 +114,7 @@ EVENTS: dict[str, dict] = {
"text": "Super Typhoon Mangkhut battered Hong Kong in September 2018 with fierce winds and a storm surge, flooding low-lying districts and swaying skyscrapers.",
"url": "https://en.wikipedia.org/wiki/Typhoon_Mangkhut"},
"tokyo-jp": {
- "text": "In October 2019, Typhoon Hagibis — one of the most powerful to strike Japan in decades — brought record rainfall and widespread flooding to the Tokyo region.",
+ "text": "In October 2019, Typhoon Hagibis, one of the most powerful to strike Japan in decades, brought record rainfall and widespread flooding to the Tokyo region.",
"url": "https://en.wikipedia.org/wiki/Typhoon_Hagibis"},
"seoul-kr": {
"text": "In August 2022, record-breaking rainfall flooded Seoul, submerging roads and low-lying semi-basement homes and killing several people across the capital region.",
@@ -122,7 +122,7 @@ EVENTS: dict[str, dict] = {
# --- Middle East ---
"dubai-ae": {
- "text": "In April 2024, Dubai was deluged by about two years' worth of rain in a single day — its heaviest on record — flooding highways and bringing its international airport to a standstill.",
+ "text": "In April 2024, Dubai was deluged by about two years' worth of rain in a single day, its heaviest on record, flooding highways and bringing its international airport to a standstill.",
"url": "https://en.wikipedia.org/wiki/2024_United_Arab_Emirates_floods"},
"jeddah-mecca-region-sa": {
"text": "In November 2009, sudden flash floods swept through Jeddah after record rainfall, killing more than 100 people in one of the city's worst modern disasters.",
@@ -138,7 +138,7 @@ EVENTS: dict[str, dict] = {
# --- Africa ---
"cape-town-western-cape-za": {
- "text": "From 2015 to 2018 a severe drought pushed Cape Town toward 'Day Zero' — the day municipal taps would be shut off — narrowly averted through drastic water conservation.",
+ "text": "From 2015 to 2018 a severe drought pushed Cape Town toward 'Day Zero', the day municipal taps would be shut off, narrowly averted through drastic water conservation.",
"url": "https://en.wikipedia.org/wiki/Cape_Town_water_crisis"},
"durban-kwazulu-natal-za": {
"text": "In April 2022, extreme rainfall triggered catastrophic floods and landslides around Durban, killing more than 400 people in one of South Africa's deadliest storms.",
diff --git a/climate.py b/climate.py
index 884391a..6c4d359 100644
--- a/climate.py
+++ b/climate.py
@@ -100,9 +100,9 @@ class WeatherUnavailable(RuntimeError):
def limit_message(daily: bool) -> str:
"""The user-facing rate-limit copy, in one place."""
if daily:
- return ("Open-Meteo's daily request limit is exhausted — new locations will work again "
+ return ("Open-Meteo's daily request limit is exhausted, so new locations will work again "
"tomorrow. Places you've already viewed still work.")
- return "The weather service is rate-limited right now — please try again in a minute."
+ return "The weather service is rate-limited right now. Please try again in a minute."
def is_rate_limit(e) -> bool:
diff --git a/content.py b/content.py
index bcaa6a6..de414bc 100644
--- a/content.py
+++ b/content.py
@@ -796,7 +796,7 @@ def hub_page(request: Request) -> Response:
"n_countries": len(groups),
"canonical_path": "/climate",
"breadcrumb": [("Home", f"{BASE}/"), ("Climate", None)],
- "page_title": "City climate pages — averages, records and how today compares",
+ "page_title": "City climate pages: averages, records and how today compares",
"page_description": ("Browse climate pages for hundreds of cities worldwide: average temperatures by "
"month, all-time records, and how today's weather compares to local history."),
}
@@ -810,19 +810,19 @@ GLOSSARY: dict[str, dict] = {
"short": "The typical value of a weather metric for a place and time of year, averaged over decades.",
"body": "A climate normal is the long-term average of a weather variable (say, the daily high) "
"for a specific place and time of year. Thermograph builds each day's normal from every "
- "historical day within ±7 days of that day-of-year, across ~45 years — so a day is judged "
+ "historical day within ±7 days of that day-of-year, across ~45 years, so a day is judged "
"against its own season, not a single annual average.",
},
"percentile": {
"term": "Percentile",
- "short": "Where a value ranks within a distribution — the 90th percentile is warmer than 90% of days.",
+ "short": "Where a value ranks within a distribution: the 90th percentile is warmer than 90% of days.",
"body": "A percentile says where a value falls within a range of past values. If today's high is at "
"the 97th percentile, only about 3% of comparable days in this location's history were warmer. "
"Thermograph grades every day by its percentile against the local ±7-day seasonal distribution.",
},
"temperature-anomaly": {
"term": "Temperature anomaly",
- "short": "How far a temperature departs from normal — the difference from the long-term average.",
+ "short": "How far a temperature departs from normal: the difference from the long-term average.",
"body": "A temperature anomaly is how much warmer or colder it is than the local normal for the "
"time of year. Thermograph expresses the same idea as a percentile and a grade (from “Below "
"Normal” to “Near Record”), so an anomaly is easy to read at a glance for any location.",
@@ -852,15 +852,15 @@ GLOSSARY: dict[str, dict] = {
"humidity": {
"term": "Absolute humidity",
"short": "The actual mass of water vapor in the air, in grams per cubic meter.",
- "body": "Thermograph reports absolute humidity (g/m³) — the real amount of water vapor in the "
- "air — rather than relative humidity, which shifts with temperature. Graded against local history, "
+ "body": "Thermograph reports absolute humidity (g/m³), the real amount of water vapor in the "
+ "air, rather than relative humidity, which shifts with temperature. Graded against local history, "
"it shows genuinely muggy or unusually dry days.",
},
"reanalysis": {
"term": "Reanalysis (ERA5)",
"short": "A gridded, physically-consistent record of past weather for anywhere on Earth.",
"body": "A reanalysis blends historical observations with a weather model to produce a consistent "
- "record of past conditions everywhere — even where no station exists. Thermograph's ~45-year history "
+ "record of past conditions everywhere, even where no station exists. Thermograph's ~45-year history "
"comes from ECMWF's ERA5 reanalysis (via Open-Meteo), which is why it works for any point on Earth.",
},
}
@@ -875,7 +875,7 @@ def glossary_index(request: Request) -> Response:
"terms": [{"slug": s, **e} for s, e in GLOSSARY.items()],
"canonical_path": "/glossary",
"breadcrumb": [("Home", f"{BASE}/"), ("Glossary", None)],
- "page_title": "Weather & climate glossary — heat index, feels-like, percentile, and more",
+ "page_title": "Weather & climate glossary: heat index, feels-like, percentile, and more",
"page_description": ("Plain-language definitions of weather and climate terms: climate normal, percentile, "
"temperature anomaly, feels-like, heat index, wind chill, humidity, and reanalysis."),
}
@@ -890,7 +890,7 @@ def glossary_term(request: Request, term: str) -> Response:
"term": entry["term"], "body": _glossary_body(entry),
"canonical_path": f"/glossary/{term}",
"breadcrumb": [("Home", f"{BASE}/"), ("Glossary", f"{BASE}/glossary"), (entry["term"], None)],
- "page_title": f"{entry['term']} — what it means | Thermograph",
+ "page_title": f"{entry['term']}: what it means | Thermograph",
"page_description": entry["short"],
"others": [{"slug": s, "term": e["term"]} for s, e in GLOSSARY.items() if s != term],
}
@@ -901,7 +901,7 @@ def about_page(request: Request) -> Response:
ctx = {
"canonical_path": "/about",
"breadcrumb": [("Home", f"{BASE}/"), ("About", None)],
- "page_title": "About Thermograph — how the weather grades are calculated",
+ "page_title": "About Thermograph: how the weather grades are calculated",
"page_description": ("How Thermograph works: ~45 years of ERA5 climate history, a ±7-day seasonal "
"window, and empirical percentiles that grade each day relative to its own location."),
}
@@ -912,7 +912,7 @@ def privacy_page(request: Request) -> Response:
ctx = {
"canonical_path": "/privacy",
"breadcrumb": [("Home", f"{BASE}/"), ("Privacy", None)],
- "page_title": "Privacy — Thermograph",
+ "page_title": "Privacy | Thermograph",
"page_description": ("What Thermograph does and does not collect: no tracking, no ads, "
"no account required, and location that never leaves your browser."),
}
@@ -933,7 +933,7 @@ _HOME_JSONLD = {
"operatingSystem": "Web, iOS, Android",
"isAccessibleForFree": True,
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"},
- "description": ("How unusual is your weather? Any day, anywhere on Earth — graded "
+ "description": ("How unusual is your weather? Any day, anywhere on Earth, graded "
"against 45 years of that place's own history."),
}
diff --git a/digest.py b/digest.py
index a889ab9..0bce6ec 100644
--- a/digest.py
+++ b/digest.py
@@ -71,7 +71,7 @@ async def _store(email: str, place: str | None, source: str | None) -> None:
email,
"Confirm your Thermograph digest",
"Thanks for signing up for the Thermograph monthly digest.\n\n"
- "Confirmation links are not active yet — you're on the list, and "
+ "Confirmation links are not active yet, but you're on the list, and "
"you'll get a confirmation as soon as they are.\n",
)
diff --git a/discord.py b/discord.py
index 7e08ece..3911fd8 100644
--- a/discord.py
+++ b/discord.py
@@ -56,7 +56,7 @@ def _line(card: dict) -> str:
tag = " · ".join(p for p in (pct, grade) if p)
when = f" ({card['date_label']})" if card.get("date_label") else ""
normal_txt = f", normal {normal}" if normal else ""
- return f"**{display}** — {metric} {val}{normal_txt}{when}" + (f" · {tag}" if tag else "")
+ return f"**{display}**: {metric} {val}{normal_txt}{when}" + (f" · {tag}" if tag else "")
def build_embed(feed: dict) -> dict:
diff --git a/notify.py b/notify.py
index 001a8f2..58e5ae7 100644
--- a/notify.py
+++ b/notify.py
@@ -104,10 +104,10 @@ def _compose(sub, event_date, metric, direction, graded):
val_txt = f" ({value}{unit})" if value is not None else ""
if sub.kind == "forecast":
body = (f"Forecast for {event_date}: the {noun} is projected at the "
- f"{pct:g}th percentile{val_txt} — {grade}.")
+ f"{pct:g}th percentile{val_txt}, {grade}.")
else:
body = (f"On {event_date}, the {noun} hit the {pct:g}th "
- f"percentile{val_txt} — {grade}.")
+ f"percentile{val_txt}, {grade}.")
return title, body
diff --git a/scoring.py b/scoring.py
index c66decd..ef486ec 100644
--- a/scoring.py
+++ b/scoring.py
@@ -186,7 +186,7 @@ def _metric_entry(metric: str, base: np.ndarray, rec: np.ndarray) -> dict:
"key": metric, "label": METRIC_LABELS[metric],
"score": score, "mad": div["mad"], "bias": div["bias"],
"tier": tier, "class": css, "direction": direction,
- "grade": tier if score < 15 else f"{tier} — {direction}",
+ "grade": tier if score < 15 else f"{tier}, {direction}",
"weight": WEIGHTS[metric], "per_q": div["per_q"],
"n_recent": int(rec.size), "n_base": int(base.size),
}
@@ -265,7 +265,7 @@ def _annual_entry(metric: str, seasonal: list, history: pl.DataFrame,
"key": metric, "label": METRIC_LABELS[metric],
"score": score, "mad": round(mad, 1), "bias": round(bias, 1),
"tier": tier, "class": css, "direction": direction,
- "grade": tier if score < 15 else f"{tier} — {direction}",
+ "grade": tier if score < 15 else f"{tier}, {direction}",
"weight": WEIGHTS[metric], "per_q": per_q,
"n_recent": sum(e["n_recent"] for e in present),
"n_base": sum(e["n_base"] for e in present),
@@ -286,7 +286,7 @@ def build_scores(history: pl.DataFrame) -> dict:
years = history["date"].dt.year()
span = int(years.max() - years.min() + 1)
if span < MIN_BASELINE_YEARS:
- return {"unavailable": f"Only {span} years of record here — climate drift "
+ return {"unavailable": f"Only {span} years of record here; climate drift "
f"needs at least {MIN_BASELINE_YEARS}."}
recent, baseline = recent_baseline_split(history)
latest = history["date"].max()
diff --git a/templates/about.html.j2 b/templates/about.html.j2
index 5d20af3..1c38215 100644
--- a/templates/about.html.j2
+++ b/templates/about.html.j2
@@ -13,14 +13,14 @@
location's own climate history.
Where the data comes from
- The ~45-year daily record — high, low, feels-like, humidity, wind, gust and precipitation —
+
The ~45-year daily record (high, low, feels-like, humidity, wind, gust and precipitation)
comes from the ERA5 reanalysis (ECMWF), served via Open-Meteo.
Because reanalysis is a gridded, model-consistent record, Thermograph works even where there's
- no weather station — anywhere on the ~4 sq mi grid.
+ no weather station, anywhere on the ~4 sq mi grid.
How a day is graded
For each metric and date, Thermograph builds a reference distribution from every historical day
- within ±7 days of that day-of-year — a 15-day seasonal window across all years. The
+ within ±7 days of that day-of-year, a 15-day seasonal window across all years. The
observed value is placed on that distribution as an empirical
percentile, then mapped to a grade from “Below Normal” through “High” to “Near Record”.
Everything is relative to the location: a {{ temp(60) }} day can be “Above Normal” in one place and
diff --git a/templates/city.html.j2 b/templates/city.html.j2
index 5eee6ed..969ae93 100644
--- a/templates/city.html.j2
+++ b/templates/city.html.j2
@@ -18,13 +18,13 @@
{% if flavor %}
{{ flavor.extract }}{% if flavor.url %}
- — via Wikipedia{% endif %}
+ via Wikipedia{% endif %}
{% endif %}
{% if today %}
How today compares
- Latest recorded day: {{ today.date }} — each reading placed on {{ display }}'s
+
Latest recorded day: {{ today.date }}. Each reading placed on {{ display }}'s
own ±7-day seasonal distribution.
{% for c in today.cards %}
@@ -82,14 +82,14 @@
{% endfor %}
Each bar spans the 10th-percentile daily low to the 90th-percentile daily
- high — the range most days fall within — coloured cold → hot on a shared
+ high (the range most days fall within), coloured cold → hot on a shared
{{ temp(-10) }} to {{ temp(115) }} scale, so the whole year's rhythm reads at a glance.
Thinking of visiting {{ name }}?
Trips live and die by the weather. See how {{ name }}'s comfort stacks up against where
- you live — {{ name }} is pre-loaded, just add your city.
+ you live. {{ name }} is pre-loaded, just add your city.
Compare {{ name }}'s comfort with your city →
@@ -105,7 +105,7 @@
{% endif %}
{% endblock %}
diff --git a/templates/glossary_term.html.j2 b/templates/glossary_term.html.j2
index 8febad1..e140efe 100644
--- a/templates/glossary_term.html.j2
+++ b/templates/glossary_term.html.j2
@@ -10,7 +10,7 @@
{{ term }}
{{ body | safe }}
- See it live — grade any location's weather →
+ See it live: grade any location's weather →