.ctool--mileage-checker{--ct-gap:16px;color:var(--cts-text);line-height:1.55}
.ctool--mileage-checker p{margin:0 0 14px}
.ctool--mileage-checker .ctool-how{font-size:.95rem;color:var(--cts-text-2);margin:0 0 18px}
.ctool--mileage-checker .ctool-panel{background:#fff;border:1px solid var(--cts-line);border-radius:16px;box-shadow:0 10px 30px rgba(7,24,44,.06);padding:22px}
.ctool--mileage-checker .ctool-grid{display:grid;grid-template-columns:1fr;gap:12px}
.ctool--mileage-checker .ctool-field label{display:block;font-weight:700;font-size:.9rem;color:var(--cts-ink);margin-bottom:6px}
.ctool--mileage-checker .ctool-field input{width:100%;box-sizing:border-box;border:1px solid var(--cts-line-2,#c9d2df);border-radius:10px;padding:12px 14px;font-size:16px;font-family:inherit;color:var(--cts-text);background:#fff}
.ctool--mileage-checker .ctool-field input:focus{outline:2px solid var(--cts-brand);outline-offset:1px;border-color:var(--cts-brand)}
.ctool--mileage-checker .ctool-swap{justify-self:center;margin:-2px 0}
.ctool--mileage-checker .ctool-swap button{width:44px;height:44px;border-radius:10px;border:1px solid var(--cts-line-2,#c9d2df);background:var(--cts-stone);color:var(--cts-ink);font-size:18px;cursor:pointer;font-family:inherit;transform:rotate(90deg)}
.ctool--mileage-checker .ctool-swap button:hover{background:var(--cts-brand-tint)}
.ctool--mileage-checker .ctool-via{margin-top:14px}
.ctool--mileage-checker .ctool-via-row{display:none;margin-top:12px}
.ctool--mileage-checker .ctool-via-row.is-on{display:block}
.ctool--mileage-checker .ctool-controls{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-top:18px}
.ctool--mileage-checker .ctool-btn:not(.ctool-btn--pdf),.ctool-cta .ctool-btn{display:inline-block;background:var(--cts-brand);color:#fff;border:0;border-radius:10px;font-weight:700;font-size:1rem;padding:12px 22px;cursor:pointer;font-family:inherit;text-decoration:none}
.ctool--mileage-checker .ctool-btn:not(.ctool-btn--pdf):hover{background:var(--cts-brand-700,#0a4fd6)}
.ctool--mileage-checker .ctool-btn-ghost{background:transparent;color:var(--cts-brand);border:1px solid var(--cts-line-2,#c9d2df);padding:11px 18px;border-radius:10px;font-weight:700;cursor:pointer;font-family:inherit;font-size:.95rem}
.ctool--mileage-checker .ctool-btn-ghost:hover{background:var(--cts-stone)}
.ctool--mileage-checker .ctool-price{display:flex;align-items:center;gap:8px;margin-top:2px;font-size:.92rem;color:var(--cts-text-2)}
.ctool--mileage-checker .ctool-price input{width:82px;border:1px solid var(--cts-line-2,#c9d2df);border-radius:8px;padding:8px 10px;font-size:15px;font-family:inherit}
.ctool--mileage-checker .ctool-msg{margin-top:14px;padding:12px 14px;border-radius:10px;font-size:.94rem;display:none}
.ctool--mileage-checker .ctool-msg.is-err{display:block;background:#fdecec;border:1px solid var(--cts-danger,#d23b3b);color:#8f1f1f}
.ctool--mileage-checker .ctool-msg.is-note{display:block;background:var(--cts-brand-tint);border:1px solid var(--cts-line);color:var(--cts-ink-700)}
.ctool--mileage-checker .ctool-placeholder{margin:0}
.ctool--mileage-checker .ctool-results{display:none}
.ctool--mileage-checker .ctool-results.is-on{display:block}
.ctool--mileage-checker .ctool-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:18px}
.ctool--mileage-checker .ctool-stat{background:var(--cts-stone);border:1px solid var(--cts-line);border-radius:14px;padding:16px}
.ctool--mileage-checker .ctool-stat .k{font-size:.82rem;font-weight:700;text-transform:none;color:var(--cts-text-2);margin-bottom:6px}
.ctool--mileage-checker .ctool-stat .v{font-size:1.7rem;font-weight:800;color:var(--cts-ink);line-height:1.1}
.ctool--mileage-checker .ctool-stat .s{font-size:.85rem;color:var(--cts-muted,#67788c);margin-top:4px}
.ctool--mileage-checker .ctool-map{height:440px;min-height:420px;border-radius:14px;border:1px solid var(--cts-line);margin-bottom:18px;z-index:0}
.ctool--mileage-checker .ctool-tablewrap{overflow-x:auto;border:1px solid var(--cts-line);border-radius:14px}
.ctool--mileage-checker table{width:100%;border-collapse:collapse;font-size:.94rem;min-width:420px}
.ctool--mileage-checker th,.ctool--mileage-checker td{text-align:left;padding:11px 14px;border-bottom:1px solid var(--cts-line)}
.ctool--mileage-checker thead th{background:var(--cts-ink);color:#fff;font-weight:700}
.ctool--mileage-checker tbody tr:last-child td{border-bottom:0}
.ctool--mileage-checker td.num,.ctool--mileage-checker th.num{text-align:right;font-variant-numeric:tabular-nums}
.ctool--mileage-checker .ctool-cap{font-size:.85rem;color:var(--cts-muted,#67788c);margin:8px 2px 0}
.ctool--mileage-checker h2{color:var(--cts-ink);margin-top:34px}
.ctool--mileage-checker .ctool-support h2:first-child{margin-top:0}
.ctool--mileage-checker details{border:1px solid var(--cts-line);border-radius:12px;padding:2px 16px;margin-bottom:10px;background:#fff}
.ctool--mileage-checker summary{cursor:pointer;font-weight:700;padding:12px 0;color:var(--cts-ink)}
.ctool--mileage-checker details p{padding-bottom:4px}
.ctool-cta{background:var(--cts-ink);border-radius:16px;padding:30px 28px;margin-top:34px;color:#fff}
.ctool-cta h2{color:#fff;margin:0 0 8px}
.ctool-cta p{color:#d8e3f2;margin:0 0 18px;max-width:60ch}
.ctool--mileage-checker .ctool-spin{display:inline-block;width:16px;height:16px;border:2px solid rgba(255,255,255,.5);border-top-color:#fff;border-radius:50%;animation:ctool-spin .7s linear infinite;vertical-align:-3px;margin-right:8px}
@keyframes ctool-spin{to{transform:rotate(360deg)}}
@media(max-width:560px){
.ctool--mileage-checker .ctool-stats{grid-template-columns:1fr}
}
(function(){
var $=function(id){return document.getElementById(id);};
var UKGALLON=4.54609;
var els={from:$('mc-from'),to:$('mc-to'),via:$('mc-via'),viaRow:$('mc-via-row'),viaToggle:$('mc-via-toggle'),
go:$('mc-go'),swap:$('mc-swap'),price:$('mc-price'),msg:$('mc-msg'),results:$('mc-results'),placeholder:$('mc-placeholder'),
miles:$('mc-miles'),km:$('mc-km'),time:$('mc-time'),route:$('mc-route'),source:$('mc-source'),fuel:$('mc-fuel')};
var map=null,layer=null,lastMiles=null,lastCalc=null,lastFuelRows=[];
var geoCache={};
function norm(s){return (s||'').toUpperCase().replace(/s+/g,' ').trim();}
function compact(s){return norm(s).replace(/s+/g,'');}
function outward(s){var c=compact(s);if(c.length<=4)return c;return c.slice(0,c.length-3);}
function showMsg(t,kind){els.msg.className='ctool-msg is-'+kind;els.msg.textContent=t;}
function clearMsg(){els.msg.className='ctool-msg';els.msg.textContent='';}
function fetchJSON(url,ms){
var ctrl=new AbortController();var timer=setTimeout(function(){ctrl.abort();},ms||8000);
return fetch(url,{signal:ctrl.signal}).then(function(r){clearTimeout(timer);return r.json();})
.catch(function(e){clearTimeout(timer);throw e;});
}
function geocode(raw){
var key=compact(raw);
if(!key)return Promise.reject(new Error('empty'));
if(geoCache[key])return Promise.resolve(geoCache[key]);
var full='https://api.postcodes.io/postcodes/'+encodeURIComponent(key);
return fetchJSON(full).then(function(j){
if(j&&j.status===200&&j.result){
var r=j.result;var g={lat:r.latitude,lon:r.longitude,label:r.postcode,area:r.admin_district||r.parish||r.region,approx:false};
geoCache[key]=g;return g;
}
throw new Error('not-full');
}).catch(function(){
var oc=outward(raw);
return fetchJSON('https://api.postcodes.io/outcodes/'+encodeURIComponent(oc)).then(function(j){
if(j&&j.status===200&&j.result&&j.result.latitude!=null){
var r=j.result;var area=(r.admin_district&&r.admin_district[0])||(r.region&&r.region[0])||oc;
var g={lat:r.latitude,lon:r.longitude,label:oc,area:area,approx:true};
geoCache[key]=g;return g;
}
throw new Error('bad-postcode');
});
});
}
function haversine(a,b){
var R=6371,rad=Math.PI/180;
var dLat=(b.lat-a.lat)*rad,dLon=(b.lon-a.lon)*rad;
var la1=a.lat*rad,la2=b.lat*rad;
var h=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(la1)*Math.cos(la2)*Math.sin(dLon/2)*Math.sin(dLon/2);
return 2*R*Math.asin(Math.min(1,Math.sqrt(h)));
}
function fmtTime(mins){
mins=Math.round(mins);var h=Math.floor(mins/60),m=mins%60;
if(h<=0)return m+' min';return h+' h '+(m=100?Math.round(mi).toLocaleString('en-GB'):mi.toFixed(1);}
function ensureMap(){
if(map)return;
map=L.map('mc-map',{scrollWheelZoom:false}).setView([54,-2.5],5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
maxZoom:19,attribution:'©
OpenStreetMap contributors'}).addTo(map);
}
function drawMap(points,line,dashed){
ensureMap();
if(layer)map.removeLayer(layer);
layer=L.layerGroup().addTo(map);
points.forEach(function(p,i){
var colour=i===0?'#15A66B':(i===points.length-1?'#1065FF':'#0B2447');
L.circleMarker([p.lat,p.lon],{radius:8,color:'#fff',weight:2,fillColor:colour,fillOpacity:1})
.bindTooltip(p.label,{permanent:false}).addTo(layer);
});
var latlngs=line?line.map(function(c){return [c[1],c[0]];}):points.map(function(p){return [p.lat,p.lon];});
var poly=L.polyline(latlngs,{color:'#1065FF',weight:5,opacity:.85,dashArray:dashed?'8,10':null}).addTo(layer);
map.fitBounds(poly.getBounds().pad(0.15));
setTimeout(function(){map.invalidateSize();map.fitBounds(poly.getBounds().pad(0.15));},80);
}
function renderFuel(miles){
lastMiles=miles;
var price=parseFloat(els.price.value);if(!(price>0))price=146.9;
var rows=[['44t articulated lorry',9.5],['3.5t diesel van',28]];
lastFuelRows=[];
els.fuel.innerHTML=rows.map(function(r){
var litres=(miles/r[1])*UKGALLON;
var cost=litres*(price/100);
lastFuelRows.push([r[0],r[1].toFixed(1)+' mpg',litres.toFixed(1)+' L','£'+cost.toFixed(2)]);
return '
| '+r[0]+' | '+r[1].toFixed(1)+' mpg | '+
litres.toFixed(1)+' L | £'+cost.toFixed(2)+' |
';
}).join('');
}
function setBusy(on){
els.go.disabled=on;
els.go.innerHTML=on?'
Calculating':'Calculate route';
}
function run(){
clearMsg();
var fromRaw=els.from.value,toRaw=els.to.value;
var viaRaw=(els.viaRow.classList.contains('is-on')?els.via.value:'').trim();
if(!compact(fromRaw)||!compact(toRaw)){showMsg('Please enter both a start and a destination postcode.','err');return;}
if(compact(fromRaw)===compact(toRaw)){showMsg('Start and destination are the same postcode, so the distance is zero.','err');return;}
setBusy(true);
var jobs=[geocode(fromRaw),geocode(toRaw)];
if(viaRaw)jobs.push(geocode(viaRaw));
Promise.all(jobs).then(function(pts){
var start=pts[0],end=pts[1],via=viaRaw?pts[2]:null;
var ordered=via?[start,via,end]:[start,end];
var coordStr=ordered.map(function(p){return p.lon+','+p.lat;}).join(';');
var osrm='https://router.project-osrm.org/route/v1/driving/'+coordStr+'?overview=full&geometries=geojson';
return fetchJSON(osrm).then(function(j){
if(!j||j.code!=='Ok'||!j.routes||!j.routes.length)throw new Error('osrm');
var rt=j.routes[0];
var miles=rt.distance/1609.344;
var carMin=rt.duration/60;
var hgvMin=carMin/0.85;
show(ordered,miles,hgvMin,rt.geometry.coordinates,false,via,start,end);
}).catch(function(){
var km=0;for(var i=0;i<ordered.length-1;i++)km+=haversine(ordered[i],ordered[i+1]);
var roadKm=km*1.25;var miles=roadKm/1.609344;
var hgvMin=(miles/45)*60;
showMsg('The live routing service did not respond, so this is a straight-line estimate (haversine x 1.25). No exact road line is shown.','note');
show(ordered,miles,hgvMin,null,true,via,start,end);
});
}).catch(function(e){
showMsg('We could not find one of those postcodes. Check for typos, or try the outward code only (for example LS10).','err');
els.results.classList.remove('is-on');
}).then(function(){setBusy(false);});
}
function show(ordered,miles,hgvMin,line,est,via,start,end){
if(els.placeholder)els.placeholder.style.display='none';
els.results.classList.add('is-on');
els.miles.innerHTML=fmtMiles(miles)+'
mi';
els.km.textContent=(miles*1.609344).toFixed(1)+' km'+(est?' (estimate)':'');
els.time.textContent=fmtTime(hgvMin)+(est?'*':'');
els.route.textContent=start.label+(via?' via '+via.label:'')+' to '+end.label;
var approx=[start,end].concat(via?[via]:[]).some(function(p){return p.approx;});
var sourceTxt=est?'Straight-line estimate':(approx?'OSRM road route (outward-level start or end)':'OSRM road route');
els.source.textContent=sourceTxt;
var maxKm=0;for(var i=0;i1200){showMsg('That is a very long distance for a single leg. Check the postcodes if this looks wrong.','note');}
drawMap(ordered,line,est);
renderFuel(miles);
lastCalc={
from:start.label,to:end.label,via:via?via.label:null,
route:start.label+(via?' via '+via.label:'')+' to '+end.label,
miles:fmtMiles(miles)+' mi',km:(miles*1.609344).toFixed(1)+' km'+(est?' (estimate)':''),
time:fmtTime(hgvMin)+(est?' (estimate)':''),source:sourceTxt,
price:(parseFloat(els.price.value)>0?parseFloat(els.price.value):146.9).toFixed(1)+' p/litre',
est:est
};
els.results.scrollIntoView({behavior:'smooth',block:'nearest'});
}
els.go.addEventListener('click',run);
[els.from,els.to,els.via].forEach(function(inp){
inp.addEventListener('keydown',function(e){if(e.key==='Enter')run();});
});
els.swap.addEventListener('click',function(){
var t=els.from.value;els.from.value=els.to.value;els.to.value=t;
if(els.results.classList.contains('is-on'))run();
});
els.viaToggle.addEventListener('click',function(){
var on=els.viaRow.classList.toggle('is-on');
els.viaToggle.textContent=on?'Remove via point':'Add via point';
if(!on){els.via.value='';if(els.results.classList.contains('is-on'))run();}
else els.via.focus();
});
els.price.addEventListener('input',function(){if(lastMiles!=null){renderFuel(lastMiles);if(lastCalc)lastCalc.price=(parseFloat(els.price.value)>0?parseFloat(els.price.value):146.9).toFixed(1)+' p/litre';}});
window.ctsToolPDF={
title:'Mileage Calculator UK',
subtitle:'Free mileage calculator for two UK postcodes: road miles, kilometres, estimated HGV journey time, diesel cost and a live route map. No sign-up needed.',
filename:'mileage-calculator',
getSections:function(){
var c=lastCalc;
if(!c){return null;}
var kv=[['Start postcode',c.from],['Destination postcode',c.to]];
if(c.via)kv.push(['Via postcode',c.via]);
kv.push(['Diesel price',c.price]);
return [
{h:'Route'},
{kv:kv},
{h:'Results'},
{kv:[
['Road distance',c.miles],
['In kilometres',c.km],
['Estimated HGV drive time',c.time],
['Route',c.route],
['Source',c.source]
]},
{h:'Indicative diesel cost'},
{table:{head:['Vehicle','Economy','Litres used','Fuel cost'],rows:lastFuelRows}},
{note:'Road distance and drive time use the OSRM routing engine on OpenStreetMap data, with a 0.85 HGV speed factor applied (about 18 percent longer than a car). Fuel cost uses 9.5 mpg for a typical 44 tonne articulated lorry and 28 mpg for a 3.5 tonne diesel van at the diesel price shown. Assumptions current as of July 2026.'}
];
}
};
})();