/* ─────────────────────────────────────────────────────────────
   Cycle Plotter — main app
   Header is omitted here — the site nav above handles navigation
   (including the single light/dark theme toggle). Actions live in
   a slim row at the top of the charts column, then the two-column
   grid (sidebar + charts).
───────────────────────────────────────────────────────────────── */
(function () {
  const { useState, useMemo, useRef, useEffect } = React;
  const PK = window.PK;
  const COMPOUNDS = window.COMPOUNDS;
  const PKChart = window.PKChart;
  const { MetricSegment, TogglePill, PeriodPicker, ProtocolRow, CompoundPicker, QuickPlotModal, doseSummary } = window.CPControls;
  const DAY = PK.DAY;

  const isoToMs = (s) => new Date(s + 'T00:00:00').getTime();
  // Format the LOCAL date — must match isoToMs, which parses 'YYYY-MM-DD' as LOCAL midnight.
  // Using toISOString() here (UTC) shifted the date back a day in UTC+ timezones (e.g. NZ),
  // so a dragged card committed one day left of where it was dropped (the "jumps a day,
  // bring it in one day to align" bug).
  const msToIso = (ms) => { const d = new Date(ms); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); };

  const METRIC_LABEL = { serum: 'Estimated serum level', release: 'Released per day', total: 'Total active in body' };

  // ── real saved-protocol loading ───────────────────────────────────────────
  // The plotter is login-gated, so anyone here is signed in. Pull the user's own
  // saved protocols (saved_dosages) and map each to a plottable row — mirroring the
  // mapping the in-app plotter (app.js) and the /account/ ProtocolList already use.
  // The hardcoded SAMPLE_PROTOCOLS are only a fallback for an account with nothing
  // saved yet, so a first-time visitor still sees a populated chart.
  const PALETTE = window.PALETTE || {};
  // ordered so consecutive protocols land on opposite sides of the wheel (cyan→orange→
  // magenta→green→blue→yellow→violet→red) for maximum mutual contrast on black.
  const PROTO_COLORS = [PALETTE.teal, PALETTE.orange, PALETTE.pink, PALETTE.green, PALETTE.blue, PALETTE.amber, PALETTE.purple, PALETTE.coral].filter(Boolean);
  // saved ester name → plotter compound id (standalone COMPOUNDS ids)
  const ESTER_TO_CID = {
    'Testosterone Enanthate': 'test-e', 'Testosterone Cypionate': 'test-c',
    'Testosterone Propionate': 'test-p', 'Testosterone Undecanoate': 'test-u',
    'Testosterone Acetate': 'test-p', 'Testosterone Suspension': 'test-p', 'Sustanon 250': 'sustanon',
  };
  const PEPTIDE_TO_CID = {
    'BPC-157': 'bpc157', 'TB-500 (Thymosin Beta-4)': 'tb500', 'CJC-1295 (no DAC)': 'cjc-nodac',
    'CJC-1295 + DAC': 'cjc-dac', 'Ipamorelin': 'ipamorelin', 'HGH (Somatropin)': 'hgh',
    'PT-141 (Bremelanotide)': 'pt141', 'IGF-1 LR3': 'igf1lr3', 'Melanotan II': 'mt2',
    'Pinealon': 'pinealon',
  };
  // A newly-plotted protocol (no saved start_date, no planner preset) realistically
  // starts TODAY and projects forward. Saved protocols keep their start_date and a
  // planner preset keeps its own dates (handled below), so only undated rows start today.
  const defaultStartIso = () => msToIso(Date.now());
  // Every plotted dosage needs a start AND a finish; the chart spans the first start
  // to the last finish. If a saved dosage has no finish yet, default to a 10-week run.
  const DEFAULT_RUN_WK = 10;
  const defaultFinishIso = (startIso) => msToIso(isoToMs(startIso) + DEFAULT_RUN_WK * 7 * DAY);
  function ensureFinish(p) {
    if (p.finish) return Object.assign({}, p, { finishMs: isoToMs(p.finish) });
    const finish = defaultFinishIso(p.start);
    return Object.assign({}, p, { finish: finish, finishMs: isoToMs(finish) });
  }

  // Stable active-first ordering: enabled cards float to the top (keeping their relative
  // order), disabled cards sink below. Filtering by chart preserves this, so each board's
  // strip shows actives-then-inactives. Used on toggle so an activated card slides up.
  function sortActiveFirst(ps) {
    return ps.map((p, i) => ({ p, i }))
      .sort((a, b) => (!!a.p.enabled === !!b.p.enabled) ? (a.i - b.i) : (a.p.enabled ? -1 : 1))
      .map(o => o.p);
  }

  // One saved dosage → { compoundId, dose (per-injection, native unit), freqDays }
  // or null if it isn't a plottable compound. dose is normalised to PER-INJECTION
  // because PK.seriesFor applies p.dose at each injection time.
  function mapDosage(type, d) {
    const cfg = d.config || {};
    let cid = null, dose = 0, freqDays = 7;
    if (type === 'trt') {
      cid = ESTER_TO_CID[cfg.esterType] || 'test-e';
      const wk = parseFloat(cfg.mgWeek) || 0;
      // honour the saved dosing MODE so the plotter spaces injections correctly:
      //   ndays  → inject every cfg.nDays days
      //   perweek/ml2mg → inject cfg.injPerWeek times a week (freq = 7 / injPerWeek)
      const mode = cfg.mode || (cfg.nDays ? 'ndays' : 'perweek');
      if (mode === 'ndays') {
        freqDays = parseFloat(cfg.nDays) || 7;
      } else {
        const ipw = parseFloat(cfg.injPerWeek) || 1;
        freqDays = ipw > 0 ? Math.round((7 / ipw) * 100) / 100 : 7;
      }
      dose = wk * freqDays / 7;   // per-injection mg (weekly total × share per injection)
    } else if (type === 'eod') {
      cid = ESTER_TO_CID[cfg.esterType] || 'test-e';
      const wk = parseFloat(cfg.mgWeek) || 0; freqDays = 2; dose = wk * 2 / 7;
    } else if (type === 'sema') {
      cid = 'semaglutide'; dose = parseFloat(cfg.dose) || 0; freqDays = 7;
    } else if (type === 'tirz') {
      cid = 'tirzepatide'; dose = parseFloat(cfg.dose) || 0; freqDays = 7;
    } else if (type === 'bpc157') {
      cid = 'bpc157'; dose = parseFloat(cfg.dose) || parseFloat(cfg.vialMcg) || 0; freqDays = 1;
    } else if (type === 'peptide') {
      cid = PEPTIDE_TO_CID[cfg.peptideType] || null;
      const c = cid ? COMPOUNDS[cid] : null;
      let dpi = parseFloat(cfg.dosePerInj) || 0;
      const unit = cfg.doseUnit || 'mcg';
      if (c) { if (c.unit === 'mcg' && unit === 'mg') dpi *= 1000; else if (c.unit === 'mg' && unit === 'mcg') dpi /= 1000; }
      const iw = parseFloat(cfg.injWeek) || parseFloat(cfg.injPerWeek) || 0;
      dose = dpi; freqDays = iw > 0 ? Math.round((7 / iw) * 100) / 100 : 1;
    }
    if (!cid || !(dose > 0) || !COMPOUNDS[cid]) return null;
    return { compoundId: cid, dose, freqDays };
  }

  // Inverse of mapDosage: turn the plotter's per-injection dose + freqDays back into the
  // saved_dosages.config fields for this protocol's type, so an edit can be merged into the
  // user's saved protocol. Mirrors mapDosage's formulas exactly for a lossless round-trip.
  function configPatchFor(type, compoundId, dose, freqDays, prevConfig) {
    const cfg = {};
    if (type === 'trt' || type === 'eod') {
      cfg.mgWeek = Math.round((dose * 7 / freqDays) * 100) / 100;
      if (type === 'trt') {
        const mode = (prevConfig && prevConfig.mode) || (prevConfig && prevConfig.nDays ? 'ndays' : 'perweek');
        cfg.mode = mode;
        if (mode === 'ndays') cfg.nDays = freqDays;
        else cfg.injPerWeek = Math.round((7 / freqDays) * 100) / 100;
      }
    } else if (type === 'sema' || type === 'tirz' || type === 'bpc157') {
      cfg.dose = dose;
    } else if (type === 'peptide') {
      const c = COMPOUNDS[compoundId];
      const unit = (prevConfig && prevConfig.doseUnit) || 'mcg';
      let dpi = dose;
      if (c) { if (c.unit === 'mcg' && unit === 'mg') dpi = dose / 1000; else if (c.unit === 'mg' && unit === 'mcg') dpi = dose * 1000; }
      cfg.dosePerInj = dpi;
      cfg.injWeek = Math.round((7 / freqDays) * 100) / 100;
    }
    return cfg;
  }

  // ── slider-card titles: match the universal rail/sidecards ──────────────────
  // Mirror of ib-rail.js protoDisplay primary + dedupeNames so the plotter's slider
  // cards read the exact same compact compound titles as the sidecards — including
  // the (1)/(2) numbering on repeated compounds ("Test E", "Test E (1)", "Test E (2)").
  const PROTO_ABBR = [
    ['Testosterone Cypionate', 'Test C'], ['Testosterone Enanthate', 'Test E'],
    ['Testosterone Propionate', 'Test P'], ['Testosterone Undecanoate', 'Test U'],
    ['Testosterone Suspension', 'Test Susp'], ['Sustanon 250', 'Sustanon'],
    ['Testosterone', 'Test'],
  ];
  function abbrevName(s) { let out = s; for (const a of PROTO_ABBR) out = out.split(a[0]).join(a[1]); return out; }
  function protoPrimary(label, fallback) {
    const raw = label ? abbrevName(label) : fallback;
    const parts = raw.split(/\s+[·—→+]\s+/);
    let primary = raw;
    if (parts.length === 2) {
      const a = parts[0].trim(), b = parts[1].trim();
      const aNum = /^[\d.]/.test(a), bNum = /^[\d.]/.test(b);
      primary = (aNum && !bNum) ? b : a;   // dose · compound → compound on top
    }
    return primary;
  }
  function dedupeNames(names) {
    const seen = {}, out = [];
    for (let i = 0; i < names.length; i++) {
      const n = names[i], c = seen[n] || 0;
      seen[n] = c + 1;
      out.push(c === 0 ? n : n + ' (' + c + ')');
    }
    return out;
  }

  function loadRealProtocols() {
    const types = ['trt', 'eod', 'sema', 'tirz', 'bpc157', 'peptide'];
    return Promise.all(types.map(t =>
      fetch('/api/dosages/?type=' + t, { credentials: 'include' })
        .then(r => (r.ok ? r.json() : { dosages: [] }))
        .then(j => ({ t, rows: (j && j.dosages) || [] }))
        .catch(() => ({ t, rows: [] }))
    )).then(results => {
      const out = [];
      let idx = 0;
      results.forEach(res => {
        res.rows.forEach(d => {
          const m = mapDosage(res.t, d);
          if (!m) return;
          const c = COMPOUNDS[m.compoundId];
          const chart = (c.type === 'glp1' || c.type === 'peptide') ? 2 : 1;
          const start = d.start_date || defaultStartIso();
          const finish = defaultFinishIso(start);
          out.push({
            id: d.id, label: protoPrimary(d.label, c.short),
            compoundId: m.compoundId, dose: m.dose, freqDays: m.freqDays,
            start, startMs: isoToMs(start),
            finish, finishMs: isoToMs(finish),
            color: PROTO_COLORS[idx % PROTO_COLORS.length] || '#0fbcad',
            // Start with NOTHING plotted — the user picks which protocols to chart. The
            // cards all list; tapping one enables it (then the Calculate button lights up).
            chart, enabled: false,
            type: res.t, config: d.config || {},   // kept so an edit can be saved back to the account
          });
          idx++;
        });
      });
      // Disambiguate repeated compound titles, same scheme as the sidecards: the
      // first "Test E" stays clean, the rest become "Test E (1)", "Test E (2)".
      const titles = dedupeNames(out.map(p => p.label));
      out.forEach((p, i) => { p.label = titles[i]; });
      return out;
    });
  }

  // ── Cycle Planner hand-off ─────────────────────────────────────────────────
  // When the user taps "Test this cycle on the plotter", the planner stashes a
  // preset in sessionStorage and navigates here with ?from=planner. Map each item's
  // compound name to a plottable compound id and preview it (changes here aren't
  // saved back — the planner is the source of truth).
  function nameToCid(name) {
    if (!name) return null;
    const raw = String(name).trim();
    if (ESTER_TO_CID[raw]) return ESTER_TO_CID[raw];
    if (PEPTIDE_TO_CID[raw]) return PEPTIDE_TO_CID[raw];
    const n = raw.toLowerCase();
    if (COMPOUNDS[n]) return n;
    let best = null;
    for (const id in COMPOUNDS) {
      const c = COMPOUNDS[id];
      const nm = (c.name || '').toLowerCase(), sh = (c.short || '').toLowerCase();
      if (nm === n || sh === n) return id;
      if (!best && nm && (n.indexOf(nm) >= 0 || nm.indexOf(n) >= 0)) best = id;
      if (!best && sh && n.indexOf(sh) >= 0) best = id;
    }
    return best;
  }

  function loadCyclePreset() {
    let fromPlanner = false;
    try { fromPlanner = new URLSearchParams(window.location.search).get('from') === 'planner'; } catch (e) {}
    if (!fromPlanner) return null;
    let preset = null;
    try { const raw = sessionStorage.getItem('__ib_cycle_plot'); if (raw) preset = JSON.parse(raw); } catch (e) {}
    if (!preset || !Array.isArray(preset.items) || !preset.items.length) return null;
    const rows = [];
    let idx = 0;
    preset.items.forEach(function (it) {
      const cid = nameToCid(it.compound || it.label);
      if (!cid || !COMPOUNDS[cid]) return;
      const c = COMPOUNDS[cid];
      const dose = parseFloat(it.dosePerInjection) || 0;
      if (!(dose > 0)) return;
      const freqDays = parseFloat(it.freqDays) || 7;
      const start = it.startIso || defaultStartIso();
      const finish = it.endIso || defaultFinishIso(start);
      const chart = (c.type === 'glp1' || c.type === 'peptide') ? 2 : 1;
      rows.push({
        id: 'cyc-' + idx, label: it.label || c.short,
        compoundId: cid, dose, freqDays,
        start, startMs: isoToMs(start),
        finish, finishMs: isoToMs(finish),
        color: PROTO_COLORS[idx % PROTO_COLORS.length] || '#0fbcad',
        chart, enabled: true,
      });
      idx++;
    });
    return rows.length ? { name: preset.name || 'Your cycle', rows } : null;
  }

  // ── ghost (placeholder) curves ────────────────────────────────────────────
  // Before any protocol is chosen, each board shows faint made-up curves so the empty
  // chart still reads as a real chart (a teaser behind "Select a protocol"). Board 1 =
  // testosterone/steroids, board 2 = GLP-1 + peptide. Non-interactive (see PKChart ghost).
  // grey starter lines — neutral so they read as a placeholder, not real data
  const GHOST_GREY_A = '#9ca3af', GHOST_GREY_B = '#6b7280';
  const GHOST_SPEC = {
    1: [{ compoundId: 'test-e', dose: 100, freqDays: 3.5, color: GHOST_GREY_A },
        { compoundId: 'test-c', dose: 90, freqDays: 7, color: GHOST_GREY_B }],
    2: [{ compoundId: 'semaglutide', dose: 1, freqDays: 7, color: GHOST_GREY_A },
        { compoundId: 'tirzepatide', dose: 5, freqDays: 7, color: GHOST_GREY_B }],
  };
  function buildGhostCurves(chartN, fromMs, toMs, metric) {
    const spec = GHOST_SPEC[chartN] || GHOST_SPEC[1];
    const span = Math.max(1, toMs - fromMs);
    const startMs = fromMs + span * 0.12;   // ease in from the left edge
    const finishMs = toMs - span * 0.06;
    return spec.map((s, i) => {
      const c = COMPOUNDS[s.compoundId]; if (!c) return null;
      const sr = PK.seriesFor({ compoundId: s.compoundId, dose: s.dose, freqDays: s.freqDays, startMs, finishMs }, fromMs, toMs, metric);
      return { id: 'ghost-' + chartN + '-' + i, compoundId: s.compoundId, short: c.short, label: c.short, color: s.color, unit: c.unit, points: sr.points, injections: sr.injections, stats: sr.stats, enabled: true };
    }).filter(Boolean);
  }

  // ── one chart board ──
  function ChartPanel({ n, protocols, plotted, metric, onMetric, showInj, showStats, showCombined, onInj, onStats, onCombined, onToggle, onStart, onFinish, onReorderTo, onEdit, onDragActive, calculated, dirty, fromMs, toMs, noDrag, showHint }) {
    // Mobile: the metric/overlay controls move into a settings popup behind a gear.
    const [settingsOpen, setSettingsOpen] = useState(false);
    // Strip / draggable bars react to the LIVE protocols (so cards highlight + drag
    // immediately). The plotted CURVES come from the snapshot (`plotted`) instead, so the
    // lines + auto-summed Total stay frozen until Calculate is pressed — no live auto-sum.
    const assigned = protocols.filter(p => p.chart === n);
    const assignedPlot = (plotted || []).filter(p => p.chart === n);

    const baseCurves = useMemo(() => assignedPlot.map(p => {
      const c = COMPOUNDS[p.compoundId];
      const s = PK.seriesFor(p, fromMs, toMs, metric);
      return { id: p.id, compoundId: p.compoundId, short: c.short, label: p.label, color: p.color, unit: c.unit, points: s.points, injections: s.injections, stats: s.stats, enabled: p.enabled };
      // finish is in the key because seriesFor stops dosing at finishMs — without it,
      // dragging a card's finish wouldn't recompute the curve (or the combined total).
    }), [assignedPlot.map(p => p.id + p.dose + p.freqDays + p.start + p.finish + (p.enabled ? '1' : '0')).join(','), metric, fromMs, toMs]);

    const enabledCurves = baseCurves.filter(c => c.enabled);

    // combined curves — sum protocols sharing an active molecule
    const hasCombinable = useMemo(() => {
      const g = {};
      enabledCurves.forEach(cv => { const k = PK.groupOf(cv.compoundId); g[k] = (g[k] || 0) + 1; });
      return Object.keys(g).some(k => g[k] >= 2 && PK.GROUP_LABELS[k]);
    }, [baseCurves]);

    const combined = useMemo(() => {
      if (!showCombined) return [];
      const groups = {};
      enabledCurves.forEach(cv => { const k = PK.groupOf(cv.compoundId); (groups[k] = groups[k] || []).push(cv); });
      const out = [];
      Object.keys(groups).forEach(k => {
        const list = groups[k];
        if (list.length < 2 || !PK.GROUP_LABELS[k]) return;
        const len = list[0].points.length;
        const pts = [];
        for (let i = 0; i < len; i++) {
          let v = 0; const t = list[0].points[i].t;
          for (const cv of list) { const pt = cv.points[i]; if (pt) v += pt.v; }
          pts.push({ t, v });
        }
        const tail = pts.slice(Math.floor(pts.length * 0.75));
        let peak = 0, trough = Infinity, sum = 0;
        tail.forEach(p => { if (p.v > peak) peak = p.v; if (p.v < trough) trough = p.v; sum += p.v; });
        out.push({
          id: 'comb-' + k, compoundId: '__' + k, short: PK.GROUP_LABELS[k], label: PK.GROUP_LABELS[k],
          color: 'var(--cp-text)', unit: list[0].unit, points: pts, injections: [], combined: true,
          stats: { steady: tail.length ? sum / tail.length : 0, peak, trough: isFinite(trough) ? trough : 0, now: pts.length ? pts[pts.length - 1].v : 0 },
        });
      });
      return out;
    }, [baseCurves, showCombined]);

    // Plotted curves = enabled protocols + combined totals. Legend lists every
    // assigned protocol (enabled or not) plus combined, so the checkboxes can
    // toggle a hidden curve back on.
    const curves = enabledCurves.concat(combined);
    const legendRows = baseCurves.concat(combined);
    const unit = enabledCurves[0] ? enabledCurves[0].unit : (baseCurves[0] ? baseCurves[0].unit : '');
    const unitLabel = metric === 'release' ? unit + '/day' : metric === 'total' ? unit : 'rel.';

    // Show the faint made-up teaser curves until the user actually clicks Calculate (or
    // when nothing is chosen on this board). Ticking a protocol on must NOT clear them —
    // they stay until Calculate, so the chart never blanks out mid-selection.
    const isGhost = !calculated || enabledCurves.length === 0;
    const ghostCurves = useMemo(() => isGhost ? buildGhostCurves(n, fromMs, toMs, metric) : [], [isGhost, n, fromMs, toMs, metric]);

    return React.createElement('section', { className: 'cp-slide' },
      React.createElement('div', { className: 'cp-panel' },
        React.createElement('div', { className: 'cp-panel-head' },
          React.createElement('div', { className: 'cp-panel-titlewrap' },
            React.createElement('h2', { className: 'cp-panel-title' }, METRIC_LABEL[metric],
              React.createElement('span', { className: 'cp-panel-unit' }, unitLabel))
          ),
          // desktop: metric segment + overlay pills inline in this row
          React.createElement('div', { className: 'cp-head-ctrls' },
            React.createElement(MetricSegment, { value: metric, onChange: onMetric }),
            React.createElement('div', { className: 'cp-head-pills' },
              React.createElement(TogglePill, { on: showStats, onClick: onStats }, 'Peak · trough · steady'),
              hasCombinable && React.createElement(TogglePill, { on: showCombined, onClick: onCombined }, 'Total')
            )
          ),
          // mobile: a gear that opens the settings popup (controls live there)
          React.createElement('button', { className: 'cp-gear-btn', 'aria-label': 'Chart settings', onClick: () => setSettingsOpen(true) },
            React.createElement('svg', { width: 19, height: 19, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' },
              React.createElement('circle', { cx: 12, cy: 12, r: 3 }),
              React.createElement('path', { d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z' })
            )
          )
        ),
        // ── mobile settings popup ──
        settingsOpen && React.createElement('div', { className: 'cp-modal-scrim', onClick: () => setSettingsOpen(false) },
          React.createElement('div', { className: 'cp-modal cp-settings-modal', onClick: e => e.stopPropagation() },
            React.createElement('div', { className: 'cp-modal-head' },
              React.createElement('div', null,
                React.createElement('div', { className: 'cp-modal-title' }, 'Chart ' + n + ' settings')
              ),
              React.createElement('button', { className: 'cp-modal-x', onClick: () => setSettingsOpen(false) }, '✕')
            ),
            React.createElement('div', { className: 'cp-settings-body' },
              React.createElement('div', { className: 'cp-settings-group' },
                React.createElement('div', { className: 'cp-settings-label' }, 'Metric'),
                React.createElement(MetricSegment, { value: metric, onChange: onMetric })
              ),
              React.createElement('div', { className: 'cp-settings-group' },
                React.createElement('div', { className: 'cp-settings-label' }, 'Overlays'),
                React.createElement('div', { className: 'cp-settings-pills' },
                  React.createElement(TogglePill, { on: showStats, onClick: onStats }, 'Peak · trough · steady'),
                  hasCombinable && React.createElement(TogglePill, { on: showCombined, onClick: onCombined }, 'Total')
                )
              )
            )
          )
        ),
        React.createElement(PKChart, {
          curves: isGhost ? ghostCurves : curves,
          ghost: isGhost, dimmed: isGhost ? false : dirty,
          // cards take their real date-span width once calculated (and not in the teaser)
          calculated: calculated && !isGhost,
          fromMs, toMs, unitLabel, showInj, showStats: isGhost ? false : showStats,
          bars: assigned, noDrag, showHint,
          onTlStart: noDrag ? undefined : (id, ms) => onStart(id, msToIso(ms)),
          onTlFinish: noDrag ? undefined : (id, ms) => onFinish(id, msToIso(ms)),
          onTlToggle: noDrag ? undefined : onToggle,
          onReorderTo: noDrag ? undefined : ((id, idx) => onReorderTo(id, idx, n)),
          onDragActive: noDrag ? undefined : onDragActive,
          onTlEdit: onEdit,   // edit ✎ works for guests (noDrag) and signed-in alike
        })
      )
    );
  }

  // ── Related calculators strip ──
  // Mirrors app.js RelatedCalcStrip for the standalone bundle. Real <a> anchors
  // (crawlable internal links). Desktop flex-wrap; mobile <=767px = seamless
  // horizontal side-scroll with mask-image edge fades. Appended at the page
  // bottom with reserved min-height so it never shifts the charts above (CLS).
  const RELATED_LINKS = [
    { href: '/blend-calculator/', label: 'Blend Calculator', desc: 'Plan a custom multi-compound blend.' },
    { href: '/nootropic-dosing-calculator/', label: 'Nootropic Dosing Calculator', desc: 'Dose research nootropic compounds accurately.' },
    { href: '/trt-calculator/', label: 'Testosterone (TRT) Dosage Calculator', desc: 'Find your testosterone dose and injection volume.' },
  ];
  function RelatedStrip() {
    useEffect(() => {
      if (document.getElementById('cp-related-styles')) return;
      const s = document.createElement('style');
      s.id = 'cp-related-styles';
      s.textContent = [
        '.cp-related{max-width:1380px;margin:0 auto;padding:4px 18px 44px;}',
        '.cp-related-h{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--cp-text-mut);margin:0 0 12px;}',
        '.cp-related-strip{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;min-height:84px;}',
        '.cp-related-chip{display:flex;flex-direction:column;align-items:flex-start;gap:4px;padding:12px 14px;border-radius:12px;background:var(--cp-teal-dim);border:1px solid rgba(15,188,173,.35);text-decoration:none;transition:border-color .15s,background .15s;}',
        '.cp-related-chip:hover{border-color:rgba(15,188,173,.6);}',
        '.cp-related-title{display:inline-flex;align-items:center;gap:7px;color:var(--cp-teal);font-size:13px;font-weight:600;}',
        '.cp-related-desc{font-size:11.5px;line-height:1.45;font-weight:400;color:var(--cp-text-sec);}',
        '.cp-related-arrow{font-size:12px;opacity:.7;}',
        '@media (max-width:767px){',
        '.cp-related-strip{display:flex;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scroll-snap-type:x proximity;scrollbar-width:none;-ms-overflow-style:none;padding:2px 0 4px;will-change:scroll-position;-webkit-mask-image:linear-gradient(to right,transparent 0,#000 20px,#000 calc(100% - 20px),transparent 100%);mask-image:linear-gradient(to right,transparent 0,#000 20px,#000 calc(100% - 20px),transparent 100%);}',
        '.cp-related-strip::-webkit-scrollbar{height:0;width:0;display:none;}',
        '.cp-related-chip{flex:0 0 72%;scroll-snap-align:start;}',
        '}',
        '@media (prefers-reduced-motion:reduce){.cp-related-strip{scroll-behavior:auto;}}',
      ].join('');
      document.head.appendChild(s);
    }, []);
    return React.createElement('nav', { className: 'cp-related', 'aria-label': 'Related calculators' },
      React.createElement('h3', { className: 'cp-related-h' }, 'Related calculators'),
      React.createElement('div', { className: 'cp-related-strip' },
        RELATED_LINKS.map(c => React.createElement('a', { key: c.href, className: 'cp-related-chip', href: c.href, 'aria-label': c.label + (c.desc ? ' – ' + c.desc : '') },
          React.createElement('span', { className: 'cp-related-title' },
            c.label,
            React.createElement('span', { className: 'cp-related-arrow' }, '→')
          ),
          c.desc && React.createElement('span', { className: 'cp-related-desc' }, c.desc)
        ))
      )
    );
  }

  // ── draggable cycle timeline ───────────────────────────────────────────────
  // Each plotted protocol is a thin bar on a shared time axis (fromMs→toMs). Grab
  // the middle to move the whole run; grab an end to change just its start or finish.
  // Commits on release (the chart recomputes once, not every frame).
  const TL_MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  function CycleTimeline({ rows, fromMs, toMs, onStart, onFinish, onToggle }) {
    const trackRef = useRef(null);
    const [w, setW] = useState(600);
    const dragRef = useRef(null);
    const [, setTick] = useState(0);
    const rerender = () => setTick(t => t + 1);

    useEffect(() => {
      const el = trackRef.current; if (!el) return;
      const ro = new ResizeObserver(() => { const r = el.getBoundingClientRect(); if (r.width) setW(r.width); });
      ro.observe(el); return () => ro.disconnect();
    }, []);

    const span = Math.max(1, toMs - fromMs);
    const xOf = (ms) => ((ms - fromMs) / span) * w;
    const localX = (ev) => (ev.touches ? ev.touches[0].clientX : ev.clientX) - trackRef.current.getBoundingClientRect().left;

    function onMove(ev) {
      const d = dragRef.current; if (!d) return;
      if (ev.cancelable) ev.preventDefault();
      const dDays = Math.round((((localX(ev) - d.originPx) / w) * span) / DAY);
      let s = d.origStart, f = d.origFinish;
      if (d.mode === 'move') { s = d.origStart + dDays * DAY; f = d.origFinish + dDays * DAY; }
      else if (d.mode === 'start') { s = Math.min(d.origStart + dDays * DAY, d.origFinish - DAY); }
      else { f = Math.max(d.origFinish + dDays * DAY, d.origStart + DAY); }
      dragRef.current = Object.assign({}, d, { startMs: s, finishMs: f });
      rerender();
    }
    function onUp() {
      const d = dragRef.current; dragRef.current = null;
      window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp);
      window.removeEventListener('touchmove', onMove); window.removeEventListener('touchend', onUp);
      if (d) {
        if (d.startMs !== d.origStart) onStart(d.id, msToIso(d.startMs));
        if (d.finishMs !== d.origFinish) onFinish(d.id, msToIso(d.finishMs));
      }
      rerender();
    }
    function onDown(ev, p) {
      if (ev.cancelable) ev.preventDefault();
      const px = localX(ev);
      const EDGE = 16;
      let mode = 'move';
      if (Math.abs(px - xOf(p.startMs)) <= EDGE) mode = 'start';
      else if (Math.abs(px - xOf(p.finishMs)) <= EDGE) mode = 'finish';
      dragRef.current = { id: p.id, mode, originPx: px, origStart: p.startMs, origFinish: p.finishMs, startMs: p.startMs, finishMs: p.finishMs };
      rerender();
      window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp);
      window.addEventListener('touchmove', onMove, { passive: false }); window.addEventListener('touchend', onUp);
    }

    const d = dragRef.current;
    const md = (ms) => { const dt = new Date(ms); return dt.getDate() + ' ' + TL_MON[dt.getMonth()]; };

    return React.createElement('div', { className: 'cp-tl' },
      React.createElement('div', { className: 'cp-tl-head' }, 'Cycle timing — drag to adjust'),
      React.createElement('div', { className: 'cp-tl-rows', ref: trackRef },
        rows.map(p => {
          const live = (d && d.id === p.id) ? d : p;
          const l = Math.max(0, Math.min(w, xOf(live.startMs)));
          const r = Math.max(0, Math.min(w, xOf(live.finishMs)));
          const dragging = !!(d && d.id === p.id);
          return React.createElement('div', { key: p.id, className: 'cp-tl-row' },
            React.createElement('div', { className: 'cp-tl-name' },
              // tick box, coloured to the protocol's line — toggles it on/off the chart
              React.createElement('button', {
                className: 'cp-tl-check' + (p.enabled ? ' on' : ''),
                style: { '--c': p.color },
                'aria-pressed': p.enabled,
                title: p.enabled ? 'Hide from chart' : 'Show on chart',
                onClick: () => onToggle(p.id),
              }),
              React.createElement('span', { className: 'cp-tl-lbl' }, p.label),
              React.createElement('span', { className: 'cp-tl-range' }, md(live.startMs) + ' → ' + md(live.finishMs))
            ),
            React.createElement('div', { className: 'cp-tl-track' },
              React.createElement('div', {
                className: 'cp-tl-bar' + (dragging ? ' drag' : '') + (p.enabled ? '' : ' off'),
                style: { left: l + 'px', width: Math.max(12, r - l) + 'px', background: p.color },
                onMouseDown: (e) => onDown(e, p), onTouchStart: (e) => onDown(e, p),
                title: 'Drag the middle to move · drag an end to change start/finish',
              },
                React.createElement('span', { className: 'cp-tl-handle' }),
                React.createElement('span', { className: 'cp-tl-handle' })
              )
            )
          );
        })
      )
    );
  }

  // ── protocol selection chips (above the plotter) ───────────────────────────
  // Tick box per protocol, coloured to its line, toggles whether it's plotted.
  function ProtocolPicker({ rows, onToggle }) {
    return React.createElement('div', { className: 'cp-pick' },
      rows.map(p => React.createElement('button', {
        key: p.id, className: 'cp-pick-chip' + (p.enabled ? ' on' : ''),
        style: { '--c': p.color }, 'aria-pressed': p.enabled,
        title: p.enabled ? 'Hide from chart' : 'Show on chart',
        onClick: () => onToggle(p.id),
      },
        React.createElement('span', { className: 'cp-pick-box' }),
        React.createElement('span', { className: 'cp-pick-name' }, p.label)
      ))
    );
  }

  function App() {
    const today = useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d.getTime(); }, []);
    // Start empty so we never flash the sample chart before the user's real protocols
    // load (samples are set explicitly below only when nothing real is saved).
    const [protocols, setProtocols] = useState([]);
    // 'loading' until the user's saved protocols come back, then 'real' (their own
    // protocols) or 'samples' (fallback for an account with nothing plottable saved).
    const [protoSource, setProtoSource] = useState('loading');

    const [metric1, setMetric1] = useState('serum');
    const [metric2, setMetric2] = useState('serum');
    const [inj1, setInj1] = useState(true);
    const [inj2, setInj2] = useState(true);
    const [stats1, setStats1] = useState(true);
    const [stats2, setStats2] = useState(true);
    const [comb1, setComb1] = useState(true);
    const [comb2, setComb2] = useState(true);

    const [pickerOpen, setPickerOpen] = useState(false);
    const [toast, setToast] = useState(null);
    // auto-dismiss any toast after a few seconds (e.g. the "Saved to your protocol" note)
    useEffect(() => {
      if (!toast) return;
      const t = setTimeout(() => setToast(null), 2600);
      return () => clearTimeout(t);
    }, [toast]);
    const [activeChart, setActiveChart] = useState(0);
    const [fromPlanner, setFromPlanner] = useState(false);
    const [presetName, setPresetName] = useState(null);
    const chartsRef = useRef(null);
    const chartsWrapRef = useRef(null);

    // ── calculate gate: charts stay behind a placeholder until the user taps
    // Calculate. After the first calculate the charts update live; the button
    // stays as a 'Recalculate' affordance that re-highlights when inputs change. ──
    const [calculated, setCalculated] = useState(false);
    const [dirty, setDirty] = useState(false);
    // ── plotted = the SNAPSHOT of protocols the curves are drawn from. It only changes
    // when the user presses Calculate (or on an explicit plot action). Editing protocols
    // updates `protocols` (so the cards/sidebar react live) but NOT `plotted`, so the
    // curves — including the auto-summed Total — stay frozen until Calculate is pressed
    // (no live auto-sum). While they're out of sync (dirty) the chart greys out. ──
    const [plotted, setPlotted] = useState([]);
    // true while a bar is actively being dragged — hides the Calculate button until release
    const [dragging, setDragging] = useState(false);
    // one-time "Pick your protocol" watermark on first load (mobile) — cleared the moment
    // the user engages (picks/calculates), and not shown again this session.
    const [hintGone, setHintGone] = useState(false);
    function doCalculate() { trackInteraction(); setHintGone(true); setPlotted(protocols); setCalculated(true); setDirty(false); }
    function markDirty() { setDirty(true); }

    // ── auth: the plotter is now usable signed-out (quick single-compound plot).
    // Seed from the synchronous session flag the gate sets (window.__ibPlotterAuthed)
    // so the first render is right, then confirm against /api/me. ──
    const initialAuthed = useMemo(() => { try { return !!window.__ibPlotterAuthed; } catch (e) { return false; } }, []);
    const [loggedIn, setLoggedIn] = useState(initialAuthed);
    const [quickOpen, setQuickOpen] = useState(false);
    const [quickEditId, setQuickEditId] = useState(null);
    const GUEST_MAX = 2;
    const GUEST_TIP = 'Sign in to drag dates, reorder and save your cycle — free, connect your Google account in seconds.';
    useEffect(() => {
      let alive = true;
      fetch('/api/me/', { credentials: 'include' })
        .then(r => (r.ok ? r.json() : null))
        .then(data => { if (alive) setLoggedIn(!!(data && data.user)); })
        .catch(() => {});
      return () => { alive = false; };
    }, []);

    // Signed-out quick plot: add (up to GUEST_MAX) or edit a single compound from the
    // modal. Stacking beyond the cap + saving needs an account (the CTAs upsell that).
    function openQuickAdd() { setQuickEditId(null); setQuickOpen(true); trackInteraction(); setHintGone(true); }
    function openQuickEdit(id) { setQuickEditId(id); setQuickOpen(true); }
    function quickPlot(o) {
      const c = COMPOUNDS[o.compoundId];
      if (!c || !(o.dose > 0)) return;
      const start = o.start || defaultStartIso();
      const finish = o.finish || defaultFinishIso(start);
      const editId = quickEditId;
      const prev = editId ? protocols.find(p => p.id === editId) : null;
      // a real, saved protocol edited by a signed-in user → also persist to the account
      const isReal = !!(prev && loggedIn && protoSource === 'real' && prev.type && String(editId).indexOf('quick') !== 0);
      const row = {
        compoundId: o.compoundId, dose: o.dose, freqDays: o.freqDays,
        start: start, startMs: isoToMs(start), finish: finish, finishMs: isoToMs(finish),
        color: o.color || '#3b9bff', chart: (prev && prev.chart) || 1, enabled: prev ? prev.enabled !== false : true,
        // keep a saved protocol's own label; guests / new rows get an auto label
        label: (isReal && prev.label) ? prev.label : (c.short + ' ' + PK.fmt(o.dose) + c.unit),
        type: prev ? prev.type : undefined, config: prev ? prev.config : undefined,
      };
      // compute the next protocols array up-front so we can also snapshot it into `plotted`
      // for the paths that plot immediately (a fresh compound / a persisted edit).
      let next;
      if (editId) next = protocols.map(p => p.id === editId ? Object.assign({}, p, row) : p);
      else if (protocols.length >= GUEST_MAX) next = protocols;
      else next = protocols.concat([Object.assign({ id: 'quick-' + Date.now() }, row)]);
      setProtocols(next);
      if (isReal) {
        // merge dose/frequency back into the saved protocol's config; keep protoSource 'real'
        const cfgPatch = configPatchFor(prev.type, o.compoundId, o.dose, o.freqDays, prev.config);
        fetch('/api/dosages/' + editId, {
          method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ config: cfgPatch }),
        }).then(r => { if (r && r.ok) setToast('Saved to your protocol'); else setToast('Updated here — couldn’t save to your account'); })
          .catch(() => setToast('Updated here — couldn’t save to your account'));
        setPlotted(next); setCalculated(true); setDirty(false);
      } else {
        // new compound → plot immediately (snapshot it); editing an existing card → mark the
        // chart pending so the Calculate banner lights up ("Update chart") and stays frozen
        // (greyed) until the user applies it.
        setProtoSource('guest'); setCalculated(true); setDirty(!!editId);
        if (!editId) setPlotted(next);
      }
      setQuickOpen(false); setQuickEditId(null);
      trackInteraction();
      try { if (typeof posthog !== 'undefined' && posthog.capture) posthog.capture('plotter_quick_plot', { compound: o.compoundId, logged_in: !!loggedIn, edit: !!editId, persisted: isReal }); } catch (e) {}
    }

    // (Removed the mobile chart-controls vertical snap — it auto-scrolled the page when
    // you stopped near the chart, which fought the user's own scrolling.)

    // ── load protocols: a Cycle Planner preset wins, else the user's saved ones ──
    useEffect(() => {
      let alive = true;
      const preset = loadCyclePreset();
      if (preset) {
        setProtocols(preset.rows);
        setPlotted(preset.rows);
        setProtoSource('cycle');
        setFromPlanner(true);
        setPresetName(preset.name);
        setCalculated(true); // they explicitly asked to test — show charts straight away
        return;
      }
      // Signed out → no saved protocols to load; start empty and offer the quick-plot flow.
      if (!initialAuthed) { setProtoSource('guest'); return; }
      loadRealProtocols()
        .then(real => {
          if (!alive) return;
          if (real && real.length) { setProtocols(real); setProtoSource('real'); }
          else { setProtocols(window.SAMPLE_PROTOCOLS.map(p => ensureFinish({ ...p, startMs: isoToMs(p.start), enabled: false }))); setProtoSource('samples'); }
        })
        .catch(() => { if (alive) setProtoSource('samples'); });
      return () => { alive = false; };
    }, []);

    // ── one-time conversion signal for logged-out users ──
    const trackedRef = useRef(false);
    function trackInteraction() {
      if (trackedRef.current || loggedIn) return;
      trackedRef.current = true;
      // Never let an analytics hiccup (adblock / slow load) break a UI action — this
      // runs on the no-login quick-plot path, so it must not throw.
      try { if (typeof posthog !== 'undefined' && posthog.capture) posthog.capture('plotter_interaction', { logged_in: false }); } catch (e) {}
    }

    // Chart window spans ALL protocols' dates (so every draggable bar fits). Desktop
    // uses a small pad (less dead space) — the range simply grows as the user drags a
    // card's start earlier / finish later, and the bars get narrower to fit. Mobile
    // keeps a month of pad each side so the timeline feels like it scrolls into space.
    // Range spans the PLOTTED (snapshot) protocols, not the live ones — so editing a card
    // while dirty doesn't shift the x-axis until Calculate is pressed.
    const enabledList = (calculated ? plotted : protocols).filter(p => p.enabled);
    const isDesktop = typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(min-width: 941px)').matches;
    // Desktop: small pad (tight, less dead space). Mobile: a big pad each side so the
    // lines keep going and it feels like an endless scroll (the chart width scales with
    // the span, so this is more scroll distance — not a more-compressed chart).
    const PAD_MS = (isDesktop ? 5 : 180) * DAY;
    // Range spans the PLOTTED (enabled) protocols so dragging their bars fits. With nothing
    // plotted yet (the teaser / default load), show a ~2-month window starting about now —
    // NOT a range derived from saved protocols' (possibly old) start dates, which made
    // mobile open on far-off past dates.
    // Default (nothing plotted): start ~now. Mobile spans ~4 months so at the default zoom
    // (~6px/day ⇒ ~2 months visible) the chart scrolls ~2× the viewport, with today pinned
    // to the left edge (the centre-on-today scroll). Desktop stays compact (fits, no scroll).
    // Only span the enabled protocols once the user has clicked Calculate — until then keep
    // the default teaser window so ticking a protocol on doesn't shift the x-axis month
    // labels (the range stays put behind the ghost lines until Calculate).
    const realRange = calculated && enabledList.length;
    const fromMs = realRange ? Math.min.apply(null, enabledList.map(p => p.startMs)) - PAD_MS : (today - (isDesktop ? 7 : 60) * DAY);
    const toMs = realRange ? Math.max.apply(null, enabledList.map(p => p.finishMs)) + PAD_MS : (today + (isDesktop ? 70 : 60) * DAY);

    function toggle(id) {
      trackInteraction(); markDirty(); setHintGone(true);
      // flip, then re-sort active-first so a newly-activated card slides to the top
      setProtocols(ps => sortActiveFirst(ps.map(p => p.id === id ? { ...p, enabled: !p.enabled } : p)));
    }
    function setChart(id, n) {
      trackInteraction(); markDirty();
      setProtocols(ps => ps.map(p => p.id === id ? { ...p, chart: n } : p));
    }
    function setStart(id, val) {
      if (!val) return;
      trackInteraction(); markDirty();
      // dated:true → the bar now spans its real dates (the user has set them); until then
      // it stays the compact uniform width of the inactive cards.
      setProtocols(ps => ps.map(p => p.id === id ? { ...p, start: val, startMs: isoToMs(val), dated: true } : p));
    }
    // Move a protocol to a target ROW within its chart's card list (0 = top). The strip
    // shows every card on the chart in array order, so we reorder the chart's items in
    // place — writing them back into the same array slots they occupied — which keeps the
    // other chart's protocols untouched. Array order drives the strip stack + chart
    // brightness (top = brightest). targetIndex is a row index in the chart's card list.
    function reorderArr(ps, id, targetIndex, chartN) {
      const slots = ps.map((p, i) => ({ p, i })).filter(o => o.p.chart === chartN);
      const from = slots.findIndex(o => o.p.id === id);
      if (from < 0) return ps;
      const tgt = Math.max(0, Math.min(slots.length - 1, targetIndex));
      if (tgt === from) return ps;
      const order = slots.map(o => o.p);
      const moved = order.splice(from, 1)[0];
      order.splice(tgt, 0, moved);
      const next = ps.slice();
      slots.forEach((o, k) => { next[o.i] = order[k]; });
      return next;
    }
    function reorderTo(id, targetIndex, chartN) {
      trackInteraction();
      // Reordering is purely cosmetic (it changes which line is brightest), so apply it to
      // the live list AND the plotted snapshot so the stack restacks immediately — no
      // Calculate needed, and it never auto-recomputes the curves.
      setProtocols(ps => reorderArr(ps, id, targetIndex, chartN));
      setPlotted(ps => reorderArr(ps, id, targetIndex, chartN));
    }
    function setFinish(id, val) {
      if (!val) return;
      trackInteraction(); markDirty();
      setProtocols(ps => ps.map(p => p.id === id ? { ...p, finish: val, finishMs: isoToMs(val), dated: true } : p));
    }

    // ── universal rail integration (desktop) ──
    // The site rail's protocol checkboxes toggle which protocols plot here; we publish
    // our enabled-id set back so those checkboxes stay in sync (two-way, plotter-owned).
    useEffect(() => {
      function onRailToggle(e) {
        const id = e && e.detail && e.detail.id; if (!id) return;
        trackInteraction(); markDirty();
        setProtocols(ps => sortActiveFirst(ps.map(p => p.id === id ? { ...p, enabled: !p.enabled } : p)));
      }
      document.addEventListener('ib-rail:plot-toggle', onRailToggle);
      return () => document.removeEventListener('ib-rail:plot-toggle', onRailToggle);
    }, []);
    useEffect(() => {
      try {
        const ids = protocols.filter(p => p.enabled).map(p => p.id);
        window.__ibPlotEnabled = ids;
        document.dispatchEvent(new CustomEvent('ib-plot:enabled', { detail: { ids } }));
      } catch (e) {}
    }, [protocols]);
    function pickCompound(it) {
      try { sessionStorage.setItem('__ib_plotter_preset', JSON.stringify({ compoundId: it.id })); } catch (e) {}
      setPickerOpen(false);
      setToast('Opening your dashboard to set up ' + it.name + '…');
      setTimeout(() => { window.location.href = '/account/'; }, 1200);
    }

    function onScroll() {
      const el = chartsRef.current; if (!el) return;
      setActiveChart(Math.round(el.scrollLeft / el.clientWidth));
    }
    function goChart(i) {
      const el = chartsRef.current; if (!el) return;
      el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' });
    }

    const enabledCount = protocols.filter(p => p.enabled).length;
    // first-load watermark: only while nothing is picked/calculated yet and not dismissed
    const showHint = !hintGone && enabledCount === 0 && !calculated;

    return React.createElement(React.Fragment, null,
      // ── layout (site nav above handles brand + theme toggle) ──
      React.createElement('div', { className: 'cp-grid' },
        // sidebar
        React.createElement('aside', { className: 'cp-side' },
          React.createElement('div', { className: 'cp-card cp-proto-card' },
            React.createElement('div', { className: 'cp-side-label cp-proto-head' },
              React.createElement('span', null, protoSource === 'samples' ? 'Example protocols' : protoSource === 'cycle' ? (presetName || 'Cycle preview') : protoSource === 'guest' ? 'Your compound' : 'Your protocols'),
              React.createElement('span', { className: 'cp-count' }, enabledCount + '/' + protocols.length + ' shown')
            ),
            protoSource === 'samples' && React.createElement('p', { className: 'cp-proto-hint' },
              'No saved protocols found — these are examples. Save a dose from any calculator to plot your own.'),
            protoSource === 'cycle' && React.createElement('p', { className: 'cp-proto-hint' },
              'Previewing your planned cycle. Tweaks here are just for the chart — they are not saved back to the planner.'),
            protoSource === 'guest' && protocols.length > 0 && React.createElement('p', { className: 'cp-proto-hint' },
              'Signed out — plotting one compound. Sign in to stack more and save your cycle.'),
            React.createElement('div', { className: 'cp-proto-list' },
              protocols.map(p => React.createElement(ProtocolRow, { key: p.id, p, onToggle: toggle, onChart: setChart, onStart: setStart }))
            ),
            React.createElement('button', { className: 'cp-add', onClick: () => { if (loggedIn) { trackInteraction(); setPickerOpen(true); } else openQuickAdd(); } },
              loggedIn ? '＋  Plot a new compound' : '＋  Plot a compound'),
            React.createElement('a', {
              className: 'cp-planner-card',
              href: loggedIn ? '/account/' : '/account/login/?next=%2Fcycle-plotter%2F',
              onClick: () => { trackInteraction(); try { if (typeof posthog !== 'undefined') posthog.capture('plotter_planner_cta', { location: 'sidebar', logged_in: loggedIn }); } catch (e) {} },
            },
              React.createElement('div', { className: 'cp-planner-card-title' },
                loggedIn ? 'Open the Cycle Planner' : 'Save & track real cycles'),
              React.createElement('div', { className: 'cp-planner-card-sub' },
                loggedIn
                  ? 'Plan phases, log your injections and track doses over time.'
                  : 'Sign in to save this plan, log injections and track real cycles in the Cycle Planner.'),
              React.createElement('span', { className: 'cp-planner-card-link' },
                (loggedIn ? 'Open Cycle Planner' : 'Sign in to continue'),
                React.createElement('span', { className: 'cp-planner-cta-arrow' }, '→'))
            )
          ),
          React.createElement('p', { className: 'cp-disclaimer' },
            'Estimates from published half-lives using a simplified pharmacokinetic model. Individual results vary — not medical advice.')
        ),

        // main / charts
        React.createElement('main', { className: 'cp-main' },
          protoSource === 'loading'
            ? React.createElement('div', { className: 'cp-gate' },
                React.createElement('div', { className: 'cp-spinner', role: 'status', 'aria-label': 'Loading' }),
                React.createElement('div', { className: 'cp-gate-sub' }, 'Loading your protocols…'))
            : protocols.length > 0
            ? (loggedIn
              ? React.createElement(React.Fragment, null,
                React.createElement('div', { className: 'cp-charts-wrap', ref: chartsWrapRef },
                  React.createElement('div', { className: 'cp-charts', ref: chartsRef, onScroll },
                    React.createElement(ChartPanel, { n: 1, protocols, plotted, metric: metric1, onMetric: setMetric1, showInj: inj1, showStats: stats1, showCombined: comb1, onInj: () => setInj1(v => !v), onStats: () => setStats1(v => !v), onCombined: () => setComb1(v => !v), onToggle: toggle, onStart: setStart, onFinish: setFinish, onReorderTo: reorderTo, onEdit: openQuickEdit, onDragActive: setDragging, calculated, dirty, fromMs, toMs, showHint }),
                    React.createElement(ChartPanel, { n: 2, protocols, plotted, metric: metric2, onMetric: setMetric2, showInj: inj2, showStats: stats2, showCombined: comb2, onInj: () => setInj2(v => !v), onStats: () => setStats2(v => !v), onCombined: () => setComb2(v => !v), onToggle: toggle, onStart: setStart, onFinish: setFinish, onReorderTo: reorderTo, onEdit: openQuickEdit, onDragActive: setDragging, calculated, dirty, fromMs, toMs, showHint })
                  ),
                  // Calculate — centred over the chart; reappears whenever something changes.
                  // Hidden while a bar is being dragged (returns once they release).
                  // With nothing ticked on, it prompts "Select a protocol" (disabled).
                  (!calculated || dirty) && !dragging && React.createElement('button', {
                    key: 'calc', className: 'cp-calc-over',
                    onClick: enabledCount === 0 ? undefined : doCalculate,
                    disabled: enabledCount === 0,
                    style: enabledCount === 0 ? { opacity: 0.72, cursor: 'default' } : undefined,
                  }, enabledCount === 0 ? 'Select a protocol' : (!calculated ? 'Calculate' : 'Update chart'))
                ),
                // mobile nav (dots + arrows)
                React.createElement('div', { className: 'cp-carousel-nav' },
                  React.createElement('button', { className: 'cp-arrow', onClick: () => goChart(0), disabled: activeChart === 0 }, '‹'),
                  React.createElement('div', { className: 'cp-dots' },
                    [0, 1].map(i => React.createElement('button', { key: i, className: 'cp-dot' + (activeChart === i ? ' on' : ''), onClick: () => goChart(i) },
                      React.createElement('span', null, 'Chart ' + (i + 1))
                    ))
                  ),
                  React.createElement('button', { className: 'cp-arrow', onClick: () => goChart(1), disabled: activeChart === 1 }, '›')
                ),
                // mobile-only: the 2nd chart (peptides & GLP-1) is parked behind a "coming
                // soon" placeholder for now (the switcher + 2nd slide are hidden via CSS).
                React.createElement('div', { className: 'cp-mobile-soon' },
                  React.createElement('span', { className: 'cp-mobile-soon-badge' }, 'Coming soon'),
                  React.createElement('div', { className: 'cp-mobile-soon-title' }, 'Second chart'),
                  React.createElement('div', { className: 'cp-mobile-soon-sub' }, 'A dedicated chart for peptides & GLP-1 is coming to mobile soon.')
                ),
                // mobile-only fixed Calculate banner: greyed when up to date, glows when
                // a recalculation is needed. Hidden while dragging a bar (returns on release).
                !dragging && React.createElement('button', {
                  className: 'cp-calc-banner' + ((!calculated || dirty) && enabledCount > 0 ? ' active' : ''),
                  onClick: enabledCount === 0 ? undefined : doCalculate,
                  disabled: enabledCount === 0 || (calculated && !dirty),
                }, enabledCount === 0 ? 'Select a protocol' : (!calculated ? 'Calculate' : (dirty ? 'Update chart' : 'Chart up to date')))
              )
              // signed-out: one chart (no dragging) + tappable compound cards underneath
              // + add-another (up to GUEST_MAX) and a sign-in upsell.
              : React.createElement(React.Fragment, null,
                  React.createElement('div', { className: 'cp-charts-wrap', ref: chartsWrapRef },
                    // no inline overflow override here: on mobile .cp-charts must keep its
                    // CSS overflow-x:hidden so the chart's inner horizontal scroll region
                    // can't leak into the page width (iOS Safari sideways-spill). Matches
                    // the signed-in chart container, which doesn't have this bug.
                    React.createElement('div', { className: 'cp-charts', ref: chartsRef },
                      React.createElement(ChartPanel, { n: (protocols[0] && protocols[0].chart) || 1, protocols, plotted, metric: metric1, onMetric: setMetric1, showInj: inj1, showStats: stats1, showCombined: comb1, onInj: () => setInj1(v => !v), onStats: () => setStats1(v => !v), onCombined: () => setComb1(v => !v), onToggle: toggle, onStart: setStart, onFinish: setFinish, onReorderTo: reorderTo, onEdit: openQuickEdit, calculated, dirty, fromMs, toMs, noDrag: true, showHint })
                    ),
                    (!calculated || dirty) && React.createElement('button', { key: 'calc', className: 'cp-calc-over', onClick: doCalculate }, !calculated ? 'Calculate' : 'Update chart')
                  ),
                  // (the grey compound boxes were removed — the coloured timeline cards under
                  //  the chart now carry the ✎ edit). Mobile Calculate / Update banner, matching
                  //  the signed-in view; greyed when up to date, glows when a change is pending.
                  React.createElement('button', {
                    className: 'cp-calc-banner' + ((!calculated || dirty) ? ' active' : ''),
                    onClick: doCalculate, disabled: calculated && !dirty,
                  }, !calculated ? 'Calculate' : (dirty ? 'Update chart' : 'Chart up to date')),
                  React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center', justifyContent: 'center', margin: '16px 0 4px' } },
                    protocols.length < GUEST_MAX && React.createElement('button', {
                      type: 'button', onClick: openQuickAdd,
                      style: { height: 44, padding: '0 22px', borderRadius: 8, cursor: 'pointer', background: 'var(--cp-input)', border: '1px solid var(--cp-border)', color: 'var(--cp-text)', fontFamily: 'Inter,sans-serif', fontSize: 14, fontWeight: 600 },
                    }, '＋  Add another compound'),
                    React.createElement('a', {
                      href: '/account/signup/?next=%2Fcycle-plotter%2F', title: GUEST_TIP,
                      onClick: () => { try { if (typeof posthog !== 'undefined' && posthog.capture) posthog.capture('plotter_signup_cta', { location: 'guest_actions', logged_in: false }); } catch (e) {} },
                      style: { height: 44, padding: '0 22px', display: 'inline-flex', alignItems: 'center', borderRadius: 8, textDecoration: 'none', background: 'var(--cp-teal)', color: '#06201d', fontFamily: 'Inter,sans-serif', fontSize: 14, fontWeight: 700 },
                    }, 'Sign in to stack more & save →')
                  )
                )
              )
            : React.createElement('div', { className: 'cp-gate' },
                React.createElement('div', { className: 'cp-gate-title' }, loggedIn ? 'Plot your cycles' : 'Plot a compound, free'),
                React.createElement('p', { className: 'cp-gate-sub' }, loggedIn ? 'Tick a protocol below to plot it, then drag its bar to set the start and finish dates.' : 'See your blood levels over time — pick a compound, dose and frequency. No account needed.'),
                React.createElement('button', { className: 'cp-calc-btn cp-gate-btn', onClick: () => { if (loggedIn) { trackInteraction(); doCalculate(); } else openQuickAdd(); } }, loggedIn ? 'Calculate' : 'Plot a compound')
              )
        )
      ),

      React.createElement(RelatedStrip, null),
      React.createElement(CompoundPicker, { open: pickerOpen, onClose: () => setPickerOpen(false), onPick: pickCompound }),
      React.createElement(QuickPlotModal, {
        open: quickOpen,
        loggedIn: loggedIn,
        onClose: () => { setQuickOpen(false); setQuickEditId(null); },
        onPlot: quickPlot,
        initial: quickEditId ? (protocols.find(p => p.id === quickEditId) || null) : null,
        suggestColor: protocols.some(p => p.color === '#3b9bff' && p.id !== quickEditId) ? '#ff8a1f' : '#3b9bff',
      }),
      toast && React.createElement('div', { className: 'cp-toast' }, toast)
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
})();
