/* global React, ReactDOM, Common, Motion, I18n, HelpContent */
// =============================================================
// HelpCommon. Shared building blocks for the Help hub and the
// article pages: block renderer, category badge, breadcrumb,
// inline link with [[slug|label]] expansion, search index,
// keyboard helpers. Kept here so /help and /help/<slug> render
// identically.
// =============================================================
const { Keycap } = Common;
const { useT, useI18n } = I18n;
const { HELP_CATEGORIES, HELP_ARTICLES, HELP_ARTICLE_BY_SLUG } = HelpContent;
const { useMemo, useState, useEffect } = React;

// Wikilink expansion: replaces [[slug|label]] with a real <a> tag.
// Slug starting with "/" becomes an absolute link (e.g. /privacy).
function renderRich(text) {
  if (!text) return text;
  const parts = [];
  const regex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
  let lastIndex = 0;
  let match;
  let key = 0;
  while ((match = regex.exec(text)) !== null) {
    if (match.index > lastIndex) {
      parts.push(text.slice(lastIndex, match.index));
    }
    const target = match[1].trim();
    const label = (match[2] || match[1]).trim();
    const href = target.startsWith("/") ? target : `/help/${target}/`;
    parts.push(
      <a
        key={`lk-${key++}`}
        href={href}
        style={{
          color: "var(--fg)",
          borderBottom: "1px solid var(--line-strong)",
          textDecoration: "none",
          transition: "border-color 180ms",
        }}
        onMouseEnter={(e) => (e.currentTarget.style.borderColor = "#38BDF8")}
        onMouseLeave={(e) => (e.currentTarget.style.borderColor = "var(--line-strong)")}
      >
        {label}
      </a>
    );
    lastIndex = regex.lastIndex;
  }
  if (lastIndex < text.length) parts.push(text.slice(lastIndex));
  return parts;
}

// Anchor id for a heading block: slugify of the text.
function slugify(s) {
  return (s || "")
    .toLowerCase()
    .replace(/[^a-z0-9\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-")
    .slice(0, 60);
}

// =============================================================
// Block renderer. Each block has a `type` field; everything else
// is type-specific.
// =============================================================
function HelpBlock({ block }) {
  switch (block.type) {
    case "heading":
      return (
        <h2
          id={slugify(block.text)}
          className="reveal"
          style={{
            fontFamily: "var(--font-display)",
            fontSize: 22,
            fontWeight: 600,
            letterSpacing: "-0.01em",
            color: "var(--fg)",
            margin: "40px 0 14px",
            scrollMarginTop: 96,
          }}
        >
          {block.text}
        </h2>
      );
    case "para":
      return (
        <p
          className="reveal"
          style={{
            margin: "0 0 18px",
            fontSize: 16,
            lineHeight: 1.65,
            color: "var(--fg-muted)",
            textWrap: "pretty",
          }}
        >
          {renderRich(block.text)}
        </p>
      );
    case "list":
      return (
        <BlockList items={block.items} ordered={block.ordered} />
      );
    case "steps":
      return <BlockSteps items={block.items} />;
    case "kbd":
      return <BlockKbd keys={block.keys} caption={block.caption} />;
    case "image":
      return (
        <BlockImage
          caption={block.caption}
          ratio={block.ratio}
          src={block.src}
          alt={block.alt}
        />
      );
    case "carousel":
      return <BlockCarousel slides={block.slides} caption={block.caption} />;
    case "video":
      return <BlockVideo caption={block.caption} />;
    case "callout":
      return <BlockCallout kind={block.kind} text={block.text} />;
    case "code":
      return <BlockCode snippet={block.snippet} lang={block.lang} />;
    case "table":
      return <BlockTable headers={block.headers} rows={block.rows} />;
    default:
      return null;
  }
}

function BlockList({ items, ordered }) {
  const Tag = ordered ? "ol" : "ul";
  return (
    <Tag
      className="reveal"
      style={{
        margin: "0 0 22px",
        paddingLeft: 0,
        listStyle: "none",
        display: "flex",
        flexDirection: "column",
        gap: 8,
      }}
    >
      {items.map((it, i) => (
        <li
          key={i}
          style={{
            display: "flex",
            gap: 12,
            alignItems: "flex-start",
            fontSize: 15.5,
            lineHeight: 1.6,
            color: "var(--fg-muted)",
            paddingLeft: 0,
          }}
        >
          <span
            aria-hidden="true"
            style={{
              width: 6,
              height: 6,
              borderRadius: 99,
              background: "var(--fg-faint)",
              marginTop: 10,
              flexShrink: 0,
            }}
          />
          <span style={{ textWrap: "pretty" }}>{renderRich(it)}</span>
        </li>
      ))}
    </Tag>
  );
}

function BlockSteps({ items }) {
  return (
    <ol
      className="reveal"
      style={{
        margin: "0 0 24px",
        padding: 0,
        listStyle: "none",
        display: "flex",
        flexDirection: "column",
        gap: 10,
        counterReset: "step",
      }}
    >
      {items.map((it, i) => (
        <li
          key={i}
          style={{
            display: "flex",
            gap: 14,
            alignItems: "flex-start",
            padding: "14px 16px",
            background: "linear-gradient(180deg, var(--bg-elev), transparent)",
            border: "1px solid var(--line)",
            borderRadius: 12,
            fontSize: 15.5,
            lineHeight: 1.6,
            color: "var(--fg)",
          }}
        >
          <span
            aria-hidden="true"
            style={{
              flexShrink: 0,
              width: 26,
              height: 26,
              borderRadius: 99,
              display: "inline-flex",
              alignItems: "center",
              justifyContent: "center",
              background: "rgba(56,189,248,0.10)",
              border: "1px solid rgba(56,189,248,0.30)",
              color: "#7DD3FC",
              fontFamily: "var(--font-mono)",
              fontSize: 12,
              fontWeight: 700,
            }}
          >
            {i + 1}
          </span>
          <span style={{ textWrap: "pretty" }}>{renderRich(it)}</span>
        </li>
      ))}
    </ol>
  );
}

function BlockKbd({ keys, caption }) {
  return (
    <div
      className="reveal"
      style={{
        display: "flex",
        alignItems: "center",
        gap: 14,
        margin: "0 0 18px",
        padding: "16px 18px",
        background: "var(--bg-deep)",
        border: "1px solid var(--line)",
        borderRadius: 12,
      }}
    >
      <span style={{ display: "inline-flex", gap: 8, alignItems: "center" }}>
        {keys.map((k, i) => (
          <React.Fragment key={i}>
            <Keycap size="lg">{k}</Keycap>
            {i < keys.length - 1 && (
              <span style={{ color: "var(--fg-faint)", fontSize: 16 }}>+</span>
            )}
          </React.Fragment>
        ))}
      </span>
      {caption && (
        <span
          style={{
            fontFamily: "var(--font-mono)",
            fontSize: 12,
            color: "var(--fg-faint)",
            letterSpacing: "0.04em",
          }}
        >
          {caption}
        </span>
      )}
    </div>
  );
}

// Lightbox overlay shown when a real screenshot is clicked. Closes on
// backdrop click or Escape. Rendered only while `src` is set.
function ImageLightbox({ src, alt, onClose }) {
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
    };
    window.addEventListener("keydown", onKey);
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = prevOverflow;
    };
  }, [onClose]);

  // Portal to <body>: ancestors with a transform (e.g. the .reveal class
  // animates translateY) would otherwise trap our position:fixed overlay
  // inside the figure instead of covering the viewport.
  return ReactDOM.createPortal(
    <div
      role="dialog"
      aria-modal="true"
      aria-label={alt || "Screenshot"}
      onClick={onClose}
      style={{
        position: "fixed",
        inset: 0,
        zIndex: 1000,
        background: "rgba(0,0,0,0.86)",
        backdropFilter: "blur(6px)",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        padding: 32,
        cursor: "zoom-out",
      }}
    >
      <img
        src={src}
        alt={alt || ""}
        onClick={(e) => e.stopPropagation()}
        style={{
          maxWidth: "100%",
          maxHeight: "100%",
          objectFit: "contain",
          borderRadius: 12,
          boxShadow: "0 24px 80px rgba(0,0,0,0.6)",
          cursor: "default",
        }}
      />
    </div>,
    document.body
  );
}

function ImagePlaceholder({ ratio }) {
  return (
    <div
      style={{
        aspectRatio: ratio,
        width: "100%",
        background:
          "repeating-linear-gradient(45deg, var(--bg-elev), var(--bg-elev) 14px, transparent 14px, transparent 28px)",
        border: "1px dashed var(--line-strong)",
        borderRadius: 14,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        color: "var(--fg-faint)",
        fontFamily: "var(--font-mono)",
        fontSize: 12,
        padding: 24,
        textAlign: "center",
        letterSpacing: "0.04em",
      }}
    >
      <span style={{ display: "inline-flex", alignItems: "center", gap: 10 }}>
        <svg
          width="18"
          height="18"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.6"
        >
          <rect x="3" y="5" width="18" height="14" rx="2" />
          <circle cx="9" cy="11" r="1.5" />
          <path d="M21 17l-5-5-9 9" />
        </svg>
        image · placeholder
      </span>
    </div>
  );
}

// Detect the visitor's OS so we can show platform-matched screenshots
// (a Windows visitor sees the Windows UI, a Mac visitor the macOS UI).
// Anything not clearly macOS falls back to "win".
function detectPlatform() {
  if (typeof navigator === "undefined") return "win";
  return /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent || "") ? "mac" : "win";
}

// Pick a theme variant. `x` is a string or { light, dark }; falls back to
// whichever variant exists.
function pickTheme(x, theme) {
  if (!x) return null;
  if (typeof x === "string") return x;
  return (theme === "light" ? x.light : x.dark) || x.dark || x.light || null;
}

// Resolve the effective src. `src` may be:
//   "/path.png"                        theme- and platform-agnostic
//   { light, dark }                    theme-aware, platform-agnostic
//   { win, mac }                       platform-aware (each string or {light,dark})
// Platform-aware objects fall back to whichever platform exists.
function resolveSrc(src, theme, platform) {
  if (!src) return null;
  if (typeof src === "string") return src;
  if (src.win || src.mac) {
    return pickTheme(src[platform] || src.win || src.mac, theme);
  }
  return pickTheme(src, theme);
}

function BlockImage({ caption, ratio = "16/9", src, alt }) {
  const { theme } = Motion.useTheme();
  const platform = detectPlatform();
  const [failed, setFailed] = useState(false);
  const [zoomed, setZoomed] = useState(false);
  const resolved = resolveSrc(src, theme, platform);
  // Reset the load-error flag when the resolved source changes (e.g. on
  // theme switch) so a fresh variant gets a chance to load.
  useEffect(() => { setFailed(false); }, [resolved]);
  const showReal = resolved && !failed;

  return (
    <figure
      className="reveal"
      style={{
        margin: "8px 0 24px",
      }}
    >
      {showReal ? (
        <button
          type="button"
          onClick={() => setZoomed(true)}
          aria-label={`${alt || caption || "Screenshot"} (click to enlarge)`}
          style={{
            display: "block",
            width: "fit-content",
            maxWidth: "100%",
            margin: "0 auto",
            padding: 0,
            border: "1px solid var(--line)",
            borderRadius: 14,
            overflow: "hidden",
            background: "var(--bg-elev)",
            cursor: "zoom-in",
            lineHeight: 0,
          }}
        >
          <img
            src={resolved}
            alt={alt || caption || ""}
            loading="lazy"
            decoding="async"
            onError={() => setFailed(true)}
            style={{
              display: "block",
              maxWidth: "100%",
              maxHeight: "60vh",
              width: "auto",
              height: "auto",
              objectFit: "contain",
            }}
          />
        </button>
      ) : (
        <ImagePlaceholder ratio={ratio} />
      )}
      {caption && (
        <figcaption
          style={{
            marginTop: 10,
            fontSize: 13,
            color: "var(--fg-faint)",
            textAlign: "center",
            fontFamily: "var(--font-mono)",
            letterSpacing: "0.02em",
          }}
        >
          {caption}
        </figcaption>
      )}
      {showReal && zoomed && (
        <ImageLightbox src={resolved} alt={alt || caption} onClose={() => setZoomed(false)} />
      )}
    </figure>
  );
}

function BlockCarousel({ slides = [], caption }) {
  const { theme } = Motion.useTheme();
  const { lang } = useI18n();
  const platform = detectPlatform();
  const [i, setI] = useState(0);
  const [zoomed, setZoomed] = useState(false);
  const [failed, setFailed] = useState(false);

  // slides can be a flat array (same for every platform) or a per-platform
  // map { win: [...], mac: [...] } so each OS gets its own step sequence
  // (the macOS wizard has an extra permissions step, for example).
  const arr = Array.isArray(slides) ? slides : (slides[platform] || slides.win || slides.mac || []);
  const n = arr.length;
  if (!n) return null;
  const idx = Math.min(i, n - 1);
  const slide = arr[idx];
  const resolved = pickTheme(slide.src, theme);
  const go = (d) => { setFailed(false); setZoomed(false); setI((idx + d + n) % n); };
  const stepWord = lang === "it" ? "Step" : "Step";
  const ofWord = lang === "it" ? "di" : "of";

  const arrowStyle = (side) => ({
    position: "absolute",
    top: "50%",
    [side]: 10,
    transform: "translateY(-50%)",
    width: 38,
    height: 38,
    borderRadius: 99,
    border: "1px solid var(--line-strong)",
    background: "var(--bg-elev)",
    backdropFilter: "blur(8px)",
    color: "var(--fg)",
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "pointer",
    fontSize: 18,
    lineHeight: 0,
  });

  return (
    <figure
      className="reveal"
      style={{ margin: "8px 0 24px" }}
      tabIndex={0}
      onKeyDown={(e) => {
        if (e.key === "ArrowLeft") { e.preventDefault(); go(-1); }
        if (e.key === "ArrowRight") { e.preventDefault(); go(1); }
      }}
    >
      <div
        style={{
          position: "relative",
          width: "100%",
          maxWidth: 720,
          margin: "0 auto",
          border: "1px solid var(--line)",
          borderRadius: 14,
          overflow: "hidden",
          background: "var(--bg-elev)",
        }}
      >
        {resolved && !failed ? (
          <button
            type="button"
            onClick={() => setZoomed(true)}
            aria-label={`${slide.label || "Step"} (click to enlarge)`}
            style={{
              display: "block",
              width: "100%",
              padding: 0,
              border: "none",
              background: "transparent",
              cursor: "zoom-in",
              lineHeight: 0,
            }}
          >
            <img
              src={resolved}
              alt={slide.label || ""}
              loading="lazy"
              decoding="async"
              onError={() => setFailed(true)}
              style={{
                display: "block",
                width: "100%",
                height: "auto",
                aspectRatio: slide.ratio || "680/600",
                objectFit: "contain",
              }}
            />
          </button>
        ) : (
          <ImagePlaceholder ratio={slide.ratio || "680/600"} />
        )}

        {n > 1 && (
          <>
            <button type="button" aria-label="Previous step" style={arrowStyle("left")} onClick={() => go(-1)}>‹</button>
            <button type="button" aria-label="Next step" style={arrowStyle("right")} onClick={() => go(1)}>›</button>
          </>
        )}
      </div>

      {/* dots */}
      {n > 1 && (
        <div style={{ display: "flex", justifyContent: "center", gap: 8, marginTop: 12 }}>
          {arr.map((_, k) => (
            <button
              key={k}
              type="button"
              aria-label={`Go to step ${k + 1}`}
              aria-current={k === idx}
              onClick={() => { setFailed(false); setI(k); }}
              style={{
                width: k === idx ? 22 : 8,
                height: 8,
                borderRadius: 99,
                border: "none",
                padding: 0,
                cursor: "pointer",
                background: k === idx ? "var(--fg)" : "var(--fg-faint)",
                transition: "width 200ms, background 200ms",
              }}
            />
          ))}
        </div>
      )}

      <figcaption
        style={{
          marginTop: 10,
          fontSize: 13,
          color: "var(--fg-faint)",
          textAlign: "center",
          fontFamily: "var(--font-mono)",
          letterSpacing: "0.02em",
        }}
      >
        {[n > 1 ? `${stepWord} ${idx + 1} ${ofWord} ${n}` : null, slide.label, caption]
          .filter(Boolean)
          .join(" · ")}
      </figcaption>

      {resolved && !failed && zoomed && (
        <ImageLightbox src={resolved} alt={slide.label || caption} onClose={() => setZoomed(false)} />
      )}
    </figure>
  );
}

function BlockVideo({ caption }) {
  return (
    <figure className="reveal" style={{ margin: "8px 0 24px" }}>
      <div
        style={{
          aspectRatio: "16/9",
          width: "100%",
          background:
            "radial-gradient(circle at 50% 50%, rgba(56,189,248,0.10), transparent 70%), var(--bg-deep)",
          border: "1px dashed var(--line-strong)",
          borderRadius: 14,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          color: "var(--fg-muted)",
          fontFamily: "var(--font-mono)",
          fontSize: 12,
          padding: 24,
          textAlign: "center",
          letterSpacing: "0.04em",
          position: "relative",
        }}
      >
        <span
          style={{
            width: 56,
            height: 56,
            borderRadius: 99,
            border: "1px solid var(--line-strong)",
            display: "inline-flex",
            alignItems: "center",
            justifyContent: "center",
            color: "var(--fg)",
            marginRight: 16,
          }}
        >
          <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
            <path d="M8 5v14l11-7z" />
          </svg>
        </span>
        <span>video · placeholder</span>
      </div>
      {caption && (
        <figcaption
          style={{
            marginTop: 10,
            fontSize: 13,
            color: "var(--fg-faint)",
            textAlign: "center",
            fontFamily: "var(--font-mono)",
            letterSpacing: "0.02em",
          }}
        >
          {caption}
        </figcaption>
      )}
    </figure>
  );
}

function BlockCallout({ kind = "info", text }) {
  const styles = {
    info: { color: "#7DD3FC", border: "rgba(56,189,248,0.30)", bg: "rgba(56,189,248,0.06)", label: "info" },
    tip: { color: "#86EFAC", border: "rgba(74,222,128,0.30)", bg: "rgba(74,222,128,0.06)", label: "tip" },
    warn: { color: "#FBBF24", border: "rgba(251,191,36,0.34)", bg: "rgba(251,191,36,0.08)", label: "warning" },
  };
  const s = styles[kind] || styles.info;
  return (
    <aside
      className="reveal"
      style={{
        margin: "8px 0 22px",
        padding: "14px 16px",
        background: s.bg,
        border: `1px solid ${s.border}`,
        borderRadius: 12,
        display: "flex",
        gap: 14,
        alignItems: "flex-start",
      }}
    >
      <span
        style={{
          flexShrink: 0,
          fontFamily: "var(--font-mono)",
          fontSize: 10,
          letterSpacing: "0.10em",
          textTransform: "uppercase",
          color: s.color,
          padding: "2px 7px",
          border: `1px solid ${s.border}`,
          borderRadius: 5,
          marginTop: 2,
        }}
      >
        {s.label}
      </span>
      <p style={{ margin: 0, fontSize: 15, lineHeight: 1.6, color: "var(--fg)", textWrap: "pretty" }}>
        {renderRich(text)}
      </p>
    </aside>
  );
}

function BlockCode({ snippet, lang }) {
  return (
    <pre
      className="reveal"
      style={{
        margin: "0 0 22px",
        padding: "14px 16px",
        background: "var(--bg-deep)",
        border: "1px solid var(--line)",
        borderRadius: 12,
        overflowX: "auto",
        fontFamily: "var(--font-mono)",
        fontSize: 13,
        lineHeight: 1.6,
        color: "var(--fg)",
        whiteSpace: "pre",
      }}
    >
      {lang && (
        <div
          style={{
            fontSize: 10,
            color: "var(--fg-faint)",
            letterSpacing: "0.08em",
            textTransform: "uppercase",
            marginBottom: 8,
          }}
        >
          {lang}
        </div>
      )}
      <code>{snippet}</code>
    </pre>
  );
}

function BlockTable({ headers, rows }) {
  return (
    <div
      className="reveal help-table-wrap"
      style={{
        margin: "0 0 24px",
        border: "1px solid var(--line)",
        borderRadius: 12,
        overflow: "hidden",
        overflowX: "auto",
      }}
    >
      <table
        style={{
          width: "100%",
          borderCollapse: "collapse",
          fontSize: 14,
          lineHeight: 1.55,
        }}
      >
        <thead>
          <tr>
            {headers.map((h, i) => (
              <th
                key={i}
                style={{
                  textAlign: "left",
                  padding: "12px 14px",
                  fontFamily: "var(--font-mono)",
                  fontSize: 11,
                  color: "var(--fg-faint)",
                  letterSpacing: "0.08em",
                  textTransform: "uppercase",
                  borderBottom: "1px solid var(--line)",
                  background: "var(--bg-elev)",
                  whiteSpace: "nowrap",
                }}
              >
                {h}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map((r, i) => (
            <tr key={i}>
              {r.map((c, j) => (
                <td
                  key={j}
                  style={{
                    padding: "12px 14px",
                    color: j === 0 ? "var(--fg)" : "var(--fg-muted)",
                    borderTop: i === 0 ? "none" : "1px solid var(--line)",
                    fontWeight: j === 0 ? 500 : 400,
                    verticalAlign: "top",
                    textWrap: "pretty",
                  }}
                >
                  {renderRich(c)}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// =============================================================
// Search. Builds an in-memory index over all articles. Matches on
// title (weighted highest), tags, summary, category, then the full
// body text. Returns results ranked by per-term score; a query
// matches only if every term lands somewhere.
// =============================================================
function flattenBlocks(blocks) {
  if (!blocks) return "";
  const out = [];
  for (const b of blocks) {
    if (!b) continue;
    if (b.text) out.push(b.text);
    if (b.snippet) out.push(b.snippet);
    if (b.caption) out.push(b.caption);
    if (Array.isArray(b.items)) {
      for (const it of b.items) {
        if (typeof it === "string") out.push(it);
        else if (it && typeof it === "object") {
          if (it.ev) out.push(it.ev);
          if (it.desc) out.push(it.desc);
        }
      }
    }
    if (Array.isArray(b.headers)) out.push(b.headers.join(" "));
    if (Array.isArray(b.rows)) {
      for (const r of b.rows) out.push(r.join(" "));
    }
    if (Array.isArray(b.keys)) out.push(b.keys.join(" "));
  }
  return out.join(" · ");
}

function useHelpSearch() {
  const { lang } = useI18n();
  return useMemo(() => {
    const index = HELP_ARTICLES.map((a) => {
      const cat = HELP_CATEGORIES.find((c) => c.id === a.category);
      const title = a.title[lang] || a.title.en;
      const summary = a.summary[lang] || a.summary.en;
      const catTitle = cat ? cat.title[lang] || cat.title.en : "";
      const blocks = a.blocks ? (a.blocks[lang] || a.blocks.en) : null;
      // Pre-lowercase each field so per-term lookups are cheap.
      const titleLc = title.toLowerCase();
      const summaryLc = summary.toLowerCase();
      const catLc = catTitle.toLowerCase();
      const tagsLc = (a.tags || []).map((t) => t.toLowerCase());
      const bodyLc = flattenBlocks(blocks).toLowerCase();
      return {
        article: a,
        category: cat,
        title,
        summary,
        catTitle,
        titleLc,
        summaryLc,
        catLc,
        tagsLc,
        bodyLc,
      };
    });
    return (rawQuery) => {
      const q = (rawQuery || "").trim().toLowerCase();
      if (!q) return [];
      const terms = q.split(/\s+/).filter(Boolean);
      const ranked = [];
      for (const e of index) {
        let score = 0;
        let allMatch = true;
        for (const t of terms) {
          // Per-term hit anywhere is required, but score depends on
          // where it landed. Title/tags worth more than body.
          let hit = false;
          if (e.tagsLc.some((tag) => tag === t)) {
            score += 6;
            hit = true;
          } else if (e.tagsLc.some((tag) => tag.includes(t))) {
            score += 4;
            hit = true;
          }
          if (e.titleLc.includes(t)) {
            score += 5;
            hit = true;
          }
          if (e.summaryLc.includes(t)) {
            score += 2;
            hit = true;
          }
          if (e.catLc.includes(t)) {
            score += 1;
            hit = true;
          }
          if (!hit && e.bodyLc.includes(t)) {
            score += 1;
            hit = true;
          }
          if (!hit) {
            allMatch = false;
            break;
          }
        }
        if (allMatch) ranked.push({ ...e, score });
      }
      ranked.sort((a, b) => b.score - a.score);
      return ranked;
    };
  }, [lang]);
}

// =============================================================
// Article card. Used in the hub and in the search results.
// =============================================================
function ArticleCard({ article, layout = "grid" }) {
  const { lang } = useI18n();
  const cat = HELP_CATEGORIES.find((c) => c.id === article.category);
  const title = article.title[lang] || article.title.en;
  const summary = article.summary[lang] || article.summary.en;
  const accent = cat ? cat.accent : "#38BDF8";
  return (
    <a
      href={`/help/${article.slug}/`}
      className="reveal help-card"
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 8,
        padding: 18,
        background: "linear-gradient(180deg, var(--bg-elev), transparent)",
        border: "1px solid var(--line)",
        borderRadius: 14,
        textDecoration: "none",
        color: "var(--fg)",
        transition: "border-color 180ms, transform 180ms, background 200ms",
        minHeight: layout === "grid" ? 132 : "auto",
      }}
      onMouseEnter={(e) => {
        e.currentTarget.style.borderColor = "var(--line-strong)";
        e.currentTarget.style.transform = "translateY(-1px)";
      }}
      onMouseLeave={(e) => {
        e.currentTarget.style.borderColor = "var(--line)";
        e.currentTarget.style.transform = "translateY(0)";
      }}
    >
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 10,
          fontFamily: "var(--font-mono)",
          fontSize: 10,
          color: "var(--fg-faint)",
          letterSpacing: "0.10em",
          textTransform: "uppercase",
        }}
      >
        <span
          style={{
            width: 6,
            height: 6,
            borderRadius: 99,
            background: accent,
            boxShadow: `0 0 6px ${accent}`,
          }}
        />
        <span>{cat ? cat.title[lang] || cat.title.en : ""}</span>
      </div>
      <div
        style={{
          fontFamily: "var(--font-display)",
          fontSize: 17,
          fontWeight: 600,
          letterSpacing: "-0.01em",
          lineHeight: 1.25,
          color: "var(--fg)",
        }}
      >
        {title}
      </div>
      <div
        style={{
          fontSize: 13.5,
          lineHeight: 1.5,
          color: "var(--fg-muted)",
          textWrap: "pretty",
        }}
      >
        {summary}
      </div>
    </a>
  );
}

window.HelpCommon = {
  HelpBlock,
  ArticleCard,
  useHelpSearch,
  renderRich,
  slugify,
};
