Move inline styles to CSS classes; update README

Extract repeated inline styles from templates into named CSS classes in
base.html: layout helpers, buttons, form groups, alert boxes, results table
rules, row highlight classes, typography utilities, and empty-state styles.
Remove the per-page <style> block from results.html.

Update README to reflect current destinations, GraphQL data source, Circle
Line timetable, configurable connection range, and GWR fare table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-04-04 15:39:04 +01:00
parent e6f310f517
commit 71be0dd8cf
4 changed files with 214 additions and 97 deletions

View file

@ -5,31 +5,24 @@
{% block twitter_title %}Bristol to {{ destination }} via Eurostar{% endblock %}
{% block twitter_description %}Train options from Bristol Temple Meads to {{ destination }} on {{ travel_date_display }} via Paddington, St Pancras, and Eurostar.{% endblock %}
{% block content %}
<style>
@media (max-width: 640px) {
.col-transfer { display: none; }
}
</style>
<p style="margin-bottom:1rem">
<p class="back-link">
<a href="{{ url_for('index') }}">&larr; New search</a>
</p>
<div class="card" style="margin-bottom:1.5rem">
<h2 style="margin-top:0">
<h2>
Bristol Temple Meads &rarr; {{ destination }}
</h2>
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem">
<div class="date-nav">
<a href="{{ url_for('results', slug=slug, travel_date=prev_date, min_connection=min_connection, max_connection=max_connection) }}"
style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px;
text-decoration:none;color:#00539f;font-size:0.9rem">&larr; Prev</a>
class="btn-nav">&larr; Prev</a>
<strong>{{ travel_date_display }}</strong>
<a href="{{ url_for('results', slug=slug, travel_date=next_date, min_connection=min_connection, max_connection=max_connection) }}"
style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px;
text-decoration:none;color:#00539f;font-size:0.9rem">Next &rarr;</a>
class="btn-nav">Next &rarr;</a>
</div>
<div style="margin:0.9rem 0 1rem">
<div style="font-size:0.9rem;font-weight:600;margin-bottom:0.45rem">Switch destination for {{ travel_date_display }}</div>
<div class="switcher-section">
<div class="section-label">Switch destination for {{ travel_date_display }}</div>
<div class="chip-row">
{% for destination_slug, destination_name in destinations.items() %}
{% if destination_slug == slug %}
@ -43,26 +36,26 @@
{% endfor %}
</div>
</div>
<div style="margin-top:0.75rem;display:flex;gap:1.5rem;align-items:center">
<div class="filter-row">
<div>
<label for="min_conn_select" style="font-size:0.9rem;font-weight:600;margin-right:0.5rem">
<label for="min_conn_select" class="filter-label">
Min connection:
</label>
<select id="min_conn_select"
onchange="applyConnectionFilter()"
style="padding:0.3rem 0.6rem;font-size:0.9rem;border:1px solid #cbd5e0;border-radius:4px">
class="select-inline">
{% for mins in valid_min_connections %}
<option value="{{ mins }}" {% if mins == min_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
<div>
<label for="max_conn_select" style="font-size:0.9rem;font-weight:600;margin-right:0.5rem">
<label for="max_conn_select" class="filter-label">
Max connection:
</label>
<select id="max_conn_select"
onchange="applyConnectionFilter()"
style="padding:0.3rem 0.6rem;font-size:0.9rem;border:1px solid #cbd5e0;border-radius:4px">
class="select-inline">
{% for mins in valid_max_connections %}
<option value="{{ mins }}" {% if mins == max_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
@ -76,45 +69,45 @@
window.location = '{{ url_for('results', slug=slug, travel_date=travel_date) }}?min_connection=' + min + '&max_connection=' + max;
}
</script>
<p style="color:#4a5568;margin:0">
<p class="card-meta">
{{ gwr_count }} GWR service{{ 's' if gwr_count != 1 }}
&nbsp;&middot;&nbsp;
{{ eurostar_count }} Eurostar service{{ 's' if eurostar_count != 1 }}
{% if unreachable_morning_services %}
&nbsp;&middot;&nbsp;
<span style="color:#718096">
<span class="text-muted">
{{ unreachable_morning_services | length }} Eurostar service{{ 's' if unreachable_morning_services | length != 1 }} unavailable from Bristol
</span>
{% endif %}
{% if from_cache %}
&nbsp;&middot;&nbsp; <span style="color:#718096;font-size:0.85rem">(cached)</span>
&nbsp;&middot;&nbsp; <span class="text-muted text-sm">(cached)</span>
{% endif %}
</p>
{% if error %}
<div style="margin-top:1rem;padding:0.75rem 1rem;background:#fff5f5;border:1px solid #fc8181;border-radius:4px;color:#c53030">
<div class="alert alert-error">
<strong>Warning:</strong> {{ error }}
</div>
{% endif %}
{% if no_prices_note %}
<div style="margin-top:1rem;padding:0.75rem 1rem;background:#fffbeb;border:1px solid #f6e05e;border-radius:4px;color:#744210">
<div class="alert alert-warning">
{{ no_prices_note }}
</div>
{% endif %}
</div>
{% if trips or unreachable_morning_services %}
<div class="card" style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:0.95rem">
<div class="card card-scroll">
<table class="results-table">
<thead>
<tr style="border-bottom:2px solid #e2e8f0;text-align:left">
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Bristol</th>
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Paddington</th>
<th style="padding:0.6rem 0.8rem;white-space:nowrap">GWR Fare</th>
<th class="col-transfer" style="padding:0.6rem 0.8rem;white-space:nowrap">Transfer</th>
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Depart STP</th>
<th style="padding:0.6rem 0.8rem">{{ destination }}</th>
<th style="padding:0.6rem 0.8rem;white-space:nowrap">ES Std</th>
<th style="padding:0.6rem 0.8rem;white-space:nowrap">Total</th>
<tr>
<th class="nowrap">Bristol</th>
<th class="nowrap">Paddington</th>
<th class="nowrap">GWR Fare</th>
<th class="col-transfer nowrap">Transfer</th>
<th class="nowrap">Depart STP</th>
<th>{{ destination }}</th>
<th class="nowrap">ES Std</th>
<th class="nowrap">Total</th>
</tr>
</thead>
<tbody>
@ -129,91 +122,91 @@
{% endif %}
{% for row in result_rows %}
{% if row.row_type == 'trip' and row.total_minutes <= best_mins + 5 and trips | length > 1 %}
{% set row_bg = 'background:#f0fff4' %}
{% set row_class = 'row-fast' %}
{% elif row.row_type == 'trip' and row.total_minutes >= worst_mins - 5 and trips | length > 1 %}
{% set row_bg = 'background:#fff5f5' %}
{% set row_class = 'row-slow' %}
{% elif row.row_type == 'unreachable' %}
{% set row_bg = 'background:#f7fafc;color:#a0aec0' %}
{% set row_class = 'row-unreachable' %}
{% elif loop.index is odd %}
{% set row_bg = 'background:#f7fafc' %}
{% set row_class = 'row-alt' %}
{% else %}
{% set row_bg = '' %}
{% set row_class = '' %}
{% endif %}
<tr style="border-bottom:1px solid #e2e8f0;{{ row_bg }}">
<tr class="{{ row_class }}">
{% if row.row_type == 'trip' %}
<td style="padding:0.6rem 0.8rem;font-weight:600">
<td class="font-bold">
{{ row.depart_bristol }}
{% if row.headcode %}<br><span style="font-size:0.75rem;font-weight:400;color:#718096">{{ row.headcode }}</span>{% endif %}
{% if row.headcode %}<br><span class="text-xs font-normal text-muted">{{ row.headcode }}</span>{% endif %}
</td>
<td style="padding:0.6rem 0.8rem">
<td>
{{ row.arrive_paddington }}
<span style="font-size:0.8rem;color:#718096;white-space:nowrap">({{ row.gwr_duration }})</span>
<span class="text-sm text-muted nowrap">({{ row.gwr_duration }})</span>
</td>
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
<td class="nowrap">
£{{ "%.2f"|format(row.ticket_price) }}
<br><span style="font-size:0.75rem;color:#718096">{{ row.ticket_name }}</span>
<br><span class="text-xs text-muted">{{ row.ticket_name }}</span>
</td>
<td class="col-transfer" style="padding:0.6rem 0.8rem;color:#4a5568;white-space:nowrap">
<td class="col-transfer nowrap" style="color:#4a5568">
{{ row.connection_duration }}{% if row.connection_minutes < 80 %} <span title="Tight connection">⚠️</span>{% endif %}
<br><span style="font-size:0.75rem;color:#718096">Circle {{ row.circle_line_depart }} → STP {{ row.circle_arrive_checkin }}</span>
<br><span class="text-xs text-muted">Circle {{ row.circle_line_depart }} → STP {{ row.circle_arrive_checkin }}</span>
</td>
<td style="padding:0.6rem 0.8rem;font-weight:600">
<td class="font-bold">
{{ row.depart_st_pancras }}
{% if row.train_number %}<br><span style="font-size:0.75rem;font-weight:400;color:#718096">{% for part in row.train_number.split(' + ') %}<span style="white-space:nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
{% if row.train_number %}<br><span class="text-xs font-normal text-muted">{% for part in row.train_number.split(' + ') %}<span class="nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
</td>
<td style="padding:0.6rem 0.8rem">
<td>
{{ row.arrive_destination }}
<span style="font-weight:400;color:#718096;font-size:0.85em">(CET)</span>
{% if row.eurostar_duration %}<br><span style="font-size:0.8rem;color:#718096;white-space:nowrap">({{ row.eurostar_duration }})</span>{% endif %}
<span class="font-normal text-muted" style="font-size:0.85em">(CET)</span>
{% if row.eurostar_duration %}<br><span class="text-sm text-muted nowrap">({{ row.eurostar_duration }})</span>{% endif %}
</td>
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
<td class="nowrap">
{% if row.eurostar_price is not none %}
£{{ "%.2f"|format(row.eurostar_price) }}
{% if row.eurostar_seats is not none %}
<br><span style="font-size:0.75rem;color:#718096">{{ row.eurostar_seats }} at this price</span>
<br><span class="text-xs text-muted">{{ row.eurostar_seats }} at this price</span>
{% endif %}
{% else %}
<span style="color:#718096">&ndash;</span>
<span class="text-muted">&ndash;</span>
{% endif %}
</td>
<td style="padding:0.6rem 0.8rem;font-weight:600;white-space:nowrap">
<td class="font-bold nowrap">
{% if row.total_minutes <= best_mins + 5 and trips | length > 1 %}
<span style="color:#276749" title="Fastest journey">{{ row.total_duration }} ⚡</span>
<span class="text-green" title="Fastest journey">{{ row.total_duration }} ⚡</span>
{% elif row.total_minutes >= worst_mins - 5 and trips | length > 1 %}
<span style="color:#c53030" title="Slowest journey">{{ row.total_duration }} 🐢</span>
<span class="text-red" title="Slowest journey">{{ row.total_duration }} 🐢</span>
{% else %}
<span style="color:#00539f">{{ row.total_duration }}</span>
<span class="text-blue">{{ row.total_duration }}</span>
{% endif %}
{% if row.total_price is not none %}
<br><span style="font-size:0.8rem;font-weight:700;color:#276749">£{{ "%.2f"|format(row.total_price) }}{% if min_price is defined and max_price is defined %}{% if row.total_price <= min_price + 10 %} <span title="Cheapest journey">🪙</span>{% elif row.total_price >= max_price - 10 %} <span title="Most expensive journey">💸</span>{% endif %}{% endif %}</span>
<br><span class="text-sm text-green" style="font-weight:700">£{{ "%.2f"|format(row.total_price) }}{% if min_price is defined and max_price is defined %}{% if row.total_price <= min_price + 10 %} <span title="Cheapest journey">🪙</span>{% elif row.total_price >= max_price - 10 %} <span title="Most expensive journey">💸</span>{% endif %}{% endif %}</span>
{% endif %}
</td>
{% else %}
<td style="padding:0.6rem 0.8rem;font-weight:600">&mdash;</td>
<td style="padding:0.6rem 0.8rem">&mdash;</td>
<td class="col-transfer" style="padding:0.6rem 0.8rem">&mdash;</td>
<td style="padding:0.6rem 0.8rem">n/a</td>
<td style="padding:0.6rem 0.8rem;font-weight:600">
<td class="font-bold">&mdash;</td>
<td>&mdash;</td>
<td class="col-transfer">&mdash;</td>
<td>n/a</td>
<td class="font-bold">
{{ row.depart_st_pancras }}
{% if row.train_number %}<br><span style="font-size:0.75rem;font-weight:400;color:#a0aec0">{% for part in row.train_number.split(' + ') %}<span style="white-space:nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
{% if row.train_number %}<br><span class="text-xs font-normal text-dimmed">{% for part in row.train_number.split(' + ') %}<span class="nowrap">{{ part }}</span>{% if not loop.last %} + {% endif %}{% endfor %}</span>{% endif %}
</td>
<td style="padding:0.6rem 0.8rem">
<td>
{{ row.arrive_destination }}
<span style="font-weight:400;color:#a0aec0;font-size:0.85em">(CET)</span>
{% if row.eurostar_duration %}<br><span style="font-size:0.8rem;color:#a0aec0;white-space:nowrap">({{ row.eurostar_duration }})</span>{% endif %}
<span class="font-normal text-dimmed" style="font-size:0.85em">(CET)</span>
{% if row.eurostar_duration %}<br><span class="text-sm text-dimmed nowrap">({{ row.eurostar_duration }})</span>{% endif %}
</td>
<td style="padding:0.6rem 0.8rem;white-space:nowrap">
<td class="nowrap">
{% if row.eurostar_price is not none %}
<span style="color:#a0aec0">£{{ "%.2f"|format(row.eurostar_price) }}</span>
<span class="text-dimmed">£{{ "%.2f"|format(row.eurostar_price) }}</span>
{% if row.eurostar_seats is not none %}
<br><span style="font-size:0.75rem;color:#a0aec0">{{ row.eurostar_seats }} at this price</span>
<br><span class="text-xs text-dimmed">{{ row.eurostar_seats }} at this price</span>
{% endif %}
{% else %}
<span style="color:#a0aec0">&ndash;</span>
<span class="text-dimmed">&ndash;</span>
{% endif %}
</td>
<td style="padding:0.6rem 0.8rem;font-weight:600">
<span title="No same-day Bristol connection" style="color:#a0aec0;white-space:nowrap">Too early</span>
<td class="font-bold">
<span title="No same-day Bristol connection" class="text-dimmed nowrap">Too early</span>
</td>
{% endif %}
</tr>
@ -222,7 +215,7 @@
</table>
</div>
<p style="margin-top:1rem;font-size:0.82rem;color:#718096">
<p class="footnote">
Paddington &rarr; St&nbsp;Pancras connection: {{ min_connection }}&ndash;{{ max_connection }}&nbsp;min.
GWR walk-on single prices for Bristol Temple Meads&nbsp;&rarr;&nbsp;Paddington.
Eurostar Standard prices are for 1 adult in GBP; always check
@ -234,9 +227,9 @@
</p>
{% else %}
<div class="card" style="color:#4a5568;text-align:center;padding:3rem 2rem">
<p style="font-size:1.1rem;margin:0 0 0.5rem">No valid journeys found.</p>
<p style="font-size:0.9rem;margin:0">
<div class="card empty-state">
<p>No valid journeys found.</p>
<p>
{% if gwr_count == 0 and eurostar_count == 0 %}
Could not retrieve train data. Check your network connection or try again.
{% elif gwr_count == 0 %}