website

Personal website
git clone https://github.com/michaelneuper/website.git
README | Files | Log | Refs

commit ecaf256df25d68630d9af7ece663d4fb6e2b5a2e
parent 0c148e7a310c2ac09bc1476d97ee0d567e520458
Author: Michael Neuper <michael@michaelneuper.com>
Date:   Mon, 12 Oct 2026 22:08:28 +0200

fix ios hover

Diffstat:
Dsrc/components/frame/Footer.astro | 8--------
Msrc/components/home/Contact.astro | 2+-
Msrc/content/projects/8-bit-computer.md | 5+----
Msrc/content/projects/chat-voip-p2p.md | 6+++---
Msrc/content/projects/jvm-compiler.md | 4++--
Msrc/content/projects/os-scheduler.md | 4++--
Msrc/content/projects/othello-mcts.md | 6+++---
Msrc/layouts/Base.astro | 6++----
Msrc/scripts/home/contact.js | 23+++++++++++++++++++++++
Msrc/scripts/home/pinout.js | 4+++-
Msrc/scripts/home/projects.js | 15+++++++++------
Msrc/styles/base.css | 20++++++++++++++------
Msrc/styles/demos/bytecode.css | 6++++--
Msrc/styles/demos/computer.css | 8+++++---
Msrc/styles/demos/othello.css | 10++++++----
Dsrc/styles/frame/footer.css | 12------------
Msrc/styles/frame/topbar.css | 15+++++++++++----
Msrc/styles/home/about.css | 8+++++++-
Msrc/styles/home/coffee.css | 6++++--
Msrc/styles/home/contact.css | 13+++++++++----
Msrc/styles/home/pinout.css | 7++++++-
Msrc/styles/home/posts.css | 12+++++++-----
Msrc/styles/home/projects.css | 6++++--
Msrc/styles/post/nav.css | 6++++--
Msrc/styles/post/outline.css | 6++++--
25 files changed, 134 insertions(+), 84 deletions(-)

diff --git a/src/components/frame/Footer.astro b/src/components/frame/Footer.astro @@ -1,8 +0,0 @@ ---- -import "../../styles/frame/footer.css"; - -const year = new Date().getFullYear(); ---- -<footer class="foot"> - <p>© {year} Michael Neuper</p> -</footer> diff --git a/src/components/home/Contact.astro b/src/components/home/Contact.astro @@ -20,7 +20,7 @@ import "../../styles/home/contact.css"; <h3 class="ds-h">Encrypt it</h3> <p class="gpg-fp"><span class="dim">fingerprint</span><code>4E7B F800 0D86 182D F484 5C37 508B CF84 373C 8930</code></p> <div class="gpg-cmd"> - <pre><code>curl -sL https://michaelneuper.com/michaelneuper.gpg | gpg --import</code></pre> + <pre><code>curl -sL https://michaelneuper.com/<wbr>michaelneuper.gpg | gpg --import</code></pre> <button type="button" class="btn" data-copy=".gpg-cmd code">copy</button> </div> </div> diff --git a/src/content/projects/8-bit-computer.md b/src/content/projects/8-bit-computer.md @@ -7,11 +7,8 @@ demo: computer demoNote: "A JavaScript simulation of the board, with the build’s instructions and microcode. Flip a bit in RAM to change the program, or select the display for signed numbers." summary: "Ben Eater's 8-bit breadboard computer, built chip by chip." stack: "74HC logic, EEPROMs, breadboards, a lot of jumper wire" -links: - - text: "Ben Eater’s series" - href: "https://eater.net/8bit" --- -My most recent finished project: a programmable computer on breadboards, built from 74-series logic chips by following Ben Eater’s video series. It is a set of modules that share one 8-bit bus: a program counter, 16 bytes of RAM, an instruction register, A and B registers feeding an adder that also subtracts, and an output register that drives a 7-segment display. +My most recent finished project: a programmable computer on breadboards, built from [74-series logic chips](https://en.wikipedia.org/wiki/7400-series_integrated_circuits) by following [Ben Eater’s video series](https://eater.net/8bit). It is a set of modules that share one 8-bit bus: a program counter, 16 bytes of RAM, an instruction register, A and B registers feeding an adder that also subtracts, and an output register that drives a 7-segment display. Every instruction takes five clock steps, and the first two fetch it from RAM. At each step, microcode stored in EEPROMs sets sixteen control lines that decide which module puts a value on the bus and which ones read it. Carry and zero flags give it conditional jumps, enough for loops like counting up and down or computing Fibonacci numbers. diff --git a/src/content/projects/chat-voip-p2p.md b/src/content/projects/chat-voip-p2p.md @@ -7,12 +7,12 @@ demo: rbudp demoNote: "The RBUDP file transfer in miniature, animated for this page: blast every packet over UDP, ask over TCP which ones arrived, and resend the gaps." summary: "Four networking projects in Java: chat, fast UDP file transfer, voice calls and P2P sharing." stack: "Java, JavaFX, TCP and UDP sockets, Java Sound, RSA, AES-GCM" -note: "private (coursework, group project)" +note: "private (university coursework, group project)" --- -Four projects my group built for a university course: +Four projects my group built for a networks course: - A chat server that handles many clients at once, with a JavaFX client, load-tested with dozens of simultaneous users. - File transfer over Reliable Blast UDP, measured against TCP across packet sizes, loss rates and file sizes from 1 to 100&nbsp;MB. - The chat extended with voice calls and group calls streamed over UDP, plus voice notes. -- Peer-to-peer file sharing: a server brokers searches, then peers send files to each other directly over TCP, encrypted with AES-GCM keys exchanged using RSA, with pause and resume. We tested it across four machines over Tailscale. +- Peer-to-peer file sharing: a server brokers searches, then peers send files to each other directly over TCP, encrypted with [AES-GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) keys exchanged using [RSA](https://en.wikipedia.org/wiki/RSA_cryptosystem), with pause and resume. We tested it across four machines over Tailscale. diff --git a/src/content/projects/jvm-compiler.md b/src/content/projects/jvm-compiler.md @@ -7,9 +7,9 @@ demo: bytecode demoNote: "Only the last step, for arithmetic expressions, rewritten in JavaScript for this page. The JVM is a stack machine, so an expression compiles to instructions that push and pop an operand stack. Type one to see its bytecode and run it." summary: "A compiler in C for a custom language that targets the JVM." stack: "C, JVM bytecode, Jasmin" -note: "private (computer architecture coursework)" +note: "private (university coursework)" --- I wrote it for a computer architecture course. It reads a program in a small procedural language, with subroutines, arrays, `if` and `while` statements, input and output, and produces a class file the Java virtual machine can run. -It works in stages. A scanner splits the source into tokens, and a recursive-descent parser checks them against the grammar. A symbol table, built on my own hash table, records every name and its type so the type checker can catch mistakes. The last stage emits JVM instructions, which Jasmin assembles into the class file. +It works in stages. A scanner splits the source into tokens, and a recursive-descent parser checks them against the grammar. A symbol table, built on my own hash table, records every name and its type so the type checker can catch mistakes. The last stage emits [JVM instructions](https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html), which [Jasmin](https://jasmin.sourceforge.net/) assembles into the class file. diff --git a/src/content/projects/os-scheduler.md b/src/content/projects/os-scheduler.md @@ -7,9 +7,9 @@ demo: scheduler demoNote: "A simplified simulation written for this page. Its processes only use the CPU, so none of them wait for resources or deadlock." summary: "A multicore operating system scheduler, for a concurrency course." stack: "C, OpenMP" -note: "private (concurrency coursework)" +note: "private (university coursework)" --- -The part of an operating system that decides which process runs on which core, and for how long. Processes come from a file that lists each one’s instructions, which request and release named resources. OpenMP threads act as the cores: each takes the next process from a shared ready queue, behind a lock so two cores never take the same one, and runs it under first come, first served, round robin or preemptive priority scheduling. +The part of an operating system that decides which process runs on which core, and for how long. Processes come from a file that lists each one’s instructions, which request and release named resources. [OpenMP](https://www.openmp.org/) threads act as the cores: each takes the next process from a shared ready queue, behind a lock so two cores never take the same one, and runs it under first come, first served, round robin or preemptive priority scheduling. A process that asks for a resource another process has goes into a waiting queue until the holder releases it, then back to the ready queue. Once nothing is left to run, the scheduler checks the waiting processes for deadlock by following who holds the resource each one needs: a chain that leads back to where it started is a deadlock. diff --git a/src/content/projects/othello-mcts.md b/src/content/projects/othello-mcts.md @@ -7,9 +7,9 @@ demo: othello demoNote: "A serial version of the player, rewritten in JavaScript for this page. While it thinks, each legal move is shaded by how often the search tried it." summary: "An Othello player in C that runs Monte Carlo tree search across MPI processes." stack: "C, MPI, Docker" -note: "private (concurrency coursework)" +note: "private (university coursework)" --- -The player picks a move by running thousands of random games from the current position and keeping the move that was explored most, using UCT to balance trying new moves against improving promising ones. Each move gets one second. +The player picks a move by running thousands of random games from the current position and keeping the move that was explored most, using [UCT](https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation) to balance trying new moves against improving promising ones. Each move gets one second. -The project was for a concurrency course, so the search also runs in parallel over MPI, in three versions: independent search trees combined at the end with `MPI_Allreduce`, trees that share their statistics every so often, and the same sharing done asynchronously with non-blocking sends. Each version is packaged as a Docker image, and a match runner plays them against each other. In ten-game matches on four processes, each version beat the one before it, except the asynchronous version, which won half its games against the synchronous one. +The project was for a concurrency course, so the search also runs in parallel over [MPI](https://www.mpi-forum.org/), in three versions: independent search trees combined at the end with `MPI_Allreduce`, trees that share their statistics every so often, and the same sharing done asynchronously with non-blocking sends. Each version is packaged as a Docker image, and a match runner plays them against each other. In ten-game matches on four processes, each version beat the one before it, except the asynchronous version, which won half its games against the synchronous one. diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro @@ -1,9 +1,8 @@ --- -// The Emacs frame every page shares: the head, the top line, the footer, the -// modeline and minibuffer, and *Help*. Pages fill the default slot with <main>, +// The Emacs frame every page shares: the head, the top line, the modeline and +// minibuffer, and *Help*. Pages fill the default slot with <main>, // add to <head> through the "head" slot and list their own keys in "help". import TopBar from "../components/frame/TopBar.astro"; -import Footer from "../components/frame/Footer.astro"; import Modeline from "../components/frame/Modeline.astro"; import Help from "../components/frame/Help.astro"; import Analytics from "../components/frame/Analytics.astro"; @@ -61,7 +60,6 @@ const { <a class="skip" href={skip}>Skip to content</a> <TopBar home={home} /> <slot /> -<Footer /> <Modeline buf={buf} mode={mode} /> <Help><slot name="help" /></Help> </body> diff --git a/src/scripts/home/contact.js b/src/scripts/home/contact.js @@ -25,4 +25,27 @@ export function initContact() { } setTimeout(() => { copy.textContent = "copy address"; }, 2000); }); + + // <button data-copy="selector"> copies that element's text (the GPG import command) + for (const b of sec.querySelectorAll("[data-copy]")) { + const label = b.textContent; + b.addEventListener("click", async () => { + const src = $(b.dataset.copy, sec); + if (!src) return; + const text = src.textContent.trim(); + try { + await navigator.clipboard.writeText(text); + b.textContent = "copied"; + message(`Copied: ${text}`, "ok", 3000); + } catch { + // no clipboard access: select it, so the reader can copy it themselves + const r = document.createRange(); + r.selectNodeContents(src); + getSelection().removeAllRanges(); + getSelection().addRange(r); + b.textContent = "selected"; + } + setTimeout(() => { b.textContent = label; }, 2000); + }); + } } diff --git a/src/scripts/home/pinout.js b/src/scripts/home/pinout.js @@ -43,7 +43,9 @@ export function initPinout() { s.textContent = parts[k]; btn.append(s); } - btn.addEventListener("pointerenter", () => show(p, btn)); + // Pointing shows a pin only with a mouse. A tap is a click: an iPhone takes a + // tap that changes the page on hover as the hover, and needs a second for the click. + btn.addEventListener("pointerenter", (e) => { if (e.pointerType === "mouse") show(p, btn); }); btn.addEventListener("focus", () => show(p, btn)); btn.addEventListener("click", () => show(p, btn)); btns.push(btn); diff --git a/src/scripts/home/projects.js b/src/scripts/home/projects.js @@ -175,13 +175,16 @@ export function initProjects() { let dragged = false; // the click that ends a drag doesn't pick the card under it wheel.addEventListener("pointerdown", (e) => { if (!geo || e.button !== 0) return; - drag = { id: e.pointerId, x: e.clientX, y: e.clientY, from: pos, moved: false, v: 0, at: performance.now() }; + // a finger wanders further than a mouse during a tap, so it has to move + // further before the tap becomes a drag and stops being a click + const slop = e.pointerType === "mouse" ? 6 : 14; + drag = { id: e.pointerId, x: e.clientX, y: e.clientY, from: pos, moved: false, v: 0, at: performance.now(), slop }; }); wheel.addEventListener("pointermove", (e) => { if (!drag || e.pointerId !== drag.id) return; const along = geo.flat ? e.clientX - drag.x : e.clientY - drag.y; if (!drag.moved) { - if (Math.abs(along) < 6) return; + if (Math.abs(along) < drag.slop) return; drag.moved = true; wheel.setPointerCapture(e.pointerId); wheel.classList.add("is-dragging"); @@ -232,9 +235,9 @@ export function initProjects() { const li = items[i]; detail.dataset.cat = li.dataset.cat; detail.setAttribute("aria-labelledby", cards[i].id); - // The title and facts, the demo, then the write-up. With room, the demo - // stands beside the other two; without, it comes before the write-up, so - // a phone gets to it without scrolling through the text. The card already + // The title and facts, the write-up, then the demo. With room, the demo + // stands beside the other two; without, it comes after the write-up, so + // the project is explained before it is tried. The card already // shows the category and the slug, and the write-up says what the summary // says, so those stay in the list for when there's no wheel. const head = document.createElement("div"); @@ -264,7 +267,7 @@ export function initProjects() { const host = document.createElement("div"); host.className = "demo"; box.append(host); - head.after(box); + row.append(box); try { const { mount } = await import(`../demos/${demo}.js`); if (shown === i && host.isConnected) unmount = mount(host) ?? null; diff --git a/src/styles/base.css b/src/styles/base.css @@ -17,6 +17,8 @@ html { body { margin: 0; + /* room at the end of the page, so the last lines clear the modeline */ + padding-bottom: calc(var(--frame-h) + 96px); font: 400 13px/1.6 var(--mono); font-variant-ligatures: none; -webkit-font-smoothing: antialiased; @@ -36,8 +38,10 @@ a { text-underline-offset: 0.2em; } -a:hover { - text-decoration-color: currentColor; +@media (hover: hover) { + a:hover { + text-decoration-color: currentColor; + } } button, @@ -125,8 +129,10 @@ code { white-space: nowrap; } -.btn:hover { - border-color: var(--fg); +@media (hover: hover) { + .btn:hover { + border-color: var(--fg); + } } /* held down: phones have no hover, so this is what shows a tap landed. @@ -156,8 +162,10 @@ select { appearance: none; } -select:hover { - border-color: var(--fg); +@media (hover: hover) { + select:hover { + border-color: var(--fg); + } } input[type="range"] { diff --git a/src/styles/demos/bytecode.css b/src/styles/demos/bytecode.css @@ -20,8 +20,10 @@ font-size: 13px; } -.bc-input input:hover { - border-color: var(--fg); +@media (hover: hover) { + .bc-input input:hover { + border-color: var(--fg); + } } .bc-cols { diff --git a/src/styles/demos/computer.css b/src/styles/demos/computer.css @@ -178,9 +178,11 @@ cursor: pointer; } -.ram-row .led:hover { - outline: 1px solid var(--faint); - outline-offset: 1px; +@media (hover: hover) { + .ram-row .led:hover { + outline: 1px solid var(--faint); + outline-offset: 1px; + } } /* An off bit gets a ring, so the zeros you can set still show: the off colour diff --git a/src/styles/demos/othello.css b/src/styles/demos/othello.css @@ -51,10 +51,12 @@ background: var(--dim); } -.oth-cell.legal:hover::after { - inset: 13%; - background: color-mix(in srgb, var(--disc-dark) 55%, transparent); - box-shadow: inset 0 0 0 1px var(--faint); +@media (hover: hover) { + .oth-cell.legal:hover::after { + inset: 13%; + background: color-mix(in srgb, var(--disc-dark) 55%, transparent); + box-shadow: inset 0 0 0 1px var(--faint); + } } .oth-cell .disc { diff --git a/src/styles/frame/footer.css b/src/styles/frame/footer.css @@ -1,12 +0,0 @@ -/* The footer at the end of every page */ - -.foot { - max-width: var(--col); - margin: 0 auto; - padding: 160px var(--gutter) calc(var(--frame-h) + 48px); - font-size: 12px; -} - -.foot p { - margin: 0 0 4px; -} diff --git a/src/styles/frame/topbar.css b/src/styles/frame/topbar.css @@ -50,11 +50,16 @@ text-decoration: none; } -.nav a:hover, .nav a[aria-current="true"] { color: var(--fg); } +@media (hover: hover) { + .nav a:hover { + color: var(--fg); + } +} + .nav a[aria-current="true"] { text-decoration: underline; text-decoration-color: var(--fg); @@ -70,9 +75,11 @@ font-size: 12px; } -.theme-key:hover, -.menu-key:hover { - color: var(--fg); +@media (hover: hover) { + .theme-key:hover, + .menu-key:hover { + color: var(--fg); + } } .menu-key { diff --git a/src/styles/home/about.css b/src/styles/home/about.css @@ -71,12 +71,18 @@ text-underline-offset: 0.25em; } -.gl-subj:hover, .gl-subj[aria-expanded="true"] { text-decoration-style: solid; text-decoration-color: currentColor; } +@media (hover: hover) { + .gl-subj:hover { + text-decoration-style: solid; + text-decoration-color: currentColor; + } +} + .gl-body, .gl-desc { color: var(--dim); diff --git a/src/styles/home/coffee.css b/src/styles/home/coffee.css @@ -90,8 +90,10 @@ line-height: 1; } -.coffee-close:hover { - color: var(--fg); +@media (hover: hover) { + .coffee-close:hover { + color: var(--fg); + } } @media (max-width: 640px) { diff --git a/src/styles/home/contact.css b/src/styles/home/contact.css @@ -32,13 +32,16 @@ text-underline-offset: 0.2em; } -.linkish:hover { - text-decoration-color: currentColor; +@media (hover: hover) { + .linkish:hover { + text-decoration-color: currentColor; + } } .contact-grid { display: grid; - grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + /* wide enough for the import command on one line */ + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 0 56px; } @@ -64,7 +67,9 @@ min-width: 0; margin: 0; padding: 7px 12px; - overflow-x: auto; + /* no scrollbar: where it doesn't fit, the command wraps onto another line */ + white-space: pre-wrap; + overflow-wrap: anywhere; border: 1px solid var(--rule); background: var(--wash); font-size: 12px; diff --git a/src/styles/home/pinout.css b/src/styles/home/pinout.css @@ -116,11 +116,16 @@ white-space: nowrap; } -.pin:hover, .pin.is-active { color: var(--fg); } +@media (hover: hover) { + .pin:hover { + color: var(--fg); + } +} + .pin.is-active .name { color: var(--c, var(--fg)); font-weight: 600; diff --git a/src/styles/home/posts.css b/src/styles/home/posts.css @@ -48,16 +48,18 @@ color: var(--magenta); } -/* the stars keep the heading's colour; the title itself is plain text */ +/* the title takes the heading's colour, like its stars */ .org-h2 a { min-width: 0; - color: var(--fg); + color: inherit; text-decoration: none; } -.org-h2 a:hover { - text-decoration: underline; - text-decoration-thickness: 1px; +@media (hover: hover) { + .org-h2 a:hover { + text-decoration: underline; + text-decoration-thickness: 1px; + } } .org-h .count { diff --git a/src/styles/home/projects.css b/src/styles/home/projects.css @@ -74,8 +74,10 @@ transition: border-color 150ms ease, color 150ms ease; } -.pj-card:hover { - border-color: var(--faint); +@media (hover: hover) { + .pj-card:hover { + border-color: var(--faint); + } } .pj-card[aria-selected="true"] { diff --git a/src/styles/post/nav.css b/src/styles/post/nav.css @@ -19,8 +19,10 @@ text-decoration: none; } -.post-nav a:hover { - text-decoration: underline; +@media (hover: hover) { + .post-nav a:hover { + text-decoration: underline; + } } .post-nav .dim { diff --git a/src/styles/post/outline.css b/src/styles/post/outline.css @@ -28,8 +28,10 @@ text-decoration: none; } -.post-toc a:hover { - color: var(--fg); +@media (hover: hover) { + .post-toc a:hover { + color: var(--fg); + } } .post-toc a[aria-current="true"] {