# The Machine

The design of the boot chain, and the backlog for it.

`ROADMAP.md` is about servers and services. `README.md` is about how the code
works. This file is about the fiction: what xanders.page is pretending to be,
why that lets it be five different decades at once, and everything still to
build.

---

## The problem this file solves

The honest version of the question: I love Windows 95, 98, XP, Vista and 7,
AND I love the Fallout terminal, and right now the boot chain is NTLDR from
2001, a Vista loading bar from 2006, a Windows 98 dialog, an XP Command
Prompt and a green phosphor terminal from a game set in 2281. Do I have to
pick one?

**No. But they cannot all stand in the same room arguing.**

The mishmash reads as a pile of references because every era is competing for
the same screen. Give each one its own room and a reason to be there and the
same pile reads as a machine with history.

---

## The frame: one machine, several partitions

Real computers accumulate eras. Your firmware is older than your operating
system. There is a recovery partition you have never booted. There is an old
install you kept because you could not face moving the files. Nobody looks at
that and calls it inconsistent, because it is what every long lived machine
looks like.

So xanders.page is **one beige box** that has been rebuilt several times.

```
        FIRMWARE  (always older than everything it boots)
        POST, the boot device menu, PXE over IPv4
                          |
                     BOOT MENU
                          |
        +-----------------+------------------+
        |                 |                  |
   Pages Web       Pages 98        Pages Server Core
   the front door    the toy box     the private wing
   Frutiger Aero     Win95/98        no GUI, green phosphor
   2006-2009         1995-1998       a serial console
```

### Why each one has earned its place

**The firmware is era neutral and that is the point.** POST screens looked
much the same from 1995 to 2010. It is the one part that never changes, which
makes it the frame the rest hangs off. Firmware being older than the OS it
boots is correct, not sloppy.

**Pages Web** is the public creative hub, the thing you show people. Aero
glass, the wallpaper cycler, the buttons, the ads. It is already built.

**Pages 98** is where the fun lives. A real desktop: draggable windows, a
Start menu, icons. This is the room that does not exist yet and it is the
biggest single thing on the backlog.

**Pages Server Core** is the private wing, and this is the one that fixes the
whole problem. Windows Server Core genuinely ships with no graphical shell.
You get a console. A green phosphor terminal is not a random Fallout
reference bolted onto a Windows site, it is **what you get when there is no
GUI**. The RobCo styling is the flourish on top of a justification that
already holds.

That is the trick. The Fallout terminal stops being a fifth competing theme
and becomes the correct answer to "what does this machine look like with the
desktop switched off".

### What this buys

- Every era you like gets a room instead of a fight.
- The private wing feels like a different machine, because it is a different
  boot.
- New ideas have an obvious home. Minesweeper goes in Pages 98. A new gallery
  goes in Pages Web. A new command goes in Server Core.
- Nothing already built has to be thrown away.

---

## Reaching the private wing

**Done, and then done again, because the first answer was half of one.**

The first version moved the link out of the sidebar and into the boot menu,
the way you would reach a recovery partition. That was better than a sidebar
link and it was still not right, because the boot menu entry and the login
screen were the same page: Pages Server Core, the green terminal that is one
of the four partitions and is meant to be shown to people, opened by asking
for a password.

So the terminal is two rooms now, and the flag in the URL decides which:

| | |
|---|---|
| `pages/private.html?booted` | **Pages Server Core.** Public. Drops straight into the guest shell. Linked from the boot menu, the sidebar and the Pages 98 Start menu. |
| `pages/private.html?booted&login` | **The private wing.** The Authelia login, exactly as it was. Linked from nowhere. |

You get to the second by booting **MS-DOS** behind F12 at the POST and typing
`TELNET PAGES`. Which is period correct rather than a puzzle: NCSA Telnet for
DOS was real, and connecting out of a DOS box to a machine that then asks who
you are is precisely what you did in 1996.

Everything that names the wing is behind the same flag. The public room does
not print the Authelia banner, does not say the words "private wing", and
`/etc/motd` and `~/README` inside the invented filesystem say which room they
are in. `exit` in the public room closes the session and goes to the boot
menu rather than dropping you at a login prompt, which is how the split
would otherwise have been undone by the most obvious command in a shell.

**Be clear with yourself about what that is and is not.** It is flavour. It
is not security, and it must never be treated as security. The flag is in the
source and anybody can read it; all it changes is how findable the door is.
The only thing actually guarding anything is Authelia on the server, checking
a password it alone knows, in front of services that are not on this site at
all. Obscurity makes it feel like a secret. Authelia makes it be one.

---

## Phase 0: three things that are not features

None of these are fun. All three are worth more than everything below them.

- [x] **Put this in version control.** 444MB and months of work with no
      history and no undo. One bad `tools/pages.py` run and it is gone.
      - `img/` is 436MB of that, and images do not belong in git. Track the
        code, `.gitignore` the wallpapers and buttons, and back those up
        separately. Losing the code is a catastrophe; losing the wallpapers
        is an afternoon of re-downloading.
- [ ] **Open the site on the 2010 and the 2012 MacBook.** STILL NOT DONE, and
      still the most valuable thing on this list. Everything here is written
      to Safari 7 spec and has only ever run in Chrome, which is theory
      rather than testing.
      - **`TESTING.md` is now the actual job**, written 21 August 2026.
        The reason this kept not happening is that "test it" is not a task,
        it is an obligation with no edges: you cannot tell when you are
        finished, so you do not start. That file is the same work with
        edges. Eight sections, about half an hour a machine, what to look at
        and what correct looks like, and a place to write down what you saw.
      - It names the two places where a Safari 7 viewport unit bug would
        actually show, `.tty` and `.osmenu`, both full screen takeovers, so
        a failure is loud rather than subtle.
      - It also says which differences are CORRECT rather than broken: the
        hardware probe legitimately prints fewer lines on Safari 7, because
        two of the APIs it reads arrived in Safari 10. A shorter list is
        right; a line saying `undefined` is not.
      - `tools/serve.py --lan` now binds to the network and prints the
        addresses to type into the MacBooks. Nothing to deploy, nothing to
        buy. Off by default, because binding to 0.0.0.0 lets anything on the
        same wifi read this folder.
      - If nothing loads it is almost always the Windows firewall. Allow
        Python on Private networks, or add an inbound rule for TCP 8080.
      - Two predicted bugs are FIXED already, without a Mac, because both
        were certain enough to act on:
        - `caret-color` is Safari 16. It was hiding the real text caret over
          the drawn cursor in `css/tty.css` and `css/shell.css`, so both Macs
          would have shown two cursors. The inputs are parked off screen now.
        - `localStorage` was unguarded in `js/main.js`, `js/wallpaper.js` and
          `js/gallerypage.js`. Safari 7 in a Private window throws on
          `setItem`, killing the rest of the file. Everything goes through
          `storeGet` and `storeSet` in `js/store.js` now.
      - Still unknown, and only a real machine can answer it: 11 viewport
        unit values in `css/tty.css`. Safari 7 has vh and vw but with known
        bugs inside `position: fixed`. Look at the terminal first.
- [x] **The documentation is served, on purpose.** DECIDED, 21 August 2026.
      It suits this site: nothing here is a secret, the whole of `js/` already
      ships to every visitor, and a site whose subject is partly itself is a
      strange place to start hiding the workings.
      - The change is not that they became readable, they always were. It is
        that they are now FINDABLE. A file that is served but that nothing
        points at is neither published nor hidden, it is just sitting there.
        The site map has a "How it is built" section linking all five, and
        `sitemap.xml` lists them, so the list of what this site publishes is
        complete.
      - **THE RULE THAT FOLLOWS: nothing goes in a markdown file at this root
        that would not go on a page.** Not a note to self, not a password, not
        a path into anything. That rule has already been broken once, by a
        paragraph naming the remote access on this machine while explaining
        that it was being kept off the site.
      - And do not leave scratch files at the root. One got left there during
        the audit that produced this item, and it was served too.

- [x] **Decide how the site gets from G: to xanders.page.** It is built and
      there is no deploy path. Worth settling now because it shapes the
      private wing: if the VPS serves the public site too, Caddy handles
      both, and the real client IP endpoint in `js/sysinfo.js` comes free.
      - SETTLED on 2026-08-25, by building it. Caddy at home serves the
        repo directly, the VPS stream proxies 443 over WireGuard, and
        `https://xanders.page` returns the real site. The thing actually
        in the way was never the deploy path: it was a Windows Firewall
        with no inbound rule on the WireGuard interface, so the tunnel
        carried packets that died one hop short of Caddy.

Also worth doing at some point, in the boring-but-real category:

- [x] **Resize the wallpapers.** RUN, on 2026-08-20. `tools/shrinkwall.py`
      turned 350 images totalling 389MB into 76MB at 1600px wide, in
      `img/wallpaper-small`. The originals in `img/wallpaper` are untouched.
      - The measurement in the script's own docstring was WRONG by half: it
        said "roughly 560KB each" and every file is actually 1920x1200 at a
        mean of 1124KB, so the saving was double what it advertised.
      - THE SWAP TURNED OUT TO BE UNNECESSARY, and that is the better
        answer. Nothing had to move: the site now SERVES `img/wallpaper-small`
        because six path strings in `js/wallpaper.js`, `js/gallerypage.js`,
        `js/featured.js` and `js/desktop.js` were changed to point at it. The
        originals stay exactly where they are, `MANIFEST` is still valid
        because the filenames are identical, and the front page went from
        about 1.2MB of wallpaper to 125KB.
      - So there is no 389MB move to approve, and no folder rename to get
        wrong at two in the morning. `img/wallpaper` is now the archive and
        the source `tools/shrinkwall.py` reads; `img/wallpaper-small` is what
        the web server hands out.
      - STILL YOURS: getting both folders onto something that is not this
        disk. Neither is in git, and `img/wallpaper-small` is now load
        bearing, so it has to reach the web server too.

- [x] **The guestbook cannot work on a static site.** Already handled: the
      form says plainly that nothing is stored, rather than appearing to
      accept a message and dropping it.

---

## Backlog

Ordered so each phase is worth shipping on its own.

## Round nine: the BIOS became a control panel

- [x] **The Setup screen was broken, and the bug was one word.** Every row is
      a `<span class="ami">` joined with NOTHING, relying on those spans being
      `display: block`. The `.is-setup` rule overrode them to `inline-block`
      to centre the panel, so the rows flowed inline and wrapped wherever they
      fit: the tab bar ended up in the middle of the screen and the two
      columns interleaved. One wrapper element is the inline-block that
      centres now, and the rows inside it are blocks again. Two elements, two
      jobs, instead of one element asked to do both.
- [x] **It fills the monitor.** It was drawn at the POST's font size, which is
      deliberately small because a real POST used the top left third of the
      screen. Setup is the opposite: it was a full screen application that
      used all 80x25. It has its own size now, `2.8vmin`, which puts 80
      columns across 79 percent of the width at 1440x900 with no overflow.
      Every row is exactly 80 characters, which it was not: an off by one put
      three of them at 81 and pushed the panel 95px off the right.
- [x] **It is a real settings panel.** Up and down walk the settings, left and
      right change tab, ENTER or plus or minus changes a value, F10 saves, F1
      jumps to Help. Monitor Effect, System Sounds, Animation, Quick Boot and
      1st Boot Device are all LIVE: each one reads and writes the same value
      the sidebar switch already owns, so there is one setting with two
      windows onto it rather than two settings.
      - A setting whose backing file is not loaded on that page reads `N/A`
        in dim ink and refuses, rather than pretending. `js/crt.js` is not on
        the private wing, so Monitor Effect is genuinely unavailable there.
- [x] **A Help tab**, which no real BIOS has. This one does, because the site
      is a machine somebody has to work out how to use and the alternative is
      a visitor who never finds three of the four partitions. F1 from
      anywhere in Setup.
- [x] **No mouse before an operating system is running.** The POST, Setup, the
      boot device menu, the network boot, the NTLDR menu and MS-DOS all hide
      the cursor and ignore both buttons. A BIOS had no mouse driver, DOS had
      none unless you loaded one, and a console never had one.
      - THE COST IS REAL AND IS WRITTEN DOWN BESIDE THE RULE. The clickable
        POST footer and boot menu rows were added deliberately, because F12
        on a Mac laptop usually needs Fn held. They are inert now. Deleting
        one `pointer-events` line brings them back.
- [x] **The POST is shown once per machine.** Seen it, and it is skipped
      entirely on every later arrival, landing at the boot menu instead,
      which is the part with a decision in it. Three things bring it back: a
      different browser, which has its own storage and so has never seen it;
      a changed machine, caught by a fingerprint of screen size, colour
      depth, pixel ratio, adapter, platform and timezone; and `?post`, for
      anybody who wants to watch it again. Quick Boot on the Advanced tab
      turns the whole behaviour off.
      - Quick boot used to be ONE stored flag meaning three different things,
        which is why it could never be switched off: the only thing that
        cleared it was never having booted.

## Round ten: one list of settings, three windows, and a disk

- [x] **Pages Server Core split from the private wing.** See the section
      above. The terminal is public and the login is behind `TELNET` on the
      MS-DOS partition, which is the least sign posted thing on the site.
      - `js/tty.js` gained `LOGIN`, and the Fallout banner's last four lines,
        the ones that set a protection on `PRIVATE.WING` and run
        `AUTHELIA/FIRSTFACTOR`, are now behind it. Not because naming Authelia
        gives anything away, since a blog post on this site names it on
        purpose, but because they are the login's preamble and the public room
        has no login: they were a caption for something that does not happen
        there.
      - `js/shell.js` decides which room it is in once, at `shellStart`, and
        dresses `/etc/motd` and `~/README` to match. Both of those used to
        open by naming the wing, and `cat` is one of four commands this shell
        has.
      - `exit` in the public room closes the session and goes to the boot
        menu. It used to go to a login prompt, which handed the front door to
        anybody who typed the most obvious command in a shell.
- [x] **The Termlink minigame came back.** Splitting the terminal took it
      away: it was offered after three failed passwords, and the public room
      has no password. It is a `hack` command now, still not in the help
      listing, with a manual page so that `man hack` does not read as a bug.

- [x] **One list of settings, in `js/settings.js`.** The five settings were
      written inside the AMIBIOS Setup screen's closure, which was correct
      while Setup was the only screen with settings on it. It is not any
      more.
      - `SITE_SETTINGS` is the list. Each entry is a live view onto a real
        switch: `get()` reads the real value, `toggle()` goes through the real
        toggle, and a setting whose backing file is not on the page reads
        `N/A` and refuses rather than pretending.
      - The two storage keys live there too. `SYS_QUICK_PREF` was declared in
        `js/sysinfo.js` and `OS_KEY` in `js/osmenu.js`, and neither of those
        files is on every page, so a panel that wanted the value without them
        had to write the key out a second time.
- [x] **A Control Panel in Pages 98.** All five, drawn as Windows would have
      drawn them, on the Start menu and the desktop. Display Properties is
      still there and still has the three display switches, because that is
      where a Windows user looks for those, but Quick Boot and 1st Boot
      Device are firmware settings with no business in a display applet.
- [x] **A Settings panel in Pages Web**, in the sidebar, under a heading that
      says Settings. Three of the five used to be appended to the bottom of
      the *wallpaper* widget by `js/crt.js`, with no heading, because that was
      the only panel on the page and they had to go somewhere. Ninety lines of
      settings UI came out of the file that draws scanlines.
- [x] **The three panels cannot disagree.** `js/crt.js`, `js/sfx.js` and
      `js/motion.js` each expose one change hook, and whoever assigns it last
      wins. Display Properties assigned all three when it opened, so opening
      the Control Panel and then throwing a switch in Display Properties left
      the Control Panel showing a state that was no longer true. There is a
      real list of listeners now, and the single slot hooks are chained rather
      than replaced.
      - `sfxSet` announces its own change. The notification used to be
        bolted onto the three callers that happened to have a switch on
        screen: the Ctrl+M handler and the `sound` and `mute` commands in the
        shell. Three copies of one line, and any new caller silently did not
        notify.

- [x] **Help under the boot menu, on F1.** The first thing anybody sees is a
      question with a clock on it and three names they have never met. It now
      says what the three are, that each can reach the other two, that
      Restart comes back here, which keys do what during the boot, and where
      the settings are. `js/osmenu.js` never sets `is-help` and `is-adv`
      together, so there is no third combination for the CSS to have an
      opinion about.
- [x] **The boot menu is sized in `vmin`, not `vw`.** The same fault the POST
      had: a screen with a fixed number of ROWS on it was being scaled by the
      window's WIDTH, so an ultrawide monitor pushed the bottom of it past the
      bottom of the window. `2.8vmin` is 22.4px at 1280x800, which is exactly
      what `1.75vw` was on the two machines this is built for.

- [x] **MS-DOS got the rest of its disk.** The root listing named `GAMES` and
      `DOS`, and `CD` answered "There is nothing in there. That is the whole
      point of this partition", which is a joke that costs more than it earns:
      `DIR` promises two directories and the next command takes them away.
      - `CD` works, the prompt says where you are, and both directories have
        contents. `HIMEM.SYS`, `SCANDISK`, `DEFRAG` and `EDIT` moved into
        `C:\DOS`, where `CONFIG.SYS` and `AUTOEXEC.BAT` were already saying
        they were.
      - **`FDISK`** is the one worth the trouble. You are standing in the
        smallest partition on the machine, running the 1993 program for
        looking at partitions, and it can see the other three and cannot read
        a byte of any of them.
      - **`MSD`** reads the same real browser values `js/sysinfo.js` gives to
        System Properties on Pages 98 and to the Main page of the BIOS. Same
        browser, three period costumes. Its Mouse line is where the site
        explains its own no pointer rule.
      - `TREE`, `CHKDSK`, `VOL`, `PATH` and `ECHO`, and the up arrow walks
        what you typed, because `DOSKEY` is loaded in `AUTOEXEC.BAT` and
        that is the only thing anybody ever loaded `DOSKEY` for.

## Round eleven: the machine turns on and off

- [x] **The order is the order a machine actually boots in.** POST, then the
      splash, then a boot device. It used to be POST, then the splash INSIDE
      the network boot, then the network boot, with no way to reach the
      firmware on any visit after the first.
      - **The splash is the firmware now**, which is what an OEM logo always
        was: the same firmware doing the same work with a badge over the top
        of it, and the same footer along the bottom offering DEL and F12. It
        is shown on EVERY boot.
      - **That is what fixed the real bug.** DEL and F12 were only ever armed
        while the verbose POST was on screen, and the verbose POST is shown
        once per machine. So Setup, the boot device menu and the whole MS-DOS
        partition behind it were reachable on a visitor's first arrival and
        never again.
      - **ESC on the badge shows the POST text**, the way ESC did on a real
        machine. It is the first way of asking for the verbose POST that does
        not involve editing the URL.
      - The boot menu is sized in `vmin` now, not `vw`. Same fault the POST
        had: a screen with a fixed number of rows was being scaled by the
        window's width.

- [x] **A real boot device order, and the BIOS can change it.** The Boot page
      said this:

      ```
      Boot Device Priority
        1st Boot Device   [Pages Web]
        2nd Boot Device   [Hard Disk]
        3rd Boot Device   [CD/DVD]
      ```

      Only the first row did anything, and what it did was not what it said:
      its value was an operating system, not a device. The two rows that did
      name devices were painted text.
      - Three slots, three devices, stored as one value so the machine can
        never have two 1st Boot Devices. Changing one swaps it with wherever
        that device was, which is what pressing + on that row did.
      - **Put Hard Disk first and ENTER at the POST goes straight to MS-DOS.**
      - CD/DVD is empty, says so, and falls through to the next device.
      - The stored order is validated rather than trusted: a corrupt value
        cannot leave the machine unable to boot, and the machine is the site.
      - `1st Boot Device` is called `Default OS` now and has moved up into
        Boot Settings Configuration, where its name is true.
      - **BIOS only.** A Windows Control Panel cannot reorder the boot devices
        of the machine it is running on. `setFor()` in `js/settings.js` is how
        a setting says which screens may draw it.

- [x] **Shut Down and Restart, on both graphical partitions.** Pages Web had
      neither: its only exit was a sidebar link that replayed the whole
      network boot.
      - **Restart** blacks the screen for about a second, the way a machine
        does between stopping and its firmware coming back, and lands on the
        splash. It does not replay the POST, because you have just watched
        this machine run.
      - **Shut Down** asks the browser to close the tab, which it will
        usually refuse, and then puts up the screen a stopped machine leaves
        behind. On Pages 98 that is the amber `It's now safe to turn off your
        computer`, which is the correct screen rather than a consolation
        prize.
      - Both mark the disk clean, so a deliberate shutdown is not greeted by
        `chkdsk` next time for having done the one thing right.
      - Pages 98's dialog is the real one: Stand by, Shut down, Restart, and
        **Restart in MS-DOS mode**, which is a one shot override of the boot
        order and does not change it.
      - Server Core gained `shutdown`, `poweroff` and `halt`. MS-DOS gained
        `SHUTDOWN`, which 6.22 never had, because turning a machine off was a
        switch on the front of it.

- [x] **THE EDITOR WAS BEING ERASED HALF A SECOND AFTER IT OPENED.** `EDIT`
      painted correctly and then the cursor blink wiped it: `draw()` repainted
      the command prompt screen unconditionally into the same element, and the
      blink calls `draw()` every 500ms.
      - It survived because of how it failed. The blink interval is only
        started when motion is ALLOWED, so anybody testing with reduced motion
        on, which halves every animation on this site and is therefore what
        you test with, got a perfectly working editor.
      - One early return at the choke point fixes every caller at once. The
        editor's block cursor blinks now, which the real one did.

- [x] **MS-DOS is the admin partition, and G: is the website.** `LOGIN` mounts
      the site's own files as a second drive: the scripts, the stylesheets,
      the page fragments and the documentation, on the machine serving them.
      `CD` and `DIR` walk it, `TYPE` fetches a file and prints it, `VIEW`
      puts a real photograph on the text screen, and `EDIT` opens a real file
      in the MS-DOS Editor.
      - **It is not the G:\ drive on the owner's computer and it cannot be.**
        The web root is `G:\Websites\xanderspage`. Above it sit an archive, a
        recycle bin and a folder called `KeePass Database`. A server rooted at
        the drive would publish a password vault, and `tools/serve.py` has a
        `--lan` flag that would then hand it to the network. The web root is
        the boundary, deliberately.
      - **F2 saves to this browser and says so in those words.** A static site
        cannot write to its own disk: there is no application on the other end
        of it. The draft survives a reload, does not survive a different
        computer, and is not on the website until it is copied into the file
        and the build is run. `js/paint.js` set that precedent and this
        follows it.
      - **`admin` is a mode, not a permission.** Everything on G: is public
        and fetchable by anybody who knows the URL, which is why `LOGIN` asks
        for nothing. A password prompt in front of a public directory teaches
        somebody the wrong thing about which of their locks are real.
      - `MANIFEST.site` is how it knows the names. A browser cannot list a
        directory over HTTP, which is the whole reason `js/manifest.js`
        exists.

- [x] **The DOS prompt could not type a colon or an underscore.** There were
      two character maps and only the editor's was finished. Typing `G:` needs
      a colon and `baby_rabbits.jpg` needs an underscore, so the new drive was
      unreachable and so was most of what is on it. One map now, used by both.

- [x] **A checker for the fault that broke four features silently.**
      `tools/manifest.py` emitted `.split("` followed by a real line break,
      which is an unterminated string literal, so `js/manifest.js` stopped
      parsing and `MANIFEST` went undefined. That took the wallpapers, the
      gallery, the Pages 98 desktop background and the home page featured
      panel with it, and nothing caught it: the manifest lane reads the names
      with a regex and the regex still matched.
      - `check_strings` walks 19,961 lines and reports any that end inside a
        quote. It knows a regex literal from a division, because the first
        version flagged six lines of `.replace(/"/g, "&quot;")` and a checker
        that cries wolf gets ignored.

## Round eleven, the part that was not done yet

Twenty nine defects, found by six reviewers reading the round eleven changes
and then an independent skeptic trying to refute each one. These are what
survived refutation. Nothing here is speculative: every one names a file, a
line, and the exact state that produces the wrong outcome.

**ALL TWENTY FOUR FIXED on 2026-08-25**, one agent per file so that no two
edits could land in the same file at once, each handed to a separate reviewer
that reread the file rather than the report.

**The reviewers were the useful half.** Five of the eleven fixes shipped with
a new false claim inside the very comment written to remove a false claim,
and those were caught and corrected before this was written down:

- `TESTING.md` told the tester to look for **green text** on the POST screen.
  `css/boot.css` paints it `#c8c8c8`. The one screen you cannot check by
  reading the code is the one a wrong instruction wastes a trip to Perth on.
- `js/sysinfo.js` said "three exits carry it" and then named four. A miscount
  inside the fix for a miscount.
- `js/firmware.js` swapped the false claim about `?boot` for a smaller false
  claim about `?boot`.
- `js/firmware.js` invented "Setup opens on DEL and on nothing else", which
  the boot menu's own Enter Setup row disproves.
- `js/dos.js` told you F2 "was not pressed" in a case where it was pressed
  and the save silently failed, which is the same lie defect 516 was
  filed against.

Two fixes went further than the ticket asked, and both were verified:

- **571 was made true rather than deleted.** The comment claimed the off
  screen put the page behind it out of reach, "Tab included". Instead of
  correcting the comment down to what the code did, `powerSeal()` now
  actually traps focus, so the sentence is true. That is 96 lines of the
  change.
- **502 was verified by execution, not by reading.** There is no `node` on
  this machine, but `cscript //E:JScript` is a real ES5 engine, so the boot
  chain was driven through a virtual clock harness: 22 assertions including
  the exact ESC-within-1.6s repro, and five deadlock probes for the risk the
  new guard introduces. It boots exactly once in all of them.

Checked mechanically afterwards, across all eleven files: every file parses
clean under that same ES5 engine, zero arrow functions, zero `let` or `const`
declarations, and every file is 100 percent ASCII, so no em dash or en dash
got in.

**Still open, and none of them are code:** testing on the two MacBooks needs
the two MacBooks, the `SERVICES` inventory is a decision rather than a
defect, and the feature ideas below are features.

One thing found and deliberately not fixed: `js/sysinfo.js` declares
`var SYS_QUICK_KEY` twice, at lines 302 and 314, with identical values. It
predates this round, `var` redeclaration is a no-op, and it was not on the
list, so it is recorded here rather than quietly changed.

### The six that break something

- [x] **`bootFrom()` can run twice and boot the machine twice.**
      `js/firmware.js`. `handOver()` guards on `chosen`, and `chosen` is set
      only by the network branch, so the CD/DVD and Removable Devices
      fallbacks sit for 1.2 to 1.6 seconds with the keyboard still attached
      and no guard set. Press ESC in that window and a second boot chain
      starts on top of the first: `opts.done()` fires twice, `typePost()`
      interleaves two PXE scrolls into one array, and the spinner slots point
      at the wrong rows.
      - Repro: F12 at the splash, arrow to Removable Devices, ENTER, then ESC
        within 1.6s. Also reachable with no F12 at all by putting CD/DVD
        first on the Boot page.
      - Fix: a re-entry guard in `bootFrom` itself, and `clearAll()` before
        the fallback timers, not only inside the hard disk branch.

- [x] **The DOS editor tells you nothing could have been saved, when it just
      lost your typing.** `js/dos.js`. `editExit()` prints the C: wording for
      a G: file when nothing was saved with F2. That message is printed at
      exactly the moment it is most wrong.

- [x] **`TYPE` on G: leaves "Reading ..." on screen.** `js/dos.js`.
      `printed.pop()` removes the blank line `submit()` pushed, not the
      progress line, so every successful read keeps a stale message above it.
      Fix: remember the index rather than popping.

- [x] **A failed `CD` moves you to another drive.** `js/dos.js`. `cmdCd()`
      switches drive before validating the rest of the path, so
      `CD G:\NOSUCH` leaves you on G: root having lost where you were. The
      captured `var here = drive` is never read, which is the tell.

- [x] **An in-flight `dosGet()` can repaint the DOS screen after DOS has
      exited.** `js/dos.js`. The callbacks never check `done`, so a slow
      response lands on top of the firmware's boot screen.

- [x] **Three places tell the visitor the logo screen is on every boot.** It
      is not: a first boot and any boot with Quick Boot disabled show the
      POST text instead. `js/firmware.js`, and the two help screens.

### The Pages 98 Shut Down dialog

- [x] **The accelerator handler destroys Escape.** `js/desktop.js`. It
      assigns `win.onkeydown`, overwriting the handler `win98Open` installed,
      so the dialog can no longer be dismissed with Escape.
- [x] **Stand by does not close the dialog.** The comment says the `return`
      closes it. `win98Open` ignores an action's return value.

### Comments and documentation that are now false

This project treats a lying comment as a bug, and a lot of behaviour moved.

- [x] `README.md` still documents the pre-change boot order, cites a line
      number that does not do what it says, and denies behaviour that exists.
- [x] `TESTING.md` section 1 describes a screen order that cannot happen.
- [x] `js/boot.js` has a stale `SPLASH_MS` comment block that survived above
      its replacement, and a comment above the firmware handover still saying
      the chain is POST then F12 then the network boot.
- [x] `js/firmware.js`: a comment claims Restart uses `?boot` and therefore
      always gives the full sequence. Restart uses `?restart`.
- [x] `js/firmware.js`: two comments claim MS-DOS is reachable only from the
      F12 menu. The boot order reaches it, and so does Restart in MS-DOS mode.
- [x] `js/firmware.js`: a block still says nothing in Setup changes anything.
- [x] `js/sysinfo.js`: the paragraph naming which exits force a full boot
      names the wrong flag for three of the four, and one of them no longer
      reboots at all.
- [x] `js/dos.js`: the header argues the partition must not have a filesystem
      or a text editor. It has both. The command count is wrong.
- [x] `js/dos.js`: the comment justifying ESC without a confirmation says
      there is nothing to save, which stopped being true with G: editing.
- [x] `js/main.js`: two comments still describe a setting called "1st Boot
      Device" whose value is an operating system.
- [x] `js/power.js`: the comment claims the off screen puts the page behind
      it out of reach, "Tab included". A z-indexed overlay does not remove
      anything from the tab order. Either make it true or stop saying it.
- [x] `js/settings.js`: the column key documents `help` as the BIOS help
      pane, but no screen renders a setting's own help array.

### Small

- [x] `README.md` spare-icon paragraph is two spliced lists, and four of the
      icons it calls spare are now used.
- [x] `css/chrome.css` focus-ring comment counts three `outline:none` rules
      and names a stylesheet that no longer exists.
- [x] `js/boot.js` splash timing comment quotes four seconds and 1.8 seconds
      under reduced motion. Both numbers are wrong.
- [x] `closeSetup()` and `closeMenu()` restore the badge on `SPLASH` (the
      mode) rather than `splashOn` (what is actually on screen), so leaving
      Setup after ESC has revealed the POST hides the POST again behind the
      badge for a beat.

### Still open from before this round

- [ ] Testing on the two MacBooks. `TESTING.md` is the list.
- [ ] The `SERVICES` inventory decision.
- [ ] The 22 feature ideas below.

## Ideas, chosen from 53

Generated on 21 August 2026 by seven lenses proposing independently, then
scored out of 30 by three judges: one for whether it can really be built
under the constraints, one for delight per hour, and one for whether it
belongs in this machine rather than just being a feature. 53 proposed, and
these are what came out on top.

- [ ] **THESE ARE ON THE LIST** Chosen on 21 August 2026. Not ordered by
priority beyond the grouping, so take whichever one you fancy: they are
deliberately independent of each other.

Every entry names the files it would touch, and those file claims were spot
checked against the tree rather than taken on trust. One number in the list
as generated was wrong and has been corrected here: the button archive is
6,781 images, not 6,784.

Build **Lost clusters** first. It is one evening in two files you already own, it needs no new page, no stylesheet and no change to tools/pages.py, and it is the only thing on this list where coming back next month is different because of what you did last month. SCANDISK currently finds nothing, which is the one thing on that partition that is inert. Make it find the machine's own past instead.

#### An evening

- [ ] **Lost clusters** SCANDISK's first run reports lost chains and asks "Convert lost chains to files?". Say yes and FILE0000.CHK appears in DIR, dated today, and is still there on every future visit; TYPE FILE0000.CHK gives a screen of garbage with one readable strip through the middle, a line from an AUTOEXEC that is not the current AUTOEXEC, a sentence from a NOTES.TXT written by somebody who does not think that way any more. Later scans turn up one more each, up to about six, numbered the way DOS numbered them. The fragments are a fixed array in the file, so every visitor's disk accumulates the same history rather than random noise, and the partition stays almost empty, it just stops being frozen.
Files: js/dos.js (cmdScandisk, cmdDir, cmdType, DOS_DIR, DOS_FILES), js/store.js.

- [ ] **Restart in MS-DOS mode** Replace the Shut Down box's two buttons with the real Windows 98 radio group: Shut down, Restart, Restart in MS-DOS mode. Shut down blacks the screen and prints "It's now safe to turn off your computer." in amber in the VGA face, alone on the tube, with a quiet press any key arriving three seconds later so it is not a trap. The third option prints the handover line and gives the whole screen to the DOS partition, which is already written, using the same detach, dosRun, done callback dance js/firmware.js performs around line 358.
Files: js/desktop.js (appShutdown), css/desktop.css, tools/pages.py (add js/dos.js and css/boot.css to the desktop tuple near line 277), fonts/vga-9x16.woff.

- [ ] **Abort, Retry, Fail?** At the C:\> prompt, A: or DIR A: prints "Not ready reading drive A" then "Abort, Retry, Fail?" and waits for a single key. R grinds and fails again, forever, exactly as it did. On Pages 98 the same absent floppy is visible from the other side: My Computer gains a drives view, and double clicking A: makes the seek noise and raises "A:\ is not accessible." over "The device is not ready." NOTES.TXT already says the backup is a box of floppies in the cupboard, so the drive is written into the fiction and is simply not there yet.
Files: js/dos.js (dispatch near line 782, plus the single-key mode the editor's handler at line 641 already models), js/sfx.js (new sfxSeek built from the cached sfxNoise buffer through the sfxTick filter chain), js/desktop.js (appDrives, on appRecycle's .w98field markup).

- [ ] **You are not alone on this box** `who` and `w` show two sessions: you on pts/0, and root on pts/1 from a LAN address, idle four minutes, running top. `finger root` prints the real layout with a Plan file, closing on a line admitting this user is invented, in the voice /home/xander/README already uses. Then once per session, unannounced, somewhere between forty and a hundred and twenty seconds in, a broadcast from root cuts across whatever you were typing, with your half typed command still sitting underneath because js/tty.js keeps input in the field rather than the buffer. The motd on that partition says nobody else is looking, and that sentence is waiting to be contradicted exactly once.
Files: js/shell.js (shellDefine for who, w, finger, write; guard on tty.stage() === "shell"), js/sfx.js (sfxBell), js/tty.js (typeOut for root's reply).

- [ ] **The battery is flat** Once the machine's own recorded first boot is more than a set number of days old, the POST starts printing "CMOS Battery Low", "CMOS Checksum Bad", "Press F1 to Run SETUP", and Setup's Main tab shows System Date as 01/01/2003, which is the date XANDERS.TXT says the disk was last touched. Load Optimal Defaults clears it for that visit and it is back the next time, because the battery is still flat. The trigger is real elapsed time, not a random draw: the machine gets old because it is old. Make it mutually exclusive with the keyboard gag in js/boot.js so two halt screens never stack.
Files: js/sysinfo.js (new cmosFlat() next to sysFirstBoot, over xanders_firstboot), js/firmware.js (setupPanels, the POST step chain), js/boot.js.

- [ ] **dmesg, reporting the visitor's real hardware** A kernel ring buffer with correct bracketed monotonic timestamps, opening on the init lines the Server Core boot already prints, then getting specific: CPU count, framebuffer resolution and pixel ratio, GPU string, platform, timezone, all read live and phrased as kernel messages, closing on mounting the wallpaper volume with a file count and Authelia binding its port. Push a line only when the value is actually present, so nothing ever reads undefined on Safari 7, where a shorter list is correct rather than broken. `dmesg -w` follows the buffer on the same takeover plus setInterval shape as top. The POST reporting real hardware is the best trick in the chain; this is that trick at the prompt, where somebody has chosen to keep typing.
Files: js/apps.js (beside top and ping), js/sysinfo.js (sysPlatform, sysBrowser, sysRenderer, sysMachine, sysZone, sysClock), js/tty.js.

- [ ] **Aero Peek, and a right click on the wallpaper** The whole site is glass over 350 wallpapers and there is currently no way to move the glass. Add a ridged Show Desktop bar at the right end of the footer: click it and every panel drops to a transparent wireframe with a white outline, exactly like Windows 7, and the picture underneath is finally visible. Second, a right click anywhere on the background gives a small Aero menu: Next background, Hold, Personalize, Show desktop, Restart, every entry calling something that already exists. Bind the context menu at document level and check the target, since the wallpaper layers sit at z-index -3 and -4.
Files: css/style.css (new html.peek block, honouring html.motion-off the way section 24 does), tools/pages.py (SHELL and SHELL_HOME footer), new js/aeromenu.js modelled on dtDesk.oncontextmenu at js/desktop.js:1228, js/wallpaper.js (step, startTimer, wallPaused).

- [ ] **Hardware configuration has changed** On the first boot the machine stores a small fingerprint of the browser reading it: screen size, colour depth, renderer, platform, timezone. Every later boot compares, and if anything moved the POST prints "Warning! Hardware configuration has changed since last boot." with the diff under it, "Adapter : was Intel HD Graphics 3000, now Apple M2", then the F1 prompt. It never fires on a first visit, so it is never noise. Keep it to one diff line: THE-MACHINE.md records the POST was cut from 42 lines to 38 to keep the American Megatrends badge on screen, and the warning plus prompt already spends most of the headroom.
Files: js/sysinfo.js (new sysFingerprint and sysConfigChanged over xanders_hwcfg), js/firmware.js (one step before the footer), js/store.js.

- [ ] **Start, Run, and "Cannot find the file"** A Run item above the Shut Down separator opens the real dialog, running figure icon, the full "Type the name of a program, folder, document, or Internet resource" sentence, an Open combo, and OK, Cancel, Browse. notepad, mspaint, pbrush, winmine, control, sysedit and telnet open what already exists; everything else gets the exact message box, "Cannot find the file 'sol' (or one of its components)." The combo remembers what you typed, most recent first, because the MRU list is the half of Run everybody used and nobody rebuilds. Browse points at a drive that is not there. Take the Run icon from the existing 98 set: tools/icons.py builds the Vista icons from an external archive and cannot supply it.
Files: js/desktop.js (appRun, RUN_MAP, the APPS registry, dtRun), js/win98.js (win98Open, dialog mode with icon "!"), js/store.js (xanders_98_run), tools/content/desktop.html, css/desktop.css (.w98combo).

- [ ] **The POST code display** A small amber pair of seven segment characters, bottom right of the boot layer, deliberately outside the 80 column text field because on a real board it is on the board rather than on the screen. D0 while the CPU initialises, C1 for the memory count, 2A for PCI, 03 for the keyboard controller, A0 at handoff. It never resets, so it rests on whatever screen you are sitting on, and if the machine halts on the keyboard gag or a bad checksum it stops on the code for that fault, which is free storytelling. Drawn with box shadows and the existing monospace face, no webfont and no image. Note that the element has to go into both tools/content/bootlayer.html and tools/content/private.html, which carries its own postLines div.
Files: js/firmware.js (one postCode call per existing step), js/boot.js, css/boot.css, tools/content/bootlayer.html, tools/content/private.html.

- [ ] **Ctrl+Alt+Del and the Close Program box** The real grey list of everything running, each open window by title, then Explorer and Systray, with End Task, Shut Down and Cancel. End Task twice on the same window gets the This program is not responding follow up first. End Task on Explorer takes the taskbar and every desktop icon away, leaving bare wallpaper for about eight seconds before it returns with the startup sound, which exposes that the desktop is a shell running on something. Pressing the combination again while the box is open reboots to the boot menu. It works on the two target MacBooks, where Ctrl+Option+Delete means nothing to macOS, but Windows swallows it, so give the box a second door: a Start menu entry or a taskbar right click.
Files: js/desktop.js (dtCloseProgram, the existing document keydown reading ctrlKey, altKey, keyCode 8 or 46, appShutdown at line 888), js/win98.js (win98Windows at line 36, win98Open with raw:true), css/desktop.css (hiding .dt__bar and .dt__icons), js/sfx.js.

- [ ] **Desktop themes, Hot Dog Stand included** appDisplay already prints an Appearance heading with nothing under it. Fill it with the real scheme names, Windows Standard, Brick, Desert, Eggplant, Maple, Plum, Rainy Day, Rose, Slate, Storm, Wheat and Hot Dog Stand, each repainting title bars, window faces, bevels, taskbar, Start menu and menu bars instantly and remembered between visits. Body class scoped overrides are exactly the technique the era used, so no custom properties are involved. Budget more than a dozen selectors per scheme: there are around 136 colour declarations across css/win98.css and css/desktop.css before paint98 and saver98. Add the class rather than setting body.className, which would clobber the pages98 class that scopes the whole font.
Files: new css/themes98.css, tools/pages.py (desktop stylesheet list near line 288), js/desktop.js (appDisplay at line 742, a key beside DT_WALL_KEY), js/store.js.

#### A weekend

- [ ] **The 88x31 Archive** There are 6,781 rescued buttons on this disk and the site shows ninety of them. Build a browsable wall of the whole archive, two hundred at a time, with a filename filter, a shuffle and a permalink for any single button. The part that gets forwarded: most filenames still carry the GeoCities neighbourhood they were scraped from, like Area51_1269_images_xvtnow1.gif, so every button can offer a Wayback search for the dead page it used to live on. Do not flip LIST_ALL_BUTTONS, the names really are about 237KB; write them to their own generated js/archive.js with the same blob() trick and load it on one page only, the way js/gallerypage.js is loaded on wallpapers.html. pages/lostmedia.html already writes the essay about this archive and then shows none of it.
Files: tools/manifest.py (new js/archive.js output), tools/pages.py (PAGES row, NAV row, DESCRIPTIONS, extra scripts), tools/content/archive.html, new js/archivepage.js (galLazy copied from js/gallerypage.js, shuffle from js/wallpaper.js), css/style.css section 19.

- [ ] **The machine remembers what you did to it** One capped ring buffer of events written by things that already happen: an unclean shutdown, which OS you picked at NTLDR, Shut Down from the Start menu, a Minesweeper win, a picture saved in Paint, a guestbook entry composed, a failed login, a completed boot. Surface it as an Event Viewer window on Pages 98 with the real three column layout and the i, ! and x icons, and as one line on the POST: "Last shutdown : Pages 98, unclean, 00:03:11 ago", which is the sentence that makes somebody realise the machine was running while they were gone. That POST line must be exactly one row. Every writer is a single line dropped into code that already runs, and logAll must always return an array even when the stored value is half written.
Files: new js/eventlog.js, js/store.js, js/chkdsk.js (CHK_KEY), js/osmenu.js (OS_KEY), js/desktop.js (appShutdown, a new APPS entry on dtWindow), js/minesweeper.js, js/paint.js (ptFilesWrite), js/guestbook.js, js/tty.js, js/sysinfo.js (sysBootSeen), js/firmware.js, tools/pages.py (three script blocks).

- [ ] **view: 350 wallpapers as phosphor ASCII** `view` draws a random wallpaper into the tube as ASCII at the measured column width, using a ramp from space through .:-=+*# to @, with the darkest run in tty__dim and the brightest in tty__hi so the phosphor has depth. `view landscapes/moraine_lake` names one, `ls /var/wallpaper` lists the seven categories, `slideshow` holds the screen and changes every eight seconds until q. Rendering is an offscreen canvas, drawImage down to tty.columns() wide by about half that tall because a character cell is twice as tall as it is wide, then getImageData, same origin so nothing is tainted. The green wing currently cannot see a single pixel of the largest thing on this machine.
Files: new js/ttyart.js, tools/pages.py (add js/manifest.js to the private wing script list near line 297, it is genuinely missing), js/tty.js, js/apps.js (the run coalescing close() helper in matrix at line 418), img/wallpaper-small.

- [ ] **CMOS that actually holds** Make the Advanced and Boot tabs real: Quick Boot, Halt On, Sound, Monitor effect, Animation, and Boot Device Priority, which reorders the NTLDR menu itself because osDefault() already reads xanders_lastos. Every one of those settings exists today with its own switch in its own room, so the machine can disagree with itself about its own settings depending on which door you came in. Then give the Exit tab Load Setup Defaults, which really does clear every xanders_ key and reload, and Discard Changes: clearing the CMOS resetting your whole computer is a joke that is also completely accurate. Two things are additions, not reuse: drawSetup has no row cursor at all, only tab inversion and heading brightening, and Setup rows are not clickable today, so the data attribute click path has to be built the way the DOS boot menu was.
Files: js/firmware.js (setupPanels, drawSetup, the setupOpen key branch near line 777), js/store.js, and the existing keys in js/sysinfo.js, js/sfx.js, js/motion.js, js/crt.js, js/osmenu.js.

- [ ] **The 88x31 forge** Type your site name, pick a palette, and a canvas draws you a real Aero glass 88x31 button on the spot: vertical gradient, glass highlight across the top half, one pixel white border, your text in the middle, with a 1x and 4x preview and the paste snippet underneath. Port the recipe out of tools/mybuttons.py, VARIANTS and all, so the four buttons you ship and the ones a visitor makes are drawn by one recipe and can never drift. Saving is right click, Save Image As, and the page says so plainly, because Safari 7 has no download attribute. This would be the first script pages/buttons.html has ever loaded, so guard every element lookup the way js/main.js does.
Files: new js/buttonforge.js (from tools/mybuttons.py), tools/content/buttons.html, tools/pages.py (buttons row of PAGES), js/guestbook.js (gbMailto for the Send me yours handoff), css/style.css.

- [ ] **Dial-Up Networking, with the handshake** A Connect To box with a connection called xanders.page, a user name filled in, a number and Save password ticked. Connect gives Dialing with the touch tone pair of each digit played in turn, then the carrier handshake, then Verifying user name and password, then Connected at 49,333 bps, and the dialog collapses to two flashing monitors in the tray with Status and Disconnect on a right click. The site has a Web Audio synth and has never once made the noise that defined being online in 1998, and every ingredient is already in js/sfx.js. Do not store the connection state: a modem still connected when you come back tomorrow is not a modem. Under reduced motion, hold the tray lights still and keep the sound.
Files: new js/dialup.js, js/desktop.js (appDialup), js/sfx.js (sfxTone at 121, sfxNoise at 194, sfxStack at 289), tools/content/desktop.html (.dt__tray), css/desktop.css line 482, tools/pages.py (desktop script block near line 268).

#### A big one

- [ ] **lynx, the text mode browser** `lynx about` or `lynx /pages/gallery.html` fetches that real page off the same server and renders it as text: headings underlined, paragraphs wrapped to the measured column width, links numbered [1] [2] [3] in bright phosphor, type a number to follow, b to go back, q to quit. It is browsing the actual public site, so the 20 pages tools/pages.py already builds are the content and nothing new gets written. Server Core has no graphical shell, so a text browser is not a novelty, it is the only way that room can see the web, and what it sees is this machine's own other partition. Strip script, style, link and img by regex before assigning into a detached div, or WebKit fires the image requests anyway; on status 0, which is what a file:// open gives, print the honest "lynx: Unable to connect to remote host."
Files: new js/lynx.js, tools/pages.py (private entry script list, lines 296 to 313, after apps.js), js/tty.js (print, esc, columns, takeOver, release, setPrompt), js/shell.js (shellDefine, shellPromptText), css/tty.css (tty__hi).

- [ ] **C: is one disk, seen through four windows** There are three unrelated invented filesystems today, DOS_DIR and DOS_FILES at js/dos.js:49 and :63, FS at js/shell.js:62, and a single Notepad buffer under xanders_98note at js/desktop.js:1342, and none of them knows the others exist. Replace the overlap with one volume: NOTES.TXT in the DOS listing, ~/notes/todo.txt in Server Core and a My Documents window on Pages 98 all read and write the same bytes. Type a line in Notepad, shut down, boot MS-DOS behind F12, TYPE NOTES.TXT, and your line is there with DIR reporting the new byte count. Canonical form is a lowercase POSIX path holding a string, shipped defaults stay as literals so an empty machine still has content, and only the differences go to storage. Two consequences to price in: everything shared lands in one key that Safari 7 in a private window silently refuses, which store.js is built to survive, and giving the MS-DOS Editor a real File > Save turns its honest "it does not save" notice into a write that DIR must resize.
Files: new js/disk.js (diskRead, diskWrite, diskList, diskStat over js/store.js), js/dos.js (DOS_FILES becomes a lookup, the EDIT exit prompt), js/shell.js (shellResolve serving /home/xander), js/desktop.js (appDocuments on dtWindow, appNotepad), tools/pages.py (HOME_SCRIPTS near 835, SHELL_98 near 269, SHELL_BARE near 297).

- [ ] **The floppy somebody left in the drive** Booting Removable gives an A: prompt with four files on a 1.44MB disk: AFUDOS.EXE, H410MA.ROM, FLASH.BAT and a README from whoever made it. FLASH.BAT plays the real AFUDOS screen, reading the file, erasing the block, the write bar crawling across, verifying, DO NOT TURN OFF THE SYSTEM across the middle, then reboots through ?boot, and from then on the banner, the Setup Main tab and the Boot tab all report the new revision, permanently. Two of the five boot devices are still drawn, selectable, clickable and dead, and this one does the thing only firmware can do: it changes the machine itself. Closing the tab mid flash is already caught by the dirty flag, because the DOS session runs before finish() clears it, so the next boot recovers from the boot block, which is what those boards genuinely did.
Files: js/dos.js (dosRun with an options.drive of "A", or a sibling js/floppy.js), js/firmware.js (DEVICES[2], chooseDevice at 337 to 368, SERVER.bios at 98 becoming a cmosGet with "1401" as its default), js/chkdsk.js, tools/check.py.

---

### Round eight: auditing the documentation against the code

The backlog audit in round five found eight false claims, and updating the
README an hour later found its description of `tools/check.py` listing four
lanes when there were nine. So the same treatment was given to the prose:
1,511 lines of README plus ROADMAP and ASSETS, six readers over six slices,
every reported error handed to a second reader told to refute it.

**347 claims checked, 55 reported, 52 confirmed.** Roughly thirty distinct
corrections after merging duplicates. The refuters killed three, which is the
same one-in-eighteen false alarm rate as the backlog audit.

- [x] **A real leak, and it was mine.** A paragraph in `README.md` named the
      two tunnel products on this machine, in the course of explaining that
      they were being kept off the site. Worse, the comment in
      `js/firmware.js` that removed them said the details lived in
      THE-MACHINE.md, "which is not deployed". **That was false.** The repo
      root IS the web root, as the first line of the README says, so every
      markdown file here answers with a 200, along with `.gitignore` and
      every script in `tools/`. Checked with curl rather than assumed. The
      names are gone, the false sentence is corrected, and the decision about
      whether the docs should be served at all is now an open item above
      rather than an accident.
- [x] **The machine was still documented as having two operating systems.**
      The front door diagram and the NTLDR transcript both listed Pages Web
      and Pages Server Core, with Pages 98 missing from between them, and the
      banner comment in `js/osmenu.js` said "Two entries" in as many words.
      The MS-DOS partition was not in the diagram either.
- [x] **`js/buttonpage.js` said it builds `pages/buttons.html`.** It builds
      `pages/collection.html`. The error was in the file's banner, which
      `tools/layout.py` copies verbatim into the README, so the README was
      regenerating it every run. Fixed at the source.
      - The same confusion ran through a whole README section, which
        described one page in five parts when it was really the first part of
        `buttons.html` and all four parts of `collection.html`. Rewritten as
        two pages.
- [x] **Counts, all measured off the disk rather than taken from the audit.**
      Fifteen scripts is sixteen. Fifteen pages is twenty. Eighteen site map
      entries is twenty. Fifty five linked buttons is ninety. Thirteen
      exo.pet gifs is fourteen. 6,781 archived buttons is 5,565, and 47MB is
      48MB. The icon set is 192KB across 48 files, not 7KB. Fifteen sidebar
      icons is twenty, and three of the four "spares" listed are now in use.
- [x] **Wallpaper sizes were wrong by half and then overtaken.** "1920x1200
      averaging 560KB" appeared twice; the real mean is 1124KB. It is moot
      either way now, because the site serves `img/wallpaper-small`, which is
      1600x1000 at a mean of 220KB.
- [x] **Things last night invalidated.** The POST transcript still showed an
      Array Configuration line it no longer prints, and two paragraphs
      described a Terminal Configuration block that was removed. The terminal
      was documented with five text shadow layers when it has three, and
      three focusable inputs when it has four.
- [x] **`cmd` survived in the command list and in the quoted motd**, weeks
      after the Command Prompt was deleted. A command list is exactly where a
      deleted command lives longest. The list is now complete and correct
      against `shellDefine` in both files: twenty six commands and seven
      programs.
- [x] **`css/login.css` does not exist and never has.** The reset it credits
      is in `css/tty.css`.
- [x] **The Fonts section and three ASSETS sections were instructions for
      work already done**, describing gradient squares in a sidebar that has
      twenty real icons, a marquee that was removed, and a `site/img/` prefix
      that has never existed. And the note explaining why `woff` is listed
      before `woff2` describes the opposite of what the file now does, for a
      reason that was wrong: an unknown `format()` is SKIPPED, not fatal.
- [x] **The 404 page does not share `SHELL_HOME`.** It has `SHELL_404`, which
      carries no boot placeholders rather than empty ones.
- [x] **The workflow itself had a bug worth keeping.** Its first run hit a
      usage limit, all six readers died, and it returned "the documentation
      checks out" because the findings list was empty. It was empty because
      nothing had been read. Silence from a checker has to mean "I looked and
      found nothing", never "I did not look", so it reports the failure now.

- [x] **The keyboard section claimed "exactly one interception"** and there
      are four: select all, extend the selection, the running program hook
      that lets Ctrl+C interrupt `sl`, and Ctrl+M for the sound. All four are
      deliberate, but four is not one. It also said arrows are taken only
      during 2FA; the password game takes them too.
- [x] **The two phase network boot was listed in the wrong order.** LOAD
      comes first: `sequence()` shows the splash and only calls `typePost()`
      four seconds later. The list had PXE first, which is what you would
      guess from the name and not what happens on screen.

### Round seven: walking the boot chain end to end

- [x] **The guestbook form overflowed the 980px column**, found by measuring
      the page at 1280x800 rather than looking at it. Two bugs from one
      cause: the 100px label offset was on the FIELD'S MARGIN.
      - The textarea carried `width: 100%`, and a percentage resolves
        against the parent, so 100% of the row plus a 100px margin reached
        100px past the row. It ended at 1087 where the column ends at 1122.
      - The two text inputs had `width: auto`, which fills the line for an
        ordinary block box but NOT for an input: an input is a replaced
        element, so auto means its intrinsic size. They sat at the browser
        default of 143px beside a 738px textarea.
      - The offset is the ROW'S PADDING now, so `width: 100%` means the
        space that is actually left. All three fields are 638px and
        identical, and the form still validates and signs.

And then the thing that actually unblocks the top of this file:

- [x] **`TESTING.md`**, a written pass for the two MacBooks. See the Phase 0
      item above for why the shape of it matters more than the content: an
      obligation with no edges never gets started.
- [x] The README now points at all four root documents, and its description
      of `check.py` was rewritten, because it still listed four lanes when
      there are nine.
- [x] Confirmed by reading `fonts/vga-9x16.woff` rather than guessing: the
      face carries all eight box drawing and block glyphs that DEFRAG and
      the editor draw with. So if those come out wrong on a Mac it is the
      font failing to LOAD, not the font lacking the character, which is a
      different fix and worth knowing in advance.

Then a resolution pass at 1280x800 and 1440x900, the two target MacBooks,
which caught two things in the editor built an hour earlier:

- [x] **The editor was double spaced and its field ran twice as wide as its
      frame.** Both from one wrong `display: block`. `js/dos.js` joins rows
      with `<br>` like every screen in that file, and a block breaks the line
      too, so every row broke twice: 658 pixels of screen where 22 single
      rows should be 337. A block also fills its container, so the blue ran
      1219px against a 78 character frame that stops at 590px.
      `display: inline-block` fixes both. Worth recording that I had already
      LOOKED at a screenshot of this and read the black gaps as part of the
      frame. Measuring it is what caught it.
- [x] **The skip link was `position: absolute`.** It is the first tab stop,
      so you normally reach it at the top of the page and never notice. Shift
      Tab from halfway down a long page is the case that was broken: absolute
      positions against the document, so it would have appeared at the top of
      the PAGE, off screen. Fixed, and lifted above the CRT overlay, because
      a control whose whole job is making the page usable should not be the
      one thing showing through a filter.
- [x] **`tools/check.py` checks the 350 wallpapers and their thumbnails.**
      This was the one class of broken path nothing could see: no wallpaper
      path exists in any file, because they are assembled at runtime in
      JavaScript from a folder name and a filename in `js/manifest.js`. So
      the largest thing on the site and its best page had no check at all.
      - It matters more since the site started serving `img/wallpaper-small`.
        That folder is gitignored, correctly, and it is now LOAD BEARING: a
        deploy that copies only what git tracks would put up a site whose
        every wallpaper is a broken image, and nothing would have caught it
        before a visitor did.
      - It reads the folder name out of the four files that build the paths
        rather than assuming it, so if those paths move again the check
        follows them.
      - Verified by hiding one wallpaper and one thumbnail: both reported by
        name, exit 1, count down from 700 to 698, and clean again once they
        were put back.

The whole chain was walked from a cleared machine state, which had not been
done since round three changed most of it: cold boot, POST, ENTER, the PXE
failure and retry, DHCP, TFTP, iPXE, the hardware probe, the operating system
menu with its countdown, and a boot into Pages 98. No errors anywhere, and
the POST held its badge the whole way.

Two things came out of actually walking it rather than reading it:

- [x] **The MS-DOS partition could not be reached with a mouse.** The POST
      footer was made clickable in an earlier round precisely so that DEL and
      F12 would not depend on a function key. That gets you as far as opening
      the boot device menu, and the menu then wanted arrow keys and ENTER, so
      the door led into a corridor with no handle. It matters more here than
      anywhere else on the site, because the MS-DOS partition is reachable
      through that menu AND NOWHERE ELSE: a visitor who cannot work the arrow
      keys could not reach a whole operating system.
      - Each row is clickable now. Click to select, click the highlighted row
        again to boot it, which is the convention `js/osmenu.js` already uses
        one screen later, and which means a single stray click can never
        start an operating system.
      - The ENTER branch was 73 lines inline, including the whole DOS
        handover with its `detachKeys` dance. It is `chooseDevice()` now, so
        the key and the click run the same code. Two copies would drift, and
        the half that drifted would be the mouse half, because somebody
        testing with the keyboard never takes that path.
      - Verified by mouse alone through every branch: Hard Disk boots MS-DOS,
        CD/DVD prints the real "Reboot and Select proper Boot device" failure
        and falls back to the network, and the keyboard still works.
- [x] **The boot said the same thing twice.** Round four added a "Your
      Terminal" block to the POST, and the network boot ends with "Probing
      host hardware" printing the same facts and more of them about twenty
      seconds later. The POST block went, because the probe block belongs to
      a screen that scrolls and the POST is a fixed screen that pins to the
      bottom, where four lines is four lines of headroom the badge needs. The
      POST is 38 lines now, and `js/firmware.js` no longer calls `sysReal()`
      at all.

### Round six: the keyboard, and the switches nobody could reach

- [x] **Every external link on the site was checked, and all 78 hosts are
      alive.** `python tools/check.py --links`, run 21 August 2026. The
      README asked for this to be re-run occasionally and it had not been
      since the pass that caught Angelfire and Tripod dying in April.
      - It found two failures and BOTH were faults in the checker, not on
        the site. `www.adobe.com` hangs for the full timeout on every
        attempt while `get.adobe.com` answers instantly and every other host
        replies in under a second, which is a bot filter rather than a dead
        host, so timeouts are a note now instead of a fault.
      - The second was better: a host called `xanders.page
`, reported dead
        every run. `js/guestbook.js` contains `"...at https://xanders.page
"`
        and in a SOURCE file that `
` is two characters, neither of which
        the URL pattern excluded, so the trailing escape became part of the
        hostname and sailed past the rule that skips this site's own domain.
        A URL never ends in a backslash, so cutting at the first one fixes
        the whole class.

- [x] **Pages 98 had the CRT filter and no way to turn it off.** `js/crt.js`
      hangs the monitor, sound and motion switches off `.wallpanel`, the
      sidebar panel every other page carries. The 98 desktop has no sidebar,
      so it took the scanlines over eleven pixel text, plus the sounds and
      the animation, with no control over any of it. The one partition most
      in need of a monitor switch was the only one without one.
      - They live in **Display Properties** now, which is where they belong
        on a Windows desktop and which was, until tonight, a window called
        Display Properties containing no display properties.
      - `crtToggle` and `crtOn` had to be exported from `js/crt.js` for this;
        `sfxToggle` and `motionToggle` were already global. The sidebar
        switch now calls the same exported toggle, so the two cannot drift.
      - The window claims `sfxOnChange`, `motionOnChange` and `crtOnChange`
        while it is open and HANDS THEM BACK on close. Each hook holds only
        one handler, so a window that took one and kept it would leave the
        sidebar switches on every other page unable to relabel themselves
        for the rest of the session.
- [x] **A skip link and landmarks**, on the seventeen pages that have a
      sidebar. Measured on the About page: **35 tab stops** sit between the
      top of the document and the first word of the content. On a responsive
      site you would put the content first in source order; this layout is
      deliberately fixed at 980px forever, so source order IS the order and a
      skip link is the whole fix.
      - Off screen and clipped until focused, NOT `display:none`, which
        would remove it from the tab order and produce a skip link that can
        never be reached.
      - Not on Pages 98 or the private wing: both are full screen takeovers
        with no navigation, so there is nothing to skip and nowhere to go.
- [x] **The terminal no longer traps the keyboard.** Two separate bugs.
      `css/tty.css` claimed `z-index:-1` and `pointer-events:none` kept the
      inactive fields out of the tab order; neither property does that, so
      Tab at the login prompt moved the real caret into the hidden PASSWORD
      field while the drawn caret stayed put. `focusField` sets `tabIndex`
      now, which is the thing that actually does it, and not `disabled`,
      which would break password manager support. Separately `js/shell.js`
      swallowed Tab on a DOCUMENT level keydown whenever the stage was
      "shell", which is the whole time a dialog is open over the tube, so
      Tab was dead page wide with nothing on screen explaining why. Scoped
      to `tty.atPrompt()`.
- [x] **Pages 98 is usable from the keyboard.** All nine desktop icons were
      tab stops that announced themselves as actionable and did nothing:
      opening was on `ondblclick`, and there is no double click on a
      keyboard. Enter opens now, which is what Windows did. `js/win98.js` had
      no key handling at all and never moved focus, so an opened window left
      the caret at the top of the page and the Shut Down dialog could not be
      dismissed; Escape closes the window and focus moves into it on open.
      The white ink focus list in `css/chrome.css` was written for the Aero
      side and never grew, so the blue screen's four exit links carried a
      black dotted ring on `#0000aa`.
- [x] **A Safari 7 lane in `tools/check.py`.** Constraint 1 of this project
      is its hard floor and was the only rule with no mechanical check, while
      the rule about dashes had a thorough one. It scans `js/` AND the inline
      scripts of the generated pages, because the head scripts live as Python
      strings in `pages.py` and are the first things that run. Verified by
      planting seventeen real violations and confirming all seventeen are
      caught with exact line numbers.
      - Its first version had two bugs of its own, both found by reading the
        output rather than trusting it. Stripping comments replaced them with
        a single space, which collapsed the newlines inside them, and this
        project is mostly forty line banners, so every reported line was tens
        of lines out. And the `-webkit-` twin check compared per BLOCK, which
        is wrong for keyframes, where the prefixed version is a whole
        separate at-rule by design: fifteen confident reports, every one
        wrong.
      - NOTES ARE NOT FAULTS. Some things cannot be decided by grep, so they
        print under their own heading and do not touch the exit code. A
        command that always exits 1 is a command whose output stops being
        read, which is the same failure the fifteen wrong reports would have
        caused.
- [x] **Pages 98 has its own sound scheme.** It was the partition a visitor
      spends longest in and the silent one, on a machine whose other three
      all beep. Windows open, close, minimise, restore, a menu tick, a
      Critical Stop and a shutdown chord, in sine and triangle rather than
      the square waves everything above them uses, because those are a PC
      SPEAKER and by 1998 the sounds came off a sound card. The error boxes
      in Paint and the guestbook window used `sfxBeep`, the PC speaker, which
      was exactly the inconsistency this fixes.
- [x] **chkdsk's arithmetic works and describes this machine.** It claimed a
      232 GiB volume matching neither the POST nor `df`, and its three
      subtotals came to 269,760 KB MORE than the disk they were on. It is the
      931 GiB mirror now, the same volume `df` reports, with the parts
      summing exactly to the total and the allocation units dividing evenly.
- [x] Smaller: the gallery's meta description promised "art and images" on a
      page whose first sentence says there is none; the front page printed
      `tools/content/index.html` at a visitor, a source path that is not on
      the web server, so anybody who followed it would get the blue screen;
      and the home page description still mentioned the scrolling buttons
      that were removed in round four.

### Round five: auditing this file against the code

The survey in round four turned up three `[x]` items whose claims the code
did not support. Three, found by three different readers who were looking for
something else, is a pattern rather than bad luck, so all 208 completed claims
in this file were checked against the code, and every reported failure was
then given to a second reader told to REFUTE it.

That second pass earned its place: it killed three of the eleven reports as
misreadings, including one that objected to the word "keeps" in a line about
persistence. Eight stood up.

**What the audit found, and what has been done about it:**

- [x] **The `portal` flag was only honoured in one of two places.** This was a
      gap in the round four fix itself, made a few hours earlier. Both no
      fetch branches in `js/authelia.js` return `portal: true`, but only
      `submitPass` in `js/tty.js` checked it. `submitCode`, the second factor
      path, still read `ok:true` as "correct code" and would print ACCESS
      GRANTED for any six digits on Safari 7. Unreachable today, because the
      first factor now hands off before you can reach that stage, which is
      EXACTLY the reasoning that let the original bug live. Fixed, and the
      two halves now match.
- [x] **The wallpaper `aria-label` reached sixteen pages and skipped the
      front page.** Also from round four. `WALLPANEL` in `tools/pages.py` is
      substituted into `SHELL`, which builds `pages/*.html` only. The home
      page is built from `SHELL_HOME`, which has no `{wallpanel}`, and the
      nav rewrite stops at a lookahead for that very block. So the page with
      the most visitors was the one page without the label. All seventeen
      have it now.
- [x] **The updates page still advertised "an NT console".** That page opens
      by inviting you to check whether the site is really being kept current,
      which makes a line selling a deliberately deleted feature the worst
      stale sentence available anywhere on the site.
- [~] **`color`, `tree` and `shutdown -s` were marked done and do not
      exist.** All three were built inside the XP Command Prompt and deleted
      with it in round three. Re-marked `[~]`, built then removed, rather
      than deleted from this file, so the record survives without the claim.
- [~] **The system configuration table was marked done and is not built.**
      Something good exists in its place, but there is no box, no second
      column, and none of the seven fields the item names. See the entry
      itself for the full correction, including that the "Probing host
      hardware" block it promised to replace is still there.
- [x] **A stale comment in `css/boot.css`** justified a white-space rule by
      "the whole drawn box of the system configuration table", a box nothing
      draws. It now names the frames that really do depend on it, drawn by
      `drawSetup` and `drawMenu`.
- [x] **The wallpaper swap note** described a 389MB folder move as still
      pending. It is not pending, it is unnecessary: the paths were pointed
      at the small set instead and nothing has to move.

**The lesson, which is the reason to write this down.** Every false claim
here was written at the moment the work was done, when it was true or nearly
true, and went stale later when something else changed. Two of the eight went
stale within hours, in the same session that wrote them. A backlog is not a
record of what happened, it is a claim about the present, and it decays like
any other. `tools/check.py` cannot check prose, so the only defence is
reading it against the code occasionally.

### Round four: from a survey of the whole site

An eight lens survey of the repository, with three skeptics arguing against
everything it produced. 58 proposals, most rejected. What survived and has
been done, on 21 August 2026, all verified in a browser:

- [x] **The POST fits on one screen again.** Round three took it to 52 lines
      against a screen that holds about 50, and `.boot__post` pins itself to
      the bottom on every write, so the overflow came off the TOP and took
      the American Megatrends badge with it. It is 42 lines now.
      **And the font is `vmin`, not `vw`,** which is the deeper fix. Sizing
      text off the width while the height is what runs out made the row
      count depend on the shape of the window: about 50 on a 16:10 laptop,
      45 on 16:9, and 33 on a 2560x1080 ultrawide, where the old rule gave a
      27px font. It was already clipping before round three added a line to
      it, and there was no way to see that without owning the other monitor.
      Verified at 1280x800, 1440x900, 1920x1080 and 2560x1080.
- [x] **Safari 7 no longer prints ACCESS GRANTED for any password.**
      `js/authelia.js` returned `{ok:true, redirect}` when `window.fetch` is
      missing, meaning to hand off to Authelia's own portal. `js/tty.js`
      reads `ok && redirect` as "authenticated, no second factor owed",
      prints ACCESS GRANTED and calls `window.shellStart`, which takes over
      so the `location.href` handoff never runs. On the PRIMARY TARGET
      MACHINE, any password opened the private wing shell.
      Latent rather than live: `autheliaWhere()` returns "local" on
      localhost and the site is not deployed, so this would have appeared on
      the first day it went up. The no-fetch branches now carry
      `portal: true`, and tty.js routes those to a real navigation.
- [x] **The guestbook accepted `javascript:` URLs.** `gbTidySite` returned
      any string containing `://` untouched, which `javascript://x%0Aalert(1)`
      satisfies, and it went straight into an href. It persisted in
      localStorage and re-rendered on every later visit, on the page and in
      the Pages 98 window, on the origin that draws the Authelia prompt.
      Now an http and https whitelist. Everything else falls through to the
      `http://` branch, which is a safe result rather than an error.
- [x] **Paint's saved images are checked and escaped.** The data URL was the
      one field in that markup not passed through `ptEsc`, and it comes back
      out of localStorage rather than out of memory. It is now verified to
      start `data:image/` and escaped, in Paint and in My Pictures. `dtEsc`
      escapes double quotes now too, which it never did, while most of its
      calls are inside attributes.
- [x] **The head paint script validates what it reads.** It puts a stored
      value straight into a CSS `url()`. It now requires the path to sit
      under the folder actually served, and rejects quotes, brackets and
      backslashes. That also caught a real regression: after the wallpaper
      switch below, anyone with an old path stored would have pulled the
      1.2MB original on the first paint of every page.
- [x] **`tools/serve.py --lan` starts.** `PORT = int(sys.argv[1])` ran at
      module level with no flag filtering, so the one command that opens the
      site to the two MacBooks died on `int('--lan')` before binding
      anything, with a traceback that never mentioned `--lan`. It had never
      worked. All five argument forms tested.
- [x] **`tools/thumbs.py` runs.** Line 21 read `ROOT` before anything bound
      it, so the module died on import with a NameError. It is the only tool
      that was missing the standard root line, and the name it bound then
      shadowed that root, which is how it hid.
- [x] **`tools/cursors.py` cannot destroy `css/cursors.css` any more.** It
      reads from a Downloads folder that is in neither the repo nor the
      backup zip, printed MISSING for each absent file, then called
      `build_css` anyway, overwriting a tracked stylesheet with a valid file
      containing no rules. Every custom cursor on the site, gone, and
      `check.py` passes a rule-less stylesheet because it has no `url()`
      lines to fail. It refuses twice now: no pack, and nothing converted.
- [x] **A failed `pages.py` stops `blog.py` before it publishes.**
      pages.py never called `sys.exit`, so it returned 0 after printing
      MISSING FRAGMENTS; blog.py judged it by that code and had a "FAILED"
      branch that could never fire; and `build_feed()` sat outside the
      guard, so the feed went out anyway. A feed item pointing at an anchor
      that was never built is cached by every reader that fetches it, and is
      the one thing here that cannot be taken back. blog.py also stopped
      swallowing pages.py's output, which was hiding the reason.
- [x] **The site serves `img/wallpaper-small`.** `tools/shrinkwall.py` was
      written to produce it and says why, and nothing had ever pointed at
      its output: six path strings still named the 1920x1200 originals. The
      home page went from about 1.2MB of wallpaper to 125KB. The folder is
      gitignored now, since it was untracked only by luck and one
      `git add -A` would have put 76MB into history forever, with a note
      that it IS load bearing for deployment even though it is not tracked.
- [x] **The wallpapers page lazy loads its thumbnails.** 350 cells all
      carried a real `src`, so opening it asked for 2.9MB in one go, on a
      page most people never scroll to the bottom of. It is 63KB on load
      now, filling in on scroll. Not IntersectionObserver, which is Safari
      12.1, and not `loading="lazy"`, which is Safari 15.4 and would be
      ignored rather than degrade on exactly the machines with the problem.
- [x] **F8 does what the POST says it does.** The screen printed "Press F8
      for BBS POPUP" and `isKey` had no case for F8, so it was dead on every
      machine. Both body lines are clickable now, like the footer.
- [x] Smaller: the crash screen's Restart link carries `?boot`, without
      which the boot gate silently drops you on the front page with no
      restart; `id` no longer claims group 27, which is exactly what puts an
      account in the sudoers file `sudo` says it is not in; the `man` page
      for `cmd` is gone, a confident manual for a command removed in round
      three; the equaliser on the front page no longer writes to fourteen
      spans seven times a second forever while nothing is playing;
      `js/sfx.js` wraps its whole setup in the try, since `js/bsod.js` owns
      `window.onerror` and a throw there would paint a crash screen over a
      half drawn POST; `js/buttons.js` is off the home page, where it was
      12.8KB fetched to look for an element that is no longer in the page;
      the wallpaper select has an `aria-label`; the guestbook form is
      `method="post"`, so a script failure cannot put somebody's message in
      the address bar; and `store.js` is no longer loaded twice.

Declined, with the reasoning recorded in the file itself rather than only
here: the splash logo stays at 256px, because it displays at 96 CSS pixels
and that is 192 physical ones on the Retina MacBook, and `srcset` is not
available on Safari 7, so the older machine is the one that would break.

### Round three: eight asked for by name

All eight done and verified in a browser on 21 August 2026. These came from
using the thing rather than from an audit, which is why every one of them is
about a rough edge no checklist would have found.

- [x] **The navigation flash.** Clicking through the site left up to two
      seconds with no CRT filter and no wallpaper. Two separate causes.
      `js/crt.js` BUILT the scanline overlay, so it could not exist until a
      script at the bottom of the page had run; the overlay is now plain
      markup in the shells and `crt.js` adopts the one it finds rather than
      appending a second. And the wallpaper layers ship empty at opacity 0
      and fade up over 1.4s once the manifest parses, which is the intended
      effect on a first visit and just a wait on the fifteenth navigation.
      `PAINT_HEAD` in `tools/pages.py` now paints the last wallpaper during
      head parsing with the transition off, and `js/wallpaper.js` resumes on
      that same picture and drops the rule once its own crossfade settles.
      Resuming also fixed a bug nobody had reported: the rotation restarted
      at `wallList[0]` on every page load, so it could never get past the
      first picture in a category.
- [x] **The guestbook signs**, on both Pages Web and Pages 98, sharing
      `js/guestbook.js`. It fills in your mail program rather than pretending
      to post. See the README for why this is the third design of it.
- [x] **Server Core scrolls back.** `.tty__screen` was `overflow: hidden`
      while `draw()` had always ended with `scrollTop = scrollHeight`, so
      output scrolled in and could never be scrolled back to. `help` prints
      more commands than fit and the top of the list was simply lost. Now
      `overflow-y: auto` with a phosphor green scrollbar, Shift+PageUp and
      Shift+PageDown the way a Linux console does it, and `draw()` only pins
      to the bottom when you were already at the bottom, so scrolling up
      during output no longer yanks you back down.
- [x] **Pages 98 uses its own logo.** The Start button carried the site orb,
      `img/logo-32.png`, while the splash two seconds earlier showed a four
      pane flag. An operating system uses one logo. `.flag98` in
      `css/desktop.css` is the splash flag at button size, built from the
      same four colours rather than exported as a PNG, so they cannot drift
      and so it stays crisp at sixteen pixels.
- [x] **The Command Prompt is gone**, and `css/shell.css` with it. Wrong joke,
      wrong room. Server Core is one tube and one prompt.
- [x] **The POST reports the real machine.** `SERVER` in `js/firmware.js`.
      The board really does have an American Megatrends BIOS.
- [x] **The badge marquee is off the home page**, with every line of the code
      kept and commented as deliberately unused.
- [x] **Paint saves.** `File`, `Save As` puts a picture in My Pictures and it
      is still there next visit. `File`, `Open` browses them with thumbnails
      and can delete one. Quota exhaustion puts up a disk full box, which is
      a real condition here rather than set dressing.
- [x] **`tools/check.py` now reads stylesheets.** Added because this round
      introduced a reference to `img/cursors/pointer.png`, which has never
      existed: the file is `link.png`. Nothing caught it, because the link
      check reads `href` and `src` out of HTML and never opens a CSS file.
      Cursors are the worst case of the three: a missing background is a
      blank area you notice, but a missing cursor image falls back to the
      keyword after the comma and looks completely normal, so the site
      quietly loses a custom pointer and nothing ever tells you. Verified
      that the check fires by breaking a path on purpose.

### Phase 1: the three that matter most

- [x] **Sound.** The single biggest upgrade available, and it is not close.
      Per character typing tick in the terminal, a short POST beep, a low CRT
      hum under the green screen. All synthesised with Web Audio, no asset
      files. `webkitAudioContext` exists in Safari 6, so both Macs are fine.
      - Modern browsers block audio until you interact with the page, so the
        POST beep on a cold load gets silently swallowed in Chrome. The 2010
        and 2012 Macs have no such restriction and will just play it.
      - Unlock the audio context on the first click or keypress. By the time
        you reach the terminal there has always been one.
      - Needs a mute control that is remembered. Sound you cannot turn off is
        a reason to close a tab.
- [x] **A real BIOS footer instead of the skip hint.** The bottom right
      currently says `click or press any key to skip` in the site font, which
      is the one thing on that screen admitting it is a web page. Real
      firmware puts its own line there. Same job, no illusion break:
      ```
      Press DEL to enter SETUP, F12 for Boot Menu, ESC to skip
      ```
- [x] **Make F12 work: the boot device menu.** This is a genuine gap, not a
      nice to have. The machine currently network boots for no stated reason.
      ```
              Please select boot device:

                Hard Disk
                CD/DVD Drive
                Removable Devices
              > Network (IPv4)
                Enter Setup
      ```
      Explains why PXE is happening, is a real screen almost nobody
      recreates, and reuses the menu widget that already exists. Auto selects
      Network after a beat if F12 is never pressed.

### Phase 2: the boot sequence

- [~] **The system configuration table.** The iconic end of POST artifact:
      the boxed two column table listing CPU Type, Base Memory, Extended
      Memory, Cache, Display Type, Serial and Parallel Ports. Also the right
      home for the real machine details, which currently sit in a slightly
      invented "Probing host hardware" block.
      - MARKED `[~]`, NOT DONE, after a check of this file against the code.
        Something was built and it is good, but it is not this. The POST ends
        with two plain unboxed blocks, "Server Configuration" and "Your
        Terminal", in `js/firmware.js`. There is no box, no second column,
        and not one of the seven named fields exists anywhere in the tree.
      - The box was deliberately dropped rather than never attempted: the
        POST was 52 lines against a screen that holds about 50, and the
        overflow was taking the American Megatrends badge off the top. The
        packing comment in `js/firmware.js` records the trade.
      - The second half was genuinely undone and IS NOW RESOLVED, the other
        way round from how the item imagined it. The audit was right that the
        details had been duplicated rather than moved: "Probing host
        hardware" in `js/boot.js` prints Display, Renderer, Platform, Agent,
        Locale, Processors, Memory, Link and Clock, and the POST had grown a
        "Your Terminal" block saying the same thing twenty seconds earlier.
      - The POST block is the one that went. The probe block belongs to the
        network boot, which scrolls and has room for nine lines; the POST is
        a fixed screen that pins to the bottom, where four lines is four
        lines of headroom the badge needs. So each fact is said once, in the
        place that can afford it, and the POST is 38 lines now rather than
        42. `sysReal()` is no longer called from `js/firmware.js` at all.
      - There is a stale comment in `css/boot.css` explaining a white-space
        rule as needed for "the whole drawn box of the system configuration
        table", describing a box nothing draws.
      - If it is ever built for real it costs rows the POST does not have, so
        it means taking lines out of the probe block first.
- [x] **A memory count that races.** `Memory Test : 262144K OK` ticking
      upward fast. Iconic, and about ten lines.
- [x] **A PXE failure that recovers.** `PXE-E51: No DHCP or proxyDHCP offers
      were received`, a pause, a retry, then success. Adds tension and length
      without implying anything is broken.
- [x] **Dots during the TFTP transfer** instead of cutting straight from
      "Downloading NBP file..." to success.
- [x] **DEL enters SETUP.** A blue and grey BIOS setup screen with menus that
      do nothing. Pure decoration, enormously satisfying. Optional but it is
      the natural partner to F12.

### Phase 3: the boot menu and loading

- [x] **Remember the last choice as the default.** Real `boot.ini` has a
      persistent `default=`. If you always pick Server Core it should become
      the highlighted entry. Authentic and genuinely useful.
- [x] **A different loading screen per operating system.** Right now both
      paths share one Windows loading bar, which is the least defensible bit
      of the whole chain.
      - Pages Web keeps the Vista bar it already has.
      - Pages Server Core gets a Linux init scroll instead:
        ```
        [  OK  ] Mounted /srv/photos
        [  OK  ] Started Authelia
        [  OK  ] Reached target Multi-User System
        ```
      - Pages 98 gets the Windows 98 splash. Done in Phase 6.
- [x] **Add Pages 98 to the menu** once Phase 6 exists. It is the middle
      entry, and the menu still opens on whichever one you booted last,
      the way `boot.ini` did.

### Phase 4: the terminal

- [x] **A CRT power on animation.** The bright horizontal line that snaps
      open vertically when a tube wakes up. Pure CSS transform, no library,
      and it is the perfect transition into the terminal.
- [x] **The Fallout hacking minigame.** The hex address grid with words
      hidden in the garbage, `likeness=3`, four attempts, duds and allowance
      resets in the brackets.
      - Offer it after three failed logins, or on a `hack` command.
      - It must NOT touch real authentication and must not pretend to. On a
        win it prints something like "impressive. Authelia is unmoved.
        Password?" The joke only works if it is honest.
- [x] **A terminal bell on error.** Needs Phase 1.
- [x] **Screen burn in on idle.** After a few minutes, the current text
      slowly ghosts and dims. Cheaper and more unsettling than a screensaver.

### Phase 5: the two shells

Linux side:

- [x] **Tab completion.** The most missed feature in any fake shell.
- [x] `top` with a live updating process table
- [x] `ping` with animated per packet output
- [x] `man` for the commands that exist
- [x] `sl` for when you typo `ls`. A steam locomotive crosses the screen.
- [x] `matrix` as a screensaver you can quit
- [x] `fortune` and `cowsay`

Command Prompt side:

**ALL OF THESE WENT WITH THE COMMAND PROMPT.** They were built, they worked,
and they were deleted in round three along with the window they lived in and
`css/shell.css`. Nothing in the current code registers any of them: typing
`color`, `tree` or `shutdown` in Server Core answers "command not found".

They stayed marked `[x]` for a while afterwards, which is how a backlog
starts lying. Marked `[~]` now, meaning built and then deliberately removed,
because deleting the lines would lose the record that they ever existed and
leaving them as done would claim the site has three commands it does not.

- [~] **`color`** that actually recolours the console. `color 0a` for green on
      black, `color 1f` for the classic blue. Authentic and cheap.
      REMOVED with the Command Prompt.
- [~] `tree` with the real ASCII branch characters. REMOVED with the Command
      Prompt.
- [~] `shutdown -s` that produces a Windows shutdown dialog. REMOVED with the
      Command Prompt. Pages 98 has its own Shut Down, which does work.
- [x] `edit` opening the MS-DOS Editor blue screen. Big, but glorious. BUILT,
      on 21 August 2026, in `js/dos.js`, which is where a DOS editor actually
      lived rather than in the Windows console that no longer exists. Type
      `EDIT`, or `EDIT XANDERS.TXT`, at the DOS prompt.
      - The grey menu bar, the blue field, the single line frame with the
        filename sitting in the top edge, and the status line counting the
        cursor. The three colours are the real ones out of the sixteen VGA
        text mode had: blue is colour 1, the bars are colour 7.
      - It EDITS. Arrows move, typing inserts with its case intact, Enter
        splits a line, Backspace joins one at the left margin, Delete works
        forwards, Home and End and the page keys work, and the buffer scrolls
        when it outgrows the eighteen visible rows.
      - It does not save, and says so on the way out rather than pretending.
        The buffer is a copy, so editing `AUTOEXEC.BAT` and closing it leaves
        the file exactly as `TYPE` will read it back, which was verified.
      - No word wrap, because EDIT scrolled sideways instead and sideways
        scrolling is a lot of machinery for a case nobody will hit. The line
        stops accepting characters at 75 and the status bar says why.
      - The prompt uppercases what you type, because DOS did. The editor does
        not, because being SHOUTED BACK AT is the most obvious way a text
        editor could feel wrong.
      - TWO BUGS FOUND BY MEASURING IT RATHER THAN LOOKING AT IT, both from
        the same wrong `display`. The rows were styled `display: block`, and
        js/dos.js joins its rows with `<br>` the way every screen in that
        file does, so each row broke twice and the whole editor was double
        spaced: 658 pixels of screen where 22 single rows should be 337. A
        block also fills its container, so the blue field ran 1219px wide
        against a frame that stops at 78 characters, leaving the box
        floating in a much wider field.
        `display: inline-block` fixes both: the `<br>` supplies the single
        break and the element is exactly as wide as its 78 characters.
        I had already looked at a screenshot of this and read the black
        gaps as part of the frame. The numbers are what caught it.
      - Verified at 1280x800 and 1440x900, the two target MacBooks: 22 rows
        at exactly one line height each, the blue matching the frame width,
        and no overflow in either direction.

### Phase 6: Pages 98, the toy box

The biggest item here and the one that makes the site genuinely interactive.
Most of the hard part already exists: `js/win98.js` draws and drags windows.

- [x] A desktop: wallpaper picked from the same manifest the Aero side
      cycles, icons on a grid, click to select
- [x] **The selection rectangle.** Press on empty desktop and drag and a
      dotted band follows the mouse, selecting by intersection rather than by
      containment, so clipping a corner counts. Four pixels of slop before it
      appears, or every ordinary click flashed a one pixel rectangle. The move
      and up handlers live on `document`, because the mouse always ends up
      leaving the window mid drag and a band bound to the desktop sticks down
      forever the first time it does.
- [x] **The icon grid wraps into a second column** when it runs out of screen,
      which is what Windows did. Done in `dtLayoutIcons` rather than in CSS:
      multi-column expresses it exactly, but only with `column-fill: auto`,
      and that is the one part Safari 7 does not have. The CSS version laid
      all nine icons out in a single row across the top.
- [x] A taskbar with a Start button, window buttons and a system tray clock
- [x] A Start menu, drawn from `tools/content/desktop.html`
      - both menus close on a click anywhere outside them, which is the part
        that took the fixing: bound to the desktop alone, the Start menu hung
        open over whatever you clicked next
- [x] **Right click opens a real context menu.** Note this reverses the
      current behaviour: right click is suppressed on the terminal, which is
      correct there and wrong here. A fake context menu on a fake desktop is
      both more authentic and much more fun than blocking the mouse.
- [x] Put existing content in windows: My Buttons reads the same table the
      Aero side uses, and My Pictures reads `MANIFEST.gifs`, so there is one
      list of each and two operating systems showing it. Webring, Guestbook
      and the Recycle Bin are in too.
      - [ ] still to move in: the media player. The WMP11 recreation is
        already built, but `MANIFEST.music` is empty, so a player window
        today would be an empty player. Worth doing once there is music.
- [x] Real toys: Minesweeper first, it is the smallest and the most loved
      - first click is never a mine, flood fill, flags that block clicks, a
        counter that goes negative when you over flag, and the fixed number
        colours everybody knows without knowing they know them
- [x] Notepad that actually types
- [x] **Paint that actually draws.** Ten tools, rubber banded shapes on a
      scratch canvas over the committed one, a real iterative flood fill, and
      right button drawing in the background colour. The stroke handlers move
      to `document` on mousedown, so a drag that ends off the canvas ends
      cleanly instead of leaving the tool stuck down.
- [x] **A screensaver on idle.** Starfield or flying windows, chosen when it
      fires, after four minutes. The hard part is not the animation: it is
      that the event which dismisses a saver must not also reach the desktop
      underneath, and killing the mousedown does not kill the click the
      browser builds from it. `js/saver.js` swallows the whole gesture.
      Preview it from Display Properties.
- [x] Window minimise and maximise, not just close
      - maximise measures its own chrome rather than trusting `width: 100%`,
        which overhung the screen by the width of the border
- [x] A shutdown sequence that returns you to the boot menu
- [x] **The Pages 98 splash**, moved down from Phase 3. Set by an inline
      script in the head, not by `desktop.js`: that file is the last of ten
      and by the time it ran you had already seen the desktop, so the splash
      arrived second and the sequence read desktop, splash, desktop.
- [x] **Its own font and its own icons.** MS Sans Serif from `fonts/msw98ui-*`
      and 32x32 icons in `img/icons98/`, both scoped to this page alone. The
      rest of the site is Selawik and Vista icons and the two cannot share a
      screen. The font scoping needs a class on `body`, because `css/chrome.css`
      sets the face with a universal selector and a universal selector beats
      an inherited value no matter where it was set.
- [x] **Content no longer leaks out of its window.** The bare pages never load
      `css/style.css`, so they never got its `* { box-sizing: border-box }`,
      and every app body asking for `height: 100%` plus 8px of padding came
      out 16px taller than the window holding it. `.w98__raw` was also styled
      only in `css/shell.css`, which this page does not load either, so the
      window body had no clipping at all. Both fixed, and the Notepad text
      area is now pinned to the bottom edge rather than guessing at 100%.
- [x] **Menu titles carry their own bevel.** File, Edit, Search and Help in
      Notepad, and the same in Paint. A departure from the original, which
      drew them flat until you touched them: on a page full of things that
      are not pressable, these should read as things that are.

### Fixed after an audit, not from the backlog

Seven read-only audits went over the whole tree. What they found and what was
done about it:

- [x] **The boot curtain failed closed.** `css/osmenu.css` says "fail open,
      never fail closed", and the CSS default really is safe, but the inline
      head script always runs and hides the page the moment it does. The only
      thing that ever unhid it was `bootReveal` in `js/boot.js`. That file
      failing to load left the site invisible forever, and `js/bsod.js` could
      not catch it because `window.onerror` does not fire for a script tag
      that 404s. Both curtains now watch for a heartbeat. NOT a fixed
      deadline: the POST waits indefinitely for ENTER by design, so a timer
      would have snatched the BIOS screen away from somebody reading it.
- [x] **Safari 7 could not sign in to the private wing at all.** `AUTH_MODE`
      is "api", both factors guard on `window.fetch`, and fetch arrived in
      Safari 10.1. On the 2010 MacBook, the stated primary target, a visitor
      typed a password and was told to go and edit a variable called
      AUTH_MODE. It now falls back to the Authelia portal, which needs no
      fetch because it does not collect anything itself.
- [x] **The fake filesystem said where the password vault lives.** No
      credential, and the vault is correctly nowhere near the web, but
      `services.txt` named the host, said it was a file rather than a service,
      and named the client that opens it. The principle was worth keeping and
      the address was not.
- [x] **Two of the three Paint fill styles were unreachable.** The options
      panel showed the width picker and the fill styles stacked, 90px of
      content in a 54px box with `overflow: hidden`, and a clipped element is
      not hit testable. `style` could never leave 0, so every rectangle and
      ellipse drew as a bare outline and two branches of the shape code were
      dead. Paint swapped the box contents rather than stacking them.
- [x] **A lost mouseup left the airbrush spraying forever.** Measured going
      from 16 inked pixels to 168 with nobody touching the mouse, and nothing
      in the interface stopped it. Strokes now abort on window blur, and
      picking a tool ends whatever was still running.
- [x] **The screen saver deleted the nearest flying windows on resize.** The
      paint loop sorts far to near, so `pop()` took the closest and largest
      one first, which is exactly the "crashed and recovered" look the
      trim was written to avoid. It now removes the furthest.
- [x] **The saver went deaf after a wheel notch.** The swallow exists to eat
      the click that follows a mousedown. Wheel and mousemove have no tail,
      so opening it for them just ate the next real click.
- [x] **woff was listed before woff2.** On the reasoning that Safari 7 needs
      the old format first. Wrong: a browser skips a format it does not know
      rather than giving up, so this only made every modern browser fetch the
      larger file.

Still open from that audit, and deliberately left for a decision rather than
changed unilaterally:

- [x] **`background-attachment: fixed` removed from `body`** in
      `css/style.css`. In WebKit any element carrying it registers as a slow
      repaint object on the FrameView, which disables threaded scrolling for
      the whole document: scrolling becomes a main thread repaint of the
      visible area on every frame. What it bought was the sky holding still,
      which is invisible whenever a wallpaper is on, because `.wallpaper` is
      `position: fixed` and opaque and covers it.
      - [ ] If you want the gradient anchored again WITHOUT the cost, move
        the four gradients onto a `position: fixed` div at `z-index: -5`,
        which is the trick `.wallpaper-scrim` already uses. Only worth doing
        if you browse with the wallpaper switched off.
- [ ] **The service inventory is public.** `SERVICES` in `js/shell.js` lists
      six real subdomains. They are discoverable through certificate
      transparency anyway once they exist, and showing off a homelab is half
      the point of a site like this, but it should be a decision rather than
      an accident.

### Phase 7: the machine misbehaving

Screens you only see when something has gone wrong. These are the ones that
sell the fiction hardest, because a machine that only ever succeeds is a
demo, and a machine that occasionally complains is a machine.

- [x] **A blue screen for the 404.** The one I would do first. The 404 page
      is currently the only screen on the site that breaks character: it
      shares the home shell and quietly appears with no boot. Give it the
      real thing instead, with the file you asked for as the fault:
      ```
      A problem has been detected and Pages has been shut down to prevent
      damage to your computer.

      PAGE_NOT_FOUND

      Technical information:

      *** STOP: 0x00000404 (0x00000000, 0x00000000, 0x00000000)
      ```
      Keep a real link back to the site on it. A joke that traps you is not
      a joke.
- [x] **chkdsk after an unclean shutdown.** If you close the tab part way
      through the boot, drop a flag. If the next visit finds that flag still
      set, run a disk scan before the boot sequence:
      ```
      Checking file system on C:
      The type of the file system is NTFS.

      One of your disks needs to be checked for consistency.
      ```
      Almost nobody does this and it is exactly the detail that makes a
      machine feel like it has been running the whole time you were away.
      Clear the flag when the boot completes normally.
- [x] **"Keyboard not detected. Press F1 to continue."** The oldest joke in
      the BIOS. Rare, random, and it must accept any key rather than
      literally requiring F1, or somebody on a laptop is stuck forever.
- [x] **A JavaScript error should look like a crash**, not like nothing.
      A `window.onerror` handler that draws the blue screen would turn the
      worst case into the best gag on the site. Only worth it once the blue
      screen exists.
- [ ] **A status page for the services**, once the VPS is real. Green and
      red lights for Nextcloud, Immich, Navidrome, mail. Fits naturally in
      Server Core as a `status` command and in Pages 98 as a control panel.

### The sweep, done in one pass

Everything the eight lens audit turned up, worked through in order. The
worklist that tracked it is gone; this is what it left behind.

**Things that were misleading a visitor**

- [x] Eight public pages were addressing the OWNER, not the reader. About
      opened with "Replace all of this with your own words." Contact said the
      address below was a placeholder, directly above the address. All eight
      rewritten in the site's own voice.
- [x] The mailing list said "not actually stored yet" AFTER you handed over an
      address. It says so above the field now.
- [x] The guestbook made a live link out of whatever you typed in the website
      field, `javascript:` included. Only http and https now.
- [x] The 404's "Restart the machine" went to `/index.html`, which the head
      script suppresses for a same origin referrer, so the one control named
      after restarting the machine did not restart it.

**Routing, so the machine is circular**

- [x] Pages 98 and Restart are both in the sidebar and the site map now. Pages
      98 was reachable from exactly one place: an arrow key at the boot menu
      inside a ten second countdown.
- [x] **The countdown was booting the wrong operating system.** `osTick`
      called `osBoot(0)` while the screen above it said "highlighted choice",
      and then wrote the wrong answer to storage, so it overwrote the
      preference it had just ignored. Invisible with two entries.
- [x] **`guest` is offered at the terminal login.** `?demo` already walked the
      entire private wing and was mentioned in one line of README.md. A guest
      account is period correct and discoverable at the moment you are stuck.

**The machine remembering itself**

- [x] The MAC and the SMBIOS GUID persist. The POST printed one MAC and
      `ifconfig` printed a different one ninety seconds later in the same
      session, because the shell called `sysFake` a second time.
- [x] `uptime` is counted from a stored first boot instead of claiming "up 41
      days" on a machine you just watched cold boot.
- [x] Pages 98 keeps its wallpaper, its Notepad buffer and a Minesweeper best
      time. It was the only partition that remembered nothing, and `store.js`
      was already loaded on the page doing nothing.

**What a crawler and a link preview see**

- [x] `robots.txt` and `sitemap.xml`, both generated from NAV.
- [x] meta description, og: and twitter: tags on every page, generated from
      the DESCRIPTIONS dict that already existed. The only meta tag on the
      whole site was the charset.
- [x] The two takeover pages have the site name back in their titles.

**How it feels on a 2010 laptop**

- [x] The terminal's 26px and 48px bloom layers are gone. A blurred
      text-shadow rasterises per glyph on the CPU, and `draw()` rewrites the
      whole buffer on every keystroke. `.tty__glass` already does the room
      glow once, statically.
- [x] `background-attachment: fixed` off the body. In WebKit it marks the page
      as a slow repaint object and disables threaded scrolling for the whole
      document.
- [x] Focus styles, in a 98 dotted idiom. There was not one `:focus` rule in
      fourteen stylesheets.
- [!] Capping the terminal scrollback is NOT done, deliberately. `caretSlot`,
      `menuAt` and the spinner's `slot` all hold indexes into `lines`, so
      trimming from the front points them at the wrong row. The one line fix
      would introduce a worse bug than it closes.

**Dead weight**

- [x] `DT_CELL_W`, `DT_CELL_H`, `ntPad`, `typingUntil` and the `loginLink2`
      branch, all gone, along with the comments that claimed they mattered.
- [x] `.ogg` off the audio whitelist. Safari has never decoded Vorbis on
      macOS and `player.js` never called `canPlayType`, so an Ogg track
      appeared in the playlist and silently did nothing.

**The fiction**

- [x] Pages 98 has the CRT overlay. `crt.js` bailed on `logon-root`, which
      Pages 98 inherited from the terminal, so the monitor vanished when you
      booted it.
- [x] chkdsk speaks in the firmware's voice. It ran BEFORE the boot menu and
      announced NTFS and "Windows will now check the disk", telling you the
      answer to a question it had not asked yet.
- [x] The 98 window chrome letters itself in MS Sans Serif everywhere, not
      just on the desktop. The Command Prompt in the private wing was an XP
      console in a 98 frame in a Windows 7 font.
- [x] **A broken comment in boot.css was deleting the POST font size.** One
      comment closed early and CSS error recovery ate `font-size: 1.05vw`
      with the loose prose. The BIOS screen had been rendering at an
      inherited 12px.
- [x] The adoptable pet is on the page. `main.js` has wired it and
      `style.css` has animated it since they were written, and no element
      with the class ever existed.
- [x] The front page leads with a featured wallpaper you can click to set,
      instead of an empty box captioned "Coming soon!".

**Writing and documentation**

- [x] Four blog posts, lifted from the source comments where the best writing
      on this site was buried: why the 404 has real exits, why the boot menu
      stops its clock, why the hacking game refuses to let you win, and why
      the machine notices you closed the tab.
- [x] ASSETS.md can actually recover the wallpapers now: which archive, how
      many, and the seven category folders. `.gitignore` promises it can.
- [x] `tools/layout.py` generates the README layout block from the banner
      comment of every file. It was hand maintained and had drifted to
      missing 13 of 29 JS files.
- [x] ROADMAP.md lost 80 lines describing a different project, called
      Nexus-7, under a `site/` directory that never existed.

**And the fourth partition**

- [x] **`js/dos.js`.** Press F12 at the POST and pick Hard Disk. Hard Disk,
      CD/DVD and Removable Devices were three drawn, arrow selectable,
      completely dead doors. One of them now holds the old install nobody
      could face moving off: a C: prompt, eleven files, about a dozen
      commands, and an AUTOEXEC.BAT that says "Xanders. Do not reformat this
      one." EXIT hands back to the firmware and the network boot carries on,
      so it is a detour rather than a dead end.

      It is deliberately almost empty. The whole value of this partition is
      that there is nothing on it.

### Round two: the first visit, and a review of the newest code

A second set of lenses, deliberately aimed away from what the first audit
covered: a stranger's first ten minutes, an adversarial read of the newest
files, the empty rooms, what the machinery now makes cheap, and what rots.

**What a stranger actually hits**

- [x] **The Private Wing link made you watch the whole boot again, in a new
      tab.** The sidebar linked `private.html` with no query string, so
      `js/boot.js` ran the entire chain a second time: POST, the indefinite
      wait for ENTER, the splash, the eleven second network boot, in a
      `target="_blank"` tab with no Back button. The operating system menu had
      always linked it as `?booted`. Now the sidebar does too. `boot.js` had
      already written the rule this broke: two network boots back to back is
      where a nice thing becomes a toll gate.
- [x] **F12 and DEL were the only doors to Setup, the boot menu and the whole
      MS-DOS partition, and neither key reaches the page on the target
      machine.** On a MacBook the key marked `delete` is Backspace, and F12 is
      a volume key unless standard function keys are switched on. On Windows,
      F12 opens DevTools and `preventDefault` cannot stop it. So the best
      drawn screen in the chain and the newest partition on the box were both
      unreachable on the 2010 laptop this site exists for. The three words in
      the POST footer are clickable now, with the wording untouched.
- [x] **The one button on the front page pointed at the emptiest page.**
      "enter the world" went to About, which opens by admitting it has not
      been written. Same failure the featured box was fixed for, one click
      downstream. It boots Pages 98 now.
- [x] **The longest screen had no skip hint.** The network boot has always
      been skippable, but "ESC to skip" only existed on the four second
      splash, which is hidden the moment the eleven second scroll starts.
- [x] **QUICK BOOT.** A bookmark or a typed URL sends no referrer, so the head
      script cannot tell a returning visitor from a stranger and played the
      full twenty one seconds every time. The machine now remembers that it
      has booted all the way through once, and every later direct arrival runs
      at the brisk pace without stopping for ENTER. Set on COMPLETION, so a
      visitor who closes the tab halfway still gets the full thing. Never
      applies to `?boot`, so Restart, Shut Down and `reboot` always give the
      whole sequence. AMI called this Quick Boot and had it on by default.

**Real defects in the newest code**

- [x] **firmware.js kept its keyboard while dos.js ran.** Both draw into the
      same element, and both were listening. Pressing ESC anywhere in the DOS
      session ran the FIRMWARE's ESC branch: it cleared the screen and started
      the network boot while dos.js carried on writing its prompt underneath,
      and the two took turns redrawing. Verified before the fix by picking
      Hard Disk and pressing ESC 300ms later: the machine network booted.
      The firmware now hands the keyboard over and keeps the handle so it can
      stop the partition before taking the screen back.
- [x] **Maximise then restore threw a window off centre.** `win98Open` centres
      with `margin-left` and `margin-top` only, so the CSSOM will not
      serialise the shorthand and `win.style.margin` reads back as the empty
      string. That empty string was saved and assigned back on restore, which
      CLEARS all four longhands rather than leaving them alone, so the
      centring margin was deleted and the window jumped half its own width.
      It saves the two longhands it actually sets now.
- [x] **The machine still contradicted itself about its own address.** Pinning
      the MAC fixed the half visible on two screens; the IP, gateway and
      subnet were still re-invented on every call, from a randomly chosen
      RFC1918 block, across five call sites in one session. The whole network
      identity is now held for the life of the page. Deliberately NOT
      persisted: a card keeps its MAC, a DHCP lease does not, so the address
      changes between visits and stays put within one.
- [x] **The featured wallpaper did not restart the cycler**, so clicking it
      44 seconds into the 45 second cycle set your pick and had it replaced a
      second later. Every other control already called `startTimer()`.
- [x] **Two Notepads shared one storage key.** Both the icon and the Start
      menu open it, each window loaded the note on open and wrote its own box
      back on close, so closing a stale window overwrote what you had just
      typed in the other. Notepad is single instance now.

One finding was a false positive: the review read `js/firmware.js` while the
clickable footer was half written and reported the spans as unwired. They were
wired a few minutes later and verified in a browser.

### The rest of round two

**The tools, where one of them was a publishing bug**

- [x] **`blog.py` published the feed before the page existed.** It wrote the
      fragment and `feed.xml`, but `pages/blog.html` was not rebuilt until
      `pages.py` ran afterwards. So one command pushed RSS to subscribers
      while the post was not on the site, and the guid anchor landed on
      nothing. It now writes the fragment, runs `pages.py`, and writes the
      feed last, in one command, because a reminder is a thing you follow
      four times and forget once.
- [x] **The two generated files in `tools/content/` say so now**, emitted by
      their own generators rather than pasted in, so a rebuild cannot lose
      the warning. That folder is the one the README calls editable, which is
      exactly where an unmarked generated file does the most damage.
- [x] **Every tool computes the repo root from its own location.** Eleven of
      them hardcoded the full drive path and three did not, so moving or
      copying the folder would have left half the toolchain writing to the
      original. Verified by running them from another drive.
- [x] **`tools/check.py`**, the checker the README asked for and never got. It
      folds in `checkhtml.py` and adds what was missing: the no-dashes rule,
      internal links, fragments newer than the pages built from them, orphan
      js and css, and external hosts with `--links`. Proved by planting an em
      dash, a broken link and an orphan file: it caught all three and exited
      1. Every check is there because that exact thing has broken here once.
- [x] **The build order is written down in one place**, at the top of
      README.md, rather than scattered across five sections and four
      docstrings.

**What the machine already made cheap**

- [x] **Paint: File, Set As Wallpaper.** Two subsystems that had never met
      were already on the same page: a canvas with an undo buffer, and a
      wallpaper with a storage key. The picture is kept whole as a data URL,
      because there is nowhere to put a file. It survives a reload, and New
      Wallpaper still goes back to a photograph.
- [x] **DEFRAG.EXE.** `DIR` had always listed it at 78,016 bytes and typing
      it got "Bad command or file name" about a file the machine had just
      named. The block grid animates by writing back into the printed buffer
      by index, which is how a console animates in place.
- [x] **`pages98` in the terminal.** Phase 8 asks that every operating system
      reach the others, and this was the missing edge: Pages 98 had icons for
      both of the others, Server Core had `vista` and `reboot` and no way to
      the desktop.

**The empty rooms, mostly reframed rather than filled**

- [x] **Web Games points at the four games that already ship.** The page was
      an argument that Flash is dead followed by "Games go here", while the
      machine one partition over had Minesweeper, Paint, the screen saver and
      DEFRAG. Nothing needed writing, only linking.
- [x] **The guestbook takes entries by email.** The form validated your input
      and then told you it had thrown it away, which was the only interaction
      on the site a visitor was invited to perform. The page's own third
      option was the answer, and it is what most people did in 1999.
      `js/guestbook.js` went with it.
- [x] **Lost Media has three entries**, written from material already in
      ASSETS.md: the GeoCities shutdown and the 31,119 buttons, GifCities and
      why redundancy beat quality, and Flash having a death with a date on it.
      It is the only empty room backed by an interest the site states twice.
- [x] **The webring lost its dummy widget**, which read "site 0 of 0" with
      dead links and was the one block making an otherwise finished essay
      look unfinished.
- [x] **Gallery says what it is for** and points at the rooms that are full,
      instead of making the same promise as Wallpapers with nothing behind
      it.
- [x] **Music is honest about why it is empty**: everything worth playing
      belongs to somebody else. It was a page of instructions to its owner.
- [x] **The under construction gif is on one page, not eight.** At that
      density it stopped being the 1999 joke about a page being alive and
      became a status report, usually attached to a caption naming the thing
      that was missing.
- [x] **Two pages stopped claiming to be constantly updated** when neither
      had ever held an item, on a site that invites you to check.

**How the site presents itself**

- [x] **The nav leads with the finished rooms.** Wallpapers and Blog now come
      before Who Am I, Projects and Gallery. The emptiness was fine; the
      order made a site with several finished rooms read as unfinished.
- [x] **The front page explains the machine.** Grepping every public page for
      "F12", "MS-DOS" or "partition" returned nothing at all, so the one
      thing a visitor would tell a friend about was invisible. There is a box
      naming all four partitions, the guest login and the F12 key.
- [x] **The updates page is current.** Its first paragraph offers to let you
      check whether the site really is being updated, and it had broken that
      promise within a day.

### Phase 8: connective tissue

- [x] Every operating system has a way back to the boot menu. The terminal
      has `reboot`, Pages 98 has Shut Down, and Pages Web now has Restart in
      the sidebar pointing at `index.html?boot`. The 404's "Restart the
      machine" carries the flag too.
- [ ] A consistent idea of "restart" that replays the whole chain.
- [ ] Decide whether the boot plays on a deep link from a search engine.
      Current rule: only when you arrive at the home page from outside the
      site. Landing on the Gallery from Google never boots, which is
      probably right.
- [ ] Keep the em dash rule. Still zero across the whole tree.

---

## Constraints that apply to all of it

- **Safari 7 on Mavericks is the floor.** No CSS variables, no grid, no
  flexbox gap, no `inset`, no `clamp()`, no `:has()`, no arrow functions, no
  `let` or `const`, no template literals, no `padStart`, no `forEach` on a
  NodeList. Prefixes on every gradient, transform and animation. See
  `ROADMAP.md`.
- **Desktop only, fixed 980px.** No mobile layout, on purpose.
- **No em dashes and no en dashes anywhere**, including HTML entities.
- **Fail open.** Anything decorative that gates the site must default to off
  when JavaScript is unavailable. `#bootlayer` already does this: it is
  `display: none` until a script says otherwise, so a crawler or a browser
  with JavaScript off sees the site immediately rather than a black
  rectangle nailed over it forever.
- **Reduced motion means less movement, not less content.** Text that types
  is content arriving. Things that sweep, spin and slide are movement. Stop
  the second kind and keep the first.
- **Nothing in the browser is a security boundary.** Every line of every file
  here ships to the visitor. The fake shell, the fake filesystem and the
  hidden route to the private wing are all flavour. Authelia is the only
  thing actually guarding anything.
