<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>Vinny</title><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="Vinny"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#111a1a"><link rel="manifest" id="manifest-link" href="#"><link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 180'%3E%3Crect width='180' height='180' rx='40' fill='%231a7a7a'/%3E%3Ctext x='90' y='116' font-family='Arial' font-weight='bold' font-size='72' text-anchor='middle' fill='white'%3EUT%3C/text%3E%3C/svg%3E"><script>window.addEventListener('DOMContentLoaded', function() {  var manifestData = {    name: "Vinny \u2014 Ultra Training",    short_name: "Vinny",    start_url: "./",    display: "standalone",    background_color: "#111a1a",    theme_color: "#111a1a",    icons: [{ src: "data:image/png;base64,iVBORw0KGgo=", sizes: "192x192", type: "image/png" }]  };  var blob = new Blob([JSON.stringify(manifestData)], {type: 'application/json'});  document.getElementById('manifest-link').href = URL.createObjectURL(blob);});</script><link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;600;700;800&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet"><style>  :root {    --teal: #1a7a7a;    --teal-light: #2a9d9d;    --teal-dark: #0f5252;    --teal-faint: #e8f5f5;    --bone: #f5f0e8;    --dark: #111a1a;    --mid: #3d5252;    --gray: #8a9e9e;    --gray-light: #c5d5d5;    --white: #ffffff;    --accent: #e8622a;    --accent-light: #fdf0ea;    --green: #2a7a4a;    --green-light: #e8f5ee;    --shadow: 0 4px 24px rgba(26,122,122,0.12);    --shadow-lg: 0 8px 40px rgba(26,122,122,0.18);  }  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }  body {    font-family: 'Syne', sans-serif;    background: var(--dark);    color: var(--dark);    min-height: 100vh;    overflow-x: hidden;  }  /* ── HEADER ── */  .header {    background: var(--dark);    padding: 20px 20px 0;    position: sticky;    top: 0;    z-index: 100;  }  .header-top {    display: flex;    align-items: center;    justify-content: space-between;    margin-bottom: 16px;  }  .logo {    font-size: 11px;    font-weight: 700;    letter-spacing: 0.2em;    text-transform: uppercase;    color: var(--teal-light);    font-family: 'DM Mono', monospace;  }  .header-date {    font-size: 11px;    color: var(--gray);    font-family: 'DM Mono', monospace;    letter-spacing: 0.05em;  }  .header-races {    display: flex;    gap: 8px;    overflow-x: auto;    padding-bottom: 16px;    -webkit-overflow-scrolling: touch;    scrollbar-width: none;  }  .header-races::-webkit-scrollbar { display: none; }  .race-chip {    flex-shrink: 0;    background: rgba(255,255,255,0.05);    border: 1px solid rgba(255,255,255,0.08);    border-radius: 8px;    padding: 10px 14px;    cursor: pointer;    transition: all 0.2s;  }  .race-chip.a-race {    background: rgba(26,122,122,0.25);    border-color: var(--teal);  }  .race-chip-name {    font-size: 11px;    font-weight: 700;    color: var(--white);    letter-spacing: 0.05em;    display: block;  }  .race-chip-date {    font-size: 10px;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin-top: 2px;    display: block;  }  .race-chip-days {    font-size: 13px;    font-weight: 800;    color: var(--teal-light);    margin-top: 4px;    display: block;  }  .race-chip.a-race .race-chip-days { color: #7fd4d4; }  /* ── NAV TABS ── */  .tabs {    background: var(--dark);    display: flex;    border-bottom: 1px solid rgba(255,255,255,0.08);    position: sticky;    top: 0;    z-index: 99;  }  .tab {    flex: 1;    padding: 14px 8px;    text-align: center;    font-size: 11px;    font-weight: 700;    letter-spacing: 0.1em;    text-transform: uppercase;    color: var(--gray);    cursor: pointer;    border-bottom: 2px solid transparent;    transition: all 0.2s;  }  .tab.active {    color: var(--teal-light);    border-bottom-color: var(--teal-light);  }  /* ── CONTENT ── */  .content {    background: var(--bone);    min-height: calc(100vh - 160px);    border-radius: 20px 20px 0 0;  }  .section { display: none; padding: 24px 20px; }  .section.active { display: block; }  /* ── BLOCK VIEW ── */  .block-header {    display: flex;    align-items: flex-start;    justify-content: space-between;    margin-bottom: 20px;  }  .block-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.2em;    text-transform: uppercase;    color: var(--teal);    font-family: 'DM Mono', monospace;  }  .block-title {    font-size: 26px;    font-weight: 800;    color: var(--dark);    line-height: 1.1;    margin-top: 4px;  }  .block-subtitle {    font-size: 13px;    color: var(--mid);    margin-top: 4px;    font-family: 'DM Mono', monospace;  }  .block-nav {    display: flex;    gap: 6px;  }  .block-btn {    width: 36px;    height: 36px;    border-radius: 50%;    border: 1.5px solid var(--gray-light);    background: white;    color: var(--dark);    font-size: 16px;    cursor: pointer;    display: flex;    align-items: center;    justify-content: center;    transition: all 0.2s;  }  .block-btn:active { background: var(--teal); color: white; border-color: var(--teal); }  /* ── DAYS GRID ── */  .week-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.15em;    text-transform: uppercase;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin: 16px 0 8px;  }  .days-grid {    display: grid;    grid-template-columns: repeat(7, 1fr);    gap: 4px;    margin-bottom: 4px;  }  .day-col-header {    text-align: center;    font-size: 9px;    font-weight: 700;    letter-spacing: 0.1em;    color: var(--gray);    font-family: 'DM Mono', monospace;    padding-bottom: 4px;  }  .day-card {    background: white;    border-radius: 10px;    padding: 8px 4px;    text-align: center;    cursor: pointer;    border: 1.5px solid transparent;    transition: all 0.15s;    position: relative;    min-height: 72px;    display: flex;    flex-direction: column;    align-items: center;    justify-content: flex-start;    gap: 2px;  }  .day-card:active { transform: scale(0.96); }  .day-card.logged { background: var(--green-light); border-color: var(--green); }  .day-card.today { border-color: var(--teal); background: var(--teal-faint); }  .day-card.rest { background: #f8f4ee; }  .day-card.active-day { border-color: var(--accent); background: var(--accent-light); }  .day-num {    font-size: 11px;    font-weight: 700;    color: var(--dark);    font-family: 'DM Mono', monospace;  }  .day-type {    font-size: 8px;    color: var(--mid);    line-height: 1.2;    font-weight: 600;    letter-spacing: 0.02em;    padding: 0 2px;  }  .day-check {    font-size: 14px;    margin-top: 2px;  }  .day-rpe {    font-size: 8px;    font-family: 'DM Mono', monospace;    color: var(--teal);    font-weight: 700;    background: var(--teal-faint);    padding: 1px 4px;    border-radius: 4px;  }  /* ── PHASE BANNER ── */  .phase-banner {    background: var(--dark);    border-radius: 12px;    padding: 14px 16px;    margin-bottom: 20px;    display: flex;    align-items: center;    gap: 12px;  }  .phase-dot {    width: 10px;    height: 10px;    border-radius: 50%;    background: var(--teal-light);    flex-shrink: 0;  }  .phase-text {    font-size: 12px;    color: var(--gray-light);    line-height: 1.4;  }  .phase-name {    font-weight: 700;    color: var(--white);    display: block;  }  /* ── LOG MODAL ── */  .modal-overlay {    display: none;    position: fixed;    inset: 0;    background: rgba(10,20,20,0.85);    z-index: 200;    align-items: flex-end;    justify-content: center;  }  .modal-overlay.open { display: flex; }  .modal {    background: var(--white);    border-radius: 24px 24px 0 0;    padding: 28px 24px 40px;    width: 100%;    max-width: 500px;    max-height: 88vh;    overflow-y: auto;    animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);  }  @keyframes slideUp {    from { transform: translateY(100%); }    to { transform: translateY(0); }  }  .modal-handle {    width: 40px;    height: 4px;    background: var(--gray-light);    border-radius: 2px;    margin: 0 auto 20px;  }  .modal-title {    font-size: 22px;    font-weight: 800;    color: var(--dark);    margin-bottom: 4px;  }  .modal-subtitle {    font-size: 13px;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin-bottom: 24px;  }  .form-group {    margin-bottom: 18px;  }  .form-label {    font-size: 11px;    font-weight: 700;    letter-spacing: 0.1em;    text-transform: uppercase;    color: var(--teal);    display: block;    margin-bottom: 8px;    font-family: 'DM Mono', monospace;  }  .form-row {    display: grid;    grid-template-columns: 1fr 1fr;    gap: 12px;  }  input[type="text"], input[type="number"], textarea, select {    width: 100%;    padding: 12px 14px;    border: 1.5px solid var(--gray-light);    border-radius: 10px;    font-family: 'Syne', sans-serif;    font-size: 15px;    color: var(--dark);    background: var(--bone);    outline: none;    transition: border-color 0.2s;    -webkit-appearance: none;  }  input:focus, textarea:focus, select:focus {    border-color: var(--teal);    background: white;  }  textarea { resize: none; height: 80px; line-height: 1.5; }  /* RPE slider */  .rpe-slider {    -webkit-appearance: none;    appearance: none;    width: 100%;    height: 6px;    border-radius: 3px;    background: var(--gray-light);    outline: none;    border: none;    padding: 0;  }  .rpe-slider::-webkit-slider-thumb {    -webkit-appearance: none;    width: 24px;    height: 24px;    border-radius: 50%;    background: var(--teal);    cursor: pointer;    box-shadow: 0 2px 8px rgba(26,122,122,0.4);  }  .rpe-labels {    display: flex;    justify-content: space-between;    margin-top: 6px;    font-size: 10px;    color: var(--gray);    font-family: 'DM Mono', monospace;  }  .rpe-value {    text-align: center;    font-size: 32px;    font-weight: 800;    color: var(--teal);    font-family: 'DM Mono', monospace;    margin-top: 4px;  }  /* Knee toggle */  .knee-options {    display: flex;    gap: 8px;  }  .knee-opt {    flex: 1;    padding: 10px 8px;    border: 1.5px solid var(--gray-light);    border-radius: 10px;    text-align: center;    cursor: pointer;    font-size: 12px;    font-weight: 700;    color: var(--mid);    transition: all 0.15s;  }  .knee-opt.selected { border-color: var(--teal); background: var(--teal-faint); color: var(--teal); }  .knee-opt.warn.selected { border-color: var(--accent); background: var(--accent-light); color: var(--accent); }  .btn-primary {    width: 100%;    padding: 16px;    background: var(--teal);    color: white;    border: none;    border-radius: 12px;    font-family: 'Syne', sans-serif;    font-size: 15px;    font-weight: 700;    cursor: pointer;    margin-top: 8px;    letter-spacing: 0.05em;    transition: all 0.2s;  }  .btn-primary:active { background: var(--teal-dark); transform: scale(0.98); }  .btn-ghost {    width: 100%;    padding: 14px;    background: transparent;    color: var(--gray);    border: 1.5px solid var(--gray-light);    border-radius: 12px;    font-family: 'Syne', sans-serif;    font-size: 14px;    font-weight: 600;    cursor: pointer;    margin-top: 8px;  }  /* ── LOG LIST ── */  .log-entry {    background: white;    border-radius: 14px;    padding: 16px;    margin-bottom: 12px;    border-left: 3px solid var(--teal);  }  .log-entry.flagged { border-left-color: var(--accent); }  .log-entry-header {    display: flex;    justify-content: space-between;    align-items: flex-start;    margin-bottom: 8px;  }  .log-entry-date {    font-size: 11px;    font-family: 'DM Mono', monospace;    color: var(--gray);  }  .log-entry-title {    font-size: 16px;    font-weight: 700;    color: var(--dark);  }  .log-entry-stats {    display: flex;    gap: 12px;    flex-wrap: wrap;    margin-top: 8px;  }  .stat-chip {    font-size: 11px;    font-family: 'DM Mono', monospace;    color: var(--teal);    background: var(--teal-faint);    padding: 3px 8px;    border-radius: 6px;    font-weight: 700;  }  .log-note {    font-size: 13px;    color: var(--mid);    margin-top: 8px;    line-height: 1.5;    font-family: 'Instrument Serif', serif;    font-style: italic;  }  /* ── CHECKIN SECTION ── */  .checkin-card {    background: white;    border-radius: 14px;    padding: 18px;    margin-bottom: 12px;    display: flex;    align-items: center;    gap: 14px;  }  .checkin-dot {    width: 12px;    height: 12px;    border-radius: 50%;    background: var(--gray-light);    flex-shrink: 0;  }  .checkin-dot.next { background: var(--teal); box-shadow: 0 0 0 4px var(--teal-faint); }  .checkin-dot.done { background: var(--green); }  .checkin-info { flex: 1; }  .checkin-title { font-size: 15px; font-weight: 700; color: var(--dark); }  .checkin-date { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--gray); margin-top: 2px; }  .checkin-status { font-size: 11px; font-weight: 700; color: var(--teal); }  /* ── SUMMARY STATS ── */  .stats-grid {    display: grid;    grid-template-columns: 1fr 1fr;    gap: 12px;    margin-bottom: 20px;  }  .stat-card {    background: white;    border-radius: 14px;    padding: 16px;    text-align: center;  }  .stat-card-value {    font-size: 32px;    font-weight: 800;    color: var(--teal);    font-family: 'DM Mono', monospace;    display: block;  }  .stat-card-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.1em;    text-transform: uppercase;    color: var(--gray);    display: block;    margin-top: 4px;  }  /* ── WEATHER WIDGET ── */  .weather-card {    background: var(--dark);    border-radius: 14px;    padding: 18px;    margin-bottom: 20px;    color: white;  }  .weather-label {    font-size: 10px;    letter-spacing: 0.2em;    text-transform: uppercase;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin-bottom: 8px;  }  .weather-location {    font-size: 14px;    font-weight: 700;    color: white;  }  .weather-note {    font-size: 12px;    color: var(--gray);    margin-top: 6px;    line-height: 1.5;  }  /* Toast */  .toast {    position: fixed;    bottom: 30px;    left: 50%;    transform: translateX(-50%) translateY(80px);    background: var(--dark);    color: white;    padding: 12px 24px;    border-radius: 100px;    font-size: 13px;    font-weight: 700;    z-index: 300;    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);    white-space: nowrap;  }  .toast.show { transform: translateX(-50%) translateY(0); }  .suunto-link {    width: 100%;    padding: 12px 14px;    border: 1.5px dashed var(--teal-light);    border-radius: 10px;    background: var(--teal-faint);    font-family: 'DM Mono', monospace;    font-size: 13px;    color: var(--teal);    outline: none;  }  .gpx-upload-btn {    display: block;    width: 100%;    padding: 12px 14px;    background: var(--teal-faint);    border: 1.5px dashed var(--teal-light);    border-radius: 10px;    font-family: 'Syne', sans-serif;    font-size: 13px;    font-weight: 600;    color: var(--teal);    cursor: pointer;    text-align: center;    transition: all 0.2s;  }  .gpx-upload-btn:active { border-style: solid; }  .gpx-status {    font-size: 12px;    font-family: 'DM Mono', monospace;    color: var(--green);    margin-top: 6px;    min-height: 16px;    font-weight: 600;  }  .gpx-status.error { color: var(--accent); }  .add-session-btn {    width: 100%;    padding: 10px;    background: transparent;    border: 1.5px dashed var(--gray-light);    border-radius: 10px;    font-family: 'Syne', sans-serif;    font-size: 13px;    font-weight: 600;    color: var(--gray);    cursor: pointer;    text-align: center;    transition: all 0.2s;    margin-bottom: 4px;  }  .add-session-btn:active { border-color: var(--teal); color: var(--teal); }  .session-divider {    display: flex;    align-items: center;    gap: 10px;    margin: 16px 0 12px;  }  .session-divider-line { flex: 1; height: 1px; background: var(--gray-light); }  .session-divider-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.15em;    text-transform: uppercase;    color: var(--teal);    font-family: 'DM Mono', monospace;  }  .session2-block { display: none; }  .session2-block.open { display: block; }  .empty-state {    text-align: center;    padding: 40px 20px;    color: var(--gray);  }  .empty-state-icon { font-size: 48px; margin-bottom: 12px; }  .empty-state-text { font-size: 15px; font-weight: 600; color: var(--mid); }  .empty-state-sub { font-size: 13px; color: var(--gray); margin-top: 4px; }</style></head><body><div class="header">  <div class="header-top">    <div class="logo">Ultra Training 2026</div>    <div class="header-date" id="todayDate"></div>  </div>  <div class="header-races" id="raceChips"></div></div><div class="tabs">  <div class="tab active" onclick="switchTab('block')">Block</div>  <div class="tab" onclick="switchTab('log')">My Runs</div>  <div class="tab" onclick="switchTab('schedule')">Schedule</div>  <div class="tab" onclick="switchTab('checkin')">Check-Ins</div></div><div class="content">  <!-- BLOCK VIEW -->  <div class="section active" id="section-block">    <div class="block-header">      <div>        <div class="block-label" id="blockLabel">Block 1</div>        <div class="block-title" id="blockTitle">Baseline</div>        <div class="block-subtitle" id="blockDates">Feb 21 – Mar 6</div>      </div>      <div class="block-nav">        <button class="block-btn" onclick="prevBlock()">‹</button>        <button class="block-btn" onclick="nextBlock()">›</button>      </div>    </div>    <div class="phase-banner" id="phaseBanner">      <div class="phase-dot"></div>      <div class="phase-text">        <span class="phase-name" id="phaseName">Phase 1: Base & Build</span>        <span id="phaseDesc">Aerobic foundation, trail volume, strength</span>      </div>    </div>    <div class="week-label">Week 1</div>    <div class="days-grid" id="dayHeaders1"></div>    <div class="days-grid" id="week1Grid"></div>    <div class="week-label">Week 2</div>    <div class="days-grid" id="dayHeaders2"></div>    <div class="days-grid" id="week2Grid"></div>  </div>  <!-- LOG VIEW -->  <div class="section" id="section-log">    <div class="stats-grid" id="statsGrid"></div>    <div id="logList"></div>  </div>  <!-- SCHEDULE VIEW -->  <div class="section" id="section-schedule">    <div class="weather-card">      <div class="weather-label">📍 Dryden, NY — 13068</div>      <div class="weather-location">Weather checked at every 2-week check-in</div>      <div class="weather-note">Snow forecast = run day moved. No heroics in heavy snow. Pull up weather at each check-in to adjust the upcoming block.</div>    </div>    <div id="fullSchedule"></div>  </div>  <!-- CHECKIN VIEW -->  <div class="section" id="section-checkin">    <div id="checkinList"></div>  </div></div><!-- LOG MODAL --><div class="modal-overlay" id="logModal">  <div class="modal">    <div class="modal-handle"></div>    <div class="modal-title" id="modalTitle">Log Run</div>    <div class="modal-subtitle" id="modalSubtitle"></div>    <div class="form-group">      <label class="form-label">Upload GPX / FIT File</label>      <label class="gpx-upload-btn" id="gpxUploadBtn" for="gpxFile">        <span id="gpxBtnText">📁 Upload Suunto export to auto-fill</span>        <input type="file" id="gpxFile" accept=".gpx,.fit" style="display:none" onchange="parseActivityFile(this, 1)">      </label>      <div class="gpx-status" id="gpxStatus"></div>    </div>    <div class="form-group">      <label class="form-label">Distance & Time</label>      <div class="form-row">        <input type="number" id="inputDist" placeholder="Miles" step="0.1">        <input type="text" id="inputTime" placeholder="H:MM e.g. 2:30">      </div>    </div>    <div class="form-group">      <label class="form-label">Effort (RPE)</label>      <input type="range" class="rpe-slider" id="rpeSlider" min="1" max="10" value="5" oninput="updateRPE(this.value)">      <div class="rpe-labels"><span>Easy</span><span>Moderate</span><span>Max</span></div>      <div class="rpe-value" id="rpeVal">5</div>    </div>    <div class="form-group">      <label class="form-label">Knee Feel</label>      <div class="knee-options">        <div class="knee-opt selected" data-val="great" onclick="selectKnee(this)">👍 Great</div>        <div class="knee-opt" data-val="fine" onclick="selectKnee(this)">👌 Fine</div>        <div class="knee-opt warn" data-val="grumpy" onclick="selectKnee(this)">⚠️ Grumpy</div>      </div>    </div>    <div class="form-group">      <label class="form-label">Notes — how did it feel?</label>      <textarea id="inputNote" placeholder="Legs felt good on the climb, HR stayed controlled, ate well at mile 10..."></textarea>    </div>    <button class="add-session-btn" id="addSession2Btn" onclick="toggleSession2()">+ Add 2nd session</button>    <div class="session2-block" id="session2Block">      <div class="session-divider">        <div class="session-divider-line"></div>        <div class="session-divider-label">Session 2</div>        <div class="session-divider-line"></div>      </div>      <div class="form-group">        <label class="form-label">Upload GPX / FIT File</label>        <label class="gpx-upload-btn" for="gpxFile2">          <span id="gpxBtnText2">📁 Upload Suunto export to auto-fill</span>          <input type="file" id="gpxFile2" accept=".gpx,.fit" style="display:none" onchange="parseActivityFile(this, 2)">        </label>        <div class="gpx-status" id="gpxStatus2"></div>      </div>      <div class="form-group">        <label class="form-label">Distance & Time</label>        <div class="form-row">          <input type="number" id="inputDist2" placeholder="Miles" step="0.1">          <input type="text" id="inputTime2" placeholder="H:MM e.g. 0:45">        </div>      </div>      <div class="form-group">        <label class="form-label">Effort (RPE)</label>        <input type="range" class="rpe-slider" id="rpeSlider2" min="1" max="10" value="5" oninput="updateRPE2(this.value)">        <div class="rpe-labels"><span>Easy</span><span>Moderate</span><span>Max</span></div>        <div class="rpe-value" id="rpeVal2">5</div>      </div>      <div class="form-group">        <label class="form-label">Notes</label>        <textarea id="inputNote2" placeholder="Second session notes..."></textarea>      </div>    </div>    <button class="btn-primary" onclick="saveLog()">Save ✓</button>    <button class="btn-ghost" onclick="closeModal()">Cancel</button>  </div></div><div class="toast" id="toast"></div><script>// ── DATA ──────────────────────────────────────────────────────const RACES = [  { name: "Laurel 70", date: "2026-06-13", goal: "15-16 hrs", aRace: false },  { name: "Escarpment", date: "2026-07-26", goal: "Enjoy it", aRace: false },  { name: "Dark Sky 220", date: "2026-10-25", goal: "A-RACE", aRace: true },];const CHECKINS = [  { label: "Block 1 Review", date: "2026-03-07" },  { label: "Block 2 Review", date: "2026-03-21" },  { label: "Block 3 Review", date: "2026-04-04" },  { label: "Block 4 Review", date: "2026-04-18" },  { label: "Block 5 Review", date: "2026-05-02" },  { label: "Block 6 Review", date: "2026-05-16" },  { label: "Block 7 / Laurel Taper", date: "2026-05-30" },  { label: "Post-Laurel Recovery", date: "2026-06-27" },  { label: "Block 9 Review", date: "2026-07-11" },  { label: "Escarpment Week", date: "2026-07-26" },  { label: "Block 11 Review", date: "2026-08-09" },  { label: "Block 12 Review", date: "2026-08-23" },  { label: "Peak Build I", date: "2026-09-06" },  { label: "Peak Build II", date: "2026-09-20" },  { label: "Cut-back Review", date: "2026-10-04" },  { label: "Dark Sky Race Prep 🏁", date: "2026-10-18" },];const PHASES = [  { name: "Phase 1: Base & Build", desc: "Aerobic foundation, trail volume, strength", color: "#1a7a7a" },  { name: "Phase 2: Laurel Prep", desc: "Race-specific training & taper", color: "#2a7a4a" },  { name: "Phase 3: Recovery & Sharpen", desc: "Rebuild after Laurel, Escarpment prep", color: "#7a4a1a" },  { name: "Phase 4: Dark Sky 220 Build", desc: "Peak volume, night running, back-to-backs", color: "#1a1a7a" },];const BLOCKS = [  // Phase 1  { num: 1, label: "Baseline", dates: "Feb 21 – Mar 6", mileage: "45-50", phase: 0,    week1: ["Easy 6-8mi", "Strength A", "Mod 8-10mi + strides", "Easy 5mi + Yoga", "Dog Walk", "Long 14-16mi", "Dog Walk + Mobility"],    week2: ["Easy 6mi", "Strength A", "Tempo intervals 7mi", "Easy 5mi + Yoga", "Dog Walk", "Long 16-18mi", "Dog Walk + Mobility"] },  { num: 2, label: "Build — Climbing", dates: "Mar 7 – Mar 20", mileage: "48-54", phase: 0,    week1: ["Easy 7mi", "Strength A", "Hill repeats 8mi", "Easy 6mi + Yoga", "Dog Walk", "Long trail 18-20mi", "Dog Walk"],    week2: ["Easy 7mi", "Strength B", "Moderate 9mi", "Easy 5mi + Yoga", "Strides 5mi", "Long 20-22mi", "Dog Walk"] },  { num: 3, label: "Back-to-Backs", dates: "Mar 21 – Apr 3", mileage: "52-58", phase: 0,    week1: ["Easy 7mi", "Strength A", "Moderate 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 22-24mi trail", "Moderate 12mi (B2B)"],    week2: ["Easy 7mi", "Strength B", "Hill work 9mi", "Easy 6mi + Yoga", "Dog Walk", "Long 24-26mi", "Moderate 12mi (B2B)"] },  { num: 4, label: "Peak Base", dates: "Apr 4 – Apr 17", mileage: "55-60", phase: 0,    week1: ["Easy 7mi", "Strength A", "Mod 10mi + surges", "Easy 6mi + Yoga", "Easy 5mi", "Long 26-28mi", "Moderate 14mi (B2B)"],    week2: ["Easy 8mi", "Strength B", "Tempo 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 28-30mi", "Easy 12mi (B2B)"] },  { num: 5, label: "Cut-back & Sharpen", dates: "Apr 18 – May 2", mileage: "52-55", phase: 0,    week1: ["Easy 6mi", "Strength A", "Moderate 9mi", "Easy 5mi + Yoga", "Dog Walk", "Long 22-24mi", "Easy 10mi"],    week2: ["Easy 6mi", "Strength A", "Race-pace 8mi", "Easy 5mi + Yoga", "Easy 4mi", "Long 18-20mi", "Dog Walk + Yoga"] },  // Phase 2  { num: 6, label: "Laurel Volume", dates: "May 3 – May 16", mileage: "55-58", phase: 1,    week1: ["Easy 7mi", "Strength A", "Long tempo 12mi", "Easy 6mi + Yoga", "Dog Walk", "Long 28-32mi trail", "Moderate 14mi (B2B)"],    week2: ["Easy 7mi", "Strength A", "Pace work 10mi", "Easy 5mi + Yoga", "Easy 5mi", "Long 24-26mi", "Easy 10mi (B2B)"] },  { num: 7, label: "Taper Begins", dates: "May 17 – May 30", mileage: "48-52", phase: 1,    week1: ["Easy 6mi", "Strength (light)", "Moderate 10mi", "Easy 5mi + Yoga", "Dog Walk", "Long 20-22mi", "Easy 8mi"],    week2: ["Easy 5mi", "Mobility only", "Easy 8mi + pace", "Easy 4mi + Yoga", "Dog Walk", "Long 14-16mi", "Easy 6mi"] },  { num: 8, label: "Race Week — Laurel 🏁", dates: "May 31 – Jun 13", mileage: "30-35", phase: 1,    week1: ["Easy 5mi", "Easy 4mi + strides", "Mobility + 3mi", "Rest / Dog Walk", "Easy 3mi shakeout", "Dog Walk", "Rest & Prep"],    week2: ["LAUREL 70", "Jun 13 🏁", "15-16 hr goal", "Rest/recover", "", "", ""] },  // Phase 3  { num: 9, label: "Post-Laurel Recovery", dates: "Jun 14 – Jun 27", mileage: "25-35", phase: 2,    week1: ["Dog Walk", "Yoga only", "Dog Walk", "Easy Mobility", "Dog Walk", "Easy Walk", "Rest"],    week2: ["Easy run 30min", "Mobility", "Easy run 35min", "Easy 4mi + Yoga", "Dog Walk", "Easy trail 6mi", "Easy Walk"] },  { num: 10, label: "Rebuild + Technical", dates: "Jun 28 – Jul 11", mileage: "38-45", phase: 2,    week1: ["Easy 6mi", "Strength A", "Technical trail 8mi", "Easy 5mi + Yoga", "Dog Walk", "Long hilly 16-18mi", "Easy 8mi"],    week2: ["Easy 6mi", "Strength A", "Hill repeats 8mi", "Easy 5mi + Yoga", "Dog Walk", "Long trail 18-20mi", "Easy 8mi"] },  { num: 11, label: "Escarpment Prep 🏁", dates: "Jul 12 – Jul 26", mileage: "35-40", phase: 2,    week1: ["Easy 6mi", "Strength (light)", "Technical climb 8mi", "Easy 5mi + Yoga", "Dog Walk", "Hilly long 16mi", "Easy 6mi"],    week2: ["Easy 4mi", "Easy Mobility", "Easy 4mi shakeout", "Dog Walk", "Travel/Rest", "Easy 2mi", "ESCARPMENT 🏁"] },  // Phase 4  { num: 12, label: "DS220 Rebuild", dates: "Jul 27 – Aug 9", mileage: "45-52", phase: 3,    week1: ["Easy 6mi", "Strength A", "Easy-mod 9mi", "Easy 6mi + Yoga", "Dog Walk", "Long 20-22mi", "Easy 10mi"],    week2: ["Easy 7mi", "Strength B", "Trail 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 22-24mi", "Easy 10mi (B2B)"] },  { num: 13, label: "Volume Rising", dates: "Aug 10 – Aug 23", mileage: "52-58", phase: 3,    week1: ["Easy 7mi", "Strength A", "Trail 10mi", "Easy 7mi + Yoga", "Dog Walk", "Long 26-28mi", "Moderate 14mi (B2B)"],    week2: ["Easy 7mi", "Strength B", "Hill + surges 10mi", "Easy 6mi + Yoga", "Easy 5mi", "Long 28-32mi", "Moderate 15mi (B2B)"] },  { num: 14, label: "Peak Build I", dates: "Aug 24 – Sep 6", mileage: "55-62", phase: 3,    week1: ["Easy 7mi", "Strength A", "Technical trail 10mi", "Easy 7mi + Yoga", "Dog Walk", "Long 30-35mi", "Moderate 16mi (B2B)"],    week2: ["Easy 8mi", "Strength B", "Moderate 11mi", "Easy 7mi + Yoga", "Easy 5mi", "Long 32-36mi", "Moderate 16mi (B2B)"] },  { num: 15, label: "Peak Build II — Biggest", dates: "Sep 7 – Sep 20", mileage: "58-65", phase: 3,    week1: ["Easy 8mi", "Strength A", "Trail 12mi", "Easy 7mi + Yoga", "Dog Walk", "Long 35-40mi", "Long 20mi (B2B)"],    week2: ["Easy 8mi", "Strength B", "Pace work 10mi", "Easy 7mi + Yoga", "Easy 5mi", "Long 38-42mi", "Moderate 18mi (B2B)"] },  { num: 16, label: "Cut-back & Sharpen", dates: "Sep 21 – Oct 4", mileage: "48-54", phase: 3,    week1: ["Easy 7mi", "Strength (light)", "Moderate trail 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 26-28mi", "Easy 12mi"],    week2: ["Easy 7mi", "Strength (light)", "Trail 9mi", "Easy 5mi + Yoga", "Dog Walk", "Long 22-24mi", "Easy 10mi"] },  { num: 17, label: "Dark Sky Taper 🏁", dates: "Oct 5 – Oct 25", mileage: "35-42", phase: 3,    week1: ["Easy 6mi", "Strength (light)", "Easy 8mi trail", "Easy 5mi + Yoga", "Dog Walk", "Long 18-20mi", "Easy 8mi"],    week2: ["Easy 5mi", "Mobility only", "Easy 6mi + strides", "Easy 4mi + Yoga", "Dog Walk", "Easy 10mi", "DARK SKY 220 🏁"] },];const DAY_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];// ── STATE ─────────────────────────────────────────────────────let currentBlock = 0;let runLogs = [];let modalDay = null;let kneeVal = "great";// Storage — uses Claude's cross-device storage when available, falls back to localStorageasync function loadLogs() {  try {    if (window.storage) {      const r = await window.storage.get('vinny-run-logs');      if (r) { runLogs = JSON.parse(r.value); return; }    }  } catch(e) {}  try {    const saved = localStorage.getItem('vinny-run-logs');    if (saved) runLogs = JSON.parse(saved);  } catch(e) { runLogs = []; }}async function saveLogs() {  const data = JSON.stringify(runLogs);  try {    if (window.storage) await window.storage.set('vinny-run-logs', data);  } catch(e) {}  try { localStorage.setItem('vinny-run-logs', data); } catch(e) {}}// ── INIT ──────────────────────────────────────────────────────async function init() {  await loadLogs();  document.getElementById('todayDate').textContent = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });  renderRaceChips();  findCurrentBlock();  renderCheckins();  renderFullSchedule();}function findCurrentBlock() {  const today = new Date();  // Default to first block, but try to find the right one based on date  // Block 1 starts Feb 21 2026  const blockStart = new Date('2026-02-21');  const diffDays = Math.floor((today - blockStart) / (1000 * 60 * 60 * 24));  if (diffDays >= 0) {    const blockIdx = Math.min(Math.floor(diffDays / 14), BLOCKS.length - 1);    currentBlock = Math.max(0, blockIdx);  }  renderBlock(currentBlock);}// ── RACE CHIPS ────────────────────────────────────────────────function renderRaceChips() {  const today = new Date();  const container = document.getElementById('raceChips');  container.innerHTML = RACES.map(r => {    const raceDate = new Date(r.date);    const days = Math.ceil((raceDate - today) / (1000 * 60 * 60 * 24));    return `<div class="race-chip ${r.aRace ? 'a-race' : ''}">      <span class="race-chip-name">${r.name}</span>      <span class="race-chip-date">${formatDate(r.date)}</span>      <span class="race-chip-days">${days > 0 ? days + 'd away' : 'Race day!'}</span>    </div>`;  }).join('');}// ── BLOCK RENDER ──────────────────────────────────────────────function renderBlock(idx) {  const b = BLOCKS[idx];  const phase = PHASES[b.phase];  document.getElementById('blockLabel').textContent = `Block ${b.num} of ${BLOCKS.length}`;  document.getElementById('blockTitle').textContent = b.label;  document.getElementById('blockDates').textContent = b.dates + ' · ~' + b.mileage + ' mi/wk';  document.getElementById('phaseName').textContent = phase.name;  document.getElementById('phaseDesc').textContent = phase.desc;  // Headers  // Calculate block start date — Block 1 starts Feb 21 2026, each block is 14 days  const blockStartDate = new Date('2026-02-21T00:00:00');  blockStartDate.setDate(blockStartDate.getDate() + (idx * 14));  // Remove static day headers — dates are now shown on cards  ['dayHeaders1', 'dayHeaders2'].forEach(id => {    document.getElementById(id).innerHTML = '';  });  renderWeek('week1Grid', b.week1, b.num, 1, blockStartDate, 0);  renderWeek('week2Grid', b.week2, b.num, 2, blockStartDate, 7);}const FULL_DAY_NAMES = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];function renderWeek(gridId, days, blockNum, weekNum, blockStartDate, dayOffset) {  const grid = document.getElementById(gridId);  grid.innerHTML = days.map((desc, i) => {    const dayKey = `b${blockNum}w${weekNum}d${i}`;    const log = runLogs.find(l => l.dayKey === dayKey);    const isRest = desc.toLowerCase().includes('dog walk') || desc.toLowerCase().includes('rest') || desc.toLowerCase().includes('yoga only') || desc.toLowerCase().includes('mobility only');    // Calculate actual date for this slot    const d = new Date(blockStartDate);    d.setDate(d.getDate() + dayOffset + i);    const dayName = FULL_DAY_NAMES[d.getDay()];    const dateLabel = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });    const today = new Date();    const isToday = d.toDateString() === today.toDateString();    let cls = 'day-card';    if (log) cls += ' logged';    else if (isRest) cls += ' rest';    if (isToday) cls += ' today';    return `<div class="${cls}" onclick="openModal('${dayKey}', '${desc.replace(/'/g, "\\'")}', ${blockNum}, ${weekNum}, ${i}, '${dateLabel}')">      <span class="day-num">${dayName}</span>      <span class="day-num" style="font-size:9px;color:var(--teal);margin-top:-1px">${dateLabel}</span>      <span class="day-type">${desc}</span>      ${log ? `<span class="day-check">✓</span><span class="day-rpe">RPE ${log.rpe}</span>` : ''}    </div>`;  }).join('');}function prevBlock() {  if (currentBlock > 0) { currentBlock--; renderBlock(currentBlock); }}function nextBlock() {  if (currentBlock < BLOCKS.length - 1) { currentBlock++; renderBlock(currentBlock); }}// ── MODAL ─────────────────────────────────────────────────────function openModal(dayKey, desc, blockNum, weekNum, dayIdx, dateLabel) {  modalDay = { dayKey, desc };  document.getElementById('modalTitle').textContent = 'Log: ' + desc;  document.getElementById('modalSubtitle').textContent = `Block ${blockNum} · Week ${weekNum} · ${DAY_NAMES[dayIdx]}${dateLabel ? ' · ' + dateLabel : ''}`;  // Reset session 2  document.getElementById('session2Block').classList.remove('open');  document.getElementById('addSession2Btn').textContent = '+ Add 2nd session';  document.getElementById('inputDist2').value = '';  document.getElementById('inputTime2').value = '';  document.getElementById('rpeSlider2').value = 5;  document.getElementById('rpeVal2').textContent = 5;  document.getElementById('inputNote2').value = '';  document.getElementById('inputSuunto2').value = '';  // Pre-fill if existing  const existing = runLogs.find(l => l.dayKey === dayKey);  if (existing) {    document.getElementById('inputDist').value = existing.dist || '';    document.getElementById('inputTime').value = existing.time || '';    document.getElementById('rpeSlider').value = existing.rpe || 5;    document.getElementById('rpeVal').textContent = existing.rpe || 5;    document.getElementById('inputNote').value = existing.note || '';    kneeVal = existing.knee || 'great';    if (existing.session2) {      document.getElementById('session2Block').classList.add('open');      document.getElementById('addSession2Btn').textContent = '− Remove 2nd session';      document.getElementById('inputDist2').value = existing.session2.dist || '';      document.getElementById('inputTime2').value = existing.session2.time || '';      document.getElementById('rpeSlider2').value = existing.session2.rpe || 5;      document.getElementById('rpeVal2').textContent = existing.session2.rpe || 5;      document.getElementById('inputNote2').value = existing.session2.note || '';    }  } else {    document.getElementById('inputDist').value = '';    document.getElementById('inputTime').value = '';    document.getElementById('rpeSlider').value = 5;    document.getElementById('rpeVal').textContent = 5;    document.getElementById('inputNote').value = '';    kneeVal = 'great';  }  document.querySelectorAll('.knee-opt').forEach(el => {    el.classList.toggle('selected', el.dataset.val === kneeVal);  });  document.getElementById('logModal').classList.add('open');}function closeModal() {  document.getElementById('logModal').classList.remove('open');}// ── GPX / FIT PARSING ─────────────────────────────────────────function parseActivityFile(input, session) {  const file = input.files[0];  if (!file) return;  const suffix = session === 1 ? '' : '2';  const statusEl = document.getElementById('gpxStatus' + suffix);  const btnText = document.getElementById('gpxBtnText' + suffix);  statusEl.className = 'gpx-status';  statusEl.textContent = 'Parsing...';  const reader = new FileReader();  reader.onload = function(e) {    try {      const ext = file.name.split('.').pop().toLowerCase();      let distMiles = null, durationSecs = null;      if (ext === 'gpx') {        const parser = new DOMParser();        const xml = parser.parseFromString(e.target.result, 'text/xml');        const trkpts = xml.querySelectorAll('trkpt');        if (trkpts.length < 2) throw new Error('Not enough track points');        // Calculate distance from lat/lon points        let totalMeters = 0;        let pts = Array.from(trkpts).map(p => ({          lat: parseFloat(p.getAttribute('lat')),          lon: parseFloat(p.getAttribute('lon')),          time: p.querySelector('time') ? new Date(p.querySelector('time').textContent) : null        }));        for (let i = 1; i < pts.length; i++) {          totalMeters += haversine(pts[i-1].lat, pts[i-1].lon, pts[i].lat, pts[i].lon);        }        distMiles = totalMeters / 1609.344;        // Duration from first to last timestamp        if (pts[0].time && pts[pts.length-1].time) {          durationSecs = (pts[pts.length-1].time - pts[0].time) / 1000;        }      } else if (ext === 'fit') {        // Parse FIT binary — look for key record messages        const buf = new Uint8Array(e.target.result);        const result = parseFitDistance(buf);        if (result) { distMiles = result.distMiles; durationSecs = result.durationSecs; }      }      if (distMiles !== null) {        document.getElementById('inputDist' + suffix).value = distMiles.toFixed(2);        btnText.textContent = '✓ ' + file.name;      }      if (durationSecs !== null) {        const h = Math.floor(durationSecs / 3600);        const m = Math.floor((durationSecs % 3600) / 60);        const timeStr = h > 0 ? `${h}:${String(m).padStart(2,'0')}` : `0:${String(m).padStart(2,'0')}`;        document.getElementById('inputTime' + suffix).value = timeStr;      }      statusEl.textContent = distMiles ? `✓ ${distMiles.toFixed(2)} mi${durationSecs ? ' · ' + formatDuration(durationSecs) : ''}` : '✓ File loaded — check fields';    } catch(err) {      statusEl.className = 'gpx-status error';      statusEl.textContent = 'Could not parse file — fill in manually';      console.warn('GPX parse error:', err);    }  };  if (file.name.endsWith('.fit')) reader.readAsArrayBuffer(file);  else reader.readAsText(file);}function haversine(lat1, lon1, lat2, lon2) {  const R = 6371000;  const dLat = (lat2 - lat1) * Math.PI / 180;  const dLon = (lon2 - lon1) * Math.PI / 180;  const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2;  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));}function parseFitDistance(buf) {  // FIT files store total distance in session/lap messages  // Look for session message (type 18) which has total_distance field  try {    // Skip 14-byte header    let offset = 14;    let localMsgDefs = {};    let distMeters = null, durationSecs = null;    while (offset < buf.length - 2) {      const recHdr = buf[offset];      const isDefn = (recHdr & 0x40) !== 0;      const localNum = recHdr & 0x0F;      if (isDefn) {        // Definition message        offset++; // skip header        offset++; // reserved        const arch = buf[offset++]; // architecture (0=little, 1=big)        const globalMsgNum = arch === 0 ? (buf[offset] | buf[offset+1]<<8) : (buf[offset]<<8 | buf[offset+1]);        offset += 2;        const numFields = buf[offset++];        const fields = [];        for (let i = 0; i < numFields; i++) {          fields.push({ defNum: buf[offset], size: buf[offset+1], type: buf[offset+2] });          offset += 3;        }        localMsgDefs[localNum] = { globalMsgNum, arch, fields };      } else {        // Data message        const def = localMsgDefs[localNum];        if (!def) { offset++; continue; }        offset++; // skip header        const msgStart = offset;        let msgSize = def.fields.reduce((s, f) => s + f.size, 0);        // session message = global 18, total_distance = field def 9, timer_time = field def 7        if (def.globalMsgNum === 18) {          let fieldOffset = msgStart;          for (const f of def.fields) {            if (f.defNum === 9 && f.size === 4) {              const val = def.arch === 0                ? (buf[fieldOffset] | buf[fieldOffset+1]<<8 | buf[fieldOffset+2]<<16 | buf[fieldOffset+3]<<24)                : (buf[fieldOffset]<<24 | buf[fieldOffset+1]<<16 | buf[fieldOffset+2]<<8 | buf[fieldOffset+3]);              distMeters = val / 100;            }            if (f.defNum === 7 && f.size === 4) {              const val = def.arch === 0                ? (buf[fieldOffset] | buf[fieldOffset+1]<<8 | buf[fieldOffset+2]<<16 | buf[fieldOffset+3]<<24)                : (buf[fieldOffset]<<24 | buf[fieldOffset+1]<<16 | buf[fieldOffset+2]<<8 | buf[fieldOffset+3]);              durationSecs = val / 1000;            }            fieldOffset += f.size;          }        }        offset += msgSize;      }    }    if (distMeters) return { distMiles: distMeters / 1609.344, durationSecs };    return null;  } catch(e) { return null; }}function formatDuration(secs) {  const h = Math.floor(secs / 3600);  const m = Math.floor((secs % 3600) / 60);  return h > 0 ? `${h}h ${m}m` : `${m}m`;}function updateRPE(val) {  document.getElementById('rpeVal').textContent = val;}function updateRPE2(val) {  document.getElementById('rpeVal2').textContent = val;}function toggleSession2() {  const block = document.getElementById('session2Block');  const btn = document.getElementById('addSession2Btn');  const isOpen = block.classList.toggle('open');  btn.textContent = isOpen ? '− Remove 2nd session' : '+ Add 2nd session';}function selectKnee(el) {  kneeVal = el.dataset.val;  document.querySelectorAll('.knee-opt').forEach(o => o.classList.remove('selected'));  el.classList.add('selected');}async function saveLog() {  if (!modalDay) return;  const session2Open = document.getElementById('session2Block').classList.contains('open');  const entry = {    dayKey: modalDay.dayKey,    desc: modalDay.desc,    dist: document.getElementById('inputDist').value,    time: document.getElementById('inputTime').value,    rpe: parseInt(document.getElementById('rpeSlider').value),    knee: kneeVal,    note: document.getElementById('inputNote').value,    loggedAt: new Date().toISOString(),    session2: session2Open ? {      dist: document.getElementById('inputDist2').value,      time: document.getElementById('inputTime2').value,      rpe: parseInt(document.getElementById('rpeSlider2').value),      note: document.getElementById('inputNote2').value,    } : null,  };  const idx = runLogs.findIndex(l => l.dayKey === entry.dayKey);  if (idx >= 0) runLogs[idx] = entry;  else runLogs.push(entry);  saveLogs();  // fire and forget — async but we don't need to wait  closeModal();  renderBlock(currentBlock);  renderLogList();  renderStats();  showToast('Run logged ✓');}// ── LOG LIST ──────────────────────────────────────────────────function renderLogList() {  const list = document.getElementById('logList');  if (!runLogs.length) {    list.innerHTML = `<div class="empty-state">      <div class="empty-state-icon">🏃</div>      <div class="empty-state-text">No runs logged yet</div>      <div class="empty-state-sub">Tap any day in the Block view to log a run</div>    </div>`;    return;  }  const sorted = [...runLogs].sort((a, b) => b.loggedAt.localeCompare(a.loggedAt));  list.innerHTML = sorted.map(l => {    const totalDist = (parseFloat(l.dist)||0) + (l.session2 ? (parseFloat(l.session2.dist)||0) : 0);    return `    <div class="log-entry ${l.knee === 'grumpy' ? 'flagged' : ''}">      <div class="log-entry-header">        <div>          <div class="log-entry-date">${new Date(l.loggedAt).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}</div>          <div class="log-entry-title">${l.desc}${l.session2 ? ' <span style="font-size:11px;color:var(--teal);font-family:\'DM Mono\',monospace">×2</span>' : ''}</div>        </div>      </div>      <div class="log-entry-stats">        ${totalDist ? `<span class="stat-chip">${totalDist.toFixed(1)} mi total</span>` : ''}        ${l.time ? `<span class="stat-chip">S1: ${l.time}</span>` : ''}        ${l.rpe ? `<span class="stat-chip">RPE ${l.rpe}</span>` : ''}        ${l.session2 && l.session2.time ? `<span class="stat-chip">S2: ${l.session2.time}</span>` : ''}        ${l.session2 && l.session2.rpe ? `<span class="stat-chip">RPE ${l.session2.rpe}</span>` : ''}        ${l.knee === 'grumpy' ? `<span class="stat-chip" style="background:#fdf0ea;color:#e8622a">⚠️ Knee</span>` : ''}        ${l.suunto ? `<span class="stat-chip" style="cursor:pointer" onclick="window.open('${l.suunto}','_blank')">📡 S1</span>` : ''}        ${l.session2 && l.session2.suunto ? `<span class="stat-chip" style="cursor:pointer" onclick="window.open('${l.session2.suunto}','_blank')">📡 S2</span>` : ''}      </div>      ${l.note ? `<div class="log-note">"${l.note}"</div>` : ''}      ${l.session2 && l.session2.note ? `<div class="log-note" style="margin-top:4px;border-left:2px solid var(--teal-light);padding-left:8px">"${l.session2.note}"</div>` : ''}    </div>  `}).join('');}function renderStats() {  const totalMiles = runLogs.reduce((s, l) => s + (parseFloat(l.dist) || 0) + (l.session2 ? (parseFloat(l.session2.dist)||0) : 0), 0);  const totalRuns = runLogs.filter(l => l.dist).length;  const avgRPE = runLogs.length ? (runLogs.reduce((s, l) => s + l.rpe, 0) / runLogs.length).toFixed(1) : '—';  const kneeFlags = runLogs.filter(l => l.knee === 'grumpy').length;  document.getElementById('statsGrid').innerHTML = `    <div class="stat-card">      <span class="stat-card-value">${totalMiles.toFixed(0)}</span>      <span class="stat-card-label">Miles Logged</span>    </div>    <div class="stat-card">      <span class="stat-card-value">${totalRuns}</span>      <span class="stat-card-label">Runs Logged</span>    </div>    <div class="stat-card">      <span class="stat-card-value">${avgRPE}</span>      <span class="stat-card-label">Avg RPE</span>    </div>    <div class="stat-card">      <span class="stat-card-value" style="color:${kneeFlags > 0 ? '#e8622a' : '#2a7a4a'}">${kneeFlags}</span>      <span class="stat-card-label">Knee Flags</span>    </div>  `;}// ── SCHEDULE ──────────────────────────────────────────────────function renderFullSchedule() {  const container = document.getElementById('fullSchedule');  container.innerHTML = BLOCKS.map((b, i) => {    const phase = PHASES[b.phase];    return `<div style="margin-bottom:16px">      <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">        <div style="width:8px;height:8px;border-radius:50%;background:var(--teal);flex-shrink:0"></div>        <div style="font-size:10px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--gray);font-family:'DM Mono',monospace">${b.dates}</div>      </div>      <div class="checkin-card" style="cursor:pointer" onclick="switchToBlock(${i})">        <div class="checkin-info">          <div class="checkin-title">Block ${b.num}: ${b.label}</div>          <div class="checkin-date">${phase.name} · ~${b.mileage} mi/wk</div>        </div>        <div style="font-size:18px">›</div>      </div>    </div>`;  }).join('');}function switchToBlock(idx) {  currentBlock = idx;  renderBlock(currentBlock);  switchTab('block');}// ── CHECKINS ──────────────────────────────────────────────────function renderCheckins() {  const today = new Date();  let nextFound = false;  document.getElementById('checkinList').innerHTML = CHECKINS.map(c => {    const d = new Date(c.date);    const isPast = d < today;    const isNext = !isPast && !nextFound;    if (isNext) nextFound = true;    return `<div class="checkin-card">      <div class="checkin-dot ${isNext ? 'next' : isPast ? 'done' : ''}"></div>      <div class="checkin-info">        <div class="checkin-title">${c.label}</div>        <div class="checkin-date">${formatDate(c.date)}</div>      </div>      <div class="checkin-status">${isPast ? '✓' : isNext ? 'NEXT' : ''}</div>    </div>`;  }).join('');}// ── TABS ──────────────────────────────────────────────────────function switchTab(tab) {  document.querySelectorAll('.tab').forEach((t, i) => {    const tabs = ['block', 'log', 'schedule', 'checkin'];    t.classList.toggle('active', tabs[i] === tab);  });  document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));  document.getElementById('section-' + tab).classList.add('active');  if (tab === 'log') { renderLogList(); renderStats(); }}// ── UTILS ─────────────────────────────────────────────────────function formatDate(dateStr) {  return new Date(dateStr + 'T12:00:00').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });}function showToast(msg) {  const t = document.getElementById('toast');  t.textContent = msg;  t.classList.add('show');  setTimeout(() => t.classList.remove('show'), 2200);}// Close modal on overlay clickdocument.getElementById('logModal').addEventListener('click', function(e) {  if (e.target === this) closeModal();});init().catch(console.error);</script></body></html><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>Vinny</title><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="Vinny"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#111a1a"><link rel="manifest" id="manifest-link" href="#"><link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 180'%3E%3Crect width='180' height='180' rx='40' fill='%231a7a7a'/%3E%3Ctext x='90' y='116' font-family='Arial' font-weight='bold' font-size='72' text-anchor='middle' fill='white'%3EUT%3C/text%3E%3C/svg%3E"><script>window.addEventListener('DOMContentLoaded', function() {  var manifestData = {    name: "Vinny \u2014 Ultra Training",    short_name: "Vinny",    start_url: "./",    display: "standalone",    background_color: "#111a1a",    theme_color: "#111a1a",    icons: [{ src: "data:image/png;base64,iVBORw0KGgo=", sizes: "192x192", type: "image/png" }]  };  var blob = new Blob([JSON.stringify(manifestData)], {type: 'application/json'});  document.getElementById('manifest-link').href = URL.createObjectURL(blob);});</script><link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;600;700;800&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet"><style>  :root {    --teal: #1a7a7a;    --teal-light: #2a9d9d;    --teal-dark: #0f5252;    --teal-faint: #e8f5f5;    --bone: #f5f0e8;    --dark: #111a1a;    --mid: #3d5252;    --gray: #8a9e9e;    --gray-light: #c5d5d5;    --white: #ffffff;    --accent: #e8622a;    --accent-light: #fdf0ea;    --green: #2a7a4a;    --green-light: #e8f5ee;    --shadow: 0 4px 24px rgba(26,122,122,0.12);    --shadow-lg: 0 8px 40px rgba(26,122,122,0.18);  }  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }  body {    font-family: 'Syne', sans-serif;    background: var(--dark);    color: var(--dark);    min-height: 100vh;    overflow-x: hidden;  }  /* ── HEADER ── */  .header {    background: var(--dark);    padding: 20px 20px 0;    position: sticky;    top: 0;    z-index: 100;  }  .header-top {    display: flex;    align-items: center;    justify-content: space-between;    margin-bottom: 16px;  }  .logo {    font-size: 11px;    font-weight: 700;    letter-spacing: 0.2em;    text-transform: uppercase;    color: var(--teal-light);    font-family: 'DM Mono', monospace;  }  .header-date {    font-size: 11px;    color: var(--gray);    font-family: 'DM Mono', monospace;    letter-spacing: 0.05em;  }  .header-races {    display: flex;    gap: 8px;    overflow-x: auto;    padding-bottom: 16px;    -webkit-overflow-scrolling: touch;    scrollbar-width: none;  }  .header-races::-webkit-scrollbar { display: none; }  .race-chip {    flex-shrink: 0;    background: rgba(255,255,255,0.05);    border: 1px solid rgba(255,255,255,0.08);    border-radius: 8px;    padding: 10px 14px;    cursor: pointer;    transition: all 0.2s;  }  .race-chip.a-race {    background: rgba(26,122,122,0.25);    border-color: var(--teal);  }  .race-chip-name {    font-size: 11px;    font-weight: 700;    color: var(--white);    letter-spacing: 0.05em;    display: block;  }  .race-chip-date {    font-size: 10px;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin-top: 2px;    display: block;  }  .race-chip-days {    font-size: 13px;    font-weight: 800;    color: var(--teal-light);    margin-top: 4px;    display: block;  }  .race-chip.a-race .race-chip-days { color: #7fd4d4; }  /* ── NAV TABS ── */  .tabs {    background: var(--dark);    display: flex;    border-bottom: 1px solid rgba(255,255,255,0.08);    position: sticky;    top: 0;    z-index: 99;  }  .tab {    flex: 1;    padding: 14px 8px;    text-align: center;    font-size: 11px;    font-weight: 700;    letter-spacing: 0.1em;    text-transform: uppercase;    color: var(--gray);    cursor: pointer;    border-bottom: 2px solid transparent;    transition: all 0.2s;  }  .tab.active {    color: var(--teal-light);    border-bottom-color: var(--teal-light);  }  /* ── CONTENT ── */  .content {    background: var(--bone);    min-height: calc(100vh - 160px);    border-radius: 20px 20px 0 0;  }  .section { display: none; padding: 24px 20px; }  .section.active { display: block; }  /* ── BLOCK VIEW ── */  .block-header {    display: flex;    align-items: flex-start;    justify-content: space-between;    margin-bottom: 20px;  }  .block-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.2em;    text-transform: uppercase;    color: var(--teal);    font-family: 'DM Mono', monospace;  }  .block-title {    font-size: 26px;    font-weight: 800;    color: var(--dark);    line-height: 1.1;    margin-top: 4px;  }  .block-subtitle {    font-size: 13px;    color: var(--mid);    margin-top: 4px;    font-family: 'DM Mono', monospace;  }  .block-nav {    display: flex;    gap: 6px;  }  .block-btn {    width: 36px;    height: 36px;    border-radius: 50%;    border: 1.5px solid var(--gray-light);    background: white;    color: var(--dark);    font-size: 16px;    cursor: pointer;    display: flex;    align-items: center;    justify-content: center;    transition: all 0.2s;  }  .block-btn:active { background: var(--teal); color: white; border-color: var(--teal); }  /* ── DAYS GRID ── */  .week-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.15em;    text-transform: uppercase;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin: 16px 0 8px;  }  .days-grid {    display: grid;    grid-template-columns: repeat(7, 1fr);    gap: 4px;    margin-bottom: 4px;  }  .day-col-header {    text-align: center;    font-size: 9px;    font-weight: 700;    letter-spacing: 0.1em;    color: var(--gray);    font-family: 'DM Mono', monospace;    padding-bottom: 4px;  }  .day-card {    background: white;    border-radius: 10px;    padding: 8px 4px;    text-align: center;    cursor: pointer;    border: 1.5px solid transparent;    transition: all 0.15s;    position: relative;    min-height: 72px;    display: flex;    flex-direction: column;    align-items: center;    justify-content: flex-start;    gap: 2px;  }  .day-card:active { transform: scale(0.96); }  .day-card.logged { background: var(--green-light); border-color: var(--green); }  .day-card.today { border-color: var(--teal); background: var(--teal-faint); }  .day-card.rest { background: #f8f4ee; }  .day-card.active-day { border-color: var(--accent); background: var(--accent-light); }  .day-num {    font-size: 11px;    font-weight: 700;    color: var(--dark);    font-family: 'DM Mono', monospace;  }  .day-type {    font-size: 8px;    color: var(--mid);    line-height: 1.2;    font-weight: 600;    letter-spacing: 0.02em;    padding: 0 2px;  }  .day-check {    font-size: 14px;    margin-top: 2px;  }  .day-rpe {    font-size: 8px;    font-family: 'DM Mono', monospace;    color: var(--teal);    font-weight: 700;    background: var(--teal-faint);    padding: 1px 4px;    border-radius: 4px;  }  /* ── PHASE BANNER ── */  .phase-banner {    background: var(--dark);    border-radius: 12px;    padding: 14px 16px;    margin-bottom: 20px;    display: flex;    align-items: center;    gap: 12px;  }  .phase-dot {    width: 10px;    height: 10px;    border-radius: 50%;    background: var(--teal-light);    flex-shrink: 0;  }  .phase-text {    font-size: 12px;    color: var(--gray-light);    line-height: 1.4;  }  .phase-name {    font-weight: 700;    color: var(--white);    display: block;  }  /* ── LOG MODAL ── */  .modal-overlay {    display: none;    position: fixed;    inset: 0;    background: rgba(10,20,20,0.85);    z-index: 200;    align-items: flex-end;    justify-content: center;  }  .modal-overlay.open { display: flex; }  .modal {    background: var(--white);    border-radius: 24px 24px 0 0;    padding: 28px 24px 40px;    width: 100%;    max-width: 500px;    max-height: 88vh;    overflow-y: auto;    animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);  }  @keyframes slideUp {    from { transform: translateY(100%); }    to { transform: translateY(0); }  }  .modal-handle {    width: 40px;    height: 4px;    background: var(--gray-light);    border-radius: 2px;    margin: 0 auto 20px;  }  .modal-title {    font-size: 22px;    font-weight: 800;    color: var(--dark);    margin-bottom: 4px;  }  .modal-subtitle {    font-size: 13px;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin-bottom: 24px;  }  .form-group {    margin-bottom: 18px;  }  .form-label {    font-size: 11px;    font-weight: 700;    letter-spacing: 0.1em;    text-transform: uppercase;    color: var(--teal);    display: block;    margin-bottom: 8px;    font-family: 'DM Mono', monospace;  }  .form-row {    display: grid;    grid-template-columns: 1fr 1fr;    gap: 12px;  }  input[type="text"], input[type="number"], textarea, select {    width: 100%;    padding: 12px 14px;    border: 1.5px solid var(--gray-light);    border-radius: 10px;    font-family: 'Syne', sans-serif;    font-size: 15px;    color: var(--dark);    background: var(--bone);    outline: none;    transition: border-color 0.2s;    -webkit-appearance: none;  }  input:focus, textarea:focus, select:focus {    border-color: var(--teal);    background: white;  }  textarea { resize: none; height: 80px; line-height: 1.5; }  /* RPE slider */  .rpe-slider {    -webkit-appearance: none;    appearance: none;    width: 100%;    height: 6px;    border-radius: 3px;    background: var(--gray-light);    outline: none;    border: none;    padding: 0;  }  .rpe-slider::-webkit-slider-thumb {    -webkit-appearance: none;    width: 24px;    height: 24px;    border-radius: 50%;    background: var(--teal);    cursor: pointer;    box-shadow: 0 2px 8px rgba(26,122,122,0.4);  }  .rpe-labels {    display: flex;    justify-content: space-between;    margin-top: 6px;    font-size: 10px;    color: var(--gray);    font-family: 'DM Mono', monospace;  }  .rpe-value {    text-align: center;    font-size: 32px;    font-weight: 800;    color: var(--teal);    font-family: 'DM Mono', monospace;    margin-top: 4px;  }  /* Knee toggle */  .knee-options {    display: flex;    gap: 8px;  }  .knee-opt {    flex: 1;    padding: 10px 8px;    border: 1.5px solid var(--gray-light);    border-radius: 10px;    text-align: center;    cursor: pointer;    font-size: 12px;    font-weight: 700;    color: var(--mid);    transition: all 0.15s;  }  .knee-opt.selected { border-color: var(--teal); background: var(--teal-faint); color: var(--teal); }  .knee-opt.warn.selected { border-color: var(--accent); background: var(--accent-light); color: var(--accent); }  .btn-primary {    width: 100%;    padding: 16px;    background: var(--teal);    color: white;    border: none;    border-radius: 12px;    font-family: 'Syne', sans-serif;    font-size: 15px;    font-weight: 700;    cursor: pointer;    margin-top: 8px;    letter-spacing: 0.05em;    transition: all 0.2s;  }  .btn-primary:active { background: var(--teal-dark); transform: scale(0.98); }  .btn-ghost {    width: 100%;    padding: 14px;    background: transparent;    color: var(--gray);    border: 1.5px solid var(--gray-light);    border-radius: 12px;    font-family: 'Syne', sans-serif;    font-size: 14px;    font-weight: 600;    cursor: pointer;    margin-top: 8px;  }  /* ── LOG LIST ── */  .log-entry {    background: white;    border-radius: 14px;    padding: 16px;    margin-bottom: 12px;    border-left: 3px solid var(--teal);  }  .log-entry.flagged { border-left-color: var(--accent); }  .log-entry-header {    display: flex;    justify-content: space-between;    align-items: flex-start;    margin-bottom: 8px;  }  .log-entry-date {    font-size: 11px;    font-family: 'DM Mono', monospace;    color: var(--gray);  }  .log-entry-title {    font-size: 16px;    font-weight: 700;    color: var(--dark);  }  .log-entry-stats {    display: flex;    gap: 12px;    flex-wrap: wrap;    margin-top: 8px;  }  .stat-chip {    font-size: 11px;    font-family: 'DM Mono', monospace;    color: var(--teal);    background: var(--teal-faint);    padding: 3px 8px;    border-radius: 6px;    font-weight: 700;  }  .log-note {    font-size: 13px;    color: var(--mid);    margin-top: 8px;    line-height: 1.5;    font-family: 'Instrument Serif', serif;    font-style: italic;  }  /* ── CHECKIN SECTION ── */  .checkin-card {    background: white;    border-radius: 14px;    padding: 18px;    margin-bottom: 12px;    display: flex;    align-items: center;    gap: 14px;  }  .checkin-dot {    width: 12px;    height: 12px;    border-radius: 50%;    background: var(--gray-light);    flex-shrink: 0;  }  .checkin-dot.next { background: var(--teal); box-shadow: 0 0 0 4px var(--teal-faint); }  .checkin-dot.done { background: var(--green); }  .checkin-info { flex: 1; }  .checkin-title { font-size: 15px; font-weight: 700; color: var(--dark); }  .checkin-date { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--gray); margin-top: 2px; }  .checkin-status { font-size: 11px; font-weight: 700; color: var(--teal); }  /* ── SUMMARY STATS ── */  .stats-grid {    display: grid;    grid-template-columns: 1fr 1fr;    gap: 12px;    margin-bottom: 20px;  }  .stat-card {    background: white;    border-radius: 14px;    padding: 16px;    text-align: center;  }  .stat-card-value {    font-size: 32px;    font-weight: 800;    color: var(--teal);    font-family: 'DM Mono', monospace;    display: block;  }  .stat-card-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.1em;    text-transform: uppercase;    color: var(--gray);    display: block;    margin-top: 4px;  }  /* ── WEATHER WIDGET ── */  .weather-card {    background: var(--dark);    border-radius: 14px;    padding: 18px;    margin-bottom: 20px;    color: white;  }  .weather-label {    font-size: 10px;    letter-spacing: 0.2em;    text-transform: uppercase;    color: var(--gray);    font-family: 'DM Mono', monospace;    margin-bottom: 8px;  }  .weather-location {    font-size: 14px;    font-weight: 700;    color: white;  }  .weather-note {    font-size: 12px;    color: var(--gray);    margin-top: 6px;    line-height: 1.5;  }  /* Toast */  .toast {    position: fixed;    bottom: 30px;    left: 50%;    transform: translateX(-50%) translateY(80px);    background: var(--dark);    color: white;    padding: 12px 24px;    border-radius: 100px;    font-size: 13px;    font-weight: 700;    z-index: 300;    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);    white-space: nowrap;  }  .toast.show { transform: translateX(-50%) translateY(0); }  .suunto-link {    width: 100%;    padding: 12px 14px;    border: 1.5px dashed var(--teal-light);    border-radius: 10px;    background: var(--teal-faint);    font-family: 'DM Mono', monospace;    font-size: 13px;    color: var(--teal);    outline: none;  }  .gpx-upload-btn {    display: block;    width: 100%;    padding: 12px 14px;    background: var(--teal-faint);    border: 1.5px dashed var(--teal-light);    border-radius: 10px;    font-family: 'Syne', sans-serif;    font-size: 13px;    font-weight: 600;    color: var(--teal);    cursor: pointer;    text-align: center;    transition: all 0.2s;  }  .gpx-upload-btn:active { border-style: solid; }  .gpx-status {    font-size: 12px;    font-family: 'DM Mono', monospace;    color: var(--green);    margin-top: 6px;    min-height: 16px;    font-weight: 600;  }  .gpx-status.error { color: var(--accent); }  .add-session-btn {    width: 100%;    padding: 10px;    background: transparent;    border: 1.5px dashed var(--gray-light);    border-radius: 10px;    font-family: 'Syne', sans-serif;    font-size: 13px;    font-weight: 600;    color: var(--gray);    cursor: pointer;    text-align: center;    transition: all 0.2s;    margin-bottom: 4px;  }  .add-session-btn:active { border-color: var(--teal); color: var(--teal); }  .session-divider {    display: flex;    align-items: center;    gap: 10px;    margin: 16px 0 12px;  }  .session-divider-line { flex: 1; height: 1px; background: var(--gray-light); }  .session-divider-label {    font-size: 10px;    font-weight: 700;    letter-spacing: 0.15em;    text-transform: uppercase;    color: var(--teal);    font-family: 'DM Mono', monospace;  }  .session2-block { display: none; }  .session2-block.open { display: block; }  .empty-state {    text-align: center;    padding: 40px 20px;    color: var(--gray);  }  .empty-state-icon { font-size: 48px; margin-bottom: 12px; }  .empty-state-text { font-size: 15px; font-weight: 600; color: var(--mid); }  .empty-state-sub { font-size: 13px; color: var(--gray); margin-top: 4px; }</style></head><body><div class="header">  <div class="header-top">    <div class="logo">Ultra Training 2026</div>    <div class="header-date" id="todayDate"></div>  </div>  <div class="header-races" id="raceChips"></div></div><div class="tabs">  <div class="tab active" onclick="switchTab('block')">Block</div>  <div class="tab" onclick="switchTab('log')">My Runs</div>  <div class="tab" onclick="switchTab('schedule')">Schedule</div>  <div class="tab" onclick="switchTab('checkin')">Check-Ins</div></div><div class="content">  <!-- BLOCK VIEW -->  <div class="section active" id="section-block">    <div class="block-header">      <div>        <div class="block-label" id="blockLabel">Block 1</div>        <div class="block-title" id="blockTitle">Baseline</div>        <div class="block-subtitle" id="blockDates">Feb 21 – Mar 6</div>      </div>      <div class="block-nav">        <button class="block-btn" onclick="prevBlock()">‹</button>        <button class="block-btn" onclick="nextBlock()">›</button>      </div>    </div>    <div class="phase-banner" id="phaseBanner">      <div class="phase-dot"></div>      <div class="phase-text">        <span class="phase-name" id="phaseName">Phase 1: Base & Build</span>        <span id="phaseDesc">Aerobic foundation, trail volume, strength</span>      </div>    </div>    <div class="week-label">Week 1</div>    <div class="days-grid" id="dayHeaders1"></div>    <div class="days-grid" id="week1Grid"></div>    <div class="week-label">Week 2</div>    <div class="days-grid" id="dayHeaders2"></div>    <div class="days-grid" id="week2Grid"></div>  </div>  <!-- LOG VIEW -->  <div class="section" id="section-log">    <div class="stats-grid" id="statsGrid"></div>    <div id="logList"></div>  </div>  <!-- SCHEDULE VIEW -->  <div class="section" id="section-schedule">    <div class="weather-card">      <div class="weather-label">📍 Dryden, NY — 13068</div>      <div class="weather-location">Weather checked at every 2-week check-in</div>      <div class="weather-note">Snow forecast = run day moved. No heroics in heavy snow. Pull up weather at each check-in to adjust the upcoming block.</div>    </div>    <div id="fullSchedule"></div>  </div>  <!-- CHECKIN VIEW -->  <div class="section" id="section-checkin">    <div id="checkinList"></div>  </div></div><!-- LOG MODAL --><div class="modal-overlay" id="logModal">  <div class="modal">    <div class="modal-handle"></div>    <div class="modal-title" id="modalTitle">Log Run</div>    <div class="modal-subtitle" id="modalSubtitle"></div>    <div class="form-group">      <label class="form-label">Upload GPX / FIT File</label>      <label class="gpx-upload-btn" id="gpxUploadBtn" for="gpxFile">        <span id="gpxBtnText">📁 Upload Suunto export to auto-fill</span>        <input type="file" id="gpxFile" accept=".gpx,.fit" style="display:none" onchange="parseActivityFile(this, 1)">      </label>      <div class="gpx-status" id="gpxStatus"></div>    </div>    <div class="form-group">      <label class="form-label">Distance & Time</label>      <div class="form-row">        <input type="number" id="inputDist" placeholder="Miles" step="0.1">        <input type="text" id="inputTime" placeholder="H:MM e.g. 2:30">      </div>    </div>    <div class="form-group">      <label class="form-label">Effort (RPE)</label>      <input type="range" class="rpe-slider" id="rpeSlider" min="1" max="10" value="5" oninput="updateRPE(this.value)">      <div class="rpe-labels"><span>Easy</span><span>Moderate</span><span>Max</span></div>      <div class="rpe-value" id="rpeVal">5</div>    </div>    <div class="form-group">      <label class="form-label">Knee Feel</label>      <div class="knee-options">        <div class="knee-opt selected" data-val="great" onclick="selectKnee(this)">👍 Great</div>        <div class="knee-opt" data-val="fine" onclick="selectKnee(this)">👌 Fine</div>        <div class="knee-opt warn" data-val="grumpy" onclick="selectKnee(this)">⚠️ Grumpy</div>      </div>    </div>    <div class="form-group">      <label class="form-label">Notes — how did it feel?</label>      <textarea id="inputNote" placeholder="Legs felt good on the climb, HR stayed controlled, ate well at mile 10..."></textarea>    </div>    <button class="add-session-btn" id="addSession2Btn" onclick="toggleSession2()">+ Add 2nd session</button>    <div class="session2-block" id="session2Block">      <div class="session-divider">        <div class="session-divider-line"></div>        <div class="session-divider-label">Session 2</div>        <div class="session-divider-line"></div>      </div>      <div class="form-group">        <label class="form-label">Upload GPX / FIT File</label>        <label class="gpx-upload-btn" for="gpxFile2">          <span id="gpxBtnText2">📁 Upload Suunto export to auto-fill</span>          <input type="file" id="gpxFile2" accept=".gpx,.fit" style="display:none" onchange="parseActivityFile(this, 2)">        </label>        <div class="gpx-status" id="gpxStatus2"></div>      </div>      <div class="form-group">        <label class="form-label">Distance & Time</label>        <div class="form-row">          <input type="number" id="inputDist2" placeholder="Miles" step="0.1">          <input type="text" id="inputTime2" placeholder="H:MM e.g. 0:45">        </div>      </div>      <div class="form-group">        <label class="form-label">Effort (RPE)</label>        <input type="range" class="rpe-slider" id="rpeSlider2" min="1" max="10" value="5" oninput="updateRPE2(this.value)">        <div class="rpe-labels"><span>Easy</span><span>Moderate</span><span>Max</span></div>        <div class="rpe-value" id="rpeVal2">5</div>      </div>      <div class="form-group">        <label class="form-label">Notes</label>        <textarea id="inputNote2" placeholder="Second session notes..."></textarea>      </div>    </div>    <button class="btn-primary" onclick="saveLog()">Save ✓</button>    <button class="btn-ghost" onclick="closeModal()">Cancel</button>  </div></div><div class="toast" id="toast"></div><script>// ── DATA ──────────────────────────────────────────────────────const RACES = [  { name: "Laurel 70", date: "2026-06-13", goal: "15-16 hrs", aRace: false },  { name: "Escarpment", date: "2026-07-26", goal: "Enjoy it", aRace: false },  { name: "Dark Sky 220", date: "2026-10-25", goal: "A-RACE", aRace: true },];const CHECKINS = [  { label: "Block 1 Review", date: "2026-03-07" },  { label: "Block 2 Review", date: "2026-03-21" },  { label: "Block 3 Review", date: "2026-04-04" },  { label: "Block 4 Review", date: "2026-04-18" },  { label: "Block 5 Review", date: "2026-05-02" },  { label: "Block 6 Review", date: "2026-05-16" },  { label: "Block 7 / Laurel Taper", date: "2026-05-30" },  { label: "Post-Laurel Recovery", date: "2026-06-27" },  { label: "Block 9 Review", date: "2026-07-11" },  { label: "Escarpment Week", date: "2026-07-26" },  { label: "Block 11 Review", date: "2026-08-09" },  { label: "Block 12 Review", date: "2026-08-23" },  { label: "Peak Build I", date: "2026-09-06" },  { label: "Peak Build II", date: "2026-09-20" },  { label: "Cut-back Review", date: "2026-10-04" },  { label: "Dark Sky Race Prep 🏁", date: "2026-10-18" },];const PHASES = [  { name: "Phase 1: Base & Build", desc: "Aerobic foundation, trail volume, strength", color: "#1a7a7a" },  { name: "Phase 2: Laurel Prep", desc: "Race-specific training & taper", color: "#2a7a4a" },  { name: "Phase 3: Recovery & Sharpen", desc: "Rebuild after Laurel, Escarpment prep", color: "#7a4a1a" },  { name: "Phase 4: Dark Sky 220 Build", desc: "Peak volume, night running, back-to-backs", color: "#1a1a7a" },];const BLOCKS = [  // Phase 1  { num: 1, label: "Baseline", dates: "Feb 21 – Mar 6", mileage: "45-50", phase: 0,    week1: ["Easy 6-8mi", "Strength A", "Mod 8-10mi + strides", "Easy 5mi + Yoga", "Dog Walk", "Long 14-16mi", "Dog Walk + Mobility"],    week2: ["Easy 6mi", "Strength A", "Tempo intervals 7mi", "Easy 5mi + Yoga", "Dog Walk", "Long 16-18mi", "Dog Walk + Mobility"] },  { num: 2, label: "Build — Climbing", dates: "Mar 7 – Mar 20", mileage: "48-54", phase: 0,    week1: ["Easy 7mi", "Strength A", "Hill repeats 8mi", "Easy 6mi + Yoga", "Dog Walk", "Long trail 18-20mi", "Dog Walk"],    week2: ["Easy 7mi", "Strength B", "Moderate 9mi", "Easy 5mi + Yoga", "Strides 5mi", "Long 20-22mi", "Dog Walk"] },  { num: 3, label: "Back-to-Backs", dates: "Mar 21 – Apr 3", mileage: "52-58", phase: 0,    week1: ["Easy 7mi", "Strength A", "Moderate 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 22-24mi trail", "Moderate 12mi (B2B)"],    week2: ["Easy 7mi", "Strength B", "Hill work 9mi", "Easy 6mi + Yoga", "Dog Walk", "Long 24-26mi", "Moderate 12mi (B2B)"] },  { num: 4, label: "Peak Base", dates: "Apr 4 – Apr 17", mileage: "55-60", phase: 0,    week1: ["Easy 7mi", "Strength A", "Mod 10mi + surges", "Easy 6mi + Yoga", "Easy 5mi", "Long 26-28mi", "Moderate 14mi (B2B)"],    week2: ["Easy 8mi", "Strength B", "Tempo 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 28-30mi", "Easy 12mi (B2B)"] },  { num: 5, label: "Cut-back & Sharpen", dates: "Apr 18 – May 2", mileage: "52-55", phase: 0,    week1: ["Easy 6mi", "Strength A", "Moderate 9mi", "Easy 5mi + Yoga", "Dog Walk", "Long 22-24mi", "Easy 10mi"],    week2: ["Easy 6mi", "Strength A", "Race-pace 8mi", "Easy 5mi + Yoga", "Easy 4mi", "Long 18-20mi", "Dog Walk + Yoga"] },  // Phase 2  { num: 6, label: "Laurel Volume", dates: "May 3 – May 16", mileage: "55-58", phase: 1,    week1: ["Easy 7mi", "Strength A", "Long tempo 12mi", "Easy 6mi + Yoga", "Dog Walk", "Long 28-32mi trail", "Moderate 14mi (B2B)"],    week2: ["Easy 7mi", "Strength A", "Pace work 10mi", "Easy 5mi + Yoga", "Easy 5mi", "Long 24-26mi", "Easy 10mi (B2B)"] },  { num: 7, label: "Taper Begins", dates: "May 17 – May 30", mileage: "48-52", phase: 1,    week1: ["Easy 6mi", "Strength (light)", "Moderate 10mi", "Easy 5mi + Yoga", "Dog Walk", "Long 20-22mi", "Easy 8mi"],    week2: ["Easy 5mi", "Mobility only", "Easy 8mi + pace", "Easy 4mi + Yoga", "Dog Walk", "Long 14-16mi", "Easy 6mi"] },  { num: 8, label: "Race Week — Laurel 🏁", dates: "May 31 – Jun 13", mileage: "30-35", phase: 1,    week1: ["Easy 5mi", "Easy 4mi + strides", "Mobility + 3mi", "Rest / Dog Walk", "Easy 3mi shakeout", "Dog Walk", "Rest & Prep"],    week2: ["LAUREL 70", "Jun 13 🏁", "15-16 hr goal", "Rest/recover", "", "", ""] },  // Phase 3  { num: 9, label: "Post-Laurel Recovery", dates: "Jun 14 – Jun 27", mileage: "25-35", phase: 2,    week1: ["Dog Walk", "Yoga only", "Dog Walk", "Easy Mobility", "Dog Walk", "Easy Walk", "Rest"],    week2: ["Easy run 30min", "Mobility", "Easy run 35min", "Easy 4mi + Yoga", "Dog Walk", "Easy trail 6mi", "Easy Walk"] },  { num: 10, label: "Rebuild + Technical", dates: "Jun 28 – Jul 11", mileage: "38-45", phase: 2,    week1: ["Easy 6mi", "Strength A", "Technical trail 8mi", "Easy 5mi + Yoga", "Dog Walk", "Long hilly 16-18mi", "Easy 8mi"],    week2: ["Easy 6mi", "Strength A", "Hill repeats 8mi", "Easy 5mi + Yoga", "Dog Walk", "Long trail 18-20mi", "Easy 8mi"] },  { num: 11, label: "Escarpment Prep 🏁", dates: "Jul 12 – Jul 26", mileage: "35-40", phase: 2,    week1: ["Easy 6mi", "Strength (light)", "Technical climb 8mi", "Easy 5mi + Yoga", "Dog Walk", "Hilly long 16mi", "Easy 6mi"],    week2: ["Easy 4mi", "Easy Mobility", "Easy 4mi shakeout", "Dog Walk", "Travel/Rest", "Easy 2mi", "ESCARPMENT 🏁"] },  // Phase 4  { num: 12, label: "DS220 Rebuild", dates: "Jul 27 – Aug 9", mileage: "45-52", phase: 3,    week1: ["Easy 6mi", "Strength A", "Easy-mod 9mi", "Easy 6mi + Yoga", "Dog Walk", "Long 20-22mi", "Easy 10mi"],    week2: ["Easy 7mi", "Strength B", "Trail 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 22-24mi", "Easy 10mi (B2B)"] },  { num: 13, label: "Volume Rising", dates: "Aug 10 – Aug 23", mileage: "52-58", phase: 3,    week1: ["Easy 7mi", "Strength A", "Trail 10mi", "Easy 7mi + Yoga", "Dog Walk", "Long 26-28mi", "Moderate 14mi (B2B)"],    week2: ["Easy 7mi", "Strength B", "Hill + surges 10mi", "Easy 6mi + Yoga", "Easy 5mi", "Long 28-32mi", "Moderate 15mi (B2B)"] },  { num: 14, label: "Peak Build I", dates: "Aug 24 – Sep 6", mileage: "55-62", phase: 3,    week1: ["Easy 7mi", "Strength A", "Technical trail 10mi", "Easy 7mi + Yoga", "Dog Walk", "Long 30-35mi", "Moderate 16mi (B2B)"],    week2: ["Easy 8mi", "Strength B", "Moderate 11mi", "Easy 7mi + Yoga", "Easy 5mi", "Long 32-36mi", "Moderate 16mi (B2B)"] },  { num: 15, label: "Peak Build II — Biggest", dates: "Sep 7 – Sep 20", mileage: "58-65", phase: 3,    week1: ["Easy 8mi", "Strength A", "Trail 12mi", "Easy 7mi + Yoga", "Dog Walk", "Long 35-40mi", "Long 20mi (B2B)"],    week2: ["Easy 8mi", "Strength B", "Pace work 10mi", "Easy 7mi + Yoga", "Easy 5mi", "Long 38-42mi", "Moderate 18mi (B2B)"] },  { num: 16, label: "Cut-back & Sharpen", dates: "Sep 21 – Oct 4", mileage: "48-54", phase: 3,    week1: ["Easy 7mi", "Strength (light)", "Moderate trail 10mi", "Easy 6mi + Yoga", "Dog Walk", "Long 26-28mi", "Easy 12mi"],    week2: ["Easy 7mi", "Strength (light)", "Trail 9mi", "Easy 5mi + Yoga", "Dog Walk", "Long 22-24mi", "Easy 10mi"] },  { num: 17, label: "Dark Sky Taper 🏁", dates: "Oct 5 – Oct 25", mileage: "35-42", phase: 3,    week1: ["Easy 6mi", "Strength (light)", "Easy 8mi trail", "Easy 5mi + Yoga", "Dog Walk", "Long 18-20mi", "Easy 8mi"],    week2: ["Easy 5mi", "Mobility only", "Easy 6mi + strides", "Easy 4mi + Yoga", "Dog Walk", "Easy 10mi", "DARK SKY 220 🏁"] },];const DAY_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];// ── STATE ─────────────────────────────────────────────────────let currentBlock = 0;let runLogs = [];let modalDay = null;let kneeVal = "great";// Storage — uses Claude's cross-device storage when available, falls back to localStorageasync function loadLogs() {  try {    if (window.storage) {      const r = await window.storage.get('vinny-run-logs');      if (r) { runLogs = JSON.parse(r.value); return; }    }  } catch(e) {}  try {    const saved = localStorage.getItem('vinny-run-logs');    if (saved) runLogs = JSON.parse(saved);  } catch(e) { runLogs = []; }}async function saveLogs() {  const data = JSON.stringify(runLogs);  try {    if (window.storage) await window.storage.set('vinny-run-logs', data);  } catch(e) {}  try { localStorage.setItem('vinny-run-logs', data); } catch(e) {}}// ── INIT ──────────────────────────────────────────────────────async function init() {  await loadLogs();  document.getElementById('todayDate').textContent = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });  renderRaceChips();  findCurrentBlock();  renderCheckins();  renderFullSchedule();}function findCurrentBlock() {  const today = new Date();  // Default to first block, but try to find the right one based on date  // Block 1 starts Feb 21 2026  const blockStart = new Date('2026-02-21');  const diffDays = Math.floor((today - blockStart) / (1000 * 60 * 60 * 24));  if (diffDays >= 0) {    const blockIdx = Math.min(Math.floor(diffDays / 14), BLOCKS.length - 1);    currentBlock = Math.max(0, blockIdx);  }  renderBlock(currentBlock);}// ── RACE CHIPS ────────────────────────────────────────────────function renderRaceChips() {  const today = new Date();  const container = document.getElementById('raceChips');  container.innerHTML = RACES.map(r => {    const raceDate = new Date(r.date);    const days = Math.ceil((raceDate - today) / (1000 * 60 * 60 * 24));    return `<div class="race-chip ${r.aRace ? 'a-race' : ''}">      <span class="race-chip-name">${r.name}</span>      <span class="race-chip-date">${formatDate(r.date)}</span>      <span class="race-chip-days">${days > 0 ? days + 'd away' : 'Race day!'}</span>    </div>`;  }).join('');}// ── BLOCK RENDER ──────────────────────────────────────────────function renderBlock(idx) {  const b = BLOCKS[idx];  const phase = PHASES[b.phase];  document.getElementById('blockLabel').textContent = `Block ${b.num} of ${BLOCKS.length}`;  document.getElementById('blockTitle').textContent = b.label;  document.getElementById('blockDates').textContent = b.dates + ' · ~' + b.mileage + ' mi/wk';  document.getElementById('phaseName').textContent = phase.name;  document.getElementById('phaseDesc').textContent = phase.desc;  // Headers  // Calculate block start date — Block 1 starts Feb 21 2026, each block is 14 days  const blockStartDate = new Date('2026-02-21T00:00:00');  blockStartDate.setDate(blockStartDate.getDate() + (idx * 14));  // Remove static day headers — dates are now shown on cards  ['dayHeaders1', 'dayHeaders2'].forEach(id => {    document.getElementById(id).innerHTML = '';  });  renderWeek('week1Grid', b.week1, b.num, 1, blockStartDate, 0);  renderWeek('week2Grid', b.week2, b.num, 2, blockStartDate, 7);}const FULL_DAY_NAMES = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];function renderWeek(gridId, days, blockNum, weekNum, blockStartDate, dayOffset) {  const grid = document.getElementById(gridId);  grid.innerHTML = days.map((desc, i) => {    const dayKey = `b${blockNum}w${weekNum}d${i}`;    const log = runLogs.find(l => l.dayKey === dayKey);    const isRest = desc.toLowerCase().includes('dog walk') || desc.toLowerCase().includes('rest') || desc.toLowerCase().includes('yoga only') || desc.toLowerCase().includes('mobility only');    // Calculate actual date for this slot    const d = new Date(blockStartDate);    d.setDate(d.getDate() + dayOffset + i);    const dayName = FULL_DAY_NAMES[d.getDay()];    const dateLabel = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });    const today = new Date();    const isToday = d.toDateString() === today.toDateString();    let cls = 'day-card';    if (log) cls += ' logged';    else if (isRest) cls += ' rest';    if (isToday) cls += ' today';    return `<div class="${cls}" onclick="openModal('${dayKey}', '${desc.replace(/'/g, "\\'")}', ${blockNum}, ${weekNum}, ${i}, '${dateLabel}')">      <span class="day-num">${dayName}</span>      <span class="day-num" style="font-size:9px;color:var(--teal);margin-top:-1px">${dateLabel}</span>      <span class="day-type">${desc}</span>      ${log ? `<span class="day-check">✓</span><span class="day-rpe">RPE ${log.rpe}</span>` : ''}    </div>`;  }).join('');}function prevBlock() {  if (currentBlock > 0) { currentBlock--; renderBlock(currentBlock); }}function nextBlock() {  if (currentBlock < BLOCKS.length - 1) { currentBlock++; renderBlock(currentBlock); }}// ── MODAL ─────────────────────────────────────────────────────function openModal(dayKey, desc, blockNum, weekNum, dayIdx, dateLabel) {  modalDay = { dayKey, desc };  document.getElementById('modalTitle').textContent = 'Log: ' + desc;  document.getElementById('modalSubtitle').textContent = `Block ${blockNum} · Week ${weekNum} · ${DAY_NAMES[dayIdx]}${dateLabel ? ' · ' + dateLabel : ''}`;  // Reset session 2  document.getElementById('session2Block').classList.remove('open');  document.getElementById('addSession2Btn').textContent = '+ Add 2nd session';  document.getElementById('inputDist2').value = '';  document.getElementById('inputTime2').value = '';  document.getElementById('rpeSlider2').value = 5;  document.getElementById('rpeVal2').textContent = 5;  document.getElementById('inputNote2').value = '';  document.getElementById('inputSuunto2').value = '';  // Pre-fill if existing  const existing = runLogs.find(l => l.dayKey === dayKey);  if (existing) {    document.getElementById('inputDist').value = existing.dist || '';    document.getElementById('inputTime').value = existing.time || '';    document.getElementById('rpeSlider').value = existing.rpe || 5;    document.getElementById('rpeVal').textContent = existing.rpe || 5;    document.getElementById('inputNote').value = existing.note || '';    kneeVal = existing.knee || 'great';    if (existing.session2) {      document.getElementById('session2Block').classList.add('open');      document.getElementById('addSession2Btn').textContent = '− Remove 2nd session';      document.getElementById('inputDist2').value = existing.session2.dist || '';      document.getElementById('inputTime2').value = existing.session2.time || '';      document.getElementById('rpeSlider2').value = existing.session2.rpe || 5;      document.getElementById('rpeVal2').textContent = existing.session2.rpe || 5;      document.getElementById('inputNote2').value = existing.session2.note || '';    }  } else {    document.getElementById('inputDist').value = '';    document.getElementById('inputTime').value = '';    document.getElementById('rpeSlider').value = 5;    document.getElementById('rpeVal').textContent = 5;    document.getElementById('inputNote').value = '';    kneeVal = 'great';  }  document.querySelectorAll('.knee-opt').forEach(el => {    el.classList.toggle('selected', el.dataset.val === kneeVal);  });  document.getElementById('logModal').classList.add('open');}function closeModal() {  document.getElementById('logModal').classList.remove('open');}// ── GPX / FIT PARSING ─────────────────────────────────────────function parseActivityFile(input, session) {  const file = input.files[0];  if (!file) return;  const suffix = session === 1 ? '' : '2';  const statusEl = document.getElementById('gpxStatus' + suffix);  const btnText = document.getElementById('gpxBtnText' + suffix);  statusEl.className = 'gpx-status';  statusEl.textContent = 'Parsing...';  const reader = new FileReader();  reader.onload = function(e) {    try {      const ext = file.name.split('.').pop().toLowerCase();      let distMiles = null, durationSecs = null;      if (ext === 'gpx') {        const parser = new DOMParser();        const xml = parser.parseFromString(e.target.result, 'text/xml');        const trkpts = xml.querySelectorAll('trkpt');        if (trkpts.length < 2) throw new Error('Not enough track points');        // Calculate distance from lat/lon points        let totalMeters = 0;        let pts = Array.from(trkpts).map(p => ({          lat: parseFloat(p.getAttribute('lat')),          lon: parseFloat(p.getAttribute('lon')),          time: p.querySelector('time') ? new Date(p.querySelector('time').textContent) : null        }));        for (let i = 1; i < pts.length; i++) {          totalMeters += haversine(pts[i-1].lat, pts[i-1].lon, pts[i].lat, pts[i].lon);        }        distMiles = totalMeters / 1609.344;        // Duration from first to last timestamp        if (pts[0].time && pts[pts.length-1].time) {          durationSecs = (pts[pts.length-1].time - pts[0].time) / 1000;        }      } else if (ext === 'fit') {        // Parse FIT binary — look for key record messages        const buf = new Uint8Array(e.target.result);        const result = parseFitDistance(buf);        if (result) { distMiles = result.distMiles; durationSecs = result.durationSecs; }      }      if (distMiles !== null) {        document.getElementById('inputDist' + suffix).value = distMiles.toFixed(2);        btnText.textContent = '✓ ' + file.name;      }      if (durationSecs !== null) {        const h = Math.floor(durationSecs / 3600);        const m = Math.floor((durationSecs % 3600) / 60);        const timeStr = h > 0 ? `${h}:${String(m).padStart(2,'0')}` : `0:${String(m).padStart(2,'0')}`;        document.getElementById('inputTime' + suffix).value = timeStr;      }      statusEl.textContent = distMiles ? `✓ ${distMiles.toFixed(2)} mi${durationSecs ? ' · ' + formatDuration(durationSecs) : ''}` : '✓ File loaded — check fields';    } catch(err) {      statusEl.className = 'gpx-status error';      statusEl.textContent = 'Could not parse file — fill in manually';      console.warn('GPX parse error:', err);    }  };  if (file.name.endsWith('.fit')) reader.readAsArrayBuffer(file);  else reader.readAsText(file);}function haversine(lat1, lon1, lat2, lon2) {  const R = 6371000;  const dLat = (lat2 - lat1) * Math.PI / 180;  const dLon = (lon2 - lon1) * Math.PI / 180;  const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2;  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));}function parseFitDistance(buf) {  // FIT files store total distance in session/lap messages  // Look for session message (type 18) which has total_distance field  try {    // Skip 14-byte header    let offset = 14;    let localMsgDefs = {};    let distMeters = null, durationSecs = null;    while (offset < buf.length - 2) {      const recHdr = buf[offset];      const isDefn = (recHdr & 0x40) !== 0;      const localNum = recHdr & 0x0F;      if (isDefn) {        // Definition message        offset++; // skip header        offset++; // reserved        const arch = buf[offset++]; // architecture (0=little, 1=big)        const globalMsgNum = arch === 0 ? (buf[offset] | buf[offset+1]<<8) : (buf[offset]<<8 | buf[offset+1]);        offset += 2;        const numFields = buf[offset++];        const fields = [];        for (let i = 0; i < numFields; i++) {          fields.push({ defNum: buf[offset], size: buf[offset+1], type: buf[offset+2] });          offset += 3;        }        localMsgDefs[localNum] = { globalMsgNum, arch, fields };      } else {        // Data message        const def = localMsgDefs[localNum];        if (!def) { offset++; continue; }        offset++; // skip header        const msgStart = offset;        let msgSize = def.fields.reduce((s, f) => s + f.size, 0);        // session message = global 18, total_distance = field def 9, timer_time = field def 7        if (def.globalMsgNum === 18) {          let fieldOffset = msgStart;          for (const f of def.fields) {            if (f.defNum === 9 && f.size === 4) {              const val = def.arch === 0                ? (buf[fieldOffset] | buf[fieldOffset+1]<<8 | buf[fieldOffset+2]<<16 | buf[fieldOffset+3]<<24)                : (buf[fieldOffset]<<24 | buf[fieldOffset+1]<<16 | buf[fieldOffset+2]<<8 | buf[fieldOffset+3]);              distMeters = val / 100;            }            if (f.defNum === 7 && f.size === 4) {              const val = def.arch === 0                ? (buf[fieldOffset] | buf[fieldOffset+1]<<8 | buf[fieldOffset+2]<<16 | buf[fieldOffset+3]<<24)                : (buf[fieldOffset]<<24 | buf[fieldOffset+1]<<16 | buf[fieldOffset+2]<<8 | buf[fieldOffset+3]);              durationSecs = val / 1000;            }            fieldOffset += f.size;          }        }        offset += msgSize;      }    }    if (distMeters) return { distMiles: distMeters / 1609.344, durationSecs };    return null;  } catch(e) { return null; }}function formatDuration(secs) {  const h = Math.floor(secs / 3600);  const m = Math.floor((secs % 3600) / 60);  return h > 0 ? `${h}h ${m}m` : `${m}m`;}function updateRPE(val) {  document.getElementById('rpeVal').textContent = val;}function updateRPE2(val) {  document.getElementById('rpeVal2').textContent = val;}function toggleSession2() {  const block = document.getElementById('session2Block');  const btn = document.getElementById('addSession2Btn');  const isOpen = block.classList.toggle('open');  btn.textContent = isOpen ? '− Remove 2nd session' : '+ Add 2nd session';}function selectKnee(el) {  kneeVal = el.dataset.val;  document.querySelectorAll('.knee-opt').forEach(o => o.classList.remove('selected'));  el.classList.add('selected');}async function saveLog() {  if (!modalDay) return;  const session2Open = document.getElementById('session2Block').classList.contains('open');  const entry = {    dayKey: modalDay.dayKey,    desc: modalDay.desc,    dist: document.getElementById('inputDist').value,    time: document.getElementById('inputTime').value,    rpe: parseInt(document.getElementById('rpeSlider').value),    knee: kneeVal,    note: document.getElementById('inputNote').value,    loggedAt: new Date().toISOString(),    session2: session2Open ? {      dist: document.getElementById('inputDist2').value,      time: document.getElementById('inputTime2').value,      rpe: parseInt(document.getElementById('rpeSlider2').value),      note: document.getElementById('inputNote2').value,    } : null,  };  const idx = runLogs.findIndex(l => l.dayKey === entry.dayKey);  if (idx >= 0) runLogs[idx] = entry;  else runLogs.push(entry);  saveLogs();  // fire and forget — async but we don't need to wait  closeModal();  renderBlock(currentBlock);  renderLogList();  renderStats();  showToast('Run logged ✓');}// ── LOG LIST ──────────────────────────────────────────────────function renderLogList() {  const list = document.getElementById('logList');  if (!runLogs.length) {    list.innerHTML = `<div class="empty-state">      <div class="empty-state-icon">🏃</div>      <div class="empty-state-text">No runs logged yet</div>      <div class="empty-state-sub">Tap any day in the Block view to log a run</div>    </div>`;    return;  }  const sorted = [...runLogs].sort((a, b) => b.loggedAt.localeCompare(a.loggedAt));  list.innerHTML = sorted.map(l => {    const totalDist = (parseFloat(l.dist)||0) + (l.session2 ? (parseFloat(l.session2.dist)||0) : 0);    return `    <div class="log-entry ${l.knee === 'grumpy' ? 'flagged' : ''}">      <div class="log-entry-header">        <div>          <div class="log-entry-date">${new Date(l.loggedAt).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}</div>          <div class="log-entry-title">${l.desc}${l.session2 ? ' <span style="font-size:11px;color:var(--teal);font-family:\'DM Mono\',monospace">×2</span>' : ''}</div>        </div>      </div>      <div class="log-entry-stats">        ${totalDist ? `<span class="stat-chip">${totalDist.toFixed(1)} mi total</span>` : ''}        ${l.time ? `<span class="stat-chip">S1: ${l.time}</span>` : ''}        ${l.rpe ? `<span class="stat-chip">RPE ${l.rpe}</span>` : ''}        ${l.session2 && l.session2.time ? `<span class="stat-chip">S2: ${l.session2.time}</span>` : ''}        ${l.session2 && l.session2.rpe ? `<span class="stat-chip">RPE ${l.session2.rpe}</span>` : ''}        ${l.knee === 'grumpy' ? `<span class="stat-chip" style="background:#fdf0ea;color:#e8622a">⚠️ Knee</span>` : ''}        ${l.suunto ? `<span class="stat-chip" style="cursor:pointer" onclick="window.open('${l.suunto}','_blank')">📡 S1</span>` : ''}        ${l.session2 && l.session2.suunto ? `<span class="stat-chip" style="cursor:pointer" onclick="window.open('${l.session2.suunto}','_blank')">📡 S2</span>` : ''}      </div>      ${l.note ? `<div class="log-note">"${l.note}"</div>` : ''}      ${l.session2 && l.session2.note ? `<div class="log-note" style="margin-top:4px;border-left:2px solid var(--teal-light);padding-left:8px">"${l.session2.note}"</div>` : ''}    </div>  `}).join('');}function renderStats() {  const totalMiles = runLogs.reduce((s, l) => s + (parseFloat(l.dist) || 0) + (l.session2 ? (parseFloat(l.session2.dist)||0) : 0), 0);  const totalRuns = runLogs.filter(l => l.dist).length;  const avgRPE = runLogs.length ? (runLogs.reduce((s, l) => s + l.rpe, 0) / runLogs.length).toFixed(1) : '—';  const kneeFlags = runLogs.filter(l => l.knee === 'grumpy').length;  document.getElementById('statsGrid').innerHTML = `    <div class="stat-card">      <span class="stat-card-value">${totalMiles.toFixed(0)}</span>      <span class="stat-card-label">Miles Logged</span>    </div>    <div class="stat-card">      <span class="stat-card-value">${totalRuns}</span>      <span class="stat-card-label">Runs Logged</span>    </div>    <div class="stat-card">      <span class="stat-card-value">${avgRPE}</span>      <span class="stat-card-label">Avg RPE</span>    </div>    <div class="stat-card">      <span class="stat-card-value" style="color:${kneeFlags > 0 ? '#e8622a' : '#2a7a4a'}">${kneeFlags}</span>      <span class="stat-card-label">Knee Flags</span>    </div>  `;}// ── SCHEDULE ──────────────────────────────────────────────────function renderFullSchedule() {  const container = document.getElementById('fullSchedule');  container.innerHTML = BLOCKS.map((b, i) => {    const phase = PHASES[b.phase];    return `<div style="margin-bottom:16px">      <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">        <div style="width:8px;height:8px;border-radius:50%;background:var(--teal);flex-shrink:0"></div>        <div style="font-size:10px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--gray);font-family:'DM Mono',monospace">${b.dates}</div>      </div>      <div class="checkin-card" style="cursor:pointer" onclick="switchToBlock(${i})">        <div class="checkin-info">          <div class="checkin-title">Block ${b.num}: ${b.label}</div>          <div class="checkin-date">${phase.name} · ~${b.mileage} mi/wk</div>        </div>        <div style="font-size:18px">›</div>      </div>    </div>`;  }).join('');}function switchToBlock(idx) {  currentBlock = idx;  renderBlock(currentBlock);  switchTab('block');}// ── CHECKINS ──────────────────────────────────────────────────function renderCheckins() {  const today = new Date();  let nextFound = false;  document.getElementById('checkinList').innerHTML = CHECKINS.map(c => {    const d = new Date(c.date);    const isPast = d < today;    const isNext = !isPast && !nextFound;    if (isNext) nextFound = true;    return `<div class="checkin-card">      <div class="checkin-dot ${isNext ? 'next' : isPast ? 'done' : ''}"></div>      <div class="checkin-info">        <div class="checkin-title">${c.label}</div>        <div class="checkin-date">${formatDate(c.date)}</div>      </div>      <div class="checkin-status">${isPast ? '✓' : isNext ? 'NEXT' : ''}</div>    </div>`;  }).join('');}// ── TABS ──────────────────────────────────────────────────────function switchTab(tab) {  document.querySelectorAll('.tab').forEach((t, i) => {    const tabs = ['block', 'log', 'schedule', 'checkin'];    t.classList.toggle('active', tabs[i] === tab);  });  document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));  document.getElementById('section-' + tab).classList.add('active');  if (tab === 'log') { renderLogList(); renderStats(); }}// ── UTILS ─────────────────────────────────────────────────────function formatDate(dateStr) {  return new Date(dateStr + 'T12:00:00').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });}function showToast(msg) {  const t = document.getElementById('toast');  t.textContent = msg;  t.classList.add('show');  setTimeout(() => t.classList.remove('show'), 2200);}// Close modal on overlay clickdocument.getElementById('logModal').addEventListener('click', function(e) {  if (e.target === this) closeModal();});init().catch(console.error);</script></body></html>