Single-page site for the Reality Bending Lab (University of Sussex). No build step, no framework, no package manager: plain ES modules, plain stylesheets, static assets. The repository root is the site.
This file is the index, and it is deliberately short. Everything specific
lives in docs/, one file per area — read only the one you need. Every fact
lives in exactly one place; if something is true of two areas, it is here.
python -m http.server 8777| Working on | Read |
|---|---|
| any CSS; the shell, hero, tabs, pager, scroll handlers, parallax | docs/layout.md |
URLs, deep links, routes.js, deep-link.js, generated pages, SEO/head |
docs/routing.md |
| the People section, roster, alumni, memories | docs/people.md |
| the Research section's Creations tab, its colours | docs/research.md |
| the Research Overview tab (the scroll-driven dive) | docs/research-zoom.md |
| News posts, the reader, porting old posts | docs/news.md |
| Publications, the ORCID/CrossRef pipeline, cards, badges | docs/publications.md |
| the Information section (Contact / Join / Services) | docs/information.md |
| adding any image, video or model | docs/assets.md |
the Python scripts, CI, .gitignore, deployment |
docs/build-and-deploy.md |
README.md is the only file here written for lab students rather than
maintainers: how to add a profile, how to write a post. Keep it that way.
index.html all markup; sections are static, contents injected by JS
css/ one numbered file per part — the numbering IS the cascade
script.js door screen, nav, hero glow, tabs, backdrop parallax
site-sections.js single source of truth for a section's colour, brain region, nav entry
brain.js the three.js brain in the hero
shared/ cross-section helpers; their CSS counterpart is css/07-shared.css
people/ research/ publications/ collaborations/ memories/ news/ information/
one module + JSON manifest each, rendered client-side
update_people.py people/ + memories/ folders → their manifests
update_publications.py
ORCID + CrossRef → publications/ + manifest
update_news.py news/ folders → news_manifest.json
generate_pages.py manifests + index.html → ~250 real pages, robots.txt,
sitemap.xml, llms.txt (all gitignored, all built in CI)
tools/ developer tools — nothing here is part of the site
img/ assets shared by more than one section
.github/workflows/ the deploy and check pipelines
LICENSE MIT for the code, plus what in here is somebody else's: the
brain mesh is CC-BY-4.0, the paintings and the papers are
not ours to relicense
Break one of these and the failure is usually silent. They are here rather than
in docs/ because they are not any one area's business.
- A section's colour lives in
site-sections.js, not in the stylesheet. The hero buttons, the nav links and the 3D brain's highlight all read from it. Changing a colour incss/01-base.cssalone desynchronises them. Note also that the hero's button colours come from:nth-childrules, so a colour belongs to a position on the arc, not to a section — see docs/layout.md. - A section's colour lives in
site-sections.js, not in the stylesheet. The hero buttons, the nav links and the 3D brain's highlight all read from it. Changing a colour incss/01-base.cssalone desynchronises them. Note also that the hero's button colours come from:nth-childrules, so a colour belongs to a position on the arc, not to a section — see docs/layout.md. #main-pageis the scroll container, not the window. It isposition: fixed; inset: 0; overflow-y: auto. Anything scroll-related listens on it;IntersectionObserverneedsroot: mainPage.css/is one stylesheet cut into numbered parts, and the number is the cascade. A rule loses to the same-specificity rule in any later file. Add a file where it belongs in the order, never at the end for convenience, and put a rule in the part that owns the component.- Relative
url()resolves against the stylesheet, not the page — fromcss/an image isurl("../img/…"). [hidden]loses to adisplayrule. Any component withdisplay: flexorgridneeds its own[hidden] { display: none }, or pagination and tab switching silently show everything.:not()carries its argument's specificity, so a blanket child selector can outrank rules it was never meant to touch.<base href="./">inindex.htmlis mandatory and must stay relative. The path moves as the reader navigates, so every URL built afterwards would otherwise resolve against the new directory.generate_pages.pyrefuses to run without it.- Every URL write is a
replaceState— no history entry, nohashchange, so a module can never re-enter its own route handler. - Nothing may key off a fraction of the scroll height. Opening the Research
zoom's gate changes
scrollHeightby ~800vh. Test a section's own rect. - Scroll handlers go through
shared/scroll-loop.js, one coalesced animation frame for the whole page, and return early when the value has not changed. - Encode every image to the size it is actually displayed at, doubled. The per-use budget is in docs/assets.md. The site once pulled 122 MB on a cold load.
.gitignore: name the generated files, never a folder that also holds source. An ignored source file is invisible locally and 404s only on the deployed site. When an image "does not show" in production but works locally, rungit check-ignore -v <path>first.- Pages must be set to Settings → Pages → Source: GitHub Actions, or the ~250 generated pages are never served and nothing anywhere says so. See docs/build-and-deploy.md.
- The Python scripts must survive both a cp1252 console and a notebook.
sys.stdout.reconfigure(encoding="utf-8")behind ahasattr(a notebook'sOutStreamhas no such method), and__file__behind atry(a cell has no__file__). Without the first they do the work and then die on their own summary line, which reads exactly like a failure to write the manifest. See docs/build-and-deploy.md.
There are no tests. Verify in a browser, and prefer computed styles and geometry over screenshots — faster, and exact:
getComputedStyle(el).position
el.getBoundingClientRect()python -m http.server sends no cache headers, so browsers serve stale JS
and CSS while you debug a fix that already landed. Before assuming the code is
wrong: fetch("/script.js", { cache: "reload" }), then reload.
Several things look exactly like real bugs and are not:
requestAnimationFramenever fires, becausedocument.visibilityStateis permanently"hidden". Sincescroll-loop.jscoalesces every scroll handler into one rAF, nothing on the page reacts to scrolling until it is stubbed.- CSS transitions and animations never advance, so a transitioned property
reads as its start value — which looks identical to a selector that does
not match. Inject
* { transition: none !important }, read, then remove. IntersectionObserverandResizeObservercallbacks do not fire.loading="lazy"images never load — no request at all,naturalWidth0.fetch()the path to prove it serves, then setimg.loading = "eager".scrollTo({behavior: "smooth"})does not move anything. Re-issue withbehavior: "instant".- Screenshots are unavailable for the same reason frames are.
The four lines that make scroll-driven work testable there:
document.getElementById("main-page").classList.add("visible") // then remove #door-screen
mainPage.style.scrollBehavior = "auto" // else scrollTop is ignored
window.requestAnimationFrame = (fn) => setTimeout(() => fn(performance.now()), 0)
window.dispatchEvent(new Event("resize")) // stands in for a scrollThen drive real events and await a tick:
mp.scrollTop = 5000
mp.dispatchEvent(new Event("scroll"))
await new Promise((r) => setTimeout(r, 40))A javascript_tool call aborted mid-flight can leave the stubbed loop wedged
with a frame scheduled and never run; reload rather than debugging the symptom.
Colours are color-mixed throughout and come back as
color(srgb 0.13 0.48 0.5) — reading those floats as 0–255 reports false
failures. Composite onto a 1×1 canvas and let the browser parse:
ctx.fillStyle = background; ctx.fillRect(0,0,1,1)
ctx.fillStyle = foreground; ctx.fillRect(0,0,1,1)
const [r,g,b] = ctx.getImageData(0,0,1,1).data--debug-margins in :root (css/01-base.css) outlines sections and content
containers. Set it to 1 while working on spacing.