<?php
/* ============================================================
   HERBIER — application + API de données, en un seul fichier.
   Les données sont stockées dans data.json, à côté de ce fichier.

   >>> CHANGEZ LE MOT DE PASSE CI-DESSOUS AVANT DE METTRE EN LIGNE <<<
   ============================================================ */
session_start();

$PASSWORD  = 'change-me';
$DATA_FILE = __DIR__ . '/data.json';

/* ---------- Connexion / déconnexion ---------- */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
    if (hash_equals($PASSWORD, (string) $_POST['password'])) {
        $_SESSION['herbier_auth'] = true;
    } else {
        $_SESSION['herbier_error'] = true;
    }
    header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
    exit;
}
if (isset($_GET['logout'])) {
    session_destroy();
    header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
    exit;
}
$isAuth = !empty($_SESSION['herbier_auth']);

/* ---------- API : lecture / écriture des données ---------- */
if (isset($_GET['api']) && $_GET['api'] === 'data') {
    header('Content-Type: application/json; charset=utf-8');

    if (!$isAuth) {
        http_response_code(401);
        echo json_encode(['error' => 'unauthorized']);
        exit;
    }

    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
        if (file_exists($DATA_FILE)) {
            echo file_get_contents($DATA_FILE);
        } else {
            echo json_encode(['families' => []]);
        }
        exit;
    }

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $raw     = file_get_contents('php://input');
        $decoded = json_decode($raw, true);
        if (!is_array($decoded) || !isset($decoded['families']) || !is_array($decoded['families'])) {
            http_response_code(400);
            echo json_encode(['error' => 'invalid payload']);
            exit;
        }
        $ok = @file_put_contents($DATA_FILE, json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        if ($ok === false) {
            http_response_code(500);
            echo json_encode(['error' => "écriture impossible : vérifiez les droits d'écriture du dossier"]);
            exit;
        }
        echo json_encode(['ok' => true]);
        exit;
    }

    http_response_code(405);
    echo json_encode(['error' => 'method not allowed']);
    exit;
}

/* ---------- Page de connexion ---------- */
if (!$isAuth) {
    $loginError = !empty($_SESSION['herbier_error']);
    unset($_SESSION['herbier_error']);
    ?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Herbier — connexion</title>
<style>
  :root{ --paper:#F1ECDE; --pine:#2F4B3C; --pine-dark:#1F3329; --ink:#1E2A20; --ink-soft:#4B5A4E; --line:rgba(30,42,32,.18); --card-bg:#FBF8EE; --danger:#A1442E; --serif:"Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,Georgia,serif; --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
  *{box-sizing:border-box;}
  body{margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; background:var(--paper); font-family:var(--sans); color:var(--ink); padding:20px;}
  .card{background:var(--card-bg); border:1px solid var(--line); border-radius:16px; padding:28px 24px; max-width:340px; width:100%; box-shadow:0 4px 16px rgba(30,42,32,.14);}
  .brand{font-family:var(--serif); font-size:22px; color:var(--pine-dark); text-align:center; margin-bottom:4px;}
  .sub{text-align:center; color:var(--ink-soft); font-size:13px; margin:0 0 20px;}
  input{width:100%; border:1px solid var(--line); background:#fff; border-radius:10px; padding:12px; font-size:15px; margin-bottom:12px; font-family:var(--sans);}
  button{width:100%; background:var(--pine); color:var(--paper); border:none; border-radius:10px; padding:12px; font-size:15px; font-weight:600; cursor:pointer;}
  button:active{background:var(--pine-dark);}
  .error{color:var(--danger); font-size:13px; text-align:center; margin:-6px 0 14px;}
</style>
</head>
<body>
  <form class="card" method="post">
    <div class="brand">🌱 Herbier</div>
    <p class="sub">Répertoire botanique privé</p>
    <?php if ($loginError): ?><p class="error">Mot de passe incorrect.</p><?php endif; ?>
    <input type="password" name="password" placeholder="Mot de passe" autofocus required>
    <button type="submit">Entrer</button>
  </form>
</body>
</html>
    <?php
    exit;
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
<title>Herbier — répertoire botanique &amp; fungi</title>
<style>
  :root{
    --paper:#F1ECDE;
    --paper-2:#E7DFC8;
    --ink:#1E2A20;
    --ink-soft:#4B5A4E;
    --pine:#2F4B3C;
    --pine-dark:#1F3329;
    --sage:#7C9473;
    --rust:#8B5A3C;
    --gold:#A9822F;
    --pastel-red:#D98E86;
    --line:rgba(30,42,32,.18);
    --card-bg:#FBF8EE;
    --danger:#A1442E;
    --radius:14px;
    --shadow:0 2px 10px rgba(30,42,32,.14);
    --serif: "Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,Georgia,serif;
    --sans: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  }
  *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
  html,body{height:100%;}
  body{
    margin:0; background:var(--paper);
    background-image:
      radial-gradient(circle at 20% 10%, rgba(124,148,115,.10), transparent 40%),
      radial-gradient(circle at 90% 80%, rgba(139,90,60,.08), transparent 45%);
    color:var(--ink); font-family:var(--sans);
    -webkit-font-smoothing:antialiased;
  }
  #app{max-width:640px; margin:0 auto; min-height:100vh; display:flex; flex-direction:column; position:relative;}

  header.topbar{
    position:sticky; top:0; z-index:20;
    background:var(--paper); border-bottom:1px solid var(--line);
    padding:env(safe-area-inset-top) 14px 0 14px;
  }
  .topbar-row{display:flex; align-items:center; gap:10px; padding:10px 0 8px;}
  .main-nav{display:flex; align-items:center; gap:2px; overflow-x:auto;}
  .nav-tab{
    display:flex; align-items:center; gap:4px; background:none; border:none; cursor:pointer;
    font-family:var(--serif); font-size:15px; color:var(--ink-soft); font-weight:600; letter-spacing:.1px;
    padding:4px 6px 4px 0; opacity:.55; white-space:nowrap; flex:none;
  }
  .nav-tab svg{width:17px; height:17px; flex:none;}
  .nav-tab.active{color:var(--pine-dark); opacity:1;}
  .brand{display:flex; align-items:center; gap:8px; font-family:var(--serif); font-size:19px; color:var(--pine-dark); font-weight:600; letter-spacing:.2px;}
  .brand svg{width:22px; height:22px; flex:none;}
  .sync-status{font-size:11px; color:var(--ink-soft); margin-left:auto; white-space:nowrap;}
  .sync-status.saving{color:var(--gold);}
  .sync-status.error{color:var(--danger); font-weight:600;}
  .menu-btn{background:none; border:none; color:var(--pine-dark); font-size:20px; padding:6px 4px; cursor:pointer; line-height:1;}
  .breadcrumb{display:flex; flex-wrap:wrap; align-items:center; gap:2px; font-size:12.5px; color:var(--ink-soft); padding-bottom:8px; min-height:18px;}
  .breadcrumb button{background:none;border:none;color:var(--ink-soft);font:inherit;padding:2px 3px;cursor:pointer;text-decoration:underline; text-decoration-color:var(--line);}
  .breadcrumb button.current{color:var(--pine-dark); font-weight:600; text-decoration:none;}
  .breadcrumb .sep{color:var(--line); padding:0 1px;}
  .search-wrap{padding:0 0 12px;}
  .search-field{position:relative;}
  .search-wrap input{
    width:100%; border:1px solid var(--line); background:var(--card-bg);
    border-radius:10px; padding:10px 36px 10px 12px; font-size:15px; font-family:var(--sans);
    color:var(--ink); outline:none;
  }
  .search-wrap input:focus{border-color:var(--sage);}
  .search-clear{
    position:absolute; right:5px; top:50%; transform:translateY(-50%);
    width:26px; height:26px; display:flex; align-items:center; justify-content:center;
    border:none; background:none; color:var(--ink-soft); font-size:15px; line-height:1;
    cursor:pointer; border-radius:50%;
  }
  .search-clear:active{background:rgba(30,42,32,.1);}
  .search-clear.hidden{display:none;}

  main#content{flex:1; padding:14px 14px 100px;}
  .eyebrow{
    text-transform:uppercase; letter-spacing:1.4px; font-size:11px; color:var(--sage);
    font-weight:700; margin:2px 0 10px;
  }
  .eyebrow-row{display:flex; align-items:center; justify-content:space-between; gap:10px; margin:2px 0 10px;}
  .eyebrow-row .eyebrow{margin:0;}
  .gallery-link{
    text-transform:uppercase; letter-spacing:1.4px; font-size:11px; color:var(--sage);
    font-weight:700; text-decoration:none; white-space:nowrap; flex:none;
    display:inline-flex; align-items:center; gap:4px; border:none; background:none; padding:0; cursor:pointer;
  }
  .type-checkboxes{display:flex; flex-wrap:wrap; gap:8px; margin-top:4px;}
  .type-checkbox-label{
    display:inline-flex; align-items:center; gap:5px; font-family:var(--sans); font-size:13px;
    color:var(--ink); border:1px solid var(--line); border-radius:20px; padding:6px 12px; cursor:pointer;
  }
  .type-checkbox-label input{margin:0; cursor:pointer;}
  .gallery-title{
    text-transform:uppercase; letter-spacing:1.4px; color:var(--sage); font-weight:700;
    font-size:20px; margin:2px 0 14px;
  }
  .gallery-link:active{opacity:.7;}
  .gallery-grid{display:flex; flex-wrap:wrap; gap:14px;}
  .gallery-item:empty{display:none;}
  h1.title{font-family:var(--serif); font-size:24px; margin:0 0 4px; color:var(--pine-dark);}
  p.sub{color:var(--ink-soft); font-size:13.5px; margin:0 0 16px;}

  .list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:9px;}
  .row{
    display:flex; align-items:center; gap:12px; background:var(--card-bg);
    border:1px solid var(--line); border-left:4px solid var(--pine);
    border-radius:var(--radius); padding:13px 12px; box-shadow:var(--shadow);
    cursor:pointer;
  }
  .row.genus{border-left-color:var(--sage);}
  .row.species{border-left-color:var(--rust); border-left-style:dashed;}
  .row .row-main{flex:1; min-width:0;}
  .row .row-title{font-family:var(--serif); font-size:16.5px; color:var(--ink); font-weight:600;}
  .row .row-title.italic{font-style:italic;}
  .row .row-title .row-count-inline{font-style:normal; font-weight:400; color:var(--ink-soft); font-size:14px;}
  .row .row-count{font-size:12px; color:var(--ink-soft); margin-top:2px;}
  .row .row-preview{font-size:13px; color:var(--ink-soft); margin-top:4px;}
  .row .row-preview em{font-style:italic; color:var(--ink);}
  .row .row-preview .row-preview-more{color:var(--ink-soft); font-style:normal;}
  .row .chevron{color:var(--line); font-size:20px; flex:none;}
  .row .trash{
    flex:none; background:none; border:none; color:var(--ink-soft); opacity:.55;
    font-size:16px; padding:6px; cursor:pointer;
  }
  .row .trash:active{opacity:1; color:var(--danger);}

  .empty{ text-align:center; padding:50px 20px; color:var(--ink-soft); }
  .empty .sprig{width:54px; height:54px; margin:0 auto 14px; opacity:.65;}
  .empty p{margin:0 0 18px; font-size:14.5px; line-height:1.5;}

  .btn{
    display:inline-flex; align-items:center; justify-content:center; gap:6px;
    font-family:var(--sans); font-size:14.5px; font-weight:600; border-radius:10px;
    padding:11px 18px; border:none; cursor:pointer;
  }
  .btn-primary{background:var(--pine); color:var(--paper);}
  .btn-primary:active{background:var(--pine-dark);}
  .btn-ghost{background:transparent; color:var(--pine-dark); border:1px solid var(--line);}
  .btn-danger{background:transparent; color:var(--danger); border:1px solid rgba(161,68,46,.35);}
  .btn-block{width:100%;}

  .detail-card{background:var(--card-bg); border:1px solid var(--line); border-radius:var(--radius); padding:20px 18px; box-shadow:var(--shadow); margin-bottom:16px;}
  .sprig-divider{width:100%; height:26px; display:flex; align-items:center; justify-content:center; margin-bottom:10px; opacity:.7;}
  .sprig-divider svg{height:100%;}
  .binomial{font-family:var(--serif); font-style:italic; font-size:23px; color:var(--pine-dark); text-align:center; margin:0;}
  .header-line{
    font-family:var(--serif); font-size:19px; color:var(--pine-dark); text-align:center; margin:0;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .header-line .sci{font-style:italic;}
  .header-line .common{font-style:normal;}
  .common-name{text-align:center; color:var(--ink-soft); font-size:14px; margin:4px 0 0;}
  .family-tag{
    display:inline-block; text-align:center; margin-top:10px; font-size:11px; letter-spacing:1px;
    text-transform:uppercase; color:var(--sage); font-weight:700;
  }
  .detail-tags{display:flex; align-items:center; justify-content:center; gap:8px; flex-wrap:wrap;}
  .note-count-tag{
    display:inline-block; margin-top:10px; font-size:11px; letter-spacing:.5px;
    color:var(--gold); font-weight:700;
  }

  .section-label{
    font-size:11.5px; text-transform:uppercase; letter-spacing:1.2px; color:var(--ink-soft);
    font-weight:700; margin:20px 0 8px;
  }
  .section-label-row{display:flex; align-items:center; gap:6px; margin:20px 0 8px;}
  .section-label-row .section-label{margin:0;}
  .section-label-row .link-btn{padding:0;}
  .section-label.highlight{color:var(--pine-dark); font-size:12.5px;}
  .description-box{ font-size:14.5px; line-height:1.6; color:var(--ink); white-space:pre-wrap; }
  .text-hr{border:none; border-top:1px solid var(--line); margin:14px 0;}
  .table-scroll{overflow-x:auto; margin:12px 0; -webkit-overflow-scrolling:touch; border-radius:8px; border:1px solid var(--line);}
  .rich-table{border-collapse:collapse; width:100%; font-size:12.5px; line-height:1.4; white-space:normal;}
  .rich-table th, .rich-table td{border:1px solid var(--line); padding:6px 8px; text-align:left; vertical-align:top;}
  .rich-table th{background:var(--paper-2); font-weight:700; color:var(--pine-dark); white-space:nowrap;}
  .rich-table td{background:var(--card-bg);}
  .rich-table tr:nth-child(even) td{background:var(--paper-2);}
  .description-box.placeholder{color:var(--ink-soft); font-style:italic;}
  .link-btn{background:none; border:none; color:var(--pine); font-weight:600; font-size:13.5px; padding:4px 0; cursor:pointer; text-decoration:underline;}
  .sort-bar{font-size:13px; color:var(--ink-soft); margin-bottom:10px;}
  .sort-link{background:none; border:none; color:var(--ink-soft); font-weight:400; font-size:13px; padding:0; cursor:pointer; text-decoration:underline; text-decoration-color:var(--line);}
  .sort-link.active{color:var(--pine-dark); font-weight:700; text-decoration:none;}
  .icon-btn{text-decoration:none; font-size:16px; padding:2px 4px; line-height:1;}
  .section-divider{border:none; border-top:1px solid var(--line); margin:24px 0 0;}
  .field textarea.textarea-lg{min-height:240px;}
  .detkey-preview{
    font-size:14px; color:var(--ink); background:var(--paper-2); border-radius:10px; padding:10px 12px;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:pointer;
  }
  .detkey-preview.placeholder{color:var(--ink-soft); font-style:italic; cursor:default;}

  .note-card{ background:var(--paper-2); border-radius:10px; padding:10px 12px; margin-bottom:8px; position:relative; }
  .note-date{font-size:11px; color:var(--gold); font-weight:700; letter-spacing:.3px;}
  .note-todo-badge{font-size:10.5px; color:var(--pastel-red); font-weight:700; letter-spacing:.3px; margin-left:6px; text-transform:uppercase;}
  .note-location-badge{font-size:10.5px; color:var(--sage); font-weight:700; letter-spacing:.3px; margin-left:6px; text-transform:uppercase;}
  .note-todo-badge.done{color:var(--sage);}
  .note-text{font-size:14px; margin-top:3px; white-space:pre-wrap; line-height:1.5;}
  .note-actions{position:absolute; top:8px; right:8px; display:flex; gap:10px;}
  .note-actions button{background:none; border:none; color:var(--ink-soft); opacity:.5; cursor:pointer; font-size:13px; padding:0;}
  .checkbox-field{display:flex; align-items:center; gap:8px; margin-bottom:14px;}
  .checkbox-field input{width:auto; margin:0;}
  .checkbox-field label{margin:0; text-transform:none; font-size:14px; font-weight:500; letter-spacing:0; color:var(--ink);}

  .task-row{background:var(--card-bg); border:1px solid var(--line); border-left:4px solid var(--rust); border-radius:var(--radius); padding:12px; box-shadow:var(--shadow); display:flex; gap:12px; align-items:flex-start; margin-bottom:9px;}
  .task-row.done{opacity:.5; border-left-color:var(--sage);}
  .task-row.done .task-text{text-decoration:line-through;}
  .task-row input[type="checkbox"]{width:19px; height:19px; margin-top:2px; flex:none;}
  .task-row .task-main{flex:1; min-width:0;}
  .task-row .task-text{font-size:14px; line-height:1.5; white-space:pre-wrap;}
  .task-row .task-header{display:flex; align-items:baseline; justify-content:space-between; gap:8px; margin-bottom:3px;}
  .task-row .task-meta{font-size:14.5px; font-weight:700; color:var(--pine-dark); cursor:pointer; text-decoration:underline; text-decoration-color:var(--line); display:inline-block;}
  .task-row .task-date{font-size:11px; font-weight:400; color:var(--ink-soft); white-space:nowrap; flex:none;}
  .row .row-date{font-size:11px; color:var(--ink-soft); flex:none; white-space:nowrap;}

  .danger-zone{margin-top:26px; padding-top:14px; border-top:1px dashed var(--line); text-align:center;}

  .fab{
    position:fixed; right:calc(50% - 320px + 20px); bottom:26px; z-index:30;
    width:56px; height:56px; border-radius:50%; background:var(--pine); color:var(--paper);
    border:none; font-size:28px; box-shadow:0 6px 16px rgba(31,51,41,.35); cursor:pointer;
    display:flex; align-items:center; justify-content:center; line-height:1;
  }
  @media (max-width:680px){ .fab{ right:20px; } }
  .fab:active{background:var(--pine-dark);}
  .fab.hidden{display:none;}

  .modal-overlay{
    position:fixed; inset:0; background:rgba(20,28,22,.45); z-index:50;
    display:flex; align-items:flex-end; justify-content:center;
  }
  .modal-overlay.hidden{display:none;}
  .modal-sheet{
    background:var(--paper); width:100%; max-width:640px; border-radius:20px 20px 0 0;
    padding:20px 20px calc(24px + env(safe-area-inset-bottom)); max-height:88vh; overflow-y:auto;
    box-shadow:0 -8px 24px rgba(0,0,0,.2);
  }
  .modal-handle{width:38px; height:4px; background:var(--line); border-radius:3px; margin:0 auto 16px;}
  .modal-sheet h2{font-family:var(--serif); font-size:19px; margin:0 0 16px; color:var(--pine-dark);}
  .field{margin-bottom:14px;}
  .field label{display:block; font-size:12px; text-transform:uppercase; letter-spacing:.8px; color:var(--ink-soft); font-weight:700; margin-bottom:6px;}
  .field input, .field textarea{
    width:100%; border:1px solid var(--line); background:var(--card-bg); border-radius:10px;
    padding:11px 12px; font-size:15px; font-family:var(--sans); color:var(--ink); outline:none; resize:vertical;
  }
  .field input:focus, .field textarea:focus{border-color:var(--sage);}
  .field-hint{font-size:11.5px; color:var(--ink-soft); margin-top:4px;}
  .modal-actions{display:flex; gap:10px; margin-top:8px;}
  .modal-actions .btn{flex:1;}
  .action-list{display:flex; flex-direction:column; gap:8px;}
  .action-list button, .action-list a{
    width:100%; text-align:left; background:var(--card-bg); border:1px solid var(--line);
    border-radius:10px; padding:13px 14px; font-size:15px; font-family:var(--sans);
    color:var(--ink); cursor:pointer; display:block; text-decoration:none; box-sizing:border-box;
  }
  .action-list button.danger{color:var(--danger);}

  .toast{
    position:fixed; bottom:96px; left:50%; transform:translateX(-50%);
    background:var(--pine-dark); color:var(--paper); padding:10px 18px; border-radius:20px;
    font-size:13.5px; z-index:60; box-shadow:0 4px 12px rgba(0,0,0,.25);
  }
  .toast.hidden{display:none;}

  .kind-emoji{font-size:.92em; margin-right:5px; display:inline-block; vertical-align:-1px;}
  .kind-filter-bar{display:flex; gap:6px; margin-bottom:14px; flex-wrap:wrap;}
  .kind-filter-btn{font-family:var(--sans); font-size:12.5px; font-weight:600; padding:6px 13px; border-radius:999px; border:1px solid var(--line); background:var(--card-bg); color:var(--ink-soft); cursor:pointer;}
  .kind-filter-btn.active{background:var(--pine); color:var(--paper); border-color:var(--pine);}
  .mycodb-link{display:inline-flex; align-items:center; gap:5px; font-family:var(--sans); font-size:13px; font-weight:600; color:var(--pine-dark); text-decoration:none; border:1px solid var(--line); border-radius:999px; padding:5px 12px; background:var(--card-bg);}
  .mycodb-link:hover{text-decoration:underline;}
  .fungi-links-row{display:flex; flex-direction:column; align-items:flex-start; gap:10px; margin-top:12px;}
  .fungi-links-row:empty{display:none;}
  .species-links-row{display:flex; flex-direction:column; align-items:flex-start; gap:10px; margin-top:12px;}
  .species-links-row:empty{display:none;}
  .species-links-row.inline{flex-direction:row; flex-wrap:wrap; align-items:center;}
  .site-logo{width:16px; height:16px; border-radius:3px; margin-right:6px; vertical-align:-3px;}
  .champi-img-preview{display:inline-flex; flex-direction:column; align-items:center; gap:4px; text-decoration:none;}
  .champi-img-preview img{width:96px; height:96px; object-fit:cover; border-radius:10px; border:1px solid var(--line); box-shadow:var(--shadow); display:block;}
  .champi-img-preview span{font-family:var(--sans); font-size:12px; font-weight:600; color:var(--pine-dark);}
  .gallery-item .champi-img-preview span{font-size:10px;}
</style>
</head>
<body>
<div id="app">
  <header class="topbar">
    <div class="topbar-row">
      <nav class="main-nav">
        <button class="nav-tab" id="navTabHerbier" data-action="go-families">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 21V8"/><path d="M12 12c0-4 3-6 7-6 0 4-2 7-7 7"/><path d="M12 15c0-3-2.5-5-6-5 0 3.2 2 5.6 6 5.8"/></svg>
          Herbier
        </button>
        <button class="nav-tab" id="navTabFungi" data-action="go-fungi">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11c0-4.5 3.6-8 8-8s8 3.5 8 8c0 .9-.9 1.4-1.9 1.4H5.9C4.9 12.4 4 11.9 4 11z"/><path d="M12 12.4V20"/><path d="M9.3 20c0 1 1.2 2 2.7 2s2.7-1 2.7-2"/></svg>
          Fungi
        </button>
        <button class="nav-tab" id="navTabKeys" data-action="go-detkeys">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M15 7a4 4 0 1 0-3.9 5H4l-2 2 2 2h2v-2h2v-2h2.1A4 4 0 0 0 15 7z"/></svg>
          Clé
        </button>
        <button class="nav-tab" id="navTabLocations" data-action="go-locations">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 21s-7-6.2-7-11.5A7 7 0 0 1 19 9.5C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></svg>
          Lieux
        </button>
        <button class="nav-tab" id="navTabTasks" data-action="go-tasks">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
            <rect x="3" y="5" width="18" height="16" rx="2"/>
            <path d="M8 3v4"/><path d="M16 3v4"/><path d="M3 10h18"/>
            <rect x="6.5" y="13" width="2" height="2" fill="currentColor" stroke="none"/>
            <rect x="11" y="13" width="2" height="2" fill="currentColor" stroke="none"/>
            <rect x="15.5" y="13" width="2" height="2" fill="currentColor" stroke="none"/>
          </svg>
          Tâches
        </button>
      </nav>
      <span class="sync-status" id="syncStatus"></span>
      <button class="menu-btn" data-action="open-menu" aria-label="Menu">⋯</button>
    </div>
    <div id="breadcrumb" class="breadcrumb"></div>
    <div class="search-wrap">
      <div class="search-field">
        <input id="searchInput" type="text" placeholder="Rechercher une plante…" autocomplete="off">
        <button type="button" class="search-clear hidden" id="searchClear" aria-label="Effacer la recherche">✕</button>
      </div>
    </div>
  </header>

  <main id="content"></main>

  <button class="fab hidden" id="fab" aria-label="Ajouter">+</button>

  <div class="modal-overlay hidden" id="modalOverlay">
    <div class="modal-sheet" id="modalSheet"></div>
  </div>

  <div class="toast hidden" id="toast"></div>
  <input type="file" id="importFile" accept="application/json" style="display:none">
</div>

<script>
(function(){
  "use strict";

  /* ---------- DATA ---------- */
  var API_URL = location.pathname + "?api=data";
  var db = { families: [] };
  var nav = { screen:"families", familyId:null, genusId:null, speciesId:null, kind:"plant" };
  var searchQuery = "";
  var filterKind = "all"; // "all" | "plant" | "fungi" — utilisé dans Clé / Lieux / Tâches

  /* Normalise une chaîne pour la recherche : minuscules + accents retirés
     (ex. "Érable" -> "erable"), afin que la recherche ne soit sensible
     ni à la casse ni aux accents. */
  function normSearch(str){
    return (str || "")
      .toString()
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .toLowerCase();
  }

  /* ---------- Herbier vs Fungi ---------- */
  var KIND_EMOJI = { plant:"🌱", fungi:"🍄" };
  var TYPE_EMOJI = { fleur:"🌸", fougere:"🌿", arbre:"🌳", herbe:"🌾" };
  var TYPE_LABEL = { fleur:"Fleur", fougere:"Fougère", arbre:"Arbre", herbe:"Herbe" };
  var KIND_LABEL = { plant:"Herbier", fungi:"Fungi" };
  function famKind(fam){ return (fam && fam.kind === "fungi") ? "fungi" : "plant"; }
  function kindEmoji(fam){ return KIND_EMOJI[famKind(fam)]; }
  function kindBadge(fam){ return '<span class="kind-emoji" title="' + KIND_LABEL[famKind(fam)] + '">' + kindEmoji(fam) + '</span>'; }
  function typeBadge(sp){
    if(!sp || !sp.type || !TYPE_EMOJI[sp.type]) return "";
    return '<span class="kind-emoji" title="' + TYPE_LABEL[sp.type] + '">' + TYPE_EMOJI[sp.type] + '</span>';
  }
  /* Emoji affiché devant le nom d'une espèce : celui du type choisi (fleur/fougère/arbre/herbe)
     s'il y en a un, sinon celui du kind (Herbier 🌱 / Fungi 🍄). */
  function speciesLeadEmoji(fam, sp){
    var tb = typeBadge(sp);
    return tb || kindBadge(fam);
  }
  function kindFilterBar(){
    return '<div class="kind-filter-bar">' +
      '<button class="kind-filter-btn' + (filterKind==="all" ? " active" : "") + '" data-action="set-kind-filter" data-kind="all">Tout</button>' +
      '<button class="kind-filter-btn' + (filterKind==="plant" ? " active" : "") + '" data-action="set-kind-filter" data-kind="plant">🌱 Herbier</button>' +
      '<button class="kind-filter-btn' + (filterKind==="fungi" ? " active" : "") + '" data-action="set-kind-filter" data-kind="fungi">🍄 Fungi</button>' +
    '</div>';
  }

  function uid(){ return "id" + Date.now().toString(36) + Math.random().toString(36).slice(2,7); }
  function esc(s){
    return String(s==null?"":s).replace(/[&<>"']/g, function(c){
      return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c];
    });
  }
  function plural(n, one, many){ return n + " " + (n<=1 ? one : many); }
  function richText(s){
    var lines = esc(s).split("\n");
    var cleaned = [];
    for(var i=0; i<lines.length; i++){
      var isBlank = lines[i].trim()==="";
      if(isBlank){
        var prevIsHr = cleaned.length && cleaned[cleaned.length-1].trim()==="---";
        var nextIsHr = (i+1<lines.length) && lines[i+1].trim()==="---";
        if(prevIsHr || nextIsHr) continue;
      }
      cleaned.push(lines[i]);
    }

    function inline(t){
      return t
        .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
        .replace(/\*(.+?)\*/g, "<em>$1</em>")
        .replace(/==(.+?)==/g, '<mark style="background:#E7DFC8">$1</mark>');
    }
    function isTableRow(line){
      var t = line.trim();
      return t.indexOf("|") !== -1 && t !== "---";
    }
    function isSepRow(line){
      var t = line.trim();
      if(t.indexOf("|")===-1) return false;
      var cells = t.replace(/^\|/,"").replace(/\|$/,"").split("|");
      return cells.length>0 && cells.every(function(c){ return /^:?-{2,}:?$/.test(c.trim()); });
    }
    function splitRow(line){
      return line.trim().replace(/^\|/,"").replace(/\|$/,"").split("|").map(function(c){ return c.trim(); });
    }
    function alignFromSep(sep){
      var t = sep.trim();
      var left = t.charAt(0)===":", right = t.charAt(t.length-1)===":";
      if(left && right) return "center";
      if(right) return "right";
      if(left) return "left";
      return "";
    }

    var out = "";
    var prevWasBlock = false;
    var i2 = 0;
    while(i2 < cleaned.length){
      var line = cleaned[i2];

      if(line.trim()==="---"){
        out += '<hr class="text-hr">';
        prevWasBlock = true;
        i2++;
        continue;
      }

      if(isTableRow(line) && (i2+1<cleaned.length) && isSepRow(cleaned[i2+1])){
        var headerCells = splitRow(line);
        var aligns = splitRow(cleaned[i2+1]).map(alignFromSep);
        var bodyRows = [];
        var j = i2+2;
        while(j<cleaned.length && isTableRow(cleaned[j])){
          bodyRows.push(splitRow(cleaned[j]));
          j++;
        }
        var thead = "<thead><tr>" + headerCells.map(function(c,idx){
          var st = aligns[idx] ? ' style="text-align:' + aligns[idx] + '"' : '';
          return "<th" + st + ">" + inline(c) + "</th>";
        }).join("") + "</tr></thead>";
        var tbody = "<tbody>" + bodyRows.map(function(row){
          return "<tr>" + row.map(function(c,idx){
            var st = aligns[idx] ? ' style="text-align:' + aligns[idx] + '"' : '';
            return "<td" + st + ">" + inline(c) + "</td>";
          }).join("") + "</tr>";
        }).join("") + "</tbody>";
        out += '<div class="table-scroll"><table class="rich-table">' + thead + tbody + '</table></div>';
        prevWasBlock = true;
        i2 = j;
        continue;
      }

      var formatted = inline(line);
      out += (out.length>0 && !prevWasBlock) ? "\n" + formatted : formatted;
      prevWasBlock = false;
      i2++;
    }
    return out;
  }
  function sortByName(arr){ return arr.slice().sort(function(a,b){ return a.name.localeCompare(b.name,"fr",{sensitivity:"base"}); }); }
  function today(){
    var d = new Date();
    return d.toLocaleDateString("fr-FR", { day:"2-digit", month:"short", year:"numeric" });
  }
  var FR_MONTHS = { "janv.":0,"févr.":1,"mars":2,"avr.":3,"mai":4,"juin":5,"juil.":6,"août":7,"sept.":8,"oct.":9,"nov.":10,"déc.":11 };
  function parseFrDate(str){
    if(!str) return 0;
    var parts = str.trim().split(/\s+/);
    if(parts.length<3) return 0;
    var day = parseInt(parts[0],10);
    var month = FR_MONTHS[parts[1].toLowerCase()];
    var year = parseInt(parts[2],10);
    if(isNaN(day) || month===undefined || isNaN(year)) return 0;
    return new Date(year, month, day).getTime();
  }

  function getFamily(id){ return db.families.find(function(f){ return f.id===id; }); }
  function getGenus(fam, id){ return fam ? fam.genera.find(function(g){ return g.id===id; }) : null; }
  function getSpecies(gen, id){ return gen ? gen.species.find(function(s){ return s.id===id; }) : null; }

  function migrateDB(){
    db.families.forEach(function(fam){
      if(fam.kind !== "fungi") fam.kind = "plant";
      if(!Array.isArray(fam.detKeys)){
        fam.detKeys = fam.detKey ? [{ id: uid(), title:"Clé de détermination", text: fam.detKey, date: fam.detKeyDate || today() }] : [];
      }
      delete fam.detKey; delete fam.detKeyDate;
      fam.genera.forEach(function(gen){
        if(!Array.isArray(gen.detKeys)){
          gen.detKeys = gen.detKey ? [{ id: uid(), title:"Clé de détermination", text: gen.detKey, date: gen.detKeyDate || today() }] : [];
        }
        delete gen.detKey; delete gen.detKeyDate;
      });
    });
  }

  /* ---------- SERVEUR : lecture / écriture ---------- */
  function fetchDB(){
    return fetch(API_URL, { credentials:"same-origin" })
      .then(function(r){ if(!r.ok) throw new Error("http "+r.status); return r.json(); })
      .then(function(data){
        db = (data && Array.isArray(data.families)) ? data : { families: [] };
        migrateDB();
      });
  }

  var saveChain = Promise.resolve();
  var pendingSaves = 0;
  function saveDB(){
    pendingSaves++;
    setSyncStatus("saving");
    saveChain = saveChain.then(function(){
      return fetch(API_URL, {
        method:"POST",
        credentials:"same-origin",
        headers:{ "Content-Type":"application/json" },
        body: JSON.stringify(db)
      });
    }).then(function(r){
      pendingSaves = Math.max(0, pendingSaves-1);
      if(!r.ok) throw new Error("http "+r.status);
      if(pendingSaves===0) setSyncStatus("saved");
    }).catch(function(){
      pendingSaves = Math.max(0, pendingSaves-1);
      setSyncStatus("error");
      toast("⚠ Échec de l'enregistrement — vérifiez votre connexion");
    });
  }
  function setSyncStatus(state){
    var el = document.getElementById("syncStatus");
    if(!el) return;
    if(state==="saving"){ el.textContent="Enregistrement…"; el.className="sync-status saving"; }
    else if(state==="saved"){
      el.textContent="Enregistré ✓"; el.className="sync-status saved";
      setTimeout(function(){ if(el.textContent==="Enregistré ✓") el.textContent=""; }, 1500);
    }
    else if(state==="error"){ el.textContent="Non enregistré ⚠"; el.className="sync-status error"; }
  }

  /* ---------- MUTATIONS ---------- */
  function addFamily(name, kind){
    db.families.push({ id: uid(), name: name.trim(), kind: (kind==="fungi" ? "fungi" : "plant"), note: "", detKeys: [], genera: [] });
    saveDB();
  }
  function familyExists(name, kind, excludeId){
    var n = name.trim().toLowerCase();
    var k = (kind==="fungi" ? "fungi" : "plant");
    return db.families.some(function(f){ return f.id!==excludeId && famKind(f)===k && f.name.trim().toLowerCase()===n; });
  }
  function renameFamily(id, newName){
    var fam = getFamily(id);
    if(!fam) return;
    fam.name = newName.trim();
    saveDB();
  }
  function addGenus(familyId, name, commonName){
    var fam = getFamily(familyId);
    if(!fam) return;
    fam.genera.push({ id: uid(), name: name.trim(), commonName: (commonName||"").trim(), note: "", detKeys: [], species: [] });
    saveDB();
  }
  function genusExists(fam, name, excludeId){
    var n = name.trim().toLowerCase();
    return fam.genera.some(function(g){ return g.id!==excludeId && g.name.trim().toLowerCase()===n; });
  }
  function renameGenus(familyId, genusId, newName, commonName){
    var fam = getFamily(familyId);
    var gen = fam ? getGenus(fam, genusId) : null;
    if(!gen) return;
    gen.name = newName.trim();
    gen.commonName = (commonName||"").trim();
    saveDB();
  }
  function updateFamilyNote(familyId, text){
    var fam = getFamily(familyId);
    if(!fam) return;
    fam.note = text.trim();
    saveDB();
  }
  function updateGenusNote(familyId, genusId, text){
    var fam = getFamily(familyId), gen = getGenus(fam, genusId);
    if(!gen) return;
    gen.note = text.trim();
    saveDB();
  }
  function ensureDetKeys(owner){
    if(!Array.isArray(owner.detKeys)) owner.detKeys = [];
    return owner.detKeys;
  }
  function saveDetKeyEntry(owner, keyId, title, text){
    var list = ensureDetKeys(owner);
    var cleanTitle = title.trim() || "Clé de détermination";
    var cleanText = text.trim();
    if(keyId){
      var k = list.find(function(x){ return x.id===keyId; });
      if(!k) return;
      k.title = cleanTitle; k.text = cleanText; k.date = today();
    } else {
      list.push({ id: uid(), title: cleanTitle, text: cleanText, date: today() });
    }
    saveDB();
  }
  function deleteDetKeyEntry(owner, keyId){
    if(!owner || !Array.isArray(owner.detKeys)) return;
    owner.detKeys = owner.detKeys.filter(function(x){ return x.id!==keyId; });
    saveDB();
  }
  function addSpecies(familyId, genusId, epithet, commonName, description, type){
    var fam = getFamily(familyId), gen = getGenus(fam, genusId);
    if(!gen) return;
    gen.species.push({ id: uid(), epithet: epithet.trim(), commonName: (commonName||"").trim(), type: type||"", description: (description||"").trim(), notes: [] });
    saveDB();
  }
  function updateDescription(familyId, genusId, speciesId, epithet, commonName, text, type){
    var fam = getFamily(familyId), gen = getGenus(fam, genusId), sp = getSpecies(gen, speciesId);
    if(!sp) return;
    sp.epithet = epithet.trim();
    sp.commonName = (commonName||"").trim();
    sp.type = type||"";
    sp.description = text.trim();
    saveDB();
  }
  function addNote(familyId, genusId, speciesId, text, todo, isLocation){
    var fam = getFamily(familyId), gen = getGenus(fam, genusId), sp = getSpecies(gen, speciesId);
    if(!sp) return;
    sp.notes.unshift({ id: uid(), date: today(), ts: Date.now(), text: text.trim(), todo: !!todo, done: false, isLocation: !!isLocation });
    saveDB();
  }
  function updateNote(familyId, genusId, speciesId, noteId, text, todo, isLocation){
    var fam = getFamily(familyId), gen = getGenus(fam, genusId), sp = getSpecies(gen, speciesId);
    var n = sp ? sp.notes.find(function(note){ return note.id===noteId; }) : null;
    if(!n) return;
    n.text = text.trim();
    n.todo = !!todo;
    if(!n.todo) n.done = false;
    n.isLocation = !!isLocation;
    saveDB();
  }
  function toggleTaskDone(familyId, genusId, speciesId, noteId, done){
    var fam = getFamily(familyId), gen = getGenus(fam, genusId), sp = getSpecies(gen, speciesId);
    var n = sp ? sp.notes.find(function(note){ return note.id===noteId; }) : null;
    if(!n) return;
    n.done = !!done;
    if(n.done) n.doneAt = Date.now();
    saveDB();
  }
  function deleteNote(sp, id){ sp.notes = sp.notes.filter(function(n){ return n.id!==id; }); saveDB(); }

  /* ---------- TOAST ---------- */
  var toastTimer = null;
  function toast(msg){
    var el = document.getElementById("toast");
    el.textContent = msg;
    el.classList.remove("hidden");
    clearTimeout(toastTimer);
    toastTimer = setTimeout(function(){ el.classList.add("hidden"); }, 1700);
  }

  /* ---------- ICONS ---------- */
  function sprigSVG(){
    return '<svg viewBox="0 0 120 26" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" style="color:var(--sage)">'
      + '<path d="M2 13 H118"/>'
      + '<path d="M60 13 C60 6 55 3 50 2 C52 8 55 11 60 13"/>'
      + '<path d="M60 13 C60 6 65 3 70 2 C68 8 65 11 60 13"/>'
      + '<path d="M42 13 C42 8 38 6 34 6 C36 10 38 12 42 13"/>'
      + '<path d="M78 13 C78 8 82 6 86 6 C84 10 82 12 78 13"/>'
      + '<circle cx="60" cy="13" r="1.8" fill="currentColor" stroke="none"/>'
      + '</svg>';
  }
  function emptySprigSVG(){
    return '<svg class="sprig" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="none" stroke-width="1.6" stroke-linecap="round">'
      + '<path d="M32 58V22"/><path d="M32 30c0-10 8-16 18-16 0 10-6 18-18 18"/>'
      + '<path d="M32 38c0-8-7-13-16-13 0 8 6 14 16 14.5"/></svg>';
  }

  /* ---------- RENDER ---------- */
  function render(){
    renderBreadcrumb();
    updateTopNav();
    var content = document.getElementById("content");
    var fab = document.getElementById("fab");

    if(nav.screen==="tasks"){
      content.innerHTML = renderTasks();
      fab.classList.add("hidden");
      return;
    }

    if(nav.screen==="detkeys"){
      content.innerHTML = renderDetKeys();
      fab.classList.add("hidden");
      return;
    }

    if(nav.screen==="locations"){
      content.innerHTML = renderLocations();
      fab.classList.add("hidden");
      return;
    }

    if(nav.screen==="location-species"){
      content.innerHTML = renderLocationSpecies();
      fab.classList.add("hidden");
      return;
    }

    if(nav.screen==="galleryFungi"){
      content.innerHTML = renderGalleryFungi();
      fab.classList.add("hidden");
      loadGalleryImages();
      return;
    }

    if(searchQuery){
      content.innerHTML = renderSearchResults();
      fab.classList.add("hidden");
      return;
    }

    fab.classList.remove("hidden");

    if(nav.screen==="families") content.innerHTML = renderFamilies();
    else if(nav.screen==="genera") content.innerHTML = renderGenera();
    else if(nav.screen==="species") content.innerHTML = renderSpeciesList();
    else if(nav.screen==="detail"){
      content.innerHTML = renderDetail();
      var famD = getFamily(nav.familyId);
      var genD = famD ? getGenus(famD, nav.genusId) : null;
      var spD = genD ? getSpecies(genD, nav.speciesId) : null;
      if(famD && genD && spD && famKind(famD)==="fungi"){
        checkChampiImageLink(genD.name, spD.epithet);
      }
    }
  }

  function updateTopNav(){
    var tabHerbier = document.getElementById("navTabHerbier");
    var tabFungi = document.getElementById("navTabFungi");
    var tabKeys = document.getElementById("navTabKeys");
    var tabLocations = document.getElementById("navTabLocations");
    var tabTasks = document.getElementById("navTabTasks");
    if(!tabHerbier || !tabTasks) return;
    var onTasks = nav.screen==="tasks";
    var onKeys = nav.screen==="detkeys";
    var onLocations = (nav.screen==="locations" || nav.screen==="location-species");
    var onMain = !onTasks && !onKeys && !onLocations;
    var onFungi = onMain && nav.kind==="fungi";
    var onHerbier = onMain && !onFungi;
    tabHerbier.classList.toggle("active", onHerbier);
    tabFungi.classList.toggle("active", onFungi);
    tabKeys.classList.toggle("active", onKeys);
    tabLocations.classList.toggle("active", onLocations);
    tabTasks.classList.toggle("active", onTasks);
  }

  function renderBreadcrumb(){
    var bc = document.getElementById("breadcrumb");

    if(nav.screen==="location-species"){
      bc.innerHTML = '<button data-action="go-locations">Lieux</button><span class="sep">›</span>' +
        '<button class="current" disabled>' + esc(nav.locationLabel||"") + '</button>';
      return;
    }

    if(searchQuery){
      bc.innerHTML = '<span>Résultats pour «\u00A0' + esc(searchQuery) + '\u00A0»</span>';
      return;
    }

    if(nav.screen==="galleryFungi"){
      bc.innerHTML = '<button data-action="go-fungi">' + KIND_EMOJI.fungi + ' Fungi</button><span class="sep">›</span>' +
        '<button class="current" disabled>Galerie</button>';
      return;
    }

    if(nav.screen==="tasks" || nav.screen==="detkeys" || nav.screen==="locations"){
      bc.innerHTML = "";
      return;
    }

    var fam = nav.familyId ? getFamily(nav.familyId) : null;
    var gen = fam && nav.genusId ? getGenus(fam, nav.genusId) : null;
    var sp = gen && nav.speciesId ? getSpecies(gen, nav.speciesId) : null;

    var parts = [];
    var rootLabel = fam ? "Famille" : ((nav.kind==="fungi" ? KIND_EMOJI.fungi : KIND_EMOJI.plant) + " Famille");
    parts.push({ label: rootLabel, action:"go-family-root", current: nav.screen==="families" });

    if(fam) parts.push({ label: kindEmoji(fam) + " " + fam.name, action:"go-genera", current: nav.screen==="genera" });
    if(gen) parts.push({ label: gen.name, action:"go-species-list", current: nav.screen==="species" });
    if(sp) parts.push({ label: (gen.name + " " + sp.epithet), action:"", current: nav.screen==="detail" });

    bc.innerHTML = parts.map(function(p, i){
      var sep = i>0 ? '<span class="sep">›</span>' : "";
      if(p.current || !p.action){
        return sep + '<button class="current" disabled>' + esc(p.label) + '</button>';
      }
      return sep + '<button data-action="' + p.action + '">' + esc(p.label) + '</button>';
    }).join("");
  }

  function previewBlock(label, text, editAction, viewAction){
    var firstLine = text ? text.split("\n")[0] : "";
    return '<div class="section-label-row" style="margin-top:22px"><span class="section-label">' + label + '</span>' +
        '<button class="link-btn icon-btn" data-action="' + editAction + '" title="Modifier">✎</button></div>' +
      (text ? '<div class="detkey-preview" data-action="' + viewAction + '">' + richText(firstLine) + '</div>' : '');
  }

  function detKeysBlock(list, kind, famId, genId){
    var attrs = ' data-kind="' + kind + '" data-fam="' + famId + '"' + (genId ? ' data-gen="' + genId + '"' : '');
    var itemsHtml = (list||[]).map(function(k){
      var firstLine = k.text.split("\n")[0];
      return '<li class="row" data-action="view-detkey"' + attrs + ' data-key="' + k.id + '">' +
        '<div class="row-main"><div class="row-title">' + esc(k.title) + '</div>' +
        '<div class="row-count">' + richText(firstLine) + '</div></div>' +
        (k.date ? '<span class="row-date">' + esc(k.date) + '</span>' : '') +
        '<button class="link-btn icon-btn" data-action="edit-detkey"' + attrs + ' data-key="' + k.id + '" title="Modifier">✎</button>' +
      '</li>';
    }).join("");
    return '<div class="section-label" style="margin-top:22px">Clés de détermination</div>' +
      (itemsHtml ? '<ul class="list">' + itemsHtml + '</ul>' : '') +
      '<div style="margin-top:8px"><button class="link-btn" data-action="add-detkey"' + attrs + '>+ Ajouter une clé</button></div>';
  }

  function renderFamilies(){
    var kind = nav.kind || "plant";
    var isFungi = kind === "fungi";
    var famList = db.families.filter(function(f){ return famKind(f)===kind; });
    if(famList.length===0){
      var emptyMsg = isFungi
        ? 'Votre carnet fungi est vide.<br>Commencez par ajouter la première famille de champignons que vous avez croisée.'
        : 'Votre herbier est vide.<br>Commencez par ajouter la première famille botanique que vous avez croisée.';
      return '<div class="empty">' + emptySprigSVG() +
        '<p>' + emptyMsg + '</p>' +
        '<button class="btn btn-primary" data-action="open-add-family">Ajouter une famille</button></div>';
    }
    var fams = sortByName(famList);
    var totalGenera = famList.reduce(function(sum, f){ return sum + f.genera.length; }, 0);
    var totalSpecies = famList.reduce(function(sum, f){
      return sum + f.genera.reduce(function(s, g){ return s + g.species.length; }, 0);
    }, 0);
    var rows = fams.map(function(f){
      var speciesCount = f.genera.reduce(function(s, g){ return s + g.species.length; }, 0);
      return '<li class="row" data-action="open-family" data-id="' + f.id + '">' +
        '<div class="row-main"><div class="row-title">' + kindBadge(f) + esc(f.name) + '</div>' +
        '<div class="row-count">' + plural(f.genera.length,"genre","genres") +
          ' · ' + plural(speciesCount,"espèce","espèces") + '</div></div>' +
        '<button class="link-btn icon-btn" data-action="edit-family-name" data-id="' + f.id + '" title="Renommer">✎</button>' +
        '<span class="chevron">›</span></li>';
    }).join("");
    var eyebrowText = plural(famList.length,"famille","familles") +
        ' · ' + plural(totalGenera,"genre","genres") +
        ' · ' + plural(totalSpecies,"espèce","espèces");
    var galleryLink = isFungi
      ? '<button class="gallery-link" data-action="open-gallery-fungi">Galerie</button>'
      : '';
    return '<div class="eyebrow-row"><p class="eyebrow">' + eyebrowText + '</p>' + galleryLink + '</div>' +
      '<ul class="list">' + rows + '</ul>';
  }

  function renderGenera(){
    var fam = getFamily(nav.familyId);
    if(!fam){ nav.screen="families"; return renderFamilies(); }
    if(fam.genera.length===0){
      return '<div class="eyebrow">' + kindEmoji(fam) + ' ' + esc(fam.name) + '</div><h1 class="title">Aucun genre</h1>' +
        '<div class="empty">' + emptySprigSVG() +
        '<p>Ajoutez un premier genre pour la famille des <strong>' + esc(fam.name) + '</strong>.</p>' +
        '<button class="btn btn-primary" data-action="open-add-genus">Ajouter un genre</button></div>' +
        detKeysBlock(fam.detKeys, "family", fam.id, null) +
        previewBlock("Note", fam.note, "edit-family-note", "view-family-note");
    }
    var gens = sortByName(fam.genera);
    var totalSpecies = fam.genera.reduce(function(s, g){ return s + g.species.length; }, 0);
    var rows = gens.map(function(g){
      return '<li class="row genus" data-action="open-genus" data-id="' + g.id + '">' +
        '<div class="row-main"><div class="row-title italic">' + esc(g.name) +
          ' <span class="row-count-inline">(' + g.species.length + ')' +
          (g.commonName ? ' — ' + esc(g.commonName) : '') + '</span></div></div>' +
        '<button class="link-btn icon-btn" data-action="edit-genus-name" data-fam="' + fam.id + '" data-id="' + g.id + '" title="Renommer">✎</button>' +
        '<span class="chevron">›</span></li>';
    }).join("");
    return '<h1 class="title">' + esc(fam.name) + '</h1>' +
      '<p class="eyebrow">' + plural(fam.genera.length,"genre","genres") +
        ' · ' + plural(totalSpecies,"espèce","espèces") + '</p>' +
      '<ul class="list">' + rows + '</ul>' +
      detKeysBlock(fam.detKeys, "family", fam.id, null) +
      previewBlock("Note", fam.note, "edit-family-note", "view-family-note");
  }

  function renderSpeciesList(){
    var fam = getFamily(nav.familyId);
    var gen = getGenus(fam, nav.genusId);
    if(!gen){ nav.screen="genera"; return renderGenera(); }
    if(gen.species.length===0){
      return '<h1 class="title" style="font-style:italic">' + esc(gen.name) + '</h1>' +
        '<div class="empty">' + emptySprigSVG() +
        '<p>Aucune espèce de <em>' + esc(gen.name) + '</em> pour l\'instant.</p>' +
        '<button class="btn btn-primary" data-action="open-add-species">Ajouter une espèce</button></div>' +
        detKeysBlock(gen.detKeys, "genus", fam.id, gen.id) +
        previewBlock("Note", gen.note, "edit-genus-note", "view-genus-note");
    }
    var sps = gen.species.slice().sort(function(a,b){ return a.epithet.localeCompare(b.epithet,"fr",{sensitivity:"base"}); });
    var rows = sps.map(function(s){
      return '<li class="row species" data-action="open-species" data-id="' + s.id + '">' +
        '<div class="row-main"><div class="row-title italic">' + speciesLeadEmoji(fam, s) + esc(gen.name) + ' ' + esc(s.epithet) + '</div>' +
        '<div class="row-count">' + (s.commonName ? esc(s.commonName) : "Sans nom commun") + '</div></div>' +
        '<span class="chevron">›</span></li>';
    }).join("");
    return '<h1 class="title" style="font-style:italic">' + esc(gen.name) + '</h1>' +
      '<p class="eyebrow">' + plural(gen.species.length,"espèce","espèces") + '</p>' +
      '<ul class="list">' + rows + '</ul>' +
      detKeysBlock(gen.detKeys, "genus", fam.id, gen.id) +
      previewBlock("Note", gen.note, "edit-genus-note", "view-genus-note");
  }

  function renderDetail(){
    var fam = getFamily(nav.familyId);
    var gen = getGenus(fam, nav.genusId);
    var sp = getSpecies(gen, nav.speciesId);
    if(!sp){ nav.screen="species"; return renderSpeciesList(); }

    var descHtml = sp.description
      ? '<div class="description-box">' + richText(sp.description) + '</div>'
      : '<div class="description-box placeholder">Aucune description pour l\'instant.</div>';

    var notesHtml = sp.notes.length
      ? sp.notes.map(function(n){
          var badge = n.todo ? '<span class="note-todo-badge' + (n.done ? ' done' : '') + '">📅 ' + (n.done ? "Fait" : "À faire") + '</span>' : '';
          var locBadge = n.isLocation ? '<span class="note-location-badge">📍 Lieu</span>' : '';
          return '<div class="note-card"><div class="note-date">' + esc(n.date) + badge + locBadge + '</div>' +
            '<div class="note-text">' + richText(n.text) + '</div>' +
            '<div class="note-actions">' +
              '<button data-action="edit-note" data-id="' + n.id + '" title="Modifier">✎</button>' +
              '<button data-action="delete-note" data-id="' + n.id + '" title="Supprimer">✕</button>' +
            '</div></div>';
        }).join("")
      : '';

    var headerLine = sp.commonName
      ? '<span class="common">' + esc(sp.commonName) + '</span> — <span class="sci">' + esc(gen.name) + ' ' + esc(sp.epithet) + '</span>'
      : '<span class="sci">' + esc(gen.name) + ' ' + esc(sp.epithet) + '</span>';

    var mycodbLinkRow = "";
    var champiImgRow = "";
    if(famKind(fam) === "fungi"){
      var mycodbUrl = "https://www.mycodb.fr/fiche.php?genre=" + encodeURIComponent(gen.name.trim()) + "&espece=" + encodeURIComponent(sp.epithet.trim());
      mycodbLinkRow = '<div class="species-links-row">' +
        '<a class="mycodb-link" href="' + mycodbUrl + '" target="_blank" rel="noopener"><img class="site-logo" src="https://www.google.com/s2/favicons?domain=mycodb.fr&sz=32" alt="">Voir sur MycoDB ↗</a>' +
      '</div>';
      champiImgRow = '<div class="species-links-row">' +
        '<span id="champiImgLink"></span>' +
      '</div>';
    }

    var herbierLinksRow = "";
    if(famKind(fam) !== "fungi"){
      var eFloreUrl = "https://www.google.com/search?q=" + encodeURIComponent("site:tela-botanica.org " + gen.name.trim() + " " + sp.epithet.trim());
      var wikiUrl = "https://fr.wikipedia.org/wiki/" + encodeURIComponent(gen.name.trim() + "_" + sp.epithet.trim());
      herbierLinksRow = '<div class="species-links-row inline">' +
        '<a class="mycodb-link" href="' + eFloreUrl + '" target="_blank" rel="noopener"><img class="site-logo" src="https://www.google.com/s2/favicons?domain=tela-botanica.org&sz=32" alt="">Eflore</a>' +
        '<a class="mycodb-link" href="' + wikiUrl + '" target="_blank" rel="noopener"><img class="site-logo" src="https://www.google.com/s2/favicons?domain=wikipedia.org&sz=32" alt="">Wikipedia</a>' +
      '</div>';
    }

    return '' +
      '<div class="detail-card">' +
        '<p class="header-line">' + speciesLeadEmoji(fam, sp) + headerLine + '</p>' +
      '</div>' +

      herbierLinksRow +
      mycodbLinkRow +

      '<div class="section-label-row"><span class="section-label highlight">Description</span>' +
        '<button class="link-btn icon-btn" data-action="edit-description" title="Modifier">✎</button></div>' +
      descHtml +
      champiImgRow +

      '<hr class="section-divider">' +

      '<div class="section-label highlight" style="margin-top:20px">Notes personnelles</div>' +
      notesHtml +
      '<div style="margin-top:8px"><button class="link-btn" data-action="add-note">+ Ajouter une note</button></div>';
  }

  /* ---------- Champi Ingall (photo Fungi) ---------- */
  function findChampiImageUrl(genusName, epithet){
    return new Promise(function(resolve){
      var base = "https://ressources.ingall-niger.org/champi/" +
        encodeURIComponent((genusName.trim() + " " + epithet.trim()).toLowerCase());
      var exts = ["jpg","jpeg","png","gif","webp","JPG","JPEG","PNG"];
      var i = 0;
      function tryNext(){
        if(i >= exts.length){ resolve(null); return; } // aucune image trouvée
        var url = base + "." + exts[i];
        var img = new Image();
        img.onload = function(){ resolve(url); };
        img.onerror = function(){ i++; tryNext(); };
        img.src = url;
      }
      tryNext();
    });
  }

  function checkChampiImageLink(genusName, epithet){
    findChampiImageUrl(genusName, epithet).then(function(url){
      if(!url) return;
      var target = document.getElementById("champiImgLink");
      if(target){
        target.innerHTML = '<a class="champi-img-preview" href="' + url + '" target="_blank" rel="noopener">' +
          '<img src="' + url + '" alt="Photo personnelle" loading="lazy">' +
          '<span>👁️ image perso ↗</span>' +
        '</a>';
      }
    });
  }

  /* ---------- Galerie de toutes les photos Fungi ---------- */
  function getFungiSpeciesList(){
    var list = [];
    db.families.forEach(function(fam){
      if(famKind(fam) !== "fungi") return;
      fam.genera.forEach(function(gen){
        gen.species.forEach(function(sp){
          list.push({ fam:fam, gen:gen, sp:sp });
        });
      });
    });
    list.sort(function(a,b){
      var famCmp = a.fam.name.localeCompare(b.fam.name, "fr", {sensitivity:"base"});
      if(famCmp !== 0) return famCmp;
      return (a.gen.name + " " + a.sp.epithet).localeCompare(b.gen.name + " " + b.sp.epithet, "fr", {sensitivity:"base"});
    });
    return list;
  }

  function renderGalleryFungi(){
    var speciesList = getFungiSpeciesList();
    var famList = db.families.filter(function(f){ return famKind(f)==="fungi"; });
    var totalGenera = famList.reduce(function(sum,f){ return sum + f.genera.length; }, 0);
    var totalSpecies = famList.reduce(function(sum,f){
      return sum + f.genera.reduce(function(s,g){ return s + g.species.length; }, 0);
    }, 0);
    var statsText = plural(famList.length,"famille","familles") +
        ' · ' + plural(totalGenera,"genre","genres") +
        ' · ' + plural(totalSpecies,"espèce","espèces");

    if(speciesList.length===0){
      return '<p class="gallery-title">Galerie photos</p>' +
        '<p class="eyebrow">' + statsText + '</p>' +
        '<div class="empty">' + emptySprigSVG() + '<p>Aucune espèce enregistrée pour le moment.</p></div>';
    }
    var famStats = {};
    famList.forEach(function(f){
      var speciesCount = f.genera.reduce(function(s,g){ return s + g.species.length; }, 0);
      famStats[f.id] = { genera: f.genera.length, species: speciesCount };
    });
    var famsHtml = "";
    var currentFamId = null;
    speciesList.forEach(function(r){
      if(r.fam.id !== currentFamId){
        if(currentFamId !== null) famsHtml += '</div></div>';
        var stats = famStats[r.fam.id] || { genera:0, species:0 };
        var famLabel = esc(r.fam.name) + ' (' + plural(stats.genera,"genre","genres") +
          ' · ' + plural(stats.species,"espèce","espèces") + ')';
        famsHtml += (currentFamId !== null ? '<hr class="section-divider" id="galsep-' + r.fam.id + '">' : '') +
          '<div class="gallery-family" id="galfam-' + r.fam.id + '">' +
          '<div class="section-label" style="margin-top:22px">' + famLabel + '</div>' +
          '<div class="gallery-grid">';
        currentFamId = r.fam.id;
      }
      famsHtml += '<div class="gallery-item" id="galimg-' + r.sp.id + '"></div>';
    });
    famsHtml += '</div></div>';
    return '<p class="gallery-title">Galerie photos</p>' +
      '<p class="eyebrow">' + statsText + '</p>' +
      famsHtml +
      '<p class="empty" id="galleryEmptyMsg" style="display:none">Aucune photo personnelle trouvée pour l\'instant.</p>';
  }

  function loadGalleryImages(){
    var speciesList = getFungiSpeciesList();
    if(speciesList.length===0) return;
    var famIds = {};
    speciesList.forEach(function(r){ famIds[r.fam.id] = true; });
    var checkedCount = 0, foundCount = 0;
    speciesList.forEach(function(r){
      findChampiImageUrl(r.gen.name, r.sp.epithet).then(function(url){
        checkedCount++;
        var target = document.getElementById("galimg-" + r.sp.id);
        if(url && target){
          foundCount++;
          var caption = r.gen.name + " " + r.sp.epithet;
          target.innerHTML = '<a class="champi-img-preview" href="' + url + '" target="_blank" rel="noopener">' +
            '<img src="' + url + '" alt="' + esc(caption) + '" loading="lazy">' +
            '<span>' + esc(caption) + '</span>' +
          '</a>';
        } else if(target){
          target.parentNode && target.parentNode.removeChild(target);
        }
        if(checkedCount === speciesList.length){
          Object.keys(famIds).forEach(function(famId){
            var famBlock = document.getElementById("galfam-" + famId);
            if(famBlock && !famBlock.querySelector(".gallery-item")){
              famBlock.style.display = "none";
              var sep = document.getElementById("galsep-" + famId);
              if(sep) sep.style.display = "none";
            }
          });
          if(foundCount === 0){
            var msg = document.getElementById("galleryEmptyMsg");
            if(msg) msg.style.display = "";
          }
        }
      });
    });
  }

  function renderSearchResults(){
    var q = normSearch(searchQuery);
    var kind = nav.kind || "plant";
    var results = [];
    db.families.forEach(function(fam){
      if(famKind(fam) !== kind) return;
      fam.genera.forEach(function(gen){
        gen.species.forEach(function(sp){
          var hay = normSearch(fam.name + " " + gen.name + " " + sp.epithet + " " + sp.commonName);
          if(hay.indexOf(q) !== -1){
            results.push({ fam:fam, gen:gen, sp:sp });
          }
        });
      });
    });
    if(results.length===0){
      var noResultMsg = kind==="fungi" ? "Aucun champignon ne correspond à" : "Aucune plante ne correspond à";
      return '<div class="empty">' + emptySprigSVG() + '<p>' + noResultMsg + ' «\u00A0' + esc(searchQuery) + '\u00A0».</p></div>';
    }
    results.sort(function(a,b){
      return (a.gen.name + " " + a.sp.epithet).localeCompare(b.gen.name + " " + b.sp.epithet, "fr", {sensitivity:"base"});
    });
    var rows = results.map(function(r){
      return '<li class="row species" data-action="open-species-search" data-fam="' + r.fam.id + '" data-gen="' + r.gen.id + '" data-sp="' + r.sp.id + '">' +
        '<div class="row-main"><div class="row-title italic">' + speciesLeadEmoji(r.fam, r.sp) + esc(r.gen.name) + ' ' + esc(r.sp.epithet) + '</div>' +
        '<div class="row-count">' + esc(r.fam.name) + (r.sp.commonName ? " · " + esc(r.sp.commonName) : "") + '</div></div>' +
        '<span class="chevron">›</span></li>';
    }).join("");
    return '<p class="sub">' + plural(results.length,"résultat","résultats") + '</p><ul class="list">' + rows + '</ul>';
  }

  function locationKey(text){
    return text.trim().toLowerCase();
  }

  function kindCountLine(plantCount, fungiCount){
    var parts = [];
    if(plantCount>0) parts.push(KIND_EMOJI.plant + ' ' + plantCount);
    if(fungiCount>0) parts.push(KIND_EMOJI.fungi + ' ' + fungiCount);
    return parts.length ? parts.join(' · ') : plural(0,"espèce","espèces");
  }

  function renderLocations(){
    var groups = {}; // key -> { label, species, refs, plantCount, fungiCount }
    db.families.forEach(function(fam){
      if(filterKind!=="all" && famKind(fam)!==filterKind) return;
      fam.genera.forEach(function(gen){
        gen.species.forEach(function(sp){
          sp.notes.forEach(function(n){
            if(!n.isLocation || !n.text) return;
            var key = locationKey(n.text);
            if(!groups[key]) groups[key] = { label:n.text.trim(), species:{}, refs:[], plantCount:0, fungiCount:0 };
            if(!groups[key].species[sp.id]){
              groups[key].species[sp.id] = true;
              groups[key].refs.push({ fam:fam, gen:gen, sp:sp });
              if(famKind(fam)==="fungi") groups[key].fungiCount++; else groups[key].plantCount++;
            }
          });
        });
      });
    });
    var list = Object.keys(groups).map(function(key){
      return { key:key, label:groups[key].label, count:Object.keys(groups[key].species).length,
        plantCount:groups[key].plantCount, fungiCount:groups[key].fungiCount, refs:groups[key].refs };
    });
    if(searchQuery){
      var locQ = normSearch(searchQuery);
      list = list.filter(function(l){
        if(normSearch(l.label).indexOf(locQ) !== -1) return true;
        return l.refs.some(function(r){
          var hay = normSearch(r.gen.name + " " + r.sp.epithet + " " + (r.sp.commonName||"") + " " + r.fam.name);
          return hay.indexOf(locQ) !== -1;
        });
      });
    }
    if(list.length===0){
      if(searchQuery){
        return kindFilterBar() + '<div class="empty">' + emptySprigSVG() +
          '<p>Aucun lieu ne correspond à «\u00A0' + esc(searchQuery) + '\u00A0».</p></div>';
      }
      return kindFilterBar() + '<h1 class="title">Aucun lieu</h1>' +
        '<div class="empty">' + emptySprigSVG() +
        '<p>Cochez « 📍 Lieu » sur une note personnelle pour la retrouver ici.</p></div>';
    }
    list.sort(function(a,b){ return a.label.localeCompare(b.label,"fr",{sensitivity:"base"}); });
    var lq = searchQuery ? normSearch(searchQuery) : "";
    var rows = list.map(function(l){
      var countLine = kindCountLine(l.plantCount, l.fungiCount);
      var previewLine = "";
      if(searchQuery){
        var matches = l.refs.filter(function(r){
          var hay = normSearch(r.gen.name + " " + r.sp.epithet + " " + (r.sp.commonName||"") + " " + r.fam.name);
          return hay.indexOf(lq) !== -1;
        });
        if(matches.length){
          matches.sort(function(a,b){ return (a.gen.name+a.sp.epithet).localeCompare(b.gen.name+b.sp.epithet,"fr",{sensitivity:"base"}); });
          countLine = plural(matches.length,"occurrence","occurrences");
          var shown = matches.slice(0,3).map(function(r){ return kindEmoji(r.fam) + ' <em>' + esc(r.gen.name) + ' ' + esc(r.sp.epithet) + '</em>'; });
          var extra = matches.length - shown.length;
          previewLine = '<div class="row-preview">' + shown.join(", ") +
            (extra>0 ? ' <span class="row-preview-more">+' + extra + ' autre' + (extra>1?"s":"") + '</span>' : '') +
            '</div>';
        }
      }
      return '<li class="row" data-action="open-location" data-key="' + esc(l.key) + '" data-label="' + esc(l.label) + '">' +
        '<div class="row-main"><div class="row-title">' + esc(l.label) + '</div>' +
        '<div class="row-count">' + countLine + '</div>' +
        previewLine +
        '</div>' +
        '<span class="chevron">›</span></li>';
    }).join("");
    return kindFilterBar() + '<p class="eyebrow">' + plural(list.length,"lieu","lieux") + '</p>' +
      '<ul class="list">' + rows + '</ul>';
  }

  function renderLocationSpecies(){
    var key = nav.locationKey;
    var mode = nav.locationSort || "sci";
    var results = [];
    db.families.forEach(function(fam){
      if(filterKind!=="all" && famKind(fam)!==filterKind) return;
      fam.genera.forEach(function(gen){
        gen.species.forEach(function(sp){
          var match = sp.notes.some(function(n){ return n.isLocation && n.text && locationKey(n.text)===key; });
          if(match) results.push({ fam:fam, gen:gen, sp:sp });
        });
      });
    });
    if(searchQuery){
      var lsQ = normSearch(searchQuery);
      results = results.filter(function(r){
        var hay = normSearch(r.gen.name + " " + r.sp.epithet + " " + (r.sp.commonName||"") + " " + r.fam.name);
        return hay.indexOf(lsQ) !== -1;
      });
    }
    function cmp(a, b){
      return a.localeCompare(b, "fr", { sensitivity:"base" });
    }
    if(mode==="family"){
      results.sort(function(a,b){
        return cmp(a.fam.name, b.fam.name) || cmp(a.gen.name+a.sp.epithet, b.gen.name+b.sp.epithet);
      });
    } else if(mode==="common"){
      results.sort(function(a,b){
        var ca = a.sp.commonName || "", cb = b.sp.commonName || "";
        if(!ca && cb) return 1;
        if(ca && !cb) return -1;
        return cmp(ca, cb) || cmp(a.gen.name+a.sp.epithet, b.gen.name+b.sp.epithet);
      });
    } else {
      results.sort(function(a,b){ return cmp(a.gen.name+a.sp.epithet, b.gen.name+b.sp.epithet); });
    }
    var sortBar = kindFilterBar() + '<div class="sort-bar">Trier par ' +
      '<button class="sort-link' + (mode==="sci" ? " active" : "") + '" data-action="set-location-sort" data-mode="sci">nom scientifique</button> · ' +
      '<button class="sort-link' + (mode==="common" ? " active" : "") + '" data-action="set-location-sort" data-mode="common">nom français</button> · ' +
      '<button class="sort-link' + (mode==="family" ? " active" : "") + '" data-action="set-location-sort" data-mode="family">famille</button>' +
      '</div>';
    if(results.length===0){
      if(searchQuery){
        return sortBar + '<div class="empty">' + emptySprigSVG() +
          '<p>Aucune espèce ne correspond à «\u00A0' + esc(searchQuery) + '\u00A0» dans ce lieu.</p></div>';
      }
      return sortBar + '<div class="empty">' + emptySprigSVG() + '<p>Aucune espèce pour ce lieu.</p></div>';
    }
    var rows = results.map(function(r){
      var sciPart = '<em>' + esc(r.gen.name) + ' ' + esc(r.sp.epithet) + '</em>';
      var commonPart = r.sp.commonName ? esc(r.sp.commonName) : null;
      var famPart = esc(r.fam.name);
      var line;
      if(mode==="family"){
        line = famPart + ' <span class="row-count-inline">· ' + sciPart + (commonPart ? ' — ' + commonPart : '') + '</span>';
      } else if(mode==="common" && commonPart){
        line = commonPart + ' <span class="row-count-inline">— ' + sciPart + ' · ' + famPart + '</span>';
      } else {
        line = sciPart + ' <span class="row-count-inline">' + (commonPart ? '— ' + commonPart + ' · ' : '') + famPart + '</span>';
      }
      return '<li class="row species" data-action="open-location-species" data-fam="' + r.fam.id + '" data-gen="' + r.gen.id + '" data-sp="' + r.sp.id + '">' +
        '<div class="row-main"><div class="row-title">' + speciesLeadEmoji(r.fam, r.sp) + line + '</div></div>' +
        '<span class="chevron">›</span></li>';
    }).join("");
    return sortBar + '<p class="eyebrow">' + (searchQuery ? plural(results.length,"occurrence","occurrences") : plural(results.length,"espèce","espèces")) + '</p>' +
      '<ul class="list">' + rows + '</ul>';
  }

  function renderDetKeys(){
    var entries = [];
    db.families.forEach(function(fam){
      if(filterKind!=="all" && famKind(fam)!==filterKind) return;
      (fam.detKeys||[]).forEach(function(k){
        entries.push({ kind:"family", famId:fam.id, name:fam.name, italic:false, title:k.title, text:k.text, date:k.date, keyId:k.id, emoji:kindEmoji(fam) });
      });
      fam.genera.forEach(function(gen){
        (gen.detKeys||[]).forEach(function(k){
          entries.push({ kind:"genus", famId:fam.id, genId:gen.id, name:gen.name, italic:true, title:k.title, text:k.text, date:k.date, keyId:k.id, emoji:kindEmoji(fam) });
        });
      });
    });
    if(searchQuery){
      var dkQ = normSearch(searchQuery);
      entries = entries.filter(function(en){
        var dkFamName = getFamily(en.famId) ? getFamily(en.famId).name : "";
        var hay = normSearch(en.name + " " + dkFamName + " " + (en.title||"") + " " + en.text);
        return hay.indexOf(dkQ) !== -1;
      });
    }
    if(entries.length===0){
      if(searchQuery){
        return kindFilterBar() + '<div class="empty">' + emptySprigSVG() +
          '<p>Aucune clé ne correspond à «\u00A0' + esc(searchQuery) + '\u00A0».</p></div>';
      }
      return kindFilterBar() + '<h1 class="title">Aucune clé</h1>' +
        '<div class="empty">' + emptySprigSVG() +
        '<p>Ajoutez une clé de détermination depuis une famille ou un genre.</p></div>';
    }
    entries.sort(function(a,b){ return a.name.localeCompare(b.name,"fr",{sensitivity:"base"}); });
    var rows = entries.map(function(en){
      var titleTag = en.title && en.title!=="Clé de détermination"
        ? ' <span class="row-count-inline">— ' + esc(en.title) + '</span>' : '';
      return '<li class="row" data-action="view-detkey" data-kind="' + en.kind + '" data-fam="' + en.famId + '"' +
          (en.genId ? ' data-gen="' + en.genId + '"' : '') + ' data-key="' + en.keyId + '">' +
        '<div class="row-main"><div class="row-title' + (en.italic ? ' italic' : '') + '">' + en.emoji + ' ' + esc(en.name) + titleTag + '</div></div>' +
        (en.date ? '<span class="row-date">' + esc(en.date) + '</span>' : '') +
        '<button class="link-btn icon-btn" data-action="edit-detkey" data-kind="' + en.kind + '" data-fam="' + en.famId + '"' +
          (en.genId ? ' data-gen="' + en.genId + '"' : '') + ' data-key="' + en.keyId + '" title="Modifier">✎</button>' +
        '<span class="chevron">›</span></li>';
    }).join("");
    return kindFilterBar() + '<p class="eyebrow">' + plural(entries.length,"clé de détermination","clés de détermination") + '</p>' +
      '<ul class="list">' + rows + '</ul>';
  }

  function renderTasks(){
    var tasks = [];
    db.families.forEach(function(fam){
      if(filterKind!=="all" && famKind(fam)!==filterKind) return;
      fam.genera.forEach(function(gen){
        gen.species.forEach(function(sp){
          sp.notes.forEach(function(n){
            if(n.todo){
              tasks.push({ fam:fam, gen:gen, sp:sp, note:n });
            }
          });
        });
      });
    });
    if(searchQuery){
      var taskQ = normSearch(searchQuery);
      tasks = tasks.filter(function(t){
        var hay = normSearch(t.note.text + " " + t.gen.name + " " + t.sp.epithet + " " + (t.sp.commonName||"") + " " + t.fam.name);
        return hay.indexOf(taskQ) !== -1;
      });
    }
    if(tasks.length===0){
      if(searchQuery){
        return kindFilterBar() + '<div class="empty">' + emptySprigSVG() +
          '<p>Aucune tâche ne correspond à «\u00A0' + esc(searchQuery) + '\u00A0».</p></div>';
      }
      return kindFilterBar() + '<h1 class="title">Rien à faire</h1>' +
        '<div class="empty">' + emptySprigSVG() +
        '<p>Cochez « À faire » sur une note personnelle pour la retrouver ici.</p></div>';
    }
    tasks.sort(function(a,b){
      var doneDiff = (a.note.done?1:0) - (b.note.done?1:0);
      if(doneDiff!==0) return doneDiff;
      var ta = a.note.done ? (a.note.doneAt || a.note.ts || parseFrDate(a.note.date))
                            : (a.note.ts || parseFrDate(a.note.date));
      var tb = b.note.done ? (b.note.doneAt || b.note.ts || parseFrDate(b.note.date))
                            : (b.note.ts || parseFrDate(b.note.date));
      return tb - ta;
    });
    var pending = tasks.filter(function(t){ return !t.note.done; }).length;
    var rows = tasks.map(function(t){
      return '<div class="task-row' + (t.note.done ? ' done' : '') + '">' +
        '<input type="checkbox" class="task-check" data-fam="' + t.fam.id + '" data-gen="' + t.gen.id + '" data-sp="' + t.sp.id + '" data-note="' + t.note.id + '"' + (t.note.done ? ' checked' : '') + '>' +
        '<div class="task-main">' +
          '<div class="task-header">' +
            '<span class="task-meta" data-action="open-task" data-fam="' + t.fam.id + '" data-gen="' + t.gen.id + '" data-sp="' + t.sp.id + '">' +
              speciesLeadEmoji(t.fam, t.sp) + ' ' + esc(t.gen.name) + ' ' + esc(t.sp.epithet) + ' · ' + esc(t.fam.name) +
            '</span>' +
            '<span class="task-date">' + esc(t.note.date) + '</span>' +
          '</div>' +
          '<div class="task-text">' + richText(t.note.text) + '</div>' +
        '</div></div>';
    }).join("");
    return kindFilterBar() + '<p class="eyebrow">' + plural(pending,"tâche à faire","tâches à faire") + '</p>' +
      rows;
  }

  /* ---------- MODAL ---------- */
  function openModal(html){
    document.getElementById("modalSheet").innerHTML = '<div class="modal-handle"></div>' + html;
    document.getElementById("modalOverlay").classList.remove("hidden");
    var firstInput = document.querySelector("#modalSheet input, #modalSheet textarea");
    if(firstInput) setTimeout(function(){ firstInput.focus(); }, 60);
  }
  function closeModal(){
    document.getElementById("modalOverlay").classList.add("hidden");
    document.getElementById("modalSheet").innerHTML = "";
  }

  function formAddFamily(){
    var isFungi = nav.kind === "fungi";
    openModal(
      '<h2>Ajouter une ' + (isFungi ? "famille de champignons" : "famille") + '</h2>' +
      '<form id="formAddFamily">' +
        '<div class="field"><label>Nom de la famille</label><input type="text" name="name" placeholder="ex. ' + (isFungi ? "Amanitaceae" : "Rosaceae") + '" required></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Ajouter</button></div>' +
      '</form>'
    );
  }
  function formAddGenus(){
    var fam = getFamily(nav.familyId);
    openModal(
      '<h2>Ajouter un genre</h2><p class="sub" style="margin-top:-8px">Famille : ' + esc(fam.name) + '</p>' +
      '<form id="formAddGenus">' +
        '<div class="field"><label>Nom du genre</label><input type="text" name="name" placeholder="ex. Rosa" required></div>' +
        '<div class="field"><label>Nom français (optionnel)</label><input type="text" name="commonName" placeholder="ex. Rosier"></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Ajouter</button></div>' +
      '</form>'
    );
  }
  function formEditFamilyName(id){
    var fam = getFamily(id);
    if(!fam) return;
    openModal(
      '<h2>Renommer la famille</h2>' +
      '<form id="formEditFamilyName" data-id="' + fam.id + '">' +
        '<div class="field"><label>Nom de la famille</label><input type="text" name="name" value="' + esc(fam.name) + '" required></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Enregistrer</button></div>' +
      '</form>'
    );
  }
  function formEditGenusName(famId, genusId){
    var fam = getFamily(famId);
    var gen = fam ? getGenus(fam, genusId) : null;
    if(!gen) return;
    openModal(
      '<h2>Renommer le genre</h2><p class="sub" style="margin-top:-8px">Famille : ' + esc(fam.name) + '</p>' +
      '<form id="formEditGenusName" data-fam="' + fam.id + '" data-id="' + gen.id + '">' +
        '<div class="field"><label>Nom du genre</label><input type="text" name="name" value="' + esc(gen.name) + '" required></div>' +
        '<div class="field"><label>Nom français (optionnel)</label><input type="text" name="commonName" value="' + esc(gen.commonName||"") + '"></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Enregistrer</button></div>' +
      '</form>'
    );
  }
  function typeCheckboxesHtml(selected){
    var boxes = Object.keys(TYPE_LABEL).map(function(k){
      return '<label class="type-checkbox-label"><input type="checkbox" class="type-checkbox" data-type="' + k + '"' +
        (selected===k ? ' checked' : '') + '> ' + TYPE_EMOJI[k] + ' ' + TYPE_LABEL[k] + '</label>';
    }).join("");
    return '<div class="field type-checkboxes">' + boxes + '</div>';
  }
  function getCheckedType(form){
    var checked = form.querySelector(".type-checkbox:checked");
    return checked ? checked.dataset.type : "";
  }
  function formAddSpecies(){
    var fam = getFamily(nav.familyId);
    var gen = getGenus(fam, nav.genusId);
    var isFungi = famKind(fam)==="fungi";
    openModal(
      '<h2>Ajouter une espèce</h2><p class="sub" style="margin-top:-8px">Genre : <em>' + esc(gen.name) + '</em></p>' +
      '<form id="formAddSpecies">' +
        '<div class="field"><label>Épithète spécifique</label><input type="text" name="epithet" placeholder="ex. canina" required>' +
          '<div class="field-hint">Le nom complet sera « ' + esc(gen.name) + ' … »</div></div>' +
        '<div class="field"><label>Nom commun (optionnel)</label><input type="text" name="commonName" placeholder="ex. Églantier"></div>' +
        (isFungi ? '' : typeCheckboxesHtml("")) +
        '<div class="field"><label>Description (optionnel)</label><textarea name="description" rows="3" placeholder="Caractéristiques, habitat…"></textarea></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Ajouter</button></div>' +
      '</form>'
    );
  }
  function formEditDescription(){
    var fam = getFamily(nav.familyId), gen = getGenus(fam, nav.genusId), sp = getSpecies(gen, nav.speciesId);
    var isFungi = famKind(fam)==="fungi";
    openModal(
      '<h2>Modifier — <em style="font-family:var(--serif)">' + esc(gen.name) + ' ' + esc(sp.epithet) + '</em></h2>' +
      '<form id="formEditDescription">' +
        '<div class="field"><label>Épithète spécifique</label><input type="text" name="epithet" value="' + esc(sp.epithet) + '" required></div>' +
        '<div class="field"><label>Nom commun (optionnel)</label><input type="text" name="commonName" value="' + esc(sp.commonName||"") + '"></div>' +
        (isFungi ? '' : typeCheckboxesHtml(sp.type||"")) +
        '<div class="field"><label>Description</label>' +
          '<textarea class="textarea-lg" name="description" rows="12" placeholder="Décrivez cette espèce…">' + esc(sp.description) + '</textarea></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Enregistrer</button></div>' +
      '</form>'
    );
  }
  function formAddNote(){
    var fam = getFamily(nav.familyId), gen = getGenus(fam, nav.genusId), sp = getSpecies(gen, nav.speciesId);
    openModal(
      '<h2>Note personnelle</h2><p class="sub" style="margin-top:-8px">' + esc(gen.name) + ' ' + esc(sp.epithet) + ' · ' + today() + '</p>' +
      '<form id="formAddNote">' +
        '<div class="field"><textarea name="text" rows="4" placeholder="Où l\'avez-vous croisée ? À quoi ressemblait-elle ?" required></textarea></div>' +
        '<div class="checkbox-field"><input type="checkbox" name="todo" id="noteTodo"><label for="noteTodo">📅 À faire</label>' +
          '<input type="checkbox" name="isLocation" id="noteLocation" style="margin-left:18px"><label for="noteLocation">📍 Lieu</label></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Ajouter la note</button></div>' +
      '</form>'
    );
  }
  function formEditNote(noteId){
    var fam = getFamily(nav.familyId), gen = getGenus(fam, nav.genusId), sp = getSpecies(gen, nav.speciesId);
    var n = sp.notes.find(function(note){ return note.id===noteId; });
    if(!n) return;
    openModal(
      '<h2>Modifier la note</h2><p class="sub" style="margin-top:-8px">' + esc(gen.name) + ' ' + esc(sp.epithet) + ' · ' + esc(n.date) + '</p>' +
      '<form id="formEditNote" data-note-id="' + n.id + '">' +
        '<div class="field"><textarea name="text" rows="4" required>' + esc(n.text) + '</textarea></div>' +
        '<div class="checkbox-field"><input type="checkbox" name="todo" id="noteTodoEdit"' + (n.todo ? ' checked' : '') + '><label for="noteTodoEdit">📅 À faire</label>' +
          '<input type="checkbox" name="isLocation" id="noteLocationEdit" style="margin-left:18px"' + (n.isLocation ? ' checked' : '') + '><label for="noteLocationEdit">📍 Lieu</label></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Enregistrer</button></div>' +
      '</form>'
    );
  }
  function formEditFamilyNote(){
    var fam = getFamily(nav.familyId);
    openModal(
      '<h2>Note — ' + esc(fam.name) + '</h2>' +
      '<form id="formEditFamilyNote">' +
        '<div class="field"><textarea name="note" rows="5" placeholder="Une remarque générale sur cette famille…">' + esc(fam.note||"") + '</textarea></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Enregistrer</button></div>' +
      '</form>'
    );
  }
  function formEditGenusNote(){
    var fam = getFamily(nav.familyId), gen = getGenus(fam, nav.genusId);
    openModal(
      '<h2>Note — <em style="font-family:var(--serif)">' + esc(gen.name) + '</em></h2>' +
      '<form id="formEditGenusNote">' +
        '<div class="field"><textarea name="note" rows="5" placeholder="Une remarque générale sur ce genre…">' + esc(gen.note||"") + '</textarea></div>' +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Enregistrer</button></div>' +
      '</form>'
    );
  }
  function formEditDetKeyEntry(kind, famId, genId, keyId){
    var fam = getFamily(famId);
    var owner = kind==="family" ? fam : getGenus(fam, genId);
    if(!owner) return;
    var k = keyId ? (owner.detKeys||[]).find(function(x){ return x.id===keyId; }) : null;
    var ownerNameHtml = kind==="family" ? esc(owner.name) : '<em style="font-family:var(--serif)">' + esc(owner.name) + '</em>';
    var attrs = ' data-kind="' + kind + '" data-fam="' + famId + '"' + (genId ? ' data-gen="' + genId + '"' : '') + (keyId ? ' data-key="' + keyId + '"' : '');
    openModal(
      '<h2>' + (k ? "Modifier la clé" : "Nouvelle clé") + ' — ' + ownerNameHtml + '</h2>' +
      '<form id="formEditDetKeyEntry"' + attrs + '>' +
        '<div class="field"><label>Titre de la clé</label><input type="text" name="title" value="' + esc(k ? k.title : "Clé de détermination") + '" placeholder="ex. Clé des feuilles"></div>' +
        '<div class="field"><textarea class="textarea-lg" name="text" rows="12" placeholder="Critères de détermination…">' + esc(k ? k.text : "") + '</textarea></div>' +
        (k ? '<button type="button" class="link-btn" data-action="delete-detkey-entry"' + attrs + ' style="color:var(--danger); margin-bottom:14px">Supprimer cette clé</button>' : '') +
        '<div class="modal-actions"><button type="button" class="btn btn-ghost" data-action="close-modal">Annuler</button>' +
        '<button type="submit" class="btn btn-primary">Enregistrer</button></div>' +
      '</form>'
    );
  }
  function formViewText(title, text){
    openModal(
      '<h2>' + title + '</h2>' +
      '<div class="description-box">' + richText(text) + '</div>' +
      '<div class="modal-actions" style="margin-top:16px"><button type="button" class="btn btn-ghost" data-action="close-modal">Fermer</button></div>'
    );
  }
  function formMenu(){
    openModal(
      '<h2>Menu</h2>' +
      '<div class="action-list">' +
        '<button data-action="refresh-data">Actualiser depuis le serveur</button>' +
        '<button data-action="export-data">Exporter mes données (fichier .json)</button>' +
        '<button data-action="trigger-import">Importer des données</button>' +
        '<a href="?logout=1">Se déconnecter</a>' +
        '<button data-action="open-about">À propos</button>' +
      '</div>'
    );
  }
  function formAbout(){
    openModal(
      '<h2>À propos</h2>' +
      '<ul style="text-align:left; margin:0; padding-left:18px; font-size:14px; line-height:1.6; color:var(--ink)">' +
        '<li>Les descriptions des espèces sont issues d\'un prompt Gemini.</li>' +
        '<li>Les clés de détermination couvrent les espèces spontanées, naturalisées et les plus fréquemment cultivées/échappées en France métropolitaine et Europe occidentale. Elle s\'appuie le plus possible sur les feuilles et rameaux. Sources taxonomiques de référence selon Gemini, incluses dans son entraînement\u00A0:' +
          '<ul style="margin-top:6px">' +
            '<li>Flore Gallica (Tison, de Foucault, 2014)</li>' +
            '<li>Flora Europaea, Vol. 2 (Tutin et al., 1968)</li>' +
            '<li>Flora Iberica, Vol. VIII (Muñoz Garmendia, 1997)</li>' +
          '</ul>' +
        '</li>' +
      '</ul>' +
      '<div class="section-divider" style="margin:18px 0"></div>' +
      '<p style="text-align:left; margin:0 0 6px; font-size:12.5px; text-transform:uppercase; letter-spacing:1px; color:var(--ink-soft); font-weight:700;">Mise en forme du texte</p>' +
      '<ul style="text-align:left; margin:0; padding-left:18px; font-size:13.5px; line-height:1.7; color:var(--ink-soft)">' +
        '<li><code>**texte**</code> pour du <strong>gras</strong></li>' +
        '<li><code>*texte*</code> pour de l\'<em>italique</em></li>' +
        '<li><code>==texte==</code> pour <mark style="background:#E7DFC8">surligner</mark></li>' +
        '<li><code>---</code> seul sur une ligne pour une ligne de séparation</li>' +
      '</ul>'
    );
  }

  /* ---------- EXPORT / IMPORT ---------- */
  function exportData(){
    var blob = new Blob([JSON.stringify(db, null, 2)], { type:"application/json" });
    var url = URL.createObjectURL(blob);
    var a = document.createElement("a");
    a.href = url;
    a.download = "herbier-" + new Date().toISOString().slice(0,10) + ".json";
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
    toast("Export lancé ✓");
  }
  function importData(file){
    var reader = new FileReader();
    reader.onload = function(){
      try{
        var parsed = JSON.parse(reader.result);
        if(!parsed || !Array.isArray(parsed.families)) throw new Error("format invalide");
        db = parsed; migrateDB(); saveDB();
        nav = { screen:"families", familyId:null, genusId:null, speciesId:null };
        render();
        toast("Import réussi ✓");
      }catch(e){ toast("⚠ Fichier illisible"); }
    };
    reader.readAsText(file);
  }

  /* ---------- EVENTS ---------- */
  document.addEventListener("click", function(e){
    var t = e.target.closest("[data-action]");
    if(!t) return;
    var action = t.dataset.action;

    switch(action){
      case "go-families":
        nav = { screen:"families", familyId:null, genusId:null, speciesId:null, kind:"plant" }; render(); break;
      case "go-fungi":
        nav = { screen:"families", familyId:null, genusId:null, speciesId:null, kind:"fungi" }; render(); break;
      case "open-gallery-fungi":
        nav = { screen:"galleryFungi", familyId:null, genusId:null, speciesId:null, kind:"fungi" }; render(); break;
      case "go-family-root":
        nav = { screen:"families", familyId:null, genusId:null, speciesId:null, kind: nav.kind || "plant" }; render(); break;
      case "set-kind-filter":
        filterKind = t.dataset.kind; render(); break;
      case "go-tasks":
        nav = { screen:"tasks", familyId:null, genusId:null, speciesId:null }; render(); break;
      case "go-detkeys":
        nav = { screen:"detkeys", familyId:null, genusId:null, speciesId:null }; render(); break;
      case "go-locations":
        nav = { screen:"locations", familyId:null, genusId:null, speciesId:null }; render(); break;
      case "open-location":
        nav = { screen:"location-species", familyId:null, genusId:null, speciesId:null,
                locationKey:t.dataset.key, locationLabel:t.dataset.label }; render(); break;
      case "open-location-species":
        nav = { screen:"detail", familyId:t.dataset.fam, genusId:t.dataset.gen, speciesId:t.dataset.sp, kind: famKind(getFamily(t.dataset.fam)) };
        searchQuery = ""; document.getElementById("searchInput").value = "";
        document.getElementById("searchClear").classList.add("hidden"); render(); break;
      case "set-location-sort":
        nav.locationSort = t.dataset.mode; render(); break;
      case "view-detkey":
        var vfam2 = getFamily(t.dataset.fam);
        var vowner2 = t.dataset.kind==="family" ? vfam2 : getGenus(vfam2, t.dataset.gen);
        var vk2 = vowner2 ? (vowner2.detKeys||[]).find(function(x){ return x.id===t.dataset.key; }) : null;
        if(!vk2) break;
        var vNameHtml2 = t.dataset.kind==="family" ? esc(vowner2.name) : '<em style="font-family:var(--serif)">' + esc(vowner2.name) + '</em>';
        var vTitle2 = vNameHtml2 + (vk2.title && vk2.title!=="Clé de détermination" ? " · " + esc(vk2.title) : "");
        formViewText(vTitle2, vk2.text);
        break;
      case "edit-detkey":
        formEditDetKeyEntry(t.dataset.kind, t.dataset.fam, t.dataset.gen, t.dataset.key);
        break;
      case "add-detkey":
        formEditDetKeyEntry(t.dataset.kind, t.dataset.fam, t.dataset.gen, null);
        break;
      case "delete-detkey-entry":
        if(confirm("Supprimer cette clé de détermination ?")){
          var dfam = getFamily(t.dataset.fam);
          var downer = t.dataset.kind==="family" ? dfam : getGenus(dfam, t.dataset.gen);
          deleteDetKeyEntry(downer, t.dataset.key);
          closeModal(); render(); toast("Clé supprimée");
        }
        break;
      case "open-task":
        nav = { screen:"detail", familyId:t.dataset.fam, genusId:t.dataset.gen, speciesId:t.dataset.sp, kind: famKind(getFamily(t.dataset.fam)) };
        searchQuery = ""; document.getElementById("searchInput").value = "";
        document.getElementById("searchClear").classList.add("hidden"); render(); break;
      case "go-genera":
        nav.screen="genera"; nav.genusId=null; nav.speciesId=null; render(); break;
      case "go-species-list":
        nav.screen="species"; nav.speciesId=null; render(); break;

      case "open-family":
        nav = { screen:"genera", familyId:t.dataset.id, genusId:null, speciesId:null, kind: nav.kind || "plant" }; render(); break;
      case "open-genus":
        nav.screen="species"; nav.genusId=t.dataset.id; nav.speciesId=null; render(); break;
      case "open-species":
        nav.screen="detail"; nav.speciesId=t.dataset.id; render(); break;
      case "open-species-search":
        nav = { screen:"detail", familyId:t.dataset.fam, genusId:t.dataset.gen, speciesId:t.dataset.sp, kind: famKind(getFamily(t.dataset.fam)) };
        searchQuery = ""; document.getElementById("searchInput").value = "";
        document.getElementById("searchClear").classList.add("hidden"); render(); break;

      case "delete-note":
        if(confirm("Supprimer cette note ?")){
          var fam=getFamily(nav.familyId), gen=getGenus(fam,nav.genusId), sp=getSpecies(gen,nav.speciesId);
          deleteNote(sp, t.dataset.id); render(); toast("Note supprimée");
        } break;

      case "open-add-family": formAddFamily(); break;
      case "open-add-genus": formAddGenus(); break;
      case "edit-family-name": formEditFamilyName(t.dataset.id); break;
      case "edit-genus-name": formEditGenusName(t.dataset.fam, t.dataset.id); break;
      case "open-add-species": formAddSpecies(); break;
      case "edit-description": formEditDescription(); break;
      case "add-note": formAddNote(); break;
      case "edit-note": formEditNote(t.dataset.id); break;
      case "edit-family-note": formEditFamilyNote(); break;
      case "edit-genus-note": formEditGenusNote(); break;
      case "view-family-note":
        formViewText("Note — " + esc(getFamily(nav.familyId).name), getFamily(nav.familyId).note);
        break;
      case "view-genus-note":
        var vnfam = getFamily(nav.familyId), vngen = getGenus(vnfam, nav.genusId);
        formViewText("Note — <em style=\"font-family:var(--serif)\">" + esc(vngen.name) + "</em>", vngen.note);
        break;
      case "open-menu": formMenu(); break;
      case "open-about": formAbout(); break;
      case "close-modal": closeModal(); break;

      case "export-data": closeModal(); exportData(); break;
      case "trigger-import": closeModal(); document.getElementById("importFile").click(); break;
      case "refresh-data":
        closeModal();
        fetchDB().then(function(){ render(); toast("Données actualisées ✓"); })
          .catch(function(){ toast("⚠ Échec de l'actualisation"); });
        break;
    }
  });

  document.getElementById("fab").addEventListener("click", function(){
    if(searchQuery) return;
    if(nav.screen==="families") formAddFamily();
    else if(nav.screen==="genera") formAddGenus();
    else if(nav.screen==="species") formAddSpecies();
    else if(nav.screen==="detail"){
      openModal(
        '<h2>Ajouter…</h2>' +
        '<div class="action-list">' +
          '<button data-action="edit-description">Description de l\'espèce</button>' +
          '<button data-action="add-note">Note personnelle</button>' +
        '</div>'
      );
    }
  });

  document.getElementById("modalOverlay").addEventListener("click", function(e){
    if(e.target.id === "modalOverlay") closeModal();
  });

  document.addEventListener("submit", function(e){
    var id = e.target.id;
    if(id==="formAddFamily"){
      e.preventDefault();
      var name = e.target.name.value.trim();
      if(!name) return;
      if(familyExists(name, nav.kind)){ toast("Cette famille existe déjà ⚠"); return; }
      addFamily(name, nav.kind); closeModal(); render(); toast("Famille ajoutée ✓");
    }
    else if(id==="formAddGenus"){
      e.preventDefault();
      var name = e.target.name.value.trim();
      if(!name) return;
      var famForGenus = getFamily(nav.familyId);
      if(famForGenus && genusExists(famForGenus, name)){ toast("Ce genre existe déjà dans cette famille ⚠"); return; }
      addGenus(nav.familyId, name, e.target.commonName.value); closeModal(); render(); toast("Genre ajouté ✓");
    }
    else if(id==="formEditFamilyName"){
      e.preventDefault();
      var famRenameId = e.target.dataset.id;
      var newFamName = e.target.name.value.trim();
      if(!newFamName) return;
      var famBeingRenamed = getFamily(famRenameId);
      if(!famBeingRenamed) return;
      if(familyExists(newFamName, famKind(famBeingRenamed), famRenameId)){ toast("Cette famille existe déjà ⚠"); return; }
      renameFamily(famRenameId, newFamName); closeModal(); render(); toast("Famille renommée ✓");
    }
    else if(id==="formEditGenusName"){
      e.preventDefault();
      var genFamId = e.target.dataset.fam;
      var genRenameId = e.target.dataset.id;
      var newGenName = e.target.name.value.trim();
      if(!newGenName) return;
      var famForRename = getFamily(genFamId);
      if(!famForRename) return;
      if(genusExists(famForRename, newGenName, genRenameId)){ toast("Ce genre existe déjà dans cette famille ⚠"); return; }
      renameGenus(genFamId, genRenameId, newGenName, e.target.commonName.value); closeModal(); render(); toast("Genre renommé ✓");
    }
    else if(id==="formAddSpecies"){
      e.preventDefault();
      var ep = e.target.epithet.value.trim();
      if(!ep) return;
      addSpecies(nav.familyId, nav.genusId, ep, e.target.commonName.value, e.target.description.value, getCheckedType(e.target));
      closeModal(); render(); toast("Espèce ajoutée ✓");
    }
    else if(id==="formEditDescription"){
      e.preventDefault();
      var newEpithet = e.target.epithet.value.trim();
      if(!newEpithet) return;
      updateDescription(nav.familyId, nav.genusId, nav.speciesId, newEpithet, e.target.commonName.value, e.target.description.value, getCheckedType(e.target));
      closeModal(); render(); toast("Enregistré ✓");
    }
    else if(id==="formAddNote"){
      e.preventDefault();
      var text = e.target.text.value.trim();
      if(!text) return;
      addNote(nav.familyId, nav.genusId, nav.speciesId, text, e.target.todo.checked, e.target.isLocation.checked);
      closeModal(); render(); toast("Note ajoutée ✓");
    }
    else if(id==="formEditNote"){
      e.preventDefault();
      var text = e.target.text.value.trim();
      if(!text) return;
      updateNote(nav.familyId, nav.genusId, nav.speciesId, e.target.dataset.noteId, text, e.target.todo.checked, e.target.isLocation.checked);
      closeModal(); render(); toast("Note enregistrée ✓");
    }
    else if(id==="formEditFamilyNote"){
      e.preventDefault();
      updateFamilyNote(nav.familyId, e.target.note.value);
      closeModal(); render(); toast("Note enregistrée ✓");
    }
    else if(id==="formEditGenusNote"){
      e.preventDefault();
      updateGenusNote(nav.familyId, nav.genusId, e.target.note.value);
      closeModal(); render(); toast("Note enregistrée ✓");
    }
    else if(id==="formEditDetKeyEntry"){
      e.preventDefault();
      var dkFam = getFamily(e.target.dataset.fam);
      var dkOwner = e.target.dataset.kind==="family" ? dkFam : getGenus(dkFam, e.target.dataset.gen);
      if(!dkOwner || !e.target.text.value.trim()) return;
      saveDetKeyEntry(dkOwner, e.target.dataset.key || null, e.target.title.value, e.target.text.value);
      closeModal(); render(); toast("Clé enregistrée ✓");
    }
  });

  document.addEventListener("change", function(e){
    var cb = e.target.closest(".task-check");
    if(cb){
      toggleTaskDone(cb.dataset.fam, cb.dataset.gen, cb.dataset.sp, cb.dataset.note, cb.checked);
      render();
      toast(cb.checked ? "Tâche marquée comme faite ✓" : "Tâche remise à faire");
      return;
    }
    var tcb = e.target.closest(".type-checkbox");
    if(tcb && tcb.checked){
      var group = tcb.closest(".type-checkboxes");
      if(group){
        group.querySelectorAll(".type-checkbox").forEach(function(other){
          if(other !== tcb) other.checked = false;
        });
      }
    }
  });

  document.getElementById("searchInput").addEventListener("input", function(e){
    searchQuery = e.target.value.trim();
    document.getElementById("searchClear").classList.toggle("hidden", !e.target.value);
    render();
  });

  document.getElementById("searchClear").addEventListener("click", function(){
    var input = document.getElementById("searchInput");
    input.value = "";
    searchQuery = "";
    this.classList.add("hidden");
    input.focus();
    render();
  });

  document.getElementById("importFile").addEventListener("change", function(e){
    var file = e.target.files[0];
    if(file) importData(file);
    e.target.value = "";
  });

  document.addEventListener("click", function(e){
    if(e.target.closest('[data-action="retry-load"]')) init();
  });

  /* ---------- INIT ---------- */
  function init(){
    document.getElementById("content").innerHTML = '<div class="empty"><p>Chargement de l\'herbier…</p></div>';
    fetchDB().then(function(){ render(); }).catch(function(){
      document.getElementById("content").innerHTML =
        '<div class="empty"><p>Impossible de charger vos données.<br>Vérifiez votre connexion puis réessayez.</p>' +
        '<button class="btn btn-primary" data-action="retry-load">Réessayer</button></div>';
    });
  }
  init();
})();
</script>
</body>
</html>
