// Agenda — vue Jour (défaut), Semaine, Mois.
// Style Vasco (crème/noir/accent) + inspiration Apple Calendar.

const AG_VT = window.VT || {};
const AG_INK = AG_VT.ink || '#0a0a0a';
const AG_SUB = AG_VT.sub || '#6b7280';
const AG_BG = AG_VT.paper || '#f6f5f1';
const AG_SURF = AG_VT.surface || '#ffffff';
const AG_HAIR = AG_VT.hair || 'rgba(10,10,10,0.06)';
const AG_HAIR2 = AG_VT.hair2 || 'rgba(10,10,10,0.10)';
const AG_OK = AG_VT.success || '#059669';
const AG_WARN = AG_VT.warning || '#d97706';   // conflit d'horaire : on avertit, on n'interdit pas
const AG_MED = AG_VT.medBlue || '#3d6b8f';
const AG_HINT_KEY = 'vasco_agenda_swipe_hint';
const AG_OK_TINT = AG_VT.successTint || 'rgba(5,150,105,0.10)';
const AG_OK_TINT_SOFT = AG_VT.successTintSoft || 'rgba(5,150,105,0.06)';

const JOURS = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
const JOURS_FULL = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
const MOIS = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
const MOIS_SHORT = ['Jan', 'Fév', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Déc'];

// Type colors — muted, premium
const TYPE_STYLE = {
  consultation: { bg: 'rgba(59,130,246,0.08)', border: '#3b82f6', text: '#1d4ed8', label: 'Consultation' },
  intervention: { bg: 'rgba(185,28,28,0.08)',  border: '#b91c1c', text: '#991b1b', label: 'Intervention' },
  urgence:      { bg: 'rgba(249,115,22,0.08)', border: '#f97316', text: '#c2410c', label: 'Urgence' },
  suivi:        { bg: AG_OK_TINT_SOFT,         border: AG_OK,     text: AG_OK,     label: 'Suivi' },
};

// ── Mock data ──
const AGENDA_DATA = [
  // Aujourd'hui (30 avril 2026)
  { id: 1, date: '2026-04-30', start: '08:30', end: '09:15', patient: 'M. Martin, Jean', type: 'consultation', lieu: 'Salle 1', notes: 'Suivi AOMI stade IIb' },
  { id: 2, date: '2026-04-30', start: '09:30', end: '12:00', patient: 'Mme Durand, Hélène', type: 'intervention', lieu: 'Bloc A', notes: 'Endartériectomie carotide D' },
  { id: 3, date: '2026-04-30', start: '10:00', end: '10:30', patient: 'M. Roux, Pierre', type: 'urgence', lieu: 'Urgences', notes: 'Ischémie aiguë MI G' },
  { id: 4, date: '2026-04-30', start: '14:00', end: '14:45', patient: 'Mme Petit, Aline', type: 'suivi', lieu: 'Salle 2', notes: 'Contrôle FAV J+30' },
  { id: 5, date: '2026-04-30', start: '15:00', end: '17:00', patient: 'M. Bernard, Paul', type: 'intervention', lieu: 'Bloc B', notes: 'EVAR — AAA 54mm' },
  { id: 6, date: '2026-04-30', start: '17:30', end: '18:00', patient: 'Mme Lefèvre, Denise', type: 'suivi', lieu: 'Salle 1', notes: 'Post-op J+5 pontage' },
  // Demain
  { id: 7, date: '2026-05-01', start: '09:00', end: '09:45', patient: 'M. Garnier, Robert', type: 'consultation', lieu: 'Salle 2', notes: 'Bilan AOMI' },
  { id: 8, date: '2026-05-01', start: '10:00', end: '12:30', patient: 'M. Lemaire, Christian', type: 'intervention', lieu: 'Bloc A', notes: 'Création FAV radio-céphalique' },
  { id: 9, date: '2026-05-01', start: '14:00', end: '14:30', patient: 'Mme Nguyen, Linh', type: 'consultation', lieu: 'Salle 1', notes: 'Bilan varices' },
  // 2 mai
  { id: 10, date: '2026-05-02', start: '08:00', end: '10:00', patient: 'M. Mercier, François', type: 'intervention', lieu: 'Bloc B', notes: 'Pontage aorto-bifémoral' },
  { id: 11, date: '2026-05-02', start: '11:00', end: '11:30', patient: 'Mme Lambert, Catherine', type: 'suivi', lieu: 'Salle 2', notes: 'Doppler contrôle' },
  { id: 12, date: '2026-05-02', start: '14:00', end: '14:45', patient: 'M. Simon, André', type: 'consultation', lieu: 'Salle 1', notes: 'Épreuve de marche' },
  // 3 mai
  { id: 13, date: '2026-05-03', start: '09:00', end: '09:30', patient: 'Mme Bonnet, Marie', type: 'suivi', lieu: 'Salle 2', notes: 'Echo-doppler veineux' },
  { id: 14, date: '2026-05-03', start: '10:00', end: '12:00', patient: 'M. Fournier, Jacques', type: 'intervention', lieu: 'Bloc A', notes: 'Stent iliaque G' },
  // 4 mai
  { id: 15, date: '2026-05-04', start: '09:00', end: '09:45', patient: 'Mme Henry, Françoise', type: 'consultation', lieu: 'Salle 1', notes: 'Post-op endartériectomie' },
  { id: 16, date: '2026-05-04', start: '14:00', end: '16:00', patient: 'M. Thomas, Georges', type: 'intervention', lieu: 'Bloc A', notes: 'Chirurgie anévrysme poplité' },
  // 5-6 mai
  { id: 17, date: '2026-05-05', start: '09:00', end: '09:30', patient: 'Mme Morel, Isabelle', type: 'suivi', lieu: 'Salle 2', notes: 'Contrôle anticoagulation' },
  { id: 18, date: '2026-05-06', start: '08:30', end: '10:30', patient: 'M. Chevalier, René', type: 'intervention', lieu: 'Bloc B', notes: 'Pontage fémoro-poplité D' },
  { id: 19, date: '2026-05-06', start: '14:00', end: '14:30', patient: 'Mme Martinez, Carmen', type: 'consultation', lieu: 'Salle 1', notes: 'Bilan veineux' },
];

// ── Helpers ──
function fmtDate(d) {
  const y = d.getFullYear(), m = String(d.getMonth() + 1).padStart(2, '0'), day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}
function addDays(d, n) { const r = new Date(d); r.setDate(r.getDate() + n); return r; }
function isSameDay(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
function parseTime(t) { const [h, m] = t.split(':').map(Number); return h * 60 + m; }
function startOfWeek(d) { const r = new Date(d); const day = r.getDay(); r.setDate(r.getDate() - (day === 0 ? 6 : day - 1)); return r; }

// Motifs de refus de l'agenda, formulés comme dans l'application native.
const AG_REASONS = {
  role_not_supported: 'Rôle non pris en charge',
  feature_unavailable: 'Fonction indisponible',
  patient_archived: 'Patient archivé',
  patient_deceased: 'Patient décédé',
  appointment_cancelled: 'Rendez-vous annulé',
  appointment_completed: 'Rendez-vous terminé',
};

const agReason = (capability) => AG_REASONS[capability?.reason_code] || 'Action indisponible';

const TODAY = new Date(2026, 3, 30); // 30 avril 2026

// ═════════════════════════════════════════════════════════════
// RDV Card — used in Day and Month detail
// ═════════════════════════════════════════════════════════════
function RdvCard({ rdv, accent, compact = false, onClick }) {
  const ts = TYPE_STYLE[rdv.type] || TYPE_STYLE.consultation;
  return (
    <div className="press" onClick={onClick} style={{
      display: 'flex', gap: 12, padding: compact ? '10px 14px' : '14px 16px',
      background: AG_SURF, borderRadius: 16,
      borderLeft: `3px solid ${ts.border}`,
      boxShadow: VT.shadowMd,
      cursor: 'pointer',
    }}>
      {/* Time column */}
      <div style={{ minWidth: 46, flexShrink: 0 }}>
        <div className="mono" style={{ fontSize: 14, fontWeight: 700, color: AG_INK, letterSpacing: '-0.3px' }}>
          {rdv.start}
        </div>
        <div className="mono" style={{ fontSize: 11, fontWeight: 500, color: AG_SUB, marginTop: 2 }}>
          {rdv.end}
        </div>
      </div>
      {/* Content */}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{
          fontSize: 14, fontWeight: 600, color: AG_INK, letterSpacing: '-0.1px',
          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        }}>{rdv.patient}</div>
        {!compact && (
          <div style={{ fontSize: 12, fontWeight: 500, color: AG_SUB, marginTop: 3 }}>
            {rdv.notes}
          </div>
        )}
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: compact ? 3 : 6 }}>
          <span style={{
            fontSize: 10, fontWeight: 600, letterSpacing: '0.3px',
            padding: '2px 8px', borderRadius: 999,
            background: ts.bg, color: ts.text,
            fontFamily: "'JetBrains Mono', monospace",
            textTransform: 'uppercase',
          }}>{ts.label}</span>
          <span style={{ fontSize: 11, fontWeight: 500, color: AG_SUB }}>
            {rdv.lieu}
          </span>
        </div>
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// Bottom sheet — RDV detail
// ═════════════════════════════════════════════════════════════
function RdvDetailSheet({ rdv, onClose, accent, runtime = null, capabilities = null, onChanged }) {
  const ts = TYPE_STYLE[rdv.type] || TYPE_STYLE.consultation;
  const rdvDate = new Date(rdv.date);
  const dateLabel = `${JOURS_FULL[rdvDate.getDay()]} ${rdvDate.getDate()} ${MOIS[rdvDate.getMonth()].toLowerCase()} ${rdvDate.getFullYear()}`;

  // « Modifier » et « Supprimer » n'avaient aucun gestionnaire dans la maquette :
  // les deux boutons étaient purement décoratifs. Ils agissent maintenant, et
  // chacun lit la capacité que le serveur attache au rendez-vous.
  const [mode, setMode] = React.useState(null); // null | 'edit' | 'cancel'
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [editStart, setEditStart] = React.useState(`${rdv.date}T${rdv.start}`);
  const [editEnd, setEditEnd] = React.useState(`${rdv.date}T${rdv.end}`);
  const [editReason, setEditReason] = React.useState(rdv.notes || '');
  const [editLocation, setEditLocation] = React.useState(rdv.lieu === 'Lieu non renseigné' ? '' : rdv.lieu);
  const [cancelReason, setCancelReason] = React.useState('');

  const updateCapability = rdv.capabilities?.update || capabilities?.update || null;
  const cancelCapability = rdv.capabilities?.cancel || capabilities?.cancel || null;
  const canUpdate = !updateCapability || updateCapability.enabled;
  const canCancel = !cancelCapability || cancelCapability.enabled;

  // Un chevauchement n'est pas une erreur : un chirurgien intercale une urgence
  // en connaissance de cause. Le serveur refuse une première fois et remet un
  // jeton ; la reprise rejoue la même commande avec le jeton. On garde donc de
  // quoi rejouer, et on demande à l'écran plutôt que par une boîte native, qui
  // bloquerait le fil du navigateur.
  const [pendingOverlap, setPendingOverlap] = React.useState(null);

  const buildChanges = () => {
    // Les heures saisies sont des heures murales du fuseau de l'agenda : la
    // conversion vers l'offset attendu est faite par le client d'API.
    if (editEnd <= editStart) return null;
    return {
      ...runtime.temporal(editStart, editEnd),
      reason: editReason.trim(),
      location: editLocation.trim() || null,
    };
  };

  const sendEdit = async (changes, attempt) => {
    setBusy(true);
    setError(null);
    try {
      await runtime.update(rdv, changes, attempt);
      setPendingOverlap(null);
      // La feuille se ferme sur un agenda rechargé : le rendez-vous déplacé s'y
      // lit à sa nouvelle place, ce qu'aucune notification ne dirait mieux.
      if (onChanged) await onChanged();
      onClose();
    } catch (failure) {
      if (failure?.code === 'appointment_overlap' && failure.detail?.confirmation_token) {
        setPendingOverlap({
          changes,
          attempt: { ...attempt, confirmationToken: failure.detail.confirmation_token },
          conflicts: failure.detail.conflicts || [],
        });
        setError(null);
        return;
      }
      setError(failure?.message || 'Modification impossible');
    } finally {
      setBusy(false);
    }
  };

  // Un refus décrit les horaires envoyés. Dès qu'on les retouche il ne décrit
  // plus rien : il s'efface à la correction, au lieu de rester affiché à
  // contredire la saisie jusqu'à la soumission suivante. Le chevauchement en
  // attente n'est pas un refus mais une question posée au praticien : il garde
  // sa propre remise à zéro.
  const editDraft = (apply) => { setError(null); apply(); };

  const submitEdit = () => {
    if (!runtime || !editReason.trim()) return;
    let changes;
    try { changes = buildChanges(); } catch (failure) { setError(failure?.message || 'Horaires invalides'); return; }
    if (!changes) { setError('La fin doit être postérieure au début'); return; }
    // La clé d'idempotence est fixée ici : la reprise après conflit doit
    // présenter la même commande, sinon le jeton n'est pas reconnu.
    void sendEdit(changes, { idempotencyKey: window.crypto?.randomUUID?.() || `vasco-agenda-${Date.now()}` });
  };

  const submitCancel = async () => {
    if (!runtime) return;
    setBusy(true);
    setError(null);
    try {
      await runtime.cancel(rdv, cancelReason.trim() || null);
      // Le rendez-vous annulé disparaît de la grille au rechargement.
      if (onChanged) await onChanged();
      onClose();
    } catch (failure) {
      setError(failure?.message || 'Annulation impossible');
    } finally {
      setBusy(false);
    }
  };

  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 50 }}>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(10,10,10,0.3)' }} />
      {/* Même géométrie que la feuille « Nouveau RDV » plus bas : la feuille est
          bornée à 85 % de l'écran et son contenu défile à l'intérieur. Elle
          poussait auparavant jusqu'en bas d'une liste haute de six rendez-vous,
          et ses commandes — avec les raisons de refus qui les accompagnent —
          tombaient sous le pli : il fallait défiler la liste, sous la feuille,
          pour les atteindre. */}
      <div style={{
        position: 'absolute', bottom: 0, left: 0, right: 0,
        background: AG_BG, borderRadius: '24px 24px 0 0',
        maxHeight: '85%', display: 'flex', flexDirection: 'column',
        animation: 'sheetUp .3s cubic-bezier(.2,.7,.2,1)',
        boxShadow: VT.shadowSheet,
      }}>
        <style>{`@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }`}</style>

        {/* Drag handle */}
        <div style={{ display: 'flex', justifyContent: 'center', padding: '10px 0 6px' }}>
          <div style={{ width: 36, height: 4, borderRadius: 2, background: 'rgba(10,10,10,0.12)' }} />
        </div>

        <div className="vasco-scroll" data-vasco="rdv-detail-scroll"
          style={{ flex: 1, overflowY: 'auto', padding: '4px 20px 32px' }}>
          {/* Header — patient name + type badge */}
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
            <div style={{ fontSize: 20, fontWeight: 800, color: AG_INK, letterSpacing: '-0.4px' }}>{rdv.patient}</div>
            <span style={{
              fontSize: 10, fontWeight: 600, letterSpacing: '0.3px',
              padding: '4px 10px', borderRadius: 999,
              background: ts.bg, color: ts.text,
              fontFamily: "'JetBrains Mono', monospace", textTransform: 'uppercase',
            }}>{ts.label}</span>
          </div>

          {/* Info rows */}
          <div style={{
            background: AG_SURF, borderRadius: 16, padding: '4px 0',
            border: `1px solid ${AG_HAIR}`, marginBottom: 16,
          }}>
            {[
              { icon: <><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></>, label: 'Horaire', value: `${rdv.start} — ${rdv.end}` },
              { icon: <><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></>, label: 'Date', value: dateLabel },
              { icon: <><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></>, label: 'Lieu', value: rdv.lieu },
            ].map((row, i, arr) => (
              <div key={row.label} style={{
                display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px',
                borderBottom: i < arr.length - 1 ? `1px solid ${AG_HAIR}` : 'none',
              }}>
                <div style={{
                  width: 32, height: 32, borderRadius: 12, background: 'rgba(10,10,10,0.04)',
                  display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
                }}>
                  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">{row.icon}</svg>
                </div>
                <div style={{ flex: 1 }}>
                  <div className="mono" style={{ fontSize: 9, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase' }}>{row.label}</div>
                  <div style={{ fontSize: 14, fontWeight: 600, color: AG_INK, marginTop: 2 }}>{row.value}</div>
                </div>
              </div>
            ))}
          </div>

          {/* Notes */}
          {rdv.notes && (
            <div style={{
              background: AG_SURF, borderRadius: 16, padding: '14px 16px',
              border: `1px solid ${AG_HAIR}`, marginBottom: 16,
            }}>
              <div className="mono" style={{ fontSize: 9, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase', marginBottom: 6 }}>Notes</div>
              <div className="serif" style={{ fontSize: 14.5, lineHeight: 1.5, color: AG_INK }}>{rdv.notes}</div>
            </div>
          )}

          {error && (
            <div role="alert" style={{
              padding: '9px 12px', borderRadius: 12, marginBottom: 12,
              background: 'rgba(185,28,28,0.06)', fontSize: 12, fontWeight: 600, color: '#b91c1c',
            }}>{error}</div>
          )}

          {pendingOverlap && (
            <div role="alert" style={{
              padding: '12px 14px', borderRadius: 16, marginBottom: 12,
              background: 'rgba(217,119,6,0.06)', border: `1px solid rgba(217,119,6,0.16)`,
            }}>
              <div style={{ fontSize: 13, fontWeight: 700, color: AG_INK }}>Créneau déjà occupé</div>
              <div style={{ fontSize: 12, fontWeight: 500, color: AG_SUB, marginTop: 4, lineHeight: 1.4 }}>
                {pendingOverlap.conflicts.length} rendez-vous se chevauche(nt) avec cet horaire.
                Programmer en parallèle reste possible.
              </div>
              <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
                <button onClick={() => setPendingOverlap(null)} disabled={busy} className="press" style={{
                  flex: 1, height: 38, borderRadius: 12, background: AG_SURF,
                  border: `1px solid ${AG_HAIR2}`, fontFamily: 'inherit',
                  fontSize: 12.5, fontWeight: 700, color: AG_INK, cursor: 'pointer',
                }}>Choisir un autre horaire</button>
                <button onClick={() => void sendEdit(pendingOverlap.changes, pendingOverlap.attempt)}
                  disabled={busy} className="press" style={{
                    flex: 1, height: 38, borderRadius: 12, background: AG_WARN,
                    border: 'none', fontFamily: 'inherit',
                    fontSize: 12.5, fontWeight: 700, color: '#fff', cursor: 'pointer',
                  }}>Programmer quand même</button>
              </div>
            </div>
          )}

          {mode === 'edit' && (
            <div style={{
              background: AG_SURF, borderRadius: 16, padding: '14px 16px',
              border: `1px solid ${AG_HAIR}`, marginBottom: 16,
            }}>
              <div className="mono" style={{ fontSize: 9, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase', marginBottom: 8 }}>Modifier</div>
              <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
                <input type="datetime-local" value={editStart} onChange={(e) => editDraft(() => setEditStart(e.target.value))}
                  aria-label="Début" style={{ flex: 1, minWidth: 0, height: 38, borderRadius: 12, border: `1px solid ${AG_HAIR2}`, padding: '0 10px', fontFamily: "'JetBrains Mono', monospace", fontSize: 11, color: AG_INK, background: AG_BG, outline: 'none' }} />
                <input type="datetime-local" value={editEnd} onChange={(e) => editDraft(() => setEditEnd(e.target.value))}
                  aria-label="Fin" style={{ flex: 1, minWidth: 0, height: 38, borderRadius: 12, border: `1px solid ${AG_HAIR2}`, padding: '0 10px', fontFamily: "'JetBrains Mono', monospace", fontSize: 11, color: AG_INK, background: AG_BG, outline: 'none' }} />
              </div>
              <input value={editReason} onChange={(e) => editDraft(() => setEditReason(e.target.value))} maxLength={160}
                placeholder="Motif" aria-label="Motif"
                style={{ width: '100%', height: 38, borderRadius: 12, border: `1px solid ${AG_HAIR2}`, padding: '0 12px', fontFamily: 'inherit', fontSize: 13, color: AG_INK, background: AG_BG, outline: 'none', marginBottom: 8 }} />
              <input value={editLocation} onChange={(e) => editDraft(() => setEditLocation(e.target.value))} maxLength={160}
                placeholder="Lieu (optionnel)" aria-label="Lieu"
                style={{ width: '100%', height: 38, borderRadius: 12, border: `1px solid ${AG_HAIR2}`, padding: '0 12px', fontFamily: 'inherit', fontSize: 13, color: AG_INK, background: AG_BG, outline: 'none' }} />
              <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
                <button onClick={() => { setMode(null); setPendingOverlap(null); setError(null); }} className="press" style={{
                  height: 38, padding: '0 14px', borderRadius: 12, background: 'transparent',
                  border: `1px solid ${AG_HAIR2}`, fontFamily: 'inherit', fontSize: 12.5, fontWeight: 700, color: AG_INK, cursor: 'pointer',
                }}>Annuler</button>
                <button onClick={submitEdit} disabled={busy || !editReason.trim() || !runtime} className="press" style={{
                  flex: 1, height: 38, borderRadius: 12, background: AG_INK, border: 'none',
                  fontFamily: 'inherit', fontSize: 12.5, fontWeight: 700, color: '#fff',
                  cursor: busy ? 'default' : 'pointer', opacity: busy || !editReason.trim() ? 0.5 : 1,
                }}>{busy ? 'Envoi…' : 'Enregistrer'}</button>
              </div>
            </div>
          )}

          {mode === 'cancel' && (
            <div style={{
              background: AG_SURF, borderRadius: 16, padding: '14px 16px',
              border: `1px solid rgba(185,28,28,0.12)`, marginBottom: 16,
            }}>
              <div style={{ fontSize: 13, fontWeight: 700, color: AG_INK }}>Annuler ce rendez-vous ?</div>
              {/* L'API annule, elle ne supprime pas : le rendez-vous reste dans
                  l'historique avec son motif. Le bouton disait « Supprimer ». */}
              <div style={{ fontSize: 12, fontWeight: 500, color: AG_SUB, marginTop: 4, lineHeight: 1.4 }}>
                Il passe en « annulé » et reste consultable dans l'historique. Aucune suppression.
              </div>
              <input value={cancelReason} onChange={(e) => setCancelReason(e.target.value)} maxLength={500}
                placeholder="Motif d'annulation (optionnel)" aria-label="Motif d'annulation"
                style={{ width: '100%', height: 38, borderRadius: 12, border: `1px solid ${AG_HAIR2}`, padding: '0 12px', fontFamily: 'inherit', fontSize: 13, color: AG_INK, background: AG_BG, outline: 'none', marginTop: 10 }} />
              <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
                <button onClick={() => { setMode(null); setError(null); }} className="press" style={{
                  flex: 1, height: 38, borderRadius: 12, background: 'transparent',
                  border: `1px solid ${AG_HAIR2}`, fontFamily: 'inherit', fontSize: 12.5, fontWeight: 700, color: AG_INK, cursor: 'pointer',
                }}>Revenir</button>
                <button onClick={() => void submitCancel()} disabled={busy || !runtime} className="press" style={{
                  flex: 1, height: 38, borderRadius: 12, background: '#b91c1c', border: 'none',
                  fontFamily: 'inherit', fontSize: 12.5, fontWeight: 700, color: '#fff',
                  cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.5 : 1,
                }}>{busy ? 'Envoi…' : 'Annuler le RDV'}</button>
              </div>
            </div>
          )}

          {/* Actions */}
          {!mode && (
          <div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={() => { if (canUpdate) setMode('edit'); else setError(agReason(updateCapability)); }}
              disabled={!runtime}
              title={canUpdate ? 'Modifier' : `Modifier — ${agReason(updateCapability)}`}
              aria-label={canUpdate ? 'Modifier' : `Modifier — ${agReason(updateCapability)}`}
              className="press" style={{
                flex: 1, height: 46, borderRadius: 16,
                background: AG_SURF, border: `1px solid ${AG_HAIR2}`,
                fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: AG_INK,
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                cursor: runtime ? 'pointer' : 'default', opacity: canUpdate ? 1 : 0.45,
              }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
              </svg>
              Modifier
            </button>
            <button onClick={() => { if (canCancel) setMode('cancel'); else setError(agReason(cancelCapability)); }}
              disabled={!runtime}
              title={canCancel ? 'Annuler le RDV' : `Annuler le RDV — ${agReason(cancelCapability)}`}
              aria-label={canCancel ? 'Annuler le RDV' : `Annuler le RDV — ${agReason(cancelCapability)}`}
              className="press" style={{
                flex: 1, height: 46, borderRadius: 16,
                background: 'rgba(185,28,28,0.06)', border: `1px solid rgba(185,28,28,0.12)`,
                fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: '#b91c1c',
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                cursor: runtime ? 'pointer' : 'default', opacity: canCancel ? 1 : 0.45,
              }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#b91c1c" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <path d="M18 6L6 18M6 6l12 12"/>
              </svg>
              Annuler le RDV
            </button>
          </div>
          {/* Un bouton éteint dit pourquoi à l'écran, pas au survol : la raison
              vivait dans le seul attribut `title`, invisible au doigt. Elle se
              lit maintenant sous les commandes, chacune avec la sienne — le
              rendez-vous d'un patient archivé et un rendez-vous terminé ne se
              refusent pas pour le même motif, et les deux commandes peuvent se
              refuser séparément. Le `title` reste en plus, pour la souris. */}
          {(!canUpdate || !canCancel) && (
            <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 4 }}>
              {!canUpdate && (
                <div style={{ fontSize: 12, fontWeight: 600, color: AG_SUB, lineHeight: 1.4 }}>
                  Modifier — {agReason(updateCapability)}
                </div>
              )}
              {!canCancel && (
                <div style={{ fontSize: 12, fontWeight: 600, color: AG_SUB, lineHeight: 1.4 }}>
                  Annuler le RDV — {agReason(cancelCapability)}
                </div>
              )}
            </div>
          )}
          </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// Vue Jour
// ═════════════════════════════════════════════════════════════
// La feuille de détail n'est plus posée ici : elle vit au niveau de l'écran,
// avec celle de création (cf. `AgendaScreen`). Cette liste ne fait plus que
// désigner le rendez-vous choisi.
function DayView({ date, rdvs, accent, onSelectRdv }) {
  const dayKey = fmtDate(date);
  const dayRdvs = rdvs.filter(r => r.date === dayKey).sort((a, b) => parseTime(a.start) - parseTime(b.start));
  const isToday = isSameDay(date, TODAY);

  // `position: relative` n'avait de raison d'être que pour la feuille de
  // détail, qui n'est plus posée ici.
  return (
    <div style={{ padding: '0 16px 120px' }}>
      {/* Day summary */}
      {dayRdvs.length > 0 && (
        <div style={{
          display: 'flex', gap: 8, padding: '12px 0 16px', overflowX: 'auto',
        }} className="vasco-scroll">
          {Object.entries(
            dayRdvs.reduce((acc, r) => { acc[r.type] = (acc[r.type] || 0) + 1; return acc; }, {})
          ).map(([type, count]) => {
            const ts = TYPE_STYLE[type];
            return (
              <div key={type} style={{
                display: 'flex', alignItems: 'center', gap: 6,
                padding: '6px 12px', borderRadius: 12,
                background: ts.bg, flexShrink: 0,
              }}>
                <div style={{ width: 6, height: 6, borderRadius: '50%', background: ts.border }} />
                <span style={{ fontSize: 12, fontWeight: 600, color: ts.text }}>
                  {count} {ts.label.toLowerCase()}{count > 1 ? 's' : ''}
                </span>
              </div>
            );
          })}
        </div>
      )}

      {/* Timeline */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {dayRdvs.length === 0 ? (
          <div style={{
            textAlign: 'center', padding: '48px 0',
            color: AG_SUB, fontSize: 14, fontWeight: 500,
          }}>
            <div style={{ fontSize: 32, marginBottom: 8 }}>📋</div>
            Aucun rendez-vous
          </div>
        ) : dayRdvs.map(r => (
          <RdvCard key={r.id} rdv={r} accent={accent} onClick={() => onSelectRdv && onSelectRdv(r)} />
        ))}
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// Vue Semaine — bande horizontale des 7 jours + liste du jour sélectionné
// ═════════════════════════════════════════════════════════════
function WeekView({ date, setDate, rdvs, accent, onSelectRdv }) {
  const week = startOfWeek(date);
  const days = Array.from({ length: 7 }, (_, i) => addDays(week, i));

  return (
    <div>
      {/* Week strip */}
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)',
        padding: '8px 12px 12px', gap: 4,
      }}>
        {days.map((d, i) => {
          const isToday_ = isSameDay(d, TODAY);
          const isSelected = isSameDay(d, date);
          const dayRdvs = rdvs.filter(r => r.date === fmtDate(d));
          const types = [...new Set(dayRdvs.map(r => r.type))];
          return (
            <button key={i} onClick={() => setDate(new Date(d))} className="press" style={{
              background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
              display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
              padding: '6px 0',
            }}>
              <span style={{
                fontSize: 11, fontWeight: 500, color: AG_SUB,
                fontFamily: "'JetBrains Mono', monospace", letterSpacing: '0.3px',
              }}>{JOURS[d.getDay()].charAt(0)}</span>
              <div style={{
                width: 34, height: 34, borderRadius: '50%',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                background: isSelected ? AG_INK : isToday_ ? `${accent}14` : 'transparent',
                transition: 'background .15s ease',
              }}>
                <span style={{
                  fontSize: 15, fontWeight: isToday_ || isSelected ? 700 : 500,
                  color: isSelected ? '#fff' : isToday_ ? accent : AG_INK,
                }}>{d.getDate()}</span>
              </div>
              {/* Type dots */}
              <div style={{ display: 'flex', gap: 3, height: 5 }}>
                {types.slice(0, 3).map((t, j) => (
                  <div key={j} style={{
                    width: 4, height: 4, borderRadius: '50%',
                    background: isSelected ? AG_INK : TYPE_STYLE[t]?.border || AG_SUB,
                  }} />
                ))}
              </div>
            </button>
          );
        })}
      </div>

      {/* Separator */}
      <div style={{ height: 1, background: AG_HAIR, margin: '0 16px' }} />

      {/* Day list below */}
      <DayView date={date} rdvs={rdvs} accent={accent} onSelectRdv={onSelectRdv} />
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// Vue Mois — calendrier grille + liste du jour sélectionné
// ═════════════════════════════════════════════════════════════
function MonthView({ date, setDate, rdvs, accent, onSelectRdv }) {
  const year = date.getFullYear(), month = date.getMonth();
  const firstDay = new Date(year, month, 1);
  const lastDay = new Date(year, month + 1, 0);
  let startDow = firstDay.getDay() - 1;
  if (startDow < 0) startDow = 6;

  const cells = [];
  for (let i = 0; i < startDow; i++) cells.push(null);
  for (let d = 1; d <= lastDay.getDate(); d++) cells.push(d);
  while (cells.length % 7 !== 0) cells.push(null);

  return (
    <div>
      {/* Day headers */}
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)',
        padding: '4px 16px 8px',
      }}>
        {['L', 'M', 'M', 'J', 'V', 'S', 'D'].map((d, i) => (
          <div key={i} style={{
            textAlign: 'center', fontSize: 11, fontWeight: 600, color: AG_SUB,
            fontFamily: "'JetBrains Mono', monospace", letterSpacing: '0.4px',
          }}>{d}</div>
        ))}
      </div>

      {/* Grid */}
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)',
        padding: '0 12px 8px', gap: 2,
      }}>
        {cells.map((day, i) => {
          if (day == null) return <div key={i} />;
          const d = new Date(year, month, day);
          const isToday_ = isSameDay(d, TODAY);
          const isSelected = isSameDay(d, date);
          const dayRdvs = rdvs.filter(r => r.date === fmtDate(d));
          const types = [...new Set(dayRdvs.map(r => r.type))];
          return (
            <button key={i} onClick={() => setDate(new Date(d))} className="press" style={{
              background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
              display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
              padding: '6px 0',
            }}>
              <div style={{
                width: 32, height: 32, borderRadius: '50%',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                background: isSelected ? AG_INK : isToday_ ? `${accent}14` : 'transparent',
                transition: 'background .15s ease',
              }}>
                <span style={{
                  fontSize: 14, fontWeight: isToday_ || isSelected ? 700 : 400,
                  color: isSelected ? '#fff' : isToday_ ? accent : AG_INK,
                }}>{day}</span>
              </div>
              <div style={{ display: 'flex', gap: 2, height: 4 }}>
                {types.slice(0, 3).map((t, j) => (
                  <div key={j} style={{
                    width: 4, height: 4, borderRadius: '50%',
                    background: isSelected ? AG_INK : TYPE_STYLE[t]?.border || AG_SUB,
                  }} />
                ))}
              </div>
            </button>
          );
        })}
      </div>

      {/* Separator */}
      <div style={{ height: 1, background: AG_HAIR, margin: '0 16px' }} />

      {/* Day detail below */}
      <div style={{ padding: '12px 16px 4px' }}>
        <span style={{
          fontSize: 13, fontWeight: 600, color: AG_INK,
        }}>
          {JOURS_FULL[date.getDay()]} {date.getDate()} {MOIS[date.getMonth()].toLowerCase()}
        </span>
      </div>
      <DayView date={date} rdvs={rdvs} accent={accent} onSelectRdv={onSelectRdv} />
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// View toggle — Jour / Semaine / Mois (Apple Calendar style)
// ═════════════════════════════════════════════════════════════
function ViewToggleAG({ mode, onChange }) {
  const modes = [
    { id: 'week', label: 'Semaine' },
    { id: 'month', label: 'Mois' },
  ];
  return (
    <div style={{
      display: 'flex', gap: 0,
      background: 'rgba(10,10,10,0.04)', borderRadius: 12, padding: 3,
      margin: '0 16px',
    }}>
      {modes.map(m => (
        <button key={m.id} onClick={() => onChange(m.id)} className="press" style={{
          flex: 1, padding: '7px 0', borderRadius: 12,
          background: mode === m.id ? AG_SURF : 'transparent',
          border: 'none', cursor: 'pointer',
          fontFamily: 'inherit', fontSize: 12, fontWeight: mode === m.id ? 700 : 500,
          color: mode === m.id ? AG_INK : AG_SUB,
          boxShadow: mode === m.id ? '0 1px 3px rgba(10,10,10,0.08)' : 'none',
          transition: 'all .15s ease',
        }}>{m.label}</button>
      ))}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// AgendaScreen — main component
// ═════════════════════════════════════════════════════════════
// ═════════════════════════════════════════════════════════════
// Bottom sheet — New RDV form (3 steps like web frontend)
// ═════════════════════════════════════════════════════════════
function NewRdvSheet({ date, onClose, onCreated, accent, runtime = null, rdvs = null,
  initialPatientId = null }) {
  const [step, setStep] = React.useState(0); // 0=date, 1=heure, 2=details
  const [selDate, setSelDate] = React.useState(date);
  const [selTime, setSelTime] = React.useState(null);
  const [selEndTime, setSelEndTime] = React.useState(null);
  // Le patient peut arriver déjà désigné, quand la création part de son dossier.
  const [patient, setPatient] = React.useState(initialPatientId || '');
  const [type, setType] = React.useState('consultation');
  const [lieu, setLieu] = React.useState('Salle 1');
  const [notes, setNotes] = React.useState('');
  const [done, setDone] = React.useState(false);
  const [patients, setPatients] = React.useState([]);
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [pendingOverlap, setPendingOverlap] = React.useState(null);

  React.useEffect(() => {
    if (!runtime?.patients) return;
    let cancelled = false;
    runtime.patients()
      .then((list) => { if (!cancelled) setPatients(list); })
      .catch(() => { if (!cancelled) setPatients([]); });
    return () => { cancelled = true; };
  }, [runtime]);

  // Generate month calendar for step 0
  const calYear = selDate.getFullYear(), calMonth = selDate.getMonth();
  const firstDay = new Date(calYear, calMonth, 1);
  const lastDay = new Date(calYear, calMonth + 1, 0);
  let startDow = firstDay.getDay() - 1; if (startDow < 0) startDow = 6;
  const calCells = [];
  for (let i = 0; i < startDow; i++) calCells.push(null);
  for (let d = 1; d <= lastDay.getDate(); d++) calCells.push(d);
  while (calCells.length % 7 !== 0) calCells.push(null);

  // Time slots for step 1
  const slots = [];
  for (let h = 8; h <= 18; h++) { slots.push(`${String(h).padStart(2,'0')}:00`); if (h < 18) slots.push(`${String(h).padStart(2,'0')}:30`); }

  // ── Détection de conflit ──
  // Un créneau neuf dure 30 min (cf. selEndTime plus bas). Deux rendez-vous se
  // chevauchent si l'un commence avant que l'autre finisse, dans les deux sens.
  // On n'interdit pas : un chirurgien surbooke sciemment (urgence intercalée).
  // On montre, et il tranche.
  // Cet aperçu ne remplace pas le contrôle serveur : il n'inspecte que la
  // fenêtre chargée. Le refus qui compte vient de l'API, sur toute la base.
  const SLOT_MIN = 30;
  const source = rdvs || AGENDA_DATA;
  const rdvsOfDay = React.useMemo(
    () => source.filter((r) => r.date === fmtDate(selDate)),
    [source, selDate]);

  const conflictsAt = React.useCallback((slot) => {
    const s = parseTime(slot), e = s + SLOT_MIN;
    return rdvsOfDay.filter((r) => s < parseTime(r.end) && e > parseTime(r.start));
  }, [rdvsOfDay]);

  const selConflicts = selTime ? conflictsAt(selTime) : [];
  const busyCount = slots.filter((s) => conflictsAt(s).length > 0).length;

  const typeOptions = [
    { id: 'consultation', label: 'Consultation', color: '#3b82f6' },
    { id: 'intervention', label: 'Intervention', color: '#b91c1c' },
    { id: 'urgence', label: 'Urgence', color: '#f97316' },
    { id: 'suivi', label: 'Suivi', color: AG_OK },
  ];
  const lieux = ['Salle 1', 'Salle 2', 'Bloc A', 'Bloc B', 'Urgences'];

  // Le rendez-vous n'était jamais envoyé : la maquette affichait un écran de
  // succès pendant une seconde et demie, puis se fermait. Rien n'existait.
  const buildDraft = () => {
    // Le créneau est choisi en heure locale du cabinet ; il part tel quel,
    // avec le décalage du fuseau de l'agenda, pas converti en UTC.
    const day = fmtDate(selDate);
    let end = selEndTime;
    if (!end || parseTime(end) <= parseTime(selTime)) {
      const total = parseTime(selTime) + SLOT_MIN;
      end = `${String(Math.floor(total / 60) % 24).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}`;
    }
    return {
      patientId: patient,
      startsAtLocal: `${day}T${selTime}`,
      endsAtLocal: `${day}T${end}`,
      type,
      reason: notes.trim() || (typeOptions.find((t) => t.id === type)?.label ?? 'Rendez-vous'),
      location: lieu || null,
    };
  };

  const sendCreate = async (draft, attempt) => {
    setBusy(true);
    setError(null);
    try {
      await runtime.create(draft, attempt);
      setPendingOverlap(null);
      setDone(true);
      // Le jour choisi remonte avec la réussite : créer un rendez-vous un autre jour
      // que celui affiché ne montrait rien: la grille rechargeait le jour courant, et
      // le seul indice était un point de plus sous une date qu'on ne regardait pas.
      setTimeout(() => { onCreated && onCreated(new Date(selDate)); }, 900);
    } catch (failure) {
      if (failure?.code === 'appointment_overlap' && failure.detail?.confirmation_token) {
        setPendingOverlap({
          draft,
          attempt: { ...attempt, confirmationToken: failure.detail.confirmation_token },
          conflicts: failure.detail.conflicts || [],
        });
        return;
      }
      setError(failure?.message || 'Création du rendez-vous impossible');
    } finally {
      setBusy(false);
    }
  };

  // Même règle que dans la fiche du rendez-vous, plus haut : un refus porte sur
  // le brouillon qui l'a provoqué — « Le début doit être dans le futur » vise
  // l'horaire envoyé. Il restait pourtant affiché jusqu'à la soumission
  // suivante, à contredire un créneau déjà corrigé ; toute retouche du
  // brouillon l'efface. Le chevauchement en attente, lui, n'est pas un refus
  // mais une question posée au praticien : il garde sa propre remise à zéro.
  const editDraft = (apply) => { setError(null); apply(); };

  const handleCreate = () => {
    if (!runtime || !patient || !selTime) return;
    let draft;
    try { draft = buildDraft(); } catch (failure) { setError(failure?.message || 'Horaires invalides'); return; }
    void sendCreate(draft, {
      idempotencyKey: window.crypto?.randomUUID?.() || `vasco-agenda-${Date.now()}`,
    });
  };

  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 50 }}>
      {/* Backdrop */}
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(10,10,10,0.3)' }} />
      {/* Sheet */}
      <div style={{
        position: 'absolute', bottom: 0, left: 0, right: 0,
        background: AG_BG, borderRadius: '24px 24px 0 0',
        maxHeight: '85%', display: 'flex', flexDirection: 'column',
        animation: 'sheetUp .3s cubic-bezier(.2,.7,.2,1)',
        boxShadow: VT.shadowSheet,
      }}>
        <style>{`@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }`}</style>

        {/* Drag handle */}
        <div style={{ display: 'flex', justifyContent: 'center', padding: '10px 0 6px' }}>
          <div style={{ width: 36, height: 4, borderRadius: 2, background: 'rgba(10,10,10,0.12)' }} />
        </div>

        {/* Header */}
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 20px 12px' }}>
          <div style={{ fontSize: 18, fontWeight: 800, color: AG_INK, letterSpacing: '-0.3px' }}>Nouveau RDV</div>
          <button onClick={onClose} className="press" style={{
            width: 30, height: 30, borderRadius: 12, background: 'rgba(10,10,10,0.04)',
            border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
              <path d="M18 6L6 18M6 6l12 12"/>
            </svg>
          </button>
        </div>

        {/* Step indicator */}
        <div style={{ display: 'flex', gap: 6, padding: '0 20px 16px' }}>
          {[0,1,2].map(i => (
            <div key={i} style={{ flex: 1, height: 3, borderRadius: 2, background: i <= step ? AG_INK : 'rgba(10,10,10,0.08)' }} />
          ))}
        </div>

        {/* Content */}
        <div className="vasco-scroll" style={{ flex: 1, overflowY: 'auto', padding: '0 20px 24px' }}>
          {done ? (
            <div style={{ textAlign: 'center', padding: '40px 0' }}>
              <div style={{
                width: 56, height: 56, borderRadius: '50%', background: AG_OK_TINT,
                display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px',
              }}>
                <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke={AG_OK} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M20 6L9 17l-5-5"/>
                </svg>
              </div>
              <div style={{ fontSize: 16, fontWeight: 700, color: AG_INK }}>RDV créé</div>
              <div style={{ fontSize: 13, fontWeight: 500, color: AG_SUB, marginTop: 4 }}>
                {patient || 'Patient'} · {fmtDate(selDate)} à {selTime}
              </div>
            </div>
          ) : step === 0 ? (
            /* Step 0: Date */
            <div>
              <div style={{ fontSize: 14, fontWeight: 700, color: AG_INK, marginBottom: 12 }}>Choisir la date</div>
              {/* Month nav */}
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
                <button onClick={() => editDraft(() => setSelDate(new Date(calYear, calMonth - 1, 1)))} className="press" style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}>
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7"><path d="M15 6l-6 6 6 6"/></svg>
                </button>
                <span style={{ fontSize: 14, fontWeight: 600, color: AG_INK }}>{MOIS[calMonth]} {calYear}</span>
                <button onClick={() => editDraft(() => setSelDate(new Date(calYear, calMonth + 1, 1)))} className="press" style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}>
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7"><path d="M9 6l6 6-6 6"/></svg>
                </button>
              </div>
              {/* Day headers */}
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', marginBottom: 4 }}>
                {['L','M','M','J','V','S','D'].map((d,i) => (
                  <div key={i} className="mono" style={{ textAlign: 'center', fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.3px' }}>{d}</div>
                ))}
              </div>
              {/* Calendar grid */}
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 2 }}>
                {calCells.map((day, i) => {
                  if (!day) return <div key={i} />;
                  const d = new Date(calYear, calMonth, day);
                  const isSel = isSameDay(d, selDate);
                  const isT = isSameDay(d, TODAY);
                  // On ne planifie pas dans le passé : les jours révolus sont
                  // grisés et non cliquables (aujourd'hui reste ouvert).
                  const isPast = d < TODAY && !isT;
                  return (
                    <button key={i} onClick={() => { if (!isPast) editDraft(() => setSelDate(d)); }}
                      disabled={isPast}
                      className={isPast ? undefined : 'press'}
                      aria-disabled={isPast ? 'true' : undefined}
                      style={{
                        background: 'none', border: 'none', fontFamily: 'inherit',
                        cursor: isPast ? 'default' : 'pointer',
                        display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '8px 0',
                      }}>
                      <div style={{
                        width: 34, height: 34, borderRadius: '50%',
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                        background: isSel ? AG_INK : isT ? `${accent}14` : 'transparent',
                      }}>
                        <span style={{
                          fontSize: 14,
                          fontWeight: isSel || isT ? 700 : 400,
                          color: isSel ? '#fff' : isT ? accent : isPast ? AG_SUB : AG_INK,
                          opacity: isPast ? 0.4 : 1,
                        }}>{day}</span>
                      </div>
                    </button>
                  );
                })}
              </div>
            </div>
          ) : step === 1 ? (
            /* Step 1: Time */
            <div>
              <div style={{ fontSize: 14, fontWeight: 700, color: AG_INK, marginBottom: 12 }}>Choisir l'horaire</div>
              <div style={{
                display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
                gap: 10, marginBottom: 12,
              }}>
                <span className="mono" style={{ fontSize: 11, fontWeight: 500, color: AG_SUB }}>
                  {JOURS_FULL[selDate.getDay()]} {selDate.getDate()} {MOIS[selDate.getMonth()].toLowerCase()}
                </span>
                {busyCount > 0 && (
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, flexShrink: 0 }}>
                    <span style={{ width: 6, height: 6, borderRadius: '50%', background: AG_WARN }} />
                    <span style={{ fontSize: 11, fontWeight: 600, color: AG_SUB }}>
                      {busyCount} occupé{busyCount > 1 ? 's' : ''}
                    </span>
                  </span>
                )}
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 6 }}>
                {slots.map(s => {
                  const isSel = s === selTime;
                  const busy = conflictsAt(s).length > 0;
                  return (
                    <button key={s} onClick={() => editDraft(() => {
                      setSelTime(s);
                      const [h,m] = s.split(':').map(Number);
                      setSelEndTime(`${String(h + (m === 30 ? 1 : 0)).padStart(2,'0')}:${m === 30 ? '00' : '30'}`);
                    })} className="press" style={{
                      position: 'relative',
                      padding: '10px 0', borderRadius: 12,
                      background: isSel ? AG_INK : (busy ? 'rgba(217,119,6,0.06)' : AG_SURF),
                      color: isSel ? '#fff' : (busy ? AG_SUB : AG_INK),
                      border: `1px solid ${isSel ? AG_INK : (busy ? 'rgba(217,119,6,0.30)' : AG_HAIR)}`,
                      fontFamily: "'JetBrains Mono', monospace", fontSize: 13, fontWeight: 600,
                      cursor: 'pointer', letterSpacing: '0.2px',
                    }}>
                      {s}
                      {/* Pastille d'occupation — conservée même sélectionné,
                          sinon on perd l'alerte au moment où elle compte. */}
                      {busy && (
                        <span style={{
                          position: 'absolute', top: 5, right: 6,
                          width: 5, height: 5, borderRadius: '50%',
                          background: isSel ? '#fff' : AG_WARN,
                        }} />
                      )}
                    </button>
                  );
                })}
              </div>

              {/* Détail du conflit sur le créneau retenu */}
              {selConflicts.length > 0 && (
                <div style={{
                  marginTop: 14, padding: '12px 14px', borderRadius: 16,
                  background: 'rgba(217,119,6,0.07)',
                  border: '1px solid rgba(217,119,6,0.25)',
                }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={AG_WARN}
                      strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
                      <path d="M12 4l10 17H2z" /><path d="M12 10v4" />
                      <circle cx="12" cy="17.4" r="0.7" fill={AG_WARN} stroke="none" />
                    </svg>
                    <span style={{ fontSize: 13, fontWeight: 700, color: AG_INK }}>
                      Créneau déjà occupé
                    </span>
                  </div>
                  {selConflicts.map((r) => (
                    <div key={r.id} style={{ marginTop: 8, paddingLeft: 23 }}>
                      <div style={{ fontSize: 12.5, fontWeight: 600, color: AG_INK }}>{r.patient}</div>
                      <div className="mono" style={{ fontSize: 11, fontWeight: 500, color: AG_SUB, marginTop: 2 }}>
                        {r.start}–{r.end} · {r.lieu} · {(TYPE_STYLE[r.type] || {}).label || r.type}
                      </div>
                    </div>
                  ))}
                  <div style={{ fontSize: 11.5, fontWeight: 500, color: AG_SUB, marginTop: 10, paddingLeft: 23, lineHeight: 1.4 }}>
                    Choisissez un autre horaire, ou continuez pour programmer en parallèle.
                  </div>
                </div>
              )}
            </div>
          ) : (
            /* Step 2: Details */
            <div>
              <div style={{ fontSize: 14, fontWeight: 700, color: AG_INK, marginBottom: 12 }}>Détails</div>
              {/* Patient */}
              <div style={{ marginBottom: 12 }}>
                <label className="mono" style={{ display: 'block', fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase', marginBottom: 6 }}>Patient *</label>
                {/* Un rendez-vous se rattache à une clé patient. Le champ libre
                    de la maquette ne pouvait désigner personne. */}
                <select value={patient} onChange={e => editDraft(() => setPatient(e.target.value))} aria-label="Patient"
                  style={{ width: '100%', height: 42, borderRadius: 16, border: `1px solid ${AG_HAIR2}`, padding: '0 14px', fontFamily: 'inherit', fontSize: 14, color: AG_INK, background: AG_SURF, outline: 'none' }}>
                  <option value="">Choisir un patient…</option>
                  {/* Un patient décédé est refusé par le serveur : le proposer
                      sans le dire ferait perdre trois écrans de saisie. */}
                  {patients.map((p) => (
                    <option key={p.id} value={p.id} disabled={p.status === 'decede'}>
                      {p.last} {p.first}{p.status === 'decede' ? ' · décédé' : ''}
                    </option>
                  ))}
                </select>
              </div>
              {/* Type */}
              <div style={{ marginBottom: 12 }}>
                <label className="mono" style={{ display: 'block', fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase', marginBottom: 6 }}>Type</label>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 6 }}>
                  {typeOptions.map(t => {
                    const isSel = t.id === type;
                    return (
                      <button key={t.id} onClick={() => editDraft(() => setType(t.id))} className="press" style={{
                        display: 'flex', alignItems: 'center', gap: 8, padding: '10px 12px',
                        borderRadius: 16, border: `1px solid ${isSel ? t.color : AG_HAIR}`,
                        background: isSel ? `${t.color}10` : AG_SURF,
                        cursor: 'pointer', fontFamily: 'inherit',
                      }}>
                        <div style={{ width: 8, height: 8, borderRadius: '50%', background: t.color }} />
                        <span style={{ fontSize: 12, fontWeight: 600, color: isSel ? t.color : AG_INK }}>{t.label}</span>
                      </button>
                    );
                  })}
                </div>
              </div>
              {/* Lieu */}
              <div style={{ marginBottom: 12 }}>
                <label className="mono" style={{ display: 'block', fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase', marginBottom: 6 }}>Lieu</label>
                <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                  {lieux.map(l => (
                    <button key={l} onClick={() => editDraft(() => setLieu(l))} className="press" style={{
                      padding: '7px 14px', borderRadius: 999,
                      background: l === lieu ? AG_INK : AG_SURF,
                      color: l === lieu ? '#fff' : AG_INK,
                      border: `1px solid ${l === lieu ? AG_INK : AG_HAIR}`,
                      fontSize: 12, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer',
                    }}>{l}</button>
                  ))}
                </div>
              </div>
              {/* Motif — c'est ce que le serveur stocke et ce que la fiche du
                  rendez-vous affiche ; le champ s'appelait « Notes ». */}
              <div style={{ marginBottom: 12 }}>
                <label className="mono" style={{ display: 'block', fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase', marginBottom: 6 }}>Motif</label>
                <textarea value={notes} onChange={e => editDraft(() => setNotes(e.target.value))} maxLength={160} placeholder="Motif de la consultation…" rows={3}
                  style={{ width: '100%', borderRadius: 16, border: `1px solid ${AG_HAIR2}`, padding: '10px 14px', fontFamily: 'inherit', fontSize: 14, color: AG_INK, background: AG_SURF, outline: 'none', resize: 'none' }} />
              </div>
              {/* Recap */}
              <div style={{ background: 'rgba(10,10,10,0.03)', borderRadius: 16, padding: '12px 14px', marginBottom: 8 }}>
                <div className="mono" style={{ fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', marginBottom: 8 }}>RÉCAPITULATIF</div>
                {[
                  ['Date', `${JOURS_FULL[selDate.getDay()]} ${selDate.getDate()} ${MOIS[selDate.getMonth()].toLowerCase()}`],
                  ['Horaire', `${selTime} — ${selEndTime}`],
                  ['Type', typeOptions.find(t => t.id === type)?.label],
                  ['Lieu', lieu],
                ].map(([k,v]) => (
                  <div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0' }}>
                    <span className="mono" style={{ fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.3px' }}>{k}</span>
                    <span style={{ fontSize: 12, fontWeight: 500, color: AG_INK }}>{v}</span>
                  </div>
                ))}
                {/* Le conflit remonte jusqu'ici : on ne valide pas un
                    chevauchement en l'ayant oublié deux écrans plus tôt. */}
                {selConflicts.length > 0 && (
                  <div style={{
                    display: 'flex', alignItems: 'center', gap: 7,
                    marginTop: 8, paddingTop: 8, borderTop: `1px solid ${AG_HAIR}`,
                  }}>
                    <span style={{ width: 6, height: 6, borderRadius: '50%', background: AG_WARN, flexShrink: 0 }} />
                    <span style={{ fontSize: 11.5, fontWeight: 600, color: AG_WARN }}>
                      En parallèle de {selConflicts[0].patient}
                      {selConflicts.length > 1 ? ` +${selConflicts.length - 1}` : ''}
                    </span>
                  </div>
                )}
              </div>
            </div>
          )}
        </div>

        {/* Refus de créneau renvoyé par le serveur */}
        {(error || pendingOverlap) && !done && (
          <div style={{ padding: '0 20px 8px' }}>
            {error && (
              <div role="alert" style={{
                padding: '9px 12px', borderRadius: 12,
                background: 'rgba(185,28,28,0.06)', fontSize: 12, fontWeight: 600, color: '#b91c1c',
              }}>{error}</div>
            )}
            {pendingOverlap && (
              <div role="alert" style={{
                padding: '12px 14px', borderRadius: 16,
                background: 'rgba(217,119,6,0.06)', border: `1px solid rgba(217,119,6,0.16)`,
              }}>
                <div style={{ fontSize: 13, fontWeight: 700, color: AG_INK }}>Créneau déjà occupé</div>
                <div style={{ fontSize: 12, fontWeight: 500, color: AG_SUB, marginTop: 4, lineHeight: 1.4 }}>
                  {pendingOverlap.conflicts.length} rendez-vous se chevauche(nt) avec cet horaire.
                </div>
                <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
                  <button onClick={() => { setPendingOverlap(null); setStep(1); }} disabled={busy} className="press" style={{
                    flex: 1, height: 38, borderRadius: 12, background: AG_SURF,
                    border: `1px solid ${AG_HAIR2}`, fontFamily: 'inherit',
                    fontSize: 12.5, fontWeight: 700, color: AG_INK, cursor: 'pointer',
                  }}>Autre horaire</button>
                  <button onClick={() => void sendCreate(pendingOverlap.draft, pendingOverlap.attempt)}
                    disabled={busy} className="press" style={{
                      flex: 1, height: 38, borderRadius: 12, background: AG_WARN,
                      border: 'none', fontFamily: 'inherit',
                      fontSize: 12.5, fontWeight: 700, color: '#fff', cursor: 'pointer',
                    }}>Programmer quand même</button>
                </div>
              </div>
            )}
          </div>
        )}

        {/* Footer buttons */}
        {!done && (
          <div style={{ padding: '12px 20px 24px', display: 'flex', gap: 8 }}>
            {step > 0 && (
              <button onClick={() => setStep(step - 1)} className="press" style={{
                height: VT.hLg, padding: '0 18px', borderRadius: VT.rLg,
                background: 'transparent', border: `1px solid ${AG_HAIR2}`,
                fontFamily: 'inherit', fontSize: 13, fontWeight: 700, color: AG_INK, cursor: 'pointer',
              }}>Précédent</button>
            )}
            <button disabled={busy} onClick={() => {
              if (step < 2) {
                if (step === 1 && !selTime) return;
                setStep(step + 1);
              } else {
                if (!patient) return;
                handleCreate();
              }
            }} className="press" style={{
              flex: 1, height: VT.hLg, borderRadius: VT.rLg,
              background: AG_INK, color: '#fff', border: 'none',
              fontFamily: 'inherit', fontSize: 13.5, fontWeight: 700,
              cursor: busy ? 'default' : 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
              opacity: busy || (step === 1 && !selTime) || (step === 2 && !patient) ? 0.4 : 1,
            }}>
              {step < 2 ? 'Suivant' : busy ? 'Envoi…' : 'Créer le RDV'}
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                {step < 2 ? <path d="M9 6l6 6-6 6"/> : <path d="M20 6L9 17l-5-5"/>}
              </svg>
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

function AgendaScreen({ accent, onTabChange, runtime = null,
  newAppointmentFor = null, onNewAppointmentHandled = null }) {
  const runtimeToday = React.useMemo(() => {
    const value = window.VASCO_RUNTIME?.dashboard?.local_date;
    return value ? new Date(`${value}T12:00:00`) : new Date(TODAY);
  }, []);
  const [date, setDate] = React.useState(new Date(runtimeToday));
  const [viewMode, setViewMode] = React.useState('week'); // week | month
  const [showNewRdv, setShowNewRdv] = React.useState(false);
  // Arrivée depuis un dossier patient : l'agenda ouvre sa feuille de création
  // avec ce patient déjà désigné. Le patient demandé est gardé en état pour ne
  // pas rouvrir la feuille si le praticien la referme sans créer.
  const [newFor, setNewFor] = React.useState(null);
  const clearNewFor = React.useCallback(() => {
    setNewFor(null);
    if (onNewAppointmentHandled) onNewAppointmentHandled();
  }, [onNewAppointmentHandled]);
  React.useEffect(() => {
    if (!newAppointmentFor) return;
    setNewFor(newAppointmentFor);
    setShowNewRdv(true);
  }, [newAppointmentFor]);
  // Le tableau `AGENDA_DATA` est réécrit sur place par le client d'API ; le
  // garder en état permet de le rafraîchir après une mutation sans remonter
  // l'écran entier.
  const [rdvs, setRdvs] = React.useState(AGENDA_DATA);
  const [capabilities, setCapabilities] = React.useState(null);
  const [error, setError] = React.useState(null);

  // Le rendez-vous ouvert est tenu ici, et non dans la liste du jour : sa
  // feuille se pose au niveau de l'écran, comme celle de création, pour être
  // bornée par l'écran et non par la hauteur de la liste.
  const [selectedRdv, setSelectedRdv] = React.useState(null);
  const dayKey = fmtDate(date);
  // La journée affichée peut changer sans que la liste soit démontée : semaine
  // suivante, mois suivant, autre jour choisi dans la bande ou dans la grille,
  // retour à aujourd'hui. La feuille de détail restait alors ouverte sur un
  // rendez-vous absent de la journée affichée — un panneau orphelin, posé sur
  // une grille qui ne le contient plus. Elle se referme donc avec la journée
  // qui l'avait ouverte, comme les feuilles du dossier patient se referment
  // avec le dossier. Le passage semaine ↔ mois la referme aussi : il démontait
  // la liste, donc la feuille, du temps où elle y vivait.
  React.useEffect(() => { setSelectedRdv(null); }, [dayKey, viewMode]);

  React.useEffect(() => {
    if (!runtime) return;
    let cancelled = false;
    runtime.capabilities()
      .then((view) => { if (!cancelled) setCapabilities(view.capabilities); })
      .catch((failure) => { if (!cancelled) setError(failure?.message || 'Capacités de l’agenda indisponibles'); });
    return () => { cancelled = true; };
  }, [runtime]);

  const reload = React.useCallback(async () => {
    if (!runtime) return;
    try {
      setRdvs([...(await runtime.reload())]);
      setError(null);
    } catch (failure) {
      setError(failure?.message || 'Rechargement de l’agenda impossible');
    }
  }, [runtime]);

  // Navigation
  const goPrev = () => {
    if (viewMode === 'week') setDate(d => addDays(d, -7));
    else setDate(d => new Date(d.getFullYear(), d.getMonth() - 1, 1));
  };
  const goNext = () => {
    if (viewMode === 'week') setDate(d => addDays(d, 7));
    else setDate(d => new Date(d.getFullYear(), d.getMonth() + 1, 1));
  };
  const goToday = () => setDate(new Date(runtimeToday));

  // Date label
  const label = (() => {
    if (viewMode === 'week') {
      const ws = startOfWeek(date);
      const we = addDays(ws, 6);
      if (ws.getMonth() === we.getMonth()) {
        return `${ws.getDate()} – ${we.getDate()} ${MOIS_SHORT[ws.getMonth()]}`;
      }
      return `${ws.getDate()} ${MOIS_SHORT[ws.getMonth()]} – ${we.getDate()} ${MOIS_SHORT[we.getMonth()]}`;
    }
    return `${MOIS[date.getMonth()]} ${date.getFullYear()}`;
  })();

  const isToday = isSameDay(date, runtimeToday);
  const todayRdvCount = rdvs.filter(r => r.date === fmtDate(date)).length;
  const canCreate = !capabilities || capabilities.create.enabled;
  // Le « + » est une icône seule, et il est désactivé quand la création est
  // refusée : son `onClick` ne peut donc jamais poser la raison à l'écran, qui
  // ne vivait que dans son `title`. Elle se lit maintenant sous le bandeau, à
  // la place et à la mesure du refus de l'agenda juste en dessous.
  const createReason = canCreate ? null : agReason(capabilities?.create);

  // Le balayage ne se devine pas : on le montre une fois, à la première
  // ouverture de l'agenda, puis plus jamais. Mémorisé comme l'onboarding.
  const [showHint, setShowHint] = React.useState(() => {
    try { return !window.localStorage.getItem(AG_HINT_KEY); } catch (e) { return false; }
  });
  const dismissHint = React.useCallback(() => {
    setShowHint(false);
    try { window.localStorage.setItem(AG_HINT_KEY, '1'); } catch (e) { /* sans mémoire */ }
  }, []);
  React.useEffect(() => {
    if (!showHint) return;
    const t = setTimeout(dismissHint, 4500);
    return () => clearTimeout(t);
  }, [showHint, dismissHint]);

  // Swipe to navigate
  const startX = React.useRef(null);
  const onTouchStart = (e) => { startX.current = e.touches[0].clientX; };
  const onTouchEnd = (e) => {
    if (startX.current == null) return;
    const dx = e.changedTouches[0].clientX - startX.current;
    startX.current = null;
    // Le premier balayage vaut démonstration : le hint n'a plus lieu d'être.
    if (Math.abs(dx) > 80) { dismissHint(); dx < 0 ? goNext() : goPrev(); }
  };

  return (
    <div style={{
      width: '100%', height: '100%', background: AG_BG, color: AG_INK,
      display: 'flex', flexDirection: 'column',
      fontFamily: "'Inter Tight', -apple-system, system-ui, sans-serif",
    }}>
      {/* Header */}
      <div style={{ paddingTop: 62, padding: '62px 16px 8px' }}>
        {/* Title row */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          marginBottom: 12,
        }}>
          <div>
            <div style={{ fontSize: 38, fontWeight: 700, color: AG_INK, letterSpacing: '-1.2px', lineHeight: 1 }}>
              Agenda
            </div>
            <div className="mono" style={{ fontSize: 11, fontWeight: 500, color: AG_SUB, marginTop: 4, letterSpacing: '0.2px' }}>
              {todayRdvCount} rendez-vous
            </div>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            {!isToday && (
              <button onClick={goToday} className="press" style={{
                padding: '6px 14px', borderRadius: 999,
                background: AG_SURF, border: `1px solid ${AG_HAIR2}`,
                fontFamily: 'inherit', fontSize: 12, fontWeight: 600, color: AG_INK,
                cursor: 'pointer',
              }}>Aujourd'hui</button>
            )}
            <button onClick={() => { if (canCreate) setShowNewRdv(true); else setError(agReason(capabilities?.create)); }}
              disabled={!canCreate}
              aria-label={canCreate ? 'Nouveau rendez-vous' : `Nouveau rendez-vous — ${createReason}`}
              title={canCreate ? 'Nouveau rendez-vous' : `Nouveau rendez-vous — ${createReason}`}
              className="press" style={{
                width: 44, height: 44, borderRadius: 16,
                background: AG_SURF, border: `1px solid ${AG_HAIR}`,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                cursor: canCreate ? 'pointer' : 'default', padding: 0,
                opacity: canCreate ? 1 : 0.45,
              }}>
              <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <path d="M12 5v14M5 12h14"/>
              </svg>
            </button>
          </div>
        </div>

        {/* View toggle */}
        <ViewToggleAG mode={viewMode} onChange={setViewMode} />

        {/* Date nav */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '12px 4px 4px',
        }}>
          <button onClick={goPrev} className="press" style={{
            width: 34, height: 34, borderRadius: '50%',
            background: AG_SURF, border: `1px solid ${AG_HAIR}`,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', padding: 0,
          }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
              <path d="M15 6l-6 6 6 6" />
            </svg>
          </button>
          <div style={{
            fontSize: 15, fontWeight: 600, color: AG_INK, letterSpacing: '-0.2px',
            textTransform: 'capitalize',
          }}>{label}</div>
          <button onClick={goNext} className="press" style={{
            width: 34, height: 34, borderRadius: '50%',
            background: AG_SURF, border: `1px solid ${AG_HAIR}`,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', padding: 0,
          }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={AG_INK} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
              <path d="M9 6l6 6-6 6" />
            </svg>
          </button>
        </div>
      </div>

      {createReason && (
        <div style={{
          margin: '0 16px 8px', fontSize: 12, fontWeight: 600, color: AG_SUB, lineHeight: 1.4,
        }}>
          Nouveau rendez-vous — {createReason}
        </div>
      )}

      {error && (
        <div role="alert" style={{
          margin: '0 16px 8px', padding: '8px 12px', borderRadius: 12,
          background: 'rgba(185,28,28,0.06)', fontSize: 12, fontWeight: 600, color: '#b91c1c',
        }}>{error}</div>
      )}

      {/* Content */}
      <div className="vasco-scroll" style={{ flex: 1, overflowY: 'auto' }}
        onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
        {viewMode === 'week' && <WeekView date={date} setDate={setDate} rdvs={rdvs} accent={accent}
          onSelectRdv={setSelectedRdv} />}
        {viewMode === 'month' && <MonthView date={date} setDate={setDate} rdvs={rdvs} accent={accent}
          onSelectRdv={setSelectedRdv} />}
      </div>

      {/* Hint de balayage — première ouverture seulement */}
      {showHint && (
        <div onClick={dismissHint} data-vasco="swipe-hint" style={{
          position: 'absolute', left: 0, right: 0, bottom: 96,
          display: 'flex', justifyContent: 'center',
          zIndex: 30, pointerEvents: 'auto',
        }}>
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: 9,
            padding: '9px 15px', borderRadius: 999,
            background: AG_INK, boxShadow: AG_VT.shadowLg,
            animation: 'agHintIn .32s cubic-bezier(.16,1,.3,1)',
          }}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff"
              strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"
              style={{ animation: 'agHintSwipe 1.6s ease-in-out infinite' }}>
              <path d="M15 6l-6 6 6 6" />
            </svg>
            <span style={{ fontSize: 12.5, fontWeight: 600, color: '#fff', whiteSpace: 'nowrap' }}>
              Balayez pour changer de semaine
            </span>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff"
              strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"
              style={{ animation: 'agHintSwipe 1.6s ease-in-out .8s infinite' }}>
              <path d="M9 6l6 6-6 6" />
            </svg>
          </div>
          <style>{`
            @keyframes agHintIn { from { opacity: 0; transform: translateY(10px); } }
            @keyframes agHintSwipe {
              0%, 100% { transform: translateX(0);    opacity: .55; }
              50%      { transform: translateX(-3px); opacity: 1;   }
            }
          `}</style>
        </div>
      )}

      {/* Bottom nav */}
      {typeof BottomNav !== 'undefined' && (
        <BottomNav active="AGE" onChange={(id) => { if (onTabChange) onTabChange(id); }} accent={accent} />
      )}

      {/* RDV detail bottom sheet — posée au niveau de l'écran, comme celle de
          création juste en dessous. */}
      {selectedRdv && (
        <RdvDetailSheet rdv={selectedRdv} accent={accent} runtime={runtime}
          capabilities={capabilities} onChanged={reload}
          onClose={() => setSelectedRdv(null)} />
      )}

      {/* New RDV bottom sheet */}
      {showNewRdv && (
        <NewRdvSheet date={date} accent={accent} runtime={runtime} rdvs={rdvs}
          initialPatientId={newFor}
          onClose={() => { setShowNewRdv(false); clearNewFor(); }}
          onCreated={(createdOn) => {
            setShowNewRdv(false);
            clearNewFor();
            if (createdOn) setDate(createdOn);
            void reload();
          }} />
      )}
    </div>
  );
}

window.AgendaScreen = AgendaScreen;
window.VascoAgendaFixtures = { AGENDA_DATA };
