Files
portfolio-next/mockups/tech-5.html
T
Lucas WintherandClaude Opus 5.5 843b6d32bc chore: bootstrap agent-ready docs, rules and design-phase artefacts
Add AGENTS.md as the single source of truth (with CLAUDE.md and GEMINI.md
as pointers), permissions for Claude Code, Codex and Gemini, the docs tree
(architecture, decisions, TODO, permissions, specs, plans), PRODUCT.md and
DESIGN.md, plus the design-phase record: the inspiration log, all mockup
rounds with briefs and screenshots, and the owner's work history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 06:47:38 +01:00

342 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lucas Winther: lab notebook, vol. II</title>
<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=Spectral:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Azeret+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--paper:#F4F2EA; --grid:rgba(96,128,140,.13); --graphite:#2E3032; --muted:#6F716F; --red:#C2443A;
--hl:#F1DE6E; --hl-soft:rgba(241,222,110,.45); --rule:#CFCFC6;
--serif:"Spectral",Georgia,serif; --mono:"Azeret Mono",ui-monospace,monospace;
}
*{box-sizing:border-box}
html{background:var(--paper)}
body{margin:0;color:var(--graphite);font-family:var(--serif);font-size:17px;line-height:1.55;
background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:24px 24px}
a{color:inherit;text-decoration-color:var(--red);text-underline-offset:3px}
a:hover{color:var(--red)}
.ph{text-decoration:underline dotted var(--muted);text-underline-offset:3px;cursor:help}
.mono{font-family:var(--mono);font-size:11.5px;letter-spacing:.02em}
.mock-note{position:fixed;top:10px;right:10px;z-index:9999;max-width:290px;background:#fff;color:#222;border:1px solid #aaa;padding:8px 30px 8px 10px;font:12px/1.4 system-ui,sans-serif}
.mock-note b{display:block}
.mock-note button{position:absolute;top:4px;right:6px;border:0;background:none;font-size:16px;cursor:pointer;color:#555}
.page{max-width:1040px;margin:0 auto;padding:48px 32px 40px;display:grid;grid-template-columns:150px 1fr;gap:0 34px;position:relative}
.page::before{content:"";position:absolute;top:0;bottom:0;left:calc(32px + 150px + 16px);width:1px;background:var(--red);opacity:.55}
.m{grid-column:1;text-align:right;color:var(--muted);font-size:14px;font-style:italic;line-height:1.4;padding-top:4px}
.m .mono{font-style:normal;display:block;color:var(--graphite)}
.b{grid-column:2;min-width:0}
/* cover */
.cover h1{font-weight:300;font-size:52px;line-height:1.02;margin:0;letter-spacing:-.01em}
.cover h1 i{font-size:26px;color:var(--muted);display:block;margin-top:6px;font-weight:300}
.label{margin:26px 0 0;border:1.5px solid var(--graphite);background:var(--paper);display:grid;grid-template-columns:1fr 1fr;max-width:760px}
.label > div{padding:12px 16px}
.label > div + div{border-left:1px solid var(--graphite)}
.label .mono{display:block;color:var(--muted);text-transform:uppercase;margin-bottom:2px;font-size:10.5px}
.label p{margin:0 0 8px;font-size:16px;line-height:1.4}
.label ol{margin:0;padding:0;list-style:none;font-size:15px}
.label li{display:grid;grid-template-columns:82px 1fr;gap:8px;padding:2px 0}
.label li span:first-child{font-family:var(--mono);font-size:11px;color:var(--muted);padding-top:3px}
.label .bar{grid-column:1 / -1;border-top:1px solid var(--graphite);padding:8px 16px;display:flex;gap:18px;flex-wrap:wrap;font-family:var(--mono);font-size:12px}
.volumes{margin:18px 0 0;max-width:760px;font-size:16px}
.volumes b{font-weight:600}
h2{font-weight:400;font-size:28px;margin:0 0 6px}
.sec{margin-top:56px}
/* stamps filter */
.stamps{display:flex;flex-wrap:wrap;gap:12px 16px;margin:12px 0 18px;align-items:center}
.stamp{font-family:var(--mono);font-size:11.5px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;border:2px solid currentColor;padding:5px 10px 4px;background:transparent;cursor:pointer;transform:rotate(-1.5deg);transition:opacity .15s,transform .15s}
.stamp:nth-child(2){transform:rotate(1deg)}
.stamp:nth-child(3){transform:rotate(-.5deg)}
.stamp.hand{color:var(--graphite)}
.stamp.mix{color:#8A6D12;background:var(--hl-soft)}
.stamp.ai{color:var(--red)}
.stamp[aria-pressed="false"]{opacity:.28;text-decoration:line-through}
.stamp:hover{transform:rotate(0)}
.stamps .hint{color:var(--muted);font-style:italic;font-size:14px}
.tag-stamp{display:inline-block;font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;border:1.5px solid currentColor;padding:1px 6px;transform:rotate(-2deg);vertical-align:2px}
/* contents */
.toc{border-top:1.5px solid var(--graphite);margin:0;padding:0;list-style:none}
.toc li{display:grid;grid-template-columns:62px 1fr 110px 92px;gap:12px;align-items:baseline;border-bottom:1px solid var(--rule);padding:6px 0}
.toc li.hide{display:none}
.toc .no{font-family:var(--mono);font-size:11px;color:var(--muted)}
.toc a{text-decoration:none}
.toc a:hover{text-decoration:underline;text-decoration-color:var(--red)}
.toc .kind{font-style:italic;color:var(--muted);font-size:14px}
.prov{display:flex;height:8px;border:1px solid var(--graphite);background:var(--paper)}
.prov i{display:block}
.prov .h{background:repeating-linear-gradient(90deg,var(--graphite) 0 1px,transparent 1px 3px)}
.prov .a{background:var(--hl)}
.toc .note-row .t{font-style:italic}
/* entries */
.entry{margin:0 0 30px;scroll-margin-top:20px}
.entry.hide{display:none}
details.case{border-top:1.5px solid var(--graphite)}
details.case > summary{list-style:none;cursor:pointer;padding:10px 0 8px;display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
details.case > summary::-webkit-details-marker{display:none}
details.case > summary::before{content:"+";font-family:var(--mono);font-size:14px;width:14px;color:var(--red)}
details.case[open] > summary::before{content:"−"}
summary h3{margin:0;font-size:23px;font-weight:400}
summary .meta{color:var(--muted);font-size:14px;font-style:italic}
summary:hover h3{color:var(--red)}
.case-body{padding:2px 0 8px 26px}
.case-body h4{font-family:var(--mono);font-size:10.5px;font-weight:500;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin:16px 0 6px}
.case-body p{margin:0 0 8px;max-width:38em}
table.who{border-collapse:collapse;width:100%;max-width:720px;font-size:15px;background:var(--paper)}
table.who th{font-family:var(--mono);font-weight:500;font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);text-align:left;padding:4px 10px 4px 0;border-bottom:1px solid var(--graphite)}
table.who td{padding:5px 10px 5px 0;border-bottom:1px solid var(--rule);vertical-align:top}
table.who td:first-child{width:170px}
table.who td.w{width:120px;font-family:var(--mono);font-size:11.5px;white-space:nowrap}
td.w mark{background:var(--hl);color:var(--graphite);padding:0 4px}
td.w .hb{border-bottom:1.5px solid var(--graphite)}
td.w .mx{background:linear-gradient(transparent 55%,var(--hl) 55%);padding:0 4px}
.m > .ph{display:block;width:max-content;margin-left:auto}
ol.dec{margin:0;padding-left:20px;max-width:40em}
ol.dec li{margin:0 0 4px}
.log{margin:0;padding:0;list-style:none;font-size:15.5px;max-width:42em}
.log li{display:grid;grid-template-columns:96px 1fr;gap:10px;padding:3px 0}
.log li span:first-child{font-family:var(--mono);font-size:11px;color:var(--muted);padding-top:4px}
.log .x{color:var(--red)}
.sketch{height:120px;max-width:420px;border:1px dashed var(--graphite);margin:6px 0 10px;position:relative;background:rgba(244,242,234,.7)}
.sketch svg{position:absolute;inset:8px;width:calc(100% - 16px);height:calc(100% - 26px)}
.sketch span{position:absolute;left:8px;bottom:4px;font-family:var(--mono);font-size:10px;color:var(--muted)}
.links{font-family:var(--mono);font-size:12px;display:flex;gap:16px;flex-wrap:wrap;margin-top:8px}
/* blog notes */
.note{border-left:3px solid var(--hl);padding:2px 0 2px 16px;margin:0 0 30px;max-width:40em}
.note h3{margin:0;font-size:20px;font-weight:400;font-style:italic}
.note h3 a{text-decoration:none}
.note h3 a:hover{text-decoration:underline;text-decoration-color:var(--red)}
.note p{margin:4px 0 0;font-size:16px;color:#4A4C4D}
.note .mono{color:var(--muted)}
/* compact older entries */
.older{margin:0;padding:0;list-style:none;border-top:1.5px solid var(--graphite)}
.older li{border-bottom:1px solid var(--rule);padding:8px 0;display:grid;grid-template-columns:1fr auto;gap:4px 16px}
.older li.hide{display:none}
.older b{font-weight:600}
.older .d{grid-column:1 / -1;font-size:15px;color:#4A4C4D}
.foot{border-top:1.5px solid var(--graphite);padding-top:14px;color:var(--muted);font-size:14px}
@media (max-width:760px){
.page{grid-template-columns:1fr;padding:32px 18px}
.page::before{display:none}
.m{grid-column:1;text-align:left;padding:0 0 4px}
.b{grid-column:1}
.b.sec{margin-top:6px}
.m > .ph{margin-left:0}
.label{grid-template-columns:1fr}
.label > div + div{border-left:0;border-top:1px solid var(--graphite)}
.toc li{grid-template-columns:50px 1fr 70px}
.toc .kind{display:none}
.cover h1{font-size:40px}
.case-body{padding-left:0}
table.who td:first-child{width:auto}
}
</style>
</head>
<body>
<div class="mock-note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>tech-5 · The lab notebook</b>Projects as case files that are honest about process: who wrote which part (highlighter = AI), decisions, and a log of what broke. Blog posts are notebook entries in between. The old site is "Volume I". Light identity in the spirit of ntch.design / beige.zone's archival labels.</div>
<div class="page">
<!-- cover -->
<div class="m"><span class="mono">Vol. II</span>2023 →</div>
<div class="b cover">
<h1>Lab notebook of Lucas Winther<i>Builds, decisions and breakages, with AI in the loop</i></h1>
<div class="label">
<div>
<span class="mono">Owner</span>
<p>Lucas Winther, computer scientist. <span class="ph" title="placeholder">Software engineer at Nordlys Systems</span>.</p>
<span class="mono">Reach</span>
<p><a href="mailto:lucas@lucaswinther.info">lucas@lucaswinther.info</a><br><a href="https://www.linkedin.com/in/lucas-winther">linkedin/lucas-winther</a> · <a href="https://github.com/StereotypicalCat">github/StereotypicalCat</a></p>
</div>
<div>
<span class="mono">Experience &amp; education</span>
<ol>
<li><span class="ph" title="placeholder">2023–now</span><span><span class="ph" title="placeholder">Software engineer, Nordlys Systems</span>: platform, CI, deployment</span></li>
<li><span class="ph" title="placeholder">2021–22</span><span><span class="ph" title="placeholder">Student developer, university IT</span></span></li>
<li><span class="ph" title="placeholder">2019–22</span><span>BSc Computer Science, <span class="ph" title="placeholder">Aarhus University</span></span></li>
</ol>
</div>
<div class="bar"><a href="#" title="placeholder: CV PDF">CV (PDF)</a><span>Skills: TypeScript · Node.js · React · Ansible · Docker · Linux · Unity/C#</span></div>
</div>
<p class="volumes"><b>Vol. I</b>, everything from 2016 to 2022, was written entirely by hand. It's kept unaltered as the old site at <a href="https://old.lucaswinther.info">old.lucaswinther.info</a>. This volume marks how much of each build was mine and how much came from an AI.</p>
</div>
<!-- contents -->
<div class="m sec"><span class="mono">p. 1</span>contents</div>
<div class="b sec">
<h2>Contents</h2>
<div class="stamps" role="group" aria-label="Filter by who wrote it">
<button class="stamp hand" data-f="hand" aria-pressed="true">Hand-written</button>
<button class="stamp mix" data-f="mix" aria-pressed="true">AI-assisted</button>
<button class="stamp ai" data-f="ai" aria-pressed="true">AI-made</button>
<span class="hint">Click a stamp to hide or show those entries.</span>
</div>
<ol class="toc" id="toc"></ol>
<p class="mono" style="color:var(--muted);margin-top:8px">Bar = share of the code: <span class="prov" style="display:inline-flex;width:40px;vertical-align:-1px"><i class="h" style="width:100%"></i></span> by hand · <span class="prov" style="display:inline-flex;width:40px;vertical-align:-1px"><i class="a" style="width:100%"></i></span> by AI (highlighter)</p>
</div>
<!-- entries -->
<div class="m sec"><span class="mono">No. 018</span><span class="ph" title="placeholder">14 Oct 2026</span></div>
<div class="b sec"><div class="note entry" data-f="mix" id="n18">
<span class="mono">Note · AI</span>
<h3><a href="https://blog.lucaswinther.info"><span class="ph" title="placeholder title">What I let the agent write, and what I didn't</span></a></h3>
<p>Going back through the gacha tracker file by file and marking every line by origin. The surprise was the tests: I thought I wrote most of them. I wrote the names. →</p>
</div></div>
<div class="m"><span class="mono">No. 017</span><span class="ph" title="placeholder">Sep 2026</span>case file</div>
<div class="b"><details class="case entry" data-f="mix" id="c17" open>
<summary><h3><span class="ph" title="placeholder project">Homelab metrics</span></h3><span class="tag-stamp" style="color:#8A6D12;background:var(--hl-soft)">AI-assisted</span><span class="meta">Prometheus · Grafana · Alertmanager</span></summary>
<div class="case-body">
<h4>Goal</h4>
<p>Know that something is wrong before a family member tells me the photo backup stopped.</p>
<h4>Who wrote what</h4>
<table class="who">
<tr><th>Part</th><th>Written by</th><th>Note</th></tr>
<tr><td>Alert rules</td><td class="w"><span class="hb">by hand</span></td><td>Every alert has to be something I would get up for.</td></tr>
<tr><td>Exporters (UPS, NAS)</td><td class="w"><span class="mx">mixed</span></td><td>AI found the SNMP OIDs; I rewrote the parsing.</td></tr>
<tr><td>Grafana dashboards</td><td class="w"><mark>AI</mark></td><td>Generated JSON, then deleted half the panels.</td></tr>
<tr><td>Compose + Ansible role</td><td class="w"><mark>AI</mark></td><td>Extends the existing Config repo.</td></tr>
</table>
<h4>Decisions</h4>
<ol class="dec"><li>Prometheus over a hosted service: the lab should work offline.</li><li>Four alerts, not forty. Dashboards are for curiosity; alerts are for action.</li></ol>
<h4>What broke</h4>
<ul class="log"><li><span>2026-09-03</span><span class="x">Alert storm on every reboot: 22 notifications for one planned restart.</span></li><li><span>2026-09-04</span><span>Added inhibition rules and a 5-minute <code>for:</code>. Quiet since.</span></li></ul>
<div class="links"><a href="#">Write-up</a><a href="https://github.com/StereotypicalCat">Source</a></div>
</div>
</details></div>
<div class="m"><span class="mono">No. 016</span><span class="ph" title="placeholder">Jun 2026</span></div>
<div class="b"><div class="note entry" data-f="hand" id="n16">
<span class="mono">Note · Homelab</span>
<h3><a href="https://blog.lucaswinther.info"><span class="ph" title="placeholder title">Three mini PCs, one k3s cluster</span></a></h3>
<p>Why the cluster happened, and why the media server went back to plain docker-compose a week later. →</p>
</div></div>
<div class="m"><span class="mono">No. 015</span><span class="ph" title="placeholder">Mar 2025</span>case file</div>
<div class="b"><details class="case entry" data-f="ai" id="c15">
<summary><h3>Event Tracker for Gacha Games</h3><span class="tag-stamp" style="color:var(--red)">AI-made</span><span class="meta">JavaScript · scraping · web app</span></summary>
<div class="case-body">
<h4>Goal</h4>
<p>One calendar for limited-time events across several gacha games, so banners stop slipping by unnoticed.</p>
<div class="sketch"><svg viewBox="0 0 400 90" preserveAspectRatio="none" aria-hidden="true"><g fill="none" stroke="#2E3032" stroke-width="1.2"><rect x="4" y="30" width="80" height="30"/><rect x="160" y="30" width="80" height="30"/><rect x="316" y="30" width="80" height="30"/><path d="M84 45H160M240 45H316"/><path d="M152 41l8 4-8 4M308 41l8 4-8 4"/></g><g font-family="Azeret Mono" font-size="9" fill="#2E3032"><text x="14" y="49">scrapers ×4</text><text x="172" y="49">normalise</text><text x="328" y="49">calendar UI</text></g></svg><span>sketch: pipeline</span></div>
<h4>Who wrote what</h4>
<table class="who">
<tr><th>Part</th><th>Written by</th><th>Note</th></tr>
<tr><td>Spec &amp; acceptance tests</td><td class="w"><span class="hb">by hand</span></td><td>One page of requirements, one failing test per game.</td></tr>
<tr><td>Scrapers</td><td class="w"><mark>AI</mark></td><td>Agent-written against my fixtures.</td></tr>
<tr><td>Event normaliser</td><td class="w"><mark>AI</mark></td><td>I rejected the first version: it guessed time zones.</td></tr>
<tr><td>Calendar UI</td><td class="w"><mark>AI</mark></td><td>Plain JS, no framework, by request.</td></tr>
<tr><td>Review</td><td class="w"><span class="hb">by hand</span></td><td>Every change read before merge.</td></tr>
</table>
<h4>Decisions</h4>
<ol class="dec"><li>Scrape announcement pages, not the games: less fragile, and no ToS grey zone.</li><li>Store everything in UTC; render in the visitor's zone.</li><li>Let the agent write code, never let it write the tests' expectations.</li></ol>
<h4>What broke</h4>
<ul class="log"><li><span class="ph" title="placeholder">2025-05-11</span><span class="x">Two scrapers broke when a site changed its markup.</span></li><li><span class="ph" title="placeholder">2025-05-11</span><span>Fixed by the agent in one pass from the failing fixtures. Took 10 minutes.</span></li><li><span class="ph" title="placeholder">2025-07-02</span><span class="x">Duplicate events after a daylight-saving change.</span></li></ul>
<div class="links"><a href="#">Live demo</a><a href="https://github.com/StereotypicalCat">GitHub</a></div>
</div>
</details></div>
<div class="m"><span class="mono">No. 014</span><span class="ph" title="placeholder">Jan 2025</span>case file</div>
<div class="b"><details class="case entry" data-f="ai" id="c14">
<summary><h3>Config</h3><span class="tag-stamp" style="color:var(--red)">AI-made</span><span class="meta">Ansible · docker-compose · IaC</span></summary>
<div class="case-body">
<h4>Goal</h4>
<p>Rebuild every homelab machine from bare metal with one command, and have git be the documentation.</p>
<h4>Who wrote what</h4>
<table class="who">
<tr><th>Part</th><th>Written by</th><th>Note</th></tr>
<tr><td>Inventory &amp; layout</td><td class="w"><span class="hb">by hand</span></td><td>Which service lives where, and why.</td></tr>
<tr><td>Playbooks &amp; roles</td><td class="w"><mark>AI</mark></td><td>Generated, then iterated over a month.</td></tr>
<tr><td>Compose files</td><td class="w"><mark>AI</mark></td><td>Templated from one pattern I wrote down.</td></tr>
</table>
<h4>What broke</h4>
<ul class="log"><li><span class="ph" title="placeholder">2025-02-20</span><span class="x">Every run restarted every container: a template changed a timestamp on each render.</span></li><li><span class="ph" title="placeholder">2025-02-21</span><span>Found it by hand with <code>--diff</code>. The agent kept suggesting handlers.</span></li></ul>
<div class="links"><a href="https://github.com/StereotypicalCat">GitHub</a></div>
</div>
</details></div>
<div class="m"><span class="mono">No. 013</span><span class="ph" title="placeholder">Sep 2025</span></div>
<div class="b"><div class="note entry" data-f="hand" id="n13">
<span class="mono">Note · Devops</span>
<h3><a href="https://blog.lucaswinther.info"><span class="ph" title="placeholder title">Ansible idempotency, the hard way</span></a></h3>
<p>The write-up of No. 014's worst week. →</p>
</div></div>
<!-- Vol I summary -->
<div class="m sec"><span class="mono">Vol. I</span>2016–2022<br>by hand</div>
<div class="b sec">
<h2>From volume I</h2>
<p style="margin:0 0 12px;max-width:40em">The older work, summarised. Nothing here used AI. The full entries, with screenshots and demos, live on the <a href="https://old.lucaswinther.info">old site</a>.</p>
<ul class="older" id="older"></ul>
</div>
<div class="m sec"></div>
<footer class="b sec"><div class="foot">
Set in Spectral and Azeret Mono. Also: <a href="https://blog.lucaswinther.info">blog.lucaswinther.info</a> (all notes) · <a href="https://old.lucaswinther.info">Vol. I, the old site</a>.<br>
Placeholders (dotted underline): employer and job titles, university, all dates, blog post titles and excerpts, CV link, the Homelab metrics project, log entries and process breakdowns.
</div></footer>
</div>
<script>
const OLD=[
{t:"HyperCache, bachelor report",y:2022,d:"Reputation systems for peer-to-peer archival, simulated in Node.js. Thesis PDF.",k:"research"},
{t:"Student Dashboard",y:2021,d:"React, built on two rounds of user studies. Report and demo.",k:"HCI"},
{t:"Monthly Expenses Tracker",y:2021,d:"Spending in DKK, all amounts in øre to dodge floating point.",k:"web app"},
{t:"Portfolio",y:2021,d:"The old React + Material UI site itself.",k:"website"},
{t:"Mixed-Reality Rowing Game",y:2020,d:"Unity, Arduino and C#; two real oars with sensors.",k:"hardware"},
{t:"Chessswap",y:2020,d:"Chess where the board swaps every few turns. React.",k:"game"},
{t:"VR-Classroom",y:2019,d:"Node.js prototype, report and video.",k:"prototype"},
{t:"LoL Winrate Discord Bot",y:2019,d:"Node.js.",k:"bot"},
{t:"Censor Simulator",y:2018,d:"VR high-school exam project in Unity.",k:"VR"},
{t:"BreakoutPython",y:2017,d:"High-school exam project.",k:"game"},
{t:"Tribute to Norman Borlaug",y:2016,d:"One of the first websites, from freeCodeCamp.",k:"website"}
];
document.getElementById("older").innerHTML=OLD.map(o=>`<li data-f="hand" class="entry-lite"><span><b>${o.t}</b> <span style="color:var(--muted);font-style:italic">· ${o.k}</span></span><span class="mono" style="color:var(--muted)">${o.y} <span class="tag-stamp" style="margin-left:6px">hand</span></span><span class="d">${o.d}</span></li>`).join("");
const TOC=[
["018","What I let the agent write, and what I didn't","note · AI","n18","mix",null,1],
["017","Homelab metrics","case file","c17","mix",55,1],
["016","Three mini PCs, one k3s cluster","note · homelab","n16","hand",null,1],
["015","Event Tracker for Gacha Games","case file","c15","ai",10,0],
["014","Config","case file","c14","ai",15,0],
["013","Ansible idempotency, the hard way","note · devops","n13","hand",null,1],
["I","Eleven projects from volume I","summary","older","hand",100,0]
];
document.getElementById("toc").innerHTML=TOC.map(r=>{
const note=r[2].startsWith("note");
const t=r[6]?`<span class="ph" title="placeholder">${r[1]}</span>`:r[1];
return `<li data-f="${r[4]}" class="${note?"note-row":""}"><span class="no">No. ${r[0]}</span><a class="t" href="#${r[3]}" data-open="${r[3]}">${t}</a><span class="kind">${r[2]}</span>${r[5]!=null?`<span class="prov" title="${r[5]}% by hand (estimate)"><i class="h" style="width:${r[5]}%"></i><i class="a" style="width:${100-r[5]}%"></i></span>`:`<span class="mono" style="color:var(--muted)">${{hand:"written",mix:"written w/ AI",ai:"AI"}[r[4]]}</span>`}</li>`;
}).join("");
document.getElementById("toc").addEventListener("click",e=>{
const a=e.target.closest("[data-open]");if(!a)return;
const d=document.getElementById(a.dataset.open);if(d&&d.tagName==="DETAILS")d.open=true;
});
const on={hand:true,mix:true,ai:true};
document.querySelectorAll(".stamp").forEach(b=>b.addEventListener("click",()=>{
on[b.dataset.f]=!on[b.dataset.f];b.setAttribute("aria-pressed",on[b.dataset.f]);
document.querySelectorAll("[data-f]").forEach(el=>{
if(el.classList.contains("stamp"))return;
const hide=!on[el.dataset.f];
el.classList.toggle("hide",hide);
// hide margin note alongside a hidden entry
const wrap=el.closest(".b");
if(wrap&&wrap.previousElementSibling&&wrap.previousElementSibling.classList.contains("m")&&el.classList.contains("entry")){
wrap.style.display=hide?"none":"";wrap.previousElementSibling.style.display=hide?"none":"";
}
});
}));
</script>
</body>
</html>