/* ============================================================
   Content detail page — full-page article / video, reader + editor.
   Route: /:section/:ref  (e.g. /blog/ds-causal-2026)

   Six regions:
   1. title + description           4. sources
   2. linked / alternative media    5. next article
   3. body / content                6. side metadata (aside)
   ============================================================ */
const { useMemo: uMc, useEffect: uEc, useState: uStc, useRef: uRc } = React;

/* readers' rating — vote once, shows running average */
function RateMini({ item }) {
  const { db, update } = useStore();
  const mine = db.myRatings && db.myRatings[item.ref];
  const [hover, setHover] = uStc(0);
  const rate = (stars) => update((d) => {
    d.myRatings = d.myRatings || {};
    if (d.myRatings[item.ref]) return;
    d.myRatings[item.ref] = stars;
    const c = d.content.find((x) => x.id === item.id);
    if (c) {c.rating = +((c.rating * (c.votes || 0) + stars) / ((c.votes || 0) + 1)).toFixed(2);c.votes = (c.votes || 0) + 1;}
  });
  return (
    <div className="rate-mini">
      <div className="rm-avg">
        <span className="rm-num">{(item.rating || 0).toFixed(1)}</span>
        <Stars n={Math.round(item.rating || 0)} size={15} />
        <span className="dim mono" style={{ fontSize: 11 }}>{item.votes || 0} votes</span>
      </div>
      {mine ?
      <div className="rm-done"><Icon name="check" size={14} /> You rated {mine}/5</div> :

      <div className="rm-vote" onMouseLeave={() => setHover(0)}>
          <span className="dim" style={{ fontSize: 12 }}>Your rating</span>
          <span className="row" style={{ color: "var(--c-amber)" }}>
            {[1, 2, 3, 4, 5].map((i) =>
          <button key={i} className="ratestar" onMouseEnter={() => setHover(i)} onClick={() => rate(i)}><Icon name={i <= hover ? "starF" : "star"} size={22} /></button>
          )}
          </span>
        </div>
      }
    </div>);

}

/* small framed editor region used inline in edit mode */
function EditRegion({ label, children }) {
  return (
    <div className="edit-region">
      <div className="er-label"><Icon name="edit" size={12} /> {label}</div>
      {children}
    </div>);

}

/* editorial workflow: Draft → Review needed → Published, with role-gated transitions */
function WorkflowControl({ status, allowed, onStatus, compact }) {
  return (
    <div className={"wf" + (compact ? " compact" : "")}>
      {WF_ORDER.map((s, i) => {
        const m = wfMeta(s);
        const active = s === status;
        const can = allowed.includes(s);
        return (
          <span className="wf-cell" key={s}>
            {i > 0 && <span className="wf-arrow"><Icon name="right" size={13} /></span>}
            <button type="button" className={"wf-step" + (active ? " on" : "") + (can ? " can" : "")} disabled={!can && !active}
            style={{ "--wf": m.color }} onClick={() => can && onStatus(s)} title={m.note}>
              <span className="wf-dot" />{m.short}
            </button>
          </span>);

      })}
    </div>);

}

/* ---- Publish popup — choose WHEN (now / a scheduled date) + confirm.
   Used by publishers and admins; private content can never be published. ---- */
function PublishModal({ item, isPage, onClose, onConfirm }) {
  const { lang } = useNav();
  const fr = lang === "fr";
  const today = new Date().toISOString().slice(0, 10);
  const [when, setWhen] = uStc("now"); // "now" | "schedule"
  const [date, setDate] = uStc(today);
  const [time, setTime] = uStc("06:00"); // go-live time — default 06:00 (UTC+2)
  const isPrivate = !!item.private;
  const langs = ["en", "fr"].filter((lg) => statusOfLang(item, lg) === "validated" && (isPage ? window.pageHasLang(item, lg) : langExists(item, lg)));
  const title = isPage ? window.pageLangFields(item, lang).label || item.label : langFields(item, lang).title || item.title;
  const scheduledFuture = when === "schedule" && date > today;
  const valid = !isPrivate && langs.length > 0 && (when === "now" || !!date);
  return (
    <Modal title={fr ? "Publier le contenu" : "Publish content"} onClose={onClose}
    footer={<>
        <button className="btn ghost" onClick={onClose}>{fr ? "Annuler" : "Cancel"}</button>
        <button className="btn accent" disabled={!valid} style={{ opacity: valid ? 1 : .5 }} onClick={() => onConfirm(when === "now" ? today : date, when, time)}>
          <Icon name="check" size={16} /> {scheduledFuture ? fr ? "Programmer la publication" : "Schedule publication" : fr ? "Publier maintenant" : "Publish now"}
        </button>
      </>}>
      <div className="pub-modal">
        <div className="pub-piece">
          <span className="pub-ic" style={{ background: wfMeta("published").color }}><Icon name="check" size={16} /></span>
          <div>
            <div className="pub-title">{title}</div>
            <div className="row gap-8" style={{ marginTop: 4 }}><LangWfBadges item={item} relevant="validated" /></div>
          </div>
        </div>
        {isPrivate ?
        <div className="pub-warn"><Icon name="lock" size={16} /> {fr ? "Ce contenu est priv\u00e9 \u2014 il ne peut jamais \u00eatre publi\u00e9 publiquement." : "This piece is private \u2014 it can never be published publicly."}</div> :
        langs.length === 0 ?
        <div className="pub-warn" style={{ background: "var(--paper-2)", color: "var(--ink-soft)", borderColor: "var(--line)" }}><Icon name="bell" size={16} /> {fr ? "Aucune langue valid\u00e9e \u2014 un relecteur doit d'abord valider ce contenu." : "No validated language yet \u2014 a reviewer must validate this piece first."}</div> :

        <>
            <p className="dim" style={{ fontSize: 13, margin: "2px 0 12px" }}>{fr ? "Mettre en ligne :" : "Going live:"} <b>{langs.map((l) => l.toUpperCase()).join(" · ")}</b></p>
            <div className="pub-when">
              <button type="button" className={"pub-opt" + (when === "now" ? " on" : "")} onClick={() => setWhen("now")}>
                <span className="po-top"><span className="rb-dot" /> {fr ? "Maintenant" : "Now"}</span>
                <span className="po-sub">{fr ? "Mise en ligne imm\u00e9diate." : "Goes live immediately."}</span>
              </button>
              <button type="button" className={"pub-opt" + (when === "schedule" ? " on" : "")} onClick={() => setWhen("schedule")}>
                <span className="po-top"><span className="rb-dot" /> {fr ? "Programmer" : "Schedule"}</span>
                <span className="po-sub">{fr ? "Choisir une date de publication." : "Pick a go-live date."}</span>
              </button>
            </div>
            {when === "schedule" &&
          <div style={{ marginTop: 12 }}>
                <div className="field-row">
                  <div><label>{fr ? "Date de publication" : "Publish date"}</label>
                  <input type="date" min={today} value={date} onChange={(e) => setDate(e.target.value)} /></div>
                  <div style={{ flex: ".7" }}><label>{fr ? "Heure" : "Time"} <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>UTC+2</span></label>
                  <input type="time" value={time} onChange={(e) => setTime(e.target.value)} /></div>
                </div>
                {scheduledFuture ?
            <div className="dim mono" style={{ fontSize: 12, marginTop: 6 }}>{fr ? "Restera \u00ab Valid\u00e9 \u00bb jusqu'au" : "Stays \u201cValidated\u201d until"} {fmtDate(date)}.</div> :
            <div className="dim mono" style={{ fontSize: 12, marginTop: 6 }}>{fr ? "Cette date publie imm\u00e9diatement." : "That date publishes immediately."}</div>}
              </div>
          }
          </>
        }
      </div>
    </Modal>);

}

/* related-items editor — drag to reorder; only items from the same section; empty = no related box */
function RelatedEditor({ item, content, onChange }) {
  const ids = (item.related || []).filter((id) => content.some((c) => c.id === id));
  const selected = ids.map((id) => content.find((c) => c.id === id)).filter(Boolean);
  const options = content.filter((c) => c.id !== item.id && c.type === item.type && !ids.includes(c.id));
  const kindWord = item.type === "video" ? "video" : item.type === "podcast" ? "podcast" : "article";
  return (
    <div className="col gap-8">
      {selected.length === 0 && <span className="dim mono" style={{ fontSize: 12 }}>No related {kindWord}s — no box is shown to readers.</span>}
      {selected.length > 0 &&
      <DragList items={selected} onReorder={(n) => onChange(n.map((c) => c.id))} renderRow={(c) =>
      <>
            <span className="dotc" style={{ background: accentFor(primaryDomain(c)).accent, width: 9, height: 9, borderRadius: "50%", flex: "none" }} />
            <span className="nm" style={{ fontSize: 13.5, lineHeight: 1.2 }}>{c.title}</span>
            <button className="iconbtn sm danger" style={{ marginLeft: "auto", flex: "none" }} onClick={() => onChange(ids.filter((x) => x !== c.id))}><Icon name="trash" size={13} /></button>
          </>
      } />
      }
      {options.length > 0 &&
      <select value="" onChange={(e) => {if (e.target.value) onChange([...ids, e.target.value]);}}>
          <option value="">+ Add related {kindWord}…</option>
          {options.map((c) => <option key={c.id} value={c.id}>{(c.title || "").slice(0, 42)}</option>)}
        </select>
      }
    </div>);

}

/* ---- per-language workflow status badges (EN / FR), colour-coded ----
   Shown on review/edit cards and in the review bar so a collaborator can see, at a
   glance, what each language still needs. `relevant` highlights the actionable status. */
function LangWfBadges({ item, relevant, langs = ["en", "fr"] }) {
  return (
    <div className="lwf">
      {langs.map((lg) => {
        const exists = itemReady(item, lg);
        const st = statusOfLang(item, lg);
        const m = wfMeta(st);
        const hot = relevant && st === relevant && exists;
        return (
          <span key={lg} className={"lwf-badge" + (hot ? " hot" : "") + (exists ? "" : " none")} style={{ "--wf": m.color }} title={exists ? m.label : "not authored in " + lg.toUpperCase()}>
            <span className="lwf-l">{lg.toUpperCase()}</span>
            <span className="lwf-s">{exists ? m.short : "—"}</span>
          </span>);

      })}
    </div>);

}

/* ---- playlist / audio player (podcast · music · audio) ---- */
function PlaylistPlayer({ item }) {
  const url = item.playlist || "";
  const kind = item.playlistKind || "spotify";
  if (kind === "file" && item.playlistFile) return <audio className="audio-player" controls src={item.playlistFile} />;
  if (kind === "url" && /\.(mp3|wav|ogg|m4a|aac)(\?|$)/i.test(url)) return <audio className="audio-player" controls src={url} />;
  if (!url) return <Placeholder label={item.type === "music" ? "music playlist embed" : item.type === "podcast" ? "podcast episode embed" : "audio source"} tag="add a playlist / embed in edit mode" style={{ width: "100%", height: 200 }} />;
  const h = item.type === "music" ? 352 : 232;
  return <iframe className="playlist-embed" src={url} height={h} style={{ width: "100%", border: "none", borderRadius: 14 }} loading="lazy" allow="encrypted-media; clipboard-write; fullscreen; picture-in-picture" title={item.title} />;
}

/* ---- playlist source editor ---- */
function PlaylistEditor({ item, onPatch }) {
  const KINDS = [{ id: "spotify", l: "Spotify" }, { id: "soundcloud", l: "SoundCloud" }, { id: "youtube", l: "YouTube" }, { id: "url", l: "Audio URL" }, { id: "file", l: "Upload file" }];
  const kind = item.playlistKind || "spotify";
  return (
    <div className="col gap-10">
      <div className="row gap-8 wrap">
        {KINDS.map((o) => <button key={o.id} type="button" className={"chip" + (kind === o.id ? " on" : "")} onClick={() => onPatch((c) => c.playlistKind = o.id)}>{o.l}</button>)}
      </div>
      {kind === "file" ?
      <div className="col gap-8">
          <label>Upload an audio file <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>stored locally in this demo</span></label>
          <input type="file" accept="audio/*" onChange={(e) => {const f = e.target.files && e.target.files[0];if (!f) return;const r = new FileReader();r.onload = () => onPatch((c) => c.playlistFile = r.result);r.readAsDataURL(f);}} />
          {item.playlistFile && <span className="leb-ok"><Icon name="check" size={13} /> file loaded</span>}
        </div> :

      <div>
          <label>{kind === "url" ? "Direct audio URL" : kind.charAt(0).toUpperCase() + kind.slice(1) + " embed URL"} <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>paste the embed / share link</span></label>
          <input className="mono" value={item.playlist || ""} onChange={(e) => onPatch((c) => c.playlist = e.target.value.trim())} placeholder={kind === "spotify" ? "https://open.spotify.com/embed/episode/…" : kind === "soundcloud" ? "https://w.soundcloud.com/player/?url=…" : kind === "youtube" ? "https://www.youtube-nocookie.com/embed/…" : "https://…/episode.mp3"} />
        </div>
      }
    </div>);

}

/* ---- document (PDF) reader + editor ---- */
function PdfReader({ item }) {
  if (!item.pdf) return <Placeholder label="PDF document" tag="upload a PDF in edit mode" style={{ width: "100%", height: 460 }} />;
  return <iframe className="pdf-frame" src={item.pdf} title={item.title} style={{ width: "100%", height: "78vh", border: "1.5px solid var(--line)", borderRadius: "var(--r-md)", background: "#fff" }} />;
}
function PdfEditor({ item, onPatch }) {
  return (
    <div className="col gap-8">
      <label>PDF file <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>shown to readers in an in-page reader · stored locally in this demo</span></label>
      <input type="file" accept="application/pdf" onChange={(e) => {const f = e.target.files && e.target.files[0];if (!f) return;const r = new FileReader();r.onload = () => onPatch((c) => {c.pdf = r.result;c.pdfName = f.name;});r.readAsDataURL(f);}} />
      {item.pdf ?
      <span className="leb-ok"><Icon name="check" size={13} /> {item.pdfName || "PDF loaded"} <button className="btn ghost sm" style={{ marginLeft: 8 }} onClick={() => onPatch((c) => {c.pdf = "";c.pdfName = "";})}><Icon name="trash" size={12} /> remove</button></span> :
      <span className="dim mono" style={{ fontSize: 12 }}>No PDF yet.</span>}
    </div>);

}

/* ---- subscribe / follow block — pick existing socials (works on every content type) ---- */
function SubscribeEditor({ item, socials, onPatch }) {
  const sel = item.subscribe || [];
  const toggle = (id) => onPatch((c) => {const cur = c.subscribe || [];c.subscribe = cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id];});
  const selSocials = sel.map((id) => socials.find((s) => s.id === id)).filter(Boolean);
  return (
    <div className="col gap-8">
      <p className="dim" style={{ fontSize: 13, margin: "-2px 0 4px" }}>Pick the networks where readers can subscribe / follow. Their logos link out, under a “Subscribe” call-to-action.</p>
      <div className="row gap-8 wrap">
        {socials.map((s) =>
        <button key={s.id} type="button" className={"sub-pick" + (sel.includes(s.id) ? " on" : "")} onClick={() => toggle(s.id)} style={{ "--sc": s.color }}>
            <span className="sub-ic" style={{ background: s.color }}><Social id={s.id} size={15} /></span>
            <span>{s.name}</span>
            {sel.includes(s.id) && <Icon name="check" size={14} />}
          </button>
        )}
        {socials.length === 0 && <span className="dim mono" style={{ fontSize: 12 }}>No social networks defined yet — add them in site settings.</span>}
      </div>
      {selSocials.length > 0 &&
      <div className="sub-preview">
          <div className="mono dim" style={{ fontSize: 10.5, letterSpacing: ".1em", marginBottom: 8 }}><Icon name="rss" size={11} style={{ verticalAlign: "-1px", marginRight: 4 }} />READER PREVIEW</div>
          <div className="sub-logos">{selSocials.map((s) => <SocialChip key={s.id} s={s} size={34} />)}</div>
        </div>
      }
    </div>);

}
function SubscribePanel({ item, socials, fr }) {
  const sel = (item.subscribe || []).map((id) => socials.find((s) => s.id === id)).filter(Boolean);
  if (!sel.length) return null;
  return (
    <section className="subscribe-panel fine">
      <div className="sp-kicker"><Icon name="rss" size={13} /> {fr ? "S'abonner" : "Subscribe"}</div>
      <div className="sub-logos">
        {sel.map((s) => <SocialChip key={s.id} s={s} size={34} />)}
      </div>
    </section>);

}

function ContentNotInLang({ item, section }) {
  const { go, lang, setLang } = useNav();
  const other = lang === "fr" ? "en" : "fr";
  const acc = accentFor(primaryDomain(item));
  return (
    <div style={{ "--accent": acc.accent, "--accent-ink": acc.ink, "--accent-wash": acc.wash }}>
      <SectionHeader />
      <div className="section-wrap" style={{ maxWidth: 620, paddingTop: 56 }}>
        <div className="surface notinlang">
          <span className="nil-ic"><Icon name="globe" size={28} /></span>
          <h1 style={{ fontSize: "clamp(26px,4vw,38px)", lineHeight: 1.08 }}>Not available in {lang === "fr" ? "French" : "English"}</h1>
          <p className="dim" style={{ maxWidth: 420, margin: "10px auto 0" }}>This piece hasn't been {lang === "fr" ? "translated to French" : "translated to English"} yet.</p>
          <div className="row gap-8" style={{ justifyContent: "center", marginTop: 18, flexWrap: "wrap" }}>
            <button className="btn" onClick={() => go(section.route)}><Icon name="left" size={16} /> Back to {section.label.toLowerCase()}</button>
            <button className="btn accent" onClick={() => setLang(other)}><Icon name="globe" size={16} /> Read in {other.toUpperCase()}</button>
          </div>
        </div>
      </div>
    </div>);

}

function ContentDetailPage({ section, refId }) {
  const { db, update } = useStore();
  const { go, edit, auth, lang, setLang, role, session, reviewMode, publishMode, testerMode, isAdmin, prevPath } = useNav();
  const fb = window.useFeedback ? window.useFeedback() : { open: () => {} };
  const fr = lang === "fr";
  const [preview, setPreview] = uStc(false);
  const [xio, setXio] = uStc(false);
  const [pubModal, setPubModal] = uStc(null);
  const [privConfirm, setPrivConfirm] = uStc(false);
  const canEdit = !!(edit && auth);
  const canReview = !!reviewMode;
  const canPublish = !!publishMode;

  const item = db.content.find((c) => c.ref === refId);
  const today = () => new Date().toISOString().slice(0, 10);
  // A PUBLISHED language is live. Editing its title/body changes the public site.
  //  • ADMIN  → no hard lock: editing stays open with a non-blocking "at your own risk"
  //             warning banner (metadata & both languages remain editable).
  //  • EDITOR → hard lock behind a confirmation popup that also offers to switch to the
  //             OTHER language when that one isn't published (edit it safely instead).
  const curPub = !!item && statusOfLang(item, lang) === "published";
  const publishedLock = canEdit && !preview && curPub && !isAdmin;
  const editing = canEdit && !preview && !publishedLock;
  const adminLiveWarn = canEdit && !preview && curPub && isAdmin;

  uEc(() => {
    if (!item || editing) return;
    update((d) => {const c = d.content.find((x) => x.id === item.id);if (c) c.views = (c.views || 0) + 1;});
    // eslint-disable-next-line
  }, [item && item.id]);

  // remember which body block sits nearest the viewport centre, so edit⇄preview keeps your place
  const lastBidRef = uRc(null);
  uEc(() => {
    if (!canEdit) return;
    const onScroll = () => {
      let best = null,bestD = Infinity;const cy = window.innerHeight / 2;
      document.querySelectorAll("[data-bid]").forEach((el) => {
        const r = el.getBoundingClientRect();
        const d = Math.abs((r.top + r.bottom) / 2 - cy);
        if (d < bestD) {bestD = d;best = el.getAttribute("data-bid");}
      });
      if (best) lastBidRef.current = best;
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [canEdit]);
  uEc(() => {
    if (!canEdit) return;
    const bid = lastBidRef.current;if (!bid) return;
    const t = setTimeout(() => {
      const el = document.querySelector('[data-bid="' + bid + '"]');
      if (el) {const r = el.getBoundingClientRect();const top = window.scrollY + r.top - (window.innerHeight / 2 - r.height / 2);window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });}
    }, 90);
    return () => clearTimeout(t);
  }, [preview]);

  const domLabel = (id) => db.domains.find((d) => d.id === id)?.label || id;
  // shared (language-agnostic) fields — also stamps the automatic "updated" date + who edited
  const patch = (fn) => update((d) => {const c = d.content.find((x) => x.id === item.id);if (c) {fn(c);c.updatedAt = today();if (session) c.editedBy = session.name;}});
  // editorial workflow status transition (role-gated) — UNIQUE PER LANGUAGE
  const setStatus = (s) => update((d) => {
    const c = d.content.find((x) => x.id === item.id);if (!c) return;
    c.statusByLang = c.statusByLang || { en: statusOf(c), fr: statusOf(c) };
    c.statusByLang[lang] = s;
    // a kick-back to review or draft drops any scheduled go-live date
    if (s === "review" || s === "draft") {c.publishAt = "";c.publishAtTime = "";}
    // legacy flags reflect the whole piece: live if ANY language is published
    c.published = Object.values(c.statusByLang).some((v) => v === "published");
    c.status = c.statusByLang.en || s;
    if (s === "review" && session) c.editedBy = session.name;
    if (s === "validated" && session) c.reviewedBy = session.name;
    if (s === "published") {if (session) c.reviewedBy = session.name;if (!c.publishedAt) c.publishedAt = today();}
    c.updatedAt = today();
  });
  // language-specific fields: EN = base, FR = c.tr.fr
  const setLangField = (key, value) => update((d) => {
    const c = d.content.find((x) => x.id === item.id);if (!c) return;
    if (lang === "fr") {c.tr = c.tr || {};c.tr.fr = c.tr.fr || {};c.tr.fr[key] = value;} else
    c[key] = value;
    c.updatedAt = today();
    if (session) c.editedBy = session.name;
  });

  const sameKind = uMc(() => item ? db.content.filter((c) => c.type === item.type).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) : [], [db.content, item]);
  // related = manually curated & ordered (item.related). No box when empty.
  const relatedItems = uMc(() => item && item.related ? item.related.map((id) => db.content.find((c) => c.id === id)).filter(Boolean) : [], [db.content, item]);
  const secOf = (it) => db.sections.find((s) => s.kind === it.type) || section;

  if (!item) {
    return (
      <div>
        <SectionHeader />
        <div className="section-wrap" style={{ maxWidth: 760, textAlign: "center", paddingTop: 60 }}>
          <h1>Not found</h1>
          <p className="dim">No content with reference <span className="mono">{refId}</span>.</p>
          <button className="btn" onClick={() => go(section.route)} style={{ marginTop: 16 }}><Icon name="left" size={16} /> Back to {section.label}</button>
        </div>
      </div>);

  }

  const acc = accentFor(primaryDomain(item));
  const styleVars = { "--accent": acc.accent, "--accent-ink": acc.ink, "--accent-wash": acc.wash };
  const isVideo = item.type === "video";
  const meta = window.kindMeta(item.type);
  const isArticle = item.type === "blog";
  const isDoc = item.type === "document";
  const isGallery = item.type === "image";
  const isAudioKind = window.isAudioContentKind(item.type);
  const isPrivate = !!item.private;
  const hasPlayer = isVideo || isAudioKind;
  const backLabel = (fr ? "Tous les " : "All ") + section.label.toLowerCase();
  const headerBack = prevPath === "/shared" ?
  { label: fr ? "Contenus partagés" : "Shared content", to: "/shared" } :
  { label: backLabel, to: section.route };const socials = db.socials || [];
  const curStatus = statusOfLang(item, lang);
  const wfAllowed = allowedStatusTransitions(role, curStatus);
  // a language may only advance once it's READY (title + description + main content).
  // backward moves (kick-backs) are always allowed; forward moves are gated.
  const langIsReady = langReady(item, lang);
  const curIdx = WF_ORDER.indexOf(curStatus);
  let wfAllowedGated = langIsReady ? wfAllowed : wfAllowed.filter((s) => WF_ORDER.indexOf(s) < curIdx);
  // a PRIVATE piece can never be published — the Published step is simply not offered.
  if (isPrivate) wfAllowedGated = wfAllowedGated.filter((s) => s !== "published");
  const missingFields = window.langMissingFields ? window.langMissingFields(item, lang) : [];
  // publish / schedule (publisher & admin) — validated languages go live
  const doPublish = (whenDate, mode, whenTime) => {
    const todayStr = today();
    const future = mode === "schedule" && whenDate > todayStr;
    update((d) => {
      const c = d.content.find((x) => x.id === item.id);if (!c) return;
      c.statusByLang = c.statusByLang || {};
      if (future) {c.publishAt = whenDate;c.publishAtTime = whenTime || "06:00";} else
      {
        ["en", "fr"].forEach((lg) => {if (c.statusByLang[lg] === "validated") c.statusByLang[lg] = "published";});
        c.published = Object.values(c.statusByLang).some((v) => v === "published");
        c.status = c.statusByLang.en || c.status;
        c.publishedAt = whenDate || todayStr;c.publishAt = "";c.publishAtTime = "";
        if (session) c.reviewedBy = c.reviewedBy || session.name;
      }
      c.updatedAt = todayStr;
    });
    setPubModal(null);
  };

  // bilingual gate — strict pieces hide when the language is missing; EN-by-default falls back to EN
  if (!editing && !isVisibleInLang(item, lang)) {
    return <ContentNotInLang item={item} section={section} />;
  }
  // status gate — a draft / in-review piece is only viewable by those allowed to see
  // that status (testers & admin for pre-released), reviewers (in review mode), or whoever
  // it's been shared with. Everyone else gets a friendly "not published yet".
  const grantedHere = !!(session && ((session.grants || []).includes(item.id) || (session.editGrants || []).includes(item.id)));
  if (!editing && !canReview && !isAdmin && !grantedHere && (isPrivate || !canSeeStatus(role, curStatus))) {
    return (
      <div style={styleVars}>
        <SectionHeader />
        <div className="section-wrap" style={{ maxWidth: 620, paddingTop: 56 }}>
          <div className="surface notinlang">
            <span className="nil-ic"><Icon name="lock" size={26} /></span>
            <h1 style={{ fontSize: "clamp(26px,4vw,38px)", lineHeight: 1.08 }}>{fr ? "Pas encore disponible" : "Not available yet"}</h1>
            <p className="dim" style={{ maxWidth: 420, margin: "10px auto 0" }}>{fr ? "Ce contenu n'est pas encore publi\u00e9 dans cette langue." : "This piece isn't published yet in this language."}</p>
            <div className="row gap-8" style={{ justifyContent: "center", marginTop: 18 }}>
              <button className="btn" onClick={() => go(section.route)}><Icon name="left" size={16} /> {fr ? "Retour à " : "Back to "}{section.label.toLowerCase()}</button>
            </div>
          </div>
        </div>
      </div>);

  }
  const viewLang = editing ? lang : publicDisplayLang(item, lang);
  const L = langFields(item, viewLang);
  // DISPLAY uses the WHOLE language unit (title + description from one language, never mixed)
  const Disp = editing ? L : wholeLang(item, viewLang);
  const Lraw = langFieldsRaw(item, lang); // raw (possibly empty) fields for the editor
  const Lother = langFieldsRaw(item, lang === "fr" ? "en" : "fr"); // other language — used as greyed phantom
  const otherCode = lang === "fr" ? "EN" : "FR";
  const langMissing = !editing && viewLang !== lang; // showing the other language as a published fallback

  const idx = sameKind.findIndex((c) => c.id === item.id);
  const next = sameKind.length > 1 ? sameKind[(idx + 1) % sameKind.length] : null;

  const bodyText = (L.body || []).map((b) => b.text || "").filter(Boolean).join(". ");
  const rt = window.readTime ? window.readTime(L.body) : 4;
  const sources = item.sources && item.sources.length ? item.sources :
  [{ label: "Original publication — cannellerichter.fr", url: "#" }];

  return (
    <div style={styleVars}>
      <SectionHeader back={headerBack} />
      <article className="article-layout">
        <div className="al-main">
          {/* admin: live content edited at your own risk — non-blocking warning, edit stays open */}
          {adminLiveWarn && (
            <div className="trans-notice" style={{ background: "#fdf3e3", color: "#b56b08", borderColor: "#f0d49a", flexWrap: "wrap" }}>
              <Icon name="bell" size={15} />
              <span style={{ flex: 1, minWidth: 220 }}>{fr
                ? `Contenu publié (${["en", "fr"].filter((lg) => statusOfLang(item, lg) === "published").map((l) => l.toUpperCase()).join(" + ")}). Vous l'éditez en direct — les changements sont visibles immédiatement sur le site public, à vos risques.`
                : `Live content (${["en", "fr"].filter((lg) => statusOfLang(item, lg) === "published").map((l) => l.toUpperCase()).join(" + ")}). You're editing it directly — changes show on the public site immediately, at your own risk.`}</span>
              <button className="btn ghost sm" style={{ borderColor: "#b56b08", color: "#b56b08" }} onClick={() => setStatus("draft")}><Icon name="edit" size={14} /> {fr ? `Repasser ${lang.toUpperCase()} en brouillon` : `Revert ${lang.toUpperCase()} to Draft`}</button>
            </div>
          )}
          {/* collaborator action bar — reviewer validates · publisher publishes, with the workflow rail */}
          {(canReview || canPublish) &&
          <div className="review-bar" style={{ "--wf": wfMeta(curStatus).color }}>
              <div className="rb-left">
                <span className="rb-kicker"><Icon name="check" size={14} /> {canPublish ? fr ? "Publication" : "Publish" : fr ? "Relecture" : "Review"} · {lang.toUpperCase()}</span>
                <WorkflowControl status={curStatus} allowed={wfAllowedGated} onStatus={setStatus} compact />
                <LangWfBadges item={item} relevant={canPublish ? "validated" : "review"} />
              </div>
              <div className="rb-actions">
                {canReview && wfAllowedGated.includes("validated") && <button className="btn accent sm" onClick={() => setStatus("validated")}><Icon name="check" size={15} /> {fr ? "Valider" : "Validate"}</button>}
                {canReview && wfAllowed.includes("draft") && <button className="btn ghost sm" onClick={() => setStatus("draft")}><Icon name="left" size={15} /> {fr ? "Renvoyer en brouillon" : "Send back to draft"}</button>}
                {canPublish && curStatus === "validated" && !isPrivate && <button className="btn accent sm" onClick={() => setPubModal(item)}><Icon name="check" size={15} /> {fr ? "Publier" : "Publish"}</button>}
                {canPublish && wfAllowed.includes("review") && <button className="btn ghost sm" onClick={() => setStatus("review")}><Icon name="left" size={15} /> {fr ? "Renvoyer en relecture" : "Send back to review"}</button>}
                <button className="btn ghost sm" onClick={() => fb.open({ kind: item.type, ref: item.ref, title: item.title, lang })}><Icon name="msg" size={15} /> {fr ? "Feedback" : "Feedback"}</button>
              </div>
            </div>
          }
          {canPublish && curStatus === "validated" && isPrivate &&
          <div className="trans-notice" style={{ background: "var(--paper-2)", color: "var(--ink-soft)" }}><Icon name="lock" size={15} /> {fr ? "Contenu priv\u00e9 \u2014 ne peut pas \u00eatre publi\u00e9 publiquement." : "Private content \u2014 it cannot be published publicly."}</div>
          }
          {item.publishAt && curStatus === "validated" &&
          <div className="trans-notice"><Icon name="bell" size={15} /> {fr ? "Publication programm\u00e9e le" : "Publication scheduled for"} {fmtDate(item.publishAt)}{item.publishAtTime ? " \u00b7 " + item.publishAtTime + " (UTC+2)" : ""}.</div>
          }
          {/* breadcrumb + export/import */}
          <div className="row between" style={{ marginBottom: 16, gap: 12, flexWrap: "wrap" }}>
            <div className="crumb">
              <a onClick={() => go("/")}>Home</a>
              <Icon name="right" size={13} />
              <a onClick={() => go(section.route)}>{section.label}</a>
              <Icon name="right" size={13} />
              <span className="cur">{(editing ? L.title : Disp.title).length > 36 ? (editing ? L.title : Disp.title).slice(0, 34) + "\u2026" : (editing ? L.title : Disp.title)}</span>
            </div>
            {canEdit &&
            <button className="chip" onClick={() => setXio(true)} title="Export / import the body as markup"><Icon name="ext" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />Export / import</button>
            }
          </div>

          {/* sticky, vertically-centred Edit / Preview toggle (left of the article) */}
          {canEdit &&
          <div className="ep-sticky">
              <button className={"eps-btn" + (!preview ? " on" : "")} onClick={() => setPreview(false)} title="Edit"><Icon name="edit" size={17} /><span>Edit</span></button>
              <button className={"eps-btn" + (preview ? " on" : "")} onClick={() => setPreview(true)} title="Preview"><Icon name="play" size={17} /><span>Preview</span></button>
            </div>
          }

          {/* which language am I editing? — one blue block: status row + (if missing) the cursor */}
          {editing &&
          <div className="lang-edit-bar two-row">
              <div className="leb-row">
                <span className="leb-k">Editing in</span>
                <div className="leb-tabs">
                  <button className={lang === "en" ? "on" : ""} onClick={() => setLang("en")}>EN</button>
                  <button className={lang === "fr" ? "on" : ""} onClick={() => setLang("fr")}>FR</button>
                </div>
                <span className="grow" />
                <BilingualStatus enOn={langReady(item, "en")} frOn={langReady(item, "fr")} fallback={item.fallback || "en"} editingLang={lang} />
              </div>
              {(() => {
                // The FR-vs-EN fallback choice (does the missing/unpublished language fall
                // back to the lead one, or stay hidden?) stays available as long as at least
                // one language has content AND the two aren't both already published.
                const enPub = publishedInLang(item, "en"), frPub = publishedInLang(item, "fr");
                const enHas = langExists(item, "en"), frHas = langExists(item, "fr");
                if (!(enHas || frHas) || (enPub && frPub)) return null;
                const score = (lg) => publishedInLang(item, lg) ? 3 : langReady(item, lg) ? 2 : langExists(item, lg) ? 1 : 0;
                const leadEn = score("en") >= score("fr");
                return <LangFallbackToggle inline enOn={leadEn} frOn={!leadEn} fallback={item.fallback || "fallback"} onChange={(v) => patch((c) => c.fallback = v)} />;
              })()}
            </div>
          }

          {/* fallback notice for readers seeing the other language */}
          {!editing && langMissing &&
          <div className="trans-notice"><Icon name="globe" size={15} /> {lang === "fr" ?
            "Désolé, le contenu n'a pas encore été traduit — version anglaise affichée." :
            "This hasn't been translated to English yet — showing the French version."}</div>
          }

          {/* (1) TITLE + DESCRIPTION */}
          {editing ?
          <EditRegion label={"1 · Title, cover & description — " + (lang === "fr" ? "FR" : "EN")}>
              <label>Cover image <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>optional — shared across languages; empty falls back to the domain illustration</span></label>
              <ImageUpload value={item.cover} onChange={(v) => patch((c) => c.cover = v)} label="upload a cover (or leave empty to use the domain illustration)" aspect="21/9" />
              <input className={!Lraw.title.trim() && Lother.title ? "phantom-ph" : ""} value={Lraw.title} onChange={(e) => setLangField("title", e.target.value)} placeholder={Lother.title || (lang === "fr" ? "Titre (FR)" : "Title (EN)")} style={{ fontFamily: "var(--font-display)", fontWeight: 800, fontSize: 26, margin: "12px 0 4px" }} />
              {!Lraw.title.trim() && <div className="miss-note"><Icon name="bell" size={12} /> Title is required — missing content in {lang === "fr" ? "FR" : "EN"}</div>}
              <textarea className={!Lraw.description.trim() && Lother.description ? "phantom-ph" : ""} style={{ marginTop: 10 }} rows={2} value={Lraw.description} onChange={(e) => setLangField("description", e.target.value)} placeholder={Lother.description || (lang === "fr" ? "Courte description (FR)" : "Short description shown under the title")} />
              {!Lraw.description.trim() && <div className="miss-note"><Icon name="bell" size={12} /> Description is missing — missing content in {lang === "fr" ? "FR" : "EN"}</div>}
            </EditRegion> :

          <header className={"article-hero" + (item.cover ? " has-cover" : " art-bg") + (isVideo ? " compact" : "")}>
              {item.cover ?
            <img className="ah-img" src={item.cover} alt="" /> :
            <div className="ah-art" style={domainsOf(item).length > 1 ? { background: "linear-gradient(135deg,#eaf0fb,#ffffff 72%)" } : undefined}><MultiDomainArt arts={domainsOf(item).map((id) => {const d = db.domains.find((x) => x.id === id);return { kind: d && d.art || (id === "human" ? "human" : "network"), accent: accentFor(id).accent };})} animate={false} /></div>}
              <div className="ah-veil" />
              <div className="ah-inner">
                <div className="row gap-8 wrap" style={{ marginBottom: 10 }}>
                  {domainsOf(item).map((id) => <span key={id} className="chip ah-chip">{domLabel(id)}</span>)}
                  <AiBadge mode={item.ai} compact />
                </div>
                <h1 className="article-title ah-title">{Disp.title}</h1>
                <p className="ah-sub">{Disp.description}</p>
              </div>
            </header>
          }

          {/* (2) LINKED / ALTERNATIVE MEDIA — blog articles only */}
          {isArticle && (editing ?
          <EditRegion label="2 · Linked & alternative media (multi-sensory)">
              <p className="dim" style={{ fontSize: 13, marginTop: -4, marginBottom: 10 }}>Same story, other senses. Link a <b>video</b>, another <b>article</b>, a <b>podcast</b> or an <b>Other page</b> (game, app…) — or offer voice, AI read-aloud or a soundtrack. Each chip shows the linked item's thumbnail; the label is optional and defaults to its title.</p>
              <AltEditor alt={item.alt} content={db.content} pages={db.otherPages} item={item} onChange={(v) => patch((c) => c.alt = v)} />
            </EditRegion> :

          <AltMediaPanel item={item} bodyText={bodyText} />)
          }

          {/* (2) ALBUM / AUDIO SOURCE — podcast · music · audio · sound */}
          {isAudioKind && editing &&
          <EditRegion label={"2 · " + (item.type === "music" ? "Album / tracks" : item.type === "podcast" ? "Episodes" : "Album of sounds")}>
              {window.AudioAlbumEditor && <window.AudioAlbumEditor item={item} onPatch={patch} />}
              <div className="src-note" style={{ marginTop: 14 }}>
                <div className="mono dim" style={{ fontSize: 11, marginBottom: 6 }}>OR a single embed (Spotify / SoundCloud / YouTube / URL / file)</div>
                <PlaylistEditor item={item} onPatch={patch} />
              </div>
            </EditRegion>
          }
          {isAudioKind && !editing && (
          item.tracks && item.tracks.length ?
          <div className="player-wrap kind-album" style={{ margin: "20px 0 24px" }}>{window.AudioAlbumReader && <window.AudioAlbumReader item={item} />}</div> :
          <div className={"player-wrap kind-" + item.type} style={{ margin: "20px 0 24px" }}><PlaylistPlayer item={item} /></div>)
          }

          {/* (2) IMAGE GALLERY — image sections (one piece = a gallery) */}
          {isGallery && editing &&
          <EditRegion label="2 · Image gallery — cover, arrows or auto-switch">
              {window.ImageGalleryEditor && <window.ImageGalleryEditor item={item} onPatch={patch} />}
            </EditRegion>
          }
          {isGallery && !editing &&
          <div className="gallery-wrap" style={{ margin: "20px 0 24px" }}>{window.ImageGalleryReader && <window.ImageGalleryReader item={item} />}</div>
          }

          {/* (2) DOCUMENT LIBRARY — documents section (one piece = several docs) */}
          {isDoc && editing &&
          <EditRegion label="2 · Documents — one piece can hold several PDFs">
              {window.DocLibraryEditor && <window.DocLibraryEditor item={item} onPatch={patch} />}
            </EditRegion>
          }
          {isDoc && !editing &&
          <div className="pdf-wrap" style={{ margin: "20px 0 24px" }}>
              {item.docs && item.docs.length ?
            window.DocLibraryReader && <window.DocLibraryReader item={item} /> :
            <PdfReader item={item} />}
            </div>
          }

          {/* video source — editable in edit mode */}
          {isVideo && editing &&
          <EditRegion label="2 · Video source">
              <div className="row gap-8 wrap" style={{ marginBottom: 10 }}>
                {[{ id: "youtube", l: "YouTube" }, { id: "file", l: "Upload file" }, { id: "url", l: "Other URL" }].map((o) =>
              <button key={o.id} type="button" className={"chip" + ((item.videoKind || (item.youtube ? "youtube" : "youtube")) === o.id ? " on" : "")} onClick={() => patch((c) => c.videoKind = o.id)}>{o.l}</button>
              )}
              </div>
              {(item.videoKind || "youtube") === "youtube" &&
            <div><label>YouTube id <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>the part after watch?v=</span></label>
                  <input value={item.youtube || ""} onChange={(e) => patch((c) => c.youtube = e.target.value.trim())} placeholder="e.g. wTcMtvyXcSE" /></div>
            }
              {item.videoKind === "url" &&
            <div><label>Video URL <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>.mp4 / .webm or any embeddable link</span></label>
                  <input className="mono" value={item.videoUrl || ""} onChange={(e) => patch((c) => c.videoUrl = e.target.value.trim())} placeholder="https://…/clip.mp4" /></div>
            }
              {item.videoKind === "file" &&
            <div className="col gap-8">
                  <label>Upload a video file <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>stored locally in this demo</span></label>
                  <input type="file" accept="video/*" onChange={(e) => {const f = e.target.files && e.target.files[0];if (!f) return;const r = new FileReader();r.onload = () => patch((c) => c.videoFile = r.result);r.readAsDataURL(f);}} />
                  {item.videoFile && <span className="leb-ok"><Icon name="check" size={13} /> file loaded</span>}
                </div>
            }
            </EditRegion>
          }

          {/* hero media */}
          {isVideo && !editing ?
          <div className="vid-embed" style={{ margin: "20px 0 24px" }}>
              {item.videoKind === "file" && item.videoFile ?
            <video src={item.videoFile} controls style={{ width: "100%", height: "100%", display: "block" }} /> :
            item.videoKind === "url" && item.videoUrl ?
            /\.(mp4|webm|ogg)(\?|$)/i.test(item.videoUrl) ?
            <video src={item.videoUrl} controls style={{ width: "100%", height: "100%", display: "block" }} /> :
            <iframe src={item.videoUrl} title={item.title} allowFullScreen loading="lazy"></iframe> :
            <iframe src={`https://www.youtube-nocookie.com/embed/${item.youtube}`} title={item.title} allowFullScreen loading="lazy"></iframe>}
            </div> :
          null}

          {/* (3) BODY / TRANSCRIPT / NOTES */}
          {editing ?
          <EditRegion label={(isAudioKind ? "3 · Transcript" : isDoc ? "3 · Notes & summary" : "3 · Body") + " — text, headings, lists, tables, callouts, images, code — " + (lang === "fr" ? "FR" : "EN")}>
              {(!Lraw.body || !Lraw.body.length) &&
            <div className="miss-note" style={{ marginBottom: 10 }}>
                  <Icon name="bell" size={12} /> Body is empty in {lang === "fr" ? "FR" : "EN"} — missing content.
                  {Lother.body && Lother.body.length > 0 && <button className="btn ghost sm" style={{ marginLeft: 10 }} onClick={() => setLangField("body", JSON.parse(JSON.stringify(Lother.body)).map((b) => ({ ...b, id: uid("b") })))}><Icon name="plus" size={13} /> Prefill from {otherCode}</button>}
                </div>
            }
              <div onFocusCapture={(e) => {const blk = e.target.closest && e.target.closest("[data-bid]");if (blk) lastBidRef.current = blk.getAttribute("data-bid");}}>
                <BlockEditor blocks={Lraw.body} onChange={(v) => setLangField("body", v)} />
              </div>
            </EditRegion> :

          <BlockReader blocks={L.body} />
          }

          {/* (4) SOURCES */}
          <section className="article-block">
            <h4>Sources</h4>
            {editing ?
            <SourcesEditor sources={item.sources} onChange={(v) => patch((c) => c.sources = v)} /> :

            <ul className="src-list">
                {sources.map((s, i) => <li key={i}><a href={s.url || "#"} onClick={(e) => (!s.url || s.url === "#") && e.preventDefault()} target={s.url && s.url !== "#" ? "_blank" : undefined} rel="noreferrer"><Icon name="ext" size={14} />{s.label || s.url}</a></li>)}
              </ul>
            }
          </section>

          {/* (4b) SUBSCRIBE / FOLLOW — moved to the sidebar (same place as the reader's panel) */}

          {/* (5) NEXT ITEM */}
          {next &&
          <a className="readnext" onClick={() => go(`${secOf(next).route}/${next.ref}`)}>
              <div>
                <div className="mono dim" style={{ fontSize: 12, letterSpacing: ".12em" }}>{(fr ? "SUIVANT" : "NEXT") + " · " + meta.label.toUpperCase()}</div>
                <div className="rn-title">{wholeLang(next, publicDisplayLang(next, lang)).title}</div>
              </div>
              <span className="rn-arrow"><Icon name="arrow" size={26} /></span>
            </a>
          }

          <div style={{ marginTop: 26 }}>
            <button className="btn ghost sm" onClick={() => go(section.route)}><Icon name="left" size={15} /> {backLabel}</button>
          </div>
        </div>

        {/* (6) SIDE METADATA */}
        <aside className="al-side">
          {/* SUBSCRIBE / FOLLOW — same place in edit & read: top of the sidebar */}
          {editing ?
          <div className="meta-card sub-edit-card" style={{ marginBottom: 16 }}>
              <div className="mc-k" style={{ marginBottom: 8 }}><Icon name="rss" size={14} style={{ verticalAlign: "-2px", marginRight: 5 }} />Subscribe / follow</div>
              <SubscribeEditor item={item} socials={socials} onPatch={patch} />
            </div> :
          <SubscribePanel item={item} socials={socials} fr={fr} />}
          <div className="meta-card">
            {!editing && <>
              <div className="mc-row"><span className="mc-k">Published</span><span className="mc-v">{fmtDate(item.createdAt)}</span></div>
              {item.updatedAt && item.updatedAt !== item.createdAt && <div className="mc-row"><span className="mc-k">Updated</span><span className="mc-v">{fmtDate(item.updatedAt)}</span></div>}
              <div className="mc-row"><span className="mc-k">{hasPlayer ? "Runtime" : "Read time"}</span><span className="mc-v">{rt} min</span></div>
              <div className="mc-row"><span className="mc-k">Views</span><span className="mc-v">{(item.views || 0).toLocaleString()}</span></div>
              <div className="mc-row"><span className="mc-k">Author</span><span className="mc-v">{db.profile.first} {db.profile.last}</span></div>
            </>}

            {/* editorial workflow (editor / admin) — dates are automatic */}
            {editing &&
            <div className="mc-block">
                <div className="mc-k" style={{ marginBottom: 8 }}>Workflow <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>· {lang.toUpperCase()}</span></div>
                <WorkflowControl status={curStatus} allowed={wfAllowedGated} onStatus={setStatus} />
                <div className="wf-note">{wfMeta(curStatus).note}</div>
                {!langIsReady &&
              <div className="miss-note" style={{ marginTop: 10 }}>
                    <Icon name="bell" size={12} /> {lang === "fr" ? "FR" : "EN"} not ready — fill {missingFields.join(", ")} to flag it for review.
                  </div>}
                <div style={{ marginTop: 10 }}><div className="mc-k" style={{ marginBottom: 6, fontSize: 11 }}>Per language</div><LangWfBadges item={item} relevant={role === "editor" ? "draft" : null} /></div>
                {role === "editor" && <div className="dim mono" style={{ fontSize: 11, marginTop: 6 }}>Editors flag work as “Review needed”. A reviewer validates, then a publisher publishes.</div>}
                <div className="auto-dates" style={{ marginTop: 12 }}>
                  <div className="row between"><span className="mc-k">Created</span><span className="mc-v">{fmtDate(item.createdAt)}</span></div>
                  <div className="row between"><span className="mc-k">Published</span><span className="mc-v">{item.published === false ? "—" : fmtDate(item.publishedAt || item.createdAt)}</span></div>
                  <div className="row between"><span className="mc-k">Last modified</span><span className="mc-v">{fmtDate(item.updatedAt || item.createdAt)}</span></div>
                </div>
                <div className="dim mono" style={{ fontSize: 11, marginTop: 8 }}>Dates are automatic. Read time — {rt} min.</div>
              </div>
            }

            {/* private content (DDD v16 §7) — never published publicly */}
            {editing &&
            <div className="mc-block">
                <div className="mc-k" style={{ marginBottom: 6 }}>Privacy</div>
                <div className="row between" style={{ alignItems: "center" }}>
                  <span style={{ fontSize: 13, fontWeight: 600, fontFamily: "var(--font-display)", color: isPrivate ? "var(--c-violet)" : "var(--ink-soft)" }}>
                    {isPrivate ? "Private — never published" : "Public (follows workflow)"}
                  </span>
                  <button className={"toggle" + (isPrivate ? " on" : "")} onClick={() => {
                  if (!isPrivate && ["en", "fr"].some((lg) => statusOfLang(item, lg) === "published")) {setPrivConfirm(true);return;}
                  patch((c) => c.private = !c.private);
                }} />
                </div>
                <p className="dim" style={{ fontSize: 11.5, margin: "8px 0 0", lineHeight: 1.45 }}>
                  {isPrivate ?
                <>Reviewable & approvable, but it can <b>never</b> go live publicly.</> :
                <>Turn on to keep this piece off the public site while still using its files elsewhere.</>}
                </p>
              </div>
            }

            {/* private editorial history — hidden from readers */}
            {editing &&
            <div className="mc-block">
                <div className="mc-k" style={{ marginBottom: 6 }}>History <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>private · hidden</span></div>
                <div className="wf-hist"><span><Icon name="edit" size={12} /> Edited by</span><b>{item.editedBy || "—"}</b></div>
                <div className="wf-hist"><span><Icon name="check" size={12} /> Reviewed by</span><b>{item.reviewedBy || "—"}</b></div>
              </div>
            }

            {/* AI transparency */}
            <div className="mc-block">
              <div className="mc-k" style={{ marginBottom: 6 }}>Transparency</div>
              {editing ?
              <div className="row gap-8 wrap">
                  {[{ id: "", l: "Human" }, { id: "augmented", l: "AI-augmented" }, { id: "generated", l: "AI-created" }, { id: "translated", l: "AI-translated" }].map((m) =>
                <button key={m.id} className={"chip" + ((item.ai || "") === m.id ? " on" : "")} onClick={() => patch((c) => c.ai = m.id)}>{m.l}</button>
                )}
                </div> :
              item.ai && AI_LABEL[item.ai] ?
              <><AiBadge mode={item.ai} /><p className="dim" style={{ fontSize: 12, margin: "8px 0 0", lineHeight: 1.45 }}>{AI_LABEL[item.ai].note}</p></> :

              <span className="ai-badge human"><Icon name="user" size={12} /> Human-written</span>
              }
            </div>

            {/* domain(s) */}
            <div className="mc-block">
              <div className="mc-k" style={{ marginBottom: 6 }}>{domainsOf(item).length > 1 ? "Domains" : "Domain"}</div>
              {editing ?
              <MultiSelect options={db.domains.map((d) => ({ id: d.id, label: d.label }))} selected={domainsOf(item)} onToggle={(id) => patch((c) => {
                const cur = domainsOf(c);
                const next = cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id];
                c.domains = next.length ? next : cur; // always keep at least one
                c.domain = c.domains[0];
              })} /> :

              <div className="row gap-8 wrap">{domainsOf(item).map((id) => <span key={id} className="chip" style={{ borderColor: accentFor(id).accent, color: accentFor(id).ink }}>{domLabel(id)}</span>)}</div>
              }
            </div>

            {/* tags */}
            <div className="mc-block">
              <div className="mc-k" style={{ marginBottom: 6 }}>Tags</div>
              {editing ?
              <TagPicker tags={item.tags} all={[...new Set(db.content.flatMap((c) => c.tags || []))].sort()} onChange={(v) => patch((c) => c.tags = v)} /> :

              <div className="row gap-8 wrap">
                  {item.tags.map((tg) => <span key={tg} className="chip" onClick={() => go(`${section.route}?tag=${tg}`)} style={{ cursor: "pointer" }}>#{tg}</span>)}
                  {item.tags.length === 0 && <span className="dim mono" style={{ fontSize: 12 }}>—</span>}
                </div>
              }
            </div>

            {/* readers' rating — reader-facing only (not editable) */}
            {!editing &&
            <div className="mc-block">
                <div className="mc-k" style={{ marginBottom: 6 }}>Readers' rating</div>
                <RateMini item={item} />
              </div>
            }

            {/* feedback button — opens transparent popup, subject pre-selected */}
            {!editing &&
            <button className="btn accent" style={{ width: "100%", justifyContent: "center", marginTop: 4 }}
            onClick={() => fb.open({ kind: item.type, ref: item.ref, title: item.title, lang })}>
                <Icon name="msg" size={16} /> {fr ? "Feedback sur ce " : "Feedback on this "}{meta.word}
              </button>
            }
          </div>

          {/* (5b) RELATED — editable & ordered in edit mode; no box when empty */}
          {editing ?
          <div className="meta-card" style={{ marginTop: 16 }}>
              <div className="mc-k" style={{ marginBottom: 10 }}>Related <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>choose & order</span></div>
              <RelatedEditor item={item} content={db.content} onChange={(v) => patch((c) => c.related = v)} />
            </div> :
          relatedItems.length > 0 &&
          <div className="meta-card" style={{ marginTop: 16 }}>
              <div className="mc-k" style={{ marginBottom: 10 }}>Related</div>
              <div className="col gap-8">
                {relatedItems.map((r) =>
              <a key={r.id} className="rel-mini" onClick={() => go(`${secOf(r).route}/${r.ref}`)}>
                    <span className="dotc" style={{ background: accentFor(primaryDomain(r)).accent }} />
                    <span className="rm-t">{wholeLang(r, publicDisplayLang(r, lang)).title}</span>
                    <Icon name="arrow" size={14} />
                  </a>
              )}
              </div>
            </div>
          }
        </aside>
      </article>
      {xio && window.ExportImportModal && <window.ExportImportModal blocks={L.body} onClose={() => setXio(false)} onImport={(b) => setLangField("body", b)} />}
      {pubModal && window.PublishModal && <window.PublishModal item={pubModal} onClose={() => setPubModal(null)} onConfirm={doPublish} />}
      {privConfirm && <Modal title={fr ? "Rendre ce contenu privé ?" : "Make this content private?"} onClose={() => setPrivConfirm(false)}
      footer={<><button className="btn ghost" onClick={() => setPrivConfirm(false)}>{fr ? "Annuler" : "Cancel"}</button>
          <button className="btn" style={{ background: "var(--c-violet)", color: "#fff", borderColor: "var(--c-violet)" }} onClick={() => {
          update((d) => {
            const c = d.content.find((x) => x.id === item.id);if (!c) return;
            c.private = true;
            c.statusByLang = c.statusByLang || {};
            ["en", "fr"].forEach((lg) => {if (c.statusByLang[lg] === "published") c.statusByLang[lg] = "validated";});
            c.published = Object.values(c.statusByLang).some((v) => v === "published");
            c.status = c.statusByLang.en || c.status;
            c.publishAt = "";c.publishAtTime = "";c.updatedAt = today();
          });
          setPrivConfirm(false);
        }}><Icon name="lock" size={16} /> {fr ? "Oui, rendre privé" : "Yes, make private"}</button></>}>
        <div className="pub-warn" style={{ marginBottom: 4 }}><Icon name="lock" size={16} /> {fr ?
          "Êtes-vous sûr ? Ce contenu ne sera plus jamais publié et disparaîtra du contenu public. Les langues publiées repassent en « Validé »." :
          "Are you sure? This content will never be published again and will disappear from public content. Published languages revert to “Validated”."}</div>
      </Modal>}
      {publishedLock && (() => {
        const other = lang === "fr" ? "en" : "fr";
        const otherPub = statusOfLang(item, other) === "published";
        const canRevert = allowedStatusTransitions(role, "published").includes("draft");
        return (
          <Modal title={fr ? "Contenu publié" : "Published content"} onClose={() => setPreview(true)}
            footer={<>
              <button className="btn ghost" onClick={() => setPreview(true)}>{fr ? "Annuler" : "Cancel"}</button>
              {!otherPub && <button className="btn accent" onClick={() => setLang(other)}><Icon name="edit" size={16} /> {fr ? `Éditer la version ${other.toUpperCase()}` : `Edit the ${other.toUpperCase()} version`}</button>}
              {canRevert && <button className="btn" style={{ background: "var(--c-amber)", color: "#fff", borderColor: "var(--c-amber)" }} onClick={() => setStatus("draft")}><Icon name="edit" size={16} /> {fr ? `Repasser ${lang.toUpperCase()} en brouillon` : `Revert ${lang.toUpperCase()} to Draft`}</button>}
            </>}>
            <div className="pub-warn" style={{ marginBottom: 4 }}><Icon name="bell" size={16} /> {fr ?
              `La version ${lang.toUpperCase()} de ce contenu est publiée (en ligne). ${canRevert ? `Pour la modifier, elle doit repasser en « Brouillon » et quittera le site public jusqu'à une nouvelle publication.` : `Vous ne pouvez pas l'éditer tant qu'elle est publiée.`}` :
              `The ${lang.toUpperCase()} version of this content is published (live). ${canRevert ? `To edit it, it must revert to “Draft” and will leave the public site until republished.` : `You can't edit it while it's published.`}`}</div>
            {!otherPub && <div className="dim" style={{ fontSize: 13, marginTop: 8 }}>{fr ?
              `La version ${other.toUpperCase()} n'est pas publiée — vous pouvez l'éditer librement.` :
              `The ${other.toUpperCase()} version isn't published — you can edit it freely.`}</div>}
          </Modal>
        );
      })()}
    </div>);

}

Object.assign(window, { ContentDetailPage, ContentNotInLang, RateMini, RelatedEditor, PublishModal });