/* Primitives UI partagées + icônes ligne */
const { useState, useEffect, useRef, useCallback } = React;

/* ---------- Icônes (ligne fine, géométriques) ---------- */
function Icon({ name, size = 20, stroke = 1.6, style }) {
  const p = { fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" };
  const paths = {
    today:    <><rect x="3.5" y="4.5" width="17" height="16" rx="2.5" {...p}/><path d="M3.5 9h17M8 3v3M16 3v3" {...p}/></>,
    learn:    <><path d="M4 5.5h7a2 2 0 0 1 2 2V20a2.4 2.4 0 0 0-2-1H4z" {...p}/><path d="M20 5.5h-7a2 2 0 0 0-2 2V20a2.4 2.4 0 0 1 2-1h7z" {...p}/></>,
    chat:     <><path d="M4.5 6.5h15v9h-9l-4 3.5v-3.5h-2z" {...p}/></>,
    pen:      <><path d="M15.5 5.5l3 3M5 19l1-3.5L16 5.5a1.6 1.6 0 0 1 2.5 0l.5.5a1.6 1.6 0 0 1 0 2.5L8.5 18z" {...p}/></>,
    chart:    <><path d="M4 20h16M7 20v-6M12 20V8M17 20v-9" {...p}/></>,
    refresh:  <><path d="M19 8a7 7 0 1 0 1 5" {...p}/><path d="M19 4v4h-4" {...p}/></>,
    plus:     <><path d="M12 5v14M5 12h14" {...p}/></>,
    ear:      <><path d="M8 10a4 4 0 1 1 8 0c0 3-3 3-3 6a2.5 2.5 0 0 1-5 0" {...p}/></>,
    wrench:   <><path d="M15 7.5a3.5 3.5 0 0 1-4.6 4.3l-4.6 4.6a1.7 1.7 0 0 0 2.4 2.4l4.6-4.6A3.5 3.5 0 0 0 17 8.6l-2 2-2-2 2-2A3.5 3.5 0 0 0 15 7.5z" {...p}/></>,
    play:     <><path d="M8 6.5v11l9-5.5z" {...p}/></>,
    pause:    <><path d="M8.5 6v12M15.5 6v12" {...p}/></>,
    prev:     <><path d="M16 6l-7 6 7 6M9 6v12" {...p}/></>,
    next:     <><path d="M8 6l7 6-7 6M15 6v12" {...p}/></>,
    repeat:   <><path d="M6 9a3 3 0 0 1 3-3h9M18 6l-2-2 2-2M18 15a3 3 0 0 1-3 3H6M6 18l2 2-2 2" {...p} transform="translate(0 -1)"/></>,
    eye:      <><path d="M2.5 12S6 6 12 6s9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6z" {...p}/><circle cx="12" cy="12" r="2.4" {...p}/></>,
    eyeoff:   <><path d="M4 4l16 16M9.5 9.6A2.4 2.4 0 0 0 12 14.4M6.5 7.2C3.9 8.8 2.5 12 2.5 12s3.5 6 9.5 6c1.5 0 2.8-.3 4-.9M14.5 6.4A9 9 0 0 0 12 6c-.5 0-1 0-1.4.1" {...p}/></>,
    chevron:  <><path d="M8 10l4 4 4-4" {...p}/></>,
    chevronR: <><path d="M10 8l4 4-4 4" {...p}/></>,
    flag:     <><path d="M6 21V4h11l-2 3.5L17 11H6" {...p}/></>,
    check:    <><path d="M5 12.5l4.5 4.5L19 7" {...p}/></>,
    settings: <><circle cx="12" cy="12" r="3" {...p}/><path d="M12 3v2.5M12 18.5V21M3 12h2.5M18.5 12H21M5.5 5.5l1.8 1.8M16.7 16.7l1.8 1.8M18.5 5.5l-1.8 1.8M7.3 16.7l-1.8 1.8" {...p}/></>,
    book:     <><path d="M5 4.5h11a2 2 0 0 1 2 2v13H7a2 2 0 0 0-2 2z" {...p}/><path d="M5 4.5v15" {...p}/></>,
    grid:     <><rect x="4" y="4" width="6.5" height="6.5" rx="1.4" {...p}/><rect x="13.5" y="4" width="6.5" height="6.5" rx="1.4" {...p}/><rect x="4" y="13.5" width="6.5" height="6.5" rx="1.4" {...p}/><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.4" {...p}/></>,
    speaker:  <><path d="M5 9.5h3l4-3.5v12l-4-3.5H5z" {...p}/><path d="M15.5 9a4 4 0 0 1 0 6" {...p}/></>,
    list:     <><path d="M8 7h12M8 12h12M8 17h12M4 7h.01M4 12h.01M4 17h.01" {...p}/></>,
    spark:    <><path d="M12 4l1.6 4.8L18.5 10l-4.9 1.2L12 16l-1.6-4.8L5.5 10l4.9-1.2z" {...p}/></>,
    globe:    <><circle cx="12" cy="12" r="8.5" {...p}/><path d="M3.5 12h17M12 3.5c2.5 2.4 2.5 14.6 0 17M12 3.5c-2.5 2.4-2.5 14.6 0 17" {...p}/></>,
    moon:     <><path d="M19 14.5A7.5 7.5 0 1 1 9.5 5a6 6 0 0 0 9.5 9.5z" {...p}/></>,
    sun:      <><circle cx="12" cy="12" r="4" {...p}/><path d="M12 2.5v2.5M12 19v2.5M2.5 12H5M19 12h2.5M5 5l1.6 1.6M17.4 17.4 19 19M19 5l-1.6 1.6M6.6 17.4 5 19" {...p}/></>,
    lock:     <><rect x="5.5" y="10.5" width="13" height="9" rx="2" {...p}/><path d="M8.5 10.5V8a3.5 3.5 0 0 1 7 0v2.5" {...p}/></>,
    search:   <><circle cx="11" cy="11" r="6.5" {...p}/><path d="m16 16 4 4" {...p}/></>,
    download: <><path d="M12 4v11M7 11l5 4 5-4M5 20h14" {...p}/></>,
    upload:   <><path d="M12 16V5M7 9l5-4 5 4M5 20h14" {...p}/></>,
    trash:    <><path d="M5 7h14M9 7V5h6v2M7 7l1 13h8l1-13" {...p}/></>,
    copy:     <><rect x="8" y="8" width="11" height="11" rx="2" {...p}/><path d="M5 16V6a2 2 0 0 1 2-2h9" {...p}/></>,
    dot:      <><circle cx="12" cy="12" r="3.5" fill="currentColor" stroke="none"/></>,
    sparkles: <><path d="M12 4l1.6 4.8L18.5 10l-4.9 1.2L12 16l-1.6-4.8L5.5 10l4.9-1.2z" {...p}/><path d="M18 4l.7 2 2 .7-2 .7-.7 2-.7-2-2-.7 2-.7z" {...p}/></>,
    layers:   <><path d="M12 4l8 4-8 4-8-4 8-4zM4 12l8 4 8-4M4 16l8 4 8-4" {...p}/></>,
    pause2:   <><rect x="6" y="5" width="4" height="14" rx="1" {...p}/><rect x="14" y="5" width="4" height="14" rx="1" {...p}/></>,
  };
  return (
    <svg viewBox="0 0 24 24" width={size} height={size} style={style} aria-hidden="true">
      {paths[name] || null}
    </svg>
  );
}

/* ---------- Eyebrow ---------- */
function Eyebrow({ children, style }) {
  return <div className="eyebrow" style={style}>{children}</div>;
}

/* ---------- Button ---------- */
function Btn({ kind = "secondary", size, icon, iconR, children, ...rest }) {
  const cls = `btn btn-${kind}${size ? " btn-" + size : ""}`;
  return (
    <button className={cls} {...rest}>
      {icon && <Icon name={icon} size={size === "lg" ? 21 : 18} />}
      {children}
      {iconR && <Icon name={iconR} size={size === "lg" ? 21 : 18} />}
    </button>
  );
}

/* ---------- Progress bar ---------- */
function Progress({ value, height = 6, color = "var(--accent)", track = "var(--rule)" }) {
  return (
    <div style={{ height, background: track, borderRadius: 999, overflow: "hidden" }}>
      <div style={{ width: `${Math.round(value * 100)}%`, height: "100%", background: color, borderRadius: 999, transition: "width .5s cubic-bezier(.2,.7,.2,1)" }} />
    </div>
  );
}

/* ---------- Statline (bandeau court, remplace gros blocs de stats) ---------- */
function StatStrip({ items }) {
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "2px 0", alignItems: "stretch",
      background: "var(--card)", border: "1px solid var(--rule)", borderRadius: "var(--radius-sm)", overflow: "hidden" }}>
      {items.map((it, i) => (
        <div key={i} style={{ flex: "1 1 0", minWidth: 120, padding: "13px 18px",
          borderLeft: i ? "1px solid var(--rule)" : "none" }}>
          <div style={{ fontFamily: "var(--serif)", fontSize: 25, color: it.accent ? "var(--accent)" : "var(--ink)", lineHeight: 1 }}>
            {it.value}
          </div>
          <div className="faint" style={{ fontSize: 12, marginTop: 6, letterSpacing: ".02em" }}>{it.label}</div>
        </div>
      ))}
    </div>
  );
}

/* ---------- Collapsible (sections repliables) ---------- */
function Collapsible({ title, eyebrow, defaultOpen = false, children, hint }) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <section className="card" style={{ overflow: "hidden" }}>
      <button onClick={() => setOpen(o => !o)} style={{
        width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "16px 20px",
        background: "transparent", border: "none", cursor: "pointer", textAlign: "left" }}>
        <div style={{ flex: 1 }}>
          {eyebrow && <Eyebrow style={{ marginBottom: 3 }}>{eyebrow}</Eyebrow>}
          <div style={{ fontFamily: "var(--serif)", fontSize: 19, color: "var(--accent-deep)" }}>{title}</div>
        </div>
        {hint && !open && <span className="faint" style={{ fontSize: 13 }}>{hint}</span>}
        <span style={{ color: "var(--ink-faint)", transform: open ? "rotate(180deg)" : "none", transition: "transform .25s" }}>
          <Icon name="chevron" size={20} />
        </span>
      </button>
      {open && <div style={{ padding: "0 20px 20px" }}>{children}</div>}
    </section>
  );
}

/* ---------- Traduction masquée (révélée au survol/clic) ---------- */
function Hideable({ children, mode = "hover", className, style }) {
  // mode: 'hover' | 'tap' | 'shown'
  const [revealed, setRevealed] = useState(false);
  const show = mode === "shown" || revealed;
  const hiddenText = "Traduction masquée";
  const handlers = mode === "hover"
    ? { onMouseEnter: () => setRevealed(true), onMouseLeave: () => setRevealed(false) }
    : mode === "tap"
    ? { onClick: () => setRevealed(r => !r) }
    : {};
  return (
    <span {...handlers} className={className} aria-label={show ? undefined : "Traduction cachée"} style={{
      cursor: mode === "shown" ? "default" : "pointer",
      color: show ? "var(--ink-soft)" : "transparent",
      background: show ? "transparent" : "var(--rule-soft)",
      borderRadius: 5, transition: "color .18s, background .18s",
      boxShadow: show ? "none" : "inset 0 0 0 1px var(--rule)",
      ...style,
    }}>{show ? children : hiddenText}</span>
  );
}

/* ---------- Audio : bouton haut-parleur + mot cliquable ----------
   Philosophie : tout ce qui est en coréen est lisible d'un clic.
   speak() choisit l'audio pré-généré si connu, sinon TTS ko-KR (cf. engine/audio.js). */
function play(text, ref, rate, url) {
  if (window.LangAudio) window.LangAudio.speak(text, { ref, rate, url });
}
function SpeakBtn({ text, audioRef, audioUrl, rate, size = 30, title = "Écouter", style, onPlayed }) {
  return (
    <button onClick={(e) => { e.stopPropagation(); play(text, audioRef, rate, audioUrl); if (typeof onPlayed === "function") onPlayed(e); }} title={title}
      aria-label={title} style={{
        width: size, height: size, borderRadius: 999, border: "none", flexShrink: 0,
        background: "var(--accent-tint)", color: "var(--accent-deep)", cursor: "pointer",
        display: "inline-grid", placeItems: "center", transition: "background .15s", ...style,
      }}
      onMouseEnter={e => { e.currentTarget.style.background = "var(--accent)"; e.currentTarget.style.color = "#fff"; }}
      onMouseLeave={e => { e.currentTarget.style.background = "var(--accent-tint)"; e.currentTarget.style.color = "var(--accent-deep)"; }}>
      <Icon name="speaker" size={Math.round(size * 0.55)} />
    </button>
  );
}
/* LE MOT EST LE BOUTON (charte II.12).
   Tout bloc coréen cliquable porte un cadre accent -> clic = on l'entend.
   Pas d'icône haut-parleur à côté. `frame={false}` pour un clic discret sans cadre. */
function Speakable({ text, audioRef, audioUrl, rate, children, className, style, as = "span", frame = true, gloss, glossRevealed = true, suppressGlossHover = false }) {
  const Tag = as;
  const [open, setOpen] = useState(false);
  // En contexte de rappel actif, le NOM ACCESSIBLE ne doit pas livrer le sens avant
  // l'effort (PROBLEMATIQUES_SEANCES §2.3) : tant que la glose n'est pas révélée
  // (carte retournée / vérifiée), l'aria-label n'annonce que le coréen + l'action audio.
  // `suppressGlossHover` ferme EN PLUS le tooltip visuel : utilisé par les cartes/runner
  // en rappel pour qu'un survol ne donne pas la réponse (§6.1). La lecture glosée des
  // dialogues ne le passe pas -> l'aide au survol y reste disponible.
  const announceGloss = gloss && glossRevealed;
  const canHoverGloss = !!gloss && !suppressGlossHover;
  const label = announceGloss ? `${text}, ${gloss}. Cliquer pour écouter.` : `${text || "Texte coréen"}. Cliquer pour écouter.`;
  const framed = frame
    ? { border: "1.5px solid var(--accent-line)", borderRadius: 8, padding: "1px 7px", background: "transparent" }
    : { borderRadius: 5 };
  const speak = (e) => {
    e.stopPropagation();
    play(text, audioRef, rate, audioUrl);
    if (canHoverGloss) setOpen(o => !o);
  };
  const keySpeak = (e) => {
    if (e.key !== "Enter" && e.key !== " ") return;
    e.preventDefault();
    speak(e);
  };
  const tag = (
    <Tag onClick={speak} onKeyDown={keySpeak} role="button" tabIndex={0} aria-label={label} className={className}
      title={announceGloss ? `${text} — ${gloss}` : "Cliquer pour écouter"} style={{ cursor: "pointer", display: "inline-block", transition: "background .15s, border-color .15s", ...framed, ...style }}
      onFocus={e => { e.currentTarget.style.outline = "2px solid var(--accent-line)"; e.currentTarget.style.outlineOffset = "2px"; if (canHoverGloss) setOpen(true); }}
      onBlur={e => { e.currentTarget.style.outline = "none"; if (gloss) setOpen(false); }}
      onMouseEnter={e => { e.currentTarget.style.background = "var(--accent-tint)"; if (frame) e.currentTarget.style.borderColor = "var(--accent)"; if (canHoverGloss) setOpen(true); }}
      onMouseLeave={e => { e.currentTarget.style.background = "transparent"; if (frame) e.currentTarget.style.borderColor = "var(--accent-line)"; if (gloss) setOpen(false); }}>
      {children != null ? children : text}
    </Tag>
  );
  if (!gloss) return tag;
  return (
    <span style={{ position: "relative", display: "inline-block" }}>
      {tag}
      {open && (
        <span className="fade-up" role="tooltip" style={{
          position: "absolute", bottom: "calc(100% + 6px)", left: "50%", transform: "translateX(-50%)",
          maxWidth: 240, width: "max-content", textAlign: "center", background: "var(--ink)", color: "var(--paper)",
          padding: "5px 10px", borderRadius: 8, fontFamily: "var(--sans)", fontSize: 13, fontWeight: 600, lineHeight: 1.3,
          zIndex: 70, boxShadow: "var(--shadow-pop)", pointerEvents: "none",
        }}>{gloss}</span>
      )}
    </span>
  );
}

function normalizeImageBank(bank) {
  if (Array.isArray(bank)) return { images: bank };
  return bank && Array.isArray(bank.images) ? bank : { images: [] };
}

function imageUrl(image) {
  const path = String((image && (image.url || image.localPath)) || "").replace(/\\/g, "/");
  if (!path) return "";
  if (/^(https?:|data:|blob:|\/)/i.test(path)) return path;
  return "/" + path.replace(/^\/+/, "");
}

function imagesForItem(imageBank, itemIds, useCase) {
  const ids = new Set((Array.isArray(itemIds) ? itemIds : [itemIds]).filter(Boolean).map(String));
  if (!ids.size) return [];
  const payload = normalizeImageBank(imageBank);
  return payload.images.filter((image) => {
    const imageIds = Array.isArray(image.itemIds) ? image.itemIds.map(String) : [];
    if (!imageIds.some((id) => ids.has(id))) return false;
    if (!useCase) return true;
    return Array.isArray(image.useCases) && image.useCases.includes(useCase);
  });
}

function primaryImageForItem(imageBank, itemIds, useCase) {
  const matches = imagesForItem(imageBank, itemIds, useCase);
  return matches[0] || null;
}

function LearningImage({ image, compact = false, wide = false, style }) {
  const src = imageUrl(image);
  if (!src) return null;
  const imageHeight = compact ? "clamp(88px, 24vw, 112px)" : "clamp(112px, 30vw, 148px)";
  return (
    <figure style={{
      margin: 0,
      width: wide ? "100%" : compact ? "min(100%, 280px)" : "min(100%, 320px)",
      maxWidth: "100%",
      boxSizing: "border-box",
      display: "grid",
      justifyItems: "center",
      gap: 6,
      ...style,
    }}>
      <img
        src={src}
        alt={image.alt || ""}
        loading="lazy"
        decoding="async"
        title={image.credit || image.sourceName || ""}
        style={{
          width: "100%",
          height: imageHeight,
          maxWidth: "100%",
          aspectRatio: "16 / 9",
          objectFit: "contain",
          borderRadius: "var(--radius-sm)",
          border: "1px solid var(--rule)",
          background: "var(--card-2)",
          display: "block",
        }}
      />
    </figure>
  );
}

/* ---------- Glose kr -> fr partagée (cartes + dialogues) ----------
   Sens exact, sinon on retire la particule/terminaison finale et on tente la
   forme de dictionnaire. Aucune traduction inventée : tout vient de gloss.json. */
const GLOSS_SUFFIXES = ["이라고", "드립니다", "습니다", "이에요", "으세요", "겠습니다", "겠어요", "았어요", "었어요", "였어요", "예요", "라고", "세요", "해요", "하면", "으면", "아요", "어요", "워요", "와요", "여요", "으로", "에서", "에게", "한테", "까지", "부터", "이야", "면", "요", "은", "는", "이", "가", "을", "를", "에", "도", "만", "의", "와", "과", "아", "어", "해", "?", ".", "!", ","].sort((a, b) => b.length - a.length);
function glossLookup(map, base) {
  if (!base) return "";
  const cands = [base, base + "다", base + "하다", base + "기"];
  for (const c of cands) if (map[c]) return map[c];
  return "";
}
function resolveGloss(map, seg) {
  if (!map || !seg) return "";
  const s = String(seg).trim();
  if (map[s]) return map[s];
  let cur = s;
  for (let i = 0; i < 2; i++) {
    const hit = glossLookup(map, cur);
    if (hit) return hit;
    let stripped = false;
    for (const suf of GLOSS_SUFFIXES) {
      if (cur.length > suf.length && cur.endsWith(suf)) { cur = cur.slice(0, -suf.length); stripped = true; break; }
    }
    if (!stripped) break;
  }
  return glossLookup(map, cur);
}

/* ---------- Polissage d'affichage des libellés (PROBLEMATIQUES_SEANCES §6.5) ----------
   Couche unique partagée par Aujourd'hui, le runner et les dialogues : beaucoup de
   clés de données/templates servent aussi de tags internes (Fluidite, Dictee ciblee,
   identites, debut…). Plutôt que d'accentuer les données brutes, on polit À L'AFFICHAGE.
   Centralisé ici (chargé en premier) pour éviter trois copies qui divergent. */
const POLISH_REPLACEMENTS = [
  [/\bEcoute\b/g, "Écoute"], [/\becoute\b/g, "écoute"],
  [/\bDictee\b/g, "Dictée"], [/\bdictee\b/g, "dictée"],
  [/\bFluidite\b/g, "Fluidité"], [/\bfluidite\b/g, "fluidité"],
  [/\bReparation\b/g, "Réparation"], [/\breparation\b/g, "réparation"],
  [/\bComprehension\b/g, "Compréhension"], [/\bcomprehension\b/g, "compréhension"],
  [/\bcomprehensible\b/g, "compréhensible"],
  [/\bRepetition\b/g, "Répétition"], [/\brepetition\b/g, "répétition"],
  [/\bReplique\b/g, "Réplique"], [/\breplique\b/g, "réplique"],
  [/\bguidee\b/g, "guidée"], [/\bciblee\b/g, "ciblée"],
  [/\bcontrolee\b/g, "contrôlée"], [/\bdifferee\b/g, "différée"],
  [/\bfrequentiels\b/g, "fréquentiels"], [/\bfrequentiel\b/g, "fréquentiel"],
  [/\bfrequents\b/g, "fréquents"], [/\bfrequent\b/g, "fréquent"],
  [/\bidentites\b/g, "identités"], [/\bidentite\b/g, "identité"],
  [/\bdebut\b/g, "début"], [/\brole\b/g, "rôle"],
  [/\bReservation\b/g, "Réservation"], [/\breservation\b/g, "réservation"],
  [/\bPresentation\b/g, "Présentation"], [/\bpresentation\b/g, "présentation"],
  [/\bCafe\b/g, "Café"], [/\bcafe\b/g, "café"],
  [/\bRecu\b/g, "Reçu"], [/\brecu\b/g, "reçu"],
  [/\bapres\b/g, "après"], [/\bModele\b/g, "Modèle"], [/\bmodele\b/g, "modèle"],
  [/\bcorrespond a\b/g, "correspond à"], [/\ba ce sens\b/g, "à ce sens"],
  [/\ba emporter\b/g, "à emporter"], [/\bA emporter\b/g, "À emporter"],
  [/\bs'il vous plait\b/g, "s'il vous plaît"],
];
function polishLabel(text) {
  if (!text) return text;
  let out = String(text);
  for (let i = 0; i < POLISH_REPLACEMENTS.length; i++) {
    out = out.replace(POLISH_REPLACEMENTS[i][0], POLISH_REPLACEMENTS[i][1]);
  }
  return out;
}

Object.assign(window, { Icon, Eyebrow, Btn, Progress, StatStrip, Collapsible, Hideable, SpeakBtn, Speakable, resolveGloss, polishLabel });

/* ---------- State primitives: empty / loading / error ---------- */
function EmptyState({ icon = "layers", title, body, action }) {
  return (
    <div style={{ textAlign: "center", padding: "38px 24px", border: "1px dashed var(--rule)", borderRadius: "var(--radius)", background: "var(--card-2)" }}>
      <div style={{ width: 46, height: 46, margin: "0 auto 14px", borderRadius: 999, background: "var(--card)", border: "1px solid var(--rule)", display: "grid", placeItems: "center", color: "var(--ink-faint)" }}>
        <Icon name={icon} size={22} />
      </div>
      <div style={{ fontFamily: "var(--serif)", fontSize: 19, color: "var(--accent-deep)" }}>{title}</div>
      {body && <p className="muted" style={{ fontSize: 14, margin: "6px auto 0", maxWidth: 360 }}>{body}</p>}
      {action && <div style={{ marginTop: 16 }}>{action}</div>}
    </div>
  );
}
function Skeleton({ h = 14, w = "100%", r = 7, style }) {
  return <div className="sk" style={{ height: h, width: w, borderRadius: r, ...style }} />;
}
function LoadingCard({ lines = 3 }) {
  return (
    <div className="card" style={{ padding: "var(--pad-card)", display: "grid", gap: 12 }}>
      <Skeleton h={12} w="40%" />
      <Skeleton h={26} w="65%" />
      {Array.from({ length: lines }).map((_, i) => <Skeleton key={i} h={12} w={`${90 - i * 12}%`} />)}
    </div>
  );
}
function ErrorState({ title = "Données indisponibles", body, onRetry }) {
  return (
    <div style={{ textAlign: "center", padding: "32px 24px", border: "1px solid var(--accent-line)", borderRadius: "var(--radius)", background: "var(--accent-tint)" }}>
      <div style={{ fontFamily: "var(--serif)", fontSize: 19, color: "var(--accent-deep)" }}>{title}</div>
      {body && <p className="muted" style={{ fontSize: 14, margin: "6px auto 0", maxWidth: 360 }}>{body}</p>}
      {onRetry && <div style={{ marginTop: 16 }}><Btn kind="secondary" size="sm" icon="refresh" onClick={onRetry}>Recharger</Btn></div>}
    </div>
  );
}

/* ---------- Segmented (generic) ---------- */
function Segmented({ value, options, onChange, size = "md" }) {
  return (
    <div role="radiogroup" style={{ display: "inline-flex", background: "var(--card-2)", border: "1px solid var(--rule)", borderRadius: 999, padding: 3, flexWrap: "wrap" }}>
      {options.map(o => {
        const v = typeof o === "object" ? o.value : o;
        const l = typeof o === "object" ? o.label : o;
        const disabled = typeof o === "object" && !!o.disabled;
        const on = v === value;
        return (
          <button key={v} role="radio" aria-checked={on} disabled={disabled} title={o.title}
            onClick={() => !disabled && onChange(v)} style={{
            border: "none", cursor: disabled ? "not-allowed" : "pointer", borderRadius: 999,
            padding: size === "sm" ? "5px 11px" : "7px 14px", fontFamily: "var(--sans)",
            fontSize: size === "sm" ? 12.5 : 13.5, fontWeight: 600,
            color: on ? "#fdf3f0" : "var(--ink-soft)", background: on ? "var(--accent)" : "transparent",
            opacity: disabled ? .48 : 1, transition: "background .15s, color .15s",
          }}>{l}</button>
        );
      })}
    </div>
  );
}

/* ---------- Filter chips (multi/single) ---------- */
function FilterChips({ options, value, onChange }) {
  return (
    <div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}>
      {options.map(o => {
        const v = typeof o === "object" ? o.value : o;
        const l = typeof o === "object" ? o.label : o;
        const on = v === value;
        return (
          <button key={v} onClick={() => onChange(v)} title={typeof o === "object" ? o.title : undefined} style={{
            padding: "6px 12px", borderRadius: 999, cursor: "pointer", fontSize: 13, fontWeight: 600,
            border: `1px solid ${on ? "var(--accent-line)" : "var(--rule)"}`,
            background: on ? "var(--accent-tint)" : "var(--card)",
            color: on ? "var(--accent-deep)" : "var(--ink-soft)", transition: "all .15s",
          }}>{l}</button>
        );
      })}
    </div>
  );
}

/* ---------- Tile (square action tile) ---------- */
function Tile({ icon, label, kr, meta, badge, onClick, accent, disabled, title }) {
  return (
    <button onClick={disabled ? undefined : onClick} disabled={disabled} title={title} aria-disabled={disabled ? "true" : undefined} style={{
      textAlign: "left", cursor: disabled ? "not-allowed" : "pointer", padding: 15, borderRadius: "var(--radius-sm)",
      border: `1px solid ${accent ? "var(--accent-line)" : "var(--rule)"}`,
      background: accent ? "var(--accent-tint)" : "var(--card)", display: "grid", gap: 9,
      opacity: disabled ? .58 : 1, transition: "all .15s",
    }}
    onMouseEnter={e => { if (!disabled) { e.currentTarget.style.borderColor = "var(--accent-line)"; e.currentTarget.style.background = "var(--accent-tint)"; } }}
    onMouseLeave={e => { if (!disabled) { e.currentTarget.style.borderColor = accent ? "var(--accent-line)" : "var(--rule)"; e.currentTarget.style.background = accent ? "var(--accent-tint)" : "var(--card)"; } }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        <span style={{ color: "var(--ochre)" }}>{icon && <Icon name={icon} size={20} />}</span>
        {badge != null && <span className="chip" style={{ fontSize: 11, padding: "2px 8px" }}>{badge}</span>}
      </div>
      <div>
        <div style={{ fontWeight: 700, fontSize: 14.5 }}>{label} {kr && <span className="kr faint" style={{ fontWeight: 400, fontSize: 13 }}>{kr}</span>}</div>
        {meta && <div className="faint" style={{ fontSize: 12.5, marginTop: 2 }}>{meta}</div>}
      </div>
    </button>
  );
}

/* ---------- Glossed token ---------- */
function TokenGloss({ token, onReport }) {
  const [open, setOpen] = useState(false);
  const audio = token.audio || {};
  return (
    <span style={{ position: "relative", display: "inline-block" }}>
      <button onClick={() => { play(token.t, audio.ref || audio.id, undefined, audio.url); setOpen(o => !o); }} title="Écouter et afficher la glose" className="kr" style={{
        border: "none", background: open ? "var(--accent-tint)" : "transparent", cursor: "pointer",
        font: "inherit", color: "inherit", padding: "0 1px", borderRadius: 4,
        borderBottom: "1.5px dotted var(--accent-line)",
      }}>{token.t}</button>
      {open && (
        <span style={{ position: "absolute", bottom: "100%", left: "50%", transform: "translateX(-50%)", marginBottom: 6, zIndex: 5,
          background: "var(--ink)", color: "var(--paper)", padding: "7px 11px", borderRadius: 9, whiteSpace: "nowrap",
          fontFamily: "var(--sans)", fontSize: 12.5, boxShadow: "var(--shadow-pop)" }}>
          {token.g}
          {onReport && <button onClick={(e) => { e.stopPropagation(); onReport(token); setOpen(false); }} title="Signaler" style={{ marginLeft: 8, border: "none", background: "transparent", color: "var(--ochre-soft)", cursor: "pointer", padding: 0 }}><Icon name="flag" size={13} /></button>}
        </span>
      )}
    </span>
  );
}

/* ---------- Bars (mini) ---------- */
function MiniBars({ data, max, labelKey, valKey, height = 110 }) {
  const m = max || Math.max(...data.map(d => d[valKey]));
  return (
    <div style={{ display: "flex", alignItems: "flex-end", gap: 10, height }}>
      {data.map((d, i) => (
        <div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 6, height: "100%", justifyContent: "flex-end" }}>
          <span className="faint" style={{ fontSize: 11, fontFamily: "var(--mono)" }}>{d[valKey]}</span>
          <div style={{ width: "100%", height: `${(d[valKey] / m) * 100}%`, minHeight: 3, background: "var(--accent)", borderRadius: "4px 4px 0 0", opacity: .85 }} />
          <span className="faint" style={{ fontSize: 11, textAlign: "center" }}>{d[labelKey]}</span>
        </div>
      ))}
    </div>
  );
}

Object.assign(window, {
  EmptyState, Skeleton, LoadingCard, ErrorState, Segmented, FilterChips, Tile, TokenGloss, MiniBars,
  LearningImage, imagesForItem, primaryImageForItem,
});
