{"id":99275,"date":"2026-10-06T13:57:56","date_gmt":"2026-10-06T20:57:56","guid":{"rendered":"https:\/\/github.blog\/?p=99275"},"modified":"2026-10-07T08:19:50","modified_gmt":"2026-10-07T15:19:50","slug":"building-git-infrastructure-for-agent-scale-development","status":"publish","type":"post","link":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/","title":{"rendered":"Building Git infrastructure for agent-scale development"},"content":{"rendered":"<!DOCTYPE html PUBLIC \"-\/\/W3C\/\/DTD HTML 4.0 Transitional\/\/EN\" \"http:\/\/www.w3.org\/TR\/REC-html40\/loose.dtd\">\n<html><body><p class=\"wp-block-paragraph\">Every day on GitHub, millions of developers build the products their customers rely on, contribute to open source, and pursue personal projects. GitHub&rsquo;s architecture has changed steadily over the years to support that work and the growing demands of the developers and organizations who depend on it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Agentic software development is driving the next architectural shift. With developers and agents working concurrently in repositories that receive millions of commits a day, these workloads demand a different Git architecture. We&rsquo;re rebuilding GitHub&rsquo;s Git infrastructure to support them. This post explores the demands shaping that work and the design principles behind it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Today&rsquo;s highest-volume workloads show the scale we&rsquo;re building for. The gap between a typical repository and the busiest ones is wider than most people expect. Here&rsquo;s a rough picture of the monthly repository activity distribution on GitHub from August 2026:<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n.ghchart {\n  \/* Theme tokens *\/\n  --ghc-text: #1f2328;\n  --ghc-muted: #656d76;\n  --ghc-grid: #d0d7de;\n  --ghc-axis: #59636e;\n  --ghc-card-bg: #ffffff;\n  --ghc-card-border: #d0d7de;\n  --ghc-canvas: #f2f5f3;\n  --ghc-divider: #b6bfb8;\n  --ghc-guide: #afb8c1;\n\n  \/* GitHub brand font \u2014 matches the github.blog stack (Mona Sans, with\n     the blog's metric fallback) so it renders identically in-context. *\/\n  font-family: \"Mona Sans\", \"Mona Sans Fallback\", -apple-system, \"system-ui\",\n    \"Segoe UI\", Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n\n  position: relative;\n  box-sizing: border-box;\n  width: 100%;\n  max-width: 699px;\n  margin: 0 auto;\n  color: var(--ghc-text);\n  line-height: 1.4;\n}\n\n.ghchart *,\n.ghchart *::before,\n.ghchart *::after { box-sizing: border-box; }\n\n\/* ------------------------------------------------------------------ *\/\n\/* Header + legend                                                     *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__header {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  min-height: 24px;\n  margin-bottom: 4px;\n}\n\n.ghchart__legend {\n  display: flex;\n  align-items: center;\n  gap: 20px;\n  flex-wrap: wrap;\n  justify-content: flex-end;\n}\n\n.ghchart__legend-item {\n  display: inline-flex;\n  align-items: center;\n  gap: 8px;\n  font-size: clamp(14px, 1.9vw, 17px);\n  font-weight: 600;\n  color: var(--ghc-muted);\n  white-space: nowrap;\n}\n\n.ghchart__swatch {\n  width: 14px;\n  height: 14px;\n  border-radius: 4px;\n  flex: 0 0 auto;\n}\n\n\/* ------------------------------------------------------------------ *\/\n\/* Plot (SVG)                                                          *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__plot {\n  display: block;\n  width: 100%;\n  height: auto;\n  overflow: visible;\n  touch-action: pan-y;\n}\n\n.ghchart__gridline {\n  stroke: var(--ghc-grid);\n  stroke-width: 1;\n  stroke-dasharray: 2 6;\n  stroke-linecap: round;\n  opacity: 0.9;\n}\n\n.ghchart__axislabel {\n  fill: var(--ghc-axis);\n  font-size: 22px;\n  font-weight: 500;\n}\n.ghchart__axislabel--x { font-weight: 600; }\n\n.ghchart__line {\n  fill: none;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n}\n\n\/* Trailing partial\/incomplete segment of a line (e.g. a month still in\n   progress) \u2014 dashed so it reads as \"not final yet\" without a legend. *\/\n.ghchart__line--dashed {\n  stroke-dasharray: 7 6;\n}\n\n.ghchart__dot { transition: r 0.12s ease; }\n\n\/* Dashed threshold line (e.g. a target\/reference level) + floating label.\n   Stroke color is set inline per chart; dash + label styling live here. *\/\n.ghchart__threshold {\n  stroke-width: 2;\n  stroke-dasharray: 7 6;\n  stroke-linecap: round;\n  opacity: 0.9;\n}\n.ghchart__threshold-label {\n  font-size: 20px;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n}\n\n\/* Hatched band marking a partial \/ projected trailing region. *\/\n.ghchart__projection { pointer-events: none; }\n\n.ghchart__guide {\n  stroke: var(--ghc-guide);\n  stroke-width: 1.5;\n  stroke-dasharray: 4 4;\n  opacity: 0;\n  transition: opacity 0.12s ease;\n}\n\n.ghchart__hit { fill: transparent; }\n\n\/* ------------------------------------------------------------------ *\/\n\/* Scatter plot                                                        *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__gridline--v { \/* vertical gridlines reuse the dashed grid style *\/ }\n\n.ghchart__axistitle {\n  fill: var(--ghc-muted);\n  font-size: 20px;\n  font-weight: 600;\n  letter-spacing: -0.01em;\n}\n\n\/* Dotted reference crosshair through the \"reference\" point. Stroke color\n   is set inline (tracks the reference series color). *\/\n.ghchart__crosshair {\n  stroke-width: 2;\n  stroke-dasharray: 2 7;\n  stroke-linecap: round;\n  opacity: 0.85;\n  pointer-events: none;\n}\n\n.ghchart__halo { opacity: 0.18; pointer-events: none; }\n.ghchart__pt { transition: r 0.12s ease; }\n\n.ghchart__pt-label {\n  fill: var(--ghc-muted);\n  font-size: 19px;\n  font-weight: 600;\n  letter-spacing: -0.01em;\n  pointer-events: none;\n}\n.ghchart__pt-label--strong { font-size: 21px; font-weight: 700; }\n\n.ghchart__pthit { fill: transparent; cursor: default; outline: none; }\n.ghchart__pthit:focus-visible {\n  outline: 2px solid var(--ghc-axis);\n  outline-offset: 1px;\n}\n\n.ghchart__swatch--dot { border-radius: 50%; }\n\n\/* ------------------------------------------------------------------ *\/\n\/* Trend chart (dated time-series with narrative headline)             *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__headline {\n  font-size: clamp(17px, 2.3vw, 20px);\n  line-height: 1.32;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n  color: var(--ghc-text);\n  margin: 0 0 8px;\n}\n\n\/* Trend legend sits left-aligned under the headline\/subtitle. *\/\n.ghchart__legend--trend {\n  justify-content: flex-start;\n  margin: 2px 0 14px;\n}\n.ghchart__glyph { flex: 0 0 auto; }\n.ghchart__glyph--square {\n  width: 13px; height: 13px; border-radius: 2px;\n}\n.ghchart__glyph--diamond {\n  width: 12px; height: 12px; border: 1.5px solid; transform: rotate(45deg);\n}\n.ghchart__glyph--line {\n  width: 20px; height: 0; border-top: 2px dashed; border-radius: 0;\n}\n\n\/* Phase background columns + separators + top labels. *\/\n.ghchart__phase { fill: #f6f8fa; opacity: 0.55; }\n.ghchart__phase--alt { fill: #ffffff; opacity: 0; }\n.ghchart__phase-sep {\n  stroke: var(--ghc-grid); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.8;\n}\n.ghchart__phase-label {\n  fill: var(--ghc-muted);\n  font-size: 15px;\n  font-weight: 700;\n  letter-spacing: 0.06em;\n}\n.ghchart__phase-avg {\n  font-size: 16px;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n}\n\n\/* Dashed trend line + \u00b1 noise band + slope label. *\/\n.ghchart__trendband { opacity: 0.12; pointer-events: none; }\n.ghchart__trendline {\n  stroke-width: 2.5;\n  stroke-dasharray: 8 6;\n  stroke-linecap: round;\n  fill: none;\n}\n.ghchart__slope {\n  font-size: 20px;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n}\n\n\/* Horizontal reference line + label. *\/\n.ghchart__refline {\n  stroke-width: 2;\n  stroke-dasharray: 7 6;\n  stroke-linecap: round;\n  opacity: 0.85;\n}\n.ghchart__reflabel {\n  font-size: 16px;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n}\n\n\/* Data squares \u2014 values reveal on hover only. *\/\n.ghchart__square { transition: width 0.12s ease, height 0.12s ease, x 0.12s ease, y 0.12s ease; }\n.ghchart__squarehit { fill: transparent; cursor: default; outline: none; }\n.ghchart__squarehit:focus-visible {\n  outline: 2px solid var(--ghc-axis);\n  outline-offset: 1px;\n}\n\n\/* Invalid-run markers + leader-line annotations. *\/\n.ghchart__invalid { stroke-width: 2; }\n.ghchart__invalid-x { stroke-width: 2; stroke-linecap: round; }\n.ghchart__leader { stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.8; }\n.ghchart__leader--callout { stroke-dasharray: none; opacity: 0.6; }\n.ghchart__anno-title {\n  font-size: 15px;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n}\n.ghchart__anno-sub {\n  fill: var(--ghc-muted);\n  font-size: 13px;\n  font-weight: 500;\n}\n\/* Final-callout labels read a touch larger than the invalid-run notes. *\/\n.ghchart__anno-title--callout { font-size: 17px; }\n.ghchart__anno-sub--callout { font-size: 14px; }\n\n\/* Bracket span annotation. *\/\n.ghchart__bracket { stroke: var(--ghc-axis); stroke-width: 1.5; opacity: 0.75; }\n.ghchart__bracket-label {\n  fill: var(--ghc-muted);\n  font-size: 13px;\n  font-weight: 700;\n  letter-spacing: 0.04em;\n}\n\n\/* Final callout ring. *\/\n.ghchart__callout-ring { fill: none; stroke-width: 2; }\n\n\/* ------------------------------------------------------------------ *\/\n\/* Title + divider                                                     *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__title {\n  \/* Matches the github.blog H2 (.wp-block-heading) exactly *\/\n  display: block;\n  font-size: 32px;\n  line-height: 40px;\n  font-weight: 700;\n  letter-spacing: -0.18px;\n  color: var(--ghc-text);\n}\n\n.ghchart__rule {\n  height: 1.5px;\n  border-radius: 1.5px;\n  background: var(--ghc-divider);\n  margin: 10px 0 18px;\n}\n\n\/* ------------------------------------------------------------------ *\/\n\/* Bar chart: subtitle, panels, bars, caption                          *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__subtitle {\n  margin: -6px 0 20px;\n  font-size: clamp(13px, 1.6vw, 15px);\n  line-height: 1.5;\n  color: var(--ghc-muted);\n}\n\n.ghchart__panel { margin-top: 30px; }\n.ghchart__panel:first-of-type { margin-top: 6px; }\n\n.ghchart__panel-title {\n  margin: 0 0 6px;\n  font-size: clamp(15px, 1.9vw, 18px);\n  font-weight: 700;\n  letter-spacing: -0.01em;\n  color: var(--ghc-text);\n}\n\n.ghchart__plot--bar { touch-action: pan-y; }\n\n.ghchart__axisline {\n  stroke: var(--ghc-axis);\n  stroke-width: 1.25;\n  opacity: 0.35;\n}\n\n.ghchart__bar { transition: opacity 0.12s ease; }\n.ghchart__barhit { fill: transparent; }\n\n\/* Always-on value labels above selected bars. *\/\n.ghchart__barvalue {\n  fill: var(--ghc-text);\n  font-size: 20px;\n  font-weight: 700;\n  font-variant-numeric: tabular-nums;\n  pointer-events: none;\n}\n\n\/* Anchor annotation: dotted reference line + floating pill. *\/\n.ghchart__anno-line {\n  stroke: #8b949e;\n  stroke-width: 1.5;\n  stroke-dasharray: 4 3;\n  stroke-linecap: round;\n  pointer-events: none;\n}\n.ghchart__anno-chip {\n  fill: #ffebe9;\n  stroke: #ffcecb;\n  stroke-width: 1;\n  pointer-events: none;\n}\n.ghchart__anno-chip-text {\n  fill: #a0111f;\n  font-weight: 700;\n  pointer-events: none;\n}\n\n.ghchart__caption {\n  margin-top: 22px;\n  padding-top: 12px;\n  border-top: 1px solid var(--ghc-card-border);\n  font-size: 12.5px;\n  line-height: 1.55;\n  color: var(--ghc-muted);\n}\n.ghchart__caption b { color: var(--ghc-text); font-weight: 700; }\n.ghchart__caption code {\n  font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n  font-size: 0.92em;\n  padding: 1px 5px;\n  background: #f6f8fa;\n  border: 1px solid var(--ghc-card-border);\n  border-radius: 5px;\n}\n.ghchart__tag {\n  display: inline-block;\n  font-size: 10px;\n  font-weight: 650;\n  letter-spacing: 0.03em;\n  text-transform: uppercase;\n  padding: 2px 7px;\n  border-radius: 999px;\n  border: 1px solid #aceebb;\n  color: #0a5223;\n  background: #dafbe1;\n  white-space: nowrap;\n  vertical-align: middle;\n}\n\n\/* ------------------------------------------------------------------ *\/\n\/* Distribution dashboard (segmented mix bar + ranked bar panels)      *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__dist-section { margin-top: 28px; }\n.ghchart__dist-section:first-of-type { margin-top: 12px; }\n\n.ghchart--panel-cards { padding: 24px; background: var(--ghc-canvas); }\n.ghchart--panel-cards .ghchart__dist-cols { gap: 24px; }\n.ghchart--panel-cards .ghchart__dist-cols .ghchart__dist-section {\n  min-width: 0;\n  margin: 0;\n  padding: 24px;\n  border: 1px solid var(--ghc-card-border);\n  border-radius: 8px;\n  background: var(--ghc-card-bg);\n}\n@media (max-width: 480px) {\n  .ghchart--panel-cards,\n  .ghchart--panel-cards .ghchart__dist-cols .ghchart__dist-section { padding: 16px; }\n}\n\n\/* Segmented proportional \"mix\" bar: name inside, count\/percent below.\n   Separators are 2px inside borders (box-sizing:border-box) rather than a\n   flex gap, so the three segments sum to exactly 100% with no overflow. *\/\n.ghchart__seg {\n  display: flex;\n  width: 100%;\n  height: 42px;\n  margin-top: 2px;\n  border-radius: 7px;\n  overflow: hidden;\n}\n.ghchart__seg-cell {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  min-width: 0;\n  color: #ffffff;\n  font-size: 15px;\n  font-weight: 700;\n  letter-spacing: -0.01em;\n  white-space: nowrap;\n  overflow: hidden;\n}\n.ghchart__seg-cell + .ghchart__seg-cell { border-left: 2px solid var(--ghc-card-bg); }\n.ghchart__seg-note-row {\n  display: flex;\n  width: 100%;\n  margin-top: 8px;\n}\n.ghchart__seg-note {\n  min-width: 0;\n  text-align: center;\n  font-size: 12.5px;\n  color: var(--ghc-muted);\n  font-variant-numeric: tabular-nums;\n  white-space: nowrap;\n}\n\/* Keep the outer notes from spilling past the chart edges. *\/\n.ghchart__seg-note:first-child { text-align: left; }\n.ghchart__seg-note:last-child { text-align: right; }\n.ghchart__seg-note b { color: var(--ghc-text); font-weight: 700; }\n\n\/* Ranked horizontal bar list: [category | bar track | value].\n   The list is the grid and rows use display:contents, so the category,\n   bar, and value columns line up across every row in a panel. *\/\n.ghchart__dist-list {\n  display: grid;\n  grid-template-columns: max-content 1fr max-content;\n  align-items: center;\n  column-gap: 12px;\n  row-gap: 10px;\n  margin-top: 4px;\n}\n.ghchart__dist-row { display: contents; }\n.ghchart__dist-cat {\n  text-align: right;\n  font-size: 13px;\n  line-height: 1.25;\n  color: var(--ghc-text);\n  white-space: nowrap;\n}\n.ghchart__dist-track { width: 100%; }\n.ghchart__dist-bar {\n  height: 15px;\n  min-width: 2px;\n  border-radius: 3px;\n}\n.ghchart__dist-val {\n  font-size: 12.5px;\n  color: var(--ghc-muted);\n  font-variant-numeric: tabular-nums;\n  white-space: nowrap;\n}\n.ghchart__dist-val b { color: var(--ghc-text); font-weight: 700; }\n\n.ghchart__dist-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 16px; margin: 0; }\n.ghchart__dist-stat { display: flex; flex-direction: column; gap: 4px; }\n.ghchart__dist-stat dt { color: var(--ghc-muted); font-size: 13px; }\n.ghchart__dist-stat dd { order: -1; margin: 0; font-size: 30px; font-weight: 800; letter-spacing: -0.02em; }\n\n.ghchart__comparison-row { display: grid; grid-template-columns: 145px minmax(0, 1fr); gap: 20px; padding: 14px 0; }\n.ghchart__comparison-label { align-self: center; font-size: 15px; font-weight: 600; }\n.ghchart__comparison-plot { position: relative; padding: 6px 0; display: grid; gap: 8px; }\n.ghchart__comparison-grid {\n  position: absolute; top: 0; bottom: 0; border-left: 1px dashed var(--ghc-grid);\n  transform: translateX(-50%); pointer-events: none;\n}\n.ghchart__comparison-zero { border-left: 2px solid var(--ghc-axis); z-index: 1; }\n.ghchart__comparison-track { position: relative; height: 28px; }\n.ghchart__comparison-bar {\n  position: absolute; top: 0; height: 28px;\n  border-radius: 0 2px 2px 0;\n  pointer-events: none;\n}\n.ghchart__comparison-bar--negative { border-radius: 2px 0 0 2px; }\n.ghchart__comparison-value {\n  position: absolute;\n  top: 4px;\n  z-index: 2;\n  width: max-content;\n  color: var(--ghc-text);\n  font-size: 13px;\n  font-weight: 600;\n  line-height: 20px;\n  font-variant-numeric: tabular-nums;\n  white-space: nowrap;\n}\n.ghchart__comparison-track--stacked .ghchart__comparison-value {\n  position: relative;\n  top: 0;\n  width: 100%;\n  white-space: normal;\n  overflow-wrap: anywhere;\n  background: var(--ghc-card-bg);\n}\n.ghchart__comparison-axis { position: relative; height: 24px; }\n.ghchart__comparison-tick { position: absolute; transform: translateX(-50%); white-space: nowrap; font-size: 12px; color: var(--ghc-axis); }\n@media (max-width: 480px) {\n  .ghchart__comparison-row { grid-template-columns: minmax(0, 1fr); gap: 8px; }\n  .ghchart__comparison-tick { font-size: 11px; }\n}\n\n\/* Two-up panel grid (collapses to one column on narrow screens).\n   minmax(0,1fr) lets columns shrink so long labels never force overflow. *\/\n.ghchart__dist-cols {\n  margin-top: 26px;\n  display: grid;\n  grid-template-columns: repeat(2, minmax(0, 1fr));\n  gap: 24px 30px;\n}\n.ghchart__dist-cols .ghchart__dist-section { margin-top: 0; }\n.ghchart__dist-cols--stack { grid-template-columns: 1fr; }\n.ghchart__dist-cols--stack .ghchart__dist-section { margin-top: 28px; }\n.ghchart__dist-cols--stack .ghchart__dist-section:first-child { margin-top: 0; }\n\n\/* Tighten the label\/value type a touch in the 2-up layout so the bars\n   keep room at the 699px blog width. *\/\n.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-cat { font-size: 12px; }\n.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-val { font-size: 11.5px; }\n.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-list { column-gap: 9px; }\n\n@media (max-width: 600px) {\n  .ghchart__dist-cols { grid-template-columns: 1fr; gap: 26px; }\n  .ghchart__dist-cols .ghchart__dist-section ~ .ghchart__dist-section { margin-top: 2px; }\n}\n\n\/* Let long category labels wrap on very narrow (mobile) widths. *\/\n@media (max-width: 480px) {\n  .ghchart__dist-cat { white-space: normal; }\n}\n\n\/* ------------------------------------------------------------------ *\/\n\/* Timeline \/ staged-duration chart (request lifecycle, before\/after)  *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__tl-eyebrow {\n  font-size: clamp(12px, 1.5vw, 13px);\n  font-weight: 700;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n  color: var(--ghc-muted);\n  margin: 0 0 6px;\n}\n.ghchart__tl-headline { margin-bottom: 14px; }\n\n\/* Before -> after summary card. *\/\n.ghchart__tl-stat {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  flex-wrap: wrap;\n  gap: 16px 24px;\n  padding: 16px 22px;\n  border: 1px solid var(--ghc-card-border);\n  border-radius: 12px;\n  \/* Greenish grey to match the AI gateway diagram canvas (--ghd-canvas). *\/\n  background: #f2f5f3;\n  margin: 0 0 18px;\n}\n.ghchart__tl-stat-main { display: flex; align-items: center; gap: 20px; }\n.ghchart__tl-stat-col { display: flex; flex-direction: column; gap: 2px; }\n.ghchart__tl-stat-k {\n  font-size: 12px; font-weight: 700; letter-spacing: 0.08em;\n  text-transform: uppercase; color: var(--ghc-muted);\n}\n.ghchart__tl-stat-v {\n  font-size: clamp(28px, 4.4vw, 40px); font-weight: 800;\n  letter-spacing: -0.02em; line-height: 1; color: var(--ghc-text);\n}\n.ghchart__tl-stat-v--muted { color: var(--ghc-muted); font-weight: 700; }\n.ghchart__tl-stat-arrow { font-size: 26px; color: var(--ghc-muted); }\n.ghchart__tl-stat-delta { text-align: right; }\n.ghchart__tl-stat-delta-v {\n  font-size: clamp(26px, 4vw, 38px); font-weight: 800;\n  letter-spacing: -0.02em; line-height: 1; color: #1a7f37;\n}\n.ghchart__tl-stat-delta-k { font-size: 13px; color: var(--ghc-muted); margin-top: 4px; }\n\n\/* Stage legend (2-up, monospace-free, stacks on mobile). *\/\n.ghchart__tl-legend {\n  display: grid;\n  grid-template-columns: repeat(2, minmax(0, 1fr));\n  gap: 10px 28px;\n  margin: 0 0 20px;\n}\n.ghchart__tl-legend-item { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }\n.ghchart__tl-swatch { width: 13px; height: 13px; border-radius: 3px; flex: 0 0 auto; }\n.ghchart__tl-legend-text {\n  font-size: clamp(13px, 1.7vw, 15px); color: var(--ghc-text);\n  font-variant-numeric: tabular-nums;\n}\n.ghchart__tl-rule {\n  height: 1.5px; border-radius: 1.5px; background: var(--ghc-divider); margin: 4px 0 18px;\n}\n\n\/* SVG plot text. *\/\n.ghchart__tl-rowlabel { fill: var(--ghc-text); font-size: 22px; font-weight: 700; letter-spacing: 0.02em; }\n.ghchart__tl-rowsub { fill: var(--ghc-muted); font-size: 18px; font-weight: 500; }\n.ghchart__tl-seg-label { fill: #ffffff; font-size: 23px; font-weight: 700; letter-spacing: -0.01em; }\n.ghchart__tl-callout-line { stroke: var(--ghc-guide); stroke-width: 1.5; fill: none; }\n.ghchart__tl-callout-label { font-size: 21px; font-weight: 600; letter-spacing: -0.01em; }\n\/* Segments + ribbons ease their opacity for the before\/after hover emphasis. *\/\n.ghchart__tl-seg { transition: opacity 0.14s ease; }\n.ghchart__tl-seg[data-stage] { cursor: pointer; }\n.ghchart__tl-seg[data-stage]:focus-visible { outline: 2px solid var(--ghc-axis); outline-offset: 1px; }\n.ghchart__tl-flow { opacity: 0.16; transition: opacity 0.14s ease; }\n\/* Timeline millisecond gridlines: thicker + larger dashes so the axis marks\n   read clearly behind the bars (scoped so scatter gridlines are unaffected). *\/\n.ghchart__plot--timeline .ghchart__gridline--v {\n  stroke-width: 1.5;\n  stroke-dasharray: 5 5;\n  opacity: 1;\n}\n\/* Timeline bracket labels read larger than the trend bracket note (they are\n   structural spans, not a small secondary annotation). *\/\n.ghchart__tl-bracket-label { font-size: 20px; }\n\/* Wrapper so DOM labels can sit over the SVG at viewBox-proportional %s. *\/\n.ghchart__tl-plotwrap { position: relative; }\n\/* Right-bracket label as a DOM node: a fixed, readable size that does NOT\n   scale down with the plot on small windows (unlike SVG text). *\/\n.ghchart__tl-bracket-label-dom {\n  position: absolute;\n  transform: translateY(-50%);\n  font-size: clamp(12px, 1.5vw, 14px);\n  font-weight: 700;\n  letter-spacing: 0.02em;\n  color: var(--ghc-muted);\n  white-space: nowrap;\n  pointer-events: none;\n}\n.ghchart__tl-tick { fill: var(--ghc-axis); }\n\/* Axis title rendered as a DOM caption below the plot; matches the legend\n   labels' size\/weight\/color instead of scaling with the SVG. *\/\n.ghchart__tl-axis-caption {\n  font-size: clamp(13px, 1.7vw, 15px);\n  font-weight: 400;\n  color: var(--ghc-text);\n  text-align: center;\n  margin-top: 8px;\n}\n\n@media (max-width: 560px) {\n  .ghchart__tl-legend { grid-template-columns: 1fr; }\n  .ghchart__tl-stat { justify-content: flex-start; }\n  .ghchart__tl-stat-delta { text-align: left; }\n}\n\n\/* ------------------------------------------------------------------ *\/\n\/* Calendar heatmap (GitHub contribution-graph style)                  *\/\n\/* ------------------------------------------------------------------ *\/\n.ghheat__eyebrow { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }\n.ghheat__eyedot {\n  width: 9px; height: 9px; border-radius: 50%;\n  background: #cf222e; box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.14);\n  flex: 0 0 auto;\n}\n.ghheat__eyetext {\n  font-size: 12px; font-weight: 700; letter-spacing: 0.08em;\n  text-transform: uppercase; color: #cf222e;\n}\n\/* Subtitle here is a prominent lede, not the muted variant. *\/\n.ghheat__subtitle {\n  color: var(--ghc-text);\n  font-size: clamp(14px, 1.7vw, 16px);\n  \/* 24px top gap matches the github.blog H2 \u2192 paragraph block spacing. *\/\n  margin: 24px 0 8px;\n}\n.ghheat__subtitle b { font-weight: 700; }\n\n.ghheat__panel { margin-top: 36px; }\n.ghheat__phead {\n  display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;\n  margin-bottom: 16px;\n}\n.ghheat__badge {\n  display: inline-flex; align-items: center; justify-content: center;\n  width: 22px; height: 22px; border-radius: 50%;\n  color: #fff; font-size: 12px; font-weight: 700;\n  flex: 0 0 auto; align-self: center;\n}\n.ghheat__ptitle {\n  font-size: clamp(16px, 2vw, 19px); font-weight: 700;\n  letter-spacing: -0.01em; color: var(--ghc-text);\n}\n.ghheat__pdesc { font-size: 14px; color: var(--ghc-muted); }\n\n.ghheat__body { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }\n.ghheat__gridwrap { flex: 1 1 340px; min-width: 260px; }\n.ghheat__grid { display: block; width: 100%; height: auto; overflow: visible; }\n.ghheat__cell {\n  stroke: rgba(27, 31, 36, 0.06);\n  stroke-width: 1;\n  transition: stroke 0.1s ease;\n}\n.ghheat__cell:hover { stroke: #24292f; stroke-width: 1.4; }\n.ghheat__cell--pad { opacity: 0; pointer-events: none; }\n.ghheat__wd, .ghheat__month {\n  fill: var(--ghc-muted); font-size: 11px; font-weight: 600;\n}\n\n.ghheat__side { flex: 1 1 250px; min-width: 230px; }\n.ghheat__stat { display: flex; align-items: baseline; gap: 7px; }\n.ghheat__statbig {\n  font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em;\n  line-height: 1;\n}\n.ghheat__statsmall { font-size: 15px; font-weight: 600; color: var(--ghc-muted); }\n.ghheat__note {\n  margin: 10px 0 16px; font-size: 14px; line-height: 1.55; color: var(--ghc-muted);\n}\n.ghheat__note b { color: var(--ghc-text); font-weight: 700; }\n.ghheat__legend {\n  display: flex; align-items: center; gap: 5px;\n  font-size: 12.5px; color: var(--ghc-muted); flex-wrap: wrap;\n}\n.ghheat__sw { width: 12px; height: 12px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }\n.ghheat__sw--empty { border: 1px solid #d0d7de; }\n.ghheat__legunit { margin-left: 2px; }\n.ghheat__extra {\n  display: flex; align-items: flex-start; gap: 7px; margin-top: 14px;\n  font-size: 12px; line-height: 1.5; color: var(--ghc-muted); max-width: 330px;\n}\n.ghheat__extra .ghheat__sw { margin-top: 2px; }\n\n\/* ------------------------------------------------------------------ *\/\n\/* Tooltip                                                             *\/\n\/* ------------------------------------------------------------------ *\/\n.ghchart__tooltip {\n  position: absolute;\n  z-index: 5;\n  top: 0;\n  left: 0;\n  \/* `width: max-content` keeps the box sized to its own content instead of\n     the browser's absolute-position \"shrink-to-fit\" formula, which derives\n     available width from how far `left` sits from the containing block's\n     edge \u2014 near the right edge that shrinks toward zero and forces the\n     row text to wrap awkwardly even though the box is later clamped into\n     view. max-content is unaffected by `left`, so wrapping only happens if\n     content truly exceeds max-width. *\/\n  width: max-content;\n  min-width: 128px;\n  max-width: 260px;\n  padding: 10px 12px;\n  background: var(--ghc-card-bg);\n  border: 1px solid var(--ghc-card-border);\n  border-radius: 12px;\n  \/* Layered drop shadow from Primer Brand's ActionMenu (.ActionMenu__menu) *\/\n  box-shadow:\n    0px 100px 80px rgba(0, 0, 0, 0.01),\n    0px 41px 33px rgba(0, 0, 0, 0.02),\n    0px 22px 17px rgba(0, 0, 0, 0.02),\n    0px 12px 10px rgba(0, 0, 0, 0.03),\n    0px 6px 5px rgba(0, 0, 0, 0.04),\n    0px 2px 2px rgba(0, 0, 0, 0.07);\n  pointer-events: none;\n  opacity: 0;\n  transform: translate(-50%, -100%);\n  \/* Fade in on show and out on hide; quickly ease left\/top when tracking\n     from one point to the next. The position ease is suppressed on the\n     first appearance (see chart.js) so the tooltip fades in place rather\n     than sliding in from its previous spot. *\/\n  transition: opacity 0.12s ease,\n    left 0.1s cubic-bezier(0.4, 0, 0.2, 1),\n    top 0.1s cubic-bezier(0.4, 0, 0.2, 1);\n  will-change: opacity, transform, left, top;\n}\n.ghchart__tooltip[data-visible=\"true\"] { opacity: 1; }\n\n.ghchart__tt-title {\n  font-size: 13px;\n  font-weight: 700;\n  color: var(--ghc-text);\n  margin-bottom: 6px;\n}\n\n.ghchart__tt-row {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  font-size: 13px;\n  line-height: 1.6;\n  color: var(--ghc-muted);\n}\n.ghchart__tt-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }\n.ghchart__tt-name { flex: 1 1 auto; }\n.ghchart__tt-value { font-weight: 700; color: var(--ghc-text); font-variant-numeric: tabular-nums; }\n\n\/* Optional \"partial data\" note, shown only for a hovered point past a\n   line's config.partial.fromIndex (see .ghchart__line--dashed). *\/\n.ghchart__tt-note {\n  margin-top: 6px;\n  padding-top: 6px;\n  border-top: 1px solid var(--ghc-card-border);\n  font-size: 12px;\n  font-style: italic;\n  color: var(--ghc-muted);\n}\n\n\/* Bar tooltip body: a title line + free-form text lines (no swatches). *\/\n.ghchart__tt-line {\n  font-size: 13px;\n  line-height: 1.5;\n  color: var(--ghc-muted);\n}\n.ghchart__tt-line:first-child { color: var(--ghc-text); font-weight: 600; }\n.ghchart__tt-line + .ghchart__tt-line { margin-top: 2px; }\n\/* Timeline before\/after tooltip: only the title is bold; both value lines stay\n   regular weight and the same color (no emphasised first line). *\/\n.ghchart__tt-body--plain .ghchart__tt-line,\n.ghchart__tt-body--plain .ghchart__tt-line:first-child {\n  font-weight: 400;\n  color: var(--ghc-text);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ghchart__dot,\n  .ghchart__guide,\n  .ghchart__bar,\n  .ghchart__pt,\n  .ghchart__square,\n  .ghheat__cell,\n  .ghchart__tl-seg,\n  .ghchart__tl-flow,\n  .ghchart__tooltip { transition: none; }\n}\n<\/style>\n\n<script data-wp-block-html=\"js\">\n\/*\n  Shared chart engine \u2014 reusable renderer for the blog-charts library.\n  ------------------------------------------------------------------\n  Exposes window.GHChartEngine = { renderers, render(config, mountEl?), destroy(mountEl) }.\n  Renderers are keyed by config.type (currently \"line\"); add a new viz type\n  by registering GHChartEngine.renderers[<type>] = function (root, config) {\u2026}.\n\n  This same file is embedded verbatim at the top of each exported WordPress\n  chart.js, followed by that chart's CONFIG + a small auto-boot. It has no\n  external dependencies and is safe to include more than once on a page.\n*\/\n(function (global) {\n  \"use strict\";\n\n  var SVGNS = \"http:\/\/www.w3.org\/2000\/svg\";\n\n  function el(tag, attrs) {\n    var node = document.createElementNS(SVGNS, tag);\n    if (attrs) {\n      for (var k in attrs) {\n        if (Object.prototype.hasOwnProperty.call(attrs, k)) {\n          node.setAttribute(k, attrs[k]);\n        }\n      }\n    }\n    return node;\n  }\n\n  function html(tag, cls) {\n    var node = document.createElement(tag);\n    if (cls) node.className = cls;\n    return node;\n  }\n\n  function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }\n\n  var WHITE = [255, 255, 255], BLACK = [0, 0, 0];\n  function hexToRgb(h) {\n    h = String(h).replace(\"#\", \"\");\n    if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n    return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n  }\n  function rgbToHex(c) {\n    return \"#\" + c.map(function (x) {\n      x = Math.max(0, Math.min(255, Math.round(x)));\n      return (x < 16 ? \"0\" : \"\") + x.toString(16);\n    }).join(\"\");\n  }\n  function mix(a, b, t) {\n    return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n  }\n  function rampFrom(baseHex) {\n    var b = hexToRgb(baseHex);\n    return [\n      rgbToHex(mix(b, WHITE, 0.80)),\n      rgbToHex(mix(b, WHITE, 0.52)),\n      rgbToHex(mix(b, WHITE, 0.20)),\n      baseHex,\n      rgbToHex(mix(b, BLACK, 0.30))\n    ];\n  }\n  function strongOf(baseHex) { return rampFrom(baseHex)[4]; }\n\n  function labelColorOn(hex) {\n    function luminance(color) {\n      var c = hexToRgb(color).map(function (channel) {\n        channel \/= 255;\n        return channel <= 0.04045 ? channel \/ 12.92 : Math.pow((channel + 0.055) \/ 1.055, 2.4);\n      });\n      return c[0] * 0.2126 + c[1] * 0.7152 + c[2] * 0.0722;\n    }\n    var background = luminance(hex), dark = luminance(\"#1f2328\");\n    if ((Math.max(background, dark) + 0.05) \/ (Math.min(background, dark) + 0.05) >= 4.5) return \"#1f2328\";\n    return 1.05 \/ (background + 0.05) >= 4.5 ? \"#ffffff\" : \"#000000\";\n  }\n\n  var renderers = {};\n  var renderCleanups = (global.GHChartEngine && global.GHChartEngine._renderCleanups) || new WeakMap();\n\n  renderers.comparison = function (root, config) {\n    var axis = config.xAxis, series = config.series, rows = config.rows;\n    if (!axis || !Number.isFinite(axis.min) || !Number.isFinite(axis.max) ||\n        axis.min > 0 || axis.max < 0 || axis.max <= axis.min ||\n        !Array.isArray(axis.ticks) || !Array.isArray(series) || !series.length ||\n        series.some(function (s) { return !s || typeof s.color !== \"string\" || !\/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$\/i.test(s.color); }) ||\n        !Array.isArray(rows) || !rows.length ||\n        axis.ticks.some(function (v) { return !Number.isFinite(v) || v < axis.min || v > axis.max; }) ||\n        rows.some(function (row) {\n          return !Array.isArray(row.values) || row.values.length !== series.length ||\n            row.values.some(function (v) { return !Number.isFinite(v) || v < axis.min || v > axis.max; }) ||\n            !Array.isArray(row.decimals) || row.decimals.length !== series.length ||\n            row.decimals.some(function (n) { return !Number.isInteger(n) || n < 0 || n > 10; });\n        })) throw new Error(\"Comparison chart requires a zero-inclusive axis, hex series colors, and matching finite values\/decimal precision for each series.\");\n    root.textContent = \"\";\n    root.setAttribute(\"role\", \"figure\");\n    root.setAttribute(\"aria-label\", config.title);\n    var heading = html(\"div\", \"ghchart__headline\");\n    heading.textContent = config.title;\n    root.appendChild(heading);\n    if (config.subtitle) {\n      var subtitle = html(\"p\", \"ghchart__subtitle\");\n      subtitle.textContent = config.subtitle;\n      root.appendChild(subtitle);\n    }\n    var legend = html(\"div\", \"ghchart__legend ghchart__legend--trend\");\n    series.forEach(function (s) {\n      var item = html(\"span\", \"ghchart__legend-item\");\n      var swatch = html(\"span\", \"ghchart__swatch\");\n      swatch.style.background = s.color;\n      item.appendChild(swatch);\n      item.appendChild(document.createTextNode(s.name));\n      legend.appendChild(item);\n    });\n    root.appendChild(legend);\n    var marks = [];\n    function at(value) { return (value - axis.min) \/ (axis.max - axis.min) * 100; }\n    function signed(value, decimals) { return (value > 0 ? \"+\" : \"\") + value.toFixed(decimals) + \"%\"; }\n    rows.forEach(function (row) {\n      var group = html(\"div\", \"ghchart__comparison-row\");\n      group.setAttribute(\"role\", \"group\");\n      group.setAttribute(\"aria-label\", row.label);\n      var label = html(\"div\", \"ghchart__comparison-label\");\n      label.textContent = row.label;\n      label.setAttribute(\"aria-hidden\", \"true\");\n      group.appendChild(label);\n      var plot = html(\"div\", \"ghchart__comparison-plot\");\n      axis.ticks.forEach(function (value) {\n        var line = html(\"span\", \"ghchart__comparison-grid\");\n        line.style.left = at(value) + \"%\";\n        line.setAttribute(\"aria-hidden\", \"true\");\n        plot.appendChild(line);\n      });\n      var zero = html(\"span\", \"ghchart__comparison-grid ghchart__comparison-zero\");\n      zero.style.left = at(0) + \"%\";\n      zero.setAttribute(\"aria-hidden\", \"true\");\n      plot.appendChild(zero);\n      series.forEach(function (s, index) {\n        var value = row.values[index], formatted = signed(value, row.decimals[index]);\n        var track = html(\"div\", \"ghchart__comparison-track\");\n        track.setAttribute(\"role\", \"group\");\n        track.setAttribute(\"aria-label\", s.name);\n        var bar = html(\"span\", \"ghchart__comparison-bar\");\n        \/\/ Anchor the zero-facing edge, without fractional transform snapping.\n        if (value < 0) {\n          bar.style.right = \"calc(100% - \" + at(0) + \"%)\";\n          bar.classList.add(\"ghchart__comparison-bar--negative\");\n        } else bar.style.left = at(0) + \"%\";\n        bar.style.width = Math.abs(at(value) - at(0)) + \"%\";\n        bar.style.background = s.color;\n        bar.setAttribute(\"aria-hidden\", \"true\");\n        var valueLabel = html(\"span\", \"ghchart__comparison-value\");\n        valueLabel.textContent = formatted;\n        track.appendChild(bar);\n        track.appendChild(valueLabel);\n        marks.push({ track: track, bar: bar, label: valueLabel, value: value, color: s.color });\n        plot.appendChild(track);\n      });\n      group.appendChild(plot);\n      root.appendChild(group);\n    });\n    var axisRow = html(\"div\", \"ghchart__comparison-row\");\n    axisRow.appendChild(html(\"span\"));\n    var ticks = html(\"div\", \"ghchart__comparison-axis\");\n    axis.ticks.forEach(function (value) {\n      var tick = html(\"span\", \"ghchart__comparison-tick\");\n      tick.style.left = at(value) + \"%\";\n      if (value === axis.min) tick.style.transform = \"none\";\n      if (value === axis.max) tick.style.transform = \"translateX(-100%)\";\n      tick.textContent = value === 0 ? \"0\" : signed(value, 0);\n      ticks.appendChild(tick);\n    });\n    axisRow.appendChild(ticks);\n    root.appendChild(axisRow);\n\n    var active = true, frame = 0, observer;\n    function positionLabel(mark, side, coordinate, placement) {\n      mark.label.style.left = side === \"left\" ? \"calc(\" + at(coordinate) + \"% + 8px)\" : \"auto\";\n      mark.label.style.right = side === \"right\" ? \"calc(100% - \" + at(coordinate) + \"% + 8px)\" : \"auto\";\n      mark.label.style.color = placement === \"inside\" ? labelColorOn(mark.color) : \"\";\n      mark.label.setAttribute(\"data-placement\", placement);\n    }\n    function layoutValues() {\n      if (!active || !root.clientWidth) return;\n      marks.forEach(function (mark) {\n        mark.track.classList.remove(\"ghchart__comparison-track--stacked\");\n        mark.track.style.height = \"\";\n        mark.bar.style.top = \"\";\n        mark.label.style.left = \"0px\";\n        mark.label.style.right = \"auto\";\n        mark.label.style.color = \"\";\n        var width = mark.track.clientWidth, textWidth = mark.label.offsetWidth + 1;\n        var zero = at(0) * width \/ 100, end = at(mark.value) * width \/ 100;\n        var negative = mark.value < 0;\n        var outside = negative ? end : width - end;\n        var opposite = negative ? width - zero : zero;\n        if (outside >= textWidth + 8) {\n          positionLabel(mark, negative ? \"right\" : \"left\", mark.value, \"outside\");\n        } else if (Math.abs(end - zero) >= textWidth + 16) {\n          positionLabel(mark, negative ? \"left\" : \"right\", mark.value, \"inside\");\n        } else if (opposite >= textWidth + 8) {\n          positionLabel(mark, negative ? \"left\" : \"right\", 0, \"opposite\");\n        } else {\n          \/\/ Very narrow plots or long precision strings get a separate label line.\n          mark.track.classList.add(\"ghchart__comparison-track--stacked\");\n          mark.label.setAttribute(\"data-placement\", \"stacked\");\n          var top = mark.label.offsetHeight + 6;\n          mark.bar.style.top = top + \"px\";\n          mark.track.style.height = top + 28 + \"px\";\n        }\n      });\n    }\n    function schedule() {\n      if (!active) return;\n      cancelAnimationFrame(frame);\n      frame = requestAnimationFrame(layoutValues);\n    }\n    layoutValues();\n    if (global.ResizeObserver) {\n      observer = new ResizeObserver(schedule);\n      observer.observe(root);\n    } else global.addEventListener(\"resize\", schedule);\n    if (document.fonts) {\n      document.fonts.ready.then(schedule);\n      document.fonts.addEventListener(\"loadingdone\", schedule);\n    }\n    return function () {\n      active = false;\n      cancelAnimationFrame(frame);\n      if (observer) observer.disconnect();\n      global.removeEventListener(\"resize\", schedule);\n      if (document.fonts) document.fonts.removeEventListener(\"loadingdone\", schedule);\n    };\n  };\n\n  \/* ================================================================\n     Line chart renderer\n     ================================================================ *\/\n  renderers.line = function (root, config) {\n    \/\/ viewBox geometry (design units; scales fluidly to any width).\n    var VB = { w: 1000, h: 560 };\n    var M = { top: 30, right: 34, bottom: 56, left: 86 };\n    \/\/ Inset the plotted points slightly from the axis so the first\/last\n    \/\/ months (e.g. \"Jan\") don't crowd the y-axis labels (e.g. \"50\").\n    var PAD_X = 18;\n\n    \/\/ Reset (in case of re-render) and use a unique id prefix so multiple\n    \/\/ charts on one page never share gradient ids.\n    root.textContent = \"\";\n    var uid = config.mountId;\n\n    var n = config.labels.length;\n    var yMin = config.yAxis.min;\n    var yMax = config.yAxis.max;\n    var step = config.yAxis.step;\n    \/\/ Optional log y (yAxis.scale: \"log\") for data spanning many decades;\n    \/\/ ticks are then explicit (yAxis.ticks), e.g. 1, 10, 100 \u2026 1B.\n    var isLog = config.yAxis.scale === \"log\";\n    var yTicks = config.yAxis.ticks;\n    if (!yTicks) {\n      yTicks = [];\n      for (var tv0 = yMin; tv0 <= yMax + 0.0001; tv0 += step) yTicks.push(tv0);\n    }\n    \/\/ Y-axis tick labels can format differently from the tooltip values\n    \/\/ (e.g. a plain \"40\" on the axis but \"40%\" in the tooltip).\n    var fmtAxis = config.formatAxis || config.formatValue;\n\n    \/\/ Widen the left gutter when the formatted tick labels need it (a unit\n    \/\/ suffix like \"2,000M\" is far wider than a bare \"40\"). Labels are real\n    \/\/ SVG <text> we can't measure before layout, so estimate from the\n    \/\/ glyph mix at the 22-unit axis font size.\n    var AXIS_FONT = 22;\n    var Y_LABEL_GAP = 22;\n    function estimateTextW(s) {\n      var w = 0;\n      for (var i = 0; i < s.length; i++) {\n        var c = s.charAt(i);\n        if (c === \",\" || c === \".\" || c === \" \") w += 0.28;\n        else if (\/[0-9]\/.test(c)) w += 0.58;\n        else if (\/[A-Z]\/.test(c)) w += 0.72;\n        else w += 0.55;\n      }\n      return w * AXIS_FONT;\n    }\n    var widestY = 0;\n    yTicks.forEach(function (tv) {\n      widestY = Math.max(widestY, estimateTextW(String(fmtAxis(tv))));\n    });\n    \/\/ A rotated y-axis title (yAxis.title) needs its own strip of gutter.\n    var Y_TITLE_W = config.yAxis.title ? 40 : 0;\n    M.left = Math.max(M.left, Math.ceil(widestY + Y_LABEL_GAP + 8 + Y_TITLE_W));\n\n    var innerW = VB.w - M.left - M.right;\n    var innerH = VB.h - M.top - M.bottom;\n\n    var plotW = innerW - 2 * PAD_X;\n    var stepX = n <= 1 ? 0 : plotW \/ (n - 1);\n    function xAt(i) { return n <= 1 ? M.left + innerW \/ 2 : M.left + PAD_X + stepX * i; }\n    var logLo = isLog ? Math.log(yMin) \/ Math.LN10 : 0;\n    var logHi = isLog ? Math.log(yMax) \/ Math.LN10 : 0;\n    function yAt(v) {\n      var t = isLog\n        ? (Math.log(Math.max(v, yMin)) \/ Math.LN10 - logLo) \/ (logHi - logLo)\n        : (v - yMin) \/ (yMax - yMin);\n      return M.top + innerH * (1 - t);\n    }\n    var baseY = yAt(yMin);\n\n    \/* ---- Title + divider (top) ---------------------------------- *\/\n    var title = html(\"span\", \"ghchart__title\");\n    title.textContent = config.title;\n    root.appendChild(title);\n    root.appendChild(html(\"div\", \"ghchart__rule\"));\n    if (config.subtitle) {\n      var lineSub = html(\"p\", \"ghchart__subtitle\");\n      lineSub.textContent = config.subtitle;\n      root.appendChild(lineSub);\n    }\n\n    \/* ---- Header + legend ---------------------------------------- *\/\n    if (!config.hideLegend) {\n      var header = html(\"div\", \"ghchart__header\");\n      var legend = html(\"div\", \"ghchart__legend\");\n      config.series.forEach(function (s) {\n        var item = html(\"span\", \"ghchart__legend-item\");\n        var sw = html(\"span\", \"ghchart__swatch\");\n        sw.style.background = s.color;\n        item.appendChild(sw);\n        item.appendChild(document.createTextNode(s.name));\n        legend.appendChild(item);\n      });\n      header.appendChild(legend);\n      root.appendChild(header);\n    }\n\n    \/* ---- SVG plot ----------------------------------------------- *\/\n    var svg = el(\"svg\", {\n      \"class\": \"ghchart__plot\",\n      viewBox: \"0 0 \" + VB.w + \" \" + VB.h,\n      preserveAspectRatio: \"xMidYMid meet\",\n      role: \"img\",\n      \"aria-label\": config.title + \" line chart\"\n    });\n\n    \/\/ Gradient defs (one per area series).\n    var defs = el(\"defs\");\n    config.series.forEach(function (s, si) {\n      if (!s.area) return;\n      var g = el(\"linearGradient\", {\n        id: uid + \"-grad-\" + si, x1: \"0\", y1: \"0\", x2: \"0\", y2: \"1\"\n      });\n      g.appendChild(el(\"stop\", { offset: \"0\", \"stop-color\": s.color, \"stop-opacity\": \"0.28\" }));\n      g.appendChild(el(\"stop\", { offset: \"0.55\", \"stop-color\": s.color, \"stop-opacity\": \"0.08\" }));\n      g.appendChild(el(\"stop\", { offset: \"1\", \"stop-color\": s.color, \"stop-opacity\": \"0\" }));\n      defs.appendChild(g);\n    });\n    \/\/ Diagonal-hatch pattern for an optional \"partial \/ projected\" band.\n    if (config.projection) {\n      var pat = el(\"pattern\", {\n        id: uid + \"-hatch\", width: 7, height: 7,\n        patternUnits: \"userSpaceOnUse\", patternTransform: \"rotate(45)\"\n      });\n      pat.appendChild(el(\"line\", {\n        x1: 0, y1: 0, x2: 0, y2: 7,\n        stroke: config.projection.color || \"#8c959f\",\n        \"stroke-width\": 1.1, \"stroke-opacity\": 0.5\n      }));\n      defs.appendChild(pat);\n    }\n    svg.appendChild(defs);\n\n    \/\/ Gridlines + y-axis labels.\n    yTicks.forEach(function (v) {\n      var gy = yAt(v);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__gridline\",\n        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy\n      }));\n      var yl = el(\"text\", {\n        \"class\": \"ghchart__axislabel ghchart__axislabel--y\",\n        x: M.left - Y_LABEL_GAP, y: gy, dy: \"0.32em\", \"text-anchor\": \"end\"\n      });\n      yl.textContent = fmtAxis(v);\n      svg.appendChild(yl);\n    });\n    if (config.yAxis.title) {\n      var lytX = 22, lytY = M.top + innerH \/ 2;\n      var lyt = el(\"text\", {\n        \"class\": \"ghchart__axistitle\", x: lytX, y: lytY, \"text-anchor\": \"middle\",\n        transform: \"rotate(-90 \" + lytX + \" \" + lytY + \")\"\n      });\n      lyt.textContent = config.yAxis.title;\n      svg.appendChild(lyt);\n    }\n\n    \/\/ x-axis labels \u2014 either one per data point (default) or a sparse set\n    \/\/ of ticks decoupled from the data (config.xTicks: [{at,text}, \u2026]),\n    \/\/ which suits a dense daily series that only needs month markers.\n    var xTicks = config.xTicks ||\n      config.labels.map(function (t, i) { return { at: i, text: t }; });\n    xTicks.forEach(function (tk) {\n      var xl = el(\"text\", {\n        \"class\": \"ghchart__axislabel ghchart__axislabel--x\",\n        x: xAt(tk.at), y: VB.h - M.bottom + 36,\n        \"text-anchor\": tk.at === 0 ? \"start\" : (tk.at === n - 1 ? \"end\" : \"middle\")\n      });\n      xl.textContent = tk.text;\n      svg.appendChild(xl);\n    });\n\n    \/\/ Optional hatched \"partial \/ projected\" band over the trailing region.\n    if (config.projection) {\n      var px = xAt(config.projection.fromIndex);\n      svg.appendChild(el(\"rect\", {\n        \"class\": \"ghchart__projection\",\n        x: px, y: M.top, width: (VB.w - M.right) - px, height: baseY - M.top,\n        fill: \"url(#\" + uid + \"-hatch)\"\n      }));\n    }\n\n    \/\/ Optional dashed threshold line + floating label (e.g. a 50% target).\n    if (config.threshold) {\n      var thy = yAt(config.threshold.value);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__threshold\",\n        x1: M.left, y1: thy, x2: VB.w - M.right, y2: thy,\n        stroke: config.threshold.color\n      }));\n      if (config.threshold.label) {\n        var thl = el(\"text\", {\n          \"class\": \"ghchart__threshold-label\",\n          x: M.left + PAD_X, y: thy - 10, \"text-anchor\": \"start\",\n          fill: config.threshold.color\n        });\n        thl.textContent = config.threshold.label;\n        svg.appendChild(thl);\n      }\n    }\n\n    \/\/ Precompute point coords per series.\n    var pts = config.series.map(function (s) {\n      return s.values.map(function (val, i) { return { x: xAt(i), y: yAt(val) }; });\n    });\n\n    \/\/ Smooth (Catmull-Rom -> cubic Bezier) path builder.\n    \/\/ config.curve: \"linear\" draws straight segments \u2014 for discrete\n    \/\/ categories (e.g. percentiles) where a curve would imply values\n    \/\/ between the points.\n    function smooth(points) {\n      if (!points.length) return \"\";\n      if (points.length < 2) return \"M\" + points[0].x + \",\" + points[0].y;\n      if (config.curve === \"linear\") {\n        return points.map(function (p, i) {\n          return (i ? \"L \" : \"M \") + p.x + \",\" + p.y;\n        }).join(\" \");\n      }\n      var d = \"M \" + points[0].x + \",\" + points[0].y;\n      for (var i = 0; i < points.length - 1; i++) {\n        var p0 = points[i - 1] || points[i];\n        var p1 = points[i];\n        var p2 = points[i + 1];\n        var p3 = points[i + 2] || p2;\n        var t = 1 \/ 6;\n        var c1x = p1.x + (p2.x - p0.x) * t;\n        var c1y = p1.y + (p2.y - p0.y) * t;\n        var c2x = p2.x - (p3.x - p1.x) * t;\n        var c2y = p2.y - (p3.y - p1.y) * t;\n        d += \" C \" + c1x + \",\" + c1y + \" \" + c2x + \",\" + c2y + \" \" + p2.x + \",\" + p2.y;\n      }\n      return d;\n    }\n\n    \/\/ Area fills (drawn first, behind lines). When config.partial marks a\n    \/\/ trailing partial\/incomplete range (e.g. a month still in progress),\n    \/\/ the fill stops at the last complete point instead of extending under\n    \/\/ the partial segment \u2014 visually distinguishing \"real\" coverage from\n    \/\/ data that isn't final yet.\n    var partialIdx = config.partial ? config.partial.fromIndex : null;\n    config.series.forEach(function (s, si) {\n      if (!s.area) return;\n      var p = pts[si];\n      var fillP = partialIdx == null ? p : p.slice(0, partialIdx + 1);\n      if (fillP.length < 2) return;\n      var d = smooth(fillP) + \" L \" + fillP[fillP.length - 1].x + \",\" + baseY +\n              \" L \" + fillP[0].x + \",\" + baseY + \" Z\";\n      svg.appendChild(el(\"path\", { d: d, fill: \"url(#\" + uid + \"-grad-\" + si + \")\" }));\n    });\n\n    \/\/ Vertical hover guide (hidden until hover).\n    var guide = el(\"line\", {\n      \"class\": \"ghchart__guide\",\n      x1: xAt(0), y1: M.top - 6, x2: xAt(0), y2: baseY\n    });\n    svg.appendChild(guide);\n\n    \/\/ Lines (draw in reverse so the first series sits on top). The trailing\n    \/\/ partial segment (from the last complete point onward) renders as its\n    \/\/ own dashed path so the \"not final yet\" range reads clearly without\n    \/\/ a legend entry \u2014 mirroring the projected-region convention above.\n    config.series.slice().reverse().forEach(function (s) {\n      var si = config.series.indexOf(s);\n      var p = pts[si];\n      var solidP = partialIdx == null ? p : p.slice(0, partialIdx + 1);\n      svg.appendChild(el(\"path\", {\n        \"class\": \"ghchart__line\",\n        d: smooth(solidP),\n        stroke: s.color,\n        \"stroke-width\": s.width || (si === 0 ? 4 : 3.25)\n      }));\n      if (partialIdx != null && partialIdx < p.length - 1) {\n        \/\/ Straight (not curve-fit) dashed segment: fitting a Catmull-Rom\n        \/\/ curve to just this short tail would use different neighbor\n        \/\/ context than the solid curve and create a visible kink at the\n        \/\/ junction point; a straight dashed line reads as \"projected\" and\n        \/\/ joins the solid curve's end point cleanly.\n        var dashedP = p.slice(partialIdx);\n        var dd = \"M \" + dashedP[0].x + \",\" + dashedP[0].y;\n        var segLen = 0;\n        for (var di = 1; di < dashedP.length; di++) {\n          dd += \" L \" + dashedP[di].x + \",\" + dashedP[di].y;\n          segLen += Math.sqrt(\n            Math.pow(dashedP[di].x - dashedP[di - 1].x, 2) +\n            Math.pow(dashedP[di].y - dashedP[di - 1].y, 2)\n          );\n        }\n        var dashPath = el(\"path\", {\n          \"class\": \"ghchart__line ghchart__line--dashed\",\n          d: dd,\n          stroke: s.color,\n          \"stroke-width\": s.width || (si === 0 ? 4 : 3.25)\n        });\n        \/\/ The stylesheet's 7\/6 dash (period 13) needs room to repeat. A\n        \/\/ short tail \u2014 a single partial week on a dense series \u2014 would\n        \/\/ otherwise render as one dash and read as solid, so shrink the\n        \/\/ pattern to fit ~3 repeats. Set it inline: CSS beats SVG\n        \/\/ presentation attributes, but an inline style beats the class.\n        var PERIOD = 13;\n        if (segLen > 0 && segLen \/ PERIOD < 3) {\n          var period = Math.max(segLen \/ 3, 3);\n          dashPath.style.strokeDasharray =\n            (period * 7 \/ PERIOD).toFixed(2) + \" \" + (period * 6 \/ PERIOD).toFixed(2);\n        }\n        svg.appendChild(dashPath);\n      }\n    });\n\n    \/\/ Dots (keep references for hover emphasis). When config.hideDots is set\n    \/\/ (dense daily lines), dots rest invisible and only the hovered one shows.\n    var dotEls = config.series.map(function (s, si) {\n      return pts[si].map(function (pt) {\n        var c = el(\"circle\", {\n          \"class\": \"ghchart__dot\",\n          cx: pt.x, cy: pt.y,\n          r: config.hideDots ? 0 : (si === 0 ? 4.5 : 4),\n          fill: s.color\n        });\n        svg.appendChild(c);\n        return c;\n      });\n    });\n\n    \/\/ Transparent hit area for pointer capture.\n    var hit = el(\"rect\", {\n      \"class\": \"ghchart__hit\",\n      x: M.left,\n      y: M.top - 6,\n      width: innerW,\n      height: baseY - M.top + 12\n    });\n    svg.appendChild(hit);\n\n    root.appendChild(svg);\n\n    \/* ---- Tooltip ------------------------------------------------ *\/\n    var tip = html(\"div\", \"ghchart__tooltip\");\n    tip.setAttribute(\"role\", \"status\");\n    var tipTitle = html(\"div\", \"ghchart__tt-title\");\n    tip.appendChild(tipTitle);\n    var tipRows = config.series.map(function (s) {\n      var row = html(\"div\", \"ghchart__tt-row\");\n      var sw = html(\"span\", \"ghchart__tt-swatch\");\n      sw.style.background = s.color;\n      var name = html(\"span\", \"ghchart__tt-name\");\n      name.textContent = s.name;\n      var val = html(\"span\", \"ghchart__tt-value\");\n      row.appendChild(sw);\n      row.appendChild(name);\n      row.appendChild(val);\n      tip.appendChild(row);\n      return val;\n    });\n    \/\/ Optional \"partial data\" note, shown only while hovering a point past\n    \/\/ config.partial.fromIndex.\n    var partialNote = null;\n    if (config.partial) {\n      partialNote = html(\"div\", \"ghchart__tt-note\");\n      partialNote.textContent = config.partial.note || \"Partial data\";\n      tip.appendChild(partialNote);\n    }\n    root.appendChild(tip);\n\n    \/* ---- Hover \/ focus interactivity ---------------------------- *\/\n    var activeIndex = -1;\n    var baseR = config.series.map(function (s, si) {\n      return config.hideDots ? 0 : (si === 0 ? 4.5 : 4);\n    });\n    \/\/ Radius of the single emphasized dot under the pointer.\n    var dotActiveR = config.series.map(function (s, si) {\n      return config.hideDots ? 6 : baseR[si] + 2.5;\n    });\n    var motionMQ = window.matchMedia\n      ? window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n      : null;\n    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }\n\n    function indexFromClientX(clientX) {\n      var rect = svg.getBoundingClientRect();\n      var relX = (clientX - rect.left) \/ rect.width * VB.w;\n      var i = stepX ? Math.round((relX - (M.left + PAD_X)) \/ stepX) : 0;\n      return clamp(i, 0, n - 1);\n    }\n\n    function show(i) {\n      if (i === activeIndex) return;\n      var reappearing = activeIndex === -1;\n      activeIndex = i;\n\n      guide.setAttribute(\"x1\", xAt(i));\n      guide.setAttribute(\"x2\", xAt(i));\n      guide.style.opacity = \"1\";\n\n      \/\/ Emphasize the hovered column's dots; reset the rest.\n      dotEls.forEach(function (col, si) {\n        col.forEach(function (c, ci) {\n          c.setAttribute(\"r\", ci === i ? dotActiveR[si] : baseR[si]);\n        });\n      });\n\n      \/\/ Fill tooltip.\n      tipTitle.textContent = config.labels[i];\n      config.series.forEach(function (s, si) {\n        tipRows[si].textContent = config.formatValue(s.values[i]);\n      });\n      if (partialNote) {\n        partialNote.style.display = (i > partialIdx) ? \"\" : \"none\";\n      }\n\n      \/\/ Anchor above the highest point in this column.\n      var topSi = 0, topY = Infinity;\n      config.series.forEach(function (s, si) {\n        var yy = yAt(s.values[i]);\n        if (yy < topY) { topY = yy; topSi = si; }\n      });\n      var anchor = dotEls[topSi][i].getBoundingClientRect();\n      var rootRect = root.getBoundingClientRect();\n      var left = anchor.left + anchor.width \/ 2 - rootRect.left;\n      var top = anchor.top - rootRect.top - 12;\n\n      \/\/ On the first appearance, fade in exactly at the point: suppress the\n      \/\/ position ease so it doesn't slide in from its previous resting spot.\n      var suppress = reappearing && !reduceMotion();\n      if (suppress) tip.style.transition = \"opacity 0.12s ease\";\n\n      tip.setAttribute(\"data-visible\", \"true\");\n      \/\/ Reset left before measuring: a browser sizes an absolutely\n      \/\/ positioned auto\/max-content-width box using the *current* `left`\n      \/\/ to derive available space, so measuring while `left` still holds\n      \/\/ the previous (possibly near-edge) position creates a feedback loop\n      \/\/ that keeps squeezing the tooltip narrower and wrapping its rows.\n      \/\/ Setting left to 0 first guarantees a true, unconstrained natural\n      \/\/ width; this happens synchronously before the next paint, so\n      \/\/ there's no visible flash.\n      tip.style.left = \"0px\";\n      \/\/ Clamp horizontally within the container.\n      var halfW = tip.offsetWidth \/ 2;\n      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);\n      \/\/ Flip below the point if there's no room above.\n      if (top - tip.offsetHeight < 0) {\n        top = anchor.bottom - rootRect.top + 12;\n        tip.style.transform = \"translate(-50%, 0)\";\n      } else {\n        tip.style.transform = \"translate(-50%, -100%)\";\n      }\n      tip.style.left = left + \"px\";\n      tip.style.top = top + \"px\";\n\n      \/\/ Commit that position without animating, then restore the CSS\n      \/\/ transition so subsequent point-to-point moves ease into place.\n      if (suppress) {\n        void tip.offsetWidth;\n        tip.style.transition = \"\";\n      }\n    }\n\n    function hide() {\n      activeIndex = -1;\n      guide.style.opacity = \"0\";\n      tip.setAttribute(\"data-visible\", \"false\");\n      dotEls.forEach(function (col, si) {\n        col.forEach(function (c) { c.setAttribute(\"r\", baseR[si]); });\n      });\n    }\n\n    hit.addEventListener(\"pointermove\", function (e) { show(indexFromClientX(e.clientX)); });\n    hit.addEventListener(\"pointerdown\", function (e) { show(indexFromClientX(e.clientX)); });\n    hit.addEventListener(\"pointerleave\", hide);\n\n    \/\/ Keyboard access.\n    svg.setAttribute(\"tabindex\", \"0\");\n    svg.addEventListener(\"keydown\", function (e) {\n      if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n        var start = activeIndex < 0 ? 0 : activeIndex;\n        show(clamp(start + (e.key === \"ArrowRight\" ? 1 : -1), 0, n - 1));\n        e.preventDefault();\n      } else if (e.key === \"Escape\") {\n        hide();\n      }\n    });\n    svg.addEventListener(\"blur\", hide);\n\n    \/* ---- Caption (optional rich copy, e.g. a partial-data footnote) *\/\n    if (config.caption) {\n      var lineCap = html(\"div\", \"ghchart__caption\");\n      lineCap.innerHTML = config.caption;\n      root.appendChild(lineCap);\n    }\n  };\n\n  \/* ================================================================\n     Bar chart renderer (composite)\n     ----------------------------------------------------------------\n     One \"graph\" can stack multiple bar panels in a single block:\n       config = { type:\"bar\", title, subtitle?, series:[{name,color}],\n                  barRadius?, panels:[ panel\u2026 ], caption? }\n       panel  = { title?, labels[], values[], yAxis:{min,max,step},\n                  formatValue(v), height?, maxBarVB?,\n                  tooltip(i, panel) -> { title, lines:[\u2026] } }\n     No printed value labels \u2014 values surface on hover (consistent with\n     the line chart). Bars use a single tunable color with rounded tops.\n     ================================================================ *\/\n  renderers.bar = function (root, config) {\n    root.textContent = \"\";\n\n    var barColor = (config.series && config.series[0] && config.series[0].color) || \"#ff7b72\";\n    var barRadius = config.barRadius != null ? config.barRadius : 8;\n\n    \/\/ Per-bar fill: a bar may point at a different series (e.g. a muted\n    \/\/ \"preliminary\" color) via panel.barSeries[index]; otherwise series[0].\n    function barColorFor(panel, j) {\n      var si = (panel.barSeries && panel.barSeries[j] != null) ? panel.barSeries[j] : 0;\n      var s = config.series && config.series[si];\n      return (s && s.color) || barColor;\n    }\n\n    var motionMQ = window.matchMedia\n      ? window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n      : null;\n    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }\n\n    \/* ---- Title + divider (+ optional subtitle) ------------------ *\/\n    var title = html(\"span\", \"ghchart__title\");\n    title.textContent = config.title;\n    root.appendChild(title);\n    root.appendChild(html(\"div\", \"ghchart__rule\"));\n\n    if (config.subtitle) {\n      var sub = html(\"p\", \"ghchart__subtitle\");\n      sub.textContent = config.subtitle;\n      root.appendChild(sub);\n    }\n\n    \/* ---- One shared tooltip for all panels ---------------------- *\/\n    var tip = html(\"div\", \"ghchart__tooltip\");\n    tip.setAttribute(\"role\", \"status\");\n    var tipTitle = html(\"div\", \"ghchart__tt-title\");\n    var tipBody = html(\"div\", \"ghchart__tt-body\");\n    tip.appendChild(tipTitle);\n    tip.appendChild(tipBody);\n    root.appendChild(tip);\n\n    var allBars = [];       \/\/ { el, pi, bi }\n    var activeKey = null;   \/\/ \"pi:bi\" of the hovered bar\n\n    function hideTip() {\n      activeKey = null;\n      tip.setAttribute(\"data-visible\", \"false\");\n      allBars.forEach(function (b) { b.el.style.opacity = \"1\"; });\n    }\n\n    \/\/ Hoisted: referenced by the per-column pointer handlers below.\n    function showTip(pi, bi, panel, panelBars) {\n      var reappearing = activeKey === null;\n      activeKey = pi + \":\" + bi;\n\n      allBars.forEach(function (b) {\n        b.el.style.opacity = (b.pi === pi && b.bi === bi) ? \"1\" : \"0.42\";\n      });\n\n      var content = panel.tooltip\n        ? panel.tooltip(bi, panel)\n        : { title: (panel.labels || [])[bi],\n            lines: [ (panel.formatValue || String)(panel.values[bi]) ] };\n\n      tipTitle.textContent = content.title != null ? content.title : \"\";\n      tipBody.textContent = \"\";\n      (content.lines || []).forEach(function (ln) {\n        var row = html(\"div\", \"ghchart__tt-line\");\n        row.textContent = ln;\n        tipBody.appendChild(row);\n      });\n\n      var anchor = panelBars[bi].el.getBoundingClientRect();\n      var rootRect = root.getBoundingClientRect();\n      var left = anchor.left + anchor.width \/ 2 - rootRect.left;\n      var top = anchor.top - rootRect.top - 12;\n\n      var suppress = reappearing && !reduceMotion();\n      if (suppress) tip.style.transition = \"opacity 0.12s ease\";\n\n      tip.setAttribute(\"data-visible\", \"true\");\n      var halfW = tip.offsetWidth \/ 2;\n      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);\n      if (top - tip.offsetHeight < 0) {\n        top = anchor.bottom - rootRect.top + 12;\n        tip.style.transform = \"translate(-50%, 0)\";\n      } else {\n        tip.style.transform = \"translate(-50%, -100%)\";\n      }\n      tip.style.left = left + \"px\";\n      tip.style.top = top + \"px\";\n\n      if (suppress) { void tip.offsetWidth; tip.style.transition = \"\"; }\n    }\n\n    \/* ---- Panels ------------------------------------------------- *\/\n    (config.panels || []).forEach(function (panel, pi) {\n      var wrap = html(\"div\", \"ghchart__panel\");\n      if (panel.title) {\n        var pt = html(\"h3\", \"ghchart__panel-title\");\n        pt.textContent = panel.title;\n        wrap.appendChild(pt);\n      }\n\n      var VB = { w: 1000, h: panel.height || 360 };\n      var M = { top: 20, right: 20, bottom: 48, left: 84 };\n      var innerW = VB.w - M.left - M.right;\n      var innerH = VB.h - M.top - M.bottom;\n\n      var labels = panel.labels || [];\n      var values = panel.values || [];\n      var nb = labels.length;\n      var ax = panel.yAxis || { min: 0, max: Math.max.apply(null, values.concat([1])), step: 0 };\n      var yMin = ax.min, yMax = ax.max, step = ax.step || 0;\n      var fmt = panel.formatValue || function (v) { return String(v); };\n\n      function yAt(v) { return M.top + innerH * (1 - (v - yMin) \/ (yMax - yMin)); }\n      var baseY = yAt(yMin);\n\n      var slot = nb ? innerW \/ nb : innerW;\n      var maxBarVB = panel.maxBarVB != null ? panel.maxBarVB : 150;\n      var barW = Math.min(slot * 0.62, maxBarVB);\n      function xCenter(i) { return M.left + slot * (i + 0.5); }\n\n      var svg = el(\"svg\", {\n        \"class\": \"ghchart__plot ghchart__plot--bar\",\n        viewBox: \"0 0 \" + VB.w + \" \" + VB.h,\n        preserveAspectRatio: \"xMidYMid meet\",\n        role: \"img\",\n        \"aria-label\": (panel.title || config.title) + \" bar chart\"\n      });\n\n      \/\/ Gridlines + y-axis labels.\n      if (step > 0) {\n        for (var v = yMin; v <= yMax + 1e-9; v += step) {\n          var gy = yAt(v);\n          svg.appendChild(el(\"line\", {\n            \"class\": \"ghchart__gridline\", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy\n          }));\n          var yl = el(\"text\", {\n            \"class\": \"ghchart__axislabel ghchart__axislabel--y\",\n            x: M.left - 18, y: gy, dy: \"0.32em\", \"text-anchor\": \"end\"\n          });\n          yl.textContent = fmt(v);\n          svg.appendChild(yl);\n        }\n      }\n\n      \/\/ Solid baseline.\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__axisline\", x1: M.left, y1: baseY, x2: VB.w - M.right, y2: baseY\n      }));\n\n      \/\/ x-axis labels.\n      for (var i = 0; i < nb; i++) {\n        var xl = el(\"text\", {\n          \"class\": \"ghchart__axislabel ghchart__axislabel--x\",\n          x: xCenter(i), y: VB.h - M.bottom + 30, \"text-anchor\": \"middle\"\n        });\n        if (panel.xLabelSize) xl.setAttribute(\"style\", \"font-size:\" + panel.xLabelSize + \"px\");\n        xl.textContent = labels[i];\n        svg.appendChild(xl);\n      }\n\n      \/\/ Bars (rounded top corners only) + full-height hover columns.\n      var panelBars = [];\n      for (var j = 0; j < nb; j++) {\n        var cx = xCenter(j);\n        var topY = yAt(values[j]);\n        var barH = baseY - topY;\n        var x = cx - barW \/ 2;\n        var r = Math.max(0, Math.min(barRadius, barW \/ 2, barH));\n        var d = \"M \" + x + \" \" + baseY +\n                \" L \" + x + \" \" + (topY + r) +\n                \" Q \" + x + \" \" + topY + \" \" + (x + r) + \" \" + topY +\n                \" L \" + (x + barW - r) + \" \" + topY +\n                \" Q \" + (x + barW) + \" \" + topY + \" \" + (x + barW) + \" \" + (topY + r) +\n                \" L \" + (x + barW) + \" \" + baseY + \" Z\";\n        var bar = el(\"path\", { \"class\": \"ghchart__bar\", d: d, fill: barColorFor(panel, j) });\n        svg.appendChild(bar);\n        var rec = { el: bar, pi: pi, bi: j };\n        panelBars.push(rec);\n        allBars.push(rec);\n\n        var col = el(\"rect\", {\n          \"class\": \"ghchart__barhit\",\n          x: M.left + slot * j, y: M.top - 6, width: slot, height: baseY - M.top + 6\n        });\n        (function (idx) {\n          col.addEventListener(\"pointermove\", function () { showTip(pi, idx, panel, panelBars); });\n          col.addEventListener(\"pointerdown\", function () { showTip(pi, idx, panel, panelBars); });\n          col.addEventListener(\"pointerleave\", hideTip);\n        })(j);\n        svg.appendChild(col);\n      }\n\n      \/* ---- Annotation layer (drawn on top; pointer-events: none) ----\n         \u00b7 always-on value labels above chosen bars (panel.valueLabels)\n         \u00b7 anchor lines: a dotted horizontal reference at one bar's top\n           extended to another, with a floating pill (panel.annotations) *\/\n      var vlabels = panel.valueLabels || null;\n      if (vlabels) {\n        for (var vk in vlabels) {\n          if (!Object.prototype.hasOwnProperty.call(vlabels, vk)) continue;\n          var vi = +vk;\n          var vt = el(\"text\", {\n            \"class\": \"ghchart__barvalue\",\n            x: xCenter(vi), y: yAt(values[vi]) - 12, \"text-anchor\": \"middle\"\n          });\n          vt.textContent = vlabels[vk];\n          svg.appendChild(vt);\n        }\n      }\n\n      (panel.annotations || []).forEach(function (an) {\n        if (an.type !== \"anchor\") return;\n        var yLine = yAt(values[an.fromIndex]);\n        svg.appendChild(el(\"line\", {\n          \"class\": \"ghchart__anno-line\",\n          x1: xCenter(an.fromIndex) - barW \/ 2, y1: yLine,\n          x2: xCenter(an.toIndex) + barW \/ 2, y2: yLine\n        }));\n\n        var chipIdx = an.chipAtIndex != null ? an.chipAtIndex : an.toIndex;\n        var cx = xCenter(chipIdx);\n        var cy = (yAt(values[chipIdx]) + yLine) \/ 2;\n        var label = an.label || \"\";\n        var fs = 19;\n        var chipW = label.length * fs * 0.55 + 24;\n        var chipH = fs + 16;\n        svg.appendChild(el(\"rect\", {\n          \"class\": \"ghchart__anno-chip\",\n          x: cx - chipW \/ 2, y: cy - chipH \/ 2, width: chipW, height: chipH, rx: chipH \/ 2\n        }));\n        var ct = el(\"text\", {\n          \"class\": \"ghchart__anno-chip-text\",\n          x: cx, y: cy, dy: \"0.34em\", \"text-anchor\": \"middle\", \"font-size\": fs\n        });\n        ct.textContent = label;\n        svg.appendChild(ct);\n      });\n\n      wrap.appendChild(svg);\n      root.appendChild(wrap);\n    });\n\n    \/* ---- Caption (optional rich copy) --------------------------- *\/\n    if (config.caption) {\n      var cap = html(\"div\", \"ghchart__caption\");\n      cap.innerHTML = config.caption;\n      root.appendChild(cap);\n    }\n  };\n\n  \/* ================================================================\n     Calendar heatmap renderer (GitHub contribution-graph style)\n     ----------------------------------------------------------------\n     One square per calendar day across a month range. Each panel is\n     shaded on its own scale \u2014 every day in a month shares that month's\n     level (the underlying series is monthly). Each panel's 5-step\n     faint->dark ramp is derived from a single tunable base color.\n     ================================================================ *\/\n  renderers.heatmap = function (root, config) {\n    root.textContent = \"\";\n\n    function trim0(s) { return s.replace(\/\\.0$\/, \"\"); }\n    function fmtM(v) {\n      if (v >= 1000) { var g = v \/ 1000; return (g >= 10 ? Math.round(g) : trim0(g.toFixed(1))) + \"B\"; }\n      if (v >= 1) return trim0(v.toFixed(1)) + \"M\";\n      return Math.round(v * 1000) + \"K\";\n    }\n\n    var MON = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n    var MON_FULL = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\n      \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\n    var WD = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n\n    var year = config.year || 2026;\n    var startM = config.startMonth != null ? config.startMonth : 0;\n    var endM = config.endMonth != null ? config.endMonth : 5;\n    var emptyColor = config.emptyColor || \"#ebedf0\";\n\n    function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }\n    function parseISO(s) { var p = s.split(\"-\"); return new Date(+p[0], +p[1] - 1, +p[2]); }\n    function dayStart(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); }\n\n    \/* ---- Eyebrow + title + subtitle ----------------------------- *\/\n    if (config.eyebrow) {\n      var eb = html(\"div\", \"ghheat__eyebrow\");\n      eb.appendChild(html(\"span\", \"ghheat__eyedot\"));\n      var ebt = html(\"span\", \"ghheat__eyetext\");\n      ebt.textContent = config.eyebrow;\n      eb.appendChild(ebt);\n      root.appendChild(eb);\n    }\n    var title = html(\"span\", \"ghchart__title\");\n    title.textContent = config.title;\n    root.appendChild(title);\n    if (config.subtitle) {\n      var sub = html(\"p\", \"ghchart__subtitle ghheat__subtitle\");\n      sub.innerHTML = config.subtitle;\n      root.appendChild(sub);\n    }\n\n    \/* ---- One shared tooltip -------------------------------------- *\/\n    var tip = html(\"div\", \"ghchart__tooltip\");\n    tip.setAttribute(\"role\", \"status\");\n    var tipTitle = html(\"div\", \"ghchart__tt-title\");\n    var tipBody = html(\"div\", \"ghchart__tt-body\");\n    tip.appendChild(tipTitle);\n    tip.appendChild(tipBody);\n    root.appendChild(tip);\n\n    var motionMQ = window.matchMedia ? window.matchMedia(\"(prefers-reduced-motion: reduce)\") : null;\n    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }\n    var activeCell = null;\n    function hideTip() { activeCell = null; tip.setAttribute(\"data-visible\", \"false\"); }\n    function showTip(rectEl, titleText, lines) {\n      var reappearing = activeCell === null;\n      activeCell = rectEl;\n      tipTitle.textContent = titleText;\n      tipBody.textContent = \"\";\n      lines.forEach(function (ln) {\n        var r = html(\"div\", \"ghchart__tt-line\");\n        r.innerHTML = ln;\n        tipBody.appendChild(r);\n      });\n      var a = rectEl.getBoundingClientRect(), rr = root.getBoundingClientRect();\n      var left = a.left + a.width \/ 2 - rr.left, top = a.top - rr.top - 12;\n      var suppress = reappearing && !reduceMotion();\n      if (suppress) tip.style.transition = \"opacity 0.12s ease\";\n      tip.setAttribute(\"data-visible\", \"true\");\n      var halfW = tip.offsetWidth \/ 2;\n      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);\n      if (top - tip.offsetHeight < 0) {\n        top = a.bottom - rr.top + 12;\n        tip.style.transform = \"translate(-50%, 0)\";\n      } else {\n        tip.style.transform = \"translate(-50%, -100%)\";\n      }\n      tip.style.left = left + \"px\";\n      tip.style.top = top + \"px\";\n      if (suppress) { void tip.offsetWidth; tip.style.transition = \"\"; }\n    }\n\n    \/* ---- Panels -------------------------------------------------- *\/\n    (config.panels || []).forEach(function (panel, pi) {\n      var base = (config.series && config.series[pi] && config.series[pi].color) || panel.base || \"#3094ff\";\n      var ramp = rampFrom(base);\n      var strong = ramp[4];\n\n      var totals = panel.months || [];\n      var activeVals = [];\n      for (var mi = startM; mi <= endM; mi++) { if (totals[mi] != null) activeVals.push(totals[mi]); }\n      var vmin = activeVals.length ? Math.min.apply(null, activeVals) : 0;\n      var vmax = activeVals.length ? Math.max.apply(null, activeVals) : 1;\n      function levelFor(t) {\n        if (t == null) return -1;\n        if (vmax === vmin) return 4;\n        return Math.round((t - vmin) \/ (vmax - vmin) * 4);\n      }\n\n      var af = panel.activeFrom ? parseISO(panel.activeFrom) : new Date(year, startM, 1);\n      var at = panel.activeTo ? parseISO(panel.activeTo) : new Date(year, endM, daysInMonth(year, endM));\n\n      var wrap = html(\"div\", \"ghheat__panel\");\n\n      var head = html(\"div\", \"ghheat__phead\");\n      var badge = html(\"span\", \"ghheat__badge\");\n      badge.textContent = panel.badge != null ? panel.badge : (pi + 1);\n      badge.style.background = strong;\n      head.appendChild(badge);\n      var pt = html(\"span\", \"ghheat__ptitle\");\n      pt.textContent = panel.title;\n      head.appendChild(pt);\n      if (panel.descriptor) {\n        var pd = html(\"span\", \"ghheat__pdesc\");\n        pd.textContent = panel.descriptor;\n        head.appendChild(pd);\n      }\n      wrap.appendChild(head);\n\n      var body = html(\"div\", \"ghheat__body\");\n\n      \/* ---- Calendar grid (SVG) ---- *\/\n      var gridStart = new Date(year, startM, 1);\n      var gs = new Date(gridStart);\n      gs.setDate(gs.getDate() - gs.getDay());            \/\/ back to Sunday\n      var gridEnd = new Date(year, endM, daysInMonth(year, endM));\n      var ge = new Date(gridEnd);\n      ge.setDate(ge.getDate() + (6 - ge.getDay()));      \/\/ forward to Saturday\n      var nCols = Math.round((ge - gs) \/ (7 * 86400000)) + 1;\n\n      var cell = 13, sq = 11, rad = 2.5, labL = 34, labT = 18;\n      var W = labL + nCols * cell, H = labT + 7 * cell;\n      var svg = el(\"svg\", {\n        \"class\": \"ghheat__grid\",\n        viewBox: \"0 0 \" + W + \" \" + H,\n        preserveAspectRatio: \"xMinYMin meet\",\n        role: \"img\",\n        \"aria-label\": panel.title + \" calendar heatmap\"\n      });\n\n      [1, 3, 5].forEach(function (r) {\n        var t = el(\"text\", {\n          \"class\": \"ghheat__wd\", x: labL - 6, y: labT + r * cell + sq \/ 2,\n          dy: \"0.32em\", \"text-anchor\": \"end\"\n        });\n        t.textContent = WD[r];\n        svg.appendChild(t);\n      });\n\n      \/\/ Month labels above the column where each month begins.\n      for (var lm = startM; lm <= endM; lm++) {\n        var first = new Date(year, lm, 1);\n        var colOf = Math.floor((dayStart(first) - gs) \/ (7 * 86400000));\n        var mlab = el(\"text\", { \"class\": \"ghheat__month\", x: labL + colOf * cell, y: 12, \"text-anchor\": \"start\" });\n        mlab.textContent = MON[lm];\n        svg.appendChild(mlab);\n      }\n\n      var afS = dayStart(af), atS = dayStart(at);\n      var d = new Date(gs);\n      for (var i = 0; i < nCols * 7; i++) {\n        var row = d.getDay();\n        var col = Math.floor(i \/ 7);\n        var x = labL + col * cell, y = labT + row * cell;\n        var m = d.getMonth();\n        var inRange = d.getFullYear() === year && m >= startM && m <= endM;\n        var ds = dayStart(d);\n        var inWindow = inRange && ds >= afS && ds <= atS;\n        var hasData = inWindow && totals[m] != null;\n        var lvl = hasData ? levelFor(totals[m]) : -1;\n        var cellEl = el(\"rect\", {\n          \"class\": \"ghheat__cell\", x: x, y: y, width: sq, height: sq, rx: rad,\n          fill: lvl >= 0 ? ramp[lvl] : emptyColor\n        });\n        if (inRange) {\n          (function (dt, mm, has) {\n            var dateStr = WD[dt.getDay()] + \" \\u00b7 \" + MON[mm] + \" \" + dt.getDate() + \", \" + year;\n            var lines;\n            if (has) {\n              var daily = totals[mm] \/ daysInMonth(year, mm);\n              lines = [\n                \"<b>\" + fmtM(daily) + \"<\/b> \" + panel.legendLabel,\n                MON_FULL[mm] + \" \\u00b7 \" + fmtM(totals[mm]) + \" \" + panel.unitTotal\n              ];\n            } else {\n              lines = [panel.emptyNote || \"No vetted data\"];\n            }\n            cellEl.addEventListener(\"pointermove\", function () { showTip(cellEl, dateStr, lines); });\n            cellEl.addEventListener(\"pointerdown\", function () { showTip(cellEl, dateStr, lines); });\n            cellEl.addEventListener(\"pointerleave\", hideTip);\n          })(new Date(d), m, hasData);\n        } else {\n          cellEl.setAttribute(\"class\", \"ghheat__cell ghheat__cell--pad\");\n        }\n        svg.appendChild(cellEl);\n        d.setDate(d.getDate() + 1);\n      }\n\n      var gridwrap = html(\"div\", \"ghheat__gridwrap\");\n      gridwrap.appendChild(svg);\n      body.appendChild(gridwrap);\n\n      \/* ---- Side column: stat + note + legend + extra ---- *\/\n      var side = html(\"div\", \"ghheat__side\");\n      if (panel.stat) {\n        var stat = html(\"div\", \"ghheat__stat\");\n        var big = html(\"span\", \"ghheat__statbig\");\n        big.textContent = panel.stat.big;\n        big.style.color = strong;\n        stat.appendChild(big);\n        if (panel.stat.small) {\n          var small = html(\"span\", \"ghheat__statsmall\");\n          small.textContent = panel.stat.small;\n          stat.appendChild(small);\n        }\n        side.appendChild(stat);\n      }\n      if (panel.note) {\n        var note = html(\"p\", \"ghheat__note\");\n        note.innerHTML = panel.note;\n        side.appendChild(note);\n      }\n      var leg = html(\"div\", \"ghheat__legend\");\n      var lw1 = html(\"span\", \"ghheat__legword\"); lw1.textContent = \"Fewer\"; leg.appendChild(lw1);\n      ramp.forEach(function (c) {\n        var s = html(\"span\", \"ghheat__sw\"); s.style.background = c; leg.appendChild(s);\n      });\n      var lw2 = html(\"span\", \"ghheat__legword\"); lw2.textContent = \"More\"; leg.appendChild(lw2);\n      if (panel.legendLabel) {\n        var lu = html(\"span\", \"ghheat__legunit\"); lu.textContent = \"\\u00b7 \" + panel.legendLabel; leg.appendChild(lu);\n      }\n      side.appendChild(leg);\n      if (panel.extraNote) {\n        var ex = html(\"div\", \"ghheat__extra\");\n        var exsw = html(\"span\", \"ghheat__sw ghheat__sw--empty\");\n        exsw.style.background = emptyColor;\n        ex.appendChild(exsw);\n        var ext = html(\"span\", \"ghheat__extratext\");\n        ext.innerHTML = panel.extraNote;\n        ex.appendChild(ext);\n        side.appendChild(ex);\n      }\n      body.appendChild(side);\n\n      wrap.appendChild(body);\n      root.appendChild(wrap);\n    });\n\n    if (config.caption) {\n      var cap = html(\"div\", \"ghchart__caption\");\n      cap.innerHTML = config.caption;\n      root.appendChild(cap);\n    }\n  };\n\n  \/* ================================================================\n     Scatter renderer \u2014 X\/Y numeric plot with categorical points.\n     Used for cost-vs-quality benchmark charts: a highlighted \"primary\"\n     point (glow halo), a \"reference\" point with a dotted crosshair, and\n     muted \"other\" points. Values never print on the marks \u2014 a point's\n     name shows for identity and the exact (y, x) reveals on hover.\n     ================================================================ *\/\n  renderers.scatter = function (root, config) {\n    root.textContent = \"\";\n    var uid = config.mountId;\n\n    var VB = { w: 1000, h: 600 };\n    var M = { top: 40, right: 30, bottom: 74, left: 96 };\n    var innerW = VB.w - M.left - M.right;\n    var innerH = VB.h - M.top - M.bottom;\n\n    var xa = config.xAxis, ya = config.yAxis;\n    var fmtX = xa.format || function (v) { return String(v); };\n    var fmtY = ya.format || function (v) { return String(v); };\n    var tipSuffix = config.tipSuffix || \"\";\n\n    function xAt(v) { return M.left + innerW * (v - xa.min) \/ (xa.max - xa.min); }\n    function yAt(v) { return M.top + innerH * (1 - (v - ya.min) \/ (ya.max - ya.min)); }\n    var baseY = yAt(ya.min);\n\n    var motionMQ = window.matchMedia\n      ? window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n      : null;\n    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }\n\n    function colorOf(pt) {\n      var s = config.series && config.series[pt.series || 0];\n      return (s && s.color) || \"#8c959f\";\n    }\n\n    \/* ---- Title (compact narrative headline, no rule) ------------ *\/\n    var title = html(\"div\", \"ghchart__headline\");\n    title.textContent = config.title;\n    root.appendChild(title);\n\n    \/* ---- Header + legend (from series) -------------------------- *\/\n    if (!config.hideLegend) {\n      var header = html(\"div\", \"ghchart__header\");\n      var legend = html(\"div\", \"ghchart__legend\");\n      (config.series || []).forEach(function (s) {\n        var item = html(\"span\", \"ghchart__legend-item\");\n        var sw = html(\"span\", \"ghchart__swatch ghchart__swatch--dot\");\n        sw.style.background = s.color;\n        item.appendChild(sw);\n        item.appendChild(document.createTextNode(s.name));\n        legend.appendChild(item);\n      });\n      header.appendChild(legend);\n      root.appendChild(header);\n    }\n\n    \/* ---- SVG plot ----------------------------------------------- *\/\n    var svg = el(\"svg\", {\n      \"class\": \"ghchart__plot ghchart__plot--scatter\",\n      viewBox: \"0 0 \" + VB.w + \" \" + VB.h,\n      preserveAspectRatio: \"xMidYMid meet\",\n      role: \"img\",\n      \"aria-label\": config.title + \" scatter plot\"\n    });\n\n    \/\/ Gridlines (both directions) + tick labels.\n    var eps = 1e-6;\n    for (var yv = ya.min; yv <= ya.max + eps; yv += ya.step) {\n      var gy = yAt(yv);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__gridline\",\n        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy\n      }));\n      var yl = el(\"text\", {\n        \"class\": \"ghchart__axislabel ghchart__axislabel--y\",\n        x: M.left - 16, y: gy, dy: \"0.32em\", \"text-anchor\": \"end\"\n      });\n      yl.textContent = fmtY(yv);\n      svg.appendChild(yl);\n    }\n    for (var xv = xa.min; xv <= xa.max + eps; xv += xa.step) {\n      var gx = xAt(xv);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__gridline ghchart__gridline--v\",\n        x1: gx, y1: M.top, x2: gx, y2: baseY\n      }));\n      var isFirst = Math.abs(xv - xa.min) < eps;\n      var isLast = Math.abs(xv - xa.max) < eps;\n      var xl = el(\"text\", {\n        \"class\": \"ghchart__axislabel ghchart__axislabel--x\",\n        x: gx, y: baseY + 34,\n        \"text-anchor\": isFirst ? \"start\" : (isLast ? \"end\" : \"middle\")\n      });\n      xl.textContent = fmtX(xv);\n      svg.appendChild(xl);\n    }\n\n    \/\/ Axis titles.\n    if (xa.title) {\n      var xt = el(\"text\", {\n        \"class\": \"ghchart__axistitle\",\n        x: M.left + innerW \/ 2, y: VB.h - 12, \"text-anchor\": \"middle\"\n      });\n      xt.textContent = xa.title;\n      svg.appendChild(xt);\n    }\n    if (ya.title) {\n      var ytX = 24, ytY = M.top + innerH \/ 2;\n      var yt = el(\"text\", {\n        \"class\": \"ghchart__axistitle\",\n        x: ytX, y: ytY, \"text-anchor\": \"middle\",\n        transform: \"rotate(-90 \" + ytX + \" \" + ytY + \")\"\n      });\n      yt.textContent = ya.title;\n      svg.appendChild(yt);\n    }\n\n    \/\/ Reference crosshair through the flagged point (dual dotted lines).\n    var refPt = (config.points || []).filter(function (p) { return p.reference; })[0];\n    if (refPt) {\n      var rColor = colorOf(refPt);\n      var rx = xAt(refPt.x), ry = yAt(refPt.y);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__crosshair\",\n        x1: rx, y1: M.top, x2: rx, y2: baseY, stroke: rColor\n      }));\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__crosshair\",\n        x1: M.left, y1: ry, x2: VB.w - M.right, y2: ry, stroke: rColor\n      }));\n    }\n\n    \/\/ Points: halo (highlight\/reference) + dot + name label.\n    var dotEls = [], hitEls = [];\n    (config.points || []).forEach(function (pt, i) {\n      var cx = xAt(pt.x), cy = yAt(pt.y), c = colorOf(pt);\n      var big = pt.highlight || pt.reference;\n\n      if (big) {\n        svg.appendChild(el(\"circle\", {\n          \"class\": \"ghchart__halo\", cx: cx, cy: cy, r: 17, fill: c\n        }));\n      }\n      var dot = el(\"circle\", {\n        \"class\": \"ghchart__pt\", cx: cx, cy: cy,\n        r: big ? 9 : 5.5, fill: c\n      });\n      svg.appendChild(dot);\n      dotEls.push({ el: dot, baseR: big ? 9 : 5.5 });\n\n      \/\/ Name label \u2014 bold+colored for the story points, muted for the rest.\n      var pos = pt.labelPos || (pt.highlight ? \"below\" : pt.reference ? \"above\" : \"right\");\n      var dx = 0, dy = 0, anchor = \"middle\";\n      var gap = big ? 15 : 9;\n      if (pos === \"right\") { dx = gap + 4; dy = 5; anchor = \"start\"; }\n      else if (pos === \"left\") { dx = -(gap + 4); dy = 5; anchor = \"end\"; }\n      else if (pos === \"above\") { dy = -(gap + 6); anchor = \"middle\"; }\n      else { dy = gap + 16; anchor = \"middle\"; }  \/\/ below\n      var lbl = el(\"text\", {\n        \"class\": \"ghchart__pt-label\" + (big ? \" ghchart__pt-label--strong\" : \"\"),\n        x: cx + dx, y: cy + dy, \"text-anchor\": anchor,\n        fill: big ? c : null\n      });\n      lbl.textContent = pt.label;\n      svg.appendChild(lbl);\n\n      \/\/ Transparent hit target for hover\/focus.\n      var hit = el(\"circle\", {\n        \"class\": \"ghchart__pthit\", cx: cx, cy: cy, r: 20,\n        tabindex: \"0\", role: \"img\",\n        \"aria-label\": pt.label + \": \" + fmtY(pt.y) + \", \" + fmtX(pt.x) + tipSuffix\n      });\n      svg.appendChild(hit);\n      hitEls.push(hit);\n    });\n\n    root.appendChild(svg);\n\n    \/* ---- Tooltip (title + free lines) --------------------------- *\/\n    var tip = html(\"div\", \"ghchart__tooltip\");\n    tip.setAttribute(\"role\", \"status\");\n    var tipTitle = html(\"div\", \"ghchart__tt-title\");\n    var tipBody = html(\"div\", \"ghchart__tt-body\");\n    tip.appendChild(tipTitle);\n    tip.appendChild(tipBody);\n    root.appendChild(tip);\n\n    var activeIndex = -1;\n    function show(i) {\n      var pt = config.points[i];\n      if (!pt) return;\n      var reappearing = activeIndex === -1;\n      activeIndex = i;\n\n      dotEls.forEach(function (d, di) {\n        d.el.setAttribute(\"r\", di === i ? d.baseR + 2.5 : d.baseR);\n      });\n\n      tipTitle.textContent = pt.label;\n      tipBody.textContent = \"\";\n      var line = html(\"div\", \"ghchart__tt-line\");\n      line.textContent = fmtY(pt.y) + \" \u00b7 \" + fmtX(pt.x) + tipSuffix;\n      tipBody.appendChild(line);\n\n      var anchor = dotEls[i].el.getBoundingClientRect();\n      var rootRect = root.getBoundingClientRect();\n      var left = anchor.left + anchor.width \/ 2 - rootRect.left;\n      var top = anchor.top - rootRect.top - 12;\n\n      var suppress = reappearing && !reduceMotion();\n      if (suppress) tip.style.transition = \"opacity 0.12s ease\";\n\n      tip.setAttribute(\"data-visible\", \"true\");\n      var halfW = tip.offsetWidth \/ 2;\n      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);\n      if (top - tip.offsetHeight < 0) {\n        top = anchor.bottom - rootRect.top + 12;\n        tip.style.transform = \"translate(-50%, 0)\";\n      } else {\n        tip.style.transform = \"translate(-50%, -100%)\";\n      }\n      tip.style.left = left + \"px\";\n      tip.style.top = top + \"px\";\n      if (suppress) { void tip.offsetWidth; tip.style.transition = \"\"; }\n    }\n    function hide() {\n      activeIndex = -1;\n      tip.setAttribute(\"data-visible\", \"false\");\n      dotEls.forEach(function (d) { d.el.setAttribute(\"r\", d.baseR); });\n    }\n\n    hitEls.forEach(function (hit, i) {\n      hit.addEventListener(\"pointerenter\", function () { show(i); });\n      hit.addEventListener(\"pointermove\", function () { show(i); });\n      hit.addEventListener(\"pointerleave\", hide);\n      hit.addEventListener(\"focus\", function () { show(i); });\n      hit.addEventListener(\"blur\", hide);\n    });\n  };\n\n  \/* ================================================================\n     Trend renderer \u2014 dated time-series with a narrative headline.\n     One renderer covers both HydraFusion time charts via config:\n       \u2022 linear OR log y-axis (yAxis.scale), explicit yAxis.ticks;\n       \u2022 phase background columns (with optional per-phase average);\n       \u2022 filled squares (values reveal on hover \u2014 never printed);\n       \u2022 red-X \"invalid run\" markers with leader-line annotations;\n       \u2022 a dashed trend line + optional \u00b1band + slope label;\n       \u2022 an optional horizontal reference line, bracket, and final callout.\n     ================================================================ *\/\n  renderers.trend = function (root, config) {\n    root.textContent = \"\";\n\n    var VB = { w: 1000, h: 560 };\n    var M = { top: 64, right: 56, bottom: 56, left: 104 };\n    var innerW = VB.w - M.left - M.right;\n    var innerH = VB.h - M.top - M.bottom;\n\n    var xa = config.xAxis, ya = config.yAxis;\n    var isLog = ya.scale === \"log\";\n    var fmtY = ya.format || function (v) { return String(v); };\n    var series = config.series || [];\n    function sColor(i, fb) {\n      var s = series[i];\n      return (s && s.color) || fb;\n    }\n    var cPoint = sColor(0, \"#8250df\");\n    var cInvalid = sColor(1, \"#cf222e\");\n    var cTrend = (config.trend && config.trend.color) || sColor(0, \"#8250df\");\n\n    function xAt(v) { return M.left + innerW * (v - xa.min) \/ (xa.max - xa.min); }\n    function yAt(v) {\n      if (isLog) {\n        var lo = Math.log(ya.min) \/ Math.LN10, hi = Math.log(ya.max) \/ Math.LN10;\n        var t = (Math.log(v) \/ Math.LN10 - lo) \/ (hi - lo);\n        return M.top + innerH * (1 - t);\n      }\n      return M.top + innerH * (1 - (v - ya.min) \/ (ya.max - ya.min));\n    }\n    var baseY = yAt(ya.min);\n\n    var motionMQ = window.matchMedia\n      ? window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n      : null;\n    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }\n\n    \/* ---- Headline + subtitle + legend (HTML flow above the plot) --- *\/\n    var headline = html(\"div\", \"ghchart__headline\");\n    headline.textContent = config.title;\n    root.appendChild(headline);\n    if (config.subtitle) {\n      var sub = html(\"div\", \"ghchart__subtitle\");\n      sub.textContent = config.subtitle;\n      root.appendChild(sub);\n    }\n    if (!config.hideLegend && series.length) {\n      var legend = html(\"div\", \"ghchart__legend ghchart__legend--trend\");\n      \/* Build one legend glyph. The \"invalid\" glyph mirrors the on-plot marker\n         exactly \u2014 a red diamond outline with an X through it (same side=13 \/\n         q=3.4 proportions used to draw the mark below) \u2014 so the key matches\n         the mark on the chart. *\/\n      function trendGlyph(glyph, color) {\n        if (glyph === \"invalid\") {\n          var g = el(\"svg\", {\n            \"class\": \"ghchart__glyph ghchart__glyph--invalid\",\n            viewBox: \"0 0 22 22\", width: \"15\", height: \"15\",\n            style: \"overflow:visible\"\n          });\n          g.appendChild(el(\"rect\", {\n            x: 4.5, y: 4.5, width: 13, height: 13, rx: 1.5,\n            transform: \"rotate(45 11 11)\", fill: \"none\",\n            stroke: color, \"stroke-width\": 2\n          }));\n          g.appendChild(el(\"line\", {\n            x1: 7.6, y1: 7.6, x2: 14.4, y2: 14.4,\n            stroke: color, \"stroke-width\": 2, \"stroke-linecap\": \"round\"\n          }));\n          g.appendChild(el(\"line\", {\n            x1: 7.6, y1: 14.4, x2: 14.4, y2: 7.6,\n            stroke: color, \"stroke-width\": 2, \"stroke-linecap\": \"round\"\n          }));\n          return g;\n        }\n        var gl = html(\"span\", \"ghchart__glyph ghchart__glyph--\" + glyph);\n        if (glyph === \"line\") { gl.style.borderTopColor = color; }\n        else { gl.style.background = color; gl.style.borderColor = color; }\n        if (glyph === \"diamond\") { gl.style.background = \"transparent\"; }\n        return gl;\n      }\n      series.forEach(function (s) {\n        var item = html(\"span\", \"ghchart__legend-item\");\n        item.appendChild(trendGlyph(s.glyph || \"square\", s.color));\n        item.appendChild(document.createTextNode(s.name));\n        legend.appendChild(item);\n      });\n      (config.legendExtra || []).forEach(function (s) {\n        var item = html(\"span\", \"ghchart__legend-item\");\n        item.appendChild(trendGlyph(s.glyph || \"line\", s.color));\n        item.appendChild(document.createTextNode(s.name));\n        legend.appendChild(item);\n      });\n      root.appendChild(legend);\n    }\n\n    \/* ---- SVG plot ------------------------------------------------- *\/\n    var svg = el(\"svg\", {\n      \"class\": \"ghchart__plot ghchart__plot--trend\",\n      viewBox: \"0 0 \" + VB.w + \" \" + VB.h,\n      preserveAspectRatio: \"xMidYMid meet\",\n      role: \"img\",\n      \"aria-label\": config.title\n    });\n\n    \/\/ Phase background columns + separators + labels (+ optional average).\n    (config.phases || []).forEach(function (ph, i) {\n      var x0 = xAt(ph.from), x1 = xAt(ph.to);\n      svg.appendChild(el(\"rect\", {\n        \"class\": \"ghchart__phase\" + (i % 2 ? \" ghchart__phase--alt\" : \"\"),\n        x: x0, y: M.top, width: Math.max(0, x1 - x0), height: innerH\n      }));\n      if (i > 0) {\n        svg.appendChild(el(\"line\", {\n          \"class\": \"ghchart__phase-sep\", x1: x0, y1: M.top, x2: x0, y2: baseY\n        }));\n      }\n      var pl = el(\"text\", {\n        \"class\": \"ghchart__phase-label\", x: (x0 + x1) \/ 2, y: M.top - 40,\n        \"text-anchor\": \"middle\"\n      });\n      pl.textContent = ph.label;\n      svg.appendChild(pl);\n      if (ph.avg) {\n        var pa = el(\"text\", {\n          \"class\": \"ghchart__phase-avg\", x: (x0 + x1) \/ 2, y: M.top + 24,\n          \"text-anchor\": \"middle\", fill: cPoint\n        });\n        pa.textContent = ph.avg;\n        svg.appendChild(pa);\n      }\n    });\n\n    \/\/ Horizontal gridlines + y tick labels (skip a gridline at the reference y).\n    var refY = config.reference ? config.reference.y : null;\n    (ya.ticks || []).forEach(function (tv) {\n      var gy = yAt(tv);\n      if (refY === null || Math.abs(tv - refY) > 1e-9) {\n        svg.appendChild(el(\"line\", {\n          \"class\": \"ghchart__gridline\", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy\n        }));\n      }\n      var yl = el(\"text\", {\n        \"class\": \"ghchart__axislabel ghchart__axislabel--y\",\n        x: M.left - 16, y: gy, dy: \"0.32em\", \"text-anchor\": \"end\"\n      });\n      yl.textContent = fmtY(tv);\n      svg.appendChild(yl);\n    });\n\n    \/\/ X tick labels (dated).\n    (xa.ticks || []).forEach(function (t) {\n      var gx = xAt(t.at);\n      var first = Math.abs(t.at - xa.min) < 1e-9, last = Math.abs(t.at - xa.max) < 1e-9;\n      var xl = el(\"text\", {\n        \"class\": \"ghchart__axislabel ghchart__axislabel--x\",\n        x: gx, y: baseY + 30, \"text-anchor\": first ? \"start\" : (last ? \"end\" : \"middle\")\n      });\n      xl.textContent = t.text;\n      svg.appendChild(xl);\n    });\n\n    \/\/ Axis titles.\n    if (ya.title) {\n      var ytX = 22, ytY = M.top + innerH \/ 2;\n      var yt = el(\"text\", {\n        \"class\": \"ghchart__axistitle\", x: ytX, y: ytY, \"text-anchor\": \"middle\",\n        transform: \"rotate(-90 \" + ytX + \" \" + ytY + \")\"\n      });\n      yt.textContent = ya.title;\n      svg.appendChild(yt);\n    }\n\n    \/\/ Trend: optional \u00b1band (linear only), then the dashed line + slope label.\n    var tr = config.trend;\n    if (tr) {\n      function trendY(x) {\n        return tr.from.y + (tr.to.y - tr.from.y) * (x - tr.from.x) \/ (tr.to.x - tr.from.x);\n      }\n      if (tr.band && !isLog) {\n        var p = tr.band;\n        var pts = [\n          [xAt(tr.from.x), yAt(trendY(tr.from.x) + p)],\n          [xAt(tr.to.x), yAt(trendY(tr.to.x) + p)],\n          [xAt(tr.to.x), yAt(trendY(tr.to.x) - p)],\n          [xAt(tr.from.x), yAt(trendY(tr.from.x) - p)]\n        ].map(function (a) { return a[0] + \",\" + a[1]; }).join(\" \");\n        svg.appendChild(el(\"polygon\", {\n          \"class\": \"ghchart__trendband\", points: pts, fill: cTrend\n        }));\n      }\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__trendline\",\n        x1: xAt(tr.from.x), y1: yAt(tr.from.y),\n        x2: xAt(tr.to.x), y2: yAt(tr.to.y), stroke: cTrend\n      }));\n      if (tr.label) {\n        var lx, lyy;\n        if (tr.labelAt) { lx = xAt(tr.labelAt.x); lyy = yAt(tr.labelAt.y); }\n        else {\n          lx = xAt(tr.to.x) + (tr.labelDX == null ? -6 : tr.labelDX);\n          lyy = yAt(tr.to.y) + (tr.labelDY == null ? -12 : tr.labelDY);\n        }\n        var sl = el(\"text\", {\n          \"class\": \"ghchart__slope\", x: lx, y: lyy,\n          \"text-anchor\": tr.labelAnchor || \"end\", fill: cTrend\n        });\n        sl.textContent = tr.label;\n        svg.appendChild(sl);\n      }\n    }\n\n    \/\/ Reference line (horizontal dashed) + right-aligned label.\n    if (config.reference) {\n      var rc = config.reference.color || \"#24292f\";\n      var ryy = yAt(config.reference.y);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__refline\", x1: M.left, y1: ryy, x2: VB.w - M.right, y2: ryy,\n        stroke: rc\n      }));\n      if (config.reference.label) {\n        var rl = el(\"text\", {\n          \"class\": \"ghchart__reflabel\", x: VB.w - M.right, y: ryy - 8,\n          \"text-anchor\": \"end\", fill: rc\n        });\n        rl.textContent = config.reference.label;\n        svg.appendChild(rl);\n      }\n    }\n\n    \/\/ Data squares (values reveal on hover only).\n    var side = 13, hSide = side \/ 2;\n    var sqEls = [], hitEls = [];\n    (config.points || []).forEach(function (pt, i) {\n      var cx = xAt(pt.x), cy = yAt(pt.y);\n      var sq = el(\"rect\", {\n        \"class\": \"ghchart__square\", x: cx - hSide, y: cy - hSide,\n        width: side, height: side, rx: 2, fill: cPoint\n      });\n      svg.appendChild(sq);\n      sqEls.push({ el: sq, cx: cx, cy: cy });\n\n      \/\/ Hover\/focus hit target \u2014 omitted when hover is disabled so the\n      \/\/ (illustrative) point values never surface, on screen or to a11y.\n      if (!config.disableHover) {\n        var hit = el(\"rect\", {\n          \"class\": \"ghchart__squarehit\", x: cx - 13, y: cy - 13,\n          width: 26, height: 26, tabindex: \"0\", role: \"img\",\n          \"aria-label\": (pt.d || \"\") + \" \" + fmtY(pt.y)\n        });\n        svg.appendChild(hit);\n        hitEls.push(hit);\n      }\n    });\n\n    \/\/ Invalid-run markers: red diamond + X, leader line, annotation text.\n    (config.invalids || []).forEach(function (iv) {\n      var cx = xAt(iv.x), cy = yAt(iv.y);\n      svg.appendChild(el(\"rect\", {\n        \"class\": \"ghchart__invalid\", x: cx - hSide, y: cy - hSide,\n        width: side, height: side, rx: 1.5, fill: \"none\", stroke: cInvalid,\n        transform: \"rotate(45 \" + cx + \" \" + cy + \")\"\n      }));\n      var q = 3.4;\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__invalid-x\", x1: cx - q, y1: cy - q, x2: cx + q, y2: cy + q,\n        stroke: cInvalid\n      }));\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__invalid-x\", x1: cx - q, y1: cy + q, x2: cx + q, y2: cy - q,\n        stroke: cInvalid\n      }));\n      if (iv.title) {\n        var tx = cx + (iv.labelDX == null ? 16 : iv.labelDX);\n        var ty = cy + (iv.labelDY == null ? -26 : iv.labelDY);\n        svg.appendChild(el(\"line\", {\n          \"class\": \"ghchart__leader\", x1: cx, y1: cy, x2: tx, y2: ty, stroke: cInvalid\n        }));\n        var anchor = iv.labelAnchor || \"start\";\n        var at = el(\"text\", {\n          \"class\": \"ghchart__anno-title\", x: tx, y: ty - 4, \"text-anchor\": anchor, fill: cInvalid\n        });\n        at.textContent = iv.title;\n        svg.appendChild(at);\n        if (iv.sub) {\n          var asu = el(\"text\", {\n            \"class\": \"ghchart__anno-sub\", x: tx, y: ty + 11, \"text-anchor\": anchor\n          });\n          asu.textContent = iv.sub;\n          svg.appendChild(asu);\n        }\n      }\n    });\n\n    \/\/ Bracket annotation (span with end ticks + centered label above).\n    (config.brackets || []).forEach(function (b) {\n      var x0 = xAt(b.fromX), x1 = xAt(b.toX), by = yAt(b.y);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__bracket\", x1: x0, y1: by, x2: x1, y2: by\n      }));\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: x0, y1: by, x2: x0, y2: by + 7 }));\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: x1, y1: by, x2: x1, y2: by + 7 }));\n      var bl = el(\"text\", {\n        \"class\": \"ghchart__bracket-label\", x: (x0 + x1) \/ 2, y: by - 8, \"text-anchor\": \"middle\"\n      });\n      bl.textContent = b.text;\n      svg.appendChild(bl);\n    });\n\n    \/\/ Final callout: ringed point + leader + title\/sub placed inside the plot.\n    if (config.callout) {\n      var co = config.callout;\n      var ccx = xAt(co.x), ccy = yAt(co.y);\n      svg.appendChild(el(\"circle\", {\n        \"class\": \"ghchart__callout-ring\", cx: ccx, cy: ccy, r: 12, stroke: cPoint\n      }));\n      var ctx = ccx + (co.labelDX == null ? -16 : co.labelDX);\n      var cty = ccy + (co.labelDY == null ? -30 : co.labelDY);\n      svg.appendChild(el(\"line\", {\n        \"class\": \"ghchart__leader ghchart__leader--callout\", x1: ccx, y1: ccy, x2: ctx, y2: cty,\n        stroke: cPoint\n      }));\n      var can = co.labelAnchor || \"end\";\n      var ct = el(\"text\", {\n        \"class\": \"ghchart__anno-title ghchart__anno-title--callout\", x: ctx, y: cty - 4, \"text-anchor\": can, fill: cPoint\n      });\n      ct.textContent = co.title;\n      svg.appendChild(ct);\n      if (co.sub) {\n        var cs = el(\"text\", {\n          \"class\": \"ghchart__anno-sub ghchart__anno-sub--callout\", x: ctx, y: cty + 12, \"text-anchor\": can\n        });\n        cs.textContent = co.sub;\n        svg.appendChild(cs);\n      }\n    }\n\n    root.appendChild(svg);\n\n    if (config.caption) {\n      var cap = html(\"div\", \"ghchart__caption\");\n      cap.textContent = config.caption;\n      root.appendChild(cap);\n    }\n\n    \/* ---- Tooltip + hover interaction (skipped when disabled) ------ *\/\n    if (!config.disableHover) {\n    var tip = html(\"div\", \"ghchart__tooltip\");\n    tip.setAttribute(\"role\", \"status\");\n    var tipTitle = html(\"div\", \"ghchart__tt-title\");\n    var tipBody = html(\"div\", \"ghchart__tt-body\");\n    tip.appendChild(tipTitle);\n    tip.appendChild(tipBody);\n    root.appendChild(tip);\n\n    var activeIndex = -1;\n    function show(i) {\n      var pt = config.points[i];\n      if (!pt) return;\n      var reappearing = activeIndex === -1;\n      activeIndex = i;\n\n      sqEls.forEach(function (s, si) {\n        var big = si === i;\n        var w = big ? side + 5 : side;\n        s.el.setAttribute(\"width\", w);\n        s.el.setAttribute(\"height\", w);\n        s.el.setAttribute(\"x\", s.cx - w \/ 2);\n        s.el.setAttribute(\"y\", s.cy - w \/ 2);\n      });\n\n      var tipInfo = config.pointTip\n        ? config.pointTip(pt)\n        : { title: pt.d || \"\", line: fmtY(pt.y) };\n      tipTitle.textContent = tipInfo.title;\n      tipBody.textContent = \"\";\n      var line = html(\"div\", \"ghchart__tt-line\");\n      line.textContent = tipInfo.line;\n      tipBody.appendChild(line);\n\n      var anchor = sqEls[i].el.getBoundingClientRect();\n      var rootRect = root.getBoundingClientRect();\n      var left = anchor.left + anchor.width \/ 2 - rootRect.left;\n      var top = anchor.top - rootRect.top - 12;\n\n      var suppress = reappearing && !reduceMotion();\n      if (suppress) tip.style.transition = \"opacity 0.12s ease\";\n\n      tip.setAttribute(\"data-visible\", \"true\");\n      var halfW = tip.offsetWidth \/ 2;\n      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);\n      if (top - tip.offsetHeight < 0) {\n        top = anchor.bottom - rootRect.top + 12;\n        tip.style.transform = \"translate(-50%, 0)\";\n      } else {\n        tip.style.transform = \"translate(-50%, -100%)\";\n      }\n      tip.style.left = left + \"px\";\n      tip.style.top = top + \"px\";\n      if (suppress) { void tip.offsetWidth; tip.style.transition = \"\"; }\n    }\n    function hide() {\n      activeIndex = -1;\n      tip.setAttribute(\"data-visible\", \"false\");\n      sqEls.forEach(function (s) {\n        s.el.setAttribute(\"width\", side);\n        s.el.setAttribute(\"height\", side);\n        s.el.setAttribute(\"x\", s.cx - hSide);\n        s.el.setAttribute(\"y\", s.cy - hSide);\n      });\n    }\n\n    hitEls.forEach(function (hit, i) {\n      hit.addEventListener(\"pointerenter\", function () { show(i); });\n      hit.addEventListener(\"pointermove\", function () { show(i); });\n      hit.addEventListener(\"pointerleave\", hide);\n      hit.addEventListener(\"focus\", function () { show(i); });\n      hit.addEventListener(\"blur\", hide);\n    });\n    }\n  };\n\n  \/* ================================================================\n     Distribution dashboard renderer\n     ----------------------------------------------------------------\n     A composition figure (no hover \u2014 every value is printed as text, so\n     the marks are inherently screen-reader accessible):\n       \u2022 a compact narrative headline + muted subtitle;\n       \u2022 an optional segmented \"mix\" bar \u2014 proportional colored segments,\n         each with its name inside and count\/percent below;\n       \u2022 one or more panels of ranked horizontal bars, each row printing\n         its category (left), a bar, and its count\/percent (right).\n     Colors come from config.series so the harness can tune them live:\n       series[0]   = bar fill (single base hex);\n       series[1..] = segment fills, in the order of config.segments.items.\n     ================================================================ *\/\n  renderers.distribution = function (root, config) {\n    root.textContent = \"\";\n    root.setAttribute(\"role\", \"figure\");\n    if (config.title) root.setAttribute(\"aria-label\", config.title);\n\n    var series = config.series || [];\n    var barColor = (series[0] && series[0].color) || \"#8250df\";\n    var total = config.total ||\n      (config.segments && config.segments.items\n        ? config.segments.items.reduce(function (a, b) { return a + b.value; }, 0)\n        : 0);\n\n    function pctText(v) {\n      if (!total) return \"\";\n      return (Math.round((v \/ total) * 1000) \/ 10).toFixed(1) + \"%\";\n    }\n    \/* Label format is config-driven so a chart can print counts, percents,\n       or both without a renderer change: config.valueFormat =\n       \"both\" (default) | \"percent\" | \"count\". Counts stay in the data either\n       way so the percentages are always derived from them. *\/\n    var valueFormat = config.valueFormat || \"both\";\n    function valueHTML(v) {\n      var p = pctText(v);\n      if (valueFormat === \"percent\") return p;\n      if (valueFormat === \"count\") return \"<b>\" + v + \"<\/b>\";\n      return \"<b>\" + v + \"<\/b>\" + (p ? \" (\" + p + \")\" : \"\");\n    }\n\n    \/* ---- Headline + subtitle ------------------------------------- *\/\n    if (config.title) {\n      var head = html(\"div\", \"ghchart__headline\");\n      head.textContent = config.title;\n      root.appendChild(head);\n    }\n    if (config.subtitle) {\n      var sub = html(\"p\", \"ghchart__subtitle\");\n      sub.textContent = config.subtitle;\n      root.appendChild(sub);\n    }\n\n    \/* ---- Segmented \"mix\" bar -------------------------------------- *\/\n    var seg = config.segments;\n    if (seg && seg.items && seg.items.length) {\n      var segSec = html(\"div\", \"ghchart__dist-section\");\n      if (seg.title) {\n        var segTitle = html(\"div\", \"ghchart__panel-title\");\n        segTitle.textContent = seg.title;\n        segSec.appendChild(segTitle);\n      }\n      var segTotal = seg.items.reduce(function (a, b) { return a + b.value; }, 0) || 1;\n      var bar = html(\"div\", \"ghchart__seg\");\n      var notes = html(\"div\", \"ghchart__seg-note-row\");\n      seg.items.forEach(function (it, i) {\n        var basis = (it.value \/ segTotal) * 100;\n        var col = (series[i + 1] && series[i + 1].color) || it.color || barColor;\n\n        var cell = html(\"div\", \"ghchart__seg-cell\");\n        cell.style.flex = \"0 0 \" + basis + \"%\";\n        cell.style.background = col;\n        cell.textContent = it.label;\n        bar.appendChild(cell);\n\n        var note = html(\"div\", \"ghchart__seg-note\");\n        note.style.flex = \"0 0 \" + basis + \"%\";\n        note.innerHTML = valueHTML(it.value);\n        notes.appendChild(note);\n      });\n      segSec.appendChild(bar);\n      segSec.appendChild(notes);\n      root.appendChild(segSec);\n    }\n\n    \/* ---- Ranked bar panels (grid: 2-up, stacking on narrow) ------ *\/\n    var panels = config.panels || [];\n    if (panels.length) {\n      var cols = html(\"div\", \"ghchart__dist-cols\");\n      if (config.columns === 1 || panels.length === 1) {\n        cols.classList.add(\"ghchart__dist-cols--stack\");\n      }\n      panels.forEach(function (panel) {\n        var sec = html(\"div\", \"ghchart__dist-section\");\n        if (panel.title) {\n          var pt = html(\"div\", \"ghchart__panel-title\");\n          pt.textContent = panel.title;\n          sec.appendChild(pt);\n        }\n        if (panel.subtitle) {\n          var descriptor = html(\"p\", \"ghchart__subtitle\");\n          descriptor.textContent = panel.subtitle;\n          sec.appendChild(descriptor);\n        }\n        if (panel.stats) {\n          var stats = html(\"dl\", \"ghchart__dist-stats\");\n          panel.stats.forEach(function (stat) {\n            var pair = html(\"div\", \"ghchart__dist-stat\");\n            var term = html(\"dt\");\n            term.textContent = stat.label;\n            var value = html(\"dd\");\n            value.textContent = stat.value;\n            value.style.color = strongOf((series[panel.series || 0] || series[0]).color);\n            pair.appendChild(term);\n            pair.appendChild(value);\n            stats.appendChild(pair);\n          });\n          sec.appendChild(stats);\n          cols.appendChild(sec);\n          return;\n        }\n        var list = html(\"div\", \"ghchart__dist-list\");\n        var pmax = (panel.items || []).reduce(function (m, it) {\n          return it.value > m ? it.value : m;\n        }, 0) || 1;\n        (panel.items || []).forEach(function (it) {\n          var row = html(\"div\", \"ghchart__dist-row\");\n\n          var cat = html(\"div\", \"ghchart__dist-cat\");\n          cat.textContent = it.label;\n          row.appendChild(cat);\n\n          var track = html(\"div\", \"ghchart__dist-track\");\n          var b = html(\"div\", \"ghchart__dist-bar\");\n          b.style.width = Math.max((it.value \/ pmax) * 100, 1.2) + \"%\";\n          b.style.background = it.color || barColor;\n          track.appendChild(b);\n          row.appendChild(track);\n\n          var val = html(\"div\", \"ghchart__dist-val\");\n          val.innerHTML = valueHTML(it.value);\n          row.appendChild(val);\n\n          list.appendChild(row);\n        });\n        sec.appendChild(list);\n        cols.appendChild(sec);\n      });\n      root.appendChild(cols);\n    }\n\n    \/* ---- Caption \/ footnote -------------------------------------- *\/\n    if (config.caption) {\n      var cap = html(\"div\", \"ghchart__caption\");\n      cap.innerHTML = config.caption;\n      root.appendChild(cap);\n    }\n  };\n\n  \/* ================================================================\n     Timeline \/ staged-duration renderer\n     ----------------------------------------------------------------\n     A horizontal time axis broken into labeled stage segments. Composes\n     only the pieces the data needs (like the trend renderer):\n       \u2022 eyebrow kicker + narrative headline + optional subtitle;\n       \u2022 an optional before\/after stat card and a stage legend;\n       \u2022 one or more rows of segmented duration bars on a shared time\n         scale \u2014 a row is either sequential (segments end to end) or\n         parallel (a full-height \"gate\" segment then concurrent lanes);\n       \u2022 optional flow ribbons linking matching stages between the first\n         two rows (the sequential -> parallel \"collapse\");\n       \u2022 optional elbow-leader callouts above narrow segments;\n       \u2022 optional span brackets (below a row, or right of a parallel group);\n       \u2022 an optional numeric axis (ticks + gridlines + title).\n     No hover \u2014 every label is printed, so it reads on paper and to screen\n     readers. Stage colors come from config.series so the harness tunes\n     them live: each entry is { id, name, color }.\n     ================================================================ *\/\n  renderers.timeline = function (root, config) {\n    root.textContent = \"\";\n    root.setAttribute(\"role\", \"img\");\n    if (config.title) root.setAttribute(\"aria-label\", config.title);\n\n    var stages = config.series || [];\n    var stageById = {};\n    stages.forEach(function (s) { if (s && s.id) stageById[s.id] = s; });\n    function colorOf(seg) {\n      return seg.color || (stageById[seg.id] && stageById[seg.id].color) || \"#8250df\";\n    }\n    function nameOf(seg) {\n      if (seg.label != null) return seg.label;\n      return seg.name || (stageById[seg.id] && stageById[seg.id].name) || \"\";\n    }\n\n    \/* ---- DOM header: eyebrow + headline + subtitle --------------- *\/\n    if (config.eyebrow) {\n      var eb = html(\"div\", \"ghchart__tl-eyebrow\");\n      eb.textContent = config.eyebrow;\n      root.appendChild(eb);\n    }\n    if (config.title) {\n      var hl = html(\"div\", \"ghchart__headline ghchart__tl-headline\");\n      hl.textContent = config.title;\n      root.appendChild(hl);\n    }\n    if (config.subtitle) {\n      var sbt = html(\"p\", \"ghchart__subtitle\");\n      sbt.textContent = config.subtitle;\n      root.appendChild(sbt);\n    }\n\n    \/* ---- Stat card (before -> after + delta) -------------------- *\/\n    if (config.stat) {\n      var st = config.stat;\n      var card = html(\"div\", \"ghchart__tl-stat\");\n      var main = html(\"div\", \"ghchart__tl-stat-main\");\n      function statCol(k, v, muted) {\n        var col = html(\"div\", \"ghchart__tl-stat-col\");\n        var kk = html(\"div\", \"ghchart__tl-stat-k\"); kk.textContent = k; col.appendChild(kk);\n        var vv = html(\"div\", \"ghchart__tl-stat-v\" + (muted ? \" ghchart__tl-stat-v--muted\" : \"\"));\n        vv.textContent = v; col.appendChild(vv);\n        return col;\n      }\n      main.appendChild(statCol(st.beforeLabel, st.beforeValue, true));\n      var arrow = html(\"div\", \"ghchart__tl-stat-arrow\"); arrow.textContent = \"\\u2192\";\n      main.appendChild(arrow);\n      main.appendChild(statCol(st.afterLabel, st.afterValue, false));\n      card.appendChild(main);\n      if (st.delta) {\n        var dl = html(\"div\", \"ghchart__tl-stat-delta\");\n        var dv = html(\"div\", \"ghchart__tl-stat-delta-v\"); dv.textContent = st.delta; dl.appendChild(dv);\n        if (st.deltaLabel) {\n          var dk = html(\"div\", \"ghchart__tl-stat-delta-k\"); dk.textContent = st.deltaLabel; dl.appendChild(dk);\n        }\n        card.appendChild(dl);\n      }\n      root.appendChild(card);\n    }\n\n    \/* ---- Legend -------------------------------------------------- *\/\n    if (config.legend && config.legend.length) {\n      var lg = html(\"div\", \"ghchart__tl-legend\");\n      config.legend.forEach(function (item) {\n        var s = stageById[item.id] || {};\n        var it = html(\"span\", \"ghchart__tl-legend-item\");\n        var sw = html(\"span\", \"ghchart__tl-swatch\");\n        sw.style.background = item.color || s.color || \"#8250df\";\n        it.appendChild(sw);\n        var tx = html(\"span\", \"ghchart__tl-legend-text\");\n        tx.textContent = (item.name || s.name || \"\") + (item.value ? \"  \\u00b7  \" + item.value : \"\");\n        it.appendChild(tx);\n        lg.appendChild(it);\n      });\n      root.appendChild(lg);\n    }\n\n    if (config.rule) root.appendChild(html(\"div\", \"ghchart__tl-rule\"));\n\n    \/* ---- Plot geometry ------------------------------------------- *\/\n    var rows = config.rows || [];\n    function rowTotal(r) {\n      if (r.parallel) {\n        var g = r.gate ? r.gate.value : 0, mx = 0;\n        r.parallel.forEach(function (p) { if (p.value > mx) mx = p.value; });\n        return g + mx;\n      }\n      var t = 0; (r.segments || []).forEach(function (s) { t += s.value; }); return t;\n    }\n    var maxTotal = 0;\n    rows.forEach(function (r) { var t = rowTotal(r); if (t > maxTotal) maxTotal = t; });\n    var axis = config.axis;\n    var scaleMax = config.scaleMax || (axis && axis.max) || maxTotal || 1;\n\n    var VB_W = 1000;\n    var hasRowLabels = rows.some(function (r) { return r.label; });\n    var M = { left: hasRowLabels ? 150 : 44, right: 46 };\n    var innerW = VB_W - M.left - M.right;\n    function xAt(v) { return M.left + innerW * v \/ scaleMax; }\n    function wAt(v) { return innerW * v \/ scaleMax; }\n\n    var SEQ_H = 56, LANE_H = 13, LANE_GAP = 6, SEG_R = 4;\n    function rowBandH(r) {\n      if (r.parallel) {\n        var n = r.parallel.length;\n        return Math.max(SEQ_H, n * LANE_H + (n - 1) * LANE_GAP);\n      }\n      return SEQ_H;\n    }\n    var flows = !!config.flows && rows.length >= 2;\n    var FLOW_GAP = config.flowGap != null ? config.flowGap : 250, ROW_GAP = 48;\n\n    \/\/ Reserve vertical space for above-callouts (tallest tier wins).\n    var anyAbove = rows.some(function (r) {\n      return (r.segments || []).some(function (s) { return s.labelPos === \"above\"; });\n    });\n    var maxTier = 0;\n    rows.forEach(function (r) {\n      (r.segments || []).forEach(function (s) {\n        if (s.labelPos === \"above\" && (s.tier || 0) > maxTier) maxTier = s.tier || 0;\n      });\n    });\n    var CALLOUT_TIER_H = 40;\n    var topPad = anyAbove ? (18 + (maxTier + 1) * CALLOUT_TIER_H) : 16;\n\n    \/\/ Assign each row a vertical band.\n    var cursor = topPad;\n    var rowLayout = rows.map(function (r, i) {\n      var top = cursor, h = rowBandH(r);\n      cursor = top + h;\n      if (i < rows.length - 1) cursor += (flows ? FLOW_GAP : ROW_GAP);\n      return { top: top, h: h, bottom: top + h };\n    });\n\n    var belowBrackets = (config.brackets || []).filter(function (b) { return b.side !== \"right\"; });\n    var rightBrackets = (config.brackets || []).filter(function (b) { return b.side === \"right\"; });\n    var bracketReserve = belowBrackets.length * 46;\n    cursor += bracketReserve;\n    var axisY = cursor + 6;\n    \/\/ Ticks stay in the SVG; the axis title is a DOM caption below it, so only\n    \/\/ reserve tick space here.\n    cursor += axis ? 40 : 8;\n    var VB_H = Math.round(cursor);\n\n    var svg = el(\"svg\", {\n      \"class\": \"ghchart__plot ghchart__plot--timeline\",\n      viewBox: \"0 0 \" + VB_W + \" \" + VB_H,\n      preserveAspectRatio: \"xMidYMid meet\",\n      role: \"img\", \"aria-label\": config.title || \"\"\n    });\n    \/\/ Wrap the SVG so DOM labels can be absolutely positioned over it at\n    \/\/ percentage (viewBox-proportional) coordinates. With height:auto the SVG\n    \/\/ box keeps the viewBox aspect ratio, so % maps straight to viewBox units.\n    var plotwrap = html(\"div\", \"ghchart__tl-plotwrap\");\n    plotwrap.appendChild(svg);\n\n    \/\/ Vertical axis gridlines (behind bars).\n    if (axis && axis.ticks) {\n      axis.ticks.forEach(function (tv) {\n        var gx = xAt(tv);\n        svg.appendChild(el(\"line\", {\n          \"class\": \"ghchart__gridline ghchart__gridline--v\",\n          x1: gx, y1: topPad - 4, x2: gx, y2: axisY\n        }));\n      });\n    }\n\n    \/\/ Per-stage hover registry: each stage's before\/after rects, its ribbon,\n    \/\/ and its before\/after values, so hovering any one can emphasise the whole\n    \/\/ connection and show a tooltip (before\/after chart only).\n    var stageHover = {};\n    function hoverSlot(id) { return (stageHover[id] = stageHover[id] || { id: id }); }\n\n    \/\/ Flow ribbons (row0 -> row1), matched by stage id (drawn under bars).\n    if (flows) {\n      var r0 = rows[0], l0 = rowLayout[0], r1 = rows[1], l1 = rowLayout[1];\n      var beforeSpan = {}, acc0 = 0;\n      (r0.segments || []).forEach(function (s) {\n        beforeSpan[s.id] = { x0: xAt(acc0), x1: xAt(acc0 + s.value) }; acc0 += s.value;\n      });\n      var afterSpan = {}, gv1 = r1.gate ? r1.gate.value : 0;\n      if (r1.gate) afterSpan[r1.gate.id] = { x0: xAt(0), x1: xAt(gv1), y: l1.top };\n      (r1.parallel || []).forEach(function (p, k) {\n        \/\/ Land each ribbon on its companion lane's actual depth, not the row top.\n        afterSpan[p.id] = {\n          x0: xAt(gv1), x1: xAt(gv1 + p.value), y: l1.top + k * (LANE_H + LANE_GAP)\n        };\n      });\n      var by = l0.bottom;\n      Object.keys(beforeSpan).forEach(function (id) {\n        var b = beforeSpan[id], a = afterSpan[id];\n        if (!a) return;\n        var ay = a.y, ddy = (ay - by) * 0.5;\n        var d = \"M \" + b.x0 + \" \" + by +\n                \" C \" + b.x0 + \" \" + (by + ddy) + \" \" + a.x0 + \" \" + (ay - ddy) + \" \" + a.x0 + \" \" + ay +\n                \" L \" + a.x1 + \" \" + ay +\n                \" C \" + a.x1 + \" \" + (ay - ddy) + \" \" + b.x1 + \" \" + (by + ddy) + \" \" + b.x1 + \" \" + by + \" Z\";\n        var flowPath = el(\"path\", {\n          \"class\": \"ghchart__tl-flow\", d: d, fill: colorOf({ id: id }), \"data-stage\": id\n        });\n        svg.appendChild(flowPath);\n        hoverSlot(id).flowEl = flowPath;\n      });\n    }\n\n    \/\/ Rows: segmented bars.\n    rows.forEach(function (r, ri) {\n      var L = rowLayout[ri];\n      if (r.label) {\n        var rl = el(\"text\", {\n          \"class\": \"ghchart__tl-rowlabel\", x: M.left - 20, y: L.top + L.h \/ 2 - 3, \"text-anchor\": \"end\"\n        });\n        rl.textContent = r.label; svg.appendChild(rl);\n        if (r.sublabel) {\n          var rs = el(\"text\", {\n            \"class\": \"ghchart__tl-rowsub\", x: M.left - 20, y: L.top + L.h \/ 2 + 18, \"text-anchor\": \"end\"\n          });\n          rs.textContent = r.sublabel; svg.appendChild(rs);\n        }\n      }\n      if (r.parallel) {\n        var gv = r.gate ? r.gate.value : 0;\n        if (r.gate) {\n          var gateEl = el(\"rect\", {\n            \"class\": \"ghchart__tl-seg\", x: xAt(0), y: L.top,\n            width: Math.max(0, wAt(gv)), height: L.h, rx: SEG_R, fill: colorOf(r.gate),\n            \"data-stage\": r.gate.id\n          });\n          svg.appendChild(gateEl);\n          var gslot = hoverSlot(r.gate.id);\n          gslot.afterEl = gateEl; gslot.after = r.gate.value;\n          gslot.name = gslot.name || nameOf(r.gate);\n        }\n        r.parallel.forEach(function (p, k) {\n          var ly = L.top + k * (LANE_H + LANE_GAP);\n          var laneEl = el(\"rect\", {\n            \"class\": \"ghchart__tl-seg\", x: xAt(gv), y: ly,\n            width: Math.max(1, wAt(p.value)), height: LANE_H, rx: 3, fill: colorOf(p),\n            \"data-stage\": p.id\n          });\n          svg.appendChild(laneEl);\n          var pslot = hoverSlot(p.id);\n          pslot.afterEl = laneEl; pslot.after = p.value;\n          pslot.name = pslot.name || nameOf(p);\n        });\n      } else {\n        var acc = 0;\n        (r.segments || []).forEach(function (s) {\n          var x = xAt(acc), w = Math.max(1, wAt(s.value));\n          var segEl = el(\"rect\", {\n            \"class\": \"ghchart__tl-seg\", x: x, y: L.top, width: w, height: L.h, rx: SEG_R,\n            fill: colorOf(s), \"data-stage\": s.id\n          });\n          svg.appendChild(segEl);\n          var slot = hoverSlot(s.id);\n          slot.name = slot.name || nameOf(s);\n          if (ri === 0) { slot.beforeEl = segEl; slot.before = s.value; }\n          else { slot.afterEl = segEl; slot.after = s.value; }\n          if (s.labelPos === \"inside\") {\n            var ilAttrs = {\n              \"class\": \"ghchart__tl-seg-label\", x: x + w \/ 2, y: L.top + L.h \/ 2,\n              dy: \"0.34em\", \"text-anchor\": \"middle\"\n            };\n            \/\/ Inline style (not a fill attribute) so it overrides the class's\n            \/\/ default white fill \u2014 lets a bright segment take dark label text.\n            if (s.labelColor) ilAttrs.style = \"fill:\" + s.labelColor;\n            var il = el(\"text\", ilAttrs);\n            il.textContent = nameOf(s); svg.appendChild(il);\n          }\n          acc += s.value;\n        });\n      }\n    });\n\n    \/\/ Elbow-leader callouts above narrow segments.\n    rows.forEach(function (r, ri) {\n      if (r.parallel) return;\n      var L = rowLayout[ri], acc = 0;\n      (r.segments || []).forEach(function (s) {\n        var x = xAt(acc), w = wAt(s.value), cx = x + w \/ 2;\n        if (s.labelPos === \"above\") {\n          var tier = s.tier || 0;\n          var tierY = topPad - 14 - tier * CALLOUT_TIER_H;\n          \/\/ labelX pins the label to an absolute data-x (so callouts can be\n          \/\/ left-aligned into a stack); labelDX is a relative nudge otherwise.\n          var lx = s.labelX != null ? xAt(s.labelX) : cx + (s.labelDX || 0);\n          var anchor = s.labelAnchor || \"start\";\n          svg.appendChild(el(\"path\", {\n            \"class\": \"ghchart__tl-callout-line\",\n            d: \"M \" + cx + \" \" + L.top + \" L \" + cx + \" \" + tierY + \" L \" + lx + \" \" + tierY, fill: \"none\"\n          }));\n          var pad = anchor === \"start\" ? 6 : (anchor === \"end\" ? -6 : 0);\n          var cl = el(\"text\", {\n            \"class\": \"ghchart__tl-callout-label\", x: lx + pad, y: tierY,\n            dy: \"0.34em\", \"text-anchor\": anchor, fill: s.labelColor || colorOf(s)\n          });\n          cl.textContent = nameOf(s); svg.appendChild(cl);\n        }\n        acc += s.value;\n      });\n    });\n\n    \/\/ Below brackets (span with up-ticks + label beneath).\n    var belowY = (rowLayout.length ? rowLayout[rowLayout.length - 1].bottom : topPad) + 14;\n    belowBrackets.forEach(function (b, bi) {\n      var x0 = xAt(b.from), x1 = xAt(b.to), yy = belowY + bi * 46;\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: x0, y1: yy, x2: x1, y2: yy }));\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: x0, y1: yy, x2: x0, y2: yy - 7 }));\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: x1, y1: yy, x2: x1, y2: yy - 7 }));\n      var bl = el(\"text\", {\n        \"class\": \"ghchart__bracket-label ghchart__tl-bracket-label\", x: (x0 + x1) \/ 2, y: yy + 22, \"text-anchor\": \"middle\"\n      });\n      bl.textContent = b.text; svg.appendChild(bl);\n    });\n\n    \/\/ Right brackets (vertical span beside a parallel group + label).\n    rightBrackets.forEach(function (b) {\n      var L = rowLayout[b.row || 0];\n      var bx = xAt(b.at != null ? b.at : rowTotal(rows[b.row || 0])) + 14;\n      var y0 = L.top, y1 = L.bottom;\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: bx, y1: y0, x2: bx, y2: y1 }));\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: bx, y1: y0, x2: bx - 7, y2: y0 }));\n      svg.appendChild(el(\"line\", { \"class\": \"ghchart__bracket\", x1: bx, y1: y1, x2: bx - 7, y2: y1 }));\n      \/\/ Label as a DOM node (not SVG text) so it keeps a fixed, readable size\n      \/\/ instead of scaling down with the plot on small windows.\n      var rlabel = html(\"div\", \"ghchart__tl-bracket-label-dom\");\n      rlabel.textContent = b.text;\n      rlabel.style.left = ((bx + 12) \/ VB_W * 100) + \"%\";\n      rlabel.style.top = (((y0 + y1) \/ 2) \/ VB_H * 100) + \"%\";\n      plotwrap.appendChild(rlabel);\n    });\n\n    \/\/ Numeric axis: tick labels + title.\n    if (axis && axis.ticks) {\n      axis.ticks.forEach(function (tv, i) {\n        var gx = xAt(tv);\n        var first = i === 0, last = i === axis.ticks.length - 1;\n        var xl = el(\"text\", {\n          \"class\": \"ghchart__axislabel ghchart__axislabel--x ghchart__tl-tick\",\n          x: gx, y: axisY + 26, \"text-anchor\": first ? \"start\" : (last ? \"end\" : \"middle\")\n        });\n        xl.textContent = axis.format ? axis.format(tv) : String(tv);\n        svg.appendChild(xl);\n      });\n    }\n\n    \/* ---- Hover interaction (before\/after chart) ------------------\n       Emphasise a stage's whole connection (its before block, after block\n       and ribbon) and show a tooltip with the before\/after values. Only\n       when there are flows to connect and hover isn't disabled. *\/\n    if (flows && config.hover !== false) {\n      var unit = config.unit ? \" \" + config.unit : \"\";\n      function fmtHover(v) { return v == null ? \"\\u2013\" : v + unit; }\n\n      var tip = html(\"div\", \"ghchart__tooltip\");\n      tip.setAttribute(\"role\", \"status\");\n      var tipTitle = html(\"div\", \"ghchart__tt-title\");\n      var tipBody = html(\"div\", \"ghchart__tt-body ghchart__tt-body--plain\");\n      tip.appendChild(tipTitle); tip.appendChild(tipBody);\n      root.appendChild(tip);\n\n      var focused = null;\n      function focusStage(id) {\n        focused = id;\n        Object.keys(stageHover).forEach(function (k) {\n          var s = stageHover[k], on = (k === id);\n          if (s.beforeEl) s.beforeEl.style.opacity = on ? \"1\" : \"0.16\";\n          if (s.afterEl) s.afterEl.style.opacity = on ? \"1\" : \"0.16\";\n          if (s.flowEl) s.flowEl.style.opacity = on ? \"0.5\" : \"0.04\";\n        });\n        var st = stageHover[id];\n        tipTitle.textContent = st.name || id;\n        tipBody.textContent = \"\";\n        var lb = html(\"div\", \"ghchart__tt-line\"); lb.textContent = \"Before  \" + fmtHover(st.before);\n        var la = html(\"div\", \"ghchart__tt-line\"); la.textContent = \"After  \" + fmtHover(st.after);\n        tipBody.appendChild(lb); tipBody.appendChild(la);\n      }\n      function clearStage() {\n        focused = null;\n        Object.keys(stageHover).forEach(function (k) {\n          var s = stageHover[k];\n          if (s.beforeEl) s.beforeEl.style.opacity = \"\";\n          if (s.afterEl) s.afterEl.style.opacity = \"\";\n          if (s.flowEl) s.flowEl.style.opacity = \"\";\n        });\n        tip.setAttribute(\"data-visible\", \"false\");\n      }\n      function placeTip(clientX, clientY) {\n        var rr = root.getBoundingClientRect();\n        var left = clientX - rr.left, top = clientY - rr.top - 14;\n        tip.setAttribute(\"data-visible\", \"true\");\n        var halfW = tip.offsetWidth \/ 2;\n        left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);\n        if (top - tip.offsetHeight < 0) {\n          top = clientY - rr.top + 20; tip.style.transform = \"translate(-50%, 0)\";\n        } else tip.style.transform = \"translate(-50%, -100%)\";\n        tip.style.left = left + \"px\"; tip.style.top = top + \"px\";\n      }\n\n      svg.addEventListener(\"pointermove\", function (e) {\n        var t = e.target, id = t && t.getAttribute ? t.getAttribute(\"data-stage\") : null;\n        if (id && stageHover[id]) {\n          if (id !== focused) focusStage(id);\n          placeTip(e.clientX, e.clientY);\n        } else if (focused !== null) clearStage();\n      });\n      svg.addEventListener(\"pointerleave\", clearStage);\n\n      \/\/ Keyboard access: the before blocks are focusable and describe both values.\n      Object.keys(stageHover).forEach(function (k) {\n        var s = stageHover[k];\n        if (!s.beforeEl) return;\n        s.beforeEl.setAttribute(\"tabindex\", \"0\");\n        s.beforeEl.setAttribute(\"role\", \"img\");\n        s.beforeEl.setAttribute(\"aria-label\",\n          (s.name || k) + \": before \" + fmtHover(s.before) + \", after \" + fmtHover(s.after));\n        s.beforeEl.addEventListener(\"focus\", function () {\n          focusStage(k);\n          var rct = s.beforeEl.getBoundingClientRect();\n          placeTip(rct.left + rct.width \/ 2, rct.top);\n        });\n        s.beforeEl.addEventListener(\"blur\", clearStage);\n      });\n    }\n\n    root.appendChild(plotwrap);\n\n    \/\/ Axis title as a DOM caption \u2014 matches the legend's px sizing instead of\n    \/\/ scaling with the SVG. Padded to the plot's left\/right insets so it centres\n    \/\/ under the axis, not the whole card.\n    if (axis && axis.title) {\n      var axcap = html(\"div\", \"ghchart__tl-axis-caption\");\n      axcap.textContent = axis.title;\n      axcap.style.paddingLeft = (M.left \/ 10) + \"%\";\n      axcap.style.paddingRight = (M.right \/ 10) + \"%\";\n      root.appendChild(axcap);\n    }\n  };\n\n  \/* ================================================================\n     Dispatcher \u2014 pick a renderer by config.type and draw into a mount\n     ================================================================ *\/\n  function destroy(root) {\n    var cleanup = renderCleanups.get(root);\n    if (cleanup) { cleanup(); renderCleanups.delete(root); }\n  }\n  function render(config, mountEl) {\n    var root = mountEl || document.getElementById(config.mountId);\n    if (!root) return;\n    destroy(root);\n    root.classList.add(\"ghchart\");\n    root.classList.toggle(\"ghchart--panel-cards\", config.type === \"distribution\" && config.panelCards === true);\n    root.removeAttribute(\"role\");\n    root.removeAttribute(\"aria-label\");\n    \/* Resolve the renderer from the SHARED registry (see registration below),\n       not this bundle's private `renderers`. When several exported charts share\n       one page they all merge into one registry, so every chart finds its\n       renderer even if an older engine bundle loaded first. *\/\n    var reg = (global.GHChartEngine && global.GHChartEngine.renderers) || renderers;\n    var fn = reg[config.type] || renderers[config.type] || reg.line || renderers.line;\n    var nextCleanup = fn(root, config);\n    if (typeof nextCleanup === \"function\") renderCleanups.set(root, nextCleanup);\n  }\n\n  \/* ----------------------------------------------------------------\n     Shared-engine registration (multi-embed safe)\n     ----------------------------------------------------------------\n     Every exported chart.js embeds this whole engine. When two or more\n     charts are placed on the same page (e.g. a WordPress post), we must NOT\n     let the first-loaded bundle silently win \u2014 an older bundle would be\n     missing renderers added later (this is why a heatmap pasted after an\n     older bar chart used to vanish).\n\n     Instead we keep ONE shared engine:\n       \u2022 every bundle merges in any renderers the shared registry lacks;\n       \u2022 the newest bundle (highest ENGINE_VERSION) provides render(), which\n         reads renderers from the shared registry at call time.\n     The result is order-independent and resilient to version skew as new\n     chart types are added. Bump ENGINE_VERSION whenever renderers or render\n     change so a newer embed upgrades an older shared engine in place. *\/\n  var ENGINE_VERSION = 29;\n  var existing = global.GHChartEngine;\n  if (existing && typeof existing.version === \"number\") {\n    if (ENGINE_VERSION > existing.version) {\n      \/* This bundle is newer. Every export embeds the COMPLETE engine, so this\n         bundle's `renderers` is a self-contained, up-to-date set. Adopt it as\n         the shared registry (upgrading older same-named renderers in place),\n         while defensively preserving any renderer types only the older engine\n         happened to have. Then take over render(). *\/\n      for (var ek in existing.renderers) {\n        if (Object.prototype.hasOwnProperty.call(existing.renderers, ek) && !renderers[ek]) {\n          renderers[ek] = existing.renderers[ek];\n        }\n      }\n      existing.renderers = renderers;\n      existing.render = render;\n      existing.destroy = destroy;\n      existing.version = ENGINE_VERSION;\n      existing._renderCleanups = renderCleanups;\n    } else {\n      \/* Same or older version already present: only contribute renderer types\n         the shared registry is missing (don't clobber an equal\/newer engine). *\/\n      for (var rk in renderers) {\n        if (Object.prototype.hasOwnProperty.call(renderers, rk) && !existing.renderers[rk]) {\n          existing.renderers[rk] = renderers[rk];\n        }\n      }\n    }\n  } else {\n    \/* No engine yet, or a pre-versioning (old-style) engine loaded first.\n       Take over, preserving any renderers the old-style engine registered. *\/\n    if (existing && existing.renderers) {\n      for (var ok in existing.renderers) {\n        if (Object.prototype.hasOwnProperty.call(existing.renderers, ok) && !renderers[ok]) {\n          renderers[ok] = existing.renderers[ok];\n        }\n      }\n    }\n    global.GHChartEngine = { renderers: renderers, render: render, destroy: destroy, version: ENGINE_VERSION, _renderCleanups: renderCleanups };\n  }\n})(typeof window !== \"undefined\" ? window : this);\n\n;(function () {\n  \"use strict\";\n  var CONFIG = {\n    mountId: \"gh-chart-git-events-by-percentile\",\n    type: \"line\",\n    title: \"Git events per repository\",\n    subtitle: \"By percentile, August 2026. The y-axis uses a logarithmic scale: each gridline is 10\u00d7 the one below.\",\n    labels: [\"P25\", \"P50\", \"P75\", \"P99\", \"P99.9\", \"P99.99\", \"P100\"],\n    curve: \"linear\",\n    series: [\n      {\n        name: \"Fetches\",\n        color: \"#3094ff\",\n        width: 3.25,\n        values: [1, 2, 8, 739, 10534, 214934, 514300000]\n      },\n      {\n        name: \"Pushes\",\n        color: \"#2da44f\",\n        width: 3.25,\n        values: [2, 4, 15, 802, 7502, 23758, 2970000]\n      },\n      {\n        name: \"Clones\",\n        color: \"#f0883e\",\n        width: 3.25,\n        values: [1, 3, 5, 283, 5619, 44691, 279300000]\n      },\n      {\n        name: \"Requests (all)\",\n        color: \"#8250df\",\n        width: 3.25,\n        values: [15, 25, 53, 7738, 80978, 570771, 1001000000]\n      }\n    ],\n    yAxis: {\n      scale: \"log\",\n      min: 1,\n      max: 1000000000,\n      ticks: [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000],\n      title: \"Events\"\n    },\n    formatAxis: function (v) {\n        if (v >= 1e9) return v \/ 1e9 + \"B\";\n        if (v >= 1e6) return v \/ 1e6 + \"M\";\n        if (v >= 1e3) return v \/ 1e3 + \"K\";\n        return String(v);\n      },\n    formatValue: function (v) {\n        if (v >= 1e9) return parseFloat((v \/ 1e9).toFixed(3)) + \"B\";\n        if (v >= 1e6) return parseFloat((v \/ 1e6).toFixed(3)) + \"M\";\n        return v.toLocaleString(\"en-US\");\n      }\n  };\n  function boot() {\n    var root = document.getElementById(CONFIG.mountId);\n    if (!root || root.getAttribute(\"data-gh-rendered\")) return;\n    root.setAttribute(\"data-gh-rendered\", \"1\");\n    window.GHChartEngine.render(CONFIG);\n  }\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", boot);\n  } else {\n    boot();\n  }\n})();\n<\/script>\n\n<div id=\"gh-chart-git-events-by-percentile\" class=\"ghchart\"><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Repository activity climbs sharply at the far end of the distribution. The busiest repository on GitHub saw roughly a billion requests in August.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Beyond these highest-volume workloads, total Git activity on GitHub is also growing rapidly: between September 2025 and August 2026, it increased to more than 2x its previous level, from 218.2 billion events per month to 473.3 billion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In September alone, developers and agents made 7.38 billion commits on GitHub, more than five times as many as a year earlier.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The repositories at the top of this curve show what agentic development looks like at its leading edge: large engineering teams running busy CI pipelines alongside growing fleets of agents. Supporting these teams means building Git infrastructure for sustained, concurrent reads and writes at a scale few repositories reach today. We&rsquo;re investing deeply in Git infrastructure to meet the demands of agentic software development and give teams a foundation built for their most ambitious workloads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Building for this scale means addressing several architectural challenges:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Commit turnaround becomes a bottleneck per agent.<\/strong> An agent in a tight loop commits or checkpoints after nearly every action. Its speed is bounded by how fast a single push completes, so latency that a human would never notice becomes the limiting factor.<\/li>\n\n\n\n<li><strong>Write throughput demand is increasing by orders of magnitude.<\/strong> Pushes grew 4.9x year over year, from 0.69 billion to 3.35 billion per month. Thousands of agents working on their own branches in one repository produce a sustained write rate that converges on a single point in our architecture.<\/li>\n\n\n\n<li><strong>Merges contend on one reference.<\/strong> Trunk-based development, release trains, and merge queues funnel all that work onto a single ref that has to absorb every merge.&nbsp;Pull request merges on GitHub grew to nearly 4x their volume a year ago.<\/li>\n\n\n\n<li><strong>Each push multiplies into thousands of reads.<\/strong>&nbsp;For example, CI and code scanning clone or fetch the same branch tip thousands of times per minute, and that fan-out has to be cheap.&nbsp;GitHub Actions alone ran 3.26 billion times in September, more than 4x as many as a year ago.<\/li>\n\n\n\n<li><strong>Operations on a repository must continue to be fast.<\/strong> To keep them fast, we continually compact repository data and clean up objects that are no longer needed. Every new write adds to that work, and the cost compounds as volume climbs.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This is why fast clones only solve part of the problem. Reads are relatively easy to scale: add caches, add replicas, and serve the same bytes to more clients. Scaling reads is essential, but these workloads require more than that. Writes are way harder. Every push has to be stored durably and made visible consistently before the next agent or CI job can build on it.<\/p>\n\n\n\n<h2 id=\"h-where-today-s-architecture-meets-new-demands\" class=\"wp-block-heading\">Where today&rsquo;s architecture meets new demands<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The current architecture has served developers well for years. Every repository is stored by <a href=\"https:\/\/github.blog\/engineering\/infrastructure\/stretching-spokes\/\">Spokes<\/a>, which keeps a full copy on the local disks of several fileservers, five by default. Those fast local disks let Git operations read native repository data with low latency, and the extra copies provide redundancy while spreading reads across fileservers. When a push updates a reference, a three-phase commit protocol uses a quorum to ensure that CI, the web UI, and API clients see a consistent repository state. That pairing serves a billion repositories today.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, the mechanism we use for durability is the same one we use for scale. The copies on disk are the source of truth, so adding read capacity means adding another durable replica. Every replica participates in every write, so a push is only as fast as the slowest replica in its set. The net effect: adding replicas to absorb read load makes writes slower.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For most repositories, this tradeoff works well. At the highest activity levels, it becomes a ceiling: adding read replicas adds overhead to writes, losing a replica reduces read capacity, and losing quorum stops writes entirely. To meet agent-first demands, we need to separate durability from scale without losing what teams rely on today.<\/p>\n\n\n\n<h2 id=\"h-built-for-the-busiest-better-for-everyone\" class=\"wp-block-heading\">Built for the busiest, better for everyone<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We&rsquo;re rebuilding the infrastructure while GitHub keeps running. There&rsquo;s no maintenance window where the world&rsquo;s code stops moving, and no version of this work where we ask people to change how they build software while we do it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We&rsquo;re building for the most demanding workloads on GitHub: an enterprise shipping under strict regulatory requirements, a team landing a change across a repo that builds an operating system, and an organization running thousands of agents against a single codebase. Engineering for that scale raises the floor for everyone. The maintainer reviewing contributions from volunteers across time zones and the student opening their first pull request get the same faster, more resilient foundation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The new architecture also must preserve the controls teams already operate on. A maintainer needs branch protections and required reviews so an unreviewed change never reaches the default branch. A security team needs audit logs and repository visibility to investigate a suspicious access event. An on-call engineer needs dependable automation and enough observability to understand why a deployment failed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the platform to keep serving everyone here while it scales for the busiest workloads, these are our guiding principles:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Build on the workflows developers already trust.<\/strong> Teams rely on workflows like branching, review, merge, and history to build, ship, and govern software at scale. Our new infrastructure is designed to support those same workflows at much higher volumes of activity.<\/li>\n\n\n\n<li><strong>Put reliability first.<\/strong> We measure every decision against the reliability that developers and organizations require. Confidence in the platform is what lets an engineering organization build automation, ship on a schedule, meet compliance obligations, and understand the software it produces. This work will meaningfully improve throughput and scale, and those gains extend a foundation of trust that&rsquo;s already there.<\/li>\n\n\n\n<li><strong>Keep people in control of their code.<\/strong> If the system isn&rsquo;t helping the people and organizations who use it, and isn&rsquo;t under their control, it isn&rsquo;t worth building. As agents take on more of the work, the people who own the code can still review, understand, and approve it.<\/li>\n<\/ul>\n\n\n\n<h2 id=\"h-the-approach\" class=\"wp-block-heading\">The approach<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We are building a new GitHub architecture that can scale much more effectively. Our approach centers around core distributed systems design tenets, applied to the concurrency and scale of agentic software development. Our goal is to continue the forward momentum for open-source communities and enterprises around the world who have built their projects with Git and GitHub, while preserving and adapting the features and controls around it to meet the new needs of the agentic era.<\/p>\n\n\n\n<h3 id=\"h-minimize-coordination\" class=\"wp-block-heading\">Minimize coordination<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A repository that receives many pushes must accept and publish updates quickly. Coordination is valuable when it protects correctness, but too much of it limits write throughput and can turn a busy repository into a bottleneck. Our current architecture is tightly coupled in places it doesn&rsquo;t need to be, which limits our ability to scale across reads and writes without tough trade-offs. We&rsquo;re redesigning the system to preserve the coordination that Git semantics require and let everything else proceed independently.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Coordinate only what needs agreement.<\/strong> The part of a push that truly needs agreement is the reference update itself. Storing the underlying objects, validating object connectivity, and secret scanning are much more work, but most of it can happen in parallel to other writes. That shrinks the critical path of a push to the small step that needs coordination, so the rest of the work no longer delays the acknowledgment.<\/li>\n\n\n\n<li><strong>Move maintenance off the serving path.<\/strong> Compaction and garbage collection are among the heaviest work a repository does, and today they run on the same hosts that answer live Git requests. In the new architecture, separate workers handle maintenance directly against durable storage. A busy repository can be optimized continuously in the background without slowing pushes and fetches.<\/li>\n<\/ul>\n\n\n\n<h3 id=\"h-decouple-storage-from-compute\" class=\"wp-block-heading\">Decouple storage from compute<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Today, complete repository copies on local disks serve both as durable storage and as the layer that answers Git requests. Separating the two lets us scale each one independently.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Scale reads without adding durable copies.<\/strong> In the new architecture, read capacity comes from lightweight workers that cache data to serve requests. The authoritative copy of the repository lives in a durable storage layer underneath. That way, the platform can absorb large read spikes from CI fan-out, agent fleets, and large clones without adding work to every push.<\/li>\n\n\n\n<li><strong>Let each layer do one job.<\/strong> Authoritative repository data lives in Azure Blob Storage, which already provides durability and replication at Azure scale. The compute layer is optimized for throughput at the lowest latency.<\/li>\n\n\n\n<li><strong>Recover faster from failures.<\/strong> When storage and compute are coupled, losing a host reduces both capacity and durability, and recovery means rebuilding a full repository copy. When they&rsquo;re separate, losing a compute worker is closer to a cache miss: a replacement worker can start serving requests right away and fill its cache from durable storage as traffic arrives.<\/li>\n\n\n\n<li><strong>Match capacity to demand.<\/strong> Compute workers can be added or removed as traffic changes instead of provisioning for peak load in advance. A repository going through a burst of activity, like a release or a new agent fleet coming online, can get extra capacity for the burst. Once it passes, that capacity goes away.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Together, these tenets allow us to support higher throughput and more concurrent work without abandoning the reliability and controls that our users need.<\/p>\n\n\n\n<h2 id=\"h-what-comes-next\" class=\"wp-block-heading\">What comes next<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We&rsquo;re building an architecture designed to provide the highest throughput and reliability available. Reads and writes scale independently, and the system recovers gracefully from failures. In internal benchmarks, it has delivered up to 35 times higher write throughput, with read capacity that scales on its own to meet demand.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As automated development increases the frequency and concurrency of software change, GitHub will evolve its foundations without trading away the governance and control that teams rely on. We&rsquo;re already putting that foundation in place. In the next post in this series, we&rsquo;ll dive deeper into our future architecture and the journey that led us there.<\/p>\n<\/body><\/html>\n","protected":false},"excerpt":{"rendered":"<p>We&#8217;re rebuilding GitHub&#8217;s Git infrastructure while GitHub keeps running, creating a foundation for agent-scale software development.<\/p>\n","protected":false},"author":2501,"featured_media":93161,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_gh_post_show_toc":"yes","_gh_post_is_no_robots":"","_gh_post_is_featured":"yes","_gh_post_is_excluded":"","_gh_post_is_unlisted":"","_gh_post_related_link_1":"","_gh_post_related_link_2":"","_gh_post_related_link_3":"","_gh_post_sq_img":"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo-1-e1767926364316.png","_gh_post_sq_img_id":"93193","_gh_post_cta_title":"","_gh_post_cta_text":"","_gh_post_cta_link":"","_gh_post_cta_button":"","_gh_post_recirc_hide":"","_gh_post_recirc_col_1":"","_gh_post_recirc_col_2":"","_gh_post_recirc_col_3":"","_gh_post_recirc_col_4":"","_featured_video":"","_gh_post_additional_query_params":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":true,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"{title}\n\n{excerpt}\n\n{url}","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"_wpas_customize_per_network":false,"jetpack_post_was_ever_published":false,"_links_to":"","_links_to_target":""},"categories":[3307,72],"tags":[132,3212,3926],"coauthors":[3933],"class_list":["post-99275","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-architecture-optimization","category-engineering","tag-git","tag-infrastructure","tag-technical-deep-dive"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v28.6 (Yoast SEO v28.6) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Building Git infrastructure for agent-scale development - The GitHub Blog<\/title>\n<meta name=\"description\" content=\"Writes are accelerating, and this growth can add stress to systems all repos depend on. Here&#039;s our approach to building architecture that can scale.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building Git infrastructure for agent-scale development\" \/>\n<meta property=\"og:description\" content=\"Writes are accelerating, and this growth can add stress to systems all repos depend on. Here&#039;s our approach to building architecture that can scale.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/\" \/>\n<meta property=\"og:site_name\" content=\"The GitHub Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-06T20:57:56+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-07T15:19:50+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1920\" \/>\n\t<meta property=\"og:image:height\" content=\"1080\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Brian Celenza\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Brian Celenza\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"9 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/\"},\"author\":{\"name\":\"Brian Celenza\",\"@id\":\"https:\\\/\\\/github.blog\\\/#\\\/schema\\\/person\\\/a90dfb1921df0a579971ea8faf399c96\"},\"headline\":\"Building Git infrastructure for agent-scale development\",\"datePublished\":\"2026-10-06T20:57:56+00:00\",\"dateModified\":\"2026-10-07T15:19:50+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/\"},\"wordCount\":1827,\"image\":{\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2026\\\/01\\\/generic-invertocat-github-logo.png?fit=1920%2C1080\",\"keywords\":[\"Git\",\"Infrastructure\",\"technical deep dive\"],\"articleSection\":[\"Architecture &amp; optimization\",\"Engineering\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/\",\"url\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/\",\"name\":\"Building Git infrastructure for agent-scale development - The GitHub Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/github.blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2026\\\/01\\\/generic-invertocat-github-logo.png?fit=1920%2C1080\",\"datePublished\":\"2026-10-06T20:57:56+00:00\",\"dateModified\":\"2026-10-07T15:19:50+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/github.blog\\\/#\\\/schema\\\/person\\\/a90dfb1921df0a579971ea8faf399c96\"},\"description\":\"Writes are accelerating, and this growth can add stress to systems all repos depend on. Here's our approach to building architecture that can scale.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/#primaryimage\",\"url\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2026\\\/01\\\/generic-invertocat-github-logo.png?fit=1920%2C1080\",\"contentUrl\":\"https:\\\/\\\/github.blog\\\/wp-content\\\/uploads\\\/2026\\\/01\\\/generic-invertocat-github-logo.png?fit=1920%2C1080\",\"width\":1920,\"height\":1080,\"caption\":\"Image featuring the GitHub invertocat logo displayed on a floating cube against a decorative background.\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/building-git-infrastructure-for-agent-scale-development\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/github.blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Engineering\",\"item\":\"https:\\\/\\\/github.blog\\\/engineering\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Architecture &amp; optimization\",\"item\":\"https:\\\/\\\/github.blog\\\/engineering\\\/architecture-optimization\\\/\"},{\"@type\":\"ListItem\",\"position\":4,\"name\":\"Building Git infrastructure for agent-scale development\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/github.blog\\\/#website\",\"url\":\"https:\\\/\\\/github.blog\\\/\",\"name\":\"The GitHub Blog\",\"description\":\"Updates, ideas, and inspiration from GitHub to help developers build and design software.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/github.blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/github.blog\\\/#\\\/schema\\\/person\\\/a90dfb1921df0a579971ea8faf399c96\",\"name\":\"Brian Celenza\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/ae4c35520b696d588a6bdc8ead3d29f49bb7d2dab6a0ce8e08406616c53e64cf?s=96&d=mm&r=g829ff8d00be9f4770512e4845c0b44b0\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/ae4c35520b696d588a6bdc8ead3d29f49bb7d2dab6a0ce8e08406616c53e64cf?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/ae4c35520b696d588a6bdc8ead3d29f49bb7d2dab6a0ce8e08406616c53e64cf?s=96&d=mm&r=g\",\"caption\":\"Brian Celenza\"},\"description\":\"Brian is a Principal Software Engineer working on the storage and core services that power all repository interactions on GitHub.\",\"url\":\"https:\\\/\\\/github.blog\\\/author\\\/bcelenza\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Building Git infrastructure for agent-scale development - The GitHub Blog","description":"Writes are accelerating, and this growth can add stress to systems all repos depend on. Here's our approach to building architecture that can scale.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/","og_locale":"en_US","og_type":"article","og_title":"Building Git infrastructure for agent-scale development","og_description":"Writes are accelerating, and this growth can add stress to systems all repos depend on. Here's our approach to building architecture that can scale.","og_url":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/","og_site_name":"The GitHub Blog","article_published_time":"2026-10-06T20:57:56+00:00","article_modified_time":"2026-10-07T15:19:50+00:00","og_image":[{"width":1920,"height":1080,"url":"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo.png","type":"image\/png"}],"author":"Brian Celenza","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Brian Celenza","Est. reading time":"9 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/#article","isPartOf":{"@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/"},"author":{"name":"Brian Celenza","@id":"https:\/\/github.blog\/#\/schema\/person\/a90dfb1921df0a579971ea8faf399c96"},"headline":"Building Git infrastructure for agent-scale development","datePublished":"2026-10-06T20:57:56+00:00","dateModified":"2026-10-07T15:19:50+00:00","mainEntityOfPage":{"@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/"},"wordCount":1827,"image":{"@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/#primaryimage"},"thumbnailUrl":"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo.png?fit=1920%2C1080","keywords":["Git","Infrastructure","technical deep dive"],"articleSection":["Architecture &amp; optimization","Engineering"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/","url":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/","name":"Building Git infrastructure for agent-scale development - The GitHub Blog","isPartOf":{"@id":"https:\/\/github.blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/#primaryimage"},"image":{"@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/#primaryimage"},"thumbnailUrl":"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo.png?fit=1920%2C1080","datePublished":"2026-10-06T20:57:56+00:00","dateModified":"2026-10-07T15:19:50+00:00","author":{"@id":"https:\/\/github.blog\/#\/schema\/person\/a90dfb1921df0a579971ea8faf399c96"},"description":"Writes are accelerating, and this growth can add stress to systems all repos depend on. Here's our approach to building architecture that can scale.","breadcrumb":{"@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/#primaryimage","url":"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo.png?fit=1920%2C1080","contentUrl":"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo.png?fit=1920%2C1080","width":1920,"height":1080,"caption":"Image featuring the GitHub invertocat logo displayed on a floating cube against a decorative background."},{"@type":"BreadcrumbList","@id":"https:\/\/github.blog\/engineering\/architecture-optimization\/building-git-infrastructure-for-agent-scale-development\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/github.blog\/"},{"@type":"ListItem","position":2,"name":"Engineering","item":"https:\/\/github.blog\/engineering\/"},{"@type":"ListItem","position":3,"name":"Architecture &amp; optimization","item":"https:\/\/github.blog\/engineering\/architecture-optimization\/"},{"@type":"ListItem","position":4,"name":"Building Git infrastructure for agent-scale development"}]},{"@type":"WebSite","@id":"https:\/\/github.blog\/#website","url":"https:\/\/github.blog\/","name":"The GitHub Blog","description":"Updates, ideas, and inspiration from GitHub to help developers build and design software.","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/github.blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/github.blog\/#\/schema\/person\/a90dfb1921df0a579971ea8faf399c96","name":"Brian Celenza","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/ae4c35520b696d588a6bdc8ead3d29f49bb7d2dab6a0ce8e08406616c53e64cf?s=96&d=mm&r=g829ff8d00be9f4770512e4845c0b44b0","url":"https:\/\/secure.gravatar.com\/avatar\/ae4c35520b696d588a6bdc8ead3d29f49bb7d2dab6a0ce8e08406616c53e64cf?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/ae4c35520b696d588a6bdc8ead3d29f49bb7d2dab6a0ce8e08406616c53e64cf?s=96&d=mm&r=g","caption":"Brian Celenza"},"description":"Brian is a Principal Software Engineer working on the storage and core services that power all repository interactions on GitHub.","url":"https:\/\/github.blog\/author\/bcelenza\/"}]}},"jetpack_publicize_connections":[],"jetpack_shortlink":"https:\/\/wp.me\/pamS32-pPd","jetpack_sharing_enabled":true,"jetpack_featured_media_url":"https:\/\/github.blog\/wp-content\/uploads\/2026\/01\/generic-invertocat-github-logo.png?fit=1920%2C1080","_links":{"self":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts\/99275","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/users\/2501"}],"replies":[{"embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/comments?post=99275"}],"version-history":[{"count":10,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts\/99275\/revisions"}],"predecessor-version":[{"id":99323,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/posts\/99275\/revisions\/99323"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/media\/93161"}],"wp:attachment":[{"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/media?parent=99275"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/categories?post=99275"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/tags?post=99275"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/github.blog\/wp-json\/wp\/v2\/coauthors?post=99275"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}