/* the turn - Conditions. The full read of what the water's doing right now,
   and a grading system: how each condition stacks for a catch today.
   Reached by tapping the conditions strip (Today/Explore) or the Conditions
   tab. Fishability gauge + factor grades, tide curve, barometric trend, wind
   & swell compass dials, the day's light (sunrise/sunset) and the moon phase.
   All charts/dials are data-driven SVG; lime stays rationed (verdict only). */

/* ---- time helpers ---- */
function mins(hhmm) { const [h, m] = String(hhmm).split(':').map(Number); return h * 60 + (m || 0); }
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }

/* ---- grading vocabulary. Good conditions read GREEN so the page scans at a
       glance: lime = this is working for you, amber = working against, muted =
       dead. (Earlier the design rationed lime to the GO badge; the owner wants
       the good stuff lit up instead.) ---- */
const GRADE = {
  A: { word: 'prime', tone: 'var(--verdict-go)',      fill: 4 },
  B: { word: 'good',  tone: 'var(--verdict-go)',      fill: 3 },
  C: { word: 'fair',  tone: 'var(--text-label)',      fill: 2 },
  D: { word: 'poor',  tone: 'var(--verdict-caution)', fill: 1 },
  E: { word: 'dead',  tone: 'var(--verdict-dont)',    fill: 0 },
};

/* impact meter - 4 segments, filled by grade */
function ImpactMeter({ grade, style = {} }) {
  const g = GRADE[grade] || GRADE.C;
  return (
    <span style={{ display: 'inline-flex', gap: 3, alignItems: 'center', ...style }}>
      {[0, 1, 2, 3].map(i => (
        <span key={i} style={{
          width: 6, height: 13, borderRadius: 2,
          background: i < g.fill ? g.tone : 'transparent',
          border: `1.5px solid ${i < g.fill ? g.tone : 'var(--border-strong)'}`,
        }} />
      ))}
    </span>
  );
}
/* grade badge - letter + impact word */
function GradeBadge({ grade, impact, style = {} }) {
  const g = GRADE[grade] || GRADE.C;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, ...style }}>
      {impact && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, letterSpacing: '0.06em', textTransform: 'uppercase', color: g.tone }}>{impact}</span>}
      <span style={{
        width: 24, height: 24, borderRadius: 'var(--r-xs)', display: 'grid', placeItems: 'center',
        fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 13, color: g.tone,
        border: `1.5px solid ${g.tone}`, background: grade === 'D' ? 'color-mix(in srgb, var(--verdict-caution) 12%, var(--bg))' : 'transparent',
      }}>{grade}</span>
    </span>
  );
}

/* ---- info affordance + deep-dive panel (the 'why it matters' learn layer) ---- */
/* The visible "i" stays 26px; the BUTTON is a 48px invisible square around it.
   Cold-thumb misses at 05:00 are real - tap target floor is 48px even where
   the visual stays small. */
function InfoButton({ open, onClick }) {
  return (
    <button type="button" onClick={onClick} aria-label="why it matters" aria-expanded={open} style={{
      flex: '0 0 auto', width: 48, height: 48, margin: -11, display: 'grid', placeItems: 'center', cursor: 'pointer',
      background: 'transparent', border: 'none', padding: 0,
    }}>
      <span aria-hidden="true" style={{
        width: 26, height: 26, borderRadius: '50%', display: 'grid', placeItems: 'center',
        border: `1.5px solid ${open ? 'var(--keyline-soft)' : 'var(--border-strong)'}`,
        background: open ? 'var(--surface-raised)' : 'transparent', color: open ? 'var(--text)' : 'var(--text-label)',
        fontFamily: 'var(--font-display)', fontWeight: 700, fontStyle: 'italic', fontSize: 14, lineHeight: 1,
      }}>i</span>
    </button>
  );
}
/* Split content on blank lines (\n\n) and render each as its own paragraph
   so the comprehensive info blocks read like a properly laid-out brief. */
function Paragraphs({ text, color = 'var(--text-dim)' }) {
  const parts = String(text || '').split(/\n\s*\n/);
  return (
    <div>
      {parts.map((p, i) => (
        <p key={i} style={{ margin: i ? '10px 0 0' : 0, fontSize: 13.5, color, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{p}</p>
      ))}
    </div>
  );
}

function InfoPanel({ data }) {
  return (
    <div style={{ marginTop: 14, padding: '14px 16px', background: 'var(--surface-raised)', border: '1px solid var(--border)', borderRadius: 'var(--r-md)' }}>
      <div className="overline" style={{ marginBottom: 8 }}>why it matters</div>
      <Paragraphs text={data.matters} color="var(--text-dim)" />
      {data.look && (
        <div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--hairline)' }}>
          <div className="overline" style={{ marginBottom: 6 }}>what to look for</div>
          <Paragraphs text={data.look} color="var(--text)" />
        </div>
      )}
    </div>
  );
}

/* ---- shared card chrome (head carries the condition's grade + a learn 'i') ---- */
function CondCard({ label, grade, impact, info, right, children, style = {} }) {
  const { useState } = React;
  const [open, setOpen] = useState(false);
  return (
    <section style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-md)', padding: 'var(--space-5)', ...style }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
        <span className="overline">{label}</span>
        <span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 10 }}>
          {grade ? <GradeBadge grade={grade} impact={impact} /> : right}
          {info && <InfoButton open={open} onClick={() => setOpen(o => !o)} />}
        </span>
      </div>
      {children}
      {info && open && <InfoPanel data={info} />}
    </section>
  );
}
function ReadNote({ children, tone = 'var(--text-dim)' }) {
  return (
    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 9, marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--hairline)' }}>
      <span style={{ flex: '0 0 auto', width: 5, height: 5, borderRadius: '50%', background: 'var(--text-label)', marginTop: 7 }} />
      <span style={{ fontSize: 13.5, color: tone, lineHeight: 1.4 }}>{children}</span>
    </div>
  );
}
function BigVal({ value, unit, color = 'var(--text)', size = 38 }) {
  return (
    <span style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: size, lineHeight: 1, color, letterSpacing: '-0.02em', whiteSpace: 'nowrap' }}>
      {value}{unit && <span style={{ fontSize: '0.42em', color: 'var(--text-label)', marginLeft: 4, letterSpacing: 0 }}>{unit}</span>}
    </span>
  );
}

/* ============================================================
   FISHABILITY - overall gauge + per-condition factor grades
   ============================================================ */
function Gauge({ score, color }) {
  const R = 50, C = 60, circ = 2 * Math.PI * R;
  const frac = clamp(score / 100, 0, 1);
  return (
    <svg viewBox="0 0 120 120" width="116" height="116" role="img" aria-label={`fishability ${score}`}>
      <circle cx={C} cy={C} r={R} fill="none" stroke="var(--border)" strokeWidth="9" />
      <circle cx={C} cy={C} r={R} fill="none" stroke={color} strokeWidth="9" strokeLinecap="round"
        strokeDasharray={`${(frac * circ).toFixed(1)} ${circ.toFixed(1)}`} transform={`rotate(-90 ${C} ${C})`} />
      <text x={C} y={C - 2} textAnchor="middle" fontFamily="var(--font-display)" fontWeight="700" fontSize="32" fill="var(--text)">{score}</text>
      <text x={C} y={C + 18} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10" letterSpacing="0.08em" fill="var(--text-label)">/ 100</text>
    </svg>
  );
}
function FactorRow({ label, grade, impact }) {
  const g = GRADE[grade] || GRADE.C;
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', borderTop: '1px solid var(--hairline)' }}>
      <span className="overline" style={{ flex: '0 0 70px' }}>{label}</span>
      <span style={{ flex: 1, fontFamily: 'var(--font-mono)', fontSize: 12, color: g.tone, textTransform: 'lowercase' }}>{impact}</span>
      <ImpactMeter grade={grade} />
      <span style={{ width: 18, textAlign: 'right', fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 13, color: g.tone }}>{grade}</span>
    </div>
  );
}
function Fishability({ c }) {
  const { useState } = React;
  const [open, setOpen] = useState(false);
  const f = c.fishability;
  const color = (window.VERDICT[f.verdict] || {}).color || 'var(--keyline)';
  const factors = [
    { label: 'tide', grade: c.tide.grade, impact: c.tide.impact },
    { label: 'swell', grade: c.swell.grade, impact: c.swell.impact },
    { label: 'wind', grade: c.wind.grade, impact: c.wind.impact },
    { label: 'pressure', grade: c.pressure.grade, impact: c.pressure.impact },
    { label: 'light', grade: c.sun.grade, impact: c.sun.impact },
    { label: 'moon', grade: c.moon.grade, impact: c.moon.impact },
  ];
  return (
    <section style={{ background: 'var(--surface)', border: `1.5px solid ${color === 'var(--keyline)' ? 'var(--border-strong)' : color}`, borderRadius: 'var(--r-lg)', padding: 'var(--space-5)', boxShadow: f.verdict === 'go' ? 'var(--glow-go)' : 'none' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
        <div style={{ flex: '0 0 auto' }}><Gauge score={f.score} color={color} /></div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <div className="overline">fishability</div>
            {f.explain && <span style={{ marginLeft: 'auto' }}><InfoButton open={open} onClick={() => setOpen(o => !o)} /></span>}
          </div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 5 }}>
            <span style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 26, color, letterSpacing: '-0.02em', textTransform: 'lowercase' }}>{f.word}</span>
          </div>
          <p style={{ margin: '8px 0 0', fontSize: 13.5, color: 'var(--text-dim)', lineHeight: 1.4 }}>{f.line}</p>
        </div>
      </div>
      {f.explain && open && <InfoPanel data={f.explain} />}
      <div style={{ marginTop: 14 }}>
        <div className="overline" style={{ fontSize: 10, marginBottom: 2 }}>how each condition's stacking</div>
        {factors.map(ft => <FactorRow key={ft.label} {...ft} />)}
      </div>
    </section>
  );
}

/* ============================================================
   COMPASS DIAL - shared by wind & swell (airflow / wave-travel arrow)
   ============================================================ */
function CompassDial({ deg, size = 124, big = false }) {
  const C = 60, r = 46;
  const ptR = (a, rr) => [C + rr * Math.sin(a * Math.PI / 180), C - rr * Math.cos(a * Math.PI / 180)];
  // meteorological dir = wind/swell FROM. Travels toward deg+180.
  const [fx, fy] = ptR(deg, r - 7);
  const [tx, ty] = ptR(deg + 180, r - 7);
  const ah = 8;
  const [hx1, hy1] = ptR(deg + 180 + 18, r - 7 - ah);
  const [hx2, hy2] = ptR(deg + 180 - 18, r - 7 - ah);
  const cardinals = [['N', 0], ['E', 90], ['S', 180], ['W', 270]];
  return (
    <svg viewBox="0 0 120 120" width={size} height={size} role="img" aria-label={`direction ${deg} degrees`}>
      <circle cx={C} cy={C} r={r} fill="none" stroke="var(--border)" strokeWidth="1" />
      <circle cx={C} cy={C} r={r - 14} fill="none" stroke="var(--hairline)" strokeWidth="1" />
      {/* 8 tick marks */}
      {[0, 45, 90, 135, 180, 225, 270, 315].map(a => {
        const [x1, y1] = ptR(a, r), [x2, y2] = ptR(a, a % 90 === 0 ? r - 6 : r - 3.5);
        return <line key={a} x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--border-strong)" strokeWidth={a % 90 === 0 ? 1.5 : 1} />;
      })}
      {cardinals.map(([lab, a]) => {
        const [lx, ly] = ptR(a, r + 9);
        return <text key={lab} x={lx} y={ly + 3.4} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10" fontWeight={lab === 'N' ? 700 : 400} fill={lab === 'N' ? 'var(--text-dim)' : 'var(--text-label)'}>{lab}</text>;
      })}
      {/* travel arrow */}
      <line x1={fx} y1={fy} x2={tx} y2={ty} stroke="var(--text)" strokeWidth="2.5" strokeLinecap="round" />
      <circle cx={fx} cy={fy} r="3.5" fill="var(--text)" />
      <path d={`M ${tx} ${ty} L ${hx1} ${hy1} M ${tx} ${ty} L ${hx2} ${hy2}`} fill="none" stroke="var(--text)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

/* ============================================================
   TIDE - 24h curve with extremes + now marker
   ============================================================ */
/* minutes-since-midnight -> "HH:MM" */
function fmtMins(m) {
  const h = Math.floor(m / 60) % 24, mm = Math.floor(m % 60);
  return `${String(h).padStart(2, '0')}:${String(mm).padStart(2, '0')}`;
}

function TideChart({ tide, now, threshold }) {
  const { useState } = React;
  const [hoverT, setHoverT] = useState(null);
  const W = 340, H = 158, padX = 12, plotTop = 16, plotBot = 116;
  const domainMax = 1.85;
  const ext = tide.series.map(p => ({ m: mins(p.t), h: p.h, type: p.type, t: p.t }));
  const HP = 372;
  const head = { m: ext[0].m - HP, h: ext[0].type === 'low' ? 1.5 : 0.35, type: ext[0].type === 'low' ? 'high' : 'low' };
  const tail = { m: ext[ext.length - 1].m + HP, h: ext[ext.length - 1].type === 'low' ? 1.5 : 0.35, type: ext[ext.length - 1].type === 'low' ? 'high' : 'low' };
  const full = [head, ...ext, tail];
  const heightAt = (t) => {
    for (let i = 0; i < full.length - 1; i++) {
      const a = full[i], b = full[i + 1];
      if (t >= a.m && t <= b.m) { const f = (t - a.m) / (b.m - a.m); return a.h + (b.h - a.h) * (1 - Math.cos(Math.PI * f)) / 2; }
    }
    return full[0].h;
  };
  const X = (t) => padX + (t / 1440) * (W - 2 * padX);
  const Y = (h) => plotBot - (h / domainMax) * (plotBot - plotTop);
  const N = 96;
  let d = '';
  for (let i = 0; i <= N; i++) { const t = (i / N) * 1440; d += `${i ? 'L' : 'M'} ${X(t).toFixed(1)} ${Y(heightAt(t)).toFixed(1)} `; }
  const area = `${d} L ${X(1440).toFixed(1)} ${plotBot} L ${X(0).toFixed(1)} ${plotBot} Z`;
  const nowM = mins(now);

  // Point anywhere to read the tide at that moment; falls back to "now".
  const onMove = (clientX, target) => {
    const rect = target.getBoundingClientRect();
    const t = clamp(((clientX - rect.left) / rect.width * W - padX) / (W - 2 * padX) * 1440, 0, 1440);
    setHoverT(t);
  };
  const aT = hoverT != null ? hoverT : nowM;
  const aX = X(aT), aY = Y(heightAt(aT));
  const aLabel = hoverT != null ? `${fmtMins(aT)} · ${heightAt(aT).toFixed(1)}m` : `now ${now}`;
  const c = 'var(--keyline)';

  // Lime "go" windows: 2 hours either side of every tide turn - the moving
  // water the fish feed on (the app's "eight productive hours of twelve" rule).
  // Built off `full` so a turn just past midnight still casts its band into view,
  // then merged so overlapping spring-tide windows don't double up.
  const GO_HALF = 120;
  const rawBands = full
    .map(p => [clamp(p.m - GO_HALF, 0, 1440), clamp(p.m + GO_HALF, 0, 1440)])
    .filter(([a, b]) => b - a > 0.5)
    .sort((a, b) => a[0] - b[0]);
  const goBands = [];
  for (const [a, b] of rawBands) {
    const last = goBands[goBands.length - 1];
    if (last && a <= last[1]) last[1] = Math.max(last[1], b);
    else goBands.push([a, b]);
  }

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', overflow: 'visible', touchAction: 'none', cursor: 'crosshair' }} role="img" aria-label="tide through the day, best fishing windows shaded"
      onMouseMove={(e) => onMove(e.clientX, e.currentTarget)}
      onMouseLeave={() => setHoverT(null)}
      onTouchStart={(e) => onMove(e.touches[0].clientX, e.currentTarget)}
      onTouchMove={(e) => onMove(e.touches[0].clientX, e.currentTarget)}
      onTouchEnd={() => setHoverT(null)}>
      {goBands.map(([a, b], i) => (
        <rect key={`go${i}`} x={X(a)} y={plotTop} width={X(b) - X(a)} height={plotBot - plotTop}
          fill="color-mix(in srgb, var(--verdict-go) 15%, transparent)" />
      ))}
      {/* height threshold (estuary calls: when the flats actually flood) -
          tailing happens over a height band, not at a phase. Same device as
          a close-out line: one dashed rule, zero new chrome. */}
      {threshold && threshold.h <= domainMax && (
        <g>
          <line x1={padX} y1={Y(threshold.h)} x2={W - padX} y2={Y(threshold.h)}
            stroke="var(--text-dim)" strokeWidth="1" strokeDasharray="5 4" opacity="0.75" />
          <text x={W - padX} y={Y(threshold.h) - 4} textAnchor="end"
            fontFamily="var(--font-mono)" fontSize="9" letterSpacing="0.06em" fill="var(--text-dim)">{threshold.label}</text>
        </g>
      )}
      <line x1={padX} y1={plotBot} x2={W - padX} y2={plotBot} stroke="var(--hairline)" strokeWidth="1" />
      <path d={area} fill="color-mix(in srgb, var(--keyline) 7%, transparent)" />
      <path d={d} fill="none" stroke="var(--text-dim)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
      {ext.map((p, i) => {
        const x = X(p.m), y = Y(p.h), high = p.type === 'high';
        return (
          <g key={i} opacity={hoverT != null ? 0.45 : 1}>
            <circle cx={x} cy={y} r="3" fill="var(--surface)" stroke="var(--text-dim)" strokeWidth="1.5" />
            <text x={x} y={high ? y - 9 : y + 16} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10.5" fill="var(--text-label)">{p.t}</text>
            <text x={x} y={high ? y - 21 : y + 28} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10.5" fontWeight="700" fill="var(--text-dim)">{p.h.toFixed(1)}m</text>
          </g>
        );
      })}
      <line x1={aX} y1={plotTop - 4} x2={aX} y2={plotBot} stroke={c} strokeWidth="1.5" strokeDasharray="2 3" opacity="0.8" />
      <circle cx={aX} cy={aY} r="5" fill={c} />
      <circle cx={aX} cy={aY} r="5" fill="none" stroke="var(--surface)" strokeWidth="1.5" />
      <text x={clamp(aX, 38, W - 38)} y={plotTop - 8} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10.5" fontWeight="700" fill="var(--text)">{aLabel}</text>
      {['06:00', '12:00', '18:00'].map((t) => (
        <text key={t} x={X(mins(t))} y={H - 4} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="9.5" fill="var(--text-label)">{t}</text>
      ))}
    </svg>
  );
}

/* ============================================================
   PRESSURE - sparkline + now dot
   ============================================================ */
function Sparkline({ series, accent = 'var(--keyline)', unit = '' }) {
  const { useState } = React;
  const [hi, setHi] = useState(null);
  const W = 300, H = 64, pad = 6;
  const min = Math.min(...series), max = Math.max(...series);
  const span = Math.max(1, max - min);
  const X = (i) => pad + (i / (series.length - 1)) * (W - 2 * pad);
  const Y = (v) => (H - pad) - ((v - min) / span) * (H - 2 * pad);
  const d = series.map((v, i) => `${i ? 'L' : 'M'} ${X(i).toFixed(1)} ${Y(v).toFixed(1)}`).join(' ');
  const li = series.length - 1;
  // series is 12 points every 2h from local midnight -> index i = hour i*2.
  const stepH = 24 / series.length;
  const timeOf = (i) => `${String(Math.round(i * stepH)).padStart(2, '0')}:00`;
  const onMove = (clientX, target) => {
    const rect = target.getBoundingClientRect();
    const i = clamp(Math.round((clientX - rect.left) / rect.width * (series.length - 1)), 0, li);
    setHi(i);
  };
  const ai = hi != null ? hi : li;
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', overflow: 'visible', touchAction: 'none', cursor: 'crosshair' }} role="img" aria-label="pressure trend"
      onMouseMove={(e) => onMove(e.clientX, e.currentTarget)}
      onMouseLeave={() => setHi(null)}
      onTouchStart={(e) => onMove(e.touches[0].clientX, e.currentTarget)}
      onTouchMove={(e) => onMove(e.touches[0].clientX, e.currentTarget)}
      onTouchEnd={() => setHi(null)}>
      <path d={`${d} L ${X(li)} ${H - pad} L ${X(0)} ${H - pad} Z`} fill="color-mix(in srgb, var(--text-dim) 8%, transparent)" />
      <path d={d} fill="none" stroke="var(--text-dim)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      {hi != null && <line x1={X(ai)} y1={2} x2={X(ai)} y2={H - pad} stroke="var(--keyline)" strokeWidth="1" strokeDasharray="2 3" opacity="0.7" />}
      <circle cx={X(ai)} cy={Y(series[ai])} r="4.5" fill={accent} />
      <circle cx={X(ai)} cy={Y(series[ai])} r="4.5" fill="none" stroke="var(--surface)" strokeWidth="1.5" />
      {hi != null && (
        <text x={clamp(X(ai), 34, W - 34)} y={Y(series[ai]) - 9} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10.5" fontWeight="700" fill="var(--text)">
          {Math.round(series[ai])}{unit} · {timeOf(ai)}
        </text>
      )}
    </svg>
  );
}

/* ============================================================
   PRESSURE TREND - 48h trajectory read against the "normal" line.
   The raw hPa number means little on its own. What predicts the bite is
   (a) how far the glass sits from normal (1013 hPa) and (b) which way it's
   sliding. So: a 48h curve (−24h..+24h), a dashed normal reference, faint
   high/low zones, and a regression line for the net direction.
   ============================================================ */
function PressureTrend({ p }) {
  const { useState } = React;
  const [hi, setHi] = useState(null);
  const win = (p.window48 && p.window48.v && p.window48.v.length) ? p.window48 : null;
  const vals = win ? win.v : (p.series && p.series.length ? p.series : [p.hpa]);
  const nowIdx = win ? win.nowIdx : vals.length - 1;
  const stepH = win ? win.stepH : 2;
  const n = vals.length;

  const NORMAL = 1013, LOWZONE = 1010, HIGHZONE = 1020;
  const dataMin = Math.min(...vals), dataMax = Math.max(...vals);
  const lo = Math.min(dataMin, LOWZONE - 2) - 1;
  const top = Math.max(dataMax, HIGHZONE + 2) + 1;
  const span = Math.max(8, top - lo);

  const W = 300, H = 96, padX = 6, padTop = 12, padBot = 16;
  const X = (i) => padX + (i / Math.max(1, n - 1)) * (W - 2 * padX);
  const Y = (v) => (H - padBot) - ((v - lo) / span) * (H - padTop - padBot);

  // least-squares fit across the window -> net trend line
  let sx = 0, sy = 0, sxx = 0, sxy = 0;
  vals.forEach((v, i) => { sx += i; sy += v; sxx += i * i; sxy += i * v; });
  const denom = n * sxx - sx * sx;
  const slope = denom ? (n * sxy - sx * sy) / denom : 0;
  const intercept = (sy - slope * sx) / n;
  const fit = (i) => intercept + slope * i;

  const trendColor = p.trend === 'falling' ? 'var(--verdict-go)'
                   : p.trend === 'rising' ? 'var(--verdict-caution)'
                   : 'var(--text-dim)';
  const d = vals.map((v, i) => `${i ? 'L' : 'M'} ${X(i).toFixed(1)} ${Y(v).toFixed(1)}`).join(' ');

  const onMove = (clientX, target) => {
    const rect = target.getBoundingClientRect();
    const i = clamp(Math.round((clientX - rect.left) / rect.width * (n - 1)), 0, n - 1);
    setHi(i);
  };
  const ai = hi != null ? hi : nowIdx;
  const offH = Math.round((ai - nowIdx) * stepH);
  const offLabel = offH === 0 ? 'now' : `${offH > 0 ? '+' : ''}${offH}h`;
  const dist = Math.round(p.hpa - NORMAL);

  return (
    <div>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', overflow: 'visible', touchAction: 'none', cursor: 'crosshair' }} role="img" aria-label="48-hour pressure trajectory against normal"
        onMouseMove={(e) => onMove(e.clientX, e.currentTarget)} onMouseLeave={() => setHi(null)}
        onTouchStart={(e) => onMove(e.touches[0].clientX, e.currentTarget)} onTouchMove={(e) => onMove(e.touches[0].clientX, e.currentTarget)} onTouchEnd={() => setHi(null)}>
        {/* high (settled - fish sulk) + low (pre-front - fish feed) zones */}
        <rect x={padX} y={Y(top)} width={W - 2 * padX} height={Math.max(0, Y(HIGHZONE) - Y(top))} fill="color-mix(in srgb, var(--verdict-caution) 7%, transparent)" />
        <rect x={padX} y={Y(LOWZONE)} width={W - 2 * padX} height={Math.max(0, Y(lo) - Y(LOWZONE))} fill="color-mix(in srgb, var(--verdict-go) 8%, transparent)" />
        {/* normal reference line */}
        <line x1={padX} y1={Y(NORMAL)} x2={W - padX} y2={Y(NORMAL)} stroke="var(--text-label)" strokeWidth="1" strokeDasharray="3 3" opacity="0.7" />
        <text x={W - padX} y={Y(NORMAL) - 3} textAnchor="end" fontFamily="var(--font-mono)" fontSize="8.5" fill="var(--text-label)">normal 1013</text>
        {/* regression / net trend */}
        <line x1={X(0)} y1={Y(fit(0))} x2={X(n - 1)} y2={Y(fit(n - 1))} stroke={trendColor} strokeWidth="1.2" strokeDasharray="4 3" opacity="0.5" />
        {/* the actual 48h trajectory */}
        <path d={d} fill="none" stroke={trendColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
        {/* now marker */}
        <line x1={X(nowIdx)} y1={padTop - 6} x2={X(nowIdx)} y2={H - padBot} stroke="var(--text-dim)" strokeWidth="1" strokeDasharray="2 3" opacity="0.6" />
        <circle cx={X(ai)} cy={Y(vals[ai])} r="4.5" fill={trendColor} />
        <circle cx={X(ai)} cy={Y(vals[ai])} r="4.5" fill="none" stroke="var(--surface)" strokeWidth="1.5" />
        <text x={clamp(X(ai), 32, W - 32)} y={Y(vals[ai]) - 9} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10.5" fontWeight="700" fill="var(--text)">{Math.round(vals[ai])} · {offLabel}</text>
        {/* x labels */}
        <text x={X(0)} y={H - 3} textAnchor="start" fontFamily="var(--font-mono)" fontSize="9" fill="var(--text-label)">−24h</text>
        <text x={X(nowIdx)} y={H - 3} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="9" fill="var(--text-label)">now</text>
        <text x={X(n - 1)} y={H - 3} textAnchor="end" fontFamily="var(--font-mono)" fontSize="9" fill="var(--text-label)">+24h</text>
      </svg>
      <div style={{ marginTop: 5, fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-label)' }}>
        {Math.abs(dist) <= 1 ? 'on the seasonal normal' : `${Math.abs(dist)} hPa ${dist > 0 ? 'above' : 'below'} normal`}
        {' · '}{p.trend === 'falling' ? 'falling - fish feed' : p.trend === 'rising' ? 'rising - bite eases' : 'holding steady'}
      </div>
    </div>
  );
}

/* wind day-build mini bars */
function MiniBars({ series, nowIdx, accent = 'var(--keyline)' }) {
  const { useState } = React;
  const [hi, setHi] = useState(null);
  const max = Math.max(...series);
  const stepH = 24 / series.length;
  const ai = hi != null ? hi : nowIdx;
  return (
    <div style={{ position: 'relative' }}>
      {ai != null && (
        <div style={{
          position: 'absolute', top: -2, left: `${(ai + 0.5) / series.length * 100}%`,
          transform: 'translateX(-50%)', fontFamily: 'var(--font-mono)', fontSize: 10,
          fontWeight: 700, color: 'var(--text)', whiteSpace: 'nowrap', pointerEvents: 'none',
        }}>{series[ai]} km/h · {String(Math.round(ai * stepH)).padStart(2, '0')}:00</div>
      )}
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 40, marginTop: 14, touchAction: 'none' }}
        onMouseLeave={() => setHi(null)}>
        {series.map((v, i) => (
          <span key={i}
            onMouseEnter={() => setHi(i)}
            onTouchStart={() => setHi(i)}
            style={{
              flex: 1, height: `${clamp((v / max) * 100, 8, 100)}%`, borderRadius: 2, cursor: 'crosshair',
              background: i === ai ? accent : 'var(--border-strong)',
            }} />
        ))}
      </div>
    </div>
  );
}

/* ============================================================
   LIGHT - daylight arc (sunrise → sunset) + sun position
   ============================================================ */
function DaylightArc({ sun, now }) {
  const W = 300, H = 96, padX = 18, baseY = 80, peakY = 14;
  const rise = mins(sun.rise), set = mins(sun.set), n = mins(now);
  const x0 = padX, x1 = W - padX;
  const arc = `M ${x0} ${baseY} A ${(x1 - x0) / 2} ${baseY - peakY} 0 0 1 ${x1} ${baseY}`;
  const f = (n - rise) / (set - rise);
  const up = f >= 0 && f <= 1;
  const fc = clamp(f, 0, 1);
  const sx = x0 + fc * (x1 - x0);
  const sy = baseY - (baseY - peakY) * Math.sin(Math.PI * fc);
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', overflow: 'visible' }} role="img" aria-label="daylight">
      <line x1={x0 - 4} y1={baseY} x2={x1 + 4} y2={baseY} stroke="var(--hairline)" strokeWidth="1" />
      <path d={arc} fill="none" stroke="var(--border-strong)" strokeWidth="1.5" strokeDasharray="3 4" />
      <circle cx={up ? sx : x0} cy={up ? sy : baseY} r="6.5" fill={up ? 'var(--verdict-caution)' : 'none'} stroke="var(--verdict-caution)" strokeWidth="2" opacity={up ? 1 : 0.6} />
      <text x={x0} y={baseY + 16} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10" fill="var(--text-label)">{sun.rise}</text>
      <text x={x1} y={baseY + 16} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="10" fill="var(--text-label)">{sun.set}</text>
      <text x={x0} y={baseY + 28} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="8.5" letterSpacing="0.06em" fill="var(--text-label)">SUNRISE</text>
      <text x={x1} y={baseY + 28} textAnchor="middle" fontFamily="var(--font-mono)" fontSize="8.5" letterSpacing="0.06em" fill="var(--text-label)">SUNSET</text>
    </svg>
  );
}

/* ============================================================
   MOON - phase disc (terminator path) + meta
   ============================================================ */
function MoonGlyph({ illum = 50, waxing = true, size = 64 }) {
  const C = 50, R = 44;
  const k = clamp(illum / 100, 0, 1);
  // invert lit-fraction → shadow-circle offset (two equal circles): lit = 1 - lens/π
  let lo = 0, hi = 1;
  for (let i = 0; i < 40; i++) {
    const u = (lo + hi) / 2;
    const lens = 2 * Math.acos(u) - 2 * u * Math.sqrt(Math.max(0, 1 - u * u));
    (1 - lens / Math.PI) < k ? (lo = u) : (hi = u);
  }
  const dx = ((lo + hi)) * R;                    // dx/R = 2u
  const shadowCx = C + (waxing ? -dx : dx);      // lune (lit) sits opposite the shadow centre
  const clipId = 'moonclip';
  return (
    <svg viewBox="0 0 100 100" width={size} height={size} role="img" aria-label="moon phase">
      <defs><clipPath id={clipId}><circle cx={C} cy={C} r={R} /></clipPath></defs>
      <circle cx={C} cy={C} r={R} fill="var(--surface-sunk)" />
      <g clipPath={`url(#${clipId})`}>
        <circle cx={C} cy={C} r={R} fill="#D8E6DC" />
        <circle cx={shadowCx} cy={C} r={R} fill="var(--surface-sunk)" />
      </g>
      <circle cx={C} cy={C} r={R} fill="none" stroke="var(--border-strong)" strokeWidth="1.5" />
    </svg>
  );
}

/* ---- small two-up / three-up meta row ---- */
function MetaPair({ items }) {
  return (
    <div style={{ display: 'flex' }}>
      {items.map((it, i) => (
        <div key={i} style={{ flex: 1, paddingLeft: i ? 16 : 0, borderLeft: i ? '1px solid var(--hairline)' : 'none' }}>
          <div className="overline" style={{ fontSize: 10 }}>{it.label}</div>
          <div className="metric" style={{ fontSize: 16, fontWeight: 700, color: 'var(--text)', marginTop: 5 }}>{it.value}</div>
        </div>
      ))}
    </div>
  );
}

/* ============================================================
   Conditions screen
   ============================================================ */
function Conditions() {
  const T = window.TURN;
  const c = T.conditions;
  // The demo bootstrap and any offline state carry only a thin conditions stub
  // (no per-factor series). The detail surface needs live engine data - show an
  // honest placeholder instead of white-screening on undefined.series.
  const hasLive = c && c.tide && Array.isArray(c.tide.series)
    && c.wind && Array.isArray(c.wind.series)
    && c.pressure && Array.isArray(c.pressure.series);
  if (!hasLive) {
    return (
      <div style={{ padding: '4px var(--gutter) 0', display: 'flex', flexDirection: 'column', gap: 14 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 22, color: 'var(--text)', letterSpacing: '-0.02em' }}>Conditions</div>
        <div style={{ display: 'grid', placeItems: 'center', minHeight: 240, textAlign: 'center', padding: 24, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-md)' }}>
          <div style={{ maxWidth: 340 }}>
            <div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 17, color: 'var(--text)' }}>
              <span style={{ width: 9, height: 9, borderRadius: '50%', background: 'var(--verdict-go)', flex: '0 0 auto', animation: 'turnpulse 1.4s ease-in-out infinite' }} />
              Pulling live conditions…
            </div>
            <div style={{ fontSize: 13, color: 'var(--text-dim)', marginTop: 8, lineHeight: 1.5 }}>Tide, pressure and wind fill in automatically the moment the forecast lands - usually a second or two. If it lingers, check your connection.</div>
            {window.__turnDataError && (
              <div style={{ marginTop: 14, padding: '8px 10px', background: 'var(--surface-sunk)', border: '1px solid var(--hairline-bold)', borderRadius: 'var(--r-sm, 8px)', fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-label)', lineHeight: 1.4, wordBreak: 'break-word' }}>
                why: {String(window.__turnDataError)}
              </div>
            )}
          </div>
          <style>{`@keyframes turnpulse { 0%,100% { opacity: 1 } 50% { opacity: .3 } }`}</style>
        </div>
      </div>
    );
  }
  const trendArrow = { rising: '▲', falling: '▼', steady: '→' }[c.pressure.trend] || '→';
  const tideArrow = c.tide.phase === 'rising' ? '▲' : c.tide.phase === 'falling' ? '▼' : '•';
  const nowWindIdx = clamp(Math.round((mins(c.now) / 1440) * (c.wind.series.length - 1)), 0, c.wind.series.length - 1);

  return (
    <div style={{ padding: '4px var(--gutter) 0', display: 'flex', flexDirection: 'column', gap: 14 }}>
      {/* title */}
      <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between' }}>
        <div>
          <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 22, color: 'var(--text)', letterSpacing: '-0.02em' }}>Conditions</div>
          <div className="overline" style={{ marginTop: 2 }}>{c.place} · {c.dateLine}</div>
        </div>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-label)' }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--text-label)' }} />cached {c.refreshed}
        </span>
      </div>

      {/* FISHABILITY - the grading system */}
      <Fishability c={c} />

      {/* TIDE */}
      <CondCard label="tide" grade={c.tide.grade} impact={c.tide.impact} info={c.tide.explain}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 6 }}>
          <BigVal value={c.tide.height} color="var(--text)" size={34} />
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--text-dim)' }}>
            <span style={{ color: 'var(--keyline)' }}>{tideArrow}</span>{c.tide.phase}
          </span>
          <span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--text-label)' }}>next {c.tide.next}</span>
        </div>
        <TideChart tide={c.tide} now={c.now} threshold={(() => {
          // Estuary pick today -> mark when the flats actually flood. Tailing
          // is a height-band question (~1.2m+), not a phase question.
          const pid = T.today && T.today.primary && T.today.primary.areaId;
          const pa = pid && areaById(pid);
          return pa && (pa.sub || '').toLowerCase().includes('estuar') ? { h: 1.2, label: 'FLATS FLOOD 1.2M' } : null;
        })()} />
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 6, fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-label)', letterSpacing: '.02em' }}>
          <span style={{ width: 16, height: 9, borderRadius: 2, background: 'color-mix(in srgb, var(--verdict-go) 28%, transparent)', border: '1px solid color-mix(in srgb, var(--verdict-go) 45%, transparent)', flex: '0 0 auto' }} />
          best windows - 2h either side of the turn, when the water's moving
        </div>
        <ReadNote>on the push into the morning high - dirty-clean seam works the gutters as it fills</ReadNote>
      </CondCard>

      {/* PRESSURE */}
      <CondCard label="barometric pressure" grade={c.pressure.grade} impact={c.pressure.impact} info={c.pressure.explain}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 10 }}>
          <BigVal value={c.pressure.hpa} unit="hPa" size={34} />
          <span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 13, color: c.pressure.trend === 'falling' ? 'var(--verdict-caution)' : 'var(--text-dim)' }}>{trendArrow} {c.pressure.trend}</span>
        </div>
        <PressureTrend p={c.pressure} />
        <ReadNote>{c.pressure.note}</ReadNote>
      </CondCard>

      {/* WIND */}
      <CondCard label="wind" grade={c.wind.grade} impact={c.wind.impact} info={c.wind.explain}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
          <div style={{ flex: '0 0 124px', position: 'relative' }}>
            <CompassDial deg={c.wind.deg} size={124} />
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <BigVal value={c.wind.kmh != null ? c.wind.kmh : c.wind.kt} unit={`km/h ${c.wind.dir}`} size={34} />
            <div className="metric" style={{ fontSize: 12, color: 'var(--text-label)', marginTop: 4 }}>from {c.wind.deg}° · gust {c.wind.gust} km/h</div>
            <div className="overline" style={{ fontSize: 10, margin: '14px 0 6px' }}>through the day</div>
            <MiniBars series={c.wind.series} nowIdx={nowWindIdx} />
            <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4, fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--text-label)' }}>
              <span>00h</span><span>now</span><span>22h</span>
            </div>
          </div>
        </div>
        <ReadNote>{c.wind.note}</ReadNote>
      </CondCard>

      {/* SWELL */}
      <CondCard label="swell" grade={c.swell.grade} impact={c.swell.impact} info={c.swell.explain}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
          <div style={{ flex: '0 0 104px' }}><CompassDial deg={c.swell.deg} size={104} /></div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 18 }}>
              <div><BigVal value={c.swell.m} unit="m" size={34} /></div>
              <div><BigVal value={c.swell.period} unit="s" size={34} /></div>
            </div>
            <div className="metric" style={{ fontSize: 12, color: 'var(--text-label)', marginTop: 6 }}>from {c.swell.dir} {c.swell.deg}°</div>
            {c.swell.secondary && c.swell.secondary.m >= 0.2 && (
              <div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--hairline)', display: 'flex', alignItems: 'center', gap: 8 }}>
                <span className="overline" style={{ fontSize: 10 }}>secondary</span>
                <span className="metric" style={{ fontSize: 13, color: 'var(--text-dim)', marginLeft: 'auto' }}>{c.swell.secondary.m}m · {c.swell.secondary.period}s {c.swell.secondary.dir}</span>
              </div>
            )}
          </div>
        </div>
        <ReadNote>{c.swell.note}</ReadNote>
      </CondCard>

      {/* LIGHT */}
      <CondCard label="light" grade={c.sun.grade} impact={c.sun.impact} info={c.sun.explain}>
        <DaylightArc sun={c.sun} now={c.now} />
        <div style={{ marginTop: 14 }}>
          <MetaPair items={[{ label: 'first light', value: c.sun.firstLight }, { label: 'sunset', value: c.sun.set }, { label: 'last light', value: c.sun.lastLight }]} />
        </div>
        <ReadNote>{c.sun.note}</ReadNote>
      </CondCard>

      {/* MOON */}
      <CondCard label="moon phase" grade={c.moon.grade} impact={c.moon.impact} info={c.moon.explain}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
          <MoonGlyph illum={c.moon.illum} waxing={c.moon.waxing} size={68} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 19, color: 'var(--text)', textTransform: 'capitalize', letterSpacing: '-0.01em' }}>{c.moon.phase}</div>
            <div className="metric" style={{ fontSize: 13, color: 'var(--text-dim)', marginTop: 4 }}>{c.moon.illum}% lit · day {c.moon.age}</div>
          </div>
        </div>
        <div style={{ marginTop: 16 }}>
          <MetaPair items={[{ label: 'moonrise', value: c.moon.rise }, { label: 'moonset', value: c.moon.set }]} />
        </div>
        <ReadNote>{c.moon.note}</ReadNote>
      </CondCard>

      {/* WATER + CLOUD */}
      <div style={{ display: 'flex', gap: 14 }}>
        <CondCard label="water temp" style={{ flex: 1, padding: '16px 18px' }}>
          <BigVal value={c.water} unit="°c" size={30} />
        </CondCard>
        <CondCard label="cloud" style={{ flex: 1, padding: '16px 18px' }}>
          <BigVal value={c.cloud} unit="%" size={30} />
        </CondCard>
      </div>

      <div style={{ height: 8 }} />
    </div>
  );
}

Object.assign(window, { Conditions });
