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

194
src/styles/diagram.css Normal file
View File

@@ -0,0 +1,194 @@
/* ============================================================================
The diagram vocabulary
============================================================================
§11 makes hand-drawn SVG the site's motif, "used where it explains something".
Phase 3 drew the first one on the homepage; phase 4 drew three more on
`/architecture/`, at which point the same fifteen rules existed in four files.
Two things live here and nothing else does:
1. The SVG vocabulary — what a node, a spine, an arrow and the boundary look
like. Shared by name, so a diagram is markup and the drawing is one
decision. `DataPath.astro` reads these too; it keeps its own layout,
because its right-hand column is a numbered walk rather than notes.
2. The `.diagram` layout — figure beside prose on a wide screen, figure
above prose on a narrow one.
Every colour is a class rather than a presentation attribute, and that is not
a style preference: `var()` is only substituted in style declarations, so
`fill="var(--line)"` on an element parses and draws nothing at all. It is also
what keeps `checkTokens.mjs` green, since no literal ever reaches the markup.
The two rules every diagram here follows, learned in phase 3:
- An inline SVG cannot reflow. A tall, ~380px-wide viewBox with only node
titles inside it is legible on a phone AND useful at 1440px; a wide
horizontal diagram is neither.
- The picture is `aria-hidden` because the prose beside it says the same
thing better. The consequence is a rule: a diagram must never carry a fact
the prose does not.
-------------------------------------------------------------------------- */
/* ---- Layout ------------------------------------------------------------- */
.diagram__head h2 {
margin: 0 0 0.75rem;
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
}
.diagram__head .prose {
margin: 0;
color: var(--muted);
}
.diagram__body {
display: grid;
gap: clamp(1.75rem, 4vw, 3rem);
margin-top: 2.5rem;
grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
align-items: start;
}
.diagram__figure {
position: sticky;
top: calc(var(--header-h) + 1.5rem);
}
.diagram__caption {
margin: 1rem 0 0;
max-width: 380px;
color: var(--dim);
font-size: 0.85rem;
}
.diagram__notes section + section {
margin-top: 1.5rem;
}
.diagram__notes h3 {
margin: 0 0 0.4rem;
color: var(--gold);
font-size: 1.06rem;
}
.diagram__notes p {
margin: 0;
max-width: var(--measure);
color: var(--muted);
}
@media (max-width: 900px) {
.diagram__body {
grid-template-columns: minmax(0, 1fr);
}
/* Sticky is a wide-screen affordance only. Once the figure sits above the
prose rather than beside it, pinning it would cover the thing it explains. */
.diagram__figure {
position: static;
justify-self: center;
}
}
/* ---- The drawing -------------------------------------------------------- */
.flow {
display: block;
width: 100%;
max-width: 380px;
}
/* An outer grouping: a machine, a process boundary, a side of a contract. Sits
under the nodes it contains, so it reads as the thing they are inside. */
.host {
fill: var(--panel-flat);
stroke: var(--line-soft);
stroke-width: 1;
}
.host-title {
fill: var(--head);
font-family: var(--sans);
font-size: 16px;
font-weight: 600;
}
.host-sub {
fill: var(--dim);
font-family: var(--sans);
font-size: 11.5px;
}
.node {
fill: var(--panel-b);
stroke: var(--line);
stroke-width: 1;
}
/* The one node that is the reader's own site. Gold edge, because gold is
emphasis everywhere else on the site too. */
.node--self {
fill: var(--panel-a);
stroke: var(--gold-deep);
}
.node-title {
fill: var(--head);
font-family: var(--sans);
font-size: 15px;
font-weight: 600;
}
.node-sub {
fill: var(--dim);
font-family: var(--sans);
font-size: 11.5px;
}
.spine {
fill: none;
stroke: var(--gold-deep);
stroke-width: 2;
}
/* Cyan is the live signal everywhere on this site — the same colour the portal
in the emblem is, and the same one the homepage draws the event feed in. A
spine in this colour means data actually moving, not a relationship. */
.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;
}
/* The emblem's concentric rings, used as a ground behind the one place a
diagram's argument actually happens. */
.rings {
fill: none;
stroke: var(--gold-deep);
stroke-width: 1;
opacity: 0.16;
}

View File

@@ -8,6 +8,12 @@
@import '@fontsource-variable/cinzel';
@import '@fontsource-variable/inter';
/* The SVG diagram vocabulary and the figure-beside-prose layout, shared by the
homepage's data path and `/architecture/`'s three. Its own file because it is
a self-contained language rather than part of the shell — see its header for
the two rules every diagram on this site follows. */
@import './diagram.css';
*,
*::before,
*::after {
@@ -437,8 +443,25 @@ svg {
Written here, before phase 3 writes that markup, because the rule and the
rewrite have to agree and they live in different files. */
/* `!important`, and it is earning its keep rather than papering over something.
This selector is specificity 0,1,0. So is a class — including the scoped class an
Astro component puts on the very same element — and a component's styles are emitted
AFTER this file, so any component that gives one of these elements a `display` wins on
source order alone. Phase 4 did exactly that: `/features/`'s `.demo-link` set
`display: inline-flex` for its arrow, and twelve links to a demo that does not exist
appeared on the page, each one pointing at `href=""` — which a browser resolves to the
page it is already on.
Nothing caught it. checkBrand.mjs verifies the ATTRIBUTES, and they were perfect; the
defect was three files away in the cascade. It was found by looking at the rendered
page, which is not a mechanism.
So the rule is stated as one: while there is no demo, these elements do not render, and
no component style may overrule that by accident. A component that genuinely needs to
lay one of these out sets every property except `display`. */
[data-demo-url=''] {
display: none;
display: none !important;
}
/* Phase 3 writes that markup as `class="btn demo-cta"`, so the slot is a button