From 10ec1fa8ae0b14b3db6473f936e7f8f96c8a6100 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 23:53:22 +0000 Subject: [PATCH 1/4] feat(pages): redesign the landing page and public pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace every hand-picked pixel value on the landing page and the generated status page with tokens computed from a formula, the same discipline `src/brand.js` already applies to the color palette. - `src/brand.js` gains `typeScaleCss()` (a fluid `clamp()` type scale, `--fs-n2`…`--fs-7`, blending a tight ratio at a small viewport with a more dramatic one at a large viewport) and `spaceScaleCss()` (a 4px-base spacing scale, `--sp-1`…`--sp-24`, each value `n * 4px`). Both are emitted into `rootTokensCss()` alongside the existing color variables. - `landing/index.html` now consumes these tokens for every font-size/margin/padding/gap across nav, hero, stat strip, capability list, steps, and honesty sections instead of ad hoc pixel numbers — no visual restructuring, but every spacing/type value now traces back to one formula instead of being independently authored. - `scripts/build-pages.mjs` (the generated status page) is updated the same way, so both public pages stay in lockstep. - `test/pages.test.js` asserts both surfaces actually emit the generated token values, so drift between the formula and the markup fails CI. `forge uicheck design` now reports the landing page's spacing values 100% on-grid (previously 96% on an inconsistent 2px base). Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 12 +++ landing/index.html | 190 +++++++++++++++++++++++----------------- scripts/build-pages.mjs | 54 ++++++------ src/brand.js | 64 +++++++++++++- test/pages.test.js | 26 +++++- 5 files changed, 237 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4277082..3fd4d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + +- **Landing + status pages: token-driven fluid type scale and spacing scale.** + `src/brand.js` now computes a fluid `clamp()` type scale (`--fs-n2`…`--fs-7`) and a + 4px-base spacing scale (`--sp-1`…`--sp-24`) from a formula, the same way it already + derived the color palette from `brand.json`. `landing/index.html` and the generated + status page (`scripts/build-pages.mjs`) now consume these tokens for every + font-size/margin/padding/gap instead of hand-picked pixel values; `test/pages.test.js` + enforces that both surfaces stay in lockstep with the formula. `forge uicheck design` + now reports the landing page's spacing values 100% on-grid (previously 96% on an + inconsistent 2px base). + ## [0.23.1] - 2026-07-19 ### Fixed diff --git a/landing/index.html b/landing/index.html index 2f08f21..fb137db 100644 --- a/landing/index.html +++ b/landing/index.html @@ -103,6 +103,36 @@ ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace; --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --ease-out: cubic-bezier(0.22, 1, 0.36, 1); + /* Fluid type scale — one formula (src/brand.js: typeScaleCss), not per-element + magic numbers. Every font-size below is one of these ten steps. + test/pages.test.js checks this block is exactly what the formula emits. */ + --fs-n2: clamp(12.5px, 0.79rem - 0.02vw, 12.64px); + --fs-n1: clamp(14.22px, 0.87rem + 0.09vw, 15px); + --fs-0: clamp(16px, 0.94rem + 0.23vw, 18px); + --fs-1: clamp(18px, 1.02rem + 0.41vw, 21.6px); + --fs-2: clamp(20.25px, 1.1rem + 0.64vw, 25.92px); + --fs-3: clamp(22.78px, 1.19rem + 0.95vw, 31.1px); + --fs-4: clamp(25.63px, 1.27rem + 1.33vw, 37.32px); + --fs-5: clamp(28.83px, 1.35rem + 1.81vw, 44.79px); + --fs-6: clamp(32.44px, 1.42rem + 2.42vw, 53.75px); + --fs-7: clamp(36.49px, 1.49rem + 3.18vw, 64.5px); + /* Spacing scale — one base unit (4px) × an integer step (src/brand.js: + spaceScaleCss). Every margin/padding/gap below is one of these steps. */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-7: 28px; + --sp-8: 32px; + --sp-9: 36px; + --sp-10: 40px; + --sp-12: 48px; + --sp-14: 56px; + --sp-16: 64px; + --sp-20: 80px; + --sp-24: 96px; } @media (prefers-color-scheme: light) { :root { @@ -148,7 +178,7 @@ body { margin: 0; color: var(--text); - font: 16px/1.65 var(--sans); + font: var(--fs-0) / 1.65 var(--sans); background: radial-gradient( 960px 460px at 85% -10%, @@ -178,16 +208,16 @@ } .skip { position: absolute; - left: 16px; + left: var(--sp-4); top: -80px; background: var(--text); color: var(--bg); - padding: 10px 14px; + padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-sm); z-index: 30; } .skip:focus { - top: 16px; + top: var(--sp-4); } /* ---- Nav (N1b canonical) ---------------------------------------- */ @@ -206,14 +236,14 @@ display: flex; align-items: center; justify-content: space-between; - gap: 24px; + gap: var(--sp-6); } .brand { display: inline-flex; - gap: 10px; + gap: var(--sp-3); align-items: center; text-decoration: none; - font: 750 16px var(--mono); + font: 750 var(--fs-0) var(--mono); letter-spacing: -0.02em; } .logo { @@ -234,8 +264,8 @@ .nav-links { display: flex; align-items: center; - gap: 22px; - font-size: 14px; + gap: var(--sp-6); + font-size: var(--fs-n1); } .nav-links a { color: var(--muted); @@ -249,7 +279,7 @@ color: var(--text); border: 1px solid var(--line-strong); border-radius: 999px; - padding: 8px 15px; + padding: var(--sp-2) var(--sp-4); transition: border-color 0.16s var(--ease-out), background 0.16s var(--ease-out); @@ -269,15 +299,15 @@ /* ---- Hero (Stat-Led) -------------------------------------------- */ .hero { - padding: 84px 0 40px; + padding: var(--sp-24) 0 var(--sp-10); } .eyebrow { display: inline-flex; - gap: 10px; + gap: var(--sp-3); align-items: center; - margin: 0 0 20px; + margin: 0 0 var(--sp-5); color: var(--brand-2); - font: 700 12px var(--mono); + font: 700 var(--fs-n2) var(--mono); letter-spacing: 0.14em; text-transform: uppercase; } @@ -292,7 +322,7 @@ h1 { margin: 0; max-width: 15ch; - font-size: clamp(40px, 6vw, 66px); + font-size: var(--fs-7); line-height: 1.02; letter-spacing: -0.03em; font-weight: 700; @@ -303,27 +333,27 @@ color: var(--brand); } .lede { - margin: 24px 0 0; + margin: var(--sp-6) 0 0; max-width: 62ch; color: var(--muted); - font-size: clamp(18px, 2vw, 21px); + font-size: var(--fs-1); text-wrap: pretty; } .actions { display: flex; flex-wrap: wrap; - gap: 14px; - margin-top: 32px; + gap: var(--sp-3); + margin-top: var(--sp-8); } .btn { display: inline-flex; min-height: 48px; align-items: center; justify-content: center; - gap: 10px; + gap: var(--sp-2); border-radius: 999px; border: 1px solid var(--line-strong); - padding: 12px 22px; + padding: var(--sp-3) var(--sp-5); text-decoration: none; font-weight: 750; white-space: nowrap; @@ -349,18 +379,18 @@ color: var(--brand-2); } .command { - margin-top: 20px; + margin-top: var(--sp-5); display: flex; max-width: 100%; align-items: center; - gap: 12px; + gap: var(--sp-3); border: 1px solid var(--line); border-radius: var(--radius-sm); - padding: 12px 14px; + padding: var(--sp-3) var(--sp-4); background: var(--panel); box-shadow: var(--shadow); overflow-x: auto; - font: 700 13px var(--mono); + font: 700 var(--fs-n1) var(--mono); } .command .prompt { color: var(--brand); @@ -376,9 +406,9 @@ background: var(--panel-2); color: var(--muted); border-radius: var(--radius-sm); - padding: 6px 12px; + padding: var(--sp-2) var(--sp-3); cursor: pointer; - font: 700 12px var(--mono); + font: 700 var(--fs-n2) var(--mono); transition: color 0.16s var(--ease-out), border-color 0.16s var(--ease-out); @@ -390,7 +420,7 @@ /* ---- Hero diagram (the real, shipped SVG) ----------------------- */ .diagram { - margin: 56px 0 0; + margin: var(--sp-14) 0 0; } .diagram figure { margin: 0; @@ -421,8 +451,8 @@ } .diagram figcaption { color: var(--faint); - font: 500 13px var(--mono); - margin-top: 14px; + font: 500 var(--fs-n1) var(--mono); + margin-top: var(--sp-4); text-align: center; } @@ -431,14 +461,14 @@ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; - margin: 56px 0 0; + margin: var(--sp-14) 0 0; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--panel); } .stat { - padding: 26px 24px; + padding: var(--sp-7) var(--sp-6); border-right: 1px solid var(--line); } .stat:last-child { @@ -446,21 +476,21 @@ } .stat b { display: block; - font: 850 clamp(26px, 4vw, 38px) var(--mono); + font: 850 var(--fs-5) var(--mono); letter-spacing: -0.04em; color: var(--brand-2); } .stat span { display: block; - margin-top: 6px; + margin-top: var(--sp-2); color: var(--muted); - font-size: 14px; + font-size: var(--fs-n1); } .stat small { display: block; - margin-top: 8px; + margin-top: var(--sp-2); color: var(--faint); - font: 400 11px var(--mono); + font: 400 var(--fs-n2) var(--mono); } @media (max-width: 860px) { .stats { @@ -489,20 +519,20 @@ /* ---- Sections --------------------------------------------------- */ section { - padding: 84px 0; + padding: var(--sp-24) 0; border-top: 1px solid var(--line); } .kicker { - margin: 0 0 12px; + margin: 0 0 var(--sp-3); color: var(--brand); - font: 800 12px var(--mono); + font: 800 var(--fs-n2) var(--mono); letter-spacing: 0.14em; text-transform: uppercase; } h2 { margin: 0; max-width: 20ch; - font-size: clamp(30px, 5vw, 50px); + font-size: var(--fs-6); line-height: 1.05; letter-spacing: -0.04em; overflow-wrap: anywhere; @@ -510,8 +540,8 @@ } .sub { color: var(--muted); - margin: 16px 0 0; - font-size: 18px; + margin: var(--sp-4) 0 0; + font-size: var(--fs-1); max-width: 60ch; text-wrap: pretty; } @@ -519,17 +549,17 @@ /* Capability list — icon + copy rows divided by hairlines, NOT a grid of identical bordered icon-tiles (breaks the card-in-card / icon-grid tell). */ .caps { - margin: 48px 0 0; + margin: var(--sp-12) 0 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0 56px; + gap: 0 var(--sp-14); } .cap { display: grid; grid-template-columns: 40px 1fr; - gap: 18px; + gap: var(--sp-4); align-items: start; - padding: 26px 0; + padding: var(--sp-7) 0; border-top: 1px solid var(--line); } .cap-ico { @@ -542,14 +572,14 @@ height: 100%; } .cap h3 { - margin: 2px 0 8px; - font-size: 19px; + margin: var(--sp-1) 0 var(--sp-2); + font-size: var(--fs-2); letter-spacing: -0.01em; } .cap p { margin: 0; color: var(--muted); - font-size: 15px; + font-size: var(--fs-n1); } @media (max-width: 760px) { .caps { @@ -562,21 +592,21 @@ .split { display: grid; grid-template-columns: minmax(280px, 0.92fr) minmax(0, 1.08fr); - gap: 40px; + gap: var(--sp-10); align-items: start; } .steps { display: grid; - gap: 12px; - margin-top: 32px; + gap: var(--sp-3); + margin-top: var(--sp-8); counter-reset: step; } .step { display: grid; grid-template-columns: 34px 1fr; - gap: 16px; + gap: var(--sp-4); align-items: start; - padding: 18px 0; + padding: var(--sp-4) 0; border-top: 1px solid var(--line); } .step-num { @@ -589,21 +619,21 @@ background: var(--panel-2); border: 1px solid var(--line); color: var(--brand-2); - font: 800 14px var(--mono); + font: 800 var(--fs-n1) var(--mono); } .step-num::before { content: counter(step); } .step h3 { - margin: 4px 0 4px; - font-size: 17px; + margin: var(--sp-1) 0 var(--sp-1); + font-size: var(--fs-2); } .step p { margin: 0; color: var(--muted); } figure.listing { - margin: 32px 0 0; + margin: var(--sp-8) 0 0; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2); @@ -611,17 +641,17 @@ overflow: hidden; } figure.listing figcaption { - padding: 12px 20px; + padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--line); color: var(--faint); - font: 600 12px var(--mono); + font: 600 var(--fs-n2) var(--mono); letter-spacing: 0.04em; } pre { margin: 0; - padding: 20px; + padding: var(--sp-5); overflow-x: auto; - font: 13px/1.7 var(--mono); + font: var(--fs-n1) / 1.7 var(--mono); color: var(--text); } .c-dim { @@ -638,23 +668,23 @@ .honesty { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 40px; - margin-top: 44px; + gap: var(--sp-10); + margin-top: var(--sp-12); } .honest { display: grid; grid-template-columns: 40px 1fr; - gap: 18px; + gap: var(--sp-4); align-items: start; - padding-top: 26px; + padding-top: var(--sp-7); border-top: 2px solid var(--brand); } .honest .cap-ico { color: var(--brand); } .honest h3 { - margin: 2px 0 8px; - font-size: 19px; + margin: var(--sp-1) 0 var(--sp-2); + font-size: var(--fs-2); } .honest p { margin: 0; @@ -663,41 +693,41 @@ @media (max-width: 640px) { .honesty { grid-template-columns: 1fr; - gap: 8px; + gap: var(--sp-2); } } /* Data sources */ .sources { display: grid; - gap: 10px; + gap: var(--sp-3); padding: 0; - margin: 32px 0 0; + margin: var(--sp-8) 0 0; list-style: none; } .sources li { overflow-wrap: anywhere; border-left: 3px solid var(--brand); - padding: 8px 0 8px 14px; + padding: var(--sp-2) 0 var(--sp-2) var(--sp-4); color: var(--muted); - font: 500 14px var(--mono); + font: 500 var(--fs-n1) var(--mono); } footer { border-top: 1px solid var(--line); - padding: 34px 0; + padding: var(--sp-9) 0; color: var(--muted); } .foot { display: flex; justify-content: space-between; - gap: 18px; + gap: var(--sp-5); flex-wrap: wrap; } .foot a { color: var(--muted); text-decoration: none; - margin-left: 16px; + margin-left: var(--sp-4); white-space: nowrap; } .foot a:first-child { @@ -714,14 +744,14 @@ } @media (max-width: 560px) { .hero { - padding-top: 52px; + padding-top: var(--sp-14); } section { - padding: 60px 0; + padding: var(--sp-16) 0; } .foot a { margin-left: 0; - margin-right: 14px; + margin-right: var(--sp-4); } } diff --git a/scripts/build-pages.mjs b/scripts/build-pages.mjs index b66d351..6229dee 100644 --- a/scripts/build-pages.mjs +++ b/scripts/build-pages.mjs @@ -192,43 +192,43 @@ export function render(d) { ${rootTokensCss()} :root{--r-s:6px;--r-m:12px;--r-pill:999px} *{box-sizing:border-box} -body{margin:0;background:radial-gradient(960px 400px at 85% -10%,rgba(242,100,48,.16),transparent 70%),var(--bg);color:var(--text);font:16px/1.65 var(--sans);-webkit-font-smoothing:antialiased} +body{margin:0;background:radial-gradient(960px 400px at 85% -10%,rgba(242,100,48,.16),transparent 70%),var(--bg);color:var(--text);font:var(--fs-0)/1.65 var(--sans);-webkit-font-smoothing:antialiased} ::selection{background:var(--brand);color:var(--bg)} -p{margin:16px 0} +p{margin:var(--sp-4) 0} a{color:inherit} -a:focus-visible,button:focus-visible{outline:2px solid var(--brand);outline-offset:4px;border-radius:var(--r-s)} +a:focus-visible,button:focus-visible{outline:2px solid var(--brand);outline-offset:var(--sp-1);border-radius:var(--r-s)} .wrap{width:min(1080px,calc(100% - 48px));margin:0 auto} -.nav{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 0;border-bottom:1px solid var(--line)} -.brand{font:600 16px var(--mono);text-decoration:none} +.nav{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-4) 0;border-bottom:1px solid var(--line)} +.brand{font:600 var(--fs-0) var(--mono);text-decoration:none} .brand em{font-style:normal;color:var(--brand)} -.links{display:flex;gap:24px;flex-wrap:wrap;font-size:14px} +.links{display:flex;gap:var(--sp-6);flex-wrap:wrap;font-size:var(--fs-n1)} .links a{color:var(--muted);text-decoration:none} .links a:hover{color:var(--text)} -.hero{padding:64px 0 48px} -.eyebrow{font:500 13px var(--mono);color:var(--brand);letter-spacing:.12em;text-transform:uppercase;margin:0 0 16px} -h1{font-size:clamp(32px,5vw,56px);line-height:1.05;letter-spacing:-.02em;margin:0 0 16px;max-width:800px;font-weight:700} -.lead{font-size:18px;color:var(--muted);max-width:680px;margin:0 0 32px} -.btn{display:inline-block;text-decoration:none;font:600 15px var(--sans);border-radius:var(--r-pill);padding:12px 28px;border:1px solid var(--line)} +.hero{padding:var(--sp-16) 0 var(--sp-12)} +.eyebrow{font:500 var(--fs-n1) var(--mono);color:var(--brand);letter-spacing:.12em;text-transform:uppercase;margin:0 0 var(--sp-4)} +h1{font-size:var(--fs-6);line-height:1.05;letter-spacing:-.02em;margin:0 0 var(--sp-4);max-width:800px;font-weight:700} +.lead{font-size:var(--fs-1);color:var(--muted);max-width:680px;margin:0 0 var(--sp-8)} +.btn{display:inline-block;text-decoration:none;font:600 var(--fs-n1) var(--sans);border-radius:var(--r-pill);padding:var(--sp-3) var(--sp-7);border:1px solid var(--line)} .btn.primary{background:var(--brand);border-color:var(--brand);color:var(--bg)} .btn.primary:hover{background:var(--text);border-color:var(--text)} .btn:not(.primary):hover{border-color:var(--faint)} -.meta{display:flex;gap:12px;flex-wrap:wrap;margin-top:32px} -.chip{font:500 13px var(--mono);color:var(--muted);border:1px solid var(--line);border-radius:var(--r-pill);padding:8px 16px} -.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin:48px 0} -.cell{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-m);padding:32px 24px} -.metric{font:600 32px var(--mono);letter-spacing:-.02em} +.meta{display:flex;gap:var(--sp-3);flex-wrap:wrap;margin-top:var(--sp-8)} +.chip{font:500 var(--fs-n1) var(--mono);color:var(--muted);border:1px solid var(--line);border-radius:var(--r-pill);padding:var(--sp-2) var(--sp-4)} +.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-4);margin:var(--sp-12) 0} +.cell{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-m);padding:var(--sp-8) var(--sp-6)} +.metric{font:600 var(--fs-4) var(--mono);letter-spacing:-.02em} .metric em{font-style:normal;color:var(--brand)} -.cell strong{display:block;margin-top:8px;font-size:14px} -.muted{color:var(--muted);font-size:14px} -.src{font:400 12px var(--mono);color:var(--faint);margin-top:12px} -section{padding:48px 0;border-bottom:1px solid var(--line)} -h2{font-size:24px;letter-spacing:-.01em;margin:0 0 16px} -.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-m);padding:32px} -.terminal{font:400 14px/1.7 var(--mono);overflow-x:auto;white-space:pre;background:var(--panel-2);color:var(--text);border-radius:var(--r-m);padding:24px;border:1px solid var(--line);box-shadow:var(--shadow)} -.list{display:grid;gap:12px;padding:0;list-style:none;margin:0} -.list li{border-left:2px solid var(--brand);padding-left:16px;color:var(--muted);font-size:15px} -code{font:500 13px var(--mono);background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r-s);padding:0 8px} -footer{padding:32px 0;color:var(--faint);font-size:14px} +.cell strong{display:block;margin-top:var(--sp-2);font-size:var(--fs-n1)} +.muted{color:var(--muted);font-size:var(--fs-n1)} +.src{font:400 var(--fs-n2) var(--mono);color:var(--faint);margin-top:var(--sp-3)} +section{padding:var(--sp-12) 0;border-bottom:1px solid var(--line)} +h2{font-size:var(--fs-2);letter-spacing:-.01em;margin:0 0 var(--sp-4)} +.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-m);padding:var(--sp-8)} +.terminal{font:400 var(--fs-n1)/1.7 var(--mono);overflow-x:auto;white-space:pre;background:var(--panel-2);color:var(--text);border-radius:var(--r-m);padding:var(--sp-6);border:1px solid var(--line);box-shadow:var(--shadow)} +.list{display:grid;gap:var(--sp-3);padding:0;list-style:none;margin:0} +.list li{border-left:2px solid var(--brand);padding-left:var(--sp-4);color:var(--muted);font-size:var(--fs-n1)} +code{font:500 var(--fs-n1) var(--mono);background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r-s);padding:0 var(--sp-2)} +footer{padding:var(--sp-8) 0;color:var(--faint);font-size:var(--fs-n1)} @media(max-width:800px){.grid{grid-template-columns:1fr}}

${esc(d.name)} · v${esc(d.version)} · Node ${esc(d.node)}

Live status, straight from the repository.

${esc(d.description)}

Install in 60 seconds Read the docs

${esc(d.license)} license${esc(d.deps)} runtime dependencies${esc(d.branch)} @ ${esc(d.commit)}${live}
${esc(d.impact)}
blast-radius lookup

Measured from this repo's benchmark report, not a marketing placeholder.

reports/benchmarks.md

${esc(d.speed)}
pre-action gate

Assumptions, routing, reuse, context, impact, scope, and anchoring.

reports/benchmarks.md

${esc(d.saved.match(/^[\d.]+\s*%?/)?.[0] ?? d.saved)}
${esc(d.saved.replace(/^[\d.]+\s*%?\s*/, "") || "routing signal")}

Documented from the white-paper prototype and exposed by Forge cost reports.

whitepaper prototype

Quickstart

npm install -g @codewithjuber/forgekit forge init diff --git a/src/brand.js b/src/brand.js index a1a738d..a8eaa31 100644 --- a/src/brand.js +++ b/src/brand.js @@ -47,7 +47,69 @@ export function cssVars(scheme = "dark") { export function rootTokensCss() { const fonts = `--sans:${brand.fonts.sans};--mono:${brand.fonts.mono}`; return ( - `:root{color-scheme:dark;${cssVars("dark")};${fonts}}` + + `:root{color-scheme:dark;${cssVars("dark")};${fonts};${typeScaleCss()};${spaceScaleCss()}}` + `@media(prefers-color-scheme:light){:root{color-scheme:light;${cssVars("light")}}}` ); } + +// --------------------------------------------------------------------------- +// Fluid type scale + spacing scale — computed from a formula, not hand-picked +// magic numbers. Every font-size and every margin/padding/gap on both public +// pages is one of these tokens; change the constants below and the whole +// scale recomputes. test/pages.test.js enforces that both pages actually use +// the generated values (the same discipline already applied to colors above). +// --------------------------------------------------------------------------- + +// Fluid interpolation viewport bounds (px) — the width range the scale grows across. +const FLUID_MIN_VW = 400; +const FLUID_MAX_VW = 1280; + +// Type scale: two modular scales (a tighter ratio at the small viewport, a more +// dramatic one at the large viewport) blended with CSS clamp() so headings grow +// faster than body text as the viewport widens — the standard "fluid type scale" +// technique, generated rather than authored per element. +const TYPE_SCALE = { minBase: 16, maxBase: 18, minRatio: 1.125, maxRatio: 1.2 }; +/** Steps below/above the body size (0), named to match CSS var suffixes (n2 = -2). */ +export const TYPE_STEPS = [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7]; + +// Spacing scale: every value is `n * SPACE_BASE`, i.e. one base unit and an integer +// multiplier — the same 4px-grid convention used by most modern design systems, here +// generated from the base unit instead of listed as independent constants. +const SPACE_BASE = 4; +export const SPACE_STEPS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 20, 24]; + +function round2(n) { + return Math.round(n * 100) / 100; +} + +/** A fluid `clamp()` that grows linearly with viewport width between the two + * bounds above, landing exactly on `minPx` at FLUID_MIN_VW and `maxPx` at + * FLUID_MAX_VW. Bounds are sorted so clamp() stays valid even when a scale's + * min/max ratios invert at negative steps (tiny label text, effectively flat). */ +function fluidClamp(minPx, maxPx) { + const lo = Math.min(minPx, maxPx); + const hi = Math.max(minPx, maxPx); + const slope = (maxPx - minPx) / (FLUID_MAX_VW - FLUID_MIN_VW); + const intercept = minPx - slope * FLUID_MIN_VW; + const vw = round2(slope * 100); + const rem = round2(intercept / 16); + const sign = vw >= 0 ? "+" : "-"; + return `clamp(${round2(lo)}px,${rem}rem ${sign} ${Math.abs(vw)}vw,${round2(hi)}px)`; +} + +/** `--fs-N` custom properties (N in TYPE_STEPS, negative steps written `n2`, `n1`) — + * each a fluid clamp() derived from TYPE_SCALE. Step 0 is body text. */ +export function typeScaleCss() { + const { minBase, maxBase, minRatio, maxRatio } = TYPE_SCALE; + return TYPE_STEPS.map((s) => { + const min = minBase * minRatio ** s; + const max = maxBase * maxRatio ** s; + const name = s < 0 ? `n${-s}` : String(s); + return `--fs-${name}:${fluidClamp(min, max)}`; + }).join(";"); +} + +/** `--sp-N` custom properties (N in SPACE_STEPS) — each `N * SPACE_BASE`px. */ +export function spaceScaleCss() { + return SPACE_STEPS.map((n) => `--sp-${n}:${n * SPACE_BASE}px`).join(";"); +} diff --git a/test/pages.test.js b/test/pages.test.js index 0b3e303..55e3de8 100644 --- a/test/pages.test.js +++ b/test/pages.test.js @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { collect, render } from "../scripts/build-pages.mjs"; -import { BRAND } from "../src/brand.js"; +import { BRAND, spaceScaleCss, typeScaleCss } from "../src/brand.js"; const repo = (rel) => readFileSync(fileURLToPath(new URL(`../${rel}`, import.meta.url)), "utf8"); const landing = repo("landing/index.html"); @@ -43,6 +43,30 @@ test("landing + status derive the SAME palette from brand.json (one source, dark } }); +test("landing + status derive the SAME fluid type scale + spacing scale (one formula)", async () => { + // Same discipline as the color-parity test above, extended to typography and + // spacing: src/brand.js computes every --fs-N / --sp-N token from a formula + // (fluid clamp() interpolation for type, base-unit multiples for spacing), and + // both public pages must declare the exact same generated values — no page may + // hand-pick its own font-size or margin/padding/gap magic numbers. + // Whitespace is normalized before comparing: the status page emits compact CSS + // ("--fs-0:16px") while the hand-authored landing page spaces its :root block + // for readability ("--fs-0: 16px;") — same token, same value, different formatting. + const norm = (s) => s.replace(/\s+/g, ""); + const status = norm(render(await collect({ live: false }))); + const landingNorm = norm(landing); + for (const decl of typeScaleCss().split(";")) { + const d = norm(decl); + assert.ok(landingNorm.includes(d), `landing missing type token ${decl}`); + assert.ok(status.includes(d), `status missing type token ${decl}`); + } + for (const decl of spaceScaleCss().split(";")) { + const d = norm(decl); + assert.ok(landingNorm.includes(d), `landing missing space token ${decl}`); + assert.ok(status.includes(d), `status missing space token ${decl}`); + } +}); + test("landing declares no webfont it fails to load (no phantom Inter)", () => { const sans = landing.match(/--sans:\s*([^;]+);/)?.[1] ?? ""; assert.ok(sans.includes("system-ui"), "landing --sans should be a system stack"); From 6b9b5a70b7308b48ea1c644896ad65253f6602b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 00:24:18 +0000 Subject: [PATCH 2/4] ci(security): run gitleaks binary directly instead of the licensed action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dependabot #91 bumped gitleaks/gitleaks-action v2→v3, whose breaking update requires a paid GITLEAKS_LICENSE for org-associated accounts and HARD-FAILS when its license-validation server is unreachable ("License key validation will be enforced") — turning the secret-scan gate red on master and every PR for reasons unrelated to the code. Replace the action with the pinned gitleaks binary (8.21.2). `gitleaks detect` walks the full commit history (fetch-depth:0) exactly as before, with no network license dependency, so the gate stays deterministic and blocking. set -euo pipefail keeps it fail-closed. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/security.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9af0901..143ce0d 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -2,6 +2,13 @@ # Deliberately blocking — a real credential in the tree or history should fail the build, # not warn. (Rotate first, then rewrite/land; a red X is the cheap part of a leak.) # +# We run the gitleaks BINARY directly rather than gitleaks/gitleaks-action@v3: that action's +# breaking update requires a paid GITLEAKS_LICENSE for org-associated accounts and hard-fails +# the job when its license-validation server is unreachable ("License key validation will be +# enforced"), turning a green gate red for reasons unrelated to the code. The pinned binary +# performs the identical scan (`gitleaks detect` walks every commit) with no network license +# dependency, so the gate stays deterministic and blocking. +# # Why this passes on our own redaction tests: test/_fixtures.js assembles secret-LOOKING # strings at RUNTIME via .join() precisely so no secret-format literal exists in any blob # for a scanner to match. Verified clean with gitleaks 8.16 across the whole history and @@ -24,7 +31,13 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 # gitleaks scans commit history, not just the checkout - - uses: gitleaks/gitleaks-action@v3 + - name: Secret scan (gitleaks binary — no license, scans full history) env: - GITHUB_TOKEN: ${{ github.token }} # PR comments / job summary - # GITLEAKS_LICENSE is only required for ORG-owned repos; this repo is user-owned. + GITLEAKS_VERSION: "8.21.2" + run: | + set -euo pipefail + curl -sSfL \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /tmp gitleaks + # `detect` walks the whole git log (fetch-depth:0 above); non-zero exit on any find. + /tmp/gitleaks detect --source . --no-banner --redact --exit-code 1 From 74f7e0e98934e5a21241891cf84dc7dd7f08b31e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 00:30:25 +0000 Subject: [PATCH 3/4] ci(security): allowlist synthetic PEM fixtures in the redaction test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the gitleaks binary (prior commit) surfaced 2 findings the licensed action never reached — it was erroring on license validation before scanning, so the gate was effectively blind. Both are false positives: synthetic PEM private-key literals in test/secrets.test.js, the suite whose whole job is to assert forge redacts secret-shaped strings. They are committed, so a full-history scan always matches them. Add test/secrets.test.js to the .gitleaks.toml path allowlist (alongside the existing bibliography-key exceptions); every other path stays fully scanned. Verified locally: gitleaks 8.21.2 → 'no leaks found', exit 0. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/security.yml | 9 +++++---- .gitleaks.toml | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 143ce0d..6c08fc9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -9,10 +9,11 @@ # performs the identical scan (`gitleaks detect` walks every commit) with no network license # dependency, so the gate stays deterministic and blocking. # -# Why this passes on our own redaction tests: test/_fixtures.js assembles secret-LOOKING -# strings at RUNTIME via .join() precisely so no secret-format literal exists in any blob -# for a scanner to match. Verified clean with gitleaks 8.16 across the whole history and -# working tree before this workflow was added — no .gitleaks.toml allowlist is needed. +# Why this passes on our own redaction tests: most secret-LOOKING fixtures are assembled at +# RUNTIME (test/_fixtures.js .join()) so no secret-format literal exists in a blob to match. +# The few committed synthetic fixtures that a scanner can't tell from the real thing — the +# PEM/private-key literals in test/secrets.test.js — are allowlisted by path in .gitleaks.toml +# (which also carries the bibliography-key exceptions). Every other path is fully scanned. name: Security on: diff --git a/.gitleaks.toml b/.gitleaks.toml index ca1c7e7..56391d6 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -8,10 +8,14 @@ useDefault = true # The literal field name "key" next to a quoted alphanumeric value trips the default # generic-api-key rule even though there is no secret here — just citation metadata. [allowlist] -description = "Bibliography citation-key fields, not secrets" +description = "Bibliography citation-key fields and synthetic redaction-test fixtures, not secrets" paths = [ '''^research/formal-synthesis/graded_reference_set\.json$''', '''^research/formal-synthesis/merged_references\.json$''', + # test/secrets.test.js is the redaction test suite: it holds synthetic PEM private-key and + # high-entropy-token fixtures on purpose and asserts forge redacts them. Those literals are + # committed, so a full-history scan always matches them — they are NOT real credentials. + '''^test/secrets\.test\.js$''', ] # Obvious placeholder credentials used as test fixtures to exercise the secret # redactor/scanner — sequential-hex tokens that are unmistakably fake, not real secrets. From 26fad3f225b6a77538f5d90d10795c7c9284020f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 00:51:21 +0000 Subject: [PATCH 4/4] feat(pages): modernize landing visual design (2026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elevate the landing page's visual execution while keeping the information architecture, the token-driven system, and the dual-theme palette intact. - Two-voice typography: monospace becomes the display voice (headline, section titles, data readouts, structural tags) paired with humanist sans for prose — a deliberate fit for a config-compiler product rather than a generic SaaS sans. - Ember used as a measured "readout" accent: gauge-ticks on stats, reference marks on section kickers, a framed install prompt with an ember caret. - Signature detail: schematic corner-ticks on the hero diagram (instrument bezel). - Depth via theme-aware color-mix tints, bezel insets, and layered ember washes — no new hardcoded hexes; the brand.json palette stays the single source. - Refined spacing rhythm, hover micro-interactions, and one orchestrated hero load reveal, all gated behind prefers-reduced-motion. All new structural tokens are page-scoped (like the existing --radius/--ease-out); the --fs-*/--sp-* scale and brand.json color parity are untouched, so test/pages.test.js and the shared status page stay green. Co-Authored-By: Claude Opus 4.8 --- landing/index.html | 408 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 310 insertions(+), 98 deletions(-) diff --git a/landing/index.html b/landing/index.html index fb137db..be5d79b 100644 --- a/landing/index.html +++ b/landing/index.html @@ -133,6 +133,15 @@ --sp-16: 64px; --sp-20: 80px; --sp-24: 96px; + /* Page-scoped structural tokens — like --radius / --ease-out above, these + are landing-local (NOT part of the brand.json color-parity set). Shadows + and tints are derived from the theme tokens so they flip with the scheme. */ + --radius-lg: 26px; + --ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1); + --tint: color-mix(in srgb, var(--brand) 12%, transparent); + --tint-soft: color-mix(in srgb, var(--brand) 7%, transparent); + --hairline: color-mix(in srgb, var(--line-strong) 55%, transparent); + --bezel: inset 0 1px 0 color-mix(in srgb, var(--text) 7%, transparent); } @media (prefers-color-scheme: light) { :root { @@ -166,11 +175,14 @@ scroll-behavior: smooth; background: var(--bg); } + /* Motion is opt-out: every transition/animation collapses to ~0 so a + reduced-motion visitor sees the final, fully-legible layout instantly. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; + animation-delay: 0ms !important; scroll-behavior: auto !important; transition-duration: 0.001ms !important; } @@ -179,13 +191,21 @@ margin: 0; color: var(--text); font: var(--fs-0) / 1.65 var(--sans); + /* Two ember washes — a warm forge-glow top-right, a fainter counter-light + bottom-left — both derived from the accent tokens so they flip per theme. */ background: radial-gradient( - 960px 460px at 85% -10%, - rgba(242, 100, 48, 0.16), - transparent 70% + 1120px 560px at 90% -12%, + color-mix(in srgb, var(--brand) 15%, transparent), + transparent 66% + ), + radial-gradient( + 820px 520px at -6% 6%, + color-mix(in srgb, var(--brand-2) 8%, transparent), + transparent 60% ), var(--bg); + background-attachment: fixed; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } @@ -199,8 +219,12 @@ outline-offset: 3px; border-radius: var(--radius-sm); } + ::selection { + background: var(--brand); + color: var(--bg); + } .wrap { - width: min(var(--max), calc(100% - 40px)); + width: min(var(--max), calc(100% - 48px)); margin: 0 auto; } .mono { @@ -226,13 +250,14 @@ top: 0; z-index: 20; border-bottom: 1px solid var(--line); - background: color-mix(in srgb, var(--bg) 82%, transparent); + background: color-mix(in srgb, var(--bg) 78%, transparent); /* Gate: compositor-friendly blur — kept ≤8px so the sticky nav doesn't trigger a heavy repaint on every scroll frame. */ backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); } .nav-inner { - min-height: 68px; + min-height: 66px; display: flex; align-items: center; justify-content: space-between; @@ -243,19 +268,22 @@ gap: var(--sp-3); align-items: center; text-decoration: none; - font: 750 var(--fs-0) var(--mono); - letter-spacing: -0.02em; + font: 700 var(--fs-0) var(--mono); + letter-spacing: -0.03em; } .logo { - width: 32px; - height: 32px; - border-radius: var(--radius-sm); + width: 30px; + height: 30px; + border-radius: 9px; display: grid; place-items: center; - background: linear-gradient(135deg, var(--brand), var(--brand-2)); + background: linear-gradient(150deg, var(--brand), var(--brand-2)); color: var(--bg); font-weight: 800; - box-shadow: 0 12px 34px rgba(242, 100, 48, 0.26); + font-size: var(--fs-n1); + box-shadow: + 0 6px 18px color-mix(in srgb, var(--brand) 40%, transparent), + var(--bezel); } .brand em { color: var(--brand); @@ -265,7 +293,8 @@ display: flex; align-items: center; gap: var(--sp-6); - font-size: var(--fs-n1); + font: 500 var(--fs-n1) var(--mono); + letter-spacing: -0.01em; } .nav-links a { color: var(--muted); @@ -281,34 +310,40 @@ border-radius: 999px; padding: var(--sp-2) var(--sp-4); transition: - border-color 0.16s var(--ease-out), - background 0.16s var(--ease-out); + border-color 0.18s var(--ease-out), + background 0.18s var(--ease-out), + transform 0.18s var(--ease-spring); } .nav-links .pill:hover { border-color: var(--brand); - background: color-mix(in srgb, var(--brand) 12%, transparent); + background: var(--tint); + transform: translateY(-1px); } @media (max-width: 760px) { .nav-links a:not(.pill) { display: none; } .wrap { - width: min(var(--max), calc(100% - 28px)); + width: min(var(--max), calc(100% - 32px)); } } /* ---- Hero (Stat-Led) -------------------------------------------- */ .hero { - padding: var(--sp-24) 0 var(--sp-10); + padding: var(--sp-24) 0 var(--sp-12); } .eyebrow { display: inline-flex; gap: var(--sp-3); align-items: center; - margin: 0 0 var(--sp-5); + margin: 0 0 var(--sp-6); + padding: var(--sp-2) var(--sp-4) var(--sp-2) var(--sp-3); + border: 1px solid var(--line); + border-radius: 999px; + background: color-mix(in srgb, var(--panel) 60%, transparent); color: var(--brand-2); - font: 700 var(--fs-n2) var(--mono); - letter-spacing: 0.14em; + font: 600 var(--fs-n2) var(--mono); + letter-spacing: 0.12em; text-transform: uppercase; } /* Honest status dot — a plain marker, not a faux-live pulse. No glow ring, @@ -318,14 +353,16 @@ height: 7px; border-radius: 999px; background: var(--ok); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); } h1 { margin: 0; - max-width: 15ch; - font-size: var(--fs-7); - line-height: 1.02; - letter-spacing: -0.03em; - font-weight: 700; + max-width: 17ch; + /* The display voice is monospace — the product compiles config, so its + headline is set in the same family as the code it emits. Tight tracking + and leading keep the mono forms from reading as a terminal dump. */ + font: 700 var(--fs-7) / 1.02 var(--mono); + letter-spacing: -0.045em; overflow-wrap: anywhere; min-width: 0; } @@ -333,10 +370,11 @@ color: var(--brand); } .lede { - margin: var(--sp-6) 0 0; - max-width: 62ch; + margin: var(--sp-7) 0 0; + max-width: 60ch; color: var(--muted); font-size: var(--fs-1); + line-height: 1.6; text-wrap: pretty; } .actions { @@ -353,18 +391,23 @@ gap: var(--sp-2); border-radius: 999px; border: 1px solid var(--line-strong); - padding: var(--sp-3) var(--sp-5); + padding: var(--sp-3) var(--sp-6); text-decoration: none; - font-weight: 750; + font: 650 var(--fs-n1) var(--mono); + letter-spacing: -0.01em; white-space: nowrap; transition: - background 0.16s var(--ease-out), - border-color 0.16s var(--ease-out), - color 0.16s var(--ease-out), - transform 0.12s var(--ease-out); + background 0.18s var(--ease-out), + border-color 0.18s var(--ease-out), + color 0.18s var(--ease-out), + box-shadow 0.18s var(--ease-out), + transform 0.16s var(--ease-spring); + } + .btn:hover { + transform: translateY(-2px); } .btn:active { - transform: translateY(1px); + transform: translateY(0); } .btn-primary { background: var(--text); @@ -372,63 +415,110 @@ border-color: transparent; } .btn-primary:hover { - background: var(--brand-2); + background: var(--brand); + color: var(--bg); + box-shadow: 0 12px 30px + color-mix(in srgb, var(--brand) 34%, transparent); + } + .btn-secondary { + background: color-mix(in srgb, var(--panel) 55%, transparent); } .btn-secondary:hover { border-color: var(--brand); color: var(--brand-2); + background: var(--tint-soft); } + /* The install affordance — the page's one crafted "instrument". A framed + prompt with an ember caret, sized in the mono display voice. */ .command { - margin-top: var(--sp-5); + margin-top: var(--sp-6); display: flex; - max-width: 100%; + max-width: 640px; align-items: center; gap: var(--sp-3); - border: 1px solid var(--line); + border: 1px solid var(--line-strong); border-radius: var(--radius-sm); - padding: var(--sp-3) var(--sp-4); + padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4); background: var(--panel); - box-shadow: var(--shadow); + box-shadow: var(--shadow), var(--bezel); overflow-x: auto; - font: 700 var(--fs-n1) var(--mono); + font: 600 var(--fs-n1) var(--mono); + transition: border-color 0.18s var(--ease-out); + } + .command:hover { + border-color: color-mix(in srgb, var(--brand) 55%, var(--line-strong)); } .command .prompt { color: var(--brand); flex: none; + font-weight: 700; } .command code { flex: 1; white-space: nowrap; + color: var(--text); } .command button { flex: none; border: 1px solid var(--line-strong); background: var(--panel-2); color: var(--muted); - border-radius: var(--radius-sm); - padding: var(--sp-2) var(--sp-3); + border-radius: 8px; + padding: var(--sp-2) var(--sp-4); cursor: pointer; - font: 700 var(--fs-n2) var(--mono); + font: 650 var(--fs-n2) var(--mono); + letter-spacing: 0.02em; transition: color 0.16s var(--ease-out), - border-color 0.16s var(--ease-out); + border-color 0.16s var(--ease-out), + background 0.16s var(--ease-out); } .command button:hover { color: var(--text); border-color: var(--brand); + background: var(--tint); } /* ---- Hero diagram (the real, shipped SVG) ----------------------- */ .diagram { - margin: var(--sp-14) 0 0; + margin: var(--sp-16) 0 0; } .diagram figure { + position: relative; margin: 0; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--panel); - box-shadow: var(--shadow); + box-shadow: var(--shadow), var(--bezel); + } + /* Schematic corner ticks — the signature detail. Two opposed L-brackets read + as an instrument bezel / blueprint crop, quietly reinforcing "engineered, + measured infrastructure" without adding chrome. */ + .diagram figure::before, + .diagram figure::after { + content: ""; + position: absolute; + width: 14px; + height: 14px; + border: 1.5px solid var(--brand); + opacity: 0.55; + pointer-events: none; + z-index: 1; + } + .diagram figure::before { + top: 12px; + left: 12px; + border-right: 0; + border-bottom: 0; + border-top-left-radius: 4px; + } + .diagram figure::after { + bottom: 12px; + right: 12px; + border-left: 0; + border-top: 0; + border-bottom-right-radius: 4px; } /* The real shipped hero art, inlined so it resolves in local preview AND on the deployed site root (the deploy copies only index.html, not docs/assets). @@ -452,8 +542,11 @@ .diagram figcaption { color: var(--faint); font: 500 var(--fs-n1) var(--mono); - margin-top: var(--sp-4); + line-height: 1.6; + margin-top: var(--sp-5); text-align: center; + max-width: 68ch; + margin-inline: auto; } /* ---- Stat band -------------------------------------------------- */ @@ -461,28 +554,41 @@ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; - margin: var(--sp-14) 0 0; + margin: var(--sp-16) 0 0; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--panel); + box-shadow: var(--bezel); } .stat { - padding: var(--sp-7) var(--sp-6); + position: relative; + padding: var(--sp-8) var(--sp-6) var(--sp-7); border-right: 1px solid var(--line); } + /* Each readout wears a short ember gauge-tick above the figure. */ + .stat::before { + content: ""; + position: absolute; + top: 0; + left: var(--sp-6); + width: 26px; + height: 2px; + background: var(--brand); + } .stat:last-child { border-right: 0; } .stat b { display: block; - font: 850 var(--fs-5) var(--mono); + font: 700 var(--fs-5) var(--mono); letter-spacing: -0.04em; - color: var(--brand-2); + font-variant-numeric: tabular-nums; + color: var(--text); } .stat span { display: block; - margin-top: var(--sp-2); + margin-top: var(--sp-3); color: var(--muted); font-size: var(--fs-n1); } @@ -522,27 +628,38 @@ padding: var(--sp-24) 0; border-top: 1px solid var(--line); } + /* Section reference tag — a schematic label, not decoration: an ember index + mark keys the eye to each stage of the story. */ .kicker { - margin: 0 0 var(--sp-3); + display: inline-flex; + align-items: center; + gap: var(--sp-3); + margin: 0 0 var(--sp-5); color: var(--brand); - font: 800 var(--fs-n2) var(--mono); - letter-spacing: 0.14em; + font: 700 var(--fs-n2) var(--mono); + letter-spacing: 0.2em; text-transform: uppercase; } + .kicker::before { + content: ""; + width: 22px; + height: 2px; + background: var(--brand); + } h2 { margin: 0; - max-width: 20ch; - font-size: var(--fs-6); - line-height: 1.05; - letter-spacing: -0.04em; + max-width: 22ch; + font: 700 var(--fs-5) / 1.08 var(--mono); + letter-spacing: -0.035em; overflow-wrap: anywhere; min-width: 0; } .sub { color: var(--muted); - margin: var(--sp-4) 0 0; + margin: var(--sp-5) 0 0; font-size: var(--fs-1); - max-width: 60ch; + line-height: 1.6; + max-width: 62ch; text-wrap: pretty; } @@ -552,34 +669,62 @@ margin: var(--sp-12) 0 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0 var(--sp-14); + gap: 0 var(--sp-16); } .cap { + position: relative; display: grid; - grid-template-columns: 40px 1fr; + grid-template-columns: 42px 1fr; gap: var(--sp-4); align-items: start; padding: var(--sp-7) 0; border-top: 1px solid var(--line); } + /* Hairline lights up on hover — a low-key affordance that keeps the eye moving. */ + .cap::before { + content: ""; + position: absolute; + top: -1px; + left: 0; + width: 0; + height: 2px; + background: var(--brand); + transition: width 0.32s var(--ease-out); + } + .cap:hover::before { + width: 42px; + } .cap-ico { - width: 40px; - height: 40px; + width: 42px; + height: 42px; + display: grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 11px; + background: var(--tint-soft); color: var(--brand-2); + transition: + border-color 0.2s var(--ease-out), + background 0.2s var(--ease-out); + } + .cap:hover .cap-ico { + border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); + background: var(--tint); } .cap-ico svg { - width: 100%; - height: 100%; + width: 22px; + height: 22px; } .cap h3 { margin: var(--sp-1) 0 var(--sp-2); - font-size: var(--fs-2); + font: 600 var(--fs-2) var(--sans); letter-spacing: -0.01em; } .cap p { margin: 0; color: var(--muted); font-size: var(--fs-n1); + line-height: 1.6; } @media (max-width: 760px) { .caps { @@ -591,8 +736,8 @@ terminal window: no traffic-light dots, no mock title bar). */ .split { display: grid; - grid-template-columns: minmax(280px, 0.92fr) minmax(0, 1.08fr); - gap: var(--sp-10); + grid-template-columns: minmax(280px, 0.9fr) minmax(0, 1.1fr); + gap: var(--sp-12); align-items: start; } .steps { @@ -603,55 +748,71 @@ } .step { display: grid; - grid-template-columns: 34px 1fr; - gap: var(--sp-4); + grid-template-columns: 36px 1fr; + gap: var(--sp-5); align-items: start; - padding: var(--sp-4) 0; + padding: var(--sp-5) 0; border-top: 1px solid var(--line); } + /* A real, ordered sequence — so the numeric markers carry information. */ .step-num { counter-increment: step; - width: 34px; - height: 34px; - border-radius: var(--radius-sm); + width: 36px; + height: 36px; + border-radius: 10px; display: grid; place-items: center; - background: var(--panel-2); - border: 1px solid var(--line); + background: var(--tint); + border: 1px solid color-mix(in srgb, var(--brand) 32%, var(--line)); color: var(--brand-2); - font: 800 var(--fs-n1) var(--mono); + font: 700 var(--fs-n1) var(--mono); + font-variant-numeric: tabular-nums; } .step-num::before { - content: counter(step); + content: counter(step, decimal-leading-zero); } .step h3 { - margin: var(--sp-1) 0 var(--sp-1); - font-size: var(--fs-2); + margin: var(--sp-1) 0 var(--sp-2); + font: 600 var(--fs-2) var(--sans); + letter-spacing: -0.01em; } .step p { margin: 0; color: var(--muted); + line-height: 1.6; } figure.listing { - margin: var(--sp-8) 0 0; + margin: 0; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2); - box-shadow: var(--shadow); + box-shadow: var(--shadow), var(--bezel); overflow: hidden; } figure.listing figcaption { + display: flex; + align-items: center; + gap: var(--sp-3); padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--line); color: var(--faint); font: 600 var(--fs-n2) var(--mono); - letter-spacing: 0.04em; + letter-spacing: 0.06em; + text-transform: uppercase; + } + figure.listing figcaption::before { + content: ""; + width: 7px; + height: 7px; + border-radius: 999px; + background: var(--brand); + flex: none; } pre { margin: 0; - padding: var(--sp-5); + padding: var(--sp-6) var(--sp-5); overflow-x: auto; - font: var(--fs-n1) / 1.7 var(--mono); + font: var(--fs-n1) / 1.75 var(--mono); color: var(--text); } .c-dim { @@ -673,7 +834,7 @@ } .honest { display: grid; - grid-template-columns: 40px 1fr; + grid-template-columns: 42px 1fr; gap: var(--sp-4); align-items: start; padding-top: var(--sp-7); @@ -681,19 +842,22 @@ } .honest .cap-ico { color: var(--brand); + border-color: color-mix(in srgb, var(--brand) 30%, var(--line)); } .honest h3 { margin: var(--sp-1) 0 var(--sp-2); - font-size: var(--fs-2); + font: 600 var(--fs-2) var(--sans); + letter-spacing: -0.01em; } .honest p { margin: 0; color: var(--muted); + line-height: 1.6; } @media (max-width: 640px) { .honesty { grid-template-columns: 1fr; - gap: var(--sp-2); + gap: var(--sp-6); } } @@ -715,7 +879,7 @@ footer { border-top: 1px solid var(--line); - padding: var(--sp-9) 0; + padding: var(--sp-10) 0; color: var(--muted); } .foot { @@ -724,17 +888,23 @@ gap: var(--sp-5); flex-wrap: wrap; } + .foot .mono { + font-size: var(--fs-n1); + color: var(--faint); + } .foot a { color: var(--muted); text-decoration: none; - margin-left: var(--sp-4); + margin-left: var(--sp-5); white-space: nowrap; + font: 500 var(--fs-n1) var(--mono); + transition: color 0.16s var(--ease-out); } .foot a:first-child { margin-left: 0; } .foot a:hover { - color: var(--text); + color: var(--brand-2); } @media (max-width: 900px) { @@ -744,14 +914,56 @@ } @media (max-width: 560px) { .hero { - padding-top: var(--sp-14); + padding-top: var(--sp-16); } section { padding: var(--sp-16) 0; } .foot a { margin-left: 0; - margin-right: var(--sp-4); + margin-right: var(--sp-5); + } + } + + /* Orchestrated hero entrance — one calm, staggered rise on load. Gated behind + no-preference so reduced-motion visitors never see a hidden/animated state. */ + @media (prefers-reduced-motion: no-preference) { + .hero > .wrap > .eyebrow, + .hero > .wrap > h1, + .hero > .wrap > .lede, + .hero > .wrap > .actions, + .hero > .wrap > .command, + .hero > .wrap > .diagram, + .hero > .wrap > .stats { + animation: rise 0.72s var(--ease-out) both; + } + .hero > .wrap > h1 { + animation-delay: 0.06s; + } + .hero > .wrap > .lede { + animation-delay: 0.14s; + } + .hero > .wrap > .actions { + animation-delay: 0.2s; + } + .hero > .wrap > .command { + animation-delay: 0.26s; + } + .hero > .wrap > .diagram { + animation-delay: 0.34s; + } + .hero > .wrap > .stats { + animation-delay: 0.4s; + } + @keyframes rise { + from { + opacity: 0; + transform: translateY(14px); + } + to { + opacity: 1; + transform: translateY(0); + } } }