You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/components/card/card.css

355 lines
15 KiB

/*
* Card — display container with optional structural slots.
*
* Five-axis variant cube:
* - data-variant: soft | solid | outline | ghost
* - data-color: primary | secondary | neutral | affirm | fulfill | risk | threat | loss
* - data-size: xs | sm | md | lg | xl
* - data-rounded: sm | md | lg
* - data-interactive (boolean) — promotes to <button> + press/focus chrome
* - data-lift (boolean) — hover elevation; implied by data-interactive
*
* Plus three state attrs:
* - data-selected — pinned by consumer; recipe paints accent ring
* - data-disabled — recipe dims + locks pointer
* - data-state="idle|selected" — emitted by morfo runtime
*
* Internal slots (rendered conditionally by the Svelte file):
* - [data-card-header] — top slot (Title + Description usually)
* - [data-card-title] — display heading
* - [data-card-description] — secondary prose
* - [data-card-body] — payload
* - [data-card-footer] — actions / metadata row
*
* Color: when data-color-custom is present, the consumer provided a
* CSS color string (hex / rgb / oklch). The recipe derives track,
* border and text via color-mix() instead of the canonical palette
* cascade.
*
* Tokens cascade through `--_card-*` private vars so the rules below
* stay flat. The recipe surface (`--card-*`) feeds the canonical
* accent slots.
*/
[data-card] {
--_card-padding-inline: var(--card-padding-inline-md);
--_card-padding-block: var(--card-padding-block-md);
--_card-gap: var(--card-gap-md);
--_card-title-font-size: var(--card-title-font-size-md);
--_card-description-font-size: var(--card-description-font-size-md);
--_card-body-font-size: var(--card-body-font-size-md);
--_card-radius: var(--card-radius-md);
/* `--_card-palette-{track,border,solid,text,contrast}` are TSC-emitted from
* `lib/recipes/base.ts > card._palette-*` with default = neutral + 7 per-color
* overrides; the THM-2 shared-layer forward routes them to `color="teal"` +
* the 33 scales. */
/* Default variant = soft; overridden by data-variant below. */
/* private: CONMUTADOR by variant, 4 sources — three are the THM-2 palette
bridge and `ghost` is the identity of the variant. A public on top would
let a theme PIN it and kill every instance's `color=` in silence
(card.md §5, signed 2026-08-23; §3.pre of the handoff). */
--_card-bg: var(--_card-palette-track);
/* private: same CONMUTADOR, and here BOTH sources are the THM-2 bridge —
read at the root, the title and the body (card.md §5, signed 2026-08-23). */
--_card-fg: var(--_card-palette-text);
--_card-border-color: transparent;
display: flex;
flex-direction: column;
gap: var(--_card-gap);
padding-inline: var(--_card-padding-inline);
padding-block: var(--_card-padding-block);
border: var(--card-border-width) solid var(--_card-border-color);
border-radius: var(--_card-radius);
background: var(--_card-bg);
color: var(--_card-fg);
font-family: var(--card-font-family);
line-height: var(--card-line-height);
text-align: start;
transition:
transform var(--duration-fast) var(--ease-default),
box-shadow var(--duration-fast) var(--ease-default),
background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
}
/* Mount animation — fade-in + slide-up, consuming the CHANNEL's registered
* keyframes (`slide-from-bottom` + `fade-in`, themeable via EidosConfig.motion)
* with the recipe's tuned tokens. Disabled by data-no-emerge (long lists) and
* by prefers-reduced-motion.
*
* Why not `motionAttrs`/a preset stamp: the card carries its own SEMANTIC
* `data-state` (selected machine) — the constant `data-state='open'` that
* `motionAttrs` spreads would clobber it. So the trigger stays a recipe rule;
* only the MATERIALS come from the channel (no local @keyframes).
*
* `:not([data-animation-style])` — when the card opts into the motion system
* (the `motion` prop sets data-animation-style), THAT system owns the element's
* single `animation` property. The mount emerge yields: otherwise toggling
* `data-motion-state` would flip animation-name and re-trigger this mount fade
* on every deselect. */
[data-card]:not([data-no-emerge]):not([data-animation-style]) {
--motion-distance-md: var(--card-emerge-distance);
animation:
slide-from-bottom var(--card-emerge-duration) var(--card-emerge-ease) both,
fade-in var(--card-emerge-duration) var(--card-emerge-ease) both;
}
/* ── Size scale ──────────────────────────────────────────────────────── */
[data-card][data-size='xs'] {
--_card-padding-inline: var(--card-padding-inline-xs);
--_card-padding-block: var(--card-padding-block-xs);
--_card-gap: var(--card-gap-xs);
--_card-title-font-size: var(--card-title-font-size-xs);
--_card-description-font-size: var(--card-description-font-size-xs);
--_card-body-font-size: var(--card-body-font-size-xs);
}
[data-card][data-size='sm'] {
--_card-padding-inline: var(--card-padding-inline-sm);
--_card-padding-block: var(--card-padding-block-sm);
--_card-gap: var(--card-gap-sm);
--_card-title-font-size: var(--card-title-font-size-sm);
--_card-description-font-size: var(--card-description-font-size-sm);
--_card-body-font-size: var(--card-body-font-size-sm);
}
[data-card][data-size='lg'] {
--_card-padding-inline: var(--card-padding-inline-lg);
--_card-padding-block: var(--card-padding-block-lg);
--_card-gap: var(--card-gap-lg);
--_card-title-font-size: var(--card-title-font-size-lg);
--_card-description-font-size: var(--card-description-font-size-lg);
--_card-body-font-size: var(--card-body-font-size-lg);
}
[data-card][data-size='xl'] {
--_card-padding-inline: var(--card-padding-inline-xl);
--_card-padding-block: var(--card-padding-block-xl);
--_card-gap: var(--card-gap-xl);
--_card-title-font-size: var(--card-title-font-size-xl);
--_card-description-font-size: var(--card-description-font-size-xl);
--_card-body-font-size: var(--card-body-font-size-xl);
}
/* ── Rounded ─────────────────────────────────────────────────────────── */
[data-card][data-rounded='sm'] {
--_card-radius: var(--card-radius-sm);
}
[data-card][data-rounded='lg'] {
--_card-radius: var(--card-radius-lg);
}
/* Custom CSS colour: the wrapper stamps `data-color-custom` + the shared
* `--color-custom` seed, and the recipe's palette forward derives every
* `--_card-palette-*` slot from it (the same generic path every component
* uses). No bespoke `[data-card][data-color-custom]` block — that one was
* shadowed by the forward and silently dropped raw colours. Trade-off
* unchanged: no light/dark adaptation, no guaranteed contrast. */
/* ── Variant slices ──────────────────────────────────────────────────── */
[data-card][data-variant='soft'] {
--_card-bg: var(--_card-palette-track);
--_card-fg: var(--_card-palette-text);
--_card-border-color: transparent;
}
[data-card][data-variant='solid'] {
--_card-bg: var(--_card-palette-solid);
--_card-fg: var(--_card-palette-contrast);
--_card-border-color: transparent;
}
[data-card][data-variant='outline'] {
--_card-bg: var(--card-outline-bg);
--_card-fg: var(--_card-palette-text);
--_card-border-color: var(--_card-palette-border);
}
[data-card][data-variant='ghost'] {
--_card-bg: transparent;
--_card-fg: var(--_card-palette-text);
--_card-border-color: transparent;
}
/* ── Interactive treatment ───────────────────────────────────────────── */
[data-card][data-interactive] {
cursor: pointer;
user-select: none;
/* Reset native <button> chrome so the recipe owns the visual. */
font: inherit;
text-align: inherit;
appearance: none;
-webkit-tap-highlight-color: transparent;
}
/* The LIFT is its own axis. `interactive` stamps `data-lift` too, so a clickable
* card is unchanged; but a card that already holds its own call to action (a
* pricing plan, an article teaser) can take the elevation WITHOUT becoming a
* `<button>`: nesting one inside another is invalid markup and makes two
* activations compete for one gesture. Cursor, press-scale and focus ring stay
* under `interactive` — they are what a CONTROL owes the visitor. */
[data-card][data-lift]:hover:not([data-disabled]) {
transform: translateY(var(--card-hover-lift));
box-shadow: var(--card-hover-shadow);
}
[data-card][data-lift][data-variant='ghost']:hover:not([data-disabled]) {
background: var(--_card-palette-track);
}
[data-card][data-lift][data-variant='outline']:hover:not([data-disabled]) {
background: var(--_card-palette-track);
}
/* `:not(:has(…:active))` — when an INTERACTIVE descendant is being pressed (a
* Footer button, a link, a form control), the card is ALSO `:active` because the
* activation chain cascades to ancestors; without this guard pressing a control
* would press-scale the whole card. Pressing a control inside the card must not
* move the card. (Plain content presses still scale: only interactive descendants
* suppress it, so `:active` on the card's own surface is unaffected.) */
[data-card][data-interactive]:active:not([data-disabled]):not(
:has(
button:active,
a:active,
input:active,
select:active,
textarea:active,
[role='button']:active
)
) {
transform: scale(var(--card-press-scale));
transition-duration: var(--card-press-duration);
}
[data-card][data-interactive]:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
/* ── Selected state ──────────────────────────────────────────────────── */
[data-card][data-selected] {
box-shadow: 0 0 0 var(--card-selected-ring-width) var(--_card-palette-solid);
}
[data-card][data-selected][data-variant='solid'] {
/* On solid bg the ring would clash with the fill — use the contrast
* tone for a visible outline. */
box-shadow: 0 0 0 var(--card-selected-ring-width) var(--_card-palette-contrast);
}
/* Selected + hovered: `box-shadow` is a single property, so the interactive
* hover shadow would otherwise REPLACE the selection ring (the ring disappears
* on hover). Compose both — ring first, hover shadow second. */
[data-card][data-selected][data-lift]:hover:not([data-disabled]) {
box-shadow:
0 0 0 var(--card-selected-ring-width) var(--_card-palette-solid),
var(--card-hover-shadow);
}
[data-card][data-selected][data-variant='solid'][data-lift]:hover:not([data-disabled]) {
box-shadow:
0 0 0 var(--card-selected-ring-width) var(--_card-palette-contrast),
var(--card-hover-shadow);
}
/* ── Disabled state ──────────────────────────────────────────────────── */
/* Attenuated through `filter`, NOT `opacity` — and that is not a style
* preference. The card's own mount animation (`card-emerge`: `slide-from-bottom,
* fade-in`, both `fill-mode: both`) ends on `opacity: 1`, and an animation
* outranks a normal declaration in the cascade, so a plain `opacity` here was
* simply swallowed: the token resolved to 0.4, the `cursor` of this same rule
* applied, and the card still painted at full strength. Measured, and isolated by
* putting `data-no-emerge` on the same node, where it dimmed correctly. Two
* owners fighting over one property on one node is the KNOWN-FRAGILE pattern the
* motion doctrine names; the repair is to stop sharing the property. */
[data-card][data-disabled] {
filter: opacity(var(--card-disabled-opacity));
cursor: not-allowed;
}
[data-card][data-interactive][data-disabled] {
pointer-events: none;
}
/* ── Header slot ─────────────────────────────────────────────────────── */
[data-card-header] {
display: flex;
flex-direction: column;
gap: var(--card-header-gap);
}
/* ── Title slot ──────────────────────────────────────────────────────── */
[data-card-title] {
margin: 0;
font-family: var(--card-title-font-family);
font-size: var(--_card-title-font-size);
font-weight: var(--card-title-font-weight);
line-height: var(--card-title-line-height);
letter-spacing: var(--card-title-letter-spacing);
color: var(--_card-fg);
}
/* ── Description slot ────────────────────────────────────────────────── */
[data-card-description] {
margin: 0;
font-family: var(--card-description-font-family);
font-size: var(--_card-description-font-size);
line-height: var(--card-description-line-height);
color: var(--card-description-fg);
}
/* Solid variant pulls the description toward the contrast tone with a
* slight transparency so it reads as secondary on the saturated bg. */
[data-card][data-variant='solid'] [data-card-description] {
color: color-mix(in srgb, var(--_card-palette-contrast) 80%, transparent);
}
/* ── Body slot ───────────────────────────────────────────────────────── */
[data-card-body] {
font-size: var(--_card-body-font-size);
line-height: var(--card-body-line-height);
color: var(--_card-fg);
}
/* ── Footer slot ─────────────────────────────────────────────────────── */
[data-card-footer] {
display: flex;
align-items: center;
gap: var(--card-footer-gap);
padding-block-start: var(--card-footer-padding-start);
border-block-start: var(--card-border-width) solid var(--card-footer-border-fg);
margin-block-start: auto;
}
/* Footer border for solid variant — use a translucent contrast line. */
[data-card][data-variant='solid'] [data-card-footer] {
border-block-start-color: color-mix(in srgb, var(--_card-palette-contrast) 18%, transparent);
}
/* ── Reduced motion ──────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
[data-card]:not([data-no-emerge]) {
animation: none;
}
[data-card][data-lift]:hover:not([data-disabled]) {
transform: none;
}
[data-card][data-interactive]:active:not([data-disabled]) {
transform: none;
}
}

Powered by TurnKey Linux.