Skip to content

Commit 3958951

Browse files
angusbezzinaclaude
andcommitted
feat(web): the list is GPU CLI's, the page rounds like the app
Three changes, one of which turned up a bug. §05 was written as an AnnotKit list. It is not — it is the GPU CLI list, and AnnotKit is one of the things it covers. The lead now says so, and the privacy line says it out loud ("one list for everything GPU CLI ships, AnnotKit included") because someone subscribing from a page about a Swift package should know what lands in their inbox. Claims are held to gpu-cli.sh's own words: it runs cloud GPUs from your terminal. Nothing is promised about cadence or list size. The footer carries a powered-by line under the wordmark, linking the org. Radii are lifted from the overlay rather than invented. The app has a three-value vocabulary — RoundedRectangle(cornerRadius: 3) on marks and small chrome, cornerRadius: 10 on the composer panel, Capsule() on its one accent button — so the page gets --radius-sm/lg/pill and nothing in between. The submit is a capsule for the same reason the app's Save is. Editorial bans a pill with a GRADIENT fill, not a pill. The bug: :focus-visible carried `border-radius: inherit`, which does not mean "keep your own corners" — it REPLACES them with the parent's. The outline already follows an element's own radius, so the declaration was never doing anything useful; with radii on the page it would have squared the capsule off the instant it took keyboard focus. Removed, and verified by reading the computed style of a focused submit (999px) and input (3px, 1px border, 2px outline) rather than by looking at it. Two instrument fixes found while auditing, not design changes: - responsive-check watched six affordance selectors and .foot__powered made seven. A new affordance that no gate watches is how gate 49 quietly stops meaning anything. - The honest-typography test spot-checked four strings for straight quotes after whitespace, so the apostrophe in "Agentation's" sat in the footer colophon undetected. It now walks every exported string and rejects any straight quote or double hyphen outside code samples. Lighthouse 100/100/100/100, LCP 0.4s, CLS 0.007. Responsive PASS at five widths, contrast 14/14, 95 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b5f58cf commit 3958951

8 files changed

Lines changed: 104 additions & 19 deletions

File tree

web/.hallmark/slop-test.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Slop test — AnnotKit landing page
22

3-
Run against the built page at 320 / 375 / 414 / 768 / 1280 px, 2026-08-19.
3+
Run against the built page at 320 / 375 / 414 / 768 / 1280 px. First run
4+
2026-08-19; re-run 2026-08-20 after three changes — corner radii lifted from
5+
the overlay, §05 widened from an AnnotKit list to the GPU CLI list, and the
6+
notes file renamed to `ANNOTKIT_NOTES.md`.
47
Every gate must answer **no**. Where a gate is answered with a justification
58
rather than a plain no, the justification is written out — a silent pass on a
69
gate that fired is the thing this file exists to prevent.
@@ -54,8 +57,8 @@ Automated backing:
5457
## Implementation · 22–27
5558

5659
22. **Zero-chroma neutral?** No. Lowest chroma on the page is 0.010.
57-
23. **Accent over ~5 % of a viewport?** No. The accent fills exactly one surface — the submit button — plus hairline underlines, the §-numbers, and the focus ring. Measured well under 3 % at every width.
58-
24. **Off-scale spacing?** No. Every gap, pad, and margin comes from `--space-*`; the only bare values are the 4 px double-rule height and 1 px hairlines, both tokenised as `--rule-*`.
60+
23. **Accent over ~5 % of a viewport?** No. The accent fills exactly one surface — the submit button — plus hairline underlines, the §-numbers, the footer's powered-by hover, and the focus ring. Measured well under 3 % at every width. The submit is a capsule because the overlay fills its one accent button with `Capsule()`; editorial bans a pill with a GRADIENT fill, not a pill, and this fill is flat.
61+
24. **Off-scale spacing?** No. Every gap, pad, and margin comes from `--space-*`; the only bare values are the 4 px double-rule height and 1 px hairlines, both tokenised as `--rule-*`. Radii are `--radius-sm/lg/pill`, lifted from the overlay (`RoundedRectangle(cornerRadius: 3)`, `cornerRadius: 10`, `Capsule()`) rather than invented, so the page rounds the way the app does.
5962
25. **Measure outside 45–75 ch?** No. `--measure: 66ch`, `--measure-narrow: 52ch`.
6063
26. **Interactive element missing focus-visible / active / disabled?** No. The copy button, the input, and the submit each ship all eight states; typographic links ship default, hover, focus-visible, active, and a disabled variant used when the repo is private.
6164
27. **Motion without a reduced-motion fallback?** No. The hero reveal collapses to a 150 ms opacity fade, the rule draw collapses to an opacity fade, and the §02 loop renders its poster with controls instead of autoplaying.
@@ -102,7 +105,7 @@ Automated backing:
102105

103106
## Honest copy · 46
104107

105-
46. **Invented metric?** No. The page carries no numbers except the platform facts from `Package.swift` (macOS 15, iOS 17, Swift 6) and the version posture from `DECISIONS.md` (0.x, MIT). No testimonials, no logos, no proof bar. Where proof would normally sit, the page shows the code, the note format, and the captures.
108+
46. **Invented metric?** No. The page carries no numbers except the platform facts from `Package.swift` (macOS 15, iOS 17, Swift 6) and the version posture from `DECISIONS.md` (0.x, MIT). No testimonials, no logos, no proof bar. Where proof would normally sit, the page shows the code, the note format, and the captures. §05's claims about the GPU CLI list are traceable to gpu-cli.sh's own tagline ("Run cloud GPUs from your terminal"); nothing is promised about cadence or subscriber count. The §03 claim that a named third-party skill consumes the output was REMOVED at the rename rather than restated, because it could not be verified (annotkit-6l8).
106109

107110
## Re-drawn chrome · 47
108111

@@ -144,4 +147,4 @@ No axis scored below 3; no revision pass was required on the scores.
144147
Neither is a gate failure; both are recorded so they are not mistaken for finished work.
145148

146149
- **Captures are placeholders.** The §02 loop and the two stills are `pending` in `src/captures.ts` and render labelled plates. L4 records them from `task demo`.
147-
- **Initial JS is 67.4 KB gzip against the epic's ~60 KB budget** (§8.5). React 19 + react-dom is 65 KB of that; the app itself is under 3 KB. Lighthouse still scores 100 for performance (LCP 0.4 s, TBT 0 ms), so this is a budget-line question, not a user-facing one. Aliasing `react`/`react-dom` to `preact/compat` was measured at **14.5 KB gzip** — a 53 KB saving with no source changes — but the epic locks React, so the swap is the maintainer's call, not a silent substitution.
150+
- **Initial JS is 67.6 KB gzip against the epic's ~60 KB budget** (§8.5). React 19 + react-dom is 65 KB of that; the app itself is under 3 KB. Lighthouse still scores 100 for performance (LCP 0.4 s, TBT 0 ms), so this is a budget-line question, not a user-facing one. Aliasing `react`/`react-dom` to `preact/compat` was measured at **14.5 KB gzip** — a 53 KB saving with no source changes — but the epic locks React, so the swap is the maintainer's call, not a silent substitution.

web/scripts/responsive-check.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ const WIDTHS = [320, 375, 414, 768, 1280];
2323
const HEIGHT = 900;
2424

2525
/** Affordances that must read as one line at every width. */
26-
const AFFORDANCES = ".link, .mast__nav a, .foot__links a, .submit, .copy, .skip";
26+
const AFFORDANCES = [
27+
".link",
28+
".mast__nav a",
29+
".foot__links a",
30+
".foot__powered",
31+
".submit",
32+
".copy",
33+
".skip",
34+
].join(", ");
2735

2836
const MIME = {
2937
".html": "text/html; charset=utf-8",

web/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { DEFAULT_SITE_URL } from "../shared/site";
1111
export const site = {
1212
name: "AnnotKit",
1313
url: import.meta.env.VITE_SITE_URL ?? DEFAULT_SITE_URL,
14+
/** The organisation behind AnnotKit; the updates list is theirs, not ours. */
15+
org: "https://gpu-cli.sh",
1416
repo: "https://github.com/gpu-cli/annotkit",
1517
issues: "https://github.com/gpu-cli/annotkit/issues",
1618
license: "https://github.com/gpu-cli/annotkit/blob/main/LICENSE",

web/src/copy.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export const mcpBridge = {
130130
export const signup = {
131131
number: "§05",
132132
title: "One email when it’s worth reading.",
133-
lead: "Releases, design notes, and the 1.0 call. No digest, no drip campaign — AnnotKit is 0.x and honest about it.",
133+
lead: "The GPU CLI list: releases and design notes for AnnotKit, for the CLI that runs cloud GPUs from your terminal, and for whatever ships next. No digest, no drip campaign.",
134134
label: "Email address",
135135
placeholder: "you@example.com",
136136
submit: "Get release notes",
@@ -146,11 +146,12 @@ export const signup = {
146146
network: "That didn’t reach us. Check your connection and try again.",
147147
},
148148
privacy:
149-
"Your address goes to Resend and nowhere else. Every email carries an unsubscribe link.",
149+
"One list for everything GPU CLI ships, AnnotKit included. Your address goes to Resend and nowhere else, and every email carries an unsubscribe link.",
150150
} as const;
151151

152152
export const footer = {
153153
wordmark: "AnnotKit",
154+
poweredBy: "Powered by GPU CLI",
154155
tagline:
155156
"The native analogue of the web Agentation tool. Pre-1.0 — the API may shift before 1.0.",
156157
links: [
@@ -159,7 +160,7 @@ export const footer = {
159160
{ label: "License", href: "license" },
160161
],
161162
colophon:
162-
"MIT licensed. The note format is a clean-room reimplementation of Agentation's; this project contains no Agentation source. Set in Fraunces, Newsreader & IBM Plex Mono.",
163+
"MIT licensed. The note format is a clean-room reimplementation of Agentations; this project contains no Agentation source. Set in Fraunces, Newsreader & IBM Plex Mono.",
163164
} as const;
164165

165166
export const codeBlock = {

web/src/sections/Footer.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ export function Footer() {
1010
<footer className="foot">
1111
<div className="u-shell">
1212
<div className="foot__band">
13-
<p className="foot__wordmark">{footer.wordmark}</p>
13+
<div className="foot__mark">
14+
<p className="foot__wordmark">{footer.wordmark}</p>
15+
<a className="foot__powered" href={site.org}>
16+
{footer.poweredBy}
17+
<span className="link__arrow" aria-hidden="true">
18+
19+
</span>
20+
</a>
21+
</div>
1422
<p className="foot__tagline">{footer.tagline}</p>
1523
</div>
1624
<nav className="foot__links" aria-label="Elsewhere">

web/src/styles/base.css

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ pre {
109109
:focus-visible {
110110
outline: var(--rule-thick) solid var(--color-focus);
111111
outline-offset: 2px;
112-
border-radius: 1px;
112+
/* No border-radius here. The outline already follows the element's own
113+
* corners, and declaring one would REPLACE that radius with the parent's —
114+
* squaring the capsule submit off the moment it takes keyboard focus. */
113115
}
114116

115117
::selection {
@@ -156,6 +158,7 @@ pre {
156158
}
157159

158160
.skip {
161+
border-radius: var(--radius-sm);
159162
position: absolute;
160163
inset-block-start: var(--space-xs);
161164
inset-inline-start: var(--space-xs);
@@ -414,7 +417,8 @@ pre {
414417
padding: var(--space-md);
415418
background: var(--color-paper-2);
416419
color: var(--color-ink);
417-
border-block-end: var(--rule-hair) solid var(--color-rule);
420+
border: var(--rule-hair) solid var(--color-rule);
421+
border-radius: var(--radius-lg);
418422
overflow-x: auto;
419423
font-size: var(--text-sm);
420424
line-height: 1.7;
@@ -427,6 +431,7 @@ pre {
427431

428432
/* Copy button — 8 states, all visibly distinct. */
429433
.copy {
434+
border-radius: var(--radius-sm);
430435
display: inline-flex;
431436
align-items: center;
432437
gap: var(--space-2xs);
@@ -481,7 +486,9 @@ pre {
481486

482487
.figure__frame {
483488
border: var(--rule-hair) solid var(--color-rule);
489+
border-radius: var(--radius-lg);
484490
background: var(--color-paper-2);
491+
overflow: hidden; /* so a capture's square corners cannot poke through */
485492
min-width: 0;
486493
}
487494

@@ -504,6 +511,7 @@ pre {
504511
* Not a drawn mockup, not a stand-in screenshot — an empty plate with the
505512
* shot list printed on it. Replaced by the real capture in L4. */
506513
.figure__pending {
514+
border-radius: var(--radius-lg);
507515
display: grid;
508516
gap: var(--space-xs);
509517
align-content: center;
@@ -583,7 +591,7 @@ pre {
583591
font-family: var(--font-body);
584592
font-size: var(--text-base);
585593
border: var(--rule-hair) solid var(--color-rule-2); /* 1px in EVERY state */
586-
border-radius: 0;
594+
border-radius: var(--radius-sm);
587595
outline: var(--rule-thick) solid transparent; /* reserved ring slot */
588596
outline-offset: 1px;
589597
transition: background-color var(--dur-short) var(--ease-out);
@@ -647,7 +655,7 @@ pre {
647655
font-size: var(--text-base);
648656
font-weight: var(--weight-body-strong);
649657
border: var(--rule-hair) solid var(--color-accent);
650-
border-radius: 0;
658+
border-radius: var(--radius-pill);
651659
cursor: pointer;
652660
transition:
653661
background-color var(--dur-short) var(--ease-out),
@@ -730,6 +738,35 @@ pre {
730738
}
731739
}
732740

741+
.foot__mark {
742+
display: grid;
743+
gap: var(--space-2xs);
744+
justify-items: start;
745+
}
746+
747+
/* Powered by — the org line. Quiet, but a real link, not a signature. */
748+
.foot__powered {
749+
display: inline-flex;
750+
align-items: center;
751+
gap: var(--space-2xs);
752+
white-space: nowrap;
753+
font-family: var(--font-mono);
754+
font-size: var(--text-xs);
755+
letter-spacing: 0.06em;
756+
text-transform: uppercase;
757+
color: var(--color-muted);
758+
text-decoration: none;
759+
border-block-end: var(--rule-hair) solid transparent;
760+
transition: color var(--dur-short) var(--ease-out);
761+
}
762+
763+
@media (hover: hover) and (pointer: fine) {
764+
.foot__powered:hover {
765+
color: var(--color-accent);
766+
border-block-end-color: var(--color-accent);
767+
}
768+
}
769+
733770
.foot__wordmark {
734771
font-family: var(--font-display);
735772
font-size: var(--text-lg);

web/src/styles/tokens.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@
9999
--rule-hair: 1px;
100100
--rule-thick: 2px;
101101

102+
/* ── Radii ────────────────────────────────────────────────────────────────
103+
* Lifted from the overlay rather than invented, so the page and the app
104+
* round the same way: RoundedRectangle(cornerRadius: 3) on marks and small
105+
* chrome, cornerRadius: 10 on the composer panel, and Capsule() on the one
106+
* accent-filled button. Three values, no in-between steps.
107+
*/
108+
--radius-sm: 3px;
109+
--radius-lg: 10px;
110+
--radius-pill: 999px;
111+
102112
/* ── Motion ───────────────────────────────────────────────────────────── */
103113
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
104114
--ease-in: cubic-bezier(0.7, 0, 0.84, 0);

web/tests/a11y.dom.test.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,28 @@ describe("typography discipline", () => {
7979
}
8080
});
8181

82-
it("uses typographic punctuation, never straight quotes or double hyphens", () => {
83-
const strings = [hero.standfirst, signup.lead, signup.privacy, signup.successNew];
82+
it("uses typographic punctuation, never straight quotes or double hyphens", async () => {
83+
// Walk every exported string rather than a hand-picked few. The earlier
84+
// spot-check only looked for a quote after whitespace, so the straight
85+
// apostrophe in "Agentation's" sat in the footer colophon undetected.
86+
const copy: Record<string, unknown> = await import("../src/copy");
87+
88+
const strings: string[] = [];
89+
const walk = (value: unknown) => {
90+
if (typeof value === "string") strings.push(value);
91+
else if (Array.isArray(value)) value.forEach(walk);
92+
else if (value && typeof value === "object") Object.values(value).forEach(walk);
93+
};
94+
walk(copy);
95+
96+
expect(strings.length).toBeGreaterThan(30);
8497
for (const value of strings) {
85-
expect(value).not.toMatch(/--/);
86-
expect(value).not.toMatch(/\.\.\./);
87-
expect(value).not.toMatch(/(^|\s)'/);
98+
// Code samples legitimately contain straight quotes and dashes.
99+
if (/\n/.test(value)) continue;
100+
expect(value, value).not.toMatch(/--/);
101+
expect(value, value).not.toMatch(/\.\.\./);
102+
expect(value, value).not.toMatch(/'/);
103+
expect(value, value).not.toMatch(/"/);
88104
}
89105
});
90106
});

0 commit comments

Comments
 (0)