// Turbocoach - Live React app (beta)
// Built by scripts/build_react_app.py from design-src/
// ----------------------------------------------------
const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* === API HELPER === */
const STORAGE_TOKEN = 'tc_token';

function getAuthToken() {
  try { return localStorage.getItem(STORAGE_TOKEN); } catch(e) { return null; }
}
function setAuthToken(t) {
  try {
    if (t) localStorage.setItem(STORAGE_TOKEN, t);
    else localStorage.removeItem(STORAGE_TOKEN);
  } catch(e) {}
}

async function api(path, opts = {}) {
  const headers = Object.assign({}, opts.headers || {});
  if (opts.body && !(opts.body instanceof FormData)) {
    headers['Content-Type'] = 'application/json';
    if (typeof opts.body !== 'string') opts.body = JSON.stringify(opts.body);
  }
  const tok = getAuthToken();
  if (tok) headers['Authorization'] = 'Bearer ' + tok;
  const res = await fetch(path, { ...opts, headers, credentials: 'include' });
  const ct = res.headers.get('content-type') || '';
  const body = ct.includes('json') ? await res.json().catch(() => ({})) : await res.text();
  if (!res.ok) {
    const err = new Error((body && body.detail) || ('HTTP ' + res.status));
    err.status = res.status; err.body = body;
    throw err;
  }
  return body;
}

/* === DATA TRANSFORMERS === */
function mapActivity(a) {
  const type =
    a.activity_type === 'cycling' ? 'bike' :
    a.activity_type === 'swimming' ? 'swim' : 'run';
  // a.avg_pace zit ALTIJD in m/s (Garmin native). Converteer per sport-type.
  const ms = Number(a.avg_pace) || 0;
  const dist = a.distance ? (type === 'swim' ? Math.round(a.distance * 1000) : Number(a.distance).toFixed(1)) : '';
  const dur = a.duration ? fmtDuration(a.duration) : '';
  let pace = '';
  let speed = '';
  if (ms > 0) {
    if (type === 'run') {
      pace = fmtPace(1000 / (ms * 60));          // min/km
    } else if (type === 'swim') {
      pace = fmtPace(100 / (ms * 60));           // min/100m
    } else if (type === 'bike') {
      speed = (ms * 3.6).toFixed(1);             // km/u
    }
  }
  return {
    id: a.id, type,
    name: a.activity_name || (type === 'run' ? 'Loop' : type === 'bike' ? 'Rit' : 'Zwem'),
    date: fmtRelativeDate(a.start_time),
    startTime: a.start_time,
    dist, pace, speed, dur,
    hr: a.avg_hr || 0,
    elev: Math.round(a.elevation_gain || 0),
    cal: a.calories || 0,
    cad: a.avg_cadence ? Math.round(Number(a.avg_cadence)) : 0,
    raw: a,
  };
}

function fmtDuration(seconds) {
  if (!seconds) return '';
  const s = Math.round(seconds);
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const ss = s % 60;
  if (h > 0) return `${h}:${String(m).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
  return `${m}:${String(ss).padStart(2,'0')}`;
}

function fmtPace(minPerKm) {
  if (!minPerKm) return '';
  const m = Math.floor(minPerKm);
  const s = Math.round((minPerKm - m) * 60);
  return `${m}:${String(s).padStart(2,'0')}`;
}

function fmtRelativeDate(iso) {
  if (!iso) return '';
  const d = new Date(iso);
  const today = new Date();
  const yest = new Date(); yest.setDate(today.getDate() - 1);
  const sameDay = (a, b) => a.toDateString() === b.toDateString();
  const hm = `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
  if (sameDay(d, today)) return `vandaag - ${hm}`;
  if (sameDay(d, yest))  return `gisteren - ${hm}`;
  const dayNames = ['zo','ma','di','wo','do','vr','za'];
  const daysDiff = Math.floor((today - d) / 86400000);
  if (daysDiff < 7) return `${dayNames[d.getDay()]} - ${hm}`;
  return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')} - ${hm}`;
}

function formatHours(h) {
  if (!h || h <= 0) return '0u';
  const hh = Math.floor(h);
  const mm = Math.round((h - hh) * 60);
  if (hh === 0) return mm + 'm';
  if (mm === 0) return hh + 'u';
  return hh + 'u ' + mm + 'm';
}

function buildWeekStrip(activities) {
  const today = new Date();
  const dayKeys = ['zo','ma','di','wo','do','vr','za'];
  const dayLabels = ['zondag','maandag','dinsdag','woensdag','donderdag','vrijdag','zaterdag'];
  const mo = ['jan','feb','mrt','apr','mei','jun','jul','aug','sep','okt','nov','dec'];
  const out = [];
  // Laatste 7 dagen (rolling): van 6 dagen geleden tot en met vandaag
  for (let i = 6; i >= 0; i--) {
    const d = new Date(today); d.setDate(today.getDate() - i); d.setHours(0,0,0,0);
    const dayEnd = new Date(d); dayEnd.setHours(23,59,59,999);
    const sessions = activities.filter(a => {
      if (!a.startTime) return false;
      const dt = new Date(a.startTime);
      return dt >= d && dt <= dayEnd;
    }).map(a => ({
      id: a.id, type: a.type, name: a.name,
      time: a.startTime ? new Date(a.startTime).toTimeString().slice(0,5) : '',
      dist: a.type === 'swim' ? `${a.dist} m` : `${a.dist} km`,
      pace: a.type === 'bike' ? `${a.speed} km/u` : (a.type === 'swim' ? `${a.pace} /100m` : `${a.pace} /km`),
      dur: a.dur, hr: a.hr, cal: a.cal,
    }));
    let kind = null;
    if (sessions.length > 1) kind = 'multi';
    else if (sessions.length === 1) kind = sessions[0].type;
    const isToday = d.toDateString() === today.toDateString();
    out.push({
      d: dayKeys[d.getDay()],
      label: dayLabels[d.getDay()],
      dateLabel: isToday ? 'vandaag' : `${d.getDate()} ${mo[d.getMonth()]}`,
      kind, today: isToday, sessions,
    });
  }
  return out;
}

/* === AUTH SCREEN === */
function AuthScreen({ onAuth }) {
  const [mode, setMode] = useState('login');
  const [username, setUsername] = useState('');
  const [displayName, setDisplayName] = useState('');
  const [password, setPassword] = useState('');
  const [inviteToken, setInviteToken] = useState('');
  const [err, setErr] = useState('');
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    const m = window.location.pathname.match(/^\/invite\/([^/]+)/);
    if (m) { setInviteToken(m[1]); setMode('register'); }
  }, []);

  async function submit(e) {
    if (e) e.preventDefault();
    setErr(''); setBusy(true);
    try {
      if (mode === 'login') {
        const r = await api('/api/auth/login', { method: 'POST', body: { username, password } });
        if (r.token || r.access_token) setAuthToken(r.token || r.access_token);
        onAuth(r.user || r);
      } else {
        const path = inviteToken ? '/api/auth/register-invite' : '/api/auth/register';
        const body = inviteToken ? { token: inviteToken, username, display_name: displayName || username, password }
                                 : { username, display_name: displayName || username, password };
        const r = await api(path, { method: 'POST', body });
        if (r.token || r.access_token) setAuthToken(r.token || r.access_token);
        onAuth(r.user || r);
      }
    } catch (ex) {
      setErr(ex.message || 'Fout');
    } finally { setBusy(false); }
  }

  const wrap = { minHeight: '100vh', padding: '32px 20px', background: '#fafafb', display: 'flex', flexDirection: 'column', justifyContent: 'center' };
  const logo = { width: 72, height: 72, margin: '0 auto 18px', borderRadius: 18, background: TC.orange, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' };
  const input = { width: '100%', padding: '12px 14px', borderRadius: 12, border: `1px solid ${TC.line}`, fontSize: 15, marginBottom: 10, background: '#fff', fontFamily: 'inherit' };
  const btn = { width: '100%', padding: '14px', borderRadius: 14, background: TC.orange, color: '#fff', fontSize: 15, fontWeight: 700, border: 'none', cursor: 'pointer', marginTop: 6 };
  const switchBtn = { background: 'transparent', border: 'none', color: TC.orange, fontSize: 14, fontWeight: 600, padding: '12px', cursor: 'pointer', marginTop: 12 };

  return (
    <div style={wrap}>
      <div style={logo}>
        <img src="/static/coach-potato.png" alt="" style={{ width: 68, height: 68 }} onError={e => { e.target.style.display = 'none'; }} />
      </div>
      <h1 style={{ textAlign: 'center', margin: '0 0 4px', fontSize: 26, fontWeight: 800, color: TC.ink }}>Turbocoach</h1>
      <p style={{ textAlign: 'center', margin: '0 0 24px', color: TC.muted, fontSize: 14 }}>
        {mode === 'login' ? 'Welkom terug' : (inviteToken ? 'Account aanmaken via uitnodiging' : 'Nieuw account')}
      </p>
      <form onSubmit={submit}>
        <input style={input} placeholder="Gebruikersnaam" value={username} onChange={e=>setUsername(e.target.value)} autoComplete="username" />
        {mode === 'register' && (
          <input style={input} placeholder="Weergavenaam (optioneel)" value={displayName} onChange={e=>setDisplayName(e.target.value)} />
        )}
        <input style={input} type="password" placeholder="Wachtwoord" value={password} onChange={e=>setPassword(e.target.value)} autoComplete={mode==='login'?'current-password':'new-password'} />
        {err && <div style={{ color: TC.red, fontSize: 13, marginBottom: 8, padding: '8px 12px', background: '#fde8e8', borderRadius: 10 }}>{err}</div>}
        <button type="submit" style={btn} disabled={busy || !username || !password}>
          {busy ? '...' : (mode === 'login' ? 'Aanmelden' : 'Account aanmaken')}
        </button>
      </form>
      {!inviteToken && (
        <div style={{ textAlign: 'center' }}>
          <button type="button" style={switchBtn} onClick={() => { setMode(mode==='login'?'register':'login'); setErr(''); }}>
            {mode === 'login' ? 'Nog geen account? Registreren' : 'Al een account? Aanmelden'}
          </button>
        </div>
      )}
      <div style={{ textAlign: 'center', marginTop: 20, fontSize: 11, color: TC.mutedSoft }}>
        beta - {window.location.hostname}
      </div>
    </div>
  );
}


/* === ONBOARDING WIZARD === */
function OnboardingWizard({ user, onDone }) {
  const [step, setStep] = useState(0);
  const [garmin, setGarmin] = useState({ email: '', pw: '', status: 'idle', mfa: '' });
  const [ouraToken, setOuraToken] = useState('');
  const [prof, setProf] = useState({ age: '', hrRest: '', hrMax: '', goal: '' });
  const [pwCurrent, setPwCurrent] = useState('');
  const [pwNew, setPwNew] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');

  const TOTAL = 4;

  async function connectGarmin() {
    if (!garmin.email || !garmin.pw) return;
    setBusy(true); setErr('');
    setGarmin(g => ({ ...g, status: 'connecting' }));
    try {
      await api('/api/settings', { method: 'POST', body: { garmin_email: garmin.email, garmin_password: garmin.pw } });
      const r = await api('/api/garmin/login', { method: 'POST' });
      if (r.status === 'mfa_required') setGarmin(g => ({ ...g, status: 'mfa' }));
      else setGarmin(g => ({ ...g, status: 'ok' }));
    } catch (ex) {
      setErr(ex.message || 'Garmin verbinding mislukt');
      setGarmin(g => ({ ...g, status: 'error' }));
    } finally { setBusy(false); }
  }

  async function submitMfa() {
    if (garmin.mfa.length < 6) return;
    setBusy(true); setErr('');
    setGarmin(g => ({ ...g, status: 'verifying' }));
    try {
      await api('/api/garmin/mfa', { method: 'POST', body: { code: garmin.mfa } });
      setGarmin(g => ({ ...g, status: 'ok' }));
    } catch (ex) {
      setErr(ex.message || 'Code onjuist');
      setGarmin(g => ({ ...g, status: 'mfa' }));
    } finally { setBusy(false); }
  }

  async function saveOura() {
    if (!ouraToken) return true;
    try {
      await api('/api/settings', { method: 'POST', body: { oura_token: ouraToken } });
      return true;
    } catch (ex) { setErr(ex.message); return false; }
  }

  async function saveProfile() {
    try {
      const body = {};
      if (prof.age) body.age = parseInt(prof.age);
      if (prof.hrRest) body.run_hr_rest = parseInt(prof.hrRest);
      if (prof.hrMax) body.run_hr_max = parseInt(prof.hrMax);
      if (prof.goal) body.run_goal = prof.goal;
      if (Object.keys(body).length > 0) await api('/api/profile', { method: 'POST', body });
      return true;
    } catch (ex) { setErr(ex.message); return false; }
  }

  async function changePw() {
    if (!pwNew || pwNew.length < 8) return true;
    try {
      await api('/api/auth/change-password', { method: 'POST', body: { current_password: pwCurrent, new_password: pwNew } });
      return true;
    } catch (ex) { setErr(ex.message); return false; }
  }

  async function finish() {
    setBusy(true);
    try {
      await api('/api/auth/onboarding-done', { method: 'POST' });
      onDone();
    } catch (ex) { setErr(ex.message); setBusy(false); }
  }

  async function next() {
    setErr('');
    let ok = true;
    if (step === 1) ok = await saveOura();
    else if (step === 2) ok = await saveProfile();
    else if (step === 3) { ok = await changePw(); if (ok) { await finish(); return; } }
    if (!ok) return;
    if (step < TOTAL - 1) setStep(step + 1);
  }

  async function primary() {
    if (step === 0) {
      if (garmin.status === 'idle' || garmin.status === 'error') {
        if (garmin.email && garmin.pw) return connectGarmin();
        return setStep(1); // skip
      }
      if (garmin.status === 'mfa') return submitMfa();
      if (garmin.status === 'ok') return setStep(1);
      return;
    }
    return next();
  }

  function primaryLabel() {
    if (step === 0) {
      if (garmin.status === 'connecting') return 'Verbinden...';
      if (garmin.status === 'mfa') return 'Code bevestigen';
      if (garmin.status === 'verifying') return 'Verifieren...';
      if (garmin.status === 'ok') return 'Volgende';
      if (garmin.email && garmin.pw) return 'Verbinden';
      return 'Overslaan';
    }
    if (step === TOTAL - 1) return busy ? '...' : 'Afronden';
    return 'Volgende';
  }

  const wrap = { minHeight: '100vh', padding: '24px 20px', background: '#fafafb', display: 'flex', flexDirection: 'column', gap: 20 };
  const h1Style = { fontSize: 20, fontWeight: 800, margin: 0, color: TC.ink };
  const subStyle = { fontSize: 13, color: TC.muted, marginTop: 4 };
  const input = { width: '100%', padding: '11px 14px', borderRadius: 11, border: `1px solid ${TC.line}`, fontSize: 15, background: '#fff', fontFamily: 'inherit' };
  const fieldLbl = { fontSize: 12, fontWeight: 600, color: TC.ink2, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.5 };
  const btn = { padding: '13px 16px', borderRadius: 12, fontSize: 15, fontWeight: 700, border: 'none', cursor: 'pointer' };
  const btnPrimary = { ...btn, background: TC.orange, color: '#fff', flex: 1.4 };
  const btnGhost = { ...btn, background: 'transparent', color: TC.muted, fontWeight: 500, fontSize: 13, padding: 8 };
  const dot = (active, done) => ({ width: active ? 18 : 8, height: 8, borderRadius: 4, background: done ? TC.good : (active ? TC.orange : TC.line), transition: 'all 200ms' });
  const badge = (ok) => ({ fontSize: 11, fontWeight: 600, padding: '4px 10px', borderRadius: 10, background: ok ? TC.goodSoft : TC.lineSoft, color: ok ? TC.good : TC.muted });
  const card = { background: '#fff', borderRadius: 14, padding: 14, border: `1px solid ${TC.line}` };
  const errBox = { color: TC.red, fontSize: 13, padding: '8px 12px', background: '#fde8e8', borderRadius: 10 };

  return (
    <div style={wrap}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
        <img src="/static/coach-potato.png" style={{ width: 48, height: 48, borderRadius: 12 }} onError={e=>e.target.style.display='none'} />
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink }}>Welkom {user?.display_name || user?.username}</div>
          <div style={{ fontSize: 12, color: TC.muted }}>Stel je account in om te beginnen</div>
        </div>
      </div>

      <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
        {[0,1,2,3].map(i => (
          <div key={i} style={dot(i === step, i < step)} />
        ))}
        <span style={{ marginLeft: 8, fontSize: 11, color: TC.muted }}>Stap {step+1} van {TOTAL}</span>
      </div>

      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 14 }}>
        {step === 0 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                <h2 style={h1Style}>Garmin Connect</h2>
                <span style={badge(garmin.status === 'ok')}>{garmin.status === 'ok' ? 'Verbonden' : 'Stap 1 van 4'}</span>
              </div>
              <div style={subStyle}>Koppel Garmin zodat de coach je trainingen ziet. Geen Garmin? Sla over.</div>
            </div>
            <div><div style={fieldLbl}>E-mailadres</div>
              <input style={input} type="email" placeholder="jij@voorbeeld.nl" value={garmin.email} onChange={e=>setGarmin(g=>({...g, email: e.target.value}))} disabled={garmin.status === 'ok'} />
            </div>
            <div><div style={fieldLbl}>Wachtwoord</div>
              <input style={input} type="password" placeholder="Garmin-wachtwoord" value={garmin.pw} onChange={e=>setGarmin(g=>({...g, pw: e.target.value}))} disabled={garmin.status === 'ok'} />
            </div>
            {garmin.status === 'mfa' || garmin.status === 'verifying' || garmin.status === 'ok' ? (
              <div><div style={fieldLbl}>Verificatiecode</div>
                <input style={{...input, letterSpacing: 4, textAlign:'center', fontSize:18}} inputMode="numeric" maxLength={6} placeholder="000000" value={garmin.mfa} onChange={e=>setGarmin(g=>({...g, mfa: e.target.value.replace(/\D/g,'').slice(0,6)}))} disabled={garmin.status === 'ok' || garmin.status === 'verifying'} />
              </div>
            ) : null}
          </div>
        )}

        {step === 1 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                <h2 style={h1Style}>Oura Ring</h2>
                <span style={badge(false)}>Optioneel</span>
              </div>
              <div style={subStyle}>Oura geeft betere slaap- en hersteldata. Geen Oura? Sla over.</div>
            </div>
            <div><div style={fieldLbl}>Personal Access Token</div>
              <input style={input} placeholder="ORAT-..." value={ouraToken} onChange={e=>setOuraToken(e.target.value)} autoCapitalize="none" />
              <div style={{ fontSize: 11, color: TC.muted, marginTop: 4 }}>Via cloud.ouraring.com/personal-access-tokens</div>
            </div>
          </div>
        )}

        {step === 2 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                <h2 style={h1Style}>Jouw profiel</h2>
                <span style={badge(false)}>Optioneel</span>
              </div>
              <div style={subStyle}>Vul in wat je weet. De AI leert de rest uit je data.</div>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
              <div><div style={fieldLbl}>Leeftijd</div><input style={input} inputMode="numeric" placeholder="35" value={prof.age} onChange={e=>setProf({...prof, age: e.target.value.replace(/\D/g,'').slice(0,3)})} /></div>
              <div><div style={fieldLbl}>Rust HR</div><input style={input} inputMode="numeric" placeholder="52" value={prof.hrRest} onChange={e=>setProf({...prof, hrRest: e.target.value.replace(/\D/g,'').slice(0,3)})} /></div>
              <div><div style={fieldLbl}>Max HR</div><input style={input} inputMode="numeric" placeholder="188" value={prof.hrMax} onChange={e=>setProf({...prof, hrMax: e.target.value.replace(/\D/g,'').slice(0,3)})} /></div>
              <div><div style={fieldLbl}>Loopdoel</div><input style={input} placeholder="Halve sub 1:45" value={prof.goal} onChange={e=>setProf({...prof, goal: e.target.value})} /></div>
            </div>
          </div>
        )}

        {step === 3 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                <h2 style={h1Style}>Nieuw wachtwoord</h2>
                <span style={badge(false)}>Optioneel</span>
              </div>
              <div style={subStyle}>Stel je eigen wachtwoord in of sla over.</div>
            </div>
            <div><div style={fieldLbl}>Huidig wachtwoord</div><input style={input} type="password" value={pwCurrent} onChange={e=>setPwCurrent(e.target.value)} /></div>
            <div><div style={fieldLbl}>Nieuw wachtwoord</div><input style={input} type="password" placeholder="min. 8 tekens" value={pwNew} onChange={e=>setPwNew(e.target.value)} />
              {pwNew && pwNew.length < 8 && <div style={{ fontSize: 11, color: TC.red, marginTop: 4 }}>Minimaal 8 tekens</div>}
            </div>
          </div>
        )}
      </div>

      {err && <div style={errBox}>{err}</div>}

      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        <div style={{ display: 'flex', gap: 10 }}>
          {step > 0 && <button style={{...btn, flex: 1, background: '#fff', border: `1px solid ${TC.line}`, color: TC.ink2}} onClick={()=>setStep(step-1)}>Vorige</button>}
          <button style={btnPrimary} onClick={primary} disabled={busy}>{primaryLabel()}</button>
        </div>
        {step < TOTAL - 1 && step !== 0 && <button style={btnGhost} onClick={()=>setStep(step+1)}>Deze stap overslaan</button>}
        {step === TOTAL - 1 && <button style={btnGhost} onClick={finish} disabled={busy}>Overslaan en afronden</button>}
      </div>
    </div>
  );
}


// ─── Sync status modal ────────────────────────────────────
function SyncModal({ status, onClose }) {
  if (!status) return null;
  const { phase, title, message, hint } = status;
  const isRunning = phase === 'running';
  const isOk      = phase === 'success';
  const isErr     = phase === 'error';
  const isInfo    = phase === 'info';
  const accent = isErr ? '#c94a2a' : (isOk ? '#2fb673' : '#d86f24');
  return (
    <div
      style={{
        position: 'absolute', inset: 0, zIndex: 60,
        background: 'rgba(20,20,25,0.45)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: 20, animation: 'tc-fade 150ms ease-out',
      }}
      onClick={isRunning ? undefined : onClose}
    >
      <div
        onClick={e => e.stopPropagation()}
        style={{
          width: '100%', maxWidth: 340, background: '#fff',
          borderRadius: 20, padding: '22px 20px 18px',
          boxShadow: '0 14px 40px rgba(0,0,0,0.25)',
          display: 'flex', flexDirection: 'column', alignItems: 'center',
          textAlign: 'center',
          animation: 'tc-slide-up 220ms ease-out',
        }}
      >
        <div
          style={{
            width: 56, height: 56, borderRadius: 16,
            background: isErr ? '#fde3da' : isOk ? '#e3f6ec' : '#fff0e0',
            color: accent,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            marginBottom: 14,
          }}
        >
          {isRunning
            ? <span style={{
                display: 'inline-block', width: 24, height: 24,
                border: '3px solid ' + accent + '30',
                borderTopColor: accent, borderRadius: '50%',
                animation: 'tc-spin 900ms linear infinite',
              }} />
            : <Icon name={isErr ? 'x' : isOk ? 'check' : 'sync'} size={26} />
          }
        </div>
        <div style={{ fontSize: 17, fontWeight: 800, color: '#1a1a1f', marginBottom: 6 }}>
          {title}
        </div>
        {message && (
          <div style={{ fontSize: 13, color: '#56565e', lineHeight: 1.5, marginBottom: hint ? 8 : 16 }}>
            {message}
          </div>
        )}
        {hint && (
          <div style={{ fontSize: 11, color: '#8c8c95', marginBottom: 16 }}>
            {hint}
          </div>
        )}
        {!isRunning && (
          <button
            onClick={onClose}
            style={{
              width: '100%', padding: '12px 16px',
              background: accent, color: '#fff',
              border: 'none', borderRadius: 12,
              fontSize: 14, fontWeight: 700, cursor: 'pointer',
            }}
          >
            Ok
          </button>
        )}
      </div>
    </div>
  );
}

// tc-spin animatie
if (typeof document !== 'undefined' && !document.getElementById('tc-spin-css')) {
  const s = document.createElement('style');
  s.id = 'tc-spin-css';
  s.textContent = '@keyframes tc-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
  document.head.appendChild(s);
}


// ─── Markdown -> Coach-blocks parser ─────────────────────
function stripMd(t) {
  if (!t) return '';
  return String(t)
    .replace(/\*\*([^*]+)\*\*/g, '$1')
    .replace(/`([^`]+)`/g, '$1')
    .trim();
}

function parseMarkdownTable(raw) {
  const rows = raw.trim().split('\n')
    .map(r => r.trim())
    .filter(r => r.startsWith('|'))
    .map(r => {
      const inner = r.replace(/^\|/, '').replace(/\|$/, '');
      return inner.split('|').map(c => stripMd(c));
    });
  if (rows.length < 2) return null;
  const [head, sep, ...body] = rows;
  // validate separator row: dashes / colons
  const sepOk = sep.every(c => /^[-:\s]+$/.test(c));
  if (!sepOk) return null;
  return { head, rows: body };
}

function parseCoachSection(title, bodyLines) {
  const iconMap = [
    ['💓','heart'], ['❤️','heart'], ['🫀','heart'],
    ['📊','info'], ['📋','info'], ['ℹ️','info'], ['🔍','info'], ['📝','info'],
    ['🎯','target'], ['📌','target'],
    ['⚡','bolt'], ['💪','bolt'], ['🏃','bolt'], ['🚴','bolt'], ['🏊','bolt'],
    ['⚠️','warn'], ['🚨','warn'], ['🔴','warn'],
    ['📅','calendar'], ['🗓️','calendar'],
    ['✅','check'], ['🟢','check'],
  ];
  let icon = 'info';
  // Strip **bold**-wrapping uit de heading (AI schrijft vaak '## 🚴 **Titel**')
  let cleanTitle = String(title || '').replace(/\*\*([^*]+)\*\*/g, '$1').trim();
  for (const [em, ic] of iconMap) {
    if (cleanTitle.startsWith(em)) { icon = ic; cleanTitle = cleanTitle.slice(em.length).trim(); break; }
  }
  // Fallback: keyword-based icon als er geen emoji is
  if (icon === 'info') {
    const lt = cleanTitle.toLowerCase();
    if (/hart|hr|hrv|slaap|rhr|gezondheid|readiness/.test(lt)) icon = 'heart';
    else if (/doel|target|focus/.test(lt)) icon = 'target';
    else if (/workout|training|voorstel|plan|interval|blok/.test(lt)) icon = 'bolt';
    else if (/let op|opgelet|risico|waarschuwing|niet/.test(lt)) icon = 'warn';
    else if (/morgen|vandaag|week|dag|planning|datum|agenda/.test(lt)) icon = 'calendar';
  }

  const body = bodyLines.join('\n');

  // Parse sequential: ondersteunt meerdere tabellen + text/bullets ertussen.
  // parts = array van { type: 'text'|'table'|'bullets', ... } in document volgorde.
  const parts = [];
  const tableRe = /(^|\n)(\|[^\n]+\|\n\|[-:\s|]+\|\n(?:\|[^\n]+\|(?:\n|$))+)/g;
  let lastIdx = 0;
  let m;
  const pushTextBlock = (txt) => {
    if (!txt || !txt.trim()) return;
    const blines = txt.split('\n');
    let currBullets = null;
    let currText = [];
    let currQuote = [];
    const flushText = () => {
      if (currText.length === 0) return;
      const joined = stripMd(currText.join(' ').replace(/\s+/g, ' ').trim());
      if (joined) parts.push({ type: 'text', text: joined });
      currText = [];
    };
    const flushBullets = () => {
      if (!currBullets || currBullets.length === 0) { currBullets = null; return; }
      parts.push({ type: 'bullets', bullets: currBullets });
      currBullets = null;
    };
    const flushQuote = () => {
      if (currQuote.length === 0) return;
      const joined = stripMd(currQuote.join(' ').replace(/\s+/g, ' ').trim());
      if (joined) parts.push({ type: 'quote', text: joined });
      currQuote = [];
    };
    const flushAll = () => { flushText(); flushBullets(); flushQuote(); };
    for (const ln of blines) {
      // horizontal rule → paragraph break
      if (/^\s*-{3,}\s*$/.test(ln) || /^\s*\*{3,}\s*$/.test(ln)) {
        flushAll();
        continue;
      }
      // subheader ### Title
      const hm = ln.match(/^\s*###\s+(.+)$/);
      if (hm) {
        flushAll();
        parts.push({ type: 'subheader', text: stripMd(hm[1]) });
        continue;
      }
      // blockquote > text (groepeer opeenvolgende quote-lijnen)
      const qm = ln.match(/^\s*>\s*(.*)$/);
      if (qm) {
        flushText();
        flushBullets();
        currQuote.push(qm[1]);
        continue;
      }
      // bullet (groepeer opeenvolgende)
      const bm = ln.match(/^\s*[-*]\s+(.*)$/);
      if (bm) {
        flushText();
        flushQuote();
        if (!currBullets) currBullets = [];
        const raw = bm[1];
        const boldFirst = raw.match(/^\*\*([^*]+)\*\*\s*:?\s*(.*)$/);
        if (boldFirst) currBullets.push({ bold: stripMd(boldFirst[1]), text: stripMd(boldFirst[2]) });
        else currBullets.push({ text: stripMd(raw) });
        continue;
      }
      // lege regel → paragraph break
      if (!ln.trim()) {
        flushAll();
        continue;
      }
      // anders: tekst-lijn
      flushBullets();
      flushQuote();
      currText.push(ln);
    }
    flushAll();
  };
  while ((m = tableRe.exec(body)) !== null) {
    // text voor deze tabel
    pushTextBlock(body.slice(lastIdx, m.index + m[1].length));
    const parsedTable = parseMarkdownTable(m[2]);
    if (parsedTable) parts.push({ type: 'table', table: parsedTable });
    lastIdx = tableRe.lastIndex;
  }
  // text na laatste tabel
  pushTextBlock(body.slice(lastIdx));

  const sec = { type: 'section', icon, title: cleanTitle, parts };
  // Backward-compat: vul ook single fields van eerste voorkomen in,
  // zodat oude CoachSection-renderer nog werkt voor niet-bijgewerkte clients.
  const firstText = parts.find(p => p.type === 'text');
  const firstTable = parts.find(p => p.type === 'table');
  const firstBullets = parts.find(p => p.type === 'bullets');
  if (firstText) sec.text = firstText.text;
  if (firstTable) sec.table = firstTable.table;
  if (firstBullets) sec.bullets = firstBullets.bullets;
  return sec;
}

function parseWorkoutJson(src) {
  const out = [];
  const re = /```workout-json\s*\n([\s\S]*?)\n```/g;
  let m;
  while ((m = re.exec(src)) !== null) {
    try {
      const w = JSON.parse(m[1]);
      const unitFor = (tt) => {
        if (tt === 'power' || tt === 'power_zone') return 'W';
        if (tt === 'cadence') return 'rpm';
        if (tt === 'pace') return '/km';
        if (tt === 'hr_zone') return ' bpm';
        return '';
      };
      const steps = (w.steps || []).map(st => {
        const dur = st.duration_seconds ? Math.round(st.duration_seconds / 60) + ' min' : (st.duration || '');
        let target = '';
        if (st.target_low != null && st.target_high != null) {
          const u = unitFor(st.target_type);
          target = (st.target_low === st.target_high)
            ? (Math.round(st.target_low) + u)
            : (Math.round(st.target_low) + '–' + Math.round(st.target_high) + u);
        } else if (st.target_zone && st.target_type === 'hr_zone') {
          target = 'Z' + st.target_zone + ' HR';
        } else if (st.target_zone && st.target_type === 'power_zone') {
          target = 'Z' + st.target_zone + ' power';
        } else if (st.target) {
          target = st.target;
        }
        const detail = [dur, target].filter(Boolean).join(' · ');
        return { label: st.name || st.type || 'Stap', detail };
      });
      out.push({
        type: 'workout',
        name: w.name || 'Training',
        duration: w.duration || '',
        sport: w.sport || '',
        steps,
        raw: w,
      });
    } catch (e) { /* ignore bad JSON */ }
  }
  return out;
}

function parseCoachResponse(md) {
  if (!md || typeof md !== 'string') return [{ type: 'intro', text: '' }];

  // Extract + strip workout-json
  const workouts = parseWorkoutJson(md);
  md = md.replace(/```workout-json\s*\n[\s\S]*?\n```/g, '').trim();

  // Split op ## headers (level 2) — lagere kopjes gaan mee als bullets
  const lines = md.split('\n');
  const introLines = [];
  const sections = [];
  let current = null;
  for (const ln of lines) {
    const h = ln.match(/^##\s+(.+)/);
    if (h && !ln.startsWith('###')) {
      if (current) sections.push(current);
      current = { title: h[1].trim(), body: [] };
    } else {
      if (current) current.body.push(ln);
      else introLines.push(ln);
    }
  }
  if (current) sections.push(current);

  const result = [];
  const introText = stripMd(introLines.join('\n').trim());
  if (introText) result.push({ type: 'intro', text: introText });
  for (const sec of sections) result.push(parseCoachSection(sec.title, sec.body));
  for (const w of workouts) result.push(w);

  // Fallback: als er niks geparsed werd, dump de hele md als intro
  if (result.length === 0) result.push({ type: 'intro', text: stripMd(md) });
  return result;
}

/* === END HEADER — below this: design code === */
// Turbocoach beta — mobile-first prototype
// Single file with all screens/components/state

// ─── Brand tokens ───────────────────────────────────────────
const TC = {
  orange: '#f57c2a',
  orangeDark: '#e56a15',
  orangeSoft: '#fff1e6',
  orangeBorder: '#ffd9b8',
  ink: '#1b1b1f',
  ink2: '#3a3a42',
  muted: '#6f6f78',
  mutedSoft: '#a0a0aa',
  line: '#ececee',
  lineSoft: '#f4f4f6',
  bg: '#ffffff',
  bgSoft: '#fafafb',
  good: '#2aa775',
  goodSoft: '#e7f6ef',
  warn: '#d97706',
  blue: '#2f6fed',
  blueSoft: '#e8efff',
  red: '#d64545',
};

// ─── Fake data ──────────────────────────────────────────────
const MASCOT = '/static/coach-potato.png';

let ACTIVITIES = []; // live data
// Each day can have 0, 1 or multiple sessions. `kind` on the button is the
// "primary" icon shown on the strip (the session with most load, for the double-day).
let WEEK_STRIP = []; // live data
let RACE_PREDICTIONS = []; // live data
const POWER_ZONES = [
  { z: 'Z1', name: 'Actief herstel', range: '< 138 W',   pct: 55, color: '#c7d8f5' },
  { z: 'Z2', name: 'Duur',           range: '138–189 W', pct: 72, color: '#8fb6ee' },
  { z: 'Z3', name: 'Tempo',          range: '190–227 W', pct: 48, color: TC.orange },
  { z: 'Z4', name: 'Drempel',        range: '228–265 W', pct: 28, color: TC.orangeDark },
  { z: 'Z5', name: 'VO₂',            range: '266–303 W', pct: 14, color: '#d64545' },
  { z: 'Z6', name: 'Anaeroob',       range: '> 303 W',   pct: 6,  color: '#9b2c2c' },
];

// Rich block templates — het type "lange antwoord" uit de ontwerpmockups
const LONG_ANSWER_BLOCKS = [
  {
    type: 'summary',
    tldr: 'Ja, rol gerust — maar hou het licht. Ik stel 70 min progressieve flush voor met 4×3 min activatie. Morgen wacht je laatste lange loop (18–20 km).',
    bullets: [
      { label: 'Readiness', value: '83/100 · groen' },
      { label: 'Focus vandaag', value: 'benen losmaken, kuit sparen' },
      { label: 'Morgen', value: 'lange loop 18–20 km @ 5:00–5:10/km' },
    ],
  },
  {
    type: 'section',
    icon: 'heart',
    title: 'Gezondheidsdata vandaag — zat 18/04',
    table: {
      head: ['Metric', 'Waarde', 'Beoordeling'],
      rows: [
        ['HRV (Oura)', '43 ms',  { ok: true, text: 'op baseline' }],
        ['Slaap',      '6:30u · score 83', { ok: true, text: 'goed' }],
        ['RHR',        '51 bpm', { ok: true, text: 'stabiel' }],
        ['Readiness',  '83/100', { ok: true, text: 'goed' }],
      ],
    },
    footer: { type: 'ok', text: 'Groen licht.' },
  },
  {
    type: 'section',
    icon: 'calendar',
    title: 'Context',
    bullets: [
      { bold: 'Gisteren', text: '↗ loop 11,4 km — kuit duidelijk beter ✅' },
      { bold: 'Donderdag', text: 'geen rollen (drukke agenda)' },
      { bold: 'Morgen zondag', text: 'lange loop gepland — laatste echte lange duurloop vóór tapering', highlight: true },
      { bold: 'Wings for Life', text: '22 dagen 🔴' },
    ],
  },
  {
    type: 'section',
    icon: 'target',
    title: 'Doel vandaag: flush + activatie voor morgen',
    text: 'Vandaag is géén dag om jezelf uit te putten. Morgen is de belangrijkste trainingsdag van de hele voorbereiding — laatste lange loop. Vandaag moet je:',
    bullets: [
      { text: 'Benen losmaken na de loop van gisteren' },
      { text: 'Aerobe basis onderhouden' },
      { text: 'Gevoelige kuit volledig ontzien', highlight: true },
      { text: 'Fris en uitgerust zijn voor morgen' },
    ],
  },
  {
    type: 'section',
    icon: 'bolt',
    title: 'Voorstel: progressieve flush met activatieblokken — 70 min',
    text: 'Geen zware intervallen. Geen maximale inspanning. Wel iets meer dan een pure hersteltrit zodat je morgen niet met stijve benen start.',
    table: {
      head: ['Onderdeel', 'Duur', 'Wattage', 'Cadans', 'Details'],
      rows: [
        ['Opwarming trap 1', '5 min',   '100–115 W', '85 rpm', 'benen losmaken'],
        ['Opwarming trap 2', '5 min',   '115–130 W', '88 rpm', 'geleidelijk ophouden'],
        ['Opwarming trap 3', '5 min',   '130–148 W', '90 rpm', 'richting Z2'],
        ['Z2 blok',          '30 min',  { em: '150–168 W' }, '88–92 rpm', 'comfortabel, gecontroleerd'],
        ['4×3 min activatie','4×3 min', { em: '178–195 W' }, '92–94 rpm', 'kort en scherp — géén uitputting'],
        ['Herstel tussen',   '2 min',   '115–125 W', '85 rpm', 'volledig loslaten'],
        ['Uitrijden',        '10 min',  '105–120 W', '83–87 rpm', 'benen uitspinnen voor morgen'],
        ['Totaal',           { em: '~70 min' }, '', '', ''],
      ],
    },
  },
  {
    type: 'section',
    icon: 'info',
    title: 'Waarom deze wattages',
    table: {
      head: ['Zone', '% FTP (228 W)', 'Wattage', 'Doel'],
      rows: [
        ['Opwarming',       '44–65 %', '100–148 W', 'geleidelijk activeren'],
        ['Z2 blok',         '66–74 %', '150–168 W', 'aerobe basis — géén stress'],
        ['Activatieblokken','78–86 %', '178–195 W', 'benen scherp houden — géén VO₂max'],
        ['Uitrijden',       '46–53 %', '105–120 W', 'flush — lactaat wegwerken'],
      ],
    },
  },
  {
    type: 'section',
    icon: 'warn',
    title: 'Waarom geen zwaardere intervallen vandaag',
    table: {
      head: ['Reden', 'Details'],
      rows: [
        ['Morgen lange loop', '18–20 km — benen moeten fris zijn'],
        ['Kuit herstelt',     'geen onnodige systemische vermoeidheid'],
        ['22 dagen tot race', 'risico/baat-verhouding: flush wint van zware training'],
      ],
    },
  },
  {
    type: 'section',
    icon: 'calendar',
    title: 'Morgen zondag 19/04 — preview',
    highlight: 'Laatste lange loop: 18–20 km @ 5:00–5:10/km',
    bullets: [
      { text: 'Hard plafond op ', bold2: '20 km', textAfter: ' — kuit niet voorbij de pijngrens duwen' },
      { text: 'Voedingsprotocol testen: ', bold2: 'gel om de 35–40 min' },
      { text: 'Zelfde opwarmroutine als gisteren: 10 min wandelen vóór je start' },
      { text: 'Stop bij uitstraling naar voet' },
    ],
  },
  {
    type: 'outro',
    text: 'Geniet van de rolsessie — lekker uitspinnen, benen losmaken, en morgen fris aan de start van je laatste lange loop. 💪',
  },
  {
    type: 'workout',
    name: 'Flush + activatie pre-lange loop',
    duration: '~70 min',
    sport: 'Fietsen · rollen',
    steps: [
      { label: 'Opwarming', detail: '5 min · 100–115 W · 85 rpm' },
      { label: 'Opwarming', detail: '5 min · 115–130 W · 88 rpm' },
      { label: 'Opwarming', detail: '5 min · 130–148 W · 90 rpm' },
      { label: 'Z2 blok',   detail: '30 min · 150–168 W · 90 rpm' },
      { label: '4× activatie', detail: '3 min · 178–195 W · 93 rpm (2 min herstel)' },
      { label: 'Uitrijden', detail: '10 min · 105–120 W · 85 rpm' },
    ],
  },
];

let COACH_SEED = []; // live data
// ─── Small UI atoms ─────────────────────────────────────────
function Avatar({ size = 40 }) {
  return (
    <div style={{
      width: size, height: size, borderRadius: '50%',
      background: '#fff', border: `1px solid ${TC.line}`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      flexShrink: 0, overflow: 'hidden',
    }}>
      <img src={MASCOT} alt="Turbocoach"
        style={{ width: size * 0.86, height: size * 0.86, objectFit: 'contain' }} />
    </div>
  );
}

function Chip({ children, active, onClick, color = TC.orange }) {
  return (
    <button onClick={onClick} style={{
      padding: '8px 14px', borderRadius: 999,
      border: `1px solid ${active ? color : TC.line}`,
      background: active ? TC.orangeSoft : '#fff',
      color: active ? TC.orangeDark : TC.ink2,
      fontSize: 13, fontWeight: 600, cursor: 'pointer',
      whiteSpace: 'nowrap',
    }}>{children}</button>
  );
}

function Card({ children, style, onClick }) {
  return (
    <div onClick={onClick} style={{
      background: '#fff', border: `1px solid ${TC.line}`,
      borderRadius: 16, padding: 16,
      cursor: onClick ? 'pointer' : 'default',
      ...style,
    }}>{children}</div>
  );
}

function SectionTitle({ children, right }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
      padding: '0 4px', marginBottom: 10,
    }}>
      <div style={{ fontSize: 13, fontWeight: 700, letterSpacing: 0.6, textTransform: 'uppercase', color: TC.muted }}>{children}</div>
      {right}
    </div>
  );
}

// Two overlapping dots — shown on week-strip days with >1 session
function MultiIcon({ color }) {
  return (
    <svg width="22" height="16" viewBox="0 0 22 16" fill="none">
      <circle cx="8" cy="8" r="5" stroke={color} strokeWidth="2" />
      <circle cx="14" cy="8" r="5" stroke={color} strokeWidth="2" fill="#fff" />
      <circle cx="14" cy="8" r="5" stroke={color} strokeWidth="2" />
    </svg>
  );
}

// tiny inline icon set (stroke, 24)
const Icon = ({ name, size = 20, color = 'currentColor', strokeWidth = 2 }) => {
  const p = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round' };
  switch (name) {
    case 'close':  return <svg {...p}><path d="M6 6l12 12M18 6l-12 12"/></svg>;
    case 'send':   return <svg {...p}><path d="M4 12l16-8-6 16-3-7-7-1z"/></svg>;
    case 'plus':   return <svg {...p}><path d="M12 5v14M5 12h14"/></svg>;
    case 'chat':   return <svg {...p}><path d="M21 12a8 8 0 11-3-6.3L21 5l-1 3.6A8 8 0 0121 12z"/></svg>;
    case 'spark':  return <svg {...p}><path d="M12 3l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/></svg>;
    case 'info':   return <svg {...p}><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 7.5v.5"/></svg>;
    case 'chev':   return <svg {...p}><path d="M9 6l6 6-6 6"/></svg>;
    case 'chevDn': return <svg {...p}><path d="M6 9l6 6 6-6"/></svg>;
    case 'run':    return <svg {...p}><circle cx="13" cy="4.5" r="1.6"/><path d="M5 20l3-5 3 2 1 3M8 12l4-3 3 3 3 1M12 15l2 5"/></svg>;
    case 'bike':   return <svg {...p}><circle cx="6" cy="17" r="3"/><circle cx="18" cy="17" r="3"/><path d="M6 17l3-7h5l3 7M13 5l3 5"/></svg>;
    case 'swim':   return <svg {...p}><path d="M2 18c2 0 2-1.5 4-1.5s2 1.5 4 1.5 2-1.5 4-1.5 2 1.5 4 1.5M2 14c2 0 2-1.5 4-1.5s2 1.5 4 1.5 2-1.5 4-1.5 2 1.5 4 1.5"/><circle cx="16" cy="7" r="2"/></svg>;
    case 'bolt':   return <svg {...p}><path d="M13 2L4 14h7l-1 8 9-12h-7z"/></svg>;
    case 'gear':   return <svg {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 00.3 1.8l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.7 1.7 0 00-1.8-.3 1.7 1.7 0 00-1 1.5V21a2 2 0 01-4 0v-.1a1.7 1.7 0 00-1-1.5 1.7 1.7 0 00-1.8.3l-.1.1a2 2 0 11-2.8-2.8l.1-.1a1.7 1.7 0 00.3-1.8 1.7 1.7 0 00-1.5-1H3a2 2 0 010-4h.1a1.7 1.7 0 001.5-1 1.7 1.7 0 00-.3-1.8l-.1-.1A2 2 0 117 5l.1.1a1.7 1.7 0 001.8.3H9a1.7 1.7 0 001-1.5V3a2 2 0 014 0v.1a1.7 1.7 0 001 1.5 1.7 1.7 0 001.8-.3l.1-.1A2 2 0 0119.7 7l-.1.1a1.7 1.7 0 00-.3 1.8V9a1.7 1.7 0 001.5 1H21a2 2 0 010 4h-.1a1.7 1.7 0 00-1.5 1z"/></svg>;
    case 'warn':   return <svg {...p}><path d="M12 3l10 18H2z"/><path d="M12 10v4M12 17.5v.5"/></svg>;
    case 'arrowU': return <svg {...p}><path d="M12 19V5M5 12l7-7 7 7"/></svg>;
    case 'arrowD': return <svg {...p}><path d="M12 5v14M5 12l7 7 7-7"/></svg>;
    case 'sync':   return <svg {...p}><path d="M3 12a9 9 0 0115.5-6.3L21 8M21 3v5h-5M21 12a9 9 0 01-15.5 6.3L3 16M3 21v-5h5"/></svg>;
    case 'check':  return <svg {...p}><path d="M4 12l5 5L20 6"/></svg>;
    case 'heart':  return <svg {...p}><path d="M12 20s-7-4.5-9-9a5 5 0 019-3 5 5 0 019 3c-2 4.5-9 9-9 9z"/></svg>;
    case 'target': return <svg {...p}><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5"/></svg>;
    case 'calendar': return <svg {...p}><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>;
    case 'garmin': return <svg {...p}><path d="M4 12l4 4 4-8 4 8 4-4"/></svg>;
    default: return null;
  }
};

// ─── Dashboard screen ───────────────────────────────────────
function Dashboard({ onOpenChat, onOpenPredictions, onOpenSettings, onActivityClick, onDayClick, onSync, syncing, onPromptVisible, health }) {
  const [tab, setTab] = useState('run'); // run / bike / swim
  const latest = useMemo(() => ACTIVITIES.find(a => a.type === tab) || { name: tab === 'run' ? 'Nog geen run' : tab === 'bike' ? 'Nog geen rit' : 'Nog geen zwemsessie', date: '', type: tab, dist: '—', pace: '—', speed: '—', dur: '—', hr: '—', tss: '—', elev: '—', cad: '—' }, [tab]);
  const promptRef = useRef(null);

  useEffect(() => {
    if (!promptRef.current) return;
    const io = new IntersectionObserver(
      ([entry]) => onPromptVisible(entry.isIntersecting && entry.intersectionRatio > 0.4),
      { threshold: [0, 0.4, 1] }
    );
    io.observe(promptRef.current);
    return () => io.disconnect();
  }, [onPromptVisible]);

  const tabs = [
    { id: 'run',  label: 'Lopen',    icon: 'run'  },
    { id: 'bike', label: 'Fietsen',  icon: 'bike' },
    { id: 'swim', label: 'Zwemmen',  icon: 'swim' },
  ];

  return (
    <div style={{
      background: TC.bgSoft, height: '100%',
      overflowY: 'auto', overflowX: 'hidden',
      position: 'relative',
    }}>
      {/* Top header bar */}
      <div style={{
        position: 'sticky', top: 0, zIndex: 5, background: '#fff',
        borderBottom: `1px solid ${TC.line}`,
        padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12,
      }}>
        <Avatar size={36} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: TC.ink, lineHeight: 1.1 }}>Turbocoach</div>
          {(() => { const parts = []; if (health && health.garmin_configured) parts.push("Garmin " + (health.garmin_connected ? "verbonden" : "niet verbonden")); if (health && health.oura_configured) parts.push("Oura " + (health.oura_connected ? "verbonden" : "niet verbonden")); return parts.length ? (<div style={{ fontSize: 11, color: TC.muted, marginTop: 2 }}>{parts.join(" · ")}</div>) : null; })()}
        </div>
        <IconBtn onClick={onSync} spin={syncing} title="Sync"><Icon name="sync" size={18} /></IconBtn>
        <IconBtn onClick={onOpenSettings} title="Instellingen"><Icon name="gear" size={18} /></IconBtn>
      </div>

      {/* Beta banner (rendered conditionally by host) */}
      <BetaBanner />

      <div style={{ padding: '14px 14px 120px', display: 'flex', flexDirection: 'column', gap: 18 }}>

        {/* Tabs */}
        <div style={{ display: 'flex', gap: 8, overflowX: 'auto' }}>
          {tabs.map(t => (
            <button key={t.id} onClick={() => setTab(t.id)} style={{
              display: 'flex', alignItems: 'center', gap: 8,
              padding: '10px 14px', borderRadius: 999,
              border: `1px solid ${tab === t.id ? TC.orange : TC.line}`,
              background: tab === t.id ? TC.orange : '#fff',
              color: tab === t.id ? '#fff' : TC.ink2,
              fontSize: 14, fontWeight: 600, cursor: 'pointer',
            }}>
              <Icon name={t.icon} size={16} /> {t.label}
            </button>
          ))}
        </div>

        {/* Activity hero card */}
        <Card style={{ padding: 0, overflow: 'hidden' }}>
          <div style={{
            padding: '14px 16px',
            borderBottom: `1px solid ${TC.line}`,
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          }}>
            <div>
              <div style={{ fontSize: 12, color: TC.muted, fontWeight: 600, letterSpacing: 0.4, textTransform: 'uppercase' }}>
                Laatste {tab === 'run' ? 'run' : tab === 'bike' ? 'rit' : 'sessie'}
              </div>
              <div style={{ fontSize: 17, fontWeight: 700, color: TC.ink, marginTop: 2 }}>{latest.name}</div>
              <div style={{ fontSize: 12, color: TC.muted, marginTop: 2 }}>{latest.date}</div>
            </div>
            <div style={{
              width: 40, height: 40, borderRadius: 12,
              background: TC.orangeSoft, color: TC.orangeDark,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <Icon name={latest.type} size={22} />
            </div>
          </div>

          {/* Main 3 stats */}
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' }}>
            {[
              { l: 'Afstand', v: latest.dist, u: tab === 'swim' ? 'm' : 'km' },
              { l: tab === 'bike' ? 'Snelheid' : 'Tempo', v: tab === 'bike' ? latest.speed : latest.pace, u: tab === 'bike' ? 'km/u' : tab === 'swim' ? '/100m' : 'min/km' },
              { l: 'Tijd', v: latest.dur, u: '' },
            ].map((s, i) => (
              <div key={i} style={{
                padding: '18px 10px',
                textAlign: 'center',
                borderRight: i < 2 ? `1px solid ${TC.line}` : 'none',
              }}>
                <div style={{ fontSize: 11, color: TC.muted, fontWeight: 600, letterSpacing: 0.4, textTransform: 'uppercase' }}>{s.l}</div>
                <div style={{ fontSize: 26, fontWeight: 800, color: TC.ink, marginTop: 6, letterSpacing: -0.5 }}>{s.v}</div>
                <div style={{ fontSize: 11, color: TC.muted, marginTop: 2 }}>{s.u}</div>
              </div>
            ))}
          </div>

          {/* Secondary stats */}
          <div style={{
            borderTop: `1px solid ${TC.line}`,
            display: 'grid',
            gridTemplateColumns: 'repeat(4, 1fr)',
            background: TC.bgSoft,
          }}>
            {[
              { l: 'Hartslag', v: latest.hr ? latest.hr + ' bpm' : '—' },
              { l: 'Hoogte',   v: latest.elev ? latest.elev + ' m' : '—' },
              { l: 'Calorieën',v: latest.cal ? latest.cal : '—' },
              { l: tab === 'run' ? 'Cadans' : tab === 'bike' ? 'Vermogen' : 'Slagen', v: tab === 'run' ? (latest.cad || '—') : tab === 'bike' ? '214 W' : '32 spm' },
            ].map((s, i) => (
              <div key={i} style={{
                padding: '10px 6px', textAlign: 'center',
                borderRight: i < 3 ? `1px solid ${TC.line}` : 'none',
              }}>
                <div style={{ fontSize: 10, color: TC.muted, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.3 }}>{s.l}</div>
                <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink, marginTop: 3 }}>{s.v}</div>
              </div>
            ))}
          </div>

          {/* Week strip */}
          <div style={{
            padding: '14px 16px',
            borderTop: `1px solid ${TC.line}`,
          }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
              <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.5, textTransform: 'uppercase' }}>Laatste 7 dagen</div>
              <div style={{ fontSize: 12, color: TC.ink2, fontWeight: 600 }}>{WEEK_STRIP.filter(d=>d.sessions && d.sessions.length>0).length} / 7 dagen</div>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 6 }}>
              {WEEK_STRIP.map((d, i) => {
                const on = d.sessions.length > 0;
                const multi = d.sessions.length > 1;
                return (
                  <button key={i} onClick={() => onDayClick(d)} aria-label={`${d.label} — ${on ? d.sessions.length + ' sessie' + (multi ? 's' : '') : 'rustdag'}`} style={{
                    display: 'block', textAlign: 'center',
                    background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                  }}>
                    <div style={{
                      position: 'relative',
                      height: 42, borderRadius: 10,
                      background: on ? (d.today ? TC.orange : TC.orangeSoft) : TC.lineSoft,
                      color: on ? (d.today ? '#fff' : TC.orangeDark) : TC.mutedSoft,
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      border: d.today ? `1px solid ${TC.orangeDark}` : `1px solid ${on ? TC.orangeBorder : TC.line}`,
                      transition: 'transform 120ms',
                    }}>
                      {on
                        ? (multi
                            ? <MultiIcon color={d.today ? '#fff' : TC.orangeDark} />
                            : <Icon name={d.sessions[0].type} size={16} />)
                        : <span style={{ width: 4, height: 4, borderRadius: 2, background: TC.mutedSoft }} />
                      }
                      {multi && (
                        <span style={{
                          position: 'absolute', top: -4, right: -4,
                          minWidth: 16, height: 16, padding: '0 4px',
                          borderRadius: 8, background: TC.orangeDark, color: '#fff',
                          fontSize: 10, fontWeight: 800,
                          display: 'flex', alignItems: 'center', justifyContent: 'center',
                          border: '1.5px solid #fff',
                        }}>{d.sessions.length}</span>
                      )}
                    </div>
                    <div style={{ fontSize: 10, color: d.today ? TC.orangeDark : TC.muted, marginTop: 4, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.3 }}>{d.d}</div>
                  </button>
                );
              })}
            </div>
          </div>
        </Card>

        {/* Coach prompt */}
        <div ref={promptRef}>
        <Card onClick={onOpenChat} style={{
          display: 'flex', alignItems: 'center', gap: 12,
          background: `linear-gradient(135deg, ${TC.orange} 0%, ${TC.orangeDark} 100%)`,
          borderColor: TC.orangeDark,
          boxShadow: '0 4px 14px rgba(245,124,42,0.35)',
        }}>
          <div style={{
            width: 44, height: 44, borderRadius: 22,
            background: 'rgba(255,255,255,0.2)',
            border: '2px solid rgba(255,255,255,0.6)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            flexShrink: 0, overflow: 'hidden',
          }}>
            <Avatar size={38} />
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 14, fontWeight: 800, color: '#fff', letterSpacing: 0.2 }}>Vraag Turbocoach iets</div>
            <div style={{ fontSize: 12, color: 'rgba(255,255,255,0.88)', marginTop: 2, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
              “Moet ik morgen rustig of intensief?”
            </div>
          </div>
          <Icon name="chev" size={18} color="#fff" />
        </Card>
        </div>

        {/* Row: race predictions */}
        <Card onClick={onOpenPredictions} style={{ padding: 0, overflow: 'hidden' }}>
          <div style={{ padding: '14px 16px 10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <div>
              <div style={{ fontSize: 13, fontWeight: 700, color: TC.ink }}>Wedstrijdvoorspellingen</div>
              <div style={{ fontSize: 11, color: TC.muted, marginTop: 2 }}>Op basis van je laatste 30 dagen</div>
            </div>
            <Icon name="chev" size={18} color={TC.muted} />
          </div>
          <div style={{
            display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)',
            borderTop: `1px solid ${TC.line}`,
          }}>
            {RACE_PREDICTIONS.map((r, i) => (
              <div key={i} style={{
                padding: '12px 6px', textAlign: 'center',
                borderRight: i < 3 ? `1px solid ${TC.line}` : 'none',
              }}>
                <div style={{ fontSize: 10, color: TC.muted, fontWeight: 700, textTransform: 'uppercase' }}>{r.label}</div>
                <div style={{ fontSize: 15, fontWeight: 800, color: TC.ink, marginTop: 4, letterSpacing: -0.3 }}>{r.time}</div>
                <div style={{
                  fontSize: 10, fontWeight: 700, marginTop: 3,
                  color: r.trend === 'down' ? TC.good : TC.red,
                  display: 'inline-flex', alignItems: 'center', gap: 2,
                }}>
                  <Icon name={r.trend === 'down' ? 'arrowD' : 'arrowU'} size={10} /> {r.delta}
                </div>
              </div>
            ))}
          </div>
        </Card>

        {/* Sporturen per week — stacked bar chart */}
        <WeeklyHoursChart />

        {/* Recent activities */}
        <div>
          <SectionTitle>Recente activiteiten</SectionTitle>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {ACTIVITIES.slice(0, 5).map(a => (
              <ActivityRow key={a.id} a={a} onClick={() => onActivityClick(a)} />
            ))}
          </div>
        </div>
      </div>

      {/* FAB wordt door App gerenderd, gebaseerd op zichtbaarheid van de coach-prompt */}
    </div>
  );
}

// ─── Weekly hours stacked bar chart ─────────────────────────
let CHART_WEEKS = []; // live data
function TooltipRow({ color, label, hours }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'space-between',
      padding: '2px 0',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
        <span style={{ width: 8, height: 8, borderRadius: 2, background: color, display: 'inline-block' }} />
        <span style={{ color: '#c9c9d1' }}>{label}</span>
      </div>
      <span>{formatHours(hours)}</span>
    </div>
  );
}

function WeeklyHoursChart() {
  const [hoverIdx, setHoverIdx] = useState(null);
  const weeks = CHART_WEEKS;
  const maxTotal = Math.max(...weeks.map(w => w.run + w.bike + w.swim));
  const ceiling  = Math.ceil(maxTotal / 2) * 2 || 2; // round up to even hours
  const yTicks   = Array.from({ length: ceiling / 2 + 1 }, (_, i) => i * 2); // 0,2,4,6,8

  // colours — tuned to the TC palette
  const C = {
    run:  TC.orange,          // #d86f24
    bike: '#3ba6d6',          // brand-friendly teal-blue
    swim: '#2fb673',          // brand-friendly green
  };

  return (
    <div>
      <SectionTitle>Sporturen per week</SectionTitle>
      <div style={{
        background: '#fff',
        border: `1px solid ${TC.line}`,
        borderRadius: 14,
        padding: '14px 12px 10px',
        boxShadow: '0 1px 0 rgba(0,0,0,0.02)',
      }}>
        {/* Legend */}
        <div style={{
          display: 'flex', gap: 14, justifyContent: 'center',
          marginBottom: 12, fontSize: 11, fontWeight: 600,
        }}>
          {[
            { label: 'Lopen',   color: C.run },
            { label: 'Fietsen', color: C.bike },
            { label: 'Zwemmen', color: C.swim },
          ].map(l => (
            <div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 6, color: TC.ink2 }}>
              <span style={{ width: 10, height: 10, borderRadius: 3, background: l.color, display: 'inline-block' }} />
              {l.label}
            </div>
          ))}
        </div>

        {/* Plot area */}
        <div style={{ display: 'flex', gap: 4, alignItems: 'stretch' }}>
          {/* Y-axis */}
          <div style={{
            width: 22, flexShrink: 0,
            display: 'flex', flexDirection: 'column-reverse',
            justifyContent: 'space-between',
            fontSize: 9, color: TC.muted,
            paddingBottom: 34, // ruimte voor weeknr + maand onder de as
          }}>
            {yTicks.map(t => (
              <div key={t} style={{ textAlign: 'right', paddingRight: 4, lineHeight: 1 }}>{t}u</div>
            ))}
          </div>

          {/* Bars */}
          <div style={{
            flex: 1, position: 'relative', minWidth: 0,
          }}>
            {/* Grid lines */}
            <div style={{
              position: 'absolute', inset: '0 0 34px 0',
              display: 'flex', flexDirection: 'column-reverse',
              justifyContent: 'space-between',
              pointerEvents: 'none',
            }}>
              {yTicks.map((t, i) => (
                <div key={t} style={{
                  height: 1,
                  background: i === 0 ? TC.line : TC.lineSoft,
                }} />
              ))}
            </div>

            {/* Bar row */}
            <div style={{
              position: 'relative', height: 160,
              display: 'flex', alignItems: 'flex-end',
              gap: 3,
            }}>
              {weeks.map((w, i) => {
                const total = w.run + w.bike + w.swim;
                const hPct  = (total / ceiling) * 100;
                const runPct  = total ? (w.run  / total) * 100 : 0;
                const bikePct = total ? (w.bike / total) * 100 : 0;
                const swimPct = total ? (w.swim / total) * 100 : 0;
                const isHover = hoverIdx === i;
                return (
                  <div
                    key={i}
                    onMouseEnter={() => setHoverIdx(i)}
                    onMouseLeave={() => setHoverIdx(null)}
                    onClick={() => setHoverIdx(hoverIdx === i ? null : i)}
                    style={{
                      flex: 1, height: '100%', position: 'relative',
                      display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
                      minWidth: 0, cursor: 'pointer',
                    }}
                  >
                    {/* Tooltip */}
                    {isHover && (
                      <div style={{
                        position: 'absolute',
                        left: '50%', bottom: `calc(${hPct}% + 8px)`,
                        transform: 'translateX(-50%)',
                        background: TC.ink, color: '#fff',
                        borderRadius: 8, padding: '8px 10px',
                        fontSize: 11, lineHeight: 1.4,
                        whiteSpace: 'nowrap',
                        boxShadow: '0 6px 16px rgba(0,0,0,0.18)',
                        zIndex: 5, pointerEvents: 'none',
                        fontWeight: 600,
                      }}>
                        <div style={{ fontSize: 10, color: '#c9c9d1', fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 4 }}>
                          {w.week} {w.month}
                        </div>
                        {w.run  > 0 && <TooltipRow color={C.run}  label="Lopen"   hours={w.run} />}
                        {w.bike > 0 && <TooltipRow color={C.bike} label="Fietsen" hours={w.bike} />}
                        {w.swim > 0 && <TooltipRow color={C.swim} label="Zwemmen" hours={w.swim} />}
                        {total > 0 && (
                          <div style={{
                            borderTop: '1px solid rgba(255,255,255,0.18)',
                            marginTop: 6, paddingTop: 5,
                            display: 'flex', justifyContent: 'space-between', gap: 12,
                          }}>
                            <span style={{ color: '#c9c9d1' }}>Totaal</span>
                            <span>{formatHours(total)}</span>
                          </div>
                        )}
                        {total === 0 && <div style={{ color: '#c9c9d1' }}>Geen activiteit</div>}
                        {/* arrow */}
                        <div style={{
                          position: 'absolute', left: '50%', bottom: -4,
                          transform: 'translateX(-50%) rotate(45deg)',
                          width: 8, height: 8, background: TC.ink,
                        }} />
                      </div>
                    )}
                    <div title={`${w.week} ${w.month} · ${formatHours(total)}`} style={{
                      height: `${hPct}%`,
                      minHeight: total ? 3 : 0,
                      width: '100%',
                      display: 'flex', flexDirection: 'column-reverse',
                      borderRadius: '4px 4px 0 0',
                      overflow: 'hidden',
                      boxShadow: isHover ? '0 0 0 2px rgba(216,111,36,0.35)' : '0 1px 0 rgba(0,0,0,0.04)',
                      animation: `tc-bar-grow 520ms ${i * 30}ms ease-out both`,
                      transition: 'box-shadow 120ms',
                    }}>
                      {w.run  > 0 && <div style={{ height: `${runPct}%`,  background: C.run }} />}
                      {w.bike > 0 && <div style={{ height: `${bikePct}%`, background: C.bike }} />}
                      {w.swim > 0 && <div style={{ height: `${swimPct}%`, background: C.swim }} />}
                    </div>
                  </div>
                );
              })}
            </div>

            {/* X-axis labels — weeknr + maand alleen bij wissel */}
            <div style={{
              display: 'flex', gap: 3, marginTop: 6,
            }}>
              {weeks.map((w, i) => {
                const showMonth = i === 0 || weeks[i - 1].month !== w.month;
                return (
                  <div key={i} style={{
                    flex: 1, minWidth: 0,
                    textAlign: 'center',
                    display: 'flex', flexDirection: 'column', gap: 2,
                  }}>
                    <div style={{
                      fontSize: 9, color: TC.muted, fontWeight: 700,
                      lineHeight: 1.1,
                    }}>{w.week.replace('W', '')}</div>
                    <div style={{
                      fontSize: 8.5, color: showMonth ? TC.orangeDark : 'transparent',
                      fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4,
                      lineHeight: 1.1,
                    }}>{showMonth ? w.month : '·'}</div>
                  </div>
                );
              })}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function IconBtn({ children, onClick, title, spin }) {
  return (
    <button onClick={onClick} title={title} style={{
      width: 36, height: 36, borderRadius: 10,
      background: TC.bgSoft, border: `1px solid ${TC.line}`,
      color: TC.ink2, cursor: 'pointer',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      animation: spin ? 'tc-spin 1s linear infinite' : 'none',
    }}>{children}</button>
  );
}

function ActivityRow({ a, onClick }) {
  return (
    <div onClick={onClick} style={{
      display: 'flex', alignItems: 'center', gap: 12,
      background: '#fff', border: `1px solid ${TC.line}`,
      borderRadius: 14, padding: '12px 14px',
      cursor: 'pointer',
    }}>
      <div style={{
        width: 40, height: 40, borderRadius: 10,
        background: TC.orangeSoft, color: TC.orangeDark,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        <Icon name={a.type} size={20} />
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.name}</div>
        <div style={{ fontSize: 11, color: TC.muted, marginTop: 2 }}>{a.date}</div>
      </div>
      <div style={{ textAlign: 'right' }}>
        <div style={{ fontSize: 14, fontWeight: 800, color: TC.ink }}>{a.dist}{a.type === 'swim' ? ' m' : ' km'}</div>
        <div style={{ fontSize: 11, color: TC.muted, marginTop: 2 }}>{a.type === 'bike' ? a.speed + ' km/u' : a.pace + (a.type === 'swim' ? ' /100m' : ' /km')}</div>
      </div>
    </div>
  );
}

// ─── Beta banner ────────────────────────────────────────────
function BetaBanner() {
  const [show, setShow] = useState(false);
  useEffect(() => {
    // Per spec: only show when 'beta' in hostname. We fake it via URL param or simulate on
    const h = window.location.hostname.toLowerCase();
    const q = new URLSearchParams(window.location.search);
    setShow(h.includes('beta') || q.get('beta') === '1');
  }, []);
  if (!show) return null;
  return (
    <div style={{
      background: TC.orangeSoft,
      borderBottom: `1px solid ${TC.orangeBorder}`,
      color: TC.orangeDark,
      fontSize: 12, fontWeight: 700,
      padding: '8px 16px',
      display: 'flex', alignItems: 'center', gap: 8,
      letterSpacing: 0.4,
    }}>
      <span style={{
        background: TC.orange, color: '#fff',
        fontSize: 10, fontWeight: 800, padding: '2px 8px', borderRadius: 999, letterSpacing: 1,
      }}>BETA</span>
      Je test de publieke beta. Feedback? Stuur Turbocoach een bericht.
    </div>
  );
}

// ─── Coach chat overlay ─────────────────────────────────────
function CoachOverlay({ open, onClose, contextActivity }) {
  const [messages, setMessages] = useState(COACH_SEED);
  const [input, setInput] = useState('');
  const [thinking, setThinking] = useState(null); // { label, phase }
  const [revealedIdx, setRevealedIdx] = useState({}); // messageIndex -> block count revealed
  const scrollRef = useRef(null);

  useEffect(() => {
    if (open && contextActivity) {
      const isTip = contextActivity.isTip;
      setMessages([
        { role: 'coach', blocks: [{
          type: 'intro',
          text: isTip
            ? `Top, ik ga met "${contextActivity.name}" aan de slag. Op basis van je laatste 3 weken zie ik je drempel +0,12 w/kg, en je halve-marathontempo daalt. Wil je dat ik een piekblok plan?`
            : `Je tikte op "${contextActivity.name}". Wil je dat ik ${contextActivity.type === 'run' ? 'de tempoverdeling' : contextActivity.type === 'bike' ? 'het vermogen' : 'de splits'} analyseer, of liever advies voor morgen?`,
        }] }
      ]);
    }
  }, [open, contextActivity]);

  useEffect(() => {
    if (!open) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await api('/api/chat/history');
        if (cancelled) return;
        const msgs = (r.messages || []).slice(-100).map(m => ({
          role: m.role === 'assistant' ? 'coach' : 'user',
          text: m.content,
          blocks: m.role === 'assistant' ? parseCoachResponse(m.content) : undefined,
        }));
        if (msgs.length > 0) {
          setMessages(msgs);
          const r2 = {};
          msgs.forEach((m, i) => { if (m.role === 'coach') r2[i] = (m.blocks ? m.blocks.length : 1); });
          setRevealedIdx(r2);
        }
        if (r.conversation_id) window.__tcConv = r.conversation_id;
      } catch (e) { /* silent */ }

      // Hervat een eventuele pending chat-task: handig na fetch-failure of refresh.
      try {
        const raw = localStorage.getItem('tc_pendingTask');
        if (raw && !cancelled) {
          const pending = JSON.parse(raw);
          // Maximaal 10 minuten oud — oudere tasks zijn waarschijnlijk al lang afgerond/cleaned.
          if (pending && pending.taskId && (Date.now() - (pending.ts || 0)) < 10 * 60 * 1000) {
            setThinking({ label: 'Antwoord ophalen', ms: 9999 });
            await _chatRunTask(pending.taskId, pending.userText || '', null);
          } else {
            localStorage.removeItem('tc_pendingTask');
          }
        }
      } catch (e) { /* silent */ }
    })();
    return () => { cancelled = true; };
  }, [open]);

  // Scroll: bij nieuwe coach-msg ankert de TOP van dat bericht bovenaan;
  // bij user-msg of thinking-indicator scrollen we naar onder zoals gewoonlijk.
  // Block-reveals (revealedIdx) triggeren GEEN scroll meer — anders zou het
  // antwoord onder je ogen vandaan vegen terwijl je leest.
  const prevLenRef = useRef(0);
  useEffect(() => {
    const sc = scrollRef.current;
    if (!sc) return;
    const prevLen = prevLenRef.current;
    const grew = messages.length > prevLen;
    const last = messages[messages.length - 1];
    prevLenRef.current = messages.length;
    if (grew && last && last.role === 'coach') {
      requestAnimationFrame(() => {
        const el = sc.querySelector(`[data-msg-idx="${messages.length - 1}"]`);
        if (el && typeof el.offsetTop === 'number') {
          sc.scrollTop = Math.max(0, el.offsetTop - 8);
        } else {
          sc.scrollTop = sc.scrollHeight;
        }
      });
    } else {
      sc.scrollTop = sc.scrollHeight;
    }
  }, [messages, thinking, open]);

  // Retry helper: bij netwerk-fouten (TypeError 'failed to fetch', geen status)
  // proberen we maxRetries keer met exponentiele backoff. HTTP 4xx/5xx (heeft
  // wel een status) gaan meteen door zonder retry.
  async function _chatRetry(path, opts, maxRetries = 3, baseMs = 800) {
    let lastErr;
    for (let i = 0; i <= maxRetries; i++) {
      try { return await api(path, opts); }
      catch (e) {
        lastErr = e;
        if (e && e.status) throw e;       // server gaf antwoord, geen netwerk-fout
        if (i < maxRetries) {
          await new Promise(r => setTimeout(r, baseMs * Math.pow(2, i)));
        }
      }
    }
    throw lastErr;
  }

  // Hervat een lopende task: gebruikt door zowel send() als de resume-useEffect.
  async function _chatRunTask(taskId, userText, phaseTimer) {
    try {
      let tries = 0;
      let consecutiveNetErr = 0;
      while (tries < 180) {
        await new Promise(res => setTimeout(res, 1000));
        let ss;
        try {
          ss = await api('/api/chat/task/' + taskId);
          consecutiveNetErr = 0;
        } catch (e) {
          if (e && e.status) {
            // 4xx/5xx — meteen falen
            throw e;
          }
          // Netwerk-blip: tel mee maar blijf doorgaan tot 8 opeenvolgende mislukten.
          consecutiveNetErr++;
          if (consecutiveNetErr >= 8) throw e;
          tries++;
          continue;
        }
        if (ss.status === 'done') {
          if (phaseTimer) clearInterval(phaseTimer);
          setThinking(null);
          setMessages(m => {
            const next = [...m, { role: 'coach', text: ss.response, blocks: parseCoachResponse(ss.response) }];
            const msgIdx = next.length - 1;
            setRevealedIdx(r => ({ ...r, [msgIdx]: next[msgIdx].blocks.length }));
            return next;
          });
          try { localStorage.removeItem('tc_pendingTask'); } catch(e) {}
          // Persist user + assistant berichten server-side (best-effort).
          try {
            if (userText) {
              const cid = window.__tcConv || null;
              const u = await _chatRetry('/api/chat/history' + (cid ? '?conversation_id=' + cid : ''),
                { method: 'POST', body: { role: 'user', content: userText } });
              window.__tcConv = u.conversation_id;
              await _chatRetry('/api/chat/history?conversation_id=' + window.__tcConv,
                { method: 'POST', body: { role: 'assistant', content: ss.response } });
            }
          } catch (e) { console.warn('chat persist failed', e); }
          return true;
        }
        if (ss.status === 'error') throw new Error(ss.error || 'Chatfout');
        tries++;
      }
      throw new Error('Tijd verstreken');
    } catch (ex) {
      if (phaseTimer) clearInterval(phaseTimer);
      setThinking(null);
      const isNetErr = !ex.status;
      const msg = isNetErr
        ? 'Verbinding kwijt voor ik je antwoord kreeg. Probeer je vraag opnieuw te sturen — als het antwoord nog onderweg was komt het ook terug bij volgende open van de chat.'
        : ('Er ging iets mis: ' + (ex.message || 'onbekende fout'));
      setMessages(m => {
        const next = [...m, { role: 'coach', blocks: [{ type: 'intro', text: msg }] }];
        const msgIdx = next.length - 1;
        setRevealedIdx(r => ({ ...r, [msgIdx]: next[msgIdx].blocks.length }));
        return next;
      });
      try { localStorage.removeItem('tc_pendingTask'); } catch(e) {}
      return false;
    }
  }

  function send(textOverride) {
    const t = (textOverride ?? input).trim();
    if (!t) return;
    setMessages(m => [...m, { role: 'user', text: t }]);
    setInput('');
    const phases = [
      { label: 'Gezondheidsdata ophalen', ms: 9999 },
      { label: 'Training-context wegen',  ms: 9999 },
      { label: 'Voorstel opbouwen',       ms: 9999 },
    ];
    let phaseIdx = 0;
    setThinking(phases[0]);
    const phaseTimer = setInterval(() => {
      phaseIdx = Math.min(phaseIdx + 1, 2);
      setThinking(phases[phaseIdx]);
    }, 1800);
    (async () => {
      let taskId = null;
      try {
        const hist = messages.filter(m => m.role === 'user' || m.role === 'coach').map(m => ({
          role: m.role === 'coach' ? 'assistant' : 'user',
          content: typeof m.text === 'string' ? m.text : ((m.blocks && m.blocks[0] && m.blocks[0].tldr) || '')
        }));
        const r = await _chatRetry('/api/chat/async', {
          method: 'POST',
          body: { message: t, history: hist, activity_id: (contextActivity && contextActivity.raw && contextActivity.raw.activity_id) || null, conversation_id: window.__tcConv || null }
        }, 3);
        taskId = r.task_id;
        try { localStorage.setItem('tc_pendingTask', JSON.stringify({ taskId, userText: t, ts: Date.now() })); } catch(e) {}
      } catch (ex) {
        clearInterval(phaseTimer);
        setThinking(null);
        const msg = ex && ex.status
          ? 'Er ging iets mis: ' + (ex.message || 'onbekende fout')
          : 'Geen verbinding met de coach. Controleer je internet en probeer opnieuw.';
        setMessages(m => {
          const next = [...m, { role: 'coach', blocks: [{ type: 'intro', text: msg }] }];
          const msgIdx = next.length - 1;
          setRevealedIdx(r => ({ ...r, [msgIdx]: next[msgIdx].blocks.length }));
          return next;
        });
        return;
      }
      await _chatRunTask(taskId, t, phaseTimer);
    })();
  }

  if (!open) return null;

  const hasUserSpoken = messages.some(m => m.role === 'user');

  return (
    <div style={{
      position: 'absolute', inset: 0, background: TC.bgSoft, zIndex: 40,
      display: 'flex', flexDirection: 'column',
      animation: 'tc-slide-up 220ms ease-out',
    }}>
      {/* Header */}
      <div style={{
        padding: '12px 14px',
        borderBottom: `1px solid ${TC.line}`,
        display: 'flex', alignItems: 'center', gap: 12,
        background: '#fff',
      }}>
        <Avatar size={40} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: TC.ink, letterSpacing: 0.2 }}>Turbocoach</div>
          <div style={{ fontSize: 11, color: TC.muted, display: 'flex', alignItems: 'center', gap: 5 }}>
            <span style={{ width: 6, height: 6, borderRadius: 3, background: TC.good, display: 'inline-block' }} />
            Online
          </div>
        </div>
        <button onClick={onClose} aria-label="Sluiten" style={{
          width: 36, height: 36, borderRadius: 10,
          background: TC.bgSoft, border: `1px solid ${TC.line}`,
          color: TC.ink2, cursor: 'pointer',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          flexShrink: 0,
        }}>
          <Icon name="close" size={18} />
        </button>
      </div>

      {/* Messages */}
      <div ref={scrollRef} style={{
        flex: 1, overflowY: 'auto', padding: '16px 14px 20px',
        display: 'flex', flexDirection: 'column', gap: 14,
      }}>
        {messages.map((m, i) => (
          <div key={i} data-msg-idx={i}>
            <ChatMessage
              msg={m}
              revealCount={revealedIdx[i] ?? (m.blocks ? m.blocks.length : 1)}
            />
          </div>
        ))}

        {thinking && <ThinkingIndicator phase={thinking} />}

        {!hasUserSpoken && !thinking && (
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 2 }}>
            {[
              'Kan ik tussen 60 en 80 min rollen vandaag, wat stel je voor?',
              'Analyseer m’n laatste run',
              'Moet ik rusten?',
              'Zet VO₂-workout klaar',
            ].map(q => (
              <button key={q} onClick={() => send(q)} style={{
                padding: '8px 12px', borderRadius: 999,
                border: `1px solid ${TC.orangeBorder}`,
                background: '#fff', color: TC.orangeDark,
                fontSize: 12, fontWeight: 600, cursor: 'pointer',
                textAlign: 'left',
              }}>{q}</button>
            ))}
          </div>
        )}
      </div>

      {/* Input */}
      <div style={{
        padding: '10px 12px',
        paddingBottom: 'calc(20px + env(safe-area-inset-bottom, 0px))',
        borderTop: `1px solid ${TC.line}`,
        display: 'flex', alignItems: 'flex-end', gap: 8, background: '#fff',
      }}>
        <textarea
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
          placeholder="Stel een vraag aan je coach…"
          rows={1}
          style={{
            flex: 1, resize: 'none', minHeight: 42, maxHeight: 120,
            padding: '10px 14px', borderRadius: 14,
            border: `1px solid ${TC.line}`, background: TC.bgSoft,
            fontSize: 14, fontFamily: 'inherit', color: TC.ink,
            outline: 'none',
          }}
        />
        <button onClick={() => send()} disabled={!input.trim() || !!thinking} style={{
          width: 42, height: 42, borderRadius: 14,
          background: input.trim() && !thinking ? TC.orange : TC.lineSoft,
          color: input.trim() && !thinking ? '#fff' : TC.mutedSoft,
          border: 'none', cursor: input.trim() && !thinking ? 'pointer' : 'default',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          flexShrink: 0,
        }}>
          <Icon name="send" size={18} />
        </button>
      </div>
    </div>
  );
}

// ─── Thinking animation ─────────────────────────────────────
function ThinkingIndicator({ phase }) {
  return (
    <div style={{
      display: 'flex', gap: 10, alignItems: 'center',
      animation: 'tc-fade 200ms ease-out',
    }}>
      <div style={{
        width: 28, height: 28, borderRadius: 14,
        background: TC.orangeSoft, color: TC.orangeDark,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        flexShrink: 0,
        animation: 'tc-pulse 1.4s infinite ease-in-out',
      }}>
        <Icon name="spark" size={16} />
      </div>
      <div style={{
        padding: '10px 14px',
        background: '#fff',
        border: `1px solid ${TC.line}`,
        borderRadius: 14,
        fontSize: 13, color: TC.ink2,
        display: 'flex', alignItems: 'center', gap: 10,
        minHeight: 38,
      }}>
        <span style={{ fontWeight: 600, color: TC.ink }}>{phase.label}</span>
        <span style={{ display: 'inline-flex', gap: 4 }}>
          <Dot delay={0} /><Dot delay={0.15} /><Dot delay={0.3} />
        </span>
      </div>
    </div>
  );
}

function Dot({ delay }) {
  return <span style={{
    width: 6, height: 6, borderRadius: 3, background: TC.orange, display: 'inline-block',
    animation: `tc-bounce 1.2s ${delay}s infinite ease-in-out`,
  }} />;
}

// Render user messages met nette structuur.
// ALTIJD toegepast op zowel nieuwe berichten als chat-history load,
// zodat lange paragrafen nooit als wall-of-text verschijnen.
function renderUserText(text) {
  if (!text) return null;
  let lines = String(text).split(/\r?\n/);
  lines = lines.flatMap(ln => {
    const trimmed = ln.trim();
    if (trimmed.length <= 120) return [ln];
    const sentences = trimmed.split(/([.!?])\s+(?=[A-Z0-9À-ſ])/);
    const out = [];
    for (let i = 0; i < sentences.length; i += 2) {
      const body = sentences[i];
      const punct = sentences[i + 1] || '';
      if (body) out.push((body + punct).trim());
    }
    return out.length > 1 ? out : [ln];
  });
  function inline(s) {
    const parts = [];
    let rest = s; let key = 0;
    const rx = /(\*\*([^*]+)\*\*|\*([^*]+)\*)/;
    while (rest.length) {
      const m = rest.match(rx);
      if (!m) { parts.push(rest); break; }
      if (m.index > 0) parts.push(rest.slice(0, m.index));
      if (m[2]) parts.push(React.createElement('strong', { key: key++ }, m[2]));
      else if (m[3]) parts.push(React.createElement('em', { key: key++ }, m[3]));
      rest = rest.slice(m.index + m[0].length);
    }
    return parts;
  }
  return lines.map((ln, i) => {
    const t = ln.trim();
    if (!t) return React.createElement('div', { key: i, style: { height: 6 } });
    const isLast = i === lines.length - 1;
    const mb = isLast ? 0 : 6;
    const labelMatch = t.match(/^([A-Za-z][A-Za-z0-9 &/_.-]{0,39}):\s+(.+)$/);
    if (labelMatch) {
      return React.createElement('div', { key: i, style: { marginBottom: mb } }, [
        React.createElement('strong', { key: 'l', style: { fontWeight: 700 } }, labelMatch[1]),
        ': ',
        ...inline(labelMatch[2]),
      ]);
    }
    const isQuestion = t.endsWith('?');
    return React.createElement('div', { key: i, style: { marginBottom: mb, fontWeight: isQuestion ? 600 : 500 } }, inline(t));
  });
}

// ─── Chat message ───────────────────────────────────────────
function ChatMessage({ msg, revealCount }) {
  if (msg.role === 'user') {
    return (
      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
        <div style={{
          maxWidth: '82%',
          padding: '10px 14px',
          borderRadius: '16px 16px 4px 16px',
          background: TC.orange, color: '#fff',
          fontSize: 14, lineHeight: 1.5, fontWeight: 500,
          boxShadow: '0 1px 2px rgba(216,111,36,0.25)',
          display: 'flex', flexDirection: 'column', gap: 6,
        }}>{renderUserText(msg.text || '')}</div>
      </div>
    );
  }

  // Coach message with rich blocks
  const blocks = msg.blocks || [];
  const n = Math.min(blocks.length, revealCount);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <Avatar size={22} />
        <div style={{ fontSize: 11, fontWeight: 700, color: TC.ink2, letterSpacing: 0.3 }}>
          Turbocoach
        </div>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {blocks.slice(0, n).map((b, i) => (
          <div key={i} style={{ animation: 'tc-rise 280ms ease-out both' }}>
            <CoachBlock block={b} />
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Block renderer ─────────────────────────────────────────
function CoachBlock({ block }) {
  switch (block.type) {
    case 'intro':
      return (
        <div style={{
          padding: '12px 14px',
          background: '#fff',
          border: `1px solid ${TC.line}`,
          borderRadius: 14,
          fontSize: 14, lineHeight: 1.55, color: TC.ink, whiteSpace: 'pre-wrap',
        }}>{block.text}</div>
      );

    case 'summary':
      return (
        <div style={{
          padding: '14px 14px 12px',
          background: '#fff',
          border: `1px solid ${TC.line}`,
          borderLeft: `3px solid ${TC.orange}`,
          borderRadius: 14,
        }}>
          <div style={{
            fontSize: 10, fontWeight: 700, letterSpacing: 1,
            color: TC.orangeDark, textTransform: 'uppercase',
            marginBottom: 6,
          }}>Samenvatting</div>
          <div style={{
            fontSize: 14, lineHeight: 1.55, color: TC.ink, fontWeight: 500,
            marginBottom: 10,
          }}>{block.tldr}</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
            {block.bullets.map((b, i) => (
              <div key={i} style={{
                display: 'flex', justifyContent: 'space-between', gap: 10,
                fontSize: 12,
                paddingTop: i === 0 ? 6 : 0,
                borderTop: i === 0 ? `1px solid ${TC.lineSoft}` : 'none',
              }}>
                <span style={{ color: TC.muted }}>{b.label}</span>
                <span style={{ color: TC.ink, fontWeight: 600, textAlign: 'right' }}>{b.value}</span>
              </div>
            ))}
          </div>
        </div>
      );

    case 'section':
      return <CoachSection block={block} />;

    case 'outro':
      return (
        <div style={{
          padding: '12px 14px',
          background: TC.orangeSoft,
          border: `1px solid ${TC.orangeBorder}`,
          borderRadius: 14,
          fontSize: 13.5, lineHeight: 1.55, color: TC.ink,
          fontWeight: 500, whiteSpace: 'pre-wrap',
        }}>{block.text}</div>
      );

    case 'workout':
      return <WorkoutCard block={block} />;

    default: return null;
  }
}

function CoachSection({ block }) {
  // Render parts array (sequential text/table/bullets) als die aanwezig is,
  // anders backward-compat via block.text/block.table/block.bullets.
  const parts = Array.isArray(block.parts) && block.parts.length
    ? block.parts
    : [
        block.text ? { type: 'text', text: block.text } : null,
        block.table ? { type: 'table', table: block.table } : null,
        block.bullets ? { type: 'bullets', bullets: block.bullets } : null,
      ].filter(Boolean);

  return (
    <div style={{
      padding: '13px 14px 14px',
      background: '#fff',
      border: `1px solid ${TC.line}`,
      borderRadius: 14,
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8,
        marginBottom: 10,
        paddingBottom: 9,
        borderBottom: `1px solid ${TC.lineSoft}`,
      }}>
        <div style={{
          width: 24, height: 24, borderRadius: 7,
          background: TC.orangeSoft, color: TC.orangeDark,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          flexShrink: 0,
        }}>
          <Icon name={block.icon || 'info'} size={14} />
        </div>
        <div style={{
          fontSize: 13, fontWeight: 700, color: TC.ink, flex: 1, minWidth: 0,
        }}>{block.title}</div>
      </div>

      {block.highlight && (
        <div style={{
          fontSize: 13, lineHeight: 1.5, color: TC.orangeDark, fontWeight: 700,
          marginBottom: 8,
        }}>{block.highlight}</div>
      )}

      {parts.map((p, i) => {
        const isLast = i === parts.length - 1;
        const mb = isLast ? 0 : 10;
        if (p.type === 'text') {
          return (
            <div key={i} style={{
              fontSize: 13, lineHeight: 1.55, color: TC.ink2,
              marginBottom: mb,
            }}>{p.text}</div>
          );
        }
        if (p.type === 'subheader') {
          return (
            <div key={i} style={{
              fontSize: 12, fontWeight: 700, color: TC.ink,
              textTransform: 'uppercase', letterSpacing: 0.3,
              marginTop: i === 0 ? 0 : 4,
              marginBottom: isLast ? 0 : 8,
            }}>{p.text}</div>
          );
        }
        if (p.type === 'quote') {
          return (
            <div key={i} style={{
              fontSize: 13, lineHeight: 1.5, color: TC.ink2,
              fontStyle: 'italic',
              borderLeft: `3px solid ${TC.orange}`,
              paddingLeft: 10,
              marginBottom: mb,
            }}>{p.text}</div>
          );
        }
        if (p.type === 'table') {
          return (
            <div key={i} style={{ marginBottom: mb }}>
              <CoachTable table={p.table} />
            </div>
          );
        }
        if (p.type === 'bullets') {
          return (
            <div key={i} style={{
              display: 'flex', flexDirection: 'column', gap: 6,
              marginBottom: mb,
            }}>
              {p.bullets.map((b, j) => <BulletLine key={j} b={b} />)}
            </div>
          );
        }
        return null;
      })}

      {block.footer && (
        <div style={{
          marginTop: 10, paddingTop: 8,
          borderTop: `1px solid ${TC.lineSoft}`,
          fontSize: 12, fontWeight: 600,
          color: block.footer.type === 'ok' ? TC.good : TC.ink2,
          display: 'flex', alignItems: 'center', gap: 6,
        }}>
          {block.footer.type === 'ok' && <Icon name="check" size={14} />}
          {block.footer.text}
        </div>
      )}
    </div>
  );
}

function BulletLine({ b }) {
  return (
    <div style={{
      display: 'flex', gap: 8, fontSize: 13, lineHeight: 1.5,
      color: b.highlight ? TC.orangeDark : TC.ink2,
      fontWeight: b.highlight ? 700 : 400,
    }}>
      <span style={{
        width: 4, height: 4, borderRadius: 2,
        background: b.highlight ? TC.orange : TC.mutedSoft,
        flexShrink: 0, marginTop: 8,
      }} />
      <div style={{ flex: 1, minWidth: 0 }}>
        {b.bold && <strong style={{ color: TC.ink, fontWeight: 700 }}>{b.bold}: </strong>}
        <span>{b.text}</span>
        {b.bold2 && <strong style={{ color: TC.ink, fontWeight: 700 }}>{b.bold2}</strong>}
        {b.textAfter && <span>{b.textAfter}</span>}
      </div>
    </div>
  );
}

function CoachTable({ table }) {
  return (
    <div style={{
      borderRadius: 10, overflow: 'hidden',
      border: `1px solid ${TC.lineSoft}`,
    }}>
      <div style={{ overflowX: 'auto' }}>
        <table style={{
          width: '100%', borderCollapse: 'collapse',
          fontSize: 12, color: TC.ink2,
        }}>
          <thead>
            <tr>
              {table.head.map((h, i) => (
                <th key={i} style={{
                  padding: '8px 10px',
                  textAlign: 'left',
                  fontSize: 10.5, fontWeight: 700,
                  color: TC.muted, textTransform: 'uppercase', letterSpacing: 0.6,
                  background: TC.bgSoft,
                  borderBottom: `1px solid ${TC.lineSoft}`,
                  whiteSpace: 'nowrap',
                }}>{h}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {table.rows.map((row, ri) => (
              <tr key={ri}>
                {row.map((cell, ci) => (
                  <td key={ci} style={{
                    padding: '8px 10px',
                    borderTop: ri > 0 ? `1px solid ${TC.lineSoft}` : 'none',
                    fontSize: 12, lineHeight: 1.4,
                    verticalAlign: 'top',
                    color: ci === 0 ? TC.ink : TC.ink2,
                    fontWeight: ci === 0 ? 600 : 400,
                  }}>
                    <TableCell cell={cell} />
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function TableCell({ cell }) {
  if (cell && typeof cell === 'object') {
    if (cell.ok) {
      return (
        <span style={{
          display: 'inline-flex', alignItems: 'center', gap: 5,
          color: TC.good, fontWeight: 600,
        }}>
          <span style={{
            width: 16, height: 16, borderRadius: 4,
            background: '#e6f7ed',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Icon name="check" size={11} color={TC.good} />
          </span>
          <span style={{ color: TC.ink2, fontWeight: 500 }}>{cell.text}</span>
        </span>
      );
    }
    if (cell.em) {
      return <strong style={{ color: TC.orangeDark, fontWeight: 700 }}>{cell.em}</strong>;
    }
  }
  return <span>{cell}</span>;
}

function WorkoutCard({ block }) {
  const [pushed, setPushed] = useState(false);
  const [pushing, setPushing] = useState(false);
  const [pushError, setPushError] = useState(null);
  async function doPush() {
    if (pushing || pushed) return;
    setPushing(true); setPushError(null);
    try {
      await api('/api/workout/garmin', { method: 'POST', body: block.raw });
      setPushed(true);
    } catch (e) {
      setPushError((e && e.message) ? e.message : 'Upload mislukt');
    } finally {
      setPushing(false);
    }
  }
  return (
    <div style={{
      padding: 14,
      background: 'linear-gradient(180deg, #1b2236 0%, #151b2d 100%)',
      border: `1px solid #2a3350`,
      borderRadius: 14,
      color: '#fff',
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4,
      }}>
        <div style={{
          width: 22, height: 22, borderRadius: 6,
          background: 'rgba(255,255,255,0.1)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <Icon name="bolt" size={13} color="#ffbf8a" />
        </div>
        <div style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: 0.8, color: '#ffbf8a', textTransform: 'uppercase' }}>
          Workout klaar om te pushen
        </div>
      </div>
      <div style={{ fontSize: 15, fontWeight: 700, color: '#fff', marginBottom: 2 }}>
        {block.name}
      </div>
      <div style={{ fontSize: 12, color: '#b0bacd', marginBottom: 12 }}>
        {block.sport} · {block.duration}
      </div>

      <div style={{
        display: 'flex', flexDirection: 'column',
        background: 'rgba(255,255,255,0.04)',
        border: `1px solid rgba(255,255,255,0.08)`,
        borderRadius: 10, overflow: 'hidden',
        marginBottom: 14,
      }}>
        {block.steps.map((s, i) => (
          <div key={i} style={{
            display: 'flex', gap: 10, alignItems: 'center',
            padding: '9px 12px',
            borderTop: i === 0 ? 'none' : `1px solid rgba(255,255,255,0.06)`,
            fontSize: 12,
          }}>
            <div style={{
              width: 18, height: 18, borderRadius: 4,
              background: 'rgba(255,191,138,0.15)', color: '#ffbf8a',
              fontSize: 10, fontWeight: 700,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              flexShrink: 0,
            }}>{i + 1}</div>
            <div style={{ color: '#fff', fontWeight: 600, minWidth: 88 }}>{s.label}</div>
            <div style={{ color: '#b0bacd', flex: 1 }}>{s.detail}</div>
          </div>
        ))}
      </div>

      <button
        disabled={pushed || pushing}
        onClick={doPush}
        style={{
          width: '100%',
          padding: '12px 14px',
          borderRadius: 12,
          border: 'none',
          cursor: pushed ? 'default' : 'pointer',
          background: pushed ? '#1f7a4d' : '#22c074',
          color: '#fff',
          fontSize: 14, fontWeight: 700,
          display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
          transition: 'background 180ms ease',
          boxShadow: pushed ? 'none' : '0 4px 12px rgba(34,192,116,0.35)',
        }}
      >
        {pushed ? (
          <>
            <Icon name="check" size={16} /> Gepusht naar Garmin
          </>
        ) : (
          <>
            <Icon name="garmin" size={16} /> Push naar Garmin
          </>
        )}
      </button>
      {pushed && (
        <div style={{
          marginTop: 8, textAlign: 'center',
          fontSize: 11, color: '#b0bacd',
        }}>
          Staat klaar in Garmin Connect. Push de workout naar je toestel via de app.
        </div>
      )}
    </div>
  );
}

// ─── Race predictions modal ─────────────────────────────────
function RacePredictionsSheet({ open, onClose, onTipTap }) {
  const [explainOpen, setExplainOpen] = useState(false);
  if (!open) return null;
  return (
    <SheetFrame onClose={onClose} title="Wedstrijdvoorspellingen">
      <div style={{ padding: '6px 4px 0', fontSize: 13, color: TC.muted, lineHeight: 1.5 }}>
        Verwachte tijden bij je huidige vorm. Gebaseerd op VO₂max-trend, drempelritten en recente ritten.
      </div>

      <div style={{
        display: 'flex', alignItems: 'center', gap: 8, margin: '14px 0',
      }}>
        <button onClick={() => setExplainOpen(v => !v)} style={{
          padding: '8px 12px', borderRadius: 999,
          border: `1px solid ${TC.line}`, background: '#fff',
          color: TC.ink2, fontSize: 12, fontWeight: 600, cursor: 'pointer',
          display: 'flex', alignItems: 'center', gap: 6,
        }}>
          <Icon name="info" size={14} /> Hoe wordt dit berekend?
          <Icon name={explainOpen ? 'chevDn' : 'chev'} size={12} />
        </button>
      </div>

      {explainOpen && (
        <div style={{
          background: TC.blueSoft, border: `1px solid #cbdbff`,
          borderRadius: 12, padding: 12, color: TC.ink2, fontSize: 13, lineHeight: 1.5,
          marginBottom: 12,
        }}>
          Turbocoach weegt je laatste 30 dagen: VO₂max-schatting (50%), drempelritten (30%) en recente wedstrijden (20%). Voorspellingen zijn indicatief — weer, route en rust tellen ook.
        </div>
      )}

      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {RACE_PREDICTIONS.map((r, i) => (
          <div key={i} style={{
            display: 'flex', alignItems: 'center', gap: 12,
            background: '#fff', border: `1px solid ${TC.line}`,
            borderRadius: 14, padding: '14px 16px',
          }}>
            <div style={{
              minWidth: 60, fontSize: 12, color: TC.muted, fontWeight: 700, letterSpacing: 0.5, textTransform: 'uppercase',
            }}>{r.label}</div>
            <div style={{ flex: 1, fontSize: 24, fontWeight: 800, color: TC.ink, letterSpacing: -0.5 }}>{r.time}</div>
            <div style={{
              padding: '4px 10px', borderRadius: 999,
              background: r.trend === 'down' ? TC.goodSoft : '#fdecec',
              color: r.trend === 'down' ? TC.good : TC.red,
              fontSize: 12, fontWeight: 700,
              display: 'flex', alignItems: 'center', gap: 4,
            }}>
              <Icon name={r.trend === 'down' ? 'arrowD' : 'arrowU'} size={12} /> {r.delta}
            </div>
          </div>
        ))}
      </div>

      <button onClick={onTipTap} style={{
        width: '100%', textAlign: 'left',
        marginTop: 14, padding: 12, borderRadius: 12,
        background: TC.orangeSoft, border: `1px solid ${TC.orangeBorder}`,
        display: 'flex', gap: 10, alignItems: 'center',
        cursor: 'pointer',
      }}>
        <Avatar size={34} />
        <div style={{ flex: 1, fontSize: 13, color: TC.ink2, lineHeight: 1.5 }}>
          <strong style={{ color: TC.ink }}>Tip van Turbocoach:</strong> je halve-marathontijd daalt al drie weken. Wil je dat ik een piekblok inplan richting je 21.1K doel?
        </div>
        <Icon name="chev" size={18} color={TC.orangeDark} />
      </button>
    </SheetFrame>
  );
}

// ─── ERG power zones sheet ──────────────────────────────────
function PowerSheet({ open, onClose }) {
  if (!open) return null;
  return (
    <SheetFrame onClose={onClose} title="ERG power zones">
      <div style={{ padding: '4px 4px 14px', fontSize: 13, color: TC.muted, lineHeight: 1.5 }}>
        Je FTP is ingesteld op <strong style={{ color: TC.ink }}>252 W</strong>. Zones zijn berekend op basis van je laatste 20-min test.
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {POWER_ZONES.map((z, i) => (
          <div key={i} style={{
            background: '#fff', border: `1px solid ${TC.line}`,
            borderRadius: 12, padding: '10px 14px',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <div style={{
                width: 28, height: 28, borderRadius: 8,
                background: z.color, color: '#fff',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                fontSize: 12, fontWeight: 800,
              }}>{z.z}</div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink }}>{z.name}</div>
                <div style={{ fontSize: 11, color: TC.muted, marginTop: 1 }}>{z.range}</div>
              </div>
              <div style={{ fontSize: 13, fontWeight: 700, color: TC.ink2 }}>{z.pct} min</div>
            </div>
            <div style={{
              marginTop: 8, height: 6, borderRadius: 3, background: TC.lineSoft, overflow: 'hidden',
            }}>
              <div style={{
                width: Math.min(100, z.pct) + '%',
                height: '100%', background: z.color, borderRadius: 3,
              }} />
            </div>
          </div>
        ))}
      </div>

      <div style={{
        marginTop: 14, padding: 12, borderRadius: 12,
        background: TC.bgSoft, border: `1px solid ${TC.line}`,
      }}>
        <div style={{ fontSize: 12, color: TC.muted, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5 }}>Laatste ERG-workout</div>
        <div style={{ fontSize: 15, fontWeight: 700, color: TC.ink, marginTop: 4 }}>Sweet spot 4×8 min · 91% FTP</div>
        <div style={{ fontSize: 12, color: TC.muted, marginTop: 2 }}>Normalized Power 231 W · IF 0.92 · TSS 84</div>
      </div>
    </SheetFrame>
  );
}

// ─── Settings modal ─────────────────────────────────────────
function SettingsSheet({ open, onClose, onOpenAdmin, isAdmin, onLogout }) {
  const [tab, setTab] = useState('connect');
  const [hasInjury, setHasInjury] = useState(false);
  const [saving, setSaving] = useState(false);
  const connectSaveRef = useRef(null);
  const profileSaveRef = useRef(null);
  useEffect(() => {
    if (!open) return;
    let alive = true;
    api('/api/injury').then(d => {
      if (alive) setHasInjury(!!(d && d.injury && String(d.injury).trim()));
    }).catch(() => {});
    return () => { alive = false; };
  }, [open]);
  if (!open) return null;
  const handleSave = async () => {
    setSaving(true);
    try {
      if (connectSaveRef.current) await connectSaveRef.current();
      if (profileSaveRef.current) await profileSaveRef.current();
    } catch (e) { console.error('Save failed', e); }
    finally { setSaving(false); onClose(); }
  };
  const footerButtons = (
    <div style={{ display: 'flex', gap: 10 }}>
      {isAdmin && (
        <button onClick={onOpenAdmin} style={{
          flex: 1, padding: '12px 14px', borderRadius: 12,
          background: '#fff', border: `1px solid ${TC.line}`,
          color: TC.ink2, fontSize: 14, fontWeight: 700, cursor: 'pointer',
          display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
        }}>
          <Icon name="gear" size={16} /> Admin
        </button>
      )}
      <button onClick={handleSave} disabled={saving} style={{
        flex: 2, padding: '12px 14px', borderRadius: 12,
        background: TC.orange, color: '#fff', border: 'none',
        fontSize: 14, fontWeight: 700, cursor: saving ? 'default' : 'pointer',
        opacity: saving ? 0.7 : 1,
      }}>{saving ? 'Opslaan…' : 'Opslaan'}</button>
    </div>
  );
  return (
    <SheetFrame onClose={onClose} title="Instellingen" footer={footerButtons}>
      <div style={{ display: 'flex', gap: 6, marginBottom: 14, borderBottom: `1px solid ${TC.line}` }}>
        {[
          { id: 'connect', label: 'Verbinding' },
          { id: 'profile', label: 'Profiel' },
          { id: 'races',   label: 'Wedstrijden' },
          { id: 'app',     label: 'App' },
          { id: 'injury',  label: 'Blessure' },
        ].map(t => (
          <button key={t.id} onClick={() => setTab(t.id)} style={{
            padding: '10px 12px', border: 'none', background: 'none',
            color: tab === t.id ? TC.orangeDark : TC.muted,
            borderBottom: `2px solid ${tab === t.id ? TC.orange : 'transparent'}`,
            fontSize: 13, fontWeight: 700, cursor: 'pointer',
            marginBottom: -1,
          }}>
            {t.label}
            {t.id === 'injury' && hasInjury && (
              <span style={{
                display: 'inline-block', width: 6, height: 6, borderRadius: 3,
                background: TC.red, marginLeft: 6, verticalAlign: 'middle',
              }} />
            )}
          </button>
        ))}
      </div>

      {tab === 'connect' && <ConnectTab saveRef={connectSaveRef} />}
      {tab === 'profile' && <ProfileTab saveRef={profileSaveRef} />}

      {tab === 'app' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          <Toggle label="Push-meldingen" defaultOn />
          <Toggle label="Automatische sync" defaultOn />
          <Toggle label="Wekelijkse e-mail samenvatting" />
          <Toggle label="Deel anonieme analytics" />
          {onLogout && (
            <button onClick={() => { if (window.confirm('Wil je uitloggen?')) { onClose(); onLogout(); } }} style={{
              marginTop: 18, padding: '12px 14px', borderRadius: 12,
              background: '#fff', border: `1px solid ${TC.red}`,
              color: TC.red, fontSize: 14, fontWeight: 700, cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
            }}>Uitloggen</button>
          )}
        </div>
      )}

      {tab === 'races'  && <RacesTab />}
      {tab === 'injury' && <InjuryTab />}

    </SheetFrame>
  );
}

// ─── Wedstrijden tab ────────────────────────────────────────
// Load + save + delete via /api/races. Authoritatief voor de AI coach.
function RacesTab() {
  const [loaded, setLoaded] = useState(false);
  const [races, setRaces] = useState([]);
  const [editing, setEditing] = useState(null); // null = lijst, {} = nieuw, {id,...} = bewerken
  const [saving, setSaving] = useState(false);
  const reload = async () => {
    try {
      const d = await api('/api/races');
      setRaces(Array.isArray(d.races) ? d.races : []);
      setLoaded(true);
    } catch (e) { setLoaded(true); }
  };
  useEffect(() => { reload(); }, []);
  if (!loaded) return <div style={{ padding: 20, textAlign: 'center', color: TC.muted, fontSize: 13 }}>Wedstrijden laden…</div>;

  if (editing !== null) {
    return <RaceForm
      initial={editing}
      onCancel={() => setEditing(null)}
      onSaved={() => { setEditing(null); reload(); }}
    />;
  }

  const today = new Date(); today.setHours(0,0,0,0);
  const upcoming = races.filter(r => new Date(r.race_date) >= today);
  const past     = races.filter(r => new Date(r.race_date) < today).reverse();

  const fmtDateNL = (iso) => {
    try {
      const d = new Date(iso + 'T12:00:00');
      const m = ['jan','feb','mrt','apr','mei','jun','jul','aug','sep','okt','nov','dec'];
      return `${d.getDate()} ${m[d.getMonth()]} ${d.getFullYear()}`;
    } catch (e) { return iso; }
  };
  const daysUntil = (iso) => {
    try {
      const d = new Date(iso + 'T12:00:00');
      const t = new Date(); t.setHours(0,0,0,0);
      return Math.round((d - t) / 86400000);
    } catch (e) { return null; }
  };

  const Row = ({ r, isPast }) => {
    const du = daysUntil(r.race_date);
    return (
      <button onClick={() => setEditing(r)} style={{
        textAlign: 'left', width: '100%',
        padding: '12px 14px', borderRadius: 12,
        border: `1px solid ${isPast ? TC.line : TC.orangeBorder}`,
        background: isPast ? TC.bgSoft : '#fff',
        cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 4,
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink, flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.name}</div>
          <div style={{ fontSize: 11, fontWeight: 700, color: isPast ? TC.muted : TC.orangeDark, flexShrink: 0 }}>
            {isPast ? `${Math.abs(du)} d geleden` : du === 0 ? 'VANDAAG' : `nog ${du} d`}
          </div>
        </div>
        <div style={{ fontSize: 12, color: TC.muted, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <span>{fmtDateNL(r.race_date)}</span>
          {r.distance_km ? <span>· {r.distance_km} km</span> : null}
          {r.elevation_m ? <span>· {r.elevation_m} m D+</span> : null}
          {r.target_time ? <span>· doel {r.target_time}</span> : null}
          {r.priority && r.priority !== 'A' ? <span>· prio {r.priority}</span> : null}
        </div>
      </button>
    );
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{
        background: TC.bgSoft, border: `1px dashed ${TC.line}`,
        borderRadius: 10, padding: '10px 12px',
        display: 'flex', gap: 10, alignItems: 'flex-start',
      }}>
        <Icon name="info" size={16} color={TC.muted} />
        <div style={{ fontSize: 12, color: TC.muted, lineHeight: 1.5 }}>
          Hier ingegeven wedstrijden zijn de énige bron van waarheid voor de AI coach. Datum, afstand en doel worden exact zo gebruikt zonder eigen interpretatie.
        </div>
      </div>

      <button onClick={() => setEditing({})} style={{
        padding: '12px 14px', borderRadius: 12,
        background: TC.orange, color: '#fff', border: 'none',
        fontSize: 14, fontWeight: 700, cursor: 'pointer',
      }}>+ Nieuwe wedstrijd</button>

      {upcoming.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          <SectionLabel>Opkomend</SectionLabel>
          {upcoming.map(r => <Row key={r.id} r={r} isPast={false} />)}
        </div>
      )}
      {past.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          <SectionLabel>Recent (afgelopen)</SectionLabel>
          {past.slice(0, 5).map(r => <Row key={r.id} r={r} isPast={true} />)}
        </div>
      )}
      {upcoming.length === 0 && past.length === 0 && (
        <div style={{ fontSize: 13, color: TC.muted, textAlign: 'center', padding: '20px 10px' }}>
          Nog geen wedstrijden ingegeven.
        </div>
      )}
    </div>
  );
}

function RaceForm({ initial, onCancel, onSaved }) {
  const [name, setName] = useState(initial.name || '');
  const [date, setDate] = useState(initial.race_date || '');
  const [sport, setSport] = useState(initial.sport || 'running');
  const [distance, setDistance] = useState(initial.distance_km != null ? String(initial.distance_km) : '');
  const [elevation, setElevation] = useState(initial.elevation_m != null ? String(initial.elevation_m) : '');
  const [target, setTarget] = useState(initial.target_time || '');
  const [priority, setPriority] = useState(initial.priority || 'A');
  const [notes, setNotes] = useState(initial.notes || '');
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState(null);
  const isEdit = !!initial.id;

  const save = async () => {
    setErr(null);
    if (!name.trim()) { setErr('Naam is verplicht'); return; }
    if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { setErr('Datum is verplicht (YYYY-MM-DD)'); return; }
    setSaving(true);
    try {
      const payload = {
        name: name.trim(),
        race_date: date,
        sport,
        priority,
      };
      if (isEdit) payload.id = initial.id;
      if (distance.trim()) { const n = parseFloat(distance.replace(',', '.')); if (!Number.isNaN(n)) payload.distance_km = n; }
      if (elevation.trim()) { const n = parseInt(elevation, 10); if (!Number.isNaN(n)) payload.elevation_m = n; }
      if (target.trim()) payload.target_time = target.trim();
      if (notes.trim()) payload.notes = notes.trim();
      await api('/api/races', { method: 'POST', body: JSON.stringify(payload) });
      onSaved();
    } catch (e) {
      setErr(e.message || 'Opslaan mislukt');
    } finally { setSaving(false); }
  };
  const remove = async () => {
    if (!isEdit) { onCancel(); return; }
    if (!window.confirm(`Wedstrijd "${initial.name}" verwijderen?`)) return;
    setSaving(true);
    try {
      await api('/api/races/' + initial.id, { method: 'DELETE' });
      onSaved();
    } catch (e) { setErr(e.message || 'Verwijderen mislukt'); setSaving(false); }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ fontSize: 13, fontWeight: 700, color: TC.ink }}>
        {isEdit ? 'Wedstrijd bewerken' : 'Nieuwe wedstrijd'}
      </div>
      <Field label="Naam" placeholder="bv. Marathon Rotterdam" value={name} onChange={setName} />
      <Field label="Datum (YYYY-MM-DD)" placeholder="bv. 2026-08-15" value={date} onChange={setDate} type="date" />
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <Field label="Afstand (km)" placeholder="bv. 42.2" value={distance} onChange={setDistance} />
        <Field label="Hoogtemeters (m)" placeholder="bv. 1500" value={elevation} onChange={setElevation} />
      </div>
      <Field label="Streeftijd" placeholder="bv. 1:45:00" value={target} onChange={setTarget} />
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <label style={{ display: 'block' }}>
          <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6 }}>Sport</div>
          <select value={sport} onChange={e => setSport(e.target.value)} style={{
            width: '100%', padding: '11px 14px', borderRadius: 12,
            border: `1px solid ${TC.line}`, background: TC.bgSoft,
            fontSize: 14, fontFamily: 'inherit', color: TC.ink, outline: 'none',
            boxSizing: 'border-box',
          }}>
            <option value="running">Hardlopen</option>
            <option value="trail">Trail</option>
            <option value="cycling">Fietsen</option>
            <option value="swimming">Zwemmen</option>
            <option value="triathlon">Triatlon</option>
            <option value="other">Andere</option>
          </select>
        </label>
        <label style={{ display: 'block' }}>
          <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6 }}>Prioriteit</div>
          <select value={priority} onChange={e => setPriority(e.target.value)} style={{
            width: '100%', padding: '11px 14px', borderRadius: 12,
            border: `1px solid ${TC.line}`, background: TC.bgSoft,
            fontSize: 14, fontFamily: 'inherit', color: TC.ink, outline: 'none',
            boxSizing: 'border-box',
          }}>
            <option value="A">A — hoofddoel</option>
            <option value="B">B — belangrijk</option>
            <option value="C">C — trainingswedstrijd</option>
          </select>
        </label>
      </div>
      <TextArea label="Notities" placeholder="bv. heuvelachtige route, weersomstandigheden…" value={notes} onChange={setNotes} />
      {err && <div style={{ fontSize: 13, color: TC.red, fontWeight: 600 }}>{err}</div>}
      <div style={{ display: 'flex', gap: 10, marginTop: 4 }}>
        {isEdit && (
          <button onClick={remove} disabled={saving} style={{
            padding: '12px 14px', borderRadius: 12,
            background: '#fff', border: `1px solid ${TC.red}`,
            color: TC.red, fontSize: 14, fontWeight: 700,
            cursor: saving ? 'default' : 'pointer',
          }}>Verwijderen</button>
        )}
        <button onClick={onCancel} disabled={saving} style={{
          flex: 1, padding: '12px 14px', borderRadius: 12,
          background: '#fff', border: `1px solid ${TC.line}`,
          color: TC.ink2, fontSize: 14, fontWeight: 700,
          cursor: saving ? 'default' : 'pointer',
        }}>Annuleren</button>
        <button onClick={save} disabled={saving} style={{
          flex: 2, padding: '12px 14px', borderRadius: 12,
          background: TC.orange, color: '#fff', border: 'none',
          fontSize: 14, fontWeight: 700,
          cursor: saving ? 'default' : 'pointer', opacity: saving ? 0.7 : 1,
        }}>{saving ? 'Opslaan…' : 'Opslaan'}</button>
      </div>
    </div>
  );
}

// ─── Helper: relatieve tijd ───────────────────────────────
function relativeTime(iso) {
  if (!iso) return null;
  const t = Date.parse(iso);
  if (Number.isNaN(t)) return null;
  const diffMs = Date.now() - t;
  if (diffMs < 0) return 'nu';
  const mins = Math.floor(diffMs / 60000);
  if (mins < 1) return 'net nu';
  if (mins < 60) return `${mins} min geleden`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return `${hrs} uur geleden`;
  const days = Math.floor(hrs / 24);
  if (days < 7) return `${days} dag${days === 1 ? '' : 'en'} geleden`;
  const weeks = Math.floor(days / 7);
  if (weeks < 5) return `${weeks} week${weeks === 1 ? '' : 'en'} geleden`;
  const months = Math.floor(days / 30);
  if (months < 12) return `${months} maand${months === 1 ? '' : 'en'} geleden`;
  return `${Math.floor(days / 365)} jaar geleden`;
}

// ─── Connect tab (backend-gebacked) ────────────────────────
function ConnectTab({ saveRef }) {
  const [loaded, setLoaded] = useState(false);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [ouraToken, setOuraToken] = useState('');
  const [ouraHint, setOuraHint] = useState('');
  const [garminConnected, setGarminConnected] = useState(false);
  const [ouraConnected, setOuraConnected] = useState(false);
  const [lastActivityAt, setLastActivityAt] = useState(null);
  useEffect(() => {
    let alive = true;
    api('/api/settings').then(d => {
      if (!alive || !d) return;
      setEmail(d.garmin_email || '');
      setGarminConnected(!!d.garmin_connected);
      setOuraConnected(!!d.oura_connected);
      setOuraHint(d.oura_token_hint || '');
      setLastActivityAt(d.last_activity_at || null);
      setLoaded(true);
    }).catch(() => setLoaded(true));
    return () => { alive = false; };
  }, []);
  if (saveRef) saveRef.current = async () => {
    const payload = {};
    if (email) payload.garmin_email = email;
    if (password) payload.garmin_password = password;
    if (ouraToken) payload.oura_token = ouraToken;
    if (Object.keys(payload).length === 0) return;
    await api('/api/settings', { method: 'POST', body: JSON.stringify(payload) });
  };
  const rel = relativeTime(lastActivityAt);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <Field label="Garmin Connect e-mail" placeholder="jouw@email.com"
             value={email} onChange={setEmail} />
      <Field label="Wachtwoord" placeholder={loaded && garminConnected ? '••••••••  (ingesteld — nieuw invoeren om te wijzigen)' : 'Garmin wachtwoord'}
             type="password" value={password} onChange={setPassword} />
      <Field label="Oura API key" placeholder={loaded && ouraConnected ? `${ouraHint || '••••••••'}  (ingesteld — nieuwe key invoeren om te wijzigen)` : 'Oura personal access token (optioneel)'}
             value={ouraToken} onChange={setOuraToken} />
      {loaded && (
        garminConnected ? (
          <div style={{
            background: TC.goodSoft, border: `1px solid #bfe6d3`,
            borderRadius: 12, padding: '10px 12px',
            color: TC.good, fontSize: 13, fontWeight: 600,
            display: 'flex', alignItems: 'center', gap: 8,
          }}>
            <span style={{ width: 8, height: 8, borderRadius: 4, background: TC.good }} />
            Verbonden met Garmin Connect{rel ? ` · laatste activiteit ${rel}` : ''}
          </div>
        ) : (
          <div style={{
            background: '#fff3e6', border: `1px solid ${TC.orangeBorder}`,
            borderRadius: 12, padding: '10px 12px',
            color: TC.orangeDark, fontSize: 13, fontWeight: 600,
            display: 'flex', alignItems: 'center', gap: 8,
          }}>
            <span style={{ width: 8, height: 8, borderRadius: 4, background: TC.orange }} />
            Nog niet verbonden — vul e-mail + wachtwoord in en tik Opslaan
          </div>
        )
      )}
    </div>
  );
}

function Field({ label, defaultValue, placeholder, type = 'text', value, onChange }) {
  // Controlled wanneer onChange meegegeven wordt; anders uncontrolled met defaultValue.
  const isControlled = typeof onChange === 'function';
  const inputProps = isControlled
    ? { value: value == null ? '' : value, onChange: (e) => onChange(e.target.value) }
    : { defaultValue };
  return (
    <label style={{ display: 'block' }}>
      <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6 }}>{label}</div>
      <input type={type} {...inputProps} placeholder={placeholder} style={{
        width: '100%', padding: '11px 14px', borderRadius: 12,
        border: `1px solid ${TC.line}`, background: TC.bgSoft,
        fontSize: 14, fontFamily: 'inherit', color: TC.ink, outline: 'none',
        boxSizing: 'border-box',
      }} />
    </label>
  );
}

function TextArea({ label, defaultValue, placeholder, rows = 3, value, onChange }) {
  const isControlled = typeof onChange === 'function';
  const areaProps = isControlled
    ? { value: value == null ? '' : value, onChange: (e) => onChange(e.target.value) }
    : { defaultValue };
  return (
    <label style={{ display: 'block' }}>
      <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6 }}>{label}</div>
      <textarea {...areaProps} placeholder={placeholder} rows={rows} style={{
        width: '100%', padding: '11px 14px', borderRadius: 12,
        border: `1px solid ${TC.line}`, background: TC.bgSoft,
        fontSize: 14, fontFamily: 'inherit', color: TC.ink, outline: 'none',
        boxSizing: 'border-box', resize: 'vertical', lineHeight: 1.5,
      }} />
    </label>
  );
}

// ─── Profile tab ────────────────────────────────────────────
// Laadt bestaande profieldata via /api/profile en slaat op via /api/profile.
function ProfileTab({ saveRef }) {
  const [loaded, setLoaded] = useState(false);
  const [dirty, setDirty] = useState(false);
  const [p, setP] = useState({});
  useEffect(() => {
    let alive = true;
    api('/api/profile').then(d => {
      if (!alive) return;
      setP(d || {});
      setLoaded(true);
    }).catch(() => setLoaded(true));
    return () => { alive = false; };
  }, []);
  const upd = (k) => (v) => { setP(prev => ({ ...prev, [k]: v })); setDirty(true); };
  if (saveRef) saveRef.current = async () => {
    if (!dirty) return;
    const intFields = ['run_hr_rest','run_hr_max','run_hr_aerobic','run_hr_anaerobic','bike_ftp','bike_hr_max','bike_hr_aerobic','bike_hr_anaerobic','age'];
    const floatFields = ['run_vo2max','bike_weight'];
    const clean = {};
    for (const [k, v] of Object.entries(p)) {
      if (v === '' || v === null || v === undefined) continue;
      if (intFields.includes(k)) {
        const n = parseInt(v, 10); if (!Number.isNaN(n)) clean[k] = n;
      } else if (floatFields.includes(k)) {
        const n = parseFloat(v); if (!Number.isNaN(n)) clean[k] = n;
      } else {
        clean[k] = String(v);
      }
    }
    await api('/api/profile', { method: 'POST', body: JSON.stringify(clean) });
  };
  if (!loaded) {
    return <div style={{ padding: 20, textAlign: 'center', color: TC.muted, fontSize: 13 }}>Profiel laden…</div>;
  }
  const val = (k) => (p[k] === undefined || p[k] === null ? '' : String(p[k]));
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <SectionLabel>Persoonlijk</SectionLabel>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <Field label="Leeftijd" placeholder="bv. 38" value={val('age')} onChange={upd('age')} />
        <Field label="Gewicht (kg)" placeholder="bv. 68" value={val('bike_weight')} onChange={upd('bike_weight')} />
      </div>

      <SectionLabel>Hartslag & drempels — lopen</SectionLabel>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <Field label="Rusthartslag (bpm)" placeholder="bv. 48" value={val('run_hr_rest')} onChange={upd('run_hr_rest')} />
        <Field label="Max hartslag (bpm)" placeholder="bv. 190" value={val('run_hr_max')} onChange={upd('run_hr_max')} />
        <Field label="Aerobe drempel (bpm)" placeholder="bv. 158" value={val('run_hr_aerobic')} onChange={upd('run_hr_aerobic')} />
        <Field label="Anaerobe drempel (bpm)" placeholder="bv. 178" value={val('run_hr_anaerobic')} onChange={upd('run_hr_anaerobic')} />
      </div>
      <SectionLabel>Fietsen</SectionLabel>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <Field label="FTP (watt)" placeholder="bv. 252" value={val('bike_ftp')} onChange={upd('bike_ftp')} />
        <Field label="Max hartslag (bpm)" placeholder="bv. 190" value={val('bike_hr_max')} onChange={upd('bike_hr_max')} />
        <Field label="Aerobe drempel (bpm)" placeholder="bv. 158" value={val('bike_hr_aerobic')} onChange={upd('bike_hr_aerobic')} />
        <Field label="Anaerobe drempel (bpm)" placeholder="bv. 178" value={val('bike_hr_anaerobic')} onChange={upd('bike_hr_anaerobic')} />
      </div>
    </div>
  );
}

function SectionLabel({ children }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8,
      marginTop: 10, marginBottom: 2,
      paddingBottom: 8, borderBottom: `1px solid ${TC.line}`,
    }}>
      <span style={{ width: 3, height: 14, borderRadius: 2, background: TC.orange }} />
      <span style={{
        fontSize: 13, color: TC.ink, fontWeight: 800,
        letterSpacing: 0.2, textTransform: 'none',
      }}>{children}</span>
    </div>
  );
}

// ─── Injury tab ─────────────────────────────────────────────
function InjuryTab() {
  const [loaded, setLoaded] = useState(false);
  const [hasInjury, setHasInjury] = useState(false);
  const [desc, setDesc] = useState('');
  const [severity, setSeverity] = useState('mild');
  const [since, setSince] = useState('');
  const [saving, setSaving] = useState(false);
  const [saveMsg, setSaveMsg] = useState('');

  useEffect(() => {
    let alive = true;
    api('/api/injury').then(data => {
      if (!alive) return;
      const d = (data && data.injury) || '';
      if (d) {
        setDesc(d);
        setHasInjury(true);
        if (data.severity) setSeverity(data.severity);
        if (data.since) setSince(data.since);
      }
      setLoaded(true);
    }).catch(() => setLoaded(true));
    return () => { alive = false; };
  }, []);

  async function saveInjury() {
    setSaving(true); setSaveMsg('');
    try {
      await api('/api/injury', { method: 'POST', body: { description: desc, severity, since } });
      setSaveMsg('Opgeslagen');
      setTimeout(() => setSaveMsg(''), 2500);
    } catch(e) { setSaveMsg('Kon niet opslaan'); } finally { setSaving(false); }
  }

  async function clearInjury() {
    setSaving(true); setSaveMsg('');
    try {
      await api('/api/injury', { method: 'POST', body: { description: '' } });
      setDesc(''); setSeverity('mild'); setSince(''); setHasInjury(false); setSaveMsg('Gewist');
      setTimeout(() => setSaveMsg(''), 2500);
    } catch(e) { setSaveMsg('Kon niet wissen'); } finally { setSaving(false); }
  }

  if (!loaded) {
    return <div style={{ padding: 22, fontSize: 13, color: TC.muted, textAlign: 'center' }}>Laden…</div>;
  }

  if (!hasInjury) {
    return (
      <div style={{
        padding: 22, borderRadius: 14,
        background: TC.goodSoft, border: `1px solid #bfe6d3`,
        textAlign: 'center',
      }}>
        <div style={{
          width: 42, height: 42, borderRadius: 21, background: '#fff',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          marginBottom: 10,
        }}>
          <Icon name="check" size={22} color={TC.good} />
        </div>
        <div style={{ fontSize: 15, fontWeight: 700, color: TC.ink, marginBottom: 4 }}>Geen actieve blessure</div>
        <div style={{ fontSize: 12, color: TC.ink2, marginBottom: 14, lineHeight: 1.5 }}>
          Meld hier een nieuwe blessure zodat de coach er rekening mee houdt in elk advies.
        </div>
        <button onClick={() => setHasInjury(true)} style={{
          padding: '10px 18px', borderRadius: 999, border: 'none',
          background: TC.red, color: '#fff', fontSize: 13, fontWeight: 700, cursor: 'pointer',
        }}>+ Blessure melden</button>
      </div>
    );
  }

  return (
    <div>
      <div style={{
        background: '#fdecec', border: `1px solid #f3c6c6`,
        borderRadius: 12, padding: '10px 12px', marginBottom: 14,
        display: 'flex', gap: 10, alignItems: 'flex-start',
      }}>
        <span style={{
          width: 8, height: 8, borderRadius: 4, background: TC.red,
          marginTop: 6, flexShrink: 0,
        }} />
        <div style={{ fontSize: 12, color: TC.ink2, lineHeight: 1.5 }}>
          <strong style={{ color: TC.red }}>Actieve blessure.</strong> Deze info wordt <strong>altijd</strong> meegenomen in elk advies van de coach.
        </div>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div>
          <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6 }}>Wat is er aan de hand?</div>
          <textarea value={desc} onChange={e => setDesc(e.target.value)} rows={5} style={{
            width: '100%', padding: '11px 14px', borderRadius: 12,
            border: `1px solid ${TC.line}`, background: TC.bgSoft,
            fontSize: 14, fontFamily: 'inherit', color: TC.ink, outline: 'none',
            boxSizing: 'border-box', resize: 'vertical', lineHeight: 1.5,
          }} />
          <div style={{ fontSize: 11, color: TC.muted, marginTop: 6, lineHeight: 1.5 }}>
            Beschrijf vrij wat er speelt en wat je wil vermijden — de coach leest het bij elk advies mee.
          </div>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <div>
            <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6 }}>Ernst</div>
            <div style={{ display: 'flex', gap: 4, background: TC.bgSoft, padding: 3, borderRadius: 10, border: `1px solid ${TC.line}`, height: 40, boxSizing: 'border-box' }}>
              {[
                { id: 'mild',     label: 'Licht',  color: '#e0a810' },
                { id: 'moderate', label: 'Matig',  color: '#e0773b' },
                { id: 'severe',   label: 'Zwaar',  color: TC.red },
              ].map(s => (
                <button key={s.id} onClick={() => setSeverity(s.id)} style={{
                  flex: 1, padding: '0 4px', border: 'none', borderRadius: 8,
                  background: severity === s.id ? '#fff' : 'transparent',
                  color: severity === s.id ? s.color : TC.muted,
                  fontSize: 12, fontWeight: 700, cursor: 'pointer',
                  boxShadow: severity === s.id ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
                }}>{s.label}</button>
              ))}
            </div>
          </div>
          <div>
            <div style={{ fontSize: 11, color: TC.muted, fontWeight: 700, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6 }}>Sinds</div>
            <input type="date" value={since} onChange={e => setSince(e.target.value)} style={{
              width: '100%', height: 40, padding: '0 14px', borderRadius: 10,
              border: `1px solid ${TC.line}`, background: TC.bgSoft,
              fontSize: 14, fontFamily: 'inherit', color: TC.ink, outline: 'none',
              boxSizing: 'border-box',
            }} />
          </div>
        </div>

        <div style={{ display: 'flex', gap: 10, marginTop: 6, alignItems: 'center' }}>
          <button onClick={saveInjury} disabled={saving} style={{
            flex: 1, padding: '11px', borderRadius: 10,
            background: TC.orange, border: 'none', color: '#fff',
            fontSize: 13, fontWeight: 700, cursor: 'pointer',
            opacity: saving ? 0.6 : 1,
          }}>{saving ? 'Bezig…' : 'Opslaan'}</button>
          <button onClick={clearInjury} disabled={saving} style={{
            padding: '11px', borderRadius: 10,
            background: '#fff', border: `1px solid ${TC.line}`,
            color: TC.muted, fontSize: 13, fontWeight: 600, cursor: 'pointer',
          }}>Blessure wissen</button>
        </div>
        {saveMsg && <div style={{ fontSize: 11, color: TC.muted, marginTop: 4, textAlign: 'center' }}>{saveMsg}</div>}
        <button onClick={() => setHasInjury(false)} style={{
          display: 'none',
          padding: '10px', borderRadius: 10,
          background: '#fff', border: `1px solid ${TC.line}`,
          color: TC.red, fontSize: 13, fontWeight: 700, cursor: 'pointer',
        }}>Blessure opgelost — verwijderen</button>
      </div>
    </div>
  );
}

function InjuryCheck({ label }) {
  const [on, setOn] = useState(false);
  return (
    <label style={{
      display: 'flex', alignItems: 'center', gap: 10,
      padding: '10px 12px', borderRadius: 10,
      background: on ? '#fdecec' : '#fff',
      border: `1px solid ${on ? '#f3c6c6' : TC.line}`,
      cursor: 'pointer',
    }}>
      <span style={{
        width: 18, height: 18, borderRadius: 5, flexShrink: 0,
        border: `1.5px solid ${on ? TC.red : TC.line}`,
        background: on ? TC.red : '#fff',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      }}>
        {on && <Icon name="check" size={12} color="#fff" />}
      </span>
      <span style={{ fontSize: 13, color: TC.ink, fontWeight: 600 }}>{label}</span>
      <input type="checkbox" checked={on} onChange={e => setOn(e.target.checked)} style={{ display: 'none' }} />
    </label>
  );
}

function Toggle({ label, defaultOn }) {
  const [on, setOn] = useState(!!defaultOn);
  return (
    <div onClick={() => setOn(v => !v)} style={{
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '12px 14px', borderRadius: 12,
      border: `1px solid ${TC.line}`, background: '#fff', cursor: 'pointer',
    }}>
      <span style={{ fontSize: 14, color: TC.ink, fontWeight: 600 }}>{label}</span>
      <span style={{
        width: 40, height: 24, borderRadius: 12, flexShrink: 0,
        background: on ? TC.orange : TC.line,
        position: 'relative', transition: 'background 150ms',
      }}>
        <span style={{
          position: 'absolute', top: 2, left: on ? 18 : 2,
          width: 20, height: 20, borderRadius: 10, background: '#fff',
          boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
          transition: 'left 150ms',
        }} />
      </span>
    </div>
  );
}

// ─── Admin modal ────────────────────────────────────────────
const INITIAL_USERS = [
  { name: 'Cedric',    handle: 'cedric',    admin: true },
  { name: 'Dries',     handle: 'dries' },
  { name: 'Alain',     handle: 'alain' },
  { name: 'Katrien',   handle: 'katrien' },
  { name: 'Stephanie', handle: 'stephanie' },
  { name: 'Rodrigue',  handle: 'Rodrigue' },
  { name: 'Sam',       handle: 'sam' },
];

const INITIAL_INVITES = [
  { code: '90ba9015', used: 'Rodrigue', when: '7 apr' },
  { code: '8cc6ea01', used: 'sam',      when: '1 apr' },
];

function AdminSheet({ open, onClose, isAdmin }) {
  if (open && !isAdmin) { setTimeout(onClose, 0); return null; }
  const [users, setUsers]     = useState(INITIAL_USERS);
  const [invites, setInvites] = useState(INITIAL_INVITES);
  const [newU, setNewU]       = useState('');
  const [newP, setNewP]       = useState('');
  const [resetFor, setResetFor] = useState(null);
  const [resetPwd, setResetPwd] = useState('');
  const [flash, setFlash]       = useState(null);

  if (!open) return null;

  function flashMsg(text) {
    setFlash(text);
    setTimeout(() => setFlash(null), 2200);
  }

  function deleteUser(h) {
    setUsers(u => u.filter(x => x.handle !== h));
    flashMsg(`Gebruiker @${h} verwijderd`);
  }
  function addUser() {
    if (!newU.trim() || newP.length < 8) return;
    setUsers(u => [...u, { name: newU.trim(), handle: newU.trim().toLowerCase() }]);
    setNewU(''); setNewP('');
    flashMsg('Gebruiker toegevoegd');
  }
  function saveReset() {
    if (resetPwd.length < 8) return;
    flashMsg(`Wachtwoord van @${resetFor.handle} gereset`);
    setResetFor(null); setResetPwd('');
  }
  function genInvite() {
    const code = Math.random().toString(16).slice(2, 10);
    setInvites(iv => [{ code, used: null, when: 'nu' }, ...iv]);
    flashMsg('Uitnodigingslink gegenereerd');
  }

  return (
    <SheetFrame onClose={onClose} title="Admin paneel">
      {flash && (
        <div style={{
          position: 'sticky', top: 0, zIndex: 5,
          background: TC.goodSoft, border: `1px solid #bfe6d3`,
          color: TC.good, fontSize: 13, fontWeight: 700,
          padding: '8px 12px', borderRadius: 10, marginBottom: 12,
          textAlign: 'center',
        }}>{flash}</div>
      )}

      <div style={{
        background: '#fff8f0', border: `1px solid ${TC.orangeBorder}`,
        borderRadius: 12, padding: 10, marginBottom: 14,
        display: 'flex', gap: 10, alignItems: 'flex-start',
      }}>
        <Icon name="warn" size={16} color={TC.orangeDark} />
        <div style={{ fontSize: 12, color: TC.ink2, lineHeight: 1.5 }}>
          Hoofdadmin-paneel. Wijzigingen hier gelden voor alle beta-testers.
        </div>
      </div>

      {/* Anthropic API key */}
      <AdminSection title="Anthropic API key">
        <Field label="Key" placeholder="sk-ant-…" defaultValue="sk-ant-••••••••••••abcd" />
        <button onClick={() => flashMsg('API key opgeslagen')} style={primaryBtn}>API key opslaan</button>
      </AdminSection>

      {/* Password reset for current admin */}
      <AdminSection title="Mijn wachtwoord">
        <Field label="Huidig wachtwoord" placeholder="••••••••" type="password" />
        <Field label="Nieuw wachtwoord (min 8)" placeholder="••••••••" type="password" />
        <button onClick={() => flashMsg('Wachtwoord gewijzigd')} style={primaryBtn}>Wachtwoord bijwerken</button>
      </AdminSection>

      {/* User management */}
      <AdminSection title="Gebruikersbeheer">
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {users.map(u => (
            <div key={u.handle} style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '10px 12px', borderRadius: 10,
              background: '#fff', border: `1px solid ${TC.line}`,
            }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <span style={{ fontSize: 14, fontWeight: 700, color: TC.ink }}>{u.name}</span>
                <span style={{ fontSize: 12, color: TC.muted, marginLeft: 6 }}>(@{u.handle})</span>
                {u.admin && (
                  <span style={{
                    marginLeft: 8, background: TC.orange, color: '#fff',
                    fontSize: 10, fontWeight: 800, padding: '2px 8px', borderRadius: 999, letterSpacing: 0.6,
                  }}>ADMIN</span>
                )}
              </div>
              {!u.admin && (
                <>
                  <button onClick={() => setResetFor(u)} style={pillBtn}>Reset</button>
                  <button onClick={() => deleteUser(u.handle)} style={{ ...pillBtn, color: TC.red, borderColor: '#f3c6c6' }}>Verwijder</button>
                </>
              )}
            </div>
          ))}
        </div>

        {/* Reset inline form */}
        {resetFor && (
          <div style={{
            marginTop: 8, padding: 12, borderRadius: 10,
            background: TC.bgSoft, border: `1px solid ${TC.line}`,
          }}>
            <div style={{ fontSize: 12, fontWeight: 700, color: TC.ink2, marginBottom: 8 }}>
              Reset wachtwoord voor <strong>@{resetFor.handle}</strong>
            </div>
            <input
              type="password" value={resetPwd} onChange={e => setResetPwd(e.target.value)}
              placeholder="Nieuw wachtwoord (min 8)"
              style={inputStyle}
            />
            <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
              <button onClick={() => { setResetFor(null); setResetPwd(''); }} style={{ ...pillBtn, flex: 1 }}>Annuleren</button>
              <button onClick={saveReset} style={{ ...primaryBtn, marginTop: 0, flex: 1 }}>Opslaan</button>
            </div>
          </div>
        )}
      </AdminSection>

      {/* New user */}
      <AdminSection title="Nieuwe gebruiker">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
          <input value={newU} onChange={e => setNewU(e.target.value)} placeholder="Gebruikersnaam" style={inputStyle} />
          <input value={newP} onChange={e => setNewP(e.target.value)} placeholder="Wachtwoord (min 8)" type="password" style={inputStyle} />
        </div>
        <button onClick={addUser} style={{
          ...primaryBtn,
          background: newU.trim() && newP.length >= 8 ? TC.orange : TC.lineSoft,
          color: newU.trim() && newP.length >= 8 ? '#fff' : TC.mutedSoft,
        }}>+ Toevoegen</button>
      </AdminSection>

      {/* Invite links */}
      <AdminSection title="Uitnodigingslinks">
        <button onClick={genInvite} style={{
          ...primaryBtn,
          background: `linear-gradient(135deg, ${TC.orange}, #ff8f6a)`,
        }}>Uitnodigingslink genereren</button>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
          {invites.map(iv => (
            <div key={iv.code} style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '10px 12px', borderRadius: 10,
              background: '#fff', border: `1px solid ${TC.line}`,
            }}>
              <span style={{ fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 13, fontWeight: 700, color: TC.ink }}>{iv.code}</span>
              {iv.used
                ? <span style={{ background: TC.goodSoft, color: TC.good, fontSize: 11, fontWeight: 700, padding: '2px 8px', borderRadius: 999 }}>{iv.used}</span>
                : <span style={{ background: TC.orangeSoft, color: TC.orangeDark, fontSize: 11, fontWeight: 700, padding: '2px 8px', borderRadius: 999 }}>open</span>
              }
              <span style={{ flex: 1, fontSize: 12, color: TC.muted }}>{iv.when}</span>
              <button onClick={() => { navigator.clipboard?.writeText(iv.code); flashMsg('Gekopieerd'); }} style={pillBtn}>Kopieer</button>
            </div>
          ))}
        </div>
      </AdminSection>
    </SheetFrame>
  );
}

const primaryBtn = {
  width: '100%', marginTop: 10, padding: '12px', borderRadius: 12,
  background: TC.orange, color: '#fff', border: 'none',
  fontSize: 14, fontWeight: 700, cursor: 'pointer', letterSpacing: 0.3,
};
const pillBtn = {
  padding: '6px 12px', borderRadius: 999,
  background: '#fff', border: `1px solid ${TC.line}`,
  color: TC.ink2, fontSize: 12, fontWeight: 700, cursor: 'pointer',
};
const inputStyle = {
  width: '100%', padding: '10px 12px', borderRadius: 10,
  border: `1px solid ${TC.line}`, background: TC.bgSoft,
  fontSize: 14, fontFamily: 'inherit', color: TC.ink, outline: 'none',
  boxSizing: 'border-box',
};

function AdminSection({ title, children }) {
  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{
        fontSize: 11, color: TC.muted, fontWeight: 800,
        letterSpacing: 0.8, textTransform: 'uppercase',
        padding: '0 4px', marginBottom: 8,
      }}>{title}</div>
      {children}
    </div>
  );
}

function AdminRow({ icon, title, subtitle }) {
  return (
    <button style={{
      display: 'flex', alignItems: 'center', gap: 12,
      padding: '12px 14px', borderRadius: 12,
      border: `1px solid ${TC.line}`, background: '#fff',
      cursor: 'pointer', textAlign: 'left',
    }}>
      <div style={{
        width: 36, height: 36, borderRadius: 10,
        background: TC.bgSoft, color: TC.ink2,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        <Icon name={icon} size={18} />
      </div>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink }}>{title}</div>
        <div style={{ fontSize: 11, color: TC.muted, marginTop: 2 }}>{subtitle}</div>
      </div>
      <Icon name="chev" size={16} color={TC.muted} />
    </button>
  );
}

// ─── Confirm modal (in-app, not native) ─────────────────────
function ConfirmModal({ open, activity, onCancel, onConfirm }) {
  if (!open) return null;
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 60,
      background: 'rgba(20, 20, 25, 0.45)',
      display: 'flex', alignItems: 'flex-end',
      animation: 'tc-fade 150ms ease-out',
    }} onClick={onCancel}>
      <div onClick={e => e.stopPropagation()} style={{
        width: '100%', background: '#fff',
        borderRadius: '20px 20px 0 0',
        padding: '20px 18px calc(28px + env(safe-area-inset-bottom, 0px))',
        animation: 'tc-slide-up 200ms ease-out',
      }}>
        <div style={{
          width: 36, height: 4, borderRadius: 2, background: TC.line,
          margin: '0 auto 16px',
        }} />
        <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
          <Avatar size={44} />
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 16, fontWeight: 800, color: TC.ink }}>
              {activity?.isTip ? 'Tip laten analyseren?' : 'Deze activiteit analyseren?'}
            </div>
            <div style={{ fontSize: 13, color: TC.muted, marginTop: 4, lineHeight: 1.5 }}>
              {activity?.isTip
                ? <>Turbocoach analyseert <strong style={{ color: TC.ink2 }}>{activity?.name}</strong> en stelt een concreet trainingsblok voor.</>
                : <>Turbocoach bekijkt <strong style={{ color: TC.ink2 }}>{activity?.name}</strong> en geeft meteen specifiek advies.</>
              }
            </div>
          </div>
        </div>
        <div style={{ display: 'flex', gap: 10, marginTop: 18 }}>
          <button onClick={onCancel} style={{
            flex: 1, padding: '12px', borderRadius: 12,
            background: TC.bgSoft, border: `1px solid ${TC.line}`,
            color: TC.ink2, fontSize: 14, fontWeight: 700, cursor: 'pointer',
          }}>Annuleren</button>
          <button onClick={onConfirm} style={{
            flex: 1.2, padding: '12px', borderRadius: 12,
            background: TC.orange, color: '#fff', border: 'none',
            fontSize: 14, fontWeight: 700, cursor: 'pointer',
          }}>Analyseren</button>
        </div>
      </div>
    </div>
  );
}

// ─── Sheet frame (bottom-sheet modal used by predictions/power/settings) ─
function SheetFrame({ title, onClose, children, footer }) {
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 30,
      background: 'rgba(20, 20, 25, 0.45)',
      display: 'flex', alignItems: 'flex-end',
      animation: 'tc-fade 150ms ease-out',
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} style={{
        width: '100%', maxHeight: '85dvh', background: '#fff',
        borderRadius: '20px 20px 0 0',
        display: 'flex', flexDirection: 'column',
        animation: 'tc-slide-up 220ms ease-out',
      }}>
        <div style={{
          padding: '14px 16px 10px',
          borderBottom: `1px solid ${TC.line}`,
          display: 'flex', alignItems: 'center', gap: 12,
        }}>
          <div style={{ flex: 1 }}>
            <div style={{ width: 36, height: 4, borderRadius: 2, background: TC.line, margin: '0 auto 10px' }} />
            <div style={{ fontSize: 17, fontWeight: 800, color: TC.ink }}>{title}</div>
          </div>
          <button onClick={onClose} aria-label="Sluiten" style={{
            width: 34, height: 34, borderRadius: 10,
            background: TC.bgSoft, border: `1px solid ${TC.line}`,
            color: TC.ink2, cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Icon name="close" size={16} />
          </button>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: '12px 14px 20px', minHeight: 0 }}>
          {children}
        </div>
        {footer && (
          <div style={{
            flexShrink: 0,
            borderTop: `1px solid ${TC.line}`,
            background: '#fff',
            padding: '10px 14px',
            paddingBottom: 'calc(10px + env(safe-area-inset-bottom, 0px))',
          }}>
            {footer}
          </div>
        )}
      </div>
    </div>
  );
}

// ─── Day detail sheet (week-strip tap) ──────────────────────
function DaySheet({ open, day, onClose, onOpenSession, onOpenChat }) {
  if (!open || !day) return null;
  const count = day.sessions.length;
  return (
    <SheetFrame onClose={onClose} title={day.label.charAt(0).toUpperCase() + day.label.slice(1) + ' · ' + day.dateLabel}>
      {count === 0 && (
        <div style={{ padding: '4px 4px 0' }}>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 12,
            padding: '16px 14px', borderRadius: 14,
            background: TC.bgSoft, border: `1px dashed ${TC.line}`,
          }}>
            <div style={{
              width: 44, height: 44, borderRadius: 12,
              background: '#fff', border: `1px solid ${TC.line}`,
              color: TC.mutedSoft,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <Icon name="info" size={20} />
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink }}>Rustdag</div>
              <div style={{ fontSize: 12, color: TC.muted, marginTop: 2 }}>Geen sessies geregistreerd — slaap telt ook als training.</div>
            </div>
          </div>
          <button onClick={onOpenChat} style={{
            width: '100%', marginTop: 12, padding: '12px 14px', borderRadius: 12,
            background: TC.orange, color: '#fff', border: 'none',
            fontSize: 14, fontWeight: 700, cursor: 'pointer',
          }}>Plan een workout met Turbocoach</button>
        </div>
      )}

      {count >= 1 && (
        <div>
          {count > 1 && (
            <div style={{
              padding: '10px 12px', marginBottom: 10, borderRadius: 12,
              background: TC.orangeSoft, border: `1px solid ${TC.orangeBorder}`,
              display: 'flex', gap: 10, alignItems: 'center',
              fontSize: 13, color: TC.ink2,
            }}>
              <span style={{
                background: TC.orange, color: '#fff', fontSize: 11, fontWeight: 800,
                padding: '2px 8px', borderRadius: 999,
              }}>{count}× sessies</span>
              Dubbele trainingsdag. Tik op een sessie voor details.
            </div>
          )}

          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {day.sessions.map(s => (
              <button key={s.id} onClick={() => onOpenSession(s)} style={{
                display: 'flex', alignItems: 'center', gap: 12,
                padding: '12px 14px', borderRadius: 14,
                background: '#fff', border: `1px solid ${TC.line}`,
                cursor: 'pointer', textAlign: 'left',
              }}>
                <div style={{
                  width: 44, height: 44, borderRadius: 12,
                  background: TC.orangeSoft, color: TC.orangeDark,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>
                  <Icon name={s.type} size={22} />
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, fontWeight: 700, color: TC.ink }}>{s.name}</div>
                  <div style={{ fontSize: 12, color: TC.muted, marginTop: 2 }}>{s.time} · {s.dur} · HR {s.hr}</div>
                </div>
                <div style={{ textAlign: 'right' }}>
                  <div style={{ fontSize: 14, fontWeight: 800, color: TC.ink }}>{s.dist}</div>
                  <div style={{ fontSize: 11, color: TC.muted, marginTop: 2 }}>{s.pace}</div>
                </div>
              </button>
            ))}
          </div>

          {/* Day totals */}
          <div style={{
            marginTop: 14, padding: 12, borderRadius: 12,
            background: TC.bgSoft, border: `1px solid ${TC.line}`,
            display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)',
          }}>
            {[
              { l: 'Sessies', v: count },
              { l: 'Totale tijd', v: sumTimes(day.sessions) },
              { l: 'Calorieën', v: day.sessions.reduce((a, s) => a + (s.cal || 0), 0) },
            ].map((t, i) => (
              <div key={i} style={{
                textAlign: 'center',
                borderRight: i < 2 ? `1px solid ${TC.line}` : 'none',
              }}>
                <div style={{ fontSize: 10, color: TC.muted, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.3 }}>{t.l}</div>
                <div style={{ fontSize: 16, fontWeight: 800, color: TC.ink, marginTop: 3 }}>{t.v}</div>
              </div>
            ))}
          </div>
        </div>
      )}
    </SheetFrame>
  );
}

function sumTimes(sessions) {
  // crude: sums "hh:mm:ss" / "mm:ss" strings; fine for mock
  let total = 0;
  sessions.forEach(s => {
    const parts = s.dur.split(':').map(Number);
    if (parts.length === 3) total += parts[0]*3600 + parts[1]*60 + parts[2];
    else if (parts.length === 2) total += parts[0]*60 + parts[1];
  });
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  return h ? `${h}:${String(m).padStart(2,'0')} u` : `${m} min`;
}

// ─── Root app ───────────────────────────────────────────────
function App({ currentUser, onLogout }) {
  const isAdmin = !!(currentUser && currentUser.is_admin);
  const [liveActivities, setLiveActivities] = useState([]);
  const [liveHealth, setLiveHealth] = useState(null);
  const [liveStats, setLiveStats] = useState(null);
  const [dataVersion, setDataVersion] = useState(0);

  const reloadData = useCallback(async () => {
    try {
      const [actsRes, healthRes, statsRes] = await Promise.all([
        api('/api/activities?limit=20').catch(() => ({ activities: [] })),
        api('/api/health').catch(() => null),
        api('/api/stats').catch(() => null),
      ]);
      const [raceRes, whRes] = await Promise.all([
        api('/api/stats/race-times').catch(() => null),
        api('/api/stats/weekly-hours').catch(() => null),
      ]);
      const mapped = (actsRes.activities || []).map(mapActivity);
      ACTIVITIES = mapped;
      WEEK_STRIP = buildWeekStrip(mapped);
      // Map race-times -> RACE_PREDICTIONS voor de 4-koloms card
      const labelFor = (name) => name === '5 km' ? '5K' : name === '10 km' ? '10K' : name === 'Halve marathon' ? 'HM' : name === 'Marathon' ? 'MAR' : name;
      const _whWeeks = (whRes && Array.isArray(whRes.weeks)) ? whRes.weeks : [];
      CHART_WEEKS = _whWeeks.map(w => {
        const lbl = (w.week_label || '').split(' ');
        return {
          run:  Number(w.run_hours)  || 0,
          bike: Number(w.bike_hours) || 0,
          swim: Number(w.swim_hours) || 0,
          week:  lbl[0] || '',
          month: lbl[1] || '',
        };
      });
      const rt = (raceRes && Array.isArray(raceRes.race_times)) ? raceRes.race_times : [];
      RACE_PREDICTIONS = rt.slice(0, 4).map(r => {
        const adj = typeof r.form_adjustment_pct === 'number' ? r.form_adjustment_pct : 0;
        return {
          label: labelFor(r.distance || ''),
          time: r.time || '—',
          trend: adj <= 0 ? 'down' : 'up',
          delta: (adj === 0 ? '0%' : (adj > 0 ? '+' : '') + adj.toFixed(1) + '%'),
        };
      });
      setLiveActivities(mapped);
      setLiveHealth(healthRes);
      setLiveStats(statsRes);
      setDataVersion(v => v + 1);
    } catch (e) { console.warn('Data fetch failed', e); }
  }, []);

  useEffect(() => { reloadData(); }, [reloadData]);

  const [syncStatus, setSyncStatus] = useState(null);

  const [chat, setChat]       = useState(false);
  const [chatCtx, setChatCtx] = useState(null);
  const [predict, setPredict] = useState(false);
  const [settings, setSet]    = useState(false);
  const [admin, setAdmin]     = useState(false);
  const [confirm, setConfirm] = useState(null); // activity OR tip
  const [day, setDay]         = useState(null); // week-strip day
  const [syncing, setSyncing] = useState(false);
  const [promptVisible, setPromptVisible] = useState(true);

  function activityClicked(a) {
    setConfirm(a);
  }
  function openChatFromConfirm() {
    setChatCtx(confirm);
    setConfirm(null);
    setChat(true);
  }
  function sync() {
    setSyncing(true); setSyncStatus({ phase: "running", title: "Synchroniseren...", message: "Nieuwe activiteiten ophalen van Garmin Connect." }); api("/api/sync",{method:"POST"}).then(data => { if (data && data.status === "in_progress") { setSyncStatus({ phase: "info", title: "Al bezig", message: "Een synchronisatie loopt al op de achtergrond." }); } else if (data && data.is_initial) { setSyncStatus({ phase: "info", title: "Eerste synchronisatie gestart", message: "Je volledige sportgeschiedenis wordt opgehaald. Dit kan enkele minuten duren.", hint: "Je kan de app gewoon blijven gebruiken." }); } else { const n = (data && typeof data.synced === "number") ? data.synced : 0; const extra = (data && data.details_caching > 0) ? " Details van " + data.details_caching + " activiteiten worden op de achtergrond opgehaald." : ""; setSyncStatus({ phase: "success", title: n > 0 ? (n + " nieuwe " + (n === 1 ? "activiteit" : "activiteiten")) : "Alles up-to-date", message: n > 0 ? ("Je dashboard is bijgewerkt." + extra) : "Geen nieuwe activiteiten sinds de laatste sync." }); } }).catch(e => { setSyncStatus({ phase: "error", title: "Synchronisatie mislukt", message: (e && e.message) ? e.message : "Onbekende fout. Probeer het later opnieuw." }); }).finally(() => { setSyncing(false); reloadData(); });
  }

  return (
    <div style={{
      position: 'absolute', inset: 0, overflow: 'hidden',
      background: TC.bgSoft, fontFamily: "'Inter', system-ui, -apple-system, sans-serif",
      display: 'flex', flexDirection: 'column',
    }}
    data-screen-label="01 Turbocoach App"
    >
      <Dashboard
        key={dataVersion}
        onOpenChat={() => { setChatCtx(null); setChat(true); }}
        onOpenPredictions={() => setPredict(true)}
        onOpenSettings={() => setSet(true)}
        onActivityClick={activityClicked}
        onDayClick={(d) => setDay(d)}
        onSync={sync}
        syncing={syncing}
        onPromptVisible={setPromptVisible}
      />

      <SyncModal status={syncStatus} onClose={() => setSyncStatus(null)} />

      {/* FAB — alleen wanneer 'Vraag Turbocoach iets' niet in beeld is */}
      {!promptVisible && !chat && !predict && !settings && !admin && !confirm && !day && (
        <button onClick={() => { setChatCtx(null); setChat(true); }} aria-label="Open coach chat" style={{
          position: 'absolute', right: 18, bottom: 20,
          width: 60, height: 60, borderRadius: 18,
          background: TC.orange, color: '#fff',
          border: 'none', cursor: 'pointer',
          boxShadow: '0 12px 28px rgba(245, 124, 42, 0.45), 0 2px 6px rgba(245,124,42,0.25)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          zIndex: 20,
          animation: 'tc-fade 180ms ease-out',
        }}>
          <Icon name="chat" size={26} />
        </button>
      )}

      <CoachOverlay open={chat} onClose={() => setChat(false)} contextActivity={chatCtx} />
      <RacePredictionsSheet
        open={predict}
        onClose={() => setPredict(false)}
        onTipTap={() => {
          setPredict(false);
          setTimeout(() => setConfirm({
            id: 'tip-piekblok',
            name: 'Piekblok richting halve marathon',
            type: 'run',
            isTip: true,
          }), 180);
        }}
      />
      <SettingsSheet
        open={settings}
        onClose={() => setSet(false)}
        onOpenAdmin={() => { if (isAdmin) { setSet(false); setTimeout(() => setAdmin(true), 200); } }}
        isAdmin={isAdmin}
        onLogout={onLogout}
      />
      <AdminSheet open={admin && isAdmin} onClose={() => setAdmin(false)} isAdmin={isAdmin} />
      <ConfirmModal
        open={!!confirm}
        activity={confirm}
        onCancel={() => setConfirm(null)}
        onConfirm={openChatFromConfirm}
      />
      <DaySheet
        open={!!day}
        day={day}
        onClose={() => setDay(null)}
        onOpenSession={(s) => { setDay(null); setConfirm({ id: s.id, name: s.name, type: s.type }); }}
        onOpenChat={() => { setDay(null); setChatCtx(null); setChat(true); }}
      />
    </div>
  );
}

Object.assign(window, { App });


/* === ERROR BOUNDARY === */
class TCErrorBoundary extends React.Component {
  constructor(p){ super(p); this.state = { err: null }; }
  static getDerivedStateFromError(err){ return { err }; }
  componentDidCatch(err, info){ console.error('TC render error', err, info); }
  render(){
    if (this.state.err) {
      return React.createElement('div', { style: { padding: 24, fontFamily: 'system-ui', background: '#fff1e6', color: '#7a2e0a', minHeight: '100vh', fontSize: 13, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
        React.createElement('h2', { style: { marginTop: 0 } }, 'Turbocoach is gecrasht'),
        React.createElement('div', { style: { marginBottom: 12 } }, 'Probeer /legacy voor de oude UI.'),
        React.createElement('pre', { style: { fontSize: 11, background: '#fff', padding: 12, borderRadius: 8, overflow: 'auto' } }, String(this.state.err && (this.state.err.stack || this.state.err.message)))
      );
    }
    return this.props.children;
  }
}

/* === ROOT MOUNT WRAPPER === */
function TurbocoachRoot() {
  const [me, setMe] = useState(undefined);
  const [reloadKey, setReloadKey] = useState(0);

  useEffect(() => {
    let cancelled = false;
    api('/api/auth/me')
      .then(u => { if (!cancelled) setMe(u); })
      .catch(() => { if (!cancelled) setMe(null); });
    return () => { cancelled = true; };
  }, [reloadKey]);

  if (me === undefined) {
    return <div style={{ padding: 40, textAlign: 'center', color: TC.muted, fontSize: 14 }}>Laden...</div>;
  }
  if (me === null) {
    return <AuthScreen onAuth={() => setReloadKey(k => k + 1)} />;
  }
  const needsOnb = (me.user && me.user.needs_onboarding) || me.needs_onboarding;
  if (needsOnb) {
    return <OnboardingWizard user={me.user || me} onDone={() => setReloadKey(k => k + 1)} />;
  }
  return <App currentUser={me.user || me} onLogout={() => { setAuthToken(null); fetch('/api/auth/logout', { method:'POST', credentials:'include' }); setMe(null); }} />;
}

window.TurbocoachRoot = TurbocoachRoot;
window.TCErrorBoundary = TCErrorBoundary;
