(function () {
// FireTail portfolio — shared rich-content block renderer.
// Used by both CaseStudyDetail.jsx (Professional Work / case-studies-data.js)
// and CaseStudy.jsx (ProjectGrid / data.js) so the two never carry diverging
// copies of the same section-type switch.
//
// Section shape (ordered array, each project/case-study supplies its own):
//   { type: "text", heading?, body?, list? }   body: string | string[] (paragraphs);
//                                               list: string[] renders as real bullets, not more
//                                               paragraphs — use for parallel items (see the
//                                               skim-first writing convention: prefer this over a
//                                               comma-chained sentence or a stack of one-line paras)
//   { type: "code", lang, code, caption? }
//   { type: "diagram", format: "mermaid", code, caption? }
//   { type: "image", src, alt, caption?, wide? }  wide: true forces full-width solo
//                                               rendering, never paired with a neighboring
//                                               image even if adjacent — use for landscape
//                                               screenshots with real text/UI in them (Editor
//                                               Inspector, Stats panel, etc.): halving the
//                                               width to fit a 2-up row makes that text
//                                               illegible. Portrait screenshots (no fine print
//                                               at risk) are the case auto-pairing is actually
//                                               meant for.
//   { type: "glossary", items: [{ term, def }] }
//   { type: "concept", heading?, body?, list?, closing? }  — "Concept — not shipped",
//                                               forward-looking, not built. body = intro
//                                               paragraph(s), list = real bullets, closing =
//                                               final paragraph (e.g. the "not built" caveat)
//   { type: "today", body }                  — "how I'd solve this now" — a small inline callout
//                                               flagging a dated implementation choice with today's
//                                               better approach, rendered directly after whatever
//                                               block it comments on (its position in the array is
//                                               the only placement mechanism needed)

// Comfortable reading measure for flowing prose — code/diagrams/images/glossary
// aren't capped by this and can use the wider column the modal actually gives
// them; only text-heavy blocks (paragraphs, concept callouts, today notes) are.
const PROSE_MAX = 680;
const MERMAID_VARS = {
  dark: {
    darkMode: true,
    background: "#0c1829",
    primaryColor: "#10141f",
    primaryTextColor: "#eef4fc",
    primaryBorderColor: "#1f9bff",
    lineColor: "#1f9bff",
    secondaryColor: "#181d2b",
    tertiaryColor: "#181d2b",
    textColor: "#d2dcee"
  },
  light: {
    darkMode: false,
    background: "#e3eefa",
    primaryColor: "#ffffff",
    primaryTextColor: "#0a0e18",
    primaryBorderColor: "#157be0",
    lineColor: "#157be0",
    secondaryColor: "#f3f6fb",
    tertiaryColor: "#f3f6fb",
    textColor: "#1e2738"
  }
};
let mermaidSeq = 0;

// Mermaid is a large library that only a fraction of visitors ever trigger
// (only case studies with a `diagram` section need it), so it's loaded
// on-demand here instead of unconditionally in index.html's <head> — shaves
// a big chunk off every other page load's critical path. Cached as a shared
// promise so multiple diagrams on the same page only fetch it once.
let mermaidLoadPromise = null;
function loadMermaid() {
  if (window.mermaid) return Promise.resolve(window.mermaid);
  if (!mermaidLoadPromise) {
    mermaidLoadPromise = new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.src = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js";
      script.onload = () => resolve(window.mermaid);
      script.onerror = () => {
        mermaidLoadPromise = null;
        reject(new Error("Failed to load mermaid"));
      };
      document.head.appendChild(script);
    });
  }
  return mermaidLoadPromise;
}
function MermaidDiagram({
  code,
  caption,
  isDark
}) {
  const [svg, setSvg] = React.useState("");
  React.useEffect(() => {
    let cancelled = false;
    loadMermaid().then(mermaid => {
      if (cancelled) return;
      mermaid.initialize({
        startOnLoad: false,
        theme: "base",
        fontFamily: "JetBrains Mono, monospace",
        themeVariables: isDark ? MERMAID_VARS.dark : MERMAID_VARS.light
      });
      mermaidSeq += 1;
      return mermaid.render("mmd-" + mermaidSeq, code).then(res => {
        if (!cancelled) setSvg(res.svg);
      });
    }).catch(err => console.error("Mermaid render failed:", err));
    return () => {
      cancelled = true;
    };
  }, [code, isDark]);
  return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
    style: {
      display: "flex",
      justifyContent: "center",
      overflowX: "auto",
      background: "var(--surface-code)",
      border: "1px solid var(--border)",
      borderRadius: "var(--radius-md)",
      padding: "20px 12px",
      contain: "layout paint",
      contentVisibility: "auto",
      containIntrinsicSize: "0 400px"
    },
    dangerouslySetInnerHTML: {
      __html: svg
    }
  }), caption && /*#__PURE__*/React.createElement("p", {
    style: captionStyle
  }, caption));
}
function CodeBlock({
  code,
  caption
}) {
  return (
    /*#__PURE__*/
    // minWidth: 0 — <pre>'s white-space: pre means a long code line can't
    // wrap, so without this the block's intrinsic width happily blows out
    // every flex/grid ancestor up to the modal's own overflow:hidden instead
    // of scrolling internally the way overflowX: auto below intends.
    React.createElement("div", {
      style: {
        minWidth: 0
      }
    }, /*#__PURE__*/React.createElement("pre", {
      style: {
        margin: 0,
        padding: "16px 18px",
        background: "var(--surface-code)",
        border: "1px solid var(--border)",
        borderRadius: "var(--radius-md)",
        overflowX: "auto",
        maxWidth: "100%",
        fontFamily: "var(--font-mono)",
        fontSize: 16,
        lineHeight: 1.65,
        color: "var(--text-body)"
      }
    }, /*#__PURE__*/React.createElement("code", null, code)), caption && /*#__PURE__*/React.createElement("p", {
      style: captionStyle
    }, caption))
  );
}

// Minimal markdown for case-study prose — three marks, each with one job:
//   **bold**     standout numbers/metrics and key outcomes
//   *italic*     named/quoted terms — "the X mechanic", in place of quote marks
//   __underline__ first mention of a proper-noun system/class name
// Nothing else (links, etc.) is supported on purpose; this is for emphasis, not
// general formatting. Underline is styled with a muted line, not the accent
// color real links use, so it never reads as clickable.
function renderRich(text) {
  const re = /\*\*(.+?)\*\*|__(.+?)__|\*(.+?)\*/g;
  const nodes = [];
  let last = 0,
    m,
    key = 0;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) nodes.push(text.slice(last, m.index));
    if (m[1] !== undefined) {
      nodes.push(/*#__PURE__*/React.createElement("strong", {
        key: key++,
        style: {
          color: "var(--text-strong)",
          fontWeight: 700
        }
      }, m[1]));
    } else if (m[2] !== undefined) {
      nodes.push(/*#__PURE__*/React.createElement("u", {
        key: key++,
        style: {
          textDecorationLine: "underline",
          textDecorationColor: "var(--border-strong)",
          textDecorationThickness: 1,
          textUnderlineOffset: 3
        }
      }, m[2]));
    } else {
      nodes.push(/*#__PURE__*/React.createElement("em", {
        key: key++,
        style: {
          fontStyle: "italic"
        }
      }, m[3]));
    }
    last = re.lastIndex;
  }
  if (last < text.length) nodes.push(text.slice(last));
  return nodes;
}
function Prose({
  body
}) {
  const paras = Array.isArray(body) ? body : [body];
  return /*#__PURE__*/React.createElement("div", {
    style: {
      display: "flex",
      flexDirection: "column",
      gap: 14
    }
  }, paras.map((p, i) => /*#__PURE__*/React.createElement("p", {
    key: i,
    style: bodyP
  }, renderRich(p))));
}

// Real bullets (not stacked paragraphs) for parallel items — bold lead-in
// label per item is a convention, not enforced, via renderRich's **..** support.
function BulletList({
  items
}) {
  return /*#__PURE__*/React.createElement("ul", {
    style: {
      margin: 0,
      padding: 0,
      listStyle: "none",
      display: "flex",
      flexDirection: "column",
      gap: 10
    }
  }, items.map((item, i) => /*#__PURE__*/React.createElement("li", {
    key: i,
    style: {
      display: "flex",
      gap: 10,
      ...bodyP
    }
  }, /*#__PURE__*/React.createElement("span", {
    "aria-hidden": "true",
    style: {
      color: "var(--accent)",
      flexShrink: 0
    }
  }, "\u2013"), /*#__PURE__*/React.createElement("span", null, renderRich(item)))));
}
function ConceptBlock({
  heading,
  body,
  list,
  closing
}) {
  return /*#__PURE__*/React.createElement("div", {
    style: {
      padding: "18px 20px",
      border: "1px dashed var(--border-strong)",
      borderRadius: "var(--radius-md)",
      background: "var(--surface-card)",
      maxWidth: PROSE_MAX
    }
  }, /*#__PURE__*/React.createElement("span", {
    style: {
      display: "inline-block",
      marginBottom: 12,
      padding: "3px 10px",
      fontFamily: "var(--font-mono)",
      fontSize: 13,
      letterSpacing: "0.1em",
      textTransform: "uppercase",
      color: "var(--text-faint)",
      border: "1px solid var(--border-strong)",
      borderRadius: "var(--radius-pill)"
    }
  }, "Concept \u2014 not shipped"), heading && /*#__PURE__*/React.createElement("h3", {
    style: {
      ...sectionH,
      marginTop: 0
    }
  }, heading), /*#__PURE__*/React.createElement("div", {
    style: {
      display: "flex",
      flexDirection: "column",
      gap: 14
    }
  }, body && /*#__PURE__*/React.createElement(Prose, {
    body: body
  }), list && /*#__PURE__*/React.createElement(BulletList, {
    items: list
  }), closing && /*#__PURE__*/React.createElement("p", {
    style: bodyP
  }, renderRich(closing))));
}
function TodayBlock({
  body
}) {
  return /*#__PURE__*/React.createElement("div", {
    style: {
      padding: "14px 18px",
      borderLeft: "3px solid var(--accent)",
      background: "var(--surface-card)",
      borderRadius: "0 var(--radius-md) var(--radius-md) 0",
      maxWidth: PROSE_MAX
    }
  }, /*#__PURE__*/React.createElement("span", {
    style: {
      display: "block",
      marginBottom: 6,
      fontFamily: "var(--font-mono)",
      fontSize: 13,
      letterSpacing: "0.1em",
      textTransform: "uppercase",
      color: "var(--accent)"
    }
  }, "Today"), /*#__PURE__*/React.createElement(Prose, {
    body: body
  }));
}
function Glossary({
  items
}) {
  return /*#__PURE__*/React.createElement("dl", {
    style: {
      margin: 0,
      display: "flex",
      flexDirection: "column",
      gap: 10
    }
  }, items.map(({
    term,
    def
  }) => /*#__PURE__*/React.createElement("div", {
    key: term,
    style: {
      display: "flex",
      gap: 12,
      flexWrap: "wrap"
    }
  }, /*#__PURE__*/React.createElement("dt", {
    style: {
      margin: 0,
      fontFamily: "var(--font-mono)",
      fontSize: 14,
      letterSpacing: "0.06em",
      color: "var(--accent)",
      flexShrink: 0,
      width: 190
    }
  }, term), /*#__PURE__*/React.createElement("dd", {
    style: {
      margin: 0,
      fontFamily: "var(--font-sans)",
      fontSize: 16,
      color: "var(--text-muted)",
      flex: 1,
      minWidth: 200
    }
  }, def))));
}

// A lone image (not sharing a row with a neighbor) defaults to full container
// width, and a tall portrait screenshot/gif at that width can balloon to
// nearly the viewport's height. Capping it at the same height a paired
// (half-width) image naturally renders at — then letting width scale down
// with it instead of stretching to 100% — keeps a solo image visually
// consistent with the 2-up rows instead of dominating the page.
const SOLO_IMAGE_MAX_H = 640;
function ImageBlock({
  src,
  alt,
  caption,
  solo
}) {
  const figureStyle = solo ? {
    margin: "0 auto",
    maxWidth: "100%",
    width: "fit-content",
    display: "flex",
    flexDirection: "column",
    alignItems: "center"
  } : {
    margin: 0
  };
  const imgStyle = solo ? {
    maxHeight: SOLO_IMAGE_MAX_H,
    maxWidth: "100%",
    width: "auto",
    height: "auto",
    borderRadius: "var(--radius-md)",
    border: "1px solid var(--border)",
    display: "block"
  } : {
    width: "100%",
    borderRadius: "var(--radius-md)",
    border: "1px solid var(--border)",
    display: "block"
  };
  return /*#__PURE__*/React.createElement("figure", {
    style: figureStyle
  }, /*#__PURE__*/React.createElement("img", {
    src: src,
    alt: alt || "",
    loading: "lazy",
    style: imgStyle
  }), caption && /*#__PURE__*/React.createElement("figcaption", {
    style: solo ? {
      ...captionStyle,
      textAlign: "center"
    } : captionStyle
  }, caption));
}
function renderBlock(s, key, isDark) {
  if (s.type === "text") {
    return /*#__PURE__*/React.createElement("div", {
      key: key,
      style: {
        display: "flex",
        flexDirection: "column",
        gap: 14,
        maxWidth: PROSE_MAX
      }
    }, s.heading && /*#__PURE__*/React.createElement("h3", {
      style: {
        ...sectionH,
        marginBottom: 0
      }
    }, s.heading), s.body && /*#__PURE__*/React.createElement(Prose, {
      body: s.body
    }), s.list && /*#__PURE__*/React.createElement(BulletList, {
      items: s.list
    }));
  }
  if (s.type === "concept") return /*#__PURE__*/React.createElement(ConceptBlock, {
    key: key,
    heading: s.heading,
    body: s.body,
    list: s.list,
    closing: s.closing
  });
  if (s.type === "today") return /*#__PURE__*/React.createElement(TodayBlock, {
    key: key,
    body: s.body
  });
  if (s.type === "code") return /*#__PURE__*/React.createElement(CodeBlock, {
    key: key,
    code: s.code,
    caption: s.caption
  });
  if (s.type === "diagram") return /*#__PURE__*/React.createElement(MermaidDiagram, {
    key: key,
    code: s.code,
    caption: s.caption,
    isDark: isDark
  });
  if (s.type === "image") return /*#__PURE__*/React.createElement(ImageBlock, {
    key: key,
    src: s.src,
    alt: s.alt,
    caption: s.caption,
    solo: true
  });
  if (s.type === "glossary") return /*#__PURE__*/React.createElement("div", {
    key: key
  }, /*#__PURE__*/React.createElement("h3", {
    style: sectionH
  }, "Terms"), /*#__PURE__*/React.createElement(Glossary, {
    items: s.items
  }));
  return null;
}

// Consecutive `image` blocks get grouped so Sections can lay them out 2-up
// instead of one-per-row. Chunked in pairs of (up to) 2, never more — a run
// of 4 becomes two 2-up rows, not one row auto-fitting 3-4 columns across a
// wide modal. An odd leftover (or a run of 1) falls through to the normal
// single-block path below, rendered full width via the "solo" ImageBlock path.
function groupImageRuns(sections) {
  const groups = [];
  let i = 0;
  while (i < sections.length) {
    // A `wide` image never joins a pairable run — treat it exactly like a
    // non-image block here, so it always falls through to the full-width
    // solo path below, even between two otherwise-pairable neighbors.
    if (sections[i].type === "image" && !sections[i].wide) {
      const run = [];
      while (i < sections.length && sections[i].type === "image" && !sections[i].wide) {
        run.push(sections[i]);
        i++;
      }
      for (let j = 0; j < run.length; j += 2) {
        groups.push({
          kind: "images",
          items: run.slice(j, j + 2)
        });
      }
    } else {
      groups.push({
        kind: "single",
        item: sections[i]
      });
      i++;
    }
  }
  return groups;
}
function Sections({
  sections,
  isDark
}) {
  return groupImageRuns(sections || []).map((g, gi) => {
    if (g.kind === "single") return renderBlock(g.item, gi, isDark);
    if (g.items.length === 1) return renderBlock(g.items[0], gi, isDark);
    // Always exactly 2 items here (groupImageRuns chunks runs pairwise) — a
    // 2-up grid, never 3+ even in a wide modal. Portrait screenshots eat a
    // huge amount of vertical space stacked one-per-row, and read fine at
    // half width since they were shot on a phone screen to begin with.
    // minmax(200px, 1fr) collapses to a single column on its own once the
    // container's too narrow for two, no separate breakpoint needed.
    return /*#__PURE__*/React.createElement("div", {
      key: gi,
      style: {
        display: "grid",
        gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
        gap: 16
      }
    }, g.items.map((s, si) => /*#__PURE__*/React.createElement(ImageBlock, {
      key: si,
      src: s.src,
      alt: s.alt,
      caption: s.caption
    })));
  });
}
const captionStyle = {
  margin: "8px 4px 0",
  fontFamily: "var(--font-sans)",
  fontSize: 15,
  fontStyle: "italic",
  color: "var(--text-faint)"
};
const sectionH = {
  margin: "0 0 12px",
  fontFamily: "var(--font-mono)",
  fontSize: 14,
  letterSpacing: "0.14em",
  textTransform: "uppercase",
  color: "var(--text-muted)"
};
const bodyP = {
  margin: 0,
  fontFamily: "var(--font-sans)",
  fontSize: 19,
  lineHeight: 1.66,
  color: "var(--text-body)"
};
window.CaseStudySections = {
  Sections,
  Prose,
  BulletList,
  renderRich,
  sectionH,
  bodyP,
  captionStyle,
  PROSE_MAX
};
})();
