2026-09-16 20:32:56 +02:00
|
|
|
/* ============================================================================
|
|
|
|
|
NAVIGATION - the dropdown's opening motion
|
|
|
|
|
----------------------------------------------------------------------------
|
|
|
|
|
The redesign opens each group's menu with a short fade and an 8px settle
|
|
|
|
|
("menuIn" in the draft). It lives here rather than in the page kit because
|
|
|
|
|
the navigation is a design-system component and must not depend on a route
|
|
|
|
|
module's stylesheet.
|
|
|
|
|
|
|
|
|
|
`:not([hidden])` is what replays it: the panel is hidden with the `hidden`
|
|
|
|
|
attribute, so the animation runs each time the group opens instead of once
|
|
|
|
|
per mount. app.css collapses it to 0.01ms under prefers-reduced-motion.
|
|
|
|
|
========================================================================= */
|
|
|
|
|
|
|
|
|
|
@layer theme, base, components, utilities;
|
|
|
|
|
|
|
|
|
|
@layer components {
|
|
|
|
|
.sh-nav-menu:not([hidden]) {
|
|
|
|
|
animation: sh-nav-menu-in var(--motion-duration-base)
|
|
|
|
|
var(--motion-ease-entrance) both;
|
|
|
|
|
}
|
2026-09-23 22:47:04 +02:00
|
|
|
|
|
|
|
|
/* The "-ER" after "SHARP" in the brand, sliding out from behind the P and
|
|
|
|
|
fading in once the page has settled. The same reveal the home hero used to
|
|
|
|
|
run on its wordmark, now owned by the navigation. The suffix sits in its
|
|
|
|
|
own box whose inline-start edge is the P's right edge, and that box clips;
|
|
|
|
|
the mark starts a full box-width back - entirely beside "SHARP", so
|
|
|
|
|
nothing overlaps it - and travels forward to rest, with the opacity
|
|
|
|
|
reaching 1 before the travel ends, so the glyphs read as solid for the
|
|
|
|
|
last, slowest part of the decelerating slide. The translate is physical
|
|
|
|
|
(translateX) on purpose: the lockup row is pinned to dir="ltr" because a
|
|
|
|
|
Latin wordmark never mirrors, so the P is always left of the suffix, in
|
2026-09-23 23:22:57 +02:00
|
|
|
Arabic too.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
whose own delay is when the revealed lockup is simply at rest. Pure CSS,
|
|
|
|
|
so it runs from the server-rendered HTML without waiting for hydration.
|
|
|
|
|
Under prefers-reduced-motion both animations are removed outright -
|
|
|
|
|
app.css only collapses the duration and would leave the delay holding the
|
|
|
|
|
suffix hidden - so the full name is simply there from the start. */
|
2026-09-23 22:47:04 +02:00
|
|
|
.sh-brand-suffix {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sh-brand-suffix__mark {
|
2026-09-23 23:22:57 +02:00
|
|
|
animation:
|
|
|
|
|
sh-brand-suffix-in var(--motion-duration-reveal)
|
|
|
|
|
var(--motion-ease-entrance) var(--motion-delay-reveal) both,
|
|
|
|
|
sh-brand-breathe calc(var(--motion-duration-reveal) * 37.5)
|
|
|
|
|
ease-in-out calc(var(--motion-duration-reveal) * 5) infinite;
|
2026-09-23 22:47:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
.sh-brand-suffix__mark {
|
|
|
|
|
animation: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes sh-brand-suffix-in {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
60% {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
2026-09-16 20:32:56 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-23 23:22:57 +02:00
|
|
|
/* One 30s breath: dwell at rest (~26s - the stillness is what stretches, the
|
|
|
|
|
gestures stay at a human pace), slide back out behind the P (the reverse of
|
|
|
|
|
the reveal - solid while it pulls away, fading as it leaves), pass on
|
|
|
|
|
hidden, then play the reveal again, and hold. Looped, with the cycle ending
|
|
|
|
|
where it starts so the seam is invisible. Each slide is ONE eased interval
|
|
|
|
|
of roughly one reveal duration - the opacity stop inside the out-slide is
|
|
|
|
|
opacity-only, so it never splits the transform's easing and the glyphs
|
|
|
|
|
cannot stall or hitch mid-gesture. */
|
|
|
|
|
@keyframes sh-brand-breathe {
|
|
|
|
|
/* settled */
|
|
|
|
|
0%, 87% {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
animation-timing-function: var(--motion-ease-standard);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* pull back solid - opacity only, so the 87→90 transform still runs one
|
|
|
|
|
eased interval and the slide never hitches */
|
|
|
|
|
88.5% {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* gone, behind the P */
|
|
|
|
|
90% {
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* pass, hidden, then the reveal again - one eased slide back in */
|
|
|
|
|
94% {
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
animation-timing-function: var(--motion-ease-entrance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
97% {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* settled until the cycle hands over (0% is settled too) */
|
|
|
|
|
100% {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
@keyframes sh-nav-menu-in {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(-8px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: none;
|
|
|
|
|
}
|
|
|
|
|
}
|