commit 2bf76e2ee6f1e03a1c2f0dfa8411924082a6660a
parent 50b459cd6d822d87d5c9981389689e1fa235487e
Author: Michael Neuper <michael@michaelneuper.com>
Date: Mon, 12 Oct 2026 16:06:40 +0200
style posts as org buffers
Diffstat:
7 files changed, 357 insertions(+), 81 deletions(-)
diff --git a/netlify.toml b/netlify.toml
@@ -16,3 +16,14 @@ for = "/*"
for = "/_astro/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
+
+# The comments' frame on giscus.app loads its stylesheet and our fonts from here.
+[[headers]]
+for = "/giscus/*"
+ [headers.values]
+ Access-Control-Allow-Origin = "https://giscus.app"
+
+[[headers]]
+for = "/fonts/*"
+ [headers.values]
+ Access-Control-Allow-Origin = "https://giscus.app"
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
@@ -25,6 +25,9 @@ const {
skip,
} = Astro.props;
const at = home ? "" : "/";
+// Google Analytics, as on the Hugo site: production builds only, so local work isn't counted.
+const GA_ID = "G-BLX24KP7CS";
+const year = new Date().getFullYear();
---
<!doctype html>
<html lang="en" data-theme="doom-gruvbox">
@@ -47,6 +50,17 @@ const at = home ? "" : "/";
if (t) document.documentElement.dataset.theme = t;
} catch (e) {}
</script>
+{import.meta.env.PROD && (
+ <Fragment>
+ <script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}></script>
+ <script is:inline define:vars={{ GA_ID }}>
+ window.dataLayer = window.dataLayer || [];
+ function gtag() { dataLayer.push(arguments); }
+ gtag("js", new Date());
+ gtag("config", GA_ID);
+ </script>
+ </Fragment>
+)}
<slot name="head" />
</head>
<!-- Reading the source? Good instinct. flag{view_source_is_recon} -->
@@ -59,7 +73,6 @@ const at = home ? "" : "/";
<span>michael neuper</span>
</a>
<nav class="nav" id="nav" aria-label="Sections">
- <a href={`${at}#top`}>pinout</a>
<a href={`${at}#about`}>about</a>
<a href={`${at}#projects`}>projects</a>
<a href={`${at}#posts`}>posts</a>
@@ -71,6 +84,10 @@ const at = home ? "" : "/";
<slot />
+<footer class="foot">
+ <p>© {year} Michael Neuper</p>
+</footer>
+
<!-- ───────────────────────── modeline and minibuffer ───────────────────────── -->
<div class="which-key" hidden aria-live="polite"></div>
<div class="frame-bottom">
diff --git a/src/pages/404.astro b/src/pages/404.astro
@@ -13,8 +13,5 @@ import Base from "../layouts/Base.astro";
<p class="lede">Nothing lives at this address. If it was a page on my old site, it didn't make the move, but every post did, at the same address as before.</p>
<p class="missing-links"><a href="/">Home</a> <a href="/#posts">Posts</a></p>
</section>
- <footer class="foot">
- <p class="dim">Press <kbd>SPC</kbd> for commands or <kbd>M-x</kbd> to run one by name.</p>
- </footer>
</main>
</Base>
diff --git a/src/pages/index.astro b/src/pages/index.astro
@@ -236,7 +236,6 @@ yearly Vac Weeks.</div></div>
<ul class="elsewhere">
<li><a href="https://github.com/michaelneuper">GitHub</a></li>
<li><a href="https://www.linkedin.com/in/michaelneuper/">LinkedIn</a></li>
- <li><a href="https://michaelneuper.com">michaelneuper.com</a></li>
<li><a href="https://buymeacoffee.com/michaelneuper" target="_blank" rel="noopener">Buy me a coffee</a></li>
</ul>
</div>
@@ -249,10 +248,6 @@ yearly Vac Weeks.</div></div>
</div>
</section>
- <footer class="foot">
- <p>Made with Astro and Emacs, without cookies.</p>
- <p class="dim">Press <kbd>SPC</kbd> for commands or <kbd>M-x</kbd> to run one by name.</p>
- </footer>
</main>
<!-- ───────────────────────── coffee ───────────────────────── -->
diff --git a/src/pages/posts/[slug].astro b/src/pages/posts/[slug].astro
@@ -1,7 +1,8 @@
---
-// One post, shown as its Markdown buffer: front matter, then the text, with an
-// imenu-list window of its headings beside it when the post asks for a table of
-// contents, and the giscus comments from the old site below.
+// One post, shown as an Org buffer: #+ keywords, then the text under starred
+// headings that fold like Org's, with an imenu-list window of the headings
+// beside it when the post asks for a table of contents, and the giscus
+// comments from the old site below. The posts are Markdown underneath.
import { render } from "astro:content";
import "katex/dist/katex.min.css";
import Base from "../../layouts/Base.astro";
@@ -19,37 +20,37 @@ export async function getStaticPaths() {
const { post, newer, older } = Astro.props;
const { Content, headings } = await render(post);
const { title, date, tags, toc } = post.data;
-const file = `${post.id}.md`;
+const file = `${post.id}.org`;
const words = wordCount(post.body ?? "");
const minutes = Math.max(1, Math.round(words / 230));
// (the footnotes' heading is only there for screen readers)
const outline = toc ? headings.filter((h) => (h.depth === 2 || h.depth === 3) && h.slug !== "footnote-label") : [];
---
-<Base title={`${title} - Michael Neuper`} description={summary(post.body ?? "")} buf={file} mode="Markdown" skip="#post">
+<Base title={`${title} - Michael Neuper`} description={summary(post.body ?? "")} buf={file} mode="Org" skip="#post">
<Fragment slot="head">
<script src="../../scripts/page.js"></script>
</Fragment>
<Fragment slot="help">
- <div><dt><code>imenu</code></dt><dd>jump to a heading in this post</dd></div>
+ <div><dt><kbd>TAB</kbd></dt><dd>fold the heading whose stars you clicked</dd></div>
+ <div><dt><kbd>S-TAB</kbd></dt><dd>cycle the whole buffer: overview, contents, show all</dd></div>
+ <div><dt><kbd>SPC i</kbd></dt><dd>imenu: jump to a heading</dd></div>
<div><dt><kbd>SPC n</kbd></dt><dd>back to the list of posts</dd></div>
</Fragment>
<main>
- <section class="sec post" id="post" data-file={file} data-mode="Markdown" aria-labelledby="post-title">
+ <section class="sec post" id="post" data-file={file} data-mode="Org" aria-labelledby="post-title">
<p class="winbar"><span class="file">{file}</span><span class="rule" aria-hidden="true"></span><span class="meta">{words.toLocaleString("en")} words, {minutes} min</span></p>
- <div class="fm">
- <p class="fm-fence" aria-hidden="true">---</p>
- <p><span class="fm-k">date:</span> <time class="fm-ts" datetime={ymd(date)}>{ymd(date)} {weekday(date)}</time></p>
+ <header class="org-head">
+ <h1 class="post-title" id="post-title"><span class="org-key" aria-hidden="true">#+title:</span> {title}</h1>
+ <p class="org-kw"><span class="org-key">#+author:</span> <span class="v">Michael Neuper</span></p>
+ <p class="org-kw"><span class="org-key">#+date:</span> <time class="org-ts" datetime={ymd(date)}><{ymd(date)} {weekday(date)}></time></p>
{tags.length > 0 && (
- <p><span class="fm-k">tags:</span> [{tags.map((t, i) => (
- <Fragment>{i > 0 && ", "}<span class="t" style={`--tag: ${TAG_COLORS[t] ?? "var(--dim)"}`}>{t}</span></Fragment>
- ))}]</p>
+ <p class="org-kw"><span class="org-key">#+filetags:</span> <span class="org-filetags">:{tags.map((t) => (
+ <Fragment><span class="t" style={`--tag: ${TAG_COLORS[t] ?? "var(--dim)"}`}>{t}</span>:</Fragment>
+ ))}</span></p>
)}
- <p class="fm-fence" aria-hidden="true">---</p>
- </div>
-
- <h1 class="post-title" id="post-title">{title}</h1>
+ </header>
<div class:list={["post-cols", { "has-toc": outline.length > 0 }]}>
{outline.length > 0 && (
@@ -57,7 +58,7 @@ const outline = toc ? headings.filter((h) => (h.depth === 2 || h.depth === 3) &&
<p class="winbar"><span class="file" id="toc-title">*Ilist*</span><span class="rule" aria-hidden="true"></span><span class="meta">contents</span></p>
<ol>
{outline.map((h) => (
- <li class={`d${h.depth}`}><a href={`#${h.slug}`}><span class="mk" aria-hidden="true">{"#".repeat(h.depth)}</span> {h.text}</a></li>
+ <li class={`d${h.depth}`}><a href={`#${h.slug}`}><span class="mk" aria-hidden="true">{"*".repeat(h.depth - 1)}</span> {h.text}</a></li>
))}
</ol>
</aside>
@@ -80,8 +81,5 @@ const outline = toc ? headings.filter((h) => (h.depth === 2 || h.depth === 3) &&
<p class="dim giscus-note">Comments load from GitHub Discussions through giscus.<noscript> They need JavaScript.</noscript></p>
</section>
- <footer class="foot">
- <p class="dim">Press <kbd>SPC</kbd> for commands or <kbd>M-x</kbd> to run one by name.</p>
- </footer>
</main>
</Base>
diff --git a/src/scripts/page.js b/src/scripts/page.js
@@ -1,10 +1,11 @@
// Pages other than the home page get the same Emacs (modeline, M-x, themes,
-// which-key), plus, on posts: imenu over the headings, the *Ilist* window
-// following along, and the giscus comments the old site had.
+// which-key), plus, on posts: Org folding, imenu over the headings, the
+// *Ilist* window following along, and the giscus comments the old site had.
import { $, $$, on, themeColors, reducedMotion } from "./util.js";
-import { defcommand, completingRead } from "./shell.js";
+import { defcommand, completingRead, message } from "./shell.js";
import { initCtf } from "./ctf.js";
+import { DEFAULT_THEME } from "./themes.js";
// SPC a, p, n and m still go to the home page's sections.
for (const id of ["top", "about", "projects", "posts", "contact"]) {
@@ -13,15 +14,121 @@ for (const id of ["top", "about", "projects", "posts", "contact"]) {
try { initCtf(); } catch (err) { console.error(err); }
+const body = $(".post-body");
+const heads = $$(".post-body :is(h2, h3)[id]:not(.sr-only)");
+const titles = new Map(heads.map((h) => [h, h.textContent.trim()]));
+
+// ── folding, like Org ─────────────────────────────────────────────────
+// Each heading owns what follows it, up to the next heading at its level or
+// above. Clicking its stars folds that away. With the stars focused, TAB does
+// the same and S-TAB cycles the whole buffer (overview, contents, show all),
+// as in the posts list on the home page. Folding toggles a class rather than
+// moving anything, so the videos keep playing.
+
+const outline = []; // { h, level, folded, owners, stars, ell, hasContent }
+const owned = []; // [element, owners] for everything under a heading
+const nodeOf = new Map();
+let contentsOnly = false;
+
+if (body) {
+ const levelOf = (el) => (/^H[234]$/.test(el.tagName) && !el.classList.contains("sr-only") ? +el.tagName[1] - 1 : 0);
+ const open = []; // the headings we're inside, outermost first
+ for (const el of body.children) {
+ if (el.matches(".footnotes")) break;
+ const level = levelOf(el);
+ if (!level) {
+ owned.push([el, [...open]]);
+ if (open.length) open.at(-1).hasContent = true;
+ continue;
+ }
+ while (open.length && open.at(-1).level >= level) open.pop();
+ const node = { h: el, level, folded: false, owners: [...open], hasContent: false };
+ node.stars = Object.assign(document.createElement("button"), { type: "button", className: "org-stars", tabIndex: -1, textContent: "*".repeat(level) });
+ node.stars.setAttribute("aria-label", "Fold or unfold");
+ node.ell = Object.assign(document.createElement("span"), { className: "org-ellipsis", textContent: "...", hidden: true });
+ node.ell.setAttribute("aria-hidden", "true");
+ el.prepend(node.stars);
+ el.append(node.ell);
+ el.classList.add("has-stars");
+ node.stars.addEventListener("click", () => {
+ toggle(node);
+ node.stars.focus({ preventScroll: true });
+ });
+ node.stars.addEventListener("keydown", (e) => {
+ if (e.key !== "Tab") return;
+ e.preventDefault();
+ if (e.shiftKey) cycle();
+ else toggle(node);
+ });
+ outline.push(node);
+ nodeOf.set(el, node);
+ open.push(node);
+ }
+}
+
+function update() {
+ const away = (owners) => owners.some((o) => o.folded);
+ for (const n of outline) {
+ n.h.classList.toggle("is-folded-away", away(n.owners));
+ n.stars.setAttribute("aria-expanded", String(!n.folded));
+ n.ell.hidden = !(n.folded || (contentsOnly && n.hasContent));
+ }
+ for (const [el, owners] of owned) {
+ el.classList.toggle("is-folded-away", away(owners) || (contentsOnly && owners.length > 0));
+ }
+}
+
+function toggle(node) {
+ if (contentsOnly) {
+ // Leaving the contents view: keep only the path to this heading open.
+ contentsOnly = false;
+ const path = new Set([node, ...node.owners]);
+ for (const n of outline) n.folded = !path.has(n);
+ node.folded = false;
+ } else {
+ node.folded = !node.folded;
+ }
+ update();
+}
+
+const STATES = ["OVERVIEW", "CONTENTS", "SHOW ALL"];
+let state = 2;
+function cycle() {
+ state = (state + 1) % 3;
+ contentsOnly = state === 1;
+ for (const n of outline) n.folded = state === 0 && n.level === 1;
+ update();
+ message(STATES[state]);
+}
+
+// Unfold whatever hides a heading, before jumping to it.
+function reveal(h) {
+ const node = nodeOf.get(h);
+ if (!node) return;
+ contentsOnly = false;
+ for (const o of node.owners) o.folded = false;
+ update();
+}
+
+if (outline.length) {
+ defcommand("org-shifttab", cycle, { doc: "Cycle the whole post: overview, contents, show all" });
+ // S-TAB with nothing focused cycles too, like in the home page's Org buffer.
+ addEventListener("keydown", (e) => {
+ if (e.key === "Tab" && e.shiftKey && document.activeElement === document.body) {
+ e.preventDefault();
+ cycle();
+ }
+ });
+}
+
// ── imenu ─────────────────────────────────────────────────────────────
-const heads = $$(".post-body :is(h2, h3)[id]:not(.sr-only)");
if (heads.length) {
defcommand("imenu", async () => {
- const text = (h) => h.textContent.trim();
- const pick = await completingRead("imenu: ", heads.map((h) => ({ value: text(h), ann: h.tagName === "H3" ? "subsection" : "" })));
- const h = heads.find((x) => text(x) === pick);
+ const pick = await completingRead("imenu: ", heads.map((h) => ({ value: titles.get(h), ann: h.tagName === "H3" ? "subsection" : "" })));
+ const h = heads.find((x) => titles.get(x) === pick);
if (!h) return;
+ reveal(h);
h.scrollIntoView({ behavior: reducedMotion.matches ? "auto" : "smooth", block: "start" });
history.replaceState(null, "", `#${h.id}`);
}, { doc: "Jump to a heading in this post", key: "SPC i" });
@@ -38,6 +145,7 @@ if (tocLinks.length) {
const line = innerHeight * 0.3;
let cur = null;
for (const h of heads) {
+ if (h.offsetParent === null) continue; // folded away
if (h.getBoundingClientRect().top > line) break;
cur = h;
}
@@ -52,19 +160,23 @@ if (tocLinks.length) {
};
addEventListener("scroll", () => { raf ||= requestAnimationFrame(mark); }, { passive: true });
mark();
+ for (const a of tocLinks) {
+ a.addEventListener("click", () => reveal(document.getElementById(decodeURIComponent(a.hash.slice(1)))));
+ }
}
// ── comments ──────────────────────────────────────────────────────────
// Threads are GitHub Discussions matched by path, so the old site's comments
// still show up. The script only loads once you scroll near the comments.
-const GISCUS_THEMES = {
- "doom-gruvbox": "gruvbox_dark",
- "doom-gruvbox-light": "gruvbox_light",
- catppuccin: "catppuccin_mocha",
+// The comments use a stylesheet made from the current theme's colours
+// (src/pages/giscus/[theme].css.js). giscus fetches it from its own origin,
+// which only the deployed site allows, so the dev server uses a built-in look.
+const giscusTheme = () => {
+ const id = document.documentElement.dataset.theme || DEFAULT_THEME;
+ if (!import.meta.env.DEV) return `${location.origin}/giscus/${id}.css`;
+ return themeColors().light ? "noborder_light" : "transparent_dark";
};
-const giscusTheme = () => GISCUS_THEMES[document.documentElement.dataset.theme]
- ?? (themeColors().light ? "noborder_light" : "transparent_dark");
const box = $(".giscus");
if (box) {
diff --git a/src/styles/style.css b/src/styles/style.css
@@ -2248,10 +2248,11 @@ input[type="range"]::-moz-range-thumb {
}
/* ───────────────────────────── posts ─────────────────────────────
- A post is its Markdown buffer: the front matter in mono, then the text in
- the serif, with headings keeping their # markers the way markdown-mode
- shows them. Code is coloured from the current theme through the variables
- Shiki's css-variables theme reads. */
+ A post is an Org buffer the way Doom shows one with mixed-pitch-mode: #+
+ keywords and markup in mono, the text in the serif, starred headings in
+ the org-level colours (the same as the posts list on the home page), and
+ code between #+begin_src and #+end_src. Code is coloured from the current
+ theme through the variables Shiki's css-variables theme reads. */
:root {
--astro-code-foreground: var(--fg);
@@ -2283,37 +2284,39 @@ input[type="range"]::-moz-range-thumb {
white-space: nowrap;
}
-.fm {
- margin: 0 0 32px;
- color: var(--faint);
+.org-head {
+ margin: 0 0 56px;
font-size: 13px;
- line-height: 1.7;
-}
-
-.fm p {
- margin: 0;
+ line-height: 1.75;
}
-.fm-k {
- color: var(--blue);
+.org-head .org-key {
+ color: var(--faint);
}
-.fm-ts {
+.org-ts {
color: var(--cyan);
}
-.fm .t {
+.org-filetags .t {
color: var(--tag, var(--dim));
}
.post-title {
max-width: 18em;
- margin: 0 0 56px;
+ margin: 0 0 20px;
font: 400 clamp(34px, 4.8vw, 54px)/1.08 var(--serif);
letter-spacing: -0.01em;
text-wrap: balance;
}
+.post-title .org-key {
+ margin-right: 0.3ch;
+ font: 400 15px/1 var(--mono);
+ letter-spacing: 0;
+ vertical-align: 0.5em;
+}
+
.post-body {
max-width: 680px;
font: 400 19px/1.6 var(--serif);
@@ -2335,14 +2338,24 @@ input[type="range"]::-moz-range-thumb {
padding-left: 1.3em;
}
+.post-body ul {
+ list-style-type: "- ";
+}
+
.post-body li {
margin: 0.25em 0;
}
+.post-body li::marker {
+ color: var(--faint);
+ font: 400 15px var(--mono);
+}
+
.post-body li > p {
margin: 0;
}
+/* headings, coloured like org-level-1, -2 and -3 */
.post-body h2,
.post-body h3,
.post-body h4 {
@@ -2353,60 +2366,145 @@ input[type="range"]::-moz-range-thumb {
}
.post-body h2 {
+ color: var(--blue);
font-size: 30px;
}
.post-body h3 {
+ color: var(--magenta);
font-size: 24px;
}
.post-body h4 {
+ color: var(--violet);
font-size: 20px;
}
-.post-body :is(h2, h3, h4)::before {
+/* The stars. page.js swaps them for buttons that fold the heading. */
+.post-body :is(h2, h3, h4)::before,
+.post-body .org-stars {
margin-right: 0.6ch;
- color: var(--faint);
font: 400 15px/1 var(--mono);
vertical-align: 0.2em;
}
-.post-body h2::before { content: "##"; }
-.post-body h3::before { content: "###"; }
-.post-body h4::before { content: "####"; }
+.post-body h2::before { content: "*"; }
+.post-body h3::before { content: "**"; }
+.post-body h4::before { content: "***"; }
+
+.post-body .has-stars::before {
+ content: none;
+}
+.post-body .org-stars {
+ cursor: pointer;
+}
+
+.post-body .org-ellipsis {
+ margin-left: 0.5ch;
+ font: 400 15px/1 var(--mono);
+}
+
+.post-body .is-folded-away {
+ display: none !important;
+}
+
+/* emphasis keeps its Org markers: ~code~ /italic/ *bold* +strike+ */
.post-body :not(pre) > code {
- padding: 0.05em 0.3em;
- border-radius: 2px;
- background: var(--hl);
+ color: var(--orange);
font-size: 0.74em;
}
+.post-body :not(pre) > code::before,
+.post-body :not(pre) > code::after {
+ content: "~";
+}
+
+.post-body em::before,
+.post-body em::after {
+ content: "/";
+}
+
+.post-body strong::before,
+.post-body strong::after {
+ content: "*";
+}
+
+.post-body :is(s, del)::before,
+.post-body :is(s, del)::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);
+ font-style: normal;
+ font-weight: 400;
+ text-decoration: none;
+}
+
.post-body kbd {
font: 400 0.72em/1 var(--mono);
vertical-align: 0.1em;
}
+/* #+begin_src lang ... #+end_src */
.post-body pre {
margin: 1.4em 0;
- padding: 14px 16px;
+ padding: 0 16px;
overflow-x: auto;
background: var(--hl);
font: 400 13px/1.6 var(--mono);
tab-size: 4;
}
+.post-body pre::before,
+.post-body pre::after {
+ display: block;
+ color: var(--faint);
+}
+
+.post-body pre::before {
+ content: "#+begin_src " attr(data-language);
+ padding-top: 8px;
+}
+
+.post-body pre[data-language="py"]::before {
+ content: "#+begin_src python";
+}
+
+.post-body pre::after {
+ content: "#+end_src";
+ padding-bottom: 8px;
+}
+
.post-body pre code {
font-size: inherit;
}
.post-body blockquote {
margin: 1.4em 0;
- padding-left: 1em;
- border-left: 2px solid var(--rule);
color: var(--dim);
}
+.post-body blockquote::before,
+.post-body blockquote::after {
+ display: block;
+ margin-left: -40px;
+ color: var(--faint);
+ font: 400 13px/1.6 var(--mono);
+}
+
+.post-body blockquote::before {
+ content: "#+begin_quote";
+}
+
+.post-body blockquote::after {
+ content: "#+end_quote";
+}
+
.post-body img,
.post-body video {
display: block;
@@ -2416,11 +2514,17 @@ input[type="range"]::-moz-range-thumb {
}
.post-body hr {
- margin: 2.4em 0;
+ margin: 2em 0;
border: 0;
- border-top: 1px solid var(--rule);
}
+.post-body hr::before {
+ content: "-----";
+ color: var(--faint);
+ font: 400 13px/1 var(--mono);
+}
+
+/* org tables: | a | b | */
.post-body table {
display: block;
margin: 1.4em 0;
@@ -2431,14 +2535,14 @@ input[type="range"]::-moz-range-thumb {
.post-body th,
.post-body td {
- padding: 6px 16px 6px 0;
- border-bottom: 1px solid var(--rule);
+ padding: 4px 1ch;
+ border: 1px solid var(--rule);
text-align: left;
vertical-align: top;
}
.post-body th {
- color: var(--dim);
+ color: var(--blue);
font-weight: 400;
}
@@ -2454,16 +2558,58 @@ input[type="range"]::-moz-range-thumb {
overflow-y: hidden;
}
+/* [fn:1] references, and the notes under a * Footnotes heading */
+.post-body sup:has(> a[data-footnote-ref]) {
+ font: 400 13px/1 var(--mono);
+ vertical-align: baseline;
+}
+
+.post-body a[data-footnote-ref] {
+ color: var(--cyan);
+ text-decoration: none;
+}
+
+.post-body a[data-footnote-ref]::before {
+ content: "[fn:";
+}
+
+.post-body a[data-footnote-ref]::after {
+ content: "]";
+}
+
.post-body .footnotes {
- margin-top: 3em;
- padding-top: 1em;
- border-top: 1px solid var(--rule);
- color: var(--dim);
- font-size: 15px;
+ margin-top: 1.7em;
+ font-size: 17px;
+}
+
+.post-body .footnotes h2.sr-only {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip-path: none;
+ white-space: normal;
}
.post-body .footnotes ol {
margin: 0;
+ padding: 0;
+ list-style: none;
+ counter-reset: fn;
+}
+
+.post-body .footnotes li {
+ counter-increment: fn;
+}
+
+.post-body .footnotes li::before {
+ content: "[fn:" counter(fn) "] ";
+ color: var(--cyan);
+ font: 400 13px var(--mono);
+}
+
+.post-body .footnotes li > p {
+ display: inline;
}
/* imenu-list: the post's headings in a window beside it */