diff --git a/app/llms-full.txt/route.test.ts b/app/llms-full.txt/route.test.ts
index 5a8beed..186c9d3 100644
--- a/app/llms-full.txt/route.test.ts
+++ b/app/llms-full.txt/route.test.ts
@@ -147,8 +147,10 @@ describe("homepage mirror", () => {
expect(body).toContain(`${figure.label}: ${figure.value}`);
});
- it("mirrors the stack chips in order", () => {
- expect(lines).toContain(`Stack: ${STACK.join(" · ")}`);
+ it("mirrors the stack strip in order", () => {
+ expect(lines).toContain(
+ `Stack: ${STACK.map((s) => `${s.name} (${s.role})`).join(" · ")}`,
+ );
});
// The header prose says "the three legal documents" in words, the way
diff --git a/app/llms-full.txt/route.ts b/app/llms-full.txt/route.ts
index 35e77d7..7a448a2 100644
--- a/app/llms-full.txt/route.ts
+++ b/app/llms-full.txt/route.ts
@@ -11,7 +11,7 @@ import {
HERO_LEAD,
METHOD_STEPS,
SITE_DESCRIPTION,
- STACK,
+ stackLine,
statusBlock,
} from "@/lib/copy";
import { FAQ_ITEMS } from "@/lib/faq";
@@ -215,7 +215,7 @@ ${compareTable}
### How it works
-Stack: ${STACK.join(" · ")}
+Stack: ${stackLine()}
${methodSteps}
diff --git a/components/site/Method.test.ts b/components/site/Method.test.ts
new file mode 100644
index 0000000..2b78277
--- /dev/null
+++ b/components/site/Method.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+import { Method } from "./Method";
+import { STACK } from "@/lib/copy";
+import {
+ asElement,
+ attrs,
+ childElements,
+ findAll,
+ findOne,
+ textOf,
+} from "@/test-utils/react-tree";
+
+const section = asElement(Method());
+const list = findOne(section, "ul");
+
+describe("Method stack strip", () => {
+ // The page and the llms-full.txt mirror both read STACK; this is what keeps
+ // the rendered side from dropping a role or reordering without a failure.
+ it("renders one cell per STACK item, name over role, in order", () => {
+ const cells = childElements(list);
+ expect(cells.map((c) => c.type)).toEqual(STACK.map(() => "li"));
+ cells.forEach((cell, i) => {
+ const [name, role] = childElements(cell);
+ expect(textOf(name)).toBe(STACK[i].name);
+ expect(textOf(role)).toBe(STACK[i].role);
+ });
+ });
+
+ it("hides the square marker and names the list by its eyebrow", () => {
+ for (const cell of childElements(list)) {
+ const [, role] = childElements(cell);
+ const [marker] = childElements(role);
+ expect(attrs(marker)["aria-hidden"]).toBe(true);
+ }
+ const id = attrs(list)["aria-labelledby"];
+ const eyebrow = findAll(section, "span").find((s) => attrs(s).id === id);
+ expect(eyebrow && textOf(eyebrow)).toBe("stack");
+ });
+});
diff --git a/components/site/Method.tsx b/components/site/Method.tsx
index 5aebaa6..fac2748 100644
--- a/components/site/Method.tsx
+++ b/components/site/Method.tsx
@@ -1,4 +1,3 @@
-import { Fragment } from "react";
import { METHOD_STEPS, STACK } from "@/lib/copy";
import { Frame } from "@/components/ui/Frame";
import { MethodRow } from "@/components/ui/MethodRow";
@@ -25,21 +24,39 @@ export function Method() {
-
stack
-
- {/* The `·` is a separator between chips, not part of one, so it is
- aria-hidden and rendered only between items. */}
- {STACK.map((name, i) => (
-
- {i > 0 && (
-
- ·
-
- )}
- {name}
-
+
+ stack
+
+ {/* Hairlines come from the list's top/left edge plus each cell's
+ right/bottom edge, so the grid stays single-ruled at any column
+ count — and closed while STACK.length divides evenly by it. In one
+ row of six the longest name (currently "BLAKE3") overflows
+ --text-h3, so names size off the frame's container width instead.
+ `.meta` and `.hug` sit outside any cascade layer, so a utility
+ beside either can't override it; the role label therefore spells
+ `.meta` out at 12px rather than adding a size to it. */}
+
+ {STACK.map(({ name, role }) => (
+ -
+
+ {name}
+
+
+
+ {role}
+
+
))}
-
+
);
diff --git a/lib/copy.test.ts b/lib/copy.test.ts
index 4999cee..651a264 100644
--- a/lib/copy.test.ts
+++ b/lib/copy.test.ts
@@ -54,7 +54,10 @@ const ALL_STRINGS: readonly [string, string][] = [
[`METHOD_STEPS.${s.n}.word`, s.word],
[`METHOD_STEPS.${s.n}.body`, s.body],
]),
- ...STACK.map((s, i): [string, string] => [`STACK[${i}]`, s]),
+ ...STACK.flatMap((s): [string, string][] => [
+ [`STACK.${s.name}.name`, s.name],
+ [`STACK.${s.name}.role`, s.role],
+ ]),
["DEMO_CAPTIONS.terminal", DEMO_CAPTIONS.terminal],
["DEMO_CAPTIONS.fleet", DEMO_CAPTIONS.fleet],
...CONTACT_HEADLINE.map((s, i): [string, string] => [
@@ -77,6 +80,20 @@ describe("copy strings", () => {
);
});
+describe("STACK", () => {
+ // components/site/Method.tsx lays the strip out 2 / 3 / 6 across, and its
+ // hairlines only close when the count divides evenly by the column count.
+ it("has exactly six items", () => {
+ expect(STACK).toHaveLength(6);
+ });
+
+ // `name` is the React key in Method.tsx and the label in ALL_STRINGS above.
+ it("names each item once", () => {
+ const names = STACK.map((s) => s.name);
+ expect(new Set(names).size).toBe(names.length);
+ });
+});
+
describe("COMPARE_ROWS", () => {
// Hardcoded, not `COMPARE_ROWS.length`: the section header says "seven
// axes" in prose (components/site/Compare.tsx), so the count is a published
diff --git a/lib/copy.ts b/lib/copy.ts
index 270bec7..f23bf8f 100644
--- a/lib/copy.ts
+++ b/lib/copy.ts
@@ -149,10 +149,27 @@ export const METHOD_STEPS: readonly MethodStep[] = [
},
];
-/** The stack chips under the method steps, rendered with an aria-hidden `·`
- * between each. Order is the render order. This is the section's only stack
- * summary — a figure strip beside it used to restate QUIC, iroh and USDC. */
-export const STACK = ["Rust", "BLAKE3", "QUIC", "iroh", "USDC", "EVM"] as const;
+export type StackItem = {
+ name: string;
+ /** What the piece does in the protocol — the small label under the name. */
+ role: string;
+};
+
+/** The stack under the method steps, in render order. Rendered by `Method`;
+ * serialised by `stackLine()`. This is the section's only stack summary: put
+ * per-piece detail in `role`, not in a separate strip beside it. */
+export const STACK: readonly StackItem[] = [
+ { name: "Rust", role: "node & client" },
+ { name: "BLAKE3", role: "verification" },
+ { name: "QUIC", role: "transport" },
+ { name: "iroh", role: "connectivity" },
+ { name: "USDC", role: "payment" },
+ { name: "EVM", role: "settlement" },
+];
+
+/** The stack as one line of text, for the machine mirror. */
+export const stackLine = (): string =>
+ STACK.map(({ name, role }) => `${name} (${role})`).join(" · ");
/** The target price, as the page and both machine surfaces quote it. */
export const TARGET_RATE = "$0.01/GB";