Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions site/build-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,8 @@ function renderPlatformHeader(pathName = "/") {
aria-expanded="false" aria-controls="surfaces-primary-menu" data-surfaces-primary-menu-button>
<span>Menu</span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
<path class="surfaces-menu-glyph-closed" stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
<path class="surfaces-menu-glyph-open" stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path>
</svg>
</button>
<div class="surfaces-primary-menu-backdrop" hidden data-surfaces-primary-menu-backdrop></div>
Expand Down Expand Up @@ -1194,6 +1195,13 @@ a {
display: block;
flex: 0 0 auto;
}
.surfaces-menu-glyph-open,
.surfaces-primary-menu-button[aria-expanded="true"] .surfaces-menu-glyph-closed {
display: none;
}
.surfaces-primary-menu-button[aria-expanded="true"] .surfaces-menu-glyph-open {
display: inline;
}
.surfaces-primary-menu-backdrop {
position: fixed;
inset: var(--site-navigation-height) 0 auto;
Expand Down Expand Up @@ -1249,16 +1257,27 @@ a {
.surfaces-navigation-inner {
width: min(1120px, calc(100% - 32px));
}
.surfaces-brand-lockup {
height: 48px;
}
}
@media (max-width: 359px) {
@media (max-width: 389px) {
.surfaces-navigation-inner {
gap: 8px;
}
.surfaces-brand-lockup {
height: 44px;
}
@media (max-width: 374px) {
.surfaces-primary-menu-button {
width: 44px;
min-width: 44px;
padding: 0;
gap: 0;
}
.surfaces-primary-menu-button > span {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
@media (forced-colors: active) {
Expand Down Expand Up @@ -1321,7 +1340,7 @@ a {
.site-brand-lockup {
display: block;
width: auto;
height: 44px;
height: 56px;
max-width: 100%;
object-fit: contain;
}
Expand Down
11 changes: 9 additions & 2 deletions site/website-presentation.css
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,16 @@
padding-block: 96px;
background: var(--bg);
}
.site-presentation :is(.homepage-problem, .homepage-workflow) .homepage-section-shell {
gap: 0;
}
.site-presentation .homepage-section-heading {
max-width: 620px;
margin-bottom: 40px;
}
.site-presentation .homepage-workflow .homepage-section-heading {
margin-bottom: 48px;
}
.site-presentation .homepage-section-heading h2 {
margin-bottom: 0;
}
Expand Down Expand Up @@ -314,11 +320,12 @@
margin: 0;
}
.site-footer-identity {
display: flex;
margin-right: auto;
}
.site-footer-description,
.site-footer-copyright {
margin: 8px 0 0;
margin: 0;
color: var(--website-nickel);
font-size: 12px;
line-height: 1.45;
Expand All @@ -338,7 +345,7 @@
.site-footer-inner {
align-items: flex-start;
flex-direction: column;
gap: 16px;
gap: 12px;
}
.site-presentation .section {
padding-block: 52px;
Expand Down
6 changes: 3 additions & 3 deletions specs/site-header.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ The primary decisions are where to learn about JudgmentKit and whether to visit

## Cross-site alignment

The live Surfaces header measured on 2026-10-01 is the reference: a 72 px sticky header, no bottom border, a centered 1120 px shell with 24 px minimum side gutters, and 16 px gutters at widths up to 820 px. The canonical outlined lockup is displayed at a 56 px canvas height, 48 px at widths up to 820 px, and 44 px below 360 px. Preserve the source ratio of 425.013342449 × 109 and its built-in padding. Scale the whole image uniformly. Do not crop the canvas, filter its colors, or reconstruct the wordmark as live text. Navigation uses 14 px Inter at weight 500, line height 1.45, 28 px gaps, and 44 px targets.
The live Surfaces header measured on 2026-10-01 is the reference: a 72 px sticky header, no bottom border, a centered 1120 px shell with 24 px minimum side gutters, and 16 px gutters at widths up to 820 px. The canonical outlined lockup is displayed at a 56 px canvas height at every viewport width. Preserve the source ratio of 425.013342449 × 109 and its built-in padding. Scale the whole image uniformly. Do not crop the canvas, filter its colors, or reconstruct the wordmark as live text. Navigation uses 14 px Inter at weight 500, line height 1.45, 28 px gaps, and 44 px targets.

The four-link navigation becomes a Menu control at widths up to 820 px. Below 360 px, the 44 px canvas and an 8 px minimum gap keep the complete lockup and menu button inside the 16 px side gutters, including when classic scrollbars consume 15 px of layout width. The brand and menu remain aligned with Surfaces at shared mobile widths. Both headers retain their geometry on scroll and page navigation. Reserve classic scrollbar space to prevent shorter pages from shifting the header; measure the shell against available layout width when scrollbars consume space. Website fonts are local and preloaded. The face covers weights 100–900 and uses swap display so the loaded Inter face replaces a fallback. Website text and chrome use antialiased/grayscale smoothing, matching Surfaces. Reference specimens retain their own font and smoothing contract.
The four-link navigation becomes a Menu control at widths up to 820 px. The control keeps its visible Menu label and 18 px glyph at 390 px. The glyph changes from three bars to an X when aria-expanded is true. Below 390 px, use an 8 px header gap; below 375 px, the control is 44 px wide and its Menu text is visually hidden while retained in the DOM and accessible name. This narrow fit exception keeps the full 56 px lockup inside the 16 px gutters even when a classic scrollbar consumes 15 px of layout width. The brand and menu remain aligned with Surfaces at shared mobile widths. Both headers retain their geometry on scroll and page navigation. Reserve classic scrollbar space to prevent shorter pages from shifting the header; measure the shell against available layout width when scrollbars consume space. Website fonts are local and preloaded. The face covers weights 100–900 and uses swap display so the loaded Inter face replaces a fallback. Website text and chrome use antialiased/grayscale smoothing, matching Surfaces. Reference specimens retain their own font and smoothing contract.

Validate header dimensions, gutters, complete lockup bounds, asset loading, current-page width stability, menu operation, and horizontal overflow in light and dark modes across desktop, mobile, and breakpoint widths. Compare the local candidate with live Surfaces in both navigation directions, including Back/Forward. Full production round-trip verification requires the candidate to be released.

## Shared footer and favicon

Every shared footer stays Ink in both appearances and links home with the complete canonical `brand/slab-j/lockups/horizontal-reversed.svg`. It displays the full canvas at 44 px high, keeps a minimum 44 px link target, and wraps the release link when space is limited. Both home links have the accessible name `JudgmentKit home`. Footer text uses Inter, with a supporting Rule-colored link and visible Ivory focus.
Every shared footer stays Ink in both appearances and links home with the complete canonical `brand/slab-j/lockups/horizontal-reversed.svg`. It displays the full canvas at 56 px high, keeps a minimum 44 px link target, and wraps the release link when space is limited. At widths up to 820 px, its identity, description, copyright and release link form a stack with 12 px gaps and no extra paragraph margins or inline-image baseline gap. Both home links have the accessible name `JudgmentKit home`. Footer text uses Inter, with a supporting Rule-colored link and visible Ivory focus.

The site build copies both lockup files byte for byte to `/assets/brand/judgmentkit-lockup.svg` and `/assets/brand/judgmentkit-lockup-reversed.svg`. Browser and installed icons remain governed by [the site brand asset contract](site-brand-assets.md). The website presentation contract is [public website presentation](site-presentation.md).

Expand Down
4 changes: 2 additions & 2 deletions specs/site-presentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ The website shares Surfaces' public presentation. Its composition follows [the w
2. Use an 1120 px shell with 24 px desktop gutters and 16 px gutters at 820 px and below. The four-link header compacts at 820 px. Preserve its 72 px height, keyboard behavior and narrow-screen lockup geometry.
3. Light pages use Ivory `#F7F6F2`, Ink `#0B0D0E`, white panels and Rule `#DAD7CF`. Dark pages use Ink, Ivory, neutral `#24282b` panels and Nickel `#8B9096` supporting text and borders. Section labels follow the muted text role; description and copyright in the Ink footer use Nickel. Preserve the existing success, warning and risk state colors.
4. Keep the homepage hero and shared footer Ink in both appearances. Use the existing reversed whole lockup on those surfaces. Use the wide lens environment behind one left text stack, with a CSS readability overlay and no separate image card, border or radius. Desktop scene height is 560 px below the 72 px header. Compact scenes have a 528 px minimum height and position the lens at 72% center behind the existing readability overlay. Keep the original portrait as reference; [art provenance](../docs/site-homepage-art-provenance.json) records the new derivative.
5. Primary website actions have a minimum 48 px height, 14 px Inter text at weight 500 and a 4 px radius. The homepage uses ruled rows rather than cards. Panels on detailed pages retain an 8 px radius. Use quiet 120 ms color transitions, visible focus and the existing reduced-motion behavior.
5. Primary website actions have a minimum 48 px height, 14 px Inter text at weight 500 and a 4 px radius. The homepage uses ruled rows rather than cards. Keep 40 px between the problem heading and its row, and 48 px between the workflow heading and its row at every breakpoint; the section shell must not add a second grid gap. Panels on detailed pages retain an 8 px radius. Use quiet 120 ms color transitions, visible focus and the existing reduced-motion behavior.

## Scope and implementation

`site/build-site.mjs` applies `site-presentation` to its owned homepage, Value, Docs, Design System and Examples pages. It appends `site/website-presentation.css` to the generated stylesheet. The shared footer applies its Ink contract across generated pages. Its lockup, description, copyright and release utility form a desktop row and a left-aligned compact stack. Docs contains a compact resource row so evaluation evidence, MCP, the installer and Handbooks remain reachable after reducing primary navigation.
`site/build-site.mjs` applies `site-presentation` to its owned homepage, Value, Docs, Design System and Examples pages. It appends `site/website-presentation.css` to the generated stylesheet. The shared footer applies its Ink contract across generated pages. Its 56 px lockup, description, copyright and release utility form a desktop row and a left-aligned compact stack with 12 px gaps and zero paragraph margins. Docs contains a compact resource row so evaluation evidence, MCP, the installer and Handbooks remain reachable after reducing primary navigation.

Evaluation and report bodies keep their existing typography, colors and wide-table layouts. Copied examples, experiments, reports and the immutable recording HTML do not receive the presentation class. Reference previews under `.jk-specimen-preview` retain their system font and adapter tokens; website heading rules exclude their descendants. Captured screenshots and the source visual-token adapters remain unchanged.

Expand Down
33 changes: 32 additions & 1 deletion tests/site-header-browser.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ async function measure(client, sessionId) {
const scene = document.querySelector('.homepage-scene');
const problem = document.querySelector('.homepage-problem');
const workflow = document.querySelector('.family-workflow-row');
const rowGap = section => {
if (!section) return null;
const heading = section.querySelector('.homepage-section-heading').getBoundingClientRect();
const row = section.querySelector('.family-problem-row,.family-workflow-row').getBoundingClientRect();
return row.top - heading.bottom;
};
const detailPage = document.querySelector('.docs-page,.design-system-page');
const detailLayout = document.querySelector('.doc-layout,.design-system-layout');
return {
Expand All @@ -142,6 +148,10 @@ async function measure(client, sessionId) {
headerImage: image(lockup), footerImage: image(footerLockup),
headerAccessibleName: brand.getAttribute('aria-label'), footerAccessibleName: footerBrand.getAttribute('aria-label'),
footerTarget: { ...rect(footerBrand), href: footerBrand.getAttribute('href') },
footerItems: [...footer.querySelector('.site-footer-inner').children].map(rect),
footerParagraphMargin: getComputedStyle(footer.querySelector('.site-footer-description')).margin,
menuLabelClip: getComputedStyle(menu.querySelector('span')).clipPath,
menuGlyphs: [...menu.querySelectorAll('path')].filter(path => getComputedStyle(path).display !== 'none').map(path => path.getAttribute('d')),
headerBackground: background(nav), footerBackground: background(footer),
links: links.map(el => ({ href: el.getAttribute('href'), rect: rect(el) })),
menuVisible: menu.getBoundingClientRect().height > 0,
Expand Down Expand Up @@ -176,6 +186,8 @@ async function measure(client, sessionId) {
actions: hero && hero.querySelectorAll('.hero-action').length,
nextSectionY: problem && problem.getBoundingClientRect().top,
problemItems: document.querySelectorAll('.family-problem-row > li').length,
problemRowGap: rowGap(problem),
workflowRowGap: rowGap(document.querySelector('.homepage-workflow')),
sectionEyebrowColor: problem && getComputedStyle(problem.querySelector('.eyebrow')).color,
workflow: workflow && [...workflow.children].map(step => {
const title = step.querySelector('h3'), description = step.querySelector('p'), ordinal = step.querySelector('.family-ordinal'), icon = step.querySelector('.family-workflow-icon');
Expand Down Expand Up @@ -251,6 +263,8 @@ async function assertBrand(client, sessionId, measurement, context) {
if (context.width > 820) assert.equal(presentation.nextSectionY, 632, `${label}: desktop header72 + hero560`);
else assert.ok(presentation.hero.height >= 528, `${label}: mobile environment retains its minimum scene height`);
assert.equal(presentation.problemItems, 3, label);
assert.equal(presentation.problemRowGap, 40, `${label}: problem heading-to-row gap`);
assert.equal(presentation.workflowRowGap, 48, `${label}: workflow heading-to-row gap`);
assert.equal(presentation.sectionEyebrowColor, context.colorScheme === 'dark' ? 'rgb(139, 144, 150)' : 'rgb(85, 91, 96)', `${label}: muted section label`);
assert.equal(presentation.workflow.length, 5, label);
for (const step of presentation.workflow) {
Expand Down Expand Up @@ -278,6 +292,14 @@ async function assertBrand(client, sessionId, measurement, context) {
assert.ok(image.rendered.x >= 0 && image.rendered.right <= measurement.layoutWidth, `${label}: image bounds inside page`);
}
assert.ok(measurement.footerTarget.height >= 44, `${label}: footer home target`);
assert.equal(measurement.footerImage.rendered.height, 56, `${label}: full footer lockup height`);
assert.equal(measurement.footerParagraphMargin, '0px', `${label}: footer copy adds no margin`);
if (context.width <= 820) {
for (let index = 1; index < measurement.footerItems.length; index++) {
const previous = measurement.footerItems[index - 1], current = measurement.footerItems[index];
assert.equal(current.y - previous.y - previous.height, 12, `${label}: compact footer rhythm`);
}
}
assert.equal(measurement.footerTarget.href, '/', label);
for (const selector of ['.surfaces-navigation-identifier', '.site-footer-brand']) {
const node = await getAxNode(client, sessionId, selector);
Expand Down Expand Up @@ -333,7 +355,7 @@ try {
assert.equal(home.header.width, home.layoutWidth, label);
assert.equal(home.shell.x, gutter, label);
assert.equal(home.shell.right, home.layoutWidth - gutter, label);
const lockupHeight = expected.width < 360 ? 44 : expected.width <= 820 ? 48 : 56;
const lockupHeight = 56;
assert.equal(home.lockup.x, gutter, label);
assert.equal(home.lockup.height, lockupHeight, label);
assert.equal(home.brand.y, (72 - lockupHeight) / 2, label);
Expand Down Expand Up @@ -361,11 +383,20 @@ try {
assert.equal(home.button.y, 14, label);
assert.equal(home.button.height, 44, label);
assert.equal(home.button.right, home.layoutWidth - gutter, label);
assert.deepEqual(home.menuGlyphs, ['M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'], `${label}: closed menu glyph`);
if (expected.width < 375) {
assert.equal(home.button.width, 44, `${label}: narrow fit target`);
assert.equal(home.menuLabelClip, 'inset(50%)', `${label}: narrow label remains in the DOM`);
} else {
assert.ok(home.button.width > 44, `${label}: Menu label retains its natural width`);
assert.equal(home.menuLabelClip, 'none', `${label}: Menu label stays visible`);
}
await tabUntil(client, sid, '[data-surfaces-primary-menu-button]');
const focus = await evaluate(client, sid, `getComputedStyle(document.activeElement).outlineStyle`);
assert.equal(focus, "solid", `${label}: menu focus must be visible`);
await pressKey(client, sid, "Enter");
assert.equal(await evaluate(client, sid, `document.querySelector('[data-surfaces-primary-menu-button]').getAttribute('aria-expanded')`), "true");
assert.deepEqual(await evaluate(client, sid, `[...document.querySelectorAll('[data-surfaces-primary-menu-button] path')].filter(path => getComputedStyle(path).display !== 'none').map(path => path.getAttribute('d'))`), ['M6 18 18 6M6 6l12 12'], `${label}: open menu uses the Surfaces X glyph`);
const menu = await evaluate(client, sid, `(() => {
const el = document.querySelector('[data-surfaces-primary-menu-list]');
return { hidden: el.hidden, top: el.getBoundingClientRect().top, count: el.querySelectorAll('a').length, last: el.querySelector('a:last-child').href };
Expand Down
2 changes: 1 addition & 1 deletion tests/site.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ assert.ok(siteCss.includes(".site-shell {\n width: 100%;\n max-width: var(--si
assert.ok(siteCss.includes(".site-footer {\n padding: 28px 24px;"));
assert.match(
siteCss,
/\.site-brand-lockup \{[^}]*width: auto;[^}]*height: 44px;[^}]*object-fit: contain;/s,
/\.site-brand-lockup \{[^}]*width: auto;[^}]*height: 56px;[^}]*object-fit: contain;/s,
"the footer must preserve the complete lockup canvas and natural ratio",
);
assert.match(
Expand Down
Loading