Skip to content
7 changes: 7 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ Deprecated until WP10: `.card` and `.cards` (now flat `--ground-2` panels; move
`.rows`), `.pill` and `.pill.accent` (aliases of `.chip` and `.chip.signal`) and
`.button.primary` (a plain `.button` is the filled one).

The page head of `/explorer/`, `/breaches/` and every spec page carries a decorative drawing
(`Plate.astro`, WP8): a small delegation chain drawn from the page id and title by
`src/lib/plate.ts`, so a page always gets the same one. It is the first child of
`header.page-head`, floats beside the title from 64rem up and is left out below that. It has no
frame and uses the existing inks only: `--signal` for the path, `--boundary` for the trust
boundary dash, hairlines for the rest. It never goes on a page with a data view above the fold.

The header scrolls away with the page, so `--header-h` (what sticky content must clear) is 0.

Product surfaces go in `product`. The matrix (`MatrixTable`, `StatusCell`, `ObservationDetail`)
Expand Down
Binary file added public/og-breaches.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/og-explorer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions scripts/generate-brand-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
import { chromium } from "@playwright/test";
import { cellKey, currentRevision, matrixFor } from "../src/lib/atlas";
import { getDataset } from "../src/lib/data";
import { plate, plateSvg, SECTION_PLATES, sectionCard } from "../src/lib/plate";

// Run with `pnpm generate:brand` after changing public/brand, the tokens or the data. No network
// requests are made.
Expand Down Expand Up @@ -95,6 +96,40 @@ try {
<p class="site">letsauth.eu</p>`);
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: "public/og.png" });

// The section roots that carry a decorative plate (WP8) get a card of their own: the same layout
// as the home card, with the page's drawing beside the headline as it sits in the page head.
for (const [id, section] of Object.entries(SECTION_PLATES)) {
await page.setContent(`<style>${tokens}
@font-face{font-family:Display;src:url('${fontUrl("geist")}');font-weight:100 900}
@font-face{font-family:Mono;src:url('${fontUrl("geist-mono")}');font-weight:100 900}
*{box-sizing:border-box}
body{margin:0;width:1200px;height:630px;padding:56px 64px;background:var(--ground);
color:var(--ink);font-family:Display;display:grid;grid-template-columns:1fr 400px;
gap:64px;align-items:center}
.logo{position:absolute;top:56px;left:64px;width:220px;height:auto}
.eyebrow{margin:0 0 18px;font-size:22px;color:var(--ink-2)}
h1{margin:0;font-weight:600;font-size:58px;line-height:1.08;letter-spacing:-0.03em}
.site{position:absolute;bottom:56px;left:64px;font:22px Mono;color:var(--ink-2)}
svg{display:block;width:100%;height:auto;fill:none;stroke-linecap:round;stroke-linejoin:round}
.pb{stroke:var(--boundary);stroke-dasharray:4 4;opacity:.7}
.pl{stroke:var(--rule)}
.pa{fill:var(--ground-2);stroke:var(--rule-strong)}
.pm{stroke:var(--rule-strong)}
.ph{display:none}
.pp{stroke:var(--signal);stroke-width:2}
.po{fill:var(--signal)}
</style>
<img class="logo" src="${svgUrl(wordmark)}" alt="letsauth">
<div>
<p class="eyebrow">${section.eyebrow}</p>
<h1>${section.title}</h1>
</div>
${plateSvg(plate(id, section.title))}
<p class="site">letsauth.eu/${id}/</p>`);
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: `public${sectionCard(id as keyof typeof SECTION_PLATES)}` });
}
} finally {
await browser.close();
}
14 changes: 12 additions & 2 deletions specs/016-visual-redesign/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,17 @@ per page; the draw-in (M4) runs once and is static under reduced motion; the dra
appears on a page that already has a data view above the fold (matrix, map, chain, report); a
`plate: false` prop removes it.

Decision for Jan: ship this, or keep letsauth's visuals strictly data-derived. The plan
Decided (22 October 2026, review follow-up 10): Jan chose to ship it on `/explorer/`,
`/breaches/` and the spec pages only. `/mcp/` and `/standards/` keep their data view as the one
visual, so the drawing stays off them; the cards are per section root (explorer and breaches),
and spec pages keep the home card. As built, the drawing departs from the file list above: a
first version (a network on its own plate under the head, one of eight hues) read as added on
top, so it is a miniature delegation chain in the explorer's vocabulary and existing inks
(`--signal` path, `--boundary` dash, hairlines), floated beside the title inside the page head
on wide screens and left out below 64rem. A page leaves it out by not placing `<Plate>`, so no
`plate: false` prop is needed.

Original decision for Jan: ship this, or keep letsauth's visuals strictly data-derived. The plan
recommends shipping it on the four section roots and spec pages only, because those pages have
no diagram of their own and the drawing is what makes the two sites read as a family. Skipping
it costs nothing else in the plan.
Expand Down Expand Up @@ -673,7 +683,7 @@ Decisions for Jan before WP1 starts
Grotesque for h1 only.
2. **Neutral hue**: cool graphite like the blog (recommended, hue 250 to 255) or keep the warm
paper of today (hue 85) with the new flat treatment.
3. **WP8 plate**: ship or skip (section WP8).
3. **WP8 plate**: ship or skip (section WP8). Decided: ship, on the explorer, breach and spec pages.
4. **Search palette**: in WP10 scope or not at all.

If no answer arrives, the packages proceed with the recommended option in each case.
20 changes: 19 additions & 1 deletion specs/016-visual-redesign/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,24 @@ Taken as review follow-up 9. Legal content keeps its substance; only the wording
- [x] W9-T04 `README.md` and `docs/*.md` pass `check:prose`; `FAIL` is true in `scripts/check-prose.ts`, which also bans "game-changer" and "holistic" from the review's list; `docs/development.md` says the check fails
- [x] W9-T05 `/about/`, `/contribute/`, `/made-in-europe/`: one idea per sentence, no semicolons, no "X, not Y" principle names, no "yours to download"
- [x] W9-T06 `/privacy/`, `/imprint/`, `/disclaimer/`, `/404`: wording only, no semicolons in body copy, headings without the colon-and-triad pattern; legal content unchanged
- [ ] W9-T07 Screenshot baselines for the pages whose head changed, through the `refresh-screenshots` label
- [x] W9-T07 Screenshot baselines for the pages whose head changed, through the `refresh-screenshots` label (on the WP8 pull request, which refreshes the same pages)

## WP8: Decorative plate

One calm, label-free drawing in the head of the pages that have no diagram of their own, as the
blog does. Jan chose to ship it on `/explorer/`, `/breaches/` and the spec pages only (review
follow-up 10); `/mcp/` and `/standards/` keep their data view as the one visual. A first version
(a network on its own framed plate under the head, in one of eight hues) read as something added
on top, so the drawing now speaks the explorer's language and sits in the head itself.

- [x] W8-T01 `src/lib/plate.ts`: the pure generator. An FNV-1a hash of the page id and title seeds a small generator; a miniature delegation chain of 3 to 5 actor heads on lifelines and 4 to 6 messages, of which a chain (each message sent by the lane the previous one reached) is the highlighted path, and one dashed trust boundary around one or two lifelines. `plateSvg()` writes the SVG with classes only, no colours, `aria-hidden`
- [x] W8-T02 Colours from the existing tokens only: lifelines `--rule`, heads `--ground-2` on `--rule-strong`, quiet messages `--rule-strong`, the path `--signal`, the boundary dash `--boundary` (plan 3.2: "trust boundary dashes"); no new tokens
- [x] W8-T03 `src/components/Plate.astro`: the first child of `header.page-head`, floated beside the title at 18rem from 64rem up, no frame, hidden below 64rem; the path draws in once on load with a halo that fades after and its ends appear when it reaches them (M4, `--dur-draw`, `--ease-draw`), only under `prefers-reduced-motion: no-preference`
- [x] W8-T04 Placed in the page head of `/explorer/`, `/breaches/` and every `/standards/<id>/` page
- [x] W8-T05 `scripts/generate-brand-assets.ts`: `public/og-explorer.png` and `public/og-breaches.png` from the same generator and seed, the drawing beside the headline as in the page head; `Base.astro` takes an `image` prop for the card
- [x] W8-T06 `tests/unit/plate.test.ts` (same input, same SVG, as a snapshot; different pages, different drawings; every plate in bounds, its path a chain and under 4 kB) and `tests/e2e/plate.spec.ts` (in the head and hidden from assistive technology on the three page types, absent on home, `/mcp/`, `/standards/` and a chain, beside the title on wide screens and hidden on phones, the section cards, axe, the reduced-motion end state)
- [x] W8-T07 `src/pages/accessibility.astro` lists the draw-in and its reduced-motion form; the plan records the decision
- [x] W8-T08 Screenshot baselines for the pages it changes: refreshed through the `refresh-screenshots` label on the pull request

## Review follow-ups (21 October 2026)

Expand All @@ -186,3 +203,4 @@ Items from the redesign review, one pull request each, in order.
- [x] R-07 Home figures and copy: the facts read Servers, Requirements, Cells observed and Specifications. "Cells observed" counts the current matrix cells that have an observation, beside the total (611 of 897), in place of the 1,664 entries of the append-only history, which counted every scan of the same cell again; `Facts.astro` takes an optional `of` total that stands still while the figure counts up. "Latest observations" shows the six newest observations of the current revision with one row per requirement and per server, so one scan run no longer fills the panel with six rows of the same feature. The seed-data sentence under the list is gone (the server page and `/contribute/` no longer carried one). No baseline changes: the home screenshot stops above the facts
- [x] R-08 The explorer as a scene: one Web Animations API timeline (a target-less scene clock) holds the playhead for Play, Pause, Previous, Next, a scrub bar in the stepper, the arrow keys, a click on a step and the decision log (M6). Auto-advance starts once the chain has sat in view for 1 s with no input, stops for good on hover, touch, focus, scroll or a key, never restarts and never runs under reduced motion; M6 in the plan is amended. Per step the sender and receiver light up in the actor head, the message draws in over 300ms, earlier steps keep a quieter arrow and later ones a hairline (colour, not opacity, so axe passes while stepping) (M5). The changed claims get their rail 40ms apart once the message lands (M7), now through the Web Animations API instead of an `is-fresh` class. A toggle swaps the chain in place through a same-document view transition of 220ms and pushes its URL; back and forward swap again, the analytics count the swap as a pageview (M17). A quiet log under the chain gains one line per step (M18). Durations come from the motion tokens (`--dur-message`, `--stagger` and `--dur-step` are new) through `durationToken()` in `src/lib/motion.ts`. Baselines through the `refresh-screenshots` label
- [x] R-09 Content pass (WP9 above): section-root headlines under 24 characters, the title suffix and footer statement, an accessibility statement that lists every motion with its reduced-motion form, `check:prose` as a gate, and the about, contribute, made-in-Europe, legal and 404 pages in plain English
- [x] R-10 Decorative plate (WP8 above): a deterministic miniature delegation chain in the head of the explorer and breach indexes and every spec page, beside the title on wide screens, drawn in once (M4) and complete under reduced motion, with social cards for the two section roots from the same generator
110 changes: 110 additions & 0 deletions src/components/Plate.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
import { plate, plateSvg } from "@/lib/plate";

// The decorative plate (plan WP8): a small label-free delegation chain in the explorer's own
// vocabulary (actor heads, lifelines, messages, the trust boundary dash), set into the page head
// beside the title on wide screens. Derived from the page's id and title, so it never changes
// between builds. Hidden from assistive technology and on narrow screens. The highlighted path
// draws in once on load (M4); under reduced motion it is drawn complete. A page that shows data
// above the fold does not place it. Place it as the first child of `header.page-head`.
interface Props {
id: string;
title: string;
}

const { id, title } = Astro.props;
---

<div class="decor" set:html={plateSvg(plate(id, title))} />

<style>
.decor {
display: none;
}
@media (min-width: 64rem) {
:global(.page-head:has(> .decor)) {
display: flow-root;
}
.decor {
display: block;
float: right;
width: 18rem;
margin: 0 0 var(--space-4) var(--space-8);
}
}
.decor :global(svg) {
display: block;
width: 100%;
height: auto;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.decor :global(.pb) {
stroke: var(--boundary);
stroke-dasharray: 4 4;
opacity: 0.7;
}
.decor :global(.pl) {
stroke: var(--rule);
}
.decor :global(.pa) {
fill: var(--ground-2);
stroke: var(--rule-strong);
}
.decor :global(.pm) {
stroke: var(--rule-strong);
}
.decor :global(.ph) {
stroke: var(--signal-soft);
stroke-width: 8;
opacity: 0;
}
.decor :global(.pp) {
stroke: var(--signal);
stroke-width: 2;
}
.decor :global(.po) {
fill: var(--signal);
}
@media (prefers-reduced-motion: no-preference) {
.decor :global(.pp) {
stroke-dasharray: 1;
animation: plate-draw var(--dur-draw) var(--ease-draw) both;
}
.decor :global(.ph) {
stroke-dasharray: 1;
animation:
plate-draw var(--dur-draw) var(--ease-draw) both,
plate-halo calc(var(--dur-draw) + var(--dur-slow)) linear both;
}
.decor :global(.po) {
animation: plate-end var(--dur-slow) var(--ease) var(--dur-draw) both;
}
}
@keyframes plate-draw {
from {
stroke-dashoffset: 1;
}
to {
stroke-dashoffset: 0;
}
}
/* the halo shows while the path draws (1200 of 1680ms), then fades out */
@keyframes plate-halo {
0%,
100% {
opacity: 0;
}
2%,
71% {
opacity: 1;
}
}
/* the path's ends appear once the path has reached them */
@keyframes plate-end {
from {
opacity: 0;
}
}
</style>
6 changes: 4 additions & 2 deletions src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ interface Props {
path?: string;
/** When set, the page is excluded from search indexing and the sitemap hint. */
noindex?: boolean;
/** The social card, a path under public/. Defaults to the home card. */
image?: string;
}

const { title, description, path, noindex = false } = Astro.props;
const { title, description, path, noindex = false, image = "/og.png" } = Astro.props;
const site = Astro.site ?? new URL("https://letsauth.eu");
const canonical = new URL(path ?? Astro.url.pathname, site);
const plausibleDomain = import.meta.env.PUBLIC_PLAUSIBLE_DOMAIN as string | undefined;
Expand Down Expand Up @@ -101,7 +103,7 @@ const nav = [
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical.href} />
<meta property="og:image" content={new URL("/og.png", site).href} />
<meta property="og:image" content={new URL(image, site).href} />
<meta name="twitter:card" content="summary_large_image" />
{/* --ground in sRGB: oklch(98.5% 0.003 250) and oklch(21% 0.008 255) (pnpm check:contrast --hex) */}
<meta name="theme-color" content="#f9fafc" media="(prefers-color-scheme: light)" />
Expand Down
Loading
Loading