248 lines
10 KiB
HTML
248 lines
10 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{{title}}</title>
|
|
<meta name="description" content="{{description}}">
|
|
<meta property="og:title" content="{{title}}">
|
|
<meta property="og:description" content="{{description}}">
|
|
<link rel="alternate" type="application/rss+xml" title="Pascal Maas"
|
|
href="{{root}}feed.xml">
|
|
<script>
|
|
// Runs before the page is drawn, so a saved dark choice doesn't flash.
|
|
try {
|
|
if (localStorage.getItem("theme") === "dark") {
|
|
document.documentElement.dataset.theme = "dark";
|
|
}
|
|
} catch (error) {}
|
|
</script>
|
|
<style>
|
|
/* Served from static/; Georgia shows until these have loaded. */
|
|
@font-face {
|
|
font-family: "Poltawski Nowy"; font-weight: 400; font-style: normal;
|
|
src: url("{{root}}static/PoltawskiNowy-Regular.ttf"); font-display: swap;
|
|
}
|
|
@font-face {
|
|
font-family: "Poltawski Nowy"; font-weight: 400; font-style: italic;
|
|
src: url("{{root}}static/PoltawskiNowy-Italic.ttf"); font-display: swap;
|
|
}
|
|
@font-face {
|
|
font-family: "Poltawski Nowy"; font-weight: 700; font-style: normal;
|
|
src: url("{{root}}static/PoltawskiNowy-Bold.ttf"); font-display: swap;
|
|
}
|
|
@font-face {
|
|
font-family: "Poltawski Nowy"; font-weight: 700; font-style: italic;
|
|
src: url("{{root}}static/PoltawskiNowy-BoldItalic.ttf"); font-display: swap;
|
|
}
|
|
:root {
|
|
--latte: #f4f2ee; --espresso: #231c15;
|
|
--espresso-button: #4a3326; --latte-button: #e3d9cb;
|
|
--bg: var(--latte); --fg: #1d1d1b; --muted: #6b6a66; --line: #dbd6cc;
|
|
--accent: #2f5d8a; --code-bg: #eae6e0;
|
|
/* Pinned header; the side list pins just below it. */
|
|
--header-height: 3.25rem; --header-bg: #ebe8e1;
|
|
--code-block-bg: #eae6e0; --code-text: #1d1d1b;
|
|
--code-comment: #7a7a7a; --code-keyword: #a85a1e; --code-string: #4f6b3f;
|
|
--code-number: #4a6f8f; --code-function: #8a6500;
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--bg: var(--espresso); --fg: #e8e6e1; --muted: #9a978f; --line: #3d3227;
|
|
--accent: #8ab4dc; --code-bg: #2f261d; --header-bg: #2c231a;
|
|
--code-block-bg: #2e2822; --code-text: #d6d4cf;
|
|
--code-comment: #808080; --code-keyword: #cc7832; --code-string: #6a8759;
|
|
--code-number: #6897bb; --code-function: #ffc66d;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0; background: var(--bg); color: var(--fg);
|
|
font: 17px/1.65 "Poltawski Nowy", Georgia, "Iowan Old Style",
|
|
"Times New Roman", serif;
|
|
}
|
|
.wrap { max-width: 56rem; margin: 0 auto; padding: 0 1rem 4rem; }
|
|
a { color: var(--accent); }
|
|
.site-header h1 {
|
|
margin: 0; font-size: 1.6rem; line-height: 1.2; letter-spacing: -0.01em;
|
|
}
|
|
.site-header h1 a { color: inherit; text-decoration: none; }
|
|
.site-header p { display: none; margin: 0; color: var(--muted); }
|
|
.site-header {
|
|
display: flex; justify-content: space-between;
|
|
align-items: center; gap: 1rem;
|
|
position: sticky; top: 0; z-index: 2; background: var(--header-bg);
|
|
height: var(--header-height); padding: .5rem 0;
|
|
/* Stretches the colour and the line under it to the full window width
|
|
while the contents stay in the column. */
|
|
box-shadow: 0 0 0 100vmax var(--header-bg), 0 1px 0 100vmax var(--line);
|
|
clip-path: inset(0 -100vmax -1px);
|
|
}
|
|
.header-actions { display: flex; align-items: center; gap: .75rem; }
|
|
.site-header p, .header-actions { font-size: 15px; }
|
|
/* Same font as the tagline; buttons don't inherit it by default. */
|
|
.header-actions a, .theme-toggle { font: inherit; }
|
|
.header-actions a { color: var(--fg); text-decoration: none; }
|
|
.header-actions a:hover { text-decoration: underline; }
|
|
/* Espresso brown in light mode, latte beige in dark mode. */
|
|
.theme-toggle {
|
|
color: var(--latte); background: var(--espresso-button); cursor: pointer;
|
|
border: 1px solid var(--espresso-button); border-radius: 999px;
|
|
/* Fixed width fits "Espresso", so the Git link doesn't shift. */
|
|
width: 6em; padding: .2rem 0; text-align: center;
|
|
}
|
|
:root[data-theme="dark"] .theme-toggle {
|
|
color: var(--espresso); background: var(--latte-button);
|
|
border-color: var(--latte-button);
|
|
}
|
|
.theme-toggle:hover { opacity: .85; }
|
|
/* Wide enough for the tagline next to the name; hidden below this. */
|
|
@media (min-width: 52rem) {
|
|
.site-title { display: flex; align-items: baseline; gap: .75rem; }
|
|
.site-header p { display: block; }
|
|
}
|
|
|
|
nav { margin: 2.5rem 0 1rem; padding: 1rem 0; border-bottom: 1px solid var(--line); }
|
|
nav h2 { margin: 0 0 .5rem; font-size: .8rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
|
|
nav ol { margin: 0; padding: 0; list-style: none; }
|
|
nav li { display: flex; justify-content: space-between; gap: 1rem; padding: .2rem 0; }
|
|
nav li time { color: var(--muted); font-size: .85rem; white-space: nowrap; }
|
|
nav a { text-decoration: none; }
|
|
nav a:hover { text-decoration: underline; }
|
|
nav a.current { font-weight: 700; color: var(--fg); }
|
|
|
|
/* Side column (14rem) + gap (3rem) + posts (56rem) + padding (2rem). */
|
|
@media (min-width: 75rem) {
|
|
.wrap { max-width: 75rem; }
|
|
.layout { display: grid; grid-template-columns: 14rem 56rem; gap: 3rem; }
|
|
nav {
|
|
position: sticky; top: var(--header-height); align-self: start;
|
|
max-height: calc(100vh - var(--header-height)); overflow-y: auto;
|
|
margin: 0; padding: 2.5rem 0 1rem; border: none;
|
|
}
|
|
nav li, nav li time { display: block; }
|
|
}
|
|
|
|
article { padding: 2.5rem 0; border-bottom: 1px solid var(--line); scroll-margin-top: 1rem; }
|
|
/* Keeps in-page jumps (footnotes) clear of the pinned header. */
|
|
html { scroll-padding-top: calc(var(--header-height) + 1rem); }
|
|
article header h2 { margin: 0; font-size: 1.45rem; line-height: 1.25; }
|
|
article header h2 a { color: inherit; text-decoration: none; }
|
|
article header h2 a:hover { color: var(--accent); }
|
|
.meta { margin: .35rem 0 1.25rem; font-size: .85rem; color: var(--muted); display: flex; gap: .75rem; align-items: center; }
|
|
.tag { border: 1px solid var(--line); border-radius: 999px; padding: 0 .55rem; }
|
|
|
|
/* Defined terms (Markdown "*[term]: definition"); the script below fills
|
|
data-definition with the text for the bubble. */
|
|
abbr[data-definition] {
|
|
position: relative; cursor: help;
|
|
text-decoration: underline dotted; text-underline-offset: .2em;
|
|
}
|
|
abbr[data-definition]:is(:hover, :focus)::after {
|
|
content: attr(data-definition);
|
|
position: absolute; left: 50%; bottom: 100%; z-index: 3;
|
|
transform: translateX(-50%); margin-bottom: .35rem;
|
|
width: max-content; max-width: 16rem; padding: .35rem .6rem;
|
|
background: var(--header-bg); color: var(--fg);
|
|
border: 1px solid var(--line); border-radius: 6px;
|
|
font-size: .85rem; line-height: 1.4;
|
|
}
|
|
|
|
img { display: block; margin: 0 auto; max-width: 100%; height: auto; }
|
|
pre, code { font-family: ui-monospace, "Cascadia Code", Menlo, Consolas, monospace; font-size: .88em; }
|
|
code { background: var(--code-bg); padding: .1em .3em; border-radius: 4px; }
|
|
pre { background: var(--code-block-bg); color: var(--code-text); padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
|
pre code { background: none; padding: 0; }
|
|
/* Pygments token classes, Darcula colours: comments, keywords, strings,
|
|
numbers, function names. */
|
|
.codehilite :is(.c, .c1, .cm, .ch, .cs) {
|
|
color: var(--code-comment); font-style: italic;
|
|
}
|
|
.codehilite :is(.k, .kc, .kd, .kn, .kr, .kt) { color: var(--code-keyword); }
|
|
.codehilite :is(.s, .s1, .s2, .sa, .sd, .se, .si) {
|
|
color: var(--code-string);
|
|
}
|
|
.codehilite :is(.m, .mi, .mf, .mh, .il) { color: var(--code-number); }
|
|
.codehilite :is(.nf, .nb, .fm) { color: var(--code-function); }
|
|
blockquote { margin: 1.25rem 0; padding-left: 1rem; border-left: 3px solid var(--line); color: var(--muted); }
|
|
table { border-collapse: collapse; width: 100%; display: block; overflow-x: auto; }
|
|
th, td { border: 1px solid var(--line); padding: .4rem .6rem; text-align: left; }
|
|
|
|
footer { margin-top: 2.5rem; font-size: .85rem; color: var(--muted); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header class="site-header">
|
|
<div class="site-title">
|
|
<h1><a href="{{root}}">Pascal Maas</a></h1>
|
|
<p>A blog about bioinformatics in metabolomics</p>
|
|
</div>
|
|
<div class="header-actions">
|
|
<a href="https://git.pascal-maas.nl/pascalmaas" target="_blank"
|
|
rel="me noopener">Git</a>
|
|
<a href="mailto:contact@pascal-maas.nl">Contact</a>
|
|
<button class="theme-toggle" type="button"
|
|
title="Switch to dark mode">Espresso</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="layout">
|
|
<nav aria-label="Posts">
|
|
<h2>Posts</h2>
|
|
<ol>
|
|
{{toc}}
|
|
</ol>
|
|
</nav>
|
|
|
|
<main>
|
|
{{content}}
|
|
</main>
|
|
</div>
|
|
|
|
<footer>© {{year}} Pascal Maas · <a href="{{root}}feed.xml">RSS</a></footer>
|
|
</div>
|
|
<script>
|
|
const root = document.documentElement;
|
|
const button = document.querySelector(".theme-toggle");
|
|
|
|
// The button names the mode it switches to, not the current one.
|
|
function applyTheme(theme) {
|
|
root.dataset.theme = theme;
|
|
if (theme === "dark") {
|
|
button.textContent = "Latte";
|
|
button.title = "Switch to light mode";
|
|
} else {
|
|
button.textContent = "Espresso";
|
|
button.title = "Switch to dark mode";
|
|
}
|
|
}
|
|
|
|
applyTheme(root.dataset.theme || "light");
|
|
button.addEventListener("click", function () {
|
|
const theme = root.dataset.theme === "dark" ? "light" : "dark";
|
|
applyTheme(theme);
|
|
try {
|
|
localStorage.setItem("theme", theme);
|
|
} catch (error) {}
|
|
});
|
|
|
|
const articles = document.querySelectorAll("main article");
|
|
|
|
// First use of each term per post keeps its definition, moved out of
|
|
// "title" so the browser's plain tooltip doesn't show beside the bubble.
|
|
for (const article of articles) {
|
|
const seen = new Set();
|
|
for (const term of article.querySelectorAll("abbr[title]")) {
|
|
if (seen.has(term.textContent)) {
|
|
term.replaceWith(term.textContent);
|
|
continue;
|
|
}
|
|
seen.add(term.textContent);
|
|
term.dataset.definition = term.title;
|
|
term.removeAttribute("title");
|
|
term.tabIndex = 0;
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|