// Tempo — Tableau de bord : indicateurs clés, P&L cumulé, Calendrier P&L.
// Expose aussi des briques partagées (FilterBar, ChartCard, CumulativeChartReal,
// CalendarBlock, DashKpiRow, DashboardEmpty, dashCumulative,
// dashCumulativeSubtitle, usePgWidth) que la version mobile (app.jsx) et
// d'autres pages réutilisent. KpiCard et LineArea restent internes à ce fichier.

// ─── Utilitaires locaux ───────────────────────────────────────────
// Titre de page lu dans la table unique NAV_LABELS (core.jsx), avec repli.
function pgNavTitle(id, fallback) {
  const t = (typeof NAV_LABELS !== 'undefined' && NAV_LABELS) ? NAV_LABELS[id] : null;
  if (typeof t === 'string') return t;
  return (t && (t.titre || t.title || t.menu)) || fallback;
}

// Jour civil 'YYYY-MM-DD' + n jours (calcul à midi UTC : aucun fuseau en jeu).
function pgAddDaysISO(iso, n) {
  const d = new Date(iso + 'T12:00:00Z');
  d.setUTCDate(d.getUTCDate() + n);
  return d.toISOString().slice(0, 10);
}

// « 0 trade », « 1 trade », « 1 246 trades » : nombre au format français
// (charte §1, séparateur de milliers) et accord du mot : formateur partagé
// fmtCount (format-prefs.jsx), espace ordinaire entre le nombre et le mot.
function pgTrades(n) {
  return fmtCount(n, 'trade');
}

// Majuscule initiale (début de ligne ou d'info-bulle).
function pgCap(s) {
  return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}

// Taille de police qui fait tenir un montant sur une ligne dans la largeur de
// son conteneur (container-type: inline-size) : jamais tronqué, jamais coupé.
function pgFitFont(text, maxPx) {
  const len = Math.max(1, String(text == null ? '' : text).length);
  return `min(${maxPx}px, ${Math.round(10000 / (len * 0.64)) / 100}cqw)`;
}

// Largeur rendue d'un élément, suivie par ResizeObserver (mesure avant l'affichage).
function usePgWidth() {
  const ref = React.useRef(null);
  const [width, setWidth] = React.useState(0);
  React.useLayoutEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    const read = () => setWidth(Math.round(el.getBoundingClientRect().width));
    read();
    if (typeof ResizeObserver !== 'function') return undefined;
    const ro = new ResizeObserver(read);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return [ref, width];
}

// « 24 G · 22 P · 1 nul » (charte §3 et §5), nombres au format français
// (« 1 246 G »).
function pgWinLossText(wins, losses, nuls) {
  const n = Math.max(0, Number(nuls) || 0);
  return `${fmtNum(Number(wins) || 0, 0)} G · ${fmtNum(Number(losses) || 0, 0)} P` + (n > 0 ? ` · ${fmtNum(n, 0)} nul${n > 1 ? 's' : ''}` : '');
}

// Signe d'un montant TEL QU'IL S'AFFICHE : 1 (gain), −1 (perte), 0 pour tout ce
// que fmtMoney écrit « 0,00 € ». Même arrondi que fmtMoneyCur (décimales de la
// devise du profil) : une somme de P&L laisse du bruit de virgule flottante
// (10,1 + 20,2 − 30,3 = −3,55e-15), qui ne doit colorer ni un texte, ni une
// case, ni une courbe. Toute couleur de montant de ce fichier passe par ici.
function pgSign(v) {
  return moneySign(v);
}

// Couleur d'un montant signé : vert, rouge, ou neutre pour zéro (charte §2).
// Gain en var(--green) : --dash-green (#16a34a) ne fait que 3,3:1 sur blanc,
// sous les 4,5:1 de la charte §9 ; il reste réservé aux fonds, barres et
// courbes. --dash-red est conforme (4,75:1).
function pgPnlColor(v) {
  const sign = pgSign(v);
  return sign > 0 ? 'var(--green)' : sign < 0 ? 'var(--dash-red)' : 'var(--fg)';
}
// Même règle en teinte « texte » (petits montants du calendrier, contraste AA).
function pgPnlTextColor(v) {
  const sign = pgSign(v);
  return sign > 0 ? 'var(--green-text)' : sign < 0 ? 'var(--red-text)' : 'var(--fg)';
}

// ─── Shared bits ──────────────────────────────────────────────────
function FilterBar({ tabs, activeTab, setActiveTab, right }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
      <div style={{ display: 'flex', gap: 8 }}>
        {tabs.map((t) =>
        <button key={t.id} onClick={() => setActiveTab(t.id)} className="tap" style={{
          padding: '7px 14px', borderRadius: 8,
          border: '1.5px solid ' + (activeTab === t.id ? 'var(--blue)' : 'var(--line)'),
          background: activeTab === t.id ? 'var(--blue-soft)' : 'var(--bg-card)',
          color: activeTab === t.id ? 'var(--blue-600)' : 'var(--fg-2)',
          fontSize: 12.5, fontWeight: activeTab === t.id ? 600 : 500, letterSpacing: '-.005em',
          cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6
        }}>
            {t.ico} {t.label}
          </button>
        )}
      </div>
      {right && <div style={{ display: 'flex', gap: 8 }}>{right}</div>}
    </div>);

}

// P&L cumulé trade après trade (ordre chronologique) et drawdown max, calculé
// comme dans Statistiques (pic initial à 0 ; drawdown = pic − P&L cumulé) pour
// afficher la même valeur au centime. `dates` = executed_at de chaque point.
function dashCumulative(trades) {
  const sorted = [...(trades || [])].sort((a, b) => new Date(a.executed_at) - new Date(b.executed_at));
  let run = 0, peak = 0, maxDD = 0;
  const points = [], dates = [];
  for (const t of sorted) {
    run += Number(t.pnl) || 0;
    points.push(run);
    dates.push(t.executed_at);
    if (run >= peak) peak = run;
    if (peak - run > maxDD) maxDD = peak - run;
  }
  return { points, dates, maxDD };
}

// Sous-titre de la courbe : « 46 trades · 29 juil. – 25 sept. ».
function dashCumulativeSubtitle(count, dates) {
  const n = pgTrades(count);
  if (!dates || dates.length < 2) return n;
  return `${n} · ${fmtDayRange(dates[0], dates[dates.length - 1])}`;
}

// ─── Dashboard ────────────────────────────────────────────────────
function Dashboard({ nav, navStats, openImport, openJournal, state }) {
  // Clic sur une carte d'indicateur → section Statistiques (vue trading). Inerte si navStats absent.
  const goStats = navStats ? () => navStats('trading') : undefined;
  const { trades, stats, tradesLoading, tradesByDay } = state || { trades: [], stats: computeStats([]), tradesLoading: true, tradesByDay: {} };
  const list = trades || [];
  const hasTrades = list.length > 0;
  // Erreur de chargement ≠ compte vide : bandeau + Réessayer, données déjà chargées conservées.
  const loadError = !tradesLoading && !!(state && state.tradesError);
  const empty = !tradesLoading && !loadError && !hasTrades;
  const cumulative = React.useMemo(() => dashCumulative(list), [list]);
  const addTrade = () => openImport && openImport(null, 'manual');
  const importFile = () => openImport && openImport(null, 'csv');

  return (
    <div style={{ width: '100%', height: '100%', overflow: 'auto', display: 'flex', flexDirection: 'column' }} className="scroll">
      <PageHeader title={pgNavTitle('dashboard', 'Tableau de bord')} />
      <div style={{ ...PAGE_BODY, padding: '20px 24px 32px', flex: 1 }}>
        {hasTrades && (
          <div style={{ display: 'flex', justifyContent: 'flex-end', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
            <button type="button" onClick={importFile} className="btn tap">{Ico.upload} Importer un fichier</button>
            <button type="button" onClick={addTrade} className="btn btn-blue tap">{Ico.plus} Ajouter un trade</button>
          </div>
        )}

        {loadError && (
          <div role="alert" className="card" style={{ padding: '12px 16px', marginBottom: 14, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', borderColor: 'var(--red)' }}>
            <span style={{ fontSize: 13, color: 'var(--fg)' }}>Impossible de charger tes trades. Vérifie ta connexion.</span>
            {state && typeof state.refreshTrades === 'function' && (
              <button type="button" onClick={() => state.refreshTrades()} className="btn tap">Réessayer</button>
            )}
          </div>
        )}

        {tradesLoading && !hasTrades ? (
          <DashKpiSkeleton />
        ) : empty ? (
          <DashboardEmpty openImport={openImport} />
        ) : hasTrades ? (
          <React.Fragment>
            {/* Indicateurs clés : P&L net, espérance, drawdown max, gain/perte moyens.
                Taux de réussite et profit factor sont portés par les jauges juste dessous. */}
            <DashKpiRow stats={stats} maxDD={cumulative.maxDD} onClick={goStats} />

            {/* Jauges type compteur : taux de réussite, profit factor, ratio gain/perte */}
            {typeof DashGaugesRow === 'function' && <DashGaugesRow stats={stats} />}

            {/* P&L cumulé */}
            {cumulative.points.length > 1 && (
              <div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', gap: 14, marginBottom: 14 }}>
                <ChartCard title="P&L cumulé" subtitle={dashCumulativeSubtitle(stats.count, cumulative.dates)}>
                  <CumulativeChartReal points={cumulative.points} dates={cumulative.dates}/>
                </ChartCard>
              </div>
            )}
          </React.Fragment>
        ) : null}

        {/* Calendrier P&L — toujours affiché, données réelles */}
        <CalendarBlock nav={nav} onDayClick={openJournal} openImport={openImport} tradesByDay={tradesByDay} today={state && state.today} showAdd={!empty}/>
      </div>
    </div>);

}

// Rangée des 4 indicateurs clés du Tableau de bord (ordinateur et mobile).
// 4 colonnes quand la place le permet, sinon grille 2×2 ; `compact` force le 2×2
// (mobile). Les montants s'ajustent à la largeur de leur carte (jamais tronqués).
function DashKpiRow({ stats, maxDD = 0, onClick, compact = false }) {
  const [ref, width] = usePgWidth();
  const cols = compact || (width > 0 && width < 4 * 230 + 3 * 14) ? 2 : 4;
  const s = stats || {};
  const count = Number(s.count) || 0;
  const wins = Number(s.wins) || 0;
  const losses = Number(s.losses) || 0;
  const decided = wins + losses > 0;
  const dd = Number(maxDD) > 0 ? -Number(maxDD) : 0;
  const big = compact ? 22 : 26;
  // Une seule taille pour les trois montants de la rangée, calculée sur le plus
  // long : le P&L net ne paraît jamais plus petit que l'espérance voisine.
  const longest = [fmtMoney(s.netPnl, true), fmtMoney(s.expectancy, true), fmtMoney(dd, true)]
    .sort((a, b) => b.length - a.length)[0];
  const moneySize = pgFitFont(longest, big);
  const money = (v) => count === 0 ? <span>—</span>
    : <AnimatedMoney value={v} sign size={moneySize} weight={700} color={pgPnlColor(v)} />;
  const avgWin = wins ? fmtMoney(s.avgWin, true) : '—';
  const avgLoss = losses ? fmtMoney(-s.avgLoss, true) : '—';
  const small = compact ? 16 : 18;
  return (
    <div ref={ref} className="stagger" style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, gap: compact ? 10 : 14, marginBottom: 14 }}>
      <KpiCard onClick={onClick} label="P&L net" main={money(s.netPnl)} sub={pgTrades(count)} />
      <KpiCard onClick={onClick} label="Espérance par trade" main={money(s.expectancy)} sub={count ? `sur ${pgTrades(count)}` : '—'} />
      <KpiCard onClick={onClick} label="Drawdown max" main={money(dd)} sub="plus forte baisse depuis un pic" />
      <KpiCard onClick={onClick} label="Gain moyen / Perte moyenne"
        main={
          <span style={{ display: 'inline-flex', flexWrap: 'wrap', alignItems: 'baseline', columnGap: 6, rowGap: 4 }}>
            <span className="num" style={{ fontSize: pgFitFont(avgWin, small), color: wins ? pgPnlColor(s.avgWin) : 'var(--fg-3)', whiteSpace: 'nowrap' }}>{avgWin}</span>
            <span style={{ fontSize: small * 0.8, color: 'var(--fg-3)', fontWeight: 500 }}>/</span>
            <span className="num" style={{ fontSize: pgFitFont(avgLoss, small), color: losses ? pgPnlColor(-s.avgLoss) : 'var(--fg-3)', whiteSpace: 'nowrap' }}>{avgLoss}</span>
          </span>
        }
        sub={decided ? pgWinLossText(wins, losses, count - wins - losses) : '—'} />
    </div>
  );
}

// Squelette pendant le premier chargement (uniquement pendant le chargement).
function DashKpiSkeleton() {
  return (
    <div role="status" aria-busy="true" aria-label="Chargement des données…" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 14, marginBottom: 14 }}>
      {[0, 1, 2, 3].map((i) => (
        <div key={i} className="card" style={{ padding: '20px 22px', minHeight: 118 }}>
          <div style={{ width: '46%', height: 10, borderRadius: 5, background: 'var(--bg-elev)' }}></div>
          <div style={{ width: '68%', height: 24, borderRadius: 6, background: 'var(--bg-elev)', marginTop: 24 }}></div>
          <div style={{ width: '36%', height: 9, borderRadius: 5, background: 'var(--bg-elev)', marginTop: 12 }}></div>
        </div>
      ))}
    </div>
  );
}

// État vide du Tableau de bord (ordinateur et mobile) : l'état vide commun
// (EmptyTrades, core.jsx) dans une carte.
function DashboardEmpty({ openImport, compact = false }) {
  return (
    <div className="card" style={{ marginBottom: 14 }}>
      <EmptyTrades openImport={openImport} compact={compact}/>
    </div>
  );
}

// Courbe du P&L cumulé à partir des points réels.
// Le viewBox prend la largeur réelle de la carte (mesurée), pour une courbe
// pleine largeur sans bandes vides ni texte déformé ; `W` n'est que le repli du
// tout premier rendu, avant la mesure. Graduations en format compact ;
// valeur finale exacte (fmtMoney, la même qu'au P&L net) dans l'étiquette posée
// à hauteur du dernier point.
// `ddRange` (optionnel, Statistiques) : { peak, trough } indices de la plus
// forte chute pic → creux, surlignée en rouge pâle. Même courbe, mêmes axes et
// même valeur finale dans le Tableau de bord et dans les Statistiques.
function CumulativeChartReal({ points, dates, W = 720, H = 200, ddRange = null }) {
  const [boxRef, boxW] = usePgWidth();
  if (!points || points.length < 2) return null;
  const width = boxW > 0 ? boxW : W;
  const max = Math.max(...points);
  const min = Math.min(...points);
  const last = points[points.length - 1];
  // Teinte sur le signe de la valeur finale telle qu'elle s'affiche dans l'étiquette.
  const sign = pgSign(last);
  const stroke = sign > 0 ? 'var(--dash-green)' : sign < 0 ? 'var(--dash-red)' : 'var(--fg-3)';
  const fill = sign > 0 ? 'rgba(22,163,74,0.14)' : sign < 0 ? 'rgba(225,29,47,0.12)' : 'rgba(100,116,139,0.12)';
  // Résumé lu à la place de l'image (role=img) : nombre de trades, période,
  // valeur finale, plus bas et plus haut — ce que la courbe montre.
  const hasDates = Array.isArray(dates) && dates.length === points.length;
  const summary = 'P&L cumulé\u00A0: ' + pgTrades(points.length)
    + (hasDates ? ', du ' + fmtDayShort(dates[0]) + ' au ' + fmtDayShort(dates[dates.length - 1]) : '')
    + '. Valeur finale ' + fmtMoney(last, true) + ', plus bas ' + fmtMoney(min, true) + ', plus haut ' + fmtMoney(max, true) + '.';
  return (
    <div ref={boxRef} style={{ width: '100%', minWidth: 0 }}>
      <LineArea data={points} dates={dates} W={width} H={H} stroke={stroke} fill={fill} ariaLabel={summary}
        labelMax={fmtMoneyCompact(max, true)} labelMin={fmtMoneyCompact(min, true)} labelZero={fmtMoneyCompact(0)}
        labelLast={fmtMoney(last, true)} valueFmt={(v) => fmtMoney(v, true)} hover
        band={ddRange && ddRange.trough > ddRange.peak ? { from: ddRange.peak, to: ddRange.trough } : null} />
    </div>
  );
}

// Carte d'un indicateur clé : libellé, valeur, précision. Cliquable (souris et
// clavier) quand `onClick` est fourni.
function KpiCard({ label, main, sub, onClick }) {
  const onKeyDown = onClick ? (e) => {
    if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); }
  } : undefined;
  return (
    <div
      className={'card lift' + (onClick ? ' tap' : '')}
      onClick={onClick}
      onKeyDown={onKeyDown}
      role={onClick ? 'button' : undefined}
      tabIndex={onClick ? 0 : undefined}
      style={{ padding: '20px 22px', position: 'relative', cursor: onClick ? 'pointer' : 'default', minWidth: 0 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 16 }}>
        <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--fg-2)', letterSpacing: '-.005em' }}>{label}</span>
      </div>
      <div style={{ minWidth: 0, containerType: 'inline-size' }}>
        <div style={{ fontSize: 30, fontWeight: 700, letterSpacing: '-.022em', color: 'var(--fg)', lineHeight: 1.05 }}>{main}</div>
        {sub && <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 10 }}>{sub}</div>}
      </div>
    </div>);

}

function ChartCard({ title, subtitle, children }) {
  return (
    <div className="card lift" style={{ padding: '18px 20px', minWidth: 0 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', columnGap: 12, rowGap: 2, marginBottom: 6 }}>
        <CardTitle style={{ fontSize: 13 }}>{title}</CardTitle>
        {subtitle && <span style={{ fontSize: 11, color: 'var(--fg-3)' }}>{subtitle}</span>}
      </div>
      <div style={{ marginTop: 12 }}>{children}</div>
    </div>);

}

// Courbe lissée avec aire, graduations à droite et infobulle au survol.
// `dates` (optionnel) : instants ISO des points, même longueur que `data` → axe X
// et infobulle datés dans le fuseau du profil (fmtDayShort). Sans `dates`, pas
// d'axe X : on n'invente pas de dates.
// `labelMax` / `labelMin` : graduations haute et basse ; `labelZero` : graduation
// de la ligne 0 quand elle est dans l'intervalle ; `labelLast` : valeur finale,
// étiquetée à hauteur du dernier point. `valueFmt` formate la valeur de l'infobulle.
// `ariaLabel` : résumé de la courbe pour un lecteur d'écran ; fourni, le
// graphique devient une image nommée (role=img).
function LineArea({ data, W, H, stroke, fill, labelMax, labelMin, labelZero, labelLast, hover, dates, valueFmt = (v) => fmtMoney(v, true), band = null, ariaLabel }) {
  const hasDates = Array.isArray(dates) && dates.length === data.length;
  const tickDate = (iso) => fmtDayShort(iso);
  // Largeur d'un caractère à 10 px en police à chasse fixe : la marge droite
  // s'adapte aux étiquettes pour que rien ne soit coupé (mobile compris).
  const CH = 6.1;
  const longest = Math.max(
    String(labelMax == null ? '' : labelMax).length, String(labelMin == null ? '' : labelMin).length,
    String(labelZero == null ? '' : labelZero).length, String(labelLast == null ? '' : labelLast).length
  );
  const padL = 6, padR = Math.max(56, Math.ceil(16 + CH * longest)), padT = 14, padB = 22;
  const w = W - padL - padR, h = H - padT - padB;
  const max = Math.max(...data), min = Math.min(...data);
  const sx = w / (data.length - 1);
  // Courbe plate (toutes les valeurs égales) : pas d'échelle à graduer, le trait
  // est centré dans le graphe et seule l'étiquette finale donne la valeur.
  const flat = max === min;
  const y = (v) => flat ? padT + h / 2 : padT + h - (v - min) / (max - min) * h;
  function smooth(pts) {
    if (!pts.length) return '';
    let d = `M ${pts[0][0]} ${pts[0][1]}`;
    for (let i = 0; i < pts.length - 1; i++) {
      const [x0, y0] = pts[i], [x1, y1] = pts[i + 1];
      const cx = (x0 + x1) / 2;
      d += ` C ${cx} ${y0}, ${cx} ${y1}, ${x1} ${y1}`;
    }
    return d;
  }
  const pts = data.map((v, i) => [padL + i * sx, y(v)]);
  const path = smooth(pts);
  const lastY = y(data[data.length - 1]);
  const lastX = padL + (data.length - 1) * sx;
  const yZero = y(0);
  const yTop = padT, yBot = padT + h;
  const area = path + ` L ${lastX} ${yBot} L ${padL} ${yBot} Z`;
  const gx = padL + w; // bord droit du tracé
  // Une graduation trop proche de l'étiquette finale s'efface derrière elle.
  const clearOfLast = (yy) => labelLast == null || Math.abs(yy - lastY) >= 15;
  const zeroInside = 0 > min && 0 < max && Math.abs(yZero - yTop) >= 12 && Math.abs(yZero - yBot) >= 12;
  const axisText = { fontSize: 10, fill: 'var(--fg-3)', fontFamily: 'JetBrains Mono, monospace' };

  // Dates de l'axe : première et dernière, plus celle du milieu quand elle
  // apporte quelque chose (différente de ses voisines) et qu'elle tient entre
  // les deux avec TICK_GAP px de marge. Largeurs estimées comme les marges
  // (CH par caractère). Sur un téléphone étroit, si même les deux dates des
  // extrémités ne tiennent pas côte à côte, elles perdent leur année (le
  // sous-titre de la carte donne déjà la plage complète) ; partout ailleurs le
  // format reste celui de fmtDayShort (charte §4).
  const TICK_GAP = 10;
  let ticks = [];
  if (hasDates) {
    const n = data.length;
    const mk = (idx, anchor) => { const txt = tickDate(dates[idx]); return { idx, anchor, txt, tw: CH * txt.length }; };
    const first = mk(0, 'start');
    ticks = [first];
    if (n > 1) {
      const last = mk(n - 1, 'end');
      ticks.push(last);
      const midIdx = Math.floor(n / 2);
      if (midIdx > 0 && midIdx < n - 1) {
        const mid = mk(midIdx, 'middle');
        const xm = padL + midIdx * sx;
        const fits = xm - mid.tw / 2 >= padL + first.tw + TICK_GAP && xm + mid.tw / 2 <= gx - last.tw - TICK_GAP;
        if (fits && mid.txt !== first.txt && mid.txt !== last.txt) ticks.splice(1, 0, mid);
      }
      if (ticks.length === 2 && first.tw + last.tw + TICK_GAP > w) {
        ticks = ticks.map((t) => { const txt = t.txt.replace(/\s\d{4}$/, ''); return { ...t, txt, tw: CH * txt.length }; });
        // Toujours trop serré (graphe minuscule) : la date de fin seule.
        if (ticks[0].tw + ticks[1].tw + TICK_GAP > w) ticks = [ticks[1]];
      }
    }
  }

  const [hoverIdx, setHoverIdx] = React.useState(null);
  const svgRef = React.useRef(null);

  let tip = null;
  if (hover && hoverIdx != null && hoverIdx < data.length) {
    const hx = padL + hoverIdx * sx, hy = y(data[hoverIdx]);
    const rank = fmtNum(hoverIdx + 1, 0);
    const l1 = hasDates ? `Trade ${rank} · ${tickDate(dates[hoverIdx])}` : `Trade ${rank}`;
    const l2 = valueFmt(data[hoverIdx]);
    const tipW = Math.ceil(12 + Math.max(l1.length * 5.4, l2.length * 6.6));
    const tx = hx + 8 + tipW <= W - 4 ? hx + 8 : Math.max(4, hx - 8 - tipW);
    const ty = Math.max(hy - 36, 2);
    tip = (
      <g>
        <line x1={hx} y1={padT} x2={hx} y2={padT + h} stroke="var(--fg)" strokeWidth="1" strokeDasharray="3 3" opacity=".35" />
        <circle cx={hx} cy={hy} r="4" fill="#fff" stroke={stroke} strokeWidth="2" />
        <g transform={`translate(${tx}, ${ty})`}>
          <rect x="0" y="0" width={tipW} height="30" rx="6" fill="#0f172a" />
          <text x="6" y="12" style={{ fontSize: 9, fill: 'rgba(255,255,255,.72)', fontFamily: 'JetBrains Mono, monospace' }}>{l1}</text>
          <text x="6" y="24" style={{ fontSize: 11, fill: '#fff', fontFamily: 'JetBrains Mono, monospace', fontWeight: 600 }}>{l2}</text>
        </g>
      </g>
    );
  }

  return (
    <svg ref={svgRef} width="100%" height={H} viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet"
    role={ariaLabel ? 'img' : undefined} aria-label={ariaLabel || undefined}
    style={{ display: 'block', cursor: hover ? 'crosshair' : 'default' }}
    onMouseMove={hover ? (e) => {
      // Mapping pixel → coordonnées viewBox EXACT : avec preserveAspectRatio="xMidYMid meet",
      // le viewBox est mis à l'échelle uniformément (min des deux ratios) puis CENTRÉ dans le
      // rect rendu → il faut retirer le letterboxing horizontal avant de diviser par l'échelle,
      // sinon le point mis en avant est décalé du curseur. Arrondi au point le PLUS PROCHE.
      const r = svgRef.current.getBoundingClientRect();
      if (!r.width || !r.height) return;
      const scale = Math.min(r.width / W, r.height / H);
      const offX = (r.width - W * scale) / 2;
      const x = (e.clientX - r.left - offX) / scale;
      setHoverIdx(Math.max(0, Math.min(data.length - 1, Math.round((x - padL) / sx))));
    } : undefined}
    onMouseLeave={() => setHoverIdx(null)}>
      {[0, 0.5, 1].map((p, i) => {
        const yy = padT + h - p * h;
        return <line key={i} x1={padL} y1={yy} x2={gx} y2={yy} stroke="var(--line)" strokeDasharray={p === 0 || p === 1 ? '' : '2 4'} />;
      })}
      {/* bande de drawdown (pic → creux), discrète */}
      {band && band.to > band.from &&
      <rect x={padL + band.from * sx} y={padT} width={Math.max(0, (band.to - band.from) * sx)} height={h}
        fill="var(--red-soft)" opacity="0.55" style={{ animation: 'fade-in .6s var(--ease) .3s both' }} />
      }
      {labelMax != null && !flat && clearOfLast(yTop) && <text x={gx + 6} y={yTop + 3.5} style={axisText}>{labelMax}</text>}
      {labelMin != null && !flat && clearOfLast(yBot) && <text x={gx + 6} y={yBot + 3.5} style={axisText}>{labelMin}</text>}

      {/* ligne 0 (et sa graduation) quand 0 est dans l'intervalle */}
      {0 >= min && 0 <= max && <line x1={padL} y1={yZero} x2={gx} y2={yZero} stroke="var(--line-2)" strokeDasharray="2 3" />}
      {labelZero != null && zeroInside && clearOfLast(yZero) && <text x={gx + 6} y={yZero + 3.5} style={axisText}>{labelZero}</text>}

      <path d={area} fill={fill} style={{ opacity: 0, animation: 'fade-in .8s var(--ease) .6s forwards' }} />
      {/* Tracé animé : pathLength="1" ramène la longueur du trait à 1, quelle que
          soit sa longueur réelle (plusieurs milliers d'unités dès que la courbe
          zigzague) ; un tiret de 1 couvre donc toujours toute la courbe. */}
      <path d={path} pathLength="1" fill="none" stroke={stroke} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
      style={{ strokeDasharray: 1, strokeDashoffset: 1, animation: 'draw 1.4s var(--ease) .2s forwards' }} />

      <circle cx={lastX} cy={lastY} r="3.5" fill="#fff" stroke={stroke} strokeWidth="2"
      style={{ opacity: 0, animation: 'fade-in .4s var(--ease) 1.4s forwards' }} />

      {/* valeur finale, à hauteur du dernier point */}
      {labelLast != null &&
      <g style={{ opacity: 0, animation: 'fade-in .4s var(--ease) 1.4s forwards' }}>
          <rect x={gx + 3} y={lastY - 8} width={Math.ceil(8 + CH * String(labelLast).length)} height="16" rx="4"
          fill="var(--bg-card)" stroke={stroke} strokeWidth="1.2" />
          <text x={gx + 7} y={lastY + 3.5} style={{ ...axisText, fill: 'var(--fg)', fontWeight: 600 }}>{labelLast}</text>
        </g>
      }

      {/* axe des dates — dates réelles des points ; absent sans prop `dates` */}
      {ticks.map((t) =>
      <text key={t.idx} x={padL + t.idx * sx} y={H - 6} textAnchor={t.anchor} style={axisText}>{t.txt}</text>
      )}

      {tip}
    </svg>);

}

// ─── Calendrier P&L (Tableau de bord, ordinateur et mobile) ───────
// Semaines du lundi au dimanche. Chaque ligne couvre la semaine complète : les
// jours des mois voisins sont affichés estompés avec leurs données, pour que le
// total de la semaine (colonne « Semaine », numéro ISO) corresponde à ce qu'on
// voit. Le total du mois ne compte que les jours du mois.
// `compact` (mobile) : en-tête empilé, jours « Lu … Di », bouton pleine largeur.
// Sur mobile, et dès que la carte est étroite, les cases n'affichent que la
// couleur : les montants exacts passent dans la liste sous la grille.
// `today` : jour du profil (state.today) ; le mois affiché le suit tant qu'on n'a
// pas navigué. `showAdd` : bouton « Ajouter un trade » (masqué sur un compte vide,
// où la carte d'accueil porte déjà l'action).
function CalendarBlock({ nav, onDayClick, openImport, tradesByDay = {}, compact = false, today: todayProp, showAdd = true }) {
  const today = todayProp || todayISO();
  const todayMonth = today.slice(0, 7);
  const [viewMonth, setViewMonth] = React.useState(todayMonth); // 'YYYY-MM'
  const [pinned, setPinned] = React.useState(false);
  React.useEffect(() => { if (!pinned) setViewMonth(todayMonth); }, [todayMonth, pinned]);
  const [cardRef, cardW] = usePgWidth();
  const narrow = compact || (cardW > 0 && cardW < 900);

  const year = Number(viewMonth.slice(0, 4));
  const monthIdx = Number(viewMonth.slice(5, 7)) - 1;
  const shiftMonth = (delta) => {
    const next = new Date(Date.UTC(year, monthIdx + delta, 1)).toISOString().slice(0, 7);
    setViewMonth(next);
    setPinned(next !== todayMonth);
  };

  // Grille : du lundi de la semaine du 1er au dimanche de la semaine du dernier jour.
  const firstDow = (new Date(Date.UTC(year, monthIdx, 1)).getUTCDay() + 6) % 7; // 0 = lundi
  const lastDay = new Date(Date.UTC(year, monthIdx + 1, 0)).getUTCDate();
  const nWeeks = Math.ceil((firstDow + lastDay) / 7);
  const gridStart = pgAddDaysISO(viewMonth + '-01', -firstDow);
  const weeks = [];
  const monthTotal = { net: 0, n: 0 };
  let maxAbsDay = 1;
  for (let wi = 0; wi < nWeeks; wi++) {
    const monday = pgAddDaysISO(gridStart, 7 * wi);
    const days = [];
    let net = 0, n = 0;
    for (let k = 0; k < 7; k++) {
      const iso = pgAddDaysISO(monday, k);
      const data = tradesByDay[iso] || null;
      const inMonth = iso.slice(0, 7) === viewMonth;
      if (data) {
        net += data.pnl; n += data.trades.length;
        maxAbsDay = Math.max(maxAbsDay, Math.abs(data.pnl));
        if (inMonth) { monthTotal.net += data.pnl; monthTotal.n += data.trades.length; }
      }
      days.push({ iso, day: Number(iso.slice(8, 10)), month: Number(iso.slice(5, 7)) - 1, inMonth, data });
    }
    weeks.push({ monday, days, net, n });
  }

  // Intensité de couleur d'un jour (vert/rouge plus ou moins fort), relative au
  // plus gros jour visible. color-mix avec --bg-card : lisible en clair ET en sombre.
  // Jours des mois voisins : teinte réduite de moitié et fond atténué. Un jour
  // tradé à P&L nul (affiché « 0,00 € ») reste repérable (fond neutre) même en
  // couleur seule.
  const dayBg = (c) => {
    if (!c.data) return c.inMonth ? 'var(--bg-card)' : 'var(--bg-soft)';
    const sign = pgSign(c.data.pnl);
    if (!sign) return 'var(--bg-elev)';
    const intensity = Math.min(1, Math.abs(c.data.pnl) / maxAbsDay);
    // 12 % → 36 %. Plafond mesuré : au-delà, le montant écrit sur la case
    // (--green-text, --red-text) passe sous 4,5:1 — dès 37 % pour le rouge en
    // clair. À 36 % : 4,7:1 et 4,6:1 en clair, 5,7:1 en sombre.
    const a = Math.round((12 + intensity * 24) * (c.inMonth ? 1 : 0.5));
    return sign > 0
      ? `color-mix(in srgb, var(--dash-green) ${a}%, var(--bg-card))`
      : `color-mix(in srgb, var(--dash-red) ${a}%, var(--bg-card))`;
  };
  // Un jour à venir n'a pas de journal (règle « jamais au-delà d'aujourd'hui »,
  // la même que les flèches du journal) : sa case est inerte. Exception : un jour
  // à venir qui contient des trades (fichier importé avec une mauvaise date)
  // reste ouvrable, sinon ces trades seraient inatteignables depuis le calendrier.
  const dayOpenable = (c) => c.iso <= today || !!c.data;
  const dayTitle = (c) => pgCap(fmtDayLong(c.iso)) + (c.data
    ? ` · ${fmtMoney(c.data.pnl, true)} · ${pgTrades(c.data.trades.length)}`
    : c.iso > today ? ' · jour à venir' : ' · aucun trade');
  const weekTitle = (wk) => `${fmtWeekLabel(wk.monday)} · ${fmtWeekRange(wk.monday)} · ` + (wk.n ? `${fmtMoney(wk.net, true)} · ${pgTrades(wk.n)}` : 'aucun trade')
    + (nav ? ' — ouvrir le bilan hebdomadaire' : '');
  const goWeekly = nav ? () => nav('weekly') : undefined;
  // Liste sous la grille (mobile / carte étroite) : les semaines tradées (même à
  // venir : leur case colorée n'a pas de montant ici), et celle d'aujourd'hui
  // même vide ; jamais une semaine vide à venir.
  const listWeeks = weeks.filter((wk) => wk.n > 0 || (wk.monday <= today && pgAddDaysISO(wk.monday, 6) >= today));
  const addTrade = () => openImport && openImport(today, 'manual');

  const dayLabels = compact ? FR_WEEKDAYS_2 : FR_WEEKDAYS_SHORT;
  const gridCols = narrow ? 'repeat(7, minmax(0, 1fr))' : 'repeat(7, minmax(0, 1fr)) 126px';
  // Mobile : cases de 44 px de côté à 375 px de large (taille tactile, charte
  // §8) — écart de 2 px et carte à 8 px de marge intérieure.
  const gridGap = compact ? 2 : narrow ? 6 : 8;
  const navBtnStyle = (size) => ({ width: size, height: size, borderRadius: 8, border: '1px solid var(--line)', background: 'var(--bg-card)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: 'var(--fg-2)', flexShrink: 0 });
  const monthLabel = <span style={{ textTransform: 'capitalize' }}>{fmtMonthYear(viewMonth)}</span>;

  return (
    <div ref={cardRef} className="card" style={{ padding: compact ? '14px 8px' : '20px 22px' }}>
      {compact ? (
        /* En-tête mobile : titre, puis navigation de mois centrée */
        <div style={{ marginBottom: 12 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
            <CardTitle style={{ fontSize: 14.5 }}>Calendrier P&L</CardTitle>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
            <button type="button" aria-label="Mois précédent" onClick={() => shiftMonth(-1)} className="tap" style={navBtnStyle(44)}>{Ico.arrL}</button>
            <span style={{ fontSize: 14, fontWeight: 600, textAlign: 'center', flex: 1 }}>{monthLabel}</span>
            <button type="button" aria-label="Mois suivant" onClick={() => shiftMonth(1)} className="tap" style={navBtnStyle(44)}>{Ico.arrR}</button>
          </div>
        </div>
      ) : (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12, marginBottom: 18 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
            <CardTitle>Calendrier P&L</CardTitle>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <button type="button" aria-label="Mois précédent" onClick={() => shiftMonth(-1)} className="tap" style={navBtnStyle(30)}>{Ico.arrL}</button>
            <span style={{ fontSize: 14, fontWeight: 600, padding: '0 8px', minWidth: 130, textAlign: 'center' }}>{monthLabel}</span>
            <button type="button" aria-label="Mois suivant" onClick={() => shiftMonth(1)} className="tap" style={navBtnStyle(30)}>{Ico.arrR}</button>
            {/* Action secondaire : l'en-tête de l'écran porte déjà « Ajouter un trade » en bleu (charte §7). */}
            {showAdd && <button type="button" onClick={addTrade} className="tap btn" style={{ marginLeft: 6, color: 'var(--fg)' }}>{Ico.plus} Ajouter un trade</button>}
          </div>
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: gridCols, gap: gridGap, marginBottom: gridGap }}>
        {dayLabels.map((d, di) => <span key={di} style={{ fontSize: compact ? 10.5 : 12, fontWeight: 600, color: 'var(--fg-3)', textTransform: compact ? 'none' : 'uppercase', letterSpacing: '.04em', padding: '0 4px', textAlign: narrow ? 'center' : 'left' }}>{d}</span>)}
        {!narrow && <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '.04em', padding: '0 4px' }}>Semaine</span>}
      </div>

      {weeks.map((wk) =>
      <div key={wk.monday} style={{ display: 'grid', gridTemplateColumns: gridCols, gap: gridGap, marginBottom: gridGap }}>
          {wk.days.map((c) => {
          const isToday = c.iso === today;
          const openable = dayOpenable(c);
          const amount = c.data ? fmtMoneyCompact(c.data.pnl, true) : '';
          return (
            <button key={c.iso} type="button"
            onClick={openable ? () => onDayClick && onDayClick(c.iso) : undefined}
            disabled={!openable}
            title={dayTitle(c)}
            aria-label={dayTitle(c)}
            className={openable ? 'tap' : undefined}
            style={{
              border: '1px ' + (c.inMonth ? 'solid ' : 'dashed ') + (isToday ? 'var(--blue)' : 'var(--line)'),
              borderRadius: narrow ? 8 : 10,
              background: dayBg(c),
              minHeight: compact ? 44 : narrow ? 52 : 88,
              padding: narrow ? '4px 5px' : '10px 10px',
              // Case inerte : ni main ni curseur « interdit », et pas le voile des
              // boutons désactivés (button:disabled, index.html) qui la griserait.
              cursor: openable ? 'pointer' : 'default',
              opacity: 1,
              display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
              textAlign: 'left', color: 'var(--fg)',
              position: 'relative', minWidth: 0,
              boxShadow: isToday ? '0 0 0 2px rgba(59,130,246,0.16)' : 'none',
              transition: 'all .15s var(--ease)',
              fontFamily: 'inherit'
            }}>
                <span style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 4, width: '100%' }}>
                  <span style={{ fontSize: compact ? 11 : 13, fontWeight: isToday ? 700 : 500, color: c.inMonth ? 'var(--fg)' : c.data ? 'var(--fg-2)' : 'var(--fg-3)', letterSpacing: '-.01em', whiteSpace: 'nowrap' }}>
                    {c.day}{!narrow && !c.inMonth ? ' ' + FR_MONTHS_SHORT[c.month] : ''}
                  </span>
                  {isToday && <span style={{ width: compact ? 5 : 6, height: compact ? 5 : 6, borderRadius: '50%', background: 'var(--blue)', flexShrink: 0 }}></span>}
                </span>
                {c.data && !narrow && (
                  <span style={{ display: 'block', width: '100%', containerType: 'inline-size' }}>
                    <span className="num" style={{
                      display: 'block', fontSize: pgFitFont(amount, 15), fontWeight: 700,
                      color: pgPnlTextColor(c.data.pnl),
                      letterSpacing: '-.018em', lineHeight: 1.1, whiteSpace: 'nowrap'
                    }}>{amount}</span>
                    <span style={{ display: 'flex', gap: 3, flexWrap: 'wrap', marginTop: 6 }}>
                      {/* Ordre de la journée, de gauche à droite (tradesByDay est trié du plus récent au plus ancien). */}
                      {[...c.data.trades].sort((a, b) => (Date.parse(a.executed_at) || 0) - (Date.parse(b.executed_at) || 0)).slice(0, 5).map((t, ti) => {
                        const v = Number(t.pnl) || 0;
                        const win = v > 0, loss = v < 0;
                        return (
                          <span key={ti} title={win ? 'Gagnant' : loss ? 'Perdant' : 'Nul'} style={{
                            minWidth: 16, height: 16, padding: '0 3px', borderRadius: 4,
                            fontSize: 10, fontWeight: 700, lineHeight: 1,
                            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                            // Teinte posée sur le fond de carte, pas sur la case : en
                            // sombre les teintes douces sont translucides, et sur une
                            // case très colorée la lettre tombait sous 4,5:1.
                            background: win ? 'linear-gradient(var(--green-soft), var(--green-soft)) var(--bg-card)'
                              : loss ? 'linear-gradient(var(--red-soft), var(--red-soft)) var(--bg-card)' : 'var(--bg-elev)',
                            color: win ? 'var(--green-text)' : loss ? 'var(--red-text)' : 'var(--fg-2)',
                          }}>{win ? 'G' : loss ? 'P' : '·'}</span>
                        );
                      })}
                      {c.data.trades.length > 5 && (
                        <span style={{ fontSize: 10.5, color: 'var(--fg-2)', alignSelf: 'center' }}>+{c.data.trades.length - 5}</span>
                      )}
                    </span>
                  </span>
                )}
              </button>);

        })}
          {!narrow &&
        <button type="button" onClick={goWeekly} disabled={!goWeekly} title={weekTitle(wk)} aria-label={weekTitle(wk)} className={goWeekly ? 'tap' : undefined} style={{
          border: '1px solid var(--line)', borderRadius: 10,
          background: 'var(--bg-soft)', padding: '10px 10px', minHeight: 80, minWidth: 0,
          display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
          textAlign: 'left', color: 'var(--fg)', cursor: goWeekly ? 'pointer' : 'default', fontFamily: 'inherit'
        }}>
            <span style={{ display: 'block' }}>
              <span style={{ display: 'block', fontSize: 11.5, fontWeight: 600, color: 'var(--fg-2)', whiteSpace: 'nowrap' }}>{fmtWeekLabel(wk.monday, true)}</span>
              <span style={{ display: 'block', fontSize: 10.5, color: 'var(--fg-3)', marginTop: 2, whiteSpace: 'nowrap' }}>{fmtWeekRange(wk.monday)}</span>
            </span>
            <span style={{ display: 'block', width: '100%', containerType: 'inline-size' }}>
              {/* Semaine sans trade : « — » (charte §7), jamais « 0,00 € · 0 trade ». */}
              <span className="num" style={{
                display: 'block', fontSize: wk.n ? pgFitFont(fmtMoneyCompact(wk.net, true), 15) : 15, fontWeight: 600,
                color: wk.n ? pgPnlTextColor(wk.net) : 'var(--fg-4)', letterSpacing: '-.018em', lineHeight: 1.1, whiteSpace: 'nowrap'
              }}>{wk.n ? fmtMoneyCompact(wk.net, true) : '—'}</span>
              {wk.n > 0 && <span style={{ display: 'block', fontSize: 10.5, color: 'var(--fg-3)', marginTop: 3 }}>{pgTrades(wk.n)}</span>}
            </span>
          </button>
        }
        </div>
      )}

      {/* Cases en couleur seule (mobile, carte étroite) : semaines et jours tradés
          en liste, avec les montants exacts. */}
      {narrow && listWeeks.length > 0 && (
        <div style={{ marginTop: 8, borderTop: '1px solid var(--line)' }}>
          {listWeeks.map((wk, wi) => (
            <div key={wk.monday} style={{ padding: compact ? '2px 2px' : '8px 2px', borderBottom: wi < listWeeks.length - 1 ? '1px solid var(--line)' : 'none' }}>
              <button type="button" onClick={goWeekly} disabled={!goWeekly} title={weekTitle(wk)} style={{
                display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: 0, border: 'none', background: 'transparent',
                color: 'var(--fg)', textAlign: 'left', cursor: goWeekly ? 'pointer' : 'default', fontFamily: 'inherit',
                // Mobile : chaque ligne de la liste fait 44 px de haut (taille tactile).
                minHeight: compact ? 44 : undefined
              }}>
                <span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg)', whiteSpace: 'nowrap' }}>{fmtWeekLabel(wk.monday, true)}</span>
                  <span style={{ fontSize: 11, color: 'var(--fg-3)', whiteSpace: 'nowrap' }}>{fmtWeekRange(wk.monday)}</span>
                </span>
                <span style={{ marginLeft: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 1 }}>
                  <span className="num" style={{ fontSize: 13, fontWeight: 700, color: wk.n ? pgPnlTextColor(wk.net) : 'var(--fg-4)', whiteSpace: 'nowrap', letterSpacing: '-.014em' }}>{wk.n ? fmtMoney(wk.net, true) : '—'}</span>
                  {wk.n > 0 && <span style={{ fontSize: 11, color: 'var(--fg-3)', whiteSpace: 'nowrap' }}>{pgTrades(wk.n)}</span>}
                </span>
              </button>
              {wk.days.filter((c) => c.data).map((c) => (
                <button key={c.iso} type="button" onClick={() => onDayClick && onDayClick(c.iso)} title={dayTitle(c)} className="tap" style={{
                  display: 'flex', alignItems: compact ? 'center' : 'baseline', gap: 10, width: '100%', padding: compact ? '0 0 0 10px' : '6px 0 0 10px', border: 'none', background: 'transparent',
                  color: 'var(--fg)', textAlign: 'left', cursor: 'pointer', fontFamily: 'inherit',
                  minHeight: compact ? 44 : undefined, borderTop: compact ? '1px solid var(--line)' : 'none'
                }}>
                  <span style={{ fontSize: 12, color: 'var(--fg-2)', whiteSpace: 'nowrap' }}>{pgCap(fmtWeekdayShort(c.iso))} {fmtDayShort(c.iso)}</span>
                  <span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--fg-3)', whiteSpace: 'nowrap' }}>{pgTrades(c.data.trades.length)}</span>
                  <span className="num" style={{ fontSize: 12.5, fontWeight: 600, color: pgPnlTextColor(c.data.pnl), whiteSpace: 'nowrap', letterSpacing: '-.012em' }}>{fmtMoney(c.data.pnl, true)}</span>
                </button>
              ))}
            </div>
          ))}
        </div>
      )}

      {/* Action mobile : ajouter un trade, pleine largeur */}
      {compact && showAdd && (
        <button type="button" onClick={addTrade} className="tap btn" style={{ marginTop: 12, width: '100%', justifyContent: 'center', color: 'var(--fg)' }}>
          {Ico.plus} Ajouter un trade
        </button>
      )}

      {/* Total du mois — en bas à gauche du calendrier */}
      <div style={{ marginTop: compact ? 12 : 4, paddingTop: 12, borderTop: '1px solid var(--line)', display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', columnGap: 10, rowGap: 2 }}>
        <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-2)', letterSpacing: '-.005em' }}>
          {`Total ${FR_MONTHS_LONG[monthIdx]} :`}
        </span>
        {/* Mois sans trade : « — » (charte §7). */}
        <span className="num" style={{
          fontSize: 17, fontWeight: 700, letterSpacing: '-.018em', whiteSpace: 'nowrap',
          color: monthTotal.n ? pgPnlTextColor(monthTotal.net) : 'var(--fg-4)'
        }}>{monthTotal.n ? fmtMoney(monthTotal.net, true) : '—'}</span>
        {monthTotal.n > 0 && <span style={{ fontSize: 11.5, color: 'var(--fg-3)' }}>{pgTrades(monthTotal.n)}</span>}
      </div>
    </div>);

}

Object.assign(window, {
  Dashboard, FilterBar, CalendarBlock, CumulativeChartReal, ChartCard,
  DashKpiRow, DashboardEmpty, dashCumulative, dashCumulativeSubtitle, usePgWidth
});
