/* eslint-disable */
// Encepagement — bottom-center glass panel that lists the grape varieties
// of the active château with the real CMGC color tokens.
// When no château is active, shows a folded "tap a château" hint.

const ENC_CEPAGE_TO_GRAPE = {
  cabernetsauvignon: "cabernet-sauvignon",
  merlot: "merlot",
  cabernetfranc: "cab-franc",
  petitverdot: "verdot",
  malbec: "malbec",
  cot: "cot",
  chardonnay: "chardonnay",
  sauvignonblanc: "sauvignon-blanc",
  sauvignongris: "sauvignon-gris",
  semillon: "semillon",
  muscadelle: "muscadelle",
  jacheres: "jacheres",
  carmenere: "carmenere",
  grenache: "grenache",
  furmint: "furmint",
  harslevelu: "harslevelu",
  zeta: "zeta",
  zinfandel: "zinfandel",
  viognier: "viognier",
  oldvines: "old-vines",
  grosmanseng: "gros-manseng",
  colombard: "colombard"
};

const ENC_PARCEL_ALIAS = {
  "lhb":      "larrivet-haut-brion",
  "shl":      "smith-haut-lafitte",
  "gcd":      "grand-corbin-despagne",
  "sociando": "sociando-mallet",
};

function encNormToken(raw) {
  return String(raw || "").
  normalize("NFD").replace(/[\u0300-\u036f]/g, "").
  toLowerCase();
}

// White grapes — used to split the cépages display into Rouge / Blanc.
// Anything not in here is treated as red. "jacheres" (fallow) and
// "old-vines" are technical categories, not real grapes — we keep them
// out of either group at the call site.
const ENC_WHITE_GRAPES = new Set([
"sauvignon-blanc", "sauvignon-gris", "semillon", "chardonnay",
"muscadelle", "harslevelu", "zeta", "furmint",
"gros-manseng", "viognier", "colombard"]
);
const ENC_NON_VARIETAL = new Set(["jacheres", "old-vines"]);

function encParseCepages(raw) {
  const tokens = encNormToken(raw).split(/[\/,;|\s]+/).filter(Boolean);
  const out = [];
  for (const t of tokens) {
    let key = ENC_CEPAGE_TO_GRAPE[t];
    if (!key && t === "cabernet") key = "cabernet-sauvignon";
    if (key && !out.includes(key)) out.push(key);
  }
  return out;
}

// ----- Bottle -----
// Cascade de packshots, du plus précis au plus générique :
//   1. assets/bottles/<slug>-<millésime>.png   (packshot du millésime affiché)
//   2. assets/bottles/<slug>.png               (packshot neutre du château)
//   3. bouteille dessinée (SVG)
// → Pour ajouter/mettre à jour une bouteille : déposez simplement un fichier
//   PNG nommé <slug>.png (ou <slug>-2022.png) dans assets/bottles/.
//   Aucune modification de code n'est nécessaire.

function BottlePhoto({ src, alt, onError }) {
  return (
    <img
      className="ec-bottle ec-bottle-photo"
      src={src}
      alt={alt}
      loading="lazy"
      onError={onError} />
  );
}

function BottleSVG({ chateau }) {
  if (!chateau) return null;
  const uid = "b-" + chateau.c;
  return (

    <svg
      className="ec-bottle"
      viewBox="0 0 48 120"
      xmlns="http://www.w3.org/2000/svg"
      aria-hidden="true">
      <defs>
        <linearGradient id={"glass-" + uid} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stopColor="#0E1A2A" />
          <stop offset="35%" stopColor="#1B2D40" />
          <stop offset="55%" stopColor="#0F1F2E" />
          <stop offset="100%" stopColor="#080F19" />
        </linearGradient>
        <linearGradient id={"cap-" + uid} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stopColor="#5C2F0A" />
          <stop offset="45%" stopColor="#A56326" />
          <stop offset="60%" stopColor="#C97D34" />
          <stop offset="100%" stopColor="#4A2406" />
        </linearGradient>
        <clipPath id={"label-" + uid}>
          <rect x="9.5" y="62" width="29" height="34" rx="1" />
        </clipPath>
      </defs>
      {/* Glass silhouette — bordelaise: épaule franche, col droit */}
      <path
        d="M 19 2.5 Q 19 1 21 1 L 27 1 Q 29 1 29 2.5
           L 29 9 L 30 13 L 30 33
           Q 30 37 33.5 41 Q 38 46 38 50
           L 38 115 Q 38 119 34 119 L 14 119 Q 10 119 10 115
           L 10 50 Q 10 46 14.5 41 Q 18 37 18 33
           L 18 13 L 19 9 Z"










        fill={"url(#glass-" + uid + ")"}
        stroke="rgba(0,0,0,0.55)"
        strokeWidth="0.4" />
      {/* Glass highlights */}
      <path
        d="M 13 52 L 13 110 Q 13 113 14 114"
        stroke="rgba(247,245,239,0.22)"
        strokeWidth="1.1"
        fill="none"
        strokeLinecap="round" />
      <path
        d="M 35 55 L 35 108"
        stroke="rgba(0,0,0,0.32)"
        strokeWidth="0.7"
        fill="none"
        strokeLinecap="round" />
      {/* Foil cap */}
      <path
        d="M 17.5 2 L 17.5 14.5 L 30.5 14.5 L 30.5 2
           Q 30.5 0 28.5 0 L 19.5 0 Q 17.5 0 17.5 2 Z"


        fill={"url(#cap-" + uid + ")"} />
      <line x1="17.5" y1="13.6" x2="30.5" y2="13.6"
      stroke="rgba(0,0,0,0.45)" strokeWidth="0.6" />
      <line x1="19" y1="2" x2="19" y2="13"
      stroke="rgba(255,225,180,0.32)" strokeWidth="0.5" />
      {/* Label */}
      <rect x="9.5" y="62" width="29" height="34" rx="1"
      fill="#F2EEE3" stroke="rgba(0,0,0,0.28)" strokeWidth="0.3" />
      <line x1="11" y1="65.5" x2="37" y2="65.5"
      stroke="rgba(155,96,26,0.55)" strokeWidth="0.4" />
      <line x1="11" y1="92.5" x2="37" y2="92.5"
      stroke="rgba(155,96,26,0.55)" strokeWidth="0.4" />
      {/* Château logo on the label (muted so it reads as printed ink) */}
      <image
        href={"assets/chateau-logos/" + chateau.l}
        x="11" y="67" width="26" height="22"
        clipPath={"url(#label-" + uid + ")"}
        preserveAspectRatio="xMidYMid meet"
        style={{ filter: "saturate(.6) contrast(1.15) brightness(.55)" }} />
    </svg>);

}

// Composant principal. Cascade : packshot local du millésime → packshot local
// neutre → packshot officiel VinCod (CDN vin.co) → bouteille dessinée (SVG).
function Bottle({ chateau, year }) {
  const [step, setStep] = React.useState(0);
  const srcs = React.useMemo(() => {
    if (!chateau) return [];
    const list = [];
    if (year) list.push("assets/bottles/" + chateau.c + "-" + year + ".png");
    list.push("assets/bottles/" + chateau.c + ".png");
    const VC = window.CMGC_VINCOD;
    const remote = VC && VC.packshotFor(chateau);
    if (remote) list.push(remote);
    return list;
  }, [chateau && chateau.c, year]);
  React.useEffect(() => {setStep(0);}, [srcs]);
  if (!chateau) return null;
  if (step >= srcs.length) return <BottleSVG chateau={chateau} />;
  return (
    <BottlePhoto
      src={srcs[step]}
      alt={chateau.n + (year ? " " + year : "")}
      onError={() => setStep((s) => s + 1)} />
  );
}

function Encepagement({ chateau, mode, year, onPickChateau, onOpenFiche, lang }) {
  const { GRAPES, GRAPE_LABELS } = window.CMGC_DATA;
  const TERROIRS = window.CMGC_TERROIRS || {};
  const WINES = window.CMGC_WINES || null;
  const ML = window.CMGC_MILLESIMES || null;
  const VC = window.CMGC_VINCOD || null;
  const FI = window.CMGC_FICHES || null;
  const AS = window.CMGC_ASSETS || null;
  // Bloc « Fiches techniques » — replié par défaut
  const [fichesOpen, setFichesOpen] = React.useState(false);
  // Dossier technique (contenu VinCod réel) — replié par défaut
  const [dossierOpen, setDossierOpen] = React.useState(false);
  // Contenu rédactionnel réel du château, chargé à la demande (content/<id>.json)
  const [fiche, setFiche] = React.useState(null);
  const [blends, setBlends] = React.useState([]); // [[grapeA, grapeB], ...]
  const [exclusiveBlend, setExclusiveBlend] = React.useState(new Set()); // grapes only in compound parcels
  // For sols / sous-sols modes: list of {key, count, parcelIndexes} computed
  // from the loaded parcels by walking the terroirs resolver.
  const [solsTally, setSolsTally] = React.useState([]);
  const [sousSolsTally, setSousSolsTally] = React.useState([]);
  // Whether the loaded parcels carry per-feature sol/sousSol properties
  // (real pedological survey) vs. synthetic defaults resolved from the
  // appellation / château. Drives the "données synthétiques" caption.
  const [realParcelSols, setRealParcelSols] = React.useState(false);
  // Solo-cepage tally from parcels — used to surface white grapes that
  // are missing from chateau.e (e.g. Pessac-Léognan estates often have
  // a small white production whose data.js entry only lists the red
  // blend). Map of grapeKey -> feature count.
  const [parcelGrapes, setParcelGrapes] = React.useState(new Map());
  // Plus d'infos toggle (sols/sous-sols only — collapses by default)
  const [solDetailOpen, setSolDetailOpen] = React.useState(false);
  // Collapse toggle — démarre replié sur mobile, déplié sur desktop
  const [collapsed, setCollapsed] = React.useState(() => window.innerWidth <= 640);
  // Réinitialise le collapse quand le château change
  React.useEffect(() => { setCollapsed(window.innerWidth <= 640); }, [chateau && chateau.c]);

  React.useEffect(() => {
    setBlends([]);
    setExclusiveBlend(new Set());
    setSolsTally([]);
    setSousSolsTally([]);
    setRealParcelSols(false);
    setParcelGrapes(new Map());
    if (!chateau) return;
    const id = ENC_PARCEL_ALIAS[chateau.c] || chateau.c;
    let cancelled = false;
    fetch(`parcels/${id}.geojson`).
    then((r) => r.ok ? r.json() : null).
    then((gj) => {
      if (cancelled || !gj || !Array.isArray(gj.features)) return;
      const seen = new Set();
      const pairs = [];
      const soloSet = new Set();
      const compoundSet = new Set();
      // Per-grape solo-feature counts — used to surface missing white
      // cépages and to compute a pct when chateau.e is silent.
      const soloCount = new Map();
      // sols / sous-sols accumulators
      const solsCount = new Map();
      const sousSolsCount = new Map();
      // Track whether ANY feature carries explicit sol data — if so we
      // consider the dataset "real" and drop the synthetic-defaults footnote.
      let anyRealSol = false;
      // Fichiers « mixtes » (Pavie, Marquis de Terme, Kirwan) : des Features
      // de cépage ET des Features de sol, deux découpages du même terrain.
      // Une Feature de cépage n'est pas une parcelle sans sol renseigné —
      // c'est une parcelle qui appartient à l'AUTRE lecture. La résoudre
      // quand même la faisait retomber sur le défaut d'appellation, et la
      // légende de Kirwan annonçait « 6 types » dont des graves günziennes
      // que le domaine ne porte pas. Même règle que `applyParcelColors`
      // dans MapStage.jsx, qui masque déjà ces Features à l'affichage.
      const aCepage = gj.features.some((f) => f.properties && f.properties.cepage);
      const aSol = gj.features.some((f) => f.properties && (f.properties.sol || f.properties.sousSol));
      const mixte = aCepage && aSol;
      let nSol = 0, nSousSol = 0;
      gj.features.forEach((f, idx) => {
        if (f.properties && (f.properties.sol || f.properties.sousSol)) anyRealSol = true;
        // cépages
        const cs = encParseCepages(f.properties && f.properties.cepage);
        if (cs.length === 1) {
          soloSet.add(cs[0]);
          soloCount.set(cs[0], (soloCount.get(cs[0]) || 0) + 1);
        } else if (cs.length >= 2) {
          cs.forEach((c) => compoundSet.add(c));
          const key = cs[0] + "|" + cs[1];
          if (!seen.has(key)) {seen.add(key);pairs.push([cs[0], cs[1]]);}
        }
        // sols / sous-sols (only if resolver available)
        const sienne = !mixte || (f.properties && (f.properties.sol || f.properties.sousSol));
        if (TERROIRS.resolveForParcel && sienne) {
          const r = TERROIRS.resolveForParcel(f, chateau, idx);
          if (r.sol) { solsCount.set(r.sol, (solsCount.get(r.sol) || 0) + 1); nSol++; }
          if (r.sousSol) { sousSolsCount.set(r.sousSol, (sousSolsCount.get(r.sousSol) || 0) + 1); nSousSol++; }
        }
      });
      const onlyBlend = new Set();
      compoundSet.forEach((c) => {if (!soloSet.has(c)) onlyBlend.add(c);});
      setBlends(pairs);
      setExclusiveBlend(onlyBlend);
      setParcelGrapes(soloCount);
      setRealParcelSols(anyRealSol);
      // Les parts se calculent sur les Features effectivement comptées, et
      // non sur le fichier entier : sur un fichier mixte, diviser par le
      // total incluait les Features de cépage et les parts ne faisaient plus
      // 100 % — Kirwan affichait cinq sols totalisant 45 %.
      const totSol = nSol || gj.features.length || 1;
      const totSousSol = nSousSol || gj.features.length || 1;
      setSolsTally(
        [...solsCount.entries()].
        map(([key, count]) => ({ key, count, pct: Math.round(100 * count / totSol) })).
        sort((a, b) => b.count - a.count)
      );
      setSousSolsTally(
        [...sousSolsCount.entries()].
        map(([key, count]) => ({ key, count, pct: Math.round(100 * count / totSousSol) })).
        sort((a, b) => b.count - a.count)
      );
    }).
    catch(() => {});
    return () => {cancelled = true;};
  }, [chateau && chateau.c]);

  // ----- Lien parent / related -----
  const allChateaux = window.CMGC_DATA?.CHATEAUX || [];
  const relatedBadge = React.useMemo(() => {
    if (!chateau) return null;
    const links = [];
    if (chateau.related) {
      chateau.related.forEach(id => {
        const ch = allChateaux.find(c => c.c === id);
        if (ch) links.push({ ch, label: "Inclut" });
      });
    }
    if (chateau.parent) {
      const ch = allChateaux.find(c => c.c === chateau.parent);
      if (ch) links.push({ ch, label: "Appartient à" });
    }
    return links.length ? links : null;
  }, [chateau && chateau.c]);

  // Millésimes documentés seulement par une fiche PDF déposée dans le repo :
  // ils n'existent pas dans l'export VinCod, donc pas de page en ligne.
  // NB: ce Hook doit être appelé à chaque rendu (avant le "if (!chateau)
  // return null" plus bas) pour ne jamais changer le nombre de Hooks entre
  // deux rendus — sinon React casse tout l'arbre (page blanche).
  const archiveYears = React.useMemo(() => {
    if (!AS || !chateau) return [];
    const fichesLocal = VC ? VC.forChateau(chateau) : null;
    const online = new Set();
    if (fichesLocal) {
      [].concat(fichesLocal.grands || [], fichesLocal.cuvees || []).forEach((w) =>
      (w.years || []).forEach((yy) => online.add(String(yy.year))));
    }
    const all = AS.ftYears(chateau.c);
    return Object.keys(all).
    filter((y) => !online.has(y)).
    sort((a, b) => Number(b) - Number(a)).
    map((y) => [y, all[y]]);
  }, [chateau && chateau.c]);

  React.useEffect(() => {setSolDetailOpen(false);}, [chateau && chateau.c, mode]);
  React.useEffect(() => {setFichesOpen(false);setDossierOpen(false);}, [chateau && chateau.c]);

  // Contenu VinCod réel : un fichier par château, chargé au premier affichage.
  React.useEffect(() => {
    if (!FI || !chateau) {setFiche(null);return;}
    let cancelled = false;
    FI.load(chateau.c).then((doc) => {if (!cancelled) setFiche(doc);});
    return () => {cancelled = true;};
  }, [FI, chateau && chateau.c]);

  const T = {
    fr: { rouge:"Rouge", blanc:"Blanc", complante:"Parcelles complantées", terroir:"Terroir", solsMapped:"Sols cartographiés à la parcelle", solsAppellation:"Répartition par appellation", noData:"Donnée non renseignée pour ce château.", synthetic:"Données synthétiques par appellation — en attente de relevés parcellaires.", apogee:"Apogée", vintageScores:"Notes du millésime", estateScore:"Note château", appScore:"Note d’appellation", profile:"Caractère du millésime", techSheet:"Fiche technique", noVintage:(y)=>`Donnée millésime non renseignée pour ${y}.`, pairings:"Accords mets & vin", belongsTo:"Appartient à", includes:"Inclut", expand:"Développer", collapse:"Réduire",
      present:"présent", blendYear:(y)=>`assemblage ${y}`, dossier:"Dossier technique", dossierSrc:"Données du château — export VinCod", terroirL:"Terroir", vigneL:"À la vigne", vinifL:"Vinification", elevageL:"Élevage", cepagesL:"Encépagement du vin", presentationL:"Le vin", accordsL:"Accords", labelsL:"Certifications", specTav:"Alcool", specRdt:"Rendement", specHa:"Superficie", specVol:"Production", specAge:"Âge des vignes", specFut:"Élevage en fût", specFrom:(y)=>`d\u2019après le millésime ${y}`, frOnly:"Contenu disponible en français", fullPage:"Fiche complète",kit:"Kit média",planche:"Planche parcellaire",fiches:"Fiches techniques", fichesCount:(n)=>`${n} millésime${n>1?"s":""} en ligne`, archives:"Millésimes en PDF", etiquette:"Étiquette", grandVin:"Grand vin", grandsVins:"Grands vins", cuvees:"Autres cuvées", pdf:"PDF",
      fichePage:(w,y)=>`Fiche technique ${w} ${y} sur vincod.com`, fichePdf:(w,y)=>`Fiche technique imprimable ${w} ${y} (PDF)`,
      ficheSearch:(w,y)=>`Rechercher la fiche ${w} ${y} sur vincod.com (code incomplet)` },
    en: { rouge:"Red", blanc:"White", complante:"Inter-planted parcels", terroir:"Terroir", solsMapped:"Parcel-mapped soils", solsAppellation:"Appellation averages", noData:"No data for this château.", synthetic:"Synthetic appellation data — awaiting parcel survey.", apogee:"Peak drinking", vintageScores:"Vintage scores", estateScore:"Estate score", appScore:"Appellation score", profile:"Vintage character", techSheet:"Technical sheet", noVintage:(y)=>`No vintage data for ${y}.`, pairings:"Food pairings", belongsTo:"Belongs to", includes:"Includes", expand:"Expand", collapse:"Collapse",
      present:"present", blendYear:(y)=>`${y} blend`, dossier:"Technical dossier", dossierSrc:"Estate data — VinCod export", terroirL:"Terroir", vigneL:"In the vineyard", vinifL:"Winemaking", elevageL:"Ageing", cepagesL:"Blend", presentationL:"The wine", accordsL:"Pairings", labelsL:"Certifications", specTav:"Alcohol", specRdt:"Yield", specHa:"Area", specVol:"Production", specAge:"Vine age", specFut:"Barrel ageing", specFrom:(y)=>`from the ${y} vintage`, frOnly:"Content available in French", fullPage:"Full page",kit:"Media kit",planche:"Parcel plate",fiches:"Technical sheets", fichesCount:(n)=>`${n} vintage${n>1?"s":""} online`, archives:"Vintages as PDF", etiquette:"Label", grandVin:"Grand vin", grandsVins:"Grands vins", cuvees:"Other cuvées", pdf:"PDF",
      fichePage:(w,y)=>`${w} ${y} technical sheet on vincod.com`, fichePdf:(w,y)=>`${w} ${y} printable technical sheet (PDF)`,
      ficheSearch:(w,y)=>`Search the ${w} ${y} sheet on vincod.com (incomplete code)` },
  };
  const t = T[lang] || T.fr;

  if (!chateau) return null;

  // ----- Wine fiche (style, tasting, garde, notes, pairings) -----
  const wine = WINES ? WINES.forChateau(chateau, mode === "millesimes" ? year : null, lang) : null;

  // ----- Vintage badge (millésimes mode only) -----
  const vintage = mode === "millesimes" && ML ? ML.vintageForChateau(chateau, year) : null;
  const vintageColor = vintage ? ML.colorForNote(vintage.note) : null;

  // ----- Fiches techniques VinCod -----
  // Code du grand vin pour le millésime affiché → fiche vincod.com directe
  // + version imprimable (PDF). Si le millésime n'a pas de fiche, le lien
  // retombe sur une recherche vincod pré-remplie.
  const vincodCode = VC ? VC.codeFor(chateau, year) : null;
  const vincodExact = !!(VC && VC.isExact(vincodCode));
  const vincodLabel = VC ? VC.labelFor(chateau, year) : chateau.n;
  const vincodPage = VC ?
  VC.pageUrl(vincodCode, vincodLabel, year) :
  `https://www.google.com/search?q=${encodeURIComponent(`site:vincod.com "${chateau.n}" ${year}`)}`;
  const vincodPdf = VC ? VC.pdfUrl(vincodCode) : null;

  // Toutes les fiches du château : grands vins + seconds vins / autres cuvées.
  const fiches = VC ? VC.forChateau(chateau) : null;
  const fichesCount = VC ? VC.countFor(chateau) : 0;

  // Liste chronologique unique par vin : millésimes en ligne (VinCod, avec
  // lien direct vers la page + un badge PDF si une version imprimable
  // existe) ET millésimes uniquement documentés par une fiche PDF déposée
  // dans le repo (archiveExtra), fondus dans le même fil au lieu de deux
  // blocs séparés — plus lisible que "Grand vin" / "Millésimes en PDF".
  const renderFicheYears = (wineLabel, years, archiveExtra) => {
    const combined = years.map(({ year: y, code }) => ({ y, code, archive: null }));
    (archiveExtra || []).forEach(([y, langs]) => combined.push({ y, code: null, archive: langs }));
    combined.sort((a, b) => Number(b.y) - Number(a.y));
    return (
      <ul className="ec-fiches-years">
        {combined.map((item) => {
        if (item.code) {
          const exact = VC.isExact(item.code);
          const pdf = VC.pdfUrl(item.code);
          return (
            <li key={item.y}>
              <a
                className={"ec-fiche-pill" + (exact ? "" : " approx") + (String(item.y) === String(year) ? " current" : "")}
                href={VC.pageUrl(item.code, wineLabel, item.y)}
                target="_blank"
                rel="noopener noreferrer"
                title={exact ? t.fichePage(wineLabel, item.y) : t.ficheSearch(wineLabel, item.y)}>
                {item.y}
                {pdf && <i className="ec-fy-pdf" title={t.fichePdf(wineLabel, item.y)}>{t.pdf}</i>}
              </a>
            </li>);

        }
        return item.archive.map((lg) =>
        <li key={item.y + lg}>
              <a
              className="ec-fiche-pill ec-fiche-archive"
              href={"docs/ft/" + chateau.c + "-" + item.y + "-" + lg + ".pdf"}
              target="_blank"
              rel="noopener noreferrer"
              title={(lang === "en" ? "PDF sheet " : "Fiche PDF ") + item.y + " — " + lg.toUpperCase()}>
                {item.y}<i className="ec-fy-lang">{lg.toUpperCase()}</i>
              </a>
            </li>);

      })}
      </ul>);

  };

  // Un vin = son libellé (masqué s'il n'y a qu'un seul grand vin) + ses millésimes
  const renderFicheWine = (w, showLabel, archiveExtra) =>
  <div className="ec-fiches-cuvee" key={w.label}>
      {showLabel &&
    <span className="ec-fiches-cuvee-name">
          {w.label}
          {w.color && <em className="ec-fiches-color">{w.color}</em>}
        </span>
    }
      {renderFicheYears(w.label, w.years, archiveExtra)}
    </div>;

  // Les liens vers la fiche complète / planche / kit média sont l'accès
  // principal au reste du contenu du château : ils restent visibles en
  // permanence, indépendamment du dépliant qui liste les millésimes
  // (autrement il fallait d'abord ouvrir "Fiches techniques" et faire
  // défiler la liste pour tomber dessus).
  // Deux arbres de pages statiques distincts (build_pages.py), pas une seule
  // page bilingue : en anglais, tout lien direct vers la fiche complète doit
  // pointer sur chateau/<id>/en/, pas sur la version française.
  const ficheBase = "chateau/" + chateau.c + "/" + (lang === "en" ? "en/" : "");
  const FichesFoot =
  <div className="ec-fiches-foot">
      <button
      type="button"
      className="ec-fiches-page ec-fiches-page-btn primary"
      onClick={() => onOpenFiche ?
      onOpenFiche(chateau.c) :
      window.location.assign(ficheBase)}
    >
        {t.fullPage}
      </button>
      <a className="ec-fiches-page" href={ficheBase + "#planche"}>
        {t.planche}
      </a>
      <a className="ec-fiches-page" href={ficheBase + "#kit"}>
        {t.kit}
      </a>
    </div>;

  const hasFiches = fiches && fichesCount > 0;
  const FichesTechniques =
  <div className="ec-fiches">
      {hasFiches &&
    <div className="ec-fiches-vintages">
          <button
        className={"ec-fiches-toggle" + (fichesOpen ? " open" : "")}
        onClick={() => setFichesOpen((o) => !o)}
        aria-expanded={fichesOpen}>
            <span className="ec-fiches-title">{t.fiches}</span>
            <span className="ec-fiches-count">{t.fichesCount(fichesCount)}</span>
            <svg viewBox="0 0 12 8" width="11" height="7" aria-hidden="true">
              <path
          d={fichesOpen ? "M1 6.5L6 1.5L11 6.5" : "M1 1.5L6 6.5L11 1.5"}
          stroke="currentColor" strokeWidth="1.5" fill="none"
          strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </button>
          {fichesOpen &&
      <div className="ec-fiches-body">
              {fiches.grands.length > 0 &&
        <div className="ec-fiches-group">
                  <span className="ec-fiches-label">
                    {fiches.grands.length > 1 ? t.grandsVins : t.grandVin}
                  </span>
                  {fiches.grands.map((w, i) => renderFicheWine(w, fiches.grands.length > 1, i === 0 ? archiveYears : null))}
                </div>
        }
              {fiches.cuvees.length > 0 &&
        <div className="ec-fiches-group">
                  <span className="ec-fiches-label">{t.cuvees}</span>
                  {fiches.cuvees.map((w) => renderFicheWine(w, true))}
                </div>
        }
              {fiches.grands.length === 0 && archiveYears.length > 0 &&
        <div className="ec-fiches-group">
                  <span className="ec-fiches-label">{t.grandVin}</span>
                  {renderFicheYears(chateau.n, [], archiveYears)}
                </div>
        }
            </div>
      }
        </div>
    }
      {FichesFoot}
    </div>;

  // ----- Dossier technique : contenu VinCod réel du millésime affiché -----
  // Remplace le texte de dégustation synthétique par appellation dès que le
  // château et le millésime sont documentés dans l'export.
  // lang pass\u00e9 \u00e0 pick() : en anglais, chaque champ lit s_en en priorit\u00e9 et
  // retombe sur le fran\u00e7ais (avec <cl\u00e9>_fr_only) tant qu'il n'est pas encore
  // traduit \u2014 voir l'algorithme dans Fiches.js. Donc plus de suppression en
  // bloc de la prose en mode EN : chaque ligne s'affiche, traduite ou non,
  // avec sa propre note "Content available in French" au besoin.
  const real = FI && fiche && mode === "millesimes" ? FI.pick(fiche, null, year, lang) : null;
  const milliers = (v) => String(v).replace(/\B(?=(\d{3})+(?!\d))/g, "\u202f");
  const specs = real && real.n ? [
  ["specTav", real.n.tav, " % vol."],
  ["specRdt", real.n.rdt, " hl/ha"],
  ["specAge", real.n.age, lang === "en" ? " yrs" : " ans"],
  ["specHa", real.n.ha, " ha"],
  ["specVol", real.n.vol && milliers(real.n.vol), lang === "en" ? " btl" : " bt"]].
  filter((x) => x[1]) : [];

  const textRows = real ? [
  ["presentationL", real.p, real.p_from, real.p_fr_only],
  ["terroirL", real.t, real.t_from, real.t_fr_only],
  ["vigneL", real.vg, real.vg_from, real.vg_fr_only],
  ["vinifL", real.vf, null, real.vf_fr_only],
  ["elevageL", real.el, null, real.el_fr_only],
  ["accordsL", real.ac, null, real.ac_fr_only],
  ["labelsL", real.lb, real.lb_from, real.lb_fr_only]].
  filter((x) => x[1]) : [];

  const Dossier = real && (textRows.length > 0 || specs.length > 0 || real.c) &&
  <div className="ec-dossier">
      <button
      className={"ec-dossier-toggle" + (dossierOpen ? " open" : "")}
      onClick={() => setDossierOpen((o) => !o)}
      aria-expanded={dossierOpen}>
        <span className="ec-dossier-title">{t.dossier} {real.year}</span>
        <svg viewBox="0 0 12 8" width="11" height="7" aria-hidden="true">
          <path
          d={dossierOpen ? "M1 6.5L6 1.5L11 6.5" : "M1 1.5L6 6.5L11 1.5"}
          stroke="currentColor" strokeWidth="1.5" fill="none"
          strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </button>
      {dossierOpen &&
    <div className="ec-dossier-body">
          {real.c &&
      <div className="ec-dossier-row">
              <span className="ec-dossier-label">{t.cepagesL}</span>
              <p className="ec-dossier-text">{real.c}</p>
            </div>
      }
          {specs.length > 0 &&
      <ul className="ec-specs">
              {specs.map(([k, v, unit]) =>
        <li key={k}>
                  <em>{v}{unit}</em>
                  <span>{t[k]}</span>
                </li>
        )}
            </ul>
      }
          {textRows.map(([k, v, from, frOnly]) =>
      <div className="ec-dossier-row" key={k}>
              <span className="ec-dossier-label">
                {t[k]}
                {from && <i className="ec-dossier-from">{t.specFrom(from)}</i>}
              </span>
              <p className="ec-dossier-text">{v}</p>
              {frOnly && <p className="ec-dossier-note">{t.frOnly}</p>}
            </div>
      )}
          <span className="ec-dossier-src">{t.dossierSrc}</span>
        </div>
    }
    </div>;

  // FicheTasting — rendered at the bottom of millesimes mode
  const FicheTasting = wine &&
  <div className="ec-tasting">
      {vintage &&
    <div className="ec-vintage-badge">
          <div className="ec-vintage-year">{year}</div>
          <div className="ec-vintage-body">
            <div className="ec-vintage-header">
              <span className="ec-vintage-qualifier">
                {ML.qualifierForNote(vintage.note)}
              </span>
              {/* Une région peut n'avoir aucun repère chiffré : il n'existe
                  pas de consensus critique d'appellation pour l'Aconcagua, et
                  Millesimes.js s'interdit d'en inventer un. Sans ce test, la
                  pastille affichait « /100 » précédé de rien. */}
              {vintage.note != null &&
              <span
                className="ec-vintage-score"
                style={{
                  color: vintageColor,
                  borderColor: vintageColor,
                }}
              >
                <em>{vintage.note}</em><i>/100</i>
              </span>
              }
            </div>
            <span className="ec-vintage-source">
              {vintage.source === "chateau" ? t.estateScore : t.appScore}
            </span>
            {/* Pas de ligne « Climat » : la phrase de caractère part de la
                saison, les deux disaient la même chose coup sur coup. */}
            {vintage.character &&
        <div className="ec-vintage-row">
              <span className="ec-vintage-label">{t.profile}</span>
              <p className="ec-vintage-text">{vintage.character}</p>
            </div>
        }
            <div className="ec-vintage-techrow">
              <a
                className={"ec-vintage-techlink" + (vincodExact ? "" : " approx")}
                href={vincodPage}
                target="_blank"
                rel="noopener noreferrer"
                title={vincodExact ? t.fichePage(vincodLabel, year) : t.ficheSearch(chateau.n, year)}
              >
                <span>{t.techSheet} {year}</span>
                <svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true">
                  <path d="M3 1h7v7M10 1L4 7" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
              </a>
              {vincodPdf &&
        <a
          className="ec-vintage-techlink ec-techlink-pdf"
          href={vincodPdf}
          target="_blank"
          rel="noopener noreferrer"
          title={t.fichePdf(vincodLabel, year)}
        >
                <svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true">
                  <path d="M6 1v7M3.2 5.4L6 8.2l2.8-2.8M2 10.6h8" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
                <span>{t.pdf}</span>
              </a>
        }
            </div>
          </div>
        </div>
    }
      {mode === "millesimes" && !vintage &&
    <div className="ec-vintage-empty">
          {t.noVintage(year)}
        </div>
    }
      <div className="ec-tasting-head">
        <Bottle chateau={chateau} year={mode === "millesimes" ? year : null} />
        <div className="ec-tasting-copy">
          <span className="ec-tasting-style">{wine.style}</span>
          {real && real.d && lang !== "en" ?
        <p className="ec-tasting-line ec-tasting-real">{real.d}</p> :
        wine.tasting && <p className="ec-tasting-line">{wine.tasting}</p>}
          {(real && real.ga || wine.garde) &&
        <div className="ec-garde">
              <span className="ec-garde-label">{t.apogee}</span>
              <span className="ec-garde-value">{real && real.ga ? real.ga : wine.garde.peak}</span>
            </div>
        }

          {mode === "millesimes" && wine.notes && wine.notes.length > 0 &&
        <div className="ec-notes">
              <span className="ec-notes-label">{t.vintageScores}</span>
              {wine.notesYear &&
          <span className="ec-notes-year">{wine.notesYear}</span>
          }
              {wine.notes.map((n, i) =>
          <span key={i} className="ec-note">
                  <span className="ec-note-src">{n.src}</span>
                  {/* noteTexte et non n.score : `score` est la borne haute
                      d'une fourchette, et l'écrire seul publie « 98 » là où
                      le critique a noté 96-98 sur échantillon de barrique. */}
                  <span className="ec-note-score">{
                    (window.CMGC_WINE_NOTES && window.CMGC_WINE_NOTES.noteTexte)
                      ? window.CMGC_WINE_NOTES.noteTexte(n) : n.score
                  }</span>
                </span>
          )}
            </div>
        }
        </div>
      </div>
      {Dossier}
      {wine.pairings && wine.pairings.length > 0 && !(real && real.ac) &&
    <div className="ec-pairings">
          <div className="ec-pairings-label">{t.pairings}</div>
          <ul className="ec-pairings-list">
            {wine.pairings.map((p, i) => <li key={i}>{p}</li>)}
          </ul>
        </div>
    }
    </div>;

  // ----- Render branch by mode -----
  const m = mode || "cepages";

  // Common header (logo + name + appellation + mobile collapse button)
  const Header = (
    <div className="ec-head">
      <img className="ec-logo" src={"assets/chateau-logos/" + chateau.l} alt={chateau.n + " — " + chateau.a} loading="lazy" decoding="async" />
      <div className="ec-titles">
        <div className="ec-name">{chateau.n}</div>
        <div className="ec-app">{chateau.a}</div>
        {relatedBadge && relatedBadge.map(({ ch, label }) => (
          <button
            key={ch.c}
            className="ec-related-badge"
            onClick={() => onPickChateau && onPickChateau(ch)}
          >
            <img src={"assets/chateau-logos/" + ch.l} alt={ch.n} className="ec-related-logo" loading="lazy" decoding="async" />
            <span className="ec-related-label">{label === "Inclut" ? t.includes : t.belongsTo}</span>
            <span className="ec-related-name">{ch.n}</span>
          </button>
        ))}
      </div>
      <button
        className="ec-collapse-btn"
        onClick={() => setCollapsed(c => !c)}
        aria-label={collapsed ? t.expand : t.collapse}>
        <svg viewBox="0 0 12 8" width="11" height="7" aria-hidden="true">
          <path
            d={collapsed ? "M1 6.5L6 1.5L11 6.5" : "M1 1.5L6 6.5L11 1.5"}
            stroke="currentColor" strokeWidth="1.5" fill="none"
            strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </button>
    </div>
  );


  // TERROIR (formerly "sols") body — horizontal list of soil types with
  // % bar + short apport phrase, replacing the vertical "carotte".
  if (m === "sols" || m === "sous-sols") {
    const tally = solsTally;
    const dict = TERROIRS.SOLS;
    const heading = t.terroir;
    const subhead = realParcelSols ? t.solsMapped : t.solsAppellation;

    return (
      <div className="encepagement">
        <div className={"encepagement-card active" + (collapsed ? " collapsed" : "")}>
          {Header}
          <div className="ec-mode-label" style={{ borderWidth: "0px" }}>
            <span className="ec-mode-name">{heading}</span>
            <span className="ec-mode-sub">{subhead}{tally.length > 0 ? " · " + tally.length + " types" : ""}</span>
          </div>
          {tally.length > 0 ?
          <ul className="ec-soil-list">
              {tally.map((it, i) => {
              const meta = dict[it.key] || {};
              return (
                <li key={i} className="ec-soil-row">
                    <span
                    className="ec-soil-swatch"
                    data-pattern={meta.pattern || "plain"}
                    style={{ backgroundColor: meta.color || "#666" }}
                    aria-hidden="true" />
                  
                    <div className="ec-soil-body">
                      <div className="ec-soil-headline">
                        <span className="ec-soil-name">{meta.label || it.key}</span>
                      </div>
                      {meta.apport &&
                    <p className="ec-soil-apport">{meta.apport}</p>
                    }
                    </div>
                  </li>);

            })}
            </ul> :
          <div className="ec-empty-note">{t.noData}</div>
          }
          {!realParcelSols &&
          <div className="ec-mode-footnote">
            {t.synthetic}
          </div>
          }
          {FichesTechniques}
        </div>
      </div>);

  }

  // CÉPAGES body (default — original behaviour)
  // Build the items list from chateau.e PLUS any grape found in the
  // parcels GeoJSON that's missing from chateau.e. The parcels file is
  // the source of truth for whether the estate cultivates a variety;
  // chateau.e holds the audited percentages of the red blend. This
  // catches the common Pessac-Léognan / Sauternes pattern where data.js
  // only lists the red and the white production is silent.
  //
  // Two distinct sources by colour:
  //   - Rouge  → chateau.e  (audited red blend)
  //   - Blanc  → chateau.eb (audited white blend) if present,
  //              else fall back to parcel-derived counts (last resort).
  //
  // Each group's bar sums to ~100 % internally — they describe two
  // separate productions, not a shared 100 %.
  const eMap = new Map();
  (chateau.e || []).forEach((it) => {
    if (ENC_NON_VARIETAL.has(it.g)) return;
    if (!exclusiveBlend.has(it.g)) eMap.set(it.g, { g: it.g, p: it.p ?? null });
  });
  // Catégories techniques (jachères, vieilles vignes) : ce ne sont pas des
  // cépages, elles sortent des barres et passent en note de bas de bloc.
  const technical = (chateau.e || []).concat(chateau.eb || []).
  filter((it) => ENC_NON_VARIETAL.has(it.g)).map((it) => it.g);
  // Whites: audited blend has priority.
  const ebMap = new Map();
  (chateau.eb || []).forEach((it) => {
    if (ENC_NON_VARIETAL.has(it.g)) return;
    if (!exclusiveBlend.has(it.g)) ebMap.set(it.g, { g: it.g, p: it.p ?? null });
  });
  // Parcel fallback for whites when chateau.eb is absent. Count only
  // white-grape parcels (so the bar sums correctly inside the Blanc
  // group). Skipped entirely if any audited eb data is present.
  if (ebMap.size === 0) {
    let whiteParcels = 0;
    parcelGrapes.forEach((cnt, g) => {
      if (ENC_NON_VARIETAL.has(g) || exclusiveBlend.has(g)) return;
      if (!ENC_WHITE_GRAPES.has(g)) return;
      whiteParcels += cnt;
    });
    parcelGrapes.forEach((cnt, g) => {
      if (ENC_NON_VARIETAL.has(g) || exclusiveBlend.has(g)) return;
      if (!ENC_WHITE_GRAPES.has(g)) return;
      // proportion de parcelles, calculée sur l'ensemble des parcelles blanches
      const pct = whiteParcels > 0 ? Math.round(cnt / whiteParcels * 1000) / 10 : null;
      ebMap.set(g, { g, p: pct, _fromParcels: true });
    });
  }
  // Parcel fallback for reds — only inject if not already in chateau.e
  // (this catches old-data files where chateau.e is incomplete).
  // Cépages vus dans le parcellaire mais absents de chateau.e : on les signale
  // comme présents, sans pourcentage. Les calculer sur le seul reliquat donnait
  // des barres fausses (un cépage isolé affiché à 100 %).
  parcelGrapes.forEach((cnt, g) => {
    if (ENC_NON_VARIETAL.has(g) || exclusiveBlend.has(g)) return;
    if (ENC_WHITE_GRAPES.has(g)) return;
    if (eMap.has(g)) return;
    eMap.set(g, { g, p: null, _fromParcels: true });
  });

  // Split into Rouge / Blanc. Each group is rendered as its own bar
  // (proportional within itself). L'encépagement affiché est toujours celui,
  // fixe, du domaine (chateau.e / chateau.eb, complété par le parcellaire) —
  // on ne le remplace plus par l'assemblage chiffré d'un millésime VinCod :
  // l'encépagement d'un domaine ne change pas d'une année sur l'autre, la
  // fiche technique du millésime reste le bon endroit pour un assemblage
  // précis.
  const itemsRouge = [...eMap.values()].
  filter((it) => !ENC_WHITE_GRAPES.has(it.g)).
  sort((a, b) => (b.p || 0) - (a.p || 0));
  const itemsBlanc = [...ebMap.values()].
  sort((a, b) => (b.p || 0) - (a.p || 0));

  // Helper — render a labelled bar + list block for one colour group.
  // Barre d'encépagement. Règle : seuls les cépages dont on connaît le
  // pourcentage occupent la barre, au prorata de leur somme. Si AUCUN
  // cépage du groupe n'a de pourcentage connu (relevés au parcellaire
  // uniquement), on n'affiche pas de barre du tout — juste la répartition
  // par couleurs dans la liste — plutôt que d'inventer une barre à parts
  // égales qui laisserait croire à un assemblage chiffré.
  const renderGroup = (items, label) => {
    if (items.length === 0) return null;
    const known = items.filter((it) => it.p != null);
    const knownSum = known.reduce((s, it) => s + it.p, 0);
    return (
      <div className="ec-group" key={label}>
        <div className="ec-group-label">{label}</div>
        {known.length > 0 &&
        <div className="ec-bar">
          {known.map((it, i) =>
          <div
            key={i}
            className="ec-bar-seg"
            style={{
              background: GRAPES[it.g] || "#7A6A58",
              width: (it.p / (knownSum || 1) * 100) + "%"
            }}
            title={(it.l || GRAPE_LABELS[it.g] || it.g) + " " + it.p + " %"} />
          )}
        </div>
        }
        <ul>
          {items.map((it, idx) =>
          <li key={idx}>
              <span style={{ background: GRAPES[it.g] || "#7A6A58" }} />
              <em>{it.p != null ? it.p + " %" : ""}</em>
              {it.l || GRAPE_LABELS[it.g] || it.g}
            </li>
          )}
        </ul>
      </div>);

  };

  return (
    <div className="encepagement">
      <div className={"encepagement-card active" + (collapsed ? " collapsed" : "")}>
        {Header}
        {renderGroup(itemsRouge, t.rouge)}
        {renderGroup(itemsBlanc, t.blanc)}
        {technical.length > 0 &&
        <div className="ec-technical">
            {technical.map((g) =>
            <span className="ec-technical-item" key={g}>
                <i style={{ background: GRAPES[g] || "#7A6A58" }} />
                {GRAPE_LABELS[g] || g}
              </span>
            )}
          </div>
        }
        {blends.length > 0 &&
        <div className="ec-blends">
            <div className="ec-blends-label">{t.complante}</div>
            <ul className="ec-blends-list">
              {blends.map(([a, b], i) =>
            <li key={i}>
                  <span
                className="ec-blend-swatch"
                style={{ background: `linear-gradient(90deg, ${GRAPES[a] || "#7A6A58"} 50%, ${GRAPES[b] || "#7A6A58"} 50%)` }} />

                  <span className="ec-blend-text">{GRAPE_LABELS[a] || a} <em className="ec-blend-sep">+</em> {GRAPE_LABELS[b] || b}</span>
                </li>
            )}
            </ul>
          </div>
        }
        {m === "millesimes" && FicheTasting}
        {FichesTechniques}
      </div>
    </div>);

}

window.Encepagement = Encepagement;