initial commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/* ============================================================================
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user