The Marathon Clinic

Consistency Is Key. Balance Is Everything.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Marathon Pace Calculator</title>
<style>
  :root{
    --bg:#0f1115;
    --panel:#171a21;
    --panel-2:#1f2430;
    --text:#e8e8ef;
    --muted:#aeb3c2;
    --accent:#6ee7ff;
    --accent-2:#8fff9f;
    --danger:#ff7b7b;
    --ok:#79e879;
    --warning:#ffd166;
    --border:#2a3040;
  }
  *{box-sizing:border-box;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,"Helvetica Neue",Arial;}
  body{
    margin:0;background:linear-gradient(140deg,#0e0f14,#151a20 45%,#10141a);
    color:var(--text);
  }
  header{
    padding:24px 20px 10px 20px;max-width:1100px;margin:0 auto;
  }
  h1{margin:0 0 6px 0;font-size:28px;letter-spacing:0.2px}
  p.sub{margin:0;color:var(--muted)}
  .app{
    max-width:1100px;margin:18px auto 40px auto;display:grid;grid-template-columns:360px 1fr;gap:16px;padding:0 20px;
  }
  @media (max-width:980px){
    .app{grid-template-columns:1fr}
  }
  .card{
    background:radial-gradient(1200px 420px at -200px -200px, rgba(110,231,255,0.08), transparent),
               radial-gradient(900px 420px at 110% -200px, rgba(143,255,159,0.06), transparent),
               var(--panel);
    border:1px solid var(--border);
    border-radius:14px;
    box-shadow:0 10px 24px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03);
  }
  .card h2{
    font-size:16px;margin:0;padding:14px 16px;border-bottom:1px solid var(--border);
    letter-spacing:0.3px;color:#f3f5ff
  }
  .card .body{padding:14px 16px}
  .field{margin-bottom:12px}
  .field label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
  .field input[type="number"],
  .field input[type="text"],
  .field select{
    width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);
    background:var(--panel-2);color:var(--text);font-size:14px;outline:none;
  }
  .row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
  .row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
  .muted{color:var(--muted);font-size:12px}
  .toggle{display:flex;gap:8px;align-items:center;margin-bottom:10px}
  .toggle input{margin-right:6px}
  .mode{
    display:flex;gap:6px;margin-bottom:10px
  }
  .mode button{
    flex:1;padding:10px;border-radius:10px;border:1px solid var(--border);background:var(--panel-2);color:var(--text);
    cursor:pointer
  }
  .mode button.active{outline:2px solid var(--accent);background:#0e1218}
  .actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px}
  .btn{
    cursor:pointer;border-radius:10px;border:1px solid var(--border);padding:10px 12px;background:var(--panel-2);color:var(--text);
  }
  .btn.primary{outline:2px solid var(--accent)}
  .metrics{
    display:grid;grid-template-columns:repeat(4,1fr);gap:12px
  }
  .metric{
    background:var(--panel-2);border:1px solid var(--border);border-radius:12px;padding:12px
  }
  .metric .label{font-size:11px;color:var(--muted);margin-bottom:4px}
  .metric .value{font-size:18px}
  table{width:100%;border-collapse:collapse}
  th,td{padding:8px 10px;border-bottom:1px solid var(--border);text-align:right}
  th:first-child, td:first-child{text-align:left}
  tr:nth-child(odd){background:rgba(255,255,255,0.02)}
  .hint{font-size:12px;color:var(--muted);margin-top:6px}
  .footer{padding:16px;color:var(--muted);font-size:12px;text-align:center}
  .pill{
    display:inline-block;padding:4px 8px;border-radius:999px;border:1px solid var(--border);background:var(--panel-2);font-size:12px;margin-left:8px
  }
</style>
</head>
<body>
<header>
  <h1>Marathon Pace Calculator <span class="pill">temp‑based heat adjustment</span></h1>
  <p class="sub">Enter your goal or target pace, set race conditions, and get kilometre splits, 5 km blocks, and a downloadable CSV.</p>
</header>

<div class="app">
  <div class="card">
    <h2>Inputs</h2>
    <div class="body">
      <div class="mode">
        <button id="mode-time" class="active">Goal time</button>
        <button id="mode-pace">Target pace</button>
      </div>
      <div id="time-inputs">
        <div class="row3 field">
          <div>
            <label>Hours</label>
            <input id="goal-h" type="number" min="0" value="3">
          </div>
          <div>
            <label>Minutes</label>
            <input id="goal-m" type="number" min="0" max="59" value="15">
          </div>
          <div>
            <label>Seconds</label>
            <input id="goal-s" type="number" min="0" max="59" value="0">
          </div>
        </div>
      </div>
      <div id="pace-inputs" style="display:none">
        <div class="row3 field">
          <div>
            <label>Pace min/km</label>
            <input id="pace-min" type="number" min="0" value="4">
          </div>
          <div>
            <label>Pace sec/km</label>
            <input id="pace-sec" type="number" min="0" max="59" value="37">
          </div>
          <div>
            <label>Units</label>
            <select id="units">
              <option value="metric">km</option>
              <option value="imperial">mile</option>
            </select>
          </div>
        </div>
      </div>

      <div class="field">
        <label>Distance</label>
        <select id="distance">
          <option value="42.195" selected>Marathon 42.195 km</option>
          <option value="21.0975">Half 21.0975 km</option>
          <option value="10">10 km</option>
          <option value="5">5 km</option>
          <option value="custom">Custom</option>
        </select>
      </div>
      <div class="field" id="custom-distance-wrap" style="display:none">
        <label>Custom distance in km</label>
        <input id="custom-distance" type="number" min="1" step="0.001" value="42.195">
      </div>

      <h2 style="margin-top:12px">Conditions</h2>
      <div class="body" style="padding:10px 0 0 0">
        <div class="row field">
          <div>
            <label>Air temperature °C</label>
            <input id="tempC" type="number" step="0.1" value="18">
          </div>
          <div>
            <label>Relative humidity %</label>
            <input id="rh" type="number" step="1" min="0" max="100" value="65">
          </div>
        </div>
        <div class="row field">
          <div>
            <label>Sun exposure</label>
            <select id="sun">
              <option value="shade">Mostly shade</option>
              <option value="partial">Partial sun</option>
              <option value="full" selected>Full sun</option>
            </select>
          </div>
          <div>
            <label>Wind effect</label>
            <select id="wind">
              <option value="calm" selected>Calm</option>
              <option value="breezy">Breezy head/tail</option>
              <option value="windy">Windy</option>
            </select>
          </div>
        </div>
        <div class="row field">
          <div>
            <label>Heat sensitivity (adjustment per WBGT degree)</label>
            <input id="heatK" type="number" step="0.001" value="0.008">
            <div class="hint">Default 0.008 means 0.8% slower per WBGT degree above baseline. Adjust if you are colder or heat-adapted.</div>
          </div>
          <div>
            <label>Baseline WBGT</label>
            <input id="wbgtBaseline" type="number" step="0.1" value="10">
            <div class="hint">Typical comfort baseline for long races sits around 8–12.</div>
          </div>
        </div>

        <h2 style="margin-top:12px">Pacing plan</h2>
        <div class="body" style="padding:10px 0 0 0">
          <div class="row field">
            <div>
              <label>Split strategy</label>
              <select id="split">
                <option value="even" selected>Even</option>
                <option value="neg-small">Negative small</option>
                <option value="neg-med">Negative medium</option>
                <option value="pos-small">Positive small</option>
                <option value="pos-med">Positive medium</option>
              </select>
              <div class="hint">Negative means slightly slower start then faster finish.</div>
            </div>
            <div>
              <label>Late-race fade start km</label>
              <input id="fadeStart" type="number" value="30">
              <div class="hint">Set to 0 to disable fade modelling.</div>
            </div>
          </div>
          <div class="row field">
            <div>
              <label>Fade per km after start point (sec/km)</label>
              <input id="fadePerKm" type="number" step="0.1" value="1.5">
            </div>
            <div>
              <label>Output</label>
              <select id="outputMode">
                <option value="km" selected>Every km</option>
                <option value="5km">Every 5 km</option>
                <option value="mile">Every mile</option>
                <option value="km-mile">Km plus mile</option>
              </select>
            </div>
          </div>
        </div>
      </div>

      <div class="actions">
        <button id="calc" class="btn primary">Calculate splits</button>
        <button id="download" class="btn">Download CSV</button>
      </div>
      <div class="hint">This tool uses a simple WBGT-like model from temperature and humidity, with optional sun and wind modifiers. You can tune sensitivity.</div>
    </div>
  </div>

  <div>
    <div class="card">
      <h2>Overview</h2>
      <div class="body">
        <div class="metrics" id="metrics">
          <div class="metric">
            <div class="label">Adjusted average pace</div>
            <div class="value" id="avgPace">–</div>
          </div>
          <div class="metric">
            <div class="label">Predicted finish time</div>
            <div class="value" id="finish">–</div>
          </div>
          <div class="metric">
            <div class="label">WBGT estimate</div>
            <div class="value" id="wbgt">–</div>
          </div>
          <div class="metric">
            <div class="label">Heat multiplier</div>
            <div class="value" id="heatMult">–</div>
          </div>
        </div>
      </div>
    </div>

    <div class="card" style="margin-top:12px">
      <h2>Splits</h2>
      <div class="body">
        <div id="splits"></div>
      </div>
    </div>
  </div>
</div>

<div class="footer">
  Built for real-world racing. Temperature-based adjustments are estimates, not gospel. Test your plan in training and adjust.
</div>

<script>
function clamp(v,min,max){return Math.max(min,Math.min(max,v));}
function fmtTime(sec){
  const s=Math.round(sec);
  const h=Math.floor(s/3600);
  const m=Math.floor((s%3600)/60);
  const ss=s%60;
  if (h>0) return h+":"+(m<10?"0":"")+m+":"+(ss<10?"0":"")+ss;
  return m+":"+(ss<10?"0":"")+ss;
}
function fmtPace(secPerKm){
  const m=Math.floor(secPerKm/60);
  const s=Math.round(secPerKm%60);
  return m+":"+(s<10?"0":"")+s+" /km";
}
function wbgtApprox(tempC, rh, sun){
  // Vapour pressure in hPa using Magnus formula
  const e = (rh/100.0) * 6.105 * Math.exp((17.27*tempC)/(237.7+tempC));
  let wbgt = 0.567*tempC + 0.393*e + 3.94; // shade approx
  // Sunlight modifier: add equivalent degrees
  if(sun==="partial") wbgt += 1.0;
  if(sun==="full") wbgt += 2.0;
  return wbgt;
}
function windModifier(wind){
  if(wind==="calm") return 0.0;
  if(wind==="breezy") return 0.002; // 0.2% extra cost
  if(wind==="windy") return 0.006;  // 0.6% extra cost
  return 0.0;
}
function splitProfile(mode, nKm){
  // Returns array length nKm with relative multipliers that sum to nKm
  const arr = new Array(nKm).fill(1);
  if(mode==="even") return arr;
  // design a gentle trend over the distance
  const strength = {
    "neg-small": -0.04,  // 4% from start to finish
    "neg-med":   -0.08,
    "pos-small":  0.04,
    "pos-med":    0.08
  }[mode];
  if(strength===undefined) return arr;
  for(let i=0;i<nKm;i++){
    const t = i/(nKm-1); // 0 to 1
    arr[i] = 1 + strength*(t-0.5)*2; // linear trend
  }
  // normalise to average 1
  const avg = arr.reduce((a,b)=>a+b,0)/nKm;
  return arr.map(x=>x/avg);
}
function buildSplits(baseSecPerKm, distKm, fadeStartKm, fadeSecPerKm, mode){
  const nKm = Math.floor(distKm+1e-9);
  const rem = distKm - nKm;
  const prof = splitProfile(mode, nKm + (rem>0?1:0));
  const splits = [];
  for(let i=1;i<=nKm;i++){
    let pace = baseSecPerKm*prof[i-1];
    if(fadeStartKm>0 && i>fadeStartKm){
      const extra = (i - fadeStartKm) * fadeSecPerKm;
      pace += extra;
    }
    splits.push({km:i, pace});
  }
  if(rem>0){
    let pace = baseSecPerKm*prof[nKm];
    if(fadeStartKm>0 && (nKm+1)>fadeStartKm){
      const extra = (nKm + 1 - fadeStartKm) * fadeSecPerKm;
      pace += extra;
    }
    splits.push({km:distKm, pace}); // final partial
  }
  // compute cumulative times
  let cum=0;
  for(let i=0;i<splits.length;i++){
    const segmentKm = i===splits.length-1 ? (distKm - (splits.length-2)) : 1;
    const time = splits[i].pace * segmentKm;
    cum += time;
    splits[i].cum = cum;
  }
  return splits;
}
function toMiles(km){return km*0.621371192;}
function generateTable(splits, mode){
  const container = document.getElementById("splits");
  container.innerHTML = "";
  const table = document.createElement("table");
  const thead = document.createElement("thead");
  const tbody = document.createElement("tbody");
  let header;
  if(mode==="km"){
    header = "<tr><th>Km</th><th>Pace</th><th>Elapsed</th></tr>";
  } else if(mode==="5km"){
    header = "<tr><th>Block</th><th>Avg pace</th><th>Elapsed</th></tr>";
  } else if(mode==="mile"){
    header = "<tr><th>Mile</th><th>Pace</th><th>Elapsed</th></tr>";
  } else {
    header = "<tr><th>Km</th><th>Pace</th><th>Elapsed</th><th>Mile</th><th>Pace</th><th>Elapsed</th></tr>";
  }
  thead.innerHTML = header;
  table.appendChild(thead);

  function addRow(cells){
    const tr = document.createElement("tr");
    cells.forEach((c,i)=>{
      const td = document.createElement("td");
      td.textContent = c;
      tr.appendChild(td);
    });
    tbody.appendChild(tr);
  }

  if(mode==="km"){
    splits.forEach(sp=> addRow([sp.km.toFixed(3).replace(/\.000$/,""), fmtPace(sp.pace), fmtTime(sp.cum)]));
  } else if(mode==="5km"){
    let cum=0, blk=1, accTime=0, accKm=0;
    for(let i=0;i<splits.length;i++){
      const prevKm = i===0?0:splits[i-1].km;
      const segKm = splits[i].km - prevKm;
      accTime += splits[i].pace * segKm;
      accKm += segKm;
      cum = splits[i].cum;
      if(accKm>=5-1e-9 || i===splits.length-1){
        const avgPace = accTime/accKm;
        addRow([`${(blk-1)*5}–${(blk*5>splits[splits.length-1].km? splits[splits.length-1].km.toFixed(3): blk*5)} km`, fmtPace(avgPace), fmtTime(cum)]);
        blk += 1; accTime=0; accKm=0;
      }
    }
  } else if(mode==="mile"){
    // Resample to miles
    const totalMiles = toMiles(splits[splits.length-1].km);
    const nMiles = Math.floor(totalMiles + 1e-9);
    for(let m=1;m<=nMiles;m++){
      // find time at mile m by interpolation of cumulative vs km
      const targetKm = m/0.621371192;
      // locate surrounding points
      let before = {km:0,cum:0,pace:splits[0].pace};
      for(let i=0;i<splits.length;i++){
        if(splits[i].km>=targetKm){
          const prevKm = i===0?0:splits[i-1].km;
          const prevCum = i===0?0:splits[i-1].cum;
          const segmentKm = splits[i].km - prevKm;
          const segmentTime = splits[i].cum - prevCum;
          const t = (targetKm - prevKm)/segmentKm;
          const cumAt = prevCum + segmentTime*t;
          const cumBefore = before.cum;
          addRow([m, fmtPace(segmentTime/segmentKm), fmtTime(cumAt)]);
          before = {km:targetKm,cum:cumAt};
          break;
        }
      }
    }
  } else {
    // both km and mile
    // Build both tables as arrays then print row by row up to max length
    const rowsKm = splits.map(sp=>[sp.km.toFixed(3).replace(/\.000$/,""), fmtPace(sp.pace), fmtTime(sp.cum)]);
    // miles
    const totalMiles = toMiles(splits[splits.length-1].km);
    const nMiles = Math.floor(totalMiles + 1e-9);
    const rowsMi = [];
    for(let m=1;m<=nMiles;m++){
      const targetKm = m/0.621371192;
      let before = {km:0,cum:0,pace:splits[0].pace};
      for(let i=0;i<splits.length;i++){
        if(splits[i].km>=targetKm){
          const prevKm = i===0?0:splits[i-1].km;
          const prevCum = i===0?0:splits[i-1].cum;
          const segmentKm = splits[i].km - prevKm;
          const segmentTime = splits[i].cum - prevCum;
          const t = (targetKm - prevKm)/segmentKm;
          const cumAt = prevCum + segmentTime*t;
          rowsMi.push([m, fmtPace(segmentTime/segmentKm), fmtTime(cumAt)]);
          break;
        }
      }
    }
    const maxLen = Math.max(rowsKm.length, rowsMi.length);
    for(let i=0;i<maxLen;i++){
      const kmRow = rowsKm[i] || ["", "", ""];
      const miRow = rowsMi[i] || ["", "", ""];
      addRow([kmRow[0], kmRow[1], kmRow[2], miRow[0], miRow[1], miRow[2]]);
    }
  }

  table.appendChild(tbody);
  container.appendChild(table);
}

function run(){
  // Mode
  const isTime = document.getElementById("mode-time").classList.contains("active");
  const units = document.getElementById("units").value;

  // Distance
  let distSel = document.getElementById("distance").value;
  let distKm = 42.195;
  if(distSel==="custom") distKm = parseFloat(document.getElementById("custom-distance").value);
  else distKm = parseFloat(distSel);

  // Base pace from inputs
  let baseSecPerKm;
  if(isTime){
    const h = parseInt(document.getElementById("goal-h").value||0);
    const m = parseInt(document.getElementById("goal-m").value||0);
    const s = parseInt(document.getElementById("goal-s").value||0);
    const total = h*3600 + m*60 + s;
    baseSecPerKm = total / distKm;
  } else {
    const pm = parseInt(document.getElementById("pace-min").value||0);
    const ps = parseInt(document.getElementById("pace-sec").value||0);
    let secPer = pm*60 + ps;
    if(units==="imperial"){
      // convert per mile to per km
      secPer = secPer / 1.609344;
    }
    baseSecPerKm = secPer;
  }

  // Heat model
  const tempC = parseFloat(document.getElementById("tempC").value);
  const rh = clamp(parseFloat(document.getElementById("rh").value),0,100);
  const sun = document.getElementById("sun").value;
  const wind = document.getElementById("wind").value;
  const k = parseFloat(document.getElementById("heatK").value);
  const wbgtBase = parseFloat(document.getElementById("wbgtBaseline").value);

  const wbgt = wbgtApprox(tempC, rh, sun);
  const windK = windModifier(wind);
  const delta = Math.max(0, wbgt - wbgtBase);
  let heatMult = 1 + k*delta + windK;
  heatMult = Math.min(1.30, heatMult); // cap at +30% slowdown

  const adjustedSecPerKm = baseSecPerKm * heatMult;

  // Pacing plan
  const split = document.getElementById("split").value;
  const fadeStart = parseFloat(document.getElementById("fadeStart").value);
  const fadePerKm = parseFloat(document.getElementById("fadePerKm").value);

  const splits = buildSplits(adjustedSecPerKm, distKm, fadeStart, fadePerKm, split);

  // Overview
  const avgPace = splits.reduce((a,b)=>a+b.pace,0)/splits.length;
  const finish = splits[splits.length-1].cum;

  document.getElementById("avgPace").textContent = fmtPace(avgPace);
  document.getElementById("finish").textContent = fmtTime(finish);
  document.getElementById("wbgt").textContent = wbgt.toFixed(1);
  document.getElementById("heatMult").textContent = (heatMult*100).toFixed(1)+"%";
  generateTable(splits, document.getElementById("outputMode").value);

  // CSV content
  window._csvRows = [["Km","Pace_sec_per_km","Pace_str","Elapsed_sec","Elapsed_str","WBGT","Heat_multiplier"]];
  splits.forEach(sp=>{
    window._csvRows.push([sp.km, (sp.pace).toFixed(2), fmtPace(sp.pace), sp.cum.toFixed(2), fmtTime(sp.cum), wbgt.toFixed(1), heatMult.toFixed(4)]);
  });
}

function downloadCSV(){
  if(!window._csvRows){alert("Calculate first");return;}
  const lines = window._csvRows.map(r=>r.join(","));
  const blob = new Blob([lines.join("\n")], {type:"text/csv"});
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "marathon_splits.csv";
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

document.getElementById("mode-time").addEventListener("click",()=>{
  document.getElementById("mode-time").classList.add("active");
  document.getElementById("mode-pace").classList.remove("active");
  document.getElementById("time-inputs").style.display="";
  document.getElementById("pace-inputs").style.display="none";
});
document.getElementById("mode-pace").addEventListener("click",()=>{
  document.getElementById("mode-pace").classList.add("active");
  document.getElementById("mode-time").classList.remove("active");
  document.getElementById("time-inputs").style.display="none";
  document.getElementById("pace-inputs").style.display="";
});

document.getElementById("distance").addEventListener("change",(e)=>{
  const show = e.target.value==="custom";
  document.getElementById("custom-distance-wrap").style.display = show ? "" : "none";
});

document.getElementById("calc").addEventListener("click", run);
document.getElementById("download").addEventListener("click", downloadCSV);
// Auto-calc on load
run();
</script>
</body>
</html>

Welcome to The Marathon Clinic

At The Marathon Clinic, we believe running should complement your life, not complicate it. Our coaching is designed to fit seamlessly into your lifestyle, helping you achieve consistency, enjoyment, and meaningful results. Whether you're a seasoned marathoner or just starting out, we’re here to support you on your journey.

Our Approach

We don’t just give you a plan—we help you balance running with everything else in life. Our focus is on creating sustainable training routines and addressing the little things that make a big difference, such as:

  • Tailoring training to your life for long-term consistency.

  • Developing lasting habits around rest, recovery, and performance.

  • Helping you improve holistically—beyond just the miles.

Who We Work With

Our coaching is ideal for runners who want a smart approach that fits their life, including:

  • Busy professionals balancing running with work and family.

  • Marathoners aiming for a personal best.

  • New runners looking for structure and support.

Our Head Coach

Jason Hunt is an elite marathon runner and former professional long-distance triathlete from New Zealand, now residing in Sydney, Australia.

He discovered his passion for endurance sports during his university years, transitioning from an amateur enthusiast to an elite athlete.

Over more than a decade, Jason has immersed himself in the world of endurance sports, gaining a comprehensive understanding of the journey from novice to elite.

As an Australian Athletics Level 2 Advanced Running Coach, he is dedicated to guiding runners of all levels to achieve their personal bests.

Ready to Run Smarter?

If you want a personalised, balanced approach to running, we’d love to help.

Get in touch today and let’s start building a plan that works for you.

Fill out the form or get in touch directly:

Phone: +61 432 388 910

Email: jason@themarathonclinic.com