syntheogenesis / dee /static /motion.css
github-actions[bot]
Deploy f3a53c2
b72b8ce
Raw History Blame Contribute Delete
14.9 kB
/* Depth and motion for Mission Control.
*
* WHAT WAS WRONG, MEASURED
*
* Of 84 elements painted in the first screen, THREE had any box-shadow. The
* whole interface sat on one plane, so nothing was above anything else and
* every separation had to be drawn with a hairline -- 410 `1px solid` rules
* across the stylesheets. That flatness, not the palette, is what reads as
* old.
*
* Motion had the same shape of problem. 138 of the transitions in the first
* screen animate `background`, `color` or `border-color` -- point at a thing
* and it changes colour -- against 4 that touch `opacity` and 4 that touch a
* dimension. Nothing arrives, nothing settles, nothing answers a press. The
* vocabulary was hover-recolour, which is the interaction language of 2004.
*
* WHAT THIS IS NOT
*
* Not a repaint: this file sets no colour. The palette was not the complaint
* and is not touched.
*
* SCOPE
*
* Mission Control only, on purpose. The elevation and motion scales below are
* general, but they are applied to .mc-* and nothing else until they have been
* looked at on a real screen. Four other views keep exactly the styling they
* have today.
*/
/* ── the theme bridge ────────────────────────────────────────────────────
Open Props derives all six shadow levels from two inputs and switches them
on `prefers-color-scheme`. The engine's theme is a USER choice carried on
[data-theme], so somebody running the dark theme on a light OS would get
light-mode shadows: a pale grey smudge under a dark card instead of depth.
Re-declare the two inputs against our switch -- with Open Props' own values,
so the two systems cannot drift -- and its scale follows. The OS-default
case needs nothing; Open Props already handles it. */
/* STRENGTH, not level. Open Props' light default is 1%, tuned for a card on
a PURE WHITE page where almost nothing is needed to separate the two. This
app's ground is off-white and the cards are white, so at 1% every level of
the ramp under-read: measured live, a 680x180 card carried a shadow nobody
could see, and raising the LEVEL only lengthened a blur that was still too
faint. Raising the strength lifts the whole ramp at once and keeps the
relationships between its six steps intact, which is the point of having a
ramp. 4% on the light ground; dark keeps Open Props' own 25%.
Three declarations, not two: the first covers the DEFAULT case, where the
reader has expressed no preference and no [data-theme] attribute exists β€”
without it, anyone who never touched the theme toggle keeps the invisible
1%. The media query then restores dark for that same default case, because
a bare :root here would otherwise beat Open Props' own dark block. */
:root { --shadow-strength: 4%; }
@media (prefers-color-scheme: dark) { :root { --shadow-strength: 25%; } }
:root[data-theme="light"] { --shadow-color: 220 3% 15%; --shadow-strength: 4%; }
:root[data-theme="dark"] { --shadow-color: 220 40% 2%; --shadow-strength: 25%; }
/* ── elevation by role ───────────────────────────────────────────────────
Height has to MEAN something or it is just decoration on every box. Three
roles, and nothing else in Mission Control lifts:
rest (--shadow-2) a card is an object sitting on the page
raised (--shadow-4) the pointer is on it and it can be acted on
press (--shadow-1) it has been pushed back down into the page
The 2px rise is small on purpose. A card that jumps is a card that was
designed to be noticed rather than used. */
.mc-cx {
box-shadow: var(--shadow-2);
/* Explicit properties, never `transition: all`: `all` animates things we
never chose -- including layout properties, which is where jank comes
from -- and it is set on 177 elements in the first screen today. */
transition: box-shadow var(--t-slow) var(--ease-3),
transform var(--t-slow) var(--ease-3),
border-color var(--t-fast) var(--ease-3);
}
@media (prefers-reduced-motion: no-preference) {
.mc-cx:hover { transform: translateY(-2px); }
/* The press answers in --t-fast: lift can be leisurely, but feedback to a
finger cannot be, or the control feels like it missed the click. */
.mc-cx:active {
transform: translateY(0) scale(0.995);
transition-duration: var(--t-fast);
}
}
/* Depth still changes without motion -- somebody on reduced-motion should
lose the movement, not the affordance. */
.mc-cx:hover { box-shadow: var(--shadow-4); }
.mc-cx:active { box-shadow: var(--shadow-1); }
/* A focus ring and an elevation live in the same property, so the ring has to
carry the shadow with it or keyboard focus would flatten the card. */
.mc-cx:focus-visible {
box-shadow: var(--ring) var(--brand-ring), var(--shadow-4);
outline: none;
}
/* ── the press on the primary action ─────────────────────────────────────
The one control on this screen that starts work. It had a colour change and
nothing else. */
@media (prefers-reduced-motion: no-preference) {
.mc-enter-cta { transition: transform var(--t-fast) var(--ease-3),
box-shadow var(--t-fast) var(--ease-3),
background var(--t-fast) var(--ease),
color var(--t-fast) var(--ease); }
.mc-enter-cta:hover { transform: translateY(-1px); }
.mc-enter-cta:active { transform: translateY(0) scale(0.97); }
}
/* The filled CTA takes LESS than a card, not more. Its dark fill already
separates it from the page; at the raised strength, a card-weight shadow
under it reads as a sticker. */
.mc-enter-cta { box-shadow: var(--shadow-1); }
.mc-enter-cta:active { box-shadow: var(--shadow-1); }
/* ── entrance ────────────────────────────────────────────────────────────
THE RULE THIS FILE MUST NOT BREAK: content is never parked invisible.
Every animation here lives inside `prefers-reduced-motion: no-preference`
and is applied by a class that app.js adds and then removes. If the CSS
never loads, if the JS never runs, if the browser refuses the animation --
the resting state is the finished state and the cards are simply there.
An entrance that can strand a paying customer on a blank grid is worse than
no entrance at all.
It also runs ONCE, when the view is first populated. Mission Control
repaints whenever work lands, and a grid that replays its arrival every
time somebody saves is a grid that punishes you for working. */
@media (prefers-reduced-motion: no-preference) {
.mc-grid.is-entering > * {
/* `backwards` so the delay holds the start frame rather than showing
the element, hiding it, then animating it in. */
animation: mc-rise var(--t-slow) var(--ease-3) backwards;
}
/* Stagger, capped. Open Props' own slide-in-up travels 100%, which is
right for a panel and wrong for a card in a grid, so the distance is
ours; the easing is theirs. */
.mc-grid.is-entering > *:nth-child(1) { animation-delay: 0ms; }
.mc-grid.is-entering > *:nth-child(2) { animation-delay: 40ms; }
.mc-grid.is-entering > *:nth-child(3) { animation-delay: 80ms; }
.mc-grid.is-entering > *:nth-child(4) { animation-delay: 120ms; }
.mc-grid.is-entering > *:nth-child(5) { animation-delay: 160ms; }
/* Everything past the fifth arrives together. A stagger long enough to
watch is a stagger that has become a loading screen. */
.mc-grid.is-entering > *:nth-child(n+6) { animation-delay: 200ms; }
}
@keyframes mc-rise {
from { opacity: 0; transform: translateY(8px); }
/* No `to`: the resting state IS the destination, so this keyframe cannot
disagree with the stylesheet about where the card ends up. */
}
/* ── the label leans toward its action ───────────────────────────────────
"Review research β†’" is the thing the card is FOR, and it sat perfectly
still while the card lit up around it. A 2px lean on hover is the oldest
refined micro-interaction there is and it still works: the eye reads it as
the card pointing at where the click goes.
The nudge is on the whole label rather than the arrow alone because the
arrow is a bare → inside the text node -- wrapping it would mean
touching two markup sites in app.js for two pixels. */
@media (prefers-reduced-motion: no-preference) {
.mc-cx-act-go { transition: transform var(--t-slow) var(--ease-3); }
.mc-cx:hover .mc-cx-act-go { transform: translateX(2px); }
}
/* ── the secondary controls answer the same way ──────────────────────────
The primary CTA got a press in the last pass and the two controls beside
it did not, which reads worse than if none of them had: one button in a row
behaving differently is a bug the eye notices before the mind does. */
@media (prefers-reduced-motion: no-preference) {
.mc-enter-find,
.ws-entry { transition: transform var(--t-fast) var(--ease-3),
box-shadow var(--t-fast) var(--ease-3),
border-color var(--t-fast) var(--ease-3),
color var(--t-fast) var(--ease); }
.mc-enter-find:active,
.ws-entry:active { transform: scale(0.97); }
/* Nav items take the press but never the lift: a sidebar that rises under
the pointer turns a list into a row of buttons competing with the work.
The transition lives on .nav-item in app.css, alongside the hover fade
it already had β€” declaring a second one here would have replaced that
fade rather than extended it. */
.nav-item:active { transform: scale(0.985); }
}
/* ── focus, everywhere the press went ────────────────────────────────────
Keyboard users get the same geometry as the mouse: one ring, carried on
var(--ring) so test_shape_scale stays true, and never `outline: none`
without a replacement. */
.mc-enter-cta:focus-visible,
.mc-enter-find:focus-visible,
.ws-entry:focus-visible {
box-shadow: var(--ring) var(--brand-ring);
outline: none;
}
/* ══ ENGINE-WIDE INTERACTION ═════════════════════════════════════════════
Mission Control was the pilot. This extends the same vocabulary to the
rest of the engine β€” and the operative word is VOCABULARY, not coverage.
There are 357 rules in these stylesheets that draw a bordered, filled,
rounded box. Elevating all of them would raise everything, and a page
where everything is raised reads exactly like a page where nothing is:
depth only means something while most things stay on the ground.
So: two roles, assigned by what the thing DOES, and nothing else gets
touched.
A CARD is content you open. It lifts under the pointer and presses in.
A CONTROL is a thing you press. It presses, and never lifts β€” a toolbar
that rises under the cursor turns into a row of hovering chiclets.
Floating surfaces (.modal-panel, .acct-menu, .toast) already carry their
own elevation and are deliberately absent: they are never pressed, and
restating their shadow here would fight the sheet that owns them. */
/* ── cards: lift, then press ─────────────────────────────────────────────
Gated on being ACTIVATABLE, not on carrying a card-ish class name.
Measured on the live engine: `.card` is 23 <section> elements and NONE of
them is clickable β€” it is a layout container. Lifting it would be a false
affordance, content that rises under the pointer and then does nothing
when you press it, which is worse than no motion at all.
`:is(button, a, [role=button])` makes that rule structural instead of a
list I maintain by hand: a class only gets the lift on the routes where it
is actually a control, and a future <section class="card"> never does. */
:is(button, a, [role="button"]):is(.mc-recent, .card, .model-card, .primer-card, .cp-eg, .lib-row, .hist-item, .cds-item):hover { box-shadow: var(--shadow-4); }
:is(button, a, [role="button"]):is(.mc-recent, .card, .model-card, .primer-card, .cp-eg, .lib-row, .hist-item, .cds-item) { transition: box-shadow var(--t-slow) var(--ease-3),
transform var(--t-slow) var(--ease-3),
border-color var(--t-fast) var(--ease-3); }
:is(button, a, [role="button"]):is(.mc-recent, .card, .model-card, .primer-card, .cp-eg, .lib-row, .hist-item, .cds-item):active { box-shadow: var(--shadow-1); }
@media (prefers-reduced-motion: no-preference) {
:is(button, a, [role="button"]):is(.mc-recent, .card, .model-card, .primer-card, .cp-eg, .lib-row, .hist-item, .cds-item):hover { transform: translateY(-2px); }
:is(button, a, [role="button"]):is(.mc-recent, .card, .model-card, .primer-card, .cp-eg, .lib-row, .hist-item, .cds-item):active { transform: translateY(0) scale(0.995);
transition-duration: var(--t-fast); }
}
:is(button, a, [role="button"]):is(.mc-recent, .card, .model-card, .primer-card, .cp-eg, .lib-row, .hist-item, .cds-item):focus-visible {
box-shadow: var(--ring) var(--brand-ring), var(--shadow-4);
outline: none;
}
/* ── controls: press only ────────────────────────────────────────────────
Every one of these already transitions its own colours; the property
lists in app.css were widened to include `transform` in the same change
that retired `transition: all`, so this adds the state and not a second,
competing shorthand. */
@media (prefers-reduced-motion: no-preference) {
.primary:not(:disabled):active,
.primary-ghost:not(:disabled):active,
.ghost:not(:disabled):active,
.identify-btn:not(:disabled):active,
.copy-btn:active, .expand-toggle:active, .modal-close:active,
.nav-toggle:active, .theme-toggle:active, .cmd-trigger:active,
.acct-trigger:active, .filter-clear:active,
.crispr-vendor-btn:not(:disabled):active,
.crispr-struct-mini:active,
.tab:active {
transform: scale(0.97);
}
}