<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>The GitHub Blog</title>
	<atom:link href="https://github.blog/feed/" rel="self" type="application/rss+xml" />
	<link>https://github.blog/</link>
	<description>Updates, ideas, and inspiration from GitHub to help developers build and design software.</description>
	<lastBuildDate>Fri, 11 Sep 2026 18:26:13 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>

<image>
	<url>https://github.blog/wp-content/uploads/2019/01/cropped-github-favicon-512.png?fit=32%2C32</url>
	<title>The GitHub Blog</title>
	<link>https://github.blog/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">153214340</site>	<item>
		<title>Marketing ops as code: Automating events from planning to follow-up on GitHub</title>
		<link>https://github.blog/ai-and-ml/github-copilot/marketing-ops-as-code-automating-events-from-planning-to-follow-up-on-github/</link>
		
		<dc:creator><![CDATA[Tomoko Tanaka]]></dc:creator>
		<pubDate>Fri, 11 Sep 2026 18:26:10 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[automation]]></category>
		<category><![CDATA[GitHub Actions]]></category>
		<category><![CDATA[GitHub Issues]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98799</guid>

					<description><![CDATA[<p>If you can write down how you do your work, you can automate it. Here's what I did to support GitHub's APAC marketing team.</p>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/marketing-ops-as-code-automating-events-from-planning-to-follow-up-on-github/">Marketing ops as code: Automating events from planning to follow-up on GitHub</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">I run marketing for GitHub in Japan and Korea, and events are the heartbeat of it: a recurring webinar series for enterprise developers, community meetups in Tokyo, invite-only executive sessions in Seoul. What does a developer in this market actually need right now? Which topics are worth an hour of their time, and who should be in the room? I&rsquo;d happily spend all day on those questions.</p>



<p class="wp-block-paragraph">What follows the decisions is another matter. Once an event is greenlit, a fixed sequence begins:</p>



<ul class="wp-block-list">
<li>Duplicate a landing page on our event platform.</li>



<li>Generate a set of UTM-tagged links: one for each channel, each formatted just so.</li>



<li>Draft the invitation email and file a request with the team that sends it.</li>



<li>Add the event to two project boards.</li>



<li>Every morning until the event: download the registrant list, clean it up, post a status update for stakeholders.</li>



<li>After the event: export the attendees, reshape the list for a CRM upload, tag the right records, and write a report.</li>
</ul>



<p class="wp-block-paragraph">While none of these tasks are hard on their own, they&rsquo;re an opportunity to paste the wrong link, skip a day, or misspell a campaign name that 15 downstream reports depend on.</p>



<p class="wp-block-paragraph">Here&rsquo;s the thing: I used to be an engineer. My first career was keeping databases alive on Linux servers for enterprise customers. While my coding may be rusty, I can still see a pipeline begging to be automated. This is where I put <a href="https://github.com/features/copilot">GitHub Copilot</a> to use and where you could too in your own work.</p>



<p class="wp-block-paragraph">So I didn&rsquo;t write the code. I wrote down my runbooks, handed them to GitHub Copilot, and grew the automation in conversation. Today, an event I used to assemble by hand over a couple of days sets itself up from a single GitHub Issue, screens its own registrants every morning, and cleans up after itself when it&rsquo;s over.</p>



<p class="wp-block-paragraph">This post walks through how that works, and why I think anyone whose job involves repetitive work across tools that offer any scriptable way in (an API, or even just a CLI) can do the same.</p>



<h2 id="h-an-event-is-an-issue" class="wp-block-heading">An event is an issue</h2>



<p class="wp-block-paragraph">I can&rsquo;t claim the foundational idea as my own. Marketing teams at GitHub already had a habit of opening one GitHub Issue per project. This becomes the place where the plan, the discussion, and the status live together. The issue was already our unit of work. What I did was make the issue do the work.</p>



<p class="wp-block-paragraph">Three GitHub primitives carry the whole system:</p>



<ul class="wp-block-list">
<li><strong>Issue forms are the application form.</strong> Instead of a blank text box, an <a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms">issue form</a> presents structured fields: event title, date, region, campaign name, target audience. We have one form per event type, such as webinars and in-person events, and they feed the same machinery.</li>



<li><strong>Labels are the switches.</strong> A label like <code>event-setup</code> isn&rsquo;t a tag, it&rsquo;s a trigger. Each automation workflow starts with a condition that says, in effect, &ldquo;only run when this label is present.&rdquo;</li>



<li><strong>Actions are the machinery.</strong> <a href="https://docs.github.com/en/actions">GitHub Actions</a> workflows fire when labels land, parse the form fields out of the issue body, and go do the work.</li>
</ul>



<p class="wp-block-paragraph">Everything a repository gives developers, it gave my marketing workflow for free: history, visibility, review, and a URL for every decision.</p>



<p class="wp-block-paragraph">One thing made this possible, and it has nothing to do with events specifically: <strong>our event management platform exposes an API</strong>. Our CRM doesn&rsquo;t even need one; its official CLI covers everything we do, and I never configured an API key for it, because the CLI signs in through the browser and handles authentication from there. API or CLI, the requirement is the same: a scriptable way in. If your repetitive work runs through a tool that offers either an event platform, a CRM, a form builder, an analytics service, the pattern in this post applies to you.</p>



<p class="wp-block-paragraph">A developer reading this may already be composing the obvious objection: isn&rsquo;t this reinventing the wheel? Marketing automation platforms exist, and a good one might have covered some of this out of the box. But APAC is less one market than a collection of very different ones, and even within my own team, workflows shift with each sub-region and each segment. The same webinar might run in Japanese for Tokyo one month and in Korean for Seoul the next, with different segments, different fields in the CRM, and a different definition of a good lead. Getting a packaged tool to absorb all of those variations means customization budgets, consulting hours, and waiting on someone else&rsquo;s roadmap. Building it ourselves, from the tools already at hand, means a workflow change is a pull request: I describe what I want, a reviewer checks it, and it lands on the main branch through exactly the process developers use to change software.</p>



<h2 id="h-planning-an-event-is-a-conversation" class="wp-block-heading">Planning an event is a conversation</h2>



<p class="wp-block-paragraph">The pipeline starts before the Issue exists. I open GitHub Copilot and say, roughly: &ldquo;I want to run a webinar about AI-assisted development in November.&rdquo;</p>



<p class="wp-block-paragraph">What happens next is shaped by a file called <code>AGENTS.md</code> at the root of our repository. It&rsquo;s our team runbook, written in plain Markdown, that defines how we name campaigns, how fiscal quarters map to dates, which time zone each region uses, and what a good invitation email looks like. GitHub Copilot reads it, and then, it finds a similar past event, proposes a campaign name that follows our naming rules, drafts two versions of the invitation email, and asks me the questions the runbook says to ask.</p>



<p class="wp-block-paragraph">Putting a conversation at the front of the pipeline was itself a design decision, and it solved two problems at once. Automate everything, and you lose flexibility; the day you want this one event to be slightly different, a rigid pipeline has no place to say so. But if you let humans fill in everything, you get mistakes. The conversation sits exactly between the two. GitHub Copilot follows the template, so the data that lands in the Issue is the right data in the right format. And because it&rsquo;s a conversation, I can bend the details for this one event without breaking the machinery downstream.</p>



<p class="wp-block-paragraph">When we started, this conversation happened in <a href="https://github.com/features/copilot/cli">GitHub Copilot CLI</a>, in a terminal. That was fine for me, but &ldquo;open a terminal&rdquo; is a barrier for many people I&rsquo;d love to bring into this workflow. With the <a href="https://github.com/features/ai/github-app">GitHub Copilot app</a>, the same conversation now happens in a regular desktop window. The barrier to entry dropped from &ldquo;comfortable with a shell&rdquo; to &ldquo;can type.&rdquo;</p>



<p class="wp-block-paragraph">I want to be precise about the division of labor, because it&rsquo;s the whole point: <strong>GitHub Copilot drafts; I decide</strong>. Every campaign name, every email subject line, every date gets my sign-off before anything moves. At the end of the conversation, GitHub Copilot files the GitHub Issue with the right labels, and that&rsquo;s when the machines take over.</p>



<h2 id="h-one-label-one-event-fully-staged" class="wp-block-heading">One label, one event, fully staged</h2>



<p class="wp-block-paragraph">The moment the <code>event-setup</code> label lands on the issue, a GitHub Actions workflow picks it up and does, in a few minutes, what used to take me the better part of a day:</p>



<ul class="wp-block-list">
<li>Duplicates a past event on our event platform to create the new landing page</li>



<li>Generates the full set of UTM-tagged URLs: one per channel, consistently formatted, every time</li>



<li>Produces the invitation email as a Word document and commits it to the repository</li>



<li>Opens request Issues with the teams that send emails and track regional marketing</li>



<li>Adds the event to our project boards and fills in the fields</li>



<li>Posts a summary comment back on the Issue, so the next human who opens it sees everything in one place</li>
</ul>



<p class="wp-block-paragraph">Registration screening runs on a schedule instead of a label. Every morning, a cron-triggered workflow fetches the latest registrants for every open event and shares the cleaned-up list. For invite-only events, it also screens the waitlist against our criteria (is this registrant a developer at an enterprise account, a student, or a competitor who would very much like to attend our executive briefing?) before anyone gets approved.</p>



<p class="wp-block-paragraph">The design decision I&rsquo;m most proud of is a single on/off switch called <code>DRY_RUN</code>, stored as a setting (in GitHub terms, a repository variable) that every workflow checks before it runs. Flip it on, and every workflow goes through the motions without touching any external system: no landing pages created, no issues filed in other repositories, no lists shared. When you&rsquo;re a team of marketers automating your own job, you need a way to rehearse. <code>DRY_RUN</code> is the rehearsal switch, and it&rsquo;s the reason I was never afraid to experiment.</p>



<h2 id="h-after-the-event-a-slash-command" class="wp-block-heading">After the event, a slash command</h2>



<p class="wp-block-paragraph">Post-event work used to be the worst part: exporting attendees, reformatting columns for the CRM upload, matching company names against account records, and writing the report. Now it&rsquo;s two commands.</p>



<p class="wp-block-paragraph"><code>/lead-upload</code> fetches the attendee list, shapes it into the exact format our marketing operations team needs for a CRM upload, files the request Issue, and closes out the tracking issues. <code>/event-report</code> pulls attendance metrics and survey results and posts a report as a comment on the event&rsquo;s Issue, back to the one URL where everything about this event lives.</p>



<p class="wp-block-paragraph">These are <a href="https://docs.github.com/copilot">GitHub Copilot agent skills</a>, and here is the part I most want you to hear: a skill is a Markdown file. Each one is a <code>SKILL.md</code>: a written procedure, in prose, that tells GitHub Copilot what to do, in what order, and what to watch out for. Mine read like the runbooks I used to keep in my head, because that&rsquo;s what they are.</p>



<p class="wp-block-paragraph">If you can write a runbook, you can write a skill.</p>



<p class="wp-block-paragraph">Skills are also what keeps the system flexible. No two markets in the Asia/Pacific (APAC) region run their follow-up identically. Audiences differ, segments differ, local conventions differ, and a hard-coded workflow would force every market into one shape. A procedure written in Markdown is flexible: each market can adapt the runbook to its own reality without touching the machinery underneath. That is precisely why the post-event steps live in GitHub Copilot skills rather than in fixed pipelines.</p>



<p class="wp-block-paragraph">We treat skills like code in one respect: new ones arrive by pull request and get reviewed before they&rsquo;re merged, with a <code>CODEOWNERS</code> file routing the review to a maintainer. Marketing automation with an approval process. The governance came free with the platform, too.</p>



<h2 class="wp-block-heading" id="built-in-guardrails-let-me-experiment">Built-in guardrails let me experiment</h2>



<p class="wp-block-paragraph">I automated a workflow that touches customer data and API credentials, in a repository my whole team can see, while barely writing code myself. Six months ago I would have said that combination was reckless. What changed my mind was realizing how many guardrails were already in place before I showed up.</p>



<p class="wp-block-paragraph">Some guardrails I built: the <code>DRY_RUN</code> switch, a test suite that runs on every pull request, and code review for every change. Standard developer habits. It turns out they protect marketing work just as well as they protect software.</p>



<p class="wp-block-paragraph">But the guardrails that mattered most came with the platform:</p>



<ul class="wp-block-list">
<li><strong>Secret scanning with push protection.</strong> The nightmare scenario for someone in my position is committing an API token by accident. GitHub&rsquo;s <a href="https://docs.github.com/code-security/secret-scanning/introduction/about-push-protection">push protection</a> blocks the push before the secret ever lands in the repository, and for GitHub&rsquo;s own tokens, even one that does slip through gets revoked automatically.</li>



<li><strong>GitHub Copilot&rsquo;s data policies.</strong> Registrant lists are business data, and the fixed scripts handle them in fixed ways. But real work is never entirely fixed; some days I need a one-off cut of the data that no script anticipates. Because GitHub Copilot&rsquo;s business plans don&rsquo;t retain prompts or use them to train models, I could ask for that one-off analysis directly, instead of doing what people all over the industry quietly do: pasting business data into whatever consumer chatbot is open in the next tab. The same goes for the models themselves: which ones I can use at all is set by organization policy, not left to my personal judgment, so even a one-off experiment runs inside boundaries the company has already decided. The safe path and the easy path were, for once, the same path.</li>
</ul>



<p class="wp-block-paragraph">One more thing the skills made possible, almost as a side effect: because each procedure is now a named, fixed unit of work, I can match the model to the job, choosing from the lineup our organization has approved. A fast, inexpensive model handles the daily list-cleaning; a stronger one drafts campaign copy.</p>



<p class="wp-block-paragraph">And one honest failure, so you don&rsquo;t repeat it: the morning screening workflow once failed silently for five days before anyone noticed the lists had gone stale. Automation you don&rsquo;t monitor is more of a time bomb with a delay. Give every scheduled workflow a way to complain loudly so you don&rsquo;t miss it.</p>



<h2 class="wp-block-heading" id="start-with-one-task">Start with one task</h2>



<p class="wp-block-paragraph">Here&rsquo;s what I&rsquo;d suggest, from one recovering manual-worker to another.</p>



<p class="wp-block-paragraph">Pick the single most repetitive task in your week. Then check whether the tools it touches have an API or a CLI. You may be surprised how many do.</p>



<p class="wp-block-paragraph">Then build the smallest possible version: an issue form that captures the inputs, a label that means &ldquo;go,&rdquo; and an Action that does one step of the work. Or skip straight to writing your runbook down as a <code>SKILL.md</code> and let GitHub Copilot execute it. Run it with a dry-run switch until you trust it. Grow it from there.</p>



<p class="wp-block-paragraph">I didn&rsquo;t write the code. I wrote down what I already knew (how the work gets done) and the platform did the rest. Whatever your version of the morning registrant list is, it&rsquo;s probably one written-down runbook away from doing itself.</p>



<p class="wp-block-paragraph"><strong>Get started:</strong></p>



<ul class="wp-block-list">
<li><a href="https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms">Syntax for issue forms</a></li>



<li><a href="https://docs.github.com/actions">GitHub Actions documentation</a></li>



<li><a href="https://docs.github.com/copilot">GitHub Copilot documentation</a></li>



<li>And the blog post that convinced me this works: <a href="https://github.blog/developer-skills/github/i-automated-my-job-and-it-made-me-a-better-leader/">I automated my job (and it made me a better leader)</a></li>
</ul>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/marketing-ops-as-code-automating-events-from-planning-to-follow-up-on-github/">Marketing ops as code: Automating events from planning to follow-up on GitHub</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98799</post-id>	</item>
		<item>
		<title>GitHub Copilot app for Beginners: Using the diff, terminal, and browser</title>
		<link>https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-using-the-diff-terminal-and-browser/</link>
		
		<dc:creator><![CDATA[Kayla Cinnamon]]></dc:creator>
		<pubDate>Thu, 10 Sep 2026 21:31:19 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[GitHub Copilot app]]></category>
		<category><![CDATA[GitHub Copilot app for Beginners]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98771</guid>

					<description><![CDATA[<p>Checking agent-generated code usually means hopping between tabs. Learn how to view diffs, run terminal commands, and preview web apps side by side in the GitHub Copilot app.</p>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-using-the-diff-terminal-and-browser/">GitHub Copilot app for Beginners: Using the diff, terminal, and browser</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">When an agent makes a change to your code, you want to review it, run it, and see what changed. Great news: now you can do all three without having to leave the <a href="https://github.com/features/ai/github-app?utm_source=blog-copilot-app-ep6&amp;utm_medium=blog&amp;utm_campaign=copilot-app-for-beginners-series">GitHub Copilot app</a>.</p>



<p class="wp-block-paragraph">Before, doing those three jobs would mean having to bounce between your editor, terminal window, and web browser. But when these steps live side by side as built-in panels in the Copilot app, checking your agent&rsquo;s work is simple.</p>



<p class="wp-block-paragraph">Let&rsquo;s walk through each of the panels in the app and how we&rsquo;ll use them to complete the AI coding loop.</p>



<h2 id="h-using-the-diff-panel-to-review-changes" class="wp-block-heading">Using the diff panel to review changes</h2>



<p class="wp-block-paragraph">The diff panel (a diff is a before and after comparison) shows exactly what lines were added, removed, or changed, with additions highlighted in green and deletions in red.</p>



<p class="wp-block-paragraph">This gives you complete clarity, so you can choose what happens. You can accept changes, leave comments, or ask Copilot to make changes. You&rsquo;re in complete control and make the final decisions.</p>



<h2 id="h-running-commands-in-the-terminal-panel" class="wp-block-heading">Running commands in the terminal panel</h2>



<p class="wp-block-paragraph">Reading code is good, but running it is even better. You can run commands right inside the session from the Copilot app&rsquo;s terminal panel. And if it looks intimidating, don&rsquo;t worry. You&rsquo;re mostly just running the project&rsquo;s own commands and reading the results.</p>



<p class="wp-block-paragraph">You can run the code by hand or configure it as a script (available through the <strong>Run</strong> button). Pretend you&rsquo;re working on a website, here&rsquo;s an example of how that works:</p>



<ul class="wp-block-list">
<li>Add the <code>dev server</code> script that opens the client folder and then runs <code>npm run dev</code>.</li>



<li>Click <strong>Run</strong> to start the server for the website.</li>
</ul>



<p class="wp-block-paragraph">You can have multiple terminal windows open at once, so you can switch between them and keep running commands.</p>



<h2 id="h-using-the-pick-amp-polish-tool-in-the-browser-panel" class="wp-block-heading">Using the pick &amp; polish tool in the browser panel</h2>



<p class="wp-block-paragraph">For anything that has a user interface, the browser panel closes the loop. And for our website example: this panel means you can open it and test your new feature as if you were using the site.</p>



<p class="wp-block-paragraph">If you want to keep iterating, you can use the <strong>Pick &amp; Polish</strong> tool to select an element and adjust it with the agent.</p>



<p class="wp-block-paragraph">And to see what got fixed, you can run the <code>dev server</code> script again after you&rsquo;ve made changes.</p>



<h2 id="h-closing-the-loop-of-reviewing-ai-code" class="wp-block-heading">Closing the loop of reviewing AI code</h2>



<p class="wp-block-paragraph">Now you&rsquo;ve reviewed the diff, started the project in the terminal, reviewed it in the browser, and iterated to get it to a working feature. Once you&rsquo;ve gotten it where you want, you can accept the change directly from the Copilot app and create a pull request. Completing this loop from one place means there&rsquo;s no tab hopping, switching apps, or losing your place.</p>



<p class="wp-block-paragraph">Having review, run, and preview side by side is what makes agent-made changes feel safe instead of scary, because you can prove that what you&rsquo;re merging works.</p>



<h2 id="h-take-this-with-you" class="wp-block-heading">Take this with you</h2>



<p class="wp-block-paragraph">These built-in panels answer three important questions you should always ask before you accept an agent&rsquo;s work.</p>



<ul class="wp-block-list">
<li>What&rsquo;s&nbsp;changed?&nbsp;</li>



<li>Does it run?&nbsp;&nbsp;</li>



<li>And does it&nbsp;<em>actually</em><strong>&nbsp;</strong>work?&nbsp;&nbsp;</li>
</ul>



<p class="wp-block-paragraph">Running through this list before accepting agent-generated code will help you understand what changed and whether it functions the way you want, keeping you in control. &#10024;</p>



<div class="wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph">Try using the <a href="https://github.com/features/ai/github-app?utm_source=blog-copilot-app-ep6&amp;utm_medium=blog&amp;utm_campaign=copilot-app-for-beginners-series">GitHub Copilot app &gt;</a></p>
</div>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-using-the-diff-terminal-and-browser/">GitHub Copilot app for Beginners: Using the diff, terminal, and browser</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98771</post-id>	</item>
		<item>
		<title>GitHub availability report: August 2026</title>
		<link>https://github.blog/news-insights/company-news/github-availability-report-august-2026/</link>
		
		<dc:creator><![CDATA[Jakub Oleksy]]></dc:creator>
		<pubDate>Thu, 10 Sep 2026 02:05:17 +0000</pubDate>
				<category><![CDATA[Company news]]></category>
		<category><![CDATA[News & insights]]></category>
		<category><![CDATA[GitHub Availability Report]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98774</guid>

					<description><![CDATA[<p>In August, we experienced five incidents that resulted in degraded performance across GitHub services.</p>
<p>The post <a href="https://github.blog/news-insights/company-news/github-availability-report-august-2026/">GitHub availability report: August 2026</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">While we continue to make progress, August proved to be a challenging month for availability. You can read more about these incidents in <a href="https://github.blog/news-insights/company-news/the-august-17-outage-and-the-work-ahead/">a blog post</a> we published last month. We are aggressively investing in both architectural improvements and moving to Azure, which will give us more capacity. Meanwhile, we continue to see significant growth on our platform. We are prioritizing the most impactful work while minimizing risk, but as these incidents in August show, we cannot completely eliminate risk.</p>



<p class="wp-block-paragraph">Ultimately, all the work needs to be done, and incidents give us an opportunity to adjust our priorities. As repair items from these incidents, we&rsquo;ve made significant improvements to our capacity monitoring and management, retry policies that led to bigger impact, and resiliency improvements to core services. We&rsquo;ve also continued to make great progress across many durable work streams.</p>



<p class="wp-block-paragraph">On August 11, GitHub ran a production MySQL primary from Azure for the first time. Client-observed write impact was minimal, and no customer impact occurred in the transition. We repeated the pattern with two more primaries on August 27. We have further primaries scheduled over the coming weeks, increasing in complexity as we learn from each failover.</p>



<p class="wp-block-paragraph">Read traffic also reached new highs. Reads from migrated services peaked at 60.4%, while reads from GitHub&rsquo;s monolith peaked at 64.3% in Azure. Git reads reached 54%.</p>



<p class="wp-block-paragraph">Away from the regional migration, the 24-table authentication-core cohort moved off GitHub&rsquo;s oldest shared database, mysql1, removing approximately one million queries per second from its replicas. Separate query-hygiene changes removed another 120,000 queries per second and eliminated approximately 59,000 seconds of wasted database work per hour.</p>



<p class="wp-block-paragraph">GitHub Actions gained additional capacity while longer-term isolation work continues. Job-routing changes moved 33% of jobs from a constrained production cluster to spare capacity, reducing peak cache CPU utilization from 98% to 80% and adding an estimated three months of headroom. This is a near-term containment measure, not the finish line; the August outage reinforced the need for more durable capacity and isolation, which continues.</p>



<p class="wp-block-paragraph">Pull request isolation work continued. In addition to unauthenticated traffic already being served, now authenticated reads for the first production cohort reached 100%.</p>



<p class="wp-block-paragraph">Investments in Git overload protection served 6.4% more traffic while improving 95th-percentile duration by 24% and maximum delay by 78%. Broader load-shedding protections at the edge also made progress, enabling levers that can protect GitHub under unexpected load&mdash;in fact, these protections were used in mitigating the aforementioned incidents in August.</p>



<p class="wp-block-paragraph">We also improved monitoring and telemetry. Pull request monitoring now measures merge, review, and comment failures independently, where high read volume could otherwise mask a failing write path. On August 21, automated high impact incident detection began combining customer-support signals with service telemetry. API monitoring was also recalibrated and validated over 30 days, reducing noise and improving signal quality. These changes improve detection and response.</p>



<p class="wp-block-paragraph">The next month of work includes moving the next database primaries, continuing to migrate services and corresponding traffic to Azure, chipping away at database health particularly on shared-databases, adding more automation around capacity management and auto-scaling, and extending dependency-failure handling across more of the pull request experience.</p>



<p class="wp-block-paragraph">This principle continues to guide us: <strong>availability, then capacity, then features.</strong></p>



<h2 id="h-august-06-15-22-utc-lasting-10-hours-and-42-minutes" class="wp-block-heading">August 06 15:22 UTC (lasting 10 hours and 42 minutes)</h2>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" fetchpriority="high" decoding="async" height="509" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Actions-Run-Start_delay.png?resize=1024%2C509" alt="Graph showing the customer impact rate from 15:00 to 00:00 UTC." class="wp-image-98775" srcset="https://github.blog/wp-content/uploads/2026/09/Actions-Run-Start_delay.png?w=1625 1625w, https://github.blog/wp-content/uploads/2026/09/Actions-Run-Start_delay.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Actions-Run-Start_delay.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Actions-Run-Start_delay.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/09/Actions-Run-Start_delay.png?w=1536 1536w" sizes="(max-width: 1000px) 100vw, 1000px" /></figure>



<h3 id="h-what-happened" class="wp-block-heading">What happened?</h3>



<aside data-color-mode="light" data-dark-theme="dark" data-light-theme="light_dimmed" class="wp-block-group post-aside--large p-4 p-md-6 is-style-light-dimmed has-global-padding is-layout-constrained wp-block-group-is-layout-constrained is-style-light-dimmed--1" style="border-top-width:4px">
<h3 id="h-impact-summary" class="wp-block-heading h5-mktg gh-aside-title">Impact Summary</h3>



<ul class="wp-block-list">
<li><strong>Duration</strong>: approximately 9 hours</li>



<li><strong>Services impacted</strong>: GitHub Actions (GitHub-hosted and self-hosted runners), plus actions-dependent features, including Copilot coding agent, Copilot code review, GitHub Pages builds, Dependabot, and repository migrations.</li>



<li><strong>Customer impact</strong>: At least 74 organizations saw GitHub Actions workflows fail to start, fail partway through, or sit queued far longer than usual, measured against each organization&rsquo;s own normal baseline. Customers during the impact window may have seen workflow runs failing or stuck in a queued state, some actions API requests returning errors, and in some cases unexpected rate limiting in your workflows. In most cases, re-running a failed workflow once the incident was over succeeded.</li>
</ul>
</aside>



<p class="wp-block-paragraph">The incident began with a routine deployment to an internal GitHub Actions service that processes incoming events and turns them into actions jobs. The deployment&rsquo;s contents were not at fault (we rolled it back to confirm this); instead, replacing pods during the rollout briefly reduced capacity in one site and pushed the remaining sites past their limit as traffic shifted to them. Impact was heaviest through the middle hours of the incident, when a large share of actions workflow runs were failing to start or complete.</p>



<h3 id="h-what-went-wrong-and-why" class="wp-block-heading">What went wrong and why?</h3>



<p class="wp-block-paragraph">The affected actions services were running close to their capacity and concurrency limits. A routine deployment that briefly reduced the number of running pods was enough to exhaust available headroom. This caused service mesh sidecars to experience CPU throttling and out-of-memory restarts, which then cascaded into cache, DNS, and API errors across multiple clusters. The ingress service mesh for these services had limited headroom so it could not absorb the temporary loss of capacity during the deployment.</p>



<p class="wp-block-paragraph">As the core services recovered, a latent bug in the job-assignment path made recovery slower: runners were handed jobs that had already been revoked, then got stuck retrying them instead of picking up valid work, which created a self-amplifying backlog.</p>



<h3 id="h-how-did-we-respond" class="wp-block-heading">How did we respond?</h3>



<ol class="wp-block-list">
<li>A routine deployment to an internal actions service briefly reduced running capacity in one data center, and within minutes the service mesh and remaining pods saturated.</li>



<li>The failures cascaded across clusters as cache, DNS, and API errors spread and actions infrastructure failures climbed. We declared a public incident, identified the triggering deployment, and rolled it back to confirm its contents were not the cause.</li>



<li>Over roughly the next two hours we expanded capacity for the saturated services and throttled incoming webhook-triggered work so the system could stabilize.</li>



<li>With the core services recovering, a large backlog of queued jobs remained. A latent bug caused runners to be assigned jobs that were no longer valid and then get stuck retrying them, holding back real work.</li>



<li>We deployed fixes so runners stopped trying to acquire invalid jobs, drained the accumulated queues, and raised the internal rate limits that were slowing recovery. Workflow success rates climbed back toward normal.</li>



<li>System-wide queues drained, and actions returned to normal operation. A smaller set of self-hosted runners stayed stuck and were recovered manually, and some events from during the incident could not be replayed automatically and had to be re-triggered.</li>
</ol>



<h3 id="h-how-are-we-making-incidents-like-this-less-likely-or-less-impactful" class="wp-block-heading">How are we making incidents like this less likely or less impactful?</h3>



<ul class="wp-block-list">
<li>Add headroom and enable autoscaling for the service mesh ingress and the affected actions services so a routine deployment cannot tip them into saturation.</li>



<li>Make deployments safer for these services by avoiding capacity reductions during rollout.</li>



<li>Strengthen monitoring for the saturation and database-proxy conditions that preceded the incident so they are caught earlier.</li>



<li>Improve how the system sheds load and drains backlogged work during large actions incidents, and prevent runners from getting stuck retrying invalid jobs.</li>



<li>Ship automatic recovery for self-hosted Actions Runner Controller runners affected by this failure mode in upcoming runner and ARC releases.</li>
</ul>



<h2 id="h-august-17-13-40-utc-lasting-7-hours-and-35-minutes" class="wp-block-heading">August 17 13:40 UTC (lasting 7 hours and 35 minutes)</h2>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" decoding="async" height="509" width="1024" src="https://github.blog/wp-content/uploads/2026/09/API_Error.png?resize=1024%2C509" alt="Graph of the front-door failure rate from 13:30 to 20:00." class="wp-image-98777" srcset="https://github.blog/wp-content/uploads/2026/09/API_Error.png?w=1625 1625w, https://github.blog/wp-content/uploads/2026/09/API_Error.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/API_Error.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/API_Error.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/09/API_Error.png?w=1536 1536w" sizes="(max-width: 1000px) 100vw, 1000px" /></figure>



<h3 id="h-what-happened-0" class="wp-block-heading">What happened?</h3>



<aside data-color-mode="light" data-dark-theme="dark" data-light-theme="light_dimmed" class="wp-block-group post-aside--large p-4 p-md-6 is-style-light-dimmed has-global-padding is-layout-constrained wp-block-group-is-layout-constrained is-style-light-dimmed--2" style="border-top-width:4px">
<h3 id="h-impact-summary-0" class="wp-block-heading h5-mktg gh-aside-title">Impact summary</h3>



<ul class="wp-block-list">
<li><strong>Duration</strong>: approximately 6 hours 44 minutes</li>



<li><strong>Services impacted</strong>: Issues and pull requests were the most visible (pages and API returned errors and slow responses). A broad set of other services that route through the same datacenter&rsquo;s load balancers&mdash;including the REST and GraphQL APIs, actions, Copilot, sign-in and authentication, and the webhooks API&mdash;also saw errors and latency</li>



<li><strong>Customer impact</strong>: At the peak, 56.07% of front-door requests to the affected services failed or ran slow at the edge. Across the window, about 29K organizations, defined as a collection of user accounts owning repositories, saw at least one failed or slow request&mdash;over roughly 4.8M failed or slow requests in total. Retrying often worked, but during the worst stretches a large share of attempts failed. Once engineers halted the overloaded load-balancer processes and reduced the retry pressure feeding them, the affected datacenter recovered and error rates returned to normal. No data was lost.</li>
</ul>
</aside>



<h3 id="h-what-went-wrong-and-why-0" class="wp-block-heading">What went wrong and why?</h3>



<p class="wp-block-paragraph">A new peak in traffic pushed one datacenter&rsquo;s load balancers past their limits. A service-mesh sidecar reached its concurrency limit and did not scale up.</p>



<p class="wp-block-paragraph">As requests backed up, several of the datacenter&rsquo;s load-balancer nodes exhausted their network flow limits, which degraded the shared gateway authentication path and produced widespread authentication latency and failures across the many services that route through that datacenter.</p>



<p class="wp-block-paragraph">A latent client retry bug sharply amplified traffic to one internal authentication endpoint, which slowed recovery for the Copilot Token Service. The core weakness was that our service-mesh sidecar did not scale up, together with our retry behavior, and clients were not bound enough to keep a partial degradation from amplifying into a broader overload.</p>



<h3 id="h-how-did-we-respond-0" class="wp-block-heading">How did we respond?</h3>



<ol class="wp-block-list">
<li>A new peak in traffic pushes a datacenter&rsquo;s load balancers toward their limits; a service-mesh sidecar hits its concurrency limit and fails to scale up.</li>



<li>The overload cascades as several load-balancer nodes exhaust their network flow limits and the shared authentication path degrades; issues, pull requests, the APIs, actions, Copilot, and other services begin returning errors and slow responses.</li>



<li>Automated monitoring detects the elevated errors and an incident is opened; the affected products are marked degraded on the public status page as engineers from across the affected services converge.</li>



<li>Engineers trace the failure to network saturation on the load balancers in a single datacenter and begin shifting some traffic to another datacenter and reducing gateway retries to relieve the pressure.</li>



<li>The team stops the load-balancer processes on the saturated nodes and blocks the retry-triggering requests to the most-affected internal endpoint, which produces broad and immediate recovery.</li>



<li>Remaining authentication errors driven by client retry amplification are stabilized by ramping traffic back up gradually, and after a sustained period of healthy telemetry the incident is resolved.</li>
</ol>



<h3 id="h-how-are-we-making-incidents-like-this-less-likely-or-less-impactful-0" class="wp-block-heading">How are we making incidents like this less likely or less impactful?</h3>



<ul class="wp-block-list">
<li>Correct autoscaling policies so they account for service-mesh sidecar concurrency and capacity, not just the host service.</li>



<li>Audit request, concurrency, and scaling limits for the service mesh across the affected services.</li>



<li>Review retry and backoff limits across gateways and clients so a partial degradation cannot be amplified into a broader overload.</li>



<li>Fix the client retry behavior that amplified authentication traffic during the incident.</li>



<li>Improve load-balancer capacity monitoring and strengthen regional failover safeguards.</li>
</ul>



<h2 id="h-august-20-14-43-utc-lasting-9-hours-and-54-minutes" class="wp-block-heading">August 20 14:43 UTC (lasting 9 hours and 54 minutes)</h2>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" decoding="async" height="509" width="1024" src="https://github.blog/wp-content/uploads/2026/09/CCA_Outage.png?resize=1024%2C509" alt="Graph of the customer-facing impact rate from 14:00 to 00:30." class="wp-image-98779" srcset="https://github.blog/wp-content/uploads/2026/09/CCA_Outage.png?w=1625 1625w, https://github.blog/wp-content/uploads/2026/09/CCA_Outage.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/CCA_Outage.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/CCA_Outage.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/09/CCA_Outage.png?w=1536 1536w" sizes="(max-width: 1000px) 100vw, 1000px" /></figure>



<h3 id="h-what-happened-1" class="wp-block-heading">What happened?</h3>



<aside data-color-mode="light" data-dark-theme="dark" data-light-theme="light_dimmed" class="wp-block-group post-aside--large p-4 p-md-6 is-style-light-dimmed has-global-padding is-layout-constrained wp-block-group-is-layout-constrained is-style-light-dimmed--3" style="border-top-width:4px">
<h3 id="h-incident-summary" class="wp-block-heading h5-mktg gh-aside-title is-typography-preset-h5" style="margin-top:0">Incident summary</h3>



<ul class="wp-block-list">
<li><strong>Duration</strong>: approximately 10 hours 40 minutes</li>



<li><strong>Services impacted</strong>: Copilot cloud agent (task status and results not updating; the agent tasks themselves continued to run and complete)</li>



<li><strong>Customer impact</strong>: At least 54 organizations saw Copilot cloud agent task status and results lag well above their normal baseline, in some cases by up to 60 to 90 minutes. The tasks kept running and finishing; only the visibility of their status and results was delayed.</li>
</ul>
</aside>



<p class="wp-block-paragraph">During the incident window, the Copilot cloud agent task was impacted. The tasks themselves still ran to completion, so no work was lost. Once processing caught up the correct status and results appeared. Waiting a short time, or checking back a little later, would have shown the up-to-date state.</p>



<p class="wp-block-paragraph">Across the incident, at least 54 organizations experienced Copilot Cloud Agent task status and results lagging well above their own normal level. Per-minute customer-facing impact peaked at 37.5% of measured task-status activity.</p>



<h3 id="h-what-went-wrong-and-why-1" class="wp-block-heading">What went wrong and why?</h3>



<p class="wp-block-paragraph">Copilot cloud agent stores the status and results of each agent task in a managed cloud database. One region of that database suffered a provider-side outage, and the calls that read and write task status in the affected region began failing and running slow.</p>



<p class="wp-block-paragraph">The processors that stream task-status updates into that database then fell behind as database latency climbed. Their throughput is bounded by a fixed number of processing partitions sized for normal latency plus some headroom. The latency in this incident went well past that headroom, causing the backlog of task-status updates to grow instead of clearing. A storage configuration on the database also made the affected region slow to fail over, so the first failover attempts did not take effect, and recovery took longer than expected.</p>



<h3 id="h-how-did-we-respond-1" class="wp-block-heading">How did we respond?</h3>



<ol class="wp-block-list">
<li>A region of the managed cloud database that stores Copilot cloud agent task status began failing and running slow after a provider-side regional outage.</li>



<li>On-call engineers were paged, opened an incident, and traced the errors to the affected database region.</li>



<li>Engineers began a regional failover, but it did not take effect&mdash;a storage configuration on the database made the region slow to move&mdash;so task-status updates kept backing up.</li>



<li>The team forced the affected region offline and shifted task-status processing to a healthy region; write latency remained elevated, and the backlog kept the status view delayed.</li>



<li>Additional streaming capacity was added and the provider&rsquo;s region gradually recovered, letting the processors work through the backlog so task status and results caught up.</li>



<li>Latency returned to normal, the backlog cleared, and the incident was mitigated and resolved.</li>
</ol>



<h3 id="h-how-are-we-making-incidents-like-this-less-likely-or-less-impactful-1" class="wp-block-heading">How are we making incidents like this less likely or less impactful?</h3>



<ul class="wp-block-list">
<li>Remove the database storage configuration that made the affected region slow to fail over, so a single region&rsquo;s problems can be exited quickly.</li>



<li>Improve runbooks for regional failover of this database, including a vetted, ordered list of fallback regions that keep the service healthy.</li>



<li>Review failover priority so the next region chosen during a failover is the next-best healthy option.</li>



<li>Make task-status streaming more resilient to elevated database latency, so a latency spike does not immediately throttle throughput and build a backlog.</li>



<li>Improve monitoring and escalation on the managed databases.</li>
</ul>



<h2 id="h-august-26-15-11-utc-lasting-2-hours-and-50-minutes" class="wp-block-heading">August 26 15:11 UTC (lasting 2 hours and 50 minutes)</h2>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="509" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Actions-run-start-delay.png?resize=1024%2C509" alt="Graph showing the customer impact rate from 15:00 to 17:45." class="wp-image-98782" srcset="https://github.blog/wp-content/uploads/2026/09/Actions-run-start-delay.png?w=1624 1624w, https://github.blog/wp-content/uploads/2026/09/Actions-run-start-delay.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Actions-run-start-delay.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Actions-run-start-delay.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/09/Actions-run-start-delay.png?w=1536 1536w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>



<h3 id="h-what-happened-2" class="wp-block-heading">What happened?</h3>



<aside data-color-mode="light" data-dark-theme="dark" data-light-theme="light_dimmed" class="wp-block-group post-aside--large p-4 p-md-6 is-style-light-dimmed has-global-padding is-layout-constrained wp-block-group-is-layout-constrained is-style-light-dimmed--4" style="border-top-width:4px">
<h3 id="h-impact-summary-1" class="wp-block-heading h5-mktg gh-aside-title is-typography-preset-h5" style="margin-top:0">Impact summary</h3>



<ul class="wp-block-list">
<li><strong>Duration</strong>: approximately 2 hours 53 minutes</li>



<li><strong>Services impacted</strong>: GitHub Actions (run starts), with knock-on delays to GitHub Pages deployments and Copilot code review.</li>



<li><strong>Customer impact</strong>: At least 24 organizations saw actions runs fail to start or start much later than usual, measured against each organization&rsquo;s own normal baseline, and a total of 386 organizations saw at least some run-start impact. At peak, more than one in five actions run starts in each minute were failing or badly delayed. Affected workflow runs would have sat queued well past their normal start time, and some would have failed to start outright.</li>
</ul>
</aside>



<p class="wp-block-paragraph">Dependent services like Copilot code review and some GitHub Pages deployments that run on top of actions, were impacted during the incident window.</p>



<p class="wp-block-paragraph">In most cases, delayed runs started once the backlog drained, and runs that failed to start succeeded when re-run after the incident. A small set of runs that were created during the earliest part of the incident could not be recovered by re-running and had to be started fresh.</p>



<h3 id="h-what-went-wrong-and-why-2" class="wp-block-heading">What went wrong and why?</h3>



<p class="wp-block-paragraph">The quick summary is that our shared infrastructure services have not kept up with our month-over-month actions growth and peak load.</p>



<p class="wp-block-paragraph">A burst of incoming events arrived on top of an already-high load and pushed the database past its tipping point. Query times climbed, and the database primary saturated.</p>



<p class="wp-block-paragraph">With the database overloaded, the internal service that turns incoming events into runner assignments could not keep up, so actions runs failed to start and began queuing well past their normal start time.</p>



<p class="wp-block-paragraph">Failing over the database primary helped only partially. The throttles used to relieve inbound load were initially set slightly too high to fully protect the database, so recovery had to be ramped up slowly and manually.</p>



<p class="wp-block-paragraph">There was no automatic circuit breaker to throttle inbound actions load when the database showed early signs of stress, so the protective throttling had to be applied and tuned manually during the incident. This is one of the learnings from this incident.</p>



<h3 id="h-how-did-we-respond-2" class="wp-block-heading">How did we respond?</h3>



<ol class="wp-block-list">
<li>During a daily traffic peak, a burst of incoming events landed while a shared database that actions depends on was already running near its limit. Write and query pressure on the database primary rose sharply and began to saturate.</li>



<li>The internal service that turns incoming events into runner assignments could no longer keep up. Actions runs began failing an began incident investigation.</li>



<li>We failed the database primary over to a replica. This improved things briefly but did not fully mitigate, so runs continued to fail or start late.</li>



<li>We throttled inbound event processing to relieve pressure on the database and let it recover. Core service health returned once the throttling and service restarts took effect, though inbound work was now intentionally slowed.</li>



<li>We raised the throttles gradually, watching telemetry at each step so we did not re-overwhelm the database, until full event processing was restored and the backlog of delayed work drained. We marked the incident mitigated.</li>



<li>A subset of jobs on larger and self-hosted runners remained stuck waiting for a runner. We deployed a change to release them, and continued follow-up work to clear runs that had been left showing as queued.</li>
</ol>



<h3 id="h-how-are-we-making-incidents-like-this-less-likely-or-less-impactful-2" class="wp-block-heading">How are we making incidents like this less likely or less impactful?</h3>



<ul class="wp-block-list">
<li>Improve query efficiency of database usage by optimizing specific code paths in the client code.</li>



<li>Add an automatic circuit breaker that throttles inbound actions load when the database shows signs of stress, instead of relying on manual throttling during an incident.</li>



<li>Add protections around how often the service falls back to the database primary when a replica is lagging, so a fallback cannot compound database pressure.</li>



<li>Improve our ability to quickly cancel or clear runs left stuck in a queued or waiting-for-runner state after an incident, so affected jobs recover sooner.</li>



<li>Continue the scaling and resiliency work already in flight for this part of actions, including changes that were completing and rolling out around the time of the incident.</li>
</ul>



<h2 id="h-august-27-10-04-utc-lasting-2-hours-and-8-minutes" class="wp-block-heading">August 27 10:04 UTC (lasting 2 hours and 8 minutes)</h2>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="509" width="1024" src="https://github.blog/wp-content/uploads/2026/09/kimi_image.png?resize=1024%2C509" alt="Graph of the customer-facing impact rate from 09:30 to 12:15." class="wp-image-98784" srcset="https://github.blog/wp-content/uploads/2026/09/kimi_image.png?w=1625 1625w, https://github.blog/wp-content/uploads/2026/09/kimi_image.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/kimi_image.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/kimi_image.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/09/kimi_image.png?w=1536 1536w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>



<h3 id="h-what-happened-3" class="wp-block-heading">What happened?</h3>



<aside data-color-mode="light" data-dark-theme="dark" data-light-theme="light_dimmed" class="wp-block-group post-aside--large p-4 p-md-6 is-style-light-dimmed has-global-padding is-layout-constrained wp-block-group-is-layout-constrained is-style-light-dimmed--5" style="border-top-width:4px">
<h3 id="h-impact-summary-2" class="wp-block-heading h5-mktg gh-aside-title is-typography-preset-h5" style="margin-top:0">Impact summary</h3>



<ul class="wp-block-list">
<li><strong>Duration</strong>: approximately 2 hours 50 minutes</li>



<li><strong>Services impacted</strong>: GitHub Copilot (the Kimi K3 model)</li>



<li><strong>Customer impact</strong>: 63.3% of Kimi K3 requests failed during this impact window.</li>
</ul>
</aside>



<p class="wp-block-paragraph">Customers who had configuration to use the Kimi K3 model were impacted by this incident. Customers who were using other models or switched to using other models were not impacted.</p>



<h3 id="h-what-went-wrong-and-why-3" class="wp-block-heading">What went wrong and why?</h3>



<p class="wp-block-paragraph">Copilot offers a choice of AI models. One of them, Kimi K3, is served by an upstream model provider.</p>



<p class="wp-block-paragraph">That provider had a serving degradation that caused a large share of Kimi K3 requests to fail with errors. Because the problem was with the upstream provider, requests that used other models&mdash;and requests made with the Auto setting, which routed to a different model&mdash;were not affected.</p>



<p class="wp-block-paragraph">A steady share of Kimi K3 requests kept failing until the provider&rsquo;s mitigation took hold. At peak, more than half of the requests using Kimi K3 were failing.</p>



<h3 id="h-how-did-we-respond-3" class="wp-block-heading">How did we respond?</h3>



<ol class="wp-block-list">
<li>The upstream provider for Kimi K3 experienced a degradation, resulting in elevated failure rates for Copilot requests routed to that model.</li>



<li>Within a few minutes, monitoring flagged the elevated errors and we began investigating.</li>



<li>We declared an incident, traced the failures to a degradation at the upstream provider affecting Kimi K3 specifically, and posted a public status update pointing to the provider.</li>



<li>Requests that used other models, or the Auto setting, kept working throughout, so retrying or switching models would have succeeded.</li>



<li>We opened a ticket with the provider and monitored recovery as success rates climbed back toward normal on our dashboards.</li>



<li>We kept the incident open until the provider confirmed Kimi K3 was fully restored, then resolved it.</li>
</ol>



<h3 id="h-how-are-we-making-incidents-like-this-less-likely-or-less-impactful-3" class="wp-block-heading">How are we making incidents like this less likely or less impactful?</h3>



<ul class="wp-block-list">
<li>Work with the upstream provider to improve the reliability of the Kimi K3 model and reduce the errors seen during this incident.</li>



<li>Investigate backup serving capacity for Kimi K3 so a single provider degradation has a fallback.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity">



<p class="wp-block-paragraph">Follow our <a href="https://www.githubstatus.com/">status page</a> for real-time updates on status changes and post-incident recaps. To learn more about what we&rsquo;re working on, check out the engineering section on the <a href="https://github.blog/category/engineering/">GitHub Blog</a>.</p>
</body></html>
<p>The post <a href="https://github.blog/news-insights/company-news/github-availability-report-august-2026/">GitHub availability report: August 2026</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98774</post-id>	</item>
		<item>
		<title>Project HydraFusion: Frontier quality via multi-model orchestration</title>
		<link>https://github.blog/ai-and-ml/github-copilot/project-hydrafusion-frontier-quality-via-multi-model-orchestration/</link>
		
		<dc:creator><![CDATA[GitHub Staff]]></dc:creator>
		<pubDate>Fri, 04 Sep 2026 16:04:14 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[LLMs]]></category>
		<category><![CDATA[benchmarking]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98615</guid>

					<description><![CDATA[<p>In controlled offline evaluations, HydraFusion’s selective coding workflows matched or exceeded the evaluated Opus 5 baseline while reducing estimated workflow cost. Now available as a research preview in GitHub Copilot.</p>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/project-hydrafusion-frontier-quality-via-multi-model-orchestration/">Project HydraFusion: Frontier quality via multi-model orchestration</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">Providing developers the best model for the task at hand has always been our goal. Earlier this year, we made that easier by launching <a href="https://docs.github.com/copilot/concepts/models/auto-model-selection" target="_blank" rel="noreferrer noopener">Auto model selection,</a> which reviews your task and matches it to the best-suited model for that task.&nbsp;</p>



<p class="wp-block-paragraph">Today, we&rsquo;re introducing Project HydraFusion, a research preview that delivers frontier intelligence through runtime orchestration. It creates a full execution plan, choosing from models across multiple providers to draft, critique and revise, or cascade to more powerful models to complete your task.&nbsp;</p>



<p class="wp-block-paragraph">HydraFusion fills a key role in our overall strategy to deliver automated semantic routing between local, cloud, and compound models. For developers, that complexity stays behind the scenes: you select HydraFusion like any other model, and it chooses a workflow that balances performance, cost, and latency for each task.&nbsp;</p>



<aside data-color-mode="light" data-dark-theme="dark" data-light-theme="light_dimmed" class="wp-block-group post-aside--large p-4 p-md-6 is-style-light-dimmed has-global-padding is-layout-constrained wp-block-group-is-layout-constrained is-style-light-dimmed--6" style="border-top-width:4px">
<h3 id="h-now-available-as-a-research-preview" class="wp-block-heading h5-mktg gh-aside-title is-typography-preset-h5" style="margin-top:0">Now available as a research preview</h3>



<p class="wp-block-paragraph">HydraFusion is available to users on all GitHub Copilot plans through <code>/experimental</code> in <a href="https://docs.github.com/copilot/how-tos/copilot-cli/use-copilot-cli/overview">GitHub Copilot CLI</a>. Usage is based on the tokens consumed by the models HydraFusion uses, priced at each model&rsquo;s <a href="https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing">standard rate</a>.</p>



<p class="wp-block-paragraph">To try HydraFusion in Copilot CLI:</p>



<ol class="wp-block-list">
<li>Run <code>/update</code> to install the latest version</li>



<li>Run <code>/experimental on</code></li>



<li>Run <code>/model</code>, then select <code>HydraFusion (Research Preview)</code></li>
</ol>



<p class="wp-block-paragraph">Please post feedback in the <a href="https://github.com/orgs/community/discussions/206492">GitHub Community</a>.</p>
</aside>



<p class="wp-block-paragraph">HydraFusion treats workflow selection as an optimization problem. It uses capability signals for reasoning, code generation, debugging, and tool use to select the most efficient execution pattern to meet the quality bar. &nbsp;</p>



<p class="wp-block-paragraph">For each request, HydraFusion currently chooses one of three execution patterns:</p>



<ul class="wp-block-list">
<li><strong>Single.</strong> One selected model solves the task directly.</li>



<li><strong>Cascade.</strong> An efficient model drafts a solution and a quality gate decides whether to accept it or escalate to a stronger model.</li>



<li><strong>Critique. </strong>One model drafts a result, an independent read-only critic from a different model family reviews it (following the same review pattern as <a href="https://docs.github.com/en/copilot/concepts/agents/copilot-cli/rubber-duck" target="_blank" rel="noopener">Rubber Duck</a>), and the drafting model revises once.</li>
</ul>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="334" width="1024" src="https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?resize=1024%2C334" alt="" class="wp-image-98688" srcset="https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?w=5904 5904w, https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?w=1536 1536w, https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?w=2048 2048w, https://github.blog/wp-content/uploads/2026/09/HydraFusion_architecture_02.png?w=3000 3000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Figure 1.&nbsp;HydraFusion&nbsp;architecture&nbsp;</figcaption></figure>



<p class="wp-block-paragraph">Each pattern addresses a different quality-to-cost trade-off. <strong>Single</strong> preserves speed and efficiency when one model can solve the task directly. <strong>Cascade</strong> gives an efficient model the first attempt while retaining a path to stronger inference when the candidate does not clear the acceptance gate. <strong>Critique</strong> adds an independent perspective for tasks where review is more useful than another unaided attempt.</p>



<p class="wp-block-paragraph">In offline evaluations across three agentic coding benchmarks, HydraFusion consistently demonstrated frontier-level quality with substantial estimated cost savings. On TerminalBench 2.1, it improved verified task quality by 4.9 percentage points at 67% lower estimated cost compared with Claude Opus 5.</p>



<p class="wp-block-paragraph">Let&rsquo;s dive into the approach, the results, and the benchmarks.</p>



<h2 id="h-adaptive-multi-model-orchestration" class="wp-block-heading">Adaptive multi-model orchestration</h2>



<p class="wp-block-paragraph">Developers already coordinate models manually: choosing one for a task, asking another to review the work, or escalating a difficult problem to a more capable model. HydraFusion brings that familiar process into the runtime. You choose HydraFusion once and stay focused on your task while it manages the models and workflow behind the scenes.&nbsp;</p>



<p class="wp-block-paragraph">The key is selectivity. Some coding tasks can be solved directly, while others benefit from review, revision, or escalation. HydraFusion evaluates each request and chooses the least complex workflow expected to meet its needs, using additional model calls only when they are likely to improve the result. This adaptive approach balances quality, cost, and latency across models.</p>



<p class="wp-block-paragraph">As the model frontier advances, so does HydraFusion. When new models become available in GitHub Copilot, we can evaluate and incorporate them into its model pool, bringing their strengths to the tasks best suited to them.</p>



<h2 id="h-building-hydrafusion" class="wp-block-heading">Building HydraFusion</h2>



<p class="wp-block-paragraph" id="h-">Turning adaptive multi-model orchestration into one dependable coding experience requires careful control of execution, review, cost, and repository state. HydraFusion is built around five operating principles:</p>



<ul class="wp-block-list">
<li><strong>Complete accounting.</strong> Aggregate cost and usage across every workflow leg, including drafting, critique, revision, escalation, retry, and fallback.</li>



<li><strong>Bounded execution.</strong> Give each leg explicit timeout and cancellation behavior to keep execution and cost within defined limits.</li>



<li><strong>Isolated review.</strong> Run review steps in isolated, tool-less contexts, while solver steps use the shared workspace and normal permission-aware agent loop. This allows models to assess the work independently without modifying the repository.</li>



<li><strong>Fail-safe application.</strong> Apply no patch when the workflow is cancelled or fails validation, preventing incomplete changes from reaching the repository.</li>



<li><strong>Validated routing.</strong> Verify workflow definitions, model bindings, fallback behavior, and model availability before execution begins.</li>
</ul>



<p class="wp-block-paragraph">Together, these principles make multi-model orchestration practical for repository-level work. Internally, the runtime records the role, outcome, cost, latency, and diagnostics of each leg so the workflow can be understood after execution. Externally, the developer receives one coherent response and one permission-aware change set.&nbsp;</p>



<aside data-color-mode="light" data-dark-theme="dark" data-light-theme="light_dimmed" class="wp-block-group post-aside--large p-4 p-md-6 is-style-light-dimmed has-global-padding is-layout-constrained wp-block-group-is-layout-constrained is-style-light-dimmed--7" style="border-top-width:4px">
<h3 id="h-showing-progress-without-showing-unfinished-work-nbsp" class="wp-block-heading h5-mktg gh-aside-title is-typography-preset-h5" style="margin-top:0">Showing progress without showing unfinished work&nbsp;</h3>



<ul class="wp-block-list">
<li><strong>Today:</strong>&nbsp;HydraFusion shows workflow stages but holds intermediate drafts until it returns one coherent result.&nbsp;</li>



<li><strong>Why:</strong>&nbsp;Those drafts may be reviewed, revised, or discarded, so showing them live could make unfinished work appear final.&nbsp;</li>



<li><strong>What we&rsquo;re learning:</strong>&nbsp;Waiting without enough visibility is a real trade-off for developers.&nbsp;</li>



<li><strong>Next:</strong>&nbsp;We&rsquo;re actively exploring better progress updates, guided by feedback from the research preview.&nbsp;</li>
</ul>
</aside>



<h2 id="h-benchmarking-results" class="wp-block-heading">Benchmarking results</h2>



<p class="wp-block-paragraph">Fixed HydraFusion policies were evaluated across three agentic coding benchmarks &mdash; TerminalBench 2.1, DeepSWE, and CheckpointBench, our internal benchmark based on real GitHub Copilot sessions &mdash; using Claude Opus 5 and GPT-5.6 Sol as comparison baselines. Each policy used the same task inputs, tools, execution limits, pricing assumptions, grading conditions, and treatment of missing results. The evaluation measured verified task quality, which is the share of tasks confirmed as correctly answered, and the complete estimated workflow cost. Cost accounting included every invoked leg, such as drafting, critique, revision, escalation, retry, and fallback. The results below show the best tuned HydraFusion configuration.&nbsp;</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><strong>Benchmarks</strong>&nbsp;</th><th><strong>Cost&nbsp;&nbsp;vs.&nbsp;Opus 5</strong>&nbsp;</th><th><strong>Quality&nbsp;&nbsp;vs.&nbsp;Opus 5</strong>&nbsp;</th></tr></thead><tbody><tr><td><strong>TerminalBench&nbsp;2.1</strong></td><td>67%&nbsp;lower</td><td>+4.9&nbsp;points&nbsp;</td></tr><tr><td><strong>DeepSWE</strong>&nbsp;</td><td>36% lower&nbsp;</td><td>-1.5&nbsp;points&nbsp;</td></tr><tr><td><strong>CheckpointBench</strong>&nbsp;</td><td>65% lower</td><td>-0.1&nbsp;points&nbsp;</td></tr></tbody></table><figcaption class="wp-element-caption">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Table 1.&nbsp;HydraFusion quality and cost&nbsp;across three agentic benchmarks, relative to Opus 5.&nbsp;</figcaption></figure>



<p class="wp-block-paragraph">These controlled offline results are specific to the evaluated benchmark revisions, workflow configurations, model pool, and pricing assumptions, with all models evaluated at the same medium reasoning level. Through this research preview, we&rsquo;ll validate how these results translate to real developer workloads and use the findings to further optimize HydraFusion for production quality, latency, reliability, caching efficiency, cost, and safety.&nbsp;</p>



<h3 id="h-terminalbench-2-1" class="wp-block-heading">TerminalBench 2.1</h3>



<p class="wp-block-paragraph">TerminalBench 2.1 evaluates coding agents on complex, multi-step tasks in terminal environments.&nbsp;</p>



<p class="wp-block-paragraph">Figure 2 compares HydraFusion and Opus 5 across verified task quality and estimated workflow cost.&nbsp;</p>



<style data-wp-block-html="css">
.ghchart {
  /* Theme tokens */
  --ghc-text: #1f2328;
  --ghc-muted: #656d76;
  --ghc-grid: #d0d7de;
  --ghc-axis: #59636e;
  --ghc-card-bg: #ffffff;
  --ghc-card-border: #d0d7de;
  --ghc-divider: #b6bfb8;
  --ghc-guide: #afb8c1;

  /* GitHub brand font — matches the github.blog stack (Mona Sans, with
     the blog's metric fallback) so it renders identically in-context. */
  font-family: "Mona Sans", "Mona Sans Fallback", -apple-system, "system-ui",
    "Segoe UI", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 699px;
  margin: 0 auto;
  color: var(--ghc-text);
  line-height: 1.4;
}

.ghchart *,
.ghchart *::before,
.ghchart *::after { box-sizing: border-box; }

/* ------------------------------------------------------------------ */
/* Header + legend                                                     */
/* ------------------------------------------------------------------ */
.ghchart__header {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  min-height: 24px;
  margin-bottom: 4px;
}

.ghchart__legend {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.ghchart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: clamp(14px, 1.9vw, 17px);
  font-weight: 600;
  color: var(--ghc-muted);
  white-space: nowrap;
}

.ghchart__swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  flex: 0 0 auto;
}

/* ------------------------------------------------------------------ */
/* Plot (SVG)                                                          */
/* ------------------------------------------------------------------ */
.ghchart__plot {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  touch-action: pan-y;
}

.ghchart__gridline {
  stroke: var(--ghc-grid);
  stroke-width: 1;
  stroke-dasharray: 2 6;
  stroke-linecap: round;
  opacity: 0.9;
}

.ghchart__axislabel {
  fill: var(--ghc-axis);
  font-size: 22px;
  font-weight: 500;
}
.ghchart__axislabel--x { font-weight: 600; }

.ghchart__line {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ghchart__dot { transition: r 0.12s ease; }

/* Dashed threshold line (e.g. a target/reference level) + floating label.
   Stroke color is set inline per chart; dash + label styling live here. */
.ghchart__threshold {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.9;
}
.ghchart__threshold-label {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Hatched band marking a partial / projected trailing region. */
.ghchart__projection { pointer-events: none; }

.ghchart__guide {
  stroke: var(--ghc-guide);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
  opacity: 0;
  transition: opacity 0.12s ease;
}

.ghchart__hit { fill: transparent; }

/* ------------------------------------------------------------------ */
/* Scatter plot                                                        */
/* ------------------------------------------------------------------ */
.ghchart__gridline--v { /* vertical gridlines reuse the dashed grid style */ }

.ghchart__axistitle {
  fill: var(--ghc-muted);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Dotted reference crosshair through the "reference" point. Stroke color
   is set inline (tracks the reference series color). */
.ghchart__crosshair {
  stroke-width: 2;
  stroke-dasharray: 2 7;
  stroke-linecap: round;
  opacity: 0.85;
  pointer-events: none;
}

.ghchart__halo { opacity: 0.18; pointer-events: none; }
.ghchart__pt { transition: r 0.12s ease; }

.ghchart__pt-label {
  fill: var(--ghc-muted);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  pointer-events: none;
}
.ghchart__pt-label--strong { font-size: 21px; font-weight: 700; }

.ghchart__pthit { fill: transparent; cursor: default; outline: none; }
.ghchart__pthit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

.ghchart__swatch--dot { border-radius: 50%; }

/* ------------------------------------------------------------------ */
/* Trend chart (dated time-series with narrative headline)             */
/* ------------------------------------------------------------------ */
.ghchart__headline {
  font-size: clamp(17px, 2.3vw, 20px);
  line-height: 1.32;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
  margin: 0 0 8px;
}

/* Trend legend sits left-aligned under the headline/subtitle. */
.ghchart__legend--trend {
  justify-content: flex-start;
  margin: 2px 0 14px;
}
.ghchart__glyph { flex: 0 0 auto; }
.ghchart__glyph--square {
  width: 13px; height: 13px; border-radius: 2px;
}
.ghchart__glyph--diamond {
  width: 12px; height: 12px; border: 1.5px solid; transform: rotate(45deg);
}
.ghchart__glyph--line {
  width: 20px; height: 0; border-top: 2px dashed; border-radius: 0;
}

/* Phase background columns + separators + top labels. */
.ghchart__phase { fill: #f6f8fa; opacity: 0.55; }
.ghchart__phase--alt { fill: #ffffff; opacity: 0; }
.ghchart__phase-sep {
  stroke: var(--ghc-grid); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.8;
}
.ghchart__phase-label {
  fill: var(--ghc-muted);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
}
.ghchart__phase-avg {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Dashed trend line + ± noise band + slope label. */
.ghchart__trendband { opacity: 0.12; pointer-events: none; }
.ghchart__trendline {
  stroke-width: 2.5;
  stroke-dasharray: 8 6;
  stroke-linecap: round;
  fill: none;
}
.ghchart__slope {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Horizontal reference line + label. */
.ghchart__refline {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.85;
}
.ghchart__reflabel {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Data squares — values reveal on hover only. */
.ghchart__square { transition: width 0.12s ease, height 0.12s ease, x 0.12s ease, y 0.12s ease; }
.ghchart__squarehit { fill: transparent; cursor: default; outline: none; }
.ghchart__squarehit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

/* Invalid-run markers + leader-line annotations. */
.ghchart__invalid { stroke-width: 2; }
.ghchart__invalid-x { stroke-width: 2; stroke-linecap: round; }
.ghchart__leader { stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.8; }
.ghchart__leader--callout { stroke-dasharray: none; opacity: 0.6; }
.ghchart__anno-title {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.ghchart__anno-sub {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 500;
}
/* Final-callout labels read a touch larger than the invalid-run notes. */
.ghchart__anno-title--callout { font-size: 17px; }
.ghchart__anno-sub--callout { font-size: 14px; }

/* Bracket span annotation. */
.ghchart__bracket { stroke: var(--ghc-axis); stroke-width: 1.5; opacity: 0.75; }
.ghchart__bracket-label {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
}

/* Final callout ring. */
.ghchart__callout-ring { fill: none; stroke-width: 2; }

/* ------------------------------------------------------------------ */
/* Title + divider                                                     */
/* ------------------------------------------------------------------ */
.ghchart__title {
  /* Matches the github.blog H2 (.wp-block-heading) exactly */
  display: block;
  font-size: 32px;
  line-height: 40px;
  font-weight: 700;
  letter-spacing: -0.18px;
  color: var(--ghc-text);
}

.ghchart__rule {
  height: 1.5px;
  border-radius: 1.5px;
  background: var(--ghc-divider);
  margin: 10px 0 18px;
}

/* ------------------------------------------------------------------ */
/* Bar chart: subtitle, panels, bars, caption                          */
/* ------------------------------------------------------------------ */
.ghchart__subtitle {
  margin: -6px 0 20px;
  font-size: clamp(13px, 1.6vw, 15px);
  line-height: 1.5;
  color: var(--ghc-muted);
}

.ghchart__panel { margin-top: 30px; }
.ghchart__panel:first-of-type { margin-top: 6px; }

.ghchart__panel-title {
  margin: 0 0 6px;
  font-size: clamp(15px, 1.9vw, 18px);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
}

.ghchart__plot--bar { touch-action: pan-y; }

.ghchart__axisline {
  stroke: var(--ghc-axis);
  stroke-width: 1.25;
  opacity: 0.35;
}

.ghchart__bar { transition: opacity 0.12s ease; }
.ghchart__barhit { fill: transparent; }

/* Always-on value labels above selected bars. */
.ghchart__barvalue {
  fill: var(--ghc-text);
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Anchor annotation: dotted reference line + floating pill. */
.ghchart__anno-line {
  stroke: #8b949e;
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  stroke-linecap: round;
  pointer-events: none;
}
.ghchart__anno-chip {
  fill: #ffebe9;
  stroke: #ffcecb;
  stroke-width: 1;
  pointer-events: none;
}
.ghchart__anno-chip-text {
  fill: #a0111f;
  font-weight: 700;
  pointer-events: none;
}

.ghchart__caption {
  margin-top: 22px;
  padding-top: 12px;
  border-top: 1px solid var(--ghc-card-border);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ghc-muted);
}
.ghchart__caption b { color: var(--ghc-text); font-weight: 700; }
.ghchart__caption code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.92em;
  padding: 1px 5px;
  background: #f6f8fa;
  border: 1px solid var(--ghc-card-border);
  border-radius: 5px;
}
.ghchart__tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid #aceebb;
  color: #0a5223;
  background: #dafbe1;
  white-space: nowrap;
  vertical-align: middle;
}

/* ------------------------------------------------------------------ */
/* Distribution dashboard (segmented mix bar + ranked bar panels)      */
/* ------------------------------------------------------------------ */
.ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-section:first-of-type { margin-top: 12px; }

/* Segmented proportional "mix" bar: name inside, count/percent below.
   Separators are 2px inside borders (box-sizing:border-box) rather than a
   flex gap, so the three segments sum to exactly 100% with no overflow. */
.ghchart__seg {
  display: flex;
  width: 100%;
  height: 42px;
  margin-top: 2px;
  border-radius: 7px;
  overflow: hidden;
}
.ghchart__seg-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  color: #ffffff;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
}
.ghchart__seg-cell + .ghchart__seg-cell { border-left: 2px solid var(--ghc-card-bg); }
.ghchart__seg-note-row {
  display: flex;
  width: 100%;
  margin-top: 8px;
}
.ghchart__seg-note {
  min-width: 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Keep the outer notes from spilling past the chart edges. */
.ghchart__seg-note:first-child { text-align: left; }
.ghchart__seg-note:last-child { text-align: right; }
.ghchart__seg-note b { color: var(--ghc-text); font-weight: 700; }

/* Ranked horizontal bar list: [category | bar track | value].
   The list is the grid and rows use display:contents, so the category,
   bar, and value columns line up across every row in a panel. */
.ghchart__dist-list {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  align-items: center;
  column-gap: 12px;
  row-gap: 10px;
  margin-top: 4px;
}
.ghchart__dist-row { display: contents; }
.ghchart__dist-cat {
  text-align: right;
  font-size: 13px;
  line-height: 1.25;
  color: var(--ghc-text);
  white-space: nowrap;
}
.ghchart__dist-track { width: 100%; }
.ghchart__dist-bar {
  height: 15px;
  min-width: 2px;
  border-radius: 3px;
}
.ghchart__dist-val {
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ghchart__dist-val b { color: var(--ghc-text); font-weight: 700; }

/* Two-up panel grid (collapses to one column on narrow screens).
   minmax(0,1fr) lets columns shrink so long labels never force overflow. */
.ghchart__dist-cols {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px 30px;
}
.ghchart__dist-cols .ghchart__dist-section { margin-top: 0; }
.ghchart__dist-cols--stack { grid-template-columns: 1fr; }
.ghchart__dist-cols--stack .ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-cols--stack .ghchart__dist-section:first-child { margin-top: 0; }

/* Tighten the label/value type a touch in the 2-up layout so the bars
   keep room at the 699px blog width. */
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-cat { font-size: 12px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-val { font-size: 11.5px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-list { column-gap: 9px; }

@media (max-width: 600px) {
  .ghchart__dist-cols { grid-template-columns: 1fr; gap: 26px; }
  .ghchart__dist-cols .ghchart__dist-section ~ .ghchart__dist-section { margin-top: 2px; }
}

/* Let long category labels wrap on very narrow (mobile) widths. */
@media (max-width: 480px) {
  .ghchart__dist-cat { white-space: normal; }
}

/* ------------------------------------------------------------------ */
/* Calendar heatmap (GitHub contribution-graph style)                  */
/* ------------------------------------------------------------------ */
.ghheat__eyebrow { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.ghheat__eyedot {
  width: 9px; height: 9px; border-radius: 50%;
  background: #cf222e; box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.14);
  flex: 0 0 auto;
}
.ghheat__eyetext {
  font-size: 12px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: #cf222e;
}
/* Subtitle here is a prominent lede, not the muted variant. */
.ghheat__subtitle {
  color: var(--ghc-text);
  font-size: clamp(14px, 1.7vw, 16px);
  /* 24px top gap matches the github.blog H2 → paragraph block spacing. */
  margin: 24px 0 8px;
}
.ghheat__subtitle b { font-weight: 700; }

.ghheat__panel { margin-top: 36px; }
.ghheat__phead {
  display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.ghheat__badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  color: #fff; font-size: 12px; font-weight: 700;
  flex: 0 0 auto; align-self: center;
}
.ghheat__ptitle {
  font-size: clamp(16px, 2vw, 19px); font-weight: 700;
  letter-spacing: -0.01em; color: var(--ghc-text);
}
.ghheat__pdesc { font-size: 14px; color: var(--ghc-muted); }

.ghheat__body { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }
.ghheat__gridwrap { flex: 1 1 340px; min-width: 260px; }
.ghheat__grid { display: block; width: 100%; height: auto; overflow: visible; }
.ghheat__cell {
  stroke: rgba(27, 31, 36, 0.06);
  stroke-width: 1;
  transition: stroke 0.1s ease;
}
.ghheat__cell:hover { stroke: #24292f; stroke-width: 1.4; }
.ghheat__cell--pad { opacity: 0; pointer-events: none; }
.ghheat__wd, .ghheat__month {
  fill: var(--ghc-muted); font-size: 11px; font-weight: 600;
}

.ghheat__side { flex: 1 1 250px; min-width: 230px; }
.ghheat__stat { display: flex; align-items: baseline; gap: 7px; }
.ghheat__statbig {
  font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em;
  line-height: 1;
}
.ghheat__statsmall { font-size: 15px; font-weight: 600; color: var(--ghc-muted); }
.ghheat__note {
  margin: 10px 0 16px; font-size: 14px; line-height: 1.55; color: var(--ghc-muted);
}
.ghheat__note b { color: var(--ghc-text); font-weight: 700; }
.ghheat__legend {
  display: flex; align-items: center; gap: 5px;
  font-size: 12.5px; color: var(--ghc-muted); flex-wrap: wrap;
}
.ghheat__sw { width: 12px; height: 12px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }
.ghheat__sw--empty { border: 1px solid #d0d7de; }
.ghheat__legunit { margin-left: 2px; }
.ghheat__extra {
  display: flex; align-items: flex-start; gap: 7px; margin-top: 14px;
  font-size: 12px; line-height: 1.5; color: var(--ghc-muted); max-width: 330px;
}
.ghheat__extra .ghheat__sw { margin-top: 2px; }

/* ------------------------------------------------------------------ */
/* Tooltip                                                             */
/* ------------------------------------------------------------------ */
.ghchart__tooltip {
  position: absolute;
  z-index: 5;
  top: 0;
  left: 0;
  min-width: 128px;
  max-width: 260px;
  padding: 10px 12px;
  background: var(--ghc-card-bg);
  border: 1px solid var(--ghc-card-border);
  border-radius: 12px;
  /* Layered drop shadow from Primer Brand's ActionMenu (.ActionMenu__menu) */
  box-shadow:
    0px 100px 80px rgba(0, 0, 0, 0.01),
    0px 41px 33px rgba(0, 0, 0, 0.02),
    0px 22px 17px rgba(0, 0, 0, 0.02),
    0px 12px 10px rgba(0, 0, 0, 0.03),
    0px 6px 5px rgba(0, 0, 0, 0.04),
    0px 2px 2px rgba(0, 0, 0, 0.07);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -100%);
  /* Fade in on show and out on hide; quickly ease left/top when tracking
     from one point to the next. The position ease is suppressed on the
     first appearance (see chart.js) so the tooltip fades in place rather
     than sliding in from its previous spot. */
  transition: opacity 0.12s ease,
    left 0.1s cubic-bezier(0.4, 0, 0.2, 1),
    top 0.1s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: opacity, transform, left, top;
}
.ghchart__tooltip[data-visible="true"] { opacity: 1; }

.ghchart__tt-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--ghc-text);
  margin-bottom: 6px;
}

.ghchart__tt-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ghc-muted);
}
.ghchart__tt-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.ghchart__tt-name { flex: 1 1 auto; }
.ghchart__tt-value { font-weight: 700; color: var(--ghc-text); font-variant-numeric: tabular-nums; }

/* Bar tooltip body: a title line + free-form text lines (no swatches). */
.ghchart__tt-line {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ghc-muted);
}
.ghchart__tt-line:first-child { color: var(--ghc-text); font-weight: 600; }
.ghchart__tt-line + .ghchart__tt-line { margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ghchart__dot,
  .ghchart__guide,
  .ghchart__bar,
  .ghchart__pt,
  .ghchart__square,
  .ghheat__cell,
  .ghchart__tooltip { transition: none; }
}

</style>

<script data-wp-block-html="js">
/*
  Shared chart engine — reusable renderer for the blog-charts library.
  ------------------------------------------------------------------
  Exposes window.GHChartEngine = { renderers, render(config, mountEl?) }.
  Renderers are keyed by config.type (currently "line"); add a new viz type
  by registering GHChartEngine.renderers[<type>] = function (root, config) {…}.

  This same file is embedded verbatim at the top of each exported WordPress
  chart.js, followed by that chart's CONFIG + a small auto-boot. It has no
  external dependencies and is safe to include more than once on a page.
*/
(function (global) {
  "use strict";

  var SVGNS = "http://www.w3.org/2000/svg";

  function el(tag, attrs) {
    var node = document.createElementNS(SVGNS, tag);
    if (attrs) {
      for (var k in attrs) {
        if (Object.prototype.hasOwnProperty.call(attrs, k)) {
          node.setAttribute(k, attrs[k]);
        }
      }
    }
    return node;
  }

  function html(tag, cls) {
    var node = document.createElement(tag);
    if (cls) node.className = cls;
    return node;
  }

  function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }

  var renderers = {};

  /* ================================================================
     Line chart renderer
     ================================================================ */
  renderers.line = function (root, config) {
    // viewBox geometry (design units; scales fluidly to any width).
    var VB = { w: 1000, h: 560 };
    var M = { top: 30, right: 34, bottom: 56, left: 86 };
    // Inset the plotted points slightly from the axis so the first/last
    // months (e.g. "Jan") don't crowd the y-axis labels (e.g. "50").
    var PAD_X = 18;
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    // Reset (in case of re-render) and use a unique id prefix so multiple
    // charts on one page never share gradient ids.
    root.textContent = "";
    var uid = config.mountId;

    var n = config.labels.length;
    var yMin = config.yAxis.min;
    var yMax = config.yAxis.max;
    var step = config.yAxis.step;
    // Y-axis tick labels can format differently from the tooltip values
    // (e.g. a plain "40" on the axis but "40%" in the tooltip).
    var fmtAxis = config.formatAxis || config.formatValue;

    var plotW = innerW - 2 * PAD_X;
    var stepX = n <= 1 ? 0 : plotW / (n - 1);
    function xAt(i) { return n <= 1 ? M.left + innerW / 2 : M.left + PAD_X + stepX * i; }
    function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
    var baseY = yAt(yMin);

    /* ---- Title + divider (top) ---------------------------------- */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    /* ---- Header + legend ---------------------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      config.series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " line chart"
    });

    // Gradient defs (one per area series).
    var defs = el("defs");
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var g = el("linearGradient", {
        id: uid + "-grad-" + si, x1: "0", y1: "0", x2: "0", y2: "1"
      });
      g.appendChild(el("stop", { offset: "0", "stop-color": s.color, "stop-opacity": "0.28" }));
      g.appendChild(el("stop", { offset: "0.55", "stop-color": s.color, "stop-opacity": "0.08" }));
      g.appendChild(el("stop", { offset: "1", "stop-color": s.color, "stop-opacity": "0" }));
      defs.appendChild(g);
    });
    // Diagonal-hatch pattern for an optional "partial / projected" band.
    if (config.projection) {
      var pat = el("pattern", {
        id: uid + "-hatch", width: 7, height: 7,
        patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)"
      });
      pat.appendChild(el("line", {
        x1: 0, y1: 0, x2: 0, y2: 7,
        stroke: config.projection.color || "#8c959f",
        "stroke-width": 1.1, "stroke-opacity": 0.5
      }));
      defs.appendChild(pat);
    }
    svg.appendChild(defs);

    // Gridlines + y-axis labels.
    for (var v = yMin; v <= yMax + 0.0001; v += step) {
      var gy = yAt(v);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 22, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtAxis(v);
      svg.appendChild(yl);
    }

    // x-axis labels — either one per data point (default) or a sparse set
    // of ticks decoupled from the data (config.xTicks: [{at,text}, …]),
    // which suits a dense daily series that only needs month markers.
    var xTicks = config.xTicks ||
      config.labels.map(function (t, i) { return { at: i, text: t }; });
    xTicks.forEach(function (tk) {
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: xAt(tk.at), y: VB.h - M.bottom + 36,
        "text-anchor": tk.at === 0 ? "start" : (tk.at === n - 1 ? "end" : "middle")
      });
      xl.textContent = tk.text;
      svg.appendChild(xl);
    });

    // Optional hatched "partial / projected" band over the trailing region.
    if (config.projection) {
      var px = xAt(config.projection.fromIndex);
      svg.appendChild(el("rect", {
        "class": "ghchart__projection",
        x: px, y: M.top, width: (VB.w - M.right) - px, height: baseY - M.top,
        fill: "url(#" + uid + "-hatch)"
      }));
    }

    // Optional dashed threshold line + floating label (e.g. a 50% target).
    if (config.threshold) {
      var thy = yAt(config.threshold.value);
      svg.appendChild(el("line", {
        "class": "ghchart__threshold",
        x1: M.left, y1: thy, x2: VB.w - M.right, y2: thy,
        stroke: config.threshold.color
      }));
      if (config.threshold.label) {
        var thl = el("text", {
          "class": "ghchart__threshold-label",
          x: M.left + PAD_X, y: thy - 10, "text-anchor": "start",
          fill: config.threshold.color
        });
        thl.textContent = config.threshold.label;
        svg.appendChild(thl);
      }
    }

    // Precompute point coords per series.
    var pts = config.series.map(function (s) {
      return s.values.map(function (val, i) { return { x: xAt(i), y: yAt(val) }; });
    });

    // Smooth (Catmull-Rom -> cubic Bezier) path builder.
    function smooth(points) {
      if (!points.length) return "";
      if (points.length < 2) return "M" + points[0].x + "," + points[0].y;
      var d = "M " + points[0].x + "," + points[0].y;
      for (var i = 0; i < points.length - 1; i++) {
        var p0 = points[i - 1] || points[i];
        var p1 = points[i];
        var p2 = points[i + 1];
        var p3 = points[i + 2] || p2;
        var t = 1 / 6;
        var c1x = p1.x + (p2.x - p0.x) * t;
        var c1y = p1.y + (p2.y - p0.y) * t;
        var c2x = p2.x - (p3.x - p1.x) * t;
        var c2y = p2.y - (p3.y - p1.y) * t;
        d += " C " + c1x + "," + c1y + " " + c2x + "," + c2y + " " + p2.x + "," + p2.y;
      }
      return d;
    }

    // Area fills (drawn first, behind lines).
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var p = pts[si];
      var d = smooth(p) + " L " + p[p.length - 1].x + "," + baseY +
              " L " + p[0].x + "," + baseY + " Z";
      svg.appendChild(el("path", { d: d, fill: "url(#" + uid + "-grad-" + si + ")" }));
    });

    // Vertical hover guide (hidden until hover).
    var guide = el("line", {
      "class": "ghchart__guide",
      x1: xAt(0), y1: M.top - 6, x2: xAt(0), y2: baseY
    });
    svg.appendChild(guide);

    // Lines (draw in reverse so the first series sits on top).
    config.series.slice().reverse().forEach(function (s) {
      var si = config.series.indexOf(s);
      svg.appendChild(el("path", {
        "class": "ghchart__line",
        d: smooth(pts[si]),
        stroke: s.color,
        "stroke-width": s.width || (si === 0 ? 4 : 3.25)
      }));
    });

    // Dots (keep references for hover emphasis). When config.hideDots is set
    // (dense daily lines), dots rest invisible and only the hovered one shows.
    var dotEls = config.series.map(function (s, si) {
      return pts[si].map(function (pt) {
        var c = el("circle", {
          "class": "ghchart__dot",
          cx: pt.x, cy: pt.y,
          r: config.hideDots ? 0 : (si === 0 ? 4.5 : 4),
          fill: s.color
        });
        svg.appendChild(c);
        return c;
      });
    });

    // Transparent hit area for pointer capture.
    var hit = el("rect", {
      "class": "ghchart__hit",
      x: M.left,
      y: M.top - 6,
      width: innerW,
      height: baseY - M.top + 12
    });
    svg.appendChild(hit);

    root.appendChild(svg);

    /* ---- Tooltip ------------------------------------------------ */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    tip.appendChild(tipTitle);
    var tipRows = config.series.map(function (s) {
      var row = html("div", "ghchart__tt-row");
      var sw = html("span", "ghchart__tt-swatch");
      sw.style.background = s.color;
      var name = html("span", "ghchart__tt-name");
      name.textContent = s.name;
      var val = html("span", "ghchart__tt-value");
      row.appendChild(sw);
      row.appendChild(name);
      row.appendChild(val);
      tip.appendChild(row);
      return val;
    });
    root.appendChild(tip);

    /* ---- Hover / focus interactivity ---------------------------- */
    var activeIndex = -1;
    var baseR = config.series.map(function (s, si) {
      return config.hideDots ? 0 : (si === 0 ? 4.5 : 4);
    });
    // Radius of the single emphasized dot under the pointer.
    var dotActiveR = config.series.map(function (s, si) {
      return config.hideDots ? 6 : baseR[si] + 2.5;
    });
    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function indexFromClientX(clientX) {
      var rect = svg.getBoundingClientRect();
      var relX = (clientX - rect.left) / rect.width * VB.w;
      var i = stepX ? Math.round((relX - (M.left + PAD_X)) / stepX) : 0;
      return clamp(i, 0, n - 1);
    }

    function show(i) {
      if (i === activeIndex) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      guide.setAttribute("x1", xAt(i));
      guide.setAttribute("x2", xAt(i));
      guide.style.opacity = "1";

      // Emphasize the hovered column's dots; reset the rest.
      dotEls.forEach(function (col, si) {
        col.forEach(function (c, ci) {
          c.setAttribute("r", ci === i ? dotActiveR[si] : baseR[si]);
        });
      });

      // Fill tooltip.
      tipTitle.textContent = config.labels[i];
      config.series.forEach(function (s, si) {
        tipRows[si].textContent = config.formatValue(s.values[i]);
      });

      // Anchor above the highest point in this column.
      var topSi = 0, topY = Infinity;
      config.series.forEach(function (s, si) {
        var yy = yAt(s.values[i]);
        if (yy < topY) { topY = yy; topSi = si; }
      });
      var anchor = dotEls[topSi][i].getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      // On the first appearance, fade in exactly at the point: suppress the
      // position ease so it doesn't slide in from its previous resting spot.
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      // Clamp horizontally within the container.
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      // Flip below the point if there's no room above.
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      // Commit that position without animating, then restore the CSS
      // transition so subsequent point-to-point moves ease into place.
      if (suppress) {
        void tip.offsetWidth;
        tip.style.transition = "";
      }
    }

    function hide() {
      activeIndex = -1;
      guide.style.opacity = "0";
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (col, si) {
        col.forEach(function (c) { c.setAttribute("r", baseR[si]); });
      });
    }

    hit.addEventListener("pointermove", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerdown", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerleave", hide);

    // Keyboard access.
    svg.setAttribute("tabindex", "0");
    svg.addEventListener("keydown", function (e) {
      if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
        var start = activeIndex < 0 ? 0 : activeIndex;
        show(clamp(start + (e.key === "ArrowRight" ? 1 : -1), 0, n - 1));
        e.preventDefault();
      } else if (e.key === "Escape") {
        hide();
      }
    });
    svg.addEventListener("blur", hide);
  };

  /* ================================================================
     Bar chart renderer (composite)
     ----------------------------------------------------------------
     One "graph" can stack multiple bar panels in a single block:
       config = { type:"bar", title, subtitle?, series:[{name,color}],
                  barRadius?, panels:[ panel… ], caption? }
       panel  = { title?, labels[], values[], yAxis:{min,max,step},
                  formatValue(v), height?, maxBarVB?,
                  tooltip(i, panel) -> { title, lines:[…] } }
     No printed value labels — values surface on hover (consistent with
     the line chart). Bars use a single tunable color with rounded tops.
     ================================================================ */
  renderers.bar = function (root, config) {
    root.textContent = "";

    var barColor = (config.series && config.series[0] && config.series[0].color) || "#ff7b72";
    var barRadius = config.barRadius != null ? config.barRadius : 8;

    // Per-bar fill: a bar may point at a different series (e.g. a muted
    // "preliminary" color) via panel.barSeries[index]; otherwise series[0].
    function barColorFor(panel, j) {
      var si = (panel.barSeries && panel.barSeries[j] != null) ? panel.barSeries[j] : 0;
      var s = config.series && config.series[si];
      return (s && s.color) || barColor;
    }

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Title + divider (+ optional subtitle) ------------------ */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip for all panels ---------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var allBars = [];       // { el, pi, bi }
    var activeKey = null;   // "pi:bi" of the hovered bar

    function hideTip() {
      activeKey = null;
      tip.setAttribute("data-visible", "false");
      allBars.forEach(function (b) { b.el.style.opacity = "1"; });
    }

    // Hoisted: referenced by the per-column pointer handlers below.
    function showTip(pi, bi, panel, panelBars) {
      var reappearing = activeKey === null;
      activeKey = pi + ":" + bi;

      allBars.forEach(function (b) {
        b.el.style.opacity = (b.pi === pi && b.bi === bi) ? "1" : "0.42";
      });

      var content = panel.tooltip
        ? panel.tooltip(bi, panel)
        : { title: (panel.labels || [])[bi],
            lines: [ (panel.formatValue || String)(panel.values[bi]) ] };

      tipTitle.textContent = content.title != null ? content.title : "";
      tipBody.textContent = "";
      (content.lines || []).forEach(function (ln) {
        var row = html("div", "ghchart__tt-line");
        row.textContent = ln;
        tipBody.appendChild(row);
      });

      var anchor = panelBars[bi].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels ------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var wrap = html("div", "ghchart__panel");
      if (panel.title) {
        var pt = html("h3", "ghchart__panel-title");
        pt.textContent = panel.title;
        wrap.appendChild(pt);
      }

      var VB = { w: 1000, h: panel.height || 360 };
      var M = { top: 20, right: 20, bottom: 48, left: 84 };
      var innerW = VB.w - M.left - M.right;
      var innerH = VB.h - M.top - M.bottom;

      var labels = panel.labels || [];
      var values = panel.values || [];
      var nb = labels.length;
      var ax = panel.yAxis || { min: 0, max: Math.max.apply(null, values.concat([1])), step: 0 };
      var yMin = ax.min, yMax = ax.max, step = ax.step || 0;
      var fmt = panel.formatValue || function (v) { return String(v); };

      function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
      var baseY = yAt(yMin);

      var slot = nb ? innerW / nb : innerW;
      var maxBarVB = panel.maxBarVB != null ? panel.maxBarVB : 150;
      var barW = Math.min(slot * 0.62, maxBarVB);
      function xCenter(i) { return M.left + slot * (i + 0.5); }

      var svg = el("svg", {
        "class": "ghchart__plot ghchart__plot--bar",
        viewBox: "0 0 " + VB.w + " " + VB.h,
        preserveAspectRatio: "xMidYMid meet",
        role: "img",
        "aria-label": (panel.title || config.title) + " bar chart"
      });

      // Gridlines + y-axis labels.
      if (step > 0) {
        for (var v = yMin; v <= yMax + 1e-9; v += step) {
          var gy = yAt(v);
          svg.appendChild(el("line", {
            "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
          }));
          var yl = el("text", {
            "class": "ghchart__axislabel ghchart__axislabel--y",
            x: M.left - 18, y: gy, dy: "0.32em", "text-anchor": "end"
          });
          yl.textContent = fmt(v);
          svg.appendChild(yl);
        }
      }

      // Solid baseline.
      svg.appendChild(el("line", {
        "class": "ghchart__axisline", x1: M.left, y1: baseY, x2: VB.w - M.right, y2: baseY
      }));

      // x-axis labels.
      for (var i = 0; i < nb; i++) {
        var xl = el("text", {
          "class": "ghchart__axislabel ghchart__axislabel--x",
          x: xCenter(i), y: VB.h - M.bottom + 30, "text-anchor": "middle"
        });
        if (panel.xLabelSize) xl.setAttribute("style", "font-size:" + panel.xLabelSize + "px");
        xl.textContent = labels[i];
        svg.appendChild(xl);
      }

      // Bars (rounded top corners only) + full-height hover columns.
      var panelBars = [];
      for (var j = 0; j < nb; j++) {
        var cx = xCenter(j);
        var topY = yAt(values[j]);
        var barH = baseY - topY;
        var x = cx - barW / 2;
        var r = Math.max(0, Math.min(barRadius, barW / 2, barH));
        var d = "M " + x + " " + baseY +
                " L " + x + " " + (topY + r) +
                " Q " + x + " " + topY + " " + (x + r) + " " + topY +
                " L " + (x + barW - r) + " " + topY +
                " Q " + (x + barW) + " " + topY + " " + (x + barW) + " " + (topY + r) +
                " L " + (x + barW) + " " + baseY + " Z";
        var bar = el("path", { "class": "ghchart__bar", d: d, fill: barColorFor(panel, j) });
        svg.appendChild(bar);
        var rec = { el: bar, pi: pi, bi: j };
        panelBars.push(rec);
        allBars.push(rec);

        var col = el("rect", {
          "class": "ghchart__barhit",
          x: M.left + slot * j, y: M.top - 6, width: slot, height: baseY - M.top + 6
        });
        (function (idx) {
          col.addEventListener("pointermove", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerdown", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerleave", hideTip);
        })(j);
        svg.appendChild(col);
      }

      /* ---- Annotation layer (drawn on top; pointer-events: none) ----
         · always-on value labels above chosen bars (panel.valueLabels)
         · anchor lines: a dotted horizontal reference at one bar's top
           extended to another, with a floating pill (panel.annotations) */
      var vlabels = panel.valueLabels || null;
      if (vlabels) {
        for (var vk in vlabels) {
          if (!Object.prototype.hasOwnProperty.call(vlabels, vk)) continue;
          var vi = +vk;
          var vt = el("text", {
            "class": "ghchart__barvalue",
            x: xCenter(vi), y: yAt(values[vi]) - 12, "text-anchor": "middle"
          });
          vt.textContent = vlabels[vk];
          svg.appendChild(vt);
        }
      }

      (panel.annotations || []).forEach(function (an) {
        if (an.type !== "anchor") return;
        var yLine = yAt(values[an.fromIndex]);
        svg.appendChild(el("line", {
          "class": "ghchart__anno-line",
          x1: xCenter(an.fromIndex) - barW / 2, y1: yLine,
          x2: xCenter(an.toIndex) + barW / 2, y2: yLine
        }));

        var chipIdx = an.chipAtIndex != null ? an.chipAtIndex : an.toIndex;
        var cx = xCenter(chipIdx);
        var cy = (yAt(values[chipIdx]) + yLine) / 2;
        var label = an.label || "";
        var fs = 19;
        var chipW = label.length * fs * 0.55 + 24;
        var chipH = fs + 16;
        svg.appendChild(el("rect", {
          "class": "ghchart__anno-chip",
          x: cx - chipW / 2, y: cy - chipH / 2, width: chipW, height: chipH, rx: chipH / 2
        }));
        var ct = el("text", {
          "class": "ghchart__anno-chip-text",
          x: cx, y: cy, dy: "0.34em", "text-anchor": "middle", "font-size": fs
        });
        ct.textContent = label;
        svg.appendChild(ct);
      });

      wrap.appendChild(svg);
      root.appendChild(wrap);
    });

    /* ---- Caption (optional rich copy) --------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Calendar heatmap renderer (GitHub contribution-graph style)
     ----------------------------------------------------------------
     One square per calendar day across a month range. Each panel is
     shaded on its own scale — every day in a month shares that month's
     level (the underlying series is monthly). Each panel's 5-step
     faint->dark ramp is derived from a single tunable base color.
     ================================================================ */
  renderers.heatmap = function (root, config) {
    root.textContent = "";

    var WHITE = [255, 255, 255], BLACK = [0, 0, 0];
    function hexToRgb(h) {
      h = String(h).replace("#", "");
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
    }
    function rgbToHex(c) {
      return "#" + c.map(function (x) {
        x = Math.max(0, Math.min(255, Math.round(x)));
        return (x < 16 ? "0" : "") + x.toString(16);
      }).join("");
    }
    function mix(a, b, t) {
      return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
    }
    // 5-step contribution ramp (level 0 faint -> level 4 dark) from one hex.
    function rampFrom(baseHex) {
      var b = hexToRgb(baseHex);
      return [
        rgbToHex(mix(b, WHITE, 0.80)),
        rgbToHex(mix(b, WHITE, 0.52)),
        rgbToHex(mix(b, WHITE, 0.20)),
        baseHex,
        rgbToHex(mix(b, BLACK, 0.30))
      ];
    }

    function trim0(s) { return s.replace(/\.0$/, ""); }
    function fmtM(v) {
      if (v >= 1000) { var g = v / 1000; return (g >= 10 ? Math.round(g) : trim0(g.toFixed(1))) + "B"; }
      if (v >= 1) return trim0(v.toFixed(1)) + "M";
      return Math.round(v * 1000) + "K";
    }

    var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    var MON_FULL = ["January", "February", "March", "April", "May", "June",
      "July", "August", "September", "October", "November", "December"];
    var WD = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

    var year = config.year || 2026;
    var startM = config.startMonth != null ? config.startMonth : 0;
    var endM = config.endMonth != null ? config.endMonth : 5;
    var emptyColor = config.emptyColor || "#ebedf0";

    function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
    function parseISO(s) { var p = s.split("-"); return new Date(+p[0], +p[1] - 1, +p[2]); }
    function dayStart(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); }

    /* ---- Eyebrow + title + subtitle ----------------------------- */
    if (config.eyebrow) {
      var eb = html("div", "ghheat__eyebrow");
      eb.appendChild(html("span", "ghheat__eyedot"));
      var ebt = html("span", "ghheat__eyetext");
      ebt.textContent = config.eyebrow;
      eb.appendChild(ebt);
      root.appendChild(eb);
    }
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle ghheat__subtitle");
      sub.innerHTML = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip -------------------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }
    var activeCell = null;
    function hideTip() { activeCell = null; tip.setAttribute("data-visible", "false"); }
    function showTip(rectEl, titleText, lines) {
      var reappearing = activeCell === null;
      activeCell = rectEl;
      tipTitle.textContent = titleText;
      tipBody.textContent = "";
      lines.forEach(function (ln) {
        var r = html("div", "ghchart__tt-line");
        r.innerHTML = ln;
        tipBody.appendChild(r);
      });
      var a = rectEl.getBoundingClientRect(), rr = root.getBoundingClientRect();
      var left = a.left + a.width / 2 - rr.left, top = a.top - rr.top - 12;
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";
      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = a.bottom - rr.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels -------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var base = (config.series && config.series[pi] && config.series[pi].color) || panel.base || "#3094ff";
      var ramp = rampFrom(base);
      var strong = ramp[4];

      var totals = panel.months || [];
      var activeVals = [];
      for (var mi = startM; mi <= endM; mi++) { if (totals[mi] != null) activeVals.push(totals[mi]); }
      var vmin = activeVals.length ? Math.min.apply(null, activeVals) : 0;
      var vmax = activeVals.length ? Math.max.apply(null, activeVals) : 1;
      function levelFor(t) {
        if (t == null) return -1;
        if (vmax === vmin) return 4;
        return Math.round((t - vmin) / (vmax - vmin) * 4);
      }

      var af = panel.activeFrom ? parseISO(panel.activeFrom) : new Date(year, startM, 1);
      var at = panel.activeTo ? parseISO(panel.activeTo) : new Date(year, endM, daysInMonth(year, endM));

      var wrap = html("div", "ghheat__panel");

      var head = html("div", "ghheat__phead");
      var badge = html("span", "ghheat__badge");
      badge.textContent = panel.badge != null ? panel.badge : (pi + 1);
      badge.style.background = strong;
      head.appendChild(badge);
      var pt = html("span", "ghheat__ptitle");
      pt.textContent = panel.title;
      head.appendChild(pt);
      if (panel.descriptor) {
        var pd = html("span", "ghheat__pdesc");
        pd.textContent = panel.descriptor;
        head.appendChild(pd);
      }
      wrap.appendChild(head);

      var body = html("div", "ghheat__body");

      /* ---- Calendar grid (SVG) ---- */
      var gridStart = new Date(year, startM, 1);
      var gs = new Date(gridStart);
      gs.setDate(gs.getDate() - gs.getDay());            // back to Sunday
      var gridEnd = new Date(year, endM, daysInMonth(year, endM));
      var ge = new Date(gridEnd);
      ge.setDate(ge.getDate() + (6 - ge.getDay()));      // forward to Saturday
      var nCols = Math.round((ge - gs) / (7 * 86400000)) + 1;

      var cell = 13, sq = 11, rad = 2.5, labL = 34, labT = 18;
      var W = labL + nCols * cell, H = labT + 7 * cell;
      var svg = el("svg", {
        "class": "ghheat__grid",
        viewBox: "0 0 " + W + " " + H,
        preserveAspectRatio: "xMinYMin meet",
        role: "img",
        "aria-label": panel.title + " calendar heatmap"
      });

      [1, 3, 5].forEach(function (r) {
        var t = el("text", {
          "class": "ghheat__wd", x: labL - 6, y: labT + r * cell + sq / 2,
          dy: "0.32em", "text-anchor": "end"
        });
        t.textContent = WD[r];
        svg.appendChild(t);
      });

      // Month labels above the column where each month begins.
      for (var lm = startM; lm <= endM; lm++) {
        var first = new Date(year, lm, 1);
        var colOf = Math.floor((dayStart(first) - gs) / (7 * 86400000));
        var mlab = el("text", { "class": "ghheat__month", x: labL + colOf * cell, y: 12, "text-anchor": "start" });
        mlab.textContent = MON[lm];
        svg.appendChild(mlab);
      }

      var afS = dayStart(af), atS = dayStart(at);
      var d = new Date(gs);
      for (var i = 0; i < nCols * 7; i++) {
        var row = d.getDay();
        var col = Math.floor(i / 7);
        var x = labL + col * cell, y = labT + row * cell;
        var m = d.getMonth();
        var inRange = d.getFullYear() === year && m >= startM && m <= endM;
        var ds = dayStart(d);
        var inWindow = inRange && ds >= afS && ds <= atS;
        var hasData = inWindow && totals[m] != null;
        var lvl = hasData ? levelFor(totals[m]) : -1;
        var cellEl = el("rect", {
          "class": "ghheat__cell", x: x, y: y, width: sq, height: sq, rx: rad,
          fill: lvl >= 0 ? ramp[lvl] : emptyColor
        });
        if (inRange) {
          (function (dt, mm, has) {
            var dateStr = WD[dt.getDay()] + " \u00b7 " + MON[mm] + " " + dt.getDate() + ", " + year;
            var lines;
            if (has) {
              var daily = totals[mm] / daysInMonth(year, mm);
              lines = [
                "<b>" + fmtM(daily) + "</b> " + panel.legendLabel,
                MON_FULL[mm] + " \u00b7 " + fmtM(totals[mm]) + " " + panel.unitTotal
              ];
            } else {
              lines = [panel.emptyNote || "No vetted data"];
            }
            cellEl.addEventListener("pointermove", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerdown", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerleave", hideTip);
          })(new Date(d), m, hasData);
        } else {
          cellEl.setAttribute("class", "ghheat__cell ghheat__cell--pad");
        }
        svg.appendChild(cellEl);
        d.setDate(d.getDate() + 1);
      }

      var gridwrap = html("div", "ghheat__gridwrap");
      gridwrap.appendChild(svg);
      body.appendChild(gridwrap);

      /* ---- Side column: stat + note + legend + extra ---- */
      var side = html("div", "ghheat__side");
      if (panel.stat) {
        var stat = html("div", "ghheat__stat");
        var big = html("span", "ghheat__statbig");
        big.textContent = panel.stat.big;
        big.style.color = strong;
        stat.appendChild(big);
        if (panel.stat.small) {
          var small = html("span", "ghheat__statsmall");
          small.textContent = panel.stat.small;
          stat.appendChild(small);
        }
        side.appendChild(stat);
      }
      if (panel.note) {
        var note = html("p", "ghheat__note");
        note.innerHTML = panel.note;
        side.appendChild(note);
      }
      var leg = html("div", "ghheat__legend");
      var lw1 = html("span", "ghheat__legword"); lw1.textContent = "Fewer"; leg.appendChild(lw1);
      ramp.forEach(function (c) {
        var s = html("span", "ghheat__sw"); s.style.background = c; leg.appendChild(s);
      });
      var lw2 = html("span", "ghheat__legword"); lw2.textContent = "More"; leg.appendChild(lw2);
      if (panel.legendLabel) {
        var lu = html("span", "ghheat__legunit"); lu.textContent = "\u00b7 " + panel.legendLabel; leg.appendChild(lu);
      }
      side.appendChild(leg);
      if (panel.extraNote) {
        var ex = html("div", "ghheat__extra");
        var exsw = html("span", "ghheat__sw ghheat__sw--empty");
        exsw.style.background = emptyColor;
        ex.appendChild(exsw);
        var ext = html("span", "ghheat__extratext");
        ext.innerHTML = panel.extraNote;
        ex.appendChild(ext);
        side.appendChild(ex);
      }
      body.appendChild(side);

      wrap.appendChild(body);
      root.appendChild(wrap);
    });

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Scatter renderer — X/Y numeric plot with categorical points.
     Used for cost-vs-quality benchmark charts: a highlighted "primary"
     point (glow halo), a "reference" point with a dotted crosshair, and
     muted "other" points. Values never print on the marks — a point's
     name shows for identity and the exact (y, x) reveals on hover.
     ================================================================ */
  renderers.scatter = function (root, config) {
    root.textContent = "";
    var uid = config.mountId;

    var VB = { w: 1000, h: 600 };
    var M = { top: 40, right: 30, bottom: 74, left: 96 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var fmtX = xa.format || function (v) { return String(v); };
    var fmtY = ya.format || function (v) { return String(v); };
    var tipSuffix = config.tipSuffix || "";

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) { return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min)); }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function colorOf(pt) {
      var s = config.series && config.series[pt.series || 0];
      return (s && s.color) || "#8c959f";
    }

    /* ---- Title (compact narrative headline, no rule) ------------ */
    var title = html("div", "ghchart__headline");
    title.textContent = config.title;
    root.appendChild(title);

    /* ---- Header + legend (from series) -------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      (config.series || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch ghchart__swatch--dot");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--scatter",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " scatter plot"
    });

    // Gridlines (both directions) + tick labels.
    var eps = 1e-6;
    for (var yv = ya.min; yv <= ya.max + eps; yv += ya.step) {
      var gy = yAt(yv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(yv);
      svg.appendChild(yl);
    }
    for (var xv = xa.min; xv <= xa.max + eps; xv += xa.step) {
      var gx = xAt(xv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline ghchart__gridline--v",
        x1: gx, y1: M.top, x2: gx, y2: baseY
      }));
      var isFirst = Math.abs(xv - xa.min) < eps;
      var isLast = Math.abs(xv - xa.max) < eps;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 34,
        "text-anchor": isFirst ? "start" : (isLast ? "end" : "middle")
      });
      xl.textContent = fmtX(xv);
      svg.appendChild(xl);
    }

    // Axis titles.
    if (xa.title) {
      var xt = el("text", {
        "class": "ghchart__axistitle",
        x: M.left + innerW / 2, y: VB.h - 12, "text-anchor": "middle"
      });
      xt.textContent = xa.title;
      svg.appendChild(xt);
    }
    if (ya.title) {
      var ytX = 24, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle",
        x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Reference crosshair through the flagged point (dual dotted lines).
    var refPt = (config.points || []).filter(function (p) { return p.reference; })[0];
    if (refPt) {
      var rColor = colorOf(refPt);
      var rx = xAt(refPt.x), ry = yAt(refPt.y);
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: rx, y1: M.top, x2: rx, y2: baseY, stroke: rColor
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: M.left, y1: ry, x2: VB.w - M.right, y2: ry, stroke: rColor
      }));
    }

    // Points: halo (highlight/reference) + dot + name label.
    var dotEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y), c = colorOf(pt);
      var big = pt.highlight || pt.reference;

      if (big) {
        svg.appendChild(el("circle", {
          "class": "ghchart__halo", cx: cx, cy: cy, r: 17, fill: c
        }));
      }
      var dot = el("circle", {
        "class": "ghchart__pt", cx: cx, cy: cy,
        r: big ? 9 : 5.5, fill: c
      });
      svg.appendChild(dot);
      dotEls.push({ el: dot, baseR: big ? 9 : 5.5 });

      // Name label — bold+colored for the story points, muted for the rest.
      var pos = pt.labelPos || (pt.highlight ? "below" : pt.reference ? "above" : "right");
      var dx = 0, dy = 0, anchor = "middle";
      var gap = big ? 15 : 9;
      if (pos === "right") { dx = gap + 4; dy = 5; anchor = "start"; }
      else if (pos === "left") { dx = -(gap + 4); dy = 5; anchor = "end"; }
      else if (pos === "above") { dy = -(gap + 6); anchor = "middle"; }
      else { dy = gap + 16; anchor = "middle"; }  // below
      var lbl = el("text", {
        "class": "ghchart__pt-label" + (big ? " ghchart__pt-label--strong" : ""),
        x: cx + dx, y: cy + dy, "text-anchor": anchor,
        fill: big ? c : null
      });
      lbl.textContent = pt.label;
      svg.appendChild(lbl);

      // Transparent hit target for hover/focus.
      var hit = el("circle", {
        "class": "ghchart__pthit", cx: cx, cy: cy, r: 20,
        tabindex: "0", role: "img",
        "aria-label": pt.label + ": " + fmtY(pt.y) + ", " + fmtX(pt.x) + tipSuffix
      });
      svg.appendChild(hit);
      hitEls.push(hit);
    });

    root.appendChild(svg);

    /* ---- Tooltip (title + free lines) --------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      dotEls.forEach(function (d, di) {
        d.el.setAttribute("r", di === i ? d.baseR + 2.5 : d.baseR);
      });

      tipTitle.textContent = pt.label;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = fmtY(pt.y) + " · " + fmtX(pt.x) + tipSuffix;
      tipBody.appendChild(line);

      var anchor = dotEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (d) { d.el.setAttribute("r", d.baseR); });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
  };

  /* ================================================================
     Trend renderer — dated time-series with a narrative headline.
     One renderer covers both HydraFusion time charts via config:
       • linear OR log y-axis (yAxis.scale), explicit yAxis.ticks;
       • phase background columns (with optional per-phase average);
       • filled squares (values reveal on hover — never printed);
       • red-X "invalid run" markers with leader-line annotations;
       • a dashed trend line + optional ±band + slope label;
       • an optional horizontal reference line, bracket, and final callout.
     ================================================================ */
  renderers.trend = function (root, config) {
    root.textContent = "";

    var VB = { w: 1000, h: 560 };
    var M = { top: 64, right: 56, bottom: 56, left: 104 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var isLog = ya.scale === "log";
    var fmtY = ya.format || function (v) { return String(v); };
    var series = config.series || [];
    function sColor(i, fb) {
      var s = series[i];
      return (s && s.color) || fb;
    }
    var cPoint = sColor(0, "#8250df");
    var cInvalid = sColor(1, "#cf222e");
    var cTrend = (config.trend && config.trend.color) || sColor(0, "#8250df");

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) {
      if (isLog) {
        var lo = Math.log(ya.min) / Math.LN10, hi = Math.log(ya.max) / Math.LN10;
        var t = (Math.log(v) / Math.LN10 - lo) / (hi - lo);
        return M.top + innerH * (1 - t);
      }
      return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min));
    }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Headline + subtitle + legend (HTML flow above the plot) --- */
    var headline = html("div", "ghchart__headline");
    headline.textContent = config.title;
    root.appendChild(headline);
    if (config.subtitle) {
      var sub = html("div", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }
    if (!config.hideLegend && series.length) {
      var legend = html("div", "ghchart__legend ghchart__legend--trend");
      /* Build one legend glyph. The "invalid" glyph mirrors the on-plot marker
         exactly — a red diamond outline with an X through it (same side=13 /
         q=3.4 proportions used to draw the mark below) — so the key matches
         the mark on the chart. */
      function trendGlyph(glyph, color) {
        if (glyph === "invalid") {
          var g = el("svg", {
            "class": "ghchart__glyph ghchart__glyph--invalid",
            viewBox: "0 0 22 22", width: "15", height: "15",
            style: "overflow:visible"
          });
          g.appendChild(el("rect", {
            x: 4.5, y: 4.5, width: 13, height: 13, rx: 1.5,
            transform: "rotate(45 11 11)", fill: "none",
            stroke: color, "stroke-width": 2
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 7.6, x2: 14.4, y2: 14.4,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 14.4, x2: 14.4, y2: 7.6,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          return g;
        }
        var gl = html("span", "ghchart__glyph ghchart__glyph--" + glyph);
        if (glyph === "line") { gl.style.borderTopColor = color; }
        else { gl.style.background = color; gl.style.borderColor = color; }
        if (glyph === "diamond") { gl.style.background = "transparent"; }
        return gl;
      }
      series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "square", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      (config.legendExtra || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "line", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      root.appendChild(legend);
    }

    /* ---- SVG plot ------------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--trend",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title
    });

    // Phase background columns + separators + labels (+ optional average).
    (config.phases || []).forEach(function (ph, i) {
      var x0 = xAt(ph.from), x1 = xAt(ph.to);
      svg.appendChild(el("rect", {
        "class": "ghchart__phase" + (i % 2 ? " ghchart__phase--alt" : ""),
        x: x0, y: M.top, width: Math.max(0, x1 - x0), height: innerH
      }));
      if (i > 0) {
        svg.appendChild(el("line", {
          "class": "ghchart__phase-sep", x1: x0, y1: M.top, x2: x0, y2: baseY
        }));
      }
      var pl = el("text", {
        "class": "ghchart__phase-label", x: (x0 + x1) / 2, y: M.top - 40,
        "text-anchor": "middle"
      });
      pl.textContent = ph.label;
      svg.appendChild(pl);
      if (ph.avg) {
        var pa = el("text", {
          "class": "ghchart__phase-avg", x: (x0 + x1) / 2, y: M.top + 24,
          "text-anchor": "middle", fill: cPoint
        });
        pa.textContent = ph.avg;
        svg.appendChild(pa);
      }
    });

    // Horizontal gridlines + y tick labels (skip a gridline at the reference y).
    var refY = config.reference ? config.reference.y : null;
    (ya.ticks || []).forEach(function (tv) {
      var gy = yAt(tv);
      if (refY === null || Math.abs(tv - refY) > 1e-9) {
        svg.appendChild(el("line", {
          "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
        }));
      }
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(tv);
      svg.appendChild(yl);
    });

    // X tick labels (dated).
    (xa.ticks || []).forEach(function (t) {
      var gx = xAt(t.at);
      var first = Math.abs(t.at - xa.min) < 1e-9, last = Math.abs(t.at - xa.max) < 1e-9;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 30, "text-anchor": first ? "start" : (last ? "end" : "middle")
      });
      xl.textContent = t.text;
      svg.appendChild(xl);
    });

    // Axis titles.
    if (ya.title) {
      var ytX = 22, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle", x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Trend: optional ±band (linear only), then the dashed line + slope label.
    var tr = config.trend;
    if (tr) {
      function trendY(x) {
        return tr.from.y + (tr.to.y - tr.from.y) * (x - tr.from.x) / (tr.to.x - tr.from.x);
      }
      if (tr.band && !isLog) {
        var p = tr.band;
        var pts = [
          [xAt(tr.from.x), yAt(trendY(tr.from.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) - p)],
          [xAt(tr.from.x), yAt(trendY(tr.from.x) - p)]
        ].map(function (a) { return a[0] + "," + a[1]; }).join(" ");
        svg.appendChild(el("polygon", {
          "class": "ghchart__trendband", points: pts, fill: cTrend
        }));
      }
      svg.appendChild(el("line", {
        "class": "ghchart__trendline",
        x1: xAt(tr.from.x), y1: yAt(tr.from.y),
        x2: xAt(tr.to.x), y2: yAt(tr.to.y), stroke: cTrend
      }));
      if (tr.label) {
        var lx, lyy;
        if (tr.labelAt) { lx = xAt(tr.labelAt.x); lyy = yAt(tr.labelAt.y); }
        else {
          lx = xAt(tr.to.x) + (tr.labelDX == null ? -6 : tr.labelDX);
          lyy = yAt(tr.to.y) + (tr.labelDY == null ? -12 : tr.labelDY);
        }
        var sl = el("text", {
          "class": "ghchart__slope", x: lx, y: lyy,
          "text-anchor": tr.labelAnchor || "end", fill: cTrend
        });
        sl.textContent = tr.label;
        svg.appendChild(sl);
      }
    }

    // Reference line (horizontal dashed) + right-aligned label.
    if (config.reference) {
      var rc = config.reference.color || "#24292f";
      var ryy = yAt(config.reference.y);
      svg.appendChild(el("line", {
        "class": "ghchart__refline", x1: M.left, y1: ryy, x2: VB.w - M.right, y2: ryy,
        stroke: rc
      }));
      if (config.reference.label) {
        var rl = el("text", {
          "class": "ghchart__reflabel", x: VB.w - M.right, y: ryy - 8,
          "text-anchor": "end", fill: rc
        });
        rl.textContent = config.reference.label;
        svg.appendChild(rl);
      }
    }

    // Data squares (values reveal on hover only).
    var side = 13, hSide = side / 2;
    var sqEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y);
      var sq = el("rect", {
        "class": "ghchart__square", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 2, fill: cPoint
      });
      svg.appendChild(sq);
      sqEls.push({ el: sq, cx: cx, cy: cy });

      // Hover/focus hit target — omitted when hover is disabled so the
      // (illustrative) point values never surface, on screen or to a11y.
      if (!config.disableHover) {
        var hit = el("rect", {
          "class": "ghchart__squarehit", x: cx - 13, y: cy - 13,
          width: 26, height: 26, tabindex: "0", role: "img",
          "aria-label": (pt.d || "") + " " + fmtY(pt.y)
        });
        svg.appendChild(hit);
        hitEls.push(hit);
      }
    });

    // Invalid-run markers: red diamond + X, leader line, annotation text.
    (config.invalids || []).forEach(function (iv) {
      var cx = xAt(iv.x), cy = yAt(iv.y);
      svg.appendChild(el("rect", {
        "class": "ghchart__invalid", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 1.5, fill: "none", stroke: cInvalid,
        transform: "rotate(45 " + cx + " " + cy + ")"
      }));
      var q = 3.4;
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy - q, x2: cx + q, y2: cy + q,
        stroke: cInvalid
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy + q, x2: cx + q, y2: cy - q,
        stroke: cInvalid
      }));
      if (iv.title) {
        var tx = cx + (iv.labelDX == null ? 16 : iv.labelDX);
        var ty = cy + (iv.labelDY == null ? -26 : iv.labelDY);
        svg.appendChild(el("line", {
          "class": "ghchart__leader", x1: cx, y1: cy, x2: tx, y2: ty, stroke: cInvalid
        }));
        var anchor = iv.labelAnchor || "start";
        var at = el("text", {
          "class": "ghchart__anno-title", x: tx, y: ty - 4, "text-anchor": anchor, fill: cInvalid
        });
        at.textContent = iv.title;
        svg.appendChild(at);
        if (iv.sub) {
          var asu = el("text", {
            "class": "ghchart__anno-sub", x: tx, y: ty + 11, "text-anchor": anchor
          });
          asu.textContent = iv.sub;
          svg.appendChild(asu);
        }
      }
    });

    // Bracket annotation (span with end ticks + centered label above).
    (config.brackets || []).forEach(function (b) {
      var x0 = xAt(b.fromX), x1 = xAt(b.toX), by = yAt(b.y);
      svg.appendChild(el("line", {
        "class": "ghchart__bracket", x1: x0, y1: by, x2: x1, y2: by
      }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x0, y1: by, x2: x0, y2: by + 7 }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x1, y1: by, x2: x1, y2: by + 7 }));
      var bl = el("text", {
        "class": "ghchart__bracket-label", x: (x0 + x1) / 2, y: by - 8, "text-anchor": "middle"
      });
      bl.textContent = b.text;
      svg.appendChild(bl);
    });

    // Final callout: ringed point + leader + title/sub placed inside the plot.
    if (config.callout) {
      var co = config.callout;
      var ccx = xAt(co.x), ccy = yAt(co.y);
      svg.appendChild(el("circle", {
        "class": "ghchart__callout-ring", cx: ccx, cy: ccy, r: 12, stroke: cPoint
      }));
      var ctx = ccx + (co.labelDX == null ? -16 : co.labelDX);
      var cty = ccy + (co.labelDY == null ? -30 : co.labelDY);
      svg.appendChild(el("line", {
        "class": "ghchart__leader ghchart__leader--callout", x1: ccx, y1: ccy, x2: ctx, y2: cty,
        stroke: cPoint
      }));
      var can = co.labelAnchor || "end";
      var ct = el("text", {
        "class": "ghchart__anno-title ghchart__anno-title--callout", x: ctx, y: cty - 4, "text-anchor": can, fill: cPoint
      });
      ct.textContent = co.title;
      svg.appendChild(ct);
      if (co.sub) {
        var cs = el("text", {
          "class": "ghchart__anno-sub ghchart__anno-sub--callout", x: ctx, y: cty + 12, "text-anchor": can
        });
        cs.textContent = co.sub;
        svg.appendChild(cs);
      }
    }

    root.appendChild(svg);

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.textContent = config.caption;
      root.appendChild(cap);
    }

    /* ---- Tooltip + hover interaction (skipped when disabled) ------ */
    if (!config.disableHover) {
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      sqEls.forEach(function (s, si) {
        var big = si === i;
        var w = big ? side + 5 : side;
        s.el.setAttribute("width", w);
        s.el.setAttribute("height", w);
        s.el.setAttribute("x", s.cx - w / 2);
        s.el.setAttribute("y", s.cy - w / 2);
      });

      var tipInfo = config.pointTip
        ? config.pointTip(pt)
        : { title: pt.d || "", line: fmtY(pt.y) };
      tipTitle.textContent = tipInfo.title;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = tipInfo.line;
      tipBody.appendChild(line);

      var anchor = sqEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      sqEls.forEach(function (s) {
        s.el.setAttribute("width", side);
        s.el.setAttribute("height", side);
        s.el.setAttribute("x", s.cx - hSide);
        s.el.setAttribute("y", s.cy - hSide);
      });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
    }
  };

  /* ================================================================
     Distribution dashboard renderer
     ----------------------------------------------------------------
     A composition figure (no hover — every value is printed as text, so
     the marks are inherently screen-reader accessible):
       • a compact narrative headline + muted subtitle;
       • an optional segmented "mix" bar — proportional colored segments,
         each with its name inside and count/percent below;
       • one or more panels of ranked horizontal bars, each row printing
         its category (left), a bar, and its count/percent (right).
     Colors come from config.series so the harness can tune them live:
       series[0]   = bar fill (single base hex);
       series[1..] = segment fills, in the order of config.segments.items.
     ================================================================ */
  renderers.distribution = function (root, config) {
    root.textContent = "";
    root.setAttribute("role", "img");
    if (config.title) root.setAttribute("aria-label", config.title);

    var series = config.series || [];
    var barColor = (series[0] && series[0].color) || "#8250df";
    var total = config.total ||
      (config.segments && config.segments.items
        ? config.segments.items.reduce(function (a, b) { return a + b.value; }, 0)
        : 0);

    function pctText(v) {
      if (!total) return "";
      return (Math.round((v / total) * 1000) / 10).toFixed(1) + "%";
    }
    function valueHTML(v) {
      var p = pctText(v);
      return "<b>" + v + "</b>" + (p ? " (" + p + ")" : "");
    }

    /* ---- Headline + subtitle ------------------------------------- */
    if (config.title) {
      var head = html("div", "ghchart__headline");
      head.textContent = config.title;
      root.appendChild(head);
    }
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- Segmented "mix" bar -------------------------------------- */
    var seg = config.segments;
    if (seg && seg.items && seg.items.length) {
      var segSec = html("div", "ghchart__dist-section");
      if (seg.title) {
        var segTitle = html("div", "ghchart__panel-title");
        segTitle.textContent = seg.title;
        segSec.appendChild(segTitle);
      }
      var segTotal = seg.items.reduce(function (a, b) { return a + b.value; }, 0) || 1;
      var bar = html("div", "ghchart__seg");
      var notes = html("div", "ghchart__seg-note-row");
      seg.items.forEach(function (it, i) {
        var basis = (it.value / segTotal) * 100;
        var col = (series[i + 1] && series[i + 1].color) || it.color || barColor;

        var cell = html("div", "ghchart__seg-cell");
        cell.style.flex = "0 0 " + basis + "%";
        cell.style.background = col;
        cell.textContent = it.label;
        bar.appendChild(cell);

        var note = html("div", "ghchart__seg-note");
        note.style.flex = "0 0 " + basis + "%";
        note.innerHTML = valueHTML(it.value);
        notes.appendChild(note);
      });
      segSec.appendChild(bar);
      segSec.appendChild(notes);
      root.appendChild(segSec);
    }

    /* ---- Ranked bar panels (grid: 2-up, stacking on narrow) ------ */
    var panels = config.panels || [];
    if (panels.length) {
      var cols = html("div", "ghchart__dist-cols");
      if (config.columns === 1 || panels.length === 1) {
        cols.classList.add("ghchart__dist-cols--stack");
      }
      panels.forEach(function (panel) {
        var sec = html("div", "ghchart__dist-section");
        if (panel.title) {
          var pt = html("div", "ghchart__panel-title");
          pt.textContent = panel.title;
          sec.appendChild(pt);
        }
        var list = html("div", "ghchart__dist-list");
        var pmax = (panel.items || []).reduce(function (m, it) {
          return it.value > m ? it.value : m;
        }, 0) || 1;
        (panel.items || []).forEach(function (it) {
          var row = html("div", "ghchart__dist-row");

          var cat = html("div", "ghchart__dist-cat");
          cat.textContent = it.label;
          row.appendChild(cat);

          var track = html("div", "ghchart__dist-track");
          var b = html("div", "ghchart__dist-bar");
          b.style.width = Math.max((it.value / pmax) * 100, 1.2) + "%";
          b.style.background = it.color || barColor;
          track.appendChild(b);
          row.appendChild(track);

          var val = html("div", "ghchart__dist-val");
          val.innerHTML = valueHTML(it.value);
          row.appendChild(val);

          list.appendChild(row);
        });
        sec.appendChild(list);
        cols.appendChild(sec);
      });
      root.appendChild(cols);
    }

    /* ---- Caption / footnote -------------------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Dispatcher — pick a renderer by config.type and draw into a mount
     ================================================================ */
  function render(config, mountEl) {
    var root = mountEl || document.getElementById(config.mountId);
    if (!root) return;
    root.classList.add("ghchart");
    /* Resolve the renderer from the SHARED registry (see registration below),
       not this bundle's private `renderers`. When several exported charts share
       one page they all merge into one registry, so every chart finds its
       renderer even if an older engine bundle loaded first. */
    var reg = (global.GHChartEngine && global.GHChartEngine.renderers) || renderers;
    var fn = reg[config.type] || renderers[config.type] || reg.line || renderers.line;
    fn(root, config);
  }

  /* ----------------------------------------------------------------
     Shared-engine registration (multi-embed safe)
     ----------------------------------------------------------------
     Every exported chart.js embeds this whole engine. When two or more
     charts are placed on the same page (e.g. a WordPress post), we must NOT
     let the first-loaded bundle silently win — an older bundle would be
     missing renderers added later (this is why a heatmap pasted after an
     older bar chart used to vanish).

     Instead we keep ONE shared engine:
       • every bundle merges in any renderers the shared registry lacks;
       • the newest bundle (highest ENGINE_VERSION) provides render(), which
         reads renderers from the shared registry at call time.
     The result is order-independent and resilient to version skew as new
     chart types are added. Bump ENGINE_VERSION whenever renderers or render
     change so a newer embed upgrades an older shared engine in place. */
  var ENGINE_VERSION = 9;
  var existing = global.GHChartEngine;
  if (existing && typeof existing.version === "number") {
    if (ENGINE_VERSION > existing.version) {
      /* This bundle is newer. Every export embeds the COMPLETE engine, so this
         bundle's `renderers` is a self-contained, up-to-date set. Adopt it as
         the shared registry (upgrading older same-named renderers in place),
         while defensively preserving any renderer types only the older engine
         happened to have. Then take over render(). */
      for (var ek in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ek) && !renderers[ek]) {
          renderers[ek] = existing.renderers[ek];
        }
      }
      existing.renderers = renderers;
      existing.render = render;
      existing.version = ENGINE_VERSION;
    } else {
      /* Same or older version already present: only contribute renderer types
         the shared registry is missing (don't clobber an equal/newer engine). */
      for (var rk in renderers) {
        if (Object.prototype.hasOwnProperty.call(renderers, rk) && !existing.renderers[rk]) {
          existing.renderers[rk] = renderers[rk];
        }
      }
    }
  } else {
    /* No engine yet, or a pre-versioning (old-style) engine loaded first.
       Take over, preserving any renderers the old-style engine registered. */
    if (existing && existing.renderers) {
      for (var ok in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ok) && !renderers[ok]) {
          renderers[ok] = existing.renderers[ok];
        }
      }
    }
    global.GHChartEngine = { renderers: renderers, render: render, version: ENGINE_VERSION };
  }
})(typeof window !== "undefined" ? window : this);

;(function () {
  "use strict";
  var CONFIG = {
    mountId: "gh-chart-hf-terminal",
    type: "scatter",
    title: "TerminalBench 2.1 - GitHub Copilot CLI",
    xAxis: {
      min: 0,
      max: 1,
      step: 0.2,
      title: "Mean cost per task (USD)",
      format: function (v) { return "$" + v.toFixed(2); }
    },
    yAxis: {
      min: 50,
      max: 90,
      step: 10,
      title: "Resolution rate (pass@1)",
      format: function (v) { return v + "%"; }
    },
    tipSuffix: "/task",
    series: [
      {
        name: "HydraFusion",
        color: "#2da44f"
      },
      {
        name: "Opus 5 (reference)",
        color: "#8250df"
      },
      {
        name: "Other solo models",
        color: "#8c959f"
      }
    ],
    points: [
      {
        label: "HydraFusion",
        x: 0.3,
        y: 84.7,
        series: 0,
        highlight: true,
        labelPos: "above"
      },
      {
        label: "Opus 5",
        x: 0.9,
        y: 79.8,
        series: 1,
        reference: true,
        labelPos: "above"
      },
      {
        label: "Sonnet 5",
        x: 0.55,
        y: 76,
        series: 2,
        labelPos: "right"
      },
      {
        label: "GPT-5.6-Sol",
        x: 0.3,
        y: 73.3,
        series: 2,
        labelPos: "right"
      },
      {
        label: "GPT-5.6-Terra",
        x: 0.25,
        y: 72.5,
        series: 2,
        labelPos: "left"
      },
      {
        label: "GPT-5.6-Luna",
        x: 0.03,
        y: 56,
        series: 2,
        labelPos: "right"
      }
    ]
  };
  function boot() {
    var root = document.getElementById(CONFIG.mountId);
    if (!root || root.getAttribute("data-gh-rendered")) return;
    root.setAttribute("data-gh-rendered", "1");
    window.GHChartEngine.render(CONFIG);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();
</script>

<div id="gh-chart-hf-terminal" class="ghchart"></div>



<h3 id="h-deepswe" class="wp-block-heading">DeepSWE</h3>



<p class="wp-block-paragraph">DeepSWE evaluates challenging repository-level software engineering tasks that require navigating large codebases, understanding cross-file dependencies, and producing end-to-end fixes. On this benchmark, HydraFusion comes within 1.5 percentage points of Opus 5 while reducing cost by 36%, demonstrating a compelling quality-cost tradeoff for complex real-world engineering tasks.</p>



<style data-wp-block-html="css">
.ghchart {
  /* Theme tokens */
  --ghc-text: #1f2328;
  --ghc-muted: #656d76;
  --ghc-grid: #d0d7de;
  --ghc-axis: #59636e;
  --ghc-card-bg: #ffffff;
  --ghc-card-border: #d0d7de;
  --ghc-divider: #b6bfb8;
  --ghc-guide: #afb8c1;

  /* GitHub brand font — matches the github.blog stack (Mona Sans, with
     the blog's metric fallback) so it renders identically in-context. */
  font-family: "Mona Sans", "Mona Sans Fallback", -apple-system, "system-ui",
    "Segoe UI", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 699px;
  margin: 0 auto;
  color: var(--ghc-text);
  line-height: 1.4;
}

.ghchart *,
.ghchart *::before,
.ghchart *::after { box-sizing: border-box; }

/* ------------------------------------------------------------------ */
/* Header + legend                                                     */
/* ------------------------------------------------------------------ */
.ghchart__header {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  min-height: 24px;
  margin-bottom: 4px;
}

.ghchart__legend {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.ghchart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: clamp(14px, 1.9vw, 17px);
  font-weight: 600;
  color: var(--ghc-muted);
  white-space: nowrap;
}

.ghchart__swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  flex: 0 0 auto;
}

/* ------------------------------------------------------------------ */
/* Plot (SVG)                                                          */
/* ------------------------------------------------------------------ */
.ghchart__plot {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  touch-action: pan-y;
}

.ghchart__gridline {
  stroke: var(--ghc-grid);
  stroke-width: 1;
  stroke-dasharray: 2 6;
  stroke-linecap: round;
  opacity: 0.9;
}

.ghchart__axislabel {
  fill: var(--ghc-axis);
  font-size: 22px;
  font-weight: 500;
}
.ghchart__axislabel--x { font-weight: 600; }

.ghchart__line {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ghchart__dot { transition: r 0.12s ease; }

/* Dashed threshold line (e.g. a target/reference level) + floating label.
   Stroke color is set inline per chart; dash + label styling live here. */
.ghchart__threshold {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.9;
}
.ghchart__threshold-label {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Hatched band marking a partial / projected trailing region. */
.ghchart__projection { pointer-events: none; }

.ghchart__guide {
  stroke: var(--ghc-guide);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
  opacity: 0;
  transition: opacity 0.12s ease;
}

.ghchart__hit { fill: transparent; }

/* ------------------------------------------------------------------ */
/* Scatter plot                                                        */
/* ------------------------------------------------------------------ */
.ghchart__gridline--v { /* vertical gridlines reuse the dashed grid style */ }

.ghchart__axistitle {
  fill: var(--ghc-muted);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Dotted reference crosshair through the "reference" point. Stroke color
   is set inline (tracks the reference series color). */
.ghchart__crosshair {
  stroke-width: 2;
  stroke-dasharray: 2 7;
  stroke-linecap: round;
  opacity: 0.85;
  pointer-events: none;
}

.ghchart__halo { opacity: 0.18; pointer-events: none; }
.ghchart__pt { transition: r 0.12s ease; }

.ghchart__pt-label {
  fill: var(--ghc-muted);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  pointer-events: none;
}
.ghchart__pt-label--strong { font-size: 21px; font-weight: 700; }

.ghchart__pthit { fill: transparent; cursor: default; outline: none; }
.ghchart__pthit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

.ghchart__swatch--dot { border-radius: 50%; }

/* ------------------------------------------------------------------ */
/* Trend chart (dated time-series with narrative headline)             */
/* ------------------------------------------------------------------ */
.ghchart__headline {
  font-size: clamp(17px, 2.3vw, 20px);
  line-height: 1.32;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
  margin: 0 0 8px;
}

/* Trend legend sits left-aligned under the headline/subtitle. */
.ghchart__legend--trend {
  justify-content: flex-start;
  margin: 2px 0 14px;
}
.ghchart__glyph { flex: 0 0 auto; }
.ghchart__glyph--square {
  width: 13px; height: 13px; border-radius: 2px;
}
.ghchart__glyph--diamond {
  width: 12px; height: 12px; border: 1.5px solid; transform: rotate(45deg);
}
.ghchart__glyph--line {
  width: 20px; height: 0; border-top: 2px dashed; border-radius: 0;
}

/* Phase background columns + separators + top labels. */
.ghchart__phase { fill: #f6f8fa; opacity: 0.55; }
.ghchart__phase--alt { fill: #ffffff; opacity: 0; }
.ghchart__phase-sep {
  stroke: var(--ghc-grid); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.8;
}
.ghchart__phase-label {
  fill: var(--ghc-muted);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
}
.ghchart__phase-avg {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Dashed trend line + ± noise band + slope label. */
.ghchart__trendband { opacity: 0.12; pointer-events: none; }
.ghchart__trendline {
  stroke-width: 2.5;
  stroke-dasharray: 8 6;
  stroke-linecap: round;
  fill: none;
}
.ghchart__slope {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Horizontal reference line + label. */
.ghchart__refline {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.85;
}
.ghchart__reflabel {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Data squares — values reveal on hover only. */
.ghchart__square { transition: width 0.12s ease, height 0.12s ease, x 0.12s ease, y 0.12s ease; }
.ghchart__squarehit { fill: transparent; cursor: default; outline: none; }
.ghchart__squarehit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

/* Invalid-run markers + leader-line annotations. */
.ghchart__invalid { stroke-width: 2; }
.ghchart__invalid-x { stroke-width: 2; stroke-linecap: round; }
.ghchart__leader { stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.8; }
.ghchart__leader--callout { stroke-dasharray: none; opacity: 0.6; }
.ghchart__anno-title {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.ghchart__anno-sub {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 500;
}
/* Final-callout labels read a touch larger than the invalid-run notes. */
.ghchart__anno-title--callout { font-size: 17px; }
.ghchart__anno-sub--callout { font-size: 14px; }

/* Bracket span annotation. */
.ghchart__bracket { stroke: var(--ghc-axis); stroke-width: 1.5; opacity: 0.75; }
.ghchart__bracket-label {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
}

/* Final callout ring. */
.ghchart__callout-ring { fill: none; stroke-width: 2; }

/* ------------------------------------------------------------------ */
/* Title + divider                                                     */
/* ------------------------------------------------------------------ */
.ghchart__title {
  /* Matches the github.blog H2 (.wp-block-heading) exactly */
  display: block;
  font-size: 32px;
  line-height: 40px;
  font-weight: 700;
  letter-spacing: -0.18px;
  color: var(--ghc-text);
}

.ghchart__rule {
  height: 1.5px;
  border-radius: 1.5px;
  background: var(--ghc-divider);
  margin: 10px 0 18px;
}

/* ------------------------------------------------------------------ */
/* Bar chart: subtitle, panels, bars, caption                          */
/* ------------------------------------------------------------------ */
.ghchart__subtitle {
  margin: -6px 0 20px;
  font-size: clamp(13px, 1.6vw, 15px);
  line-height: 1.5;
  color: var(--ghc-muted);
}

.ghchart__panel { margin-top: 30px; }
.ghchart__panel:first-of-type { margin-top: 6px; }

.ghchart__panel-title {
  margin: 0 0 6px;
  font-size: clamp(15px, 1.9vw, 18px);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
}

.ghchart__plot--bar { touch-action: pan-y; }

.ghchart__axisline {
  stroke: var(--ghc-axis);
  stroke-width: 1.25;
  opacity: 0.35;
}

.ghchart__bar { transition: opacity 0.12s ease; }
.ghchart__barhit { fill: transparent; }

/* Always-on value labels above selected bars. */
.ghchart__barvalue {
  fill: var(--ghc-text);
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Anchor annotation: dotted reference line + floating pill. */
.ghchart__anno-line {
  stroke: #8b949e;
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  stroke-linecap: round;
  pointer-events: none;
}
.ghchart__anno-chip {
  fill: #ffebe9;
  stroke: #ffcecb;
  stroke-width: 1;
  pointer-events: none;
}
.ghchart__anno-chip-text {
  fill: #a0111f;
  font-weight: 700;
  pointer-events: none;
}

.ghchart__caption {
  margin-top: 22px;
  padding-top: 12px;
  border-top: 1px solid var(--ghc-card-border);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ghc-muted);
}
.ghchart__caption b { color: var(--ghc-text); font-weight: 700; }
.ghchart__caption code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.92em;
  padding: 1px 5px;
  background: #f6f8fa;
  border: 1px solid var(--ghc-card-border);
  border-radius: 5px;
}
.ghchart__tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid #aceebb;
  color: #0a5223;
  background: #dafbe1;
  white-space: nowrap;
  vertical-align: middle;
}

/* ------------------------------------------------------------------ */
/* Distribution dashboard (segmented mix bar + ranked bar panels)      */
/* ------------------------------------------------------------------ */
.ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-section:first-of-type { margin-top: 12px; }

/* Segmented proportional "mix" bar: name inside, count/percent below.
   Separators are 2px inside borders (box-sizing:border-box) rather than a
   flex gap, so the three segments sum to exactly 100% with no overflow. */
.ghchart__seg {
  display: flex;
  width: 100%;
  height: 42px;
  margin-top: 2px;
  border-radius: 7px;
  overflow: hidden;
}
.ghchart__seg-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  color: #ffffff;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
}
.ghchart__seg-cell + .ghchart__seg-cell { border-left: 2px solid var(--ghc-card-bg); }
.ghchart__seg-note-row {
  display: flex;
  width: 100%;
  margin-top: 8px;
}
.ghchart__seg-note {
  min-width: 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Keep the outer notes from spilling past the chart edges. */
.ghchart__seg-note:first-child { text-align: left; }
.ghchart__seg-note:last-child { text-align: right; }
.ghchart__seg-note b { color: var(--ghc-text); font-weight: 700; }

/* Ranked horizontal bar list: [category | bar track | value].
   The list is the grid and rows use display:contents, so the category,
   bar, and value columns line up across every row in a panel. */
.ghchart__dist-list {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  align-items: center;
  column-gap: 12px;
  row-gap: 10px;
  margin-top: 4px;
}
.ghchart__dist-row { display: contents; }
.ghchart__dist-cat {
  text-align: right;
  font-size: 13px;
  line-height: 1.25;
  color: var(--ghc-text);
  white-space: nowrap;
}
.ghchart__dist-track { width: 100%; }
.ghchart__dist-bar {
  height: 15px;
  min-width: 2px;
  border-radius: 3px;
}
.ghchart__dist-val {
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ghchart__dist-val b { color: var(--ghc-text); font-weight: 700; }

/* Two-up panel grid (collapses to one column on narrow screens).
   minmax(0,1fr) lets columns shrink so long labels never force overflow. */
.ghchart__dist-cols {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px 30px;
}
.ghchart__dist-cols .ghchart__dist-section { margin-top: 0; }
.ghchart__dist-cols--stack { grid-template-columns: 1fr; }
.ghchart__dist-cols--stack .ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-cols--stack .ghchart__dist-section:first-child { margin-top: 0; }

/* Tighten the label/value type a touch in the 2-up layout so the bars
   keep room at the 699px blog width. */
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-cat { font-size: 12px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-val { font-size: 11.5px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-list { column-gap: 9px; }

@media (max-width: 600px) {
  .ghchart__dist-cols { grid-template-columns: 1fr; gap: 26px; }
  .ghchart__dist-cols .ghchart__dist-section ~ .ghchart__dist-section { margin-top: 2px; }
}

/* Let long category labels wrap on very narrow (mobile) widths. */
@media (max-width: 480px) {
  .ghchart__dist-cat { white-space: normal; }
}

/* ------------------------------------------------------------------ */
/* Calendar heatmap (GitHub contribution-graph style)                  */
/* ------------------------------------------------------------------ */
.ghheat__eyebrow { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.ghheat__eyedot {
  width: 9px; height: 9px; border-radius: 50%;
  background: #cf222e; box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.14);
  flex: 0 0 auto;
}
.ghheat__eyetext {
  font-size: 12px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: #cf222e;
}
/* Subtitle here is a prominent lede, not the muted variant. */
.ghheat__subtitle {
  color: var(--ghc-text);
  font-size: clamp(14px, 1.7vw, 16px);
  /* 24px top gap matches the github.blog H2 → paragraph block spacing. */
  margin: 24px 0 8px;
}
.ghheat__subtitle b { font-weight: 700; }

.ghheat__panel { margin-top: 36px; }
.ghheat__phead {
  display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.ghheat__badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  color: #fff; font-size: 12px; font-weight: 700;
  flex: 0 0 auto; align-self: center;
}
.ghheat__ptitle {
  font-size: clamp(16px, 2vw, 19px); font-weight: 700;
  letter-spacing: -0.01em; color: var(--ghc-text);
}
.ghheat__pdesc { font-size: 14px; color: var(--ghc-muted); }

.ghheat__body { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }
.ghheat__gridwrap { flex: 1 1 340px; min-width: 260px; }
.ghheat__grid { display: block; width: 100%; height: auto; overflow: visible; }
.ghheat__cell {
  stroke: rgba(27, 31, 36, 0.06);
  stroke-width: 1;
  transition: stroke 0.1s ease;
}
.ghheat__cell:hover { stroke: #24292f; stroke-width: 1.4; }
.ghheat__cell--pad { opacity: 0; pointer-events: none; }
.ghheat__wd, .ghheat__month {
  fill: var(--ghc-muted); font-size: 11px; font-weight: 600;
}

.ghheat__side { flex: 1 1 250px; min-width: 230px; }
.ghheat__stat { display: flex; align-items: baseline; gap: 7px; }
.ghheat__statbig {
  font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em;
  line-height: 1;
}
.ghheat__statsmall { font-size: 15px; font-weight: 600; color: var(--ghc-muted); }
.ghheat__note {
  margin: 10px 0 16px; font-size: 14px; line-height: 1.55; color: var(--ghc-muted);
}
.ghheat__note b { color: var(--ghc-text); font-weight: 700; }
.ghheat__legend {
  display: flex; align-items: center; gap: 5px;
  font-size: 12.5px; color: var(--ghc-muted); flex-wrap: wrap;
}
.ghheat__sw { width: 12px; height: 12px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }
.ghheat__sw--empty { border: 1px solid #d0d7de; }
.ghheat__legunit { margin-left: 2px; }
.ghheat__extra {
  display: flex; align-items: flex-start; gap: 7px; margin-top: 14px;
  font-size: 12px; line-height: 1.5; color: var(--ghc-muted); max-width: 330px;
}
.ghheat__extra .ghheat__sw { margin-top: 2px; }

/* ------------------------------------------------------------------ */
/* Tooltip                                                             */
/* ------------------------------------------------------------------ */
.ghchart__tooltip {
  position: absolute;
  z-index: 5;
  top: 0;
  left: 0;
  min-width: 128px;
  max-width: 260px;
  padding: 10px 12px;
  background: var(--ghc-card-bg);
  border: 1px solid var(--ghc-card-border);
  border-radius: 12px;
  /* Layered drop shadow from Primer Brand's ActionMenu (.ActionMenu__menu) */
  box-shadow:
    0px 100px 80px rgba(0, 0, 0, 0.01),
    0px 41px 33px rgba(0, 0, 0, 0.02),
    0px 22px 17px rgba(0, 0, 0, 0.02),
    0px 12px 10px rgba(0, 0, 0, 0.03),
    0px 6px 5px rgba(0, 0, 0, 0.04),
    0px 2px 2px rgba(0, 0, 0, 0.07);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -100%);
  /* Fade in on show and out on hide; quickly ease left/top when tracking
     from one point to the next. The position ease is suppressed on the
     first appearance (see chart.js) so the tooltip fades in place rather
     than sliding in from its previous spot. */
  transition: opacity 0.12s ease,
    left 0.1s cubic-bezier(0.4, 0, 0.2, 1),
    top 0.1s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: opacity, transform, left, top;
}
.ghchart__tooltip[data-visible="true"] { opacity: 1; }

.ghchart__tt-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--ghc-text);
  margin-bottom: 6px;
}

.ghchart__tt-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ghc-muted);
}
.ghchart__tt-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.ghchart__tt-name { flex: 1 1 auto; }
.ghchart__tt-value { font-weight: 700; color: var(--ghc-text); font-variant-numeric: tabular-nums; }

/* Bar tooltip body: a title line + free-form text lines (no swatches). */
.ghchart__tt-line {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ghc-muted);
}
.ghchart__tt-line:first-child { color: var(--ghc-text); font-weight: 600; }
.ghchart__tt-line + .ghchart__tt-line { margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ghchart__dot,
  .ghchart__guide,
  .ghchart__bar,
  .ghchart__pt,
  .ghchart__square,
  .ghheat__cell,
  .ghchart__tooltip { transition: none; }
}
</style>

<script data-wp-block-html="js">
/*
  Shared chart engine — reusable renderer for the blog-charts library.
  ------------------------------------------------------------------
  Exposes window.GHChartEngine = { renderers, render(config, mountEl?) }.
  Renderers are keyed by config.type (currently "line"); add a new viz type
  by registering GHChartEngine.renderers[<type>] = function (root, config) {…}.

  This same file is embedded verbatim at the top of each exported WordPress
  chart.js, followed by that chart's CONFIG + a small auto-boot. It has no
  external dependencies and is safe to include more than once on a page.
*/
(function (global) {
  "use strict";

  var SVGNS = "http://www.w3.org/2000/svg";

  function el(tag, attrs) {
    var node = document.createElementNS(SVGNS, tag);
    if (attrs) {
      for (var k in attrs) {
        if (Object.prototype.hasOwnProperty.call(attrs, k)) {
          node.setAttribute(k, attrs[k]);
        }
      }
    }
    return node;
  }

  function html(tag, cls) {
    var node = document.createElement(tag);
    if (cls) node.className = cls;
    return node;
  }

  function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }

  var renderers = {};

  /* ================================================================
     Line chart renderer
     ================================================================ */
  renderers.line = function (root, config) {
    // viewBox geometry (design units; scales fluidly to any width).
    var VB = { w: 1000, h: 560 };
    var M = { top: 30, right: 34, bottom: 56, left: 86 };
    // Inset the plotted points slightly from the axis so the first/last
    // months (e.g. "Jan") don't crowd the y-axis labels (e.g. "50").
    var PAD_X = 18;
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    // Reset (in case of re-render) and use a unique id prefix so multiple
    // charts on one page never share gradient ids.
    root.textContent = "";
    var uid = config.mountId;

    var n = config.labels.length;
    var yMin = config.yAxis.min;
    var yMax = config.yAxis.max;
    var step = config.yAxis.step;
    // Y-axis tick labels can format differently from the tooltip values
    // (e.g. a plain "40" on the axis but "40%" in the tooltip).
    var fmtAxis = config.formatAxis || config.formatValue;

    var plotW = innerW - 2 * PAD_X;
    var stepX = n <= 1 ? 0 : plotW / (n - 1);
    function xAt(i) { return n <= 1 ? M.left + innerW / 2 : M.left + PAD_X + stepX * i; }
    function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
    var baseY = yAt(yMin);

    /* ---- Title + divider (top) ---------------------------------- */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    /* ---- Header + legend ---------------------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      config.series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " line chart"
    });

    // Gradient defs (one per area series).
    var defs = el("defs");
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var g = el("linearGradient", {
        id: uid + "-grad-" + si, x1: "0", y1: "0", x2: "0", y2: "1"
      });
      g.appendChild(el("stop", { offset: "0", "stop-color": s.color, "stop-opacity": "0.28" }));
      g.appendChild(el("stop", { offset: "0.55", "stop-color": s.color, "stop-opacity": "0.08" }));
      g.appendChild(el("stop", { offset: "1", "stop-color": s.color, "stop-opacity": "0" }));
      defs.appendChild(g);
    });
    // Diagonal-hatch pattern for an optional "partial / projected" band.
    if (config.projection) {
      var pat = el("pattern", {
        id: uid + "-hatch", width: 7, height: 7,
        patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)"
      });
      pat.appendChild(el("line", {
        x1: 0, y1: 0, x2: 0, y2: 7,
        stroke: config.projection.color || "#8c959f",
        "stroke-width": 1.1, "stroke-opacity": 0.5
      }));
      defs.appendChild(pat);
    }
    svg.appendChild(defs);

    // Gridlines + y-axis labels.
    for (var v = yMin; v <= yMax + 0.0001; v += step) {
      var gy = yAt(v);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 22, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtAxis(v);
      svg.appendChild(yl);
    }

    // x-axis labels — either one per data point (default) or a sparse set
    // of ticks decoupled from the data (config.xTicks: [{at,text}, …]),
    // which suits a dense daily series that only needs month markers.
    var xTicks = config.xTicks ||
      config.labels.map(function (t, i) { return { at: i, text: t }; });
    xTicks.forEach(function (tk) {
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: xAt(tk.at), y: VB.h - M.bottom + 36,
        "text-anchor": tk.at === 0 ? "start" : (tk.at === n - 1 ? "end" : "middle")
      });
      xl.textContent = tk.text;
      svg.appendChild(xl);
    });

    // Optional hatched "partial / projected" band over the trailing region.
    if (config.projection) {
      var px = xAt(config.projection.fromIndex);
      svg.appendChild(el("rect", {
        "class": "ghchart__projection",
        x: px, y: M.top, width: (VB.w - M.right) - px, height: baseY - M.top,
        fill: "url(#" + uid + "-hatch)"
      }));
    }

    // Optional dashed threshold line + floating label (e.g. a 50% target).
    if (config.threshold) {
      var thy = yAt(config.threshold.value);
      svg.appendChild(el("line", {
        "class": "ghchart__threshold",
        x1: M.left, y1: thy, x2: VB.w - M.right, y2: thy,
        stroke: config.threshold.color
      }));
      if (config.threshold.label) {
        var thl = el("text", {
          "class": "ghchart__threshold-label",
          x: M.left + PAD_X, y: thy - 10, "text-anchor": "start",
          fill: config.threshold.color
        });
        thl.textContent = config.threshold.label;
        svg.appendChild(thl);
      }
    }

    // Precompute point coords per series.
    var pts = config.series.map(function (s) {
      return s.values.map(function (val, i) { return { x: xAt(i), y: yAt(val) }; });
    });

    // Smooth (Catmull-Rom -> cubic Bezier) path builder.
    function smooth(points) {
      if (!points.length) return "";
      if (points.length < 2) return "M" + points[0].x + "," + points[0].y;
      var d = "M " + points[0].x + "," + points[0].y;
      for (var i = 0; i < points.length - 1; i++) {
        var p0 = points[i - 1] || points[i];
        var p1 = points[i];
        var p2 = points[i + 1];
        var p3 = points[i + 2] || p2;
        var t = 1 / 6;
        var c1x = p1.x + (p2.x - p0.x) * t;
        var c1y = p1.y + (p2.y - p0.y) * t;
        var c2x = p2.x - (p3.x - p1.x) * t;
        var c2y = p2.y - (p3.y - p1.y) * t;
        d += " C " + c1x + "," + c1y + " " + c2x + "," + c2y + " " + p2.x + "," + p2.y;
      }
      return d;
    }

    // Area fills (drawn first, behind lines).
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var p = pts[si];
      var d = smooth(p) + " L " + p[p.length - 1].x + "," + baseY +
              " L " + p[0].x + "," + baseY + " Z";
      svg.appendChild(el("path", { d: d, fill: "url(#" + uid + "-grad-" + si + ")" }));
    });

    // Vertical hover guide (hidden until hover).
    var guide = el("line", {
      "class": "ghchart__guide",
      x1: xAt(0), y1: M.top - 6, x2: xAt(0), y2: baseY
    });
    svg.appendChild(guide);

    // Lines (draw in reverse so the first series sits on top).
    config.series.slice().reverse().forEach(function (s) {
      var si = config.series.indexOf(s);
      svg.appendChild(el("path", {
        "class": "ghchart__line",
        d: smooth(pts[si]),
        stroke: s.color,
        "stroke-width": s.width || (si === 0 ? 4 : 3.25)
      }));
    });

    // Dots (keep references for hover emphasis). When config.hideDots is set
    // (dense daily lines), dots rest invisible and only the hovered one shows.
    var dotEls = config.series.map(function (s, si) {
      return pts[si].map(function (pt) {
        var c = el("circle", {
          "class": "ghchart__dot",
          cx: pt.x, cy: pt.y,
          r: config.hideDots ? 0 : (si === 0 ? 4.5 : 4),
          fill: s.color
        });
        svg.appendChild(c);
        return c;
      });
    });

    // Transparent hit area for pointer capture.
    var hit = el("rect", {
      "class": "ghchart__hit",
      x: M.left,
      y: M.top - 6,
      width: innerW,
      height: baseY - M.top + 12
    });
    svg.appendChild(hit);

    root.appendChild(svg);

    /* ---- Tooltip ------------------------------------------------ */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    tip.appendChild(tipTitle);
    var tipRows = config.series.map(function (s) {
      var row = html("div", "ghchart__tt-row");
      var sw = html("span", "ghchart__tt-swatch");
      sw.style.background = s.color;
      var name = html("span", "ghchart__tt-name");
      name.textContent = s.name;
      var val = html("span", "ghchart__tt-value");
      row.appendChild(sw);
      row.appendChild(name);
      row.appendChild(val);
      tip.appendChild(row);
      return val;
    });
    root.appendChild(tip);

    /* ---- Hover / focus interactivity ---------------------------- */
    var activeIndex = -1;
    var baseR = config.series.map(function (s, si) {
      return config.hideDots ? 0 : (si === 0 ? 4.5 : 4);
    });
    // Radius of the single emphasized dot under the pointer.
    var dotActiveR = config.series.map(function (s, si) {
      return config.hideDots ? 6 : baseR[si] + 2.5;
    });
    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function indexFromClientX(clientX) {
      var rect = svg.getBoundingClientRect();
      var relX = (clientX - rect.left) / rect.width * VB.w;
      var i = stepX ? Math.round((relX - (M.left + PAD_X)) / stepX) : 0;
      return clamp(i, 0, n - 1);
    }

    function show(i) {
      if (i === activeIndex) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      guide.setAttribute("x1", xAt(i));
      guide.setAttribute("x2", xAt(i));
      guide.style.opacity = "1";

      // Emphasize the hovered column's dots; reset the rest.
      dotEls.forEach(function (col, si) {
        col.forEach(function (c, ci) {
          c.setAttribute("r", ci === i ? dotActiveR[si] : baseR[si]);
        });
      });

      // Fill tooltip.
      tipTitle.textContent = config.labels[i];
      config.series.forEach(function (s, si) {
        tipRows[si].textContent = config.formatValue(s.values[i]);
      });

      // Anchor above the highest point in this column.
      var topSi = 0, topY = Infinity;
      config.series.forEach(function (s, si) {
        var yy = yAt(s.values[i]);
        if (yy < topY) { topY = yy; topSi = si; }
      });
      var anchor = dotEls[topSi][i].getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      // On the first appearance, fade in exactly at the point: suppress the
      // position ease so it doesn't slide in from its previous resting spot.
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      // Clamp horizontally within the container.
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      // Flip below the point if there's no room above.
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      // Commit that position without animating, then restore the CSS
      // transition so subsequent point-to-point moves ease into place.
      if (suppress) {
        void tip.offsetWidth;
        tip.style.transition = "";
      }
    }

    function hide() {
      activeIndex = -1;
      guide.style.opacity = "0";
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (col, si) {
        col.forEach(function (c) { c.setAttribute("r", baseR[si]); });
      });
    }

    hit.addEventListener("pointermove", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerdown", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerleave", hide);

    // Keyboard access.
    svg.setAttribute("tabindex", "0");
    svg.addEventListener("keydown", function (e) {
      if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
        var start = activeIndex < 0 ? 0 : activeIndex;
        show(clamp(start + (e.key === "ArrowRight" ? 1 : -1), 0, n - 1));
        e.preventDefault();
      } else if (e.key === "Escape") {
        hide();
      }
    });
    svg.addEventListener("blur", hide);
  };

  /* ================================================================
     Bar chart renderer (composite)
     ----------------------------------------------------------------
     One "graph" can stack multiple bar panels in a single block:
       config = { type:"bar", title, subtitle?, series:[{name,color}],
                  barRadius?, panels:[ panel… ], caption? }
       panel  = { title?, labels[], values[], yAxis:{min,max,step},
                  formatValue(v), height?, maxBarVB?,
                  tooltip(i, panel) -> { title, lines:[…] } }
     No printed value labels — values surface on hover (consistent with
     the line chart). Bars use a single tunable color with rounded tops.
     ================================================================ */
  renderers.bar = function (root, config) {
    root.textContent = "";

    var barColor = (config.series && config.series[0] && config.series[0].color) || "#ff7b72";
    var barRadius = config.barRadius != null ? config.barRadius : 8;

    // Per-bar fill: a bar may point at a different series (e.g. a muted
    // "preliminary" color) via panel.barSeries[index]; otherwise series[0].
    function barColorFor(panel, j) {
      var si = (panel.barSeries && panel.barSeries[j] != null) ? panel.barSeries[j] : 0;
      var s = config.series && config.series[si];
      return (s && s.color) || barColor;
    }

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Title + divider (+ optional subtitle) ------------------ */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip for all panels ---------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var allBars = [];       // { el, pi, bi }
    var activeKey = null;   // "pi:bi" of the hovered bar

    function hideTip() {
      activeKey = null;
      tip.setAttribute("data-visible", "false");
      allBars.forEach(function (b) { b.el.style.opacity = "1"; });
    }

    // Hoisted: referenced by the per-column pointer handlers below.
    function showTip(pi, bi, panel, panelBars) {
      var reappearing = activeKey === null;
      activeKey = pi + ":" + bi;

      allBars.forEach(function (b) {
        b.el.style.opacity = (b.pi === pi && b.bi === bi) ? "1" : "0.42";
      });

      var content = panel.tooltip
        ? panel.tooltip(bi, panel)
        : { title: (panel.labels || [])[bi],
            lines: [ (panel.formatValue || String)(panel.values[bi]) ] };

      tipTitle.textContent = content.title != null ? content.title : "";
      tipBody.textContent = "";
      (content.lines || []).forEach(function (ln) {
        var row = html("div", "ghchart__tt-line");
        row.textContent = ln;
        tipBody.appendChild(row);
      });

      var anchor = panelBars[bi].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels ------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var wrap = html("div", "ghchart__panel");
      if (panel.title) {
        var pt = html("h3", "ghchart__panel-title");
        pt.textContent = panel.title;
        wrap.appendChild(pt);
      }

      var VB = { w: 1000, h: panel.height || 360 };
      var M = { top: 20, right: 20, bottom: 48, left: 84 };
      var innerW = VB.w - M.left - M.right;
      var innerH = VB.h - M.top - M.bottom;

      var labels = panel.labels || [];
      var values = panel.values || [];
      var nb = labels.length;
      var ax = panel.yAxis || { min: 0, max: Math.max.apply(null, values.concat([1])), step: 0 };
      var yMin = ax.min, yMax = ax.max, step = ax.step || 0;
      var fmt = panel.formatValue || function (v) { return String(v); };

      function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
      var baseY = yAt(yMin);

      var slot = nb ? innerW / nb : innerW;
      var maxBarVB = panel.maxBarVB != null ? panel.maxBarVB : 150;
      var barW = Math.min(slot * 0.62, maxBarVB);
      function xCenter(i) { return M.left + slot * (i + 0.5); }

      var svg = el("svg", {
        "class": "ghchart__plot ghchart__plot--bar",
        viewBox: "0 0 " + VB.w + " " + VB.h,
        preserveAspectRatio: "xMidYMid meet",
        role: "img",
        "aria-label": (panel.title || config.title) + " bar chart"
      });

      // Gridlines + y-axis labels.
      if (step > 0) {
        for (var v = yMin; v <= yMax + 1e-9; v += step) {
          var gy = yAt(v);
          svg.appendChild(el("line", {
            "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
          }));
          var yl = el("text", {
            "class": "ghchart__axislabel ghchart__axislabel--y",
            x: M.left - 18, y: gy, dy: "0.32em", "text-anchor": "end"
          });
          yl.textContent = fmt(v);
          svg.appendChild(yl);
        }
      }

      // Solid baseline.
      svg.appendChild(el("line", {
        "class": "ghchart__axisline", x1: M.left, y1: baseY, x2: VB.w - M.right, y2: baseY
      }));

      // x-axis labels.
      for (var i = 0; i < nb; i++) {
        var xl = el("text", {
          "class": "ghchart__axislabel ghchart__axislabel--x",
          x: xCenter(i), y: VB.h - M.bottom + 30, "text-anchor": "middle"
        });
        if (panel.xLabelSize) xl.setAttribute("style", "font-size:" + panel.xLabelSize + "px");
        xl.textContent = labels[i];
        svg.appendChild(xl);
      }

      // Bars (rounded top corners only) + full-height hover columns.
      var panelBars = [];
      for (var j = 0; j < nb; j++) {
        var cx = xCenter(j);
        var topY = yAt(values[j]);
        var barH = baseY - topY;
        var x = cx - barW / 2;
        var r = Math.max(0, Math.min(barRadius, barW / 2, barH));
        var d = "M " + x + " " + baseY +
                " L " + x + " " + (topY + r) +
                " Q " + x + " " + topY + " " + (x + r) + " " + topY +
                " L " + (x + barW - r) + " " + topY +
                " Q " + (x + barW) + " " + topY + " " + (x + barW) + " " + (topY + r) +
                " L " + (x + barW) + " " + baseY + " Z";
        var bar = el("path", { "class": "ghchart__bar", d: d, fill: barColorFor(panel, j) });
        svg.appendChild(bar);
        var rec = { el: bar, pi: pi, bi: j };
        panelBars.push(rec);
        allBars.push(rec);

        var col = el("rect", {
          "class": "ghchart__barhit",
          x: M.left + slot * j, y: M.top - 6, width: slot, height: baseY - M.top + 6
        });
        (function (idx) {
          col.addEventListener("pointermove", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerdown", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerleave", hideTip);
        })(j);
        svg.appendChild(col);
      }

      /* ---- Annotation layer (drawn on top; pointer-events: none) ----
         · always-on value labels above chosen bars (panel.valueLabels)
         · anchor lines: a dotted horizontal reference at one bar's top
           extended to another, with a floating pill (panel.annotations) */
      var vlabels = panel.valueLabels || null;
      if (vlabels) {
        for (var vk in vlabels) {
          if (!Object.prototype.hasOwnProperty.call(vlabels, vk)) continue;
          var vi = +vk;
          var vt = el("text", {
            "class": "ghchart__barvalue",
            x: xCenter(vi), y: yAt(values[vi]) - 12, "text-anchor": "middle"
          });
          vt.textContent = vlabels[vk];
          svg.appendChild(vt);
        }
      }

      (panel.annotations || []).forEach(function (an) {
        if (an.type !== "anchor") return;
        var yLine = yAt(values[an.fromIndex]);
        svg.appendChild(el("line", {
          "class": "ghchart__anno-line",
          x1: xCenter(an.fromIndex) - barW / 2, y1: yLine,
          x2: xCenter(an.toIndex) + barW / 2, y2: yLine
        }));

        var chipIdx = an.chipAtIndex != null ? an.chipAtIndex : an.toIndex;
        var cx = xCenter(chipIdx);
        var cy = (yAt(values[chipIdx]) + yLine) / 2;
        var label = an.label || "";
        var fs = 19;
        var chipW = label.length * fs * 0.55 + 24;
        var chipH = fs + 16;
        svg.appendChild(el("rect", {
          "class": "ghchart__anno-chip",
          x: cx - chipW / 2, y: cy - chipH / 2, width: chipW, height: chipH, rx: chipH / 2
        }));
        var ct = el("text", {
          "class": "ghchart__anno-chip-text",
          x: cx, y: cy, dy: "0.34em", "text-anchor": "middle", "font-size": fs
        });
        ct.textContent = label;
        svg.appendChild(ct);
      });

      wrap.appendChild(svg);
      root.appendChild(wrap);
    });

    /* ---- Caption (optional rich copy) --------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Calendar heatmap renderer (GitHub contribution-graph style)
     ----------------------------------------------------------------
     One square per calendar day across a month range. Each panel is
     shaded on its own scale — every day in a month shares that month's
     level (the underlying series is monthly). Each panel's 5-step
     faint->dark ramp is derived from a single tunable base color.
     ================================================================ */
  renderers.heatmap = function (root, config) {
    root.textContent = "";

    var WHITE = [255, 255, 255], BLACK = [0, 0, 0];
    function hexToRgb(h) {
      h = String(h).replace("#", "");
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
    }
    function rgbToHex(c) {
      return "#" + c.map(function (x) {
        x = Math.max(0, Math.min(255, Math.round(x)));
        return (x < 16 ? "0" : "") + x.toString(16);
      }).join("");
    }
    function mix(a, b, t) {
      return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
    }
    // 5-step contribution ramp (level 0 faint -> level 4 dark) from one hex.
    function rampFrom(baseHex) {
      var b = hexToRgb(baseHex);
      return [
        rgbToHex(mix(b, WHITE, 0.80)),
        rgbToHex(mix(b, WHITE, 0.52)),
        rgbToHex(mix(b, WHITE, 0.20)),
        baseHex,
        rgbToHex(mix(b, BLACK, 0.30))
      ];
    }

    function trim0(s) { return s.replace(/\.0$/, ""); }
    function fmtM(v) {
      if (v >= 1000) { var g = v / 1000; return (g >= 10 ? Math.round(g) : trim0(g.toFixed(1))) + "B"; }
      if (v >= 1) return trim0(v.toFixed(1)) + "M";
      return Math.round(v * 1000) + "K";
    }

    var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    var MON_FULL = ["January", "February", "March", "April", "May", "June",
      "July", "August", "September", "October", "November", "December"];
    var WD = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

    var year = config.year || 2026;
    var startM = config.startMonth != null ? config.startMonth : 0;
    var endM = config.endMonth != null ? config.endMonth : 5;
    var emptyColor = config.emptyColor || "#ebedf0";

    function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
    function parseISO(s) { var p = s.split("-"); return new Date(+p[0], +p[1] - 1, +p[2]); }
    function dayStart(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); }

    /* ---- Eyebrow + title + subtitle ----------------------------- */
    if (config.eyebrow) {
      var eb = html("div", "ghheat__eyebrow");
      eb.appendChild(html("span", "ghheat__eyedot"));
      var ebt = html("span", "ghheat__eyetext");
      ebt.textContent = config.eyebrow;
      eb.appendChild(ebt);
      root.appendChild(eb);
    }
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle ghheat__subtitle");
      sub.innerHTML = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip -------------------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }
    var activeCell = null;
    function hideTip() { activeCell = null; tip.setAttribute("data-visible", "false"); }
    function showTip(rectEl, titleText, lines) {
      var reappearing = activeCell === null;
      activeCell = rectEl;
      tipTitle.textContent = titleText;
      tipBody.textContent = "";
      lines.forEach(function (ln) {
        var r = html("div", "ghchart__tt-line");
        r.innerHTML = ln;
        tipBody.appendChild(r);
      });
      var a = rectEl.getBoundingClientRect(), rr = root.getBoundingClientRect();
      var left = a.left + a.width / 2 - rr.left, top = a.top - rr.top - 12;
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";
      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = a.bottom - rr.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels -------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var base = (config.series && config.series[pi] && config.series[pi].color) || panel.base || "#3094ff";
      var ramp = rampFrom(base);
      var strong = ramp[4];

      var totals = panel.months || [];
      var activeVals = [];
      for (var mi = startM; mi <= endM; mi++) { if (totals[mi] != null) activeVals.push(totals[mi]); }
      var vmin = activeVals.length ? Math.min.apply(null, activeVals) : 0;
      var vmax = activeVals.length ? Math.max.apply(null, activeVals) : 1;
      function levelFor(t) {
        if (t == null) return -1;
        if (vmax === vmin) return 4;
        return Math.round((t - vmin) / (vmax - vmin) * 4);
      }

      var af = panel.activeFrom ? parseISO(panel.activeFrom) : new Date(year, startM, 1);
      var at = panel.activeTo ? parseISO(panel.activeTo) : new Date(year, endM, daysInMonth(year, endM));

      var wrap = html("div", "ghheat__panel");

      var head = html("div", "ghheat__phead");
      var badge = html("span", "ghheat__badge");
      badge.textContent = panel.badge != null ? panel.badge : (pi + 1);
      badge.style.background = strong;
      head.appendChild(badge);
      var pt = html("span", "ghheat__ptitle");
      pt.textContent = panel.title;
      head.appendChild(pt);
      if (panel.descriptor) {
        var pd = html("span", "ghheat__pdesc");
        pd.textContent = panel.descriptor;
        head.appendChild(pd);
      }
      wrap.appendChild(head);

      var body = html("div", "ghheat__body");

      /* ---- Calendar grid (SVG) ---- */
      var gridStart = new Date(year, startM, 1);
      var gs = new Date(gridStart);
      gs.setDate(gs.getDate() - gs.getDay());            // back to Sunday
      var gridEnd = new Date(year, endM, daysInMonth(year, endM));
      var ge = new Date(gridEnd);
      ge.setDate(ge.getDate() + (6 - ge.getDay()));      // forward to Saturday
      var nCols = Math.round((ge - gs) / (7 * 86400000)) + 1;

      var cell = 13, sq = 11, rad = 2.5, labL = 34, labT = 18;
      var W = labL + nCols * cell, H = labT + 7 * cell;
      var svg = el("svg", {
        "class": "ghheat__grid",
        viewBox: "0 0 " + W + " " + H,
        preserveAspectRatio: "xMinYMin meet",
        role: "img",
        "aria-label": panel.title + " calendar heatmap"
      });

      [1, 3, 5].forEach(function (r) {
        var t = el("text", {
          "class": "ghheat__wd", x: labL - 6, y: labT + r * cell + sq / 2,
          dy: "0.32em", "text-anchor": "end"
        });
        t.textContent = WD[r];
        svg.appendChild(t);
      });

      // Month labels above the column where each month begins.
      for (var lm = startM; lm <= endM; lm++) {
        var first = new Date(year, lm, 1);
        var colOf = Math.floor((dayStart(first) - gs) / (7 * 86400000));
        var mlab = el("text", { "class": "ghheat__month", x: labL + colOf * cell, y: 12, "text-anchor": "start" });
        mlab.textContent = MON[lm];
        svg.appendChild(mlab);
      }

      var afS = dayStart(af), atS = dayStart(at);
      var d = new Date(gs);
      for (var i = 0; i < nCols * 7; i++) {
        var row = d.getDay();
        var col = Math.floor(i / 7);
        var x = labL + col * cell, y = labT + row * cell;
        var m = d.getMonth();
        var inRange = d.getFullYear() === year && m >= startM && m <= endM;
        var ds = dayStart(d);
        var inWindow = inRange && ds >= afS && ds <= atS;
        var hasData = inWindow && totals[m] != null;
        var lvl = hasData ? levelFor(totals[m]) : -1;
        var cellEl = el("rect", {
          "class": "ghheat__cell", x: x, y: y, width: sq, height: sq, rx: rad,
          fill: lvl >= 0 ? ramp[lvl] : emptyColor
        });
        if (inRange) {
          (function (dt, mm, has) {
            var dateStr = WD[dt.getDay()] + " \u00b7 " + MON[mm] + " " + dt.getDate() + ", " + year;
            var lines;
            if (has) {
              var daily = totals[mm] / daysInMonth(year, mm);
              lines = [
                "<b>" + fmtM(daily) + "</b> " + panel.legendLabel,
                MON_FULL[mm] + " \u00b7 " + fmtM(totals[mm]) + " " + panel.unitTotal
              ];
            } else {
              lines = [panel.emptyNote || "No vetted data"];
            }
            cellEl.addEventListener("pointermove", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerdown", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerleave", hideTip);
          })(new Date(d), m, hasData);
        } else {
          cellEl.setAttribute("class", "ghheat__cell ghheat__cell--pad");
        }
        svg.appendChild(cellEl);
        d.setDate(d.getDate() + 1);
      }

      var gridwrap = html("div", "ghheat__gridwrap");
      gridwrap.appendChild(svg);
      body.appendChild(gridwrap);

      /* ---- Side column: stat + note + legend + extra ---- */
      var side = html("div", "ghheat__side");
      if (panel.stat) {
        var stat = html("div", "ghheat__stat");
        var big = html("span", "ghheat__statbig");
        big.textContent = panel.stat.big;
        big.style.color = strong;
        stat.appendChild(big);
        if (panel.stat.small) {
          var small = html("span", "ghheat__statsmall");
          small.textContent = panel.stat.small;
          stat.appendChild(small);
        }
        side.appendChild(stat);
      }
      if (panel.note) {
        var note = html("p", "ghheat__note");
        note.innerHTML = panel.note;
        side.appendChild(note);
      }
      var leg = html("div", "ghheat__legend");
      var lw1 = html("span", "ghheat__legword"); lw1.textContent = "Fewer"; leg.appendChild(lw1);
      ramp.forEach(function (c) {
        var s = html("span", "ghheat__sw"); s.style.background = c; leg.appendChild(s);
      });
      var lw2 = html("span", "ghheat__legword"); lw2.textContent = "More"; leg.appendChild(lw2);
      if (panel.legendLabel) {
        var lu = html("span", "ghheat__legunit"); lu.textContent = "\u00b7 " + panel.legendLabel; leg.appendChild(lu);
      }
      side.appendChild(leg);
      if (panel.extraNote) {
        var ex = html("div", "ghheat__extra");
        var exsw = html("span", "ghheat__sw ghheat__sw--empty");
        exsw.style.background = emptyColor;
        ex.appendChild(exsw);
        var ext = html("span", "ghheat__extratext");
        ext.innerHTML = panel.extraNote;
        ex.appendChild(ext);
        side.appendChild(ex);
      }
      body.appendChild(side);

      wrap.appendChild(body);
      root.appendChild(wrap);
    });

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Scatter renderer — X/Y numeric plot with categorical points.
     Used for cost-vs-quality benchmark charts: a highlighted "primary"
     point (glow halo), a "reference" point with a dotted crosshair, and
     muted "other" points. Values never print on the marks — a point's
     name shows for identity and the exact (y, x) reveals on hover.
     ================================================================ */
  renderers.scatter = function (root, config) {
    root.textContent = "";
    var uid = config.mountId;

    var VB = { w: 1000, h: 600 };
    var M = { top: 40, right: 30, bottom: 74, left: 96 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var fmtX = xa.format || function (v) { return String(v); };
    var fmtY = ya.format || function (v) { return String(v); };
    var tipSuffix = config.tipSuffix || "";

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) { return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min)); }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function colorOf(pt) {
      var s = config.series && config.series[pt.series || 0];
      return (s && s.color) || "#8c959f";
    }

    /* ---- Title (compact narrative headline, no rule) ------------ */
    var title = html("div", "ghchart__headline");
    title.textContent = config.title;
    root.appendChild(title);

    /* ---- Header + legend (from series) -------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      (config.series || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch ghchart__swatch--dot");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--scatter",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " scatter plot"
    });

    // Gridlines (both directions) + tick labels.
    var eps = 1e-6;
    for (var yv = ya.min; yv <= ya.max + eps; yv += ya.step) {
      var gy = yAt(yv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(yv);
      svg.appendChild(yl);
    }
    for (var xv = xa.min; xv <= xa.max + eps; xv += xa.step) {
      var gx = xAt(xv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline ghchart__gridline--v",
        x1: gx, y1: M.top, x2: gx, y2: baseY
      }));
      var isFirst = Math.abs(xv - xa.min) < eps;
      var isLast = Math.abs(xv - xa.max) < eps;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 34,
        "text-anchor": isFirst ? "start" : (isLast ? "end" : "middle")
      });
      xl.textContent = fmtX(xv);
      svg.appendChild(xl);
    }

    // Axis titles.
    if (xa.title) {
      var xt = el("text", {
        "class": "ghchart__axistitle",
        x: M.left + innerW / 2, y: VB.h - 12, "text-anchor": "middle"
      });
      xt.textContent = xa.title;
      svg.appendChild(xt);
    }
    if (ya.title) {
      var ytX = 24, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle",
        x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Reference crosshair through the flagged point (dual dotted lines).
    var refPt = (config.points || []).filter(function (p) { return p.reference; })[0];
    if (refPt) {
      var rColor = colorOf(refPt);
      var rx = xAt(refPt.x), ry = yAt(refPt.y);
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: rx, y1: M.top, x2: rx, y2: baseY, stroke: rColor
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: M.left, y1: ry, x2: VB.w - M.right, y2: ry, stroke: rColor
      }));
    }

    // Points: halo (highlight/reference) + dot + name label.
    var dotEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y), c = colorOf(pt);
      var big = pt.highlight || pt.reference;

      if (big) {
        svg.appendChild(el("circle", {
          "class": "ghchart__halo", cx: cx, cy: cy, r: 17, fill: c
        }));
      }
      var dot = el("circle", {
        "class": "ghchart__pt", cx: cx, cy: cy,
        r: big ? 9 : 5.5, fill: c
      });
      svg.appendChild(dot);
      dotEls.push({ el: dot, baseR: big ? 9 : 5.5 });

      // Name label — bold+colored for the story points, muted for the rest.
      var pos = pt.labelPos || (pt.highlight ? "below" : pt.reference ? "above" : "right");
      var dx = 0, dy = 0, anchor = "middle";
      var gap = big ? 15 : 9;
      if (pos === "right") { dx = gap + 4; dy = 5; anchor = "start"; }
      else if (pos === "left") { dx = -(gap + 4); dy = 5; anchor = "end"; }
      else if (pos === "above") { dy = -(gap + 6); anchor = "middle"; }
      else { dy = gap + 16; anchor = "middle"; }  // below
      var lbl = el("text", {
        "class": "ghchart__pt-label" + (big ? " ghchart__pt-label--strong" : ""),
        x: cx + dx, y: cy + dy, "text-anchor": anchor,
        fill: big ? c : null
      });
      lbl.textContent = pt.label;
      svg.appendChild(lbl);

      // Transparent hit target for hover/focus.
      var hit = el("circle", {
        "class": "ghchart__pthit", cx: cx, cy: cy, r: 20,
        tabindex: "0", role: "img",
        "aria-label": pt.label + ": " + fmtY(pt.y) + ", " + fmtX(pt.x) + tipSuffix
      });
      svg.appendChild(hit);
      hitEls.push(hit);
    });

    root.appendChild(svg);

    /* ---- Tooltip (title + free lines) --------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      dotEls.forEach(function (d, di) {
        d.el.setAttribute("r", di === i ? d.baseR + 2.5 : d.baseR);
      });

      tipTitle.textContent = pt.label;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = fmtY(pt.y) + " · " + fmtX(pt.x) + tipSuffix;
      tipBody.appendChild(line);

      var anchor = dotEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (d) { d.el.setAttribute("r", d.baseR); });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
  };

  /* ================================================================
     Trend renderer — dated time-series with a narrative headline.
     One renderer covers both HydraFusion time charts via config:
       • linear OR log y-axis (yAxis.scale), explicit yAxis.ticks;
       • phase background columns (with optional per-phase average);
       • filled squares (values reveal on hover — never printed);
       • red-X "invalid run" markers with leader-line annotations;
       • a dashed trend line + optional ±band + slope label;
       • an optional horizontal reference line, bracket, and final callout.
     ================================================================ */
  renderers.trend = function (root, config) {
    root.textContent = "";

    var VB = { w: 1000, h: 560 };
    var M = { top: 64, right: 56, bottom: 56, left: 104 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var isLog = ya.scale === "log";
    var fmtY = ya.format || function (v) { return String(v); };
    var series = config.series || [];
    function sColor(i, fb) {
      var s = series[i];
      return (s && s.color) || fb;
    }
    var cPoint = sColor(0, "#8250df");
    var cInvalid = sColor(1, "#cf222e");
    var cTrend = (config.trend && config.trend.color) || sColor(0, "#8250df");

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) {
      if (isLog) {
        var lo = Math.log(ya.min) / Math.LN10, hi = Math.log(ya.max) / Math.LN10;
        var t = (Math.log(v) / Math.LN10 - lo) / (hi - lo);
        return M.top + innerH * (1 - t);
      }
      return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min));
    }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Headline + subtitle + legend (HTML flow above the plot) --- */
    var headline = html("div", "ghchart__headline");
    headline.textContent = config.title;
    root.appendChild(headline);
    if (config.subtitle) {
      var sub = html("div", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }
    if (!config.hideLegend && series.length) {
      var legend = html("div", "ghchart__legend ghchart__legend--trend");
      /* Build one legend glyph. The "invalid" glyph mirrors the on-plot marker
         exactly — a red diamond outline with an X through it (same side=13 /
         q=3.4 proportions used to draw the mark below) — so the key matches
         the mark on the chart. */
      function trendGlyph(glyph, color) {
        if (glyph === "invalid") {
          var g = el("svg", {
            "class": "ghchart__glyph ghchart__glyph--invalid",
            viewBox: "0 0 22 22", width: "15", height: "15",
            style: "overflow:visible"
          });
          g.appendChild(el("rect", {
            x: 4.5, y: 4.5, width: 13, height: 13, rx: 1.5,
            transform: "rotate(45 11 11)", fill: "none",
            stroke: color, "stroke-width": 2
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 7.6, x2: 14.4, y2: 14.4,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 14.4, x2: 14.4, y2: 7.6,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          return g;
        }
        var gl = html("span", "ghchart__glyph ghchart__glyph--" + glyph);
        if (glyph === "line") { gl.style.borderTopColor = color; }
        else { gl.style.background = color; gl.style.borderColor = color; }
        if (glyph === "diamond") { gl.style.background = "transparent"; }
        return gl;
      }
      series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "square", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      (config.legendExtra || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "line", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      root.appendChild(legend);
    }

    /* ---- SVG plot ------------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--trend",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title
    });

    // Phase background columns + separators + labels (+ optional average).
    (config.phases || []).forEach(function (ph, i) {
      var x0 = xAt(ph.from), x1 = xAt(ph.to);
      svg.appendChild(el("rect", {
        "class": "ghchart__phase" + (i % 2 ? " ghchart__phase--alt" : ""),
        x: x0, y: M.top, width: Math.max(0, x1 - x0), height: innerH
      }));
      if (i > 0) {
        svg.appendChild(el("line", {
          "class": "ghchart__phase-sep", x1: x0, y1: M.top, x2: x0, y2: baseY
        }));
      }
      var pl = el("text", {
        "class": "ghchart__phase-label", x: (x0 + x1) / 2, y: M.top - 40,
        "text-anchor": "middle"
      });
      pl.textContent = ph.label;
      svg.appendChild(pl);
      if (ph.avg) {
        var pa = el("text", {
          "class": "ghchart__phase-avg", x: (x0 + x1) / 2, y: M.top + 24,
          "text-anchor": "middle", fill: cPoint
        });
        pa.textContent = ph.avg;
        svg.appendChild(pa);
      }
    });

    // Horizontal gridlines + y tick labels (skip a gridline at the reference y).
    var refY = config.reference ? config.reference.y : null;
    (ya.ticks || []).forEach(function (tv) {
      var gy = yAt(tv);
      if (refY === null || Math.abs(tv - refY) > 1e-9) {
        svg.appendChild(el("line", {
          "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
        }));
      }
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(tv);
      svg.appendChild(yl);
    });

    // X tick labels (dated).
    (xa.ticks || []).forEach(function (t) {
      var gx = xAt(t.at);
      var first = Math.abs(t.at - xa.min) < 1e-9, last = Math.abs(t.at - xa.max) < 1e-9;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 30, "text-anchor": first ? "start" : (last ? "end" : "middle")
      });
      xl.textContent = t.text;
      svg.appendChild(xl);
    });

    // Axis titles.
    if (ya.title) {
      var ytX = 22, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle", x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Trend: optional ±band (linear only), then the dashed line + slope label.
    var tr = config.trend;
    if (tr) {
      function trendY(x) {
        return tr.from.y + (tr.to.y - tr.from.y) * (x - tr.from.x) / (tr.to.x - tr.from.x);
      }
      if (tr.band && !isLog) {
        var p = tr.band;
        var pts = [
          [xAt(tr.from.x), yAt(trendY(tr.from.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) - p)],
          [xAt(tr.from.x), yAt(trendY(tr.from.x) - p)]
        ].map(function (a) { return a[0] + "," + a[1]; }).join(" ");
        svg.appendChild(el("polygon", {
          "class": "ghchart__trendband", points: pts, fill: cTrend
        }));
      }
      svg.appendChild(el("line", {
        "class": "ghchart__trendline",
        x1: xAt(tr.from.x), y1: yAt(tr.from.y),
        x2: xAt(tr.to.x), y2: yAt(tr.to.y), stroke: cTrend
      }));
      if (tr.label) {
        var lx, lyy;
        if (tr.labelAt) { lx = xAt(tr.labelAt.x); lyy = yAt(tr.labelAt.y); }
        else {
          lx = xAt(tr.to.x) + (tr.labelDX == null ? -6 : tr.labelDX);
          lyy = yAt(tr.to.y) + (tr.labelDY == null ? -12 : tr.labelDY);
        }
        var sl = el("text", {
          "class": "ghchart__slope", x: lx, y: lyy,
          "text-anchor": tr.labelAnchor || "end", fill: cTrend
        });
        sl.textContent = tr.label;
        svg.appendChild(sl);
      }
    }

    // Reference line (horizontal dashed) + right-aligned label.
    if (config.reference) {
      var rc = config.reference.color || "#24292f";
      var ryy = yAt(config.reference.y);
      svg.appendChild(el("line", {
        "class": "ghchart__refline", x1: M.left, y1: ryy, x2: VB.w - M.right, y2: ryy,
        stroke: rc
      }));
      if (config.reference.label) {
        var rl = el("text", {
          "class": "ghchart__reflabel", x: VB.w - M.right, y: ryy - 8,
          "text-anchor": "end", fill: rc
        });
        rl.textContent = config.reference.label;
        svg.appendChild(rl);
      }
    }

    // Data squares (values reveal on hover only).
    var side = 13, hSide = side / 2;
    var sqEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y);
      var sq = el("rect", {
        "class": "ghchart__square", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 2, fill: cPoint
      });
      svg.appendChild(sq);
      sqEls.push({ el: sq, cx: cx, cy: cy });

      // Hover/focus hit target — omitted when hover is disabled so the
      // (illustrative) point values never surface, on screen or to a11y.
      if (!config.disableHover) {
        var hit = el("rect", {
          "class": "ghchart__squarehit", x: cx - 13, y: cy - 13,
          width: 26, height: 26, tabindex: "0", role: "img",
          "aria-label": (pt.d || "") + " " + fmtY(pt.y)
        });
        svg.appendChild(hit);
        hitEls.push(hit);
      }
    });

    // Invalid-run markers: red diamond + X, leader line, annotation text.
    (config.invalids || []).forEach(function (iv) {
      var cx = xAt(iv.x), cy = yAt(iv.y);
      svg.appendChild(el("rect", {
        "class": "ghchart__invalid", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 1.5, fill: "none", stroke: cInvalid,
        transform: "rotate(45 " + cx + " " + cy + ")"
      }));
      var q = 3.4;
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy - q, x2: cx + q, y2: cy + q,
        stroke: cInvalid
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy + q, x2: cx + q, y2: cy - q,
        stroke: cInvalid
      }));
      if (iv.title) {
        var tx = cx + (iv.labelDX == null ? 16 : iv.labelDX);
        var ty = cy + (iv.labelDY == null ? -26 : iv.labelDY);
        svg.appendChild(el("line", {
          "class": "ghchart__leader", x1: cx, y1: cy, x2: tx, y2: ty, stroke: cInvalid
        }));
        var anchor = iv.labelAnchor || "start";
        var at = el("text", {
          "class": "ghchart__anno-title", x: tx, y: ty - 4, "text-anchor": anchor, fill: cInvalid
        });
        at.textContent = iv.title;
        svg.appendChild(at);
        if (iv.sub) {
          var asu = el("text", {
            "class": "ghchart__anno-sub", x: tx, y: ty + 11, "text-anchor": anchor
          });
          asu.textContent = iv.sub;
          svg.appendChild(asu);
        }
      }
    });

    // Bracket annotation (span with end ticks + centered label above).
    (config.brackets || []).forEach(function (b) {
      var x0 = xAt(b.fromX), x1 = xAt(b.toX), by = yAt(b.y);
      svg.appendChild(el("line", {
        "class": "ghchart__bracket", x1: x0, y1: by, x2: x1, y2: by
      }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x0, y1: by, x2: x0, y2: by + 7 }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x1, y1: by, x2: x1, y2: by + 7 }));
      var bl = el("text", {
        "class": "ghchart__bracket-label", x: (x0 + x1) / 2, y: by - 8, "text-anchor": "middle"
      });
      bl.textContent = b.text;
      svg.appendChild(bl);
    });

    // Final callout: ringed point + leader + title/sub placed inside the plot.
    if (config.callout) {
      var co = config.callout;
      var ccx = xAt(co.x), ccy = yAt(co.y);
      svg.appendChild(el("circle", {
        "class": "ghchart__callout-ring", cx: ccx, cy: ccy, r: 12, stroke: cPoint
      }));
      var ctx = ccx + (co.labelDX == null ? -16 : co.labelDX);
      var cty = ccy + (co.labelDY == null ? -30 : co.labelDY);
      svg.appendChild(el("line", {
        "class": "ghchart__leader ghchart__leader--callout", x1: ccx, y1: ccy, x2: ctx, y2: cty,
        stroke: cPoint
      }));
      var can = co.labelAnchor || "end";
      var ct = el("text", {
        "class": "ghchart__anno-title ghchart__anno-title--callout", x: ctx, y: cty - 4, "text-anchor": can, fill: cPoint
      });
      ct.textContent = co.title;
      svg.appendChild(ct);
      if (co.sub) {
        var cs = el("text", {
          "class": "ghchart__anno-sub ghchart__anno-sub--callout", x: ctx, y: cty + 12, "text-anchor": can
        });
        cs.textContent = co.sub;
        svg.appendChild(cs);
      }
    }

    root.appendChild(svg);

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.textContent = config.caption;
      root.appendChild(cap);
    }

    /* ---- Tooltip + hover interaction (skipped when disabled) ------ */
    if (!config.disableHover) {
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      sqEls.forEach(function (s, si) {
        var big = si === i;
        var w = big ? side + 5 : side;
        s.el.setAttribute("width", w);
        s.el.setAttribute("height", w);
        s.el.setAttribute("x", s.cx - w / 2);
        s.el.setAttribute("y", s.cy - w / 2);
      });

      var tipInfo = config.pointTip
        ? config.pointTip(pt)
        : { title: pt.d || "", line: fmtY(pt.y) };
      tipTitle.textContent = tipInfo.title;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = tipInfo.line;
      tipBody.appendChild(line);

      var anchor = sqEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      sqEls.forEach(function (s) {
        s.el.setAttribute("width", side);
        s.el.setAttribute("height", side);
        s.el.setAttribute("x", s.cx - hSide);
        s.el.setAttribute("y", s.cy - hSide);
      });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
    }
  };

  /* ================================================================
     Distribution dashboard renderer
     ----------------------------------------------------------------
     A composition figure (no hover — every value is printed as text, so
     the marks are inherently screen-reader accessible):
       • a compact narrative headline + muted subtitle;
       • an optional segmented "mix" bar — proportional colored segments,
         each with its name inside and count/percent below;
       • one or more panels of ranked horizontal bars, each row printing
         its category (left), a bar, and its count/percent (right).
     Colors come from config.series so the harness can tune them live:
       series[0]   = bar fill (single base hex);
       series[1..] = segment fills, in the order of config.segments.items.
     ================================================================ */
  renderers.distribution = function (root, config) {
    root.textContent = "";
    root.setAttribute("role", "img");
    if (config.title) root.setAttribute("aria-label", config.title);

    var series = config.series || [];
    var barColor = (series[0] && series[0].color) || "#8250df";
    var total = config.total ||
      (config.segments && config.segments.items
        ? config.segments.items.reduce(function (a, b) { return a + b.value; }, 0)
        : 0);

    function pctText(v) {
      if (!total) return "";
      return (Math.round((v / total) * 1000) / 10).toFixed(1) + "%";
    }
    function valueHTML(v) {
      var p = pctText(v);
      return "<b>" + v + "</b>" + (p ? " (" + p + ")" : "");
    }

    /* ---- Headline + subtitle ------------------------------------- */
    if (config.title) {
      var head = html("div", "ghchart__headline");
      head.textContent = config.title;
      root.appendChild(head);
    }
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- Segmented "mix" bar -------------------------------------- */
    var seg = config.segments;
    if (seg && seg.items && seg.items.length) {
      var segSec = html("div", "ghchart__dist-section");
      if (seg.title) {
        var segTitle = html("div", "ghchart__panel-title");
        segTitle.textContent = seg.title;
        segSec.appendChild(segTitle);
      }
      var segTotal = seg.items.reduce(function (a, b) { return a + b.value; }, 0) || 1;
      var bar = html("div", "ghchart__seg");
      var notes = html("div", "ghchart__seg-note-row");
      seg.items.forEach(function (it, i) {
        var basis = (it.value / segTotal) * 100;
        var col = (series[i + 1] && series[i + 1].color) || it.color || barColor;

        var cell = html("div", "ghchart__seg-cell");
        cell.style.flex = "0 0 " + basis + "%";
        cell.style.background = col;
        cell.textContent = it.label;
        bar.appendChild(cell);

        var note = html("div", "ghchart__seg-note");
        note.style.flex = "0 0 " + basis + "%";
        note.innerHTML = valueHTML(it.value);
        notes.appendChild(note);
      });
      segSec.appendChild(bar);
      segSec.appendChild(notes);
      root.appendChild(segSec);
    }

    /* ---- Ranked bar panels (grid: 2-up, stacking on narrow) ------ */
    var panels = config.panels || [];
    if (panels.length) {
      var cols = html("div", "ghchart__dist-cols");
      if (config.columns === 1 || panels.length === 1) {
        cols.classList.add("ghchart__dist-cols--stack");
      }
      panels.forEach(function (panel) {
        var sec = html("div", "ghchart__dist-section");
        if (panel.title) {
          var pt = html("div", "ghchart__panel-title");
          pt.textContent = panel.title;
          sec.appendChild(pt);
        }
        var list = html("div", "ghchart__dist-list");
        var pmax = (panel.items || []).reduce(function (m, it) {
          return it.value > m ? it.value : m;
        }, 0) || 1;
        (panel.items || []).forEach(function (it) {
          var row = html("div", "ghchart__dist-row");

          var cat = html("div", "ghchart__dist-cat");
          cat.textContent = it.label;
          row.appendChild(cat);

          var track = html("div", "ghchart__dist-track");
          var b = html("div", "ghchart__dist-bar");
          b.style.width = Math.max((it.value / pmax) * 100, 1.2) + "%";
          b.style.background = it.color || barColor;
          track.appendChild(b);
          row.appendChild(track);

          var val = html("div", "ghchart__dist-val");
          val.innerHTML = valueHTML(it.value);
          row.appendChild(val);

          list.appendChild(row);
        });
        sec.appendChild(list);
        cols.appendChild(sec);
      });
      root.appendChild(cols);
    }

    /* ---- Caption / footnote -------------------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Dispatcher — pick a renderer by config.type and draw into a mount
     ================================================================ */
  function render(config, mountEl) {
    var root = mountEl || document.getElementById(config.mountId);
    if (!root) return;
    root.classList.add("ghchart");
    /* Resolve the renderer from the SHARED registry (see registration below),
       not this bundle's private `renderers`. When several exported charts share
       one page they all merge into one registry, so every chart finds its
       renderer even if an older engine bundle loaded first. */
    var reg = (global.GHChartEngine && global.GHChartEngine.renderers) || renderers;
    var fn = reg[config.type] || renderers[config.type] || reg.line || renderers.line;
    fn(root, config);
  }

  /* ----------------------------------------------------------------
     Shared-engine registration (multi-embed safe)
     ----------------------------------------------------------------
     Every exported chart.js embeds this whole engine. When two or more
     charts are placed on the same page (e.g. a WordPress post), we must NOT
     let the first-loaded bundle silently win — an older bundle would be
     missing renderers added later (this is why a heatmap pasted after an
     older bar chart used to vanish).

     Instead we keep ONE shared engine:
       • every bundle merges in any renderers the shared registry lacks;
       • the newest bundle (highest ENGINE_VERSION) provides render(), which
         reads renderers from the shared registry at call time.
     The result is order-independent and resilient to version skew as new
     chart types are added. Bump ENGINE_VERSION whenever renderers or render
     change so a newer embed upgrades an older shared engine in place. */
  var ENGINE_VERSION = 9;
  var existing = global.GHChartEngine;
  if (existing && typeof existing.version === "number") {
    if (ENGINE_VERSION > existing.version) {
      /* This bundle is newer. Every export embeds the COMPLETE engine, so this
         bundle's `renderers` is a self-contained, up-to-date set. Adopt it as
         the shared registry (upgrading older same-named renderers in place),
         while defensively preserving any renderer types only the older engine
         happened to have. Then take over render(). */
      for (var ek in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ek) && !renderers[ek]) {
          renderers[ek] = existing.renderers[ek];
        }
      }
      existing.renderers = renderers;
      existing.render = render;
      existing.version = ENGINE_VERSION;
    } else {
      /* Same or older version already present: only contribute renderer types
         the shared registry is missing (don't clobber an equal/newer engine). */
      for (var rk in renderers) {
        if (Object.prototype.hasOwnProperty.call(renderers, rk) && !existing.renderers[rk]) {
          existing.renderers[rk] = renderers[rk];
        }
      }
    }
  } else {
    /* No engine yet, or a pre-versioning (old-style) engine loaded first.
       Take over, preserving any renderers the old-style engine registered. */
    if (existing && existing.renderers) {
      for (var ok in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ok) && !renderers[ok]) {
          renderers[ok] = existing.renderers[ok];
        }
      }
    }
    global.GHChartEngine = { renderers: renderers, render: render, version: ENGINE_VERSION };
  }
})(typeof window !== "undefined" ? window : this);

;(function () {
  "use strict";
  var CONFIG = {
    mountId: "gh-chart-hf-deepswe",
    type: "scatter",
    title: "DeepSWE - GitHub Copilot CLI",
    xAxis: {
      min: 0,
      max: 4.5,
      step: 0.5,
      title: "Mean cost per task (USD)",
      format: function (v) { return "$" + v.toFixed(2); }
    },
    yAxis: {
      min: 20,
      max: 70,
      step: 10,
      title: "Resolution rate (pass@1)",
      format: function (v) { return v + "%"; }
    },
    tipSuffix: "/task",
    series: [
      {
        name: "HydraFusion",
        color: "#2da44f"
      },
      {
        name: "Opus 5 (reference)",
        color: "#8250df"
      },
      {
        name: "Other solo models",
        color: "#8c959f"
      }
    ],
    points: [
      {
        label: "HydraFusion",
        x: 2.7,
        y: 62.6,
        series: 0,
        highlight: true,
        labelPos: "above"
      },
      {
        label: "Opus 5",
        x: 4.2,
        y: 64.1,
        series: 1,
        reference: true,
        labelPos: "above"
      },
      {
        label: "GPT-5.6-Sol",
        x: 2.4,
        y: 60.4,
        series: 2,
        labelPos: "below"
      },
      {
        label: "Sonnet 5",
        x: 2.47,
        y: 38.9,
        series: 2,
        labelPos: "right"
      },
      {
        label: "GPT-5.6-Terra",
        x: 0.75,
        y: 37.8,
        series: 2,
        labelPos: "right"
      },
      {
        label: "GPT-5.6-Luna",
        x: 0.08,
        y: 20.5,
        series: 2,
        labelPos: "right"
      }
    ]
  };
  function boot() {
    var root = document.getElementById(CONFIG.mountId);
    if (!root || root.getAttribute("data-gh-rendered")) return;
    root.setAttribute("data-gh-rendered", "1");
    window.GHChartEngine.render(CONFIG);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();
</script>

<div id="gh-chart-hf-deepswe" class="ghchart"></div>



<h3 id="h-checkpointbench" class="wp-block-heading">CheckpointBench</h3>



<style data-wp-block-html="css">
.ghchart {
  /* Theme tokens */
  --ghc-text: #1f2328;
  --ghc-muted: #656d76;
  --ghc-grid: #d0d7de;
  --ghc-axis: #59636e;
  --ghc-card-bg: #ffffff;
  --ghc-card-border: #d0d7de;
  --ghc-divider: #b6bfb8;
  --ghc-guide: #afb8c1;

  /* GitHub brand font — matches the github.blog stack (Mona Sans, with
     the blog's metric fallback) so it renders identically in-context. */
  font-family: "Mona Sans", "Mona Sans Fallback", -apple-system, "system-ui",
    "Segoe UI", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 699px;
  margin: 0 auto;
  color: var(--ghc-text);
  line-height: 1.4;
}

.ghchart *,
.ghchart *::before,
.ghchart *::after { box-sizing: border-box; }

/* ------------------------------------------------------------------ */
/* Header + legend                                                     */
/* ------------------------------------------------------------------ */
.ghchart__header {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  min-height: 24px;
  margin-bottom: 4px;
}

.ghchart__legend {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.ghchart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: clamp(14px, 1.9vw, 17px);
  font-weight: 600;
  color: var(--ghc-muted);
  white-space: nowrap;
}

.ghchart__swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  flex: 0 0 auto;
}

/* ------------------------------------------------------------------ */
/* Plot (SVG)                                                          */
/* ------------------------------------------------------------------ */
.ghchart__plot {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  touch-action: pan-y;
}

.ghchart__gridline {
  stroke: var(--ghc-grid);
  stroke-width: 1;
  stroke-dasharray: 2 6;
  stroke-linecap: round;
  opacity: 0.9;
}

.ghchart__axislabel {
  fill: var(--ghc-axis);
  font-size: 22px;
  font-weight: 500;
}
.ghchart__axislabel--x { font-weight: 600; }

.ghchart__line {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ghchart__dot { transition: r 0.12s ease; }

/* Dashed threshold line (e.g. a target/reference level) + floating label.
   Stroke color is set inline per chart; dash + label styling live here. */
.ghchart__threshold {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.9;
}
.ghchart__threshold-label {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Hatched band marking a partial / projected trailing region. */
.ghchart__projection { pointer-events: none; }

.ghchart__guide {
  stroke: var(--ghc-guide);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
  opacity: 0;
  transition: opacity 0.12s ease;
}

.ghchart__hit { fill: transparent; }

/* ------------------------------------------------------------------ */
/* Scatter plot                                                        */
/* ------------------------------------------------------------------ */
.ghchart__gridline--v { /* vertical gridlines reuse the dashed grid style */ }

.ghchart__axistitle {
  fill: var(--ghc-muted);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Dotted reference crosshair through the "reference" point. Stroke color
   is set inline (tracks the reference series color). */
.ghchart__crosshair {
  stroke-width: 2;
  stroke-dasharray: 2 7;
  stroke-linecap: round;
  opacity: 0.85;
  pointer-events: none;
}

.ghchart__halo { opacity: 0.18; pointer-events: none; }
.ghchart__pt { transition: r 0.12s ease; }

.ghchart__pt-label {
  fill: var(--ghc-muted);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  pointer-events: none;
}
.ghchart__pt-label--strong { font-size: 21px; font-weight: 700; }

.ghchart__pthit { fill: transparent; cursor: default; outline: none; }
.ghchart__pthit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

.ghchart__swatch--dot { border-radius: 50%; }

/* ------------------------------------------------------------------ */
/* Trend chart (dated time-series with narrative headline)             */
/* ------------------------------------------------------------------ */
.ghchart__headline {
  font-size: clamp(17px, 2.3vw, 20px);
  line-height: 1.32;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
  margin: 0 0 8px;
}

/* Trend legend sits left-aligned under the headline/subtitle. */
.ghchart__legend--trend {
  justify-content: flex-start;
  margin: 2px 0 14px;
}
.ghchart__glyph { flex: 0 0 auto; }
.ghchart__glyph--square {
  width: 13px; height: 13px; border-radius: 2px;
}
.ghchart__glyph--diamond {
  width: 12px; height: 12px; border: 1.5px solid; transform: rotate(45deg);
}
.ghchart__glyph--line {
  width: 20px; height: 0; border-top: 2px dashed; border-radius: 0;
}

/* Phase background columns + separators + top labels. */
.ghchart__phase { fill: #f6f8fa; opacity: 0.55; }
.ghchart__phase--alt { fill: #ffffff; opacity: 0; }
.ghchart__phase-sep {
  stroke: var(--ghc-grid); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.8;
}
.ghchart__phase-label {
  fill: var(--ghc-muted);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
}
.ghchart__phase-avg {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Dashed trend line + ± noise band + slope label. */
.ghchart__trendband { opacity: 0.12; pointer-events: none; }
.ghchart__trendline {
  stroke-width: 2.5;
  stroke-dasharray: 8 6;
  stroke-linecap: round;
  fill: none;
}
.ghchart__slope {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Horizontal reference line + label. */
.ghchart__refline {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.85;
}
.ghchart__reflabel {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Data squares — values reveal on hover only. */
.ghchart__square { transition: width 0.12s ease, height 0.12s ease, x 0.12s ease, y 0.12s ease; }
.ghchart__squarehit { fill: transparent; cursor: default; outline: none; }
.ghchart__squarehit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

/* Invalid-run markers + leader-line annotations. */
.ghchart__invalid { stroke-width: 2; }
.ghchart__invalid-x { stroke-width: 2; stroke-linecap: round; }
.ghchart__leader { stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.8; }
.ghchart__leader--callout { stroke-dasharray: none; opacity: 0.6; }
.ghchart__anno-title {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.ghchart__anno-sub {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 500;
}
/* Final-callout labels read a touch larger than the invalid-run notes. */
.ghchart__anno-title--callout { font-size: 17px; }
.ghchart__anno-sub--callout { font-size: 14px; }

/* Bracket span annotation. */
.ghchart__bracket { stroke: var(--ghc-axis); stroke-width: 1.5; opacity: 0.75; }
.ghchart__bracket-label {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
}

/* Final callout ring. */
.ghchart__callout-ring { fill: none; stroke-width: 2; }

/* ------------------------------------------------------------------ */
/* Title + divider                                                     */
/* ------------------------------------------------------------------ */
.ghchart__title {
  /* Matches the github.blog H2 (.wp-block-heading) exactly */
  display: block;
  font-size: 32px;
  line-height: 40px;
  font-weight: 700;
  letter-spacing: -0.18px;
  color: var(--ghc-text);
}

.ghchart__rule {
  height: 1.5px;
  border-radius: 1.5px;
  background: var(--ghc-divider);
  margin: 10px 0 18px;
}

/* ------------------------------------------------------------------ */
/* Bar chart: subtitle, panels, bars, caption                          */
/* ------------------------------------------------------------------ */
.ghchart__subtitle {
  margin: -6px 0 20px;
  font-size: clamp(13px, 1.6vw, 15px);
  line-height: 1.5;
  color: var(--ghc-muted);
}

.ghchart__panel { margin-top: 30px; }
.ghchart__panel:first-of-type { margin-top: 6px; }

.ghchart__panel-title {
  margin: 0 0 6px;
  font-size: clamp(15px, 1.9vw, 18px);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
}

.ghchart__plot--bar { touch-action: pan-y; }

.ghchart__axisline {
  stroke: var(--ghc-axis);
  stroke-width: 1.25;
  opacity: 0.35;
}

.ghchart__bar { transition: opacity 0.12s ease; }
.ghchart__barhit { fill: transparent; }

/* Always-on value labels above selected bars. */
.ghchart__barvalue {
  fill: var(--ghc-text);
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Anchor annotation: dotted reference line + floating pill. */
.ghchart__anno-line {
  stroke: #8b949e;
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  stroke-linecap: round;
  pointer-events: none;
}
.ghchart__anno-chip {
  fill: #ffebe9;
  stroke: #ffcecb;
  stroke-width: 1;
  pointer-events: none;
}
.ghchart__anno-chip-text {
  fill: #a0111f;
  font-weight: 700;
  pointer-events: none;
}

.ghchart__caption {
  margin-top: 22px;
  padding-top: 12px;
  border-top: 1px solid var(--ghc-card-border);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ghc-muted);
}
.ghchart__caption b { color: var(--ghc-text); font-weight: 700; }
.ghchart__caption code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.92em;
  padding: 1px 5px;
  background: #f6f8fa;
  border: 1px solid var(--ghc-card-border);
  border-radius: 5px;
}
.ghchart__tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid #aceebb;
  color: #0a5223;
  background: #dafbe1;
  white-space: nowrap;
  vertical-align: middle;
}

/* ------------------------------------------------------------------ */
/* Distribution dashboard (segmented mix bar + ranked bar panels)      */
/* ------------------------------------------------------------------ */
.ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-section:first-of-type { margin-top: 12px; }

/* Segmented proportional "mix" bar: name inside, count/percent below.
   Separators are 2px inside borders (box-sizing:border-box) rather than a
   flex gap, so the three segments sum to exactly 100% with no overflow. */
.ghchart__seg {
  display: flex;
  width: 100%;
  height: 42px;
  margin-top: 2px;
  border-radius: 7px;
  overflow: hidden;
}
.ghchart__seg-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  color: #ffffff;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
}
.ghchart__seg-cell + .ghchart__seg-cell { border-left: 2px solid var(--ghc-card-bg); }
.ghchart__seg-note-row {
  display: flex;
  width: 100%;
  margin-top: 8px;
}
.ghchart__seg-note {
  min-width: 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Keep the outer notes from spilling past the chart edges. */
.ghchart__seg-note:first-child { text-align: left; }
.ghchart__seg-note:last-child { text-align: right; }
.ghchart__seg-note b { color: var(--ghc-text); font-weight: 700; }

/* Ranked horizontal bar list: [category | bar track | value].
   The list is the grid and rows use display:contents, so the category,
   bar, and value columns line up across every row in a panel. */
.ghchart__dist-list {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  align-items: center;
  column-gap: 12px;
  row-gap: 10px;
  margin-top: 4px;
}
.ghchart__dist-row { display: contents; }
.ghchart__dist-cat {
  text-align: right;
  font-size: 13px;
  line-height: 1.25;
  color: var(--ghc-text);
  white-space: nowrap;
}
.ghchart__dist-track { width: 100%; }
.ghchart__dist-bar {
  height: 15px;
  min-width: 2px;
  border-radius: 3px;
}
.ghchart__dist-val {
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ghchart__dist-val b { color: var(--ghc-text); font-weight: 700; }

/* Two-up panel grid (collapses to one column on narrow screens).
   minmax(0,1fr) lets columns shrink so long labels never force overflow. */
.ghchart__dist-cols {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px 30px;
}
.ghchart__dist-cols .ghchart__dist-section { margin-top: 0; }
.ghchart__dist-cols--stack { grid-template-columns: 1fr; }
.ghchart__dist-cols--stack .ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-cols--stack .ghchart__dist-section:first-child { margin-top: 0; }

/* Tighten the label/value type a touch in the 2-up layout so the bars
   keep room at the 699px blog width. */
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-cat { font-size: 12px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-val { font-size: 11.5px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-list { column-gap: 9px; }

@media (max-width: 600px) {
  .ghchart__dist-cols { grid-template-columns: 1fr; gap: 26px; }
  .ghchart__dist-cols .ghchart__dist-section ~ .ghchart__dist-section { margin-top: 2px; }
}

/* Let long category labels wrap on very narrow (mobile) widths. */
@media (max-width: 480px) {
  .ghchart__dist-cat { white-space: normal; }
}

/* ------------------------------------------------------------------ */
/* Calendar heatmap (GitHub contribution-graph style)                  */
/* ------------------------------------------------------------------ */
.ghheat__eyebrow { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.ghheat__eyedot {
  width: 9px; height: 9px; border-radius: 50%;
  background: #cf222e; box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.14);
  flex: 0 0 auto;
}
.ghheat__eyetext {
  font-size: 12px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: #cf222e;
}
/* Subtitle here is a prominent lede, not the muted variant. */
.ghheat__subtitle {
  color: var(--ghc-text);
  font-size: clamp(14px, 1.7vw, 16px);
  /* 24px top gap matches the github.blog H2 → paragraph block spacing. */
  margin: 24px 0 8px;
}
.ghheat__subtitle b { font-weight: 700; }

.ghheat__panel { margin-top: 36px; }
.ghheat__phead {
  display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.ghheat__badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  color: #fff; font-size: 12px; font-weight: 700;
  flex: 0 0 auto; align-self: center;
}
.ghheat__ptitle {
  font-size: clamp(16px, 2vw, 19px); font-weight: 700;
  letter-spacing: -0.01em; color: var(--ghc-text);
}
.ghheat__pdesc { font-size: 14px; color: var(--ghc-muted); }

.ghheat__body { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }
.ghheat__gridwrap { flex: 1 1 340px; min-width: 260px; }
.ghheat__grid { display: block; width: 100%; height: auto; overflow: visible; }
.ghheat__cell {
  stroke: rgba(27, 31, 36, 0.06);
  stroke-width: 1;
  transition: stroke 0.1s ease;
}
.ghheat__cell:hover { stroke: #24292f; stroke-width: 1.4; }
.ghheat__cell--pad { opacity: 0; pointer-events: none; }
.ghheat__wd, .ghheat__month {
  fill: var(--ghc-muted); font-size: 11px; font-weight: 600;
}

.ghheat__side { flex: 1 1 250px; min-width: 230px; }
.ghheat__stat { display: flex; align-items: baseline; gap: 7px; }
.ghheat__statbig {
  font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em;
  line-height: 1;
}
.ghheat__statsmall { font-size: 15px; font-weight: 600; color: var(--ghc-muted); }
.ghheat__note {
  margin: 10px 0 16px; font-size: 14px; line-height: 1.55; color: var(--ghc-muted);
}
.ghheat__note b { color: var(--ghc-text); font-weight: 700; }
.ghheat__legend {
  display: flex; align-items: center; gap: 5px;
  font-size: 12.5px; color: var(--ghc-muted); flex-wrap: wrap;
}
.ghheat__sw { width: 12px; height: 12px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }
.ghheat__sw--empty { border: 1px solid #d0d7de; }
.ghheat__legunit { margin-left: 2px; }
.ghheat__extra {
  display: flex; align-items: flex-start; gap: 7px; margin-top: 14px;
  font-size: 12px; line-height: 1.5; color: var(--ghc-muted); max-width: 330px;
}
.ghheat__extra .ghheat__sw { margin-top: 2px; }

/* ------------------------------------------------------------------ */
/* Tooltip                                                             */
/* ------------------------------------------------------------------ */
.ghchart__tooltip {
  position: absolute;
  z-index: 5;
  top: 0;
  left: 0;
  min-width: 128px;
  max-width: 260px;
  padding: 10px 12px;
  background: var(--ghc-card-bg);
  border: 1px solid var(--ghc-card-border);
  border-radius: 12px;
  /* Layered drop shadow from Primer Brand's ActionMenu (.ActionMenu__menu) */
  box-shadow:
    0px 100px 80px rgba(0, 0, 0, 0.01),
    0px 41px 33px rgba(0, 0, 0, 0.02),
    0px 22px 17px rgba(0, 0, 0, 0.02),
    0px 12px 10px rgba(0, 0, 0, 0.03),
    0px 6px 5px rgba(0, 0, 0, 0.04),
    0px 2px 2px rgba(0, 0, 0, 0.07);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -100%);
  /* Fade in on show and out on hide; quickly ease left/top when tracking
     from one point to the next. The position ease is suppressed on the
     first appearance (see chart.js) so the tooltip fades in place rather
     than sliding in from its previous spot. */
  transition: opacity 0.12s ease,
    left 0.1s cubic-bezier(0.4, 0, 0.2, 1),
    top 0.1s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: opacity, transform, left, top;
}
.ghchart__tooltip[data-visible="true"] { opacity: 1; }

.ghchart__tt-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--ghc-text);
  margin-bottom: 6px;
}

.ghchart__tt-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ghc-muted);
}
.ghchart__tt-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.ghchart__tt-name { flex: 1 1 auto; }
.ghchart__tt-value { font-weight: 700; color: var(--ghc-text); font-variant-numeric: tabular-nums; }

/* Bar tooltip body: a title line + free-form text lines (no swatches). */
.ghchart__tt-line {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ghc-muted);
}
.ghchart__tt-line:first-child { color: var(--ghc-text); font-weight: 600; }
.ghchart__tt-line + .ghchart__tt-line { margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ghchart__dot,
  .ghchart__guide,
  .ghchart__bar,
  .ghchart__pt,
  .ghchart__square,
  .ghheat__cell,
  .ghchart__tooltip { transition: none; }
}

</style>

<script data-wp-block-html="js">
/*
  Shared chart engine — reusable renderer for the blog-charts library.
  ------------------------------------------------------------------
  Exposes window.GHChartEngine = { renderers, render(config, mountEl?) }.
  Renderers are keyed by config.type (currently "line"); add a new viz type
  by registering GHChartEngine.renderers[<type>] = function (root, config) {…}.

  This same file is embedded verbatim at the top of each exported WordPress
  chart.js, followed by that chart's CONFIG + a small auto-boot. It has no
  external dependencies and is safe to include more than once on a page.
*/
(function (global) {
  "use strict";

  var SVGNS = "http://www.w3.org/2000/svg";

  function el(tag, attrs) {
    var node = document.createElementNS(SVGNS, tag);
    if (attrs) {
      for (var k in attrs) {
        if (Object.prototype.hasOwnProperty.call(attrs, k)) {
          node.setAttribute(k, attrs[k]);
        }
      }
    }
    return node;
  }

  function html(tag, cls) {
    var node = document.createElement(tag);
    if (cls) node.className = cls;
    return node;
  }

  function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }

  var renderers = {};

  /* ================================================================
     Line chart renderer
     ================================================================ */
  renderers.line = function (root, config) {
    // viewBox geometry (design units; scales fluidly to any width).
    var VB = { w: 1000, h: 560 };
    var M = { top: 30, right: 34, bottom: 56, left: 86 };
    // Inset the plotted points slightly from the axis so the first/last
    // months (e.g. "Jan") don't crowd the y-axis labels (e.g. "50").
    var PAD_X = 18;
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    // Reset (in case of re-render) and use a unique id prefix so multiple
    // charts on one page never share gradient ids.
    root.textContent = "";
    var uid = config.mountId;

    var n = config.labels.length;
    var yMin = config.yAxis.min;
    var yMax = config.yAxis.max;
    var step = config.yAxis.step;
    // Y-axis tick labels can format differently from the tooltip values
    // (e.g. a plain "40" on the axis but "40%" in the tooltip).
    var fmtAxis = config.formatAxis || config.formatValue;

    var plotW = innerW - 2 * PAD_X;
    var stepX = n <= 1 ? 0 : plotW / (n - 1);
    function xAt(i) { return n <= 1 ? M.left + innerW / 2 : M.left + PAD_X + stepX * i; }
    function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
    var baseY = yAt(yMin);

    /* ---- Title + divider (top) ---------------------------------- */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    /* ---- Header + legend ---------------------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      config.series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " line chart"
    });

    // Gradient defs (one per area series).
    var defs = el("defs");
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var g = el("linearGradient", {
        id: uid + "-grad-" + si, x1: "0", y1: "0", x2: "0", y2: "1"
      });
      g.appendChild(el("stop", { offset: "0", "stop-color": s.color, "stop-opacity": "0.28" }));
      g.appendChild(el("stop", { offset: "0.55", "stop-color": s.color, "stop-opacity": "0.08" }));
      g.appendChild(el("stop", { offset: "1", "stop-color": s.color, "stop-opacity": "0" }));
      defs.appendChild(g);
    });
    // Diagonal-hatch pattern for an optional "partial / projected" band.
    if (config.projection) {
      var pat = el("pattern", {
        id: uid + "-hatch", width: 7, height: 7,
        patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)"
      });
      pat.appendChild(el("line", {
        x1: 0, y1: 0, x2: 0, y2: 7,
        stroke: config.projection.color || "#8c959f",
        "stroke-width": 1.1, "stroke-opacity": 0.5
      }));
      defs.appendChild(pat);
    }
    svg.appendChild(defs);

    // Gridlines + y-axis labels.
    for (var v = yMin; v <= yMax + 0.0001; v += step) {
      var gy = yAt(v);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 22, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtAxis(v);
      svg.appendChild(yl);
    }

    // x-axis labels — either one per data point (default) or a sparse set
    // of ticks decoupled from the data (config.xTicks: [{at,text}, …]),
    // which suits a dense daily series that only needs month markers.
    var xTicks = config.xTicks ||
      config.labels.map(function (t, i) { return { at: i, text: t }; });
    xTicks.forEach(function (tk) {
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: xAt(tk.at), y: VB.h - M.bottom + 36,
        "text-anchor": tk.at === 0 ? "start" : (tk.at === n - 1 ? "end" : "middle")
      });
      xl.textContent = tk.text;
      svg.appendChild(xl);
    });

    // Optional hatched "partial / projected" band over the trailing region.
    if (config.projection) {
      var px = xAt(config.projection.fromIndex);
      svg.appendChild(el("rect", {
        "class": "ghchart__projection",
        x: px, y: M.top, width: (VB.w - M.right) - px, height: baseY - M.top,
        fill: "url(#" + uid + "-hatch)"
      }));
    }

    // Optional dashed threshold line + floating label (e.g. a 50% target).
    if (config.threshold) {
      var thy = yAt(config.threshold.value);
      svg.appendChild(el("line", {
        "class": "ghchart__threshold",
        x1: M.left, y1: thy, x2: VB.w - M.right, y2: thy,
        stroke: config.threshold.color
      }));
      if (config.threshold.label) {
        var thl = el("text", {
          "class": "ghchart__threshold-label",
          x: M.left + PAD_X, y: thy - 10, "text-anchor": "start",
          fill: config.threshold.color
        });
        thl.textContent = config.threshold.label;
        svg.appendChild(thl);
      }
    }

    // Precompute point coords per series.
    var pts = config.series.map(function (s) {
      return s.values.map(function (val, i) { return { x: xAt(i), y: yAt(val) }; });
    });

    // Smooth (Catmull-Rom -> cubic Bezier) path builder.
    function smooth(points) {
      if (!points.length) return "";
      if (points.length < 2) return "M" + points[0].x + "," + points[0].y;
      var d = "M " + points[0].x + "," + points[0].y;
      for (var i = 0; i < points.length - 1; i++) {
        var p0 = points[i - 1] || points[i];
        var p1 = points[i];
        var p2 = points[i + 1];
        var p3 = points[i + 2] || p2;
        var t = 1 / 6;
        var c1x = p1.x + (p2.x - p0.x) * t;
        var c1y = p1.y + (p2.y - p0.y) * t;
        var c2x = p2.x - (p3.x - p1.x) * t;
        var c2y = p2.y - (p3.y - p1.y) * t;
        d += " C " + c1x + "," + c1y + " " + c2x + "," + c2y + " " + p2.x + "," + p2.y;
      }
      return d;
    }

    // Area fills (drawn first, behind lines).
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var p = pts[si];
      var d = smooth(p) + " L " + p[p.length - 1].x + "," + baseY +
              " L " + p[0].x + "," + baseY + " Z";
      svg.appendChild(el("path", { d: d, fill: "url(#" + uid + "-grad-" + si + ")" }));
    });

    // Vertical hover guide (hidden until hover).
    var guide = el("line", {
      "class": "ghchart__guide",
      x1: xAt(0), y1: M.top - 6, x2: xAt(0), y2: baseY
    });
    svg.appendChild(guide);

    // Lines (draw in reverse so the first series sits on top).
    config.series.slice().reverse().forEach(function (s) {
      var si = config.series.indexOf(s);
      svg.appendChild(el("path", {
        "class": "ghchart__line",
        d: smooth(pts[si]),
        stroke: s.color,
        "stroke-width": s.width || (si === 0 ? 4 : 3.25)
      }));
    });

    // Dots (keep references for hover emphasis). When config.hideDots is set
    // (dense daily lines), dots rest invisible and only the hovered one shows.
    var dotEls = config.series.map(function (s, si) {
      return pts[si].map(function (pt) {
        var c = el("circle", {
          "class": "ghchart__dot",
          cx: pt.x, cy: pt.y,
          r: config.hideDots ? 0 : (si === 0 ? 4.5 : 4),
          fill: s.color
        });
        svg.appendChild(c);
        return c;
      });
    });

    // Transparent hit area for pointer capture.
    var hit = el("rect", {
      "class": "ghchart__hit",
      x: M.left,
      y: M.top - 6,
      width: innerW,
      height: baseY - M.top + 12
    });
    svg.appendChild(hit);

    root.appendChild(svg);

    /* ---- Tooltip ------------------------------------------------ */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    tip.appendChild(tipTitle);
    var tipRows = config.series.map(function (s) {
      var row = html("div", "ghchart__tt-row");
      var sw = html("span", "ghchart__tt-swatch");
      sw.style.background = s.color;
      var name = html("span", "ghchart__tt-name");
      name.textContent = s.name;
      var val = html("span", "ghchart__tt-value");
      row.appendChild(sw);
      row.appendChild(name);
      row.appendChild(val);
      tip.appendChild(row);
      return val;
    });
    root.appendChild(tip);

    /* ---- Hover / focus interactivity ---------------------------- */
    var activeIndex = -1;
    var baseR = config.series.map(function (s, si) {
      return config.hideDots ? 0 : (si === 0 ? 4.5 : 4);
    });
    // Radius of the single emphasized dot under the pointer.
    var dotActiveR = config.series.map(function (s, si) {
      return config.hideDots ? 6 : baseR[si] + 2.5;
    });
    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function indexFromClientX(clientX) {
      var rect = svg.getBoundingClientRect();
      var relX = (clientX - rect.left) / rect.width * VB.w;
      var i = stepX ? Math.round((relX - (M.left + PAD_X)) / stepX) : 0;
      return clamp(i, 0, n - 1);
    }

    function show(i) {
      if (i === activeIndex) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      guide.setAttribute("x1", xAt(i));
      guide.setAttribute("x2", xAt(i));
      guide.style.opacity = "1";

      // Emphasize the hovered column's dots; reset the rest.
      dotEls.forEach(function (col, si) {
        col.forEach(function (c, ci) {
          c.setAttribute("r", ci === i ? dotActiveR[si] : baseR[si]);
        });
      });

      // Fill tooltip.
      tipTitle.textContent = config.labels[i];
      config.series.forEach(function (s, si) {
        tipRows[si].textContent = config.formatValue(s.values[i]);
      });

      // Anchor above the highest point in this column.
      var topSi = 0, topY = Infinity;
      config.series.forEach(function (s, si) {
        var yy = yAt(s.values[i]);
        if (yy < topY) { topY = yy; topSi = si; }
      });
      var anchor = dotEls[topSi][i].getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      // On the first appearance, fade in exactly at the point: suppress the
      // position ease so it doesn't slide in from its previous resting spot.
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      // Clamp horizontally within the container.
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      // Flip below the point if there's no room above.
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      // Commit that position without animating, then restore the CSS
      // transition so subsequent point-to-point moves ease into place.
      if (suppress) {
        void tip.offsetWidth;
        tip.style.transition = "";
      }
    }

    function hide() {
      activeIndex = -1;
      guide.style.opacity = "0";
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (col, si) {
        col.forEach(function (c) { c.setAttribute("r", baseR[si]); });
      });
    }

    hit.addEventListener("pointermove", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerdown", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerleave", hide);

    // Keyboard access.
    svg.setAttribute("tabindex", "0");
    svg.addEventListener("keydown", function (e) {
      if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
        var start = activeIndex < 0 ? 0 : activeIndex;
        show(clamp(start + (e.key === "ArrowRight" ? 1 : -1), 0, n - 1));
        e.preventDefault();
      } else if (e.key === "Escape") {
        hide();
      }
    });
    svg.addEventListener("blur", hide);
  };

  /* ================================================================
     Bar chart renderer (composite)
     ----------------------------------------------------------------
     One "graph" can stack multiple bar panels in a single block:
       config = { type:"bar", title, subtitle?, series:[{name,color}],
                  barRadius?, panels:[ panel… ], caption? }
       panel  = { title?, labels[], values[], yAxis:{min,max,step},
                  formatValue(v), height?, maxBarVB?,
                  tooltip(i, panel) -> { title, lines:[…] } }
     No printed value labels — values surface on hover (consistent with
     the line chart). Bars use a single tunable color with rounded tops.
     ================================================================ */
  renderers.bar = function (root, config) {
    root.textContent = "";

    var barColor = (config.series && config.series[0] && config.series[0].color) || "#ff7b72";
    var barRadius = config.barRadius != null ? config.barRadius : 8;

    // Per-bar fill: a bar may point at a different series (e.g. a muted
    // "preliminary" color) via panel.barSeries[index]; otherwise series[0].
    function barColorFor(panel, j) {
      var si = (panel.barSeries && panel.barSeries[j] != null) ? panel.barSeries[j] : 0;
      var s = config.series && config.series[si];
      return (s && s.color) || barColor;
    }

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Title + divider (+ optional subtitle) ------------------ */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip for all panels ---------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var allBars = [];       // { el, pi, bi }
    var activeKey = null;   // "pi:bi" of the hovered bar

    function hideTip() {
      activeKey = null;
      tip.setAttribute("data-visible", "false");
      allBars.forEach(function (b) { b.el.style.opacity = "1"; });
    }

    // Hoisted: referenced by the per-column pointer handlers below.
    function showTip(pi, bi, panel, panelBars) {
      var reappearing = activeKey === null;
      activeKey = pi + ":" + bi;

      allBars.forEach(function (b) {
        b.el.style.opacity = (b.pi === pi && b.bi === bi) ? "1" : "0.42";
      });

      var content = panel.tooltip
        ? panel.tooltip(bi, panel)
        : { title: (panel.labels || [])[bi],
            lines: [ (panel.formatValue || String)(panel.values[bi]) ] };

      tipTitle.textContent = content.title != null ? content.title : "";
      tipBody.textContent = "";
      (content.lines || []).forEach(function (ln) {
        var row = html("div", "ghchart__tt-line");
        row.textContent = ln;
        tipBody.appendChild(row);
      });

      var anchor = panelBars[bi].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels ------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var wrap = html("div", "ghchart__panel");
      if (panel.title) {
        var pt = html("h3", "ghchart__panel-title");
        pt.textContent = panel.title;
        wrap.appendChild(pt);
      }

      var VB = { w: 1000, h: panel.height || 360 };
      var M = { top: 20, right: 20, bottom: 48, left: 84 };
      var innerW = VB.w - M.left - M.right;
      var innerH = VB.h - M.top - M.bottom;

      var labels = panel.labels || [];
      var values = panel.values || [];
      var nb = labels.length;
      var ax = panel.yAxis || { min: 0, max: Math.max.apply(null, values.concat([1])), step: 0 };
      var yMin = ax.min, yMax = ax.max, step = ax.step || 0;
      var fmt = panel.formatValue || function (v) { return String(v); };

      function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
      var baseY = yAt(yMin);

      var slot = nb ? innerW / nb : innerW;
      var maxBarVB = panel.maxBarVB != null ? panel.maxBarVB : 150;
      var barW = Math.min(slot * 0.62, maxBarVB);
      function xCenter(i) { return M.left + slot * (i + 0.5); }

      var svg = el("svg", {
        "class": "ghchart__plot ghchart__plot--bar",
        viewBox: "0 0 " + VB.w + " " + VB.h,
        preserveAspectRatio: "xMidYMid meet",
        role: "img",
        "aria-label": (panel.title || config.title) + " bar chart"
      });

      // Gridlines + y-axis labels.
      if (step > 0) {
        for (var v = yMin; v <= yMax + 1e-9; v += step) {
          var gy = yAt(v);
          svg.appendChild(el("line", {
            "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
          }));
          var yl = el("text", {
            "class": "ghchart__axislabel ghchart__axislabel--y",
            x: M.left - 18, y: gy, dy: "0.32em", "text-anchor": "end"
          });
          yl.textContent = fmt(v);
          svg.appendChild(yl);
        }
      }

      // Solid baseline.
      svg.appendChild(el("line", {
        "class": "ghchart__axisline", x1: M.left, y1: baseY, x2: VB.w - M.right, y2: baseY
      }));

      // x-axis labels.
      for (var i = 0; i < nb; i++) {
        var xl = el("text", {
          "class": "ghchart__axislabel ghchart__axislabel--x",
          x: xCenter(i), y: VB.h - M.bottom + 30, "text-anchor": "middle"
        });
        if (panel.xLabelSize) xl.setAttribute("style", "font-size:" + panel.xLabelSize + "px");
        xl.textContent = labels[i];
        svg.appendChild(xl);
      }

      // Bars (rounded top corners only) + full-height hover columns.
      var panelBars = [];
      for (var j = 0; j < nb; j++) {
        var cx = xCenter(j);
        var topY = yAt(values[j]);
        var barH = baseY - topY;
        var x = cx - barW / 2;
        var r = Math.max(0, Math.min(barRadius, barW / 2, barH));
        var d = "M " + x + " " + baseY +
                " L " + x + " " + (topY + r) +
                " Q " + x + " " + topY + " " + (x + r) + " " + topY +
                " L " + (x + barW - r) + " " + topY +
                " Q " + (x + barW) + " " + topY + " " + (x + barW) + " " + (topY + r) +
                " L " + (x + barW) + " " + baseY + " Z";
        var bar = el("path", { "class": "ghchart__bar", d: d, fill: barColorFor(panel, j) });
        svg.appendChild(bar);
        var rec = { el: bar, pi: pi, bi: j };
        panelBars.push(rec);
        allBars.push(rec);

        var col = el("rect", {
          "class": "ghchart__barhit",
          x: M.left + slot * j, y: M.top - 6, width: slot, height: baseY - M.top + 6
        });
        (function (idx) {
          col.addEventListener("pointermove", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerdown", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerleave", hideTip);
        })(j);
        svg.appendChild(col);
      }

      /* ---- Annotation layer (drawn on top; pointer-events: none) ----
         · always-on value labels above chosen bars (panel.valueLabels)
         · anchor lines: a dotted horizontal reference at one bar's top
           extended to another, with a floating pill (panel.annotations) */
      var vlabels = panel.valueLabels || null;
      if (vlabels) {
        for (var vk in vlabels) {
          if (!Object.prototype.hasOwnProperty.call(vlabels, vk)) continue;
          var vi = +vk;
          var vt = el("text", {
            "class": "ghchart__barvalue",
            x: xCenter(vi), y: yAt(values[vi]) - 12, "text-anchor": "middle"
          });
          vt.textContent = vlabels[vk];
          svg.appendChild(vt);
        }
      }

      (panel.annotations || []).forEach(function (an) {
        if (an.type !== "anchor") return;
        var yLine = yAt(values[an.fromIndex]);
        svg.appendChild(el("line", {
          "class": "ghchart__anno-line",
          x1: xCenter(an.fromIndex) - barW / 2, y1: yLine,
          x2: xCenter(an.toIndex) + barW / 2, y2: yLine
        }));

        var chipIdx = an.chipAtIndex != null ? an.chipAtIndex : an.toIndex;
        var cx = xCenter(chipIdx);
        var cy = (yAt(values[chipIdx]) + yLine) / 2;
        var label = an.label || "";
        var fs = 19;
        var chipW = label.length * fs * 0.55 + 24;
        var chipH = fs + 16;
        svg.appendChild(el("rect", {
          "class": "ghchart__anno-chip",
          x: cx - chipW / 2, y: cy - chipH / 2, width: chipW, height: chipH, rx: chipH / 2
        }));
        var ct = el("text", {
          "class": "ghchart__anno-chip-text",
          x: cx, y: cy, dy: "0.34em", "text-anchor": "middle", "font-size": fs
        });
        ct.textContent = label;
        svg.appendChild(ct);
      });

      wrap.appendChild(svg);
      root.appendChild(wrap);
    });

    /* ---- Caption (optional rich copy) --------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Calendar heatmap renderer (GitHub contribution-graph style)
     ----------------------------------------------------------------
     One square per calendar day across a month range. Each panel is
     shaded on its own scale — every day in a month shares that month's
     level (the underlying series is monthly). Each panel's 5-step
     faint->dark ramp is derived from a single tunable base color.
     ================================================================ */
  renderers.heatmap = function (root, config) {
    root.textContent = "";

    var WHITE = [255, 255, 255], BLACK = [0, 0, 0];
    function hexToRgb(h) {
      h = String(h).replace("#", "");
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
    }
    function rgbToHex(c) {
      return "#" + c.map(function (x) {
        x = Math.max(0, Math.min(255, Math.round(x)));
        return (x < 16 ? "0" : "") + x.toString(16);
      }).join("");
    }
    function mix(a, b, t) {
      return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
    }
    // 5-step contribution ramp (level 0 faint -> level 4 dark) from one hex.
    function rampFrom(baseHex) {
      var b = hexToRgb(baseHex);
      return [
        rgbToHex(mix(b, WHITE, 0.80)),
        rgbToHex(mix(b, WHITE, 0.52)),
        rgbToHex(mix(b, WHITE, 0.20)),
        baseHex,
        rgbToHex(mix(b, BLACK, 0.30))
      ];
    }

    function trim0(s) { return s.replace(/\.0$/, ""); }
    function fmtM(v) {
      if (v >= 1000) { var g = v / 1000; return (g >= 10 ? Math.round(g) : trim0(g.toFixed(1))) + "B"; }
      if (v >= 1) return trim0(v.toFixed(1)) + "M";
      return Math.round(v * 1000) + "K";
    }

    var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    var MON_FULL = ["January", "February", "March", "April", "May", "June",
      "July", "August", "September", "October", "November", "December"];
    var WD = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

    var year = config.year || 2026;
    var startM = config.startMonth != null ? config.startMonth : 0;
    var endM = config.endMonth != null ? config.endMonth : 5;
    var emptyColor = config.emptyColor || "#ebedf0";

    function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
    function parseISO(s) { var p = s.split("-"); return new Date(+p[0], +p[1] - 1, +p[2]); }
    function dayStart(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); }

    /* ---- Eyebrow + title + subtitle ----------------------------- */
    if (config.eyebrow) {
      var eb = html("div", "ghheat__eyebrow");
      eb.appendChild(html("span", "ghheat__eyedot"));
      var ebt = html("span", "ghheat__eyetext");
      ebt.textContent = config.eyebrow;
      eb.appendChild(ebt);
      root.appendChild(eb);
    }
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle ghheat__subtitle");
      sub.innerHTML = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip -------------------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }
    var activeCell = null;
    function hideTip() { activeCell = null; tip.setAttribute("data-visible", "false"); }
    function showTip(rectEl, titleText, lines) {
      var reappearing = activeCell === null;
      activeCell = rectEl;
      tipTitle.textContent = titleText;
      tipBody.textContent = "";
      lines.forEach(function (ln) {
        var r = html("div", "ghchart__tt-line");
        r.innerHTML = ln;
        tipBody.appendChild(r);
      });
      var a = rectEl.getBoundingClientRect(), rr = root.getBoundingClientRect();
      var left = a.left + a.width / 2 - rr.left, top = a.top - rr.top - 12;
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";
      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = a.bottom - rr.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels -------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var base = (config.series && config.series[pi] && config.series[pi].color) || panel.base || "#3094ff";
      var ramp = rampFrom(base);
      var strong = ramp[4];

      var totals = panel.months || [];
      var activeVals = [];
      for (var mi = startM; mi <= endM; mi++) { if (totals[mi] != null) activeVals.push(totals[mi]); }
      var vmin = activeVals.length ? Math.min.apply(null, activeVals) : 0;
      var vmax = activeVals.length ? Math.max.apply(null, activeVals) : 1;
      function levelFor(t) {
        if (t == null) return -1;
        if (vmax === vmin) return 4;
        return Math.round((t - vmin) / (vmax - vmin) * 4);
      }

      var af = panel.activeFrom ? parseISO(panel.activeFrom) : new Date(year, startM, 1);
      var at = panel.activeTo ? parseISO(panel.activeTo) : new Date(year, endM, daysInMonth(year, endM));

      var wrap = html("div", "ghheat__panel");

      var head = html("div", "ghheat__phead");
      var badge = html("span", "ghheat__badge");
      badge.textContent = panel.badge != null ? panel.badge : (pi + 1);
      badge.style.background = strong;
      head.appendChild(badge);
      var pt = html("span", "ghheat__ptitle");
      pt.textContent = panel.title;
      head.appendChild(pt);
      if (panel.descriptor) {
        var pd = html("span", "ghheat__pdesc");
        pd.textContent = panel.descriptor;
        head.appendChild(pd);
      }
      wrap.appendChild(head);

      var body = html("div", "ghheat__body");

      /* ---- Calendar grid (SVG) ---- */
      var gridStart = new Date(year, startM, 1);
      var gs = new Date(gridStart);
      gs.setDate(gs.getDate() - gs.getDay());            // back to Sunday
      var gridEnd = new Date(year, endM, daysInMonth(year, endM));
      var ge = new Date(gridEnd);
      ge.setDate(ge.getDate() + (6 - ge.getDay()));      // forward to Saturday
      var nCols = Math.round((ge - gs) / (7 * 86400000)) + 1;

      var cell = 13, sq = 11, rad = 2.5, labL = 34, labT = 18;
      var W = labL + nCols * cell, H = labT + 7 * cell;
      var svg = el("svg", {
        "class": "ghheat__grid",
        viewBox: "0 0 " + W + " " + H,
        preserveAspectRatio: "xMinYMin meet",
        role: "img",
        "aria-label": panel.title + " calendar heatmap"
      });

      [1, 3, 5].forEach(function (r) {
        var t = el("text", {
          "class": "ghheat__wd", x: labL - 6, y: labT + r * cell + sq / 2,
          dy: "0.32em", "text-anchor": "end"
        });
        t.textContent = WD[r];
        svg.appendChild(t);
      });

      // Month labels above the column where each month begins.
      for (var lm = startM; lm <= endM; lm++) {
        var first = new Date(year, lm, 1);
        var colOf = Math.floor((dayStart(first) - gs) / (7 * 86400000));
        var mlab = el("text", { "class": "ghheat__month", x: labL + colOf * cell, y: 12, "text-anchor": "start" });
        mlab.textContent = MON[lm];
        svg.appendChild(mlab);
      }

      var afS = dayStart(af), atS = dayStart(at);
      var d = new Date(gs);
      for (var i = 0; i < nCols * 7; i++) {
        var row = d.getDay();
        var col = Math.floor(i / 7);
        var x = labL + col * cell, y = labT + row * cell;
        var m = d.getMonth();
        var inRange = d.getFullYear() === year && m >= startM && m <= endM;
        var ds = dayStart(d);
        var inWindow = inRange && ds >= afS && ds <= atS;
        var hasData = inWindow && totals[m] != null;
        var lvl = hasData ? levelFor(totals[m]) : -1;
        var cellEl = el("rect", {
          "class": "ghheat__cell", x: x, y: y, width: sq, height: sq, rx: rad,
          fill: lvl >= 0 ? ramp[lvl] : emptyColor
        });
        if (inRange) {
          (function (dt, mm, has) {
            var dateStr = WD[dt.getDay()] + " \u00b7 " + MON[mm] + " " + dt.getDate() + ", " + year;
            var lines;
            if (has) {
              var daily = totals[mm] / daysInMonth(year, mm);
              lines = [
                "<b>" + fmtM(daily) + "</b> " + panel.legendLabel,
                MON_FULL[mm] + " \u00b7 " + fmtM(totals[mm]) + " " + panel.unitTotal
              ];
            } else {
              lines = [panel.emptyNote || "No vetted data"];
            }
            cellEl.addEventListener("pointermove", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerdown", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerleave", hideTip);
          })(new Date(d), m, hasData);
        } else {
          cellEl.setAttribute("class", "ghheat__cell ghheat__cell--pad");
        }
        svg.appendChild(cellEl);
        d.setDate(d.getDate() + 1);
      }

      var gridwrap = html("div", "ghheat__gridwrap");
      gridwrap.appendChild(svg);
      body.appendChild(gridwrap);

      /* ---- Side column: stat + note + legend + extra ---- */
      var side = html("div", "ghheat__side");
      if (panel.stat) {
        var stat = html("div", "ghheat__stat");
        var big = html("span", "ghheat__statbig");
        big.textContent = panel.stat.big;
        big.style.color = strong;
        stat.appendChild(big);
        if (panel.stat.small) {
          var small = html("span", "ghheat__statsmall");
          small.textContent = panel.stat.small;
          stat.appendChild(small);
        }
        side.appendChild(stat);
      }
      if (panel.note) {
        var note = html("p", "ghheat__note");
        note.innerHTML = panel.note;
        side.appendChild(note);
      }
      var leg = html("div", "ghheat__legend");
      var lw1 = html("span", "ghheat__legword"); lw1.textContent = "Fewer"; leg.appendChild(lw1);
      ramp.forEach(function (c) {
        var s = html("span", "ghheat__sw"); s.style.background = c; leg.appendChild(s);
      });
      var lw2 = html("span", "ghheat__legword"); lw2.textContent = "More"; leg.appendChild(lw2);
      if (panel.legendLabel) {
        var lu = html("span", "ghheat__legunit"); lu.textContent = "\u00b7 " + panel.legendLabel; leg.appendChild(lu);
      }
      side.appendChild(leg);
      if (panel.extraNote) {
        var ex = html("div", "ghheat__extra");
        var exsw = html("span", "ghheat__sw ghheat__sw--empty");
        exsw.style.background = emptyColor;
        ex.appendChild(exsw);
        var ext = html("span", "ghheat__extratext");
        ext.innerHTML = panel.extraNote;
        ex.appendChild(ext);
        side.appendChild(ex);
      }
      body.appendChild(side);

      wrap.appendChild(body);
      root.appendChild(wrap);
    });

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Scatter renderer — X/Y numeric plot with categorical points.
     Used for cost-vs-quality benchmark charts: a highlighted "primary"
     point (glow halo), a "reference" point with a dotted crosshair, and
     muted "other" points. Values never print on the marks — a point's
     name shows for identity and the exact (y, x) reveals on hover.
     ================================================================ */
  renderers.scatter = function (root, config) {
    root.textContent = "";
    var uid = config.mountId;

    var VB = { w: 1000, h: 600 };
    var M = { top: 40, right: 30, bottom: 74, left: 96 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var fmtX = xa.format || function (v) { return String(v); };
    var fmtY = ya.format || function (v) { return String(v); };
    var tipSuffix = config.tipSuffix || "";

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) { return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min)); }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function colorOf(pt) {
      var s = config.series && config.series[pt.series || 0];
      return (s && s.color) || "#8c959f";
    }

    /* ---- Title (compact narrative headline, no rule) ------------ */
    var title = html("div", "ghchart__headline");
    title.textContent = config.title;
    root.appendChild(title);

    /* ---- Header + legend (from series) -------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      (config.series || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch ghchart__swatch--dot");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--scatter",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " scatter plot"
    });

    // Gridlines (both directions) + tick labels.
    var eps = 1e-6;
    for (var yv = ya.min; yv <= ya.max + eps; yv += ya.step) {
      var gy = yAt(yv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(yv);
      svg.appendChild(yl);
    }
    for (var xv = xa.min; xv <= xa.max + eps; xv += xa.step) {
      var gx = xAt(xv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline ghchart__gridline--v",
        x1: gx, y1: M.top, x2: gx, y2: baseY
      }));
      var isFirst = Math.abs(xv - xa.min) < eps;
      var isLast = Math.abs(xv - xa.max) < eps;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 34,
        "text-anchor": isFirst ? "start" : (isLast ? "end" : "middle")
      });
      xl.textContent = fmtX(xv);
      svg.appendChild(xl);
    }

    // Axis titles.
    if (xa.title) {
      var xt = el("text", {
        "class": "ghchart__axistitle",
        x: M.left + innerW / 2, y: VB.h - 12, "text-anchor": "middle"
      });
      xt.textContent = xa.title;
      svg.appendChild(xt);
    }
    if (ya.title) {
      var ytX = 24, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle",
        x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Reference crosshair through the flagged point (dual dotted lines).
    var refPt = (config.points || []).filter(function (p) { return p.reference; })[0];
    if (refPt) {
      var rColor = colorOf(refPt);
      var rx = xAt(refPt.x), ry = yAt(refPt.y);
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: rx, y1: M.top, x2: rx, y2: baseY, stroke: rColor
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: M.left, y1: ry, x2: VB.w - M.right, y2: ry, stroke: rColor
      }));
    }

    // Points: halo (highlight/reference) + dot + name label.
    var dotEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y), c = colorOf(pt);
      var big = pt.highlight || pt.reference;

      if (big) {
        svg.appendChild(el("circle", {
          "class": "ghchart__halo", cx: cx, cy: cy, r: 17, fill: c
        }));
      }
      var dot = el("circle", {
        "class": "ghchart__pt", cx: cx, cy: cy,
        r: big ? 9 : 5.5, fill: c
      });
      svg.appendChild(dot);
      dotEls.push({ el: dot, baseR: big ? 9 : 5.5 });

      // Name label — bold+colored for the story points, muted for the rest.
      var pos = pt.labelPos || (pt.highlight ? "below" : pt.reference ? "above" : "right");
      var dx = 0, dy = 0, anchor = "middle";
      var gap = big ? 15 : 9;
      if (pos === "right") { dx = gap + 4; dy = 5; anchor = "start"; }
      else if (pos === "left") { dx = -(gap + 4); dy = 5; anchor = "end"; }
      else if (pos === "above") { dy = -(gap + 6); anchor = "middle"; }
      else { dy = gap + 16; anchor = "middle"; }  // below
      var lbl = el("text", {
        "class": "ghchart__pt-label" + (big ? " ghchart__pt-label--strong" : ""),
        x: cx + dx, y: cy + dy, "text-anchor": anchor,
        fill: big ? c : null
      });
      lbl.textContent = pt.label;
      svg.appendChild(lbl);

      // Transparent hit target for hover/focus.
      var hit = el("circle", {
        "class": "ghchart__pthit", cx: cx, cy: cy, r: 20,
        tabindex: "0", role: "img",
        "aria-label": pt.label + ": " + fmtY(pt.y) + ", " + fmtX(pt.x) + tipSuffix
      });
      svg.appendChild(hit);
      hitEls.push(hit);
    });

    root.appendChild(svg);

    /* ---- Tooltip (title + free lines) --------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      dotEls.forEach(function (d, di) {
        d.el.setAttribute("r", di === i ? d.baseR + 2.5 : d.baseR);
      });

      tipTitle.textContent = pt.label;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = fmtY(pt.y) + " · " + fmtX(pt.x) + tipSuffix;
      tipBody.appendChild(line);

      var anchor = dotEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (d) { d.el.setAttribute("r", d.baseR); });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
  };

  /* ================================================================
     Trend renderer — dated time-series with a narrative headline.
     One renderer covers both HydraFusion time charts via config:
       • linear OR log y-axis (yAxis.scale), explicit yAxis.ticks;
       • phase background columns (with optional per-phase average);
       • filled squares (values reveal on hover — never printed);
       • red-X "invalid run" markers with leader-line annotations;
       • a dashed trend line + optional ±band + slope label;
       • an optional horizontal reference line, bracket, and final callout.
     ================================================================ */
  renderers.trend = function (root, config) {
    root.textContent = "";

    var VB = { w: 1000, h: 560 };
    var M = { top: 64, right: 56, bottom: 56, left: 104 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var isLog = ya.scale === "log";
    var fmtY = ya.format || function (v) { return String(v); };
    var series = config.series || [];
    function sColor(i, fb) {
      var s = series[i];
      return (s && s.color) || fb;
    }
    var cPoint = sColor(0, "#8250df");
    var cInvalid = sColor(1, "#cf222e");
    var cTrend = (config.trend && config.trend.color) || sColor(0, "#8250df");

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) {
      if (isLog) {
        var lo = Math.log(ya.min) / Math.LN10, hi = Math.log(ya.max) / Math.LN10;
        var t = (Math.log(v) / Math.LN10 - lo) / (hi - lo);
        return M.top + innerH * (1 - t);
      }
      return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min));
    }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Headline + subtitle + legend (HTML flow above the plot) --- */
    var headline = html("div", "ghchart__headline");
    headline.textContent = config.title;
    root.appendChild(headline);
    if (config.subtitle) {
      var sub = html("div", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }
    if (!config.hideLegend && series.length) {
      var legend = html("div", "ghchart__legend ghchart__legend--trend");
      /* Build one legend glyph. The "invalid" glyph mirrors the on-plot marker
         exactly — a red diamond outline with an X through it (same side=13 /
         q=3.4 proportions used to draw the mark below) — so the key matches
         the mark on the chart. */
      function trendGlyph(glyph, color) {
        if (glyph === "invalid") {
          var g = el("svg", {
            "class": "ghchart__glyph ghchart__glyph--invalid",
            viewBox: "0 0 22 22", width: "15", height: "15",
            style: "overflow:visible"
          });
          g.appendChild(el("rect", {
            x: 4.5, y: 4.5, width: 13, height: 13, rx: 1.5,
            transform: "rotate(45 11 11)", fill: "none",
            stroke: color, "stroke-width": 2
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 7.6, x2: 14.4, y2: 14.4,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 14.4, x2: 14.4, y2: 7.6,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          return g;
        }
        var gl = html("span", "ghchart__glyph ghchart__glyph--" + glyph);
        if (glyph === "line") { gl.style.borderTopColor = color; }
        else { gl.style.background = color; gl.style.borderColor = color; }
        if (glyph === "diamond") { gl.style.background = "transparent"; }
        return gl;
      }
      series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "square", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      (config.legendExtra || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "line", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      root.appendChild(legend);
    }

    /* ---- SVG plot ------------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--trend",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title
    });

    // Phase background columns + separators + labels (+ optional average).
    (config.phases || []).forEach(function (ph, i) {
      var x0 = xAt(ph.from), x1 = xAt(ph.to);
      svg.appendChild(el("rect", {
        "class": "ghchart__phase" + (i % 2 ? " ghchart__phase--alt" : ""),
        x: x0, y: M.top, width: Math.max(0, x1 - x0), height: innerH
      }));
      if (i > 0) {
        svg.appendChild(el("line", {
          "class": "ghchart__phase-sep", x1: x0, y1: M.top, x2: x0, y2: baseY
        }));
      }
      var pl = el("text", {
        "class": "ghchart__phase-label", x: (x0 + x1) / 2, y: M.top - 40,
        "text-anchor": "middle"
      });
      pl.textContent = ph.label;
      svg.appendChild(pl);
      if (ph.avg) {
        var pa = el("text", {
          "class": "ghchart__phase-avg", x: (x0 + x1) / 2, y: M.top + 24,
          "text-anchor": "middle", fill: cPoint
        });
        pa.textContent = ph.avg;
        svg.appendChild(pa);
      }
    });

    // Horizontal gridlines + y tick labels (skip a gridline at the reference y).
    var refY = config.reference ? config.reference.y : null;
    (ya.ticks || []).forEach(function (tv) {
      var gy = yAt(tv);
      if (refY === null || Math.abs(tv - refY) > 1e-9) {
        svg.appendChild(el("line", {
          "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
        }));
      }
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(tv);
      svg.appendChild(yl);
    });

    // X tick labels (dated).
    (xa.ticks || []).forEach(function (t) {
      var gx = xAt(t.at);
      var first = Math.abs(t.at - xa.min) < 1e-9, last = Math.abs(t.at - xa.max) < 1e-9;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 30, "text-anchor": first ? "start" : (last ? "end" : "middle")
      });
      xl.textContent = t.text;
      svg.appendChild(xl);
    });

    // Axis titles.
    if (ya.title) {
      var ytX = 22, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle", x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Trend: optional ±band (linear only), then the dashed line + slope label.
    var tr = config.trend;
    if (tr) {
      function trendY(x) {
        return tr.from.y + (tr.to.y - tr.from.y) * (x - tr.from.x) / (tr.to.x - tr.from.x);
      }
      if (tr.band && !isLog) {
        var p = tr.band;
        var pts = [
          [xAt(tr.from.x), yAt(trendY(tr.from.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) - p)],
          [xAt(tr.from.x), yAt(trendY(tr.from.x) - p)]
        ].map(function (a) { return a[0] + "," + a[1]; }).join(" ");
        svg.appendChild(el("polygon", {
          "class": "ghchart__trendband", points: pts, fill: cTrend
        }));
      }
      svg.appendChild(el("line", {
        "class": "ghchart__trendline",
        x1: xAt(tr.from.x), y1: yAt(tr.from.y),
        x2: xAt(tr.to.x), y2: yAt(tr.to.y), stroke: cTrend
      }));
      if (tr.label) {
        var lx, lyy;
        if (tr.labelAt) { lx = xAt(tr.labelAt.x); lyy = yAt(tr.labelAt.y); }
        else {
          lx = xAt(tr.to.x) + (tr.labelDX == null ? -6 : tr.labelDX);
          lyy = yAt(tr.to.y) + (tr.labelDY == null ? -12 : tr.labelDY);
        }
        var sl = el("text", {
          "class": "ghchart__slope", x: lx, y: lyy,
          "text-anchor": tr.labelAnchor || "end", fill: cTrend
        });
        sl.textContent = tr.label;
        svg.appendChild(sl);
      }
    }

    // Reference line (horizontal dashed) + right-aligned label.
    if (config.reference) {
      var rc = config.reference.color || "#24292f";
      var ryy = yAt(config.reference.y);
      svg.appendChild(el("line", {
        "class": "ghchart__refline", x1: M.left, y1: ryy, x2: VB.w - M.right, y2: ryy,
        stroke: rc
      }));
      if (config.reference.label) {
        var rl = el("text", {
          "class": "ghchart__reflabel", x: VB.w - M.right, y: ryy - 8,
          "text-anchor": "end", fill: rc
        });
        rl.textContent = config.reference.label;
        svg.appendChild(rl);
      }
    }

    // Data squares (values reveal on hover only).
    var side = 13, hSide = side / 2;
    var sqEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y);
      var sq = el("rect", {
        "class": "ghchart__square", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 2, fill: cPoint
      });
      svg.appendChild(sq);
      sqEls.push({ el: sq, cx: cx, cy: cy });

      // Hover/focus hit target — omitted when hover is disabled so the
      // (illustrative) point values never surface, on screen or to a11y.
      if (!config.disableHover) {
        var hit = el("rect", {
          "class": "ghchart__squarehit", x: cx - 13, y: cy - 13,
          width: 26, height: 26, tabindex: "0", role: "img",
          "aria-label": (pt.d || "") + " " + fmtY(pt.y)
        });
        svg.appendChild(hit);
        hitEls.push(hit);
      }
    });

    // Invalid-run markers: red diamond + X, leader line, annotation text.
    (config.invalids || []).forEach(function (iv) {
      var cx = xAt(iv.x), cy = yAt(iv.y);
      svg.appendChild(el("rect", {
        "class": "ghchart__invalid", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 1.5, fill: "none", stroke: cInvalid,
        transform: "rotate(45 " + cx + " " + cy + ")"
      }));
      var q = 3.4;
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy - q, x2: cx + q, y2: cy + q,
        stroke: cInvalid
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy + q, x2: cx + q, y2: cy - q,
        stroke: cInvalid
      }));
      if (iv.title) {
        var tx = cx + (iv.labelDX == null ? 16 : iv.labelDX);
        var ty = cy + (iv.labelDY == null ? -26 : iv.labelDY);
        svg.appendChild(el("line", {
          "class": "ghchart__leader", x1: cx, y1: cy, x2: tx, y2: ty, stroke: cInvalid
        }));
        var anchor = iv.labelAnchor || "start";
        var at = el("text", {
          "class": "ghchart__anno-title", x: tx, y: ty - 4, "text-anchor": anchor, fill: cInvalid
        });
        at.textContent = iv.title;
        svg.appendChild(at);
        if (iv.sub) {
          var asu = el("text", {
            "class": "ghchart__anno-sub", x: tx, y: ty + 11, "text-anchor": anchor
          });
          asu.textContent = iv.sub;
          svg.appendChild(asu);
        }
      }
    });

    // Bracket annotation (span with end ticks + centered label above).
    (config.brackets || []).forEach(function (b) {
      var x0 = xAt(b.fromX), x1 = xAt(b.toX), by = yAt(b.y);
      svg.appendChild(el("line", {
        "class": "ghchart__bracket", x1: x0, y1: by, x2: x1, y2: by
      }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x0, y1: by, x2: x0, y2: by + 7 }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x1, y1: by, x2: x1, y2: by + 7 }));
      var bl = el("text", {
        "class": "ghchart__bracket-label", x: (x0 + x1) / 2, y: by - 8, "text-anchor": "middle"
      });
      bl.textContent = b.text;
      svg.appendChild(bl);
    });

    // Final callout: ringed point + leader + title/sub placed inside the plot.
    if (config.callout) {
      var co = config.callout;
      var ccx = xAt(co.x), ccy = yAt(co.y);
      svg.appendChild(el("circle", {
        "class": "ghchart__callout-ring", cx: ccx, cy: ccy, r: 12, stroke: cPoint
      }));
      var ctx = ccx + (co.labelDX == null ? -16 : co.labelDX);
      var cty = ccy + (co.labelDY == null ? -30 : co.labelDY);
      svg.appendChild(el("line", {
        "class": "ghchart__leader ghchart__leader--callout", x1: ccx, y1: ccy, x2: ctx, y2: cty,
        stroke: cPoint
      }));
      var can = co.labelAnchor || "end";
      var ct = el("text", {
        "class": "ghchart__anno-title ghchart__anno-title--callout", x: ctx, y: cty - 4, "text-anchor": can, fill: cPoint
      });
      ct.textContent = co.title;
      svg.appendChild(ct);
      if (co.sub) {
        var cs = el("text", {
          "class": "ghchart__anno-sub ghchart__anno-sub--callout", x: ctx, y: cty + 12, "text-anchor": can
        });
        cs.textContent = co.sub;
        svg.appendChild(cs);
      }
    }

    root.appendChild(svg);

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.textContent = config.caption;
      root.appendChild(cap);
    }

    /* ---- Tooltip + hover interaction (skipped when disabled) ------ */
    if (!config.disableHover) {
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      sqEls.forEach(function (s, si) {
        var big = si === i;
        var w = big ? side + 5 : side;
        s.el.setAttribute("width", w);
        s.el.setAttribute("height", w);
        s.el.setAttribute("x", s.cx - w / 2);
        s.el.setAttribute("y", s.cy - w / 2);
      });

      var tipInfo = config.pointTip
        ? config.pointTip(pt)
        : { title: pt.d || "", line: fmtY(pt.y) };
      tipTitle.textContent = tipInfo.title;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = tipInfo.line;
      tipBody.appendChild(line);

      var anchor = sqEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      sqEls.forEach(function (s) {
        s.el.setAttribute("width", side);
        s.el.setAttribute("height", side);
        s.el.setAttribute("x", s.cx - hSide);
        s.el.setAttribute("y", s.cy - hSide);
      });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
    }
  };

  /* ================================================================
     Distribution dashboard renderer
     ----------------------------------------------------------------
     A composition figure (no hover — every value is printed as text, so
     the marks are inherently screen-reader accessible):
       • a compact narrative headline + muted subtitle;
       • an optional segmented "mix" bar — proportional colored segments,
         each with its name inside and count/percent below;
       • one or more panels of ranked horizontal bars, each row printing
         its category (left), a bar, and its count/percent (right).
     Colors come from config.series so the harness can tune them live:
       series[0]   = bar fill (single base hex);
       series[1..] = segment fills, in the order of config.segments.items.
     ================================================================ */
  renderers.distribution = function (root, config) {
    root.textContent = "";
    root.setAttribute("role", "img");
    if (config.title) root.setAttribute("aria-label", config.title);

    var series = config.series || [];
    var barColor = (series[0] && series[0].color) || "#8250df";
    var total = config.total ||
      (config.segments && config.segments.items
        ? config.segments.items.reduce(function (a, b) { return a + b.value; }, 0)
        : 0);

    function pctText(v) {
      if (!total) return "";
      return (Math.round((v / total) * 1000) / 10).toFixed(1) + "%";
    }
    /* Label format is config-driven so a chart can print counts, percents,
       or both without a renderer change: config.valueFormat =
       "both" (default) | "percent" | "count". Counts stay in the data either
       way so the percentages are always derived from them. */
    var valueFormat = config.valueFormat || "both";
    function valueHTML(v) {
      var p = pctText(v);
      if (valueFormat === "percent") return p;
      if (valueFormat === "count") return "<b>" + v + "</b>";
      return "<b>" + v + "</b>" + (p ? " (" + p + ")" : "");
    }

    /* ---- Headline + subtitle ------------------------------------- */
    if (config.title) {
      var head = html("div", "ghchart__headline");
      head.textContent = config.title;
      root.appendChild(head);
    }
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- Segmented "mix" bar -------------------------------------- */
    var seg = config.segments;
    if (seg && seg.items && seg.items.length) {
      var segSec = html("div", "ghchart__dist-section");
      if (seg.title) {
        var segTitle = html("div", "ghchart__panel-title");
        segTitle.textContent = seg.title;
        segSec.appendChild(segTitle);
      }
      var segTotal = seg.items.reduce(function (a, b) { return a + b.value; }, 0) || 1;
      var bar = html("div", "ghchart__seg");
      var notes = html("div", "ghchart__seg-note-row");
      seg.items.forEach(function (it, i) {
        var basis = (it.value / segTotal) * 100;
        var col = (series[i + 1] && series[i + 1].color) || it.color || barColor;

        var cell = html("div", "ghchart__seg-cell");
        cell.style.flex = "0 0 " + basis + "%";
        cell.style.background = col;
        cell.textContent = it.label;
        bar.appendChild(cell);

        var note = html("div", "ghchart__seg-note");
        note.style.flex = "0 0 " + basis + "%";
        note.innerHTML = valueHTML(it.value);
        notes.appendChild(note);
      });
      segSec.appendChild(bar);
      segSec.appendChild(notes);
      root.appendChild(segSec);
    }

    /* ---- Ranked bar panels (grid: 2-up, stacking on narrow) ------ */
    var panels = config.panels || [];
    if (panels.length) {
      var cols = html("div", "ghchart__dist-cols");
      if (config.columns === 1 || panels.length === 1) {
        cols.classList.add("ghchart__dist-cols--stack");
      }
      panels.forEach(function (panel) {
        var sec = html("div", "ghchart__dist-section");
        if (panel.title) {
          var pt = html("div", "ghchart__panel-title");
          pt.textContent = panel.title;
          sec.appendChild(pt);
        }
        var list = html("div", "ghchart__dist-list");
        var pmax = (panel.items || []).reduce(function (m, it) {
          return it.value > m ? it.value : m;
        }, 0) || 1;
        (panel.items || []).forEach(function (it) {
          var row = html("div", "ghchart__dist-row");

          var cat = html("div", "ghchart__dist-cat");
          cat.textContent = it.label;
          row.appendChild(cat);

          var track = html("div", "ghchart__dist-track");
          var b = html("div", "ghchart__dist-bar");
          b.style.width = Math.max((it.value / pmax) * 100, 1.2) + "%";
          b.style.background = it.color || barColor;
          track.appendChild(b);
          row.appendChild(track);

          var val = html("div", "ghchart__dist-val");
          val.innerHTML = valueHTML(it.value);
          row.appendChild(val);

          list.appendChild(row);
        });
        sec.appendChild(list);
        cols.appendChild(sec);
      });
      root.appendChild(cols);
    }

    /* ---- Caption / footnote -------------------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Dispatcher — pick a renderer by config.type and draw into a mount
     ================================================================ */
  function render(config, mountEl) {
    var root = mountEl || document.getElementById(config.mountId);
    if (!root) return;
    root.classList.add("ghchart");
    /* Resolve the renderer from the SHARED registry (see registration below),
       not this bundle's private `renderers`. When several exported charts share
       one page they all merge into one registry, so every chart finds its
       renderer even if an older engine bundle loaded first. */
    var reg = (global.GHChartEngine && global.GHChartEngine.renderers) || renderers;
    var fn = reg[config.type] || renderers[config.type] || reg.line || renderers.line;
    fn(root, config);
  }

  /* ----------------------------------------------------------------
     Shared-engine registration (multi-embed safe)
     ----------------------------------------------------------------
     Every exported chart.js embeds this whole engine. When two or more
     charts are placed on the same page (e.g. a WordPress post), we must NOT
     let the first-loaded bundle silently win — an older bundle would be
     missing renderers added later (this is why a heatmap pasted after an
     older bar chart used to vanish).

     Instead we keep ONE shared engine:
       • every bundle merges in any renderers the shared registry lacks;
       • the newest bundle (highest ENGINE_VERSION) provides render(), which
         reads renderers from the shared registry at call time.
     The result is order-independent and resilient to version skew as new
     chart types are added. Bump ENGINE_VERSION whenever renderers or render
     change so a newer embed upgrades an older shared engine in place. */
  var ENGINE_VERSION = 10;
  var existing = global.GHChartEngine;
  if (existing && typeof existing.version === "number") {
    if (ENGINE_VERSION > existing.version) {
      /* This bundle is newer. Every export embeds the COMPLETE engine, so this
         bundle's `renderers` is a self-contained, up-to-date set. Adopt it as
         the shared registry (upgrading older same-named renderers in place),
         while defensively preserving any renderer types only the older engine
         happened to have. Then take over render(). */
      for (var ek in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ek) && !renderers[ek]) {
          renderers[ek] = existing.renderers[ek];
        }
      }
      existing.renderers = renderers;
      existing.render = render;
      existing.version = ENGINE_VERSION;
    } else {
      /* Same or older version already present: only contribute renderer types
         the shared registry is missing (don't clobber an equal/newer engine). */
      for (var rk in renderers) {
        if (Object.prototype.hasOwnProperty.call(renderers, rk) && !existing.renderers[rk]) {
          existing.renderers[rk] = renderers[rk];
        }
      }
    }
  } else {
    /* No engine yet, or a pre-versioning (old-style) engine loaded first.
       Take over, preserving any renderers the old-style engine registered. */
    if (existing && existing.renderers) {
      for (var ok in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ok) && !renderers[ok]) {
          renderers[ok] = existing.renderers[ok];
        }
      }
    }
    global.GHChartEngine = { renderers: renderers, render: render, version: ENGINE_VERSION };
  }
})(typeof window !== "undefined" ? window : this);

;(function () {
  "use strict";
  var CONFIG = {
    mountId: "gh-chart-cb-distribution",
    type: "distribution",
    title: "What's inside CheckpointBench: real developer checkpoints",
    subtitle: "Distribution by programming language, task difficulty, and the kind of work the developer was doing",
    total: 276,
    columns: 2,
    valueFormat: "percent",
    series: [
      {
        name: "Bars",
        color: "#8250df"
      },
      {
        name: "Easy",
        color: "#1a7f37"
      },
      {
        name: "Medium",
        color: "#bf8700"
      },
      {
        name: "Hard",
        color: "#cf222e"
      }
    ],
    segments: {
      title: "Difficulty mix",
      items: [
        {
          label: "Easy",
          value: 116
        },
        {
          label: "Medium",
          value: 107
        },
        {
          label: "Hard",
          value: 53
        }
      ]
    },
    panels: [
      {
        title: "Programming language",
        items: [
          {
            label: "Python family",
            value: 92
          },
          {
            label: "TypeScript",
            value: 48
          },
          {
            label: "C/C++",
            value: 27
          },
          {
            label: "JavaScript",
            value: 21
          },
          {
            label: "Unknown",
            value: 21
          },
          {
            label: "HTML/CSS",
            value: 14
          },
          {
            label: "C#",
            value: 13
          },
          {
            label: "Shell",
            value: 13
          },
          {
            label: "Go",
            value: 7
          },
          {
            label: "Java",
            value: 4
          },
          {
            label: "Rust",
            value: 4
          }
        ]
      },
      {
        title: "Task type",
        items: [
          {
            label: "Bug fix / debugging",
            value: 90
          },
          {
            label: "Feature implementation",
            value: 73
          },
          {
            label: "Code explanation / Q&A",
            value: 44
          },
          {
            label: "Build / config / deployment",
            value: 22
          },
          {
            label: "Planning / documentation",
            value: 19
          },
          {
            label: "Unresolved",
            value: 13
          },
          {
            label: "Refactor / maintenance",
            value: 8
          },
          {
            label: "Testing / validation",
            value: 3
          },
          {
            label: "Other",
            value: 3
          },
          {
            label: "Data / ML / analysis",
            value: 1
          }
        ]
      }
    ]
  };
  function boot() {
    var root = document.getElementById(CONFIG.mountId);
    if (!root || root.getAttribute("data-gh-rendered")) return;
    root.setAttribute("data-gh-rendered", "1");
    window.GHChartEngine.render(CONFIG);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();

</script>

<div id="gh-chart-cb-distribution" class="ghchart"></div>



<p class="wp-block-paragraph">CheckpointBench is an internal multi-turn benchmark curated from real GitHub Copilot agentic coding sessions. Each conversation is anchored to a specific public repository and immutable commit, ensuring every session is replayable. The benchmark is balanced across language, task type, difficulty, scrubbed for quality, resulting in a realistic evaluation set that closely mirrors production agentic sessions. On this benchmark, HydraFusion comes within 0.1 percentage points of Opus 5 at 65% lower cost.</p>



<style data-wp-block-html="css">
.ghchart {
  /* Theme tokens */
  --ghc-text: #1f2328;
  --ghc-muted: #656d76;
  --ghc-grid: #d0d7de;
  --ghc-axis: #59636e;
  --ghc-card-bg: #ffffff;
  --ghc-card-border: #d0d7de;
  --ghc-divider: #b6bfb8;
  --ghc-guide: #afb8c1;

  /* GitHub brand font — matches the github.blog stack (Mona Sans, with
     the blog's metric fallback) so it renders identically in-context. */
  font-family: "Mona Sans", "Mona Sans Fallback", -apple-system, "system-ui",
    "Segoe UI", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 699px;
  margin: 0 auto;
  color: var(--ghc-text);
  line-height: 1.4;
}

.ghchart *,
.ghchart *::before,
.ghchart *::after { box-sizing: border-box; }

/* ------------------------------------------------------------------ */
/* Header + legend                                                     */
/* ------------------------------------------------------------------ */
.ghchart__header {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  min-height: 24px;
  margin-bottom: 4px;
}

.ghchart__legend {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.ghchart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: clamp(14px, 1.9vw, 17px);
  font-weight: 600;
  color: var(--ghc-muted);
  white-space: nowrap;
}

.ghchart__swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  flex: 0 0 auto;
}

/* ------------------------------------------------------------------ */
/* Plot (SVG)                                                          */
/* ------------------------------------------------------------------ */
.ghchart__plot {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  touch-action: pan-y;
}

.ghchart__gridline {
  stroke: var(--ghc-grid);
  stroke-width: 1;
  stroke-dasharray: 2 6;
  stroke-linecap: round;
  opacity: 0.9;
}

.ghchart__axislabel {
  fill: var(--ghc-axis);
  font-size: 22px;
  font-weight: 500;
}
.ghchart__axislabel--x { font-weight: 600; }

.ghchart__line {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ghchart__dot { transition: r 0.12s ease; }

/* Dashed threshold line (e.g. a target/reference level) + floating label.
   Stroke color is set inline per chart; dash + label styling live here. */
.ghchart__threshold {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.9;
}
.ghchart__threshold-label {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Hatched band marking a partial / projected trailing region. */
.ghchart__projection { pointer-events: none; }

.ghchart__guide {
  stroke: var(--ghc-guide);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
  opacity: 0;
  transition: opacity 0.12s ease;
}

.ghchart__hit { fill: transparent; }

/* ------------------------------------------------------------------ */
/* Scatter plot                                                        */
/* ------------------------------------------------------------------ */
.ghchart__gridline--v { /* vertical gridlines reuse the dashed grid style */ }

.ghchart__axistitle {
  fill: var(--ghc-muted);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Dotted reference crosshair through the "reference" point. Stroke color
   is set inline (tracks the reference series color). */
.ghchart__crosshair {
  stroke-width: 2;
  stroke-dasharray: 2 7;
  stroke-linecap: round;
  opacity: 0.85;
  pointer-events: none;
}

.ghchart__halo { opacity: 0.18; pointer-events: none; }
.ghchart__pt { transition: r 0.12s ease; }

.ghchart__pt-label {
  fill: var(--ghc-muted);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  pointer-events: none;
}
.ghchart__pt-label--strong { font-size: 21px; font-weight: 700; }

.ghchart__pthit { fill: transparent; cursor: default; outline: none; }
.ghchart__pthit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

.ghchart__swatch--dot { border-radius: 50%; }

/* ------------------------------------------------------------------ */
/* Trend chart (dated time-series with narrative headline)             */
/* ------------------------------------------------------------------ */
.ghchart__headline {
  font-size: clamp(17px, 2.3vw, 20px);
  line-height: 1.32;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
  margin: 0 0 8px;
}

/* Trend legend sits left-aligned under the headline/subtitle. */
.ghchart__legend--trend {
  justify-content: flex-start;
  margin: 2px 0 14px;
}
.ghchart__glyph { flex: 0 0 auto; }
.ghchart__glyph--square {
  width: 13px; height: 13px; border-radius: 2px;
}
.ghchart__glyph--diamond {
  width: 12px; height: 12px; border: 1.5px solid; transform: rotate(45deg);
}
.ghchart__glyph--line {
  width: 20px; height: 0; border-top: 2px dashed; border-radius: 0;
}

/* Phase background columns + separators + top labels. */
.ghchart__phase { fill: #f6f8fa; opacity: 0.55; }
.ghchart__phase--alt { fill: #ffffff; opacity: 0; }
.ghchart__phase-sep {
  stroke: var(--ghc-grid); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.8;
}
.ghchart__phase-label {
  fill: var(--ghc-muted);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
}
.ghchart__phase-avg {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Dashed trend line + ± noise band + slope label. */
.ghchart__trendband { opacity: 0.12; pointer-events: none; }
.ghchart__trendline {
  stroke-width: 2.5;
  stroke-dasharray: 8 6;
  stroke-linecap: round;
  fill: none;
}
.ghchart__slope {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Horizontal reference line + label. */
.ghchart__refline {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.85;
}
.ghchart__reflabel {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Data squares — values reveal on hover only. */
.ghchart__square { transition: width 0.12s ease, height 0.12s ease, x 0.12s ease, y 0.12s ease; }
.ghchart__squarehit { fill: transparent; cursor: default; outline: none; }
.ghchart__squarehit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

/* Invalid-run markers + leader-line annotations. */
.ghchart__invalid { stroke-width: 2; }
.ghchart__invalid-x { stroke-width: 2; stroke-linecap: round; }
.ghchart__leader { stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.8; }
.ghchart__leader--callout { stroke-dasharray: none; opacity: 0.6; }
.ghchart__anno-title {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.ghchart__anno-sub {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 500;
}
/* Final-callout labels read a touch larger than the invalid-run notes. */
.ghchart__anno-title--callout { font-size: 17px; }
.ghchart__anno-sub--callout { font-size: 14px; }

/* Bracket span annotation. */
.ghchart__bracket { stroke: var(--ghc-axis); stroke-width: 1.5; opacity: 0.75; }
.ghchart__bracket-label {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
}

/* Final callout ring. */
.ghchart__callout-ring { fill: none; stroke-width: 2; }

/* ------------------------------------------------------------------ */
/* Title + divider                                                     */
/* ------------------------------------------------------------------ */
.ghchart__title {
  /* Matches the github.blog H2 (.wp-block-heading) exactly */
  display: block;
  font-size: 32px;
  line-height: 40px;
  font-weight: 700;
  letter-spacing: -0.18px;
  color: var(--ghc-text);
}

.ghchart__rule {
  height: 1.5px;
  border-radius: 1.5px;
  background: var(--ghc-divider);
  margin: 10px 0 18px;
}

/* ------------------------------------------------------------------ */
/* Bar chart: subtitle, panels, bars, caption                          */
/* ------------------------------------------------------------------ */
.ghchart__subtitle {
  margin: -6px 0 20px;
  font-size: clamp(13px, 1.6vw, 15px);
  line-height: 1.5;
  color: var(--ghc-muted);
}

.ghchart__panel { margin-top: 30px; }
.ghchart__panel:first-of-type { margin-top: 6px; }

.ghchart__panel-title {
  margin: 0 0 6px;
  font-size: clamp(15px, 1.9vw, 18px);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
}

.ghchart__plot--bar { touch-action: pan-y; }

.ghchart__axisline {
  stroke: var(--ghc-axis);
  stroke-width: 1.25;
  opacity: 0.35;
}

.ghchart__bar { transition: opacity 0.12s ease; }
.ghchart__barhit { fill: transparent; }

/* Always-on value labels above selected bars. */
.ghchart__barvalue {
  fill: var(--ghc-text);
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Anchor annotation: dotted reference line + floating pill. */
.ghchart__anno-line {
  stroke: #8b949e;
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  stroke-linecap: round;
  pointer-events: none;
}
.ghchart__anno-chip {
  fill: #ffebe9;
  stroke: #ffcecb;
  stroke-width: 1;
  pointer-events: none;
}
.ghchart__anno-chip-text {
  fill: #a0111f;
  font-weight: 700;
  pointer-events: none;
}

.ghchart__caption {
  margin-top: 22px;
  padding-top: 12px;
  border-top: 1px solid var(--ghc-card-border);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ghc-muted);
}
.ghchart__caption b { color: var(--ghc-text); font-weight: 700; }
.ghchart__caption code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.92em;
  padding: 1px 5px;
  background: #f6f8fa;
  border: 1px solid var(--ghc-card-border);
  border-radius: 5px;
}
.ghchart__tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid #aceebb;
  color: #0a5223;
  background: #dafbe1;
  white-space: nowrap;
  vertical-align: middle;
}

/* ------------------------------------------------------------------ */
/* Distribution dashboard (segmented mix bar + ranked bar panels)      */
/* ------------------------------------------------------------------ */
.ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-section:first-of-type { margin-top: 12px; }

/* Segmented proportional "mix" bar: name inside, count/percent below.
   Separators are 2px inside borders (box-sizing:border-box) rather than a
   flex gap, so the three segments sum to exactly 100% with no overflow. */
.ghchart__seg {
  display: flex;
  width: 100%;
  height: 42px;
  margin-top: 2px;
  border-radius: 7px;
  overflow: hidden;
}
.ghchart__seg-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  color: #ffffff;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
}
.ghchart__seg-cell + .ghchart__seg-cell { border-left: 2px solid var(--ghc-card-bg); }
.ghchart__seg-note-row {
  display: flex;
  width: 100%;
  margin-top: 8px;
}
.ghchart__seg-note {
  min-width: 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Keep the outer notes from spilling past the chart edges. */
.ghchart__seg-note:first-child { text-align: left; }
.ghchart__seg-note:last-child { text-align: right; }
.ghchart__seg-note b { color: var(--ghc-text); font-weight: 700; }

/* Ranked horizontal bar list: [category | bar track | value].
   The list is the grid and rows use display:contents, so the category,
   bar, and value columns line up across every row in a panel. */
.ghchart__dist-list {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  align-items: center;
  column-gap: 12px;
  row-gap: 10px;
  margin-top: 4px;
}
.ghchart__dist-row { display: contents; }
.ghchart__dist-cat {
  text-align: right;
  font-size: 13px;
  line-height: 1.25;
  color: var(--ghc-text);
  white-space: nowrap;
}
.ghchart__dist-track { width: 100%; }
.ghchart__dist-bar {
  height: 15px;
  min-width: 2px;
  border-radius: 3px;
}
.ghchart__dist-val {
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ghchart__dist-val b { color: var(--ghc-text); font-weight: 700; }

/* Two-up panel grid (collapses to one column on narrow screens).
   minmax(0,1fr) lets columns shrink so long labels never force overflow. */
.ghchart__dist-cols {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px 30px;
}
.ghchart__dist-cols .ghchart__dist-section { margin-top: 0; }
.ghchart__dist-cols--stack { grid-template-columns: 1fr; }
.ghchart__dist-cols--stack .ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-cols--stack .ghchart__dist-section:first-child { margin-top: 0; }

/* Tighten the label/value type a touch in the 2-up layout so the bars
   keep room at the 699px blog width. */
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-cat { font-size: 12px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-val { font-size: 11.5px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-list { column-gap: 9px; }

@media (max-width: 600px) {
  .ghchart__dist-cols { grid-template-columns: 1fr; gap: 26px; }
  .ghchart__dist-cols .ghchart__dist-section ~ .ghchart__dist-section { margin-top: 2px; }
}

/* Let long category labels wrap on very narrow (mobile) widths. */
@media (max-width: 480px) {
  .ghchart__dist-cat { white-space: normal; }
}

/* ------------------------------------------------------------------ */
/* Calendar heatmap (GitHub contribution-graph style)                  */
/* ------------------------------------------------------------------ */
.ghheat__eyebrow { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.ghheat__eyedot {
  width: 9px; height: 9px; border-radius: 50%;
  background: #cf222e; box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.14);
  flex: 0 0 auto;
}
.ghheat__eyetext {
  font-size: 12px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: #cf222e;
}
/* Subtitle here is a prominent lede, not the muted variant. */
.ghheat__subtitle {
  color: var(--ghc-text);
  font-size: clamp(14px, 1.7vw, 16px);
  /* 24px top gap matches the github.blog H2 → paragraph block spacing. */
  margin: 24px 0 8px;
}
.ghheat__subtitle b { font-weight: 700; }

.ghheat__panel { margin-top: 36px; }
.ghheat__phead {
  display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.ghheat__badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  color: #fff; font-size: 12px; font-weight: 700;
  flex: 0 0 auto; align-self: center;
}
.ghheat__ptitle {
  font-size: clamp(16px, 2vw, 19px); font-weight: 700;
  letter-spacing: -0.01em; color: var(--ghc-text);
}
.ghheat__pdesc { font-size: 14px; color: var(--ghc-muted); }

.ghheat__body { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }
.ghheat__gridwrap { flex: 1 1 340px; min-width: 260px; }
.ghheat__grid { display: block; width: 100%; height: auto; overflow: visible; }
.ghheat__cell {
  stroke: rgba(27, 31, 36, 0.06);
  stroke-width: 1;
  transition: stroke 0.1s ease;
}
.ghheat__cell:hover { stroke: #24292f; stroke-width: 1.4; }
.ghheat__cell--pad { opacity: 0; pointer-events: none; }
.ghheat__wd, .ghheat__month {
  fill: var(--ghc-muted); font-size: 11px; font-weight: 600;
}

.ghheat__side { flex: 1 1 250px; min-width: 230px; }
.ghheat__stat { display: flex; align-items: baseline; gap: 7px; }
.ghheat__statbig {
  font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em;
  line-height: 1;
}
.ghheat__statsmall { font-size: 15px; font-weight: 600; color: var(--ghc-muted); }
.ghheat__note {
  margin: 10px 0 16px; font-size: 14px; line-height: 1.55; color: var(--ghc-muted);
}
.ghheat__note b { color: var(--ghc-text); font-weight: 700; }
.ghheat__legend {
  display: flex; align-items: center; gap: 5px;
  font-size: 12.5px; color: var(--ghc-muted); flex-wrap: wrap;
}
.ghheat__sw { width: 12px; height: 12px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }
.ghheat__sw--empty { border: 1px solid #d0d7de; }
.ghheat__legunit { margin-left: 2px; }
.ghheat__extra {
  display: flex; align-items: flex-start; gap: 7px; margin-top: 14px;
  font-size: 12px; line-height: 1.5; color: var(--ghc-muted); max-width: 330px;
}
.ghheat__extra .ghheat__sw { margin-top: 2px; }

/* ------------------------------------------------------------------ */
/* Tooltip                                                             */
/* ------------------------------------------------------------------ */
.ghchart__tooltip {
  position: absolute;
  z-index: 5;
  top: 0;
  left: 0;
  min-width: 128px;
  max-width: 260px;
  padding: 10px 12px;
  background: var(--ghc-card-bg);
  border: 1px solid var(--ghc-card-border);
  border-radius: 12px;
  /* Layered drop shadow from Primer Brand's ActionMenu (.ActionMenu__menu) */
  box-shadow:
    0px 100px 80px rgba(0, 0, 0, 0.01),
    0px 41px 33px rgba(0, 0, 0, 0.02),
    0px 22px 17px rgba(0, 0, 0, 0.02),
    0px 12px 10px rgba(0, 0, 0, 0.03),
    0px 6px 5px rgba(0, 0, 0, 0.04),
    0px 2px 2px rgba(0, 0, 0, 0.07);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -100%);
  /* Fade in on show and out on hide; quickly ease left/top when tracking
     from one point to the next. The position ease is suppressed on the
     first appearance (see chart.js) so the tooltip fades in place rather
     than sliding in from its previous spot. */
  transition: opacity 0.12s ease,
    left 0.1s cubic-bezier(0.4, 0, 0.2, 1),
    top 0.1s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: opacity, transform, left, top;
}
.ghchart__tooltip[data-visible="true"] { opacity: 1; }

.ghchart__tt-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--ghc-text);
  margin-bottom: 6px;
}

.ghchart__tt-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ghc-muted);
}
.ghchart__tt-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.ghchart__tt-name { flex: 1 1 auto; }
.ghchart__tt-value { font-weight: 700; color: var(--ghc-text); font-variant-numeric: tabular-nums; }

/* Bar tooltip body: a title line + free-form text lines (no swatches). */
.ghchart__tt-line {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ghc-muted);
}
.ghchart__tt-line:first-child { color: var(--ghc-text); font-weight: 600; }
.ghchart__tt-line + .ghchart__tt-line { margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ghchart__dot,
  .ghchart__guide,
  .ghchart__bar,
  .ghchart__pt,
  .ghchart__square,
  .ghheat__cell,
  .ghchart__tooltip { transition: none; }
}
</style>

<script data-wp-block-html="js">
/*
  Shared chart engine — reusable renderer for the blog-charts library.
  ------------------------------------------------------------------
  Exposes window.GHChartEngine = { renderers, render(config, mountEl?) }.
  Renderers are keyed by config.type (currently "line"); add a new viz type
  by registering GHChartEngine.renderers[<type>] = function (root, config) {…}.

  This same file is embedded verbatim at the top of each exported WordPress
  chart.js, followed by that chart's CONFIG + a small auto-boot. It has no
  external dependencies and is safe to include more than once on a page.
*/
(function (global) {
  "use strict";

  var SVGNS = "http://www.w3.org/2000/svg";

  function el(tag, attrs) {
    var node = document.createElementNS(SVGNS, tag);
    if (attrs) {
      for (var k in attrs) {
        if (Object.prototype.hasOwnProperty.call(attrs, k)) {
          node.setAttribute(k, attrs[k]);
        }
      }
    }
    return node;
  }

  function html(tag, cls) {
    var node = document.createElement(tag);
    if (cls) node.className = cls;
    return node;
  }

  function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }

  var renderers = {};

  /* ================================================================
     Line chart renderer
     ================================================================ */
  renderers.line = function (root, config) {
    // viewBox geometry (design units; scales fluidly to any width).
    var VB = { w: 1000, h: 560 };
    var M = { top: 30, right: 34, bottom: 56, left: 86 };
    // Inset the plotted points slightly from the axis so the first/last
    // months (e.g. "Jan") don't crowd the y-axis labels (e.g. "50").
    var PAD_X = 18;
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    // Reset (in case of re-render) and use a unique id prefix so multiple
    // charts on one page never share gradient ids.
    root.textContent = "";
    var uid = config.mountId;

    var n = config.labels.length;
    var yMin = config.yAxis.min;
    var yMax = config.yAxis.max;
    var step = config.yAxis.step;
    // Y-axis tick labels can format differently from the tooltip values
    // (e.g. a plain "40" on the axis but "40%" in the tooltip).
    var fmtAxis = config.formatAxis || config.formatValue;

    var plotW = innerW - 2 * PAD_X;
    var stepX = n <= 1 ? 0 : plotW / (n - 1);
    function xAt(i) { return n <= 1 ? M.left + innerW / 2 : M.left + PAD_X + stepX * i; }
    function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
    var baseY = yAt(yMin);

    /* ---- Title + divider (top) ---------------------------------- */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    /* ---- Header + legend ---------------------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      config.series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " line chart"
    });

    // Gradient defs (one per area series).
    var defs = el("defs");
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var g = el("linearGradient", {
        id: uid + "-grad-" + si, x1: "0", y1: "0", x2: "0", y2: "1"
      });
      g.appendChild(el("stop", { offset: "0", "stop-color": s.color, "stop-opacity": "0.28" }));
      g.appendChild(el("stop", { offset: "0.55", "stop-color": s.color, "stop-opacity": "0.08" }));
      g.appendChild(el("stop", { offset: "1", "stop-color": s.color, "stop-opacity": "0" }));
      defs.appendChild(g);
    });
    // Diagonal-hatch pattern for an optional "partial / projected" band.
    if (config.projection) {
      var pat = el("pattern", {
        id: uid + "-hatch", width: 7, height: 7,
        patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)"
      });
      pat.appendChild(el("line", {
        x1: 0, y1: 0, x2: 0, y2: 7,
        stroke: config.projection.color || "#8c959f",
        "stroke-width": 1.1, "stroke-opacity": 0.5
      }));
      defs.appendChild(pat);
    }
    svg.appendChild(defs);

    // Gridlines + y-axis labels.
    for (var v = yMin; v <= yMax + 0.0001; v += step) {
      var gy = yAt(v);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 22, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtAxis(v);
      svg.appendChild(yl);
    }

    // x-axis labels — either one per data point (default) or a sparse set
    // of ticks decoupled from the data (config.xTicks: [{at,text}, …]),
    // which suits a dense daily series that only needs month markers.
    var xTicks = config.xTicks ||
      config.labels.map(function (t, i) { return { at: i, text: t }; });
    xTicks.forEach(function (tk) {
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: xAt(tk.at), y: VB.h - M.bottom + 36,
        "text-anchor": tk.at === 0 ? "start" : (tk.at === n - 1 ? "end" : "middle")
      });
      xl.textContent = tk.text;
      svg.appendChild(xl);
    });

    // Optional hatched "partial / projected" band over the trailing region.
    if (config.projection) {
      var px = xAt(config.projection.fromIndex);
      svg.appendChild(el("rect", {
        "class": "ghchart__projection",
        x: px, y: M.top, width: (VB.w - M.right) - px, height: baseY - M.top,
        fill: "url(#" + uid + "-hatch)"
      }));
    }

    // Optional dashed threshold line + floating label (e.g. a 50% target).
    if (config.threshold) {
      var thy = yAt(config.threshold.value);
      svg.appendChild(el("line", {
        "class": "ghchart__threshold",
        x1: M.left, y1: thy, x2: VB.w - M.right, y2: thy,
        stroke: config.threshold.color
      }));
      if (config.threshold.label) {
        var thl = el("text", {
          "class": "ghchart__threshold-label",
          x: M.left + PAD_X, y: thy - 10, "text-anchor": "start",
          fill: config.threshold.color
        });
        thl.textContent = config.threshold.label;
        svg.appendChild(thl);
      }
    }

    // Precompute point coords per series.
    var pts = config.series.map(function (s) {
      return s.values.map(function (val, i) { return { x: xAt(i), y: yAt(val) }; });
    });

    // Smooth (Catmull-Rom -> cubic Bezier) path builder.
    function smooth(points) {
      if (!points.length) return "";
      if (points.length < 2) return "M" + points[0].x + "," + points[0].y;
      var d = "M " + points[0].x + "," + points[0].y;
      for (var i = 0; i < points.length - 1; i++) {
        var p0 = points[i - 1] || points[i];
        var p1 = points[i];
        var p2 = points[i + 1];
        var p3 = points[i + 2] || p2;
        var t = 1 / 6;
        var c1x = p1.x + (p2.x - p0.x) * t;
        var c1y = p1.y + (p2.y - p0.y) * t;
        var c2x = p2.x - (p3.x - p1.x) * t;
        var c2y = p2.y - (p3.y - p1.y) * t;
        d += " C " + c1x + "," + c1y + " " + c2x + "," + c2y + " " + p2.x + "," + p2.y;
      }
      return d;
    }

    // Area fills (drawn first, behind lines).
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var p = pts[si];
      var d = smooth(p) + " L " + p[p.length - 1].x + "," + baseY +
              " L " + p[0].x + "," + baseY + " Z";
      svg.appendChild(el("path", { d: d, fill: "url(#" + uid + "-grad-" + si + ")" }));
    });

    // Vertical hover guide (hidden until hover).
    var guide = el("line", {
      "class": "ghchart__guide",
      x1: xAt(0), y1: M.top - 6, x2: xAt(0), y2: baseY
    });
    svg.appendChild(guide);

    // Lines (draw in reverse so the first series sits on top).
    config.series.slice().reverse().forEach(function (s) {
      var si = config.series.indexOf(s);
      svg.appendChild(el("path", {
        "class": "ghchart__line",
        d: smooth(pts[si]),
        stroke: s.color,
        "stroke-width": s.width || (si === 0 ? 4 : 3.25)
      }));
    });

    // Dots (keep references for hover emphasis). When config.hideDots is set
    // (dense daily lines), dots rest invisible and only the hovered one shows.
    var dotEls = config.series.map(function (s, si) {
      return pts[si].map(function (pt) {
        var c = el("circle", {
          "class": "ghchart__dot",
          cx: pt.x, cy: pt.y,
          r: config.hideDots ? 0 : (si === 0 ? 4.5 : 4),
          fill: s.color
        });
        svg.appendChild(c);
        return c;
      });
    });

    // Transparent hit area for pointer capture.
    var hit = el("rect", {
      "class": "ghchart__hit",
      x: M.left,
      y: M.top - 6,
      width: innerW,
      height: baseY - M.top + 12
    });
    svg.appendChild(hit);

    root.appendChild(svg);

    /* ---- Tooltip ------------------------------------------------ */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    tip.appendChild(tipTitle);
    var tipRows = config.series.map(function (s) {
      var row = html("div", "ghchart__tt-row");
      var sw = html("span", "ghchart__tt-swatch");
      sw.style.background = s.color;
      var name = html("span", "ghchart__tt-name");
      name.textContent = s.name;
      var val = html("span", "ghchart__tt-value");
      row.appendChild(sw);
      row.appendChild(name);
      row.appendChild(val);
      tip.appendChild(row);
      return val;
    });
    root.appendChild(tip);

    /* ---- Hover / focus interactivity ---------------------------- */
    var activeIndex = -1;
    var baseR = config.series.map(function (s, si) {
      return config.hideDots ? 0 : (si === 0 ? 4.5 : 4);
    });
    // Radius of the single emphasized dot under the pointer.
    var dotActiveR = config.series.map(function (s, si) {
      return config.hideDots ? 6 : baseR[si] + 2.5;
    });
    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function indexFromClientX(clientX) {
      var rect = svg.getBoundingClientRect();
      var relX = (clientX - rect.left) / rect.width * VB.w;
      var i = stepX ? Math.round((relX - (M.left + PAD_X)) / stepX) : 0;
      return clamp(i, 0, n - 1);
    }

    function show(i) {
      if (i === activeIndex) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      guide.setAttribute("x1", xAt(i));
      guide.setAttribute("x2", xAt(i));
      guide.style.opacity = "1";

      // Emphasize the hovered column's dots; reset the rest.
      dotEls.forEach(function (col, si) {
        col.forEach(function (c, ci) {
          c.setAttribute("r", ci === i ? dotActiveR[si] : baseR[si]);
        });
      });

      // Fill tooltip.
      tipTitle.textContent = config.labels[i];
      config.series.forEach(function (s, si) {
        tipRows[si].textContent = config.formatValue(s.values[i]);
      });

      // Anchor above the highest point in this column.
      var topSi = 0, topY = Infinity;
      config.series.forEach(function (s, si) {
        var yy = yAt(s.values[i]);
        if (yy < topY) { topY = yy; topSi = si; }
      });
      var anchor = dotEls[topSi][i].getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      // On the first appearance, fade in exactly at the point: suppress the
      // position ease so it doesn't slide in from its previous resting spot.
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      // Clamp horizontally within the container.
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      // Flip below the point if there's no room above.
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      // Commit that position without animating, then restore the CSS
      // transition so subsequent point-to-point moves ease into place.
      if (suppress) {
        void tip.offsetWidth;
        tip.style.transition = "";
      }
    }

    function hide() {
      activeIndex = -1;
      guide.style.opacity = "0";
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (col, si) {
        col.forEach(function (c) { c.setAttribute("r", baseR[si]); });
      });
    }

    hit.addEventListener("pointermove", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerdown", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerleave", hide);

    // Keyboard access.
    svg.setAttribute("tabindex", "0");
    svg.addEventListener("keydown", function (e) {
      if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
        var start = activeIndex < 0 ? 0 : activeIndex;
        show(clamp(start + (e.key === "ArrowRight" ? 1 : -1), 0, n - 1));
        e.preventDefault();
      } else if (e.key === "Escape") {
        hide();
      }
    });
    svg.addEventListener("blur", hide);
  };

  /* ================================================================
     Bar chart renderer (composite)
     ----------------------------------------------------------------
     One "graph" can stack multiple bar panels in a single block:
       config = { type:"bar", title, subtitle?, series:[{name,color}],
                  barRadius?, panels:[ panel… ], caption? }
       panel  = { title?, labels[], values[], yAxis:{min,max,step},
                  formatValue(v), height?, maxBarVB?,
                  tooltip(i, panel) -> { title, lines:[…] } }
     No printed value labels — values surface on hover (consistent with
     the line chart). Bars use a single tunable color with rounded tops.
     ================================================================ */
  renderers.bar = function (root, config) {
    root.textContent = "";

    var barColor = (config.series && config.series[0] && config.series[0].color) || "#ff7b72";
    var barRadius = config.barRadius != null ? config.barRadius : 8;

    // Per-bar fill: a bar may point at a different series (e.g. a muted
    // "preliminary" color) via panel.barSeries[index]; otherwise series[0].
    function barColorFor(panel, j) {
      var si = (panel.barSeries && panel.barSeries[j] != null) ? panel.barSeries[j] : 0;
      var s = config.series && config.series[si];
      return (s && s.color) || barColor;
    }

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Title + divider (+ optional subtitle) ------------------ */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip for all panels ---------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var allBars = [];       // { el, pi, bi }
    var activeKey = null;   // "pi:bi" of the hovered bar

    function hideTip() {
      activeKey = null;
      tip.setAttribute("data-visible", "false");
      allBars.forEach(function (b) { b.el.style.opacity = "1"; });
    }

    // Hoisted: referenced by the per-column pointer handlers below.
    function showTip(pi, bi, panel, panelBars) {
      var reappearing = activeKey === null;
      activeKey = pi + ":" + bi;

      allBars.forEach(function (b) {
        b.el.style.opacity = (b.pi === pi && b.bi === bi) ? "1" : "0.42";
      });

      var content = panel.tooltip
        ? panel.tooltip(bi, panel)
        : { title: (panel.labels || [])[bi],
            lines: [ (panel.formatValue || String)(panel.values[bi]) ] };

      tipTitle.textContent = content.title != null ? content.title : "";
      tipBody.textContent = "";
      (content.lines || []).forEach(function (ln) {
        var row = html("div", "ghchart__tt-line");
        row.textContent = ln;
        tipBody.appendChild(row);
      });

      var anchor = panelBars[bi].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels ------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var wrap = html("div", "ghchart__panel");
      if (panel.title) {
        var pt = html("h3", "ghchart__panel-title");
        pt.textContent = panel.title;
        wrap.appendChild(pt);
      }

      var VB = { w: 1000, h: panel.height || 360 };
      var M = { top: 20, right: 20, bottom: 48, left: 84 };
      var innerW = VB.w - M.left - M.right;
      var innerH = VB.h - M.top - M.bottom;

      var labels = panel.labels || [];
      var values = panel.values || [];
      var nb = labels.length;
      var ax = panel.yAxis || { min: 0, max: Math.max.apply(null, values.concat([1])), step: 0 };
      var yMin = ax.min, yMax = ax.max, step = ax.step || 0;
      var fmt = panel.formatValue || function (v) { return String(v); };

      function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
      var baseY = yAt(yMin);

      var slot = nb ? innerW / nb : innerW;
      var maxBarVB = panel.maxBarVB != null ? panel.maxBarVB : 150;
      var barW = Math.min(slot * 0.62, maxBarVB);
      function xCenter(i) { return M.left + slot * (i + 0.5); }

      var svg = el("svg", {
        "class": "ghchart__plot ghchart__plot--bar",
        viewBox: "0 0 " + VB.w + " " + VB.h,
        preserveAspectRatio: "xMidYMid meet",
        role: "img",
        "aria-label": (panel.title || config.title) + " bar chart"
      });

      // Gridlines + y-axis labels.
      if (step > 0) {
        for (var v = yMin; v <= yMax + 1e-9; v += step) {
          var gy = yAt(v);
          svg.appendChild(el("line", {
            "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
          }));
          var yl = el("text", {
            "class": "ghchart__axislabel ghchart__axislabel--y",
            x: M.left - 18, y: gy, dy: "0.32em", "text-anchor": "end"
          });
          yl.textContent = fmt(v);
          svg.appendChild(yl);
        }
      }

      // Solid baseline.
      svg.appendChild(el("line", {
        "class": "ghchart__axisline", x1: M.left, y1: baseY, x2: VB.w - M.right, y2: baseY
      }));

      // x-axis labels.
      for (var i = 0; i < nb; i++) {
        var xl = el("text", {
          "class": "ghchart__axislabel ghchart__axislabel--x",
          x: xCenter(i), y: VB.h - M.bottom + 30, "text-anchor": "middle"
        });
        if (panel.xLabelSize) xl.setAttribute("style", "font-size:" + panel.xLabelSize + "px");
        xl.textContent = labels[i];
        svg.appendChild(xl);
      }

      // Bars (rounded top corners only) + full-height hover columns.
      var panelBars = [];
      for (var j = 0; j < nb; j++) {
        var cx = xCenter(j);
        var topY = yAt(values[j]);
        var barH = baseY - topY;
        var x = cx - barW / 2;
        var r = Math.max(0, Math.min(barRadius, barW / 2, barH));
        var d = "M " + x + " " + baseY +
                " L " + x + " " + (topY + r) +
                " Q " + x + " " + topY + " " + (x + r) + " " + topY +
                " L " + (x + barW - r) + " " + topY +
                " Q " + (x + barW) + " " + topY + " " + (x + barW) + " " + (topY + r) +
                " L " + (x + barW) + " " + baseY + " Z";
        var bar = el("path", { "class": "ghchart__bar", d: d, fill: barColorFor(panel, j) });
        svg.appendChild(bar);
        var rec = { el: bar, pi: pi, bi: j };
        panelBars.push(rec);
        allBars.push(rec);

        var col = el("rect", {
          "class": "ghchart__barhit",
          x: M.left + slot * j, y: M.top - 6, width: slot, height: baseY - M.top + 6
        });
        (function (idx) {
          col.addEventListener("pointermove", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerdown", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerleave", hideTip);
        })(j);
        svg.appendChild(col);
      }

      /* ---- Annotation layer (drawn on top; pointer-events: none) ----
         · always-on value labels above chosen bars (panel.valueLabels)
         · anchor lines: a dotted horizontal reference at one bar's top
           extended to another, with a floating pill (panel.annotations) */
      var vlabels = panel.valueLabels || null;
      if (vlabels) {
        for (var vk in vlabels) {
          if (!Object.prototype.hasOwnProperty.call(vlabels, vk)) continue;
          var vi = +vk;
          var vt = el("text", {
            "class": "ghchart__barvalue",
            x: xCenter(vi), y: yAt(values[vi]) - 12, "text-anchor": "middle"
          });
          vt.textContent = vlabels[vk];
          svg.appendChild(vt);
        }
      }

      (panel.annotations || []).forEach(function (an) {
        if (an.type !== "anchor") return;
        var yLine = yAt(values[an.fromIndex]);
        svg.appendChild(el("line", {
          "class": "ghchart__anno-line",
          x1: xCenter(an.fromIndex) - barW / 2, y1: yLine,
          x2: xCenter(an.toIndex) + barW / 2, y2: yLine
        }));

        var chipIdx = an.chipAtIndex != null ? an.chipAtIndex : an.toIndex;
        var cx = xCenter(chipIdx);
        var cy = (yAt(values[chipIdx]) + yLine) / 2;
        var label = an.label || "";
        var fs = 19;
        var chipW = label.length * fs * 0.55 + 24;
        var chipH = fs + 16;
        svg.appendChild(el("rect", {
          "class": "ghchart__anno-chip",
          x: cx - chipW / 2, y: cy - chipH / 2, width: chipW, height: chipH, rx: chipH / 2
        }));
        var ct = el("text", {
          "class": "ghchart__anno-chip-text",
          x: cx, y: cy, dy: "0.34em", "text-anchor": "middle", "font-size": fs
        });
        ct.textContent = label;
        svg.appendChild(ct);
      });

      wrap.appendChild(svg);
      root.appendChild(wrap);
    });

    /* ---- Caption (optional rich copy) --------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Calendar heatmap renderer (GitHub contribution-graph style)
     ----------------------------------------------------------------
     One square per calendar day across a month range. Each panel is
     shaded on its own scale — every day in a month shares that month's
     level (the underlying series is monthly). Each panel's 5-step
     faint->dark ramp is derived from a single tunable base color.
     ================================================================ */
  renderers.heatmap = function (root, config) {
    root.textContent = "";

    var WHITE = [255, 255, 255], BLACK = [0, 0, 0];
    function hexToRgb(h) {
      h = String(h).replace("#", "");
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
    }
    function rgbToHex(c) {
      return "#" + c.map(function (x) {
        x = Math.max(0, Math.min(255, Math.round(x)));
        return (x < 16 ? "0" : "") + x.toString(16);
      }).join("");
    }
    function mix(a, b, t) {
      return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
    }
    // 5-step contribution ramp (level 0 faint -> level 4 dark) from one hex.
    function rampFrom(baseHex) {
      var b = hexToRgb(baseHex);
      return [
        rgbToHex(mix(b, WHITE, 0.80)),
        rgbToHex(mix(b, WHITE, 0.52)),
        rgbToHex(mix(b, WHITE, 0.20)),
        baseHex,
        rgbToHex(mix(b, BLACK, 0.30))
      ];
    }

    function trim0(s) { return s.replace(/\.0$/, ""); }
    function fmtM(v) {
      if (v >= 1000) { var g = v / 1000; return (g >= 10 ? Math.round(g) : trim0(g.toFixed(1))) + "B"; }
      if (v >= 1) return trim0(v.toFixed(1)) + "M";
      return Math.round(v * 1000) + "K";
    }

    var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    var MON_FULL = ["January", "February", "March", "April", "May", "June",
      "July", "August", "September", "October", "November", "December"];
    var WD = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

    var year = config.year || 2026;
    var startM = config.startMonth != null ? config.startMonth : 0;
    var endM = config.endMonth != null ? config.endMonth : 5;
    var emptyColor = config.emptyColor || "#ebedf0";

    function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
    function parseISO(s) { var p = s.split("-"); return new Date(+p[0], +p[1] - 1, +p[2]); }
    function dayStart(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); }

    /* ---- Eyebrow + title + subtitle ----------------------------- */
    if (config.eyebrow) {
      var eb = html("div", "ghheat__eyebrow");
      eb.appendChild(html("span", "ghheat__eyedot"));
      var ebt = html("span", "ghheat__eyetext");
      ebt.textContent = config.eyebrow;
      eb.appendChild(ebt);
      root.appendChild(eb);
    }
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle ghheat__subtitle");
      sub.innerHTML = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip -------------------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }
    var activeCell = null;
    function hideTip() { activeCell = null; tip.setAttribute("data-visible", "false"); }
    function showTip(rectEl, titleText, lines) {
      var reappearing = activeCell === null;
      activeCell = rectEl;
      tipTitle.textContent = titleText;
      tipBody.textContent = "";
      lines.forEach(function (ln) {
        var r = html("div", "ghchart__tt-line");
        r.innerHTML = ln;
        tipBody.appendChild(r);
      });
      var a = rectEl.getBoundingClientRect(), rr = root.getBoundingClientRect();
      var left = a.left + a.width / 2 - rr.left, top = a.top - rr.top - 12;
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";
      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = a.bottom - rr.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels -------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var base = (config.series && config.series[pi] && config.series[pi].color) || panel.base || "#3094ff";
      var ramp = rampFrom(base);
      var strong = ramp[4];

      var totals = panel.months || [];
      var activeVals = [];
      for (var mi = startM; mi <= endM; mi++) { if (totals[mi] != null) activeVals.push(totals[mi]); }
      var vmin = activeVals.length ? Math.min.apply(null, activeVals) : 0;
      var vmax = activeVals.length ? Math.max.apply(null, activeVals) : 1;
      function levelFor(t) {
        if (t == null) return -1;
        if (vmax === vmin) return 4;
        return Math.round((t - vmin) / (vmax - vmin) * 4);
      }

      var af = panel.activeFrom ? parseISO(panel.activeFrom) : new Date(year, startM, 1);
      var at = panel.activeTo ? parseISO(panel.activeTo) : new Date(year, endM, daysInMonth(year, endM));

      var wrap = html("div", "ghheat__panel");

      var head = html("div", "ghheat__phead");
      var badge = html("span", "ghheat__badge");
      badge.textContent = panel.badge != null ? panel.badge : (pi + 1);
      badge.style.background = strong;
      head.appendChild(badge);
      var pt = html("span", "ghheat__ptitle");
      pt.textContent = panel.title;
      head.appendChild(pt);
      if (panel.descriptor) {
        var pd = html("span", "ghheat__pdesc");
        pd.textContent = panel.descriptor;
        head.appendChild(pd);
      }
      wrap.appendChild(head);

      var body = html("div", "ghheat__body");

      /* ---- Calendar grid (SVG) ---- */
      var gridStart = new Date(year, startM, 1);
      var gs = new Date(gridStart);
      gs.setDate(gs.getDate() - gs.getDay());            // back to Sunday
      var gridEnd = new Date(year, endM, daysInMonth(year, endM));
      var ge = new Date(gridEnd);
      ge.setDate(ge.getDate() + (6 - ge.getDay()));      // forward to Saturday
      var nCols = Math.round((ge - gs) / (7 * 86400000)) + 1;

      var cell = 13, sq = 11, rad = 2.5, labL = 34, labT = 18;
      var W = labL + nCols * cell, H = labT + 7 * cell;
      var svg = el("svg", {
        "class": "ghheat__grid",
        viewBox: "0 0 " + W + " " + H,
        preserveAspectRatio: "xMinYMin meet",
        role: "img",
        "aria-label": panel.title + " calendar heatmap"
      });

      [1, 3, 5].forEach(function (r) {
        var t = el("text", {
          "class": "ghheat__wd", x: labL - 6, y: labT + r * cell + sq / 2,
          dy: "0.32em", "text-anchor": "end"
        });
        t.textContent = WD[r];
        svg.appendChild(t);
      });

      // Month labels above the column where each month begins.
      for (var lm = startM; lm <= endM; lm++) {
        var first = new Date(year, lm, 1);
        var colOf = Math.floor((dayStart(first) - gs) / (7 * 86400000));
        var mlab = el("text", { "class": "ghheat__month", x: labL + colOf * cell, y: 12, "text-anchor": "start" });
        mlab.textContent = MON[lm];
        svg.appendChild(mlab);
      }

      var afS = dayStart(af), atS = dayStart(at);
      var d = new Date(gs);
      for (var i = 0; i < nCols * 7; i++) {
        var row = d.getDay();
        var col = Math.floor(i / 7);
        var x = labL + col * cell, y = labT + row * cell;
        var m = d.getMonth();
        var inRange = d.getFullYear() === year && m >= startM && m <= endM;
        var ds = dayStart(d);
        var inWindow = inRange && ds >= afS && ds <= atS;
        var hasData = inWindow && totals[m] != null;
        var lvl = hasData ? levelFor(totals[m]) : -1;
        var cellEl = el("rect", {
          "class": "ghheat__cell", x: x, y: y, width: sq, height: sq, rx: rad,
          fill: lvl >= 0 ? ramp[lvl] : emptyColor
        });
        if (inRange) {
          (function (dt, mm, has) {
            var dateStr = WD[dt.getDay()] + " \u00b7 " + MON[mm] + " " + dt.getDate() + ", " + year;
            var lines;
            if (has) {
              var daily = totals[mm] / daysInMonth(year, mm);
              lines = [
                "<b>" + fmtM(daily) + "</b> " + panel.legendLabel,
                MON_FULL[mm] + " \u00b7 " + fmtM(totals[mm]) + " " + panel.unitTotal
              ];
            } else {
              lines = [panel.emptyNote || "No vetted data"];
            }
            cellEl.addEventListener("pointermove", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerdown", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerleave", hideTip);
          })(new Date(d), m, hasData);
        } else {
          cellEl.setAttribute("class", "ghheat__cell ghheat__cell--pad");
        }
        svg.appendChild(cellEl);
        d.setDate(d.getDate() + 1);
      }

      var gridwrap = html("div", "ghheat__gridwrap");
      gridwrap.appendChild(svg);
      body.appendChild(gridwrap);

      /* ---- Side column: stat + note + legend + extra ---- */
      var side = html("div", "ghheat__side");
      if (panel.stat) {
        var stat = html("div", "ghheat__stat");
        var big = html("span", "ghheat__statbig");
        big.textContent = panel.stat.big;
        big.style.color = strong;
        stat.appendChild(big);
        if (panel.stat.small) {
          var small = html("span", "ghheat__statsmall");
          small.textContent = panel.stat.small;
          stat.appendChild(small);
        }
        side.appendChild(stat);
      }
      if (panel.note) {
        var note = html("p", "ghheat__note");
        note.innerHTML = panel.note;
        side.appendChild(note);
      }
      var leg = html("div", "ghheat__legend");
      var lw1 = html("span", "ghheat__legword"); lw1.textContent = "Fewer"; leg.appendChild(lw1);
      ramp.forEach(function (c) {
        var s = html("span", "ghheat__sw"); s.style.background = c; leg.appendChild(s);
      });
      var lw2 = html("span", "ghheat__legword"); lw2.textContent = "More"; leg.appendChild(lw2);
      if (panel.legendLabel) {
        var lu = html("span", "ghheat__legunit"); lu.textContent = "\u00b7 " + panel.legendLabel; leg.appendChild(lu);
      }
      side.appendChild(leg);
      if (panel.extraNote) {
        var ex = html("div", "ghheat__extra");
        var exsw = html("span", "ghheat__sw ghheat__sw--empty");
        exsw.style.background = emptyColor;
        ex.appendChild(exsw);
        var ext = html("span", "ghheat__extratext");
        ext.innerHTML = panel.extraNote;
        ex.appendChild(ext);
        side.appendChild(ex);
      }
      body.appendChild(side);

      wrap.appendChild(body);
      root.appendChild(wrap);
    });

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Scatter renderer — X/Y numeric plot with categorical points.
     Used for cost-vs-quality benchmark charts: a highlighted "primary"
     point (glow halo), a "reference" point with a dotted crosshair, and
     muted "other" points. Values never print on the marks — a point's
     name shows for identity and the exact (y, x) reveals on hover.
     ================================================================ */
  renderers.scatter = function (root, config) {
    root.textContent = "";
    var uid = config.mountId;

    var VB = { w: 1000, h: 600 };
    var M = { top: 40, right: 30, bottom: 74, left: 96 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var fmtX = xa.format || function (v) { return String(v); };
    var fmtY = ya.format || function (v) { return String(v); };
    var tipSuffix = config.tipSuffix || "";

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) { return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min)); }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function colorOf(pt) {
      var s = config.series && config.series[pt.series || 0];
      return (s && s.color) || "#8c959f";
    }

    /* ---- Title (compact narrative headline, no rule) ------------ */
    var title = html("div", "ghchart__headline");
    title.textContent = config.title;
    root.appendChild(title);

    /* ---- Header + legend (from series) -------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      (config.series || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch ghchart__swatch--dot");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--scatter",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " scatter plot"
    });

    // Gridlines (both directions) + tick labels.
    var eps = 1e-6;
    for (var yv = ya.min; yv <= ya.max + eps; yv += ya.step) {
      var gy = yAt(yv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(yv);
      svg.appendChild(yl);
    }
    for (var xv = xa.min; xv <= xa.max + eps; xv += xa.step) {
      var gx = xAt(xv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline ghchart__gridline--v",
        x1: gx, y1: M.top, x2: gx, y2: baseY
      }));
      var isFirst = Math.abs(xv - xa.min) < eps;
      var isLast = Math.abs(xv - xa.max) < eps;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 34,
        "text-anchor": isFirst ? "start" : (isLast ? "end" : "middle")
      });
      xl.textContent = fmtX(xv);
      svg.appendChild(xl);
    }

    // Axis titles.
    if (xa.title) {
      var xt = el("text", {
        "class": "ghchart__axistitle",
        x: M.left + innerW / 2, y: VB.h - 12, "text-anchor": "middle"
      });
      xt.textContent = xa.title;
      svg.appendChild(xt);
    }
    if (ya.title) {
      var ytX = 24, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle",
        x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Reference crosshair through the flagged point (dual dotted lines).
    var refPt = (config.points || []).filter(function (p) { return p.reference; })[0];
    if (refPt) {
      var rColor = colorOf(refPt);
      var rx = xAt(refPt.x), ry = yAt(refPt.y);
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: rx, y1: M.top, x2: rx, y2: baseY, stroke: rColor
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: M.left, y1: ry, x2: VB.w - M.right, y2: ry, stroke: rColor
      }));
    }

    // Points: halo (highlight/reference) + dot + name label.
    var dotEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y), c = colorOf(pt);
      var big = pt.highlight || pt.reference;

      if (big) {
        svg.appendChild(el("circle", {
          "class": "ghchart__halo", cx: cx, cy: cy, r: 17, fill: c
        }));
      }
      var dot = el("circle", {
        "class": "ghchart__pt", cx: cx, cy: cy,
        r: big ? 9 : 5.5, fill: c
      });
      svg.appendChild(dot);
      dotEls.push({ el: dot, baseR: big ? 9 : 5.5 });

      // Name label — bold+colored for the story points, muted for the rest.
      var pos = pt.labelPos || (pt.highlight ? "below" : pt.reference ? "above" : "right");
      var dx = 0, dy = 0, anchor = "middle";
      var gap = big ? 15 : 9;
      if (pos === "right") { dx = gap + 4; dy = 5; anchor = "start"; }
      else if (pos === "left") { dx = -(gap + 4); dy = 5; anchor = "end"; }
      else if (pos === "above") { dy = -(gap + 6); anchor = "middle"; }
      else { dy = gap + 16; anchor = "middle"; }  // below
      var lbl = el("text", {
        "class": "ghchart__pt-label" + (big ? " ghchart__pt-label--strong" : ""),
        x: cx + dx, y: cy + dy, "text-anchor": anchor,
        fill: big ? c : null
      });
      lbl.textContent = pt.label;
      svg.appendChild(lbl);

      // Transparent hit target for hover/focus.
      var hit = el("circle", {
        "class": "ghchart__pthit", cx: cx, cy: cy, r: 20,
        tabindex: "0", role: "img",
        "aria-label": pt.label + ": " + fmtY(pt.y) + ", " + fmtX(pt.x) + tipSuffix
      });
      svg.appendChild(hit);
      hitEls.push(hit);
    });

    root.appendChild(svg);

    /* ---- Tooltip (title + free lines) --------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      dotEls.forEach(function (d, di) {
        d.el.setAttribute("r", di === i ? d.baseR + 2.5 : d.baseR);
      });

      tipTitle.textContent = pt.label;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = fmtY(pt.y) + " · " + fmtX(pt.x) + tipSuffix;
      tipBody.appendChild(line);

      var anchor = dotEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (d) { d.el.setAttribute("r", d.baseR); });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
  };

  /* ================================================================
     Trend renderer — dated time-series with a narrative headline.
     One renderer covers both HydraFusion time charts via config:
       • linear OR log y-axis (yAxis.scale), explicit yAxis.ticks;
       • phase background columns (with optional per-phase average);
       • filled squares (values reveal on hover — never printed);
       • red-X "invalid run" markers with leader-line annotations;
       • a dashed trend line + optional ±band + slope label;
       • an optional horizontal reference line, bracket, and final callout.
     ================================================================ */
  renderers.trend = function (root, config) {
    root.textContent = "";

    var VB = { w: 1000, h: 560 };
    var M = { top: 64, right: 56, bottom: 56, left: 104 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var isLog = ya.scale === "log";
    var fmtY = ya.format || function (v) { return String(v); };
    var series = config.series || [];
    function sColor(i, fb) {
      var s = series[i];
      return (s && s.color) || fb;
    }
    var cPoint = sColor(0, "#8250df");
    var cInvalid = sColor(1, "#cf222e");
    var cTrend = (config.trend && config.trend.color) || sColor(0, "#8250df");

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) {
      if (isLog) {
        var lo = Math.log(ya.min) / Math.LN10, hi = Math.log(ya.max) / Math.LN10;
        var t = (Math.log(v) / Math.LN10 - lo) / (hi - lo);
        return M.top + innerH * (1 - t);
      }
      return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min));
    }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Headline + subtitle + legend (HTML flow above the plot) --- */
    var headline = html("div", "ghchart__headline");
    headline.textContent = config.title;
    root.appendChild(headline);
    if (config.subtitle) {
      var sub = html("div", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }
    if (!config.hideLegend && series.length) {
      var legend = html("div", "ghchart__legend ghchart__legend--trend");
      /* Build one legend glyph. The "invalid" glyph mirrors the on-plot marker
         exactly — a red diamond outline with an X through it (same side=13 /
         q=3.4 proportions used to draw the mark below) — so the key matches
         the mark on the chart. */
      function trendGlyph(glyph, color) {
        if (glyph === "invalid") {
          var g = el("svg", {
            "class": "ghchart__glyph ghchart__glyph--invalid",
            viewBox: "0 0 22 22", width: "15", height: "15",
            style: "overflow:visible"
          });
          g.appendChild(el("rect", {
            x: 4.5, y: 4.5, width: 13, height: 13, rx: 1.5,
            transform: "rotate(45 11 11)", fill: "none",
            stroke: color, "stroke-width": 2
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 7.6, x2: 14.4, y2: 14.4,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 14.4, x2: 14.4, y2: 7.6,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          return g;
        }
        var gl = html("span", "ghchart__glyph ghchart__glyph--" + glyph);
        if (glyph === "line") { gl.style.borderTopColor = color; }
        else { gl.style.background = color; gl.style.borderColor = color; }
        if (glyph === "diamond") { gl.style.background = "transparent"; }
        return gl;
      }
      series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "square", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      (config.legendExtra || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "line", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      root.appendChild(legend);
    }

    /* ---- SVG plot ------------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--trend",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title
    });

    // Phase background columns + separators + labels (+ optional average).
    (config.phases || []).forEach(function (ph, i) {
      var x0 = xAt(ph.from), x1 = xAt(ph.to);
      svg.appendChild(el("rect", {
        "class": "ghchart__phase" + (i % 2 ? " ghchart__phase--alt" : ""),
        x: x0, y: M.top, width: Math.max(0, x1 - x0), height: innerH
      }));
      if (i > 0) {
        svg.appendChild(el("line", {
          "class": "ghchart__phase-sep", x1: x0, y1: M.top, x2: x0, y2: baseY
        }));
      }
      var pl = el("text", {
        "class": "ghchart__phase-label", x: (x0 + x1) / 2, y: M.top - 40,
        "text-anchor": "middle"
      });
      pl.textContent = ph.label;
      svg.appendChild(pl);
      if (ph.avg) {
        var pa = el("text", {
          "class": "ghchart__phase-avg", x: (x0 + x1) / 2, y: M.top + 24,
          "text-anchor": "middle", fill: cPoint
        });
        pa.textContent = ph.avg;
        svg.appendChild(pa);
      }
    });

    // Horizontal gridlines + y tick labels (skip a gridline at the reference y).
    var refY = config.reference ? config.reference.y : null;
    (ya.ticks || []).forEach(function (tv) {
      var gy = yAt(tv);
      if (refY === null || Math.abs(tv - refY) > 1e-9) {
        svg.appendChild(el("line", {
          "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
        }));
      }
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(tv);
      svg.appendChild(yl);
    });

    // X tick labels (dated).
    (xa.ticks || []).forEach(function (t) {
      var gx = xAt(t.at);
      var first = Math.abs(t.at - xa.min) < 1e-9, last = Math.abs(t.at - xa.max) < 1e-9;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 30, "text-anchor": first ? "start" : (last ? "end" : "middle")
      });
      xl.textContent = t.text;
      svg.appendChild(xl);
    });

    // Axis titles.
    if (ya.title) {
      var ytX = 22, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle", x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Trend: optional ±band (linear only), then the dashed line + slope label.
    var tr = config.trend;
    if (tr) {
      function trendY(x) {
        return tr.from.y + (tr.to.y - tr.from.y) * (x - tr.from.x) / (tr.to.x - tr.from.x);
      }
      if (tr.band && !isLog) {
        var p = tr.band;
        var pts = [
          [xAt(tr.from.x), yAt(trendY(tr.from.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) - p)],
          [xAt(tr.from.x), yAt(trendY(tr.from.x) - p)]
        ].map(function (a) { return a[0] + "," + a[1]; }).join(" ");
        svg.appendChild(el("polygon", {
          "class": "ghchart__trendband", points: pts, fill: cTrend
        }));
      }
      svg.appendChild(el("line", {
        "class": "ghchart__trendline",
        x1: xAt(tr.from.x), y1: yAt(tr.from.y),
        x2: xAt(tr.to.x), y2: yAt(tr.to.y), stroke: cTrend
      }));
      if (tr.label) {
        var lx, lyy;
        if (tr.labelAt) { lx = xAt(tr.labelAt.x); lyy = yAt(tr.labelAt.y); }
        else {
          lx = xAt(tr.to.x) + (tr.labelDX == null ? -6 : tr.labelDX);
          lyy = yAt(tr.to.y) + (tr.labelDY == null ? -12 : tr.labelDY);
        }
        var sl = el("text", {
          "class": "ghchart__slope", x: lx, y: lyy,
          "text-anchor": tr.labelAnchor || "end", fill: cTrend
        });
        sl.textContent = tr.label;
        svg.appendChild(sl);
      }
    }

    // Reference line (horizontal dashed) + right-aligned label.
    if (config.reference) {
      var rc = config.reference.color || "#24292f";
      var ryy = yAt(config.reference.y);
      svg.appendChild(el("line", {
        "class": "ghchart__refline", x1: M.left, y1: ryy, x2: VB.w - M.right, y2: ryy,
        stroke: rc
      }));
      if (config.reference.label) {
        var rl = el("text", {
          "class": "ghchart__reflabel", x: VB.w - M.right, y: ryy - 8,
          "text-anchor": "end", fill: rc
        });
        rl.textContent = config.reference.label;
        svg.appendChild(rl);
      }
    }

    // Data squares (values reveal on hover only).
    var side = 13, hSide = side / 2;
    var sqEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y);
      var sq = el("rect", {
        "class": "ghchart__square", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 2, fill: cPoint
      });
      svg.appendChild(sq);
      sqEls.push({ el: sq, cx: cx, cy: cy });

      // Hover/focus hit target — omitted when hover is disabled so the
      // (illustrative) point values never surface, on screen or to a11y.
      if (!config.disableHover) {
        var hit = el("rect", {
          "class": "ghchart__squarehit", x: cx - 13, y: cy - 13,
          width: 26, height: 26, tabindex: "0", role: "img",
          "aria-label": (pt.d || "") + " " + fmtY(pt.y)
        });
        svg.appendChild(hit);
        hitEls.push(hit);
      }
    });

    // Invalid-run markers: red diamond + X, leader line, annotation text.
    (config.invalids || []).forEach(function (iv) {
      var cx = xAt(iv.x), cy = yAt(iv.y);
      svg.appendChild(el("rect", {
        "class": "ghchart__invalid", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 1.5, fill: "none", stroke: cInvalid,
        transform: "rotate(45 " + cx + " " + cy + ")"
      }));
      var q = 3.4;
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy - q, x2: cx + q, y2: cy + q,
        stroke: cInvalid
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy + q, x2: cx + q, y2: cy - q,
        stroke: cInvalid
      }));
      if (iv.title) {
        var tx = cx + (iv.labelDX == null ? 16 : iv.labelDX);
        var ty = cy + (iv.labelDY == null ? -26 : iv.labelDY);
        svg.appendChild(el("line", {
          "class": "ghchart__leader", x1: cx, y1: cy, x2: tx, y2: ty, stroke: cInvalid
        }));
        var anchor = iv.labelAnchor || "start";
        var at = el("text", {
          "class": "ghchart__anno-title", x: tx, y: ty - 4, "text-anchor": anchor, fill: cInvalid
        });
        at.textContent = iv.title;
        svg.appendChild(at);
        if (iv.sub) {
          var asu = el("text", {
            "class": "ghchart__anno-sub", x: tx, y: ty + 11, "text-anchor": anchor
          });
          asu.textContent = iv.sub;
          svg.appendChild(asu);
        }
      }
    });

    // Bracket annotation (span with end ticks + centered label above).
    (config.brackets || []).forEach(function (b) {
      var x0 = xAt(b.fromX), x1 = xAt(b.toX), by = yAt(b.y);
      svg.appendChild(el("line", {
        "class": "ghchart__bracket", x1: x0, y1: by, x2: x1, y2: by
      }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x0, y1: by, x2: x0, y2: by + 7 }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x1, y1: by, x2: x1, y2: by + 7 }));
      var bl = el("text", {
        "class": "ghchart__bracket-label", x: (x0 + x1) / 2, y: by - 8, "text-anchor": "middle"
      });
      bl.textContent = b.text;
      svg.appendChild(bl);
    });

    // Final callout: ringed point + leader + title/sub placed inside the plot.
    if (config.callout) {
      var co = config.callout;
      var ccx = xAt(co.x), ccy = yAt(co.y);
      svg.appendChild(el("circle", {
        "class": "ghchart__callout-ring", cx: ccx, cy: ccy, r: 12, stroke: cPoint
      }));
      var ctx = ccx + (co.labelDX == null ? -16 : co.labelDX);
      var cty = ccy + (co.labelDY == null ? -30 : co.labelDY);
      svg.appendChild(el("line", {
        "class": "ghchart__leader ghchart__leader--callout", x1: ccx, y1: ccy, x2: ctx, y2: cty,
        stroke: cPoint
      }));
      var can = co.labelAnchor || "end";
      var ct = el("text", {
        "class": "ghchart__anno-title ghchart__anno-title--callout", x: ctx, y: cty - 4, "text-anchor": can, fill: cPoint
      });
      ct.textContent = co.title;
      svg.appendChild(ct);
      if (co.sub) {
        var cs = el("text", {
          "class": "ghchart__anno-sub ghchart__anno-sub--callout", x: ctx, y: cty + 12, "text-anchor": can
        });
        cs.textContent = co.sub;
        svg.appendChild(cs);
      }
    }

    root.appendChild(svg);

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.textContent = config.caption;
      root.appendChild(cap);
    }

    /* ---- Tooltip + hover interaction (skipped when disabled) ------ */
    if (!config.disableHover) {
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      sqEls.forEach(function (s, si) {
        var big = si === i;
        var w = big ? side + 5 : side;
        s.el.setAttribute("width", w);
        s.el.setAttribute("height", w);
        s.el.setAttribute("x", s.cx - w / 2);
        s.el.setAttribute("y", s.cy - w / 2);
      });

      var tipInfo = config.pointTip
        ? config.pointTip(pt)
        : { title: pt.d || "", line: fmtY(pt.y) };
      tipTitle.textContent = tipInfo.title;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = tipInfo.line;
      tipBody.appendChild(line);

      var anchor = sqEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      sqEls.forEach(function (s) {
        s.el.setAttribute("width", side);
        s.el.setAttribute("height", side);
        s.el.setAttribute("x", s.cx - hSide);
        s.el.setAttribute("y", s.cy - hSide);
      });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
    }
  };

  /* ================================================================
     Distribution dashboard renderer
     ----------------------------------------------------------------
     A composition figure (no hover — every value is printed as text, so
     the marks are inherently screen-reader accessible):
       • a compact narrative headline + muted subtitle;
       • an optional segmented "mix" bar — proportional colored segments,
         each with its name inside and count/percent below;
       • one or more panels of ranked horizontal bars, each row printing
         its category (left), a bar, and its count/percent (right).
     Colors come from config.series so the harness can tune them live:
       series[0]   = bar fill (single base hex);
       series[1..] = segment fills, in the order of config.segments.items.
     ================================================================ */
  renderers.distribution = function (root, config) {
    root.textContent = "";
    root.setAttribute("role", "img");
    if (config.title) root.setAttribute("aria-label", config.title);

    var series = config.series || [];
    var barColor = (series[0] && series[0].color) || "#8250df";
    var total = config.total ||
      (config.segments && config.segments.items
        ? config.segments.items.reduce(function (a, b) { return a + b.value; }, 0)
        : 0);

    function pctText(v) {
      if (!total) return "";
      return (Math.round((v / total) * 1000) / 10).toFixed(1) + "%";
    }
    function valueHTML(v) {
      var p = pctText(v);
      return "<b>" + v + "</b>" + (p ? " (" + p + ")" : "");
    }

    /* ---- Headline + subtitle ------------------------------------- */
    if (config.title) {
      var head = html("div", "ghchart__headline");
      head.textContent = config.title;
      root.appendChild(head);
    }
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- Segmented "mix" bar -------------------------------------- */
    var seg = config.segments;
    if (seg && seg.items && seg.items.length) {
      var segSec = html("div", "ghchart__dist-section");
      if (seg.title) {
        var segTitle = html("div", "ghchart__panel-title");
        segTitle.textContent = seg.title;
        segSec.appendChild(segTitle);
      }
      var segTotal = seg.items.reduce(function (a, b) { return a + b.value; }, 0) || 1;
      var bar = html("div", "ghchart__seg");
      var notes = html("div", "ghchart__seg-note-row");
      seg.items.forEach(function (it, i) {
        var basis = (it.value / segTotal) * 100;
        var col = (series[i + 1] && series[i + 1].color) || it.color || barColor;

        var cell = html("div", "ghchart__seg-cell");
        cell.style.flex = "0 0 " + basis + "%";
        cell.style.background = col;
        cell.textContent = it.label;
        bar.appendChild(cell);

        var note = html("div", "ghchart__seg-note");
        note.style.flex = "0 0 " + basis + "%";
        note.innerHTML = valueHTML(it.value);
        notes.appendChild(note);
      });
      segSec.appendChild(bar);
      segSec.appendChild(notes);
      root.appendChild(segSec);
    }

    /* ---- Ranked bar panels (grid: 2-up, stacking on narrow) ------ */
    var panels = config.panels || [];
    if (panels.length) {
      var cols = html("div", "ghchart__dist-cols");
      if (config.columns === 1 || panels.length === 1) {
        cols.classList.add("ghchart__dist-cols--stack");
      }
      panels.forEach(function (panel) {
        var sec = html("div", "ghchart__dist-section");
        if (panel.title) {
          var pt = html("div", "ghchart__panel-title");
          pt.textContent = panel.title;
          sec.appendChild(pt);
        }
        var list = html("div", "ghchart__dist-list");
        var pmax = (panel.items || []).reduce(function (m, it) {
          return it.value > m ? it.value : m;
        }, 0) || 1;
        (panel.items || []).forEach(function (it) {
          var row = html("div", "ghchart__dist-row");

          var cat = html("div", "ghchart__dist-cat");
          cat.textContent = it.label;
          row.appendChild(cat);

          var track = html("div", "ghchart__dist-track");
          var b = html("div", "ghchart__dist-bar");
          b.style.width = Math.max((it.value / pmax) * 100, 1.2) + "%";
          b.style.background = it.color || barColor;
          track.appendChild(b);
          row.appendChild(track);

          var val = html("div", "ghchart__dist-val");
          val.innerHTML = valueHTML(it.value);
          row.appendChild(val);

          list.appendChild(row);
        });
        sec.appendChild(list);
        cols.appendChild(sec);
      });
      root.appendChild(cols);
    }

    /* ---- Caption / footnote -------------------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Dispatcher — pick a renderer by config.type and draw into a mount
     ================================================================ */
  function render(config, mountEl) {
    var root = mountEl || document.getElementById(config.mountId);
    if (!root) return;
    root.classList.add("ghchart");
    /* Resolve the renderer from the SHARED registry (see registration below),
       not this bundle's private `renderers`. When several exported charts share
       one page they all merge into one registry, so every chart finds its
       renderer even if an older engine bundle loaded first. */
    var reg = (global.GHChartEngine && global.GHChartEngine.renderers) || renderers;
    var fn = reg[config.type] || renderers[config.type] || reg.line || renderers.line;
    fn(root, config);
  }

  /* ----------------------------------------------------------------
     Shared-engine registration (multi-embed safe)
     ----------------------------------------------------------------
     Every exported chart.js embeds this whole engine. When two or more
     charts are placed on the same page (e.g. a WordPress post), we must NOT
     let the first-loaded bundle silently win — an older bundle would be
     missing renderers added later (this is why a heatmap pasted after an
     older bar chart used to vanish).

     Instead we keep ONE shared engine:
       • every bundle merges in any renderers the shared registry lacks;
       • the newest bundle (highest ENGINE_VERSION) provides render(), which
         reads renderers from the shared registry at call time.
     The result is order-independent and resilient to version skew as new
     chart types are added. Bump ENGINE_VERSION whenever renderers or render
     change so a newer embed upgrades an older shared engine in place. */
  var ENGINE_VERSION = 9;
  var existing = global.GHChartEngine;
  if (existing && typeof existing.version === "number") {
    if (ENGINE_VERSION > existing.version) {
      /* This bundle is newer. Every export embeds the COMPLETE engine, so this
         bundle's `renderers` is a self-contained, up-to-date set. Adopt it as
         the shared registry (upgrading older same-named renderers in place),
         while defensively preserving any renderer types only the older engine
         happened to have. Then take over render(). */
      for (var ek in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ek) && !renderers[ek]) {
          renderers[ek] = existing.renderers[ek];
        }
      }
      existing.renderers = renderers;
      existing.render = render;
      existing.version = ENGINE_VERSION;
    } else {
      /* Same or older version already present: only contribute renderer types
         the shared registry is missing (don't clobber an equal/newer engine). */
      for (var rk in renderers) {
        if (Object.prototype.hasOwnProperty.call(renderers, rk) && !existing.renderers[rk]) {
          existing.renderers[rk] = renderers[rk];
        }
      }
    }
  } else {
    /* No engine yet, or a pre-versioning (old-style) engine loaded first.
       Take over, preserving any renderers the old-style engine registered. */
    if (existing && existing.renderers) {
      for (var ok in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ok) && !renderers[ok]) {
          renderers[ok] = existing.renderers[ok];
        }
      }
    }
    global.GHChartEngine = { renderers: renderers, render: render, version: ENGINE_VERSION };
  }
})(typeof window !== "undefined" ? window : this);

;(function () {
  "use strict";
  var CONFIG = {
    mountId: "gh-chart-hf-checkpoint",
    type: "scatter",
    title: "HydraFusion - Checkpoint Bench - GitHub Copilot CLI",
    xAxis: {
      min: 0,
      max: 25,
      step: 5,
      title: "Mean cost per task (USD)",
      format: function (v) { return "$" + v.toFixed(2); }
    },
    yAxis: {
      min: 74,
      max: 79,
      step: 1,
      title: "Mean session score",
      format: function (v) { return v + "%"; }
    },
    tipSuffix: "/task",
    series: [
      {
        name: "HydraFusion",
        color: "#2da44f"
      },
      {
        name: "Opus 5 (reference)",
        color: "#8250df"
      },
      {
        name: "Other solo models",
        color: "#8c959f"
      }
    ],
    points: [
      {
        label: "HydraFusion",
        x: 8.5,
        y: 77.5,
        series: 0,
        highlight: true,
        labelPos: "below"
      },
      {
        label: "Opus 5",
        x: 24.1,
        y: 77.6,
        series: 1,
        reference: true,
        labelPos: "above"
      },
      {
        label: "GPT-5.6-Sol",
        x: 7.5,
        y: 76.1,
        series: 2,
        labelPos: "right"
      },
      {
        label: "Sonnet 5",
        x: 5.3,
        y: 75.85,
        series: 2,
        labelPos: "right"
      },
      {
        label: "GPT-5.6-Terra",
        x: 2,
        y: 75.3,
        series: 2,
        labelPos: "right"
      },
      {
        label: "GPT-5.6-Luna",
        x: 0.3,
        y: 74.35,
        series: 2,
        labelPos: "right"
      }
    ]
  };
  function boot() {
    var root = document.getElementById(CONFIG.mountId);
    if (!root || root.getAttribute("data-gh-rendered")) return;
    root.setAttribute("data-gh-rendered", "1");
    window.GHChartEngine.render(CONFIG);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();
</script>

<div id="gh-chart-hf-checkpoint" class="ghchart"></div>



<p class="wp-block-paragraph">Early internal testing has echoed that result.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">So far, the reasoning and task solving capability [of HydraFusion] is at or better than Opus.</p>
<cite>Principal Software Engineer at Microsoft</cite></blockquote>



<h2 id="h-hill-climbing-hydrafusion" class="wp-block-heading">Hill-climbing HydraFusion</h2>



<p class="wp-block-paragraph">HydraFusion&rsquo;s routing policies were shaped by how developers use GitHub Copilot on real coding tasks. To make those workflows reproducible, we curated CheckpointBench from real Copilot coding-session trajectories. We refined HydraFusion repeatedly across CheckpointBench, DeepSWE, and TerminalBench 2.1, optimizing across the evaluation sets rather than for any single benchmark.</p>



<p class="wp-block-paragraph">HydraFusion&rsquo;s per-capability scores provided a consistent basis for comparing candidate routing policies. Instead of manually tuning thresholds, we used beam search to build the optimal decision policy. Each candidate was measured against a frozen baseline on quality, cost, and failure modes, so improvements were evaluated on stable ground.</p>



<p class="wp-block-paragraph">TerminalBench 2.1 provides the most complete sequence of runs, making it the clearest view of this iterative improvement. The progression was not linear. Between August 11 and August 25, two operational failures in the evaluation harness produced invalid runs. Those failures were excluded from the performance trend, corrected, and followed by continued gains in the HydraFusion configurations. By August 25, HydraFusion had reached its strongest operating points in the recorded series.</p>



<style data-wp-block-html="css">
.ghchart {
  /* Theme tokens */
  --ghc-text: #1f2328;
  --ghc-muted: #656d76;
  --ghc-grid: #d0d7de;
  --ghc-axis: #59636e;
  --ghc-card-bg: #ffffff;
  --ghc-card-border: #d0d7de;
  --ghc-divider: #b6bfb8;
  --ghc-guide: #afb8c1;

  /* GitHub brand font — matches the github.blog stack (Mona Sans, with
     the blog's metric fallback) so it renders identically in-context. */
  font-family: "Mona Sans", "Mona Sans Fallback", -apple-system, "system-ui",
    "Segoe UI", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 699px;
  margin: 0 auto;
  color: var(--ghc-text);
  line-height: 1.4;
}

.ghchart *,
.ghchart *::before,
.ghchart *::after { box-sizing: border-box; }

/* ------------------------------------------------------------------ */
/* Header + legend                                                     */
/* ------------------------------------------------------------------ */
.ghchart__header {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  min-height: 24px;
  margin-bottom: 4px;
}

.ghchart__legend {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.ghchart__legend-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: clamp(14px, 1.9vw, 17px);
  font-weight: 600;
  color: var(--ghc-muted);
  white-space: nowrap;
}

.ghchart__swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  flex: 0 0 auto;
}

/* ------------------------------------------------------------------ */
/* Plot (SVG)                                                          */
/* ------------------------------------------------------------------ */
.ghchart__plot {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  touch-action: pan-y;
}

.ghchart__gridline {
  stroke: var(--ghc-grid);
  stroke-width: 1;
  stroke-dasharray: 2 6;
  stroke-linecap: round;
  opacity: 0.9;
}

.ghchart__axislabel {
  fill: var(--ghc-axis);
  font-size: 22px;
  font-weight: 500;
}
.ghchart__axislabel--x { font-weight: 600; }

.ghchart__line {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ghchart__dot { transition: r 0.12s ease; }

/* Dashed threshold line (e.g. a target/reference level) + floating label.
   Stroke color is set inline per chart; dash + label styling live here. */
.ghchart__threshold {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.9;
}
.ghchart__threshold-label {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Hatched band marking a partial / projected trailing region. */
.ghchart__projection { pointer-events: none; }

.ghchart__guide {
  stroke: var(--ghc-guide);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
  opacity: 0;
  transition: opacity 0.12s ease;
}

.ghchart__hit { fill: transparent; }

/* ------------------------------------------------------------------ */
/* Scatter plot                                                        */
/* ------------------------------------------------------------------ */
.ghchart__gridline--v { /* vertical gridlines reuse the dashed grid style */ }

.ghchart__axistitle {
  fill: var(--ghc-muted);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Dotted reference crosshair through the "reference" point. Stroke color
   is set inline (tracks the reference series color). */
.ghchart__crosshair {
  stroke-width: 2;
  stroke-dasharray: 2 7;
  stroke-linecap: round;
  opacity: 0.85;
  pointer-events: none;
}

.ghchart__halo { opacity: 0.18; pointer-events: none; }
.ghchart__pt { transition: r 0.12s ease; }

.ghchart__pt-label {
  fill: var(--ghc-muted);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  pointer-events: none;
}
.ghchart__pt-label--strong { font-size: 21px; font-weight: 700; }

.ghchart__pthit { fill: transparent; cursor: default; outline: none; }
.ghchart__pthit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

.ghchart__swatch--dot { border-radius: 50%; }

/* ------------------------------------------------------------------ */
/* Trend chart (dated time-series with narrative headline)             */
/* ------------------------------------------------------------------ */
.ghchart__headline {
  font-size: clamp(17px, 2.3vw, 20px);
  line-height: 1.32;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
  margin: 0 0 8px;
}

/* Trend legend sits left-aligned under the headline/subtitle. */
.ghchart__legend--trend {
  justify-content: flex-start;
  margin: 2px 0 14px;
}
.ghchart__glyph { flex: 0 0 auto; }
.ghchart__glyph--square {
  width: 13px; height: 13px; border-radius: 2px;
}
.ghchart__glyph--diamond {
  width: 12px; height: 12px; border: 1.5px solid; transform: rotate(45deg);
}
.ghchart__glyph--line {
  width: 20px; height: 0; border-top: 2px dashed; border-radius: 0;
}

/* Phase background columns + separators + top labels. */
.ghchart__phase { fill: #f6f8fa; opacity: 0.55; }
.ghchart__phase--alt { fill: #ffffff; opacity: 0; }
.ghchart__phase-sep {
  stroke: var(--ghc-grid); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.8;
}
.ghchart__phase-label {
  fill: var(--ghc-muted);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
}
.ghchart__phase-avg {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Dashed trend line + ± noise band + slope label. */
.ghchart__trendband { opacity: 0.12; pointer-events: none; }
.ghchart__trendline {
  stroke-width: 2.5;
  stroke-dasharray: 8 6;
  stroke-linecap: round;
  fill: none;
}
.ghchart__slope {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Horizontal reference line + label. */
.ghchart__refline {
  stroke-width: 2;
  stroke-dasharray: 7 6;
  stroke-linecap: round;
  opacity: 0.85;
}
.ghchart__reflabel {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Data squares — values reveal on hover only. */
.ghchart__square { transition: width 0.12s ease, height 0.12s ease, x 0.12s ease, y 0.12s ease; }
.ghchart__squarehit { fill: transparent; cursor: default; outline: none; }
.ghchart__squarehit:focus-visible {
  outline: 2px solid var(--ghc-axis);
  outline-offset: 1px;
}

/* Invalid-run markers + leader-line annotations. */
.ghchart__invalid { stroke-width: 2; }
.ghchart__invalid-x { stroke-width: 2; stroke-linecap: round; }
.ghchart__leader { stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.8; }
.ghchart__leader--callout { stroke-dasharray: none; opacity: 0.6; }
.ghchart__anno-title {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.ghchart__anno-sub {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 500;
}
/* Final-callout labels read a touch larger than the invalid-run notes. */
.ghchart__anno-title--callout { font-size: 17px; }
.ghchart__anno-sub--callout { font-size: 14px; }

/* Bracket span annotation. */
.ghchart__bracket { stroke: var(--ghc-axis); stroke-width: 1.5; opacity: 0.75; }
.ghchart__bracket-label {
  fill: var(--ghc-muted);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
}

/* Final callout ring. */
.ghchart__callout-ring { fill: none; stroke-width: 2; }

/* ------------------------------------------------------------------ */
/* Title + divider                                                     */
/* ------------------------------------------------------------------ */
.ghchart__title {
  /* Matches the github.blog H2 (.wp-block-heading) exactly */
  display: block;
  font-size: 32px;
  line-height: 40px;
  font-weight: 700;
  letter-spacing: -0.18px;
  color: var(--ghc-text);
}

.ghchart__rule {
  height: 1.5px;
  border-radius: 1.5px;
  background: var(--ghc-divider);
  margin: 10px 0 18px;
}

/* ------------------------------------------------------------------ */
/* Bar chart: subtitle, panels, bars, caption                          */
/* ------------------------------------------------------------------ */
.ghchart__subtitle {
  margin: -6px 0 20px;
  font-size: clamp(13px, 1.6vw, 15px);
  line-height: 1.5;
  color: var(--ghc-muted);
}

.ghchart__panel { margin-top: 30px; }
.ghchart__panel:first-of-type { margin-top: 6px; }

.ghchart__panel-title {
  margin: 0 0 6px;
  font-size: clamp(15px, 1.9vw, 18px);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ghc-text);
}

.ghchart__plot--bar { touch-action: pan-y; }

.ghchart__axisline {
  stroke: var(--ghc-axis);
  stroke-width: 1.25;
  opacity: 0.35;
}

.ghchart__bar { transition: opacity 0.12s ease; }
.ghchart__barhit { fill: transparent; }

/* Always-on value labels above selected bars. */
.ghchart__barvalue {
  fill: var(--ghc-text);
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Anchor annotation: dotted reference line + floating pill. */
.ghchart__anno-line {
  stroke: #8b949e;
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  stroke-linecap: round;
  pointer-events: none;
}
.ghchart__anno-chip {
  fill: #ffebe9;
  stroke: #ffcecb;
  stroke-width: 1;
  pointer-events: none;
}
.ghchart__anno-chip-text {
  fill: #a0111f;
  font-weight: 700;
  pointer-events: none;
}

.ghchart__caption {
  margin-top: 22px;
  padding-top: 12px;
  border-top: 1px solid var(--ghc-card-border);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ghc-muted);
}
.ghchart__caption b { color: var(--ghc-text); font-weight: 700; }
.ghchart__caption code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.92em;
  padding: 1px 5px;
  background: #f6f8fa;
  border: 1px solid var(--ghc-card-border);
  border-radius: 5px;
}
.ghchart__tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid #aceebb;
  color: #0a5223;
  background: #dafbe1;
  white-space: nowrap;
  vertical-align: middle;
}

/* ------------------------------------------------------------------ */
/* Distribution dashboard (segmented mix bar + ranked bar panels)      */
/* ------------------------------------------------------------------ */
.ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-section:first-of-type { margin-top: 12px; }

/* Segmented proportional "mix" bar: name inside, count/percent below.
   Separators are 2px inside borders (box-sizing:border-box) rather than a
   flex gap, so the three segments sum to exactly 100% with no overflow. */
.ghchart__seg {
  display: flex;
  width: 100%;
  height: 42px;
  margin-top: 2px;
  border-radius: 7px;
  overflow: hidden;
}
.ghchart__seg-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  color: #ffffff;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
}
.ghchart__seg-cell + .ghchart__seg-cell { border-left: 2px solid var(--ghc-card-bg); }
.ghchart__seg-note-row {
  display: flex;
  width: 100%;
  margin-top: 8px;
}
.ghchart__seg-note {
  min-width: 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Keep the outer notes from spilling past the chart edges. */
.ghchart__seg-note:first-child { text-align: left; }
.ghchart__seg-note:last-child { text-align: right; }
.ghchart__seg-note b { color: var(--ghc-text); font-weight: 700; }

/* Ranked horizontal bar list: [category | bar track | value].
   The list is the grid and rows use display:contents, so the category,
   bar, and value columns line up across every row in a panel. */
.ghchart__dist-list {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  align-items: center;
  column-gap: 12px;
  row-gap: 10px;
  margin-top: 4px;
}
.ghchart__dist-row { display: contents; }
.ghchart__dist-cat {
  text-align: right;
  font-size: 13px;
  line-height: 1.25;
  color: var(--ghc-text);
  white-space: nowrap;
}
.ghchart__dist-track { width: 100%; }
.ghchart__dist-bar {
  height: 15px;
  min-width: 2px;
  border-radius: 3px;
}
.ghchart__dist-val {
  font-size: 12.5px;
  color: var(--ghc-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ghchart__dist-val b { color: var(--ghc-text); font-weight: 700; }

/* Two-up panel grid (collapses to one column on narrow screens).
   minmax(0,1fr) lets columns shrink so long labels never force overflow. */
.ghchart__dist-cols {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px 30px;
}
.ghchart__dist-cols .ghchart__dist-section { margin-top: 0; }
.ghchart__dist-cols--stack { grid-template-columns: 1fr; }
.ghchart__dist-cols--stack .ghchart__dist-section { margin-top: 28px; }
.ghchart__dist-cols--stack .ghchart__dist-section:first-child { margin-top: 0; }

/* Tighten the label/value type a touch in the 2-up layout so the bars
   keep room at the 699px blog width. */
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-cat { font-size: 12px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-val { font-size: 11.5px; }
.ghchart__dist-cols:not(.ghchart__dist-cols--stack) .ghchart__dist-list { column-gap: 9px; }

@media (max-width: 600px) {
  .ghchart__dist-cols { grid-template-columns: 1fr; gap: 26px; }
  .ghchart__dist-cols .ghchart__dist-section ~ .ghchart__dist-section { margin-top: 2px; }
}

/* Let long category labels wrap on very narrow (mobile) widths. */
@media (max-width: 480px) {
  .ghchart__dist-cat { white-space: normal; }
}

/* ------------------------------------------------------------------ */
/* Calendar heatmap (GitHub contribution-graph style)                  */
/* ------------------------------------------------------------------ */
.ghheat__eyebrow { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.ghheat__eyedot {
  width: 9px; height: 9px; border-radius: 50%;
  background: #cf222e; box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.14);
  flex: 0 0 auto;
}
.ghheat__eyetext {
  font-size: 12px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: #cf222e;
}
/* Subtitle here is a prominent lede, not the muted variant. */
.ghheat__subtitle {
  color: var(--ghc-text);
  font-size: clamp(14px, 1.7vw, 16px);
  /* 24px top gap matches the github.blog H2 → paragraph block spacing. */
  margin: 24px 0 8px;
}
.ghheat__subtitle b { font-weight: 700; }

.ghheat__panel { margin-top: 36px; }
.ghheat__phead {
  display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.ghheat__badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  color: #fff; font-size: 12px; font-weight: 700;
  flex: 0 0 auto; align-self: center;
}
.ghheat__ptitle {
  font-size: clamp(16px, 2vw, 19px); font-weight: 700;
  letter-spacing: -0.01em; color: var(--ghc-text);
}
.ghheat__pdesc { font-size: 14px; color: var(--ghc-muted); }

.ghheat__body { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }
.ghheat__gridwrap { flex: 1 1 340px; min-width: 260px; }
.ghheat__grid { display: block; width: 100%; height: auto; overflow: visible; }
.ghheat__cell {
  stroke: rgba(27, 31, 36, 0.06);
  stroke-width: 1;
  transition: stroke 0.1s ease;
}
.ghheat__cell:hover { stroke: #24292f; stroke-width: 1.4; }
.ghheat__cell--pad { opacity: 0; pointer-events: none; }
.ghheat__wd, .ghheat__month {
  fill: var(--ghc-muted); font-size: 11px; font-weight: 600;
}

.ghheat__side { flex: 1 1 250px; min-width: 230px; }
.ghheat__stat { display: flex; align-items: baseline; gap: 7px; }
.ghheat__statbig {
  font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em;
  line-height: 1;
}
.ghheat__statsmall { font-size: 15px; font-weight: 600; color: var(--ghc-muted); }
.ghheat__note {
  margin: 10px 0 16px; font-size: 14px; line-height: 1.55; color: var(--ghc-muted);
}
.ghheat__note b { color: var(--ghc-text); font-weight: 700; }
.ghheat__legend {
  display: flex; align-items: center; gap: 5px;
  font-size: 12.5px; color: var(--ghc-muted); flex-wrap: wrap;
}
.ghheat__sw { width: 12px; height: 12px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }
.ghheat__sw--empty { border: 1px solid #d0d7de; }
.ghheat__legunit { margin-left: 2px; }
.ghheat__extra {
  display: flex; align-items: flex-start; gap: 7px; margin-top: 14px;
  font-size: 12px; line-height: 1.5; color: var(--ghc-muted); max-width: 330px;
}
.ghheat__extra .ghheat__sw { margin-top: 2px; }

/* ------------------------------------------------------------------ */
/* Tooltip                                                             */
/* ------------------------------------------------------------------ */
.ghchart__tooltip {
  position: absolute;
  z-index: 5;
  top: 0;
  left: 0;
  min-width: 128px;
  max-width: 260px;
  padding: 10px 12px;
  background: var(--ghc-card-bg);
  border: 1px solid var(--ghc-card-border);
  border-radius: 12px;
  /* Layered drop shadow from Primer Brand's ActionMenu (.ActionMenu__menu) */
  box-shadow:
    0px 100px 80px rgba(0, 0, 0, 0.01),
    0px 41px 33px rgba(0, 0, 0, 0.02),
    0px 22px 17px rgba(0, 0, 0, 0.02),
    0px 12px 10px rgba(0, 0, 0, 0.03),
    0px 6px 5px rgba(0, 0, 0, 0.04),
    0px 2px 2px rgba(0, 0, 0, 0.07);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -100%);
  /* Fade in on show and out on hide; quickly ease left/top when tracking
     from one point to the next. The position ease is suppressed on the
     first appearance (see chart.js) so the tooltip fades in place rather
     than sliding in from its previous spot. */
  transition: opacity 0.12s ease,
    left 0.1s cubic-bezier(0.4, 0, 0.2, 1),
    top 0.1s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: opacity, transform, left, top;
}
.ghchart__tooltip[data-visible="true"] { opacity: 1; }

.ghchart__tt-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--ghc-text);
  margin-bottom: 6px;
}

.ghchart__tt-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ghc-muted);
}
.ghchart__tt-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.ghchart__tt-name { flex: 1 1 auto; }
.ghchart__tt-value { font-weight: 700; color: var(--ghc-text); font-variant-numeric: tabular-nums; }

/* Bar tooltip body: a title line + free-form text lines (no swatches). */
.ghchart__tt-line {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ghc-muted);
}
.ghchart__tt-line:first-child { color: var(--ghc-text); font-weight: 600; }
.ghchart__tt-line + .ghchart__tt-line { margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ghchart__dot,
  .ghchart__guide,
  .ghchart__bar,
  .ghchart__pt,
  .ghchart__square,
  .ghheat__cell,
  .ghchart__tooltip { transition: none; }
}

</style>

<script data-wp-block-html="js">
/*
  Shared chart engine — reusable renderer for the blog-charts library.
  ------------------------------------------------------------------
  Exposes window.GHChartEngine = { renderers, render(config, mountEl?) }.
  Renderers are keyed by config.type (currently "line"); add a new viz type
  by registering GHChartEngine.renderers[<type>] = function (root, config) {…}.

  This same file is embedded verbatim at the top of each exported WordPress
  chart.js, followed by that chart's CONFIG + a small auto-boot. It has no
  external dependencies and is safe to include more than once on a page.
*/
(function (global) {
  "use strict";

  var SVGNS = "http://www.w3.org/2000/svg";

  function el(tag, attrs) {
    var node = document.createElementNS(SVGNS, tag);
    if (attrs) {
      for (var k in attrs) {
        if (Object.prototype.hasOwnProperty.call(attrs, k)) {
          node.setAttribute(k, attrs[k]);
        }
      }
    }
    return node;
  }

  function html(tag, cls) {
    var node = document.createElement(tag);
    if (cls) node.className = cls;
    return node;
  }

  function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }

  var renderers = {};

  /* ================================================================
     Line chart renderer
     ================================================================ */
  renderers.line = function (root, config) {
    // viewBox geometry (design units; scales fluidly to any width).
    var VB = { w: 1000, h: 560 };
    var M = { top: 30, right: 34, bottom: 56, left: 86 };
    // Inset the plotted points slightly from the axis so the first/last
    // months (e.g. "Jan") don't crowd the y-axis labels (e.g. "50").
    var PAD_X = 18;
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    // Reset (in case of re-render) and use a unique id prefix so multiple
    // charts on one page never share gradient ids.
    root.textContent = "";
    var uid = config.mountId;

    var n = config.labels.length;
    var yMin = config.yAxis.min;
    var yMax = config.yAxis.max;
    var step = config.yAxis.step;
    // Y-axis tick labels can format differently from the tooltip values
    // (e.g. a plain "40" on the axis but "40%" in the tooltip).
    var fmtAxis = config.formatAxis || config.formatValue;

    var plotW = innerW - 2 * PAD_X;
    var stepX = n <= 1 ? 0 : plotW / (n - 1);
    function xAt(i) { return n <= 1 ? M.left + innerW / 2 : M.left + PAD_X + stepX * i; }
    function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
    var baseY = yAt(yMin);

    /* ---- Title + divider (top) ---------------------------------- */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    /* ---- Header + legend ---------------------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      config.series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " line chart"
    });

    // Gradient defs (one per area series).
    var defs = el("defs");
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var g = el("linearGradient", {
        id: uid + "-grad-" + si, x1: "0", y1: "0", x2: "0", y2: "1"
      });
      g.appendChild(el("stop", { offset: "0", "stop-color": s.color, "stop-opacity": "0.28" }));
      g.appendChild(el("stop", { offset: "0.55", "stop-color": s.color, "stop-opacity": "0.08" }));
      g.appendChild(el("stop", { offset: "1", "stop-color": s.color, "stop-opacity": "0" }));
      defs.appendChild(g);
    });
    // Diagonal-hatch pattern for an optional "partial / projected" band.
    if (config.projection) {
      var pat = el("pattern", {
        id: uid + "-hatch", width: 7, height: 7,
        patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)"
      });
      pat.appendChild(el("line", {
        x1: 0, y1: 0, x2: 0, y2: 7,
        stroke: config.projection.color || "#8c959f",
        "stroke-width": 1.1, "stroke-opacity": 0.5
      }));
      defs.appendChild(pat);
    }
    svg.appendChild(defs);

    // Gridlines + y-axis labels.
    for (var v = yMin; v <= yMax + 0.0001; v += step) {
      var gy = yAt(v);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 22, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtAxis(v);
      svg.appendChild(yl);
    }

    // x-axis labels — either one per data point (default) or a sparse set
    // of ticks decoupled from the data (config.xTicks: [{at,text}, …]),
    // which suits a dense daily series that only needs month markers.
    var xTicks = config.xTicks ||
      config.labels.map(function (t, i) { return { at: i, text: t }; });
    xTicks.forEach(function (tk) {
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: xAt(tk.at), y: VB.h - M.bottom + 36,
        "text-anchor": tk.at === 0 ? "start" : (tk.at === n - 1 ? "end" : "middle")
      });
      xl.textContent = tk.text;
      svg.appendChild(xl);
    });

    // Optional hatched "partial / projected" band over the trailing region.
    if (config.projection) {
      var px = xAt(config.projection.fromIndex);
      svg.appendChild(el("rect", {
        "class": "ghchart__projection",
        x: px, y: M.top, width: (VB.w - M.right) - px, height: baseY - M.top,
        fill: "url(#" + uid + "-hatch)"
      }));
    }

    // Optional dashed threshold line + floating label (e.g. a 50% target).
    if (config.threshold) {
      var thy = yAt(config.threshold.value);
      svg.appendChild(el("line", {
        "class": "ghchart__threshold",
        x1: M.left, y1: thy, x2: VB.w - M.right, y2: thy,
        stroke: config.threshold.color
      }));
      if (config.threshold.label) {
        var thl = el("text", {
          "class": "ghchart__threshold-label",
          x: M.left + PAD_X, y: thy - 10, "text-anchor": "start",
          fill: config.threshold.color
        });
        thl.textContent = config.threshold.label;
        svg.appendChild(thl);
      }
    }

    // Precompute point coords per series.
    var pts = config.series.map(function (s) {
      return s.values.map(function (val, i) { return { x: xAt(i), y: yAt(val) }; });
    });

    // Smooth (Catmull-Rom -> cubic Bezier) path builder.
    function smooth(points) {
      if (!points.length) return "";
      if (points.length < 2) return "M" + points[0].x + "," + points[0].y;
      var d = "M " + points[0].x + "," + points[0].y;
      for (var i = 0; i < points.length - 1; i++) {
        var p0 = points[i - 1] || points[i];
        var p1 = points[i];
        var p2 = points[i + 1];
        var p3 = points[i + 2] || p2;
        var t = 1 / 6;
        var c1x = p1.x + (p2.x - p0.x) * t;
        var c1y = p1.y + (p2.y - p0.y) * t;
        var c2x = p2.x - (p3.x - p1.x) * t;
        var c2y = p2.y - (p3.y - p1.y) * t;
        d += " C " + c1x + "," + c1y + " " + c2x + "," + c2y + " " + p2.x + "," + p2.y;
      }
      return d;
    }

    // Area fills (drawn first, behind lines).
    config.series.forEach(function (s, si) {
      if (!s.area) return;
      var p = pts[si];
      var d = smooth(p) + " L " + p[p.length - 1].x + "," + baseY +
              " L " + p[0].x + "," + baseY + " Z";
      svg.appendChild(el("path", { d: d, fill: "url(#" + uid + "-grad-" + si + ")" }));
    });

    // Vertical hover guide (hidden until hover).
    var guide = el("line", {
      "class": "ghchart__guide",
      x1: xAt(0), y1: M.top - 6, x2: xAt(0), y2: baseY
    });
    svg.appendChild(guide);

    // Lines (draw in reverse so the first series sits on top).
    config.series.slice().reverse().forEach(function (s) {
      var si = config.series.indexOf(s);
      svg.appendChild(el("path", {
        "class": "ghchart__line",
        d: smooth(pts[si]),
        stroke: s.color,
        "stroke-width": s.width || (si === 0 ? 4 : 3.25)
      }));
    });

    // Dots (keep references for hover emphasis). When config.hideDots is set
    // (dense daily lines), dots rest invisible and only the hovered one shows.
    var dotEls = config.series.map(function (s, si) {
      return pts[si].map(function (pt) {
        var c = el("circle", {
          "class": "ghchart__dot",
          cx: pt.x, cy: pt.y,
          r: config.hideDots ? 0 : (si === 0 ? 4.5 : 4),
          fill: s.color
        });
        svg.appendChild(c);
        return c;
      });
    });

    // Transparent hit area for pointer capture.
    var hit = el("rect", {
      "class": "ghchart__hit",
      x: M.left,
      y: M.top - 6,
      width: innerW,
      height: baseY - M.top + 12
    });
    svg.appendChild(hit);

    root.appendChild(svg);

    /* ---- Tooltip ------------------------------------------------ */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    tip.appendChild(tipTitle);
    var tipRows = config.series.map(function (s) {
      var row = html("div", "ghchart__tt-row");
      var sw = html("span", "ghchart__tt-swatch");
      sw.style.background = s.color;
      var name = html("span", "ghchart__tt-name");
      name.textContent = s.name;
      var val = html("span", "ghchart__tt-value");
      row.appendChild(sw);
      row.appendChild(name);
      row.appendChild(val);
      tip.appendChild(row);
      return val;
    });
    root.appendChild(tip);

    /* ---- Hover / focus interactivity ---------------------------- */
    var activeIndex = -1;
    var baseR = config.series.map(function (s, si) {
      return config.hideDots ? 0 : (si === 0 ? 4.5 : 4);
    });
    // Radius of the single emphasized dot under the pointer.
    var dotActiveR = config.series.map(function (s, si) {
      return config.hideDots ? 6 : baseR[si] + 2.5;
    });
    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function indexFromClientX(clientX) {
      var rect = svg.getBoundingClientRect();
      var relX = (clientX - rect.left) / rect.width * VB.w;
      var i = stepX ? Math.round((relX - (M.left + PAD_X)) / stepX) : 0;
      return clamp(i, 0, n - 1);
    }

    function show(i) {
      if (i === activeIndex) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      guide.setAttribute("x1", xAt(i));
      guide.setAttribute("x2", xAt(i));
      guide.style.opacity = "1";

      // Emphasize the hovered column's dots; reset the rest.
      dotEls.forEach(function (col, si) {
        col.forEach(function (c, ci) {
          c.setAttribute("r", ci === i ? dotActiveR[si] : baseR[si]);
        });
      });

      // Fill tooltip.
      tipTitle.textContent = config.labels[i];
      config.series.forEach(function (s, si) {
        tipRows[si].textContent = config.formatValue(s.values[i]);
      });

      // Anchor above the highest point in this column.
      var topSi = 0, topY = Infinity;
      config.series.forEach(function (s, si) {
        var yy = yAt(s.values[i]);
        if (yy < topY) { topY = yy; topSi = si; }
      });
      var anchor = dotEls[topSi][i].getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      // On the first appearance, fade in exactly at the point: suppress the
      // position ease so it doesn't slide in from its previous resting spot.
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      // Clamp horizontally within the container.
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      // Flip below the point if there's no room above.
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      // Commit that position without animating, then restore the CSS
      // transition so subsequent point-to-point moves ease into place.
      if (suppress) {
        void tip.offsetWidth;
        tip.style.transition = "";
      }
    }

    function hide() {
      activeIndex = -1;
      guide.style.opacity = "0";
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (col, si) {
        col.forEach(function (c) { c.setAttribute("r", baseR[si]); });
      });
    }

    hit.addEventListener("pointermove", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerdown", function (e) { show(indexFromClientX(e.clientX)); });
    hit.addEventListener("pointerleave", hide);

    // Keyboard access.
    svg.setAttribute("tabindex", "0");
    svg.addEventListener("keydown", function (e) {
      if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
        var start = activeIndex < 0 ? 0 : activeIndex;
        show(clamp(start + (e.key === "ArrowRight" ? 1 : -1), 0, n - 1));
        e.preventDefault();
      } else if (e.key === "Escape") {
        hide();
      }
    });
    svg.addEventListener("blur", hide);
  };

  /* ================================================================
     Bar chart renderer (composite)
     ----------------------------------------------------------------
     One "graph" can stack multiple bar panels in a single block:
       config = { type:"bar", title, subtitle?, series:[{name,color}],
                  barRadius?, panels:[ panel… ], caption? }
       panel  = { title?, labels[], values[], yAxis:{min,max,step},
                  formatValue(v), height?, maxBarVB?,
                  tooltip(i, panel) -> { title, lines:[…] } }
     No printed value labels — values surface on hover (consistent with
     the line chart). Bars use a single tunable color with rounded tops.
     ================================================================ */
  renderers.bar = function (root, config) {
    root.textContent = "";

    var barColor = (config.series && config.series[0] && config.series[0].color) || "#ff7b72";
    var barRadius = config.barRadius != null ? config.barRadius : 8;

    // Per-bar fill: a bar may point at a different series (e.g. a muted
    // "preliminary" color) via panel.barSeries[index]; otherwise series[0].
    function barColorFor(panel, j) {
      var si = (panel.barSeries && panel.barSeries[j] != null) ? panel.barSeries[j] : 0;
      var s = config.series && config.series[si];
      return (s && s.color) || barColor;
    }

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Title + divider (+ optional subtitle) ------------------ */
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    root.appendChild(html("div", "ghchart__rule"));

    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip for all panels ---------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var allBars = [];       // { el, pi, bi }
    var activeKey = null;   // "pi:bi" of the hovered bar

    function hideTip() {
      activeKey = null;
      tip.setAttribute("data-visible", "false");
      allBars.forEach(function (b) { b.el.style.opacity = "1"; });
    }

    // Hoisted: referenced by the per-column pointer handlers below.
    function showTip(pi, bi, panel, panelBars) {
      var reappearing = activeKey === null;
      activeKey = pi + ":" + bi;

      allBars.forEach(function (b) {
        b.el.style.opacity = (b.pi === pi && b.bi === bi) ? "1" : "0.42";
      });

      var content = panel.tooltip
        ? panel.tooltip(bi, panel)
        : { title: (panel.labels || [])[bi],
            lines: [ (panel.formatValue || String)(panel.values[bi]) ] };

      tipTitle.textContent = content.title != null ? content.title : "";
      tipBody.textContent = "";
      (content.lines || []).forEach(function (ln) {
        var row = html("div", "ghchart__tt-line");
        row.textContent = ln;
        tipBody.appendChild(row);
      });

      var anchor = panelBars[bi].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";

      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels ------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var wrap = html("div", "ghchart__panel");
      if (panel.title) {
        var pt = html("h3", "ghchart__panel-title");
        pt.textContent = panel.title;
        wrap.appendChild(pt);
      }

      var VB = { w: 1000, h: panel.height || 360 };
      var M = { top: 20, right: 20, bottom: 48, left: 84 };
      var innerW = VB.w - M.left - M.right;
      var innerH = VB.h - M.top - M.bottom;

      var labels = panel.labels || [];
      var values = panel.values || [];
      var nb = labels.length;
      var ax = panel.yAxis || { min: 0, max: Math.max.apply(null, values.concat([1])), step: 0 };
      var yMin = ax.min, yMax = ax.max, step = ax.step || 0;
      var fmt = panel.formatValue || function (v) { return String(v); };

      function yAt(v) { return M.top + innerH * (1 - (v - yMin) / (yMax - yMin)); }
      var baseY = yAt(yMin);

      var slot = nb ? innerW / nb : innerW;
      var maxBarVB = panel.maxBarVB != null ? panel.maxBarVB : 150;
      var barW = Math.min(slot * 0.62, maxBarVB);
      function xCenter(i) { return M.left + slot * (i + 0.5); }

      var svg = el("svg", {
        "class": "ghchart__plot ghchart__plot--bar",
        viewBox: "0 0 " + VB.w + " " + VB.h,
        preserveAspectRatio: "xMidYMid meet",
        role: "img",
        "aria-label": (panel.title || config.title) + " bar chart"
      });

      // Gridlines + y-axis labels.
      if (step > 0) {
        for (var v = yMin; v <= yMax + 1e-9; v += step) {
          var gy = yAt(v);
          svg.appendChild(el("line", {
            "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
          }));
          var yl = el("text", {
            "class": "ghchart__axislabel ghchart__axislabel--y",
            x: M.left - 18, y: gy, dy: "0.32em", "text-anchor": "end"
          });
          yl.textContent = fmt(v);
          svg.appendChild(yl);
        }
      }

      // Solid baseline.
      svg.appendChild(el("line", {
        "class": "ghchart__axisline", x1: M.left, y1: baseY, x2: VB.w - M.right, y2: baseY
      }));

      // x-axis labels.
      for (var i = 0; i < nb; i++) {
        var xl = el("text", {
          "class": "ghchart__axislabel ghchart__axislabel--x",
          x: xCenter(i), y: VB.h - M.bottom + 30, "text-anchor": "middle"
        });
        if (panel.xLabelSize) xl.setAttribute("style", "font-size:" + panel.xLabelSize + "px");
        xl.textContent = labels[i];
        svg.appendChild(xl);
      }

      // Bars (rounded top corners only) + full-height hover columns.
      var panelBars = [];
      for (var j = 0; j < nb; j++) {
        var cx = xCenter(j);
        var topY = yAt(values[j]);
        var barH = baseY - topY;
        var x = cx - barW / 2;
        var r = Math.max(0, Math.min(barRadius, barW / 2, barH));
        var d = "M " + x + " " + baseY +
                " L " + x + " " + (topY + r) +
                " Q " + x + " " + topY + " " + (x + r) + " " + topY +
                " L " + (x + barW - r) + " " + topY +
                " Q " + (x + barW) + " " + topY + " " + (x + barW) + " " + (topY + r) +
                " L " + (x + barW) + " " + baseY + " Z";
        var bar = el("path", { "class": "ghchart__bar", d: d, fill: barColorFor(panel, j) });
        svg.appendChild(bar);
        var rec = { el: bar, pi: pi, bi: j };
        panelBars.push(rec);
        allBars.push(rec);

        var col = el("rect", {
          "class": "ghchart__barhit",
          x: M.left + slot * j, y: M.top - 6, width: slot, height: baseY - M.top + 6
        });
        (function (idx) {
          col.addEventListener("pointermove", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerdown", function () { showTip(pi, idx, panel, panelBars); });
          col.addEventListener("pointerleave", hideTip);
        })(j);
        svg.appendChild(col);
      }

      /* ---- Annotation layer (drawn on top; pointer-events: none) ----
         · always-on value labels above chosen bars (panel.valueLabels)
         · anchor lines: a dotted horizontal reference at one bar's top
           extended to another, with a floating pill (panel.annotations) */
      var vlabels = panel.valueLabels || null;
      if (vlabels) {
        for (var vk in vlabels) {
          if (!Object.prototype.hasOwnProperty.call(vlabels, vk)) continue;
          var vi = +vk;
          var vt = el("text", {
            "class": "ghchart__barvalue",
            x: xCenter(vi), y: yAt(values[vi]) - 12, "text-anchor": "middle"
          });
          vt.textContent = vlabels[vk];
          svg.appendChild(vt);
        }
      }

      (panel.annotations || []).forEach(function (an) {
        if (an.type !== "anchor") return;
        var yLine = yAt(values[an.fromIndex]);
        svg.appendChild(el("line", {
          "class": "ghchart__anno-line",
          x1: xCenter(an.fromIndex) - barW / 2, y1: yLine,
          x2: xCenter(an.toIndex) + barW / 2, y2: yLine
        }));

        var chipIdx = an.chipAtIndex != null ? an.chipAtIndex : an.toIndex;
        var cx = xCenter(chipIdx);
        var cy = (yAt(values[chipIdx]) + yLine) / 2;
        var label = an.label || "";
        var fs = 19;
        var chipW = label.length * fs * 0.55 + 24;
        var chipH = fs + 16;
        svg.appendChild(el("rect", {
          "class": "ghchart__anno-chip",
          x: cx - chipW / 2, y: cy - chipH / 2, width: chipW, height: chipH, rx: chipH / 2
        }));
        var ct = el("text", {
          "class": "ghchart__anno-chip-text",
          x: cx, y: cy, dy: "0.34em", "text-anchor": "middle", "font-size": fs
        });
        ct.textContent = label;
        svg.appendChild(ct);
      });

      wrap.appendChild(svg);
      root.appendChild(wrap);
    });

    /* ---- Caption (optional rich copy) --------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Calendar heatmap renderer (GitHub contribution-graph style)
     ----------------------------------------------------------------
     One square per calendar day across a month range. Each panel is
     shaded on its own scale — every day in a month shares that month's
     level (the underlying series is monthly). Each panel's 5-step
     faint->dark ramp is derived from a single tunable base color.
     ================================================================ */
  renderers.heatmap = function (root, config) {
    root.textContent = "";

    var WHITE = [255, 255, 255], BLACK = [0, 0, 0];
    function hexToRgb(h) {
      h = String(h).replace("#", "");
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
    }
    function rgbToHex(c) {
      return "#" + c.map(function (x) {
        x = Math.max(0, Math.min(255, Math.round(x)));
        return (x < 16 ? "0" : "") + x.toString(16);
      }).join("");
    }
    function mix(a, b, t) {
      return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
    }
    // 5-step contribution ramp (level 0 faint -> level 4 dark) from one hex.
    function rampFrom(baseHex) {
      var b = hexToRgb(baseHex);
      return [
        rgbToHex(mix(b, WHITE, 0.80)),
        rgbToHex(mix(b, WHITE, 0.52)),
        rgbToHex(mix(b, WHITE, 0.20)),
        baseHex,
        rgbToHex(mix(b, BLACK, 0.30))
      ];
    }

    function trim0(s) { return s.replace(/\.0$/, ""); }
    function fmtM(v) {
      if (v >= 1000) { var g = v / 1000; return (g >= 10 ? Math.round(g) : trim0(g.toFixed(1))) + "B"; }
      if (v >= 1) return trim0(v.toFixed(1)) + "M";
      return Math.round(v * 1000) + "K";
    }

    var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    var MON_FULL = ["January", "February", "March", "April", "May", "June",
      "July", "August", "September", "October", "November", "December"];
    var WD = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

    var year = config.year || 2026;
    var startM = config.startMonth != null ? config.startMonth : 0;
    var endM = config.endMonth != null ? config.endMonth : 5;
    var emptyColor = config.emptyColor || "#ebedf0";

    function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
    function parseISO(s) { var p = s.split("-"); return new Date(+p[0], +p[1] - 1, +p[2]); }
    function dayStart(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); }

    /* ---- Eyebrow + title + subtitle ----------------------------- */
    if (config.eyebrow) {
      var eb = html("div", "ghheat__eyebrow");
      eb.appendChild(html("span", "ghheat__eyedot"));
      var ebt = html("span", "ghheat__eyetext");
      ebt.textContent = config.eyebrow;
      eb.appendChild(ebt);
      root.appendChild(eb);
    }
    var title = html("span", "ghchart__title");
    title.textContent = config.title;
    root.appendChild(title);
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle ghheat__subtitle");
      sub.innerHTML = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- One shared tooltip -------------------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }
    var activeCell = null;
    function hideTip() { activeCell = null; tip.setAttribute("data-visible", "false"); }
    function showTip(rectEl, titleText, lines) {
      var reappearing = activeCell === null;
      activeCell = rectEl;
      tipTitle.textContent = titleText;
      tipBody.textContent = "";
      lines.forEach(function (ln) {
        var r = html("div", "ghchart__tt-line");
        r.innerHTML = ln;
        tipBody.appendChild(r);
      });
      var a = rectEl.getBoundingClientRect(), rr = root.getBoundingClientRect();
      var left = a.left + a.width / 2 - rr.left, top = a.top - rr.top - 12;
      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";
      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = a.bottom - rr.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }

    /* ---- Panels -------------------------------------------------- */
    (config.panels || []).forEach(function (panel, pi) {
      var base = (config.series && config.series[pi] && config.series[pi].color) || panel.base || "#3094ff";
      var ramp = rampFrom(base);
      var strong = ramp[4];

      var totals = panel.months || [];
      var activeVals = [];
      for (var mi = startM; mi <= endM; mi++) { if (totals[mi] != null) activeVals.push(totals[mi]); }
      var vmin = activeVals.length ? Math.min.apply(null, activeVals) : 0;
      var vmax = activeVals.length ? Math.max.apply(null, activeVals) : 1;
      function levelFor(t) {
        if (t == null) return -1;
        if (vmax === vmin) return 4;
        return Math.round((t - vmin) / (vmax - vmin) * 4);
      }

      var af = panel.activeFrom ? parseISO(panel.activeFrom) : new Date(year, startM, 1);
      var at = panel.activeTo ? parseISO(panel.activeTo) : new Date(year, endM, daysInMonth(year, endM));

      var wrap = html("div", "ghheat__panel");

      var head = html("div", "ghheat__phead");
      var badge = html("span", "ghheat__badge");
      badge.textContent = panel.badge != null ? panel.badge : (pi + 1);
      badge.style.background = strong;
      head.appendChild(badge);
      var pt = html("span", "ghheat__ptitle");
      pt.textContent = panel.title;
      head.appendChild(pt);
      if (panel.descriptor) {
        var pd = html("span", "ghheat__pdesc");
        pd.textContent = panel.descriptor;
        head.appendChild(pd);
      }
      wrap.appendChild(head);

      var body = html("div", "ghheat__body");

      /* ---- Calendar grid (SVG) ---- */
      var gridStart = new Date(year, startM, 1);
      var gs = new Date(gridStart);
      gs.setDate(gs.getDate() - gs.getDay());            // back to Sunday
      var gridEnd = new Date(year, endM, daysInMonth(year, endM));
      var ge = new Date(gridEnd);
      ge.setDate(ge.getDate() + (6 - ge.getDay()));      // forward to Saturday
      var nCols = Math.round((ge - gs) / (7 * 86400000)) + 1;

      var cell = 13, sq = 11, rad = 2.5, labL = 34, labT = 18;
      var W = labL + nCols * cell, H = labT + 7 * cell;
      var svg = el("svg", {
        "class": "ghheat__grid",
        viewBox: "0 0 " + W + " " + H,
        preserveAspectRatio: "xMinYMin meet",
        role: "img",
        "aria-label": panel.title + " calendar heatmap"
      });

      [1, 3, 5].forEach(function (r) {
        var t = el("text", {
          "class": "ghheat__wd", x: labL - 6, y: labT + r * cell + sq / 2,
          dy: "0.32em", "text-anchor": "end"
        });
        t.textContent = WD[r];
        svg.appendChild(t);
      });

      // Month labels above the column where each month begins.
      for (var lm = startM; lm <= endM; lm++) {
        var first = new Date(year, lm, 1);
        var colOf = Math.floor((dayStart(first) - gs) / (7 * 86400000));
        var mlab = el("text", { "class": "ghheat__month", x: labL + colOf * cell, y: 12, "text-anchor": "start" });
        mlab.textContent = MON[lm];
        svg.appendChild(mlab);
      }

      var afS = dayStart(af), atS = dayStart(at);
      var d = new Date(gs);
      for (var i = 0; i < nCols * 7; i++) {
        var row = d.getDay();
        var col = Math.floor(i / 7);
        var x = labL + col * cell, y = labT + row * cell;
        var m = d.getMonth();
        var inRange = d.getFullYear() === year && m >= startM && m <= endM;
        var ds = dayStart(d);
        var inWindow = inRange && ds >= afS && ds <= atS;
        var hasData = inWindow && totals[m] != null;
        var lvl = hasData ? levelFor(totals[m]) : -1;
        var cellEl = el("rect", {
          "class": "ghheat__cell", x: x, y: y, width: sq, height: sq, rx: rad,
          fill: lvl >= 0 ? ramp[lvl] : emptyColor
        });
        if (inRange) {
          (function (dt, mm, has) {
            var dateStr = WD[dt.getDay()] + " \u00b7 " + MON[mm] + " " + dt.getDate() + ", " + year;
            var lines;
            if (has) {
              var daily = totals[mm] / daysInMonth(year, mm);
              lines = [
                "<b>" + fmtM(daily) + "</b> " + panel.legendLabel,
                MON_FULL[mm] + " \u00b7 " + fmtM(totals[mm]) + " " + panel.unitTotal
              ];
            } else {
              lines = [panel.emptyNote || "No vetted data"];
            }
            cellEl.addEventListener("pointermove", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerdown", function () { showTip(cellEl, dateStr, lines); });
            cellEl.addEventListener("pointerleave", hideTip);
          })(new Date(d), m, hasData);
        } else {
          cellEl.setAttribute("class", "ghheat__cell ghheat__cell--pad");
        }
        svg.appendChild(cellEl);
        d.setDate(d.getDate() + 1);
      }

      var gridwrap = html("div", "ghheat__gridwrap");
      gridwrap.appendChild(svg);
      body.appendChild(gridwrap);

      /* ---- Side column: stat + note + legend + extra ---- */
      var side = html("div", "ghheat__side");
      if (panel.stat) {
        var stat = html("div", "ghheat__stat");
        var big = html("span", "ghheat__statbig");
        big.textContent = panel.stat.big;
        big.style.color = strong;
        stat.appendChild(big);
        if (panel.stat.small) {
          var small = html("span", "ghheat__statsmall");
          small.textContent = panel.stat.small;
          stat.appendChild(small);
        }
        side.appendChild(stat);
      }
      if (panel.note) {
        var note = html("p", "ghheat__note");
        note.innerHTML = panel.note;
        side.appendChild(note);
      }
      var leg = html("div", "ghheat__legend");
      var lw1 = html("span", "ghheat__legword"); lw1.textContent = "Fewer"; leg.appendChild(lw1);
      ramp.forEach(function (c) {
        var s = html("span", "ghheat__sw"); s.style.background = c; leg.appendChild(s);
      });
      var lw2 = html("span", "ghheat__legword"); lw2.textContent = "More"; leg.appendChild(lw2);
      if (panel.legendLabel) {
        var lu = html("span", "ghheat__legunit"); lu.textContent = "\u00b7 " + panel.legendLabel; leg.appendChild(lu);
      }
      side.appendChild(leg);
      if (panel.extraNote) {
        var ex = html("div", "ghheat__extra");
        var exsw = html("span", "ghheat__sw ghheat__sw--empty");
        exsw.style.background = emptyColor;
        ex.appendChild(exsw);
        var ext = html("span", "ghheat__extratext");
        ext.innerHTML = panel.extraNote;
        ex.appendChild(ext);
        side.appendChild(ex);
      }
      body.appendChild(side);

      wrap.appendChild(body);
      root.appendChild(wrap);
    });

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Scatter renderer — X/Y numeric plot with categorical points.
     Used for cost-vs-quality benchmark charts: a highlighted "primary"
     point (glow halo), a "reference" point with a dotted crosshair, and
     muted "other" points. Values never print on the marks — a point's
     name shows for identity and the exact (y, x) reveals on hover.
     ================================================================ */
  renderers.scatter = function (root, config) {
    root.textContent = "";
    var uid = config.mountId;

    var VB = { w: 1000, h: 600 };
    var M = { top: 40, right: 30, bottom: 74, left: 96 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var fmtX = xa.format || function (v) { return String(v); };
    var fmtY = ya.format || function (v) { return String(v); };
    var tipSuffix = config.tipSuffix || "";

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) { return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min)); }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    function colorOf(pt) {
      var s = config.series && config.series[pt.series || 0];
      return (s && s.color) || "#8c959f";
    }

    /* ---- Title (compact narrative headline, no rule) ------------ */
    var title = html("div", "ghchart__headline");
    title.textContent = config.title;
    root.appendChild(title);

    /* ---- Header + legend (from series) -------------------------- */
    if (!config.hideLegend) {
      var header = html("div", "ghchart__header");
      var legend = html("div", "ghchart__legend");
      (config.series || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        var sw = html("span", "ghchart__swatch ghchart__swatch--dot");
        sw.style.background = s.color;
        item.appendChild(sw);
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      header.appendChild(legend);
      root.appendChild(header);
    }

    /* ---- SVG plot ----------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--scatter",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title + " scatter plot"
    });

    // Gridlines (both directions) + tick labels.
    var eps = 1e-6;
    for (var yv = ya.min; yv <= ya.max + eps; yv += ya.step) {
      var gy = yAt(yv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline",
        x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
      }));
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(yv);
      svg.appendChild(yl);
    }
    for (var xv = xa.min; xv <= xa.max + eps; xv += xa.step) {
      var gx = xAt(xv);
      svg.appendChild(el("line", {
        "class": "ghchart__gridline ghchart__gridline--v",
        x1: gx, y1: M.top, x2: gx, y2: baseY
      }));
      var isFirst = Math.abs(xv - xa.min) < eps;
      var isLast = Math.abs(xv - xa.max) < eps;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 34,
        "text-anchor": isFirst ? "start" : (isLast ? "end" : "middle")
      });
      xl.textContent = fmtX(xv);
      svg.appendChild(xl);
    }

    // Axis titles.
    if (xa.title) {
      var xt = el("text", {
        "class": "ghchart__axistitle",
        x: M.left + innerW / 2, y: VB.h - 12, "text-anchor": "middle"
      });
      xt.textContent = xa.title;
      svg.appendChild(xt);
    }
    if (ya.title) {
      var ytX = 24, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle",
        x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Reference crosshair through the flagged point (dual dotted lines).
    var refPt = (config.points || []).filter(function (p) { return p.reference; })[0];
    if (refPt) {
      var rColor = colorOf(refPt);
      var rx = xAt(refPt.x), ry = yAt(refPt.y);
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: rx, y1: M.top, x2: rx, y2: baseY, stroke: rColor
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__crosshair",
        x1: M.left, y1: ry, x2: VB.w - M.right, y2: ry, stroke: rColor
      }));
    }

    // Points: halo (highlight/reference) + dot + name label.
    var dotEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y), c = colorOf(pt);
      var big = pt.highlight || pt.reference;

      if (big) {
        svg.appendChild(el("circle", {
          "class": "ghchart__halo", cx: cx, cy: cy, r: 17, fill: c
        }));
      }
      var dot = el("circle", {
        "class": "ghchart__pt", cx: cx, cy: cy,
        r: big ? 9 : 5.5, fill: c
      });
      svg.appendChild(dot);
      dotEls.push({ el: dot, baseR: big ? 9 : 5.5 });

      // Name label — bold+colored for the story points, muted for the rest.
      var pos = pt.labelPos || (pt.highlight ? "below" : pt.reference ? "above" : "right");
      var dx = 0, dy = 0, anchor = "middle";
      var gap = big ? 15 : 9;
      if (pos === "right") { dx = gap + 4; dy = 5; anchor = "start"; }
      else if (pos === "left") { dx = -(gap + 4); dy = 5; anchor = "end"; }
      else if (pos === "above") { dy = -(gap + 6); anchor = "middle"; }
      else { dy = gap + 16; anchor = "middle"; }  // below
      var lbl = el("text", {
        "class": "ghchart__pt-label" + (big ? " ghchart__pt-label--strong" : ""),
        x: cx + dx, y: cy + dy, "text-anchor": anchor,
        fill: big ? c : null
      });
      lbl.textContent = pt.label;
      svg.appendChild(lbl);

      // Transparent hit target for hover/focus.
      var hit = el("circle", {
        "class": "ghchart__pthit", cx: cx, cy: cy, r: 20,
        tabindex: "0", role: "img",
        "aria-label": pt.label + ": " + fmtY(pt.y) + ", " + fmtX(pt.x) + tipSuffix
      });
      svg.appendChild(hit);
      hitEls.push(hit);
    });

    root.appendChild(svg);

    /* ---- Tooltip (title + free lines) --------------------------- */
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      dotEls.forEach(function (d, di) {
        d.el.setAttribute("r", di === i ? d.baseR + 2.5 : d.baseR);
      });

      tipTitle.textContent = pt.label;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = fmtY(pt.y) + " · " + fmtX(pt.x) + tipSuffix;
      tipBody.appendChild(line);

      var anchor = dotEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      dotEls.forEach(function (d) { d.el.setAttribute("r", d.baseR); });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
  };

  /* ================================================================
     Trend renderer — dated time-series with a narrative headline.
     One renderer covers both HydraFusion time charts via config:
       • linear OR log y-axis (yAxis.scale), explicit yAxis.ticks;
       • phase background columns (with optional per-phase average);
       • filled squares (values reveal on hover — never printed);
       • red-X "invalid run" markers with leader-line annotations;
       • a dashed trend line + optional ±band + slope label;
       • an optional horizontal reference line, bracket, and final callout.
     ================================================================ */
  renderers.trend = function (root, config) {
    root.textContent = "";

    var VB = { w: 1000, h: 560 };
    var M = { top: 64, right: 56, bottom: 56, left: 104 };
    var innerW = VB.w - M.left - M.right;
    var innerH = VB.h - M.top - M.bottom;

    var xa = config.xAxis, ya = config.yAxis;
    var isLog = ya.scale === "log";
    var fmtY = ya.format || function (v) { return String(v); };
    var series = config.series || [];
    function sColor(i, fb) {
      var s = series[i];
      return (s && s.color) || fb;
    }
    var cPoint = sColor(0, "#8250df");
    var cInvalid = sColor(1, "#cf222e");
    var cTrend = (config.trend && config.trend.color) || sColor(0, "#8250df");

    function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); }
    function yAt(v) {
      if (isLog) {
        var lo = Math.log(ya.min) / Math.LN10, hi = Math.log(ya.max) / Math.LN10;
        var t = (Math.log(v) / Math.LN10 - lo) / (hi - lo);
        return M.top + innerH * (1 - t);
      }
      return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min));
    }
    var baseY = yAt(ya.min);

    var motionMQ = window.matchMedia
      ? window.matchMedia("(prefers-reduced-motion: reduce)")
      : null;
    function reduceMotion() { return !!(motionMQ && motionMQ.matches); }

    /* ---- Headline + subtitle + legend (HTML flow above the plot) --- */
    var headline = html("div", "ghchart__headline");
    headline.textContent = config.title;
    root.appendChild(headline);
    if (config.subtitle) {
      var sub = html("div", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }
    if (!config.hideLegend && series.length) {
      var legend = html("div", "ghchart__legend ghchart__legend--trend");
      /* Build one legend glyph. The "invalid" glyph mirrors the on-plot marker
         exactly — a red diamond outline with an X through it (same side=13 /
         q=3.4 proportions used to draw the mark below) — so the key matches
         the mark on the chart. */
      function trendGlyph(glyph, color) {
        if (glyph === "invalid") {
          var g = el("svg", {
            "class": "ghchart__glyph ghchart__glyph--invalid",
            viewBox: "0 0 22 22", width: "15", height: "15",
            style: "overflow:visible"
          });
          g.appendChild(el("rect", {
            x: 4.5, y: 4.5, width: 13, height: 13, rx: 1.5,
            transform: "rotate(45 11 11)", fill: "none",
            stroke: color, "stroke-width": 2
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 7.6, x2: 14.4, y2: 14.4,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          g.appendChild(el("line", {
            x1: 7.6, y1: 14.4, x2: 14.4, y2: 7.6,
            stroke: color, "stroke-width": 2, "stroke-linecap": "round"
          }));
          return g;
        }
        var gl = html("span", "ghchart__glyph ghchart__glyph--" + glyph);
        if (glyph === "line") { gl.style.borderTopColor = color; }
        else { gl.style.background = color; gl.style.borderColor = color; }
        if (glyph === "diamond") { gl.style.background = "transparent"; }
        return gl;
      }
      series.forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "square", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      (config.legendExtra || []).forEach(function (s) {
        var item = html("span", "ghchart__legend-item");
        item.appendChild(trendGlyph(s.glyph || "line", s.color));
        item.appendChild(document.createTextNode(s.name));
        legend.appendChild(item);
      });
      root.appendChild(legend);
    }

    /* ---- SVG plot ------------------------------------------------- */
    var svg = el("svg", {
      "class": "ghchart__plot ghchart__plot--trend",
      viewBox: "0 0 " + VB.w + " " + VB.h,
      preserveAspectRatio: "xMidYMid meet",
      role: "img",
      "aria-label": config.title
    });

    // Phase background columns + separators + labels (+ optional average).
    (config.phases || []).forEach(function (ph, i) {
      var x0 = xAt(ph.from), x1 = xAt(ph.to);
      svg.appendChild(el("rect", {
        "class": "ghchart__phase" + (i % 2 ? " ghchart__phase--alt" : ""),
        x: x0, y: M.top, width: Math.max(0, x1 - x0), height: innerH
      }));
      if (i > 0) {
        svg.appendChild(el("line", {
          "class": "ghchart__phase-sep", x1: x0, y1: M.top, x2: x0, y2: baseY
        }));
      }
      var pl = el("text", {
        "class": "ghchart__phase-label", x: (x0 + x1) / 2, y: M.top - 40,
        "text-anchor": "middle"
      });
      pl.textContent = ph.label;
      svg.appendChild(pl);
      if (ph.avg) {
        var pa = el("text", {
          "class": "ghchart__phase-avg", x: (x0 + x1) / 2, y: M.top + 24,
          "text-anchor": "middle", fill: cPoint
        });
        pa.textContent = ph.avg;
        svg.appendChild(pa);
      }
    });

    // Horizontal gridlines + y tick labels (skip a gridline at the reference y).
    var refY = config.reference ? config.reference.y : null;
    (ya.ticks || []).forEach(function (tv) {
      var gy = yAt(tv);
      if (refY === null || Math.abs(tv - refY) > 1e-9) {
        svg.appendChild(el("line", {
          "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy
        }));
      }
      var yl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--y",
        x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end"
      });
      yl.textContent = fmtY(tv);
      svg.appendChild(yl);
    });

    // X tick labels (dated).
    (xa.ticks || []).forEach(function (t) {
      var gx = xAt(t.at);
      var first = Math.abs(t.at - xa.min) < 1e-9, last = Math.abs(t.at - xa.max) < 1e-9;
      var xl = el("text", {
        "class": "ghchart__axislabel ghchart__axislabel--x",
        x: gx, y: baseY + 30, "text-anchor": first ? "start" : (last ? "end" : "middle")
      });
      xl.textContent = t.text;
      svg.appendChild(xl);
    });

    // Axis titles.
    if (ya.title) {
      var ytX = 22, ytY = M.top + innerH / 2;
      var yt = el("text", {
        "class": "ghchart__axistitle", x: ytX, y: ytY, "text-anchor": "middle",
        transform: "rotate(-90 " + ytX + " " + ytY + ")"
      });
      yt.textContent = ya.title;
      svg.appendChild(yt);
    }

    // Trend: optional ±band (linear only), then the dashed line + slope label.
    var tr = config.trend;
    if (tr) {
      function trendY(x) {
        return tr.from.y + (tr.to.y - tr.from.y) * (x - tr.from.x) / (tr.to.x - tr.from.x);
      }
      if (tr.band && !isLog) {
        var p = tr.band;
        var pts = [
          [xAt(tr.from.x), yAt(trendY(tr.from.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) + p)],
          [xAt(tr.to.x), yAt(trendY(tr.to.x) - p)],
          [xAt(tr.from.x), yAt(trendY(tr.from.x) - p)]
        ].map(function (a) { return a[0] + "," + a[1]; }).join(" ");
        svg.appendChild(el("polygon", {
          "class": "ghchart__trendband", points: pts, fill: cTrend
        }));
      }
      svg.appendChild(el("line", {
        "class": "ghchart__trendline",
        x1: xAt(tr.from.x), y1: yAt(tr.from.y),
        x2: xAt(tr.to.x), y2: yAt(tr.to.y), stroke: cTrend
      }));
      if (tr.label) {
        var lx, lyy;
        if (tr.labelAt) { lx = xAt(tr.labelAt.x); lyy = yAt(tr.labelAt.y); }
        else {
          lx = xAt(tr.to.x) + (tr.labelDX == null ? -6 : tr.labelDX);
          lyy = yAt(tr.to.y) + (tr.labelDY == null ? -12 : tr.labelDY);
        }
        var sl = el("text", {
          "class": "ghchart__slope", x: lx, y: lyy,
          "text-anchor": tr.labelAnchor || "end", fill: cTrend
        });
        sl.textContent = tr.label;
        svg.appendChild(sl);
      }
    }

    // Reference line (horizontal dashed) + right-aligned label.
    if (config.reference) {
      var rc = config.reference.color || "#24292f";
      var ryy = yAt(config.reference.y);
      svg.appendChild(el("line", {
        "class": "ghchart__refline", x1: M.left, y1: ryy, x2: VB.w - M.right, y2: ryy,
        stroke: rc
      }));
      if (config.reference.label) {
        var rl = el("text", {
          "class": "ghchart__reflabel", x: VB.w - M.right, y: ryy - 8,
          "text-anchor": "end", fill: rc
        });
        rl.textContent = config.reference.label;
        svg.appendChild(rl);
      }
    }

    // Data squares (values reveal on hover only).
    var side = 13, hSide = side / 2;
    var sqEls = [], hitEls = [];
    (config.points || []).forEach(function (pt, i) {
      var cx = xAt(pt.x), cy = yAt(pt.y);
      var sq = el("rect", {
        "class": "ghchart__square", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 2, fill: cPoint
      });
      svg.appendChild(sq);
      sqEls.push({ el: sq, cx: cx, cy: cy });

      // Hover/focus hit target — omitted when hover is disabled so the
      // (illustrative) point values never surface, on screen or to a11y.
      if (!config.disableHover) {
        var hit = el("rect", {
          "class": "ghchart__squarehit", x: cx - 13, y: cy - 13,
          width: 26, height: 26, tabindex: "0", role: "img",
          "aria-label": (pt.d || "") + " " + fmtY(pt.y)
        });
        svg.appendChild(hit);
        hitEls.push(hit);
      }
    });

    // Invalid-run markers: red diamond + X, leader line, annotation text.
    (config.invalids || []).forEach(function (iv) {
      var cx = xAt(iv.x), cy = yAt(iv.y);
      svg.appendChild(el("rect", {
        "class": "ghchart__invalid", x: cx - hSide, y: cy - hSide,
        width: side, height: side, rx: 1.5, fill: "none", stroke: cInvalid,
        transform: "rotate(45 " + cx + " " + cy + ")"
      }));
      var q = 3.4;
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy - q, x2: cx + q, y2: cy + q,
        stroke: cInvalid
      }));
      svg.appendChild(el("line", {
        "class": "ghchart__invalid-x", x1: cx - q, y1: cy + q, x2: cx + q, y2: cy - q,
        stroke: cInvalid
      }));
      if (iv.title) {
        var tx = cx + (iv.labelDX == null ? 16 : iv.labelDX);
        var ty = cy + (iv.labelDY == null ? -26 : iv.labelDY);
        svg.appendChild(el("line", {
          "class": "ghchart__leader", x1: cx, y1: cy, x2: tx, y2: ty, stroke: cInvalid
        }));
        var anchor = iv.labelAnchor || "start";
        var at = el("text", {
          "class": "ghchart__anno-title", x: tx, y: ty - 4, "text-anchor": anchor, fill: cInvalid
        });
        at.textContent = iv.title;
        svg.appendChild(at);
        if (iv.sub) {
          var asu = el("text", {
            "class": "ghchart__anno-sub", x: tx, y: ty + 11, "text-anchor": anchor
          });
          asu.textContent = iv.sub;
          svg.appendChild(asu);
        }
      }
    });

    // Bracket annotation (span with end ticks + centered label above).
    (config.brackets || []).forEach(function (b) {
      var x0 = xAt(b.fromX), x1 = xAt(b.toX), by = yAt(b.y);
      svg.appendChild(el("line", {
        "class": "ghchart__bracket", x1: x0, y1: by, x2: x1, y2: by
      }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x0, y1: by, x2: x0, y2: by + 7 }));
      svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x1, y1: by, x2: x1, y2: by + 7 }));
      var bl = el("text", {
        "class": "ghchart__bracket-label", x: (x0 + x1) / 2, y: by - 8, "text-anchor": "middle"
      });
      bl.textContent = b.text;
      svg.appendChild(bl);
    });

    // Final callout: ringed point + leader + title/sub placed inside the plot.
    if (config.callout) {
      var co = config.callout;
      var ccx = xAt(co.x), ccy = yAt(co.y);
      svg.appendChild(el("circle", {
        "class": "ghchart__callout-ring", cx: ccx, cy: ccy, r: 12, stroke: cPoint
      }));
      var ctx = ccx + (co.labelDX == null ? -16 : co.labelDX);
      var cty = ccy + (co.labelDY == null ? -30 : co.labelDY);
      svg.appendChild(el("line", {
        "class": "ghchart__leader ghchart__leader--callout", x1: ccx, y1: ccy, x2: ctx, y2: cty,
        stroke: cPoint
      }));
      var can = co.labelAnchor || "end";
      var ct = el("text", {
        "class": "ghchart__anno-title ghchart__anno-title--callout", x: ctx, y: cty - 4, "text-anchor": can, fill: cPoint
      });
      ct.textContent = co.title;
      svg.appendChild(ct);
      if (co.sub) {
        var cs = el("text", {
          "class": "ghchart__anno-sub ghchart__anno-sub--callout", x: ctx, y: cty + 12, "text-anchor": can
        });
        cs.textContent = co.sub;
        svg.appendChild(cs);
      }
    }

    root.appendChild(svg);

    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.textContent = config.caption;
      root.appendChild(cap);
    }

    /* ---- Tooltip + hover interaction (skipped when disabled) ------ */
    if (!config.disableHover) {
    var tip = html("div", "ghchart__tooltip");
    tip.setAttribute("role", "status");
    var tipTitle = html("div", "ghchart__tt-title");
    var tipBody = html("div", "ghchart__tt-body");
    tip.appendChild(tipTitle);
    tip.appendChild(tipBody);
    root.appendChild(tip);

    var activeIndex = -1;
    function show(i) {
      var pt = config.points[i];
      if (!pt) return;
      var reappearing = activeIndex === -1;
      activeIndex = i;

      sqEls.forEach(function (s, si) {
        var big = si === i;
        var w = big ? side + 5 : side;
        s.el.setAttribute("width", w);
        s.el.setAttribute("height", w);
        s.el.setAttribute("x", s.cx - w / 2);
        s.el.setAttribute("y", s.cy - w / 2);
      });

      var tipInfo = config.pointTip
        ? config.pointTip(pt)
        : { title: pt.d || "", line: fmtY(pt.y) };
      tipTitle.textContent = tipInfo.title;
      tipBody.textContent = "";
      var line = html("div", "ghchart__tt-line");
      line.textContent = tipInfo.line;
      tipBody.appendChild(line);

      var anchor = sqEls[i].el.getBoundingClientRect();
      var rootRect = root.getBoundingClientRect();
      var left = anchor.left + anchor.width / 2 - rootRect.left;
      var top = anchor.top - rootRect.top - 12;

      var suppress = reappearing && !reduceMotion();
      if (suppress) tip.style.transition = "opacity 0.12s ease";

      tip.setAttribute("data-visible", "true");
      var halfW = tip.offsetWidth / 2;
      left = clamp(left, halfW + 2, root.clientWidth - halfW - 2);
      if (top - tip.offsetHeight < 0) {
        top = anchor.bottom - rootRect.top + 12;
        tip.style.transform = "translate(-50%, 0)";
      } else {
        tip.style.transform = "translate(-50%, -100%)";
      }
      tip.style.left = left + "px";
      tip.style.top = top + "px";
      if (suppress) { void tip.offsetWidth; tip.style.transition = ""; }
    }
    function hide() {
      activeIndex = -1;
      tip.setAttribute("data-visible", "false");
      sqEls.forEach(function (s) {
        s.el.setAttribute("width", side);
        s.el.setAttribute("height", side);
        s.el.setAttribute("x", s.cx - hSide);
        s.el.setAttribute("y", s.cy - hSide);
      });
    }

    hitEls.forEach(function (hit, i) {
      hit.addEventListener("pointerenter", function () { show(i); });
      hit.addEventListener("pointermove", function () { show(i); });
      hit.addEventListener("pointerleave", hide);
      hit.addEventListener("focus", function () { show(i); });
      hit.addEventListener("blur", hide);
    });
    }
  };

  /* ================================================================
     Distribution dashboard renderer
     ----------------------------------------------------------------
     A composition figure (no hover — every value is printed as text, so
     the marks are inherently screen-reader accessible):
       • a compact narrative headline + muted subtitle;
       • an optional segmented "mix" bar — proportional colored segments,
         each with its name inside and count/percent below;
       • one or more panels of ranked horizontal bars, each row printing
         its category (left), a bar, and its count/percent (right).
     Colors come from config.series so the harness can tune them live:
       series[0]   = bar fill (single base hex);
       series[1..] = segment fills, in the order of config.segments.items.
     ================================================================ */
  renderers.distribution = function (root, config) {
    root.textContent = "";
    root.setAttribute("role", "img");
    if (config.title) root.setAttribute("aria-label", config.title);

    var series = config.series || [];
    var barColor = (series[0] && series[0].color) || "#8250df";
    var total = config.total ||
      (config.segments && config.segments.items
        ? config.segments.items.reduce(function (a, b) { return a + b.value; }, 0)
        : 0);

    function pctText(v) {
      if (!total) return "";
      return (Math.round((v / total) * 1000) / 10).toFixed(1) + "%";
    }
    function valueHTML(v) {
      var p = pctText(v);
      return "<b>" + v + "</b>" + (p ? " (" + p + ")" : "");
    }

    /* ---- Headline + subtitle ------------------------------------- */
    if (config.title) {
      var head = html("div", "ghchart__headline");
      head.textContent = config.title;
      root.appendChild(head);
    }
    if (config.subtitle) {
      var sub = html("p", "ghchart__subtitle");
      sub.textContent = config.subtitle;
      root.appendChild(sub);
    }

    /* ---- Segmented "mix" bar -------------------------------------- */
    var seg = config.segments;
    if (seg && seg.items && seg.items.length) {
      var segSec = html("div", "ghchart__dist-section");
      if (seg.title) {
        var segTitle = html("div", "ghchart__panel-title");
        segTitle.textContent = seg.title;
        segSec.appendChild(segTitle);
      }
      var segTotal = seg.items.reduce(function (a, b) { return a + b.value; }, 0) || 1;
      var bar = html("div", "ghchart__seg");
      var notes = html("div", "ghchart__seg-note-row");
      seg.items.forEach(function (it, i) {
        var basis = (it.value / segTotal) * 100;
        var col = (series[i + 1] && series[i + 1].color) || it.color || barColor;

        var cell = html("div", "ghchart__seg-cell");
        cell.style.flex = "0 0 " + basis + "%";
        cell.style.background = col;
        cell.textContent = it.label;
        bar.appendChild(cell);

        var note = html("div", "ghchart__seg-note");
        note.style.flex = "0 0 " + basis + "%";
        note.innerHTML = valueHTML(it.value);
        notes.appendChild(note);
      });
      segSec.appendChild(bar);
      segSec.appendChild(notes);
      root.appendChild(segSec);
    }

    /* ---- Ranked bar panels (grid: 2-up, stacking on narrow) ------ */
    var panels = config.panels || [];
    if (panels.length) {
      var cols = html("div", "ghchart__dist-cols");
      if (config.columns === 1 || panels.length === 1) {
        cols.classList.add("ghchart__dist-cols--stack");
      }
      panels.forEach(function (panel) {
        var sec = html("div", "ghchart__dist-section");
        if (panel.title) {
          var pt = html("div", "ghchart__panel-title");
          pt.textContent = panel.title;
          sec.appendChild(pt);
        }
        var list = html("div", "ghchart__dist-list");
        var pmax = (panel.items || []).reduce(function (m, it) {
          return it.value > m ? it.value : m;
        }, 0) || 1;
        (panel.items || []).forEach(function (it) {
          var row = html("div", "ghchart__dist-row");

          var cat = html("div", "ghchart__dist-cat");
          cat.textContent = it.label;
          row.appendChild(cat);

          var track = html("div", "ghchart__dist-track");
          var b = html("div", "ghchart__dist-bar");
          b.style.width = Math.max((it.value / pmax) * 100, 1.2) + "%";
          b.style.background = it.color || barColor;
          track.appendChild(b);
          row.appendChild(track);

          var val = html("div", "ghchart__dist-val");
          val.innerHTML = valueHTML(it.value);
          row.appendChild(val);

          list.appendChild(row);
        });
        sec.appendChild(list);
        cols.appendChild(sec);
      });
      root.appendChild(cols);
    }

    /* ---- Caption / footnote -------------------------------------- */
    if (config.caption) {
      var cap = html("div", "ghchart__caption");
      cap.innerHTML = config.caption;
      root.appendChild(cap);
    }
  };

  /* ================================================================
     Dispatcher — pick a renderer by config.type and draw into a mount
     ================================================================ */
  function render(config, mountEl) {
    var root = mountEl || document.getElementById(config.mountId);
    if (!root) return;
    root.classList.add("ghchart");
    /* Resolve the renderer from the SHARED registry (see registration below),
       not this bundle's private `renderers`. When several exported charts share
       one page they all merge into one registry, so every chart finds its
       renderer even if an older engine bundle loaded first. */
    var reg = (global.GHChartEngine && global.GHChartEngine.renderers) || renderers;
    var fn = reg[config.type] || renderers[config.type] || reg.line || renderers.line;
    fn(root, config);
  }

  /* ----------------------------------------------------------------
     Shared-engine registration (multi-embed safe)
     ----------------------------------------------------------------
     Every exported chart.js embeds this whole engine. When two or more
     charts are placed on the same page (e.g. a WordPress post), we must NOT
     let the first-loaded bundle silently win — an older bundle would be
     missing renderers added later (this is why a heatmap pasted after an
     older bar chart used to vanish).

     Instead we keep ONE shared engine:
       • every bundle merges in any renderers the shared registry lacks;
       • the newest bundle (highest ENGINE_VERSION) provides render(), which
         reads renderers from the shared registry at call time.
     The result is order-independent and resilient to version skew as new
     chart types are added. Bump ENGINE_VERSION whenever renderers or render
     change so a newer embed upgrades an older shared engine in place. */
  var ENGINE_VERSION = 9;
  var existing = global.GHChartEngine;
  if (existing && typeof existing.version === "number") {
    if (ENGINE_VERSION > existing.version) {
      /* This bundle is newer. Every export embeds the COMPLETE engine, so this
         bundle's `renderers` is a self-contained, up-to-date set. Adopt it as
         the shared registry (upgrading older same-named renderers in place),
         while defensively preserving any renderer types only the older engine
         happened to have. Then take over render(). */
      for (var ek in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ek) && !renderers[ek]) {
          renderers[ek] = existing.renderers[ek];
        }
      }
      existing.renderers = renderers;
      existing.render = render;
      existing.version = ENGINE_VERSION;
    } else {
      /* Same or older version already present: only contribute renderer types
         the shared registry is missing (don't clobber an equal/newer engine). */
      for (var rk in renderers) {
        if (Object.prototype.hasOwnProperty.call(renderers, rk) && !existing.renderers[rk]) {
          existing.renderers[rk] = renderers[rk];
        }
      }
    }
  } else {
    /* No engine yet, or a pre-versioning (old-style) engine loaded first.
       Take over, preserving any renderers the old-style engine registered. */
    if (existing && existing.renderers) {
      for (var ok in existing.renderers) {
        if (Object.prototype.hasOwnProperty.call(existing.renderers, ok) && !renderers[ok]) {
          renderers[ok] = existing.renderers[ok];
        }
      }
    }
    global.GHChartEngine = { renderers: renderers, render: render, version: ENGINE_VERSION };
  }
})(typeof window !== "undefined" ? window : this);

;(function () {
  "use strict";
  var CONFIG = {
    mountId: "gh-chart-hf-completion",
    type: "trend",
    disableHover: true,
    title: "HydraFusion recovered from two operational breaks and continued climbing on August 25 to 91.8% best-of-2 equivalent.",
    subtitle: "28 August 25 completion points. Filled HydraFusion squares use the same encoding from first observation to last, and the robust trend includes the final result.",
    series: [
      {
        name: "HydraFusion",
        color: "#8250df",
        glyph: "square"
      },
      {
        name: "Harness-invalid run",
        color: "#cf222e",
        glyph: "invalid"
      }
    ],
    legendExtra: [
      {
        name: "Robust trend",
        glyph: "line",
        color: "#8250df"
      }
    ],
    xAxis: {
      min: 11,
      max: 26.15,
      ticks: [
        {
          at: 11,
          text: "Aug 11"
        },
        {
          at: 13,
          text: "Aug 13"
        },
        {
          at: 15,
          text: "Aug 15"
        },
        {
          at: 17,
          text: "Aug 17"
        },
        {
          at: 19,
          text: "Aug 19"
        },
        {
          at: 21,
          text: "Aug 21"
        },
        {
          at: 23,
          text: "Aug 23"
        },
        {
          at: 25,
          text: "Aug 25"
        }
      ]
    },
    yAxis: {
      scale: "linear",
      min: 0,
      max: 89,
      ticks: [0, 20, 40, 60, 77, 89],
      title: "TASKS RESOLVED · OF 89",
      format: function (v) { return String(v); }
    },
    phases: [
      {
        from: 11,
        to: 18,
        label: "REPAIR"
      },
      {
        from: 18,
        to: 22,
        label: "REVIEW"
      },
      {
        from: 22,
        to: 26.15,
        label: "RELEASE"
      }
    ],
    reference: {
      y: 77,
      color: "#24292f"
    },
    trend: {
      from: {
        x: 11,
        y: 64.1
      },
      to: {
        x: 26.15,
        y: 74.7
      },
      band: 7.3,
      label: "+0.7 tasks/day",
      labelAnchor: "middle",
      labelAt: {
        x: 25.3,
        y: 69
      }
    },
    brackets: [
      {
        fromX: 19.4,
        toX: 20.9,
        y: 83,
        text: "11 RERUNS · ±7.3-TASK NOISE"
      }
    ],
    invalids: [
      {
        x: 11.33,
        y: 0,
        title: "AUG 11 · FALLBACK FAILURE",
        sub: "0/89 · 89/89 AGENT ERRORS",
        labelDX: 28,
        labelDY: -64,
        labelAnchor: "start"
      },
      {
        x: 14.07,
        y: 40,
        title: "AUG 14 · SECOND HARNESS BREAK",
        labelDX: 26,
        labelDY: -46,
        labelAnchor: "start"
      }
    ],
    points: [
      {
        x: 11.06,
        y: 67
      },
      {
        x: 11.68,
        y: 75
      },
      {
        x: 13.22,
        y: 70
      },
      {
        x: 13.81,
        y: 73
      },
      {
        x: 14.18,
        y: 67
      },
      {
        x: 18.09,
        y: 76
      },
      {
        x: 18.53,
        y: 66
      },
      {
        x: 18.86,
        y: 67
      },
      {
        x: 19.44,
        y: 72
      },
      {
        x: 19.58,
        y: 65
      },
      {
        x: 19.58,
        y: 70
      },
      {
        x: 19.71,
        y: 64
      },
      {
        x: 19.85,
        y: 69
      },
      {
        x: 19.99,
        y: 67
      },
      {
        x: 20.12,
        y: 69
      },
      {
        x: 20.25,
        y: 73
      },
      {
        x: 20.39,
        y: 72
      },
      {
        x: 20.52,
        y: 66
      },
      {
        x: 20.66,
        y: 67
      },
      {
        x: 20.83,
        y: 71
      },
      {
        x: 20.97,
        y: 72
      },
      {
        x: 22.31,
        y: 76
      },
      {
        x: 23.44,
        y: 69
      },
      {
        x: 24.34,
        y: 74
      },
      {
        x: 25.56,
        y: 77
      },
      {
        x: 26.02,
        y: 82
      }
    ],
    pointTip: function (pt) {
        return { title: "Aug " + Math.round(pt.x), line: pt.y + " of 89 tasks" };
      }
  };
  function boot() {
    var root = document.getElementById(CONFIG.mountId);
    if (!root || root.getAttribute("data-gh-rendered")) return;
    root.setAttribute("data-gh-rendered", "1");
    window.GHChartEngine.render(CONFIG);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();

</script>

<div id="gh-chart-hf-completion" class="ghchart"></div>



<p class="wp-block-paragraph">This development record shows how the policies improved from repeated experiments. TerminalBench 2.1 was one of several benchmarks used during development. Its relative saturation makes broader validation important, so the three-benchmark evaluation also includes DeepSWE&rsquo;s more demanding repository-level tasks. The research preview extends that learning loop to real developer workloads.</p>



<h2 id="h-try-the-research-preview" class="wp-block-heading">Try the research preview</h2>



<p class="wp-block-paragraph">For this preview, first-turn, single-prompt coding tasks are the best place to start. We&rsquo;ll be focusing on strong multi-turn performance with longer, iterative sessions next.</p>



<p class="wp-block-paragraph">This preview is designed to learn which tasks benefit from compound workflows and how orchestration affects latency and cost in practice. For the best experience today, start with substantial, well-scoped coding tasks that you can hand to Copilot in autopilot mode in a single prompt. Share what you find, including where it excels, where it falls short, and what you&rsquo;d want to see next, through <code>/feedback</code> in Copilot CLI or in the <a href="https://github.com/orgs/community/discussions/206492">GitHub Community discussion</a>.</p>



<p class="wp-block-paragraph">HydraFusion remains an active research effort. Results, models, workflows, availability, names, and product behavior may change as we learn from the preview. We believe the next real gain in coding agents will come from combining frontier intelligence with runtime orchestration. HydraFusion is our first bet on that idea: moving from choosing the best model to dynamically constructing the best way to solve each task.</p>



<h2 id="h-acknowledgments" class="wp-block-heading">Acknowledgments</h2>



<p class="wp-block-paragraph">A huge thank-you to the researchers, engineers, product managers, and designers across GitHub and Microsoft who curated the training data and built the training pipeline, evaluation suites, client experience, and serving stack. We are especially grateful to the GitHub Copilot CLI, Copilot API and VS Code team for overcoming numerous challenges to bring this research preview to our customers.&nbsp;</p>



<h2 id="h-meet-the-team" class="wp-block-heading">Meet the Team</h2>



<p class="wp-block-paragraph"><img data-recalc-dims="1" loading="lazy" decoding="async" width="150" height="150" class="wp-image-98620" style="width: 150px;" src="https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?resize=150%2C150" alt="" srcset="https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?w=460 460w, https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?w=150 150w, https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?w=300 300w, https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?w=400 400w, https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?w=200 200w, https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?w=90 90w, https://github.blog/wp-content/uploads/2026/09/2555061.jpeg?w=116 116w" sizes="auto, (max-width: 150px) 100vw, 150px" /> <a href="https://github.com/aashna">Aashna Garg</a>, Principal Applied Scientist, Code AI</p>



<p class="wp-block-paragraph"><img data-recalc-dims="1" loading="lazy" decoding="async" width="150" height="150" class="wp-image-98621" style="width: 150px;" src="https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?resize=150%2C150" alt="" srcset="https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?w=460 460w, https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?w=150 150w, https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?w=300 300w, https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?w=400 400w, https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?w=200 200w, https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?w=90 90w, https://github.blog/wp-content/uploads/2026/09/23369389.jpeg?w=116 116w" sizes="auto, (max-width: 150px) 100vw, 150px" /> <a href="https://github.com/shengyfu">Shengyu Fu</a>, Partner Applied Science Manager, Code AI</p>



<p class="wp-block-paragraph"><img data-recalc-dims="1" loading="lazy" decoding="async" width="150" height="150" class="wp-image-98619" style="width: 150px;" src="https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?resize=150%2C150" alt="" srcset="https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?w=460 460w, https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?w=150 150w, https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?w=300 300w, https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?w=400 400w, https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?w=200 200w, https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?w=90 90w, https://github.blog/wp-content/uploads/2026/09/21318528.jpeg?w=116 116w" sizes="auto, (max-width: 150px) 100vw, 150px" /> <a href="https://github.com/carlosscastro">Carlos Castro</a>, Partner Architect, GitHub Copilot</p>



<p class="wp-block-paragraph"><img data-recalc-dims="1" loading="lazy" decoding="async" width="150" height="150" class="wp-image-98681" style="width: 150px;" src="https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?resize=150%2C150" alt="" srcset="https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?w=460 460w, https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?w=150 150w, https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?w=300 300w, https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?w=400 400w, https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?w=200 200w, https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?w=90 90w, https://github.blog/wp-content/uploads/2026/09/42297087.jpeg?w=116 116w" sizes="auto, (max-width: 150px) 100vw, 150px" /><a href="https://github.com/sid200026"> Siddharth Singha Roy</a>, Research Scientist II, Code AI&nbsp;</p>



<p class="wp-block-paragraph"><img data-recalc-dims="1" loading="lazy" decoding="async" width="150" height="150" class="wp-image-98643" style="width: 150px;" src="https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?resize=150%2C150" alt="" srcset="https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?w=460 460w, https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?w=150 150w, https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?w=300 300w, https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?w=400 400w, https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?w=200 200w, https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?w=90 90w, https://github.blog/wp-content/uploads/2026/09/4208418.jpeg?w=116 116w" sizes="auto, (max-width: 150px) 100vw, 150px" /> <a href="https://github.com/andysalerno">Andy Salerno</a>, Principal Software Engineer, GitHub Copilot</p>



<p class="wp-block-paragraph"></p>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/project-hydrafusion-frontier-quality-via-multi-model-orchestration/">Project HydraFusion: Frontier quality via multi-model orchestration</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98615</post-id>	</item>
		<item>
		<title>GitHub Copilot app for Beginners: Run several agents at once</title>
		<link>https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-run-several-agents-at-once/</link>
		
		<dc:creator><![CDATA[Kayla Cinnamon]]></dc:creator>
		<pubDate>Thu, 03 Sep 2026 16:00:00 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[GitHub Copilot app]]></category>
		<category><![CDATA[GitHub Copilot app for Beginners]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98638</guid>

					<description><![CDATA[<p>Learn how to run parallel agents in the GitHub Copilot app, and experience the moment it stops feeling scary and starts feeling powerful.</p>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-run-several-agents-at-once/">GitHub Copilot app for Beginners: Run several agents at once</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">Running multiple AI agents on the same project seems like pure chaos, with too many cooks in your development kitchen. But with the <a href="https://github.com/features/ai/github-app?utm_source=blog-copilot-app-ep5&amp;utm_medium=blog&amp;utm_campaign=copilot-app-for-beginners-series">GitHub Copilot app</a>, these agents work separately and don&rsquo;t interfere with each other, allowing you to get more done in less time.</p>



<p class="wp-block-paragraph">Think of parallel agent sessions like a trip to the laundromat. You can start multiple loads of laundry at the same time, in their own machines. You can set each washer with its own settings, and it won&rsquo;t impact the other loads. Most importantly, you don&rsquo;t have to wait for one to finish before you can start another one.</p>



<p class="wp-block-paragraph">Let&rsquo;s look at how it works.</p>



<h2 id="h-agent-sessions-git-worktrees-and-context" class="wp-block-heading">Agent sessions, Git worktrees, and context</h2>



<p class="wp-block-paragraph">An agent session is a task you&rsquo;ve given Copilot, start to finish.</p>



<p class="wp-block-paragraph">In the GitHub Copilot app, you can manage several sessions through the sessions view. Each card shows its title and how far along the agent is in completing its task.</p>



<p class="wp-block-paragraph">Each session runs independently of the others. That means you can start a new session whenever you want, and the ones already running won&rsquo;t be disturbed.</p>



<p class="wp-block-paragraph">But here&rsquo;s where the real magic happens. Each agent session in the GitHub Copilot app can run on its own Git worktree. Since each session is isolated, they can run in parallel, all at the same time.</p>



<p class="wp-block-paragraph">That means you spend your time reviewing and making decisions rather than watching your agents at work.</p>



<p class="wp-block-paragraph">Plus, each session keeps its own context, so you can switch between them freely. Each picks up exactly where you left it, and you never have to re-explain what you were doing. Less context switching means you feel less scattered.</p>



<h2 id="h-what-this-looks-like" class="wp-block-heading">What this looks like</h2>



<p class="wp-block-paragraph">Let&rsquo;s look at an example repository, <code>tailspin-toys</code>. There are three things I want to do to this project today: add funded sort, perform an accessibility review, and run some tests on this project.</p>



<p class="wp-block-paragraph">The first step is to ask Copilot to build the funded sort feature. Just as it gets started, open a new session and ask Copilot to perform an accessibility review. As that gets under way, start prompting Copilot to run some tests in a third session.</p>



<p class="wp-block-paragraph">In the session view, you can keep track of all of these as they progress and review the results as they finish. Or you can step away entirely, grab a cup of coffee, and know your work is happening without you babysitting it.</p>



<h2 id="h-get-started" class="wp-block-heading">Get started</h2>



<p class="wp-block-paragraph">Ready to feel the power of parallel agent sessions?</p>



<p class="wp-block-paragraph">Try starting two small tasks at the same time in the <a href="https://github.com/features/ai/github-app?utm_source=blog-copilot-app-ep5&amp;utm_medium=blog&amp;utm_campaign=copilot-app-for-beginners-series">GitHub Copilot app</a>. It&rsquo;s a low-stakes way to watch your tasks progress independently.</p>



<div class="wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph">Start using the <a href="https://github.com/features/ai/github-app?utm_source=blog-copilot-app-ep5&amp;utm_medium=blog&amp;utm_campaign=copilot-app-for-beginners-series">GitHub Copilot app &gt;</a></p>
</div>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-run-several-agents-at-once/">GitHub Copilot app for Beginners: Run several agents at once</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98638</post-id>	</item>
		<item>
		<title>Decoding the new AI lingo: Loops, harnesses, squads, hill climbing&#8230; oh my!</title>
		<link>https://github.blog/ai-and-ml/decoding-the-new-ai-lingo-loops-harnesses-squads-hill-climbing-oh-my/</link>
		
		<dc:creator><![CDATA[Cassidy Williams]]></dc:creator>
		<pubDate>Wed, 02 Sep 2026 21:00:00 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[agentic AI]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[GitHub Podcast]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98631</guid>

					<description><![CDATA[<p>From loop engineering to harnesses, squads, and open weights, the GitHub Podcast breaks down the AI terms showing up in developer conversations.</p>
<p>The post <a href="https://github.blog/ai-and-ml/decoding-the-new-ai-lingo-loops-harnesses-squads-hill-climbing-oh-my/">Decoding the new AI lingo: Loops, harnesses, squads, hill climbing&#8230; oh my!</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">It might be overwhelming to see all of the new vocabulary popping up in software development these days thanks to AI tools introducing them&hellip; all the time.</p>



<p class="wp-block-paragraph">Some of this new vocab describes useful patterns that people are newly pursuing, others are just fancy names on top of things that already exist, and some are still actively being defined as we speak.</p>



<p class="wp-block-paragraph">In our latest episode of the <a href="https://gh.io/podcast">GitHub Podcast</a>, Marlene Mhangami, GPS, and I talked through some of the AI terms developers are learning right now: loop engineering, Ralph loops, squads, harness engineering, hill climbing, forward deployed engineers, closed models, open weights, and open source models.</p>



<p class="wp-block-paragraph">If you&rsquo;re a reader instead of a listener, here&rsquo;s a guide to what those terms mean, why they matter, and how to think about them.</p>



<p class="wp-block-paragraph"><em>Listen to the full episode below! &#128071;</em></p>



<iframe loading="lazy" height="200px" width="100%" frameborder="no" scrolling="no" seamless src="https://player.simplecast.com/4a915475-d195-451a-84e1-d466efda6749?dark=false"></iframe>



<h2 id="h-loop-engineering-moving-beyond-one-shot-prompts" class="wp-block-heading">Loop engineering: Moving beyond one-shot prompts</h2>



<p class="wp-block-paragraph">Loop engineering is the practice of designing repeatable systems around agents, instead of manually prompting them for one task at a time.</p>



<p class="wp-block-paragraph">A simple example: instead of asking an agent every morning to review new issues, summarize them, and propose fixes, you create a loop that runs on a schedule. That loop might fetch issues, pass them to an agent, validate the output, and escalate anything that gets stuck. It&rsquo;s a glorified AI-native cron job.</p>



<h2 id="h-ralph-loops-the-brute-force-cousin-of-loop-engineering" class="wp-block-heading">Ralph loops: The brute-force cousin of loop engineering</h2>



<p class="wp-block-paragraph">A Ralph loop is one implementation of this &ldquo;loop&rdquo; concept: you give an agent a detailed task, often from a product requirements document or spec, and have it keep working until the job is done.</p>



<p class="wp-block-paragraph">That can be useful, especially for breaking down large tasks into repeated plan-act-check cycles. But, on the other hand, it can also be expensive and inefficient because every iteration uses more tokens, more context, and more compute.</p>



<p class="wp-block-paragraph">Loop engineering aims to make this pattern more structured, so you&rsquo;re not caught asking an agent to &ldquo;try again&rdquo; all the time. A well-designed loop adds primitives like skills, observability, validation, routing, and checkpoints.</p>



<h2 id="h-squads-fleets-and-multi-agent-workflows" class="wp-block-heading">Squads, fleets, and multi-agent workflows</h2>



<p class="wp-block-paragraph">If loops define a workflow, &ldquo;squads&rdquo; and &ldquo;fleets&rdquo; describe how multiple agents can participate in that workflow.</p>



<p class="wp-block-paragraph">A squad is a group of agents with different roles. They often reflect a real-world team. One agent might plan, another agent might vet that plan, another agent might implement it, another might test it, and another might review it.</p>



<p class="wp-block-paragraph">A fleet refers to parallel agents working on tasks at the same time. You can have a squad working in a fleet in parallel, or in a sequence.</p>



<p class="wp-block-paragraph">Operating this way lets different agents handle different parts of a process, and you can fine-tune and specialize each one with specific skills to be more efficient.</p>



<p class="wp-block-paragraph">The core idea is parallelization and specialization. Instead of one agent trying to do everything, different agents can handle different parts of a development process.</p>



<h2 id="h-harnesses-the-system-around-the-model" class="wp-block-heading">Harnesses: The system around the model</h2>



<p class="wp-block-paragraph">Outside of what a model generates, a harness is everything surrounding it that makes it useful in your workflows.</p>



<p class="wp-block-paragraph">That could be the tools, permissions, memory, context, orchestration (and so on) that guides how the model behaves. If it helps you remember: harnesses are aptly named after the harnesses for horses. Horses are like models that can run wild, and a harness helps direct the horse&rsquo;s weight safely as it completes tasks. Get it?</p>



<p class="wp-block-paragraph">Anyway, a good example of a software harness is GitHub Copilot. It connects models to codebases, editors, pull requests, terminals, and so on.</p>



<p class="wp-block-paragraph">When you hear the term &ldquo;harness engineering&rdquo; tossed around, that&rsquo;s the work of designing and improving that system that surrounds the models.</p>



<h2 id="h-hill-climbing-improving-agents-with-feedback" class="wp-block-heading">Hill climbing: Improving agents with feedback</h2>



<p class="wp-block-paragraph">The term &ldquo;hill climbing&rdquo; is used to describe the process of improving agents and harnesses over time.</p>



<p class="wp-block-paragraph">That could mean, for example, using evals to measure whether an agent is producing the right kind of output (and then adjusting the harnesses until the results improve).</p>



<p class="wp-block-paragraph">Or, another example, if your agent is supposed to review pull requests, hill climbing might be checking if it indeed finds meaningful bugs and produces useful recommendations, and adjusting tooling to improve that.</p>



<h2 id="h-forward-deployed-engineer-a-familiar-role-with-an-ai-focus" class="wp-block-heading">Forward deployed engineer: A familiar role with an AI focus</h2>



<p class="wp-block-paragraph">A forward-deployed engineer job has already existed, but AI branding makes it sound edgy and new. Now, it&rsquo;s a customer-facing software engineer, or sales engineer, or solutions engineer, often with an AI focus.</p>



<p class="wp-block-paragraph">If you haven&rsquo;t seen those job titles before, this person generally works closely with customers to implement or adapt technical solutions into their environments. With the AI focus, that means helping teams integrate AI tools, workflows, agents, etc. into their existing systems.</p>



<h2 id="h-closed-models-open-weights-and-open-source-models" class="wp-block-heading">Closed models, open weights, and open source models</h2>



<p class="wp-block-paragraph">Not all models are shared in the same way.</p>



<p class="wp-block-paragraph">Closed models are accessed through an API or hosted product. Developers can use the model, but they don&rsquo;t get access to the underlying weights, training data, or training process. The big, famous frontier models you hear about are often all closed models.</p>



<p class="wp-block-paragraph">Open weight models make the model weights (which are like dials that decide how important certain inputs are) available. Developers can download and run these models, often locally or in their own infrastructure. But, to be clear, the dataset and training method may not be fully available.</p>



<p class="wp-block-paragraph">Open source models go a step further, in that the model, code, data, and training process are all available for inspection, reuse, and modification.</p>



<p class="wp-block-paragraph">The more open the model, the more you can run, customize, audit, and trust it.</p>



<h2 id="h-the-terms-are-ever-evolving" class="wp-block-heading">The terms are ever-evolving</h2>



<p class="wp-block-paragraph">This is just a sampler of some of the terms we&rsquo;re hearing a lot today. Some will stick around, and others will fade into our memories, and others will be replaced by better language as the industry matures.</p>



<p class="wp-block-paragraph">Don&rsquo;t worry about falling behind on buzzwords. They&rsquo;re just words, and more important are the practices under them! Ask yourself if workflows can repeat reliably, how you validate tasks, how humans should (or shouldn&rsquo;t) interfere, how much you can rely on a model, and how you can improve that your system. It&rsquo;s a new era of engineering, and best practices still matter!</p>



<div class="wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph">Subscribe to the <a href="https://gh.io/podcast">GitHub Podcast</a> so you never miss an episode!</p>
</div>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/decoding-the-new-ai-lingo-loops-harnesses-squads-hill-climbing-oh-my/">Decoding the new AI lingo: Loops, harnesses, squads, hill climbing&#8230; oh my!</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98631</post-id>	</item>
		<item>
		<title>How we make AI coding more cost efficient without sacrificing task quality</title>
		<link>https://github.blog/ai-and-ml/github-copilot/how-we-make-ai-coding-more-cost-efficient-without-sacrificing-task-quality/</link>
		
		<dc:creator><![CDATA[Erik Kristensen]]></dc:creator>
		<pubDate>Wed, 02 Sep 2026 18:00:00 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[Architecture & optimization]]></category>
		<category><![CDATA[Engineering]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[agentic AI]]></category>
		<category><![CDATA[GitHub Copilot CLI]]></category>
		<category><![CDATA[GitHub Copilot code review]]></category>
		<category><![CDATA[LLMs]]></category>
		<category><![CDATA[prompt engineering]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98586</guid>

					<description><![CDATA[<p>Why shorter outputs can cost more, and how GitHub Copilot reduces wasted work across the complete coding task.</p>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/how-we-make-ai-coding-more-cost-efficient-without-sacrificing-task-quality/">How we make AI coding more cost efficient without sacrificing task quality</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">Output quality is important when working with AI coding agents, but true efficiency comes from getting work done quickly, efficiently, and with the right context.</p>



<p class="wp-block-paragraph">That&rsquo;s why token count of individual interactions alone isn&rsquo;t a meaningful measure of efficiency. The goal shouldn&rsquo;t be to use fewer tokens, but to tap into the right amount of context to move a task forward. A concise tool response can sometimes require additional calls or work if it leaves out information the agent needs, ultimately making the task slower and more expensive.</p>



<p class="wp-block-paragraph">That&rsquo;s why we want to optimize for the outcome rather than the tool call. This post examines four changes in GitHub Copilot that put that principle into practice:</p>



<ul class="wp-block-list">
<li>Preserve useful context while reducing repetitive output.</li>



<li>Remove formatting that adds no value to the task.</li>



<li>Shorten instructions without changing useful behavior.</li>



<li>Deliver completed background work without an extra retrieval step.</li>
</ul>



<p class="wp-block-paragraph">Possible changes were evaluated offline using agentic coding benchmarks. The most promising changes were then validated through controlled online experiments before shipping. The examples in this post come from GitHub Copilot CLI. Multiple other Copilot products, such as the GitHub Copilot app and Copilot code review, use the same underlying harness and also become more efficient through these improvements.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="329" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-01.png?resize=1024%2C329" alt="Chart showing 3.1% 'Remove view previxes', 5.5% 'Selective output compaction', 2.9% 'Compact task-tool prompt', and 2.3% 'Reduce notification roundtrips'." class="wp-image-98587" srcset="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-01.png?w=1200 1200w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-01.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-01.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-01.png?w=1024 1024w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Figure&nbsp;1:&nbsp;Four independent A/B experiments using the same AI-credit metric. The segments are shown together for comparison; their effects are not necessarily strictly additive.&nbsp;</figcaption></figure>



<h2 id="h-the-local-metric-trap" class="wp-block-heading">The local metric trap</h2>



<p class="wp-block-paragraph">It&rsquo;s common to shorten the output from each tool call as a way to reduce agent costs. <a href="https://www.rtk-ai.app/docs">RTK</a> (Rust Token Killer) is a utility that shortens shell output before an agent reads it. We evaluated its effect on GitHub Copilot using our agentic coding benchmarks.</p>



<p class="wp-block-paragraph">In our harness and benchmark configuration, RTK shortened some responses, but when the omitted text mattered, the model sometimes reopened the original output or reran the command to recover what it needed.</p>



<p class="wp-block-paragraph">Those recovery steps added turns and carried more context forward. The individual tool response was shorter, but on average, the task used more tokens and took longer. We saved tokens locally and spent more globally.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="448" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-02.png?resize=1024%2C448" alt="Flow chart showing: RTK, compresses shell output &gt; Local win, tool output gets shorter &gt; Useful detail is missing &gt; Recovery, reread or rerun &gt; More turns and context carried forward. Then the option of finishing at 'End-to-end result, Tokens and cost up, Task duration up, Task completion: steady,' or 'Recovery repeats' going back to 'useful detail is missing'." class="wp-image-98588" srcset="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-02.png?w=1200 1200w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-02.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-02.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-02.png?w=1024 1024w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Figure&nbsp;2:&nbsp;A shorter tool response can make the completed task more expensive when missing details force the agent to reread output, rerun commands, and carry more context forward.&nbsp;</figcaption></figure>



<p class="wp-block-paragraph">This result applies to the integration and workloads we tested, not to every RTK configuration or to output compression in general. This meant that tokens per tool call is the wrong objective. An efficiency change has to be evaluated across the complete task, from the user&rsquo;s request through the final result.</p>



<p class="wp-block-paragraph">More useful was to look at what can we remove without making the model repeat work.</p>



<h2 id="h-compress-noise-preserve-useful-information" class="wp-block-heading">Compress noise, preserve useful information</h2>



<p class="wp-block-paragraph">The goal was to shorten repetitive output while preserving the context an agent needs to complete its task without retracing steps.</p>



<p class="wp-block-paragraph">Analysis of benchmark runs showed that install, build, test, and lint output often contains repetitive noise, while source-like output and arbitrary command results are more likely to contain the information an agent needs. That analysis informed a selective output compressor, informed in part by RTK and similar approaches.</p>



<p class="wp-block-paragraph">The prototype was evaluated on agentic coding benchmarks and a range of open source repositories, exercising their build, test, and lint systems.</p>



<p class="wp-block-paragraph">Early versions were too aggressive. They made the model repeat work or read the full saved output, increasing end-to-end cost and reducing task success. For example, we initially compressed <code>git diff</code> but removed that filter after benchmark tasks showed agents reopening the original output to recover missing information.</p>



<p class="wp-block-paragraph">Those early failures led to a three-part policy:</p>



<ol class="wp-block-list">
<li><strong>Preserve source-like and arbitrary output.</strong> Commands such as <code>cat</code>, <code>git diff</code>, <code>git show</code>, and arbitrary scripts are returned unchanged.</li>



<li><strong>Reorganize search results without dropping content.</strong> Matches and file lists from tools such as <code>grep</code> can be grouped more efficiently while retaining every result.</li>



<li><strong>Compress repetitive noise selectively.</strong> Install, build, test, and progress output is compressed only when the savings are substantial.</li>
</ol>



<p class="wp-block-paragraph">The shipped version emerged through repeated evaluation and refinement. It is conservative not because the goal was to build a conservative compressor, but because that is what the evaluations supported.</p>



<p class="wp-block-paragraph">When output is compressed, the agent can still retrieve the complete original through a direct recovery path.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="770" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-04.png?resize=1024%2C770" alt="Flowchart showing how GitHub Copilot handles shell-command output. Copilot calls a shell command, classifies the output, then chooses one of three paths: keep arbitrary/source output unchanged, reorganize search results without losing any matches, or selectively compress repetitive noise (like install/build/test logs) while preserving full output and providing a recovery path. The processed result is returned to Copilot." class="wp-image-98590" srcset="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-04.png?w=1200 1200w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-04.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-04.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-04.png?w=1024 1024w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Figure 3: The shipped compressor preserves source-like output, reorganizes search results without loss, and compresses only predictable repetitive noise while retaining the full original.</figcaption></figure>



<p class="wp-block-paragraph">That recovery path is both a safety mechanism and an evaluation signal. We tracked whether the agent opened the saved original, reran commands, repeated exploration, narrowed its searches, or took additional turns. Frequent recovery would indicate that the compressor had removed something valuable.</p>



<p class="wp-block-paragraph">On offline tasks where output compression triggered, no statistically significant task-success regression was detected, and agents extremely rarely opened the saved originals. In the online experiment, average cost decreased slightly with no material regression detected in the tracked quality metrics.</p>



<h2 id="h-remove-formatting-before-removing-information" class="wp-block-heading">Remove formatting before removing information</h2>



<p class="wp-block-paragraph">One clean token optimization came from the <code>view</code> tool, which agents use to read file contents into context.</p>



<p class="wp-block-paragraph">Previously, <code>view</code> prefixed every line with a number before showing the contents to the model. Earlier file-editing tools used those numbers to target changes, but current tools instead match surrounding code and do not use line numbers. The line-number prefixes remained even though the normal workflow no longer used them.</p>



<p class="wp-block-paragraph">Each prefix was small. Repeated across every line and every file read, however, that unused formatting accumulated throughout a session. So, we removed it.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="185" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-03.png?resize=1024%2C185" alt="Before-and-after image of code snippets. The line-number prefixes re removed from the 'After' image." class="wp-image-98589" srcset="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-03.png?w=1200 1200w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-03.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-03.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-03.png?w=1024 1024w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Figure 4: Removing line-number prefixes preserves the source exactly while eliminating formatting that was repeated across every file read.</figcaption></figure>



<p class="wp-block-paragraph">Line numbers remain useful in diffs and short snippets. They were wasteful here because they were attached to every file read without serving the current editing workflow.</p>



<p class="wp-block-paragraph">Removing them caused model-inference cost to fall by roughly 5% in offline agentic coding benchmarks. Success rates stayed within the expected run-to-run variance, and edit failures did not increase.</p>



<p class="wp-block-paragraph">We then tested the change with Copilot CLI users. The online experiment reduced average daily model-inference cost per user by about 3%, with no material regression detected in the quality or satisfaction metrics we tracked.</p>



<p class="wp-block-paragraph">For developers, that means more of the context window is available for the work itself rather than formatting the agent does not use.</p>



<p class="wp-block-paragraph">This was the ideal change: no new instructions for the model, no source of information to recover, and no additional decision to make. The file contents reached the model unchanged.</p>



<h2 id="h-compress-prompts-without-compressing-intent" class="wp-block-heading">Compress prompts without compressing intent</h2>



<p class="wp-block-paragraph">Prompts carry instructions that shape how an agent works, and they are sent to the model on every turn. Shortening them only improves efficiency if the agent keeps the behaviors developers depend on.</p>



<p class="wp-block-paragraph">In GitHub Copilot, the task tool launches specialized agents for parallel work. Its guidance had accumulated across tool descriptions, schemas, agent definitions, system instructions, and companion tools.</p>



<p class="wp-block-paragraph">A meta-prompting loop, in which Copilot iteratively wrote its own prompt, reduced that prompt by roughly half. Copilot produced and refined smaller candidates, and targeted behavioral tests checked the requirements we wanted to preserve.</p>



<p class="wp-block-paragraph">The first online experiment found a regression that the initial offline evaluations had missed. The meta-prompting loop had rewritten cautious parallelism guidance into a hard scheduling policy, causing independent custom agents to run sequentially.</p>



<p class="wp-block-paragraph">We stopped the experiment. Before changing the prompt again, we wrote a regression evaluation for the behavior users had exposed. The eventual fix replaced an explicit allowlist and denylist with one sentence:</p>



<p class="wp-block-paragraph"><em>Independent agents can run in parallel; consider side effects.</em></p>



<p class="wp-block-paragraph">That sentence was shorter and less restrictive; it deferred the choice of whether to run sub-agents in parallel to the model instead of the previous explicit guidance. With it, our new behavior test passed without causing any existing behavioral tests to fail.</p>



<p class="wp-block-paragraph">Prompt behavior needs tests. If a behavior is not tested, a shorter prompt can remove it without anyone noticing.&nbsp;</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="550" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-05.png?resize=1024%2C550" alt="Three-stage diagram labeled Compression &rarr; Regression + fix &rarr; Completed. Left panel shows an original prompt compressed by about 50%. Middle panel highlights a regression where agents became serialized, then a fix by editing one sentence to restore parallelism. Right panel shows final shipped prompt with restored behavior and cumulative savings of about 1,300 fewer tokens per turn across steps." class="wp-image-98591" srcset="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-05.png?w=1200 1200w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-05.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-05.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-05.png?w=1024 1024w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Figure 5 Prompt compression became safe only after a regression test exposed serialized agents and a one-sentence fix restored parallelism; the resulting token savings recur on every model turn.</figcaption></figure>



<p class="wp-block-paragraph">The shipped prompt removes about 1,300 <code>task</code>-tool prompt tokens per turn, corresponding to approximately 1.8% fewer total prompt tokens per session and 2.9% lower normalized cost per active hour, with no quality regression detected in the measured evaluations.</p>



<h2 id="h-deliver-completed-background-work-without-an-extra-retrieval-turn" class="wp-block-heading">Deliver completed background work without an extra retrieval turn</h2>



<p class="wp-block-paragraph">Agents often run independent work in the background, such as a long-running shell command alongside a sub-agent investigation. Notifications let the agent continue until that work is ready without spending a tool call waiting.</p>



<p class="wp-block-paragraph">If the agent does not explicitly wait for either task, the harness wakes the model and notifies it when the shell command or sub-agent finishes.</p>



<p class="wp-block-paragraph">Previously, that notification did not include the completed result, so the agent had to spend another turn retrieving output Copilot had already received. When several tasks finished close together, that detour could repeat. Copilot now batches eligible completion notifications and delivers completed results directly in the existing tool-result format. The agent can continue with the information it needs, without spending an extra turn asking for it again. Explicit reads for work that is still running behave as before.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="671" width="1024" src="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-06.png?resize=1024%2C671" alt="Before-and-after sequence diagram comparing orchestration behavior.

Before: model waits on separate shell and sub-agent completions, causing retrieval detours and four LLM calls to process two results.
After: a harness batches related completions and emits synthetic tool events so background work continues while waiting; both results are processed together in a single LLM call.
The visual emphasizes reduced latency and fewer model round trips." class="wp-image-98592" srcset="https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-06.png?w=1320 1320w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-06.png?w=300 300w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-06.png?w=768 768w, https://github.blog/wp-content/uploads/2026/09/Blog-Graphic-06.png?w=1024 1024w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Figure 6 Before, each background completion could wake a retrieval-only model turn. After, the harness batches eligible completions and delivers completed results in the existing tool-result format.</figcaption></figure>



<p class="wp-block-paragraph">Before this change, each completed task required one model call to request its result and another to process it. For the shell command and sub-agent shown above, that meant four model calls before work could continue.</p>



<p class="wp-block-paragraph">Now, the harness batches both completions and supplies their results together, so a single model call can process both. Removing those retrieval detours also avoids carrying the full session context through unnecessary calls.</p>



<p class="wp-block-paragraph">By delivering completed results directly, without compressing, summarizing, or withholding anything, the harness reduced average token-related usage, as measured in AI Credits, by about 2.3%.</p>



<h2 id="h-measure-changes-in-context" class="wp-block-heading">Measure changes in context</h2>



<p class="wp-block-paragraph">A change that saves tokens in one Copilot workflow can increase costs in another.</p>



<p class="wp-block-paragraph">For example, a tighter set of file-tool instructions was inspired by positive results in Copilot code review. In a Copilot CLI online experiment, it increased cost, so we did not ship it.</p>



<p class="wp-block-paragraph">By contrast, removing line-number prefixes and selectively compressing output each reduced average prompt tokens per review by roughly 5% in independent evaluations across a large set of Copilot code review tasks using the production model. We detected no material change in the tracked review-quality metrics.</p>



<p class="wp-block-paragraph">These findings are separate from <a href="https://github.blog/changelog/2026-06-25-copilot-code-review-analysis-depth-and-efficiency-updates/">the earlier migration of Copilot code review to the shared file tools</a>, which, together with review-instruction tuning, reduced code review cost by about 20%.</p>



<p class="wp-block-paragraph">Each change needs to be measured in the workflow where it runs.</p>



<h2 id="h-five-lessons-for-building-efficient-ai-coding-agents" class="wp-block-heading">Five lessons for building efficient AI coding agents</h2>



<ol class="wp-block-list">
<li><strong>Optimize the completed task, not the tool call.</strong> Shorter output is not cheaper if the agent spends more turns recovering what was removed.</li>



<li><strong>Optimize orchestration, not just model output.</strong> Eliminate model turns that perform work the harness can complete deterministically.</li>



<li><strong>Compress by what the output represents.</strong> Preserve exact content, prefer lossless transformations, and measure how often agents use the recovery path.</li>



<li><strong>Prompt rewrites sometimes have unintended consequences.</strong> Validate that intended behavior is preserved.</li>



<li><strong>Evidence is local to the workload.</strong> Re-evaluate changes in offline benchmarks, online experiments, and every product surface where they ship.</li>
</ol>



<p class="wp-block-paragraph">None of these changes made the model smarter. They removed work the model never needed to do.</p>



<p class="wp-block-paragraph">The changes described in this post are shipping across GitHub Copilot experiences that use the same underlying harness.</p>



<div class="wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph">Bring agentic workflows to your terminal<br>with <a href="https://github.com/features/copilot/cli">GitHub Copilot CLI &gt;</a></p>
</div>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/how-we-make-ai-coding-more-cost-efficient-without-sacrificing-task-quality/">How we make AI coding more cost efficient without sacrificing task quality</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98586</post-id>	</item>
		<item>
		<title>OpenClaw went viral. Meet the maintainers building and securing it.</title>
		<link>https://github.blog/open-source/maintainers/openclaw-went-viral-meet-the-maintainers-building-and-securing-it/</link>
		
		<dc:creator><![CDATA[Gregg Cochran]]></dc:creator>
		<pubDate>Thu, 27 Aug 2026 16:00:00 +0000</pubDate>
				<category><![CDATA[Maintainers]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[maintainers]]></category>
		<category><![CDATA[open source]]></category>
		<category><![CDATA[OpenClaw]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98468</guid>

					<description><![CDATA[<p>OpenClaw is the fastest-growing project in GitHub history. Peter Steinberger and several maintainers share what they learned in the project's first six months.</p>
<p>The post <a href="https://github.blog/open-source/maintainers/openclaw-went-viral-meet-the-maintainers-building-and-securing-it/">OpenClaw went viral. Meet the maintainers building and securing it.</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">What began as a personal experiment quickly became a global open source project with extraordinary momentum.</p>



<p class="wp-block-paragraph"><a href="https://github.com/openclaw/openclaw">OpenClaw</a> is a personal AI assistant that runs on users&rsquo; devices and connects with the messaging channels they already use. Started by Peter Steinberger as a weekend project in November 2025, its GitHub repository has grown to approximately 388,000 stars, 81,000 forks, and more than 80,000 commits by August 26, 2026.</p>



<p class="wp-block-paragraph">In this video interview, filmed just six months into the project, creator Peter Steinberger and several OpenClaw maintainers discuss managing a surge of pull requests, rethinking contributor trust and code review, addressing software supply chain risks, and balancing powerful agent capabilities with security. They also share security lessons from the <a href="https://github.com/open-source/github-secure-open-source-fund">GitHub Secure Open Source Fund</a> and the value of connecting with maintainers facing similar challenges. Watch the full video above, then explore the key lessons below.</p>



<h2 id="h-people-in-this-video" class="wp-block-heading">People in this video</h2>



<p class="wp-block-paragraph">The following maintainers shared their experiences maintaining and securing OpenClaw.</p>



<ul class="wp-block-list">
<li><a href="https://www.linkedin.com/in/steipete/">Peter Steinberger</a>, Creator of OpenClaw</li>



<li><a href="https://www.linkedin.com/in/bradgroux/">Brad Groux</a>, CEO, Digital Meld</li>



<li><a href="https://www.linkedin.com/in/joshavant/?skipRedirect=true">Josh Avant</a>, Member of technical staff, OpenClaw Foundation</li>



<li><a href="https://www.linkedin.com/in/jalehman1/">Josh Lehman</a>, Martian Engineering</li>



<li><a href="https://www.linkedin.com/in/sally-ann-omalley/">Sally O&rsquo;Malley</a>, Principal software engineer at Red Hat</li>



<li><a href="https://www.linkedin.com/in/buns/">Val Alexander</a>, OpenCoven</li>



<li><a href="https://www.linkedin.com/in/koconder/">Vincent Koc</a>, Chief architect, OpenClaw Foundation</li>
</ul>



<p class="wp-block-paragraph">Here are the top 10 lessons that we took away from the conversation.</p>



<h2 id="h-lessons-1-3-how-ai-changed-contributions-and-community" class="wp-block-heading">Lessons 1&ndash;3: How AI changed contributions and community</h2>



<h3 id="h-1-pull-requests-became-prompt-requests" class="wp-block-heading">1. Pull requests became prompt requests</h3>



<p class="wp-block-paragraph">OpenClaw&rsquo;s maintainers found themselves managing thousands of pull requests and issues, with some contributors opening hundreds of pull requests at once.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I&nbsp;don&rsquo;t&nbsp;even call them pull requests. I call them prompt requests.</p>
<cite>Peter Steinberger</cite></blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">There were some contributors that had multiple hundreds of pull requests running these sort of automated software factories that were just mining everything for issues.</p>
<cite>Josh Lehman</cite></blockquote>



<p class="wp-block-paragraph">The challenge shifted from attracting participation to finding valuable contributions amid a flood of activity that could overwhelm human review.</p>



<h3 id="h-2-keep-the-door-open-for-new-contributors" class="wp-block-heading">2. Keep the door open for new contributors</h3>



<p class="wp-block-paragraph">OpenClaw&rsquo;s maintainers wanted the project to be welcoming to new participants, whether they were first-time open source contributors, non-developers solving a specific problem, or people using AI agents to help. Rather than dismissing imperfect contributions, they looked for promising ideas and worked with contributors to refine, rewrite, or complete the final changes themselves.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I know how it felt when, many years ago, my first pull request was accepted on a project.</p>
<cite>Peter Steinberger</cite></blockquote>



<p class="wp-block-paragraph">Some of the first-time contributions that were merged came from people without a development background. They used an agent to create a pull request, and worked with maintainers to finish the change.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">A good proportion of those first-time pull requests that got merged are from non-developers. They&rsquo;re just people that have a specific problem and a need.</p>
<cite>Vincent Koc</cite></blockquote>



<h3 id="h-3-agents-save-time-but-make-it-harder-to-sign-off" class="wp-block-heading">3. Agents save time but make it harder to sign off</h3>



<p class="wp-block-paragraph">The maintainers described two very different outcomes from the same technology: agents can help people reclaim time, but they can also make it harder to stop working.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I&rsquo;ve seen the other side of it, where people are just so in love with it and they realize, wow, if I don&rsquo;t sleep tonight, I can do what used to take a week for me to do.</p>
<cite>Val Alexander</cite></blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I have three kids. They&rsquo;re very small. OpenClaw lets me manage agents that go and work for me so I can get back to playing with my kids.</p>
<cite>Josh Lehman</cite></blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Sometimes the maintainers will go on the channel and say, &lsquo;I&rsquo;m&nbsp;going to touch grass now.&nbsp;I&rsquo;m&nbsp;taking a few hours off.&rsquo;</p>
<cite>Sally O&rsquo;Malley</cite></blockquote>



<p class="wp-block-paragraph">Agents are neither good nor bad for work-life balance. But they amplify both the opportunity to do more and the importance of knowing when to step away.</p>



<h2 id="h-lessons-4-6-how-maintainers-adapted" class="wp-block-heading">Lessons 4-6: How maintainers adapted</h2>



<h3 id="h-4-earn-trust-by-finding-where-you-can-add-value" class="wp-block-heading">4. Earn trust by finding where you can add value</h3>



<p class="wp-block-paragraph">There was no single path to becoming an OpenClaw maintainer. Some contributors arrived through security work, others through integrations or community participation, but the common thread was finding a way to add value and taking ownership.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Peter ignored me, so I was like, how else can I get his attention? Security.</p>
<cite>Vincent Koc</cite></blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I&rsquo;m a Microsoft guy, so I thought, is there a plugin for Microsoft Teams?</p>
<cite>Brad Groux</cite></blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I looked into the community and I was in voice chat, and people were asking a lot of questions, and I was like, well, how can I add value in these conversations?</p>
<cite>Val Alexander</cite></blockquote>



<h3 id="h-5-the-new-trust-signal-is-showing-your-work" class="wp-block-heading">5. The new trust signal is showing your work</h3>



<p class="wp-block-paragraph">As contribution counts became less informative, the team identified evidence that could help a pull request stand out: agent transcripts, screenshots, testing, and an explanation of the contributor&rsquo;s thinking.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">If you provide us with the transcripts, we&nbsp;actually see&nbsp;how you came to the pull request and your discussion with the agent. Incredibly valuable. If you add screenshots, you can prove that you tested this.</p>
<cite>Peter Steinberger</cite></blockquote>



<p class="wp-block-paragraph">The important question was not simply whether a human or an agent wrote the code. It was whether the contributor understood the feature and had considered how it interacted with the rest of the project.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Nobody cares if you wrote the code or not, but we care if you actually thought about this feature.</p>
<cite>Peter Steinberger</cite></blockquote>



<h3 id="h-6-maintainers-are-reviewing-agent-code-with-agents" class="wp-block-heading">6. Maintainers are reviewing agent code with agents</h3>



<p class="wp-block-paragraph">Maintainers increasingly relied on AI tools to help review AI-generated contributions, while also taking a more hands-on approach to improving submitted code.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Whenever I get a pull request from an AI, one thing I love to do now is use GitHub Copilot for all the reviews. I just press a button right there. It does a review and generates clarity on all the files that are attached, what the files mean, and how they changed.</p>
<cite>Val Alexander</cite></blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">This is the first project where I saw it become normalized that when someone submits a pull request, as a maintainer, you just edit it. You just make it right.</p>
<cite>Josh Lehman</cite></blockquote>



<h2 id="h-lessons-7-9-security-challenges" class="wp-block-heading">Lessons 7&ndash;9: Security challenges</h2>



<h3 id="h-7-reputation-became-an-attack-surface" class="wp-block-heading">7. Reputation became an attack surface</h3>



<p class="wp-block-paragraph">Contribution history itself could be manipulated. OpenClaw&rsquo;s maintainers saw people duplicate existing pull requests, and Vincent Koc explains why.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">People would basically duplicate other people&rsquo;s pull requests. What they were attempting to do here was to build credibility, because we had these badges, like how many you&rsquo;ve merged. So the more merges you had, it was like a trust signal to us maintainers.</p>
<cite>Vincent Koc</cite></blockquote>



<p class="wp-block-paragraph">Peter described a company using an automated pull request to promote their product. The team had to identify duplicate work and determine which pull request was the original.</p>



<p class="wp-block-paragraph">The code was not the only thing the project needed to evaluate. Maintainers also had to reconsider the social signals they used to decide what, and whom, to trust.</p>



<h3 id="h-8-safe-by-default-depends-on-who-you-ask" class="wp-block-heading">8. &ldquo;Safe by default&rdquo; depends on who you ask</h3>



<p class="wp-block-paragraph">What feels safe to one user may feel unnecessarily restrictive to another.</p>



<p class="wp-block-paragraph">The tradeoff was clear in practice. Tighter workspace restrictions generated user complaints, while fewer restrictions could expose the project to security incidents.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">It&rsquo;s really often a hard game to find the right balance between making it really convenient for users and also building something that is safe enough as a default.</p>
<cite>Peter Steinberger</cite></blockquote>



<p class="wp-block-paragraph">Secure defaults must account for an agent&rsquo;s capabilities, what users understand, and what a particular environment is prepared to allow.</p>



<h3 id="h-9-know-who-maintains-your-dependencies" class="wp-block-heading">9. Know who maintains your dependencies</h3>



<p class="wp-block-paragraph">Recent supply chain attacks pushed the maintainers to think more carefully about both the dependencies they relied on and their relationship with the projects behind them.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">We went through our dependencies with a fine-tooth comb. What it&rsquo;s pushed us to do is actually reduce the core dependencies, but also create a relationship with the maintainers that we have a dependency on.</p>
<cite>Vincent Koc</cite></blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">It&rsquo;s not the default that companies actually try to contribute back instead of just maintaining a fork and not caring.</p>
<cite>Peter Steinberger</cite></blockquote>



<h2 id="h-lesson-10-how-the-github-secure-open-source-fund-helped" class="wp-block-heading">Lesson 10: How the GitHub Secure Open Source Fund helped</h2>



<p class="wp-block-paragraph">Participants described the GitHub Secure Open Source Fund as both a security learning experience and a way to connect with maintainers confronting similar, often overwhelming, problems.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The presenter was like, first, go get a cup of coffee. Step one, take a breath. It connected us to the human element of being a maintainer.</p>
<cite>Josh Avant</cite></blockquote>



<p class="wp-block-paragraph">The program provided greater awareness of security practices and helped participating maintainers understand how to prompt agents.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">We have agents now, and they can do just about anything that you ask them to do, but you still have to know what to ask them to do. Now I have the ability to know what to ask for.</p>
<cite>Josh Lehman</cite></blockquote>



<p class="wp-block-paragraph">Vincent emphasized the value of meeting other maintainers who were experiencing the same challenges of securing open source projects. The program gave participants a community they could tap into and learn from as those challenges continued.</p>



<h2 id="h-continue-the-conversation" class="wp-block-heading">Continue the conversation</h2>



<p class="wp-block-paragraph"><a href="https://www.youtube.com/watch?v=5VSwaUXtPIE">Watch the full conversation</a> to hear how OpenClaw&rsquo;s maintainers are adapting when contributions scale faster than the human systems used to review, secure, and sustain them.</p>



<p class="wp-block-paragraph">OpenClaw participated in Session 4 of the GitHub Secure Open Source Fund. To learn more, read <a href="https://github.blog/open-source/maintainers/what-50-open-source-projects-taught-us-about-security-in-the-ai-era/">What 50 open source projects taught us about security in the AI era.</a></p>



<p class="wp-block-paragraph"><a href="https://github.com/open-source/github-secure-open-source-fund">Applications for the GitHub Secure Open Source Fund are now open</a><strong>.</strong> If you&rsquo;re maintaining an open source project, apply to learn from security experts, connect with fellow maintainers, and strengthen the security of your project.</p>



<p class="wp-block-paragraph"><a href="https://github.com/orgs/community/discussions/205852">Head over to the GitHub Community</a> and ask the maintainers what it&rsquo;s really like building the fastest-growing open source project in GitHub history!</p>



<h2 id="h-thank-you-to-all-github-secure-open-source-fund-partners" class="wp-block-heading">Thank you to all GitHub Secure Open Source Fund Partners</h2>



<p class="wp-block-paragraph">Together, we are helping secure the open source ecosystem for everyone!</p>



<p class="wp-block-paragraph"><strong>Funding Partners:</strong> Alfred P. Sloan Foundation, American Express, Chainguard, Datadog, Herodevs, Kraken, Mayfield, Microsoft, Shopify, Stripe, Superbloom, Vercel, Zerodha, 1Password</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="538" width="1024" src="https://github.blog/wp-content/uploads/2026/02/header.jpg?resize=1024%2C538" alt="A decorative header image showing GitHub Secure Open Source Fund, powered by GitHub Sponsors. Logos below are: Alfred P. Sloan Foundation, American Express, chainguard, Datadog, herdevs, Kraken, Microsoft, Mayfield, Shopify, stripe, superbloom, Vercel, 1Password, Zerodha" class="wp-image-93832" srcset="https://github.blog/wp-content/uploads/2026/02/header.jpg?w=2400 2400w, https://github.blog/wp-content/uploads/2026/02/header.jpg?w=300 300w, https://github.blog/wp-content/uploads/2026/02/header.jpg?w=768 768w, https://github.blog/wp-content/uploads/2026/02/header.jpg?w=1024 1024w, https://github.blog/wp-content/uploads/2026/02/header.jpg?w=1536 1536w, https://github.blog/wp-content/uploads/2026/02/header.jpg?w=2048 2048w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>



<p class="wp-block-paragraph"><strong>Ecosystem Partners</strong>: Atlantic Council, Ecosyste.ms, CURIOSS, Digital Data Design Institute Lab for Innovation Science, Digital Infrastructure Insights Fund, Microsoft for Startups, Mozilla, OpenForum Europe, Open Source Collective, OpenUK, Open Technology Fund, OpenSSF, Open Source Initiative, OpenJS Foundation, University of California, OWASP, Santa Cruz OSPO, Sovereign Tech Agency, SustainOSS</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="328" width="1024" src="https://github.blog/wp-content/uploads/2026/08/soss_grid_feb_2026_3.webp?resize=1024%2C328" alt="" class="wp-image-98184" srcset="https://github.blog/wp-content/uploads/2026/08/soss_grid_feb_2026_3.webp?w=1440 1440w, https://github.blog/wp-content/uploads/2026/08/soss_grid_feb_2026_3.webp?w=300 300w, https://github.blog/wp-content/uploads/2026/08/soss_grid_feb_2026_3.webp?w=768 768w, https://github.blog/wp-content/uploads/2026/08/soss_grid_feb_2026_3.webp?w=1024 1024w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>



<div class="wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph">Looking for practical security steps you can take for your project? <a href="https://gh.io/gh-secure">Set up the security baseline for your project in one minute &gt;</a></p>
</div>
</body></html>
<p>The post <a href="https://github.blog/open-source/maintainers/openclaw-went-viral-meet-the-maintainers-building-and-securing-it/">OpenClaw went viral. Meet the maintainers building and securing it.</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98468</post-id>	</item>
		<item>
		<title>GitHub Copilot app for Beginners: Automate Dependabot pull request triage</title>
		<link>https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-automate-dependabot-pull-request-triage/</link>
		
		<dc:creator><![CDATA[Christopher Harrison]]></dc:creator>
		<pubDate>Wed, 26 Aug 2026 20:12:53 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[GitHub Copilot app]]></category>
		<category><![CDATA[GitHub Copilot app for Beginners]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98460</guid>

					<description><![CDATA[<p>Managing library updates can be tedious at times. Learn how the GitHub Copilot app can handle this type of repetitive task.</p>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-automate-dependabot-pull-request-triage/">GitHub Copilot app for Beginners: Automate Dependabot pull request triage</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">I might be biased, but I think Dependabot is pretty amazing. It helps keep my projects up to date, ensuring I&rsquo;m always using secure libraries. But because there&rsquo;re frequently new vulnerabilities, there&rsquo;re frequently new pull requests from Dependabot.</p>



<p class="wp-block-paragraph">Sometimes it&rsquo;s a minor version bump. Sometimes it&rsquo;s a major version upgrade. Sometimes everything will work just fine. And sometimes&hellip; well, every single developer has been caught by a breaking change.</p>



<p class="wp-block-paragraph">How can we best triage these pull requests? The work isn&rsquo;t particularly difficult per se, but it certainly is repetitive.</p>



<p class="wp-block-paragraph">It&rsquo;s the perfect task to offload to Copilot! With <a href="https://docs.github.com/en/copilot/concepts/agents/cloud-agent/about-automations">GitHub Copilot app automations</a>, you can hand off that first round of review. Instead of manually inspecting every Dependabot pull request, you can create an automation that reviews open pull requests, groups them by risk, verifies CI status, and delivers a summary before your day begins.</p>



<p class="wp-block-paragraph">Follow the steps below to build a daily Dependabot triage automation.</p>



<h2 id="h-step-1-create-a-new-automation" class="wp-block-heading">Step 1: Create a new automation</h2>



<p class="wp-block-paragraph">From the GitHub Copilot app, <a href="https://docs.github.com/en/copilot/how-tos/github-copilot-app/using-automations">create a new automation</a>.</p>



<p class="wp-block-paragraph">You&rsquo;ll configure two things first:</p>



<ul class="wp-block-list">
<li><strong>Name:</strong> Give the automation a descriptive name, such as <strong>Daily Dependabot Triage</strong>.</li>



<li><strong>Trigger:</strong> Decide when it should run.</li>
</ul>



<p class="wp-block-paragraph">Available trigger options include:</p>



<ul class="wp-block-list">
<li>Manual</li>



<li>Hourly</li>



<li>Daily</li>



<li>Weekly</li>



<li>When an issue is created</li>
</ul>



<p class="wp-block-paragraph">For recurring maintenance tasks like <a href="https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/dependabot-quickstart">Dependabot reviews</a>, a daily schedule is often a good choice. For example, you might schedule it to run before your workday begins so the results are waiting when you log in.</p>



<p class="wp-block-paragraph">You can also choose whether the automation runs in the cloud or on your local machine.</p>



<h2 class="wp-block-heading" id="step-2-describe-the-task-in-natural-language">Step 2: Describe the task in natural language</h2>



<p class="wp-block-paragraph">Next, tell Copilot what you want it to do.</p>



<p class="wp-block-paragraph">For example:</p>


<div class="wp-block-code-wrapper">
<pre class="wp-block-code language-plaintext"><code>Review the open Dependabot pull requests, group them by risk, identify the safe patch and minor version updates, verify that CI is passing for each pull request, and provide a short summary of the recommended next steps.</code></pre>
<clipboard-copy aria-label="Copy" class="code-copy-btn" data-copy-feedback="Copied!" value="Review the open Dependabot pull requests, group them by risk, identify the safe patch and minor version updates, verify that CI is passing for each pull request, and provide a short summary of the recommended next steps." tabindex="0" role="button"><svg aria-hidden="true" height="16" viewbox="0 0 16 16" version="1.1" width="16" class="octicon octicon-copy js-clipboard-copy-icon"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg><svg aria-hidden="true" height="16" viewbox="0 0 16 16" version="1.1" width="16" class="octicon octicon-check js-clipboard-check-icon"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg></clipboard-copy></div>


<p class="wp-block-paragraph">Because the prompt uses natural language, you can customize it to match your team&rsquo;s workflow.</p>



<h2 class="wp-block-heading" id="step-3-select-the-repository">Step 3: Select the repository</h2>



<p class="wp-block-paragraph">Choose the repository or project the automation should analyze.</p>



<p class="wp-block-paragraph">Once you&rsquo;ve selected the repository, create the automation.</p>



<p class="wp-block-paragraph">If you want to test it immediately instead of waiting for the scheduled run, choose <strong>Create and Run</strong>.</p>



<h2 class="wp-block-heading" id="step-4-review-the-results">Step 4: Review the results</h2>



<p class="wp-block-paragraph">When the automation finishes, Copilot returns a summary instead of a list of individual pull requests.</p>



<p class="wp-block-paragraph">For example, it might:</p>



<ul class="wp-block-list">
<li>Group safe patch updates together</li>



<li>Separate minor and major version upgrades</li>



<li>Identify which pull requests have passing CI</li>



<li>Highlight dependencies that require additional investigation</li>
</ul>



<p class="wp-block-paragraph">Rather than interrupting your morning with dozens of small decisions, you can quickly identify which updates are ready to merge and which deserve closer attention.</p>



<h2 class="wp-block-heading" id="step-5-continue-the-work-in-a-copilot-session">Step 5: Continue the work in a Copilot session</h2>



<p class="wp-block-paragraph">If one of the updates requires additional work, you can continue directly from the automation results.</p>



<p class="wp-block-paragraph">For example, if the summary identifies a major framework upgrade, you can start a new Copilot session from the results and ask Copilot to help complete the migration.</p>



<p class="wp-block-paragraph">Because the session starts with the automation&rsquo;s context, you don&rsquo;t have to gather the information again.</p>



<h2 class="wp-block-heading" id="review-previous-automation-runs">Review previous automation runs</h2>



<p class="wp-block-paragraph">Every automation run is saved, making it easy to see:</p>



<ul class="wp-block-list">
<li>When it ran</li>



<li>What actions it performed</li>



<li>What results it produced</li>
</ul>



<p class="wp-block-paragraph">Having a history of each run makes automations transparent. You can always review what happened instead of treating them as a black box.</p>



<h2 class="wp-block-heading" id="turn-repetitive-work-into-background-work">Turn repetitive work into background work</h2>



<p class="wp-block-paragraph">Dependabot triage is a good example of the kind of recurring task that&rsquo;s well suited for automation. You describe the workflow once, choose when it should run, and let Copilot perform the repetitive steps automatically.</p>



<p class="wp-block-paragraph">If you&rsquo;re just getting started with automations, begin with a task you already perform on autopilot. Let Copilot handle the routine work so you can spend your time on the decisions that require your expertise.</p>



<div class="wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>Ready to automate your next recurring task?</strong> <a href="https://github.com/features/ai/github-app">Create your first automation in the GitHub Copilot app &gt;</a></p>
</div>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/github-copilot/github-copilot-app-for-beginners-automate-dependabot-pull-request-triage/">GitHub Copilot app for Beginners: Automate Dependabot pull request triage</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98460</post-id>	</item>
		<item>
		<title>How to evaluate LLMs before production</title>
		<link>https://github.blog/ai-and-ml/llms/how-to-evaluate-llms-before-production/</link>
		
		<dc:creator><![CDATA[Mariko Wakabayashi]]></dc:creator>
		<pubDate>Tue, 25 Aug 2026 21:35:11 +0000</pubDate>
				<category><![CDATA[AI & ML]]></category>
		<category><![CDATA[LLMs]]></category>
		<category><![CDATA[Secret Scanning]]></category>
		<guid isPermaLink="false">https://github.blog/?p=98316</guid>

					<description><![CDATA[<p>These are the lessons we learned evaluating LLMs for real-world secret scanning.</p>
<p>The post <a href="https://github.blog/ai-and-ml/llms/how-to-evaluate-llms-before-production/">How to evaluate LLMs before production</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p class="wp-block-paragraph">A language model can perform well on a clean benchmark and still struggle with the cases that matter in production.</p>



<p class="wp-block-paragraph">Benchmarks and curated datasets are useful when prototyping an LLM-based system. They help teams compare models, test an initial prompt, and determine whether an idea is technically plausible.</p>



<p class="wp-block-paragraph">But as a system moves closer to production, the evaluation problem changes.</p>



<p class="wp-block-paragraph">Real inputs are often ambiguous. Labels may be inconsistent. Important context may be missing or truncated. The evaluation set may not reflect the production distribution. Edge cases that rarely appear in benchmarks can become common sources of failure. Even when offline metrics improve, those results may not translate cleanly into production behavior.</p>



<p class="wp-block-paragraph">We encountered these challenges while evaluating an LLM-based system designed to reduce false positives in GitHub secret scanning.</p>



<p class="wp-block-paragraph">Secret scanning identifies credentials such as tokens and keys that may have been committed to a repository. Because some candidate strings resemble secrets, but don&rsquo;t actually represent real credentials, developers may spend time investigating alerts that don&rsquo;t require remediation.</p>



<p class="wp-block-paragraph">Rather than determine whether an LLM could classify a string correctly, we needed to understand whether the system could reduce noisy alerts while preserving enough recall to remain safe for a security workflow.</p>



<p class="wp-block-paragraph">In this post, we share the practices that helped us move from promising prototype results to production. The lessons apply broadly to LLM-powered systems in code analysis, developer tools, security, data analysis, and other production workflows.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="441" width="1024" src="https://github.blog/wp-content/uploads/2026/08/633085615-8fa1a231-a731-4c36-b53d-007d212d811d-1.png?resize=1024%2C441" alt="Diagram titled &ldquo;The LLM evaluation lifecycle&rdquo; showing seven stages connected by arrows: product decision, representative dataset, offline evaluation, error analysis, targeted change, regression evaluation, and online experiment. A dashed feedback loop labeled &ldquo;Iterate and learn&rdquo; connects regression evaluation back to the dataset and targeted-change stages." class="wp-image-98319" srcset="https://github.blog/wp-content/uploads/2026/08/633085615-8fa1a231-a731-4c36-b53d-007d212d811d-1.png?w=1920 1920w, https://github.blog/wp-content/uploads/2026/08/633085615-8fa1a231-a731-4c36-b53d-007d212d811d-1.png?w=300 300w, https://github.blog/wp-content/uploads/2026/08/633085615-8fa1a231-a731-4c36-b53d-007d212d811d-1.png?w=768 768w, https://github.blog/wp-content/uploads/2026/08/633085615-8fa1a231-a731-4c36-b53d-007d212d811d-1.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/08/633085615-8fa1a231-a731-4c36-b53d-007d212d811d-1.png?w=1536 1536w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>



<h2 id="h-1-start-with-the-product-decision-not-the-model" class="wp-block-heading">1. Start with the product decision, not the model</h2>



<p class="wp-block-paragraph">When an LLM system doesn&rsquo;t perform as expected, the first instinct is often to adjust its technical components.</p>



<p class="wp-block-paragraph">Teams may rewrite the prompt, add context, introduce another reasoning step, adjust the surrounding pipeline, or switch models. Before making any of these changes, they should define the decision the evaluation is meant to support.</p>



<p class="wp-block-paragraph">For our secret-scanning work, we asked:</p>



<p class="wp-block-paragraph"><em>Can the system reduce false positives while preserving enough recall to be safe in a production security workflow?</em></p>



<p class="wp-block-paragraph">To answer this question, teams must decide which mistakes are acceptable, which metrics should drive the product decision, and which guardrails must remain within their defined thresholds.</p>



<p class="wp-block-paragraph">In secret scanning, incorrectly suppressing a real credential can be more consequential than asking a developer to review an additional alert. We therefore did not treat precision and recall as equally interchangeable metrics.</p>



<p class="wp-block-paragraph">Our primary objective was to reduce false positives and improve precision. Recall served as a safety constraint: an experiment could advance only if any decrease remained within a predefined acceptable range. This gave us a clear way to evaluate tradeoffs. We selected the configuration that achieved the strongest false-positive reduction while satisfying the recall requirement and meeting our operational guardrails.</p>



<p class="wp-block-paragraph" id="h-">We organized the evaluation criteria into three levels:</p>



<h3 id="h-primary-outcome" class="wp-block-heading">Primary outcome</h3>



<p class="wp-block-paragraph">This measured the user benefit we were trying to improve:</p>



<ul class="wp-block-list">
<li>False-positive reduction</li>



<li>Precision</li>
</ul>



<h3 id="h-safety-constraint" class="wp-block-heading">Safety constraint</h3>



<p class="wp-block-paragraph">This prevented an apparent improvement from introducing unacceptable security risk:</p>



<ul class="wp-block-list">
<li>Recall</li>
</ul>



<h3 id="h-operational-guardrails" class="wp-block-heading">Operational guardrails</h3>



<p class="wp-block-paragraph">These determined whether the result was practical to deploy:</p>



<ul class="wp-block-list">
<li>Latency</li>



<li>Cost</li>



<li>Reliability</li>



<li>Production compatibility</li>
</ul>



<p class="wp-block-paragraph">This distinction prevented us from treating every metric as interchangeable. A change that reduced false positives but significantly lowered recall wasn&rsquo;t automatically an improvement. Neither was a change that improved quality while making the system too slow, expensive, or difficult to integrate.</p>



<p class="wp-block-paragraph">Consider two hypothetical experiment results:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><strong>Experiment</strong>&nbsp;</th><th><strong>Precision</strong>&nbsp;</th><th><strong>Recall</strong>&nbsp;</th><th><strong>Latency</strong>&nbsp;</th><th><strong>Decision</strong>&nbsp;</th></tr></thead><tbody><tr><td>Experiment A&nbsp;</td><td>Large improvement&nbsp;</td><td>Falls below the safety guardrail&nbsp;</td><td>Acceptable&nbsp;</td><td>Don&rsquo;t&nbsp;advance&nbsp;</td></tr><tr><td>Experiment B&nbsp;</td><td>Moderate improvement&nbsp;</td><td>Remains within the guardrail&nbsp;</td><td>Acceptable&nbsp;</td><td>Continue testing&nbsp;</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Experiment A may look stronger if precision is viewed in isolation. Experiment B is more aligned with the product goal because it improves the developer experience without violating the recall guardrail.</p>



<p class="wp-block-paragraph">Before evaluating an LLM system, decide what success means for the user and which guardrails the system must respect. We want to generate evidence that supports a product decision.</p>



<h2 id="h-2-treat-offline-evaluation-like-integration-testing" class="wp-block-heading">2. Treat offline evaluation like integration testing</h2>



<p class="wp-block-paragraph">An LLM-based system continues to change after its first successful evaluation, so evaluation should not be a one-time exercise. Teams revise prompts, adopt new models, change how inputs and context are constructed, and refine the surrounding business logic.</p>



<p class="wp-block-paragraph">Any of these changes can improve the system, introduce a regression, or shift its behavior in an unexpected way.</p>



<p class="wp-block-paragraph">For that reason, we treated offline evaluation similarly to an end-to-end integration test. We reran it whenever we made a meaningful change to the prompt, model, input construction, or broader system logic.</p>



<p class="wp-block-paragraph">The evaluation also needed to be repeatable enough that each new result could be compared against a known baseline. For every run, we recorded the prompt, model, dataset version, and system configuration.</p>



<p class="wp-block-paragraph">This made it possible to answer questions such as:</p>



<ul class="wp-block-list">
<li>Did the new prompt improve precision without reducing recall?</li>



<li>Did the model upgrade help across the dataset or only within certain categories?</li>



<li>Did a change to the input or context fix one error pattern while introducing another?</li>



<li>Did a change to the surrounding logic improve the result consistently, or simply shift where errors appeared?</li>
</ul>



<p class="wp-block-paragraph">Without this discipline, teams can easily compare results generated under different conditions and attribute an improvement to the wrong change.</p>



<h3 id="h-change-one-major-variable-at-a-time" class="wp-block-heading">Change one major variable at a time</h3>



<p class="wp-block-paragraph">Repeatability alone is not enough. Experiments also need to be designed so that the cause of a result is clear.</p>



<p class="wp-block-paragraph">We changed one major variable at a time and compared each run against a known baseline. For example, we evaluated a prompt revision separately from a model upgrade before testing the two together.</p>



<p class="wp-block-paragraph">This mattered because even small prompt changes could shift model behavior, while a model upgrade could affect quality, cost, latency, or output consistency. If both changed in the same experiment, we would not know which one caused the improvement or regression.</p>



<p class="wp-block-paragraph">We treated prompts and evaluation configurations like code. We versioned them, recorded what changed, kept previous configurations reproducible, and made rollback possible.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><strong>Run ID</strong>&nbsp;</th><th><strong>Prompt version</strong>&nbsp;</th><th><strong>Model version</strong>&nbsp;</th><th><strong>Precision</strong>&nbsp;</th><th><strong>Recall</strong>&nbsp;</th><th><strong>Latency</strong>&nbsp;</th><th><strong>Notes</strong>&nbsp;</th></tr></thead><tbody><tr><td>R-001&nbsp;</td><td>v1&nbsp;</td><td>Model A&nbsp;</td><td>0.71&nbsp;</td><td>0.78&nbsp;</td><td>1.2s&nbsp;</td><td>Baseline&nbsp;</td></tr><tr><td>R-002&nbsp;</td><td>v2&nbsp;</td><td>Model A&nbsp;</td><td>0.75&nbsp;</td><td>0.77&nbsp;</td><td>1.2s&nbsp;</td><td>Prompt-only change&nbsp;</td></tr><tr><td>R-003&nbsp;</td><td>v1&nbsp;</td><td>Model B&nbsp;</td><td>0.74&nbsp;</td><td>0.80&nbsp;</td><td>1.0s</td><td>Model-only change&nbsp;</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><em>The values in the evaluation run tracking table above shown are hypothetical and included only to illustrate how evaluation runs can be tracked and compared.</em></p>



<h3 id="h-test-model-upgrades-regularly" class="wp-block-heading">Test model upgrades regularly</h3>



<p class="wp-block-paragraph">When an LLM system underperforms, developers often respond by adding more instructions to the prompt. Sometimes that helps, but not always. For example, the prompt may be carrying complexity that comes from the model itself.</p>



<p class="wp-block-paragraph">A stronger model may perform better with a simpler prompt than an older model does with extensive tuning. Simpler prompts are also easier to understand, test, and maintain.</p>



<p class="wp-block-paragraph">Model upgrades still need careful evaluation. A new model may improve performance in one category while introducing regressions elsewhere. It may also affect cost, latency, output formatting, or compatibility with the existing pipeline.</p>



<p class="wp-block-paragraph">The evaluation process should be inexpensive and repeatable enough that testing a new model becomes routine. Any meaningful change to the prompt, model, or pipeline should go through offline evaluation before reaching production.</p>



<h2 id="h-3-keep-offline-evaluation-close-to-production" class="wp-block-heading">3. Keep offline evaluation close to production</h2>



<p class="wp-block-paragraph">An offline evaluation is only useful when it resembles the task the system will perform in production.</p>



<p class="wp-block-paragraph">In a secret-scanning workflow, the model is rarely evaluating one clean, isolated value. It may need to assess a specific candidate alongside surrounding code and other information that is relevant, incomplete, or potentially distracting. Differences in how that information is presented can materially affect the result.</p>



<p class="wp-block-paragraph">Our offline evaluation therefore needed to preserve the important characteristics of the production task, including:</p>



<ul class="wp-block-list">
<li>The candidate being evaluated</li>



<li>The surrounding context available to the model</li>



<li>Relevant supporting information</li>



<li>The way inputs are formatted and constrained</li>



<li>The broader system logic around the model</li>
</ul>



<p class="wp-block-paragraph">Even small differences can skew the results. A cleaner dataset may exclude ambiguous cases, provide more complete context, or remove nearby values that could distract the model.</p>



<p class="wp-block-paragraph">Consider a simplified example:</p>


<div class="wp-block-code-wrapper">
<pre class="wp-block-code language-plaintext"><code>example_token = "sample_value_for_documentation" 
production_api_key = get_secret_from_environment() 
candidate_value = "flagged_value"</code></pre>
<clipboard-copy aria-label="Copy" class="code-copy-btn" data-copy-feedback="Copied!" value='example_token = "sample_value_for_documentation" 
production_api_key = get_secret_from_environment() 
candidate_value = "flagged_value"' tabindex="0" role="button"><svg aria-hidden="true" height="16" viewbox="0 0 16 16" version="1.1" width="16" class="octicon octicon-copy js-clipboard-copy-icon"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg><svg aria-hidden="true" height="16" viewbox="0 0 16 16" version="1.1" width="16" class="octicon octicon-check js-clipboard-check-icon"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg></clipboard-copy></div>


<p class="wp-block-paragraph">Suppose <code>candidate_value</code> is the value the system is expected to assess. The model may instead focus on <code>example_token</code> because its variable name appears more security-relevant, producing a plausible explanation about the wrong value.</p>



<p class="wp-block-paragraph">This kind of failure is easy to miss when evaluation examples contain only one obvious candidate. It surfaced because the offline evaluation preserved some of the ambiguity and distractions found in real secret-scanning workflows.</p>



<p class="wp-block-paragraph">The closer the offline pipeline is to the production pipeline, the more useful the evaluation becomes. When the two differ, a strong offline score may simply reflect an easier problem than the one being deployed.</p>



<h2 id="h-4-treat-production-labels-as-signals-not-unquestionable-truth" class="wp-block-heading">4. Treat production labels as signals, not unquestionable truth</h2>



<p class="wp-block-paragraph">Production data can make an evaluation more representative, but its labels often capture workflow outcomes rather than reliable ground truth. A dismissed or resolved secret-scanning alert, for example, does not necessarily represent a false positive.</p>



<p class="wp-block-paragraph">A developer might resolve an alert because:</p>



<ul class="wp-block-list">
<li>The credential was rotated</li>



<li>The risk was accepted</li>



<li>The alert needed to be cleared to unblock a workflow</li>



<li>The alert was incorrectly classified</li>
</ul>



<p class="wp-block-paragraph">These outcomes may look similar in product data while representing different ground-truth states.</p>



<p class="wp-block-paragraph">Before using production labels, ask:</p>



<ul class="wp-block-list">
<li>How was the label created?</li>



<li>Does it match the question the evaluation is trying to answer?</li>



<li>Are different workflow outcomes being grouped into the same category?</li>
</ul>



<p class="wp-block-paragraph">For important or ambiguous subsets, you may need to complete a manual review. You&rsquo;re not trying to eliminate every imperfect label, but you need to make sure the evaluation data is accurate enough to support the decision being made.</p>



<h2 id="h-5-use-synthetic-and-open-datasets-to-fill-coverage-gaps" class="wp-block-heading">5. Use synthetic and open datasets to fill coverage gaps</h2>



<p class="wp-block-paragraph">Representative production data may be limited, sensitive, or unavailable early in development. Synthetic examples, academic benchmarks, and open datasets can help developers bootstrap an evaluation and expand coverage, but these examples should supplement rather than stand in for production-like data.</p>



<p class="wp-block-paragraph">With that in mind, synthetic examples can greatly help fill in the gaps for testing cases that are rare or difficult to collect, such as ambiguous inputs, missing context, unusual formatting, and underrepresented failure patterns. A list of credential strings, for example, can test whether a model recognizes common formats, but it cannot fully evaluate how the model reasons about a candidate within real code.</p>



<p class="wp-block-paragraph">We adapted external examples to match our task and reviewed labels that did not align with our product definition. We also used realistic failure patterns to create targeted synthetic cases involving nearby credential-like values, test code, placeholders, indirect references, and missing context.</p>



<h2 id="h-6-use-error-analysis-to-find-what-aggregate-metrics-hide" class="wp-block-heading">6. Use error analysis to find what aggregate metrics hide</h2>



<p class="wp-block-paragraph">Aggregate metrics tell you whether a system improved overall. Error analysis tells you what to change next.</p>



<p class="wp-block-paragraph">A higher precision score doesn&rsquo;t reveal whether the remaining errors come from ambiguous inputs, poor prompt framing, missing context, noisy labels, or a narrow dataset.</p>



<p class="wp-block-paragraph">To understand those problems, inspect the failures.</p>



<p class="wp-block-paragraph">We reviewed samples of false positives and false negatives and grouped them by their likely source: the model, prompt, input, pipeline, dataset, or label. The recurring issues included several already discussed, such as reasoning about the wrong candidate, missing context, and labels that did not match the evaluation definition.</p>



<p class="wp-block-paragraph">Each category suggested a different response. Reasoning about the wrong value pointed to prompt or input framing, missing evidence pointed to context construction, and incorrect labels required data cleanup. Repeated domain-specific ambiguity could indicate the need for a clearer product policy or a dedicated evaluation category.</p>



<p class="wp-block-paragraph">Manually reviewing dozens or hundreds of examples takes time, but it often leads to faster progress. Once a recurring failure pattern is clear, the team can make a targeted change and measure whether it solved the problem.</p>



<p class="wp-block-paragraph">A useful question for each error is: <em>Did this failure come from the model, prompt, input, pipeline, dataset, or label?</em></p>



<p class="wp-block-paragraph">That classification turns a vague quality problem into a concrete engineering task.</p>



<h2 id="h-7-use-llm-as-judge-to-focus-human-review" class="wp-block-heading">7. Use LLM-as-judge to focus human review</h2>



<p class="wp-block-paragraph">Reviewing every evaluation example manually may not scale. LLM-as-judge can reduce that burden by classifying clear cases, identifying potentially mislabeled examples, and prioritizing ambiguous cases for human review. Because the judge can also make mistakes or agree with another model for the wrong reason, its output should be treated as another prediction rather than ground truth.</p>



<p class="wp-block-paragraph">A safer pattern is to use the judge for triage:</p>



<ol class="wp-block-list">
<li>Automatically process clear, low-risk cases.</li>



<li>Route low-confidence, conflicting, or high-impact cases to human reviewers.</li>



<li>Periodically sample high-confidence cases to check for systematic errors.</li>



<li>Track disagreement between the judge, the evaluated system, and human reviewers.</li>



<li>Version and evaluate the judge prompt like any other model component.</li>
</ol>



<p class="wp-block-paragraph">Used this way, the judge concentrates human attention on the cases where review is most likely to change the outcome.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" height="692" width="1024" src="https://github.blog/wp-content/uploads/2026/08/633087173-78b05bce-4a6d-4730-a35a-968bc56ec8a8.png?resize=1024%2C692" alt="Diagram titled &ldquo;Human review triage funnel.&rdquo; All evaluation examples enter the funnel and are sorted into four groups: clear agreement, low confidence, model and label disagreement, and high-impact cases. Clear-agreement examples move to automated processing, while the other three groups go to human review for outcome decisions and label correction. Reviewed examples, corrected labels, and new test cases feed back into the evaluation dataset." class="wp-image-98320" srcset="https://github.blog/wp-content/uploads/2026/08/633087173-78b05bce-4a6d-4730-a35a-968bc56ec8a8.png?w=1920 1920w, https://github.blog/wp-content/uploads/2026/08/633087173-78b05bce-4a6d-4730-a35a-968bc56ec8a8.png?w=300 300w, https://github.blog/wp-content/uploads/2026/08/633087173-78b05bce-4a6d-4730-a35a-968bc56ec8a8.png?w=768 768w, https://github.blog/wp-content/uploads/2026/08/633087173-78b05bce-4a6d-4730-a35a-968bc56ec8a8.png?w=1024 1024w, https://github.blog/wp-content/uploads/2026/08/633087173-78b05bce-4a6d-4730-a35a-968bc56ec8a8.png?w=1536 1536w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>



<h2 id="h-8-what-secret-scanning-taught-us" class="wp-block-heading">8. What secret scanning taught us</h2>



<p class="wp-block-paragraph">Our goal was to reduce false positives while preserving recall in a security-sensitive workflow. Offline evaluation gave us a controlled way to compare prompt, model, input, and pipeline changes before beginning online experimentation.</p>



<p class="wp-block-paragraph">Through repeated evaluation and targeted error analysis, we reached a 95% reduction in false positives on the evaluated offline dataset while keeping recall within our defined guardrail. More importantly, we understood how the result had been produced: the evaluation reflected the production task more closely, changes were measured against reproducible baselines, and the remaining failure patterns were documented.</p>



<p class="wp-block-paragraph">Offline evaluation did not prove how the system would behave in every production scenario. It provided enough structured evidence to justify moving to online experimentation with clearly understood risks and guardrails.</p>



<h2 id="h-checklist-before-moving-an-llm-system-toward-production" class="wp-block-heading">Checklist: Before moving an LLM system toward production</h2>



<p class="wp-block-paragraph">Use this checklist to assess whether your evaluation provides enough evidence to move the system forward. Work through each section to confirm that the goals, data, experiments, and remaining production risks are clearly understood.</p>



<p class="wp-block-paragraph"><strong>Product Goals</strong></p>



<ul class="wp-block-list">
<li>Is the product decision and primary success metric clear?</li>



<li>Are the safety and operational guardrails defined?</li>
</ul>



<p class="wp-block-paragraph"><strong>Data and Labels</strong></p>



<ul class="wp-block-list">
<li>Does the evaluation data resemble the production workflow and include difficult cases?</li>



<li>Do we understand how the labels were created and where human review is needed?</li>
</ul>



<p class="wp-block-paragraph"><strong>Evaluation Rigor</strong></p>



<ul class="wp-block-list">
<li>Are the prompt, model, dataset, and pipeline versions recorded?</li>



<li>Are major changes isolated and compared against a known baseline?</li>
</ul>



<p class="wp-block-paragraph"><strong>Error Analysis and Production Readiness</strong></p>



<ul class="wp-block-list">
<li>Have false positives and false negatives been reviewed by category?</li>



<li>Can we rerun the evaluation and explain where offline results may differ from production?</li>
</ul>



<h2 id="h-evaluate-before-you-trust" class="wp-block-heading">Evaluate before you trust</h2>



<p class="wp-block-paragraph">As LLM-based systems move into production, evaluation should become part of the regular engineering workflow. A strong offline evaluation can show whether the product goal has been met under representative conditions, where uncertainty remains, and whether the system is ready for a controlled production rollout.</p>



<p class="wp-block-paragraph">Production uncertainty is unavoidable. Evaluation makes it visible, measurable, and manageable.</p>



<div class="wp-block-group post-content-cta has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><a href="https://docs.github.com/code-security/concepts/secret-security/secret-scanning">Explore secret scanning documentation &gt;</a></p>
</div>
</body></html>
<p>The post <a href="https://github.blog/ai-and-ml/llms/how-to-evaluate-llms-before-production/">How to evaluate LLMs before production</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">98316</post-id>	</item>
	</channel>
</rss>
