docs: a Markdown reader on the site, and the flag stays up

Two additions to docs/Website.

**The reader.** docs.html renders the 29 documents in docs/ in the site's own
theme -- index down the left, table of contents down the right, and links
between documents rewritten so ../history/schfs.md navigates inside the
reader instead of downloading a file.

The parser is ~150 lines in docs.js, not a dependency: it handles exactly
what docs/ uses, and a Markdown library would outweigh every other asset here
combined. Everything is escaped before a tag is emitted, so the only HTML
that reaches the page is HTML docs.js wrote. Verified by rendering all 30
documents (302 KB of Markdown) with zero failures, no unrestored sentinels
and no runaway loops.

One caveat, stated in the page itself rather than hidden: browsers block
fetch() on file:// URLs, so this page needs a server. It says so, with the
command, and offers the raw file as a fallback. The rest of the site is
unaffected.

**The flag.** A Progress Pride flag in the top bar and a line in the footer,
on both pages, permanently. Support with an end date is marketing. It is
deliberately small and deliberately not a page of its own -- this is an
operating system, and the flag is a statement, not a subject.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-12 21:53:06 +02:00
commit ef94fac4ac
6 changed files with 708 additions and 8 deletions

View file

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 18" width="28" height="18" role="img" aria-labelledby="pride-title">
<title id="pride-title">Progress Pride flag</title>
<desc>The six-stripe rainbow with the Progress chevron: white, pink and light
blue for trans people, brown and black for queer people of colour and for
those lost to AIDS. It sits in the top bar every day of the year, which is
the whole point of it.</desc>
<!-- the six -->
<rect width="28" height="3" y="0" fill="#E40303"/>
<rect width="28" height="3" y="3" fill="#FF8C00"/>
<rect width="28" height="3" y="6" fill="#FFED00"/>
<rect width="28" height="3" y="9" fill="#008026"/>
<rect width="28" height="3" y="12" fill="#24408E"/>
<rect width="28" height="3" y="15" fill="#732982"/>
<!-- the chevron: nested, outermost first, so each one leaves a band -->
<polygon points="0,0 0,18 13,9" fill="#FFFFFF"/>
<polygon points="0,0 0,18 11,9" fill="#FFAFC8"/>
<polygon points="0,0 0,18 9,9" fill="#74D7EE"/>
<polygon points="0,0 0,18 7,9" fill="#613915"/>
<polygon points="0,0 0,18 5,9" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -5,12 +5,49 @@ Three files and a folder of marks. No build step, no dependencies, no
framework — open `index.html` and it works, including from a `file://` URL.
```
index.html the page
index.html the front page
docs.html the documentation reader
style.css the palette, transcribed from include/theme.h
app.js two behaviours: the boot log types, sections fade in
Assets/ shift.svg (the logo), uberblock.svg (SchFS), guru.svg (PSOGM)
app.js the front page: the boot log types, sections fade in
docs.js the Markdown renderer and the doc index
Assets/ shift.svg (logo), uberblock.svg (SchFS), guru.svg (PSOGM),
pride.svg
```
## The docs reader
`docs.html` renders the Markdown in `docs/` in the site's own theme, with the
index down the left and a table of contents down the right. Links between
documents are rewritten to stay inside the reader, so `../history/schfs.md`
in an architecture page navigates rather than downloads.
The parser is about 150 lines in `docs.js` and handles what `docs/` actually
uses: headings, fenced code, tables, lists, quotes, rules and inline marks.
A Markdown library would be larger than everything else here put together.
Every document is escaped before a single tag is emitted — the only HTML that
reaches the page is HTML `docs.js` wrote.
**This one page needs a server.** Browsers refuse `fetch()` on `file://` for
the same reason they refuse it cross-origin, so opening `docs.html` from a
file gives a panel explaining that instead of the docs. Everything else works
from a file. To read them locally:
```sh
cd /home/nexoniarz/Dokumenty/Projects/SchOS
python3 -m http.server 8000
# then http://localhost:8000/docs/Website/docs.html
```
Adding a document means adding one line to the `DOCS` array in `docs.js`. The
reader will only load a path that appears there.
## The flag
`Assets/pride.svg` is in the top bar and in the footer, on both pages, and it
is not seasonal. Support with an end date is marketing; this is a project that
belongs to whoever writes it. It is also deliberately small — the site is
about an operating system, and the flag is a statement, not a subject.
## The rules it follows
**The palette is not a design decision.** It is `include/theme.h`, copied. A

63
docs/Website/docs.html Normal file
View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Docs — SchOS</title>
<meta name="description" content="The SchOS documentation: architecture, guides, plans and the history of the bugs worth remembering.">
<link rel="icon" href="Assets/shift.svg" type="image/svg+xml">
<link rel="stylesheet" href="style.css">
</head>
<body class="reading">
<header class="top">
<div class="wrap">
<a class="brand" href="index.html">
<img src="Assets/shift.svg" alt="">
SchOS
</a>
<a class="pride" href="#pride-note" title="SchOS stands with the LGBTQIAP+ community — every month of the year">
<img src="Assets/pride.svg" alt="Progress Pride flag">
</a>
<nav>
<a href="index.html">Home</a>
<a href="docs.html" class="on">Docs</a>
</nav>
</div>
</header>
<div class="docshell">
<aside class="sidebar" aria-label="Documentation index">
<div class="sticky">
<nav id="docnav"></nav>
</div>
</aside>
<main>
<article id="doc" class="prose" aria-live="polite">
<p class="loading">Loading…</p>
</article>
</main>
<aside class="rail" aria-label="On this page">
<div class="sticky">
<nav id="toc" hidden></nav>
</div>
</aside>
</div>
<footer>
<div class="wrap">
<span>SchOS — kernel <b>SchIFT</b>, filesystem <b>SchFS</b>.</span>
<span class="mono">Apache-2.0, with the xv6-derived core under MIT.</span>
<span class="pride-note" id="pride-note">
<img src="Assets/pride.svg" alt="">
Built by, and for, people of every orientation and gender identity.
The flag stays up all year.
</span>
</div>
</footer>
<script src="docs.js"></script>
</body>
</html>

362
docs/Website/docs.js Normal file
View file

@ -0,0 +1,362 @@
/* SPDX-License-Identifier: Apache-2.0
*
* The documentation reader.
*
* Renders the project's Markdown in the site's own theme, so the docs can be
* read without leaving the page and without a build step. No dependency: the
* parser below handles exactly what docs/ actually uses -- headings, fenced
* code, tables, lists, quotes, and inline marks. Pulling in a 40 KB Markdown
* library to render 29 files would outweigh every other asset here combined.
*
* Everything is escaped before any tag is emitted. The only HTML that reaches
* the page is HTML this file wrote.
*/
(function () {
"use strict";
/* ---- the index ---------------------------------------------------- */
var DOCS = [
{ group: "Start here", items: [
["README.md", "Overview"],
["plan/1.0.md", "The 1.0 definition"],
["licensing.md", "Licensing"]
]},
{ group: "Architecture", items: [
["architecture/README.md", "The system, layer by layer"],
["architecture/boot.md", "Boot"],
["architecture/kernel.md", "Kernel"],
["architecture/syscalls.md", "Syscalls"],
["architecture/filesystem.md", "The VFS"],
["architecture/schfs.md", "SchFS"],
["architecture/devices.md", "Device model"],
["architecture/usb.md", "USB"],
["architecture/graphics.md", "Graphics"],
["architecture/networking.md", "Networking"],
["architecture/init.md", "init"]
]},
{ group: "Guides", items: [
["guides/building.md", "Building"],
["guides/testing.md", "Testing"],
["guides/layout.md", "Tree layout"],
["guides/adding-a-driver.md", "Adding a driver"]
]},
{ group: "Plan", items: [
["plan/README.md", "What plan/ is for"],
["plan/roadmap.md", "Roadmap"],
["plan/foundations.md", "Foundations"],
["plan/init.md", "schinit"],
["plan/wayland.md", "Wayland"],
["plan/hybrid.md", "Hybrid kernel"],
["plan/schapi.md", "SchAPI"]
]},
{ group: "History", items: [
["history/README.md", "Why history/ exists"],
["history/kernel.md", "Kernel"],
["history/schfs.md", "SchFS"],
["history/moving-to-linux.md", "Moving to Linux"]
]},
{ group: "Branding", items: [
["branding/README.md", "The three marks"]
]}
];
/* ---- helpers ------------------------------------------------------- */
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
/* Resolve a doc-relative link against the doc it appeared in, so
"../history/schfs.md" inside architecture/ lands on history/schfs.md. */
function resolve(from, href) {
var base = from.split("/").slice(0, -1);
var parts = href.split("/");
for (var i = 0; i < parts.length; i++) {
if (parts[i] === "." || parts[i] === "") continue;
if (parts[i] === "..") base.pop();
else base.push(parts[i]);
}
return base.join("/");
}
/* encodeURIComponent turns every "/" into %2F, which works but makes a
shared link unreadable. Slashes are legal in a query value, so keep them. */
function docHref(p) {
return "?doc=" + encodeURIComponent(p).replace(/%2F/g, "/");
}
function slug(s) {
return s.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
}
/* ---- inline marks --------------------------------------------------- */
/* U+0001 as the placeholder sentinel: it cannot occur in a Markdown source
file, so a code span lifted out here can never be confused with document
text when it is put back. */
var SENTINEL = "\u0001";
function inline(s, from) {
/* Code spans come out first and go back last, so nothing inside a backtick
pair is mistaken for emphasis or for a link. */
var spans = [];
s = s.replace(/`([^`]+)`/g, function (_, code) {
spans.push("<code>" + esc(code) + "</code>");
return SENTINEL + (spans.length - 1) + SENTINEL;
});
s = esc(s);
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/g, function (_, alt, src) {
return '<img src="' + encodeURI(src) + '" alt="' + alt + '">';
});
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, function (_, text, href) {
if (/^https?:/.test(href))
return '<a href="' + encodeURI(href) + '" target="_blank" rel="noopener">' + text + "</a>";
if (href.charAt(0) === "#")
return '<a href="' + encodeURI(href) + '">' + text + "</a>";
if (/\.md($|#)/.test(href)) {
var cut = href.indexOf("#");
var hash = cut >= 0 ? href.slice(cut) : "";
var path = cut >= 0 ? href.slice(0, cut) : href;
return '<a href="' + docHref(resolve(from, path)) + hash + '">' + text + "</a>";
}
/* Anything else is a file in the repo rather than a document. Link at it
relative to docs/, which is where this page's ../ points. */
return '<a href="../' + encodeURI(resolve(from, href)) + '">' + text + "</a>";
});
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
var restore = new RegExp(SENTINEL + "(\\d+)" + SENTINEL, "g");
return s.replace(restore, function (_, n) { return spans[+n]; });
}
/* ---- blocks --------------------------------------------------------- */
function render(md, from, headings) {
md = md.replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->/g, "");
var L = md.split("\n"), out = [], i = 0, m;
function listBlock(ordered) {
var tag = ordered ? "ol" : "ul", items = [];
while (i < L.length) {
var li = L[i].match(ordered ? /^\s*\d+\.\s+(.*)$/ : /^\s*[-*+]\s+(.*)$/);
if (!li) {
/* an indented continuation line belongs to the item above it */
if (items.length && /^\s+\S/.test(L[i])) {
items[items.length - 1] += " " + L[i].trim();
i++;
continue;
}
break;
}
items.push(li[1]);
i++;
}
out.push("<" + tag + ">");
items.forEach(function (t) { out.push("<li>" + inline(t, from) + "</li>"); });
out.push("</" + tag + ">");
}
function cells(row) {
return row.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|")
.map(function (c) { return c.trim(); });
}
while (i < L.length) {
var line = L[i];
if (!line.trim()) { i++; continue; }
/* fenced code */
if (/^\s*```/.test(line)) {
var lang = line.replace(/^\s*```/, "").trim(), buf = [];
i++;
while (i < L.length && !/^\s*```/.test(L[i])) buf.push(L[i++]);
i++;
out.push('<pre class="code"' + (lang ? ' data-lang="' + esc(lang) + '"' : "") +
"><code>" + esc(buf.join("\n")) + "</code></pre>");
continue;
}
/* table: a pipe row followed by a separator row */
if (/^\s*\|/.test(line) && /^\s*\|?[\s:|-]+$/.test(L[i + 1] || "") && /-/.test(L[i + 1] || "")) {
var head = cells(line);
i += 2;
out.push('<div class="tablewrap"><table><thead><tr>');
head.forEach(function (c) { out.push("<th>" + inline(c, from) + "</th>"); });
out.push("</tr></thead><tbody>");
while (i < L.length && /^\s*\|/.test(L[i])) {
out.push("<tr>");
cells(L[i++]).forEach(function (c) { out.push("<td>" + inline(c, from) + "</td>"); });
out.push("</tr>");
}
out.push("</tbody></table></div>");
continue;
}
/* heading */
if ((m = line.match(/^(#{1,6})\s+(.*?)\s*#*$/))) {
var lvl = m[1].length, text = m[2], id = slug(text);
if (lvl <= 3) headings.push({ level: lvl, text: text, id: id });
out.push("<h" + lvl + ' id="' + id + '">' + inline(text, from) + "</h" + lvl + ">");
i++;
continue;
}
/* rule */
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { out.push("<hr>"); i++; continue; }
/* blockquote */
if (/^\s*>/.test(line)) {
var q = [];
while (i < L.length && /^\s*>/.test(L[i])) q.push(L[i++].replace(/^\s*>\s?/, ""));
out.push("<blockquote>" + render(q.join("\n"), from, []) + "</blockquote>");
continue;
}
if (/^\s*[-*+]\s+/.test(line)) { listBlock(false); continue; }
if (/^\s*\d+\.\s+/.test(line)) { listBlock(true); continue; }
/* paragraph: runs to the next blank line or block opener */
var para = [];
while (i < L.length && L[i].trim() &&
!/^\s*(```|>|#{1,6}\s|[-*+]\s|\d+\.\s|\|)/.test(L[i]) &&
!/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(L[i])) {
para.push(L[i++]);
}
if (para.length) out.push("<p>" + inline(para.join(" "), from) + "</p>");
else i++; /* a block opener we do not handle: never spin on it */
}
return out.join("\n");
}
/* ---- the page ------------------------------------------------------- */
var nav = document.getElementById("docnav");
var article = document.getElementById("doc");
var tocEl = document.getElementById("toc");
function buildNav() {
DOCS.forEach(function (g) {
var h = document.createElement("p");
h.className = "navgroup";
h.textContent = g.group;
nav.appendChild(h);
var ul = document.createElement("ul");
g.items.forEach(function (it) {
var li = document.createElement("li");
var a = document.createElement("a");
a.href = docHref(it[0]);
a.textContent = it[1];
a.dataset.path = it[0];
li.appendChild(a);
ul.appendChild(li);
});
nav.appendChild(ul);
});
}
function markCurrent(path) {
Array.prototype.forEach.call(nav.querySelectorAll("a"), function (a) {
a.classList.toggle("on", a.dataset.path === path);
});
}
function buildToc(headings) {
tocEl.innerHTML = "";
var real = headings.filter(function (h) { return h.level >= 2; });
if (real.length < 2) { tocEl.hidden = true; return; }
tocEl.hidden = false;
var t = document.createElement("p");
t.className = "navgroup";
t.textContent = "On this page";
tocEl.appendChild(t);
var ul = document.createElement("ul");
real.forEach(function (h) {
var li = document.createElement("li");
li.className = "lvl" + h.level;
var a = document.createElement("a");
a.href = "#" + h.id;
a.textContent = h.text;
li.appendChild(a);
ul.appendChild(li);
});
tocEl.appendChild(ul);
}
function fail(title, body) {
article.innerHTML = "<h1>" + title + "</h1>" + body;
tocEl.hidden = true;
}
function load(path) {
markCurrent(path);
document.title = "Docs — SchOS";
article.setAttribute("aria-busy", "true");
fetch("../" + path).then(function (r) {
if (!r.ok) throw new Error(r.status + " " + r.statusText);
return r.text();
}).then(function (text) {
var headings = [];
article.innerHTML = render(text, path, headings);
buildToc(headings);
article.removeAttribute("aria-busy");
if (headings.length) document.title = headings[0].text + " — SchOS docs";
var hash = location.hash.slice(1);
var target = hash && document.getElementById(hash);
if (target) target.scrollIntoView();
else window.scrollTo(0, 0);
}).catch(function (e) {
article.removeAttribute("aria-busy");
/* The common case, and worth naming precisely: browsers refuse fetch()
on file:// for the same reason they refuse it cross-origin, so this
one page needs a server even though nothing here is dynamic. */
if (location.protocol === "file:") {
fail("This page needs a local server",
"<p>Browsers block <code>fetch()</code> on <code>file://</code> URLs, so the " +
"reader cannot load the Markdown. The rest of the site works from a file; " +
"this page does not. Serve the repository root:</p>" +
'<pre class="code"><code>cd /home/nexoniarz/Dokumenty/Projects/SchOS\n' +
"python3 -m http.server 8000</code></pre>" +
"<p>Then open <code>http://localhost:8000/docs/Website/docs.html</code>.</p>" +
'<p>Or read this one directly: <a href="../' + esc(path) + '">' + esc(path) + "</a></p>");
} else {
fail("Could not load that document",
"<p><code>" + esc(path) + "</code> — " + esc(e.message) + "</p>" +
'<p><a href="?doc=README.md">Back to the overview</a></p>');
}
});
}
function pathFromUrl() {
var m = /[?&]doc=([^&#]+)/.exec(location.search);
var p = m ? decodeURIComponent(m[1]) : "README.md";
/* Only ever load something that is in the index above. */
var known = DOCS.some(function (g) {
return g.items.some(function (it) { return it[0] === p; });
});
return known ? p : "README.md";
}
buildNav();
load(pathFromUrl());
/* Intercept in-page navigation so the reader does not reload its own shell. */
document.addEventListener("click", function (ev) {
var a = ev.target.closest ? ev.target.closest("a") : null;
if (!a || ev.metaKey || ev.ctrlKey || ev.shiftKey || a.target) return;
var href = a.getAttribute("href") || "";
if (href.indexOf("?doc=") !== 0) return;
ev.preventDefault();
history.pushState(null, "", href);
load(pathFromUrl());
});
window.addEventListener("popstate", function () { load(pathFromUrl()); });
})();

View file

@ -16,12 +16,15 @@
<img src="Assets/shift.svg" alt="">
SchOS
</a>
<a class="pride" href="#pride-note" title="SchOS stands with the LGBTQIAP+ community — every month of the year">
<img src="Assets/pride.svg" alt="Progress Pride flag">
</a>
<nav>
<a href="#origin">Origin</a>
<a href="#stack">The stack</a>
<a href="#what">What it has</a>
<a href="#numbers">Numbers</a>
<a href="#docs">Docs</a>
<a href="docs.html">Docs</a>
</nav>
</div>
</header>
@ -263,17 +266,18 @@
<code>plan/</code>; and the bugs worth remembering are kept in
<code>history/</code>, in full, including the wrong turns.
</p>
<p class="readerlink"><a class="btn primary" href="docs.html">Open the docs reader</a></p>
<div class="links">
<a class="link-card" href="../architecture/README.md">
<a class="link-card" href="docs.html?doc=architecture/README.md">
<b>Architecture</b><span>What the system is, layer by layer</span>
</a>
<a class="link-card" href="../plan/1.0.md">
<a class="link-card" href="docs.html?doc=plan/1.0.md">
<b>The 1.0 definition</b><span>What is in, what is out, and why</span>
</a>
<a class="link-card" href="../history/kernel.md">
<a class="link-card" href="docs.html?doc=history/kernel.md">
<b>Kernel history</b><span>The bugs, and what each one taught</span>
</a>
<a class="link-card" href="../guides/building.md">
<a class="link-card" href="docs.html?doc=guides/building.md">
<b>Building it</b><span>Toolchain, ports, and running under QEMU</span>
</a>
</div>
@ -286,6 +290,11 @@
<div class="wrap">
<span>SchOS — kernel <b>SchIFT</b>, filesystem <b>SchFS</b>.</span>
<span class="mono">Apache-2.0, with the xv6-derived core under MIT.</span>
<span class="pride-note" id="pride-note">
<img src="Assets/pride.svg" alt="">
Built by, and for, people of every orientation and gender identity.
The flag stays up all year.
</span>
</div>
</footer>

View file

@ -369,3 +369,210 @@ footer .mono { font-size: 0.78rem; }
vertical-align: -0.22em;
margin-right: 0.4em;
}
/* =====================================================================
The pride mark
---------------------------------------------------------------------
Small, and permanent. It is in the top bar in November the same way it
is in June, which is the only version of this that means anything --
support with an end date is marketing.
Deliberately not bigger than this. The site is about an operating
system; the flag is a statement, not a subject.
===================================================================== */
.pride {
display: inline-flex;
align-items: center;
line-height: 0;
border-radius: 2px;
text-decoration: none;
}
.pride img {
width: 26px;
height: auto;
display: block;
border-radius: 2px;
/* Sits down slightly against the dark ground so it reads as part of the
bar rather than as a sticker on top of it; full colour on hover. */
opacity: 0.85;
transition: opacity .18s ease, transform .18s ease;
}
.pride:hover img { opacity: 1; transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) {
.pride img { transition: none; }
.pride:hover img { transform: none; }
}
.pride-note {
display: flex;
align-items: center;
gap: 0.6rem;
flex-basis: 100%;
color: var(--dim);
font-size: 0.82rem;
}
.pride-note img { width: 20px; height: auto; border-radius: 2px; flex: none; }
/* =====================================================================
The documentation reader
===================================================================== */
body.reading { display: flex; flex-direction: column; min-height: 100vh; }
body.reading footer { margin-top: auto; }
.docshell {
width: min(100% - 2.5rem, 84rem);
margin-inline: auto;
display: grid;
grid-template-columns: 15rem minmax(0, 1fr) 13rem;
gap: clamp(1.5rem, 3vw, 3rem);
align-items: start;
padding-block: 2.5rem 4rem;
}
@media (max-width: 72rem) { .docshell { grid-template-columns: 14rem minmax(0, 1fr); } .rail { display: none; } }
@media (max-width: 54rem) { .docshell { grid-template-columns: 1fr; } .sidebar .sticky { position: static; max-height: none; } }
.sticky {
position: sticky;
top: 4.5rem;
max-height: calc(100vh - 6rem);
overflow-y: auto;
scrollbar-width: thin;
}
.navgroup {
font-family: var(--mono);
font-size: 0.68rem;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--dim);
margin: 1.6rem 0 0.5rem;
}
.navgroup:first-child { margin-top: 0; }
#docnav ul, #toc ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
#docnav a, #toc a {
display: block;
padding: 0.28rem 0.6rem;
margin-left: -0.6rem;
border-radius: 3px;
color: var(--dim);
text-decoration: none;
font-size: 0.875rem;
line-height: 1.45;
}
#docnav a:hover, #toc a:hover { color: var(--fg); background: var(--surface); }
#docnav a.on {
color: var(--fg);
background: var(--accent-dim);
box-shadow: inset 2px 0 0 var(--accent);
}
#toc .lvl3 a { padding-left: 1.5rem; font-size: 0.8rem; }
/* ---- prose ---------------------------------------------------------- */
.prose { max-width: 46rem; }
.prose .loading { color: var(--dim); font-family: var(--mono); font-size: 0.85rem; }
.prose h1 {
font-size: clamp(1.9rem, 1.4rem + 2.2vw, 2.6rem);
font-weight: 700;
letter-spacing: -0.025em;
margin: 0 0 1.25rem;
}
.prose h2 {
font-size: clamp(1.3rem, 1.1rem + 1vw, 1.65rem);
font-weight: 700;
letter-spacing: -0.015em;
margin: 2.75rem 0 0.9rem;
padding-top: 1.6rem;
border-top: 1px solid var(--line);
}
.prose h3 { font-size: 1.1rem; font-weight: 700; margin: 2rem 0 0.6rem; }
.prose h4, .prose h5, .prose h6 { font-size: 0.95rem; font-weight: 700; margin: 1.5rem 0 0.5rem; color: var(--dim); }
.prose p, .prose li { color: #C6CAD3; }
.prose p { margin: 0 0 1.1rem; max-width: none; }
.prose strong { color: var(--fg); }
.prose a { color: var(--accent); }
.prose ul, .prose ol { margin: 0 0 1.2rem; padding-left: 1.35rem; display: flex; flex-direction: column; gap: 0.4rem; }
.prose li { line-height: 1.6; }
.prose li::marker { color: var(--accent); }
.prose hr { border: 0; border-top: 1px solid var(--line); margin: 2.25rem 0; }
.prose blockquote {
margin: 1.4rem 0;
padding: 0.2rem 0 0.2rem 1.1rem;
border-left: 2px solid var(--accent);
color: var(--dim);
}
.prose blockquote p:last-child { margin-bottom: 0; }
.prose pre.code {
background: #090B0F;
border: 1px solid var(--line);
border-radius: 5px;
padding: 0.95rem 1.1rem;
overflow-x: auto;
margin: 0 0 1.3rem;
position: relative;
}
.prose pre.code code {
font-family: var(--mono);
font-size: 0.815rem;
line-height: 1.7;
color: #CBD1DC;
background: none;
padding: 0;
white-space: pre;
}
.prose pre.code[data-lang]::after {
content: attr(data-lang);
position: absolute;
top: 0.5rem;
right: 0.7rem;
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--dim);
}
.prose :not(pre) > code {
font-family: var(--mono);
font-size: 0.85em;
background: var(--surface-2);
border: 1px solid var(--line);
color: #D8DCE4;
padding: 0.08em 0.36em;
border-radius: 3px;
}
.tablewrap { overflow-x: auto; margin: 0 0 1.4rem; }
.prose table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
.prose th, .prose td {
text-align: left;
padding: 0.55rem 0.85rem;
border-bottom: 1px solid var(--line);
vertical-align: top;
}
.prose th {
font-family: var(--mono);
font-size: 0.72rem;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--accent);
border-bottom-color: var(--accent-dim);
white-space: nowrap;
}
.prose td { color: #C6CAD3; }
.prose tbody tr:hover td { background: var(--surface); }
.prose img { max-width: 100%; height: auto; }
.top nav a.on { color: var(--fg); }
.readerlink { margin-top: 2rem; }