feat: build the site with Astro Starlight

generate_site.py writes the Starlight project in website/ and nothing
else: mkdocs.yml, the MkDocs hooks, theme overrides and stylesheet go,
and the wiki is written in Starlight's syntax. Both workflows set up
Node with the official actions/setup-node pinned by commit, run npm ci
from the committed lockfile, build with the strict link validator and
check the result with validate_site.py.

The search weighs a page by what it answers: Start here pages first,
a system page's cards and file names before a guide citing them, deep
guides, launchers, aliases and frozen copies after. Status badges,
hash blocks and the export change lists leave the index. The ranking
sits at the centre of a range where the 13 queries of search_gate.py
all answer, in the search box itself.

839 pages, every URL and 45,611 ids match the MkDocs build. The tests
read the Starlight content and syntax.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 18:47:22 +02:00
1 parent abe240073c
commit 123d698e43
64 files changed
+742 -3118

No files matched your search

File renamed without changes.
File renamed without changes.
-977
View File
@@ -1,977 +0,0 @@
/* RetroBIOS theme on top of mkdocs-material.
Classes the generator and the wiki pages can use:
.rb-lead first paragraph under a page title
.rb-level-* level label (start, setup, explore, reference, contribute)
.rb-page-header logo or image beside the title block of an entity page
.rb-cards, .rb-card card grid without Material's list markup
.grid.cards Material card grid, restyled here
.rb-card-logo 40 px logo tile inside a card
.rb-steps ordered list drawn as numbered steps
.rb-terminal wraps a code block that shows terminal output
.rb-cmd wraps a command that must wrap instead of scrolling
code.rb-path a folder path shown as a chip
.rb-figure a generated diagram (scripts/sitediagrams.py)
.rb-bar inline coverage bar, width from --v
*/
:root {
--rb-primary: #3b3f7a;
--rb-primary-dark: #2b2e5c;
--rb-accent: #b42318;
--rb-link: #273c8f;
--rb-fg: rgba(0, 0, 0, 0.87);
--rb-surface: #f5f6fa;
--rb-border: #e0e3eb;
--rb-border-strong: #b8bdcc;
--rb-text-secondary: #546e7a;
--rb-ok-fg: #1b5e20;
--rb-ok-bg: #e8f5e9;
--rb-warn-fg: #a33f00;
--rb-warn-bg: #fff3e0;
--rb-err-fg: #b71c1c;
--rb-err-bg: #ffebee;
--rb-info-fg: #0d47a1;
--rb-info-bg: #e3f2fd;
--rb-muted-fg: #455a64;
--rb-muted-bg: #eceff1;
--rb-level-start: #1b5e20;
--rb-level-setup: #0d47a1;
--rb-level-explore: #6a1b9a;
--rb-level-reference: #455a64;
--rb-level-contribute: #a33f00;
--rb-terminal-bg: #1b1d2e;
--rb-terminal-fg: #e6e6f0;
--rb-terminal-label: #9aa0c3;
--rb-diagram-fill: #f5f6fa;
--rb-diagram-stroke: #8c92ac;
--rb-diagram-accent: #3b3f7a;
--md-code-font: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
[data-md-color-scheme="slate"] {
--rb-accent: #ff9a8f;
--rb-link: #aebcff;
--rb-fg: #e3e4ee;
--rb-surface: #252838;
--rb-border: #3a3d52;
--rb-border-strong: #5a5e7a;
--rb-text-secondary: #a6adc8;
--rb-ok-fg: #a5d6a7;
--rb-ok-bg: #1b3a1f;
--rb-warn-fg: #ffcc80;
--rb-warn-bg: #7a2e00;
--rb-err-fg: #ef9a9a;
--rb-err-bg: #7f1414;
--rb-info-fg: #90caf9;
--rb-info-bg: #0d2a55;
--rb-muted-fg: #cfd8dc;
--rb-muted-bg: #37474f;
--rb-level-start: #a5d6a7;
--rb-level-setup: #90caf9;
--rb-level-explore: #ce93d8;
--rb-level-reference: #cfd8dc;
--rb-level-contribute: #ffcc80;
--rb-terminal-bg: #12131c;
--rb-diagram-fill: #252838;
--rb-diagram-stroke: #7c82a3;
--rb-diagram-accent: #aebcff;
}
/* Colour roles. The header keeps the brand indigo in both schemes: the
lighter indigo it used in dark mode left the tab labels at 3.5:1. */
[data-md-color-scheme="default"] {
--md-primary-fg-color: var(--rb-primary);
--md-primary-fg-color--light: #4a4e8a;
--md-primary-fg-color--dark: var(--rb-primary-dark);
--md-accent-fg-color: var(--rb-accent);
--md-typeset-a-color: var(--rb-link);
/* Comments and punctuation sat at 4.47:1 on the code background. */
--md-code-hl-comment-color: #5c5c5c;
--md-code-hl-generic-color: #5c5c5c;
--md-code-hl-variable-color: #5c5c5c;
--md-code-hl-operator-color: #5c5c5c;
--md-code-hl-punctuation-color: #5c5c5c;
}
[data-md-color-scheme="slate"] {
--md-primary-fg-color: var(--rb-primary-dark);
--md-primary-fg-color--light: var(--rb-primary);
--md-primary-fg-color--dark: #1f2147;
--md-accent-fg-color: var(--rb-accent);
--md-accent-bg-color: #1e2129;
--md-typeset-a-color: var(--rb-link);
--md-code-hl-constant-color: #b39ddb;
}
/* Type scale: the page title is the strongest heading on the page. */
.md-typeset h1 {
color: var(--md-default-fg-color);
font-size: 2em;
font-weight: 700;
letter-spacing: -0.01em;
line-height: 1.25;
margin: 0 0 0.9em;
}
.md-typeset h2 {
font-size: 1.45em;
font-weight: 600;
letter-spacing: -0.005em;
margin-top: 2em;
}
.md-typeset h3 {
font-size: 1.15em;
font-weight: 600;
}
.md-typeset h4 {
font-weight: 600;
}
.md-typeset .rb-lead {
font-size: 1.05em;
line-height: 1.55;
max-width: 44em;
}
/* Level label under a page title */
.rb-level {
display: inline-block;
margin: 0 0 0.8rem;
padding: 0.05rem 0.45rem;
border: 1px solid currentColor;
border-radius: 2px;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.01em;
line-height: 1.6;
color: var(--rb-level-reference);
}
.rb-level-start { color: var(--rb-level-start); }
.rb-level-setup { color: var(--rb-level-setup); }
.rb-level-explore { color: var(--rb-level-explore); }
.rb-level-reference { color: var(--rb-level-reference); }
.rb-level-contribute { color: var(--rb-level-contribute); }
/* Header block of platform, system and emulator pages */
.rb-page-header {
display: grid;
grid-template-columns: 56px 1fr;
gap: 0.4rem 1rem;
align-items: center;
margin: 0 0 1.2rem;
}
.rb-page-header > :first-child img,
.rb-page-header > img {
width: 56px;
height: 56px;
object-fit: contain;
}
.md-typeset .rb-page-header h1 {
margin: 0;
}
.rb-page-header p {
margin: 0.2rem 0 0;
}
/* A page that names its logo next to the title floats it right; the logo
keeps its own box instead of shifting the counters below. */
.md-typeset img[align="right"] {
width: 56px;
height: 56px;
object-fit: contain;
margin: 0 0 0.6rem 1rem;
padding: 4px;
border: 1px solid var(--rb-border);
border-radius: 4px;
background: #fff;
}
/* Logos are drawn for a light background: they keep one in dark mode. */
.md-typeset img[src*="assets/logos/"] {
padding: 2px;
border-radius: 2px;
background: #fff;
}
/* Headline figures, read as one line of numbers: four fit on one row of
the text column. */
.rb-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
gap: 0.6rem 1rem;
margin: 1rem 0 1.5rem;
padding: 0.8rem 1rem;
border: 1px solid var(--rb-border);
border-radius: 4px;
background: var(--rb-surface);
}
.md-typeset .rb-stat p {
margin: 0;
}
.rb-stat .rb-stat-value {
font-size: 1.25rem;
font-weight: 700;
display: block;
font-variant-numeric: tabular-nums;
}
.rb-stat .rb-stat-label {
font-size: 0.75rem;
color: var(--rb-text-secondary);
display: block;
}
.rb-badge {
display: inline-block;
padding: 0 0.4em;
border-radius: 2px;
font-size: 0.75rem;
font-weight: 600;
line-height: 1.6;
white-space: nowrap;
}
.rb-badge-success { background: var(--rb-ok-bg); color: var(--rb-ok-fg); }
.rb-badge-warning { background: var(--rb-warn-bg); color: var(--rb-warn-fg); }
.rb-badge-danger { background: var(--rb-err-bg); color: var(--rb-err-fg); }
.rb-badge-info { background: var(--rb-info-bg); color: var(--rb-info-fg); }
.rb-badge-muted { background: var(--rb-muted-bg); color: var(--rb-muted-fg); }
/* A link inside a badge keeps the badge's tested colour pair. */
.md-typeset .rb-badge a:not(.md-button),
.md-typeset a.rb-badge {
color: inherit;
}
/* Inline code inside a link reads as a link in both schemes. */
.md-typeset a code {
color: var(--rb-link);
}
/* Tables */
.md-typeset table:not([class]) {
font-variant-numeric: tabular-nums;
}
.md-typeset table:not([class]) th {
position: sticky;
top: 0;
z-index: 1;
font-weight: 600;
}
.md-typeset table:not([class]) th,
.md-typeset table:not([class]) td {
padding: 0.55em 0.9em;
}
.md-typeset__scrollwrap {
margin: 1rem 0;
}
/* A table lines up with the text around it. Material indents the table
by 0.8rem and offsets that with a negative margin on the wrapper, which
the margin above removes, and the table's own margin doubled the space
above it. */
.md-typeset .md-typeset__table {
padding: 0;
}
.md-typeset .md-typeset__table > table {
margin: 0;
}
.rb-timestamp {
font-size: 0.8rem;
color: var(--rb-text-secondary);
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--rb-border);
}
/* One block per file on emulator and system pages, separated by a rule */
.rb-file-entry,
.rb-sys-file {
border-top: 1px solid var(--rb-border);
padding: 0.75rem 0 0.25rem;
margin: 0;
}
.rb-file-entry > p:first-child,
.rb-sys-file > p:first-child {
margin-top: 0;
}
.rb-file-header code {
font-size: 0.95rem;
font-weight: 700;
}
/* A card's title is a heading so the search lands on it, set as body text */
.md-typeset .rb-file-title {
margin: 0 0 0.3rem;
font-size: inherit;
font-weight: 400;
letter-spacing: normal;
scroll-margin-top: 4rem;
}
.md-typeset .rb-file-title code {
font-weight: 700;
}
/* Lines the search skips continue the card's list */
.rb-search-skip > ul {
margin-top: 0.1rem;
}
.rb-file-entry ul,
.rb-sys-file ul {
margin: 0.3rem 0 0;
padding-left: 1.2rem;
font-size: 0.85rem;
}
.rb-file-entry li,
.rb-sys-file li {
margin: 0.1rem 0;
}
/* Hashes are printed whole so the search finds them, and wrap on phones */
.rb-hash {
font-family: var(--md-code-font);
font-size: 0.78rem;
color: var(--rb-text-secondary);
overflow-wrap: anywhere;
}
/* Key and value cards: the header row is empty, so it is not drawn and
not announced. */
.rb-meta-card thead,
.md-typeset table thead:not(:has(th:not(:empty))) {
display: none;
}
.rb-meta-card td {
overflow-wrap: anywhere;
}
.rb-meta-card td:first-child {
font-weight: 600;
color: var(--rb-text-secondary);
white-space: nowrap;
width: 140px;
}
.md-typeset table .md-button {
font-size: 0.75rem;
min-height: 44px;
padding: 0.55em 0.8em;
}
/* Buttons. The dark scheme draws them in the link colour: the header indigo
would sit at 3:1 on the page. */
[data-md-color-scheme="slate"] .md-typeset .md-button {
color: var(--rb-link);
border-color: var(--rb-link);
}
[data-md-color-scheme="slate"] .md-typeset .md-button--primary {
background-color: var(--rb-link);
color: #1e2129;
}
[data-md-color-scheme="slate"] .md-typeset .md-button:is(:hover, :focus) {
background-color: var(--rb-accent);
border-color: var(--rb-accent);
color: #1e2129;
}
.md-typeset a:not(.md-button):not(.headerlink):not(.rb-badge) {
color: var(--rb-link);
text-decoration: underline;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.14em;
}
.md-typeset a:not(.md-button):not(.headerlink):hover {
text-decoration-thickness: 0.14em;
}
:where(a, button, input, select, textarea, label, [tabindex]):focus-visible {
outline: 3px solid var(--rb-accent);
outline-offset: 3px;
}
.md-typeset .md-button,
.md-typeset button,
.md-typeset input,
.md-typeset select {
min-height: 44px;
}
/* Buttons that wrap onto a second line keep a gap between the rows. */
.md-typeset .md-button {
margin-block: 0.25rem;
}
/* The copy button stays inside a one-line code block: at 44 px it hung
below the block and covered the end of the line. 24 px is the minimum
target size of WCAG 2.2. */
.md-typeset .md-code__button {
min-height: 0;
width: 24px;
height: 24px;
}
.md-typeset pre > code {
padding-right: 2.6rem;
}
.rb-visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.rb-table-filter {
display: grid;
grid-template-columns: minmax(14rem, 28rem) auto;
gap: 0.35rem 0.8rem;
align-items: end;
margin: 1rem 0 0.45rem;
}
.rb-table-filter label {
grid-column: 1 / -1;
color: var(--rb-text-secondary);
font-size: 0.78rem;
font-weight: 700;
}
.rb-table-filter input {
width: 100%;
border: 1px solid var(--rb-text-secondary);
border-radius: 2px;
background: var(--md-default-bg-color);
color: var(--md-default-fg-color);
padding: 0.55rem 0.75rem;
font: inherit;
}
.rb-table-filter-count {
align-self: center;
color: var(--rb-text-secondary);
font-size: 0.8rem;
white-space: nowrap;
}
.md-typeset__scrollwrap[role="region"]:focus-visible {
outline: 3px solid var(--rb-accent);
outline-offset: 2px;
}
/* Admonitions: one meaning per colour.
blue note, info, abstract, example, question, quote: background
teal tip: a shortcut
green success: what it looks like when it worked
orange warning: a risk of losing data or using the wrong file
red danger, failure, bug: something is broken or refused */
.md-typeset :is(.admonition, details):is(.info, .abstract, .example, .question, .quote, .note) {
border-color: #448aff;
}
.md-typeset :is(.info, .abstract, .example, .question, .quote, .note) > :is(.admonition-title, summary) {
background-color: rgba(68, 138, 255, 0.1);
}
.md-typeset :is(.info, .abstract, .example, .question, .quote, .note) > :is(.admonition-title, summary)::before {
background-color: #448aff;
-webkit-mask-image: var(--md-admonition-icon--info);
mask-image: var(--md-admonition-icon--info);
}
.md-typeset :is(.admonition, details).tip {
border-color: #00897b;
}
.md-typeset .tip > :is(.admonition-title, summary) {
background-color: rgba(0, 137, 123, 0.1);
}
.md-typeset .tip > :is(.admonition-title, summary)::before {
background-color: #00897b;
-webkit-mask-image: var(--rb-icon-shortcut);
mask-image: var(--rb-icon-shortcut);
}
.md-typeset :is(.admonition, details):is(.danger, .failure, .bug, .error) {
border-color: #d32f2f;
}
.md-typeset :is(.danger, .failure, .bug, .error) > :is(.admonition-title, summary) {
background-color: rgba(211, 47, 47, 0.1);
}
.md-typeset :is(.danger, .failure, .bug, .error) > :is(.admonition-title, summary)::before {
background-color: #d32f2f;
-webkit-mask-image: var(--md-admonition-icon--danger);
mask-image: var(--md-admonition-icon--danger);
}
:root {
--rb-icon-shortcut: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 0-7 7c0 2.38 1.19 4.47 3 5.74V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.26c1.81-1.27 3-3.36 3-5.74a7 7 0 0 0-7-7M9 21a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1H9z"/></svg>');
}
/* Code: shell commands wrap so the whole command shows on a phone, and the
copy button keeps its own column instead of covering the end. */
.md-typeset :is(.language-sh, .language-bash, .language-shell, .language-console,
.language-powershell, .language-ps1, .language-pwsh, .language-cmd, .language-bat) pre > code,
.md-typeset .rb-cmd pre > code {
white-space: pre-wrap;
overflow-wrap: anywhere;
padding-right: 2.6rem;
}
/* Terminal output: what the screen shows, not something to type */
.md-typeset .rb-terminal pre,
.md-typeset .rb-terminal pre > code {
background: var(--rb-terminal-bg);
color: var(--rb-terminal-fg);
}
.md-typeset .rb-terminal pre {
border: 1px solid var(--rb-border);
border-radius: 4px;
}
.md-typeset .rb-terminal pre::before {
content: "Terminal output";
display: block;
padding: 0.5rem 1.2em 0;
font: 600 0.65rem var(--md-text-font-family);
color: var(--rb-terminal-label);
}
.md-typeset .rb-terminal .md-code__button {
color: var(--rb-terminal-label);
}
/* Numbered steps */
.md-typeset ol.rb-steps,
.md-typeset .rb-steps > ol {
counter-reset: rb-step;
list-style: none;
margin-left: 0;
padding-left: 0;
}
.md-typeset ol.rb-steps > li,
.md-typeset .rb-steps > ol > li {
counter-increment: rb-step;
position: relative;
margin: 0 0 1.2rem;
padding-left: 2.6rem;
min-height: 1.8rem;
}
.md-typeset ol.rb-steps > li::before,
.md-typeset .rb-steps > ol > li::before {
content: counter(rb-step);
position: absolute;
top: 0;
left: 0;
width: 1.8rem;
height: 1.8rem;
border-radius: 4px;
background: var(--md-primary-fg-color);
color: #fff;
font-weight: 700;
line-height: 1.8rem;
text-align: center;
}
[data-md-color-scheme="slate"] .md-typeset ol.rb-steps > li::before,
[data-md-color-scheme="slate"] .md-typeset .rb-steps > ol > li::before {
background: var(--rb-link);
color: #1e2129;
}
/* Card grids. Material's grid cards keep their markup; the hover shadow
gives way to a stronger border. */
.md-typeset .grid.cards {
--md-shadow-z2: 0 0 #0000;
}
.md-typeset .grid.cards > :is(ul, ol) > li,
.md-typeset .grid > .card {
border: 1px solid var(--rb-border);
border-radius: 4px;
}
.md-typeset .grid.cards > :is(ul, ol) > li:is(:hover, :focus-within),
.md-typeset .grid > .card:is(:hover, :focus-within) {
border-color: var(--rb-border-strong);
}
.rb-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
gap: 0.8rem;
margin: 1rem 0 1.5rem;
}
.rb-card {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.8rem;
border: 1px solid var(--rb-border);
border-radius: 4px;
background: var(--md-default-bg-color);
}
.rb-card:is(:hover, :focus-within) {
border-color: var(--rb-border-strong);
}
.md-typeset .rb-card p {
margin: 0;
}
.md-typeset .rb-card .md-button {
margin-top: auto;
align-self: flex-start;
}
.md-typeset .rb-card-logo,
.md-typeset .grid.cards img.rb-card-logo {
width: 40px;
height: 40px;
object-fit: contain;
padding: 4px;
border: 1px solid var(--rb-border);
border-radius: 4px;
background: #fff;
vertical-align: middle;
}
/* Path chip */
.md-typeset code.rb-path {
padding: 0.15rem 0.45rem;
border: 1px solid var(--rb-border);
border-radius: 2px;
font-size: 0.8em;
overflow-wrap: anywhere;
}
/* Content tabs (operating systems): the selected tab reads clearly. */
.md-typeset .tabbed-labels > label {
font-size: 0.75rem;
font-weight: 600;
color: var(--rb-text-secondary);
}
.md-typeset .tabbed-set > input:checked + label,
.md-typeset .tabbed-labels > label:hover {
color: var(--rb-link);
}
.md-typeset .tabbed-labels::before {
background: var(--rb-link);
}
/* Inline coverage bar beside a number */
.rb-bar {
display: inline-block;
width: 5rem;
height: 0.4rem;
margin-right: 0.4rem;
border-radius: 2px;
background: var(--rb-border);
vertical-align: middle;
}
.rb-bar > span {
display: block;
width: var(--v, 0%);
height: 100%;
border-radius: 2px;
background: var(--rb-ok-fg);
}
/* Diagrams written by scripts/sitediagrams.py. Text takes the page font and
colour, boxes take the theme surface, so the drawing follows the palette
toggle. Each diagram carries a wide and a narrow layout. */
.md-typeset .rb-figure {
margin: 1.2rem 0 1.6rem;
}
.md-typeset .rb-figure svg {
display: block;
width: 100%;
height: auto;
max-width: 52rem;
color: var(--md-default-fg-color);
}
.md-typeset .rb-figure figcaption {
max-width: 44em;
margin: 0.4rem 0 0;
font-size: 0.75rem;
font-style: normal;
color: var(--rb-text-secondary);
text-align: left;
}
.rb-diagram text {
font-family: var(--md-text-font-family);
fill: currentColor;
}
.rb-diagram .rb-d-box {
fill: var(--rb-diagram-fill);
stroke: var(--rb-diagram-stroke);
}
.rb-diagram .rb-d-accent {
stroke: var(--rb-diagram-accent);
}
.rb-diagram .rb-d-line {
stroke: var(--rb-diagram-stroke);
}
.rb-diagram .rb-d-head {
fill: var(--rb-diagram-stroke);
}
.rb-diagram .rb-d-num {
fill: var(--rb-diagram-accent);
}
.rb-diagram .rb-d-numtext {
fill: var(--md-default-bg-color);
}
.rb-diagram .rb-d-muted {
fill: var(--rb-text-secondary);
}
.rb-diagram-narrow {
display: none !important;
}
@media (max-width: 37.5em) {
.rb-diagram-wide {
display: none !important;
}
.rb-diagram-narrow {
display: block !important;
}
}
/* The sidebar title stays legible over the list scrolling under it. With
tabs the title shown is the active section's label, held at the top of
the sidebar: a rule under it ends the list cleanly instead of a half
line of text touching the label. */
@media (min-width: 76.25em) {
.md-nav--primary > .md-nav__title,
.md-nav--secondary > .md-nav__title,
.md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link {
background: var(--md-default-bg-color);
border-bottom: 1px solid var(--rb-border);
}
.md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link {
padding-bottom: 0.4rem;
}
}
/* Material's compact footer defaults sit just below WCAG AA on the dark bar. */
.md-footer-meta .md-copyright {
color: #b5b5b5;
}
.md-footer-meta.md-typeset .md-copyright a {
color: #b8c6ff !important;
}
/* 404 page */
.rb-notfound-search {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0.55em 1.2em;
border: 1px solid currentColor;
border-radius: 2px;
color: var(--rb-link);
font-weight: 700;
cursor: pointer;
}
@media (max-width: 44.984375em) {
/* Tables with an action column become one block per row, so the button
is never behind a horizontal scroll (site.js marks them). */
.md-typeset__table:has(table[data-rb-stack]) {
display: block;
}
.md-typeset table[data-rb-stack] {
display: block;
width: 100%;
}
.md-typeset table[data-rb-stack] thead {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
.md-typeset table[data-rb-stack] tbody,
.md-typeset table[data-rb-stack] tr {
display: block;
}
.md-typeset table[data-rb-stack] tr {
padding: 0.5rem 0;
border-top: 1px solid var(--rb-border);
}
.md-typeset table[data-rb-stack] td {
display: block;
position: relative;
min-height: 1.6rem;
padding: 0.25rem 0.6rem 0.25rem 8.4rem;
border: 0;
text-align: left !important;
overflow-wrap: anywhere;
}
.md-typeset table[data-rb-stack] td::before {
content: attr(data-label);
position: absolute;
top: 0.25rem;
left: 0.6rem;
width: 7.4rem;
font-weight: 600;
color: var(--rb-text-secondary);
}
.md-typeset table[data-rb-stack] td .md-button {
margin: 0 0.3rem 0.3rem 0;
}
.md-typeset table[data-rb-stack] td:empty {
display: none;
}
/* Other wide tables scroll with their first column held in place. */
.md-typeset table:not([class]):not([data-rb-stack]) :is(td, th):first-child {
position: sticky;
left: 0;
min-width: min(6rem, 30vw);
max-width: 40vw;
background: var(--md-default-bg-color);
overflow-wrap: break-word;
}
/* A long link or path breaks anywhere, so it never squeezes the first
column down to one letter per line. */
.md-typeset table:not([class]):not([data-rb-stack]) td:not(:first-child) :is(a, code) {
overflow-wrap: anywhere;
}
.md-typeset table:not([class]):not([data-rb-stack]) th:first-child {
z-index: 2;
}
/* On a phone every command wraps: the end of a one-liner (the "| sh"
that makes it work) is never off-screen. Terminal output keeps its
columns and scrolls. */
.md-typeset pre > code {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.md-typeset :is(.rb-terminal, .language-text) pre > code {
white-space: pre;
overflow-wrap: normal;
}
/* Text on reference pages stays at 12 px or more on a phone. */
.rb-file-entry ul,
.rb-sys-file ul,
.rb-hash {
font-size: 0.75rem;
}
.rb-page-header {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.rb-table-filter {
grid-template-columns: 1fr;
}
.rb-table-filter label {
grid-column: auto;
}
.rb-stats {
grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
}
.rb-stat .rb-stat-value {
font-size: 1.1rem;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
/* A file block can be cited by URL: its link shows on hover, and the block a
link lands on is marked. */
.rb-permalink {
margin-left: 0.4rem;
color: var(--md-default-fg-color--light, var(--rb-border));
text-decoration: none;
opacity: 0;
}
.rb-file-entry:hover .rb-permalink,
.rb-sys-file:hover .rb-permalink,
.rb-permalink:focus {
opacity: 1;
}
.rb-file-entry:has(> .rb-file-title:target),
.rb-sys-file:has(> .rb-file-title:target) {
background: var(--rb-surface);
}
-37
View File
@@ -1,37 +0,0 @@
{% extends "main.html" %}
{#
The page a stale link lands on. Pages have moved before (file cards
refiled between manufacturers), so it offers the search, the two pages a
visitor most often wants, and the list a moved emulator, system or
platform page belongs to.
#}
{% block content %}
<h1>Page not found</h1>
<p class="rb-lead">This address has no page. It may have moved since the link was written.</p>
<p><label class="rb-notfound-search" for="__search" tabindex="0">Search the site</label></p>
<ul>
<li><a href="{{ config.site_url }}">Start here</a>: what BIOS files are and how to install them</li>
<li><a href="{{ config.site_url }}which-pack/">Get your files</a>: the pack or the command for your device</li>
<li>Browse <a href="{{ config.site_url }}platforms/">platforms</a>, <a href="{{ config.site_url }}systems/">systems</a> or <a href="{{ config.site_url }}emulators/">emulators</a></li>
</ul>
<p id="rb-notfound-hint" hidden></p>
<script>
(() => {
const match = window.location.pathname.match(/\/(emulators|systems|platforms)\/([^/]+)\/?$/);
const hint = document.getElementById("rb-notfound-hint");
if (!match || !hint) return;
const link = document.createElement("a");
link.href = "{{ config.site_url }}" + match[1] + "/";
link.textContent = "the list of " + match[1];
hint.append("No page is named " + decodeURIComponent(match[2]) + " here. It may be listed under another name in ", link, ".");
hint.hidden = false;
})();
document.querySelector(".rb-notfound-search")?.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.currentTarget.click();
}
});
</script>
{% endblock %}
-58
View File
@@ -1,58 +0,0 @@
{% extends "base.html" %}
{#
Open Graph and Twitter card tags.
mkdocs-material only emits these through its social plugin, which pulls in
Pillow and CairoSVG to render preview images. The pages here already carry a
per-page title and description, so the tags can be filled from those without
taking on two image dependencies. The links get shared on Discord, Reddit and
forums, where a bare URL with no preview is the difference between a card and
a grey rectangle.
One sharing card per section (docs_assets/og, made by
scripts/make_og_cards.py). The section is the page's level when the
generator gives one, otherwise it follows from the URL.
#}
{% block extrahead %}
{% set page_title = page.meta.title if page and page.meta and page.meta.title else (page.title if page and page.title else config.site_name) %}
{% set page_description = page.meta.description if page and page.meta and page.meta.description else config.site_description %}
{% set page_url = page.canonical_url if page and page.canonical_url else config.site_url %}
{% set url = page.url if page and page.url else '' %}
{% set level = page.meta.level if page and page.meta and page.meta.level else '' %}
{% if not url %}{% set section = 'home' %}
{% elif level in ['start', 'setup', 'explore', 'reference', 'contribute'] %}{% set section = level %}
{% elif url.startswith('which-pack') or url.startswith('wiki/first-install') or url.startswith('wiki/faq') or url.startswith('wiki/getting-started') %}{% set section = 'start' %}
{% elif url.startswith('platforms') %}{% set section = 'setup' %}
{% elif url.startswith('systems') or url.startswith('emulators') or url.startswith('files') %}{% set section = 'explore' %}
{% elif url.startswith('contributing') or url.startswith('wiki/adding') or url.startswith('wiki/profiling') or url.startswith('wiki/testing') or url.startswith('wiki/release') %}{% set section = 'contribute' %}
{% else %}{% set section = 'reference' %}{% endif %}
{% set preview_image = config.site_url ~ 'assets/og/' ~ section ~ '.png' if config.site_url else '' %}
{# The theme's favicon is the 32 px PNG. Browsers that read an SVG icon
take this one, listed after it, and a home-screen shortcut takes the
opaque 180 px icon (scripts/make_site_icons.py makes all three). #}
<link rel="icon" type="image/svg+xml" href="{{ 'assets/brand/favicon.svg' | url }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ 'assets/brand/apple-touch-icon.png' | url }}">
<meta property="og:type" content="{{ 'website' if not url else 'article' }}">
<meta property="og:site_name" content="{{ config.site_name }}">
<meta property="og:title" content="{{ page_title }}">
<meta property="og:description" content="{{ page_description }}">
{% if page_url %}<meta property="og:url" content="{{ page_url }}">{% endif %}
{% if preview_image %}
<meta property="og:image" content="{{ preview_image }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="{{ config.site_name }}: {{ page_title }}">
{% endif %}
{# The sitemap, named from every page: a project site under /retrobios/
cannot serve the host-root robots.txt that would announce it. #}
{% if config.site_url %}<link rel="sitemap" type="application/xml" title="Sitemap" href="{{ config.site_url }}sitemap.xml">{% endif %}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ page_title }}">
<meta name="twitter:description" content="{{ page_description }}">
{% if preview_image %}<meta name="twitter:image" content="{{ preview_image }}">{% endif %}
{% endblock %}
-298
View File
@@ -1,298 +0,0 @@
/* Progressive enhancement for generated RetroBIOS reference tables. */
(() => {
"use strict";
// A heading's words without its permalink sign.
const headingText = (heading) => {
const copy = heading.cloneNode(true);
copy.querySelectorAll(".headerlink").forEach((link) => link.remove());
return copy.textContent.trim();
};
const textFromHeading = (element) => {
let cursor = element;
while (cursor) {
cursor = cursor.previousElementSibling;
if (!cursor) break;
if (/^H[1-6]$/.test(cursor.tagName)) {
return headingText(cursor);
}
}
const title = document.querySelector("h1");
return title ? headingText(title) : "Data table";
};
// A scroller is a stop in the tab order only while it overflows: a table
// in a closed block or a hidden tab gains its overflow later.
const markScrollable = (container, heading) => {
if (!container) return;
container.dataset.rbHeading = heading;
if (container.scrollWidth > container.clientWidth + 1) {
container.tabIndex = 0;
container.setAttribute("role", "region");
container.setAttribute("aria-label", `${heading}, scrollable table`);
} else if (container.getAttribute("role") === "region") {
container.removeAttribute("tabindex");
container.removeAttribute("role");
container.removeAttribute("aria-label");
}
};
const recheckScrollable = () => {
document.querySelectorAll(".md-typeset__scrollwrap[data-rb-heading]").forEach((container) => {
markScrollable(container, container.dataset.rbHeading);
});
};
// A table whose rows end in an action (a download button) is laid out one
// block per row on a phone, each cell under its column name, so the action
// never sits behind a horizontal scroll.
const stackActionTable = (table) => {
if (!table.querySelector("tbody .md-button")) return;
const labels = Array.from(table.querySelectorAll("thead th")).map((cell) =>
cell.textContent.trim()
);
if (!labels.length) return;
table.dataset.rbStack = "true";
table.querySelectorAll("tbody tr").forEach((row) => {
Array.from(row.children).forEach((cell, column) => {
if (labels[column] && !cell.hasAttribute("data-label")) {
cell.setAttribute("data-label", labels[column]);
}
});
});
};
const enhanceTables = () => {
document.querySelectorAll(".md-typeset table").forEach((table, index) => {
if (table.dataset.rbEnhanced === "true") return;
table.dataset.rbEnhanced = "true";
table.querySelectorAll("thead th").forEach((cell) => {
if (!cell.hasAttribute("scope")) cell.setAttribute("scope", "col");
});
table.querySelectorAll("tbody tr").forEach((row) => {
const first = row.querySelector(":scope > th");
if (first && !first.hasAttribute("scope")) first.setAttribute("scope", "row");
});
// The scrolling element is Material's scrollwrap, one level above
// the table's own wrapper.
const container = table.closest(".md-typeset__scrollwrap") || table.parentElement;
const heading = textFromHeading(container);
if (!table.querySelector("caption")) {
const caption = document.createElement("caption");
caption.className = "rb-visually-hidden";
caption.textContent = heading;
table.prepend(caption);
}
stackActionTable(table);
markScrollable(container, heading);
const rows = Array.from(table.querySelectorAll("tbody tr"));
if (rows.length < 20 || !container || container.dataset.rbFilter === "true") {
return;
}
container.dataset.rbFilter = "true";
const controls = document.createElement("div");
controls.className = "rb-table-filter";
const id = `rb-table-filter-${index}-${Math.random().toString(36).slice(2, 8)}`;
const label = document.createElement("label");
label.htmlFor = id;
label.textContent = `Filter ${heading}`;
const input = document.createElement("input");
input.id = id;
input.type = "search";
input.autocomplete = "off";
input.placeholder = "Name, platform, system, hash...";
const count = document.createElement("span");
count.className = "rb-table-filter-count";
count.setAttribute("aria-live", "polite");
count.textContent = `${rows.length} rows`;
controls.append(label, input, count);
container.before(controls);
const searchable = rows.map((row) => ({
row,
text: row.textContent.normalize("NFKD").toLocaleLowerCase(),
}));
input.addEventListener("input", () => {
const query = input.value.trim().normalize("NFKD").toLocaleLowerCase();
let visible = 0;
searchable.forEach((entry) => {
const matches = !query || entry.text.includes(query);
entry.row.hidden = !matches;
if (matches) visible += 1;
});
count.textContent = `${visible} of ${rows.length} rows`;
});
});
};
const nameMaterialWidgets = () => {
document.querySelectorAll('[role="dialog"]').forEach((dialog) => {
if (dialog.hasAttribute("aria-label") || dialog.hasAttribute("aria-labelledby")) return;
const heading = dialog.querySelector("h1, h2, h3");
dialog.setAttribute("aria-label", heading?.textContent.trim() || "Site search");
});
// The page-load bar is drawn for the eye only: the new page announces
// itself when it arrives.
document.querySelectorAll(".md-progress").forEach((progress) => {
progress.setAttribute("aria-hidden", "true");
});
// Material wraps each copy button in a nav element: one unnamed
// navigation landmark per code block. The button stays, the landmark goes.
document.querySelectorAll("nav.md-code__nav").forEach((nav) => {
nav.setAttribute("role", "none");
});
};
// files/?hash= reads one shard of the lookup, named by the first two hex
// characters of the value: 20 KB where the whole index was 675 KB.
const HASH_LENGTHS = { 8: "CRC32", 32: "MD5", 40: "SHA1", 64: "SHA256" };
const LAYER_LABELS = {
collection: "In the collection",
"data-directory": "In a data directory the packs ship",
platform: "Declared by a platform",
emulator: "Read by an emulator",
};
const siteRoot = () => {
const script = document.querySelector('script[src$="javascripts/site.js"]');
return script ? new URL("../", script.src) : new URL("../", window.location.href);
};
const lookupShard = (value) =>
fetch(new URL(`api/v1/lookup/${value.slice(0, 2)}.json`, siteRoot()))
.then((response) => (response.ok ? response.json() : { items: [] }))
.then((shard) => shard.items || []);
const normalizeHash = (raw) => raw.trim().toLowerCase().replace(/^0x/, "").replace(/\s+/g, "");
const renderMatches = (status, value, items) => {
status.textContent = "";
const kind = HASH_LENGTHS[value.length];
const heading = document.createElement("p");
heading.textContent = `${items.length} ${items.length === 1 ? "match" : "matches"} for ${kind} ${value}:`;
status.append(heading);
Object.entries(LAYER_LABELS).forEach(([layer, label]) => {
const found = items.filter((item) => item.layer === layer);
if (!found.length) return;
const title = document.createElement("p");
const strong = document.createElement("strong");
strong.textContent = label;
title.append(strong);
const list = document.createElement("ul");
found.forEach((item) => {
const row = document.createElement("li");
const link = document.createElement("a");
link.href = new URL(item.url, siteRoot()).href;
const code = document.createElement("code");
code.textContent = item.name || item.value;
link.append(code);
row.append(link);
if (item.owner) row.append(` (${item.owner})`);
list.append(row);
});
status.append(title, list);
});
};
// files/?sha1=<sha1> is a file's content address and opens its card
// wherever it is now; files/?hash= also takes an MD5, CRC32 or SHA256 and
// lists every page that names the value.
const resolveFileAddress = () => {
const status = document.getElementById("rb-file-lookup-status");
if (!status) return;
const params = new URLSearchParams(window.location.search);
const permalink = params.has("sha1");
const value = normalizeHash(params.get("sha1") || params.get("hash") || "");
if (!value) return;
const input = document.getElementById("rb-hash");
if (input) input.value = value;
const prefix = permalink && /^[0-9a-f]{12}$/.test(value);
if (!prefix && !(/^[0-9a-f]+$/.test(value) && HASH_LENGTHS[value.length])) {
status.textContent =
"A hash is 8 (CRC32), 32 (MD5), 40 (SHA1) or 64 (SHA256) hexadecimal characters.";
return;
}
status.textContent = "Looking it up...";
lookupShard(value)
.then((items) => {
const matches = items.filter((item) =>
prefix
? item.algorithm === "sha1" && item.value.startsWith(value)
: item.value === value
);
const cards = matches.filter((item) => item.layer === "collection" || item.layer === "data-directory");
const targets = new Set(cards.map((item) => item.url));
if (permalink && targets.size === 1) {
window.location.replace(new URL(cards[0].url, siteRoot()));
} else if (matches.length) {
renderMatches(status, prefix ? matches[0].value : value, prefix ? cards : matches);
} else {
status.textContent = "No file the site knows has this hash.";
}
})
.catch(() => {
status.textContent = "The file index could not be read.";
});
};
// A file card moved to another page keeps its address: an id of the form
// sha1-<12 hex> that this page does not hold is looked up by content, and
// an emulator card listed in the table of a split page opens its page.
const followMovedCard = () => {
const id = decodeURIComponent(window.location.hash.slice(1));
if (!id) return;
const target = document.getElementById(id);
const row = target?.closest(".rb-card-index tr");
const link = row?.querySelector("a[href]");
if (link) {
window.location.replace(link.href);
return;
}
const match = /^sha1-([0-9a-f]{12})$/.exec(id);
if (!match || target) return;
window.location.replace(new URL(`files/?sha1=${match[1]}`, siteRoot()));
};
// The site search no longer holds hashes: Enter on a hash opens the lookup.
const hashSearch = () => {
const query = document.querySelector('input[data-md-component="search-query"]');
if (!query || query.dataset.rbHash === "true") return;
query.dataset.rbHash = "true";
query.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
const value = normalizeHash(query.value);
if (/^[0-9a-f]+$/.test(value) && HASH_LENGTHS[value.length]) {
event.preventDefault();
window.location.assign(new URL(`files/?hash=${value}`, siteRoot()));
}
});
};
const enhance = () => {
enhanceTables();
nameMaterialWidgets();
resolveFileAddress();
followMovedCard();
hashSearch();
};
let resizeTimer = 0;
window.addEventListener("resize", () => {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(recheckScrollable, 150);
});
document.addEventListener("toggle", recheckScrollable, true);
document.addEventListener("change", (event) => {
if (event.target.matches?.(".tabbed-set > input")) recheckScrollable();
});
if (window.document$?.subscribe) {
window.document$.subscribe(enhance);
} else if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", enhance, { once: true });
} else {
enhance();
}
})();