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>
352 lines
24 KiB
HTML
352 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Lucas Winther</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=Fraunces:ital,opsz,wght@0,9..144,300..700;1,9..144,300..500&family=Figtree:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--ox:#3A1D1A; --ox-2:#4A2722; --cream:#F2E7D7; --cream-dim:#C9B8A4; --saffron:#E3A43C;
|
||
--page:#F7F4EE; --ink:#2B201D; --muted:#7A6D66; --rule:#E2DAD0;
|
||
--serif:"Fraunces",Georgia,serif; --sans:"Figtree",system-ui,sans-serif;
|
||
}
|
||
*{box-sizing:border-box}
|
||
html{background:var(--page)}
|
||
body{margin:0;font-family:var(--sans);font-size:16px;line-height:1.6;color:var(--ink)}
|
||
a{color:inherit}
|
||
.ph{text-decoration:underline dotted currentColor;text-decoration-thickness:1px;text-underline-offset:3px;cursor:help}
|
||
.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}
|
||
|
||
.shell{display:grid;grid-template-columns:minmax(340px,42%) 1fr;min-height:100vh}
|
||
|
||
/* left */
|
||
.side{position:sticky;top:0;height:100vh;background:var(--ox);color:var(--cream);padding:56px 48px 36px 56px;display:flex;flex-direction:column;overflow:hidden}
|
||
.side::before{content:"";position:absolute;inset:0;pointer-events:none;
|
||
background-image:radial-gradient(circle,rgba(242,231,215,.5) 1px,transparent 1.4px);background-size:18px 18px;
|
||
-webkit-mask-image:radial-gradient(220px circle at var(--mx,-500px) var(--my,-500px),#000 0,transparent 70%);
|
||
mask-image:radial-gradient(220px circle at var(--mx,-500px) var(--my,-500px),#000 0,transparent 70%);opacity:.55}
|
||
.side > *{position:relative}
|
||
.side h1{font-family:var(--serif);font-weight:400;font-size:54px;line-height:1;margin:0;letter-spacing:-.015em;font-variation-settings:"SOFT" 100}
|
||
.side .role{font-family:var(--serif);font-style:italic;font-size:20px;color:var(--cream-dim);margin:10px 0 0}
|
||
.side .intro{max-width:24em;margin:18px 0 0;color:var(--cream)}
|
||
.mode{margin-top:22px;display:inline-flex;border:1px solid rgba(242,231,215,.35);align-self:flex-start}
|
||
.mode button{font:inherit;font-size:13px;background:none;border:0;color:var(--cream-dim);padding:5px 12px;cursor:pointer}
|
||
.mode button[aria-pressed="true"]{background:var(--cream);color:var(--ox)}
|
||
.mode-hint{font-size:12.5px;color:var(--cream-dim);margin-top:6px}
|
||
|
||
.toc{list-style:none;margin:34px 0 0;padding:0}
|
||
.toc a{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--cream-dim);font-size:13px;letter-spacing:.08em;text-transform:uppercase;padding:5px 0}
|
||
.toc a::before{content:"";height:1px;width:28px;background:currentColor;transition:width .2s}
|
||
.toc a:hover,.toc a.on{color:var(--cream)}
|
||
.toc a.on::before{width:56px;background:var(--saffron)}
|
||
.toc .n{font-family:var(--serif);font-style:italic;text-transform:none;letter-spacing:0;color:var(--cream-dim);font-size:13px;margin-left:auto}
|
||
|
||
.readout{margin-top:auto;border-top:1px solid rgba(242,231,215,.22);padding-top:14px;min-height:118px;font-size:14px}
|
||
.readout .lbl{font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--saffron)}
|
||
.readout h3{font-family:var(--serif);font-weight:400;font-size:21px;margin:2px 0 4px;line-height:1.2}
|
||
.readout p{margin:0;color:var(--cream-dim)}
|
||
.readout .mix{display:flex;height:6px;margin:8px 0 4px;max-width:240px;background:rgba(242,231,215,.15)}
|
||
.readout .mix i{background:var(--cream)}
|
||
.readout .mix b{background:var(--saffron)}
|
||
.side-foot{display:flex;gap:16px;margin-top:16px;font-size:14px;flex-wrap:wrap}
|
||
.side-foot a{color:var(--cream);text-decoration:none;border-bottom:1px solid rgba(242,231,215,.3)}
|
||
.side-foot a:hover{border-color:var(--saffron)}
|
||
|
||
/* right */
|
||
main{padding:56px 64px 40px 64px;max-width:820px}
|
||
section{padding:0 0 64px;scroll-margin-top:40px}
|
||
h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);font-weight:600;margin:0 0 18px;display:flex;align-items:center;gap:12px}
|
||
h2::after{content:"";flex:1;height:1px;background:var(--rule)}
|
||
.about p{margin:0 0 14px;max-width:36em}
|
||
.about p:first-child{font-family:var(--serif);font-size:24px;line-height:1.35;font-weight:350}
|
||
.peek{border-bottom:1px dashed var(--muted);cursor:help}
|
||
.peek:hover{border-bottom-color:var(--ox);color:var(--ox)}
|
||
|
||
.row{display:grid;grid-template-columns:120px 1fr;gap:20px;padding:16px 14px;margin:0 -14px;transition:background .15s}
|
||
.row:hover{background:#EFE9E0}
|
||
.row .when{font-size:13px;color:var(--muted);padding-top:3px}
|
||
.row h3{margin:0;font-size:17px;font-weight:600;line-height:1.35}
|
||
.row h3 small{font-weight:400;color:var(--muted)}
|
||
.row p{margin:4px 0 8px;color:#4E423D;font-size:15px}
|
||
.tags{display:flex;flex-wrap:wrap;gap:6px;padding:0;margin:0;list-style:none}
|
||
.tags li{font-size:12px;padding:1px 8px;background:#EAE2D6;color:var(--ox)}
|
||
.cv-link{display:inline-block;margin-top:8px;font-weight:600;text-decoration:none;border-bottom:2px solid var(--saffron)}
|
||
|
||
.ai-sentence{font-family:var(--serif);font-size:20px;margin:0 0 14px}
|
||
.ai-sentence select{font:inherit;font-style:italic;color:var(--ox);background:transparent;border:0;border-bottom:2px solid var(--saffron);padding:0 22px 0 2px;cursor:pointer;appearance:none;-webkit-appearance:none;
|
||
background-image:linear-gradient(45deg,transparent 50%,var(--ox) 50%),linear-gradient(135deg,var(--ox) 50%,transparent 50%);background-position:calc(100% - 9px) 55%,calc(100% - 4px) 55%;background-size:5px 5px;background-repeat:no-repeat}
|
||
.ai-sentence .cnt{font-family:var(--sans);font-size:13px;color:var(--muted);margin-left:8px}
|
||
.proj{grid-template-columns:120px 1fr;position:relative}
|
||
.proj .thumb{height:72px;border:1px solid var(--rule);background:repeating-linear-gradient(-45deg,transparent 0 5px,rgba(58,29,26,.07) 5px 6px),#EFE8DE;font-size:10px;color:var(--muted);padding:3px 5px;display:flex;align-items:flex-end;line-height:1.2}
|
||
.proj .gauge{position:absolute;left:0;top:16px;bottom:16px;width:3px;background:var(--rule)}
|
||
.proj .gauge i{position:absolute;left:0;right:0;bottom:0;background:var(--saffron)}
|
||
.proj .ailab{font-size:12px;color:var(--muted);margin-left:8px;font-weight:400;white-space:nowrap}
|
||
.proj .ailab b{font-weight:600;color:var(--ox)}
|
||
.proj.hide{display:none}
|
||
.proj h3 a{text-decoration:none}
|
||
.proj h3 a:hover{color:var(--ox);text-decoration:underline;text-decoration-color:var(--saffron)}
|
||
.more-proj{font:inherit;font-size:14px;background:none;border:0;padding:8px 0;color:var(--ox);font-weight:600;cursor:pointer;border-bottom:2px solid var(--saffron)}
|
||
|
||
.topics{display:flex;gap:4px;margin:0 0 6px}
|
||
.topics button{font:inherit;font-size:13px;background:none;border:1px solid var(--rule);padding:3px 10px;cursor:pointer;color:var(--muted)}
|
||
.topics button[aria-pressed="true"]{border-color:var(--ox);color:var(--ox);background:#EFE9E0}
|
||
.post{display:grid;grid-template-columns:90px 1fr auto;gap:16px;align-items:baseline;padding:12px 0;border-bottom:1px solid var(--rule);text-decoration:none}
|
||
.post .d{font-size:13px;color:var(--muted)}
|
||
.post .t{font-family:var(--serif);font-size:19px;line-height:1.3}
|
||
.post .t small{display:block;font-family:var(--sans);font-size:14px;color:var(--muted);margin-top:2px}
|
||
.post .tp{font-size:12px;color:var(--ox)}
|
||
.post:hover .t{color:var(--ox)}
|
||
.post.hide{display:none}
|
||
|
||
.archive{display:grid;grid-template-columns:220px 1fr;gap:28px;align-items:center;padding:20px;border:1px solid var(--rule);text-decoration:none;transition:border-color .15s}
|
||
.archive:hover{border-color:var(--ox)}
|
||
.oldsite{height:130px;background:#212121;display:grid;grid-template-columns:34% 1fr;gap:6px;padding:8px;font-size:8px;color:#bbb}
|
||
.oldsite .l{background:#2c2c2c;display:flex;flex-direction:column;align-items:center;padding-top:10px;gap:4px}
|
||
.oldsite .av{width:26px;height:26px;border-radius:50%;background:#555}
|
||
.oldsite .ln{height:3px;width:70%;background:#555}
|
||
.oldsite .g{display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;align-content:start}
|
||
.oldsite .g i{height:30px;background:#333;border-top:3px solid #90caf9}
|
||
.archive h3{margin:0 0 4px;font-family:var(--serif);font-weight:400;font-size:22px}
|
||
.archive p{margin:0;color:var(--muted);font-size:15px}
|
||
|
||
footer{font-size:13px;color:var(--muted);border-top:1px solid var(--rule);padding-top:18px}
|
||
|
||
/* recruiter mode */
|
||
body.rec .not-rec{display:none}
|
||
body.rec .proj p, body.rec .proj .thumb{display:none}
|
||
body.rec .proj{padding-top:8px;padding-bottom:8px}
|
||
body.rec .proj .gauge{top:8px;bottom:8px}
|
||
.rec-only{display:none}
|
||
body.rec .rec-only{display:block}
|
||
.rec-box{background:#EFE9E0;padding:16px 18px;margin:0 0 24px;border-left:3px solid var(--saffron)}
|
||
.rec-box dl{margin:0;display:grid;grid-template-columns:110px 1fr;gap:4px 14px;font-size:15px}
|
||
.rec-box dt{color:var(--muted)}
|
||
.rec-box dd{margin:0}
|
||
|
||
@media (max-width:900px){
|
||
.shell{display:block}
|
||
.side{position:relative;height:auto;padding:40px 24px 28px}
|
||
.side h1{font-size:42px}
|
||
.toc,.readout{display:none}
|
||
main{padding:36px 24px}
|
||
.row,.proj{grid-template-columns:1fr;gap:6px}
|
||
.proj .thumb{display:none}
|
||
.archive{grid-template-columns:1fr}
|
||
.post{grid-template-columns:1fr;gap:2px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="mock-note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>tech-3 · The evolved split page</b>Brittany Chiang's sticky identity / scrolling content split, warmed up: a reader toggle (recruiter or curious), a left panel that reacts to what you point at, an AI filter written as a sentence, and a real blog section.</div>
|
||
|
||
<div class="shell">
|
||
<aside class="side" id="side">
|
||
<h1>Lucas Winther</h1>
|
||
<p class="role"><span class="ph" title="placeholder">Software engineer at Nordlys Systems</span></p>
|
||
<p class="intro">I build web tools and infrastructure, by hand for years, and with AI in the loop now. I write about both.</p>
|
||
<div class="mode" role="group" aria-label="Reading as">
|
||
<button data-mode="curious" aria-pressed="true">Just looking</button>
|
||
<button data-mode="rec" aria-pressed="false">Hiring</button>
|
||
</div>
|
||
<div class="mode-hint" id="modeHint">Everything, including writing and side notes.</div>
|
||
|
||
<ul class="toc" id="toc">
|
||
<li><a href="#about">About<span class="n">01</span></a></li>
|
||
<li><a href="#experience">Experience<span class="n">02</span></a></li>
|
||
<li><a href="#projects">Projects<span class="n">03</span></a></li>
|
||
<li class="not-rec"><a href="#writing">Writing<span class="n">04</span></a></li>
|
||
<li><a href="#before">By hand, before<span class="n">05</span></a></li>
|
||
</ul>
|
||
|
||
<div class="readout" id="readout" aria-live="polite">
|
||
<div class="lbl">Pointing at</div>
|
||
<h3>Nothing yet</h3>
|
||
<p>Hover a project, job or underlined word on the right and the details show up here.</p>
|
||
</div>
|
||
<div class="side-foot">
|
||
<a href="https://github.com/StereotypicalCat">GitHub</a><a href="https://www.linkedin.com/in/lucas-winther">LinkedIn</a><a href="mailto:lucas@lucaswinther.info">Email</a><a href="#" title="placeholder: CV PDF">CV</a>
|
||
</div>
|
||
</aside>
|
||
|
||
<main>
|
||
<section id="about" class="about">
|
||
<div class="rec-only rec-box">
|
||
<dl>
|
||
<dt>Role</dt><dd><span class="ph" title="placeholder">Software engineer, platform team, Nordlys Systems (2023–)</span></dd>
|
||
<dt>Education</dt><dd>BSc Computer Science, <span class="ph" title="placeholder">Aarhus University, 2022</span></dd>
|
||
<dt>Core stack</dt><dd>TypeScript, Node.js, React, Ansible, Docker, Linux</dd>
|
||
<dt>Languages</dt><dd>Danish, English</dd>
|
||
<dt>Contact</dt><dd><a href="mailto:lucas@lucaswinther.info">lucas@lucaswinther.info</a> · <a href="https://www.linkedin.com/in/lucas-winther">LinkedIn</a></dd>
|
||
</dl>
|
||
<a class="cv-link" href="#" title="placeholder: CV PDF">Download CV (PDF)</a>
|
||
</div>
|
||
<h2>About</h2>
|
||
<p>Computer scientist with a passion for development, which is what the old site said, and it still holds.</p>
|
||
<p>I studied computer science and wrote my bachelor thesis on <span class="peek" data-peek="thesis">trust in peer-to-peer archival</span>. Before that I built VR exam projects and a rowing game with <span class="peek" data-peek="oars">two real oars</span>. These days I work on <span class="ph" title="placeholder">platform tooling</span> and run a small <span class="peek" data-peek="homelab">homelab</span> that I rebuild from code.</p>
|
||
<p class="not-rec">Since 2023 I've used AI for a growing share of the typing. Every project below says how much, and the <a href="#before">by-hand archive</a> is still up for anyone who'd rather see work without it.</p>
|
||
</section>
|
||
|
||
<section id="experience">
|
||
<h2>Experience & education</h2>
|
||
<div class="row" data-peek="job">
|
||
<div class="when"><span class="ph" title="placeholder">2023 — now</span></div>
|
||
<div><h3><span class="ph" title="placeholder">Software engineer · Nordlys Systems</span></h3>
|
||
<p>Platform team. CI pipelines, internal developer tooling, deployment automation with Ansible and containers.</p>
|
||
<ul class="tags"><li>TypeScript</li><li>Ansible</li><li>Docker</li><li>GitHub Actions</li></ul></div>
|
||
</div>
|
||
<div class="row" data-peek="stud">
|
||
<div class="when"><span class="ph" title="placeholder">2021 — 2022</span></div>
|
||
<div><h3><span class="ph" title="placeholder">Student developer · University IT</span></h3>
|
||
<p>Small React and Node.js tools for teaching staff; first taste of maintaining something other people rely on.</p>
|
||
<ul class="tags"><li>React</li><li>Node.js</li></ul></div>
|
||
</div>
|
||
<div class="row" data-peek="bsc">
|
||
<div class="when"><span class="ph" title="placeholder">2019 — 2022</span></div>
|
||
<div><h3>BSc Computer Science <small>· <span class="ph" title="placeholder">Aarhus University</span></small></h3>
|
||
<p>Distributed systems, HCI, VR. Thesis: reputation systems in peer-to-peer archival.</p>
|
||
<ul class="tags"><li>Distributed systems</li><li>HCI</li><li>User studies</li></ul></div>
|
||
</div>
|
||
<a class="cv-link" href="#" title="placeholder: CV PDF">Full CV (PDF) →</a>
|
||
</section>
|
||
|
||
<section id="projects">
|
||
<h2>Projects</h2>
|
||
<p class="ai-sentence">Show me projects made
|
||
<select id="aiSel" aria-label="AI involvement">
|
||
<option value="all">any way</option>
|
||
<option value="none">entirely by hand</option>
|
||
<option value="some">with some AI help</option>
|
||
<option value="full">fully by AI</option>
|
||
</select><span class="cnt" id="cnt"></span></p>
|
||
<div id="projList"></div>
|
||
<button class="more-proj" id="moreBtn">Show all 15 projects</button>
|
||
</section>
|
||
|
||
<section id="writing" class="not-rec">
|
||
<h2>Writing</h2>
|
||
<div class="topics" role="group" aria-label="Topic">
|
||
<button aria-pressed="true" data-t="all">All</button><button aria-pressed="false" data-t="AI">AI</button><button aria-pressed="false" data-t="Devops">Devops</button><button aria-pressed="false" data-t="Homelab">Homelab</button>
|
||
</div>
|
||
<div id="posts"></div>
|
||
<p><a class="cv-link" href="https://blog.lucaswinther.info">Everything on blog.lucaswinther.info →</a></p>
|
||
</section>
|
||
|
||
<section id="before">
|
||
<h2>By hand, before</h2>
|
||
<a class="archive" href="https://old.lucaswinther.info" data-peek="old">
|
||
<div class="oldsite" aria-hidden="true"><div class="l"><i class="av"></i><i class="ln"></i><i class="ln" style="width:50%"></i></div><div class="g"><i></i><i></i><i></i><i></i><i></i><i></i></div></div>
|
||
<div><h3>The 2021 site, frozen</h3><p>React and Material UI, written entirely by hand before I used any AI tools. Kept exactly as it was, at old.lucaswinther.info.</p></div>
|
||
</a>
|
||
</section>
|
||
|
||
<footer>
|
||
Set in Fraunces and Figtree. Placeholders (dotted underline): employer, job titles and dates, university, blog post titles, CV link, the “Homelab metrics” and “this site” projects, AI-share percentages.
|
||
</footer>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
const PROJ=[
|
||
{id:"site",y:2026,t:"lucaswinther.info",n:"this site",ai:"some",pct:30,ph:1,d:"Design, copy and layout by hand; scroll tracking and the filter pair-written with AI.",tags:["HTML","CSS","JS"]},
|
||
{id:"metrics",y:2026,t:"Homelab metrics",ai:"some",pct:40,ph:1,d:"Prometheus and Grafana for the homelab. Alert rules by hand; dashboards drafted by AI and rewritten.",tags:["Prometheus","Grafana"]},
|
||
{id:"gacha",y:2025,t:"Event Tracker for Gacha Games",ai:"full",pct:90,d:"Scrapes event announcements from several games into one calendar. I wrote the spec and tests, an agent wrote the code.",tags:["JavaScript","Scraping"],links:"Live · GitHub"},
|
||
{id:"config",y:2025,t:"Config",ai:"full",pct:85,d:"Ansible and docker-compose that rebuild my homelab from bare metal.",tags:["Ansible","docker-compose","IaC"],links:"GitHub"},
|
||
{id:"hyper",y:2022,t:"HyperCache",n:"bachelor report",ai:"none",pct:0,d:"Reputation systems in peer-to-peer archival, simulated in Node.js.",tags:["Node.js","P2P"],links:"Thesis PDF"},
|
||
{id:"dash",y:2021,t:"Student Dashboard",ai:"none",pct:0,d:"React dashboard built on two rounds of user studies.",tags:["React","User studies"],links:"Report · Demo"},
|
||
{id:"rowing",y:2020,t:"Mixed-Reality Rowing Game",ai:"none",pct:0,d:"Two real oars with sensors drive a Unity boat race.",tags:["Unity","Arduino","C#"]},
|
||
{id:"chess",y:2020,t:"Chessswap",ai:"none",pct:0,d:"Chess where the board swaps every few turns.",tags:["React"]},
|
||
{id:"exp",y:2021,t:"Monthly Expenses Tracker",ai:"none",pct:0,d:"Monthly spending in DKK against a budget.",tags:["JavaScript"]},
|
||
{id:"vrc",y:2019,t:"VR-Classroom",ai:"none",pct:0,d:"Node.js prototype of a shared VR classroom, with report and video.",tags:["Node.js","VR"]},
|
||
{id:"lol",y:2019,t:"LoL Winrate Discord Bot",ai:"none",pct:0,d:"Champion win rates on request in Discord.",tags:["Node.js"]},
|
||
{id:"censor",y:2018,t:"Censor Simulator",ai:"none",pct:0,d:"VR high-school exam project: you are the examiner.",tags:["Unity","VR"]},
|
||
{id:"breakout",y:2017,t:"BreakoutPython",ai:"none",pct:0,d:"High-school exam project.",tags:["Python"]},
|
||
{id:"borlaug",y:2016,t:"Tribute to Norman Borlaug",ai:"none",pct:0,d:"One of my first websites, from freeCodeCamp.",tags:["HTML","CSS"]},
|
||
{id:"port",y:2021,t:"Portfolio (2021)",ai:"none",pct:0,d:"The old React site, now the archive.",tags:["React","Material UI"]}
|
||
];
|
||
const AIL={none:"by hand",some:"some AI help",full:"fully AI"};
|
||
const PEEK={
|
||
thesis:["HyperCache","Bachelor thesis, 2022. Which peers do you trust to keep a copy of your data when anyone can leave at any time?",0],
|
||
oars:["Mixed-Reality Rowing Game","Arduino reads two oar angles; Unity turns them into strokes. Recalibrates from a resting pose.",0],
|
||
homelab:["The homelab","Three mini PCs, a NAS and a UPS. Rebuilt from one git repo (Config). Placeholder specs.",null],
|
||
job:["Nordlys Systems (placeholder)","Platform team, 2023–now. Uses AI for boilerplate and reviews; owns the deployment pipeline.",null],
|
||
stud:["University IT (placeholder)","Student job, 2021–2022. Tools for teaching staff.",null],
|
||
bsc:["BSc Computer Science","Placeholder university, 2019–2022. Thesis on P2P archival reputation.",null],
|
||
old:["old.lucaswinther.info","The 2021 site, untouched. 13 projects, none made with AI.",0]
|
||
};
|
||
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
||
let showAll=false;
|
||
function renderProj(){
|
||
const v=document.getElementById("aiSel").value;
|
||
const list=PROJ.slice().sort((a,b)=>b.y-a.y);
|
||
const match=list.filter(p=>v==="all"||p.ai===v);
|
||
const shown=(showAll||v!=="all")?match:match.slice(0,6);
|
||
document.getElementById("projList").innerHTML=shown.map(p=>`
|
||
<div class="row proj" data-proj="${p.id}">
|
||
<i class="gauge" title="${p.pct}% AI-written (estimate)"><i style="height:${p.pct}%"></i></i>
|
||
<div><div class="thumb">placeholder: ${esc(p.t)}</div></div>
|
||
<div><h3><a href="#">${p.ph?`<span class="ph" title="placeholder">${esc(p.t)}</span>`:esc(p.t)}</a> <small>· ${p.y}${p.n?" · "+esc(p.n):""}</small><span class="ailab">— <b>${AIL[p.ai]}</b></span></h3>
|
||
<p>${esc(p.d)}</p><ul class="tags">${p.tags.map(t=>`<li>${esc(t)}</li>`).join("")}</ul></div>
|
||
</div>`).join("");
|
||
document.getElementById("cnt").textContent=match.length+" of "+PROJ.length;
|
||
const btn=document.getElementById("moreBtn");
|
||
btn.style.display=(v==="all"&&!showAll)?"":"none";
|
||
}
|
||
document.getElementById("aiSel").addEventListener("change",renderProj);
|
||
document.getElementById("moreBtn").addEventListener("click",()=>{showAll=true;renderProj();});
|
||
renderProj();
|
||
|
||
const POSTS=[
|
||
["Oct 2026","What I let the agent write, and what I didn't","A line-by-line audit of the gacha tracker.","AI"],
|
||
["Aug 2026","Rebuilding the homelab from one git repo","Disaster recovery, tested by wiping a machine.","Devops"],
|
||
["Jun 2026","Three mini PCs, one k3s cluster","And why some services went back to plain compose.","Homelab"],
|
||
["Mar 2026","Specs are prompts with manners","Writing a good ticket and a good prompt are the same skill.","AI"],
|
||
["Nov 2025","Backups that have actually been restored","Restic, a schedule, and a restore drill.","Homelab"],
|
||
["Sep 2025","Ansible idempotency, the hard way","The bug that restarted every container on every run.","Devops"]
|
||
];
|
||
document.getElementById("posts").innerHTML=POSTS.map(p=>`<a class="post" href="https://blog.lucaswinther.info" data-t="${p[3]}"><span class="d"><span class="ph" title="placeholder">${p[0]}</span></span><span class="t"><span class="ph" title="placeholder">${p[1]}</span><small>${p[2]}</small></span><span class="tp">${p[3]}</span></a>`).join("");
|
||
document.querySelectorAll(".topics button").forEach(b=>b.addEventListener("click",()=>{
|
||
document.querySelectorAll(".topics button").forEach(x=>x.setAttribute("aria-pressed",x===b));
|
||
document.querySelectorAll(".post").forEach(p=>p.classList.toggle("hide",b.dataset.t!=="all"&&p.dataset.t!==b.dataset.t));
|
||
}));
|
||
|
||
// reader mode
|
||
const HINT={curious:"Everything, including writing and side notes.",rec:"CV summary first, projects condensed, blog hidden."};
|
||
document.querySelectorAll(".mode button").forEach(b=>b.addEventListener("click",()=>{
|
||
document.querySelectorAll(".mode button").forEach(x=>x.setAttribute("aria-pressed",x===b));
|
||
document.body.classList.toggle("rec",b.dataset.mode==="rec");
|
||
document.getElementById("modeHint").textContent=HINT[b.dataset.mode];
|
||
}));
|
||
|
||
// readout: left panel shows what you point at
|
||
const ro=document.getElementById("readout");
|
||
const DEFAULT=ro.innerHTML;
|
||
function show(title,text,pct,label){
|
||
ro.innerHTML=`<div class="lbl">${label||"Pointing at"}</div><h3>${title}</h3><p>${text}</p>`+
|
||
(pct!=null?`<div class="mix" title="hand vs AI"><i style="width:${100-pct}%"></i><b style="width:${pct}%"></b></div><p style="font-size:12px">${100-pct}% by hand · ${pct}% AI <span class="ph" title="placeholder estimate">(est.)</span></p>`:"");
|
||
}
|
||
document.querySelector("main").addEventListener("mouseover",e=>{
|
||
const pr=e.target.closest("[data-proj]");
|
||
if(pr){const p=PROJ.find(x=>x.id===pr.dataset.proj);show(esc(p.t)+" · "+p.y,esc(p.tags.join(", "))+". "+(p.links?esc(p.links):""),p.pct,"Project");return;}
|
||
const pk=e.target.closest("[data-peek]");
|
||
if(pk){const d=PEEK[pk.dataset.peek];show(esc(d[0]),esc(d[1]),d[2]);}
|
||
});
|
||
document.querySelector("main").addEventListener("mouseleave",()=>{ro.innerHTML=DEFAULT;});
|
||
|
||
// cursor-following dot grid on the left panel (only moves when the mouse does)
|
||
const side=document.getElementById("side");
|
||
side.addEventListener("mousemove",e=>{const r=side.getBoundingClientRect();side.style.setProperty("--mx",(e.clientX-r.left)+"px");side.style.setProperty("--my",(e.clientY-r.top)+"px");});
|
||
side.addEventListener("mouseleave",()=>{side.style.setProperty("--mx","-500px");side.style.setProperty("--my","-500px");});
|
||
|
||
// active section in the index
|
||
const links=[...document.querySelectorAll("#toc a")];
|
||
const io=new IntersectionObserver(es=>{es.forEach(en=>{if(en.isIntersecting){links.forEach(a=>a.classList.toggle("on",a.getAttribute("href")==="#"+en.target.id));}});},{rootMargin:"-30% 0px -60% 0px"});
|
||
document.querySelectorAll("main section").forEach(s=>io.observe(s));
|
||
</script>
|
||
</body>
|
||
</html>
|