130 lines
4.9 KiB
CSS
130 lines
4.9 KiB
CSS
/* ============================================================================
|
|||
|
|
CARD - mechanics that cannot be expressed as a utility
|
||
|
|
----------------------------------------------------------------------------
|
||
|
|
Everything token-driven lives in the TSX as a tier-2 utility
|
||
|
|
(bg-surface-raised, text-text-accent, rounded-surface, ...). This file only
|
||
|
|
holds the three things a utility cannot do:
|
||
|
|
|
||
|
|
1. paint an exported SVG asset in the current token colour (mask),
|
||
|
|
2. the whole-card link overlay + its focus ring,
|
||
|
|
3. the hover transitions, which need a duration/easing token pair.
|
||
|
|
|
||
|
|
Class names are prefixed `sh-card` because this file is imported by the
|
||
|
|
component rather than by app/app.css, so it is not scoped by anything else.
|
||
|
|
|
||
|
|
Everything lives in @layer components so a Tailwind utility passed through
|
||
|
|
`className` (`rounded-surface`, `text-text-secondary`, ...) still wins.
|
||
|
|
The layer statement is a defensive no-op when app.css has already declared
|
||
|
|
the same order, and makes this file correct on its own if it ever loads first.
|
||
|
|
========================================================================= */
|
||
|
|
|
||
|
|
@layer theme, base, components, utilities;
|
||
|
|
|
||
|
|
@layer components {
|
||
|
|
/* ---------------------------------------------------------------------------
|
||
|
|
ICONS
|
||
|
|
The SVGs exported from Figma (public/assets/icons/*.svg) have the brand
|
||
|
|
green baked in as stroke="#008764", which would be wrong on the dark surface
|
||
|
|
and inside the accent chips. Drawing the asset as a mask over a currentColor
|
||
|
|
fill keeps the exact exported geometry while the colour comes from whichever
|
||
|
|
semantic token the parent set. Size is set per instance so each icon keeps
|
||
|
|
its designed box (14px, or 10px for the stat trend arrow).
|
||
|
|
------------------------------------------------------------------------ */
|
||
|
|
.sh-card-icon {
|
||
|
|
display: block;
|
||
|
|
flex: none;
|
||
|
|
background-color: currentColor;
|
||
|
|
-webkit-mask-image: var(--sh-card-icon-src);
|
||
|
|
mask-image: var(--sh-card-icon-src);
|
||
|
|
-webkit-mask-repeat: no-repeat;
|
||
|
|
mask-repeat: no-repeat;
|
||
|
|
-webkit-mask-position: center;
|
||
|
|
mask-position: center;
|
||
|
|
-webkit-mask-size: contain;
|
||
|
|
mask-size: contain;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Arrows that mean "forward" point the other way in Arabic. Only the
|
||
|
|
horizontal arrow mirrors - the stat trend arrow is vertical. */
|
||
|
|
[dir="rtl"] .sh-card-icon--directional {
|
||
|
|
transform: scaleX(-1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------------------------
|
||
|
|
WHOLE-CARD LINK
|
||
|
|
The anchor around the title stretches over the card with ::after, so the
|
||
|
|
entire card is clickable while the title stays the accessible name and no
|
||
|
|
interactive element is nested inside another.
|
||
|
|
|
||
|
|
z-index 1 / 2 are structural, not design values: 1 is the overlay, 2 lifts
|
||
|
|
anything that has to stay clickable above it (the team card's social links).
|
||
|
|
------------------------------------------------------------------------ */
|
||
|
|
.sh-card-link::after {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
inset: 0;
|
||
|
|
z-index: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sh-card-above {
|
||
|
|
position: relative;
|
||
|
|
z-index: 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The ring belongs around the card, not around the inline title text. Where
|
||
|
|
:has() is unavailable the anchor keeps the global :focus-visible ring from
|
||
|
|
app.css, so focus is never invisible. */
|
||
|
|
@supports selector(:has(*)) {
|
||
|
|
.sh-card-link:focus-visible {
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sh-card:has(.sh-card-link:focus-visible) {
|
||
|
|
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
||
|
|
outline-offset: var(--focus-ring-offset);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------------------------
|
||
|
|
HOVER / FOCUS AFFORDANCES
|
||
|
|
------------------------------------------------------------------------ */
|
||
|
|
.sh-card--linked {
|
||
|
|
transition:
|
||
|
|
box-shadow var(--motion-duration-base) var(--motion-ease-standard),
|
||
|
|
border-color var(--motion-duration-base) var(--motion-ease-standard);
|
||
|
|
}
|
||
|
|
|
||
|
|
.sh-card--linked:hover {
|
||
|
|
border-color: var(--border-default);
|
||
|
|
box-shadow: var(--elevation-popover);
|
||
|
|
}
|
||
|
|
|
||
|
|
.sh-card--linked:hover .sh-card-title,
|
||
|
|
.sh-card--linked:has(.sh-card-link:focus-visible) .sh-card-title {
|
||
|
|
text-decoration: underline;
|
||
|
|
text-underline-offset: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sh-card-media img {
|
||
|
|
transition: transform var(--motion-duration-slow) var(--motion-ease-standard);
|
||
|
|
}
|
||
|
|
|
||
|
|
.sh-card--linked:hover .sh-card-media img {
|
||
|
|
transform: scale(1.04);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* app.css already neutralises transition durations under reduced motion, but a
|
||
|
|
transform still lands instantly - drop it entirely. */
|
||
|
|
@media (prefers-reduced-motion: reduce) {
|
||
|
|
.sh-card--linked:hover .sh-card-media img {
|
||
|
|
transform: none;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The CTA sits on the accent fill, where the green focus ring has almost no
|
||
|
|
contrast. Inside its data-surface="dark" subtree the inverse ring is white. */
|
||
|
|
.sh-card-cta-action:focus-visible {
|
||
|
|
outline-color: var(--focus-ring-color-inverse);
|
||
|
|
}
|
||
|
|
}
|