website

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

commit aad4bf10dede4fe6afa66fa8ea86d9082e6c1caa
parent 4c4913d39097c109c298cbed73692d837ef38d84
Author: Michael Neuper <michael@michaelneuper.com>
Date:   Mon, 12 Oct 2026 20:59:12 +0200

improve ui

Diffstat:
Msrc/components/home/Hero.astro | 4++--
Msrc/components/home/Projects.astro | 18+++++++++---------
Msrc/content.config.ts | 5++++-
Msrc/content/projects/8-bit-computer.md | 3++-
Msrc/content/projects/chat-voip-p2p.md | 3++-
Msrc/content/projects/jvm-compiler.md | 3++-
Msrc/content/projects/os-scheduler.md | 3++-
Msrc/content/projects/othello-mcts.md | 3++-
Msrc/scripts/demos/computer/board.js | 17+++++++++++------
Msrc/scripts/demos/scheduler.js | 2+-
Msrc/scripts/emacs/buffers.js | 20+++++++++++++++++++-
Msrc/scripts/home/pinout.js | 33++++++++++++++++++++++++++++++++-
Msrc/scripts/home/projects.js | 15+++++++++------
Msrc/styles/base.css | 1-
Msrc/styles/demos/computer.css | 23+++++++++++------------
Msrc/styles/frame/buffer.css | 17++++++++---------
Msrc/styles/frame/modeline.css | 2+-
Msrc/styles/home/hero.css | 23+++++++++++++++++------
Msrc/styles/home/pinout.css | 25++++++++++++++++++++++---
Msrc/styles/home/posts.css | 5+++--
Msrc/styles/home/projects.css | 124+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/styles/missing/status-bits.css | 5+++--
Msrc/styles/post/org.css | 40+++++++++++++++++++++++++++++++++-------
Msrc/styles/post/outline.css | 2+-
Msrc/styles/themes.css | 10++++++++++
25 files changed, 283 insertions(+), 123 deletions(-)

diff --git a/src/components/home/Hero.astro b/src/components/home/Hero.astro @@ -21,8 +21,8 @@ import "../../styles/home/pinout.css"; <div class="hero-chip"> <figure class="pinout"> <div class="chip-wrap"></div> - <div class="pin-readout" aria-live="polite"><p class="dim">Point at a pin to read its function.</p></div> - <figcaption>Figure 1. MN-2026, DIP-16 package, top view.</figcaption> + <div class="pin-readout"></div> + <figcaption>Figure 1. MN-2026, DIP-16 package, top view.<span class="pin-hint"> Point at a pin to read its function.</span></figcaption> <ol class="pin-list"> {PINS.map((p) => <li data-name={p.name} data-type={p.type} data-cat={p.cat}>{p.text}</li>)} </ol> diff --git a/src/components/home/Projects.astro b/src/components/home/Projects.astro @@ -17,7 +17,7 @@ const projects = await Promise.all( .map(async (p) => ({ ...p.data, Body: (await render(p)).Content })), ); --- -<Buffer id="projects" file="projects.map" mode="Fundamental" metaAttrs={{ "data-pj": "meta" }}> +<Buffer id="projects" file="~/projects/" mode="Dired" metaAttrs={{ "data-pj": "meta" }}> <h2 class="h2">Projects</h2> <div class="pj"> @@ -38,16 +38,16 @@ const projects = await Promise.all( <ol class="pj-list"> {projects.map((p) => ( - <li class="proj" data-cat={p.cat} data-demo={p.demo} data-demo-note={p.demoNote}> + <li class="proj" data-name={p.name} data-cat={p.cat} data-demo={p.demo} data-demo-note={p.demoNote}> <p class="proj-cat">{CATS[p.cat] ?? p.cat}</p> - <h3 class="proj-name">{p.name}</h3> + <h3 class="proj-name">{p.title}</h3> <p class="proj-sum">{p.summary}</p> - <div class="proj-body"> - <p.Body /> - {p.stack && <p class="proj-stack">{p.stack}</p>} - {p.links.length > 0 && <p class="proj-links">{p.links.map((l) => <a href={l.href}>{l.text}</a>)}</p>} - {p.note && <p class="proj-links dim">{p.note}</p>} - </div> + <dl class="proj-facts"> + {p.stack && <div><dt>stack</dt><dd>{p.stack}</dd></div>} + {p.links.length > 0 && <div><dt>links</dt><dd>{p.links.map((l) => <a href={l.href}>{l.text}</a>)}</dd></div>} + {p.note && <div><dt>source</dt><dd>{p.note}</dd></div>} + </dl> + <div class="proj-body"><p.Body /></div> </li> ))} </ol> diff --git a/src/content.config.ts b/src/content.config.ts @@ -22,7 +22,10 @@ const posts = defineCollection({ const projects = defineCollection({ loader: glob({ pattern: "*.md", base: "./src/content/projects" }), schema: z.object({ + // the slug, shown in mono on the project's card name: z.string(), + // what the project is, as its heading + title: z.string(), order: z.number(), cat: z.enum(["hw", "sw", "math", "sec"]), summary: z.string(), @@ -34,7 +37,7 @@ const projects = defineCollection({ demoNote: z.string().optional(), stack: z.string().optional(), links: z.array(z.object({ text: z.string(), href: z.string() })).default([]), - // A line in place of links, like "Source private (coursework)". + // Where the source is when there's no link, shown as "source": "private (coursework)". note: z.string().optional(), }), }); diff --git a/src/content/projects/8-bit-computer.md b/src/content/projects/8-bit-computer.md @@ -1,9 +1,10 @@ --- name: 8-bit-computer +title: "8-bit breadboard computer" order: 1 cat: hw demo: computer -demoNote: "A JavaScript simulation of the board, made for this page. It runs the same instructions and microcode as the breadboards, so you can step through a program one control word at a time." +demoNote: "A JavaScript simulation of the board, with the build’s instructions and microcode. Click a bit in RAM to change the program, or 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: diff --git a/src/content/projects/chat-voip-p2p.md b/src/content/projects/chat-voip-p2p.md @@ -1,12 +1,13 @@ --- name: chat-voip-p2p +title: "Chat, voice calls and P2P file sharing" order: 5 cat: sw 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: "Source private (coursework, group project)" +note: "private (coursework, group project)" --- Four projects my group built for a university course: diff --git a/src/content/projects/jvm-compiler.md b/src/content/projects/jvm-compiler.md @@ -1,12 +1,13 @@ --- name: jvm-compiler +title: "Compiler to JVM bytecode" order: 4 cat: sw 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: "Source private (computer architecture coursework)" +note: "private (computer architecture 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. diff --git a/src/content/projects/os-scheduler.md b/src/content/projects/os-scheduler.md @@ -1,12 +1,13 @@ --- name: os-scheduler +title: "Multicore OS scheduler" order: 2 cat: sw 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: "Source private (concurrency coursework)" +note: "private (concurrency 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. diff --git a/src/content/projects/othello-mcts.md b/src/content/projects/othello-mcts.md @@ -1,12 +1,13 @@ --- name: othello-mcts +title: "Othello with Monte Carlo tree search" order: 3 cat: sw 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: "Source private (concurrency coursework)" +note: "private (concurrency 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. diff --git a/src/scripts/demos/computer/board.js b/src/scripts/demos/computer/board.js @@ -24,12 +24,13 @@ let boost = 1; let boostTimer = 0; let current = null; +// The controls go above the board so they're in sight with it; c, n and r are +// in the buttons' tooltips and in M-x (commands.js). const TEMPLATE = ` - <div class="board" role="group" aria-label="Simulated 8-bit breadboard computer"></div> <div class="sim-bar"> - <button type="button" class="btn" data-sim="run" aria-pressed="false">run</button> - <button type="button" class="btn" data-sim="step">step</button> - <button type="button" class="btn" data-sim="reset">reset</button> + <button type="button" class="btn" data-sim="run" aria-pressed="false" title="Run or pause (c)" aria-keyshortcuts="c">run</button> + <button type="button" class="btn" data-sim="step" title="Step one clock cycle (n)" aria-keyshortcuts="n">step</button> + <button type="button" class="btn" data-sim="reset" title="Reset, keeping RAM (r)" aria-keyshortcuts="r">reset</button> <label class="sim-field"><span>program</span> <select data-sim="program"> <option value="fibonacci">fibonacci</option> @@ -43,7 +44,7 @@ const TEMPLATE = ` <output>10 Hz</output> </label> </div> - <p class="sim-note">Simulated one clock cycle at a time. <kbd>c</kbd> runs or pauses, <kbd>n</kbd> steps one cycle, <kbd>r</kbd> resets. Click a bit in RAM to change the program, or the display to switch to signed.</p>`; + <div class="board" role="group" aria-label="Simulated 8-bit breadboard computer"></div>`; export function mount(host) { host.innerHTML = TEMPLATE; @@ -445,7 +446,11 @@ export function mount(host) { load("fibonacci"); setHz(+hzIn.value); - run(!reducedMotion.matches || boost > 1); + // It waits for run (or c) so it doesn't flicker beside the write-up, unless a coffee overclock is on. + run(boost > 1); + // Drawn once now, so the board is its full height before it scrolls into view + // (its values fill in, and the page under it would shift). + render(); const api = { run, step, reset, load, board, halted: () => s.halted, running: () => running }; current = api; return () => { diff --git a/src/scripts/demos/scheduler.js b/src/scripts/demos/scheduler.js @@ -19,7 +19,7 @@ export function mount(host) { <button type="button" class="btn" data-k="pause" aria-pressed="false">pause</button> <span class="dim" data-k="stats"></span> </div> - <canvas height="150" aria-label="Gantt chart of processes running on four cores" role="img"></canvas> + <canvas height="150" style="height:150px" aria-label="Gantt chart of processes running on four cores" role="img"></canvas> <p class="cap">Four cores share one ready queue. First come, first served runs each process to completion. Round robin preempts a process after ${QUANTUM} ticks. Preemptive priority always runs the highest-priority processes (p1 is highest), so a new arrival can take a core from a lower-priority one.</p>`; const cv = host.querySelector("canvas"); const sel = host.querySelector("select"); diff --git a/src/scripts/emacs/buffers.js b/src/scripts/emacs/buffers.js @@ -24,7 +24,7 @@ sections.forEach((s) => sectionIO.observe(s)); export function goto(id, quiet = false) { const s = document.getElementById(id); if (!s) return; - // Sections snap to their header line, so scroll to that rather than the padding above it. + // Scroll to the section's header line rather than the padding above it. ($(".winbar", s) ?? s).scrollIntoView({ behavior: reducedMotion.matches ? "auto" : "smooth", block: "start" }); history.replaceState(null, "", id === "top" ? location.pathname : `#${id}`); if (!quiet) message(`"${s.dataset.file}"`); @@ -39,6 +39,24 @@ document.addEventListener("click", (e) => { goto(id, true); }); +// A link from another page (/#posts) lands on the section's header line. The +// browser jumps there before the sections above it have finished building (the +// fonts, the git log, the projects wheel and demo), and they push it down, so +// keep it in place until the page settles or the reader starts scrolling. +const landing = location.hash && sections.find((s) => `#${s.id}` === location.hash); +if (landing && landing.id !== "top") { + const bar = $(".winbar", landing) ?? landing; + const ro = new ResizeObserver(() => bar.scrollIntoView({ behavior: "instant", block: "start" })); + const inputs = ["wheel", "touchstart", "keydown", "pointerdown"]; + const stop = () => { + ro.disconnect(); + for (const t of inputs) removeEventListener(t, stop, true); + }; + for (const t of inputs) addEventListener(t, stop, { capture: true, passive: true }); + ro.observe(document.body); + setTimeout(stop, 3000); +} + export function findSection(word = "") { const w = word.toLowerCase(); return sections.find((s) => s.id === w || s.dataset.file === w) || sections.find((s) => s.id.startsWith(w) || s.dataset.file.startsWith(w)); diff --git a/src/scripts/home/pinout.js b/src/scripts/home/pinout.js @@ -1,5 +1,7 @@ // Builds the DIP-16 pinout from the ordered list of pins in the HTML. -// Pins 1–8 run down the left side, 9–16 run back up the right. +// Pins 1–8 run down the left side, 9–16 run back up the right. VCC is the pin +// shown to start with, and the readout is kept as tall as the longest pin's +// text, so pointing at another pin never moves the page. import { $, $$ } from "../util.js"; import { call } from "../emacs/index.js"; @@ -16,6 +18,7 @@ export function initPinout() { })); const half = pins.length / 2; const readout = $(".pin-readout", fig); + const btns = []; const chip = document.createElement("div"); chip.className = "chip"; @@ -43,6 +46,7 @@ export function initPinout() { btn.addEventListener("pointerenter", () => show(p, btn)); btn.addEventListener("focus", () => show(p, btn)); btn.addEventListener("click", () => show(p, btn)); + btns.push(btn); if (i < half) left.append(btn); else right.prepend(btn); }); @@ -50,9 +54,36 @@ export function initPinout() { $(".chip-wrap", fig).append(chip); fig.classList.add("is-built"); + let current = pins[0]; function show(p, btn) { $$(".pin.is-active", chip).forEach((b) => b.classList.remove("is-active")); btn.classList.add("is-active"); + current = p; + render(p); + } + + // Each pin's text in turn, measured at the readout's width; the tallest sets its height. + let width = 0; + function fit() { + if (readout.clientWidth === width) return; + width = readout.clientWidth; + readout.style.minHeight = ""; + let tallest = 0; + for (const p of pins) { + render(p); + tallest = Math.max(tallest, readout.offsetHeight); + } + readout.style.minHeight = `${tallest}px`; + render(current); + } + + show(pins[0], btns[0]); + fit(); + new ResizeObserver(fit).observe(readout); + // announced from now on, not the VCC text shown on load + readout.setAttribute("aria-live", "polite"); + + function render(p) { if (p.ch) readout.dataset.cat = p.ch; else delete readout.dataset.cat; readout.innerHTML = ""; diff --git a/src/scripts/home/projects.js b/src/scripts/home/projects.js @@ -5,7 +5,8 @@ // the detail, with its demo. // // On wide screens the wheel stands in the left margin, bulging towards the -// content; on narrow ones it lies across the top of the section. +// content; on narrower ones it lies across the top of the section, so the +// project gets the whole column. import { $, $$, on, onFrame, reducedMotion, clamp } from "../util.js"; @@ -24,7 +25,7 @@ export function initProjects() { const spine = $(".pj-spine", root); const meta = $('[data-pj="meta"]'); const hint = $(".pj-hint", root); - const across = matchMedia("(max-width: 999px)"); + const across = matchMedia("(max-width: 1399px)"); const n = cards.length; if (!n) return; @@ -233,10 +234,12 @@ export function initProjects() { const li = items[i]; detail.dataset.cat = li.dataset.cat; detail.setAttribute("aria-labelledby", cards[i].id); - // the write-up, and the demo beside it (or under it when there isn't room) + // the write-up, and the demo beside it (or under it when there isn't room); + // 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 text = document.createElement("div"); text.className = "pj-text"; - text.append(...[".proj-cat", ".proj-name", ".proj-sum", ".proj-body"].map((s) => $(s, li).cloneNode(true))); + text.append(...[".proj-name", ".proj-facts", ".proj-body"].map((s) => $(s, li).cloneNode(true))); const row = document.createElement("div"); row.className = "pj-row"; row.append(text); @@ -271,9 +274,9 @@ export function initProjects() { } } - // Other parts of the page can open a project by name (the computer commands do). + // Other parts of the page can open a project by its slug (the computer commands do). on("project", (name) => { - const i = items.findIndex((li) => $(".proj-name", li).textContent.trim() === name); + const i = items.findIndex((li) => li.dataset.name === name); if (i >= 0) goTo(i, { now: true }); }); diff --git a/src/styles/base.css b/src/styles/base.css @@ -10,7 +10,6 @@ html { background: var(--bg); color: var(--fg); scroll-behavior: smooth; - scroll-snap-type: y proximity; scroll-padding-top: calc(var(--top-h) + 24px); -webkit-text-size-adjust: 100%; text-size-adjust: 100%; diff --git a/src/styles/demos/computer.css b/src/styles/demos/computer.css @@ -1,22 +1,14 @@ -/* The 8-bit computer: Ben Eater's layout, with its controls underneath */ +/* The 8-bit computer: its controls, then Ben Eater's layout */ .sim-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 10px; - margin-top: 16px; + margin-bottom: 12px; font-size: 12px; } -.sim-note { - max-width: 78ch; - margin: 12px 0 0; - font-size: 12px; - line-height: 1.6; - color: var(--dim); -} - /* board: Ben Eater's layout, two columns of modules either side of the bus */ .board { @@ -88,7 +80,7 @@ .sig { padding: 0 0.4ch; border-radius: 2px; - color: var(--faint); + color: var(--dim); font-size: 10.5px; } @@ -264,12 +256,13 @@ align-items: center; gap: 3px; width: 3.4ch; - color: var(--faint); + color: var(--dim); font-size: 10px; } .cw-bit.on { color: var(--fg); + font-weight: 600; } /* what the step does, kept to two lines so the board doesn't jump as it changes */ @@ -335,9 +328,15 @@ padding: 6px 8px 8px; } + /* the sixteen rows packed tighter, so RAM's column is no taller than the other one */ + .ram-rows { + gap: 0; + } + .ram-row { --led: 6px; grid-template-columns: 1.5ch 2ch auto; + line-height: 12px; } .ram-row .dis { diff --git a/src/styles/frame/buffer.css b/src/styles/frame/buffer.css @@ -1,19 +1,18 @@ /* A buffer: a section of the page with its winbar (file name, a rule, and a note) */ .sec { + --sec-pad: 136px; max-width: var(--col); margin: 0 auto; - padding: 136px var(--gutter) 0; -} - -.hero, -.sec > .winbar { - scroll-snap-align: start; + padding: var(--sec-pad) var(--gutter) 0; + /* A link to a section from another page (/#projects) lands on its winbar, the + same place the menu scrolls it to, not on the padding above. */ + scroll-margin-top: calc(-1 * var(--sec-pad)); } /* On the home page each section is at least a screen tall from its winbar down, - so with its winbar at the top (where scrolling snaps it) the next one is out of - sight. Every section has the same padding above its winbar, so that padding + so with its winbar at the top (where the menu scrolls it) the next one is out + of sight. Every section has the same padding above its winbar, so that padding cancels out. lvh: tall enough when a phone's toolbars are hidden too. */ .hero ~ .sec { min-height: calc(100vh - var(--top-h) - 24px); @@ -73,7 +72,7 @@ @media (max-width: 640px) { .sec { - padding-top: 104px; + --sec-pad: 104px; } .winbar { diff --git a/src/styles/frame/modeline.css b/src/styles/frame/modeline.css @@ -220,7 +220,7 @@ } .wk-arrow { - color: var(--faint); + color: var(--dim); } .wk-desc { diff --git a/src/styles/home/hero.css b/src/styles/home/hero.css @@ -1,13 +1,15 @@ /* The landing section: the name (which decodes itself on load), a lede and the colour key */ +/* The section fills the screen, but its contents hang from a shared top line in the + upper part of it: the name and the chip start level, and the space is left below. */ .hero { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 32px clamp(32px, 4vw, 64px); - align-items: center; + align-items: start; min-height: 100vh; min-height: 100svh; - padding: calc(var(--top-h) + 28px) var(--gutter) calc(var(--frame-h) + 32px); + padding: calc(var(--top-h) + clamp(32px, 12vh, 136px)) var(--gutter) calc(var(--frame-h) + 32px); max-width: 1440px; margin: 0 auto; } @@ -59,10 +61,9 @@ } .hero-lede { - max-width: 44ch; - margin: 0 0 20px; - font-size: 14px; - line-height: 1.6; + max-width: 30em; + margin: 0 0 24px; + font: 400 19px/1.55 var(--serif); } .legend { @@ -97,3 +98,13 @@ max-width: 640px; } } + +@media (max-width: 640px) { + .hero { + padding-top: calc(var(--top-h) + 28px); + } + + .hero-lede { + font-size: 17px; + } +} diff --git a/src/styles/home/pinout.css b/src/styles/home/pinout.css @@ -18,8 +18,11 @@ height: 9px; } -.hero-chip .pin-readout { - min-height: 6.5em; +/* the readout's height is set by src/scripts/home/pinout.js to fit the longest pin; + without JavaScript the pin list below says it all, and there is nothing to point at */ +.pinout:not(.is-built) .pin-readout, +.pinout:not(.is-built) .pin-hint { + display: none; } .pinout { @@ -129,7 +132,6 @@ } .pin-readout { - min-height: 7.5em; margin-top: 20px; padding-top: 12px; border-top: 1px solid var(--rule); @@ -177,3 +179,20 @@ max-width: 420px; } } + +/* on a phone the chip is about two thirds of the width, with the readout full width under it */ +@media (max-width: 640px) { + .hero-chip .chip-wrap { + width: 72%; + margin-inline: auto; + } + + .hero-chip .chip { + grid-template-columns: minmax(0, 1fr) 84px minmax(0, 1fr); + font-size: 12px; + } + + .hero-chip .pin { + height: 30px; + } +} diff --git a/src/styles/home/posts.css b/src/styles/home/posts.css @@ -48,9 +48,10 @@ color: var(--magenta); } +/* the stars keep the heading's colour; the title itself is plain text */ .org-h2 a { min-width: 0; - color: inherit; + color: var(--fg); text-decoration: none; } @@ -86,7 +87,7 @@ } .org-drawer .k { - color: var(--faint); + color: var(--dim); } .org-drawer .ts { diff --git a/src/styles/home/projects.css b/src/styles/home/projects.css @@ -1,19 +1,20 @@ /* Projects: a wheel of cards (out in the left margin on wide screens, across the top - on narrow ones) and the write-up and demo of the one picked on it + on narrower ones) and the write-up and demo of the one picked on it (src/scripts/home/projects.js) */ .pj { - --wheel: 200px; - --pj-gap: 44px; - display: grid; - grid-template-columns: var(--wheel) minmax(0, 1fr); - column-gap: var(--pj-gap); - align-items: start; + /* the wheel's height, and the least a project's split takes */ + --pj-h: min(620px, calc(100svh - var(--top-h) - var(--frame-h) - 72px)); } /* with room beside the content column, the wheel stands out in the margin */ @media (min-width: 1400px) { .pj { + --pj-gap: 44px; + display: grid; + grid-template-columns: var(--wheel) minmax(0, 1fr); + column-gap: var(--pj-gap); + align-items: start; --wheel: min(236px, calc((100vw - var(--col)) / 2 + var(--gutter) - var(--pj-gap) - 20px)); margin-left: calc(-1 * (var(--wheel) + var(--pj-gap))); } @@ -24,7 +25,7 @@ .pj-wheel { position: sticky; top: calc(var(--top-h) + 24px); - height: min(620px, calc(100svh - var(--top-h) - var(--frame-h) - 72px)); + height: var(--pj-h); touch-action: none; -webkit-user-select: none; user-select: none; @@ -165,7 +166,7 @@ bottom: 4px; margin: 0; font-size: 11px; - color: var(--faint); + color: var(--dim); } .pj-hint kbd { @@ -253,12 +254,18 @@ /* ── a project ─────────────────────────────────────────────────────── */ +/* The write-up and the demo are two windows side by side, as after C-x 3: the + same height, with a divider between them. The split is at least as tall as + the wheel, so picking another project hardly changes the page's height. */ .pj-detail { container: pj-detail / inline-size; - min-height: 420px; } +/* the category, as a header line over each project in the list without + JavaScript (on the wheel, the card shows it) */ .proj-cat { + display: flex; + align-items: center; margin: 0 0 12px; font-size: 12px; color: var(--dim); @@ -266,30 +273,63 @@ .proj-cat::before { content: ""; - display: inline-block; + flex: none; width: 7px; height: 7px; margin-right: 1ch; background: var(--c, var(--fg)); - vertical-align: 0.06em; +} + +.proj-cat::after { + content: ""; + flex: 1; + height: 1px; + margin-left: 1.5ch; + background: var(--rule); } .proj-name { - margin: 0 0 10px; + margin: 0 0 16px; font: 400 clamp(30px, 3.6vw, 44px)/1.06 var(--serif); } .proj-sum { max-width: 64ch; - margin: 0 0 28px; + margin: 0 0 18px; font-size: 13px; color: var(--dim); } +/* stack, links and source, labelled */ +.proj-facts { + margin: 0 0 26px; + font-size: 13px; +} + +.proj-facts div { + display: flex; + gap: 1.5ch; +} + +.proj-facts dt { + flex: none; + width: 6ch; + color: var(--dim); +} + +.proj-facts dd { + margin: 0; +} + +.proj-facts a { + margin-right: 2ch; +} + +/* the write-up reads at the same size as About */ .proj-body > p, .proj-body li { - max-width: 36em; - font: 400 17px/1.6 var(--serif); + max-width: 34em; + font: 400 19px/1.55 var(--serif); } .proj-body > p { @@ -306,20 +346,15 @@ margin-bottom: 6px; } -.proj-body > .proj-stack, -.proj-body > .proj-links { - font: 400 12px/1.6 var(--mono); -} - -.proj-stack { - color: var(--dim); -} - -.proj-links a { - margin-right: 2ch; +/* on phones, like About's text */ +@media (max-width: 640px) { + .proj-body > p, + .proj-body li { + font-size: 17px; + } } -/* the demo, under a header line like a winbar's: *demo*, a rule, then a note on what it is */ +/* the demo, under a header line of its own: *demo*, a rule, then a note on what it is */ .pj-demo { margin-top: 36px; font-size: 12px; @@ -329,7 +364,7 @@ display: flex; align-items: center; gap: 1.5ch; - margin: 0 0 6px; + margin: 0 0 12px; } .pj-demo-head .rule { @@ -345,27 +380,30 @@ color: var(--dim); } -/* with room, the demo sits beside the write-up */ +/* with room, the two windows sit side by side */ @container pj-detail (min-width: 820px) { .pj-row { display: flex; - gap: 48px; - align-items: flex-start; + min-height: var(--pj-h); } .pj-row > .pj-text { - flex: 0 0 calc(46% - 24px); + flex: 0 0 50%; + padding-right: 32px; } - .pj-row .proj-body > p:not(.proj-stack, .proj-links), + /* a touch smaller beside the demo, so the lines don't get too short */ + .pj-row .proj-body > p, .pj-row .proj-body li { - font-size: 16px; + font-size: 18px; } .pj-row > .pj-demo { flex: 1; min-width: 0; margin-top: 0; + padding-left: 32px; + border-left: 1px solid var(--rule); } } @@ -377,8 +415,7 @@ } .pj-list .proj { - padding: 32px 0; - border-top: 1px solid var(--rule); + padding: 32px 0 0; } .pj:not(.is-built) { @@ -392,13 +429,10 @@ display: none; } -/* ── narrow screens: the wheel lies across the top ────────────────── */ - -@media (max-width: 999px) { - .pj { - display: block; - } +/* ── narrower screens: the wheel lies across the top, leaving the whole + column to the project ───────────────────────────────────────────── */ +@media (max-width: 1399px) { .pj-wheel { position: relative; top: auto; @@ -447,8 +481,4 @@ bottom: 0; left: var(--gutter); } - - .pj-detail { - min-height: 0; - } } diff --git a/src/styles/missing/status-bits.css b/src/styles/missing/status-bits.css @@ -19,7 +19,8 @@ align-items: center; gap: 0.14em; padding: 0 0.04em; - color: var(--faint); + /* unlit 0s, dimmer than the labels but still 3:1 against the background, as large text needs */ + color: color-mix(in srgb, var(--fg) 48%, var(--bg)); font-variant-numeric: tabular-nums; transition: color 120ms ease; } @@ -33,7 +34,7 @@ } .sb-bit .w { - color: var(--faint); + color: var(--dim); font: 400 clamp(9px, 1.2vw, 12px)/1 var(--mono); } diff --git a/src/styles/post/org.css b/src/styles/post/org.css @@ -47,7 +47,7 @@ } .org-head .org-key { - color: var(--faint); + color: var(--dim); } .org-ts { @@ -105,7 +105,7 @@ } .post-body li::marker { - color: var(--faint); + color: var(--dim); font: 400 15px var(--mono); } @@ -196,17 +196,43 @@ content: "+"; } +/* A strike around whole paragraphs is a block, with its markers at the start + of the first paragraph and the end of the last instead of on lines of their own. */ +.post-body :is(s, del):has(> :is(p, ul, ol, blockquote)) { + display: block; +} + +.post-body :is(s, del):has(> :is(p, ul, ol, blockquote))::before, +.post-body :is(s, del):has(> :is(p, ul, ol, blockquote))::after { + content: none; +} + +.post-body :is(s, del) > p:first-child::before, +.post-body :is(s, del) > p:last-child::after { + content: "+"; +} + .post-body :not(pre) > code::before, .post-body :not(pre) > code::after, .post-body :is(em, strong, s, del)::before, -.post-body :is(em, strong, s, del)::after { - color: var(--faint); +.post-body :is(em, strong, s, del)::after, +.post-body :is(s, del) > p:first-child::before, +.post-body :is(s, del) > p:last-child::after { + color: var(--dim); font-family: var(--mono); font-style: normal; font-weight: 400; text-decoration: none; } +/* inline-block, so the strike's line doesn't run through its own markers */ +.post-body :is(s, del)::before, +.post-body :is(s, del)::after, +.post-body :is(s, del) > p:first-child::before, +.post-body :is(s, del) > p:last-child::after { + display: inline-block; +} + .post-body kbd { font: 400 0.72em/1 var(--mono); vertical-align: 0.1em; @@ -225,7 +251,7 @@ .post-body pre::before, .post-body pre::after { display: block; - color: var(--faint); + color: var(--dim); } .post-body pre::before { @@ -255,7 +281,7 @@ .post-body blockquote::after { display: block; margin-left: -40px; - color: var(--faint); + color: var(--dim); font: 400 13px/1.6 var(--mono); } @@ -282,7 +308,7 @@ .post-body hr::before { content: "-----"; - color: var(--faint); + color: var(--dim); font: 400 13px/1 var(--mono); } diff --git a/src/styles/post/outline.css b/src/styles/post/outline.css @@ -38,7 +38,7 @@ } .post-toc .mk { - color: var(--faint); + color: var(--dim); } @media (min-width: 1100px) { diff --git a/src/styles/themes.css b/src/styles/themes.css @@ -104,6 +104,16 @@ --glow: 0; } +/* gruvbox's blue is a grey-green beside its yellow, purple and red, so the + software colour pulls it towards blue, keeping a little of its own tone */ +:root[data-theme="doom-gruvbox"] { + --c-sw: color-mix(in oklch, var(--blue) 20%, oklch(0.7 0.11 242)); +} + +:root[data-theme="doom-gruvbox-light"] { + --c-sw: color-mix(in oklch, var(--blue) 40%, oklch(0.47 0.11 245)); +} + [data-cat="hw"] { --c: var(--c-hw); } [data-cat="sw"] { --c: var(--c-sw); } [data-cat="math"] { --c: var(--c-math); }