> ## Oymyakon DS index
> Fetch the full index at: https://super-dollop-pzmo65r.pages.github.io/llms.txt
> HTML preview: https://super-dollop-pzmo65r.pages.github.io/card.html
> Source: specs/components/card/card.md, specs/components/card/card-a11y.md, specs/components/card/capabilities.json

---

# Card · Oymyakon DS 3

> Vertical content surface — a self-contained unit combining optional media/icon slots with a text block. Figma lineage `[Card] 3.1` (node `6617-45261`).

**Version:** 3.3.22 · **Status:** Ready

---

## 1. Overview

Card is a surface component for presenting a distinct unit of content. The component shipped in `[Card] 3.1` is the **customCard** base: a vertical stack of StartSlot, StartText, and EndSlot on an OnWhite surface, with an optional CornerSlot (Checkbox) overlaying the top-right corner. The model has three levels: mechanics = **axes** (§4a–4b), product data contracts = **variants** (§8 — serviceCard, storyCard), product composition = **patterns** (the composition layer above components).

Unlike Cell (a full-width horizontal list row), Card sizes itself per instance (`--card-width` / `--card-height`), carries its own background and radius, and stacks its slots vertically.

Card is not interactive by default. Instances opt into press behaviour with `.card--interactive` (§11).

---

## 2. Slot structure

```
Card (customCard)
├── StartSlot         .card__start-slot   fill × hug · align left/top
│                       IconContainer by default · optional (visible by default)
│
├── StartText         .card__start-text   vertical stack of TextRow instances
│     gap: s4 · align left
│     ├── Title       .card__title-row     TextRow · always visible
│     └── Subtitle    .card__subtitle-row  TextRow · visible by default, below Title
│
├── EndSlot           .card__end-slot     fill × hug · align left/top
│                       IconContainer by default · optional (visible by default)
│
└── CornerSlot        .card__corner-slot  s48 × s48 overlay, top-right corner
                        Checkbox instance by default · optional (visible by default)
```

### Slot geometry contract

| Element | Visibility | Geometry rule |
|---|---|---|
| **StartSlot** | Optional (default on) | Inside Card padding, first in the stack |
| **StartText** | Always present | Inside Card padding, cannot be hidden |
| **EndSlot** | Optional (default on) | Inside Card padding, last in the stack |
| **CornerSlot** | Optional (default on) | Absolute overlay pinned to the card's top-right corner; does not participate in the stack and does not change the padding contract |

Slot containers stay inside the Card padding area. Bleed, overflow, crop, scale, or offset belongs to the content instance inside a slot, never to the slot container.

---

## 3. Anatomy

```
┌──────────────────────────────────┐
│  [StartSlot]       [CornerSlot]  │
│                                  │
│  [Title      — StartText]        │
│  [Subtitle   — StartText]        │
│                                  │
│  [EndSlot]                       │
└──────────────────────────────────┘
```

| Slot | Type | Required | Default | Can be hidden |
|---|---|---|---|---|
| **StartSlot** | CustomSlot › IconContainer | Optional | Visible (IconContainer) | Yes |
| **StartText** | Stack of TextRow | Required | Title visible, Subtitle visible below | No |
| **EndSlot** | CustomSlot › IconContainer | Optional | Visible (IconContainer) | Yes |
| **CornerSlot** | Checkbox | Optional | Visible (Checkbox) | Yes |

---

## 4. Layout & Spacing

Vertical autolayout on the Surface/OnWhite surface:

```
direction:        vertical
gap:              --card-gap      · default var(--sp-s8)   (StartSlot / StartText / EndSlot)
padding:          --card-padding  · default var(--sp-s16)  all sides
border-radius:    --card-radius   · default var(--sp-s20)
background:       var(--surface-on-white)
width:            --card-width    · default calc(var(--sp-s80) + var(--sp-s80)) — 160
height:           --card-height   · default auto (hug)
border:           none by default · instances may add 1px solid var(--border-default)
```

**Openness contract (customCard):** surface (`--card-surface`, default `var(--surface-on-white)`), radius, paddings, gaps, and TextRow typography are *defaults*, not locks — any instance overrides them with DS tokens via the `--card-*` props (full-bleed media follows `--card-padding` automatically). Variants (§8) lock their own values; the base stays open. Width and height are instance-level: the consuming layout sets `--card-width` / `--card-height` (or lets the card hug). The Figma default example is 160 × 136.

### CornerSlot geometry

```
size:             var(--sp-s48) × var(--sp-s48)   (touch target)
position:         absolute, top/right of the card surface
padding:          var(--sp-s8) top and right
content:          centred — the 24px Checkbox lands s16 from both card edges
RTL:              mirrors to the top-left (padding mirrors with it)
```

### 4a. Layout flow (axis)

| Value | Class | Behaviour |
|---|---|---|
| `stack` (default) | — | Vertical flow: StartSlot → StartText → EndSlot with the `--card-gap` |
| `overlap` | `.card--overlap` | Grid stack: the three parts share the padding area; StartText renders on top (z-index 1), EndSlot stretches and anchors its content end/bottom |

A mechanism, not a preset — any purpose combines it with the media pin below (vertical tiles, stories, promos all use the same flow).

### 4b. Media pin (axis)

Where the media content sits inside its slot in `overlap` flow. Applied to the **content**, never to the slot container (§2):

| Value | Content classes | Behaviour |
|---|---|---|
| `bottom` | `.card__artwork .card__artwork--full-bleed` | Full-width strip flush to the bottom edge (the tall tile / story shape) |
| `corner` | `.card__img-container .card__img-container--full-bleed` | s80 crop window bled into the end/bottom corner |
| `end` | `.card__img-container .card__img-container--pin-end` | Crop window centred on the end edge (the wide tile shape) |

Media knobs (instance-level, any SP token):

| Prop | Applies to | Default | Effect |
|---|---|---|---|
| `--card-media-offset` | crop window | `var(--sp-s0)` | Shifts the artwork toward the end edge — less of it stays visible, the rest runs out past the crop boundary |
| `--card-media-size` | crop window | `var(--sp-s80)` | The crop window itself — a smaller window shows a smaller artwork |
| `--card-media-shift-y` | artwork strip | `var(--sp-s0)` | Nudges the strip vertically (positive = down); the card's `overflow: hidden` clips whatever leaves the surface |

`--mirror-source-x` flips the source; `--offset-start-s8` is a legacy alias that pulls the artwork toward the *start* edge (`--card-media-offset: calc(-1 * var(--sp-s8))`) — the opposite sign, kept for the existing mockups. Full-bleed margins follow `--card-padding`.

---

## 5. StartSlot / EndSlot

Free-content slots at the top and bottom of the stack. Identical rules:

```
direction:    horizontal autolayout (fill)
height:       hug (vertical)
align:        left / top
padding:      var(--sp-s0)
```

| Property | Default | Options |
|---|---|---|
| Content | IconContainer | Any DS element |
| Visibility | Visible | Can be hidden |

Hiding a slot removes it from the stack — the `gap` closes up (no phantom spacing).

Slot sizing uses the [Slot](https://super-dollop-pzmo65r.pages.github.io/slot.md) vocabulary: **hug** (shrink to content) or **fill** (expand to the parent); alignment names the position of the content inside the slot on both axes (`start` / `center` / `end` horizontally, `top` / `center` / `bottom` vertically). In `stack` flow the slots hug their content vertically; in `overlap` flow the EndSlot stretches (`fill`) so its media can anchor to the bottom — set it back to `hug` when the slot's own bounds should follow the content instead.

### Slot content

Both slots are [Slot](https://super-dollop-pzmo65r.pages.github.io/slot.md) instances — fill × hug, no visual style of their own. [IconContainer](https://github.com/inDriver/oymyakon-ds/blob/main/specs/primitives/icon-container.md) is the *default* content, not the only option:

```
StartSlot / EndSlot
├── IconContainer          — s24 DS icon, --text-and-icon-primary (default)
├── ImgContainer           — illustration or photo: .card__artwork (full-width strip)
│                            or .card__img-container (crop window), placed by the
│                            mediaPin axis (§4b) with its media knobs
├── lottie animation host  — any animation container the composition needs
└── Tag · Button · Squircle · Rating · other DS components
```

A nested component keeps its own default classes and states — no opacity or colour bleed from the card; a non-default colour goes inline on that element. `ImgContainer` is Card-owned content in this spec version; if it becomes reusable outside Card it receives its own primitive spec first.

---

## 6. StartText

Vertical stack of TextRow instances. Always present.

```
gap:       --card-text-gap · default var(--sp-s4)  (between Title and Subtitle)
align:     left
```

Each row supports RichText — typography and inline formatting per [`specs/components/rich-text/rich-text.md`](https://github.com/inDriver/oymyakon-ds/blob/main/specs/components/rich-text/rich-text.md).

### Title (`.card__title-row`, TextRow)

Always visible.

- Typography: `var(--text-body-main-body-*)` (Main Body) — the *default*; any DS text style per instance (set family / size / weight / line-height together on the row wrapper)
- Colour: inherits `var(--text-and-icon-primary)` from the card root
- `multiline`: off by default (single line, ellipsis); can be set to on

### Subtitle (`.card__subtitle-row`, TextRow)

Visible by default, positioned below the Title.

- Typography: `var(--text-body-compact-body-*)` (Compact Body) — the *default*; any DS text style per instance
- Colour: `var(--text-and-icon-secondary)`
- `multiline`: off by default; can be set to on
- Position: `below` (default) / `off` / `above`

---

## 7. CornerSlot

Checkbox pinned over the top-right corner of the card surface. The slot is the s48 touch target; the Checkbox instance inside keeps its own props and layout. No background override — the card surface shows through.

| Property | Value |
|---|---|
| Default content | Checkbox (`[Checkbox] 3.1` instance) |
| Visibility | Visible by default (can be hidden) |
| Position | Top-right corner (top-left in RTL) |
| Touch target | `var(--sp-s48)` × `var(--sp-s48)` |
| Background | None (inherits card surface) |

CornerSlot consumes the [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md) component directly (since 3.3.22). The slot is the s48 touch target and carries the semantics — `role="checkbox"`, `aria-checked`, focus; the Checkbox instance inside it is `.checkbox--no-safezone` (the bare s24 box, Figma `.CheckNoSafezone`) and is visual only, marked `aria-hidden="true"` so the control is announced once. The slot's s8 top/right padding lands the box `var(--sp-s16)` from both card edges.

Keyboard follows the Checkbox contract: Space toggles, Enter does not.

---

## 8. Variants

A **variant** is a product-level data contract: what the slots carry and what the card is for. Mechanics stay in the axes (§4a–4c, §5–7, the openness contract); sizes come from the consuming grid via `--card-width` / `--card-height`. A configuration earns a variant only when it brings its own data and semantics — a different layout alone is an axis value, and several cards arranged together are a **pattern** (the composition level above components).

| Variant | `data-ds-variant` | Class | Data contract |
|---|---|---|---|
| customCard | — (base) | `.card` | none — all axes open |
| serviceCard | `service-card` | `.card--service` | vertical / service name + transport artwork, opens the service |
| storyCard | `story-card` | `.card--story` | promo headline + background medium, opens the story |

Both variants are web-first (owner decision 2026-07-29); their Figma definitions are pending, and they join the `[Card] 3.1` lineage until then.

### Variant: `serviceCard`

Entry into a vertical or service. Locked: Compact Body title, radius `var(--sp-s24)`, OnWhite surface, `layoutFlow: overlap`, CornerSlot off, Subtitle off. Open: size and the artwork itself.

| Size | Axes | Designer reference |
|---|---|---|
| **L** | `mediaPin: bottom` — full-width artwork strip flush to the bottom | "City Rides" (170 × 168) |
| **M** | `mediaPin: corner` at the full row width | "Couriers" (170 × 82) |
| **S** | `mediaPin: corner` in a half-row cell | "City to city" / "Freight" (83 × 82) |

Sizes are marked with `data-ds-size="l|m|s"`; their pixel values come from the consuming grid via `--card-width` / `--card-height`, not from the variant. Two **S** cards plus the grid gap equal one **M** (83 + 4 + 83 = 170 in the source design), so the tiles align on both edges — that relationship belongs to a future `vertical-grid` pattern.

Both shapes are **vertical** — Card is a vertical surface by definition (§1). A horizontal composition (text start, media end, both centred) is what **Cell** already is, so the designer's wide tile ("Couriers", 170 × 82) belongs to Cell with media in its end-slot, not to Card. A `direction: row` axis was briefly added in 3.3.0 and removed the same day for exactly this reason (owner decision).

### Variant: `storyCard`

Promo story tile. Locked: Compact Body title, radius `var(--sp-s24)`, `layoutFlow: overlap` with `mediaPin: bottom`, default size `104 × 156`, CornerSlot off, Subtitle off. Required per instance: a **non-OnWhite** surface via `--card-surface` (pastel or dark).

Text and icon colours are the instance's responsibility when the surface departs from OnWhite — the contrast rule applies (see [`card-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/card.md) § 4).

### Not a variant: site navigation cards

The docs-site Foundations landing cards (`.site-nav-card`, `DS.initNavCards()`) are **site chrome**, not part of the component contract — they exist only on this site, so they carry no `data-ds-variant`. They were briefly modelled as the `navCard` preset in 3.1.3–3.2.0 and moved out in 3.3.0. Their CSS/JS remains in `shared.css` / `shared.js` under the `site-` prefix so it never reads as a DS variant.

---

## 9. States

| State | Visual |
|---|---|
| **Standard** | Surface `var(--surface-on-white)`, all content live |
| **Skeleton** | `.card--skeleton` — content replaced by Skeleton placeholders; CornerSlot hidden; stack switches to `justify-content: space-between` with zero gap |

### Skeleton composition

The Skeleton state composes the [Skeleton](https://super-dollop-pzmo65r.pages.github.io/skeleton.md) component:

| Placeholder | Composition |
|---|---|
| StartSlot icon | `.skeleton .card__skeleton-icon` — s24 × s24, radius `var(--sp-s6)` |
| Title bar | `.skeleton-text .skeleton-text--main-body` › `.skeleton` — full width |
| Subtitle bar | `.skeleton-text .skeleton-text--compact-body` › `.skeleton` — `var(--sp-s80)` wide |

EndSlot has no skeleton placeholder — the state renders the icon and the two text bars only, and the space-between geometry assumes exactly those two children.

The skeleton keeps the **loaded card height** (`--card-height: calc(var(--sp-s128) + var(--sp-s8))` — 136, the height of a full Standard card) rather than hugging its placeholders: hugging collapses `space-between` to zero and the icon ends up flush against the text bars. With the height in place the icon sits at the top, the bars at the bottom, matching Figma `State=Skeleton`.

On the OnWhite surface the placeholders re-tint to `var(--surface-overlay)` (`.card--skeleton .skeleton` override) — Skeleton/OnWhite would not read against the beige surface. The shimmer wave and its reduced-motion handling come from the Skeleton master.

---

## 10. Color Tokens

| Element | Token |
|---|---|
| Card background | `var(--surface-on-white)` |
| StartSlot / EndSlot icon | `var(--text-and-icon-primary)` (inherited from root) |
| Title text | `var(--text-and-icon-primary)` (inherited from root) |
| Subtitle text | `var(--text-and-icon-secondary)` |
| CornerSlot checkbox border | `var(--text-and-icon-secondary)` |
| Skeleton placeholders | `var(--surface-overlay)` |
| Optional instance border | `var(--border-default)` |

---

## 11. Motion

Card is static by default. Interactive instances (links, tappable tiles) add `.card--interactive` and press with the catalogued **pushItem** recipe (motion-rules §6.1 — cards are medium/large components, scale not colour):

| Event | Pattern | Duration | Curve |
|---|---|---|---|
| Press (touch down) | `pushItem` — scale 100% → 95% | 200ms | `standard-ease-in-out` |
| Release (touch up) | `pushItem` — scale 95% → 100% | 200ms | `standard-ease-in-out` |

```css
.card--interactive {
  cursor: pointer;
  transition: transform var(--card-push-item-release);
}
.card--interactive:active {
  transform: scale(0.95);
  transition: transform var(--card-push-item-press);
}
```

`--card-push-item-press` / `--card-push-item-release` are zeroed under `prefers-reduced-motion: reduce`. The Skeleton shimmer is owned by the Skeleton component and handles reduced motion there.

---

## 12. CSS Implementation

Real classes shipped in `src/shared/shared.css`:

| Class | Role |
|---|---|
| `.card` | Root — surface, radius s20, padding s16, vertical stack gap s8, `--card-width` / `--card-height` |
| `.card__start-slot` | StartSlot — fill × hug, left/top |
| `.card__start-text` | StartText — column, gap s4 |
| `.card__title-row` | Title TextRow wrapper — sets Main Body on `.text-row__text` |
| `.card__subtitle-row` | Subtitle TextRow wrapper — sets Compact Body + secondary on `.text-row__text` |
| `.card__end-slot` | EndSlot — fill × hug, left/top |
| `.card__corner-slot` | CornerSlot — s48 absolute overlay, top-right (top-left in RTL) |
| `.card--skeleton` | Skeleton state — zero gap, space-between, hides CornerSlot, re-tints `.skeleton` |
| `.card__skeleton-icon` | s24 icon placeholder, radius s6 |
| `.card--interactive` | Opt-in pushItem press |
| `.card--service` | serviceCard variant (§8) — Compact Body title, radius s24 |
| `.card--story` | storyCard variant (§8) — Compact Body title, radius s24, 104 × 156 default |
| `.card--overlap` | `layoutFlow: overlap` axis (§4a) — grid stack, StartText over EndSlot media |
| `.card__artwork` / `.card__img-container` | `mediaPin` axis content classes (§4b): `--full-bleed`, `--pin-end`, `--offset-start-s8`, `--mirror-source-x` |

Title/Subtitle follow the TextRow wrapper pattern (as Rating's Description): the row wrapper carries typography and colour on `.text-row__text`; the Title inherits the root colour, the Subtitle declares its own.

`.site-nav-card` and `DS.initNavCards()` also live in the shared files, but they are **site chrome** rather than component API (§8) — the `site-` prefix marks the boundary.

---

## 13. Accessibility

Brief summary. Full spec: [`card-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/card.md).

- Card root carries a meaningful `accessibilityLabel` combining Title and Subtitle.
- Interactive card: role `button` (or link semantics when it navigates).
- CornerSlot Checkbox is announced as a separate interactive element with its own state.
- Skeleton state: hidden from assistive tech (`aria-hidden="true"`) with a busy region announced on the container that owns the loading.

---

## 14. Usage Context

### When to use Card

- A discrete content unit with visual context (icon, media) plus text.
- Product tiles, category cards, option cards, selectable cards (CornerSlot).
- Any surface needing its own background and radius inside a grid or scrollable row.

### When not to use Card

- Full-width horizontal list row — use Cell.
- Plain text without a surface — use TextRow or RichText.

### Related components

| Component | Relation |
|---|---|
| Cell | Horizontal list row; no own surface |
| IconContainer | Default StartSlot / EndSlot content |
| [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md) | Default CornerSlot content — consumed as `.checkbox--no-safezone` |
| TextRow | Title and Subtitle rows |
| Skeleton | Skeleton-state placeholders |
| RichText | Inline formatting inside TextRow |

---

## 15. Analytics and coverage contract

| Field | Value |
|---|---|
| `data-ds-component` | `card` |
| `data-ds-variant` | absent (base customCard) · `service-card` · `story-card` (§8) |
| Coverage unit | Yes — one Card root counts as one DS component instance |
| Tap target model | Root target when the Card is interactive |
| Root actions | `tap`, `select` |
| Internal targets | `corner-slot` (Checkbox) when it is an independent action |
| Emits value | Optional `data-ds-value` for the selected/checked state |

```html
<div
  class="card"
  data-ds-component="card"
  data-ds-preset="custom-card"
  data-ds-state="standard">
  ...
</div>
```

---

## RTL

**Default: RTL = Off.** When an instance enables `dir="rtl"`:

| Element | RTL behaviour |
|---|---|
| Card root | Stack order unchanged (vertical); inline content mirrors |
| StartSlot / EndSlot | Content inside the slot mirrors with `dir="rtl"` |
| CornerSlot | Mirrors to the **top-left** corner; its s8 padding mirrors with it |
| StartText | Text alignment follows `dir` automatically; TextRow inherits `dir` |

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.3.22 | 2026-07-30 | CornerSlot consumes the [Checkbox](https://super-dollop-pzmo65r.pages.github.io/checkbox.md) component (`.checkbox--no-safezone`) instead of composing the geometry page-locally; the `.card-checkbox` stand-in CSS is gone from `card.njk`. Geometry is unchanged (box s16 from both edges). Behaviour change: Enter no longer toggles — the slot now follows the Checkbox keyboard contract, which reserves Enter for form submit. Space and click are unaffected. |
| 3.3.21 | 2026-07-29 | CornerSlot checkbox: the check glyph was rendered at 16px inside the 24px box, which thinned its strokes; it now fills the box's inner area (20 × 20 at native 24-grid proportions). The path itself is the DS `icons/outlined/actions/check.svg`, unchanged. |
| 3.3.20 | 2026-07-29 | Preview: the control example's slot fills vertically (`fill × fill`, content centred at the end edge) — its read-out updated to match, so the three examples now cover hug and fill on both axes. |
| 3.3.19 | 2026-07-29 | Preview: the control example pushes its Tag to the slot's **end** edge, and each of the three `Slot content` examples carries a read-out of its slot sizing and alignment (`fill × hug · align …`) so the placement rules are visible, not implied. |
| 3.3.18 | 2026-07-29 | Preview: the media slot in the `Slot content` example is set to **hug** (Slot §1 — shrink to content) instead of the stretch that `layoutFlow: overlap` applies, so its dashed frame no longer runs under the title. |
| 3.3.17 | 2026-07-29 | Preview: the `Slot content` examples outline the **slots** with a dashed frame (accent-blue2, rx4) so each example reads as "this is the slot, this is what sits inside"; the text blocks stay unframed. |
| 3.3.16 | 2026-07-29 | Preview: the `Slot content` media example fills the free space — cards grown to 136 × 136 (all three aligned) and the artwork strip to 92, so the whole vehicle renders at 92 × 92 with an 8px clearance under the title. |
| 3.3.15 | 2026-07-29 | Preview: the `Slot content` media example keeps the full vehicle artwork (no crop) but sizes it to the space left below the title — whole illustration, 16px clearance, flush to the bottom edge. |
| 3.3.14 | 2026-07-29 | Preview fix: the Layout `Slot content` media example overlapped its own title — a square full-bleed artwork does not fit under text in a 108-tall card. Fixed by sizing the media (first via a crop window, then as a sized full artwork in 3.3.15). |
| 3.3.13 | 2026-07-29 | Preview: the Layout `Slot content` examples put the caption **above** the card (label first, example under it), all three cards top-aligned on one line. |
| 3.3.12 | 2026-07-29 | Preview fix: the Layout `Slot content` examples were misaligned — the Tag overflowed its 104-wide card and the three cards had different heights. All three are now 136 × 108 with zero overflow and captions on two lines. |
| 3.3.11 | 2026-07-29 | §5 gains a **Slot content** block: both slots are Slot instances and IconContainer is only the default — ImgContainer (artwork / crop window via the mediaPin axis), a Lottie host, Tag / Button / Squircle / Rating and other DS components all fit, each keeping its own default classes and states. Mirrored on the preview page as a `Slot content` column in Layout with three live examples. |
| 3.3.10 | 2026-07-29 | Terminology: the card's fill is called **Background** in the States point and the §10 colour table (was "Surface", which collided with the token group name). |
| 3.3.9 | 2026-07-29 | Skeleton state keeps the loaded card height (136 via `--card-height`) instead of hugging the placeholders — hugging collapsed `space-between` to a zero gap and the icon sat flush against the text bars; now the icon is pinned top and the bars bottom (gap 40), as in Figma `State=Skeleton`. States/Skeleton pointers repositioned for the new geometry. |
| 3.3.8 | 2026-07-29 | States/Standard column corrected: the surface point now names the real token `--surface-on-white` (the CSS prop `--card-surface` read as a token and misled); point 2 renamed **StartText** with Title/Subtitle in bold and its pointer leg extended onto the glyphs; the slot-content pointer moved above the StartSlot icon; a fourth point added for **CornerSlot** (Checkbox border token, s48 touch target, no own background). |
| 3.3.7 | 2026-07-29 | Preview page: **States** rebuilt to the DS convention — one `spec-column` per state (canvas with numbered pointers on the left, tokens on the right) instead of a shared wide canvas: Standard (surface · Title/Subtitle · slot content) and Skeleton (placeholder tint · icon placeholder · text placeholders). All pointer dots DOM-verified against the real geometry. |
| 3.3.6 | 2026-07-29 | Preview page: the **Variants** section removed (Overview already shows customCard · serviceCard L/M/S · storyCard, so the section only repeated it); sticky bar back to seven tabs. The variant contracts themselves stay here in §8 and in capabilities.json — the page is their showcase, not their source. |
| 3.3.5 | 2026-07-29 | storyCard title corrected **Main Body → Compact Body** (matches the source Figma story, 14/16). Story photo raised to `var(--sp-s60)` (13px higher than the half-artwork position — the SP scale has no odd steps). |
| 3.3.4 | 2026-07-29 | Demo media positions tuned via `--card-media-shift-y`: serviceCard **L** vehicle to `var(--sp-s24)`, storyCard photo to `calc(var(--sp-s72) + var(--sp-s2))` — half of its own artwork height, so the lower half clips away. |
| 3.3.3 | 2026-07-29 | Vertical media knob on the `mediaPin` axis (§4b): `--card-media-shift-y` nudges the artwork strip up/down inside the card (the surface clips the overflow). serviceCard **L** demos use `var(--sp-s12)` so the vehicle sits lower. |
| 3.3.2 | 2026-07-29 | serviceCard sizes named **L / M / S** (`data-ds-size`) after the designer tiles — L City Rides, M Couriers, S City to city / Freight. Page restructured so Overview and Variants stop overlapping: Overview shows the three shipped things (customCard · serviceCard L/M/S · storyCard), Variants carries the contract only (locks vs open, one shape shown locked and hand-assembled side by side). storyCard demo uses the source Figma photo (`illu/photo/story-delivery.png`) on `--pastel-blue2`. |
| 3.3.1 | 2026-07-29 | Crop-window offset and size tokenized on the `mediaPin` axis (§4b): `--card-media-offset` (shifts the artwork toward the end edge so less of it is visible) and `--card-media-size` (the window itself). `--offset-start-s8` stays as a legacy alias. Narrow serviceCard demos use `var(--sp-s24)` offset. |
| 3.3.0 | 2026-07-29 | **Variants replace presets** as the product level (industry criterion: a variant carries its own data + semantics; a layout alone is an axis). Added **serviceCard** (`.card--service` — vertical/service entry: name + transport artwork, Compact Body, radius s24) and **storyCard** (`.card--story` — promo headline over full-bleed media on a required non-OnWhite surface, 104 × 156). Card stays a **vertical** surface: both serviceCard shapes are vertical stacks (tall = `mediaPin: bottom`, narrow = `mediaPin: corner`); the designer's wide tile is a **Cell** case, not a Card one — a `direction: column|row` axis was added and removed the same day (owner decision), since a horizontal row with start text and end media is exactly what Cell provides. **navCard left the component contract** — it is docs-site chrome, now `.site-nav-card` (site- prefix), removed from capabilities and §8. |
| 3.2.0 | 2026-07-29 | Three-level model codified: mechanics = axes, behaviour = presets, product composition = patterns. New axes: **layoutFlow** `stack|overlap` (§4a — `.card--overlap` is now the axis modifier), **mediaPin** `bottom|corner|end` (§4b — content classes; new `.card__img-container--pin-end`), **surface** (`--card-surface`, openness contract). The `overlap` and `tallCard` presets **dissolved into the axes** — they were mechanics with no behaviour; mockup instances migrated to `data-ds-preset="custom-card"`. navCard remains the only behavioural preset. Grounded in the designer frames: vertical tiles (node 27494-11525, tall/wide/small = mediaPin bottom/end/corner) and stories (node 27497-11856, same shape + surface override). |
| 3.1.5 | 2026-07-29 | customCard openness contract (owner decision): radius / paddings / gaps / TextRow typography are defaults, overridable per instance with DS tokens — new props `--card-radius`, `--card-padding`, `--card-gap`, `--card-text-gap` (full-bleed media margins now follow `--card-padding`). Ready-made presets keep their locks. New axes in capabilities.json. |
| 3.1.4 | 2026-07-28 | Third ready-made preset: **tallCard** (`.card--tall`, `data-ds-preset="tall-card"`) — the "Share your ride" tall promo tile: overlap grid stack + full-width bottom `.card__artwork--full-bleed`; height set by the consuming grid. colors-usage artwork tiles migrated from `overlap` to `tall-card`; `overlap` narrowed to the crop-window (`.card__img-container`) media case. |
| 3.1.3 | 2026-07-28 | Ready-made presets formalized from the shipped site instances (§8): **navCard** (`.card--nav` — Colors landing navigation card: link surface, hover invert, H4 title, arrow stretch via `DS.initNavCards()`) and **overlap** (`.card--overlap` — media card, promoted from the prototype-extension layer with its artwork/img-container content classes). Preset axis added to capabilities.json. Figma definitions pending — web-first, owner-approved. |
| 3.1.2 | 2026-07-28 | Full rework to the Figma lineage `[Card] 3.1` (version realigned from the repo-local 3.3.0). Slots renamed to the DS-wide semantics: top-slot → **StartSlot**, text-container → **StartText**, bottom-slot → **EndSlot**, end-slot → **CornerSlot** (s48 touch target, Checkbox). Title/Subtitle are TextRow wrapper classes. Added the **Skeleton** state (Skeleton composition, Surface/Overlay tint). Press recipe corrected pushHighlight → **pushItem** via `.card--interactive`. Width/height instance-level (`--card-width`/`--card-height`); min-height and space-between removed from the master. `overlap`/full-bleed machinery moved out of the contract to the prototype-extension layer (candidate ready-made preset). States renamed Default → Standard; Pressed/Disabled dropped (not in `[Card] 3.1`). |
| 3.3.0 | 2026-06-02 | (pre-realignment lineage) Defined two universal presets: `default` and `overlap`; slot geometry contract; `layoutFlow`/`heightMode`; full-bleed props. |
| 3.2.0 | 2026-05-31 | (pre-realignment lineage) Added `bottomSlotFullBleed` boolean contract. |
| 3.1.1 | 2026-05-15 | Added §4a image overlap layout. |
| 3.1.0 | 2026-05-15 | Initial version. Card base + CustomCard preset; top-slot / end-slot (Checkbox) / text-container / bottom-slot. |

---

# Card · Accessibility Spec · Oymyakon DS 3

**Version:** 3.1.3 · **Status:** Ready
**Paired spec:** [`specs/components/card/card.md`](https://super-dollop-pzmo65r.pages.github.io/card.md)

---

## 1. Roles and Labels

### Card root

| Scenario | Role | Label |
|---|---|---|
| Card is non-interactive (display only) | `group` / `article` | Composed from Title + Subtitle text |
| Card is interactive (tappable) | `button` (or link when it navigates) | Composed from Title + Subtitle text |
| Card contains CornerSlot Checkbox | `group` | Card label + Checkbox announced separately |
| navCard preset | native `<a>` (link) | Composed from Title + Subtitle text; the dc-icon and arrow are `aria-hidden` decoration |

**Label composition rule:** `"{Title text}, {Subtitle text}"` when the Subtitle is visible; Title only when it is hidden.

### Checkbox (CornerSlot)

The Checkbox is a fully independent interactive element. It is announced separately from the card body, with its own label and checked state.

| Property | Value |
|---|---|
| Role | `checkbox` |
| Label | Context-specific (e.g. "Select transport card") |
| State announcement | "checked" / "unchecked" |

---

## 2. Focus & Keyboard Navigation

| Scenario | Behaviour |
|---|---|
| Non-interactive Card | Not in tab order. Not focusable. |
| Interactive Card (`.card--interactive`) | Focusable. `Tab` reaches the card. `Enter` / `Space` triggers the action. |
| CornerSlot Checkbox | Always focusable independently of the card's interactive state. `Tab` reaches it. `Space` toggles. |
| Skeleton Card | Not focusable — the whole card is `aria-hidden="true"` while loading. |

When both the Card and the Checkbox are focusable, tab order is:
1. Card root (if interactive)
2. Checkbox (CornerSlot)

---

## 3. Screen Reader Behaviour

### iOS / VoiceOver

| Element | Announcement |
|---|---|
| Non-interactive Card | "{Title}, {Subtitle}" (grouping role) |
| Interactive Card | "{Title}, {Subtitle}, button" |
| Checkbox unchecked | "{label}, checkbox, unchecked" |
| Checkbox checked | "{label}, checkbox, checked" |
| Skeleton Card | Not announced (hidden); the owning container reports loading |

### Android / TalkBack

| Element | Announcement |
|---|---|
| Non-interactive Card | "{Title}, {Subtitle}" |
| Interactive Card | "{Title}, {Subtitle}, double-tap to activate" |
| Checkbox unchecked | "{label}, not checked, checkbox" |
| Checkbox checked | "{label}, checked, checkbox" |
| Skeleton Card | Not announced (hidden); the owning container reports loading |

### Web / NVDA + JAWS

| Element | HTML role | Announcement |
|---|---|---|
| Non-interactive Card | `<article>` or `<div role="group">` | "{Title}, {Subtitle}, group" |
| Interactive Card | `<button>` / `<a>` | "{Title}, {Subtitle}, button/link" |
| Checkbox | `<input type="checkbox">` with `<label>` | "{label}, checkbox, checked/not checked" |
| Skeleton Card | `aria-hidden="true"` on the card root | Silent; the owning list/container carries `aria-busy="true"` |

---

## 4. Color Contrast

All text meets WCAG 2.1 AA minimum contrast:

| Text element | Token | Requirement |
|---|---|---|
| Title | `var(--text-and-icon-primary)` on `var(--surface-on-white)` | ≥ 4.5:1 (normal text) |
| Subtitle | `var(--text-and-icon-secondary)` on `var(--surface-on-white)` | ≥ 4.5:1 (normal text) |
| StartSlot / EndSlot icon | `var(--text-and-icon-primary)` on `var(--surface-on-white)` | ≥ 3:1 (non-text) |
| CornerSlot checkbox border | `var(--text-and-icon-secondary)` on `var(--surface-on-white)` | ≥ 3:1 (non-text UI) |

Skeleton placeholders (`var(--surface-overlay)`) are decorative and exempt — the state is hidden from assistive tech.

**Rule:** colour is never the only signal. A state conveyed by colour alone (selected, error) also gets a text label, icon, or border change.

---

## 5. States

| State | Visual | Accessible signal |
|---|---|---|
| Standard | Normal | — |
| Interactive press | pushItem scale | No extra signal — activation is announced by the role |
| Selected (CornerSlot flow) | Checkbox checked | Checkbox state; optionally `aria-selected="true"` on the card in a listbox context |
| Skeleton | Placeholders + shimmer | `aria-hidden="true"` on the card; `aria-busy="true"` on the owning container |

---

## 6. Touch Target

Minimum touch target size: **48×48px** (WCAG 2.5.5).

- The Figma default example is 160 × 136 — comfortably above the minimum; instance sizes below s48 in either dimension are out of contract for interactive cards.
- CornerSlot **is** the s48 × s48 touch target for its 24px Checkbox — never shrink the slot to the icon size.

---

## 7. Reduced Motion

Under `prefers-reduced-motion: reduce`:

- `--card-push-item-press` / `--card-push-item-release` collapse to `0ms` — the press scale is instant (no motion).
- The Skeleton shimmer wave stops (handled by the Skeleton master); the static placeholder tint remains.

See card.md §10.

---

## 8. Localization & RTL

- Title and Subtitle follow the `dir` attribute automatically (TextRow inherits `dir` from the card root).
- When `dir="rtl"`, the CornerSlot mirrors to the top-left corner visually. Screen readers follow DOM order — the CornerSlot stays last in DOM regardless of direction, so reading order is unaffected.

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.1.3 | 2026-07-28 | navCard preset row added: native link semantics, decorative icon/arrow hidden. The whole card is the affordance — the Title is a heading inside the link, not an inline text link (the underline rule applies to inline links, not block-level interactive surfaces). |
| 3.1.2 | 2026-07-28 | Realigned to the reworked card.md 3.1.2: slots renamed (StartSlot / StartText / EndSlot / CornerSlot), Skeleton state a11y added (aria-hidden + aria-busy on owner), Pressed/Disabled rows removed (not in `[Card] 3.1`), touch-target section corrected (no min-height claim; CornerSlot = s48 target), reduced-motion updated to the pushItem tokens. |
| 3.1.0 | 2026-05-15 | Initial accessibility spec for Card and CustomCard preset. |

---

## Machine contract — `specs/components/card/capabilities.json`

Axes, allowed values, defaults, constraints and CSS/Figma bindings. Read this instead of guessing what the component can do.

```json
{
  "$schema": "../component-capabilities.schema.json",
  "id": "card",
  "name": "Card",
  "version": "3.3.22",
  "description": "Vertical content surface — StartSlot, StartText (Title/Subtitle TextRows), EndSlot on a configurable surface, with an optional CornerSlot (Checkbox) overlay. customCard is the open base; variants (serviceCard, storyCard) are product-level data contracts on top of the axes.",
  "files": {
    "spec": "specs/components/card/card.md",
    "a11y": "specs/components/card/card-a11y.md",
    "preview": "src/card.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon 3.28.0 (components)",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {
      "[Card] 3.1": {
        "key": "0267593b399919c268d5ee8eecfa28641a8ef587"
      },
      "TextContainer": {
        "key": "9943a52b72563a198716c9b57ddc48d6b146f356",
        "note": "StartText in web semantics — Title/Subtitle stack consumed by the Card set."
      }
    },
    "capturedAt": "2026-07-28"
  },
  "root": {
    "class": "card",
    "dataDsComponent": "card"
  },
  "anatomy": {
    "startSlot": {
      "class": "card__start-slot",
      "optional": true,
      "notes": "Figma 🔴TopSlot. Free-content row, fill × hug, left/top; IconContainer by default — the slot is a free Slot instance: ImgContainer (artwork / crop window), a Lottie host, Tag / Button / Squircle / Rating or any other DS element fits, each keeping its own default classes and states. Hiding it closes the stack gap."
    },
    "startText": {
      "class": "card__start-text",
      "notes": "Figma TextContainer. Always present — vertical stack of TextRow instances, gap s4: .card__title-row (Main Body, inherits root colour) + .card__subtitle-row (Compact Body, --text-and-icon-secondary)."
    },
    "endSlot": {
      "class": "card__end-slot",
      "optional": true,
      "notes": "Figma 🔵BottomSlot. Same contract as startSlot, last in the stack. Same free-content contract as startSlot (IconContainer is only the default)."
    },
    "cornerSlot": {
      "class": "card__corner-slot",
      "optional": true,
      "notes": "Figma checkbox prop. s48 × s48 absolute overlay pinned to the top-right corner (top-left in RTL); s8 top/right padding lands the 24px Checkbox s16 from both card edges. Consumes the Checkbox component as .checkbox--no-safezone (since 3.3.22): the slot owns the s48 target and the semantics (role, aria-checked, focus), the instance inside is visual only (aria-hidden). Space toggles, Enter does not."
    }
  },
  "axes": {
    "variant": {
      "title": "Variant",
      "type": "enum",
      "values": [
        "custom-card",
        "service-card",
        "story-card"
      ],
      "default": "custom-card",
      "css": {
        "mechanism": "custom-card = bare .card (all axes open, no data-ds-variant); service-card = .card--service (+ .card--overlap and a direction value); story-card = .card--story (+ .card--overlap, requires a --card-surface override) — variants carry matching data-ds-variant"
      },
      "figma": {
        "kind": "none",
        "notes": "Web-first variants (owner decision 2026-07-29); Figma definitions pending."
      },
      "constraints": [
        "A variant carries its own DATA contract + semantics; a different layout alone is an axis value, and several cards arranged together are a pattern (composition level).",
        "service-card: vertical/service name + transport artwork; locks Compact Body title, radius s24, OnWhite surface, layoutFlow: overlap, CornerSlot off, Subtitle off. Sizes L / M / S (data-ds-size): L = mediaPin bottom (full-width artwork), M = mediaPin corner at row width, S = mediaPin corner in a half-row cell. All vertical; pixel sizes come from the consuming grid.",
        "story-card: promo headline + background medium; locks Compact Body title, radius s24, layoutFlow: overlap + mediaPin: bottom, default 104x156, CornerSlot off, Subtitle off. REQUIRES a non-OnWhite --card-surface per instance (pastel or dark); text/icon contrast is the instance authors duty.",
        "The docs-site navigation cards (.site-nav-card, DS.initNavCards()) are SITE CHROME, not a variant — they were the navCard preset in 3.1.3-3.2.0 and left the contract in 3.3.0."
      ]
    },
    "startSlot": {
      "title": "StartSlot",
      "type": "boolean",
      "default": true,
      "css": {
        "mechanism": "render or omit .card__start-slot — the flex gap closes when the node is absent (no hidden-but-spacing state)"
      },
      "figma": {
        "kind": "variant-property",
        "property": "1st slot"
      }
    },
    "endSlot": {
      "title": "EndSlot",
      "type": "boolean",
      "default": true,
      "css": {
        "mechanism": "render or omit .card__end-slot"
      },
      "figma": {
        "kind": "variant-property",
        "property": "2nd slot"
      },
      "notes": "No skeleton placeholder — the skeleton state renders only the StartSlot icon + the two text bars (card.md §8); EndSlot is omitted there regardless of this axis."
    },
    "cornerSlot": {
      "title": "CornerSlot (Checkbox)",
      "type": "boolean",
      "default": true,
      "css": {
        "mechanism": "render or omit .card__corner-slot; the slot is auto-hidden in the skeleton state (.card--skeleton .card__corner-slot { display: none })"
      },
      "figma": {
        "kind": "variant-property",
        "property": "checkbox"
      }
    },
    "title": {
      "title": "Title",
      "type": "text",
      "default": "Text",
      "constraints": [
        "Always visible — StartText cannot be hidden."
      ],
      "css": {
        "mechanism": ".card__title-row (a TextRow instance) — the wrapper sets Main Body on .text-row__text; colour inherits --text-and-icon-primary from the root"
      },
      "figma": {
        "kind": "component-set",
        "notes": "The •Title row lives inside the TextContainer set (see figma.componentSets) — a text override on the instance, not an axis-value → set mapping."
      }
    },
    "subtitle": {
      "title": "Subtitle position",
      "type": "enum",
      "values": [
        "below",
        "off",
        "above"
      ],
      "default": "below",
      "css": {
        "mechanism": ".card__subtitle-row (a TextRow instance) — Compact Body + --text-and-icon-secondary on .text-row__text; omit the node for `off`, reorder in DOM for `above`"
      },
      "figma": {
        "kind": "variant-property",
        "property": "Subtile",
        "notes": "TextContainer's subtitle-position property (Figma spelling 'Subtile'). Only the value 'Below' was captured from the design context; 'off'/'above' rest on card.md §6 — value names uncaptured."
      }
    },
    "layoutFlow": {
      "title": "Layout flow",
      "type": "enum",
      "values": [
        "stack",
        "overlap"
      ],
      "default": "stack",
      "css": {
        "modifier": ".card--overlap"
      },
      "figma": {
        "kind": "layout",
        "notes": "stack = vertical autolayout; overlap = stacked composition (grid stack on web, StartText on top)."
      },
      "notes": "A mechanism, not a preset — combine with mediaPin for media cards of any purpose (vertical tiles, stories, promos). In overlap the EndSlot is stretched (fill) so media can anchor to the bottom; an instance may set it back to hug (Slot vocabulary) when the slot bounds should follow its content."
    },
    "mediaPin": {
      "title": "Media pin",
      "type": "enum",
      "values": [
        "bottom",
        "corner",
        "end"
      ],
      "default": "bottom",
      "css": {
        "mechanism": "content classes inside the slot (never on the slot container): bottom = .card__artwork.card__artwork--full-bleed (full-width strip flush to the bottom); corner = .card__img-container.card__img-container--full-bleed (s80 crop window bled into the end/bottom corner); end = .card__img-container.card__img-container--pin-end (crop window centred on the end edge). Media knobs (any SP token): --card-media-offset (crop window: shifts the artwork toward the end edge — less of it visible) · --card-media-size (crop window size) · --card-media-shift-y (artwork strip: nudges it vertically, positive = down; the card surface clips the overflow). --mirror-source-x flips the source; --offset-start-s8 is a legacy alias pulling the artwork toward the START edge (negative offset), kept for existing mockups. Full-bleed margins follow --card-padding."
      },
      "figma": {
        "kind": "layout",
        "notes": "Constraints/position of the media inside its slot; crop windows = the rectangle2/* wrappers in the designer frames."
      },
      "constraints": [
        "Meaningful in layoutFlow: overlap — in stack flow media simply fills its slot row."
      ],
      "notes": "Where the media content sits inside its slot (spec §4b)."
    },
    "surface": {
      "title": "Surface",
      "type": "token",
      "tokens": [
        "--surface-on-white"
      ],
      "customizable": "any Surface/*, Background/* or Pastel/* semantic token",
      "default": "--surface-on-white",
      "css": {
        "customProperty": "--card-surface"
      },
      "figma": {
        "kind": "none",
        "notes": "Fill override on the instance (stories use pastel and dark surfaces)."
      },
      "notes": "Openness contract: a default, not a lock. Text/icon colours are the instance author's responsibility when the surface departs from OnWhite (contrast rule)."
    },
    "width": {
      "title": "Width",
      "type": "text",
      "default": "calc(var(--sp-s80) + var(--sp-s80))",
      "customizable": "any SP-token expression (single var(--sp-sN) or a calc() sum of them) — never a raw px value",
      "css": {
        "customProperty": "--card-width"
      },
      "figma": {
        "kind": "layout",
        "notes": "Instance width; the Figma default example is 160."
      },
      "notes": "Instance-level."
    },
    "height": {
      "title": "Height",
      "type": "text",
      "default": "auto",
      "customizable": "auto (hug) or any SP-token expression — never a raw px value",
      "css": {
        "customProperty": "--card-height"
      },
      "figma": {
        "kind": "layout",
        "notes": "hug by default (136 with all slots visible); fixed per instance."
      },
      "notes": "Instance-level."
    },
    "radius": {
      "title": "Corner radius",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-s20",
      "css": {
        "customProperty": "--card-radius"
      },
      "figma": {
        "kind": "layout",
        "notes": "Instance corner radius."
      },
      "notes": "Openness contract (owner decision 2026-07-29): a default, not a lock — any SP token per instance. Ready-made presets lock their own value."
    },
    "padding": {
      "title": "Padding",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-s16",
      "css": {
        "customProperty": "--card-padding"
      },
      "figma": {
        "kind": "layout",
        "notes": "Autolayout padding, all sides."
      },
      "notes": "A default, not a lock. Full-bleed media margins follow this prop automatically."
    },
    "gap": {
      "title": "Stack gap",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-s8",
      "css": {
        "customProperty": "--card-gap"
      },
      "figma": {
        "kind": "layout",
        "notes": "Autolayout item spacing between StartSlot / StartText / EndSlot."
      },
      "notes": "A default, not a lock."
    },
    "textGap": {
      "title": "StartText gap",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-s4",
      "css": {
        "customProperty": "--card-text-gap"
      },
      "figma": {
        "kind": "layout",
        "notes": "TextContainer item spacing (Title ↔ Subtitle)."
      },
      "notes": "A default, not a lock."
    },
    "titleStyle": {
      "title": "Title text style",
      "type": "text",
      "default": "Body/Main Body",
      "customizable": "any DS text style Group/Name — apply all four var(--text-{group}-*) props (family/size/weight/line-height) together on .card__title-row .text-row__text",
      "css": {
        "mechanism": "defaults live on .card__title-row .text-row__text; an instance-scoped rule overrides the full text-style token set (never a partial/numeric override)"
      },
      "figma": {
        "kind": "none",
        "notes": "Text-style override on the •Title instance."
      },
      "notes": "Openness contract: a default, not a lock."
    },
    "subtitleStyle": {
      "title": "Subtitle text style",
      "type": "text",
      "default": "Body/Compact Body",
      "customizable": "any DS text style Group/Name — apply all four var(--text-{group}-*) props together on .card__subtitle-row .text-row__text",
      "css": {
        "mechanism": "defaults live on .card__subtitle-row .text-row__text; instance-scoped full text-style override"
      },
      "figma": {
        "kind": "none",
        "notes": "Text-style override on the ••Subtitle instance."
      },
      "notes": "Openness contract: a default, not a lock."
    },
    "interactive": {
      "title": "Interactive",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".card--interactive"
      },
      "figma": {
        "kind": "none",
        "notes": "Interactivity is a web/runtime concern."
      },
      "notes": "Opts into the catalogued pushItem press (scale 0.95) via --card-push-item-press/-release; both zeroed under prefers-reduced-motion."
    }
  },
  "states": {
    "static": [
      "standard",
      "skeleton"
    ],
    "interactive": [
      "standard",
      "press",
      "skeleton"
    ]
  },
  "constraints": [
    "StartText is the only mandatory part — it cannot be hidden.",
    "Slot containers stay inside the card padding; bleed/crop/offset belongs to the content instance inside a slot, never to the slot container.",
    "layoutFlow: overlap = the .card--overlap grid stack (StartText on top, z-index 1); StartSlot/StartText/EndSlot share the padding area; slot containers never leave the padding — bleed lives on the media content (mediaPin).",
    "CornerSlot is an overlay — it does not participate in the stack and never changes the padding contract; it is the s48 touch target for its 24px Checkbox.",
    "Skeleton state: placeholders re-tint to --surface-overlay (.card--skeleton .skeleton) — Skeleton/OnWhite does not read on the OnWhite surface; CornerSlot hidden; EndSlot has no placeholder and is omitted; gap 0 + space-between with the LOADED card height (--card-height 136) — hugging the placeholders would collapse space-between to zero; the intended geometry assumes exactly two children.",
    "Press motion only via --card-push-item-press/-release (catalogued pushItem, motion-rules §6.1) — cards scale, they do not colour-flash; both props zeroed under prefers-reduced-motion.",
    "All spacing/sizing via SP tokens (var(--sp-sN)); raw px/em/rem/hex prohibited — including every --card-* override.",
    "Openness contract (customCard): radius / padding / gaps / TextRow typography are defaults — instance-overridable with DS tokens; typography overrides always set the full text style (family/size/weight/line-height together). Ready-made presets LOCK their values.",
    "Border is none by default; instances may add 1px solid var(--border-default).",
    "serviceCard / storyCard lock their radius, title style, surface and layoutFlow — for freedom use the customCard base.",
    "Site chrome (.site-nav-card + DS.initNavCards()) lives in shared.css/js under the site- prefix and is NOT part of this contract.",
    "Card is a VERTICAL surface: horizontal compositions (text start, media/control end) belong to Cell. A direction: column|row axis was added and removed in 3.3.0 (owner decision) for this reason."
  ],
  "analytics": {
    "dataDsComponent": "card",
    "actions": [
      "tap",
      "select"
    ],
    "targets": [
      "corner-slot"
    ],
    "valueAttr": "data-ds-value",
    "notes": "data-ds-variant: absent (base customCard) | service-card | story-card. Root tap only when interactive; corner-slot is an independent target when the Checkbox acts on its own. Site navigation cards carry no data-ds-variant (site chrome). serviceCard instances also carry data-ds-size=\"l|m|s\"."
  },
  "rtl": {
    "supported": true,
    "notes": "Default Off. CornerSlot mirrors to the top-left ([dir=\"rtl\"] .card__corner-slot); the stack order is vertical and unchanged; TextRow content follows dir."
  }
}
```
