feat(marketing): phase 4 — the marketing pages
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s

PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and
/community/ — plus the two scope items the phase table never assigned to anyone.

Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as
D20-D25:

- D20 /features/ is the homepage's list with a `detail` line, not a second list.
  One data file, two renderings, so they cannot disagree about what exists.
- D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per
  boundary. No endpoint tables, no config keys — those are phase 8's and stay
  canonical in docs/.
- D22 The deliberate absences of §2 become one tagged data file, rendered on the
  three pages that promise them.
- D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the
  header since phase 1, built by no phase) and checkLinks.mjs.
- D24 `needsModule`: writing the Teams detail exposed a false claim phase 3
  shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is
  no create route, sync is gated on providerModuleId() — so the Community group
  no longer says a bare core does all of it.
- D25 The per-capability demo affordance brand.json had promised since phase 2 is
  a deep link, filled at boot from data-demo-path.

checkLinks.mjs reads the built HTML rather than src/, because half these links
are assembled from data files and template literals. Its PLANNED_ROUTES list is
checked in both directions, so it cannot rot into a permanent exemption.

applyBrand.mjs gained a pass that recomputes deep links from their immutable
path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out
and runs it against the stock markup so the two cannot drift. Both proved
against a real mount, in both directions.

Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped
component class are both specificity 0,1,0, so .demo-link's `display` beat the
hide rule and twelve links to a nonexistent demo rendered, each resolving to the
current page. The rule is now !important.

The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css.

Verified from a clean checkout: npm ci, all five checks, astro check (0 errors),
production build, and a live browser pass at desktop and 390px.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-24 01:53:11 -05:00
parent d9d7a8d47f
commit 2d19ee4220
21 changed files with 3332 additions and 119 deletions

View File

@@ -0,0 +1,132 @@
---
import { notBuiltFor, assertScopeNonEmpty } from '../data/notBuilt.mjs';
/**
* The deliberate absences (PLAN.md §2, D22), rendered for one page's scope.
*
* §2 describes its absent-features list as "as load-bearing as the rest", and this is the
* component that makes that true on a page rather than in a plan. It reads the shared list
* so `/features/`, `/integrations/` and `/modules/` cannot drift into telling three
* different stories about the same six things.
*
* ---------------------------------------------------------------------------------------
* WHY IT LOOKS LIKE THE REST OF THE PAGE
* ---------------------------------------------------------------------------------------
* Not a warning box, not a muted footnote, not an accordion. D8's "understated honesty" is
* a house style with a specific consequence here: a section that is visually apologetic
* teaches a reader that absences are embarrassing, and a section that is visually hidden
* teaches them to go looking for the ones you did not mention. These are decisions with
* reasons, so they are set as decisions with reasons — the same panels as everything else,
* in the same place in the rhythm.
*
* The one visual difference is the `resolvedBy` line, which every entry carries. An absence
* with an exit condition is a position; an absence without one is a hole. D8 gives the
* Integration Kit's draft status a defined removal condition and this generalises it.
*/
interface Props {
/** Which page is asking: `features`, `integrations` or `modules`. */
scope: string;
/** Section heading. Each page frames the same list for its own reader. */
title: string;
}
const { scope, title } = Astro.props;
assertScopeNonEmpty(scope);
const entries = notBuiltFor(scope);
---
<section class="page section notbuilt">
<p class="eyebrow">Not built</p>
<h2>{title}</h2>
<p class="prose notbuilt__lede">
Every one of these is a decision rather than a backlog item, so each says why. Where the
reasoning was written down in the open, it is linked.
</p>
<ul class="notbuilt__grid">
{
entries.map((entry) => (
<li class="panel notbuilt__item">
<h3>{entry.title}</h3>
<p class="notbuilt__body">{entry.body}</p>
<p class="notbuilt__resolved">
<span class="notbuilt__resolved-label">What would change it</span>
{entry.resolvedBy}
</p>
{entry.link && (
<p class="notbuilt__link">
<a href={entry.link.href} rel="noopener noreferrer">
{entry.link.label}
</a>
</p>
)}
</li>
))
}
</ul>
</section>
<style>
.notbuilt h2 {
margin: 0 0 0.75rem;
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
}
.notbuilt__lede {
margin: 0;
color: var(--muted);
}
.notbuilt__grid {
display: grid;
gap: 1rem;
margin: 2.25rem 0 0;
padding: 0;
list-style: none;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}
.notbuilt__item {
display: flex;
flex-direction: column;
}
.notbuilt__item h3 {
margin: 0 0 0.6rem;
color: var(--gold);
font-size: 1.02rem;
}
/* Takes the slack, so the exit condition sits at the foot of every card in a row
rather than immediately under a body of whatever length — the same kind of
statement in the same place on each, which is what makes them readable as a row. */
.notbuilt__body {
flex: 1;
margin: 0;
color: var(--muted);
font-size: 0.94rem;
}
.notbuilt__resolved {
margin: 1rem 0 0;
padding-top: 0.85rem;
border-top: 1px solid var(--line-soft);
color: var(--dim);
font-size: 0.88rem;
}
.notbuilt__resolved-label {
display: block;
color: var(--muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.11em;
text-transform: uppercase;
}
.notbuilt__link {
margin: 0.85rem 0 0;
font-size: 0.88rem;
}
</style>

View File

@@ -0,0 +1,55 @@
---
/**
* The opening of every marketing page except the homepage — eyebrow, `<h1>`, lede.
*
* A component rather than four copies of the same three elements, because phase 4 writes
* five pages and phases 5 and 6 write four more. The homepage is deliberately not one of
* them: its `<h1>` is the tagline inside the hero, set against the emblem, and pulling that
* into a shared header would either flatten the hero or push its layout in here (D19).
*
* The `<h1>` is the page's own name, not the product's, and `Base` appends the site name to
* the document title — so a page sets a short `title` and gets "Features — Runic Gateway"
* in the tab and "Features" on the page.
*/
interface Props {
/** Small uppercase line above the title. What kind of page this is. */
eyebrow: string;
title: string;
}
const { eyebrow, title } = Astro.props;
---
<header class="page section pagehead">
<p class="eyebrow">{eyebrow}</p>
<h1>{title}</h1>
<div class="prose pagehead__lede">
<slot />
</div>
</header>
<style>
/* The section rhythm gives generous space below; the header wants less, because the
first section under it is part of the same thought. */
.pagehead {
padding-bottom: clamp(1rem, 2.5vw, 1.75rem);
}
.pagehead h1 {
margin: 0 0 1rem;
font-size: clamp(2rem, 5vw, 2.9rem);
}
.pagehead__lede {
color: var(--muted);
font-size: 1.06rem;
}
.pagehead__lede :global(p) {
margin: 0 0 0.85rem;
}
.pagehead__lede :global(p:last-child) {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,142 @@
---
/**
* "What reaches the public" — the second of `/architecture/`'s three diagrams (D21).
*
* The homepage states the split in one sentence inside the data-path walk ("a public one
* carrying an allowlist of safe events, and a staff-only one carrying the rest… that split
* is a security boundary, not a preference"). This is the page where that sentence has to
* become a picture, because it is the single design decision a technical evaluator is most
* entitled to be suspicious of: a live feed of a game world contains things that must never
* be published, and "we filter it" is a claim, not a mechanism.
*
* So the diagram draws the shape of the mechanism — one stream in, one decision, two streams
* out — and the notes say where the decision lives and what happens when it is wrong in
* either direction. What it deliberately does NOT do is enumerate event kinds: that is the
* catalog's job in the docs, it changes with the protocol, and a marketing page holding a
* copy of it would be a copy that goes stale (§1).
*
* The rings sit behind the filter rather than behind the whole picture, on the phase-3
* principle that they mark the one place the argument actually happens.
*/
---
<section class="page section diagram" id="allowlist">
<div class="diagram__head">
<p class="eyebrow">What reaches the public</p>
<h2>One feed in, two feeds out</h2>
<p class="prose">
A live game world emits things that are fine on a front page and things that are not:
who logged in from which address, what the cheat detector flagged, what a staff member
did to whom. Both arrive on the same connection, so something has to divide them.
</p>
</div>
<div class="diagram__body">
<div class="diagram__figure">
<svg viewBox="0 0 380 470" class="flow" aria-hidden="true" focusable="false">
<!-- Centred on the filter: the one place in the picture where the argument is. -->
<g class="rings">
<circle cx="190" cy="178" r="96" />
<circle cx="190" cy="178" r="136" />
<circle cx="190" cy="178" r="176" />
</g>
<rect class="node" x="20" y="12" width="340" height="60" rx="10" />
<text class="node-title" x="40" y="38">Everything the game emits</text>
<text class="node-sub" x="40" y="58">one authenticated stream, from the sidecar</text>
<path class="spine spine--live" d="M190 80 V132" />
<path class="arrow arrow--live" d="M190 140 l-6 -10 h12 Z" />
<rect class="node node--self" x="20" y="142" width="340" height="72" rx="10" />
<text class="node-title" x="40" y="172">Your site decides</text>
<text class="node-sub" x="40" y="192">one allowlist, in one place, on your server</text>
<!-- Diverging: the public leg in cyan because it is still a live feed; the staff
leg in gold because it is the privileged one. -->
<path class="spine spine--live" d="M120 222 C120 268 96 268 96 306" />
<path class="arrow arrow--live" d="M96 314 l-6 -10 h12 Z" />
<path class="spine" d="M260 222 C260 268 284 268 284 306" />
<path class="arrow" d="M284 314 l-6 -10 h12 Z" />
<rect class="node" x="8" y="316" width="176" height="128" rx="10" />
<text class="node-title" x="26" y="344">Public pages</text>
<text class="node-sub" x="26" y="366">an allowlist of event</text>
<text class="node-sub" x="26" y="382">kinds, and nothing</text>
<text class="node-sub" x="26" y="398">outside it</text>
<text class="node-audience" x="26" y="424">anyone at all</text>
<rect class="node" x="196" y="316" width="176" height="128" rx="10" />
<text class="node-title" x="214" y="344">Staff console</text>
<text class="node-sub" x="214" y="366">the rest: audit trail,</text>
<text class="node-sub" x="214" y="382">login attempts,</text>
<text class="node-sub" x="214" y="398">addresses, cheat flags</text>
<text class="node-audience" x="214" y="424">signed-in staff only</text>
</svg>
<p class="diagram__caption">
The allowlist is the security boundary. A new kind of event is invisible to the public
until somebody adds it, which is the safe direction to fail in.
</p>
</div>
<div class="diagram__notes">
<section>
<h3>It is an allowlist, not a blocklist</h3>
<p>
The public stream carries the kinds of event that are named as safe; everything else
goes to the staff stream by default. That ordering is the whole point. A blocklist
fails open — the day the game emits something new, it is already published — and an
allowlist fails closed, so the worst case is a page that is missing something rather
than a page that has published an address.
</p>
</section>
<section>
<h3>The decision lives on your server</h3>
<p>
Not in the sidecar and not in the game. The bridge is a deliberately dumb forwarder:
it moves what the game emits and makes no judgements about audience. Everything
about who may see what is decided by the site you run, in one place, where you can
read it — and where changing it does not mean redeploying anything on the game host.
</p>
</section>
<section>
<h3>More than two audiences, in practice</h3>
<p>
Two streams is the transport. Above it sits a configurable audience model — logged
out, signed in, linked to a game account, staff — that decides how much of a given
surface each of those sees. The public stream is the floor of that, and it is the
one that is a boundary rather than a setting.
</p>
</section>
<section>
<h3>When the game is down</h3>
<p>
Nothing arrives, and the site carries on. Live surfaces say the server is offline
and everything that does not depend on it — the wiki, the news, accounts, the forums
— is unaffected. A site that goes down with the game it reports on is not much of a
status page.
</p>
</section>
</div>
</div>
</section>
<style>
/* Each outcome node ends with a line naming its audience, set apart from the
description above it rather than reading as another line of it.
Its own class, not `:nth-last-of-type`: an index into a list of `<text>`
siblings is correct only until somebody adds a label, and it fails by
styling the wrong words rather than by failing. */
.node-audience {
fill: var(--muted);
font-family: var(--sans);
font-size: 11.5px;
font-style: italic;
}
</style>

View File

@@ -0,0 +1,166 @@
---
import platform from '../../data/platform.json';
/**
* "Where the game stops and the platform starts" — the third of `/architecture/`'s diagrams
* (D21).
*
* The other two draw runtime shapes. This one draws a code boundary, and it is here because
* it is the claim the whole project rests on: that a community platform can be built once
* and pointed at any game. An evaluator has every reason to read that as marketing, so the
* page draws the seam and then says plainly what does and does not prove it — one module
* exists, the second is a paper exercise, and the exit criterion for calling the contract
* proven is written down (§2, and the entries `/modules/` renders from `notBuilt.mjs`).
*
* The Module API version is read from `platform.json` like every other number on this site
* (§12). It is the one place a version genuinely belongs in this diagram: the seam is
* literally a version check, and a module whose declared range does not match refuses to
* load rather than half-loading.
*/
---
<section class="page section diagram" id="module-seam">
<div class="diagram__head">
<p class="eyebrow">Where the game stops</p>
<h2>A seam, with a version on it</h2>
<p class="prose">
The core site does not know what a shard is, what a guild is, or that Ultima Online
exists. Everything that does lives in an installable module on the other side of a
declared interface — which is what makes "put your game on it" a shape rather than a
slogan.
</p>
</div>
<div class="diagram__body">
<div class="diagram__figure">
<svg viewBox="0 0 380 500" class="flow" aria-hidden="true" focusable="false">
<!-- Core: what ships in the image, on every deployment, module or not. -->
<rect class="host" x="8" y="8" width="364" height="186" rx="14" />
<text class="host-title" x="28" y="42">Runic Gateway core</text>
<text class="host-sub" x="28" y="62">game-agnostic; the same image everywhere</text>
<rect class="node node--self" x="28" y="80" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="108">Accounts</text>
<rect class="node node--self" x="196" y="80" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="108">Teams</text>
<rect class="node node--self" x="28" y="134" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="162">Wiki and posts</text>
<rect class="node node--self" x="196" y="134" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="162">Admin and API</text>
<!-- The seam. Both boundary lines and the label between them: this is the one
thing in the picture that is neither core nor module. -->
<path class="boundary" d="M8 224 H372" />
<text class="seam-label" x="190" y="252" text-anchor="middle">
Module API {platform.moduleApi}
</text>
<path class="boundary" d="M8 272 H372" />
<!-- Registers upward; is asked downward. Two arrows, opposite directions, because
the traffic across a seam is not one-way and drawing it as one-way is what
makes people think a module is a plugin that only listens. -->
<path class="spine" d="M120 300 V206" />
<path class="arrow" d="M120 198 l-6 10 h12 Z" />
<text class="seam-arrow" x="136" y="216">registers</text>
<path class="spine" d="M260 200 V294" />
<path class="arrow" d="M260 302 l-6 -10 h12 Z" />
<text class="seam-arrow" x="244" y="290" text-anchor="end">calls</text>
<!-- The module: everything that knows a game exists. -->
<rect class="host" x="8" y="306" width="364" height="186" rx="14" />
<text class="host-title" x="28" y="340">Game module</text>
<text class="host-sub" x="28" y="360">one per deployment; UO today</text>
<rect class="node" x="28" y="378" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="406">Routes</text>
<rect class="node" x="196" y="378" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="406">Screens</text>
<rect class="node" x="28" y="432" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="460">Its own tables</text>
<rect class="node" x="196" y="432" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="460">Nav rows</text>
</svg>
<p class="diagram__caption">
A module declares which versions of the interface it speaks. If that does not match
what the site offers, it refuses to load and the site comes up without it.
</p>
</div>
<div class="diagram__notes">
<section>
<h3>The module brings its own everything</h3>
<p>
Not just screens: its routes, its database tables, its navigation rows, its slice of
the OpenAPI spec and its own prebuilt client bundle. Installing it is a paste in the
admin panel or a line in your environment — never a build step, because production
runs an image you pulled, and an operator who has to compile something has been
handed a maintenance job rather than a feature.
</p>
</section>
<section>
<h3>Failure is contained by design</h3>
<p>
A module that will not load is marked as failed and the site starts without it.
Disabling one is a kill switch, not a visibility flag — its routes stop answering
and its live connections close. Uninstalling keeps the data, and destroying the data
is a separate, deliberate choice made in its own dialog.
</p>
</section>
<section>
<h3>Teams is the shape of the contract</h3>
<p>
Core owns the Teams primitive — the roster, the forum, the notifications, the voice
channel — and does not own the <em>word</em>. A Team cannot be created in core at
all; it arrives from the module, which is why the UO module calls them guilds and
builds those pages itself. That is the pattern the whole interface is built on: core
supplies the machinery, the module supplies the meaning.
</p>
</section>
<section>
<h3>What this does not yet prove</h3>
<p>
One module exists and it is Ultima Online. A second, for a different game, is a
written dry-run that was deliberately never implemented — it exists to test whether
the contract generalises on paper. Until somebody builds the second one, the seam is
a well-argued design rather than a demonstrated one, and this site says so wherever
it comes up.
</p>
</section>
</div>
</div>
</section>
<style>
/* The seam label sits between the two boundary rules rather than beside them: it is
the name of the gap, not an annotation on either side of it. Gold, because it is
the one contract in the picture. */
.seam-label {
fill: var(--gold);
font-family: var(--sans);
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* Two words, because two arrows crossing a boundary in opposite directions is
ambiguous without them — and the ambiguity is the exact misreading this diagram
exists to prevent, that a module is something core talks at. */
.seam-arrow {
fill: var(--dim);
font-family: var(--sans);
font-size: 11px;
font-style: italic;
}
</style>

View File

@@ -0,0 +1,122 @@
---
/**
* "What you actually deploy" — the first of `/architecture/`'s three diagrams (D21).
*
* This one exists because of a specific, repeated misunderstanding that §10 names and the
* homepage's CTA already spends two sentences on: a Runic Gateway install is two
* independent installs, on two machines, and neither installs the other. The homepage says
* it; this page draws it, because an evaluator deciding whether to run the software is
* doing capacity planning, and "how many machines is this" is the first question they have.
*
* Drawn generically for the same reason the homepage's diagram is (D17) — "your game host",
* not "your ServUO box" — with the prose beside it naming the real components. The boundary
* is the one drawn argument: everything above it is reachable because you published it, and
* everything below it is not reachable at all.
*
* The vocabulary and the layout are `src/styles/diagram.css`; only the geometry is here.
*/
---
<section class="page section diagram" id="two-hosts">
<div class="diagram__head">
<p class="eyebrow">What you deploy</p>
<h2>Two hosts, two installs</h2>
<p class="prose">
Almost everyone gets this wrong once. The website and the game-side bridge are separate
deployments on separate machines, and neither one installs the other — so a "Runic
Gateway install" is really two, done in that order.
</p>
</div>
<div class="diagram__body">
<div class="diagram__figure">
<svg viewBox="0 0 380 546" class="flow" aria-hidden="true" focusable="false">
<!-- The web host, and everything that runs on it. -->
<rect class="host" x="8" y="8" width="364" height="232" rx="14" />
<text class="host-title" x="28" y="42">Your web host</text>
<text class="host-sub" x="28" y="62">a VPS, a home server, anything running Docker</text>
<rect class="node node--self" x="28" y="80" width="324" height="60" rx="10" />
<text class="node-title" x="46" y="106">Runic Gateway</text>
<text class="node-sub" x="46" y="126">one container, pulled not built</text>
<rect class="node" x="28" y="150" width="156" height="60" rx="10" />
<text class="node-title" x="46" y="176">Game module</text>
<text class="node-sub" x="46" y="196">installed, not built</text>
<rect class="node" x="196" y="150" width="156" height="60" rx="10" />
<text class="node-title" x="214" y="176">Database</text>
<text class="node-sub" x="214" y="196">your data, your disk</text>
<!-- The one hop between them, and the only one. Two arrowheads because the traffic
genuinely goes both ways: the site calls the sidecar for point-in-time reads,
and the sidecar pushes the live feed back up. -->
<path class="spine spine--live" d="M190 248 V312" />
<path class="arrow arrow--live" d="M190 240 l-6 10 h12 Z" />
<path class="arrow arrow--live" d="M190 320 l-6 -10 h12 Z" />
<path class="boundary" d="M8 280 H372" />
<text class="boundary-label" x="372" y="273" text-anchor="end">the network</text>
<!-- The game host. Nothing here is reachable from outside except the sidecar. -->
<rect class="host" x="8" y="320" width="364" height="214" rx="14" />
<text class="host-title" x="28" y="354">Your game host</text>
<text class="host-sub" x="28" y="374">where the game server already runs</text>
<rect class="node" x="28" y="392" width="324" height="60" rx="10" />
<text class="node-title" x="46" y="418">Sidecar</text>
<text class="node-sub" x="46" y="438">the only part of this with a port open</text>
<rect class="node" x="28" y="462" width="324" height="60" rx="10" />
<text class="node-title" x="46" y="488">Game server</text>
<text class="node-sub" x="46" y="508">dials out over loopback; listens for nothing</text>
</svg>
<p class="diagram__caption">
Today the game server is a ServUO shard and the sidecar is uo-link. Two machines is
the minimum and also the maximum — nothing here scales by adding a third.
</p>
</div>
<div class="diagram__notes">
<section>
<h3>The web host</h3>
<p>
A Docker Compose deployment: the site, its database, and whichever game module you
installed. Images are pulled rather than built, so nothing compiles here and an
upgrade is a pull and a restart. This is the only machine anybody points a browser
at, and the only one that needs a certificate.
</p>
</section>
<section>
<h3>The game host</h3>
<p>
The machine your game server is already on. One installer binary puts the plugin
into the server's tree, installs the sidecar beside it and registers the service —
then prints four values. It never contacts your website; you paste those four
values into the admin panel yourself, and that is the moment the two halves meet.
</p>
</section>
<section>
<h3>Why they share a host</h3>
<p>
The game talks to the sidecar over loopback, on the same machine, and dials
<em>out</em> to do it. That is what lets the game server open no port at all — and it
is also why there is no macOS installer build. The pair has to sit together, and no
game server anybody runs is on one.
</p>
</section>
<section>
<h3>What crosses between them</h3>
<p>
One authenticated connection, in both directions: a WebSocket carrying the live feed
up, and REST calls going down for point-in-time questions. Nothing else on either
machine talks to the other, and the sidecar answers your site and nobody else.
</p>
</section>
</div>
</div>
</section>

View File

@@ -159,12 +159,6 @@ import platform from '../../data/platform.json';
top: calc(var(--header-h) + 1.5rem);
}
.flow {
display: block;
width: 100%;
max-width: 380px;
}
.datapath__caption {
margin: 1rem 0 0;
max-width: 380px;
@@ -172,75 +166,14 @@ import platform from '../../data/platform.json';
font-size: 0.85rem;
}
/* ---- The drawing ------------------------------------------------------
SVG presentation attributes cannot take a var(), so every colour here is
set as a CSS property on a class instead. That is also what keeps
checkTokens.mjs satisfied: no literal reaches the markup. */
.node {
fill: var(--panel-b);
stroke: var(--line);
stroke-width: 1;
}
/* The SVG vocabulary this diagram draws with -- .node, .spine, .arrow,
.boundary, .rings -- now lives in src/styles/diagram.css, shared with
/architecture/'s three. It was duplicated in four files the moment the
second diagram existed, and the rules it holds are decisions about what a
diagram on this site looks like rather than about this one.
.node--self {
fill: var(--panel-a);
stroke: var(--gold-deep);
}
.node-title {
fill: var(--head);
font-family: var(--sans);
font-size: 17px;
font-weight: 600;
}
.node-sub {
fill: var(--dim);
font-family: var(--sans);
font-size: 12.5px;
}
.spine {
fill: none;
stroke: var(--gold-deep);
stroke-width: 2;
}
.spine--live {
stroke: var(--portal);
filter: drop-shadow(0 0 6px var(--portal-deep));
}
.arrow {
fill: var(--gold-deep);
stroke: none;
}
.arrow--live {
fill: var(--portal);
}
.boundary {
fill: none;
stroke: var(--line);
stroke-width: 1;
stroke-dasharray: 4 5;
}
.boundary-label {
fill: var(--dim);
font-family: var(--sans);
font-size: 11px;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.rings {
fill: none;
stroke: var(--gold-deep);
stroke-width: 1;
opacity: 0.16;
}
The layout below stays here: the right-hand column is a numbered walk,
not the notes column .diagram__body assumes. */
/* ---- The list ---------------------------------------------------------- */
.datapath__steps {