// Torpen — App produit "Au mouillage"
// Mobile-first SPA. Garde la richesse de Dir4 (carte top-down, ruban horaire,
// fenêtres prochaines, coupe bathymétrique) ET ajoute le croquis vivant.
// Style: carnet de bord — Fraunces, papier crème, dessins à la main + annotations magenta SHOM.

const { useState, useEffect, useMemo, useRef } = React;

const P = {
  paper:   '#F0E6CC',
  paperDk: '#E4D5A8',
  paperLt: '#F7F0DC',
  ink:     '#1A2330',
  inkSoft: '#3D4A5C',
  inkFaint:'rgba(26,35,48,0.45)',
  rule:    'rgba(26,35,48,0.18)',
  ruleDk:  'rgba(26,35,48,0.35)',
  sun:     '#B8782E',
  golden:  '#D88B3A',
  sea:     '#3D6E8E',
  seaDk:   '#2A5471',
  seaSurface: '#3268A0',
  sea1:    '#E5EEF6',
  sea2:    '#CFE0F0',
  sea3:    '#A5C6DF',
  shallow: '#F8F1D4',
  sand:    '#D9C58E',
  sandDk:  '#B8A06B',
  red:     '#A4332B',
  green:   '#2E7D4F',
  amber:   '#C8841C',
  magenta: '#B5276F',
};
const SERIF = '"Fraunces", "Times New Roman", serif';
const SANS  = '"Inter", system-ui, sans-serif';
const MONO  = '"JetBrains Mono", monospace';

const fmt = (d) => d ? `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}` : '—';
const fmtDate = (d) => {
  if (!d) return '';
  const dow = ['dim.','lun.','mar.','mer.','jeu.','ven.','sam.'][d.getDay()];
  const m = ['janv.','févr.','mars','avr.','mai','juin','juil.','août','sept.','oct.','nov.','déc.'];
  return `${dow} ${d.getDate()} ${m[d.getMonth()]}`;
};
const sector = (deg) => {
  const sectors = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSO','SO','OSO','O','ONO','NO','NNO'];
  return sectors[Math.round(deg / 22.5) % 16];
};

function App() {
  const [settings, setSettings] = useState(window.DEFAULT_SETTINGS);
  const data = window.useTorpenData(settings);
  const [now, setNow] = useState(new Date());
  const [dayIdx, setDayIdx] = useState(0);
  const [scrubH, setScrubH] = useState(null);
  const [tweaksOpen, setTweaksOpen] = useState(false);

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 60000);
    return () => clearInterval(id);
  }, []);

  useEffect(() => {
    const onMsg = (e) => {
      if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
      if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({type: '__edit_mode_available'}, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  if (!data.days) return <Loading />;

  const days = data.days;
  const series = data.series;
  const day = days[dayIdx];
  let activeHour;
  if (scrubH != null) activeHour = day.hours.find(h => h.date.getHours() === scrubH) || day.hours[12];
  else if (dayIdx === 0) activeHour = day.hours.find(h => h.date.getHours() === now.getHours()) || day.hours[12];
  else activeHour = day.window?.start || day.hours[12];

  const verdict = activeHour.score?.verdict || 'nogo';
  const stats = window.windowStatsLive(day, day.hours);

  return (
    <div style={{ minHeight: '100vh', background: P.paper, color: P.ink, fontFamily: SANS, paddingBottom: 60 }}>
      <Masthead now={now} status={data.status} />
      <DayTabs days={days} idx={dayIdx} onSelect={(i) => { setDayIdx(i); setScrubH(null); }} />

      <Section subtitle="Croquis" title="Le Torpen au mouillage" magenta>
        <window.CroquisTorpen day={day} time={activeHour.date} palette={P} width={720} height={760} />
        <ScrubBar day={day} activeHour={activeHour} onScrub={setScrubH} now={dayIdx === 0 ? now : null} />
      </Section>

      <Verdict day={day} hour={activeHour} stats={stats} verdict={verdict} />

      <BigTiles hour={activeHour} stats={stats} />

      <Almanach day={day} />

      <Section subtitle="Carte de mouillage" title="Vue en plan" magenta>
        <ChartView day={day} activeHour={activeHour} />
        <ChartLegend />
      </Section>

      <Section subtitle="Ruban horaire — 24 h" title="L'heure par l'heure" magenta>
        <ScopeLegend />
        <ChartScope day={day} activeHour={activeHour} onPick={(h) => setScrubH(h)} />
      </Section>

      <Section subtitle="Coupe de fond" title="Profil bathymétrique" magenta>
        <BathySection day={day} hour={activeHour} />
        <p style={{ fontFamily: SERIF, fontStyle: 'italic', fontSize: 14, color: P.inkSoft, lineHeight: 1.5, margin: '12px 0 0' }}>
          Sole asséchant à l'Est ; chenal à l'Ouest dans 4 m. Le <i>Torpen</i> mouille à 80 m du rivage par 1,8 m de fond aux pleines mers de vives-eaux.
        </p>
      </Section>

      <Section subtitle="Fenêtres prochaines" title="5 sorties à venir">
        <NextWindows series={series} />
      </Section>

      <WeekStrip days={days} idx={dayIdx} onSelect={(i) => { setDayIdx(i); setScrubH(null); }} />

      <Footer status={data.status} updatedAt={data.updatedAt} error={data.error} />

      {tweaksOpen && <Tweaks settings={settings} setSettings={setSettings} onClose={() => { setTweaksOpen(false); window.parent.postMessage({type:'__edit_mode_dismissed'},'*'); }} />}
    </div>
  );
}

// ===== Layout primitives =====

function Section({ subtitle, title, children, magenta }) {
  return (
    <section style={{ padding: '24px 20px', borderTop: `1px solid ${P.rule}` }}>
      <div style={{ fontFamily: MONO, fontSize: 9, letterSpacing: 2.5, color: magenta ? P.magenta : P.inkFaint, textTransform: 'uppercase', fontWeight: 600 }}>
        {subtitle}
      </div>
      <h2 style={{ fontFamily: SERIF, fontStyle: 'italic', fontSize: 28, fontWeight: 500, margin: '4px 0 16px', lineHeight: 1.1 }}>
        {title}
      </h2>
      {children}
    </section>
  );
}

function Masthead({ now, status }) {
  return (
    <header style={{ padding: '20px 20px 14px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: MONO, fontSize: 9, letterSpacing: 2.5, color: P.inkFaint, marginBottom: 12 }}>
        <span>SH·7142 · ÉD. 2026</span>
        <span>SONDES EN MÈTRES · ZH</span>
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
        <div>
          <div style={{ fontFamily: MONO, fontSize: 9, letterSpacing: 3, color: P.magenta, fontWeight: 600 }}>BUREAU NAUTIQUE — TORPEN</div>
          <div style={{ fontFamily: SERIF, fontSize: 38, fontWeight: 500, lineHeight: 1, letterSpacing: -0.5, marginTop: 6 }}>
            Anse de Zanflamme
          </div>
          <div style={{ fontFamily: SERIF, fontStyle: 'italic', fontSize: 13, color: P.inkSoft, marginTop: 4 }}>
            Mouillage du <i>Torpen</i> · Rade de Lorient
          </div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{ fontFamily: MONO, fontSize: 22, fontWeight: 600, lineHeight: 1, letterSpacing: 1 }}>{fmt(now)}</div>
          <div style={{ fontFamily: MONO, fontSize: 9, letterSpacing: 1.5, color: P.inkFaint, marginTop: 4 }}>
            {status === 'live' ? '● LIVE' : status === 'mock' ? '○ DÉMO' : '… …'}
          </div>
        </div>
      </div>
    </header>
  );
}

function DayTabs({ days, idx, onSelect }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: `repeat(${days.length}, 1fr)`, borderTop: `1px solid ${P.ruleDk}`, borderBottom: `1px solid ${P.ruleDk}`, background: P.paperDk }}>
      {days.slice(0, 7).map((d, i) => {
        const v = d.window ? d.window.mode : 'nogo';
        const col = v === 'go' ? P.green : v === 'caution' ? P.amber : P.red;
        const active = i === idx;
        return (
          <button key={i} onClick={() => onSelect(i)} style={{
            padding: '10px 6px', borderRight: i < days.length - 1 ? `1px solid ${P.rule}` : 'none',
            background: active ? P.paper : 'transparent', cursor: 'pointer', position: 'relative',
            border: 'none', borderBottom: active ? `3px solid ${col}` : `3px solid transparent`,
            textAlign: 'left',
          }}>
            <div style={{ fontFamily: MONO, fontSize: 8, letterSpacing: 1.5, color: P.inkSoft }}>
              {d.hours[0].date.getDate()}/{d.hours[0].date.getMonth() + 1}
            </div>
            <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginTop: 2 }}>
              <span style={{ fontFamily: SERIF, fontSize: 16, fontWeight: 500 }}>{d.label.toLowerCase()}</span>
              <span style={{ fontFamily: MONO, fontSize: 9, color: col, fontWeight: 700 }}>{d.window ? `${d.window.hours}h` : '—'}</span>
            </div>
          </button>
        );
      })}
    </div>
  );
}

// ===== Hero =====

function ScrubBar({ day, activeHour, onScrub, now }) {
  return (
    <div style={{ marginTop: 8, padding: '0 4px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
        <span style={{ fontFamily: SERIF, fontStyle: 'italic', fontSize: 12, color: P.inkSoft }}>défiler la journée</span>
        <span style={{ fontFamily: MONO, fontSize: 11, color: P.ink, letterSpacing: 1, fontWeight: 600 }}>{fmt(activeHour.date)}</span>
      </div>
      <input type="range" min={0} max={23} value={activeHour.date.getHours()} onChange={e => onScrub(parseInt(e.target.value))} style={{ width: '100%', accentColor: P.ink }} />
      <div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: MONO, fontSize: 8, color: P.inkFaint, letterSpacing: 1.5 }}>
        <span>00</span><span>06</span><span>12</span><span>18</span><span>24</span>
      </div>
    </div>
  );
}

function Verdict({ day, hour, stats, verdict }) {
  const col = verdict === 'go' ? P.green : verdict === 'caution' ? P.amber : P.red;
  const label = verdict === 'go' ? 'NAVIGABLE' : verdict === 'caution' ? 'AVEC PRUDENCE' : 'NON NAVIGABLE';
  const win = day.window;
  const summary = verdict === 'go' && win
    ? <>Fenêtre de <b style={{ color: P.ink }}>{win.hours} heures</b> de {fmt(win.start.date)} à {fmt(win.end.date)}. Vent {sector(hour.windDir)} {Math.round(stats.windAvg)} nds, houle {stats.swellMax.toFixed(2)} m, sous-quille mini {stats.depthMin.toFixed(2)} m.</>
    : verdict === 'caution'
    ? <>Conditions limites : vent {Math.round(stats.windAvg)} nds avec rafales jusqu'à {Math.round(stats.gustMax)}, houle {stats.swellMax.toFixed(2)} m. La marge sous la quille tombe à {stats.depthMin.toFixed(2)} m.</>
    : <>Vent {Math.round(stats.windMax)} nds et houle {stats.swellMax.toFixed(2)} m rendent la sortie déconseillée. Patientez : la mer revient toujours.</>;
  return (
    <section style={{ padding: '24px 20px', borderTop: `1px solid ${P.rule}` }}>
      <div style={{ fontFamily: MONO, fontSize: 9, letterSpacing: 2.5, color: P.magenta, fontWeight: 600 }}>VERDICT · {fmtDate(hour.date).toUpperCase()}</div>
      <div style={{ fontFamily: SERIF, fontStyle: 'italic', fontSize: 44, fontWeight: 500, lineHeight: 0.95, letterSpacing: -1, color: col, marginTop: 6 }}>
        {label}
      </div>
      <p style={{ fontFamily: SERIF, fontSize: 15, lineHeight: 1.55, color: P.inkSoft, margin: '14px 0 0' }}>{summary}</p>
    </section>
  );
}

function BigTiles({ hour, stats }) {
  const tiles = [
    { k: 'Vent moyen', v: Math.round(stats.windAvg), u: 'nds', sub: `raf. ${Math.round(stats.gustMax)}`, st: stats.windAvg <= 12 ? 'go' : stats.windAvg <= 18 ? 'caution' : 'nogo' },
    { k: 'Houle', v: stats.swellMax.toFixed(2), u: 'm', sub: `pér. ${Math.round(stats.wavePeriod)} s`, st: stats.swellMax <= 0.8 ? 'go' : stats.swellMax <= 1.5 ? 'caution' : 'nogo' },
    { k: 'Sous quille', v: stats.depthMin.toFixed(2), u: 'm', sub: `coef ${hour.tideCoef ?? '?'}`, st: stats.depthMin >= 0.5 ? 'go' : stats.depthMin >= 0.2 ? 'caution' : 'nogo' },
  ];
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 0, borderTop: `1px solid ${P.rule}`, borderBottom: `1px solid ${P.rule}` }}>
      {tiles.map((t, i) => {
        const col = t.st === 'go' ? P.green : t.st === 'caution' ? P.amber : P.red;
        return (
          <div key={i} style={{ padding: 14, borderRight: i < 2 ? `1px solid ${P.rule}` : 'none', position: 'relative', background: 'rgba(255,255,255,0.3)' }}>
            <div style={{ position: 'absolute', top: 0, left: 0, width: 3, height: 18, background: col }} />
            <div style={{ fontFamily: MONO, fontSize: 8, letterSpacing: 1.5, color: P.inkSoft, textTransform: 'uppercase' }}>{t.k}</div>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 3, marginTop: 4 }}>
              <span style={{ fontFamily: SERIF, fontSize: 28, fontWeight: 500, lineHeight: 1 }}>{t.v}</span>
              <span style={{ fontFamily: SERIF, fontStyle: 'italic', fontSize: 13, color: P.inkSoft }}>{t.u}</span>
            </div>
            <div style={{ fontFamily: MONO, fontSize: 9, color: P.inkSoft, marginTop: 4 }}>{t.sub}</div>
          </div>
        );
      })}
    </div>
  );
}

function Almanach({ day }) {
  const tides = day.hours.map(h => h.tide).filter(t => t != null);
  const max = Math.max(...tides), min = Math.min(...tides);
  const maxH = day.hours.find(h => h.tide === max);
  const minH = day.hours.find(h => h.tide === min);
  const items = [
    ['Lever', day.sunrise ? fmt(day.sunrise) : '—', '☀'],
    ['Coucher', day.sunset ? fmt(day.sunset) : '—', '☾'],
    ['Pleine mer', maxH ? fmt(maxH.date) : '—', '▲'],
    ['Basse mer', minH ? fmt(minH.date) : '—', '▼'],
  ];
  return (
    <section style={{ padding: '20px', borderTop: `1px solid ${P.rule}`, background: P.paperDk }}>
      <div style={{ fontFamily: MONO, fontSize: 9, letterSpacing: 2.5, color: P.magenta, fontWeight: 600 }}>ALMANACH</div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, marginTop: 10 }}>
        {items.map(([k, v, icon]) => (
          <div key={k}>
            <div style={{ fontFamily: MONO, fontSize: 8, letterSpacing: 1.2, color: P.inkSoft }}>{icon} {k.toUpperCase()}</div>
            <div style={{ fontFamily: SERIF, fontSize: 18, fontWeight: 500, marginTop: 2 }}>{v}</div>
          </div>
        ))}
      </div>
    </section>
  );
}

// ===== Top-down chart =====

function ChartView({ day, activeHour }) {
  const cur = activeHour;
  return (
    <svg viewBox="0 0 600 540" style={{ width: '100%', height: 'auto', display: 'block', background: P.shallow, border: `0.5px solid ${P.ink}` }}>
      <defs>
        <pattern id="dryShoal2" width="6" height="6" patternUnits="userSpaceOnUse">
          <circle cx="2" cy="2" r="0.5" fill={P.ink} opacity="0.4" />
          <circle cx="5" cy="4.5" r="0.4" fill={P.ink} opacity="0.3" />
        </pattern>
      </defs>
      {/* bathymetry zones */}
      <path d="M 0 0 L 180 0 Q 220 120 200 240 Q 180 360 220 480 Q 240 540 0 540 Z" fill={P.sea3} />
      <path d="M 0 0 L 260 0 Q 300 140 280 270 Q 260 400 300 520 Q 320 540 0 540 Z" fill={P.sea2} />
      <path d="M 0 0 L 340 0 Q 380 160 360 300 Q 340 430 390 540 L 0 540 Z" fill={P.sea1} />
      <path d="M 220 0 Q 320 80 380 180 Q 440 280 470 380 Q 480 460 460 540 L 600 540 L 600 0 Z" fill={P.paperDk} />
      <path d="M 380 100 Q 440 180 460 270 Q 470 360 460 440 Q 455 500 460 540 L 600 540 L 600 0 L 440 0 Q 410 50 380 100 Z" fill="url(#dryShoal2)" opacity="0.6" />
      {/* isobaths magenta */}
      <path d="M 0 0 L 180 0 Q 220 120 200 240 Q 180 360 220 480 Q 240 540 200 540" fill="none" stroke={P.magenta} strokeWidth="0.6" opacity="0.6" />
      <path d="M 0 0 L 260 0 Q 300 140 280 270 Q 260 400 300 520 Q 320 540 280 540" fill="none" stroke={P.magenta} strokeWidth="0.6" opacity="0.6" />
      <path d="M 0 0 L 340 0 Q 380 160 360 300 Q 340 430 390 540" fill="none" stroke={P.magenta} strokeWidth="0.6" opacity="0.6" />
      <text x="100" y="270" fontFamily={MONO} fontSize="10" fill={P.magenta} fontStyle="italic">10</text>
      <text x="240" y="270" fontFamily={MONO} fontSize="10" fill={P.magenta} fontStyle="italic">5</text>
      <text x="320" y="270" fontFamily={MONO} fontSize="10" fill={P.magenta} fontStyle="italic">2</text>
      {/* coastline */}
      <path d="M 0 0 L 220 0 Q 320 80 380 180 Q 440 280 470 380 Q 480 460 460 540 L 600 540 L 600 0 Z" fill="none" stroke={P.ink} strokeWidth="1.5" />
      {/* soundings */}
      <g fontFamily={MONO} fontSize="9" fill={P.inkSoft}>
        <text x="60" y="120">14</text><text x="80" y="240">12</text><text x="100" y="380">11</text>
        <text x="160" y="200">8</text><text x="180" y="340">7</text><text x="200" y="450">8</text>
        <text x="240" y="160">5</text><text x="240" y="320">4</text><text x="270" y="440">5</text>
        <text x="300" y="200">3</text><text x="310" y="350">2</text><text x="340" y="160">1</text>
        <text x="360" y="280">0,8</text><text x="380" y="400" fill={P.magenta}>0,2</text>
      </g>
      {/* rocks */}
      <g fontFamily={MONO} fontSize="11" fill={P.magenta}>
        <text x="290" y="140">+</text><text x="310" y="380">+</text><text x="345" y="450">+</text>
      </g>
      {/* place names */}
      <text x="500" y="80" fontFamily={SERIF} fontSize="13" fontStyle="italic" fill={P.ink}>Pointe de</text>
      <text x="500" y="96" fontFamily={SERIF} fontSize="13" fontStyle="italic" fill={P.ink}>l'Espérance</text>
      <text x="100" y="50" fontFamily={SERIF} fontSize="12" fontStyle="italic" fill={P.sea3}>Rade de Lorient</text>
      <text x="430" y="500" fontFamily={SERIF} fontSize="12" fontStyle="italic" fill={P.ink}>Anse de Zanflamme</text>
      {/* mouillage */}
      <g transform="translate(280, 280)">
        <circle r="14" fill="none" stroke={P.magenta} strokeWidth="1.5" strokeDasharray="2 2" />
        <circle r="3" fill={P.magenta} />
        <text y="-20" textAnchor="middle" fontFamily={MONO} fontSize="9" fill={P.magenta} fontWeight="700" letterSpacing="1.5">MOUILLAGE</text>
        <text y="32" textAnchor="middle" fontFamily={SERIF} fontSize="13" fill={P.magenta} fontStyle="italic">Torpen</text>
      </g>
      {/* approach route */}
      <path d="M 60 540 Q 100 480 140 420 Q 180 360 220 320 Q 250 295 280 280" fill="none" stroke={P.magenta} strokeWidth="1.5" strokeDasharray="6 4" />
      <text x="160" y="500" fontFamily={MONO} fontSize="9" fill={P.magenta} letterSpacing="1.5">CHENAL D'APPROCHE</text>
      {/* compass rose with current wind */}
      <g transform="translate(490, 200)">
        <circle r="50" fill="rgba(255,255,255,0.4)" stroke={P.ink} strokeWidth="0.8" />
        <circle r="34" fill="none" stroke={P.ink} strokeWidth="0.4" />
        {['N','NE','E','SE','S','SO','O','NO'].map((d, i) => {
          const a = (i * 45 - 90) * Math.PI / 180;
          const x = 56 * Math.cos(a), y = 56 * Math.sin(a);
          return <text key={d} x={x} y={y + 4} textAnchor="middle" fontFamily={MONO} fontSize="9" fill={P.ink} fontWeight={d === 'N' ? 700 : 400}>{d}</text>;
        })}
        <polygon points="0,-30 -5,0 0,6 5,0" fill={P.magenta} />
        <polygon points="0,30 -5,0 0,-6 5,0" fill={P.paper} stroke={P.ink} strokeWidth="0.4" />
        {(() => {
          const wd = (cur.windDir - 90) * Math.PI / 180;
          return (
            <g>
              <line x1={-15 * Math.cos(wd)} y1={-15 * Math.sin(wd)} x2={26 * Math.cos(wd)} y2={26 * Math.sin(wd)} stroke={P.magenta} strokeWidth="2" />
              <text x={28 * Math.cos(wd)} y={28 * Math.sin(wd) + 4} textAnchor="middle" fontFamily={MONO} fontSize="9" fill={P.magenta} fontWeight="700">{Math.round(cur.wind)} nds</text>
            </g>
          );
        })()}
      </g>
      {/* coords */}
      <g fontFamily={MONO} fontSize="9" fill={P.inkFaint}>
        <text x="6" y="14">47°43'30"N</text>
        <text x="6" y="535">47°43'00"N</text>
        <text x="594" y="14" textAnchor="end">3°22'00"O</text>
      </g>
    </svg>
  );
}

function ChartLegend() {
  const items = [[P.shallow, '0 – 2 m · sole'], [P.sea1, '2 – 5 m'], [P.sea2, '5 – 10 m'], [P.sea3, '> 10 m']];
  return (
    <div style={{ marginTop: 12, padding: 10, border: `0.5px solid ${P.ruleDk}`, background: 'rgba(255,255,255,0.3)', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px 12px', fontFamily: MONO, fontSize: 10 }}>
      {items.map(([s, l], i) => (
        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <div style={{ width: 18, height: 10, background: s, border: `0.5px solid ${P.ink}` }} />
          <span style={{ color: P.inkSoft }}>{l}</span>
        </div>
      ))}
    </div>
  );
}

// ===== Hourly scope =====

function ScopeLegend() {
  const items = [[P.ink, 'VENT'], [P.magenta, 'HOULE'], [P.sea3, 'MARÉE'], [P.green, 'VERDICT']];
  return (
    <div style={{ display: 'flex', gap: 14, fontFamily: MONO, fontSize: 9, color: P.inkSoft, letterSpacing: 1.5, marginBottom: 8, flexWrap: 'wrap' }}>
      {items.map(([c, l], i) => (
        <span key={i} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
          <span style={{ width: 8, height: 8, background: c, display: 'inline-block' }} />{l}
        </span>
      ))}
    </div>
  );
}

function ChartScope({ day, activeHour, onPick }) {
  const W = 600, H = 240, padL = 36, padR = 12, padT = 16, padB = 50;
  const innerW = W - padL - padR, innerH = H - padT - padB;
  const hourW = innerW / 24;
  const tides = day.hours.map(h => h.tide).filter(t => t != null);
  const wMax = 30, sMax = 3, tMin = Math.min(...tides), tMax = Math.max(...tides);
  const xAt = i => padL + i * hourW + hourW / 2;
  const yWind = v => padT + innerH - (v / wMax) * innerH * 0.7;
  const ySwell = v => padT + innerH - (v / sMax) * innerH * 0.5;
  const yTide = v => padT + innerH - ((v - tMin) / Math.max(0.1, tMax - tMin)) * innerH * 0.45 - 6;
  const activeX = xAt(activeHour.date.getHours());

  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}
      onClick={(e) => {
        const rect = e.currentTarget.getBoundingClientRect();
        const x = (e.clientX - rect.left) * W / rect.width;
        const h = Math.max(0, Math.min(23, Math.round((x - padL) / hourW)));
        onPick(h);
      }}>
      {/* day band */}
      {day.sunrise && day.sunset && (() => {
        const sr = day.sunrise.getHours() + day.sunrise.getMinutes()/60;
        const ss = day.sunset.getHours() + day.sunset.getMinutes()/60;
        return <rect x={padL + sr * hourW} y={padT} width={(ss - sr) * hourW} height={innerH} fill="#FFD66B" opacity="0.10" />;
      })()}
      {/* gridlines */}
      {[0, 0.25, 0.5, 0.75, 1].map(p => (
        <line key={p} x1={padL} x2={W - padR} y1={padT + innerH * p} y2={padT + innerH * p} stroke={P.rule} strokeWidth="0.4" strokeDasharray="2 4" />
      ))}
      {/* tide filled */}
      <path d={`M ${xAt(0)} ${padT + innerH} ${day.hours.map((h, i) => h.tide != null ? `L ${xAt(i)} ${yTide(h.tide)}` : '').join(' ')} L ${xAt(23)} ${padT + innerH} Z`} fill={P.sea3} opacity="0.30" />
      <path d={day.hours.map((h, i) => h.tide != null ? `${i===0?'M':'L'} ${xAt(i)} ${yTide(h.tide)}` : '').join(' ')} fill="none" stroke={P.sea3} strokeWidth="1.5" />
      {/* swell */}
      <path d={day.hours.map((h, i) => `${i===0?'M':'L'} ${xAt(i)} ${ySwell(h.wave || 0)}`).join(' ')} fill="none" stroke={P.magenta} strokeWidth="1.5" />
      {/* wind + gust */}
      <path d={day.hours.map((h, i) => `${i===0?'M':'L'} ${xAt(i)} ${yWind(h.wind || 0)}`).join(' ')} fill="none" stroke={P.ink} strokeWidth="2" />
      <path d={day.hours.map((h, i) => `${i===0?'M':'L'} ${xAt(i)} ${yWind(h.gust || 0)}`).join(' ')} fill="none" stroke={P.ink} strokeWidth="0.8" strokeDasharray="3 3" opacity="0.6" />
      {/* wind dir arrows */}
      {day.hours.filter((_, i) => i % 3 === 0).map((h) => {
        const i = day.hours.indexOf(h);
        const x = xAt(i), y = yWind(h.wind || 0) - 12;
        const a = (h.windDir - 90) * Math.PI / 180;
        const len = 6;
        return (
          <g key={i} transform={`translate(${x}, ${y})`}>
            <line x1={-len * Math.cos(a)} y1={-len * Math.sin(a)} x2={len * Math.cos(a)} y2={len * Math.sin(a)} stroke={P.ink} strokeWidth="0.8" />
            <polygon points={`${len * Math.cos(a)},${len * Math.sin(a)} ${(len-3)*Math.cos(a-0.4)},${(len-3)*Math.sin(a-0.4)} ${(len-3)*Math.cos(a+0.4)},${(len-3)*Math.sin(a+0.4)}`} fill={P.ink} />
          </g>
        );
      })}
      {/* sunrise/sunset */}
      {day.sunrise && day.sunset && [['☀', day.sunrise], ['☾', day.sunset]].map(([icon, d], idx) => {
        const h = d.getHours() + d.getMinutes() / 60;
        const x = padL + h * hourW;
        return (
          <g key={idx}>
            <line x1={x} x2={x} y1={padT} y2={padT + innerH} stroke={P.amber} strokeWidth="0.8" strokeDasharray="3 3" />
            <text x={x} y={padT - 4} textAnchor="middle" fontFamily={SERIF} fontStyle="italic" fontSize="11" fill={P.amber}>{icon}{fmt(d)}</text>
          </g>
        );
      })}
      {/* verdict ribbon */}
      {day.hours.map((h, i) => {
        const v = h.score?.verdict;
        const c = !h.navigable ? P.inkFaint : v === 'go' ? P.green : v === 'caution' ? P.amber : P.red;
        return <rect key={i} x={padL + i * hourW + 0.5} y={H - padB + 6} width={hourW - 1} height="12" fill={c} opacity={!h.navigable ? 0.4 : 0.95} />;
      })}
      {/* active line */}
      <line x1={activeX} x2={activeX} y1={padT} y2={H - padB + 18} stroke={P.ink} strokeWidth="1.2" />
      <circle cx={activeX} cy={padT - 2} r="3" fill={P.ink} />
      {/* axes */}
      <text x={padL - 6} y={padT + 4} textAnchor="end" fontFamily={MONO} fontSize="9" fill={P.inkSoft}>30 nds</text>
      <text x={padL - 6} y={padT + innerH * 0.7 + 2} textAnchor="end" fontFamily={MONO} fontSize="9" fill={P.inkSoft}>0</text>
      {[0, 4, 8, 12, 16, 20].map(h => (
        <text key={h} x={padL + h * hourW} y={H - padB + 32} textAnchor="middle" fontFamily={MONO} fontSize="9" fill={P.inkSoft}>{String(h).padStart(2,'0')}h</text>
      ))}
    </svg>
  );
}

// ===== Bathy section =====

function BathySection({ day, hour }) {
  const W = 600, H = 220;
  const cur = hour;
  const tides = day.hours.map(h => h.tide).filter(t => t != null);
  const tMax = Math.max(...tides), tMin = Math.min(...tides);
  const datumY = H - 36;
  const waterY = datumY - ((cur.tide || 2) / 4) * 70;
  const pmY = datumY - (tMax / 4) * 70;
  const bmY = datumY - (tMin / 4) * 70;
  const sbY = (x) => (datumY + 36) + (datumY - 30 - (datumY + 36)) * (x / W);

  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block', background: 'rgba(255,255,255,0.4)', border: `0.5px solid ${P.ink}` }}>
      <rect x="0" y="0" width={W} height={pmY} fill={P.paperLt} />
      <rect x="0" y={pmY} width={W} height={bmY - pmY} fill={P.sea1} opacity="0.6" />
      <rect x="0" y={waterY} width={W} height={H - waterY} fill={P.sea3} opacity="0.35" />
      <line x1="0" x2={W} y1={pmY} y2={pmY} stroke={P.magenta} strokeWidth="0.8" strokeDasharray="3 3" />
      <text x="6" y={pmY - 4} fontFamily={MONO} fontSize="9" fill={P.magenta}>PM {tMax.toFixed(2)} m</text>
      <line x1="0" x2={W} y1={bmY} y2={bmY} stroke={P.magenta} strokeWidth="0.8" strokeDasharray="3 3" />
      <text x="6" y={bmY - 4} fontFamily={MONO} fontSize="9" fill={P.magenta}>BM {tMin.toFixed(2)} m</text>
      <line x1="0" x2={W} y1={waterY} y2={waterY} stroke={P.sea3} strokeWidth="1.5" />
      <text x={W - 6} y={waterY - 5} textAnchor="end" fontFamily={SERIF} fontStyle="italic" fontSize="13" fill={P.sea3}>marée {(cur.tide ?? 0).toFixed(2)} m</text>
      <path d={`M 0 ${datumY + 36} L ${W} ${datumY - 30} L ${W} ${H} L 0 ${H} Z`} fill={P.paperDk} stroke={P.ink} strokeWidth="1.2" />
      {/* boat */}
      <g transform={`translate(${W * 0.55}, ${waterY - 10})`}>
        <path d="M -28 0 L 28 0 L 24 8 L -24 8 Z" fill={P.paper} stroke={P.ink} strokeWidth="1.2" />
        <rect x="-16" y="-6" width="32" height="6" fill={P.paper} stroke={P.ink} strokeWidth="1" />
        <line x1="0" y1="-6" x2="0" y2="-58" stroke={P.ink} strokeWidth="1.5" />
        <circle cx="0" cy="-58" r="2" fill={P.magenta} />
      </g>
      {/* under-keel measure */}
      {(() => {
        const x = W * 0.55 + 32;
        const top = waterY + 4;
        const sy = sbY(x);
        return (
          <g>
            <line x1={x} x2={x} y1={top} y2={sy} stroke={P.green} strokeWidth="1.2" />
            <line x1={x - 4} x2={x + 4} y1={top} y2={top} stroke={P.green} strokeWidth="1.2" />
            <line x1={x - 4} x2={x + 4} y1={sy} y2={sy} stroke={P.green} strokeWidth="1.2" />
            <text x={x + 8} y={(top + sy) / 2 + 4} fontFamily={SERIF} fontStyle="italic" fontSize="13" fill={P.green} fontWeight="600">{(cur.depth ?? 0).toFixed(2)} m</text>
            <text x={x + 8} y={(top + sy) / 2 + 18} fontFamily={MONO} fontSize="8" fill={P.green} letterSpacing="1">SOUS QUILLE</text>
          </g>
        );
      })()}
      <text x="14" y="20" fontFamily={MONO} fontSize="9" fill={P.inkSoft} letterSpacing="1.5">OUEST · CHENAL</text>
      <text x={W - 14} y="20" textAnchor="end" fontFamily={MONO} fontSize="9" fill={P.inkSoft} letterSpacing="1.5">EST · RIVAGE</text>
    </svg>
  );
}

// ===== Next windows =====

function NextWindows({ series }) {
  const wins = window.findLiveWindows(series, window.DEFAULT_SETTINGS).slice(0, 5);
  if (wins.length === 0) return <p style={{ fontFamily: SERIF, fontStyle: 'italic', color: P.inkSoft }}>Aucune fenêtre favorable identifiée sur 7 jours.</p>;
  return (
    <div>
      {wins.map((w, i) => {
        const col = w.mode === 'go' ? P.green : P.amber;
        return (
          <div key={i} style={{ display: 'grid', gridTemplateColumns: '52px 1fr auto', gap: 12, padding: '12px 0', borderTop: i > 0 ? `0.5px solid ${P.rule}` : 'none', alignItems: 'center' }}>
            <div style={{ fontFamily: SERIF, fontSize: 30, fontWeight: 500, fontStyle: 'italic', color: col, lineHeight: 1, textAlign: 'right' }}>{w.hours}h</div>
            <div>
              <div style={{ fontFamily: SERIF, fontSize: 15, fontWeight: 500 }}>
                {w.start.date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}
              </div>
              <div style={{ fontFamily: MONO, fontSize: 10, color: P.inkSoft, marginTop: 2 }}>
                {fmt(w.start.date)} → {fmt(w.end.date)} · COEF {w.start.tideCoef} · {sector(w.start.windDir)} {Math.round(w.start.wind)} NDS
              </div>
            </div>
            <div style={{ fontFamily: MONO, fontSize: 9, color: col, fontWeight: 700, letterSpacing: 1.5, padding: '3px 7px', border: `0.5px solid ${col}` }}>
              {w.mode === 'go' ? 'GO' : 'PRUD'}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ===== Week strip =====

function WeekStrip({ days, idx, onSelect }) {
  return (
    <Section subtitle="Semaine" title="7 jours d'un coup d'œil">
      {days.slice(0, 7).map((d, i) => {
        const v = d.window ? d.window.mode : 'nogo';
        const col = v === 'go' ? P.green : v === 'caution' ? P.amber : P.red;
        const isSel = i === idx;
        const winStr = d.window ? `${fmt(d.window.start.date)}–${fmt(d.window.end.date)} · ${d.window.hours}h` : 'pas de créneau';
        return (
          <button key={i} onClick={() => onSelect(i)} style={{
            display: 'grid', gridTemplateColumns: '10px 1fr auto', gap: 12, alignItems: 'center', width: '100%',
            padding: '12px 8px', border: 'none', borderTop: i > 0 ? `1px solid ${P.rule}` : 'none',
            background: isSel ? P.paperLt : 'transparent', cursor: 'pointer', textAlign: 'left',
          }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: col }} />
            <span>
              <div style={{ fontFamily: SERIF, fontStyle: 'italic', fontSize: 15, fontWeight: 500 }}>{fmtDate(d.hours[0].date)}</div>
              <div style={{ fontFamily: MONO, fontSize: 10, color: P.inkSoft, marginTop: 2 }}>{winStr} · coef {d.tideCoef}</div>
            </span>
            <span style={{ fontFamily: MONO, fontSize: 10, color: col, letterSpacing: 1, fontWeight: 700 }}>
              {v === 'go' ? 'GO' : v === 'caution' ? '~' : '✕'}
            </span>
          </button>
        );
      })}
    </Section>
  );
}

// ===== Footer / Loading / Tweaks =====

function Footer({ status, updatedAt, error }) {
  return (
    <footer style={{ padding: '20px', borderTop: `1px solid ${P.ruleDk}`, fontFamily: MONO, fontSize: 9, letterSpacing: 1.5, color: P.inkFaint, lineHeight: 1.6 }}>
      <div>{window.TORPEN_LOC.name.toUpperCase()} · {window.TORPEN_LOC.lat.toFixed(4)}°N · {Math.abs(window.TORPEN_LOC.lon).toFixed(4)}°O</div>
      <div style={{ marginTop: 4 }}>SOURCES — OPEN-METEO · MARINE · SHOM (CARTO)</div>
      <div style={{ marginTop: 4 }}>{status === 'live' ? 'DONNÉES LIVE' : 'MODE DÉMO'}{updatedAt ? ` · MAJ ${fmt(updatedAt)}` : ''}{error ? ` · ${error}` : ''}</div>
      <div style={{ marginTop: 10, fontFamily: SERIF, fontStyle: 'italic', fontSize: 12, color: P.inkSoft }}>« vers le large, avec soin »</div>
    </footer>
  );
}

function Loading() {
  return <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: SERIF, fontStyle: 'italic', fontSize: 22, color: P.inkSoft }}>relevé en cours…</div>;
}

function Tweaks({ settings, setSettings, onClose }) {
  const set = (path, v) => {
    const next = JSON.parse(JSON.stringify(settings));
    const keys = path.split('.');
    let o = next;
    for (let i = 0; i < keys.length - 1; i++) o = o[keys[i]];
    o[keys[keys.length - 1]] = v;
    setSettings(next);
  };
  return (
    <div style={{ position: 'fixed', right: 12, bottom: 12, width: 280, background: P.paperLt, border: `1px solid ${P.ink}`, padding: 14, fontFamily: SERIF, zIndex: 1000, boxShadow: '0 8px 24px rgba(0,0,0,0.15)' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
        <div style={{ fontStyle: 'italic', fontSize: 16, fontWeight: 600 }}>Tweaks</div>
        <button onClick={onClose} style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: MONO }}>✕</button>
      </div>
      {[
        ['Tirant d\'eau', 'draft', 0.4, 1.5, 0.05, 'm'],
        ['Sole (ZH)', 'chartDepth', -2, 1, 0.1, 'm'],
        ['Vent · go', 'thresholds.windGo', 5, 25, 1, 'nds'],
        ['Vent · caution', 'thresholds.windCaution', 10, 35, 1, 'nds'],
        ['Sous-quille mini', 'thresholds.depthGo', 0.1, 1.5, 0.05, 'm'],
      ].map(([lbl, path, mn, mx, st, u]) => {
        const keys = path.split('.');
        let v = settings;
        for (const k of keys) v = v[k];
        return (
          <div key={path} style={{ marginBottom: 8 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, fontStyle: 'italic' }}>
              <span>{lbl}</span><span style={{ fontFamily: MONO, fontStyle: 'normal' }}>{typeof v === 'number' ? v.toFixed(2) : v} {u}</span>
            </div>
            <input type="range" min={mn} max={mx} step={st} value={v} onChange={e => set(path, parseFloat(e.target.value))} style={{ width: '100%', accentColor: P.ink }} />
          </div>
        );
      })}
    </div>
  );
}

window.TorpenApp = App;
