From ef94fac4acc0cf139a7abdcf31914ce95769eb11 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 12 Sep 2026 21:53:06 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- docs/Website/Assets/pride.svg | 22 +++ docs/Website/README.md | 43 +++- docs/Website/docs.html | 63 ++++++ docs/Website/docs.js | 362 ++++++++++++++++++++++++++++++++++ docs/Website/index.html | 19 +- docs/Website/style.css | 207 +++++++++++++++++++ 6 files changed, 708 insertions(+), 8 deletions(-) create mode 100644 docs/Website/Assets/pride.svg create mode 100644 docs/Website/docs.html create mode 100644 docs/Website/docs.js diff --git a/docs/Website/Assets/pride.svg b/docs/Website/Assets/pride.svg new file mode 100644 index 0000000..be1b140 --- /dev/null +++ b/docs/Website/Assets/pride.svg @@ -0,0 +1,22 @@ + + Progress Pride flag + 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. + + + + + + + + + + + + + + + + diff --git a/docs/Website/README.md b/docs/Website/README.md index 4007a92..c67ad36 100644 --- a/docs/Website/README.md +++ b/docs/Website/README.md @@ -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 diff --git a/docs/Website/docs.html b/docs/Website/docs.html new file mode 100644 index 0000000..77a0481 --- /dev/null +++ b/docs/Website/docs.html @@ -0,0 +1,63 @@ + + + + + +Docs — SchOS + + + + + + +
+ +
+ +
+ + +
+
+

Loading…

+
+
+ + +
+ +
+
+ SchOS — kernel SchIFT, filesystem SchFS. + Apache-2.0, with the xv6-derived core under MIT. + + + Built by, and for, people of every orientation and gender identity. + The flag stays up all year. + +
+
+ + + + diff --git a/docs/Website/docs.js b/docs/Website/docs.js new file mode 100644 index 0000000..10061d8 --- /dev/null +++ b/docs/Website/docs.js @@ -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, "&").replace(//g, ">"); + } + + /* 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("" + esc(code) + ""); + return SENTINEL + (spans.length - 1) + SENTINEL; + }); + + s = esc(s); + + s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/g, function (_, alt, src) { + return '' + alt + ''; + }); + + s = s.replace(/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, function (_, text, href) { + if (/^https?:/.test(href)) + return '' + text + ""; + if (href.charAt(0) === "#") + return '' + text + ""; + 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 '' + text + ""; + } + /* 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 '' + text + ""; + }); + + s = s.replace(/\*\*([^*]+)\*\*/g, "$1"); + s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1$2"); + + 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(//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("
  • " + inline(t, from) + "
  • "); }); + out.push(""); + } + + 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('
    " + esc(buf.join("\n")) + "
    "); + 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('
    '); + head.forEach(function (c) { out.push(""); }); + out.push(""); + while (i < L.length && /^\s*\|/.test(L[i])) { + out.push(""); + cells(L[i++]).forEach(function (c) { out.push(""); }); + out.push(""); + } + out.push("
    " + inline(c, from) + "
    " + inline(c, from) + "
    "); + 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("' + inline(text, from) + ""); + i++; + continue; + } + + /* rule */ + if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { out.push("
    "); 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("
    " + render(q.join("\n"), from, []) + "
    "); + 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("

    " + inline(para.join(" "), from) + "

    "); + 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 = "

    " + title + "

    " + 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", + "

    Browsers block fetch() on file:// 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:

    " + + '
    cd /home/nexoniarz/Dokumenty/Projects/SchOS\n' +
    +             "python3 -m http.server 8000
    " + + "

    Then open http://localhost:8000/docs/Website/docs.html.

    " + + '

    Or read this one directly: ' + esc(path) + "

    "); + } else { + fail("Could not load that document", + "

    " + esc(path) + " — " + esc(e.message) + "

    " + + '

    Back to the overview

    '); + } + }); + } + + 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()); }); +})(); diff --git a/docs/Website/index.html b/docs/Website/index.html index 619b2a6..0239fe0 100644 --- a/docs/Website/index.html +++ b/docs/Website/index.html @@ -16,12 +16,15 @@ SchOS + + Progress Pride flag + @@ -263,17 +266,18 @@ plan/; and the bugs worth remembering are kept in history/, in full, including the wrong turns.

    + @@ -286,6 +290,11 @@
    SchOS — kernel SchIFT, filesystem SchFS. Apache-2.0, with the xv6-derived core under MIT. + + + Built by, and for, people of every orientation and gender identity. + The flag stays up all year. +
    diff --git a/docs/Website/style.css b/docs/Website/style.css index a795708..26f96f4 100644 --- a/docs/Website/style.css +++ b/docs/Website/style.css @@ -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; }