more changes

- Restore the SHARP-ER in the homepage hero section
- Match the design for the 3-image section in the homepage
This commit is contained in:
2026-09-23 23:36:36 +02:00
parent cc2b426119
commit 2ff5cf47cf
6 changed files with 130 additions and 28 deletions
@@ -42,11 +42,17 @@ export interface BrandProps {
/** Accessible name of the link; the images are decorative. */
label: string;
surface: NavSurface;
/**
* Run the "-ER" reveal and its breathing loop. Off on the home page, whose
* hero plays the same reveal once - a second, looping copy beside it is
* jarring. Anywhere else the two animations run as designed.
*/
animate?: boolean;
className?: string;
}
/** The SHARP-ER wordmark, linking home. Sits at the inline start of the bar. */
export function Brand({ href, label, surface, className }: BrandProps) {
export function Brand({ href, label, surface, animate = true, className }: BrandProps) {
const { base, suffix } = BRAND_LOCKUP[surface];
return (
@@ -75,10 +81,12 @@ export function Brand({ href, label, surface, className }: BrandProps) {
/>
{/* The clipping box starts at the P's right edge, so the mark inside
it can slide out from behind the P (Navigation.css). */}
it can slide out from behind the P (Navigation.css). Without
`animate` the box and the reveal classes are dropped and the full
lockup simply sits there. */}
<span
style={{ flex: `${suffix.drawnWidth} 1 0%` }}
className="sh-brand-suffix block min-w-0"
className={cx("block min-w-0", animate && "sh-brand-suffix")}
>
<img
src={suffix.src}
@@ -88,7 +96,7 @@ export function Brand({ href, label, surface, className }: BrandProps) {
loading="eager"
decoding="async"
draggable={false}
className="sh-brand-suffix__mark block h-auto w-full"
className={cx("block h-auto w-full", animate && "sh-brand-suffix__mark")}
/>
</span>
</span>
@@ -268,7 +268,12 @@ export function MobileNav({
{/* Mirrors the compact bar it replaces (20:220): the same 64px row and
the same gutter, so the wordmark does not shift when it opens. */}
<div className="flex h-[64px] shrink-0 items-center justify-between gap-md px-[var(--layout-gutter)]">
<Brand href={homeHref} label={strings.homeLinkLabel} surface={surface} />
<Brand
href={homeHref}
label={strings.homeLinkLabel}
surface={surface}
animate={!isCurrentPath(homeHref, pathname)}
/>
<button
ref={closeRef}
type="button"
@@ -34,7 +34,10 @@
After that one-time reveal the mark keeps breathing: after a long rest
(~26s of stillness per cycle) it slides back out from behind the P and
returns, so the wordmark quietly draws the eye without the glyphs moving
while they are visible. The two
while they are visible. On the home page none of this runs (Brand.tsx
drops the classes when the lockup links the page you are on): the hero
there plays the same reveal once, and a second, looping copy right above
it would be jarring. The two
animations are sequenced by delay - the reveal (once, `both` fill, so the
suffix stays hidden through its delay instead of flashing in) hands over
to the loop, whose later position in the list wins from its start, and
@@ -3,6 +3,7 @@ import { useMemo } from "react";
import {
buildLocalizedHref,
getDirection,
isCurrentPath,
LanguageToggle,
type Locale,
} from "../LanguageSwitcher";
@@ -114,6 +115,9 @@ export function Navigation({
href={resolvedHomeHref}
label={strings.homeLinkLabel}
surface={surface}
/* The home page's own hero plays the reveal once; the brand beside
it stays still there instead of running a second copy. */
animate={!isCurrentPath(resolvedHomeHref, pathname)}
/>
<DesktopNav
+7 -15
View File
@@ -203,22 +203,14 @@
}
}
/* A picture pushing in a little while the frame around it is hovered. The
frame clips, so the picture grows into its own box and not over the page.
Paired with .sh-lift the tile rises and its picture closes in, which is
how the draft hovers the gallery. */
.sh-zoom {
overflow: hidden;
}
.sh-zoom img {
transition: transform var(--motion-duration-slow)
var(--motion-ease-standard);
}
/* A gallery tile that grows as a WHOLE frame instead of pushing the picture
inside it - the draft hovers the frame, not the image. It keeps `.sh-lift`'s
rise and adds a uniform scale to it. Written after `.sh-lift:hover` so the
compound transform wins at equal specificity; `.sh-lift`'s transition
already covers `transform`, so the two move together. */
@media (hover: hover) {
.sh-zoom:hover img {
transform: scale(1.09);
.sh-lift--grow:hover {
transform: translateY(-5px) scale(1.04);
}
}
+96 -6
View File
@@ -89,10 +89,47 @@ import {
PageHero,
PageSection,
SectionHeader,
type PageHeroWordmark,
} from "./_page-kit/sections";
import "./_page-kit/motion.css";
/**
* The hero lockup above the headline: the full SHARP-ER pair at the SVGs' own
* box (218 + 112 wide), picked per surface the way the navigation's brand
* picks it (Brand.tsx) - white "inverse" exports on the dark hero, the ink
* pair on a light one. `alt` is empty: the sticky bar's brand link already
* carries the accessible project name.
*/
const HERO_WORDMARK: Record<AppTheme, PageHeroWordmark> = {
dark: {
src: "/assets/sharp-wordmark-inverse.svg",
alt: "",
width: 218,
height: 46,
drawnWidth: 218,
suffix: {
src: "/assets/sharp-er-suffix-inverse.svg",
width: 112,
height: 46,
drawnWidth: 112,
},
},
light: {
src: "/assets/sharp-wordmark.svg",
alt: "",
width: 218,
height: 46,
drawnWidth: 218,
suffix: {
src: "/assets/sharp-er-suffix.svg",
width: 112,
height: 46,
drawnWidth: 112,
},
},
};
/** The network logo wall - a wide but finite grid. */
const PARTNER_PAGE_SIZE = 36;
/**
@@ -312,24 +349,70 @@ function StatusPill({ label }: { label: string }) {
}
/**
* The approach band's three pictures. The CMS alt text doubles as the visible
* The approach band's pictures. The CMS alt text doubles as the visible
* caption - so the picture is described once, in the caption, and the <img>
* itself is marked decorative rather than having a screen reader read the same
* sentence twice.
*
* The frame and the picture hover apart, as the third draft has them: the
* frame lifts and takes the accent hairline (`sh-lift`) while the picture
* closes in inside it (`sh-zoom`, which also does the clipping).
* The frame and the picture move as one: the frame lifts and takes the accent
* hairline (`sh-lift`) and the whole tile grows under the pointer
* (`sh-lift--grow`) - the draft grows the frame, it does not push the picture
* inside it.
*
* THREE PICTURES RENDER AS THE DRAFT'S MOSAIC (figma 559:4089): the middle
* tile dominates - 605px wide, full height, full opacity - and the two flanks
* sit shorter beside it (427/426px wide, 0.72 of its height) at 50% opacity,
* vertically centred on the middle. One or two pictures fall back to the
* uniform grid, where the mosaic's "faded flank" reading has no partner.
*/
function GalleryShots({ images }: { images: readonly ResolvedImage[] }) {
if (images.length === 0) return null;
const mosaic = images.length === 3;
if (mosaic) {
return (
/* The track widths are the draft's own drawing (605 centre, ~427/426
flanks); the aspect ratios keep each tile at its drawn proportions, so
the row only reads as a mosaic from lg up and stacks full-width, full
opacity below it. `lg:items-center` sits the shorter flanks level with
the middle one, as the draft has them. gap-3xl is the closest step to
the draft's 60px. */
<ul className="grid list-none grid-cols-1 gap-3xl lg:grid-cols-[427fr_605fr_426fr] lg:items-center">
{images.map((image, index) => (
<li key={image.src} className="sh-reveal">
<figure className="flex flex-col gap-xs">
<div
className={cx(
"sh-lift sh-lift--grow rounded-control border border-[var(--border-subtle)] hover:border-[var(--border-accent)]",
index === 1 ? "aspect-[605/310]" : "aspect-[427/228] lg:opacity-50",
)}
>
<img
src={image.src}
alt=""
loading="lazy"
className="h-full w-full object-cover"
/>
</div>
{image.alt ? (
<figcaption className="body-sm-bold text-[var(--text-secondary)]">
{image.alt}
</figcaption>
) : null}
</figure>
</li>
))}
</ul>
);
}
return (
<ul className="grid list-none grid-cols-1 gap-lg sm:grid-cols-2 lg:grid-cols-3">
{images.map((image) => (
<li key={image.src} className="sh-reveal">
<figure className="flex flex-col gap-xs">
<div className="sh-lift sh-zoom rounded-control border border-[var(--border-subtle)] hover:border-[var(--border-accent)]">
<div className="sh-lift sh-lift--grow rounded-control border border-[var(--border-subtle)] hover:border-[var(--border-accent)]">
<img
src={image.src}
alt=""
@@ -498,6 +581,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
},
]}
reveal
wordmark={HERO_WORDMARK[theme]}
/* EU visibility sits at the top of the hero (the draft's badge). The
emblem matches the page surface the hero stands on, which the theme
cookie decides - dark emblem in dark mode, light emblem in light
@@ -521,11 +605,17 @@ export default function Home({ loaderData }: Route.ComponentProps) {
/>
{/* WHAT THE PROJECT IS, AND WHO DELIVERS IT */}
{/* `bleed` drops the 80rem Container so the band's content - the three
pictures especially - runs the full width of the viewport, the way
the draft draws the mosaic. The gutter the Container used to add is
re-applied here, and the header column still constrains itself to its
own measure, so only the gallery actually strays to the edges. */}
<PageSection
aria-labelledby="home-approach-heading"
band
bleed
>
<div className="flex flex-col gap-3xl">
<div className="flex w-full flex-col gap-3xl px-[var(--layout-gutter)] lg:px-[var(--layout-gutter-lg)]">
<div className="sh-reveal mx-auto flex max-w-[55rem] flex-col items-center gap-lg text-center">
<SectionHeader
id="home-approach-heading"