/** * The last word of the hero's pitch, which does not stay still. * * "brag about it on Bluesky" is true and is also the one sentence on the front * page that can leave a stranger with the wrong idea — that Bluesky is the * thing lance.blue is attached to. It is not: any Atmosphere account works, * which is what the sign-in form already says ("Supports Bluesky, Blacksky, or * any other ATproto PDS"). So the word rolls through hosts a player could * really be arriving from, and ends on their own server. * * The list will go stale. Hosts open and hosts close, and nothing here notices * — somebody has to check. Two rules for anything added: a player must be able * to actually hold an account there, and it has to be about as long as the * names already here, because the box is as wide as the widest of them and * every extra character is paid for by the whole paragraph. * * The motion is in styles.css with the rest of the hero's. This file only * decides when to swap, and it does not run at all for a player who has asked * for less motion: something that never stops is what that setting is for, so * they get the first name, sitting still. */ import { el } from "./dom"; /** * Checked 2026-08-07 against the community PDS hosting list at * discourse.atmosphere.community and the hosts named on atmosphereaccount.com, * which is where the sign-in form's heading already sends people. * * The first three are what the order is about: Bluesky, because it is the one * a stranger recognises and it is what the sentence said before it started * moving; then mu.social; then the player's own machine, which is the point * being made. The rest follow, arranged only so that no two of them look alike * in a row — Blacksky and Northsky are both purple, Sprk and mu.social are both * pink. * * The full stop belongs to each name rather than to the sentence. The names are * stacked in one box, so anything after them would have to sit at the far edge * of the widest one, stranded behind a gap after every short name. * * Each name wears its host's own colours, sampled off their sites on * 2026-08-07. `ink` is the type and `glow` is the halo feathered behind it. * * Sample the whole page, not the part above the fold. Two mistakes here were * that. Blacksky came out lime, which is only their bracketed links over a page * that is black and periwinkle four to one; Tangled came out green, which is * one button on a page that is slate and white by two orders of magnitude. * * Both colours have to come off the site. Where a host has two of their own * that is the pair: Sprk's green against their pink, Blacksky's lime against * their periwinkle, Tangled's green against their slate. Bluesky, mu.social and * Northsky are each one colour and nothing else, and an earlier set gave them a * complement off the wheel instead — amber, cyan, gold. It read as invented, * because it was; nothing on bsky.app is amber. So those three take a pale tint * of their own colour as the ink and that colour at full strength as the halo, * which reads as the brand lighting the word from behind rather than as a * second brand nobody has. * * "your PDS" is the one entry that is nobody else's, so it gets ours: the * masthead's teal lit by the accent blue. * * Both are light-dark() pairs. The theme control has to be able to change them, * and light needs the opposite of what dark needs: on a dark page a halo * carries by being lighter than the ground, on a light one by being darker. * * Every glow is saturated and none is a pastel. A pale colour blurred over a * dark background has almost no chroma left to show and comes out white, which * is what an earlier set of these all looked like. */ const HOSTS = [ { name: "Bluesky.", ink: "light-dark(#0a5fd0, #93c8ff)", glow: "light-dark(#0069cc, #0085ff)", }, { name: "mu.social.", ink: "light-dark(#a52d7c, #f9b3e2)", glow: "light-dark(#db4aa6, #e0369f)", }, { name: "your PDS.", ink: "var(--brand)", glow: "var(--accent)", }, { name: "Sprk.", ink: "light-dark(#c4006e, #ff3da6)", glow: "light-dark(#0b6b32, #00c853)", }, { name: "Blacksky.", ink: "light-dark(#4a4ad4, #8b8bff)", glow: "light-dark(#5f7a00, #b8e000)", }, { name: "Tangled.", ink: "light-dark(#3b4d63, #b9cce6)", glow: "light-dark(#0f6b32, #16b356)", }, { name: "Northsky.", ink: "light-dark(#6d28d9, #cdb8ff)", glow: "light-dark(#7325d6, #8338ec)", }, ]; /** Long enough that the sentence gets read as written before anything moves. */ const FIRST_HOLD_MS = 1700; const HOLD_MS = 2250; export function hostCycle(): HTMLElement { // Every name, all in the one grid cell, all hidden but for the one being // shown. Hidden is `visibility`, not `display`, so they all still size the // box: it is as wide as the longest name from the first paint, and the // paragraph around it wraps the same way whichever name is up. Measuring // instead would mean measuring again after the webfont lands, and being // wrong until it did. const words = HOSTS.map((host) => { const word = el("span", { className: "cycle-word", textContent: host.name, }); // Set here rather than in styles.css: this is the host's colour, which is // data about the host, not a decision about how the page looks. word.style.setProperty("--host-ink", host.ink); word.style.setProperty("--host-glow", host.glow); return word; }); const cycle = el("span", { className: "hero-cycle" }, words); let showing = words[0]!; showing.className = "cycle-word cycle-here"; if (!matchMedia("(prefers-reduced-motion: no-preference)").matches) { return cycle; } let index = 0; const tick = (): void => { // The nav can replace this screen at any point, and a detached node is the // only notice of it we get. Returning without booking the next tick ends // the rotation for good. if (!cycle.isConnected) return; // Nothing to see, so nothing to spend: hold the current name and look again // later rather than rolling through the list into a hidden tab. if (document.visibilityState === "visible") { index = (index + 1) % words.length; const arriving = words[index]!; // The names that have already had their turn keep cycle-out, and the end // of that animation is holding them hidden. Nothing has to put them back. showing.className = "cycle-word cycle-out"; arriving.className = "cycle-word cycle-in"; showing = arriving; } setTimeout(tick, HOLD_MS); }; setTimeout(tick, FIRST_HOLD_MS); return cycle; }