# ui - mrly.net's own design kit, not a package: plain CSS, a little vanilla JS, a little React. No build step, no Tailwind, no CSS-in-JS. - House style: palette first (the fifteen colors, generated), theme and tokens second (`tokens.css`, by hand), one class per idea, semantic HTML, light on `:root` and dark twice (`prefers-color-scheme` guarded by `:root:not([data-theme="light"])`, then `:root[data-theme="dark"]`), AA contrast in both, 44px targets on coarse pointers, no motion under `prefers-reduced-motion`. - The palette is generated, never hand-edited: `palette.css` for CSS and `palette.js` for scripts, the same numbers in both. - `palette.css` names black, white and thirteen hues (`--red --orange --yellow --green --mint --teal --cyan --blue --indigo --purple --pink --brown --gray`), one flat value each, no shades. - `tokens.css` resolves the roles `--ground --bg --panel --deep --line --fg --dim --accent --on-accent` per theme, plus the figure switches `--show-dark` / `--show-light`. - `--art` is `var(--ground)`, the ground of every canvas and figure; figures ship as `` and `` pairs and `--show-dark` / `--show-light` pick one. - `chrome.js` fires `window` event `theme` whenever `data-theme` or `data-tint` changes; scripts that paint with `palette.js` listen and repaint. - `config.js` `headScript(prefix)` is a tiny inline script for the ``, before any stylesheet: it adds class `js` and replays `data-theme`, `data-font`, `data-tint` and `data-saver` from localStorage, so a page never flashes the wrong theme or the wrong footer. - Tint is a runtime setting: `data-tint` on `html` is a palette hue name, absent means the site default, and `tintCss(hue)` writes the site default plus a `:root[data-tint=]` block for every hue but grey. - A tint is the plain hue on both grounds; `--on-accent` is white and `--ring` follows `--accent`. ## CHROME - One frame: `--frame` = `--pane` + `--page` + `--pane`, with `--pane` a clamp between 13rem and 17.5rem so a small screen keeps a wide main. At and above 74rem the page is three columns centred as one block, tree on the left, main in the middle, tools on the right; main never drifts, and zoomed out the block floats. Below 74rem the two panes are drawers. - Two bars. The header is three routes in the pixel font: `+` to `menu`, the wordmark to `/`, `O` to `cart`; it scrolls away. The dock under it is sticky: a left-drawer button, the last route segment in capitals (`/demos/spectra/` reads SPECTRA), a right-drawer button; the buttons show only below 74rem, and an open drawer fills its button into a solid square, one cell at a time, and drains it on close. - The scroll rule: `chrome.js` keeps `scrollY` per `location.href` in sessionStorage on `pagehide`, then on `load` drops the key and puts the spot back only on a back-forward or a reload with no hash, so a fresh load never scrolls itself; opening a drawer pins the dock to the viewport top so the drawers hang from the bar. - The `O` is the font's O around nine inner cells; `chrome.js` lights one cell per item in the cart, read from localStorage `${prefix}cart` (an array of `{ qty }`), and repaints on the `cart`, `storage` and `pageshow` events. - Drawer state lives as `data-left` / `data-right` = `open` | `shut` on `html`, never persisted. Theme is `data-theme` = `light` | `dark` (absent = auto), font is `data-font` = `sans` | `serif` | `mono` | `mrly` (absent = system), tint is `data-tint` = a hue name (absent = the site's), saver is `data-saver` = `matrix` | `sleep` | `mandelbrot` | `julia` (absent = the wordmark; `chrome.js` takes no other name and drops the attribute and the key when it reads one), all four in localStorage under `prefix`. `html.js` marks a page with JS, set by the boot script before the first frame. - Settings, in the right pane: four rows in one control style, a label left and its value right. Theme is a button cycling auto, light, dark; Font, Tint and Saver are `.pick` labels around a select, Auto first on Tint and Wordmark first on Saver, every option a plain word. `tokens.css` maps `data-font` to `--face`; `base.css` sets body and `.prose` in `var(--face, ...)`, so the chrome keeps the system face and the reading text changes. - The tree takes nodes `{ name, href?, nodes?, lazy?, icon? }` and shows them as given: the site hands it one flat list per door, every leaf named by the last segment of its url capitalised, in alphabetical order. A branch is open only when it holds the current page, nothing else is open by default, and `chrome.js` scrolls the pane so the current leaf sits mid-pane. `icon` is a seti class drawn before the name. A node with `lazy` renders a `
` whose children arrive on first open from the JSON at `/git/tree.json` (`{ base, c: [{ n, k: d|f, i?, c? }] }`, `i` the seti kind); the arrow expands, the name navigates. On a code page the tree is the repository alone, root first, the path to the page open, so the viewer reads like an editor. - The footer is one screen: the site's wordmark written and held by the pixel-font animation across the whole width, and `Copyright © {company} {since}-{year}. All rights reserved.` under it. No links: the menu page holds them. - `Grid({ nodes })` is the one gallery: a leaf with `figure: { dark, light }` is a picture tile, `text` its caption, `dates` its stamps, a leaf without a figure a plain tile. Every index page, the demo gallery, the home doors and the menu draw it, so they all look the same. - `Menu({ tree })` lays a whole tree out as sections, one per group with a shelf per subgroup, leaves under Pages, each a `Grid`. A site's `/menu/` route is that over its navigator dressed with figures. - The footer's legal line sits two pixels off the bottom of the page, over the full-screen animation and never under it. - Print is one story in three files: `base.css` sets the page margin, black on white, 11pt, the light figure of every pair, the read column full width, headings kept with their text, figures, tables, code and block math unbroken, code wrapped, and every link's target in brackets after it; `chrome.css` turns the skip link, both bars, both panes, the scrim and the footer off, lets main fill the sheet and keeps the opener at 20rem; `code.css` drops the line numbers and the code frame and wraps long lines. Nothing is hidden but the chrome: a paper or a note prints from the browser as it reads. - The opener is the page's own figure over the title: one `figure.opener`, at most 32rem wide, centred, hairline framed on the `--art` ground. ## FOOTER - One `canvas.mark` inside the home link, with the static wordmark SVG `.still` beside it. `chrome.js` mounts the canvas; everything else is CSS off `data-saver`. - Absent `data-saver` is the wordmark: the canvas is a `cols+2` by `rows+2` backing store stretched to `width: 100%` with `image-rendering: pixelated`, painted by `font.js` `mark()`. - Any other `data-saver` mounts `savers/index.js` `saver(canvas, name)`, and CSS lays the same canvas out `position: absolute; inset: 0`, so `frame.js` sizes it to the footer's CSS box times dpr and the pixels are real. - A canvas keeps its first context for life, so every mount swaps in a fresh `cloneNode` of the old one; the 2d wordmark and a WebGL fractal never share an element. - The wordmark canvas is `role="img"` with the site title; a screensaver is decoration, so its clone takes `aria-hidden="true"` and drops the role and the label, and gets the role and the label back when the wordmark returns. - The stretched saver canvas is `pointer-events: none`: it lies over the home link and the link stays clickable through it. - `mark()` and `saver()` both return a stop function, kept as `canvas.stop`; switching savers stops the old one first, and both pause offscreen. - No JS shows the SVG and hides the canvas; `prefers-reduced-motion` does the same for the wordmark, while the other savers keep the canvas and draw their one still frame. ## FILES - `palette.css`: generated, one `:root` block, the fifteen colors and nothing else. - `palette.js`: generated, the same palette for scripts. - `tokens.css`: loaded after `palette.css`; the roles light on `:root` and dark twice, `--art --scrim --mix`, type (system stacks ending in Noto Symbols 2 and Noto Color Emoji as fallbacks), `--face` per `data-font`, space, shape, frame, motion. - `base.css`: reset, text, links, focus, `.prose`, `img.dark` / `img.light`, reduced motion, print. - `chrome.css`: skip link, `.top` header and `.dock` bar, `.panes` with `.pane.left` / `.pane.right` and `.scrim`, `.tree`, `.contents`, `.settings` with its `.theme` button and `.pick` selects, `.menu`, `.base` footer, controls (`.row`, `.set`, label, select, range, checkbox, `button` and `.button`, `.tabs`), `.stats`, `.chip`, `.badge`, tables, `.cards`, `.gallery` / `.tile`, `.opener`, `.elsewhere`, and the chrome's print rules. - `chrome.js`: vanilla ESM, runs on load; the scroll spot, drawers, theme, font, tint, saver, cart, contents highlight, footer mark, tree reveal, lazy tree; exports `wire()` for pages that render later. - `font.js`: vanilla ESM, the pixel font and its choreography. The crate `crates/mrlyfont` is the source of truth: `scripts/wasm.sh` runs its `book` example into `font.json`, which carries every glyph's rows and its stroke path (every glyph hand-penned in the crate's `pens.rs`). `font.js` reads those and ports only the arithmetic: the layout, the write, the phased merge that folds the letters into one centred stack, and the loop. It uses the wasm bridge `globalThis.mrly.font_*` when present; `font.test.js` pins the crate's numbers and the site's `font.test.ts` checks every frame against the wasm. - `font.json`: generated by the crate, `{ char: { rows, path } }`, the bitmap rows and the cell-by-cell stroke order over the trimmed glyph. - `theme.js`: the two role sets, `dark` and `light`, over `palette.js` for scripts that paint without the DOM; `savers/frame.js` and the demos' `mrly.js` read it. Listed under `kit.files` like every file a kit script imports. - `logo.js`: the MrlyLogo mask, which is the font's `X`, plus `grid(level)` and `logoSvg(level, fill, ground)`; the builder draws every site's favicon and icons from it. - `chrome.jsx`: React, renders the whole page for `react-dom/client` and `react-dom/server`. - `config.js`: `configure(site)` takes `site.json`, `conf()` reads it back, `tintCss()` writes the accent blocks, `headScript()` writes the boot script. Keys the kit itself reads: `title since prefix tint menu cart company`. - `savers/`: the four screensavers the footer can wear, vanilla ESM over one canvas, with their own README. Every one inks itself from `--accent`, so the Tint setting is their primary colour. A site lists `savers/*.js` (`tiles.js` included) beside `palette.js`, `theme.js` and `logo.js` under `kit.files` or the footer has nothing to import. - `code.css`: the code viewer: the path bar, the file list, the numbered `
` with its `d2`-`d6` gutter, images and PDFs, and the `tk-*` token colours the `git` highlighter emits. Loaded on code pages only, beside `seti/seti.css`.
- `fonts/`: `fonts.css` and the vendored faces, all OFL with their licences beside them: Noto Sans, Noto Serif and Noto Sans Mono (variable 400-700, Latin), MrlyFont from `crates/mrlyfont`, Noto Sans Symbols 2, and Noto Color Emoji in ten unicode-range shards. A site lists them under `kit.files` with `hash: false`, since the css names them by relative url, and links `fonts/fonts.css` in its head; a face is fetched only when a page needs it.
- `seti/`: the SETI file-icon font for the code viewer.

## EXPORTS

- `font.js`: `letters(text)` gives `{ rows, cols, grid }`; `animate(text, pad)` writes, `merge(text, pad)` folds, `cycle(text, pad, hold)` chains write, hold, merge, hold, unfold, hold, unwrite, hold into `{ rows, cols, fps, frames }`; `mark(canvas, anim)` plays an anim in the canvas's own `color`, repaints on the `theme` event, and returns a stop function the caller must keep.
- `chrome.js`: `wire()`, idempotent, syncs aria state, applies theme, font, tint and saver, paints the cart, reveals the current leaf in the tree, and attaches the contents observer and the footer mark.
- `config.js`: `configure(site)`, `conf()`, `HUES`, `tintCss(hue)`, `headScript(prefix)`.
- `seti/seti.ts`: `seti(path)` gives the class string for a path.
- `chrome.jsx`: `Shell({ route, title, lead, tree, current, contents, controls, wide, children })`, `Glyph({ text, className, label })`, `Grid({ nodes })`, `Menu({ tree })`; the header, dock, tree, contents, controls, settings and footer are Shell's own parts, not doors.
- The header always draws the site's wordmark; `prefix` moves the localStorage keys and is read in the browser from ``.