First blog post, added theming
All checks were successful
Build blog / build (push) Successful in 1s
All checks were successful
Build blog / build (push) Successful in 1s
Coffee-themed, font changes and a first post.
This commit is contained in:
242
template.html
242
template.html
@@ -4,48 +4,158 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pascal Maas · Blog</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fbfaf7; --fg: #1d1d1b; --muted: #6b6a66; --line: #e4e1da;
|
||||
--accent: #2f5d8a; --code-bg: #f1eee7;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #161616; --fg: #e8e6e1; --muted: #9a978f; --line: #2c2b29;
|
||||
--accent: #8ab4dc; --code-bg: #222120;
|
||||
<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("static/PoltawskiNowy-Regular.ttf"); font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Poltawski Nowy"; font-weight: 400; font-style: italic;
|
||||
src: url("static/PoltawskiNowy-Italic.ttf"); font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Poltawski Nowy"; font-weight: 700; font-style: normal;
|
||||
src: url("static/PoltawskiNowy-Bold.ttf"); font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Poltawski Nowy"; font-weight: 700; font-style: italic;
|
||||
src: url("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 Georgia, "Iowan Old Style", "Times New Roman", serif;
|
||||
font: 17px/1.65 "Poltawski Nowy", Georgia, "Iowan Old Style",
|
||||
"Times New Roman", serif;
|
||||
}
|
||||
.wrap { max-width: 44rem; margin: 0 auto; padding: 3rem 1rem 4rem; }
|
||||
.wrap { max-width: 56rem; margin: 0 auto; padding: 0 1rem 4rem; }
|
||||
a { color: var(--accent); }
|
||||
h1, h2, h3, .meta, nav, footer { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.site-header h1 { margin: 0; font-size: 1.6rem; letter-spacing: -0.01em; }
|
||||
.site-header p { margin: .25rem 0 0; color: var(--muted); }
|
||||
.site-header h1 {
|
||||
margin: 0; font-size: 1.6rem; line-height: 1.2; letter-spacing: -0.01em;
|
||||
}
|
||||
.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-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||
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; }
|
||||
|
||||
img { max-width: 100%; height: auto; }
|
||||
/* 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-bg); padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
||||
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; }
|
||||
@@ -56,22 +166,102 @@
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site-header">
|
||||
<h1>Pascal Maas</h1>
|
||||
<p>Notes and writing.</p>
|
||||
<div class="site-title">
|
||||
<h1>Pascal Maas</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>
|
||||
|
||||
<nav aria-label="Posts">
|
||||
<h2>Posts</h2>
|
||||
<ol>
|
||||
<div class="layout">
|
||||
<nav aria-label="Posts">
|
||||
<h2>Posts</h2>
|
||||
<ol>
|
||||
{{toc}}
|
||||
</ol>
|
||||
</nav>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<main>
|
||||
{{posts}}
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>© {{year}} Pascal Maas</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 header = document.querySelector(".site-header");
|
||||
const articles = Array.from(document.querySelectorAll("main article"));
|
||||
const links = document.querySelectorAll("nav a");
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// The current post is the last one whose top has passed a line 3rem
|
||||
// (48px) below the header; at the very bottom, it is always the last post.
|
||||
function markCurrentPost() {
|
||||
if (articles.length === 0) {
|
||||
return;
|
||||
}
|
||||
const line = header.getBoundingClientRect().bottom + 48;
|
||||
const viewBottom = window.innerHeight + window.scrollY;
|
||||
const atBottom = viewBottom >= root.scrollHeight - 2;
|
||||
let current = articles[0];
|
||||
for (const article of articles) {
|
||||
if (atBottom || article.getBoundingClientRect().top <= line) {
|
||||
current = article;
|
||||
}
|
||||
}
|
||||
for (const link of links) {
|
||||
link.classList.toggle("current", link.hash === "#" + current.id);
|
||||
}
|
||||
}
|
||||
|
||||
markCurrentPost();
|
||||
window.addEventListener("scroll", markCurrentPost, { passive: true });
|
||||
window.addEventListener("resize", markCurrentPost);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user