/* ========================================================================== Niveus — Card -------------------------------------------------------------------------- .nv-card resting surface, elevation 1 .nv-card--elevated separated from its context, elevation 2 .nv-card--inset recessed into its parent .nv-card--frosted over imagery or scrolling content .nv-card--interactive the whole card is one target .nv-card--lit lit along its top edge in its tint .nv-card--glow gives off its tint — the one card that is on .nv-card--spotlight catches a light that follows the pointer .nv-card__header / __body / __footer / __media A card is a container, not a look. If a card needs a shadow that is not on the elevation scale, the answer is a different elevation — not a different shadow. ========================================================================== */ .nv-card { /* Content surfaces are mostly solid: enough of the canvas comes through to tie a card to the light around it, never enough to compete with what it holds. The blur keeps anything busy behind it (a bed, an image) from reading through as texture. */ --_bg: color-mix(in oklab, var(--nv-color-surface) 90%, transparent); --_border: var(--nv-color-border-subtle); --_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1); --_sheen: var(--nv-sheen-surface); --_padding: var(--nv-space-5); /* Light layers the variants switch on. Kept separate from --_shadow so a lit or glowing card can still be elevated, floating or interactive. */ --_lit: 0 0 transparent; --_glow: 0 0 transparent; --_spot: linear-gradient(transparent, transparent); position: relative; display: flex; flex-direction: column; min-inline-size: 0; background-color: var(--_bg); background-image: var(--_spot), var(--_sheen); color: var(--nv-color-text-primary); border: var(--nv-border-width) solid var(--_border); border-radius: var(--nv-radius-lg); box-shadow: var(--_lit), var(--_shadow), var(--_glow); backdrop-filter: var(--nv-backdrop-glass); -webkit-backdrop-filter: var(--nv-backdrop-glass); /* Children are clipped to the corner radius so media and inset regions cannot square off the card. */ overflow: hidden; transition: var(--nv-transition-surface); } .nv-card--elevated { --_bg: color-mix(in oklab, var(--nv-color-surface-elevated) 90%, transparent); --_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2); } .nv-card--floating { --_bg: color-mix(in oklab, var(--nv-color-surface-elevated) 88%, transparent); --_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3); } .nv-card--inset { --_bg: color-mix(in oklab, var(--nv-color-surface-inset) 85%, transparent); --_shadow: var(--nv-elevation-inset-md), var(--nv-ledge); --_sheen: none; --_border: var(--nv-color-border-subtle); } .nv-card--frosted { --_bg: var(--nv-material-frosted-bg); --_border: var(--nv-material-frosted-border); --_shadow: var(--nv-material-frosted-shadow); --_sheen: none; backdrop-filter: var(--nv-backdrop-frosted); -webkit-backdrop-filter: var(--nv-backdrop-frosted); } .nv-card--flush { --_padding: 0; } /* --- Interactive ------------------------------------------------------------ Rises on hover and sinks on press, exactly like a button — one physical language for anything that responds to a pointer. */ .nv-card--interactive { cursor: pointer; text-align: start; text-decoration: none; } @media (hover: hover) { .nv-card--interactive:hover { box-shadow: var(--_lit), var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3), var(--_glow); translate: 0 calc(var(--nv-lift-hover) * 2); } } .nv-card--interactive:active { box-shadow: var(--_lit), var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1), var(--_glow); translate: 0 var(--nv-press-depth); transition-duration: var(--nv-duration-instant); } .nv-card--interactive:focus-visible { outline: none; box-shadow: var(--_lit), var(--_shadow), var(--_glow), var(--nv-focus-ring); } /* --- Light ------------------------------------------------------------------ Three ways a card can carry light, each stated once and combinable with every elevation above. They all take the card's tint — the accent unless --nv-card-tint names another color, the way --nv-bed-tint does for a bed. Lit: a strip of light along the top edge, spilling a little way down the face. For a card that belongs to something with a color: a project, a status, a category. Glow: the card gives off its tint. For the one card that is on — the current plan, the live item. Two glowing cards side by side cancel each other out. Spotlight: a soft light follows the pointer across the face (niveus.js tracks it). A showcase effect; a list of work items does not need one. */ .nv-card--lit, .nv-card--glow, .nv-card--spotlight { --nv-card-tint: var(--nv-color-accent); } .nv-card--lit { --_lit: inset 0 2px 0 color-mix(in oklab, var(--nv-card-tint) 85%, transparent), inset 0 18px 28px -22px color-mix(in oklab, var(--nv-card-tint) 70%, transparent); --_sheen: radial-gradient( 120% 70% at 0% 0%, color-mix(in oklab, var(--nv-card-tint) 12%, transparent), transparent 70% ), var(--nv-sheen-surface); } .nv-card--glow { --_border: color-mix(in oklab, var(--nv-card-tint) 40%, var(--nv-color-border-subtle)); --_glow: 0 0 var(--nv-glow-blur-md) var(--nv-glow-spread-md) color-mix(in oklab, var(--nv-card-tint) var(--nv-glow-strength), transparent); } @media (hover: hover) { .nv-card--glow.nv-card--interactive:hover { --_glow: 0 0 var(--nv-glow-blur-lg) var(--nv-glow-spread-md) color-mix(in oklab, var(--nv-card-tint) var(--nv-glow-strength), transparent); } .nv-card--spotlight:hover { --_spot: radial-gradient( 22rem circle at var(--nv-pointer-x, 50%) var(--nv-pointer-y, 0%), color-mix(in oklab, var(--nv-card-tint) 14%, var(--nv-color-sheen-top)), transparent 70% ); } } /* Cards whose whole area is clickable via a link inside: the link's target is stretched over the card, so the markup stays semantic and only the link text is announced. */ .nv-card__link::after { content: ""; position: absolute; inset: 0; } /* --- Parts ------------------------------------------------------------------- */ .nv-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--nv-space-4); padding: var(--_padding); padding-block-end: var(--nv-space-3); } .nv-card__title { font: var(--nv-font-title); letter-spacing: var(--nv-letter-spacing-tight); margin: 0; } .nv-card__subtitle { font: var(--nv-font-caption); color: var(--nv-color-text-secondary); margin-block-start: var(--nv-space-1); } .nv-card__body { flex: 1; padding: var(--_padding); color: var(--nv-color-text-secondary); font: var(--nv-font-body); } /* A header already opened the block, so the body should not open it again. */ .nv-card__header + .nv-card__body { padding-block-start: 0; } .nv-card__footer { display: flex; align-items: center; gap: var(--nv-space-2); padding: var(--_padding); padding-block-start: var(--nv-space-3); border-block-start: var(--nv-border-width) solid var(--nv-color-border-subtle); margin-block-start: auto; } .nv-card__footer--plain { border-block-start: 0; padding-block-start: 0; } .nv-card__media { display: block; inline-size: 100%; aspect-ratio: 16 / 9; object-fit: cover; background-color: var(--nv-color-surface-inset); } /* --- Group --------------------------------------------------------------------- Equal-height cards on one rhythm, collapsing to a single column without a media query. */ .nv-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr)); gap: var(--nv-space-4); align-items: stretch; }