diff --git a/package.json b/package.json index db310d2..023367b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "site:build": "node site/build-site.mjs", "site:dev": "npm run site:build && node scripts/site-local-server.mjs", "site:serve": "node scripts/site-local-server.mjs", + "test:site": "node tests/site.test.mjs && node tests/site-local-server.test.mjs", "smoke:icons": "node scripts/lucide-icon-catalog-smoke.mjs", "smoke:openai-ui-workflow": "node scripts/smoke-openai-ui-workflow.mjs", "test:presentation-theme": "node tests/presentation-theme.test.mjs && npm run presentation-theme:actual:evidence && npm run presentation-theme:pack:smoke", diff --git a/scripts/site-local-server.mjs b/scripts/site-local-server.mjs index 7f5db4e..85b5048 100644 --- a/scripts/site-local-server.mjs +++ b/scripts/site-local-server.mjs @@ -23,6 +23,7 @@ const CONTENT_TYPES = new Map([ [".png", "image/png"], [".svg", "image/svg+xml; charset=utf-8"], [".txt", "text/plain; charset=utf-8"], + [".webp", "image/webp"], ]); function parseArgs(argv) { diff --git a/site/assets/judgment-lens-hero.webp b/site/assets/judgment-lens-hero.webp new file mode 100644 index 0000000..f0b599f Binary files /dev/null and b/site/assets/judgment-lens-hero.webp differ diff --git a/site/build-site.mjs b/site/build-site.mjs index c2565cb..fa0c0e4 100755 --- a/site/build-site.mjs +++ b/site/build-site.mjs @@ -29,6 +29,8 @@ const SOCIAL_THUMBNAIL_SOURCE_FILENAME = "judgmentkit-social-thumbnail.png"; const SOCIAL_THUMBNAIL_FILENAME = "judgmentkit-social-thumbnail-20260611.png"; const SOCIAL_THUMBNAIL_PATH = `/assets/${SOCIAL_THUMBNAIL_FILENAME}`; const SOCIAL_THUMBNAIL_ALT = "JudgmentKit. Before the UI."; +const HOMEPAGE_HERO_ART_FILENAME = "judgment-lens-hero.webp"; +const HOMEPAGE_HERO_ART_PATH = `/assets/${HOMEPAGE_HERO_ART_FILENAME}`; const DESIGN_SYSTEM_SPECIMEN_RENDERER = { id: "judgmentkit-static-specimens", version: "0.1.0", @@ -820,6 +822,12 @@ const stylesheet = ` --system-map-node-llm-bg: #fbf3e7; --system-map-node-output-bg: #f0f8f2; --system-map-node-blocked-bg: #fff7ec; + --hero-bg-end: #edf2ef; + --hero-glow: rgba(36, 95, 115, 0.17); + --hero-action-secondary-bg: rgba(255, 255, 255, 0.62); + --hero-art-border: rgba(255, 255, 255, 0.34); + --hero-art-shadow: rgba(15, 35, 41, 0.22); + --hero-caption: #525854; --eval-serif: "Source Serif 4", "Iowan Old Style", Charter, "Palatino Linotype", "Book Antiqua", Georgia, serif; --site-gutter: clamp(18px, 4vw, 56px); --site-shell-width: 1220px; @@ -864,6 +872,12 @@ const stylesheet = ` --system-map-node-llm-bg: rgba(224, 177, 93, 0.14); --system-map-node-output-bg: rgba(130, 201, 154, 0.14); --system-map-node-blocked-bg: rgba(224, 177, 93, 0.18); + --hero-bg-end: #15211e; + --hero-glow: rgba(125, 182, 199, 0.15); + --hero-action-secondary-bg: rgba(24, 29, 27, 0.72); + --hero-art-border: rgba(169, 215, 228, 0.2); + --hero-art-shadow: rgba(0, 0, 0, 0.46); + --hero-caption: #b8c0bb; } } * { @@ -1163,11 +1177,122 @@ a { min-height: 78vh; } .homepage-hero { + position: relative; + display: block; + min-height: 0; + overflow: hidden; + isolation: isolate; + background: + radial-gradient(circle at 77% 24%, var(--hero-glow), transparent 34%), + linear-gradient(132deg, var(--bg) 0%, var(--bg) 54%, var(--hero-bg-end) 100%); +} +.homepage-hero::before { + content: ""; + position: absolute; + z-index: -1; + top: 12%; + right: -9%; + width: min(54vw, 760px); + aspect-ratio: 1; + border: 1px solid var(--hero-glow); + border-radius: 50%; + opacity: 0.55; + pointer-events: none; + transform: translate3d(0, 0, 0); +} +.homepage-hero-shell { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(390px, 0.82fr); + gap: clamp(40px, 6vw, 78px); + align-items: center; + min-height: clamp(590px, calc(100vh - var(--site-navigation-height)), 780px); +} +.homepage-hero-copy { + position: relative; + z-index: 2; + max-width: 690px; + padding-block: 28px; +} +.homepage-hero .eyebrow { + display: inline-flex; + align-items: center; + gap: 10px; + margin-bottom: 24px; + font-size: 13px; + letter-spacing: 0.07em; + text-transform: uppercase; +} +.homepage-hero .eyebrow::before { + content: ""; + width: 28px; + height: 2px; + border-radius: 999px; + background: var(--accent-strong); +} +.homepage-hero h1 { + max-width: 10.8ch; + margin-bottom: 26px; + font-size: clamp(56px, 6.25vw, 92px); + line-height: 0.91; + letter-spacing: -0.055em; +} +.homepage-hero .lede { + max-width: 58ch; + font-size: clamp(18px, 1.55vw, 21px); + line-height: 1.55; +} +.homepage-hero .hero-actions { + margin-top: 32px; +} +.homepage-hero-visual { + width: 100%; + max-width: 510px; + margin: 0; + justify-self: end; +} +.homepage-hero-art { + position: relative; + overflow: hidden; + aspect-ratio: 4 / 5; + border: 1px solid var(--hero-art-border); + border-radius: clamp(20px, 2.4vw, 34px); + background: #08181d; + box-shadow: + 0 42px 100px var(--hero-art-shadow), + 0 12px 28px rgba(15, 35, 41, 0.18); +} +.homepage-hero-art::after { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(180deg, rgba(4, 18, 23, 0) 58%, rgba(4, 18, 23, 0.3) 100%), + linear-gradient(90deg, rgba(255, 255, 255, 0.07), transparent 24%); + pointer-events: none; +} +.homepage-hero-art img { display: block; - min-height: clamp(420px, 60vh, 640px); + width: 100%; + height: 100%; + object-fit: cover; + object-position: 50% 50%; } -.homepage-hero > div { - max-width: 780px; +.homepage-hero-visual figcaption { + display: flex; + align-items: center; + gap: 10px; + margin: 14px 6px 0; + color: var(--hero-caption); + font-size: 12px; + font-weight: 650; + letter-spacing: 0.025em; +} +.homepage-hero-visual figcaption::before { + content: ""; + width: 26px; + height: 1px; + flex: 0 0 auto; + background: currentColor; } .eyebrow { color: var(--accent-strong); @@ -1231,20 +1356,33 @@ h2 { display: inline-flex; align-items: center; justify-content: center; - min-height: 42px; - padding: 9px 14px; + min-height: 46px; + padding: 10px 16px; border: 1px solid var(--accent-strong); border-radius: 8px; font-weight: 800; text-decoration: none; + transition: + transform 160ms ease, + box-shadow 160ms ease, + background-color 160ms ease; +} +.hero-action:hover { + transform: translateY(-2px); +} +.hero-action:focus-visible { + outline: 0; + box-shadow: 0 0 0 3px var(--focus-ring); } .hero-action-primary { color: var(--bg); background: var(--accent-strong); + box-shadow: 0 12px 28px rgba(19, 63, 78, 0.18); } .hero-action-secondary { color: var(--accent-strong); - background: transparent; + background: var(--hero-action-secondary-bg); + backdrop-filter: blur(8px); } .proof-panel, .route-grid article { @@ -3808,6 +3946,36 @@ pre { .doc-layout { display: block; } + .homepage-hero { + padding-top: clamp(44px, 8vw, 68px); + padding-bottom: clamp(52px, 9vw, 76px); + } + .homepage-hero::before { + top: auto; + right: -28%; + bottom: -10%; + width: min(88vw, 680px); + } + .homepage-hero-shell { + grid-template-columns: minmax(0, 1fr); + gap: clamp(34px, 7vw, 52px); + min-height: 0; + } + .homepage-hero-copy { + max-width: 740px; + padding-block: 0; + } + .homepage-hero-visual { + width: min(100%, 680px); + max-width: none; + justify-self: start; + } + .homepage-hero-art { + aspect-ratio: 16 / 11; + } + .homepage-hero-art img { + object-position: 50% 44%; + } .proof-panel { margin-top: 18px; } @@ -3958,6 +4126,44 @@ pre { border-bottom: 0; } } +@media (max-width: 560px) { + .homepage-hero { + padding-top: 40px; + padding-bottom: 52px; + } + .homepage-hero .eyebrow { + margin-bottom: 20px; + font-size: 12px; + } + .homepage-hero h1 { + max-width: 10ch; + margin-bottom: 22px; + font-size: clamp(48px, 14.6vw, 66px); + } + .homepage-hero .hero-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: 28px; + } + .homepage-hero .hero-action-primary { + grid-column: 1 / -1; + } + .homepage-hero-art { + aspect-ratio: 1 / 1; + border-radius: 20px; + } + .homepage-hero-art img { + object-position: 50% 43%; + } + .homepage-hero-visual figcaption { + align-items: flex-start; + line-height: 1.45; + } + .homepage-hero-visual figcaption::before { + margin-top: 0.7em; + } +} @media (max-width: 767px) { .surfaces-navigation-inner { padding-left: 16px; @@ -3986,6 +4192,43 @@ pre { display: none; } } +@media (prefers-reduced-motion: reduce) { + .homepage-hero-art img { + transform: none; + } + .hero-action { + transition: none; + } + .hero-action:hover { + transform: none; + } +} +@media (prefers-contrast: more) { + .homepage-hero-art, + .hero-action { + border-width: 2px; + border-color: var(--ink); + box-shadow: none; + } +} +@media (forced-colors: active) { + .homepage-hero { + background: Canvas; + } + .homepage-hero::before { + display: none; + } + .homepage-hero-art, + .hero-action { + border-color: CanvasText; + box-shadow: none; + } + .hero-action-primary, + .hero-action-secondary { + color: LinkText; + background: Canvas; + } +} `; function homepage() { @@ -3993,15 +4236,23 @@ function homepage() { "JudgmentKit", `
-
-

Activity-first judgment for AI agents

-

Judgment before generation.

-

JudgmentKit catches implementation-shaped UI before it ships, then gives the agent a repair path grounded in the user's real work.

-
- What it prevents - Examples - Evals +
+
+

Activity-first judgment for AI agents

+

Judgment before generation.

+

JudgmentKit catches implementation-shaped UI before it ships, then gives the agent a repair path grounded in the user's real work.

+
+
+
+ Rough stone fragments pass through a teal glass lens and emerge as an ordered path. +
+
Raw structure, judged against the work, becomes purposeful product direction.
+
@@ -8205,6 +8456,10 @@ export async function buildSite(outDir = DEFAULT_OUT_DIR) { socialThumbnailSourcePath, path.join(outDir, "assets", SOCIAL_THUMBNAIL_SOURCE_FILENAME), ); + await fs.copyFile( + path.join(__dirname, "assets", HOMEPAGE_HERO_ART_FILENAME), + path.join(outDir, "assets", HOMEPAGE_HERO_ART_FILENAME), + ); await buildSystemMapFlowAssets(outDir); await fs.writeFile( path.join(outDir, "favicon.svg"), diff --git a/tests/site-local-server.test.mjs b/tests/site-local-server.test.mjs index c961f29..7646a3e 100644 --- a/tests/site-local-server.test.mjs +++ b/tests/site-local-server.test.mjs @@ -340,6 +340,30 @@ try { assert.equal(response.headers.get("content-type"), "image/png"); } + { + const response = await fetchRoute(url, "/assets/judgment-lens-hero.webp"); + const body = Buffer.from(await response.arrayBuffer()); + + assert.equal(response.status, 200, "homepage hero art should return 200"); + assert.equal(response.headers.get("content-type"), "image/webp"); + assert.equal(Number(response.headers.get("content-length")), body.length); + assert.equal(body.subarray(0, 4).toString("ascii"), "RIFF"); + assert.equal(body.subarray(8, 12).toString("ascii"), "WEBP"); + + const headResponse = await fetchRoute(url, "/assets/judgment-lens-hero.webp", { + method: "HEAD", + }); + + assert.equal(headResponse.status, 200, "homepage hero art HEAD should return 200"); + assert.equal(headResponse.headers.get("content-type"), "image/webp"); + assert.equal( + Number(headResponse.headers.get("content-length")), + body.length, + "homepage hero art HEAD should report the full body size", + ); + assert.equal(await headResponse.text(), ""); + } + { const response = await fetchRoute( url, diff --git a/tests/site.test.mjs b/tests/site.test.mjs index 05612ed..ca12da3 100644 --- a/tests/site.test.mjs +++ b/tests/site.test.mjs @@ -302,10 +302,16 @@ assert.ok(siteCss.includes(".system-map-flow-root .react-flow,\n.system-map-flow assert.ok(siteCss.includes("box-shadow: 0 0 0 2px var(--focus-ring);")); const stepMarkerBackgrounds = cssCustomPropertyValues(siteCss, "--step-marker-bg"); const stepMarkerTextColors = cssCustomPropertyValues(siteCss, "--step-marker-ink"); +const heroPrimaryBackgrounds = cssCustomPropertyValues(siteCss, "--accent-strong"); +const heroPrimaryTextColors = cssCustomPropertyValues(siteCss, "--bg"); assert.deepEqual(stepMarkerBackgrounds, ["#245f73", "#a9d7e4"]); assert.deepEqual(stepMarkerTextColors, ["#ffffff", "#101312"]); +assert.deepEqual(heroPrimaryBackgrounds, ["#133f4e", "#a9d7e4"]); +assert.deepEqual(heroPrimaryTextColors, ["#f8f7f2", "#101312"]); assertContrastPair("light design-system step marker", stepMarkerTextColors[0], stepMarkerBackgrounds[0]); assertContrastPair("dark design-system step marker", stepMarkerTextColors[1], stepMarkerBackgrounds[1]); +assertContrastPair("light homepage hero primary action", heroPrimaryTextColors[0], heroPrimaryBackgrounds[0]); +assertContrastPair("dark homepage hero primary action", heroPrimaryTextColors[1], heroPrimaryBackgrounds[1]); assert.equal(platformNavCss.includes("position: sticky;"), false); assert.ok(siteCss.includes(".surfaces-primary-menu")); assert.ok(siteCss.includes(".surfaces-primary-menu-button")); @@ -358,8 +364,22 @@ assert.ok(homepage.includes("[data-surfaces-primary-menu-button]")); assert.ok(homepage.includes("[data-surfaces-system-menu-button]")); assert.ok(homepage.includes("Judgment before generation.")); assert.ok(homepage.includes("JudgmentKit catches implementation-shaped UI before it ships")); +assert.equal((homepageMain.match(/', + ), +); +assert.ok( + homepage.includes( + "
Raw structure, judged against the work, becomes purposeful product direction.
", + ), +); +assert.equal(homepage.includes('src="/assets/judgment-lens-hero.webp" loading="lazy"'), false); assert.ok(homepage.includes('class="hero-actions" aria-label="Primary proof paths"')); assert.ok(homepage.includes('class="hero-action hero-action-primary" data-hero-action="primary" href="/value/"')); assert.ok(homepage.includes('class="hero-action hero-action-secondary" data-hero-action="secondary" href="/examples/"')); @@ -391,6 +411,14 @@ assert.ok(siteCss.includes(".evaluation-panel")); assert.ok(siteCss.includes(".failure-grid")); assert.ok(siteCss.includes(".route-grid-proof")); assert.ok(siteCss.includes(".route-grid-adoption")); +assert.ok(siteCss.includes(".homepage-hero-shell {\n display: grid;")); +assert.ok(siteCss.includes("grid-template-columns: minmax(0, 1.08fr) minmax(390px, 0.82fr);")); +assert.ok(siteCss.includes(".homepage-hero-art img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;")); +assert.ok(siteCss.includes(".hero-action:focus-visible {\n outline: 0;\n box-shadow: 0 0 0 3px var(--focus-ring);")); +assert.ok(siteCss.includes("@media (prefers-reduced-motion: reduce)")); +assert.ok(siteCss.includes("@media (prefers-contrast: more)")); +assert.ok(siteCss.includes("@media (forced-colors: active)")); +assert.ok(siteCss.includes(".homepage-hero-shell {\n grid-template-columns: minmax(0, 1fr);")); assert.equal(siteCss.includes("text-decoration-line: underline;"), false); assert.equal(homepageMain.includes("Prompt"), false); assert.equal(homepageMain.includes("JSON schema"), false); @@ -1856,6 +1884,15 @@ const versionedSocialThumbnail = fs.readFileSync(path.join(tempDir, "assets", "j assert.equal(versionedSocialThumbnail.subarray(1, 4).toString("ascii"), "PNG"); assert.equal(versionedSocialThumbnail.readUInt32BE(16), 1200); assert.equal(versionedSocialThumbnail.readUInt32BE(20), 630); +const homepageHeroArt = fs.readFileSync(path.join(tempDir, "assets", "judgment-lens-hero.webp")); +const homepageHeroArtSource = fs.readFileSync( + new URL("../site/assets/judgment-lens-hero.webp", import.meta.url), +); +assert.deepEqual(homepageHeroArt, homepageHeroArtSource); +assert.equal(homepageHeroArt.subarray(0, 4).toString("ascii"), "RIFF"); +assert.equal(homepageHeroArt.subarray(8, 12).toString("ascii"), "WEBP"); +assert.ok(homepageHeroArt.length > 0); +assert.ok(homepageHeroArt.length < 250_000); for (const experimentPath of [ ["experiments", "netflix-library", "index.html"],