diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 15398f3f5..20b456b1c 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -25,9 +25,10 @@ test('landing page renders hero headline', async ({ page }) => { expect(headline?.toLowerCase()).toContain('angular'); }); -test('landing page renders the Yes wall', async ({ page }) => { +test('landing page renders the dark proof band', async ({ page }) => { await page.goto('/'); - await expect(page.locator('#yes-wall-heading')).toBeVisible(); + await expect(page.locator('#proof-heading')).toBeVisible(); + await expect(page.locator('#proof[data-surface="dark"]')).toBeVisible(); }); test('landing page renders feature blocks (Stream/Render/Ship)', async ({ page }) => { diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index 241153c59..0af8e9659 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -1,6 +1,6 @@ import { Hero } from '../components/landing/Hero'; import { LogoRibbon } from '../components/landing/LogoRibbon'; -import { YesWall } from '../components/landing/YesWall'; +import { ProofStrip } from '../components/landing/ProofStrip'; import { FeatureBlock } from '../components/landing/FeatureBlock'; import { StackDiagramSection } from '../components/landing/StackDiagramSection'; import { HomeConceptGrid } from '../components/landing/HomeConceptGrid'; @@ -9,7 +9,6 @@ import { MediumSwitcher } from '../components/landing/MediumSwitcher'; import { SECTION_MEDIA } from '../lib/section-media'; import { buildPanes } from '../lib/build-panes'; import { PilotBlock } from '../components/landing/PilotBlock'; -import { ProofStrip } from '../components/landing/ProofStrip'; import { WhitePaperBlock } from '../components/landing/WhitePaperBlock'; import { HomeFAQ } from '../components/landing/HomeFAQ'; import { FinalCTA } from '../components/landing/FinalCTA'; @@ -38,7 +37,7 @@ export default async function HomePage() { <> - + {/* - The homepage claims human-in-the-loop in the Yes wall, but nothing - before this section showed it. This is the only section whose heading the approval clip actually illustrates — the same rule the solutions pages follow. */} @@ -135,7 +132,6 @@ export default async function HomePage() { - diff --git a/apps/website/src/components/landing/ProofStrip.spec.tsx b/apps/website/src/components/landing/ProofStrip.spec.tsx index 8ee7e0266..ac71144b3 100644 --- a/apps/website/src/components/landing/ProofStrip.spec.tsx +++ b/apps/website/src/components/landing/ProofStrip.spec.tsx @@ -18,4 +18,32 @@ describe('ProofStrip', () => { const badge = screen.getByAltText(/HVTrust grade/i); expect(badge.getAttribute('src')).toBe('https://hvtracker.net/badge/threadplane.svg'); }); + + it('renders on the dark contrast band, with a watermark carrying no text', () => { + const { container } = render(); + + const section = container.querySelector('[data-ui="section"]'); + expect(section?.getAttribute('data-surface')).toBe('dark'); + expect(section?.getAttribute('id')).toBe('proof'); + + const mark = container.querySelector('.proof-strip-watermark'); + expect(mark?.getAttribute('aria-hidden')).toBe('true'); + expect(mark?.getAttribute('data-watermark-text')).toBe('Proof'); + expect(mark?.textContent).toBe(''); + }); + + it('keeps the "Reliable to the core" framing on the band', () => { + render(); + expect(screen.getByText('Reliable to the core')).toBeTruthy(); + const heading = screen.getByRole('heading', { name: 'Audited, scored, published.' }); + expect(heading.id).toBe('proof-heading'); + }); + + it('links every number to a human-readable page, never a raw API', () => { + for (const cell of PROOF_CELLS) { + const { hostname, pathname } = new URL(cell.sourceHref); + expect(hostname.startsWith('api.'), cell.sourceHref).toBe(false); + expect(pathname.startsWith('/api/'), cell.sourceHref).toBe(false); + } + }); }); diff --git a/apps/website/src/components/landing/ProofStrip.tsx b/apps/website/src/components/landing/ProofStrip.tsx index 030128544..7e2e0a12e 100644 --- a/apps/website/src/components/landing/ProofStrip.tsx +++ b/apps/website/src/components/landing/ProofStrip.tsx @@ -14,32 +14,36 @@ interface ProofCell { } /** - * Verified 2026-08-27 against live sources (see the homepage design spec, + * Verified 2026-09-04 against live sources (see the homepage design spec, * "Verification results"). The rank and score drift over time — re-verify on * touch, and never "round up". The HVTrust grade is deliberately a LIVE badge: - * it sits at 81.2 against an A-band floor of 80 and has flipped grade six - * times in one month; a hardcoded letter would be wrong on some days. + * it sits at 84.3 against an A-band floor of 80 and has flipped grade several + * times in a month; a hardcoded letter would be wrong on some days. + * + * Every href must be a page a human can read. The Scorecard number comes from + * api.securityscorecards.dev, but the LINK goes to the scorecard.dev viewer — + * the API URL renders as raw JSON. */ export const PROOF_CELLS: readonly ProofCell[] = [ { - value: '#13', + value: '#8', suffix: 'of 119', caption: 'Of all agent frameworks ranked', sourceLabel: 'hvtracker.net', sourceHref: 'https://hvtracker.net/categories/agent-frameworks/', }, { - value: '8.1', + value: '8.2', suffix: '/10', - caption: 'OpenSSF Scorecard, official API', - sourceLabel: 'securityscorecards.dev', + caption: 'OpenSSF Scorecard, official scan', + sourceLabel: 'scorecard.dev', sourceHref: - 'https://api.securityscorecards.dev/projects/github.com/cacheplane/angular-agent-framework', + 'https://scorecard.dev/viewer/?uri=github.com/cacheplane/angular-agent-framework', }, { value: null, caption: 'HVTrust supply-chain grade, live', - sourceLabel: 'hvtracker.net/agents/threadplane', + sourceLabel: 'hvtracker.net/agents', sourceHref: 'https://hvtracker.net/agents/threadplane/', }, { @@ -47,56 +51,65 @@ export const PROOF_CELLS: readonly ProofCell[] = [ // ^20 || ^21 || ^22 (verified 2026-08-31). Derived, never hardcoded. value: `${WEBSITE_SUPPORTED_ANGULAR_MAJORS[0]}–${WEBSITE_SUPPORTED_ANGULAR_MAJORS.at(-1)}`, caption: 'Angular majors supported, CI-tested', - sourceLabel: 'npmjs.com/@threadplane/langgraph', + sourceLabel: 'npmjs.com', sourceHref: 'https://www.npmjs.com/package/@threadplane/langgraph', }, ]; export function ProofStrip() { return ( -
+
-
- + {/* Garamond watermark; the glyph comes from the data attribute so no + * stray text reaches screen readers or getAllByText. */} +
diff --git a/apps/website/src/components/landing/YesWall.spec.tsx b/apps/website/src/components/landing/YesWall.spec.tsx deleted file mode 100644 index 19835002c..000000000 --- a/apps/website/src/components/landing/YesWall.spec.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { YesWall, YES_WALL_GROUPS } from './YesWall'; - -vi.mock('../../lib/analytics/client', () => ({ - trackCtaClick: vi.fn(), -})); - -describe('YesWall', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('renders 15 questions across 4 groups', () => { - render(); - const total = YES_WALL_GROUPS.reduce((n, g) => n + g.rows.length, 0); - expect(YES_WALL_GROUPS).toHaveLength(4); - expect(total).toBe(15); - for (const group of YES_WALL_GROUPS) { - expect(screen.getByText(group.label)).toBeTruthy(); - for (const row of group.rows) { - expect(screen.getByText(row.question)).toBeTruthy(); - expect(screen.getByText(row.api)).toBeTruthy(); - } - } - }); - - it('answers every question Yes', () => { - render(); - expect(screen.getAllByText('Yes')).toHaveLength(15); - }); - - it('renders the dark specimen chrome', () => { - const { container } = render(); - expect( - container.querySelector('[data-ui="section"]')?.getAttribute('data-surface'), - ).toBe('dark'); - const mark = container.querySelector('.yes-wall-watermark'); - expect(mark?.getAttribute('aria-hidden')).toBe('true'); - }); - - it('links the footer to the docs', () => { - render(); - const link = screen.getByRole('link', { name: /Every question answered/ }); - expect(link.getAttribute('href')).toBe('/docs'); - }); -}); - -describe('YesWall promise surface', () => { - it('makes no installation or phone-home claim', () => { - render(); - - const wall = document.body.textContent ?? ''; - expect(wall).not.toMatch(/phon(e|ing) home/i); - expect(wall).not.toMatch(/installation is inert/i); - expect( - YES_WALL_GROUPS.flatMap((group) => group.rows).map((row) => row.question) - ).not.toContain('Can I install it without phoning home?'); - }); -}); diff --git a/apps/website/src/components/landing/YesWall.tsx b/apps/website/src/components/landing/YesWall.tsx deleted file mode 100644 index 927c1f04f..000000000 --- a/apps/website/src/components/landing/YesWall.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client'; - -import { Container } from '../ui/Container'; -import { Section } from '../ui/Section'; -import { SectionHeader } from '../ui/SectionHeader'; -import { trackCtaClick } from '../../lib/analytics/client'; - -interface YesRow { - question: string; - api: string; -} - -interface YesGroup { - label: string; - rows: YesRow[]; -} - -export const YES_WALL_GROUPS: readonly YesGroup[] = [ - { - label: 'State that survives', - rows: [ - { question: 'Can a conversation survive a page reload?', api: 'threadId + durable transports' }, - { question: 'Can I resume a thread days later, on another device?', api: 'checkpointed threads' }, - { question: 'Can I branch or replay a conversation from any point?', api: 'branch / replay' }, - { question: 'Can I persist threads without building a persistence layer?', api: 'durable transports' }, - ], - }, - { - label: 'Humans in the loop', - rows: [ - { question: 'Can I stop the agent before it does something irreversible?', api: 'interrupt()' }, - { question: 'Can the pause survive a refresh while someone decides?', api: 'the pause is a checkpoint' }, - { question: 'Can I show the human what the agent is about to do?', api: '' }, - { question: "Can the human's decision land in the thread record?", api: 'submit({ resume })' }, - ], - }, - { - label: 'On my design system', - rows: [ - { question: 'Can agent output render as my components, not a chat widget?', api: '@threadplane/render' }, - { question: 'Can I fall back per-component when a spec is unknown?', api: 'fallback + readiness gate' }, - { question: 'Can the browser own its own tools and render them inline?', api: 'client tools' }, - ], - }, - { - label: 'Shipping it', - rows: [ - { question: 'Can I swap LangGraph for AG-UI without rewriting the UI?', api: 'one Agent contract' }, - { question: 'Can I unit-test components that depend on an agent?', api: 'provideFakeAgent' }, - { question: 'Can I run all of it inside my own VPC?', api: 'self-host, no runtime SaaS' }, - { question: 'Can I use every package commercially without a license fee?', api: 'MIT, all packages' }, - ], - }, -]; - -// NOTE: the SectionHeader aside says "Sixteen questions" in prose — update it -// (and the spec's toBe(16)) together with any row change; the footer count -// derives automatically. -const TOTAL_QUESTIONS = YES_WALL_GROUPS.reduce((n, g) => n + g.rows.length, 0); - -export function YesWall() { - return ( -
- -
- - -
- ); -} diff --git a/apps/website/src/components/ui/SectionHeader.spec.tsx b/apps/website/src/components/ui/SectionHeader.spec.tsx index b4fa02d04..64a1f0ef0 100644 --- a/apps/website/src/components/ui/SectionHeader.spec.tsx +++ b/apps/website/src/components/ui/SectionHeader.spec.tsx @@ -9,7 +9,7 @@ describe('SectionHeader', () => { variant="rail" eyebrow="The Yes wall" heading="Yes, it does that." - headingId="yes-wall-heading" + headingId="proof-heading" aside="Sixteen questions teams ask before they commit." />, ); @@ -17,7 +17,7 @@ describe('SectionHeader', () => { expect(root?.getAttribute('data-variant')).toBe('rail'); expect(screen.getByText('The Yes wall')).toBeTruthy(); const h2 = screen.getByRole('heading', { level: 2 }); - expect(h2.id).toBe('yes-wall-heading'); + expect(h2.id).toBe('proof-heading'); expect(h2.textContent).toBe('Yes, it does that.'); expect(screen.getByText(/Sixteen questions/)).toBeTruthy(); }); diff --git a/apps/website/src/lib/analytics/events.ts b/apps/website/src/lib/analytics/events.ts index 9e6f3e2d6..036761600 100644 --- a/apps/website/src/lib/analytics/events.ts +++ b/apps/website/src/lib/analytics/events.ts @@ -69,7 +69,6 @@ export type CtaId = // Why this exists section | 'home_why_pilot_to_prod' // Yes Wall section - | 'home_yes_wall_docs' // Pricing tier CTAs | 'pricing_tier_community' | 'pricing_tier_production_assurance' diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index cc00bf023..7cad40fde 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1077,34 +1077,35 @@ color: var(--color-chat-purple); } -/* ---------- Yes wall (dark specimen, spec change A + treatment B) ---------- */ -/* Match FinalCTA's containment: the glow and watermark stay inside the band. */ -#yes-wall { +/* ---------- Proof strip (dark contrast band, top of the homepage) ---------- + * Formerly a light tinted strip low on the page. It now occupies the single + * dark band under the logo ribbon: same cells and sources, re-toned for the + * dark surface scope in ui.css. Containment for the watermark and glow lives + * on #proof, mirroring FinalCTA. */ +#proof { overflow: clip; } -.yes-wall { +.proof-strip { position: relative; } -/* 330px italic Garamond watermark, gradient-filled, bleeding off the right. - * The div holds no text (screen readers + getAllByText stay clean); the glyph - * comes from the data attribute. */ -.yes-wall-watermark::after { +/* Gradient-filled Garamond watermark bleeding off the right edge. */ +.proof-strip-watermark::after { content: attr(data-watermark-text); } -.yes-wall-watermark { +.proof-strip-watermark { position: absolute; right: -40px; - top: -20px; + top: -78px; font-family: var(--font-garamond); font-style: italic; font-weight: 800; - font-size: clamp(160px, 24vw, 330px); + font-size: clamp(150px, 21vw, 300px); line-height: 0.76; letter-spacing: -0.04em; background: linear-gradient( 160deg, - rgba(100, 195, 253, 0.16), - rgba(100, 195, 253, 0.03) 70% + rgba(100, 195, 253, 0.11), + rgba(100, 195, 253, 0.02) 70% ); -webkit-background-clip: text; background-clip: text; @@ -1112,8 +1113,8 @@ pointer-events: none; user-select: none; } -/* Radial accent glow rising behind the watermark (treatment B). */ -.yes-wall::before { +/* Radial accent glow rising behind the watermark. */ +.proof-strip::before { content: ''; position: absolute; top: -160px; @@ -1128,154 +1129,29 @@ ); pointer-events: none; } -.yes-wall-grid { +/* The rail header stacks ABOVE the cells here: four big Garamond numerals need + * the full container width to stay on one line. */ +.proof-strip-grid { position: relative; display: grid; - grid-template-columns: 300px 1fr; - gap: 64px; -} -.yes-wall-group { - display: grid; - grid-template-columns: 112px 1fr; -} -.yes-wall-group + .yes-wall-group { - margin-top: 34px; -} -.yes-wall-group-numeral { - font-family: var(--font-garamond); - font-size: 62px; - font-weight: 700; - line-height: 0.85; - letter-spacing: -0.03em; - color: var(--color-border-strong); - padding-top: 14px; -} -.yes-wall-group-rows { - border-top: 2px solid var(--color-text-primary); -} -.yes-wall-group-label { - font-family: var(--font-mono); - font-size: 11px; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-accent); - margin: 0; - padding: 13px 0 5px; -} -/* The answer is SECOND in the DOM (question first, for screen-reader order) - * but placed visually first via grid areas. */ -.yes-wall-row { - display: grid; - grid-template-columns: 33px 1fr 250px; - grid-template-areas: 'yes question api'; - gap: 0 13px; - align-items: baseline; - border-top: 1px solid var(--color-border); - padding: 12px 0; - transition: background 140ms ease; -} -.yes-wall-row:hover { - background: var(--color-accent-surface); -} -.yes-wall-question { - grid-area: question; - font-family: var(--font-inter); - font-size: 16px; - line-height: 1.45; - color: var(--color-text-primary); - margin: 0; -} -.yes-wall-yes { - grid-area: yes; - font-family: var(--font-garamond); - font-style: italic; - font-weight: 700; - font-size: 20px; - color: var(--color-accent); - text-shadow: 0 0 16px var(--color-accent-glow); + gap: 46px; } -.yes-wall-api { - grid-area: api; - font-family: var(--font-mono); - font-size: 12px; - line-height: 1.4; - color: var(--color-text-muted); - text-align: right; - margin: 0; -} -.yes-wall-footer { - display: flex; - justify-content: space-between; - align-items: center; - gap: 16px; - margin-top: 34px; - padding-top: 20px; - border-top: 1px solid var(--color-border); -} -.yes-wall-footer-link { - font-family: var(--font-inter); - font-size: 15px; - font-weight: 500; - color: var(--color-accent); - text-decoration: underline; - text-underline-offset: 3px; -} -.yes-wall-footer-count { - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-text-muted); - margin: 0; -} - -/* Responsive: the specimen devices are the fragile part. */ -@media (max-width: 900px) { - .yes-wall-grid { - grid-template-columns: 1fr; - gap: 40px; - } - .yes-wall-group { - grid-template-columns: 1fr; - } - .yes-wall-group-numeral { - font-size: 40px; - padding: 0 0 8px; - } - .yes-wall-watermark { - font-size: 140px; - right: -16px; - } - .yes-wall-row { - grid-template-columns: 33px 1fr; - grid-template-areas: - 'yes question' - '. api'; - gap: 4px 13px; - } - .yes-wall-api { - text-align: left; - } -} - -/* ---------- Proof strip (light, paper elevation — spec change D) ---------- */ -.proof-strip-grid { - display: grid; - grid-template-columns: 300px 1fr; - gap: 64px; - align-items: start; +.proof-strip-grid > [data-ui="section-header"] { + max-width: 620px; } .proof-strip-cells { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 18px; + gap: 20px; list-style: none; margin: 0; padding: 0; } .proof-strip-cell { position: relative; + /* Grid items default to min-width:auto; the long mono source URLs would then + * push the four columns past the container. */ + min-width: 0; background: var(--color-surface); background-image: linear-gradient( 180deg, @@ -1289,6 +1165,20 @@ 0 1px 2px rgba(0, 0, 0, 0.04), 0 12px 28px -12px rgba(28, 28, 28, 0.14); } +/* On the dark band the paper gradient inverts: a faint lift off the surface + * token rather than a white sheet. The token re-scope in ui.css handles the + * rest (background, border, text). */ +[data-ui="section"][data-surface="dark"] .proof-strip-cell { + background-image: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.05), + rgba(255, 255, 255, 0.01) + ); + border-color: var(--color-border-strong); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.35), + 0 16px 34px -16px rgba(0, 0, 0, 0.6); +} /* 1px top highlight so the card reads as paper, not an outline. */ .proof-strip-cell::before { content: ''; @@ -1303,6 +1193,14 @@ transparent ); } +[data-ui="section"][data-surface="dark"] .proof-strip-cell::before { + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.16), + transparent + ); +} .proof-strip-value { font-family: var(--font-garamond); font-size: 58px; @@ -1332,18 +1230,39 @@ } .proof-strip-source { display: inline-block; + max-width: 100%; font-family: var(--font-mono); font-size: 11px; - letter-spacing: 0.08em; + letter-spacing: 0.05em; text-transform: uppercase; color: var(--color-text-muted); margin-top: 9px; + overflow-wrap: anywhere; +} +[data-ui="section"][data-surface="dark"] .proof-strip-source { + color: var(--color-accent); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: var(--color-accent-border-hover); +} +[data-ui="section"][data-surface="dark"] .proof-strip-source:hover { + color: var(--color-accent-hover); + text-decoration-color: currentColor; } @media (max-width: 900px) { .proof-strip-grid { - grid-template-columns: 1fr; gap: 32px; } + .proof-strip-cells { + grid-template-columns: repeat(2, 1fr); + } + .proof-strip-watermark { + font-size: 130px; + right: -16px; + top: -46px; + } +} +@media (max-width: 560px) { .proof-strip-cells { grid-template-columns: 1fr; } @@ -1353,8 +1272,8 @@ .logo-ribbon { background: var(--color-canvas); border-top: 1px solid var(--color-border); - /* No border-bottom: the Yes wall directly below draws its own accent seam - * at the boundary; two hairlines would stack. */ + /* No border-bottom: the dark proof band directly below draws its own accent + * seam at the boundary; two hairlines would stack. */ } .logo-ribbon-line { display: flex; diff --git a/apps/website/src/styles/ui.css b/apps/website/src/styles/ui.css index 252283c94..13aed0676 100644 --- a/apps/website/src/styles/ui.css +++ b/apps/website/src/styles/ui.css @@ -153,16 +153,11 @@ [data-ui="section"][data-surface="tinted"] { background: var(--color-surface-tinted); } -/* Treatment D: the proof strip's tinted band is a whisper of gradient. Scoped - * to #proof so other tinted sections keep the flat token. */ -#proof[data-ui="section"][data-surface="tinted"] { - background: linear-gradient(180deg, #fdfdfd, #f6f6f6); -} [data-ui="section"][data-surface="white"] { background: var(--color-surface); } -/* Dark section scope (homepage Yes wall + FinalCTA dark variant). +/* Dark section scope (homepage proof band + FinalCTA dark variant). * Values mirror libs/design-tokens/src/lib/dark.ts — the website hardwires * the light theme in TS, so dark sections re-scope the CSS variables instead. * If dark.ts changes, update here. Treatment B (spec): the canvas is a subtle