diff --git a/frontend/apps/artcraft-website-next/src/app/layout.tsx b/frontend/apps/artcraft-website-next/src/app/layout.tsx index fecfc1e5b0..26b4a23f2b 100644 --- a/frontend/apps/artcraft-website-next/src/app/layout.tsx +++ b/frontend/apps/artcraft-website-next/src/app/layout.tsx @@ -1,7 +1,6 @@ import type { Metadata, Viewport } from "next"; import { Archivo, - Archivo_Black, Instrument_Serif, Inter, Geist_Mono, @@ -15,8 +14,12 @@ import "./globals.css"; const SITE_URL = "https://getartcraft.com"; -// Display face: Archivo with its width axis loaded, so headings can run -// slightly expanded (font-stretch) for the industrial-grotesque look. +// Display face: variable Archivo with its width axis loaded — headings run +// slightly expanded (font-stretch) for the industrial-grotesque look, and +// the hero wordmark renders the same family at its poster extreme +// (wght 900 / wdth 125%, the Archivo Black look) so the ruler's hero flip +// can interpolate weight and width down to the heading setting. A separate +// Archivo Black cut could never interpolate. const archivo = Archivo({ subsets: ["latin"], variable: "--font-archivo", @@ -24,15 +27,6 @@ const archivo = Archivo({ axes: ["wdth"], }); -// Wordmark face: Archivo Black, the display family's poster-weight cut -// (a single-weight family of its own, not Archivo at 900). -const archivoBlack = Archivo_Black({ - subsets: ["latin"], - weight: "400", - variable: "--font-archivo-black", - display: "swap", -}); - const instrumentSerif = Instrument_Serif({ subsets: ["latin"], weight: "400", @@ -110,7 +104,7 @@ export default function RootLayout({
diff --git a/frontend/apps/artcraft-website-next/src/components/landing/hero-wordmark.tsx b/frontend/apps/artcraft-website-next/src/components/landing/hero-wordmark.tsx index 5a6aeea92f..de7aa034f2 100644 --- a/frontend/apps/artcraft-website-next/src/components/landing/hero-wordmark.tsx +++ b/frontend/apps/artcraft-website-next/src/components/landing/hero-wordmark.tsx @@ -4,8 +4,13 @@ import { useEffect, useRef, useState } from "react"; import { heroWordmark } from "@/components/ruler/ruler-shared"; const WORDMARK_TEXT = "ARTCRAFT"; -const WORDMARK_FONT = - "var(--font-archivo-black), var(--font-archivo), system-ui, sans-serif"; +// Variable Archivo at its poster extreme — the Archivo Black look, but on +// the same variable family as every heading, so HeadingFlow can +// interpolate weight/width down to the display setting (620 / 118%) +// during the hero flip. +const WORDMARK_FONT = "var(--font-archivo), system-ui, sans-serif"; +const WORDMARK_WEIGHT = 900; +const WORDMARK_STRETCH = "125%"; // The poster masthead: the wordmark justified flush across the hero rails in // the site's display type — per-letter spans instead of one text node, @@ -37,6 +42,8 @@ export default function HeroMasthead() { probe.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;white-space:pre;font-size:100px;line-height:1;"; probe.style.fontFamily = WORDMARK_FONT; + probe.style.fontWeight = String(WORDMARK_WEIGHT); + probe.style.fontStretch = WORDMARK_STRETCH; probe.textContent = WORDMARK_TEXT; document.body.appendChild(probe); const w100 = probe.getBoundingClientRect().width; @@ -67,7 +74,13 @@ export default function HeroMasthead() { if (els.length !== WORDMARK_TEXT.length) return; const measure = () => { - for (const el of els) el.style.transform = ""; + for (const el of els) { + // Reset transforms AND the variable-font morph to the resting cut + // before measuring, so a mid-flip resize re-baselines cleanly. + el.style.transform = ""; + el.style.fontWeight = ""; + el.style.fontStretch = ""; + } const rects = els.map((el) => el.getBoundingClientRect()); heroWordmark.els = els; heroWordmark.baseX = rects.map((r) => r.left + r.width / 2); @@ -119,6 +132,8 @@ export default function HeroMasthead() { className="relative whitespace-pre text-ink-strong" style={{ fontFamily: WORDMARK_FONT, + fontWeight: WORDMARK_WEIGHT, + fontStretch: WORDMARK_STRETCH, fontSize: fontPx || "13vw", lineHeight: 1, }} diff --git a/frontend/apps/artcraft-website-next/src/components/ruler/heading-flow.tsx b/frontend/apps/artcraft-website-next/src/components/ruler/heading-flow.tsx index 2a8a2bba97..99cca4d869 100644 --- a/frontend/apps/artcraft-website-next/src/components/ruler/heading-flow.tsx +++ b/frontend/apps/artcraft-website-next/src/components/ruler/heading-flow.tsx @@ -48,9 +48,10 @@ import { // the top current-heading slot as the wordmark is about to duck under the // nav (title-condenses-into-header), no queue home and no rail ride. From // the stack onward it behaves like any section (demotes when FEATURES -// flips in). The hero letters keep their Archivo Black wordmark face -// through the whole lifecycle — brand identity, and font families can't -// interpolate. +// flips in). The hero letters render variable Archivo at the poster +// extreme (wght 900 / wdth 125% — the Archivo Black look) and morph to +// the headings' display setting (620 / 118%) during the flip: one +// variable family, so weight and width genuinely interpolate. // A letter's pose on screen. x/y are the letter center in viewport px. type Pose = { @@ -442,6 +443,12 @@ export default function HeadingFlow({ }px, ${ y - (heroWordmark.baseDocY[i] - scrollY) }px, 0) rotate(${rot}deg) scale(${scale * scaleFix})`; + // Variable-font morph: the wordmark's poster cut (wght 900, + // wdth 125%) eases into the headings' display setting + // (620, 118%) letter by letter with the flight — the weight + // difference lands unnoticed inside the motion. + el.style.fontWeight = String(Math.round(900 - 280 * e)); + el.style.fontStretch = `${(125 - 7 * e).toFixed(1)}%`; } else { el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) rotate(${rot}deg) scale(${scale})`; }