// fc-cycle.jsx, cycle-intelligence UI (Logic & Flows §15).
// Building blocks the Menstrual Cycle screen composes by mode:
//   CycleModeSeg     , natural / contraception / absent selector (§15.1)
//   PhaseIndicator   , where you are + plain-language read (§15.3)
//   AbsentCycleCard  , supportive "cycle adjustments paused" state (§15.2)
//   ContraceptionCard, why the phase model is off on HC (§15.1)
//   LearnedCard      , learned vs population, confidence (§15.4)
//   SymptomCheckCard , quick log + period confirm/flag (§15.3)
// Theme-aware via CSS vars; reuses window primitives.

const { Card, Eyebrow, GroupedList, Row, Toggle, SectionHeader } = window;

// segmented control for the three cycle modes
function CycleModeSeg({ value, onChange }) {
  const opts = [['natural', 'Natural'], ['contraception', 'On contraception'], ['absent', 'No / irregular']];
  return (
    <div style={{ display: 'flex', background: 'var(--paper-2)', borderRadius: 11, padding: 3, gap: 3 }}>
      {opts.map(([k, lbl]) => {
        const on = k === value;
        return (
          <button key={k} onClick={() => onChange(k)} style={{ flex: 1, textAlign: 'center', fontFamily: 'var(--sans)', fontSize: 12.5, fontWeight: on ? 600 : 500, color: on ? 'var(--ink)' : 'var(--ink-3)', background: on ? 'var(--paper)' : 'transparent', boxShadow: on ? '0 1px 2px rgba(0,0,0,0.12)' : 'none', borderRadius: 8, padding: '8px 4px', lineHeight: 1.2 }}>{lbl}</button>
        );
      })}
    </div>
  );
}

// the four-phase ring + "you are here"
function PhaseRing({ size = 64, dayToday, length }) {
  const CP = window.CYCLE_PHASE;
  const segs = [['menstrual', 0, 5], ['follicular', 5, 13], ['ovulation', 13, 16], ['luteal', 16, length]];
  const r = size / 2 - 5, cx = size / 2, cy = size / 2, C = 2 * Math.PI * r;
  const arc = (a, b, col) => {
    const off = (a / length) * C, len = ((b - a) / length) * C;
    return <circle key={a} cx={cx} cy={cy} r={r} fill="none" stroke={col} strokeWidth="5" strokeDasharray={`${len.toFixed(1)} ${(C - len).toFixed(1)}`} strokeDashoffset={(-off).toFixed(1)} transform={`rotate(-90 ${cx} ${cy})`} opacity="0.85" />;
  };
  const ang = (dayToday / length) * 2 * Math.PI - Math.PI / 2;
  const mx = cx + r * Math.cos(ang), my = cy + r * Math.sin(ang);
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ flexShrink: 0 }}>
      {segs.map(([k, a, b]) => arc(a, b, CP[k].color))}
      <circle cx={mx} cy={my} r="4.5" fill="var(--ink)" stroke="var(--paper)" strokeWidth="2" />
    </svg>
  );
}

function PhaseIndicator({ inverted = false }) {
  const FC = window.FC, CP = window.CYCLE_PHASE;
  const phaseKey = window.cyclePhaseAt(FC.todayDay);
  const ph = CP[phaseKey];
  const hold = window.cycleOffsetKg(FC.todayDay);
  const fg = inverted ? 'var(--paper)' : 'var(--ink)';
  const sub = inverted ? 'rgba(246,244,239,0.78)' : 'var(--ink-2)';
  return (
    <Card inverted={inverted} pad={22}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Eyebrow color={inverted ? 'rgba(246,244,239,0.6)' : 'var(--accent)'}>Current phase</Eyebrow>
        <span style={{ fontFamily: 'var(--display)', fontSize: 11, letterSpacing: '0.12em', fontWeight: 600, color: inverted ? 'rgba(246,244,239,0.55)' : 'var(--ink-3)' }}>DAY {FC.cycle.dayToday} / {FC.cycle.length}</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginTop: 14 }}>
        <PhaseRing dayToday={FC.cycle.dayToday} length={FC.cycle.length} />
        <div style={{ flex: 1 }}>
          <div style={{ fontFamily: 'var(--sans)', fontSize: 21, fontWeight: 700, color: fg, letterSpacing: '-0.01em' }}>{ph.label}</div>
          <div style={{ fontFamily: 'var(--sans)', fontSize: 14, lineHeight: 1.45, color: sub, marginTop: 5, textWrap: 'pretty' }}>
            {hold > 0.15 ? <>Expect to hold about <strong style={{ color: inverted ? '#f4a299' : 'var(--accent)', fontWeight: 600 }}>+{hold.toFixed(1)} kg</strong> of water. Read the trend, not the daily scale.</>
              : hold < -0.15 ? <>Water’s at its lowest, readings run a touch light right now.</>
              : <>Water-neutral phase. The scale reads close to true.</>}
          </div>
        </div>
      </div>
    </Card>
  );
}

// Compact dashboard glance, surfaces the cycle read on Today and links through
// to the full cycle section (Zoe's note). Only shown when cycle tracking is on.
function CycleGlanceRow({ onOpen }) {
  const FC = window.FC, CP = window.CYCLE_PHASE;
  const phaseKey = window.cyclePhaseAt(FC.todayDay), ph = CP[phaseKey];
  const hold = window.cycleOffsetKg(FC.todayDay);
  const note = hold > 0.15 ? <>Holding about <strong style={{ color: 'var(--accent)', fontWeight: 600 }}>+{hold.toFixed(1)} kg</strong> water. Read the trend</>
    : hold < -0.15 ? 'Water at its low. Readings run light' : 'Water-neutral. Scale reads true';
  return (
    <button onClick={onOpen} style={{ width: '100%', textAlign: 'left', display: 'flex', alignItems: 'center', gap: 14, background: 'var(--paper)', border: 'var(--card-border)', boxShadow: 'var(--card-shadow)', borderRadius: 'var(--radius)', padding: '14px 16px' }}>
      <PhaseRing size={42} dayToday={FC.cycle.dayToday} length={FC.cycle.length} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--display)', fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--ink-3)', fontWeight: 700 }}>Your cycle · day {FC.cycle.dayToday}/{FC.cycle.length}</div>
        <div style={{ fontFamily: 'var(--sans)', fontSize: 15, fontWeight: 600, color: 'var(--ink)', marginTop: 3 }}>{ph.label}</div>
        <div style={{ fontFamily: 'var(--sans)', fontSize: 12.5, color: 'var(--ink-2)', lineHeight: 1.4, marginTop: 2, textWrap: 'pretty' }}>{note}</div>
      </div>
      <svg width="8" height="14" viewBox="0 0 8 14" fill="none" style={{ flexShrink: 0 }}><path d="M1 1 L7 7 L1 13" stroke="var(--rule-2)" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>
    </button>
  );
}

// Absent / paused-cycle state, supportive and matter-of-fact, NOT a medical
// warning. Losing your period through hard training and cutting is a reality for
// many fighters; selecting "no / irregular" simply switches cycle adjustments off
// (prior cycle data is kept) and offers a one-tap way to flag when it returns.
function AbsentCycleCard({ severity = 'amenorrhea', onLearn = null }) {
  const watch = severity === 'watch';
  const eyebrow = watch ? 'Period running late' : 'Cycle adjustments paused';
  const head = watch ? 'Looks like your period’s running late.'
    : 'Cycle tracking is switched off.';
  const body = watch
    ? 'Your period’s just late, no stress. Cycle adjustments stay paused until you log the next one, so a late start can’t skew your targets. Tap below when it arrives.'
    : 'Hard training and big cuts stop a lot of fighters’ periods. It’s common. Cycle adjustments are off so they can’t skew your targets, and your past data’s saved for when things settle.';
  return (
    <div style={{ background: 'rgba(21,20,15,0.04)', border: '1px solid var(--rule)', borderRadius: 'var(--radius)', padding: 18 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <span aria-hidden="true" style={{ display: 'inline-flex', gap: 2.5 }}>
          <span style={{ width: 2.5, height: 12, background: 'var(--ink-3)', borderRadius: 1 }} />
          <span style={{ width: 2.5, height: 12, background: 'var(--ink-3)', borderRadius: 1 }} />
        </span>
        <span style={{ fontFamily: 'var(--display)', fontSize: 10.5, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--ink-3)', fontWeight: 700 }}>{eyebrow}</span>
      </div>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 17, fontWeight: 700, lineHeight: 1.3, color: 'var(--ink)', marginTop: 11, textWrap: 'balance' }}>{head}</div>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 13.5, lineHeight: 1.55, color: 'var(--ink-2)', marginTop: 8, textWrap: 'pretty' }}>{body}</div>
      <button style={{ width: '100%', background: 'var(--ink)', color: 'var(--paper)', border: 'none', borderRadius: 'var(--radius-ctl)', minHeight: 46, fontFamily: 'var(--sans)', fontSize: 14, fontWeight: 600, marginTop: 15 }}>{watch ? 'Log period start' : 'My period’s back'}</button>
    </div>
  );
}

function ContraceptionCard() {
  return (
    <Card pad={20}>
      <Eyebrow color="var(--accent)">On hormonal contraception</Eyebrow>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 16.5, fontWeight: 700, lineHeight: 1.3, color: 'var(--ink)', marginTop: 9, textWrap: 'balance' }}>We don’t apply cycle phases on hormonal birth control.</div>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 13.5, lineHeight: 1.55, color: 'var(--ink-2)', marginTop: 8, textWrap: 'pretty' }}>
        Birth control flattens the natural hormone swing, so the calendar model doesn’t fit. Any effect on water is small and personal, so we read <strong style={{ fontWeight: 600 }}>your logged symptoms</strong> instead.
      </div>
      <div style={{ marginTop: 14 }}>
        <GroupedList>
          <Row label="Has a withdrawal / placebo week" chevron={false} control={<Toggle on={true} />} note="If your method has a break week, flag it and we’ll watch for water then." last />
        </GroupedList>
      </div>
    </Card>
  );
}

// Learned-from-your-cycles surface (§15.4)
function LearnedCard() {
  const L = window.LEARNED_CYCLE;
  const conf = { low: ['Low confidence', 'var(--ink-3)'], medium: ['Building confidence', 'var(--mustard-deep)'], high: ['High confidence', 'var(--t-green)'] }[L.confidence];
  return (
    <Card pad={20}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10 }}>
        <Eyebrow color="var(--accent)">Learned from your cycles</Eyebrow>
        <span style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.05em', textTransform: 'uppercase', color: conf[1], fontWeight: 700, border: `1px solid ${conf[1]}`, borderRadius: 100, padding: '2px 9px' }}>{L.cyclesObserved} cycles · {conf[0]}</span>
      </div>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 16.5, fontWeight: 700, lineHeight: 1.3, color: 'var(--ink)', marginTop: 10, textWrap: 'balance' }}>
        You hold more water than average, about +{L.learnedPeakKg.toFixed(1)} kg, peaking a little later.
      </div>
      <div style={{ display: 'flex', gap: 0, marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--rule)' }}>
        {[['Your peak', `+${L.learnedPeakKg.toFixed(1)}`, 'kg', 'var(--accent)'], ['Typical', `+${L.populationPeakKg.toFixed(1)}`, 'kg', 'var(--ink-3)'], ['Peak day', `${L.learnedPeakDay}`, `/ ${window.FC.cycle.length}`, 'var(--ink)']].map(([lb, v, u, c], i) => (
          <div key={lb} style={{ flex: 1, paddingLeft: i ? 16 : 0, borderLeft: i ? '1px solid var(--rule)' : 'none' }}>
            <div style={{ fontFamily: 'var(--sans)', fontSize: 11, fontWeight: 600, color: 'var(--ink-3)' }}>{lb}</div>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 3, marginTop: 7 }}><span style={{ fontFamily: 'var(--num)', fontWeight: 300, fontSize: 25, letterSpacing: '-0.02em', color: c }}>{v}</span><span style={{ fontFamily: 'var(--mono)', fontSize: 9.5, color: 'var(--ink-3)', fontWeight: 600 }}>{u}</span></div>
          </div>
        ))}
      </div>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 12.5, color: 'var(--ink-3)', lineHeight: 1.5, marginTop: 14, textWrap: 'pretty' }}>
        Your plan now uses your number, not the textbook one. The more you log, the sharper this gets. <span style={{ color: 'var(--accent)', fontWeight: 600 }}>Adjust manually</span>
      </div>
    </Card>
  );
}

// Cycle placement aid (§15.3). The job: help a fighter identify WHERE she is in
// her cycle, because cycles run irregular for so many in this sport and the
// calendar drifts. The payoff is always the same, the water swing is already
// built into the weight plan, so an unexpected scale bump never threatens making
// weight. NOT training, NOT readiness (future), NOT a medical diagnosis. Period
// confirm is the hard anchor; a few water-relevant body signs place you when the
// calendar can't. Works in natural / contraception / irregular modes.
const _PLACE_SIGNS = [
  ['Bloated / puffy', 'luteal'], ['Breast tenderness', 'luteal'], ['Cravings', 'luteal'],
  ['Cramps', 'menstrual'], ['Feeling light', 'follicular'],
];
function _holdAt(cycleDay) {
  const { length, retention } = window.FC.cycle;
  return +(retention * -Math.cos(2 * Math.PI * (cycleDay - 10) / length)).toFixed(2);
}
// representative cycle day per phase → so the water figure is the model's, not invented
const _PHASE_DAY = { menstrual: 2, follicular: 9, ovulation: 14, luteal: 24 };
const _PLACE_READ = {
  luteal:     { tone: 'var(--accent)',  read: (h) => `You’ll hold about +${h.toFixed(1)} kg of water through this stretch. It’s already in your weekly target, so a higher scale number isn’t a setback. It clears once your period starts.` },
  menstrual:  { tone: 'var(--accent)',  read: (h) => `Water clings for the first day or two of your period, then drops away. Your plan already expects this, so don’t chase a stalled scale right now.` },
  follicular: { tone: 'var(--t-green)', read: (h) => `The fresh week after your period. Water sits at its lowest, so the scale reads close to true. A clean point to see where you really are.` },
  ovulation:  { tone: 'var(--t-green)', read: (h) => `Mid-cycle. Water’s low to neutral around now, so the scale’s reading honest.` },
};
function SymptomCheckCard() {
  const FC = window.FC, CP = window.CYCLE_PHASE;
  const [picked, setPicked] = React.useState(['Bloated / puffy', 'Breast tenderness']);
  const [bleeding, setBleeding] = React.useState(false);
  const toggle = (s) => setPicked(l => l.includes(s) ? l.filter(x => x !== s) : [...l, s]);
  // place the athlete: a logged bleed is the hard anchor; else the body signs vote.
  let est = null;
  if (bleeding) est = 'menstrual';
  else if (picked.length) {
    const score = {};
    picked.forEach(s => { const ph = (_PLACE_SIGNS.find(x => x[0] === s) || [])[1]; if (ph) score[ph] = (score[ph] || 0) + 1; });
    est = Object.keys(score).sort((a, b) => score[b] - score[a])[0] || null;
  }
  const calKey = window.cyclePhaseAt(FC.todayDay);          // what the calendar thinks
  const hold = est ? Math.abs(_holdAt(_PHASE_DAY[est])) : 0;
  const R = est ? _PLACE_READ[est] : null;
  const agrees = est && est === calKey;

  return (
    <Card pad={20}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
        <Eyebrow color="var(--accent)">Where are you?</Eyebrow>
        <span style={{ fontFamily: 'var(--sans)', fontSize: 13, color: 'var(--ink-3)' }}>Optional</span>
      </div>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 14, color: 'var(--ink-2)', marginTop: 8, lineHeight: 1.45, textWrap: 'pretty' }}>Cycles run irregular for a lot of fighters, so the calendar drifts. Tell us what your body’s doing and we’ll place you.</div>

      {/* hard anchor: a logged period re-fixes the dates */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginTop: 16, padding: '12px 14px', background: bleeding ? 'var(--accent-soft)' : 'var(--paper-2)', border: `1px solid ${bleeding ? 'var(--accent)' : 'var(--rule)'}`, borderRadius: 12 }}>
        <div style={{ flex: 1 }}>
          <div style={{ fontFamily: 'var(--sans)', fontSize: 14.5, fontWeight: 600, color: 'var(--ink)' }}>My period started</div>
          <div style={{ fontFamily: 'var(--sans)', fontSize: 12, color: 'var(--ink-3)', marginTop: 2, textWrap: 'pretty' }}>The one sure signal. It re-anchors your dates.</div>
        </div>
        <button onClick={() => setBleeding(b => !b)} style={{ flexShrink: 0, background: bleeding ? 'var(--accent)' : 'var(--ink)', color: 'var(--paper)', border: 'none', borderRadius: 'var(--radius-ctl)', minHeight: 42, padding: '0 18px', fontFamily: 'var(--sans)', fontSize: 14, fontWeight: 600 }}>{bleeding ? 'Logged ✓' : 'Log'}</button>
      </div>

      {/* soft signs: place you when the calendar can't */}
      <div style={{ fontFamily: 'var(--sans)', fontSize: 12.5, fontWeight: 600, color: 'var(--ink-3)', marginTop: 16, marginBottom: 9 }}>Or tap what your body’s doing today</div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
        {_PLACE_SIGNS.map(([s]) => {
          const on = picked.includes(s);
          return <button key={s} onClick={() => toggle(s)} disabled={bleeding} style={{ fontFamily: 'var(--sans)', fontSize: 13, fontWeight: on ? 600 : 500, color: bleeding ? 'var(--ink-3)' : on ? 'var(--ink)' : 'var(--ink-2)', background: on && !bleeding ? 'var(--accent-soft)' : 'var(--paper-2)', border: `1px solid ${on && !bleeding ? 'var(--accent)' : 'var(--rule)'}`, borderRadius: 100, padding: '8px 14px', minHeight: 38, opacity: bleeding ? 0.5 : 1 }}>{s}</button>;
        })}
      </div>

      {/* the placement + the throughline: it's already in your plan */}
      <div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--rule)' }}>
        {R ? (
          <div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{ width: 8, height: 8, borderRadius: '50%', background: R.tone, flexShrink: 0 }} />
              <span style={{ fontFamily: 'var(--display)', fontSize: 11, letterSpacing: '0.14em', textTransform: 'uppercase', color: R.tone, fontWeight: 700 }}>Looks like {CP[est].label.toLowerCase()}</span>
              <span style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.04em', textTransform: 'uppercase', color: 'var(--ink-3)', fontWeight: 600, marginLeft: 'auto' }}>{bleeding ? 'Confirmed' : 'Estimate'}</span>
            </div>
            <div style={{ fontFamily: 'var(--sans)', fontSize: 13.5, color: 'var(--ink-2)', lineHeight: 1.5, marginTop: 9, textWrap: 'pretty' }}>{R.read(hold)}</div>
            {/* does it match the calendar? */}
            <div style={{ marginTop: 12, fontFamily: 'var(--sans)', fontSize: 12.5, color: 'var(--ink-3)', lineHeight: 1.45, textWrap: 'pretty' }}>
              {agrees
                ? <>Lines up with your calendar, day {FC.cycle.dayToday} of {FC.cycle.length}. Nothing to change.</>
                : <>Your calendar had you in <b style={{ color: 'var(--ink-2)' }}>{CP[calKey].label.toLowerCase()}</b>. Going by your body, we’ll nudge the plan to match, <a style={{ color: 'var(--accent)', fontWeight: 600 }}>re-anchor dates</a>.</>}
            </div>
          </div>
        ) : (
          <div style={{ fontFamily: 'var(--sans)', fontSize: 13, color: 'var(--ink-3)', lineHeight: 1.5, textWrap: 'pretty' }}>Log your period or tap a sign and we’ll place you, then show how that water swing is handled in your plan.</div>
        )}
      </div>
    </Card>
  );
}

Object.assign(window, { CycleModeSeg, PhaseRing, PhaseIndicator, CycleGlanceRow, AbsentCycleCard, RedSCard: AbsentCycleCard, ContraceptionCard, LearnedCard, SymptomCheckCard });

// ── Cycle forecast graph (§15.3/§15.4) ───────────────────────────────────────
// Forward-looking: predicted water swing across the cycle (personalised), your
// recent actual weigh-ins (de-trended) against it, a YOU-ARE-HERE marker, and
// what's coming. Answers "is this normal, and what should I expect next?"
function _cfsp(pts, k = 0.6) {
  if (pts.length < 2) return '';
  let d = `M ${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[Math.max(0, i - 1)], p1 = pts[i], p2 = pts[i + 1], p3 = pts[Math.min(pts.length - 1, i + 2)];
    d += ` C ${(p1[0] + (p2[0] - p0[0]) * k / 3).toFixed(2)} ${(p1[1] + (p2[1] - p0[1]) * k / 3).toFixed(2)}, ${(p2[0] - (p3[0] - p1[0]) * k / 3).toFixed(2)} ${(p2[1] - (p3[1] - p1[1]) * k / 3).toFixed(2)}, ${p2[0].toFixed(2)} ${p2[1].toFixed(2)}`;
  }
  return d;
}

function FCCycleForecastChart({ width = 331, height = 210 }) {
  const FC = window.FC, L = window.LEARNED_CYCLE, CP = window.CYCLE_PHASE;
  const len = FC.cycle.length, dayToday = FC.cycle.dayToday;
  const amp = L.blendKg, peakDay = L.learnedPeakDay, band = 0.45;   // ± uncertainty
  // expected water offset (kg above/below de-trended baseline) at a cycle day
  const off = (d) => amp * Math.cos(2 * Math.PI * (((d - peakDay) % len + len) % len > len / 2 ? (((d - peakDay) % len + len) % len) - len : (((d - peakDay) % len + len) % len)) / len);
  const phaseOf = (d) => { const c = ((d % len) + len) % len; return c < 5 ? 'menstrual' : c < 13 ? 'follicular' : c < 16 ? 'ovulation' : 'luteal'; };
  const back = 9, fwd = 18;                       // window: -9d … +18d around today
  const d0 = dayToday - back, d1 = dayToday + fwd;
  const W = width, H = height, padL = 8, padR = 38, padT = 26, padB = 30;
  const innerW = W - padL - padR, innerH = H - padT - padB;
  const yMax = amp + band + 0.35, yMin = -(amp + band + 0.35);
  const xS = (d) => padL + ((d - d0) / (d1 - d0)) * innerW;
  const yS = (v) => padT + ((yMax - v) / (yMax - yMin)) * innerH;
  const zeroY = yS(0);

  // sampled predicted curve + band
  const samp = []; for (let d = d0; d <= d1; d++) samp.push(d);
  const mid = samp.map(d => [xS(d), yS(off(d))]);
  const top = samp.map(d => [xS(d), yS(off(d) + band)]);
  const bot = samp.map(d => [xS(d), yS(off(d) - band)]);
  const bandPath = 'M ' + top.map(p => `${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ') + ' L ' + bot.reverse().map(p => `${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ') + ' Z';

  // recent actual de-trended weigh-ins (sit inside the band → "normal")
  const actualDays = [dayToday - 8, dayToday - 6, dayToday - 4, dayToday - 2, dayToday];
  const jit = [-0.10, 0.12, -0.06, 0.14, 0.05];
  const actual = actualDays.map((d, i) => [d, off(d) + jit[i]]);
  const todayActual = actual[actual.length - 1];
  const inBand = Math.abs(todayActual[1] - off(dayToday)) <= band;

  // phase bands across window
  const segs = []; let segStart = d0, segPh = phaseOf(d0);
  for (let d = d0 + 1; d <= d1; d++) { const ph = phaseOf(d); if (ph !== segPh || d === d1) { segs.push([segStart, d, segPh]); segStart = d; segPh = ph; } }

  // upcoming peak (max offset strictly ahead of today)
  let peak = null; for (let d = dayToday + 1; d <= d1; d++) { if (!peak || off(d) > peak.v) peak = { d, v: off(d) }; }

  return (
    <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ display: 'block', overflow: 'visible' }} role="img"
      aria-label={`Cycle weight forecast. Today you're ${todayActual[1] >= 0 ? 'holding' : 'below'} about ${todayActual[1].toFixed(1)} kg of water, ${inBand ? 'right where expected' : 'outside the expected range'}.`}>
      {/* phase bands */}
      {segs.map(([a, b, ph], i) => <rect key={i} x={xS(a)} y={padT} width={xS(b) - xS(a)} height={innerH} fill={CP[ph].color} opacity="0.12" />)}
      {segs.filter(([a, b]) => xS(b) - xS(a) > 26).map(([a, b, ph], i) => <text key={'l' + i} x={(xS(a) + xS(b)) / 2} y={padT - 9} textAnchor="middle" fontFamily="var(--mono)" fontSize="7.5" letterSpacing="0.04em" fill={CP[ph].color} fontWeight="700">{CP[ph].short.toUpperCase()}</text>)}
      {/* baseline (your trend) */}
      <line x1={padL} x2={W - padR} y1={zeroY} y2={zeroY} stroke="var(--ink-3)" strokeWidth="1" strokeDasharray="2 3" opacity="0.6" />
      <text x={W - padR + 5} y={zeroY + 3} fontFamily="var(--mono)" fontSize="8" fill="var(--ink-3)" fontWeight="600">TREND</text>
      {/* predicted band + line */}
      <path d={bandPath} fill="var(--accent)" opacity="0.12" />
      <path d={_cfsp(mid)} fill="none" stroke="var(--accent)" strokeWidth="2" strokeDasharray="5 3" opacity="0.85" />
      {/* +kg axis ticks */}
      {[amp, -amp].map(v => <text key={v} x={W - padR + 5} y={yS(v) + 3} fontFamily="var(--num)" fontSize="11" fill="var(--ink-3)">{v > 0 ? '+' : ''}{v.toFixed(1)}</text>)}
      {/* actual de-trended dots */}
      {actual.slice(0, -1).map(([d, v], i) => <circle key={i} cx={xS(d)} cy={yS(v)} r="2.8" fill="var(--ink)" opacity="0.4" />)}
      {/* today marker */}
      <line x1={xS(dayToday)} x2={xS(dayToday)} y1={padT} y2={padT + innerH} stroke="var(--ink)" strokeWidth="1" opacity="0.45" />
      <circle cx={xS(dayToday)} cy={yS(todayActual[1])} r="5" fill={inBand ? 'var(--t-green)' : 'var(--mustard-deep)'} stroke="var(--paper)" strokeWidth="1.5" />
      {/* upcoming peak marker */}
      {peak && (<g>
        <circle cx={xS(peak.d)} cy={yS(peak.v)} r="3.4" fill="none" stroke="var(--accent)" strokeWidth="1.6" />
        <text x={xS(peak.d)} y={yS(peak.v) - 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="8" fill="var(--accent)" fontWeight="700">PEAK +{peak.v.toFixed(1)}</text>
      </g>)}
      {/* x labels */}
      <g style={{ fontFeatureSettings: '"tnum"' }}>
        <text x={xS(dayToday)} y={H - padB + 16} textAnchor="middle" fontFamily="var(--sans)" fontSize="10.5" fill="var(--ink-2)" fontWeight="600">Today</text>
        <text x={W - padR} y={H - padB + 16} textAnchor="end" fontFamily="var(--sans)" fontSize="10.5" fill="var(--ink-3)">+{fwd}d</text>
        <text x={padL} y={H - padB + 16} textAnchor="start" fontFamily="var(--sans)" fontSize="10.5" fill="var(--ink-3)">−{back}d</text>
      </g>
    </svg>
  );
}

function FCCycleForecastCard({ width = 331 }) {
  const FC = window.FC, L = window.LEARNED_CYCLE, CP = window.CYCLE_PHASE, len = FC.cycle.length, dayToday = FC.cycle.dayToday;
  const amp = L.blendKg, peakDay = L.learnedPeakDay;
  const off = (d) => amp * Math.cos(2 * Math.PI * (((d - peakDay) % len + len) % len > len / 2 ? (((d - peakDay) % len + len) % len) - len : (((d - peakDay) % len + len) % len)) / len);
  const todayOff = off(dayToday);
  const phaseName = (d) => { const c = ((d % len) + len) % len; return c < 5 ? 'menstrual' : c < 13 ? 'follicular' : c < 16 ? 'ovulation' : 'luteal'; };
  // scan ahead for the next turning point (peak or trough)
  let peak = { d: dayToday, v: -99 }, trough = { d: dayToday, v: 99 };
  for (let d = dayToday + 1; d <= dayToday + 21; d++) { if (off(d) > peak.v) peak = { d, v: off(d) }; if (off(d) < trough.v) trough = { d, v: off(d) }; }
  const nearPeakNow = todayOff > 0 && (peak.v - todayOff) < 0.12;     // already at/over the crest
  const plural = (n) => `${n} day${n === 1 ? '' : 's'}`;
  let coming;
  if (nearPeakNow) {
    coming = <><strong style={{ fontWeight: 600 }}>Coming up:</strong> your water’s peaking now. It should ease over the next week toward your <strong style={{ fontWeight: 600 }}>{phaseName(trough.d)}</strong> low. The scale will drift down. That part is fluid, not fat.</>;
  } else {
    const daysToPeak = peak.d - dayToday;
    coming = <><strong style={{ fontWeight: 600 }}>Coming up:</strong> your water peaks ~<strong style={{ fontWeight: 600 }}>+{peak.v.toFixed(1)} kg in {plural(daysToPeak)}</strong> ({phaseName(peak.d)}). The scale will climb then. That’s fluid, not fat, so hold the plan.</>;
  }
  const head = todayOff > 0.15
    ? `You’re holding about +${todayOff.toFixed(1)} kg of water right now, and that’s exactly what we’d expect.`
    : todayOff < -0.15
      ? `Water’s near its low. Readings run a touch light right now, which is normal.`
      : `You’re water-neutral today. The scale’s reading close to true.`;
  return (
    <div style={{ background: 'var(--paper)', borderRadius: 'var(--radius)', boxShadow: 'var(--card-shadow)', border: 'var(--card-border)', padding: 20 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10 }}>
        <span style={{ fontFamily: 'var(--display)', fontSize: 10.5, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--accent)', fontWeight: 600 }}>Cycle weight forecast</span>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.05em', textTransform: 'uppercase', color: 'var(--t-green)', fontWeight: 700 }}><span style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--t-green)' }} />On track</span>
      </div>
      <div style={{ fontFamily: 'var(--sans)', fontSize: 18, fontWeight: 700, lineHeight: 1.28, color: 'var(--ink)', marginTop: 9, textWrap: 'balance' }}>{head}</div>
      <div style={{ marginTop: 14 }}><FCCycleForecastChart width={width} /></div>
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 9, marginTop: 14, paddingTop: 13, borderTop: '1px solid var(--rule)' }}>
        <svg width="15" height="15" viewBox="0 0 16 16" fill="none" style={{ marginTop: 2, flexShrink: 0 }}><path d="M8 4 V8 L11 10" stroke="var(--ink-3)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><circle cx="8" cy="8" r="6.3" stroke="var(--ink-3)" strokeWidth="1.4"/></svg>
        <div style={{ fontFamily: 'var(--sans)', fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.5, textWrap: 'pretty' }}>{coming}</div>
      </div>
    </div>
  );
}

Object.assign(window, { FCCycleForecastChart, FCCycleForecastCard });
