Files
blog/pi-flow-native-brain.html
T

382 lines
24 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Retiring the Supervisor: Pi's Flow-Native Brain — Tinqs Blog</title>
<meta name="description" content="We deleted 1,050 lines of hardcoded supervisor logic and replaced it with oracle-backed pi-flows. The verify_build oracle now powers a gate-based pipeline that agents compose dynamically.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/pi-flow-native-brain">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/pi-flow-native-brain">
<meta property="og:title" content="Retiring the Supervisor: Pi's Flow-Native Brain">
<meta property="og:description" content="Pi's standalone supervisor is gone — replaced by a flow-native brain with oracle-backed gates.">
<meta property="og:image" content="https://www.tinqs.com/img/og-cover.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Retiring the Supervisor: Pi's Flow-Native Brain">
<meta name="twitter:description" content="Pi's standalone supervisor is gone — replaced by a flow-native brain with oracle-backed gates.">
<meta name="twitter:image" content="https://www.tinqs.com/img/og-cover.jpg">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Retiring the Supervisor: Pi's Flow-Native Brain",
"datePublished": "2026-06-03",
"author": {
"@type": "Person",
"name": "Ozan Bozkurt"
},
"publisher": {
"@type": "Organization",
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "We deleted 1,050 lines of hardcoded supervisor logic and replaced it with oracle-backed pi-flows. The verify_build oracle now powers a gate-based pipeline that agents compose dynamically."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
<style>
/* ── Team guide aesthetic: self-contained overrides ── */
/* ── Gradient title (amber → warm gold, hint of blue) ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #38bdf8;
border: 1px solid rgba(147, 140, 129, 0.25);
border-radius: 999px;
padding: 4px 14px;
margin-bottom: 16px;
}
/* ── Lead ── */
.post__lead {
color: #9aa7b4;
font-size: 1.08rem;
line-height: 1.7;
}
/* ── H2: left accent bar ── */
.post__body h2 {
font-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid #c9935a;
}
/* ── H3: purple secondary accent ── */
.post__body h3 {
color: #a855f7;
font-size: 1.18rem;
margin: 30px 0 4px;
}
/* ── Inline code ── */
.post__body code {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.86em;
background: #1c2230;
color: #9fe6c0;
padding: 2px 6px;
border-radius: 5px;
border: 1px solid #2a3340;
}
/* ── Code blocks (dark panel) ── */
.post__body pre {
background: #0a0e14;
border: 1px solid #2a3340;
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: #e6edf3;
}
/* Reset inline-code double-up inside pre */
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote callout (ready for future use; build.js does not emit blockquote yet) ── */
.post__body blockquote {
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.25);
border-left: 4px solid #f59e0b;
border-radius: 0 12px 12px 0;
padding: 16px 18px;
margin: 18px 0;
color: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a {
color: #38bdf8;
}
.post__body a:hover {
color: #a855f7;
}
/* ── Strong ── */
.post__body strong {
color: #f59e0b;
}
/* ── HR ── */
.post__body hr {
border: none;
border-top: 1px solid #2a3340;
margin: 32px 0;
}
/* ── Figures ── */
.post__body figure img {
border-radius: 12px;
border: 1px solid #2a3340;
}
.post__body figcaption {
color: #9aa7b4;
font-size: 0.85rem;
margin-top: 6px;
}
/* ── List spacing ── */
.post__body li {
margin: 4px 0;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">3 June 2026</span>
<h1 class="post__title">Retiring the Supervisor: Pi's Flow-Native Brain</h1>
<p class="post__lead">The supervisor was 1,050 lines of TypeScript spread across 15 files — a hardcoded orchestration loop that ran contract-gated, verify-heavy sessions over isolated Pi processes. Today we deleted it. What replaced it is simpler, more flexible, and already passing real builds.</p>
<div class="post__body">
<h2>What the Supervisor Did</h2>
<p>The <code>.pi/supervisor/</code> directory was the orchestration brain Pi left to us. For every task, it ran a fixed loop:</p>
<p>1. <strong>Contract gate</strong> — skip-to-human if "done" wasn't programmatically verifiable</p>
<p>2. <strong>TDAID phase A</strong> — a test-writer agent writes RED tests, never implementation</p>
<p>3. <strong>TDAID phase B</strong> — a code-writer agent makes them green; on failure, a Reflexion follow-up retries (capped)</p>
<p>4. <strong>Verification gate</strong> — run the build, check tests, pass or fail with a report</p>
<p>It worked. It caught broken builds before they hit CI. It enforced the discipline of "define done before you start." But it had a structural problem: the loop was <strong>hardcoded</strong>. Every decision tree, every gate, every retry policy was baked into TypeScript. To change the workflow, you changed code. To add a new gate — vision QA, linting, asset validation — you added more code to the same monolithic loop.</p>
<p>The supervisor was doing what <code>pi-flows</code> was designed to do, but from the wrong side of the architecture. Flows composes agents, gates, and decision points into pipelines. The supervisor reimplemented that logic in a single file. It was fighting the framework.</p>
<h2>What Replaced It</h2>
<p>The verify-heavy brain now runs <strong>as a pi-flows flow</strong> — a pipeline of oracle-backed gates orchestrated by the flow engine, visualized in FlowDashboard, and composable by agents themselves.</p>
<p>The core pieces:</p>
<ul>
<li><strong>Oracle-backed gates.</strong> The <code>verify_build</code> tool (<code>.pi/extensions/tinqs-verify.ts</code>) is the canonical gate. It compiles the game and sim, runs tests, and returns a structured PASS/FAIL verdict with file:line errors. Agents route through it; the gate decides whether to proceed.</li>
<li><strong>Agent-loop-decision Reflexion.</strong> Instead of a fixed two-phase TDAID loop, agents self-reflect on build failures. The flow engine gives them the failure report; they decide whether to fix and retry or escalate.</li>
<li><strong>Role-split agents.</strong> G1 build, G2 tests, G3 behaviour (drives the live game), G4 feel (measured game-feel) and G5 visual (animation) are separate sub-agents, each with its own toolset and context, composed by the flow.</li>
</ul>
<p>The result is a pipeline that flows naturally — a plan, an implementation, then a ladder of oracle-backed gates:</p>
<figure style="margin:28px 0;">
<svg viewBox="0 0 920 350" role="img" aria-label="The verify-heavy flow: context, plan, implement, five gates, a Reflexion loop, and one judge" style="width:100%;height:auto;display:block;background:#0a0e14;border:1px solid #2a3340;border-radius:12px;font-family:'IBM Plex Sans',system-ui,sans-serif;">
<defs>
<marker id="ah" markerWidth="10" markerHeight="10" refX="7" refY="3.2" orient="auto"><path d="M0,0 L7,3.2 L0,6.4 Z" fill="#5b6b7d"/></marker>
<marker id="ahA" markerWidth="10" markerHeight="10" refX="7" refY="3.2" orient="auto"><path d="M0,0 L7,3.2 L0,6.4 Z" fill="#f59e0b"/></marker>
</defs>
<rect x="40" y="40" width="140" height="46" rx="9" fill="#121821" stroke="#2a3340"/>
<text x="110" y="68" text-anchor="middle" fill="#cdd7e2" font-size="15">Context</text>
<rect x="210" y="40" width="140" height="46" rx="9" fill="#121821" stroke="#2a3340"/>
<text x="280" y="68" text-anchor="middle" fill="#cdd7e2" font-size="15">Plan</text>
<rect x="400" y="40" width="150" height="46" rx="9" fill="#15202e" stroke="#3a4656"/>
<text x="475" y="68" text-anchor="middle" fill="#e6edf3" font-size="15">Implement</text>
<line x1="180" y1="63" x2="206" y2="63" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
<line x1="350" y1="63" x2="396" y2="63" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
<rect x="40" y="150" width="840" height="82" rx="12" fill="#0c1119" stroke="#2a3340"/>
<text x="56" y="171" fill="#6b7a8d" font-size="11" letter-spacing="1.4">VERIFY-HEAVY GATES — most compute is spent checking, not writing</text>
<rect x="56" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#38bdf8" stroke-opacity="0.55"/>
<text x="130" y="206" text-anchor="middle" fill="#38bdf8" font-size="13.5">G1 · Build</text>
<rect x="222" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#34d399" stroke-opacity="0.55"/>
<text x="296" y="206" text-anchor="middle" fill="#9fe6c0" font-size="13.5">G2 · Tests</text>
<rect x="388" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#a855f7" stroke-opacity="0.55"/>
<text x="462" y="206" text-anchor="middle" fill="#c4a0f7" font-size="13.5">G3 · Behaviour</text>
<rect x="554" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#f59e0b" stroke-opacity="0.55"/>
<text x="628" y="206" text-anchor="middle" fill="#f5b44b" font-size="13.5">G4 · Feel</text>
<rect x="720" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#c9935a" stroke-opacity="0.55"/>
<text x="794" y="206" text-anchor="middle" fill="#d9ac7b" font-size="13.5">G5 · Visual</text>
<line x1="475" y1="86" x2="475" y2="148" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
<line x1="460" y1="232" x2="460" y2="276" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
<text x="472" y="258" fill="#6b7a8d" font-size="11">all green &#8658; done&#8195;&#183;&#8195;any fail &#8658; report</text>
<rect x="380" y="278" width="160" height="46" rx="9" fill="#1b1505" stroke="#c9935a"/>
<text x="460" y="306" text-anchor="middle" fill="#f3d6a0" font-size="15">Judge &#8212; honest verdict</text>
<path d="M820,150 C 908,96 716,50 556,61" fill="none" stroke="#f59e0b" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#ahA)"/>
<text x="694" y="96" fill="#f59e0b" font-size="12.5">Reflexion &#183; fix &amp; retry &#8804; 3</text>
</svg>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:8px;">A real in-game failure loops back to <em>implement</em> with the gate evidence (bounded to three tries); anything green — or skipped because no live instance is running — falls through to a single honest judge.</figcaption>
</figure>
<p>It started as three gates — build, test, vision. Gates are cheap to add, so it grew: a feature now also passes a live-game <strong>behaviour</strong> probe and a measured <strong>feel</strong> check before the judge signs off. Critically, the flow is not fixed. Agents can add gates, reorder steps, or branch on conditions. The flow engine handles orchestration; the agents handle decisions.</p>
<h2>What We Deleted</h2>
<p>The commit removes 1,050 lines across 15 files:</p>
<ul>
<li><code>runner.ts</code> (115 lines) — the main orchestration loop</li>
<li><code>supervisor.ts</code> (119 lines) — the state machine driving sessions</li>
<li><code>gates.ts</code> (75 lines) — hardcoded gate definitions</li>
<li><code>policy.ts</code> (92 lines) — retry limits and decision logic</li>
<li><code>store.ts</code> (54 lines) — session state persistence</li>
<li><code>types.ts</code> (76 lines) — type definitions for the whole system</li>
<li><code>events.ts</code> (47 lines) — inter-process event bus</li>
<li>Plus tests, examples, and documentation</li>
</ul>
<figure style="margin:24px 0;">
<svg viewBox="0 0 920 180" role="img" aria-label="Lines of code: 1,050 deleted versus about 300 kept" style="width:100%;height:auto;display:block;background:#0a0e14;border:1px solid #2a3340;border-radius:12px;font-family:'IBM Plex Sans',system-ui,sans-serif;">
<text x="40" y="34" fill="#9aa7b4" font-size="13">Net change: <tspan fill="#f59e0b" font-weight="600">&#8722;750 lines</tspan>, &#43; a composable pipeline</text>
<text x="40" y="76" fill="#f0816a" font-size="13">Deleted</text>
<rect x="150" y="58" width="730" height="30" rx="6" fill="#2a1416" stroke="#f0816a" stroke-opacity="0.6"/>
<text x="868" y="78" text-anchor="end" fill="#f3b4a8" font-size="12.5">supervisor/ &#8212; 1,050 lines &#183; 15 files</text>
<text x="40" y="136" fill="#34d399" font-size="13">Kept</text>
<rect x="150" y="118" width="209" height="30" rx="6" fill="#0f2a22" stroke="#34d399" stroke-opacity="0.6"/>
<text x="369" y="138" fill="#9fe6c0" font-size="12.5">verify_build &#8212; ~300 lines &#183; 1 oracle</text>
</svg>
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:8px;">The whole orchestration loop was deleted; only the build oracle survived — and it became the gate that powers the flow.</figcaption>
</figure>
<p>None of this was bad code. It was just the wrong layer. Flows gives us all of this — orchestration, state, gates, retry policy, event routing — as a framework primitive. We were maintaining a parallel implementation of something the framework already provided.</p>
<p>The durable asset we kept: <code>verify_build</code>, the build oracle. It's now reused as the gate tool that powers the flow pipeline.</p>
<h2>The Bug That Took a Day to Find</h2>
<p>Moving to flows exposed a subtle problem. Flow sub-agents run in their <strong>own extension stack</strong> — the main session's extensions don't reach them. The build-verifier and test-runner agents declared <code>verify_build</code> in their frontmatter, but the tool was never actually in their toolset.</p>
<p>The symptom was confusing: the agents would report "oracle not available" and route to fail/report, silently skipping the test gate entirely. The build would pass, tests would never run, and the pipeline would report success. A false green.</p>
<p>The fix was a single pattern: emit <code>flow:register-tool</code> with the full tool definition at extension activation, and re-announce on <code>flow:rediscover</code>. The flow engine collects these into <code>getExtensionTools()</code> and hands them to every sub-agent that declares the tool. Three lines of orchestration, a day of debugging.</p>
<p>Verified live: <code>game-check</code> now routes <code>context → build → build-gate(pass) → tests → tests-gate(pass) → vision</code>. Every gate fires. No false greens.</p>
<h2>Why This Architecture Wins</h2>
<p><strong>Composability.</strong> Agents can add gates without touching framework code. Want a linting gate? Add a sub-agent with a linter tool. Want a security scan? Same pattern. The flow engine handles routing; you just declare the gate.</p>
<p><strong>Reusability.</strong> The <code>verify_build</code> oracle that powered the old supervisor now powers the flow gates. Same tool, same interface, different orchestration. No rewrite needed.</p>
<p><strong>Observability.</strong> FlowDashboard visualizes the entire pipeline. You can see which gates passed, which failed, and where the agent decided to retry. The old supervisor logged to stdout.</p>
<p><strong>Self-modification.</strong> Agents can read the flow graph, understand where they are in the pipeline, and decide what to do next. The supervisor's decision tree was opaque to the agents it was supervising. Flows makes the pipeline itself part of the agent's context.</p>
<h2>The Stack Today</h2>
<table style="width:100%;border-collapse:collapse;margin:18px 0;font-size:0.92rem;">
<thead>
<tr style="text-align:left;border-bottom:1px solid #2a3340;">
<th style="padding:10px 12px;color:#c9935a;font-weight:600;">Layer</th>
<th style="padding:10px 12px;color:#c9935a;font-weight:600;">What</th>
<th style="padding:10px 12px;color:#c9935a;font-weight:600;">How</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Flow engine</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">pi-flows orchestrator</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Composes agents, gates and decision points</td></tr>
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Gates</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">verify_build oracle</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Compiles, tests, returns PASS/FAIL with file:line errors</td></tr>
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Sub-agents</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">G1 build &#183; G2 tests &#183; G3 behaviour &#183; G4 feel &#183; G5 visual</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Role-split, each with its own toolset</td></tr>
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Decision</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">Agent-loop Reflexion</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Self-reflect on failures, retry (&#8804;3) or escalate</td></tr>
<tr><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Visualization</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">FlowDashboard</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Real-time pipeline state at localhost:33634</td></tr>
</tbody>
</table>
<hr>
<p>The old supervisor was 1,050 lines of code that did one thing well: verify that agent output compiled and passed tests. The new flow-native brain does the same thing with less code, more flexibility, and a bug we'll never hit again. Sometimes the best commit is a deletion.</p>
<p><em>The flow-native brain runs on our <a href="https://tinqs.com/tinqs/pi" style="color: var(&ndash;c-accent-l);">Pi fork</a> inside <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a>. The verify_build extension is ~300 lines of TypeScript, MIT licensed, and reusable in any Pi project.</em></p>
</div>
<div class="post__author">
<div class="post__author-avatar">OB</div>
<div class="post__author-info">
<span class="post__author-name">Ozan Bozkurt</span><br>
CTO & Developer, Tinqs
</div>
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>