/* ============================================================================ 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; } /* 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 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. 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 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. */ .sh-brand-suffix { overflow: hidden; } .sh-brand-suffix__mark { 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; } @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); } } /* 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; } } @keyframes sh-nav-menu-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }