From 91c05290cf67e07110487c8f92ef8c2bc19f6d4d Mon Sep 17 00:00:00 2001 From: Mike Long Date: Sat, 17 Oct 2026 23:40:28 -0700 Subject: [PATCH] Align public site spacing and navigation with Surfaces --- site/build-site.mjs | 35 +++++++++++++++++++++++------- site/website-presentation.css | 11 ++++++++-- specs/site-header.md | 6 ++--- specs/site-presentation.md | 4 ++-- tests/site-header-browser.test.mjs | 33 +++++++++++++++++++++++++++- tests/site.test.mjs | 2 +- 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/site/build-site.mjs b/site/build-site.mjs index df11f34..f92c06e 100755 --- a/site/build-site.mjs +++ b/site/build-site.mjs @@ -587,7 +587,8 @@ function renderPlatformHeader(pathName = "/") { aria-expanded="false" aria-controls="surfaces-primary-menu" data-surfaces-primary-menu-button> Menu @@ -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; @@ -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) { @@ -1321,7 +1340,7 @@ a { .site-brand-lockup { display: block; width: auto; - height: 44px; + height: 56px; max-width: 100%; object-fit: contain; } diff --git a/site/website-presentation.css b/site/website-presentation.css index 054b6f8..1c04a43 100644 --- a/site/website-presentation.css +++ b/site/website-presentation.css @@ -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; } @@ -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; @@ -338,7 +345,7 @@ .site-footer-inner { align-items: flex-start; flex-direction: column; - gap: 16px; + gap: 12px; } .site-presentation .section { padding-block: 52px; diff --git a/specs/site-header.md b/specs/site-header.md index c3d8dce..ef7d953 100644 --- a/specs/site-header.md +++ b/specs/site-header.md @@ -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). diff --git a/specs/site-presentation.md b/specs/site-presentation.md index 35696ea..0f7ed46 100644 --- a/specs/site-presentation.md +++ b/specs/site-presentation.md @@ -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. diff --git a/tests/site-header-browser.test.mjs b/tests/site-header-browser.test.mjs index 8bfde07..324c712 100644 --- a/tests/site-header-browser.test.mjs +++ b/tests/site-header-browser.test.mjs @@ -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 { @@ -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, @@ -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'); @@ -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) { @@ -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); @@ -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); @@ -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 }; diff --git a/tests/site.test.mjs b/tests/site.test.mjs index ed457b9..960c2d1 100644 --- a/tests/site.test.mjs +++ b/tests/site.test.mjs @@ -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(