550 lines
16 KiB
CSS
550 lines
16 KiB
CSS
/* ============================================================================
|
|
HOME MOTION - the reveals the redesigned home page runs
|
|
----------------------------------------------------------------------------
|
|
Every animation in the Claude Design draft ("SHARP-ER Home v2 Dark"), carried
|
|
over as classes so the page modules stay free of inline keyframes and raw
|
|
values. Durations and easings are the motion tokens from semantic.css; the
|
|
few long, decorative loops are expressed as multiples of them rather than as
|
|
bare seconds, so retiming the system retimes the page.
|
|
|
|
TWO KINDS OF MOTION LIVE HERE
|
|
* entrance - time based, runs once from the server-rendered HTML
|
|
(.sh-rise, .sh-bar-grow), staggered with .sh-rise--2..5
|
|
* scroll - scroll-driven (`animation-timeline`), so a band reveals as
|
|
it enters the viewport and the top progress bar tracks the
|
|
document
|
|
|
|
WHY THE @supports GUARD: scroll-driven timelines ship in Chromium only. The
|
|
from-state (opacity 0) is applied ONLY where `animation-timeline: view()` is
|
|
understood, so Safari and Firefox render every band at rest, fully visible,
|
|
instead of holding an invisible element forever.
|
|
|
|
REDUCED MOTION: app.css already collapses every animation to 0.01ms. The
|
|
scroll block is additionally gated on `prefers-reduced-motion: no-preference`
|
|
so a reduced-motion visitor never gets a timeline-driven opacity ramp at all.
|
|
|
|
DIRECTION: `transform-origin`, `linear-gradient()`, `clip-path: inset()` and
|
|
`translateX()` have no logical keywords, so the progress bar, the brand
|
|
stripe, the headline wipe, the hover rule and the logo marquee each carry an
|
|
explicit RTL override. These are the only physical directions in the file,
|
|
and they exist because the property itself has no logical form - every box
|
|
edge elsewhere is still ps-/pe-/start-/end-.
|
|
========================================================================= */
|
|
|
|
@layer theme, base, components, utilities;
|
|
|
|
@layer components {
|
|
/* -------------------------------------------------------------------------
|
|
ENTRANCE - hero content, first paint
|
|
---------------------------------------------------------------------- */
|
|
.sh-rise {
|
|
animation: sh-rise var(--motion-duration-reveal) var(--motion-ease-entrance)
|
|
var(--sh-rise-delay, 0ms) both;
|
|
}
|
|
|
|
/* The hero's cascade. The step is the system's fast duration, so the whole
|
|
stagger retimes with the token instead of drifting on hard-coded ms. */
|
|
.sh-rise--2 {
|
|
--sh-rise-delay: calc(var(--motion-duration-fast) * 0.75);
|
|
}
|
|
|
|
.sh-rise--3 {
|
|
--sh-rise-delay: calc(var(--motion-duration-fast) * 1.5);
|
|
}
|
|
|
|
.sh-rise--4 {
|
|
--sh-rise-delay: calc(var(--motion-duration-fast) * 2.25);
|
|
}
|
|
|
|
.sh-rise--5 {
|
|
--sh-rise-delay: calc(var(--motion-duration-fast) * 3);
|
|
}
|
|
|
|
/* The home headline. The third design drop stops lifting it and uncovers it
|
|
instead, from the inline start, so the words are read into place rather
|
|
than slid in. It reads the SAME `--sh-rise-delay` as the cascade above, so
|
|
`.sh-rise--3` &c. stagger the wipe alongside everything else. */
|
|
.sh-wipe {
|
|
animation: sh-wipe var(--motion-duration-reveal) var(--motion-ease-entrance)
|
|
var(--sh-rise-delay, 0ms) backwards;
|
|
}
|
|
|
|
[dir="rtl"] .sh-wipe {
|
|
animation-name: sh-wipe-rtl;
|
|
}
|
|
|
|
/* `backwards`, not `both`: the clip has to be GONE once the wipe is over.
|
|
`inset(0)` at rest would keep clipping to the border box, which shaves a
|
|
descender or an Arabic mark off a tight heading line. */
|
|
|
|
/* app.css shortens every animation but leaves its delay alone, which for a
|
|
clip means the headline is not merely faded but absent until the delay has
|
|
passed. Removing the animation outright puts the words on screen from the
|
|
first paint, which is what a reduced-motion reader asked for. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.sh-wipe {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
/* The brand stripe under the hero picture, drawn on from the inline start. */
|
|
.sh-bar-grow {
|
|
transform-origin: left center;
|
|
animation: sh-bar-grow var(--motion-duration-reveal)
|
|
var(--motion-ease-entrance) calc(var(--motion-duration-fast) * 4) both;
|
|
}
|
|
|
|
[dir="rtl"] .sh-bar-grow {
|
|
transform-origin: right center;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
AMBIENT LOOPS - decorative, never carrying meaning
|
|
---------------------------------------------------------------------- */
|
|
|
|
/* The two soft washes behind the hero. Long, slow drifts: 22.5x and 30x the
|
|
reveal duration (18s / 24s at the shipped token). */
|
|
.sh-glow {
|
|
animation: sh-glow calc(var(--motion-duration-reveal) * 22.5) ease-in-out
|
|
infinite;
|
|
}
|
|
|
|
/* A highlight travelling across the hero picture. */
|
|
.sh-sheen {
|
|
background-size: 240% 100%;
|
|
animation: sh-sheen calc(var(--motion-duration-reveal) * 8) linear infinite;
|
|
}
|
|
|
|
/* The live dot in the hero status pill. */
|
|
.sh-pulse {
|
|
animation: sh-pulse calc(var(--motion-duration-reveal) * 2.75) ease-in-out
|
|
infinite;
|
|
}
|
|
|
|
/* The network logo wall, drifting past. The track carries the tiles TWICE,
|
|
so moving it exactly half its own width lands on an identical frame and
|
|
the loop never shows a seam; 42.5x the reveal duration is the draft's 34s
|
|
at the shipped token.
|
|
|
|
`width` is set here rather than with a `w-max` utility on purpose: the
|
|
reduced-motion block below has to be able to take it back, and a utility
|
|
sits in a later layer than this one.
|
|
|
|
It stops under the pointer and whenever anything inside it takes focus, so
|
|
a reader can hold the movement still to look at - or tab to - a logo
|
|
(WCAG 2.2.2). Under reduced motion it stops being a track at all: the row
|
|
simply wraps and the duplicate half is removed, so nothing moves and no
|
|
logo is shown twice. */
|
|
.sh-marquee {
|
|
width: max-content;
|
|
animation: sh-marquee calc(var(--motion-duration-reveal) * 42.5) linear
|
|
infinite;
|
|
}
|
|
|
|
/* RTL reads the other way, so the drift does too. */
|
|
[dir="rtl"] .sh-marquee {
|
|
animation-name: sh-marquee-rtl;
|
|
}
|
|
|
|
.sh-marquee:hover,
|
|
.sh-marquee:focus-within {
|
|
animation-play-state: paused;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.sh-marquee {
|
|
width: auto;
|
|
flex-wrap: wrap;
|
|
animation: none;
|
|
}
|
|
|
|
.sh-marquee__clone {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* The wall fades out at both ends instead of being cut off mid-tile. `black`
|
|
and `transparent` here are alpha, not colour - a mask reads the channel
|
|
only - which is why this is not a surface token. The ramp is symmetric, so
|
|
it needs no RTL form. */
|
|
.sh-marquee-mask {
|
|
-webkit-mask-image: linear-gradient(
|
|
to right,
|
|
transparent,
|
|
black 8%,
|
|
black 92%,
|
|
transparent
|
|
);
|
|
mask-image: linear-gradient(
|
|
to right,
|
|
transparent,
|
|
black 8%,
|
|
black 92%,
|
|
transparent
|
|
);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
INTERACTION
|
|
---------------------------------------------------------------------- */
|
|
|
|
/* Tiles that rise a little under the pointer. No shadow: the draft's hover
|
|
glow is deliberately left out. */
|
|
.sh-lift {
|
|
transition:
|
|
transform var(--motion-duration-base) var(--motion-ease-standard),
|
|
border-color var(--motion-duration-base) var(--motion-ease-standard),
|
|
background-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.sh-lift:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
}
|
|
|
|
/* 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-lift--grow:hover {
|
|
transform: translateY(-5px) scale(1.04);
|
|
}
|
|
}
|
|
|
|
/* An accent rule drawn across the top of a tile under the pointer, growing
|
|
from the inline start. It is a pseudo-element so the tile's own markup
|
|
stays a heading and a paragraph. */
|
|
.sh-rule-top {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sh-rule-top::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset-block-start: 0;
|
|
inset-inline: 0;
|
|
block-size: 3px;
|
|
background: var(--surface-accent);
|
|
transform: scaleX(0);
|
|
transform-origin: left center;
|
|
transition: transform var(--motion-duration-slow)
|
|
var(--motion-ease-entrance);
|
|
}
|
|
|
|
[dir="rtl"] .sh-rule-top::before {
|
|
transform-origin: right center;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.sh-rule-top:hover::before {
|
|
transform: scaleX(1);
|
|
}
|
|
}
|
|
|
|
/* An FAQ answer opening under its question. */
|
|
.sh-answer {
|
|
animation: sh-answer var(--motion-duration-base) var(--motion-ease-entrance)
|
|
both;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
SCROLL DRIVEN - Chromium today, inert and harmless elsewhere
|
|
---------------------------------------------------------------------- */
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
@supports (animation-timeline: view()) {
|
|
.sh-reveal {
|
|
animation: sh-rise linear both;
|
|
animation-timeline: view();
|
|
animation-range: entry 0% entry 70%;
|
|
}
|
|
|
|
/* Cards and pictures: the same reveal with a touch of scale. */
|
|
.sh-reveal--in {
|
|
animation-name: sh-reveal-in;
|
|
animation-range: entry 0% entry 80%;
|
|
}
|
|
|
|
/* The edge tiles of a row land in from their own side of the line - the
|
|
reading start and the reading end - instead of rising, so a row drags
|
|
into place edge-first while its middle rises. `translateX` has no
|
|
logical keyword, so the RTL block swaps the two names: the drift always
|
|
comes off the start edge, whichever script is on screen. */
|
|
.sh-reveal--from-start {
|
|
animation-name: sh-rise-from-start;
|
|
}
|
|
|
|
.sh-reveal--from-end {
|
|
animation-name: sh-rise-from-end;
|
|
}
|
|
|
|
[dir="rtl"] .sh-reveal--from-start {
|
|
animation-name: sh-rise-from-end;
|
|
}
|
|
|
|
[dir="rtl"] .sh-reveal--from-end {
|
|
animation-name: sh-rise-from-start;
|
|
}
|
|
|
|
/* A beat between the tiles of a row: each item after the first holds at
|
|
its hidden state until it is a little deeper in the viewport, so a row
|
|
cascades start-to-end instead of landing as one. Only the start of the
|
|
range shifts; `.sh-reveal--in` and the edge slides keep their ends. */
|
|
.sh-reveal--step-1 {
|
|
animation-range-start: entry 12%;
|
|
}
|
|
|
|
.sh-reveal--step-2 {
|
|
animation-range-start: entry 24%;
|
|
}
|
|
|
|
/* The hero picture drifting as the page scrolls past it. It is scaled up
|
|
before it travels, so the drift can never uncover an edge of the
|
|
frame. Chromium-only, like every timeline here: elsewhere the picture
|
|
simply sits still, which is the state the design falls back to. */
|
|
.sh-pan {
|
|
animation: sh-pan linear both;
|
|
animation-timeline: view();
|
|
animation-range: entry 100% exit 0%;
|
|
}
|
|
}
|
|
|
|
@supports (animation-timeline: scroll()) {
|
|
/* The reading-progress rule across the top of the page. It is painted
|
|
ONLY inside this @supports block: without a scroll timeline the bar
|
|
would sit there permanently at full width, which is a rule the design
|
|
never asked for. */
|
|
.sh-progress {
|
|
background-image: linear-gradient(
|
|
to right,
|
|
var(--surface-accent),
|
|
var(--text-accent) 55%,
|
|
var(--surface-danger)
|
|
);
|
|
transform-origin: left center;
|
|
animation: sh-bar-grow linear both;
|
|
animation-timeline: scroll();
|
|
}
|
|
|
|
[dir="rtl"] .sh-progress {
|
|
background-image: linear-gradient(
|
|
to left,
|
|
var(--surface-accent),
|
|
var(--text-accent) 55%,
|
|
var(--surface-danger)
|
|
);
|
|
transform-origin: right center;
|
|
}
|
|
}
|
|
|
|
/* THE SAME REVEAL, WITHOUT A TIMELINE. `_page-kit/reveal.ts` tags a
|
|
`.sh-reveal` element `.sh-reveal-js` (hidden) in browsers that lack
|
|
scroll timelines, then `.sh-reveal-js--in` plays `sh-rise` once it
|
|
scrolls into view. Both classes are inert without the helper, so
|
|
content is always readable. */
|
|
.sh-reveal-js {
|
|
opacity: 0;
|
|
}
|
|
|
|
.sh-reveal-js--in {
|
|
animation: sh-rise var(--motion-duration-reveal)
|
|
var(--motion-ease-entrance) both;
|
|
}
|
|
|
|
/* The edge tiles keep their drift here too: the reader who never sees a
|
|
scroll timeline still gets the same from-start / from-end arrival. The
|
|
RTL mirrors swap the names, as the Chromium block above does. */
|
|
.sh-reveal--from-start.sh-reveal-js--in {
|
|
animation-name: sh-rise-from-start;
|
|
}
|
|
|
|
.sh-reveal--from-end.sh-reveal-js--in {
|
|
animation-name: sh-rise-from-end;
|
|
}
|
|
|
|
[dir="rtl"] .sh-reveal--from-start.sh-reveal-js--in {
|
|
animation-name: sh-rise-from-end;
|
|
}
|
|
|
|
[dir="rtl"] .sh-reveal--from-end.sh-reveal-js--in {
|
|
animation-name: sh-rise-from-start;
|
|
}
|
|
|
|
/* The same beat, expressed in time where there is no timeline. `both`
|
|
already answers for the hidden hold during the delay. */
|
|
.sh-reveal--step-1.sh-reveal-js--in {
|
|
animation-delay: var(--motion-duration-fast);
|
|
}
|
|
|
|
.sh-reveal--step-2.sh-reveal-js--in {
|
|
animation-delay: calc(var(--motion-duration-fast) * 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes sh-rise {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(26px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
@keyframes sh-reveal-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px) scale(0.985);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
/* The same reveal turned sideways. `--from-start` is picked up by the RTL
|
|
block as the mirror of `--from-end`, so both names exist and the swap is
|
|
legible there instead of shipping one keyframe and praying. */
|
|
@keyframes sh-rise-from-start {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-20px) scale(0.985);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
@keyframes sh-rise-from-end {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(20px) scale(0.985);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
@keyframes sh-bar-grow {
|
|
from {
|
|
transform: scaleX(0);
|
|
}
|
|
|
|
to {
|
|
transform: scaleX(1);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-glow {
|
|
0%,
|
|
100% {
|
|
opacity: 0.5;
|
|
transform: translate3d(-4%, 0, 0) scale(1);
|
|
}
|
|
|
|
50% {
|
|
opacity: 0.85;
|
|
transform: translate3d(6%, -3%, 0) scale(1.12);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-sheen {
|
|
from {
|
|
background-position: -160% 0;
|
|
}
|
|
|
|
to {
|
|
background-position: 260% 0;
|
|
}
|
|
}
|
|
|
|
@keyframes sh-pulse {
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
|
|
50% {
|
|
opacity: 0.35;
|
|
transform: scale(0.72);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-wipe {
|
|
from {
|
|
opacity: 0.4;
|
|
clip-path: inset(0 100% 0 0);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
clip-path: inset(0 0 0 0);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-wipe-rtl {
|
|
from {
|
|
opacity: 0.4;
|
|
clip-path: inset(0 0 0 100%);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
clip-path: inset(0 0 0 0);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-marquee {
|
|
from {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
to {
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-marquee-rtl {
|
|
from {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
to {
|
|
transform: translateX(50%);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-pan {
|
|
from {
|
|
transform: translateY(-5%) scale(1.14);
|
|
}
|
|
|
|
to {
|
|
transform: translateY(5%) scale(1.14);
|
|
}
|
|
}
|
|
|
|
@keyframes sh-answer {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-8px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|