/* ════════════════════════════════════════════════
   Oymyakon DS 3 · Shared Preview Styles
   Подключается во всех preview/*.html файлах
   ════════════════════════════════════════════════ */

/* ── Fonts ── */
@font-face {
  font-family: 'Suisse Intl';
  src: url('../fonts/SuisseIntl-Book.otf') format('opentype');
  font-weight: 400; font-style: normal;
}
@font-face {
  font-family: 'Suisse Intl';
  src: url('../fonts/SuisseIntl-Regular.otf') format('opentype');
  font-weight: 400; font-style: normal;
}
@font-face {
  font-family: 'Suisse Intl';
  src: url('../fonts/SuisseIntl-SemiBold.otf') format('opentype');
  font-weight: 600; font-style: normal;
}
@font-face {
  font-family: 'Suisse Intl';
  src: url('../fonts/SuisseIntl-Bold.otf') format('opentype');
  font-weight: 700; font-style: normal;
}
@font-face {
  font-family: 'PP Agrandir';
  src: url('../fonts/PPAgrandir-Bold.otf') format('opentype');
  font-weight: 700; font-style: normal;
}

/* ── Tokens: Light ── */
:root {
  --checker-a:          #E6E5DC;
  --checker-b:          #F2F1EB;

  --sidebar-width:      228px;
}

/* ── Tokens: Dark ── */
[data-theme="dark"] {
  --checker-a:          #53534F;
  --checker-b:          #31302E;
}

/* ════════════════════════════════════════════════
   Reset
   ════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  line-height: 1.5; /* reset; individual components use token line-heights */
  background: var(--background-primary);
  color: var(--text-and-icon-primary);
  -webkit-font-smoothing: antialiased;
  transition: background .25s, color .25s;
}

/* ════════════════════════════════════════════════
   Topbar
   ════════════════════════════════════════════════ */
.topbar {
  background: var(--background-always-dark);
  padding: 0 var(--sp-s24);
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; height: var(--sp-s72);
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  border-radius: 0;
  transition: background .25s;
}
.topbar-left { display: flex; align-items: center; gap: 12px; }
.topbar-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; }

.logo-mark {
  width: 26px; height: 26px; border-radius: 7px;
  background: var(--background-brand);
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 800; color: var(--text-and-icon-brand);
  letter-spacing: -.03em; flex-shrink: 0;
}
.logo-name {
  font-family: var(--text-heading-heading2-family);
  font-size: var(--text-heading-heading2-size);
  font-weight: var(--text-heading-heading2-weight);
  line-height: var(--text-heading-heading2-line-height);
  color: var(--text-and-icon-always-light);
  position: relative;
}
.logo-driven {
  position: absolute;
  top: calc(100% - var(--sp-s8));
  right: 0;
  width: auto; height: 16px;
  pointer-events: none;
}
.topbar-divider { width: 1px; height: 20px; background: var(--border-default); }
.topbar-page-name {
  font-size: 13px; font-weight: 500;
  color: var(--text-and-icon-secondary);
  letter-spacing: -.01em;
}

/* Theme toggle — DS Segmented Control, compact topbar size */
.theme-toggle {
  position: relative;
  display: inline-flex;
  height: var(--sp-s36);
  padding: var(--sp-s4);
  border-radius: var(--sp-s20);
  background: var(--surface-on-white);
  gap: var(--sp-s0);
  flex-shrink: 0;
  box-sizing: border-box;
}
.theme-toggle .sc-thumb {
  position: absolute;
  top: var(--sp-s4);
  left: 0;
  height: var(--sp-s28);
  border-radius: var(--sp-s16);
  background: var(--background-inverse-primary);
  box-shadow: var(--shadow-s);
  transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
              width 300ms cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
  will-change: transform;
  z-index: 0;
}
.theme-toggle button {
  position: relative; z-index: 1;
  appearance: none; border: 0; outline: none; background: transparent;
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size);
  font-weight: var(--text-heading-heading4-weight);
  color: var(--double-color-outline);
  padding: 0 var(--sp-s12);
  height: var(--sp-s28);
  border-radius: var(--sp-s16);
  cursor: pointer;
  white-space: nowrap;
  display: flex; align-items: center; gap: var(--sp-s4);
  transition: color 200ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
  box-sizing: border-box;
}
.theme-toggle button svg { width: 16px; height: 16px; flex-shrink: 0; }
.theme-toggle button svg .dc-bg     { fill: var(--double-color-bg); transition: fill 200ms cubic-bezier(0.25, 0.25, 0.75, 0.75); }
.theme-toggle button svg .dc-vector { fill: var(--double-color-outline); transition: fill 200ms cubic-bezier(0.25, 0.25, 0.75, 0.75); }
.theme-toggle button.active { color: var(--double-color-outline-inverse); }
.theme-toggle button.active svg .dc-bg     { fill: var(--double-color-bg-inverse); }
.theme-toggle button.active svg .dc-vector { fill: var(--double-color-outline-inverse); }
@media (prefers-reduced-motion: reduce) {
  .theme-toggle .sc-thumb { transition: none; }
  .theme-toggle button { transition: none; }
}

/* ════════════════════════════════════════════════
   Layout
   ════════════════════════════════════════════════ */
.layout {
  display: flex;
  margin-top: var(--sp-s72);
  min-height: calc(100vh - var(--sp-s72));
}

/* ── Sidebar ── */
.sidebar {
  width: var(--sidebar-width);
  flex-shrink: 0;
  position: fixed;
  top: var(--sp-s72);
  left: 0;
  bottom: 0;
  overflow-y: auto;
  background: var(--background-primary);
  border-radius: 0;
  border-right: 1px solid var(--border-default);
  transition: background .25s;
  z-index: 30;
  padding: 16px 0 40px;
}
.sidebar::-webkit-scrollbar { width: 4px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 4px; }

.sidebar-section { margin-bottom: 0; }
.nav-divider { display: none; }

.nav-root {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; padding: var(--sp-s16) var(--sp-s24) var(--sp-s12) var(--sp-s24);
  background: none; border: none; cursor: pointer;
  font-family: var(--text-heading-heading3-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading3-size);
  font-weight: var(--text-heading-heading3-weight);
  line-height: var(--text-heading-heading3-line-height);
  letter-spacing: normal; text-transform: none;
  color: var(--text-and-icon-disabled);
  text-align: left;
  transition: color .15s;
}
.nav-root:hover { color: var(--text-and-icon-primary); }
.nav-root.nav-root--open { color: var(--text-and-icon-secondary); }
.nav-root.nav-root--active { color: var(--text-and-icon-primary); }

.nav-root-chevron { display: flex; align-items: center; flex-shrink: 0; transition: transform var(--transforming-state-default), opacity var(--transforming-state-fast); opacity: .6; }
.nav-root:hover .nav-root-chevron { opacity: .85; }
/* Rotation tracks the open state only — nav-root--active must not pin the chevron
   down, or collapsing the active page's group would fold the list with no chevron
   animation. Active only keeps the chevron fully opaque. */
.nav-root.nav-root--open .nav-root-chevron { transform: rotate(90deg); }
.nav-root.nav-root--open .nav-root-chevron,
.nav-root.nav-root--active .nav-root-chevron { opacity: 1; }

/* ── nav-group — collapsible list under a nav-root header ──
   Mask pattern (§5.4 — expanding sections unfold like a curtain): the 1fr→0fr grid row is the
   curtain, the inner wrapper clips. Transitions arm only after DS.initSidebar() applied the saved
   state (.sidebar--ready), so restoring a collapsed group on load never plays the fold. Reduced
   motion is handled at the token. */
.nav-group { display: grid; grid-template-rows: 1fr; }
.nav-group--collapsed { grid-template-rows: 0fr; }
.sidebar--ready .nav-group { transition: grid-template-rows var(--pattern-mask-appear); }
.sidebar--ready .nav-group--collapsed { transition: grid-template-rows var(--pattern-mask-hide); }
.nav-group-inner { overflow: hidden; min-height: 0; }

/* ── nav-topic — Cell component (start-slot hidden, end-slot: chevron) ── */
.nav-topic {
  display: flex; align-items: center;
  width: 100%; padding: var(--sp-s16) var(--sp-s24);
  min-height: var(--sp-s48);
  gap: var(--sp-s4);
  background: none; border: none; cursor: pointer;
  text-align: left; text-decoration: none;
  transition: background .1s, color .1s;
  position: relative;
}
.nav-topic:hover { background: var(--surface-on-white); }

.nav-topic .cell-start-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.nav-topic .cell-top-row { display: flex; align-items: center; gap: var(--sp-s4); }
.nav-topic .cell-title {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  color: var(--text-and-icon-disabled);
  transition: color .15s;
}
.nav-topic:hover .cell-title { color: var(--text-and-icon-primary); }
.nav-topic.nav-topic--open .cell-title { color: var(--text-and-icon-secondary); }
.nav-topic.nav-topic--active .cell-title { color: var(--text-and-icon-primary); }

.nav-topic .cell-end-slot { display: flex; align-items: center; gap: var(--sp-s4); flex-shrink: 0; }

/* chevron: shown only on active, no rotation */
.nav-topic-chevron { display: none; }
.nav-topic.nav-topic--active .nav-topic-chevron { display: flex; align-items: center; opacity: 1; color: var(--text-and-icon-primary); }

/* nav-items: hidden */
.nav-items { display: none; }

/* ── nav-item — Cell component layout, main-body font ── */
.nav-item {
  display: flex; align-items: center;
  width: 100%;
  padding: var(--sp-s16) var(--sp-s24);
  min-height: var(--sp-s48);
  gap: var(--sp-s4);
  text-decoration: none;
  background: none; border: none; cursor: pointer;
  text-align: left;
  transition: background .1s, color .1s;
  position: relative;
}
.nav-item:hover { background: var(--surface-on-white); }
.nav-item.nav-item--active { background: var(--surface-on-white); }
.nav-item.nav-item--active::before {
  content: '';
  position: absolute;
  left: 0; top: 4px; bottom: 4px;
  width: 3px; border-radius: 0 3px 3px 0;
  background: var(--background-brand);
}

.nav-item .cell-start-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.nav-item .cell-top-row { display: flex; align-items: center; gap: var(--sp-s4); }

/* main-body — same weight as nav-topic but one step smaller */
.nav-item .cell-title {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-disabled);
  transition: color .1s;
}
.nav-item:hover .cell-title { color: var(--text-and-icon-primary); }
.nav-item.nav-item--active .cell-title { color: var(--text-and-icon-primary); }

.nav-item .cell-end-slot { display: flex; align-items: center; gap: var(--sp-s4); flex-shrink: 0; }

/* ── Main ── */
.main {
  flex: 1;
  margin-left: var(--sidebar-width);
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* ════════════════════════════════════════════════
   Hero
   ════════════════════════════════════════════════ */
.hero {
  background: var(--background-primary);
  margin: 0;
  padding: var(--sp-s24) var(--sp-s32);
  border-radius: 0;
  border: none;
  position: relative;
  transition: background .25s;
}
.hero::after {
  content: '';
  display: block;
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 100vw;
  transform: translateX(-50%);
  height: 1px;
  background: var(--border-default);
  pointer-events: none;
}
.hero-eyebrow {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  color: var(--text-and-icon-secondary);
  margin-bottom: 12px;
}
.hero h1 {
  font-family: var(--text-heading-promo-heading-family), 'Noto Serif', Georgia, serif;
  font-size: var(--text-heading-promo-heading-size);
  font-weight: var(--text-heading-promo-heading-weight);
  line-height: var(--text-heading-promo-heading-line-height);
  color: var(--text-and-icon-primary);
  max-width: 560px;
}
.hero-sub {
  font-family: var(--text-body-promo-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-promo-body-size);
  font-weight: var(--text-body-promo-body-weight);
  line-height: var(--text-body-promo-body-line-height);
  color: var(--text-and-icon-secondary);
  margin-top: 16px;
  max-width: 500px;
}
.hero-meta { margin-top: 20px; display: flex; flex-wrap: wrap; gap: 7px; }
.hero-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px; border-radius: 999px;
  background: var(--surface-on-white);
  border: 1px solid var(--border-default);
  font-size: 11px; font-weight: 500;
  color: var(--text-and-icon-secondary);
  font-family: 'Suisse Intl', 'Noto Sans', sans-serif;
  transition: background .25s, border-color .25s, color .25s;
}
.hero-badge-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--background-brand); }

/* ════════════════════════════════════════════════
   Content
   ════════════════════════════════════════════════ */
.hero,
.content { max-width: 960px; width: 100%; }
.page-sticky-bar { width: 100%; }
.page-sticky-bar.sticky { width: auto; }
.content { margin: 0; padding: var(--sp-s32) var(--sp-s32) 80px; }

/* ── One left edge: the s32 gutter has exactly one owner ──
   `.content` is the content column and owns the gutter, the max-width and
   the page tail. Every spec page wraps its sections in it (normalized
   2026-07-31), so the section content lands on the same left edge as the
   cover, which the spec-cover include wraps in its own `.content`.

   The s32 the blocks carry themselves is a fallback for a block dropped
   straight into `.main`; inside `.content` it is dropped so the gutter is
   never paid twice. See the component-authoring rule § "One left edge". */
.content .spec-wide,
.content .spec-column {
  padding-left: var(--sp-s0);
  padding-right: var(--sp-s0);
}

/* ── Section / group card ── */
section.group { margin-bottom: var(--sp-s0); scroll-margin-top: var(--sticky-bar-height, 120px); }

.group-card {
  background: var(--background-primary);
  border: none;
  border-radius: 0;
  transition: background .25s;
}
.group-card > :last-child:not(table) { padding-bottom: var(--sp-s24); }
.group-head {
  padding: var(--sp-s24) var(--sp-s0);
  border-bottom: none;
  background-color: var(--background-primary);
  background-image: repeating-linear-gradient(to right, var(--border-default) 0px, var(--border-default) 2px, transparent 2px, transparent 7px);
  background-repeat: repeat-x;
  background-size: 7px 2px;
  background-position: bottom;
  display: flex; align-items: baseline; gap: 10px;
  position: sticky;
  top: calc(var(--sticky-bar-height, 0px) + var(--sp-s0));
  z-index: 10;
  border-radius: 0;
  transition: background-color .25s;
}
.group-title {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  letter-spacing: -.01em;
}
.group-count {
  font-size: 11px; font-weight: 500;
  color: var(--text-and-icon-disabled);
  font-family: 'Suisse Intl', 'Noto Sans', sans-serif;
}
.group-desc {
  padding: var(--sp-s24) var(--sp-s0);
  font-family: var(--text-body-body-paragraph-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-body-paragraph-size);
  line-height: var(--text-body-body-paragraph-line-height);
  color: var(--text-and-icon-secondary);
  border-bottom: 1px solid var(--border-default);
}
.group-rules {
  padding: var(--sp-s24) var(--sp-s0);
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-secondary);
  background: var(--surface-on-white);
  border-bottom: 1px solid var(--border-default);
  transition: background .25s;
}
.group-rules strong { color: var(--text-and-icon-primary); font-weight: 600; }
.group-rules code {
  background: var(--background-base-grey); color: var(--text-and-icon-secondary);
  padding: 1px 5px; border-radius: 4px; font-size: 11px;
  font-family: 'Suisse Intl', 'Noto Sans', sans-serif;
}
.group-rules ul { padding-left: 16px; }
.group-rules li { margin-bottom: 2px; }

/* ── Section label ── */
.section-label {
  font-family: var(--text-heading-heading2-family), 'Noto Serif', Georgia, serif;
  font-size: var(--text-heading-heading2-size);
  font-weight: var(--text-heading-heading2-weight);
  line-height: var(--text-heading-heading2-line-height);
  color: var(--text-and-icon-primary);
  margin-top: var(--sp-s20); margin-bottom: var(--sp-s12);
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border-default);
}

/* ════════════════════════════════════════════════
   Footer
   ════════════════════════════════════════════════ */
.site-footer {
  background: var(--background-always-graphite);
  padding: 20px 40px;
  border-top: 1px solid rgba(255,255,255,.06);
  margin-left: var(--sidebar-width);
}
.site-footer-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
.footer-text {
  font-family: var(--text-caption-caption-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size);
  line-height: var(--text-caption-caption-line-height);
  color: var(--text-and-icon-graphite);
}
.footer-text code {
  background: rgba(255,255,255,.08); color: rgba(255,255,255,.65);
  padding: 1px 5px; border-radius: 4px; font-size: 11px;
  font-family: 'Suisse Intl', 'Noto Sans', sans-serif;
}
.footer-hint {
  font-size: 11px; color: var(--background-brand); opacity: .7;
  font-family: 'Suisse Intl', 'Noto Sans', sans-serif;
}

/* ════════════════════════════════════════════════
   Page sticky bar (universal — all pages)
   ════════════════════════════════════════════════ */
.page-sticky-bar {
  display: flex;
  flex-direction: column;
  background: var(--background-primary);
  border: none;
  border-bottom: 1px solid var(--border-default);
  border-radius: 0;
  padding: 0;
  box-shadow: none;
  margin: 0;
  transition: background .25s;
}

/* Inner content wrapper — centres tabs/chips within the 960px content field */
.sticky-bar-inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 960px;
  margin: 0 auto;
  padding: var(--sp-s12) var(--sp-s32) 0;
}
.page-sticky-bar.sticky {
  position: fixed;
  top: var(--sp-s72);
  left: var(--sidebar-width, 228px);
  right: 0;
  width: auto;
  max-width: none;
  margin: 0;
  z-index: 20;
  box-shadow: var(--shadow-s);
}
/* tabs-only variant */
.page-sticky-bar.tabs-only .sticky-bar-inner {
  padding: var(--sp-s16) var(--sp-s32);
}
.page-sticky-bar.tabs-only .sticky-tabs {
  padding-bottom: 0;
}
.page-sticky-bar.tabs-only .sticky-tab.active::after {
  bottom: calc(-1 * var(--sp-s16) - 1px);
  border-radius: 0;
}

/* tabs-with-controls variant */
.page-sticky-bar.tabs-with-controls .sticky-bar-inner {
  padding: var(--sp-s16) var(--sp-s32) 0;
}
.page-sticky-bar.tabs-with-controls .sticky-tabs {
  padding-bottom: var(--sp-s16);
}
.page-sticky-bar.tabs-with-controls .sticky-tab.active::after {
  bottom: calc(-1 * var(--sp-s16) - 1px);
  border-radius: 0;
}
.page-sticky-bar.tabs-with-controls .sticky-sp-mode {
  padding: var(--sp-s16) 0 var(--sp-s16);
}

/* chips-only variant */
.page-sticky-bar.chips-only .sticky-bar-inner {
  padding: var(--sp-s16) var(--sp-s32);
}
.page-sticky-bar.chips-only .sticky-chips {
  padding: 0;
}

/* Tab row */
.sticky-tabs {
  display: flex;
  gap: var(--sp-s16);
  padding-bottom: var(--sp-s12);
  position: relative;
}
.sticky-tab {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-disabled);
  cursor: pointer;
  background: none; border: none; padding: 0;
  position: relative;
  transition: color .15s;
  white-space: nowrap;
}
.sticky-tab:hover { color: var(--text-and-icon-secondary); }
.sticky-tab.active { color: var(--text-and-icon-primary); }
.sticky-tab.active::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  bottom: calc(-1 * var(--sp-s12) - 1px);
  height: 2px;
  background: var(--text-and-icon-primary);
  border-radius: 1px 1px 0 0;
}

/* Divider between tabs and chips */
.sticky-divider {
  height: 1px;
  background: var(--border-default);
  margin: 0 calc(-1 * var(--sp-s16));
}

/* Chip row */
.sticky-chips {
  display: flex;
  align-items: center;
  gap: var(--sp-s8);
  flex-wrap: wrap;
  padding: var(--sp-s16) 0;
}
.prim-chip {
  display: inline-flex; align-items: center;
  padding: var(--sp-s4) var(--sp-s12);
  border-radius: 999px;
  border: 1px solid var(--border-default);
  background: var(--surface-on-white);
  font-family: var(--text-caption-caption-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size);
  font-weight: var(--text-caption-caption-weight);
  line-height: var(--text-caption-caption-line-height);
  color: var(--text-and-icon-secondary);
  cursor: pointer;
  text-decoration: none;
  transition: background .12s, color .12s, border-color .12s;
  white-space: nowrap;
}
.prim-chip:hover {
  background: var(--surface-on-grey);
  color: var(--text-and-icon-primary);
  border-color: var(--border-active);
}

/* ════════════════════════════════════════════════
   View panels (for multi-view pages)
   ════════════════════════════════════════════════ */
.view-panel { display: none; }
.view-panel.view-panel--active { display: block; }

/* ════════════════════════════════════════════════
   Segmented Control component
   ════════════════════════════════════════════════ */

:root {
  --sc-thumb-motion: 300ms cubic-bezier(0.4, 0, 0.2, 1);
  --sc-color-motion: 200ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
}
@media (prefers-reduced-motion: reduce) {
  :root {
    --sc-thumb-motion: 0ms linear;
    --sc-color-motion: 0ms linear;
  }
}

.segmented-control {
  position: relative;
  display: flex;
  height: var(--sp-s56);
  padding: var(--sp-s4);
  border-radius: var(--sp-s20);
  background: var(--surface-on-white);
  width: 100%;
  box-sizing: border-box;
  gap: var(--sp-s0);
}

.sc-thumb {
  position: absolute;
  top: var(--sp-s4);
  left: 0;
  height: var(--sp-s48);
  border-radius: var(--sp-s16);
  background: var(--background-inverse-primary);
  box-shadow: var(--shadow-s);
  transition: transform var(--sc-thumb-motion), width var(--sc-thumb-motion);
  pointer-events: none;
  will-change: transform;
  z-index: 0;
}

.sc-segment {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-s4);
  height: var(--sp-s48);
  padding: var(--sp-s10) var(--sp-s12);
  border-radius: var(--sp-s16);
  border: none;
  background: transparent;
  cursor: pointer;
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-primary);
  transition: color var(--sc-color-motion);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
}

.sc-segment--active {
  color: var(--text-and-icon-brand);
}

.sc-segment--disabled {
  color: var(--text-and-icon-disabled);
  pointer-events: none;
}

.sc-segment svg {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
}

.sc-segment svg .dc-bg,
.sc-segment svg .dc-vector {
  transition: fill var(--sc-color-motion);
}
.sc-segment--active svg .dc-bg     { fill: var(--double-color-bg-inverse); }
.sc-segment--active svg .dc-vector { fill: var(--double-color-outline-inverse); }
.sc-segment:not(.sc-segment--active) svg .dc-bg     { fill: var(--double-color-bg); }
.sc-segment:not(.sc-segment--active) svg .dc-vector { fill: var(--double-color-outline); }

/* ════════════════════════════════════════════════
   Indicator component
   ════════════════════════════════════════════════ */

/* [Indicator] 3.1 — a non-interactive status marker: Simple (dot) or Number
   (count pill), sizes M / L. The three --indicator-* custom properties are the
   colour contract; the fill is Accent/Red 2 on BOTH sizes. Sizing tokens cap
   at ×130% (the SP mode carries it); motion tokens come from motion.css, which
   also zeroes them under reduced motion. */
.indicator {
  --indicator-fill: var(--accent-red2);
  --indicator-content: var(--text-and-icon-always-light);
  --indicator-border: var(--background-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--sp-s32);
  background: var(--indicator-fill);
  color: var(--indicator-content);
  box-sizing: border-box;
  flex-shrink: 0;
  transition: transform var(--component-scale-s-appear), opacity var(--component-scale-s-appear);
}

/* Border — OFF by default (Figma Border=No). An OUTSIDE 2dp ring drawn as a
   spread shadow: it never moves the layout and never shrinks the fill. */
.indicator--border { box-shadow: 0 0 0 var(--sp-s2) var(--indicator-border); }

/* ── Simple type — a bare dot ── */
.indicator--simple.indicator--m {
  width: var(--sp-s8);
  height: var(--sp-s8);
}
.indicator--simple.indicator--l {
  width: var(--sp-s12);
  height: var(--sp-s12);
}

/* ── Number type — a count pill: minimum square, grows with the count ── */
.indicator--number.indicator--m {
  min-width: var(--sp-s16);
  height: var(--sp-s16);
  padding: var(--sp-s0) var(--sp-s4);
  font-family: var(--text-caption-caption-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size);
  font-weight: var(--text-caption-caption-weight);
  line-height: var(--text-caption-caption-line-height);
}
.indicator--number.indicator--l {
  min-width: var(--sp-s24);
  height: var(--sp-s24);
  padding: var(--sp-s0) var(--sp-s6);
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}

/* ── Count change — the numericText roll (iOS 16 parity) ──
   When a composition animates the count, the value sits in an optional
   .indicator__count wrapper: the outgoing value rolls up and out, the
   incoming one rolls in from below (the JS swaps the text between the two
   phases). Only transform and opacity animate — the pill's width re-hugs the
   new count instantly; the root clips the travel. Transforming/State/Fast,
   zeroed under reduced motion at the token. */
.indicator--number { overflow: hidden; }
.indicator__count {
  display: inline-block;
  transition: transform var(--transforming-state-fast), opacity var(--transforming-state-fast);
}
.indicator--count-out .indicator__count {
  transform: translateY(-100%);
  opacity: 0;
}
.indicator--count-in .indicator__count {
  transform: translateY(100%);
  opacity: 0;
  transition: none;
}
/* The width glides on the same token when digits come or go — the composition
   pins the measured from/to widths (FLIP) while the new value rolls in, the
   way SwiftUI animates the frame alongside numericText. A recorded deviation
   from the compositor-only guidance: a badge-sized layout tween. */
.indicator--count-resizing {
  transition: width var(--transforming-state-fast), min-width var(--transforming-state-fast);
}

/* ── Appear / hide — scaleS; count 0 renders nothing ── */
.indicator--hidden {
  display: none;
}
.indicator--entering {
  transform: scale(0);
  opacity: 0;
}
.indicator--visible {
  transform: scale(1);
  opacity: 1;
}
.indicator--exiting {
  transform: scale(0);
  opacity: 0;
  transition: transform var(--component-scale-s-hide), opacity var(--component-scale-s-hide);
}

/* ════════════════════════════════════════════════
   Pointer — annotation component for spec pages
   indicator--l + 1px line (s32) + 4px dot
   Directions: right (default), left, up, down
   ════════════════════════════════════════════════ */

.pointer {
  display: inline-flex;
  align-items: center;
  gap: 0;
  --pointer-line-length: var(--sp-s32);
  --pointer-color: var(--border-inform);
}

/* Indicator colors */
.pointer .indicator {
  background: var(--pointer-color);
  color: var(--text-and-icon-always-light);
  border: none;
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}

/* Line */
.pointer__line {
  background: var(--pointer-color);
  flex-shrink: 0;
}

/* Dot */
.pointer__dot {
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: var(--pointer-color);
  flex-shrink: 0;
}

/* ── right (default) ── indicator left-outer, dot right-inner touching element ── */
/* HTML order: indicator → line → dot. row-reverse → dot, line, indicator (left-to-right visual) */
.pointer,
.pointer--right {
  flex-direction: row-reverse;
  align-items: center;
}
.pointer .pointer__line,
.pointer--right .pointer__line {
  width: var(--pointer-line-length);
  height: 1px;
}

/* ── left ── indicator right-outer, dot left-inner touching element ── */
/* HTML order: indicator → line → dot. row (normal) → indicator, line, dot (left-to-right visual) */
.pointer--left {
  flex-direction: row;
  align-items: center;
}
.pointer--left .pointer__line {
  width: var(--pointer-line-length);
  height: 1px;
}

/* ── down ── dot top-inner (touches element above), indicator bottom-outer ── */
/* HTML order: indicator → line → dot. column-reverse → dot(top), line, indicator(bottom) ✓ */
.pointer--down {
  flex-direction: column-reverse;
  align-items: center;
}
.pointer--down .pointer__line {
  width: 1px;
  height: var(--pointer-line-length);
}

/* ── up ── indicator top-outer, dot bottom-inner (touches element below) ── */
/* HTML order: indicator → line → dot. column (normal) → indicator(top), line, dot(bottom) ✓ */
.pointer--up {
  flex-direction: column;
  align-items: center;
}
.pointer--up .pointer__line {
  width: 1px;
  height: var(--pointer-line-length);
}

/* ════════════════════════════════════════════════
   Tag component
   ════════════════════════════════════════════════ */
/* [Tag] 3.1 geometry — the StandardTag model: uniform s4 padding, zero gap,
   the label carries its own s2 side padding (Figma's TextContainer). Text
   therefore sits s6 from a bare edge and s2 from an icon with no per-side
   switches, and height hugs content: s16 + 2×s4 = 24 at 100%.
   The three --tag-* custom properties ARE the CustomTag contract: every
   Standard style below only re-points them, and a Custom instance re-points
   them inline. */
.tag {
  --tag-fill: var(--background-secondary);
  --tag-content: var(--text-and-icon-primary);
  --tag-border: var(--border-default);
  display: inline-flex;
  align-items: center;
  padding: var(--sp-s4);
  gap: var(--sp-s0);
  border-radius: var(--sp-s10);
  background: var(--tag-fill);
  color: var(--tag-content);
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  white-space: nowrap;
  box-sizing: border-box;
  flex-shrink: 0;
}

/* Border — OFF by default (the Figma Border layer). An inset shadow, not a
   CSS border: the stroke sits inside the box and never moves the layout.
   Always 1dp — the one value that does not scale with the SP mode. */
.tag--border { box-shadow: inset 0 0 0 1px var(--tag-border); }

.tag__slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s16);
  height: var(--sp-s16);
  flex-shrink: 0;
}
.tag__slot svg { display: block; width: 100%; height: 100%; }
/* Position markers — same geometry on both sides; the suffix names the slot
   for markup, a11y and analytics, and RTL flips them with the flex row. */
.tag__slot--start,
.tag__slot--end { flex-shrink: 0; }
.tag__label { padding: 0 var(--sp-s2); flex-shrink: 0; }

/* Style axis (StandardTag). Surface is the base .tag itself — the explicit
   class restates the defaults so every Style value has its modifier. */
.tag--surface  { --tag-fill: var(--background-secondary);        --tag-content: var(--text-and-icon-primary);         --tag-border: var(--border-default); }
.tag--brand    { --tag-fill: var(--background-brand);            --tag-content: var(--text-and-icon-always-dark);     --tag-border: var(--surface-overlay); }
.tag--color    { --tag-fill: var(--pastel-drive-green2);         --tag-content: var(--text-and-icon-primary);         --tag-border: var(--surface-overlay); }
.tag--inverse  { --tag-fill: var(--background-inverse-primary);  --tag-content: var(--text-and-icon-inverse-primary); --tag-border: var(--surface-white-overlay); }
.tag--disabled { --tag-fill: var(--surface-white-overlay);       --tag-content: var(--text-and-icon-disabled);        --tag-border: var(--surface-overlay); }

/* Skeleton — an empty fixed s56×s24 box: no label, no slots, no border. */
.tag--skeleton {
  width: var(--sp-s56);
  height: var(--sp-s24);
  padding: var(--sp-s0);
  --tag-fill: var(--skeleton-on-white);
  position: relative;
  overflow: hidden;
}
.tag--skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0%, var(--skeleton-wave) 50%, transparent 100%);
  animation: var(--pattern-shimmer);
}

/* ════════════════════════════════════════════════
   Cell — DS component
   ════════════════════════════════════════════════ */
.cell {
  display: flex; flex-direction: column;
  border-bottom: 1px solid var(--border-default);
  transition: background-color var(--component-push-highlight);
  cursor: pointer; position: relative;
  background: var(--background-primary);
  width: 100%; box-sizing: border-box;
  border-left: none; border-right: none; border-top: none; border-radius: 0;
  text-align: left;
  color: var(--text-and-icon-primary);
}
.cell-top-content {
  display: flex; flex-direction: row; align-items: flex-start;
  padding: var(--sp-s10) var(--sp-s16);
  min-height: var(--sp-s48);
  gap: 0;
  width: 100%; box-sizing: border-box;
}
/* Every gap between slots inside topContainer is s8 */
.cell-top-content .cell-start-text { margin-left: var(--sp-s8); }
.cell-top-content .cell-end-text { margin-left: var(--sp-s8); }
.cell-top-content .cell-end-slot { margin-left: var(--sp-s8); }
/* when no start-slot: no left margin on start-text */
.cell--simple .cell-top-content .cell-start-text { margin-left: 0; }
.cell-description {
  display: none;
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-secondary);
}
.cell-description.is-visible {
  display: block;
}
.cell-description__divider {
  height: 1px;
  background: var(--border-default);
  margin: 0 var(--sp-s16) var(--sp-s8);
}
.cell-description__text {
  padding: 0 var(--sp-s16) var(--sp-s10);
}
.cell:last-child { border-bottom: none; }
/* hover intentionally omitted — hover behaviour is platform-specific (iOS highlight / Android ripple) */
.cell:active, .cell.is-pressed { background: var(--surface-on-grey); }
.cell.is-disabled {
  pointer-events: none;
  color: var(--text-and-icon-disabled);
}
.cell.is-disabled .cell-subtitle,
.cell.is-disabled .cell-additional,
.cell.is-disabled .cell-description {
  color: var(--text-and-icon-disabled);
}
.cell.is-selected {
  background: var(--pastel-drive-green1);
  transition: background-color var(--transforming-state-default);
}

.cell-list {
  display: flex; flex-direction: column;
  border-radius: var(--sp-s12);
  overflow: hidden;
  border: 1px solid var(--border-default);
  width: 360px;
}

/* Card-wrap pattern — wraps any Cell preset to look like a button */
.cell-card-wrap {
  border-radius: var(--sp-s16);
  overflow: hidden;
  margin: 0 var(--sp-s16);
  background: var(--background-secondary);
}

/* Slots */
/* Simple-cell predefined preset */
/* Slot heights are the base contract (min s36, hug above it) — a preset never
   lowers or caps them. The old per-preset min/max pairs are gone: Simple-cell
   floored its end-slot at s28, below the base, and Icon-cell merely restated
   s36 while capping hug at s40. */

/* Icon-cell predefined preset */

/* Squircle-cell predefined preset — slot tracks the Squircle size exactly
   (Figma [SquircleCell] 3.4: S 40 / M 48 / L 56; M locks vertical padding
   to s8 so the two-row cell lands on 64, S/M/L heights 60/64/76) */
.cell--squircle-s .cell-start-slot {
  width: var(--sp-s40); height: var(--sp-s40);
}
.cell--squircle-m .cell-start-slot {
  width: var(--sp-s48); height: var(--sp-s48);
}
.cell--squircle-m .cell-top-content {
  padding-top: var(--sp-s8); padding-bottom: var(--sp-s8);
}
.cell--squircle-l .cell-start-slot {
  width: var(--sp-s56); height: var(--sp-s56);
}

/* Squircle component — [Squircle] 3.4
   The superellipse container: avatar, category icon, text badge. Five sizes
   as separate Figma sets behind the wrapper's Size swap; the web approximates
   the 60%-smoothing superellipse with the plain corner radius — a recorded
   platform deviation (iOS draws continuous corners natively). The three
   --squircle-* custom properties are the colour contract; the base IS the
   Default state and the Custom-Squircle geometry (= the M box). */
.squircle {
  --squircle-fill: var(--surface-on-white);
  --squircle-content: var(--text-and-icon-primary);
  --squircle-border: var(--border-default);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  width: var(--sp-s48); height: var(--sp-s48);
  padding: var(--sp-s0);
  border-radius: var(--sp-s16);
  background: var(--squircle-fill);
  color: var(--squircle-content);
  overflow: hidden; box-sizing: border-box;
}
.squircle__slot {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%;
}
.squircle__slot img { width: 100%; height: 100%; object-fit: cover; }

/* Border — OFF by default (the hidden Figma Border layer, 1dp INSIDE).
   Only the Custom-Squircle set defaults it ON. Inset shadow: no layout
   shift, and 1dp never scales with the SP mode. */
.squircle--border { box-shadow: inset 0 0 0 1px var(--squircle-border); }

/* Sizes — box + locked radius + the size's own text style (family + size +
   weight + line-height together). Scaling gradient per the Figma bindings:
   mini/xs full ×150 · s/m cap ×130 · l fixed. */
.squircle--mini {
  width: var(--sp-s24); height: var(--sp-s24); border-radius: var(--sp-s8);
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}
.squircle--xs {
  width: var(--sp-s32); height: var(--sp-s32); border-radius: var(--sp-s12);
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}
.squircle--s {
  width: var(--sp-s40); height: var(--sp-s40); border-radius: var(--sp-s16);
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
}
.squircle--m {
  width: var(--sp-s48); height: var(--sp-s48); border-radius: var(--sp-s16);
  font-family: var(--text-body-promo-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-promo-body-size);
  font-weight: var(--text-body-promo-body-weight);
  line-height: var(--text-body-promo-body-line-height);
}
.squircle--l {
  width: var(--sp-s56); height: var(--sp-s56); border-radius: var(--sp-s20);
  font-family: var(--text-heading-heading2-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading2-size);
  font-weight: var(--text-heading-heading2-weight);
  line-height: var(--text-heading-heading2-line-height);
}

/* State axis — Standard (Figma names it Default) is the base .squircle
   itself; the explicit class restates the defaults so every State value has
   its modifier. */
.squircle--standard {
  --squircle-fill: var(--surface-on-white);
  --squircle-content: var(--text-and-icon-primary);
  --squircle-border: var(--border-default);
}
.squircle--inverse {
  --squircle-fill: var(--background-inverse-primary);
  --squircle-content: var(--text-and-icon-inverse-primary);
}
.squircle--color {
  --squircle-fill: var(--pastel-blue1);
  --squircle-content: var(--accent-blue1);
}
.squircle--disabled {
  --squircle-content: var(--text-and-icon-disabled);
}
/* The Figma Img treatment for Disabled: the picture desaturates through the
   luminosity blend AND dims to 0.3 (layer blendMode + opacity on the image
   in every size set) — the one sanctioned opacity use, image content has no
   token to re-point. */
.squircle--disabled .squircle__slot img { mix-blend-mode: luminosity; opacity: 0.3; }

/* Skeleton — the fill swaps and the content hides; the DS shimmer sweeps. */
.squircle--skeleton {
  --squircle-fill: var(--skeleton-on-white);
  position: relative;
}
.squircle--skeleton .squircle__slot { display: none; }
.squircle--skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0%, var(--skeleton-wave) 50%, transparent 100%);
  animation: var(--pattern-shimmer);
}

/* Text type — the label carries its own padding (the Figma Text part):
   s4 vertical / s2 horizontal on every size except L, which pads s4 all
   round. Single line; the size's text style comes from the size class. */
.squircle--text .squircle__slot { padding: var(--sp-s4) var(--sp-s2); }
.squircle--l.squircle--text .squircle__slot { padding: var(--sp-s4); }

/* Img type — the picture is the surface: no fill of its own. */
.squircle--img { --squircle-fill: transparent; }

.cell-start-slot {
  width: var(--sp-s40); min-height: var(--sp-s36); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-and-icon-primary);
}
.cell-start-slot--box {
  border-radius: var(--sp-s8);
  background: var(--surface-on-white);
  border: 1px solid var(--border-default);
  color: var(--text-and-icon-secondary);
}
.cell-start-text {
  flex: 1; min-width: 0; min-height: var(--sp-s36);
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
}
/* TextRow — §8 text-row.md */
.text-row { display: flex; flex-direction: row; align-items: center; width: 100%; gap: var(--sp-s4); }
.text-row__text { flex: 1 1 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.text-row--multiline .text-row__text { white-space: normal; overflow: visible; text-overflow: unset; }
.text-row__slot-start-1, .text-row__slot-start-2, .text-row__slot-end-1, .text-row__slot-end-2 { display: none; flex-shrink: 0; width: var(--sp-s24); height: var(--sp-s24); align-items: center; justify-content: center; }
.text-row--expanded .text-row__slot-start-1, .text-row--expanded .text-row__slot-start-2,
.text-row--expanded .text-row__slot-end-1, .text-row--expanded .text-row__slot-end-2 { display: flex; }
.text-row--expanded .text-row__text { flex: 0 0 auto; min-width: 0; }
.text-row--expanded.text-row--multiline .text-row__text { flex: 1 1 0; }
.text-row--multiline .text-row__slot-end-1, .text-row--multiline .text-row__slot-end-2 { display: none; }
.cell-top-row { display: flex; align-items: center; gap: var(--sp-s4); }
.cell-title {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cell-subtitle {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-secondary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cell-additional {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-secondary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cell-end-slot {
  min-height: var(--sp-s36); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; gap: var(--sp-s8);
}
.cell-value {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
}
.cell-end-text {
  flex-shrink: 0; min-height: var(--sp-s36);
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  text-align: right;
}

/* ════════════════════════════════════════════════
   Spec Cover — pattern 2 (cover block for component spec pages)
   ════════════════════════════════════════════════ */

/* Outer wrapper — 960px, grid MD (8 col, 24px gutter, 32px margin) */
.spec-cover {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-s24);
  width: 100%;
  margin: var(--sp-s32) 0;
}

/* Left block — fixed width, info */
.spec-cover__info {
  grid-column: 1 / 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-s16);
}

/* Figma version row */
.spec-cover__figma-row {
  display: flex;
  align-items: center;
  gap: var(--sp-s6);
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-secondary);
}
.spec-cover__figma-row svg {
  flex-shrink: 0;
  color: var(--text-and-icon-secondary);
}

/* Component name */
.spec-cover__name {
  font-family: var(--text-heading-promo-heading-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-promo-heading-size);
  font-weight: var(--text-heading-promo-heading-weight);
  line-height: var(--text-heading-promo-heading-line-height);
  color: var(--text-and-icon-primary);
  letter-spacing: -.02em;
}

/* Description */
.spec-cover__desc {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  color: var(--text-and-icon-primary);
}

/* Platform tags row */
.spec-cover__platforms {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-s8);
}

/* Single platform tag */
.spec-cover__platform {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-s6);
  height: var(--sp-s28);
  padding: var(--sp-s0) var(--sp-s10);
  border-radius: 999px;
  border: 1px solid var(--border-default);
  background: var(--background-primary);
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: 1;
  color: var(--text-and-icon-secondary);
  white-space: nowrap;
}
.spec-cover__platform svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--text-and-icon-secondary);
}
.spec-cover__platform--na {
  color: var(--text-and-icon-disabled);
  border-color: var(--border-default);
}

/* Right block — stretches to right edge of content */
.spec-cover__canvas {
  grid-column: 2 / 3;
  height: 432px;
  border-radius: var(--sp-s24);
  border: 1px solid var(--border-default);
  background-color: var(--background-primary);
  background-image:
    repeating-conic-gradient(var(--checker-a) 0% 25%, var(--checker-b) 0% 50%);
  background-size: 10px 10px;
  background-position: 0 0;
  /* 24% opacity on the checkerboard pattern via mix */
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Overlay that makes bg-primary show through at 76% to give 24% checker visibility */
.spec-cover__canvas::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--background-primary);
  opacity: 0.76;
  border-radius: inherit;
  pointer-events: none;
}
.spec-cover__canvas-content {
  /* No z-index: relative alone keeps the content above the positioned
     ::before wash by tree order WITHOUT a stacking context — a z-index here
     isolates mix-blend-mode content (squircle disabled Img) from the
     checkerboard backdrop and silently mutes the blend. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

/* ════════════════════════════════════════════════
   spec-wide — Full-width canvas content block (Pattern 3)
   ════════════════════════════════════════════════ */

.spec-wide {
  display: flex;
  flex-direction: column;
  max-width: 960px;
  width: 100%;
  margin-top: var(--sp-s48);
  padding: 0 var(--sp-s32) var(--sp-s32);
}

.spec-wide__heading + .spec-wide__text {
  margin-top: var(--sp-s16);
}

/* Subtitle — same Heading 4 contract as .spec-column__subtitle, for a
   section that covers several subjects one after another. */
.spec-wide__subtitle {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-primary);
  margin: 0;
}
.spec-wide__heading + .spec-wide__subtitle { margin-top: var(--sp-s24); }
.spec-wide__subtitle + .spec-wide__text    { margin-top: var(--sp-s8); }
.spec-wide__subtitle + .spec-wide__canvas  { margin-top: var(--sp-s16); }

.spec-wide__heading + .spec-wide__points,
.spec-wide__text + .spec-wide__points,
.spec-wide__heading + .spec-wide__canvas,
.spec-wide__text + .spec-wide__canvas,
.spec-wide__points + .spec-wide__canvas,
.spec-wide__canvas + .spec-wide__text,
.spec-wide__canvas + .spec-wide__points {
  margin-top: var(--sp-s24);
}

.spec-wide__heading {
  font-family: var(--text-heading-heading1-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading1-size);
  font-weight: var(--text-heading-heading1-weight);
  line-height: var(--text-heading-heading1-line-height);
  color: var(--text-and-icon-primary);
  margin: 0;
  max-width: 714px;
}

.spec-wide__text {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  color: var(--text-and-icon-primary);
  margin: 0;
  max-width: 714px;
}

.spec-wide__points {
  display: flex;
  flex-direction: column;
  gap: var(--sp-s16);
  margin: 0;
  padding: 0;
  list-style: none;
  max-width: 714px;
}

.spec-wide__point {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-s16);
}

/* Annotation heads only: a REAL Indicator instance in a canvas carries
   data-ds-component and keeps its own component colours. */
.spec-wide__point .indicator,
.spec-wide__canvas .indicator--number:not([data-ds-component]) {
  background: var(--border-inform);
  color: var(--text-and-icon-always-light);
  border: none;
  flex-shrink: 0;
}

/* Full-width canvas — same checkerboard as spec-cover */
.spec-wide__canvas--spaced {
  margin-top: var(--sp-s24);
}

.spec-wide__canvas {
  width: 100%;
  height: 432px;
  border-radius: var(--sp-s24);
  border: 1px solid var(--border-default);
  background-color: var(--background-primary);
  background-image: repeating-conic-gradient(var(--checker-a) 0% 25%, var(--checker-b) 0% 50%);
  background-size: 10px 10px;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.spec-wide__canvas::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--background-primary);
  opacity: 0.76;
  border-radius: inherit;
  pointer-events: none;
}

.spec-wide__canvas-content {
  /* No z-index: relative alone keeps the content above the positioned
     ::before wash by tree order WITHOUT a stacking context — a z-index here
     isolates mix-blend-mode content (squircle disabled Img) from the
     checkerboard backdrop and silently mutes the blend. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

/* ════════════════════════════════════════════════
   spec-column — Content block (Pattern 4)
   Heading + optional text (full width),
   then two-column row: canvas 468px left, points right
   ════════════════════════════════════════════════ */

.spec-column {
  display: flex;
  flex-direction: column;
  max-width: 960px;
  width: 100%;
  margin-top: var(--sp-s48);
  padding: 0 var(--sp-s32) var(--sp-s32);
  scroll-margin-top: var(--sticky-bar-height, 120px);   /* anchor clears the sticky bar */
}

.spec-column__heading + .spec-column__text {
  margin-top: var(--sp-s16);
}

.spec-column__heading + .spec-column__row,
.spec-column__text + .spec-column__row,
.spec-column__heading + .spec-column__subtitle,
.spec-column__text + .spec-column__subtitle,
.spec-column__wide-canvas + .spec-column__subtitle {
  margin-top: var(--sp-s48);
}
/* h4 heading sits closer to its block than the default h1 heading */
.spec-column__heading--h4 + .spec-column__row {
  margin-top: var(--sp-s24);
}

.spec-column__heading + .spec-column__wide-canvas,
.spec-column__text + .spec-column__wide-canvas {
  margin-top: var(--sp-s24);
}

.spec-column__subtitle + .spec-column__row {
  margin-top: var(--sp-s24);
}

/* Full-width canvas inside spec-column — same style as spec-wide__canvas */
.spec-column__wide-canvas {
  width: 100%;
  height: 432px;
  border-radius: var(--sp-s24);
  border: 1px solid var(--border-default);
  background-color: var(--background-primary);
  background-image: repeating-conic-gradient(var(--checker-a) 0% 25%, var(--checker-b) 0% 50%);
  background-size: 10px 10px;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.spec-column__wide-canvas::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--background-primary);
  opacity: 0.76;
  border-radius: inherit;
  pointer-events: none;
}

.spec-column__wide-canvas-content {
  /* No z-index: relative alone keeps the content above the positioned
     ::before wash by tree order WITHOUT a stacking context — a z-index here
     isolates mix-blend-mode content (squircle disabled Img) from the
     checkerboard backdrop and silently mutes the blend. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

.spec-column__subtitle {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-primary);
  margin: 0;
}

/* Two-column row: canvas left, points right */
.spec-column__row {
  display: grid;
  grid-template-columns: 468px 1fr;
  gap: clamp(var(--sp-s24), calc(var(--sp-s24) + (48 - 24) * ((100vw - 228px - 64px - 960px) / (1280 - 960))), var(--sp-s48));
  align-items: start;
}

/* Left — canvas */
.spec-column__canvas {
  height: 432px;
  border-radius: var(--sp-s24);
  border: 1px solid var(--border-default);
  background-color: var(--background-primary);
  background-image: repeating-conic-gradient(var(--checker-a) 0% 25%, var(--checker-b) 0% 50%);
  background-size: 10px 10px;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.spec-column__canvas::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--background-primary);
  opacity: 0.76;
  border-radius: inherit;
  pointer-events: none;
}

.spec-column__canvas-content {
  /* No z-index: relative alone keeps the content above the positioned
     ::before wash by tree order WITHOUT a stacking context — a z-index here
     isolates mix-blend-mode content (squircle disabled Img) from the
     checkerboard backdrop and silently mutes the blend. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

.spec-column__heading {
  font-family: var(--text-heading-heading1-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading1-size);
  font-weight: var(--text-heading-heading1-weight);
  line-height: var(--text-heading-heading1-line-height);
  color: var(--text-and-icon-primary);
  margin: 0;
  max-width: 714px;
}
/* Heading-4 variant — a smaller section heading (Heading 4 token) kept at the top of the column */
.spec-column__heading--h4 {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
}
/* Promo variant — a large section-level heading (Promo Heading token, PP Agrandir) for a
   block that opens a whole section (e.g. Layout) above its per-size sub-columns. */
.spec-column__heading--promo {
  font-family: var(--text-heading-promo-heading-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-promo-heading-size);
  font-weight: var(--text-heading-promo-heading-weight);
  line-height: var(--text-heading-promo-heading-line-height);
}

.spec-column__text {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  color: var(--text-and-icon-primary);
  max-width: 714px;
  margin: 0;
}

.spec-column__points {
  display: flex;
  flex-direction: column;
  gap: var(--sp-s16);
  margin: 0;
  padding: 0;
  list-style: none;
}

.spec-column__point {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-s16);
}

.spec-column__point .indicator {
  background: var(--border-inform);
  color: var(--text-and-icon-always-light);
  border: none;
  flex-shrink: 0;
}

.spec-column__separator {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-and-icon-disabled);
  padding: var(--sp-s8) 0 var(--sp-s4);
  list-style: none;
}

/* ════════════════════════════════════════════════
   spec-text — inline text modifiers for spec content
   ════════════════════════════════════════════════ */

.spec-text--heading4 {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-primary);
}

/* ════════════════════════════════════════════════
   spec-point — shared point body (used in spec-wide + spec-column)
   ════════════════════════════════════════════════ */

.spec-point__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-s8);
  padding-top: var(--sp-s2); /* aligns first text line with indicator center */
}

.spec-point__title {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-primary);
}

.spec-point__desc {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  color: var(--text-and-icon-primary);
}

/* ════════════════════════════════════════════════
   AnnotationCallout — patterns.js component
   ════════════════════════════════════════════════ */

/* Indicator anchor */
.ac-indicator {
  position: absolute;
  transform: translate(-50%, -50%);
  opacity: 0;
  /* Transforming/Enter/Fast — 150ms standard-ease-in-out */
  transition: opacity 150ms cubic-bezier(0.25, 1, 0.5, 1);
  pointer-events: none;
}
.ac-indicator.visible { opacity: 1; }

/* Row: dashed line + tag under single clip-path mask */
.ac-row {
  position: absolute;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: var(--sp-s8);
  pointer-events: none;
  user-select: none;
  clip-path: inset(0 100% 0 0 round 4px);
  opacity: 0;
}
.ac-row.reveal {
  opacity: 1;
  /* Mask/Appear — 300ms slow-ease-out */
  transition: clip-path 300ms cubic-bezier(0.0, 0.0, 0.2, 1);
  clip-path: inset(0 0% 0 0 round 4px);
}

/* Dashed line */
.ac-line {
  height: 1px;
  border-top: 1px dashed var(--border-default);
  flex-shrink: 0;
}

/* Elbow connector: dot + horizontal-then-vertical dashed line, no tag/tooltip.
   Two absolutely-positioned dashed segments revealed together under a clip-path mask. */
.ac-elbow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  user-select: none;
  opacity: 0;
}
.ac-elbow.reveal {
  opacity: 1;
  transition: opacity 300ms cubic-bezier(0.0, 0.0, 0.2, 1);
}
.ac-elbow__seg {
  position: absolute;
  clip-path: inset(0 100% 0 0);
}
.ac-elbow__seg--h {
  height: 1px;
  border-top: 1px dashed var(--border-default);
}
.ac-elbow__seg--v {
  width: 1px;
  border-left: 1px dashed var(--border-default);
}
.ac-elbow.reveal .ac-elbow__seg--h {
  /* Mask/Appear — horizontal draws left→right first */
  clip-path: inset(0 0% 0 0);
  transition: clip-path 300ms cubic-bezier(0.0, 0.0, 0.2, 1);
}
.ac-elbow.reveal .ac-elbow__seg--v {
  /* then vertical draws bottom→top, after the horizontal */
  clip-path: inset(0 0 0% 0);
  transition: clip-path 300ms cubic-bezier(0.0, 0.0, 0.2, 1) 220ms;
}
.ac-elbow__seg--v { clip-path: inset(100% 0 0 0); }

/* Tooltip block */
.ac-tooltip {
  position: absolute;
  pointer-events: none;
  user-select: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-s4);
}

.ac-tooltip-title {
  font-family: var(--text-heading-heading4-family);
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-primary);
  /* Mask/Appear — hidden until .visible on parent */
  clip-path: inset(0 100% 0 0);
  transition: clip-path 300ms cubic-bezier(0.0, 0.0, 0.2, 1);
}

.ac-tooltip-body {
  font-family: var(--text-body-compact-body-family);
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-secondary);
  /* Mask/Appear — hidden until .visible on parent */
  clip-path: inset(0 100% 0 0);
  transition: clip-path 400ms cubic-bezier(0.0, 0.0, 0.2, 1);
}

/* Reveal: title and body simultaneously with row */
.ac-tooltip.visible .ac-tooltip-title {
  clip-path: inset(0 0% 0 0);
}
.ac-tooltip.visible .ac-tooltip-body {
  clip-path: inset(0 0% 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .ac-indicator { transition: none; }
  .ac-row { transition: none; clip-path: inset(0 0% 0 0 round 4px); }
  .ac-row.reveal { transition: none; }
  .ac-elbow { transition: none; }
  .ac-elbow.reveal { transition: none; }
  .ac-elbow__seg { transition: none; clip-path: inset(0 0 0 0); }
  .ac-elbow.reveal .ac-elbow__seg--h,
  .ac-elbow.reveal .ac-elbow__seg--v { transition: none; }
  .ac-tooltip-title,
  .ac-tooltip-body { transition: none; clip-path: inset(0 0% 0 0); }
}

/* ════════════════════════════════════════════════
   Skeleton 3.1.0
   ════════════════════════════════════════════════ */

/* Base skeleton block */
.skeleton {
  background: var(--skeleton-on-white);
  overflow: hidden;
  position: relative;
  /* A placeholder is not a control — skeleton-a11y.md § Focus. No hover, no
     selection, and no tabindex anywhere, so it cannot offer an interaction
     for content that has not arrived. */
  pointer-events: none;
  user-select: none;
}

.skeleton-text {
  pointer-events: none;
  user-select: none;
}

/* Animation wrapper — clips the walker */
.skeleton-animation {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

/* Gradient walker — sweeps left to right */
.skeleton-gradient-walker {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 150px;
  background: linear-gradient(
    to right,
    transparent 0%,
    var(--skeleton-wave) 50%,
    transparent 100%
  );
  animation: skeleton-sweep 1500ms linear infinite;
}

@keyframes skeleton-sweep {
  from { transform: translateX(-150px); }
  to   { transform: translateX(calc(var(--skeleton-container-width, 100%) + 150px)); }
}

/* RTL — reverse sweep direction: walker starts beyond right edge, moves left */
@keyframes skeleton-sweep-rtl {
  from { left: 100%; }
  to   { left: -150px; }
}

[dir="rtl"] .skeleton-gradient-walker {
  animation-name: skeleton-sweep-rtl;
}

/* Text skeleton wrapper — padding isolates bar from line-height */
.skeleton-text {
  display: flex;
  flex-direction: column;
  width: 100%;
}
.skeleton-text .skeleton {
  width: 100%;
  flex-shrink: 0;
}

/* Text skeleton size modifiers */
.skeleton-text--promo-heading { padding: var(--sp-s4) 0; }
.skeleton-text--promo-heading .skeleton { height: 36px; border-radius: var(--sp-s12); }

.skeleton-text--heading1 { padding: var(--sp-s4) 0; }
.skeleton-text--heading1 .skeleton { height: 30px; border-radius: var(--sp-s12); }

.skeleton-text--heading2 { padding: var(--sp-s2) 0; }
.skeleton-text--heading2 .skeleton { height: 24px; border-radius: var(--sp-s8); }

.skeleton-text--heading3 { padding: var(--sp-s4) 0; }
.skeleton-text--heading3 .skeleton { height: 20px; border-radius: var(--sp-s6); }

.skeleton-text--heading4 { padding: var(--sp-s2) 0; }
.skeleton-text--heading4 .skeleton { height: 16px; border-radius: var(--sp-s6); }

.skeleton-text--promo-body { padding: var(--sp-s4) 0; }
.skeleton-text--promo-body .skeleton { height: 20px; border-radius: var(--sp-s8); }

.skeleton-text--main-body { padding: var(--sp-s2) 0; }
.skeleton-text--main-body .skeleton { height: 16px; border-radius: var(--sp-s6); }

.skeleton-text--compact-body { padding: var(--sp-s2) 0; }
.skeleton-text--compact-body .skeleton { height: 12px; border-radius: var(--sp-s4); }

.skeleton-text--caption { padding: var(--sp-s4) 0; }
.skeleton-text--caption .skeleton { height: 8px; border-radius: var(--sp-s2); }

.skeleton-text--main-link { padding: var(--sp-s4) 0; }
.skeleton-text--main-link .skeleton { height: 12px; border-radius: var(--sp-s4); }

.skeleton-text--compact-link { padding: var(--sp-s2) 0; }
.skeleton-text--compact-link .skeleton { height: 12px; border-radius: var(--sp-s4); }

/* Reduced motion — disable wave, keep background */
@media (prefers-reduced-motion: reduce) {
  .skeleton-gradient-walker { animation: none; }
}

/* ════════════════════════════════════════════════
   Responsive
   ════════════════════════════════════════════════ */
@media (max-width: 900px) {
  :root { --sidebar-width: 0px; }
  .sidebar { display: none; }
  .main { margin-left: 0; }
  .site-footer { margin-left: 0; }
  .hero { padding: var(--sp-s24) var(--sp-s20); }
  .content { padding: var(--sp-s24) var(--sp-s20) 64px; }
}
@media (max-width: 640px) {
  .topbar { padding: 0 16px; }
  .hero h1 { font-size: var(--text-heading-heading2-size); }
  .site-footer { padding: 20px 16px; }
}

/* ════════════════════════════════════════════════
   measure — redline annotation: size / gap arrow
   Usage:
     <div class="measure measure--h" style="--mv:'16'; width:64px;"></div>
     <div class="measure measure--v" style="--mv:'36'; height:36px;"></div>
   ════════════════════════════════════════════════ */

:root {
  --measure-color: #E1102E;
  --measure-label-color: var(--text-and-icon-primary);
}

.measure {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* Label */
.measure::after {
  content: var(--mv, '0');
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size, 12px);
  line-height: var(--text-caption-caption-line-height, 16px);
  color: var(--measure-label-color);
  position: absolute;
  white-space: nowrap;
  z-index: 1;
}

/* ── Horizontal: ←——— N ———→ ── */
.measure--h {
  height: 8px;
  flex-direction: row;
}
/* horizontal line */
.measure--h::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  height: 0.5px;
  background: var(--measure-color);
}
/* left cap */
.measure--h .measure__cap-a,
/* right cap */
.measure--h .measure__cap-b {
  position: absolute;
  top: 0; bottom: 0;
  width: 0.5px;
  background: var(--measure-color);
}
.measure--h .measure__cap-a { left: 0; }
.measure--h .measure__cap-b { right: 0; }
/* label below */
.measure--h::after {
  top: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
}

/* ── Vertical: ↑ N ↓ ── */
.measure--v {
  width: 8px;
  flex-direction: column;
}
/* vertical line */
.measure--v::before {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  left: 50%;
  width: 0.5px;
  background: var(--measure-color);
}
/* top cap */
.measure--v .measure__cap-a,
/* bottom cap */
.measure--v .measure__cap-b {
  position: absolute;
  left: 0; right: 0;
  height: 0.5px;
  background: var(--measure-color);
}
.measure--v .measure__cap-a { top: 0; }
.measure--v .measure__cap-b { bottom: 0; }
/* label right */
.measure--v::after {
  left: calc(100% + 4px);
  top: 50%;
  transform: translateY(-50%);
}

/* ════════════════════════════════════════════════
   measurement — redline annotation: padding indent block
   Structure (all variants):
     <div class="measurement measurement--right" style="--mv:'16'; --ms:40px;">
       <div class="measurement__block"></div>
       <div class="measurement__arrow"></div>
     </div>

   --ms = size of the block (width for --left/--right, height for --top/--bot)
   --mv = label value string e.g. '16'

   Variants:  --left  --right  --bot  --top
   ════════════════════════════════════════════════ */

.measurement {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--sp-s8, 8px);
}

/* ── Block — the pink filled rect ── */
.measurement__block {
  background: rgba(225, 16, 46, 0.2);
  flex-shrink: 0;
}

/* ── Arrow — the ├—┤ bracket ── */
.measurement__arrow {
  position: relative;
  flex-shrink: 0;
}
/* label */
.measurement__arrow::after {
  content: var(--mv, '0');
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size, 12px);
  line-height: var(--text-caption-caption-line-height, 16px);
  color: var(--measure-label-color);
  position: absolute;
  white-space: nowrap;
}
/* center line */
.measurement__arrow::before {
  content: '';
  position: absolute;
  background: var(--measure-color);
}

/* ══ --left / --right: вертикальный блок + стрелка одной высоты ══ */
.measurement--left,
.measurement--right {
  flex-direction: row;
  align-items: stretch;
}
.measurement--left .measurement__block,
.measurement--right .measurement__block {
  width: var(--ms, 16px);
  height: var(--ms, 16px);
  border-top: 0.5px solid var(--measure-color);
  border-bottom: 0.5px solid var(--measure-color);
}
.measurement--left .measurement__arrow,
.measurement--right .measurement__arrow {
  position: relative;
  width: 8px;
  height: var(--ms, 16px);
}
.measurement--left .measurement__arrow::before,
.measurement--right .measurement__arrow::before {
  top: 0; bottom: 0; left: 50%;
  width: 0.5px;
}
.measurement--left .measurement__arrow .measure__cap-a,
.measurement--left .measurement__arrow .measure__cap-b,
.measurement--right .measurement__arrow .measure__cap-a,
.measurement--right .measurement__arrow .measure__cap-b {
  position: absolute; left: 0; right: 0;
  height: 0.5px; background: var(--measure-color);
}
.measurement--left .measurement__arrow .measure__cap-a,
.measurement--right .measurement__arrow .measure__cap-a { top: 0; }
.measurement--left .measurement__arrow .measure__cap-b,
.measurement--right .measurement__arrow .measure__cap-b { bottom: 0; }

/* --left: стрелка слева, блок справа, число слева от стрелки (band → bracket → label, gap s8) */
.measurement--left { flex-direction: row; }
.measurement--left .measurement__arrow::after {
  right: calc(100% + var(--sp-s8));
  top: 50%; transform: translateY(-50%);
}
/* --right: блок слева, стрелка справа, число справа от стрелки (band → bracket → label, gap s8) */
.measurement--right .measurement__arrow::after {
  left: calc(100% + var(--sp-s8));
  top: 50%; transform: translateY(-50%);
}

/* ══ --bot / --top: горизонтальный блок + стрелка одной ширины ══ */
.measurement--bot,
.measurement--top {
  flex-direction: column;
  align-items: stretch;
}
.measurement--bot .measurement__block,
.measurement--top .measurement__block {
  height: var(--ms, 16px);
  width: var(--ms, 16px);
  border-left: 0.5px solid var(--measure-color);
  border-right: 0.5px solid var(--measure-color);
}
.measurement--bot .measurement__arrow,
.measurement--top .measurement__arrow {
  position: relative;
  height: 8px;
  width: var(--ms, 16px);
}
.measurement--bot .measurement__arrow::before,
.measurement--top .measurement__arrow::before {
  left: 0; right: 0; top: 50%;
  height: 0.5px;
}
.measurement--bot .measurement__arrow .measure__cap-a,
.measurement--bot .measurement__arrow .measure__cap-b,
.measurement--top .measurement__arrow .measure__cap-a,
.measurement--top .measurement__arrow .measure__cap-b {
  position: absolute; top: 0; bottom: 0;
  width: 0.5px; background: var(--measure-color);
}
.measurement--bot .measurement__arrow .measure__cap-a,
.measurement--top .measurement__arrow .measure__cap-a { left: 0; }
.measurement--bot .measurement__arrow .measure__cap-b,
.measurement--top .measurement__arrow .measure__cap-b { right: 0; }

/* --bot: блок сверху, стрелка снизу, число под стрелкой (band → bracket → label, gap s8) */
.measurement--bot .measurement__arrow::after {
  top: calc(100% + var(--sp-s8));
  left: 50%; transform: translateX(-50%);
}
/* --top: блок снизу, стрелка сверху, число над стрелкой (band → bracket → label, gap s8) */
/* ЯКОРЕНИЕ. Из-за column-reverse обёртка --top на 16px выше самого блока
   (arrow 8px + gap s8 над ним). Поэтому --top С РЕАЛЬНОЙ ПОЛОСОЙ
   (.measurement__block ненулевой высоты) позиционируй по bottom, а не по top —
   иначе полоса уедет на эти 16px вниз от измеряемого элемента.
   Пример band-версии: sliding-button.njk (полосы s68 у Track, bottom:0).
   --top БЕЗ ПОЛОСЫ (bracket-only: блок height:0, border:0) выравнивать нечему —
   он держит свою координату top. Пример: checkbox.njk (скобка 48, top:16px). */
/* --top: блок снизу, стрелка сверху, число над стрелкой (band → bracket → label, gap s8)
   ANCHORING a --top that carries a BAND: the arrow + label sit BEFORE the block in
   the box, so the wrapper is (arrow 8px + gap s8) = 16px taller than the band.
   Position it by its BOTTOM (`bottom: 0`, not `top: 0`), or the band lands 16px
   below the element it measures and the label eats into the element instead of
   overflowing into the margin. A bracket-only --top (block `height: 0; border: 0`,
   as on checkbox.njk) has no band to align, so it is placed by its own coordinate. */
.measurement--top { flex-direction: column-reverse; }
.measurement--top .measurement__arrow .measure__cap-a { left: 0; }
.measurement--top .measurement__arrow .measure__cap-b { right: 0; }
/* label above arrow */
.measurement--top .measurement__arrow::after {
  bottom: calc(100% + var(--sp-s8));
  left: 50%; transform: translateX(-50%);
}

/* ── Gap fill: horizontal gap between two side-by-side elements ──
   Блок заполняет gap по всей высоте строки.
   Рогатка выходит снизу за пределы контейнера.
   Usage:
     <div class="measurement--gap-h" style="--mv:'4'; width:4px; align-self:stretch;"></div>
   ── */
.measurement--gap-h {
  position: relative;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  background: rgba(225, 16, 46, 0.2);
  border-left: 0.5px solid var(--measure-color);
  border-right: 0.5px solid var(--measure-color);
}
/* рогатка снизу — горизонтальная линия с вертикальными капами */
.measurement--gap-h::before {
  content: '';
  position: absolute;
  top: calc(100% + var(--sp-s8, 8px));
  left: 0;
  right: 0;
  height: 6px;
  border-left: 0.5px solid var(--measure-color);
  border-right: 0.5px solid var(--measure-color);
  border-top: 0.5px solid var(--measure-color);
}
/* число под рогаткой */
.measurement--gap-h::after {
  content: var(--mv, '0');
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size, 12px);
  line-height: var(--text-caption-caption-line-height, 16px);
  color: var(--measure-label-color);
  position: absolute;
  top: calc(100% + var(--sp-s8, 8px) + 8px);
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
}

/* ── Gap fill: vertical gap between two stacked elements ──
   Блок заполняет gap по всей ширине.
   Рогатка выходит вправо за пределы контейнера.
   Usage:
     <div class="measurement--gap-v" style="--mv:'8'; height:8px; align-self:stretch;"></div>
   ── */
.measurement--gap-v {
  position: relative;
  display: flex;
  flex-direction: row;
  flex-shrink: 0;
  background: rgba(225, 16, 46, 0.2);
  border-top: 0.5px solid var(--measure-color);
  border-bottom: 0.5px solid var(--measure-color);
}
.measurement--gap-v::after {
  content: var(--mv, '0');
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size, 12px);
  line-height: var(--text-caption-caption-line-height, 16px);
  color: var(--measure-label-color);
  position: absolute;
  left: calc(100% + 4px);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
}

/* ════════════════════════════════════════════════
   roundness marker — corner-radius annotation (redline, shares --measure-color)
   1:1 port of the Figma "roundness marker" component set (node 26530:7966,
   variants Position=TL/TR/BL/BR). Frame 26×33: a 16.5×16.5 dashed corner glyph
   pinned to one corner + the radius value labelled on the opposite diagonal.
   The glyph is a single dashed <path> (two short edge ticks + a quarter-arc) —
   each corner has its OWN exact path (verbatim from Figma, not a flip).

   Usage — place absolute on the annotated corner, pick the corner modifier and
   paste that corner's path (see below):
     <span class="roundness roundness--br" style="--mv:'0';">
       <svg class="roundness__glyph" viewBox="0 0 17 17" fill="none" aria-hidden="true">
         <path d="M0.5 9L0.5 17M17 0.5L9 0.5M1 13C7.62742 13 13 7.62742 13 1"
               stroke="currentColor" stroke-dasharray="1 1"/>
       </svg>
     </span>
   Per-corner path (viewBox 0 0 17 17):
     TL  M16.5 8V0M0 16.5H8M16 4C9.37258 4 4 9.37258 4 16
     TR  M0.5 8V0M17 16.5H9M1 4C7.62742 4 13 9.37258 13 16
     BR  M0.5 9L0.5 17M17 0.5L9 0.5M1 13C7.62742 13 13 7.62742 13 1
     BL  M16.5 9V17M0 0.5L8 0.5M16 13C9.37258 13 4 7.62742 4 1
   ════════════════════════════════════════════════ */
.roundness {
  position: relative;
  display: block;
  width: 26px;    /* Figma frame 26×33 — annotation graphic, matches component exactly */
  height: 33px;
  color: var(--measure-color);
  flex-shrink: 0;
}
/* 16.5×16.5 glyph, pinned to the frame corner nearest the annotated corner */
.roundness__glyph {
  position: absolute;
  display: block;
  width: 16.5px;
  height: 16.5px;
  overflow: visible;
}
.roundness--tl .roundness__glyph { right: 0.5px; bottom: 0.5px; }
.roundness--tr .roundness__glyph { left: 0;      bottom: 0.5px; }
.roundness--br .roundness__glyph { left: 0.5px;  top: 0.5px; }
.roundness--bl .roundness__glyph { right: 0.5px; top: 0.5px; }

/* label — radius value, on the outward diagonal (opposite the glyph corner) */
.roundness::after {
  content: var(--mv, '0');
  position: absolute;
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size, 12px);
  line-height: var(--text-caption-caption-line-height, 16px);
  color: var(--measure-label-color);
  white-space: nowrap;
}
.roundness--tl::after { left: 0;   top: 0; }
.roundness--tr::after { right: 0;  top: 0; }
.roundness--br::after { right: 0;  bottom: 0; }
.roundness--bl::after { left: 0;   bottom: 0; }

/* ════════════════════════════════════════════════
   Card component — spec: specs/components/card/card.md
   Figma lineage [Card] 3.1 (node 6617-45261). Base preset: customCard —
   ready-made presets are assembled from it. Root is a vertical stack on
   Surface/OnWhite (radius s20, padding s16, gap s8):
   StartSlot (fill × hug) → StartText (Title / Subtitle TextRow instances,
   gap s4) → EndSlot (fill × hug). CornerSlot (s48 touch target, Checkbox
   instance by default) overlays the top-right corner. Width/height are
   instance-level via --card-width/--card-height; the Figma default example
   is 160×136. States: Standard, Skeleton (composes the Skeleton component;
   placeholders on Surface/Overlay — OnWhite needs the overlay tint, not
   Skeleton/OnWhite). Interactive instances press with catalogued pushItem
   (motion-rules §6.1) via .card--interactive. */
/* customCard openness contract (owner decision 2026-07-29): radius, paddings,
   gaps and TextRow typography are DEFAULTS — any instance may override them
   with DS tokens via the --card-* props below (typography: set the four
   var(--text-{group}-*) props together on the row wrapper). Ready-made
   presets (§8) LOCK their values; the base stays open. */
.card {
  --card-width: calc(var(--sp-s80) + var(--sp-s80));
  --card-height: auto;
  --card-radius: var(--sp-s20);
  --card-padding: var(--sp-s16);
  --card-gap: var(--sp-s8);
  --card-text-gap: var(--sp-s4);
  --card-surface: var(--surface-on-white);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
  width: var(--card-width);
  height: var(--card-height);
  padding: var(--card-padding);
  box-sizing: border-box;
  background: var(--card-surface);
  border-radius: var(--card-radius);
  overflow: hidden;
  color: var(--text-and-icon-primary);
}
.card__start-slot,
.card__end-slot {
  display: flex;
  flex-direction: row;
  align-self: stretch;
  align-items: flex-start;
  justify-content: flex-start;
}
.card__start-text {
  display: flex;
  flex-direction: column;
  align-self: stretch;
  gap: var(--card-text-gap);
}
/* Title / Subtitle are TextRow instances; the row wrapper sets typography +
   colour on .text-row__text (the Rating description pattern). Title inherits
   the root colour; Subtitle declares its own (colour-architecture rule). */
.card__title-row .text-row__text {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
}
.card__subtitle-row .text-row__text {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-and-icon-secondary);
}
/* CornerSlot — s48 touch target pinned to the card's top-right corner;
   s8 top/right padding lands the 24px Checkbox s16 from both card edges. */
.card__corner-slot {
  position: absolute;
  top: var(--sp-s0);
  right: var(--sp-s0);
  width: var(--sp-s48);
  height: var(--sp-s48);
  padding: var(--sp-s8) var(--sp-s8) var(--sp-s0) var(--sp-s0);
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* RTL (opt-in per component rule) — CornerSlot mirrors to the top-left */
[dir="rtl"] .card__corner-slot {
  right: auto;
  left: var(--sp-s0);
  padding: var(--sp-s8) var(--sp-s0) var(--sp-s0) var(--sp-s8);
}
/* Skeleton state — Figma State=Skeleton: the ends are pushed apart, so the
   card keeps its loaded height instead of hugging the placeholders (hugging
   collapses space-between to zero and the bars end up flush). No CornerSlot;
   placeholders are Skeleton instances re-tinted for this surface. */
.card--skeleton {
  --card-height: calc(var(--sp-s128) + var(--sp-s8));
  gap: var(--sp-s0);
  justify-content: space-between;
}
.card--skeleton .card__corner-slot { display: none; }
.card--skeleton .skeleton { background: var(--surface-overlay); }
.card__skeleton-icon {
  width: var(--sp-s24);
  height: var(--sp-s24);
  border-radius: var(--sp-s6);
}
/* Interactive instances — catalogued pushItem (motion-rules §6.1). The base
   card is NOT interactive; presets/instances opt in with .card--interactive. */
.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);
}
:root {
  /* pushItem recipe (§6.1): 200ms standard-ease-in-out */
  --card-push-item-press:   200ms cubic-bezier(0.25, 1, 0.5, 1);
  --card-push-item-release: 200ms cubic-bezier(0.25, 1, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
  :root {
    --card-push-item-press:   0ms;
    --card-push-item-release: 0ms;
  }
}

/* ────────────────────────────────────────────────
   Card — ready-made presets (assembled from customCard; spec card.md §8).
   Owner-approved web-first 2026-07-28; Figma definitions pending.
   ──────────────────────────────────────────────── */

/* layoutFlow axis — `overlap` value (spec card.md §4a): the vertical flow is
   replaced by a grid stack where StartSlot / StartText / EndSlot share the
   padding area (StartText on top). A mechanism, not a preset — combine with
   the mediaPin content classes (.card__artwork / .card__img-container) for
   media cards of any purpose (vertical tiles, stories, promos). */

.card--overlap {
  display: grid;
  grid-template-columns: minmax(var(--sp-s0), 1fr);
  grid-template-rows: minmax(var(--sp-s0), 1fr);
  grid-template-areas: "card-overlap";
  align-content: stretch;
  justify-content: stretch;
}
.card--overlap > .card__start-slot,
.card--overlap > .card__start-text,
.card--overlap > .card__end-slot {
  grid-area: card-overlap;
}
.card--overlap > .card__start-slot {
  align-self: flex-start;
  z-index: 0;
}
.card--overlap > .card__start-text {
  align-self: flex-start;
  z-index: 1;
}
.card--overlap > .card__end-slot {
  align-self: stretch;
  align-items: flex-end;
  justify-content: flex-end;
  z-index: 0;
}

/* ── Card VARIANTS (spec card.md §8) — product-level data contracts.
   A variant carries its own data + semantics (what goes in the slots and what
   the card is for); mechanics stay in the axes above. Sizes come from the
   consuming grid via --card-width/--card-height, not from the variant. ── */

/* serviceCard (data-ds-variant="service-card") — entry into a vertical /
   service: vertical name + transport artwork on the OnWhite surface.
   Two directions: column (artwork full-width, flush bottom) · row (artwork
   centred on the end edge). Matches the designer tiles — City Rides (column),
   Couriers / City to city / Freight (row). */
.card--service {
  --card-radius: var(--sp-s24);
}
.card--service .card__title-row .text-row__text {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}

/* storyCard (data-ds-variant="story-card") — promo story tile: headline over a
   full-bleed background medium on a NON-OnWhite surface (pastel or dark, set
   per instance via --card-surface; text colour is the instance's duty). */
.card--story {
  --card-radius: var(--sp-s24);
  --card-width: calc(var(--sp-s96) + var(--sp-s8));
  --card-height: calc(var(--sp-s144) + var(--sp-s12));
}
.card--story .card__title-row .text-row__text {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}

/* navCard — SITE-LEVEL chrome, NOT a DS variant (owner decision 2026-07-29:
   it exists only on the docs site, so it stays out of the component contract).
   Kept here because the site consumes it on every Foundations landing page;
   class deliberately not .card--* so it never reads as a DS preset. */
   customCard config, locked: StartSlot = dc-icon (24) · StartText = H4 Title
   + Compact Subtitle · EndSlot = forward-arrow (stretches on hover —
   DS.initNavCards()) · CornerSlot off · interactive link · 1px border ·
   hover inverts the surface. Motion: hover colours = Transforming/State/Fast;
   arrow = Transforming/Enter/Default (JS reads --site-nav-card-arrow, so reduced
   motion zeroes it). */
.site-nav-card {
  --site-nav-card-state-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
  --site-nav-card-arrow: 250ms cubic-bezier(0.25, 1, 0.5, 1);
  --card-width: 100%;
  max-width: calc(var(--sp-s144) + var(--sp-s144) + var(--sp-s132));
  min-height: var(--sp-s56);
  gap: var(--sp-s0);
  background: var(--background-primary);
  border: 1px solid var(--border-default);
  text-decoration: none;
  transition:
    background-color var(--site-nav-card-state-fast),
    border-color var(--site-nav-card-state-fast),
    transform var(--card-push-item-release);
}
@media (prefers-reduced-motion: reduce) {
  .site-nav-card {
    --site-nav-card-state-fast: 0ms;
    --site-nav-card-arrow: 0ms;
  }
}
.site-nav-card:hover {
  background: var(--background-inverse-primary);
  border-color: var(--background-inverse-primary);
}
.site-nav-card:hover .card__title-row .text-row__text {
  color: var(--text-and-icon-inverse-primary);
}
.site-nav-card:hover .card__subtitle-row .text-row__text {
  color: var(--text-and-icon-secondary);
}
.site-nav-card:hover .card__start-slot .dc-outline { fill: var(--double-color-outline-inverse); }
.site-nav-card:hover .card__start-slot .dc-bg      { fill: var(--double-color-bg-inverse); }
.site-nav-card:hover .card__end-slot .site-nav-card__icon svg path {
  fill: var(--text-and-icon-inverse-primary);
}
.site-nav-card .card__start-slot { margin-bottom: var(--sp-s16); }
.site-nav-card .card__end-slot {
  align-items: center;
  margin-top: var(--sp-s16);
  overflow: hidden;
}
.site-nav-card .card__end-slot .site-nav-card__icon {
  position: relative;
  overflow: visible;
}
/* IconContainer stand-in (no shipped .icon-container CSS yet) */
.site-nav-card__icon {
  display: flex;
  align-items: center;
  justify-content: center;
}
.site-nav-card .card__start-slot .dc-outline { fill: var(--double-color-outline); }
.site-nav-card .card__start-slot .dc-bg      { fill: var(--double-color-bg); }
.site-nav-card .card__end-slot .site-nav-card__icon svg path {
  fill: var(--text-and-icon-primary);
  transition: fill var(--site-nav-card-state-fast);
}
.site-nav-card .card__start-text {
  align-items: flex-start;
  justify-content: center;
  flex: 1;
}
/* Title override — H4 (nav cards), not Main Body */
.site-nav-card .card__title-row .text-row__text {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  color: var(--text-and-icon-primary);
  transition: color var(--site-nav-card-state-fast);
}

/* mediaPin axis — content classes for the media inside a slot (spec §4b).
   bottom = full-width .card__artwork--full-bleed strip flush to the bottom ·
   corner = .card__img-container--full-bleed crop window in the end corner ·
   end = .card__img-container--pin-end crop window centred on the end edge.
   Bleed lives on the CONTENT, never on the slot container. */
/* Artwork strip. --card-media-shift-y nudges it vertically inside the card
   (positive = down, so more of the top shows and the bottom clips off);
   the card's overflow:hidden does the cropping. */
.card__artwork {
  --card-media-shift-y: var(--sp-s0);
  display: block;
  width: 100%;
  transform: translateY(var(--card-media-shift-y));
}
.card__artwork--full-bleed {
  margin-left: calc(-1 * var(--card-padding));
  margin-right: calc(-1 * var(--card-padding));
  width: calc(100% + var(--card-padding) + var(--card-padding));
}
.card__end-slot .card__artwork--full-bleed {
  margin-bottom: calc(-1 * var(--card-padding));
}
.card__artwork > img {
  display: block;
  width: 100%;
  height: auto;
}
/* Crop window. Two instance-level knobs (spec §4b):
   --card-media-offset — shifts the artwork toward the end edge, so LESS of it
                         stays visible (it runs out past the crop boundary);
   --card-media-size   — the window itself (smaller window = smaller artwork). */
.card__img-container {
  --card-media-offset: var(--sp-s0);
  --card-media-size: var(--sp-s80);
  width: var(--card-media-size);
  height: var(--card-media-size);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: flex-start;
}
.card__end-slot .card__img-container--full-bleed {
  margin-right: calc(-1 * var(--card-padding));
  margin-bottom: calc(-1 * var(--card-padding));
}
/* mediaPin: end — crop window pinned to the end edge, vertically centred */
.card__img-container--pin-end {
  align-self: center;
  margin-right: calc(-1 * var(--card-padding));
}
/* Legacy alias kept for the colors-usage mockups: it pulls the artwork toward
   the START edge (the opposite sign of --card-media-offset) — prefer the prop. */
.card__img-container--offset-start-s8 {
  --card-media-offset: calc(-1 * var(--sp-s8));
}
.card__img-container img {
  display: block;
  width: auto;
  height: calc(var(--card-media-size) + var(--sp-s36) + var(--sp-s2));
  flex-shrink: 0;
  margin-left: var(--card-media-offset);
  transform: scaleX(-1) translateY(var(--sp-s4));
}
.card__img-container--mirror-source-x img {
  transform: scaleX(1) translateY(var(--sp-s4));
}

/* ════════════════════════════════════════════════
   HeaderAlert component — spec: specs/components/header-alert.md
   Full-bleed, edge-to-edge page-level status banner (no corner radius).
   Reserves the top safe-area (status bar) so the fill runs under it; content
   is pinned to the bottom of the banner. Three tones (Error / Warning /
   Success); all text + icon TextAndIcon/AlwaysLight (fixed light, no invert).
   ════════════════════════════════════════════════ */
.header-alert {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;      /* content pinned to the bottom, below the status-bar zone */
  align-items: center;
  width: 100%;
  /* Top safe-area: fill runs under the system status bar. Platform-driven
     (iOS ~44, Android varies); s44 is the fallback when env() is unavailable. */
  padding-top: env(safe-area-inset-top, var(--sp-s44));
  background: var(--accent-red2);   /* Error — the base tone */
  color: var(--text-and-icon-always-light);   /* children inherit via currentColor */
  box-sizing: border-box;
}
/* Tone modifiers — swap only the fill; text/icon stay AlwaysLight */
.header-alert--error   { background: var(--accent-red2); }
.header-alert--warning { background: var(--accent-orange2); }
.header-alert--success { background: var(--accent-green2); }
/* Content row.
   Slots stretch to the CenterSlot's height, and each side slot centres its own content.
   → No subtitle: CenterSlot is the title line box, so the close icon sits on the title line.
   → With subtitle: CenterSlot grows to s40 (title 20 + gap s4 + subtitle 16 = 40), so all slots
     align to s40 and the close icon centres on the whole text container. */
.header-alert__content {
  display: flex;
  flex-direction: row;
  align-items: flex-start;        /* top-align: start/end slots + title stay pinned to the top;
                                     a 2-line subtitle grows DOWN without moving the close/spacer */
  justify-content: center;
  width: 100%;
  padding: var(--sp-s8);
  box-sizing: border-box;
}
/* Leading spacer — mirrors the end slot so the message stays optically centered; fixed s40, top-anchored */
.header-alert__spacer {
  width: var(--sp-s40);
  height: var(--sp-s40);
  flex-shrink: 0;
}
/* Center slot — title + subtitle.
   min-height s40 + hug + centered content: the slot is always s40 tall (title 20 + gap s4 +
   subtitle 16 = 40, or a single centered title), so the composition and the side slots' alignment
   stay identical whether or not the subtitle is shown. */
.header-alert__center {
  flex: 1;
  min-width: 0;
  min-height: var(--sp-s40);
  display: flex;
  flex-direction: column;
  justify-content: center;        /* content centered within the s40 box (a lone title sits centered);
                                     the start/end slots are independently top-anchored, so they stay
                                     fixed even when a 2-line subtitle grows the center-slot below s40 */
  align-items: center;
  gap: var(--sp-s4);
}
.header-alert__title {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
  text-align: center;
}
.header-alert__subtitle {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  text-align: center;
  /* Clamp to 2 lines; overflow is truncated with an ellipsis */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* End slot — close control (IconContainer).
   Stretches to the Content height and centres the icon: on the title line when there is no
   subtitle, on the text-container centre (s40) when a subtitle is present. */
.header-alert__close {
  width: var(--sp-s40);
  height: var(--sp-s40);
  flex-shrink: 0;
  align-self: flex-start;         /* top-anchored s40 slot: stays put when the subtitle grows to 2 lines */
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--text-and-icon-always-light);
  cursor: pointer;
  padding: 0;
  overflow: visible;
}
/* Close-off: the end slot is removed; the leading spacer keeps the message centered */
/* Close off: the end slot AND the leading spacer are both removed (they are paired — the spacer
   only exists to balance the close, so with no close the message centers on its own). */
.header-alert--no-close .header-alert__close,
.header-alert--no-close .header-alert__spacer { display: none; }

/* RTL (opt-in) — mirror the Content row's horizontal slot order (end-slot ↔ leading-spacer).
   Matches the Figma RTL=On variant. Opt in with [dir="rtl"] on the root or .header-alert--rtl.
   Title/subtitle stay centered; the close glyph is directional-neutral, so it is not mirrored.
   NOTE: row-reverse applies ONLY to the class path — with [dir="rtl"] the rtl writing direction
   already reverses the flex row, and adding row-reverse on top double-flips back to LTR. */
.header-alert--rtl:not([dir="rtl"]) .header-alert__content {
  flex-direction: row-reverse;
}

/* ── HeaderAlert motion tokens (Motion 1.3.0) ──
   Off-screen enter/exit at medium1 (250ms): Transforming/Enter/Default in
   (standard-ease-in-out), medium1 out with the exit curve (accelerated-ease-in) —
   the banner slides from above the screen edge. Pressed/Released (§4.1–4.2) drive
   the close control; the exit fires the instant the close press is released.
   Components reference these tokens (duration + curve); only the translate
   direction is component-specific. */
:root {
  /* medium slide — Enter/Default in (250ms), medium1 + exit curve out (250ms) */
  --ha-enter: 250ms cubic-bezier(0.25, 1, 0.5, 1);
  --ha-exit:  250ms cubic-bezier(0.5, 0, 0.75, 0);
  /* close press — Transforming/Pressed 100ms accelerated-ease-in, Released 150ms standard */
  --ha-pressed:  100ms cubic-bezier(0.5, 0, 0.75, 0);
  --ha-released: 150ms cubic-bezier(0.25, 1, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
  :root {
    --ha-enter:    0ms linear;
    --ha-exit:     0ms linear;
    --ha-pressed:  0ms linear;
    --ha-released: 0ms linear;
  }
}

/* ── Animation-section live demo — auto-looping preview of the move-in ──
   Page-level demo helper: the banner slides down from behind the status bar
   (translateY(-100%) → 0, Enter/Default 250ms), holds, the close control presses —
   and the banner slides back up off-screen the instant the press releases
   (medium1 250ms, exit curve), then repeats. Only transform is animated. */
.ha-anim { animation: ha-anim-cycle 4400ms both infinite; will-change: transform; }
.ha-anim .header-alert__close { animation: ha-anim-close 4400ms cubic-bezier(0.25, 1, 0.5, 1) infinite; }

@keyframes ha-anim-cycle {
  /* start off-screen, above the top edge */
  0%   { transform: translateY(-100%); animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1); }
  /* appear — slide down into place (Enter/Default 250ms of 4400ms ≈ 5.7%) */
  5.7% { transform: translateY(0); }
  /* hold on screen through the close press (release at ~63%) */
  63%  { transform: translateY(0); animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0); }
  /* hide — starts the instant the close releases; slide up off-screen (250ms ≈ 5.7%) */
  68.7% { transform: translateY(-100%); }
  /* gap before the loop repeats */
  100% { transform: translateY(-100%); }
}
@keyframes ha-anim-close {
  /* close press while the banner is fully on screen; release at 63% hands off to the exit */
  0%,  60% { transform: scale(1); }
  61.5%    { transform: scale(0.86); }   /* Pressed */
  63%      { transform: scale(1); }        /* Released → banner exit begins here */
  100%     { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .ha-anim,
  .ha-anim .header-alert__close { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════
   Rating component — spec: specs/components/rating/rating.md 3.2.3
   Four sizes (L/M/S/Mini), half-star values (0–5 in 0.5 steps).
   Root is a vertical stack: .rating__slots-container row + optional .rating__description
   (Description since 3.2, L/M only). Each Slot = two stacked copies of
   fav-filled: an empty base in the disabled colour + a fill overlay in
   the fill colour, clipped L→R.
   ══════════════════════════════════════════════════════════════════ */
.rating {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-s8);              /* SlotsContainer → Description; collapses when there is no Description */
  /* Optional container padding (since 3.2.2). Defaults to s0 (no padding). Override per instance to
     inset the SlotsContainer + Description — horizontal and vertical are set independently. */
  padding: var(--rating-padding-y, var(--sp-s0)) var(--rating-padding-x, var(--sp-s0));
}
.rating__slots-container {
  display: flex;
  align-items: center;
}
.rating__slot {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  cursor: default;
  flex-shrink: 0;
}
.star-layers {
  display: grid;
  align-items: center;
  justify-items: center;
}
.star-layers > .star-svg {
  grid-area: 1 / 1;
  display: block;
  fill: currentColor;
}
.star-svg--filled { clip-path: inset(0 100% 0 0); }

/* Sizes — gap is per-size (not linear), matching Figma [Rating-L/M/S/Mini] 3.1.
   Hug mode (default): the SlotsContainer wraps its Slots with the fixed per-size gap. */
.rating--mini .rating__slots-container { gap: var(--sp-s2); }
.rating--s    .rating__slots-container { gap: var(--sp-s4); }
.rating--m    .rating__slots-container { gap: var(--sp-s8); }
.rating--l    .rating__slots-container { gap: var(--sp-s6); }

/* Fill mode — Rating stretches to a fixed-width parent; the SlotsContainer distributes the free
   space evenly between the Slots (auto gap, space-between). Overrides the fixed per-size gap. */
.rating--fill,
.rating--fill .rating__slots-container { width: 100%; }
.rating--fill .rating__slots-container {
  justify-content: space-between;
  gap: 0;
}

.rating--mini .star-svg { width: var(--sp-s16); height: var(--sp-s16); }
.rating--s    .star-svg { width: var(--sp-s24); height: var(--sp-s24); }
.rating--m    .star-svg { width: var(--sp-s40); height: var(--sp-s40); }
.rating--l    .star-svg { width: var(--sp-s56); height: var(--sp-s56); }

/* Description (since 3.2) — a TextRow instance below the SlotsContainer (L and M only). The
   .rating__description wrapper (also .text-row) sets colour + centering; the text node carries the
   clamp + typography. */
.rating__description {
  color: var(--text-and-icon-secondary);
  justify-content: center;     /* centre the TextRow content */
}
.rating__description .text-row__text {
  text-align: center;
  flex: 0 1 auto;              /* hug, so centring works; still truncates via line-clamp */
  /* clamp to 2 lines */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.rating--l .rating__description .text-row__text {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
}
.rating--m .rating__description .text-row__text {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}
.rating--s .rating__description,
.rating--mini .rating__description { display: none; }

/* Colours — empty = fav-filled shape in the Slot-background colour (--surface-overlay, a faint overlay);
   filled = the same shape in the fill colour on top */
.star-svg--empty                   { color: var(--surface-overlay); }
.star-svg--filled                  { color: var(--text-and-icon-primary); }
.rating--brand   .star-svg--filled { color: var(--text-and-icon-brand);   }
.rating--primary .star-svg--filled { color: var(--text-and-icon-primary); }
.rating--accent  .star-svg--filled { color: var(--text-and-icon-accent);  }

/* Disabled */
.rating--disabled .rating__slot     { pointer-events: none; }
.rating--disabled .star-svg--filled { color: var(--text-and-icon-disabled); }
.rating--disabled .star-svg--empty  { color: var(--text-and-icon-disabled); }

/* Input mode — hover / press scales are token-driven (spec §10).
   Base transition = pushItem release; hover-in = the bounceWave companion hover (×1.18 via
   Transforming/State/Fast, motion-rules.md §6.3); press 0.95 = the catalogued pushItem (§6.1). */
.rating--input .rating__slot {
  cursor: pointer;
  transition: transform var(--rating-push-item-release);
}
.rating--input .rating__slot:hover {
  transform: scale(1.18);
  transition: transform var(--rating-state-fast);
}
.rating--input .rating__slot:active {
  transform: scale(0.95);
  transition: transform var(--rating-push-item-press);
}

/* ── Rating motion tokens (from motion-rules.md, Motion 1.6.0) ──
   state-fast   = Transforming/State/Fast (§4.2): 150ms slow-ease-out — hover scale.
   push-item-*  = pushItem recipe (§6.1): 200ms standard-ease-in-out — Slot press/release.
   bounce-wave-* = bounceWave selection recipe (§6.3) — a DS token now, declared in
   tokens/generated/motion.css and zeroed there under reduced motion. */
:root {
  --rating-state-fast:            150ms cubic-bezier(0.4, 0, 0.2, 1);
  --rating-push-item-press:       200ms cubic-bezier(0.25, 1, 0.5, 1);
  --rating-push-item-release:     200ms cubic-bezier(0.25, 1, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
  :root {
    --rating-state-fast:            0ms linear;
    --rating-push-item-press:       0ms linear;
    --rating-push-item-release:     0ms linear;
  }
}

/* ────────────────────────────────────────────────
   Checkbox — spec specs/components/checkbox/checkbox.md
   Figma [Checkbox] 3.1 (node 17855:6623). Three layers, mirroring the Figma
   component chain exactly:
     .checkbox        s48 x s48 touch target (WCAG 2.5.5) — the focusable element
     .checkbox__box   s24 x s24 visible box, radius s8, s2 border  (.CheckNoSafezone)
     .checkbox__check the check glyph, icons/outlined/actions/check.svg  (.checkbox-icon)
   States are State x Disabled (4), matching the Figma variant matrix. There is
   no indeterminate state — it is not in the Figma contract (deferred).
   ──────────────────────────────────────────────── */

.checkbox {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s48);
  height: var(--sp-s48);
  padding: var(--sp-s0);
  border: none;
  background: none;
  cursor: pointer;
  flex-shrink: 0;
}

/* No safe zone — Figma .CheckNoSafezone (node 4282:12760): the bare s24 box,
   for a parent that already owns the s48 hit area (Card's CornerSlot). The
   wrapper collapses onto the box; every state selector keeps working because
   the modifier stays on the .checkbox element. The parent carries the role,
   aria-checked and focus, so this instance is visual only. */
.checkbox--no-safezone {
  width: var(--sp-s24);
  height: var(--sp-s24);
}

.checkbox__box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s24);
  height: var(--sp-s24);
  border-radius: var(--sp-s8);
  border: var(--sp-s2) solid var(--text-and-icon-secondary);
  background: transparent;
  box-sizing: border-box;
  flex-shrink: 0;
  /* Unchecked <-> Checked — motion-rules.md §6.6 Checkbox: Transforming/State/Default */
  transition:
    background-color var(--transforming-state-default),
    border-color var(--transforming-state-default);
}

/* The glyph fills the box's inner area at native 24-grid proportions — drawing
   it smaller thins the strokes (the fix Card 3.3.21 landed for the same glyph). */
.checkbox__check {
  width: 100%;
  height: 100%;
  color: var(--text-and-icon-inverse-primary);
  opacity: 0;
  transition: opacity var(--transforming-state-default);
}
.checkbox__check svg { display: block; width: 100%; height: 100%; }

/* Selected — filled box, glyph visible. Border matches the fill so the box
   keeps its s24 outer size across states. */
.checkbox--selected .checkbox__box {
  background: var(--text-and-icon-primary);
  border-color: var(--text-and-icon-primary);
}
.checkbox--selected .checkbox__check { opacity: 1; }

/* Disabled — one token swap per state; never opacity (component-authoring). */
.checkbox--disabled { cursor: not-allowed; }
.checkbox--disabled .checkbox__box { border-color: var(--text-and-icon-disabled); }
.checkbox--selected.checkbox--disabled .checkbox__box {
  background: var(--text-and-icon-disabled);
  border-color: var(--text-and-icon-disabled);
}

.checkbox:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(var(--sp-s2) * -1);
  border-radius: var(--sp-s12);
}

/* Motion: Transforming/State/Default — the catalogued token for both
   Unchecked -> Checked and Checked -> Unchecked (motion-rules.md §6.6).
   Declared DS-wide in tokens/generated/motion.css and zeroed there under
   reduced motion, so this component only consumes it. */

/* ────────────────────────────────────────────────
   Section canvas floor.
   Pages set a per-demo height (often `auto`, which collapses the stage to
   98–210px and reads as cramped). This floor keeps every section canvas a
   usable stage while leaving the deliberately taller ones untouched — the
   floor equals the smallest height already chosen by hand in the pages
   (card, cell), so it is the existing low end rather than a new number.
   The SP scale tops out at s144, so the value is expressed as an SP sum
   (s144 + s144 + s32), the same convention card.njk uses for its 320 width.
   The cover canvas (.spec-cover__canvas) is deliberately NOT included: its
   height is part of the cover composition.
   ──────────────────────────────────────────────── */
.spec-column__canvas,
.spec-column__wide-canvas,
.spec-wide__canvas {
  min-height: calc(var(--sp-s144) + var(--sp-s144) + var(--sp-s32));
}

/* ────────────────────────────────────────────────
   Spec-page annotation captions.
   The caption under a Layout / Usage example, and the stacked description
   lines used in a point's "General" entry. Shared by every component page —
   card.njk and rating.njk each carried a byte-identical private copy, which
   broke the moment a third page (checkbox) reused the markup.
   ──────────────────────────────────────────────── */
.usage-caption {
  font-family: var(--text-caption-caption-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size);
  font-weight: 600;
}
.usage-caption--good { color: var(--text-and-icon-success); }
.usage-caption--bad  { color: var(--text-and-icon-error); }

.spec-desc-lines {
  display: flex;
  flex-direction: column;
  gap: var(--sp-s16);
}
.spec-desc-lines__muted { color: var(--text-and-icon-secondary); }

/* ────────────────────────────────────────────────
   Token chip — the ONLY way a token name is shown on a spec page.
   Any --token / var(--token) named in prose, in a point description or in a
   table goes inside <span class="tok">…</span>; bare token text is not used.
   Rule: docs/agents/rules/token-usage.md § "Showing a token on a page".
   Canonical values are Card's; five pages (card, cell, rating, header-alert,
   tag) each carried a private copy and three of them had drifted, two with
   raw px. Note: the 1px vertical padding has no SP token (the scale starts at
   s2) — same known gap as the DS hairline border.
   ──────────────────────────────────────────────── */
.tok {
  display: inline-block;
  font-family: 'SF Mono', ui-monospace, monospace;
  font-size: var(--text-body-compact-body-size);
  color: var(--text-and-icon-accent);
  background: var(--pastel-blue1);
  padding: 1px var(--sp-s4);
  border-radius: var(--sp-s4);
  white-space: nowrap;
}

/* ══════════════════════════════════════════════════════════════════
   Button — spec: specs/components/button/button.md (3.3.0)
   One base (customButton); L / M / S / Main CTA are locked
   configurations of it. Slots follow the DS-wide
   StartSlot / StartText / EndSlot semantics.
   ══════════════════════════════════════════════════════════════════ */

/* Motion: pushButton for the press (motion-rules.md § 6.1, scale and colour
   together), Transforming/State/Default for the label colour, Patterns/Spin for
   the Loader. All three come from tokens/generated/motion.css — declared once
   for the DS, zeroed there under reduced motion, so this component neither
   redeclares them nor carries an override of its own. */

/* ── Base: customButton ── */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-s12);
  height: var(--sp-s56);
  padding: 0 var(--sp-s16);
  border: none;
  border-radius: var(--sp-s20);
  background: var(--background-brand);
  color: var(--text-and-icon-always-dark);
  cursor: pointer;
  white-space: nowrap;
  box-sizing: border-box;
  /* Press moves scale and fill together (pushButton). The label colour is a
     state change, not a press, so it runs on Transforming/State/Default. */
  transition: transform var(--component-push-button-release),
              background-color var(--component-push-button-release),
              color var(--transforming-state-default);
}

.button:active {
  transform: scale(0.95);
  transition: transform var(--component-push-button-press),
              background-color var(--component-push-button-press);
}

/* ── Slots ── */
.button__start-slot,
.button__end-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--sp-s24);
  flex-shrink: 0;
}
.button__start-slot > *,
.button__end-slot > * { width: var(--sp-s24); height: var(--sp-s24); display: block; }

.button__start-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-s2);
}

/* Title: Heading 4 on the base and L, Main Body on M and S, Heading 2 on
   Main CTA — spec §7. The row owns its style rather than inheriting the
   root's, so both rows read the same way in CSS. */
.button__title-row {
  font-family: var(--text-heading-heading4-family);
  font-weight: var(--text-heading-heading4-weight);
  font-size: var(--text-heading-heading4-size);
  line-height: var(--text-heading-heading4-line-height);
}
.button--m .button__title-row,
.button--s .button__title-row {
  font-family: var(--text-body-main-body-family);
  font-weight: var(--text-body-main-body-weight);
  font-size: var(--text-body-main-body-size);
  line-height: var(--text-body-main-body-line-height);
}
.button--cta .button__title-row {
  font-family: var(--text-heading-heading2-family);
  font-weight: var(--text-heading-heading2-weight);
  font-size: var(--text-heading-heading2-size);
  line-height: var(--text-heading-heading2-line-height);
}

/* Subtitle: Compact Body on the base, L and M — Caption on S, where the
   title is already Main Body and Compact Body would not step down. */
.button__subtitle-row {
  font-family: var(--text-body-compact-body-family);
  font-weight: var(--text-body-compact-body-weight);
  font-size: var(--text-body-compact-body-size);
  line-height: var(--text-body-compact-body-line-height);
}
.button--s .button__subtitle-row {
  font-family: var(--text-caption-caption-family);
  font-weight: var(--text-caption-caption-weight);
  font-size: var(--text-caption-caption-size);
  line-height: var(--text-caption-caption-line-height);
}

/* ── Sizes: locked configurations ── */
.button--l,
.button--m,
.button--s { gap: var(--sp-s4); padding: 0 var(--sp-s12); }

/* Optical balance on the presets. An icon carries its own air, so a side
   that shows one needs less room than a side that ends in text. The inner
   paddings turn the s12 root padding into an effective s16 to an icon and
   s20 to a label — which is why a one-slot button still reads centred.
   The base does not use them: it balances with s16 padding and a s12 gap. */
.button--l .button__start-slot,
.button--m .button__start-slot,
.button--s .button__start-slot { padding-left: var(--sp-s4); }

.button--l .button__end-slot,
.button--m .button__end-slot,
.button--s .button__end-slot { padding-right: var(--sp-s4); }

.button--l .button__start-text,
.button--m .button__start-text,
.button--s .button__start-text { padding-inline: var(--sp-s8); }

.button--l { height: var(--sp-s56); }

.button--m { height: var(--sp-s48); }
.button--s { height: var(--sp-s40); }

.button--cta {
  width: 100%;
  height: var(--sp-s64);
  gap: var(--sp-s0);
  padding: 0 var(--sp-s16);
}

/* ── Styles: fill and label per §6 of the spec ── */
.button--primary      { background: var(--background-brand);           color: var(--text-and-icon-always-dark); }
.button--secondary    { background: var(--surface-on-white);           color: var(--text-and-icon-primary); }
.button--always-light { background: var(--background-always-light);    color: var(--text-and-icon-always-dark); }
.button--ghost        { background: transparent;                       color: var(--text-and-icon-primary); }
.button--inverse      { background: var(--background-inverse-primary); color: var(--text-and-icon-inverse-primary); }
.button--error        { background: var(--surface-on-white);           color: var(--text-and-icon-error); }

/* Focus ring — button-a11y.md § Focus. s2 in TextAndIcon/Primary, radius s20
   so it follows the pill. Offset inward, the same way Checkbox does it, so a
   fill-width Main CTA cannot push the ring past its container. */
.button:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(var(--sp-s2) * -1);
  border-radius: var(--sp-s20);
}

/* ── Loading: the label and both side slots give way to a Loader, and the
      button takes the same fixed width as its Skeleton — 72 on L and M,
      64 on S — while Main CTA keeps its full width. The Loader size steps
      with the button: s32 on Main CTA, s24 on L, s16 on M and S
      (Figma [Button] 3.3).

      The glyph is the arc of [Loader] 3.1 Style=AlwaysDark, whose ring
      is 20% of the diameter at every size — so one path scales. Loader
      is a DS component in its own right and has no spec or web build
      here yet; its conic fade is not reproduced. ── */
.button--loading .button__start-text,
.button--loading .button__start-slot,
.button--loading .button__end-slot { display: none; }

.button__spinner {
  position: relative;
  width: var(--sp-s24);
  height: var(--sp-s24);
  flex-shrink: 0;
  border-radius: 50%;
  /* The track is an angular gradient in Figma — SVG has no conic gradient,
     which is why the component's own export fakes one inside a mask. Built
     here from the component's own numbers: start 90deg, inner radius 60% of
     the outer (a band 20% of the diameter), and the fade reaching zero at
     337deg so a gap stays open before the head, as [Loader] 3.1 L (32)
     declares (stops: 0 at 337.35deg, opaque again at 345.31deg). */
  background: conic-gradient(from 90deg, currentColor 0deg, transparent 337deg, transparent 360deg);
  -webkit-mask: radial-gradient(farthest-side, transparent 60%, #000 60%);
          mask: radial-gradient(farthest-side, transparent 60%, #000 60%);
  animation: var(--pattern-spin);
}

/* Rounded head, the cap the component's arc carries. Its diameter is the
   band width, so it sits inside the ring and the mask never clips it. */
.button__spinner::after {
  content: '';
  position: absolute;
  top: 40%;
  right: 0;
  width: 20%;
  height: 20%;
  border-radius: 50%;
  background: currentColor;
}

.button--m .button__spinner,
.button--s .button__spinner   { width: var(--sp-s16); height: var(--sp-s16); }
.button--cta .button__spinner { width: var(--sp-s32); height: var(--sp-s32); }

.button--loading.button--l,
.button--loading.button--m { width: var(--sp-s72); }
.button--loading.button--s { width: var(--sp-s64); }

/* ── Disabled: a token swap for every style, never opacity ── */
.button:disabled,
.button--disabled {
  background: var(--surface-on-white);
  color: var(--text-and-icon-disabled);
  cursor: default;
}
.button:disabled:active,
.button--disabled:active { transform: none; }

/* ── Skeleton: the shape alone, filled by the Skeleton component.
      Width is fixed in Figma rather than hugging — 72 on L and M, 64 on
      S — because there is no label left to hug. ── */
.button--skeleton {
  background: transparent;
  padding: var(--sp-s0);
  overflow: hidden;
  cursor: default;
}
.button--skeleton.button--l,
.button--skeleton.button--m { width: var(--sp-s72); }
.button--skeleton.button--s { width: var(--sp-s64); }
.button--skeleton .skeleton {
  width: 100%;
  height: 100%;
  border-radius: var(--sp-s20);
}
.button--skeleton:active { transform: none; }

/* ═════════════════════════════════════════════════════════
   RadioButton — spec specs/components/radiobutton/radiobutton.md
   Figma [RadioButton] 3.1 (node 17855:6503). Three layers, mirroring the
   Figma component chain exactly:
     .radiobutton       s48 x s48 touch target (WCAG 2.5.5) — the focusable element
     .radiobutton__box  s24 x s24 round box, radius s12, s2 border  (.RadioNoSafezone)
     .radiobutton__dot  s12 dot, radius s6                          (radio-icon)
   States are State x Disabled (4), matching the Figma variant matrix. The
   Unselected variant holds the same dot layer collapsed to zero radius at
   zero opacity, so both variants are one geometry with two dot sizes.
   ──────────────────────────────────────────────── */

.radiobutton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s48);
  height: var(--sp-s48);
  padding: var(--sp-s0);
  border: none;
  background: none;
  cursor: pointer;
  flex-shrink: 0;
}

.radiobutton__box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s24);
  height: var(--sp-s24);
  border-radius: var(--sp-s12);
  border: var(--sp-s2) solid var(--text-and-icon-secondary);
  background: transparent;
  box-sizing: border-box;
  flex-shrink: 0;
  /* Unselected <-> Selected — motion-rules.md §6.6: Transforming/State/Default */
  transition:
    background-color var(--transforming-state-default),
    border-color var(--transforming-state-default);
}

/* The dot grows from the centre; scale + opacity are the only animated
   properties, so nothing reflows while a group's selection moves. */
.radiobutton__dot {
  width: var(--sp-s12);
  height: var(--sp-s12);
  border-radius: var(--sp-s6);
  background: var(--text-and-icon-inverse-primary);
  flex-shrink: 0;
  opacity: 0;
  transform: scale(0);
  transition:
    opacity var(--transforming-state-default),
    transform var(--transforming-state-default);
}

/* Selected — filled box, dot at full size. Border matches the fill so the box
   keeps its s24 outer size across states. */
.radiobutton--selected .radiobutton__box {
  background: var(--text-and-icon-primary);
  border-color: var(--text-and-icon-primary);
}
.radiobutton--selected .radiobutton__dot {
  opacity: 1;
  transform: scale(1);
}

/* Disabled — one token swap per state; never opacity (component-authoring). */
.radiobutton--disabled { cursor: not-allowed; }
.radiobutton--disabled .radiobutton__box { border-color: var(--text-and-icon-disabled); }
.radiobutton--selected.radiobutton--disabled .radiobutton__box {
  background: var(--text-and-icon-disabled);
  border-color: var(--text-and-icon-disabled);
}

/* The ring follows the s48 target; s24 radius keeps it round like the control. */
.radiobutton:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(var(--sp-s2) * -1);
  border-radius: var(--sp-s24);
}

/* Motion: Transforming/State/Default — the catalogued token for selection
   controls, in both directions (motion-rules.md §6.6). Declared DS-wide in
   tokens/generated/motion.css and zeroed there under reduced motion, so this
   component only consumes it. */

/* ════════════════════════════════════════════════
   SlidingButton — spec specs/components/sliding-button/sliding-button.md
   Figma [SlidingButton] 3.0 (node 27954:1393). A swipe-to-confirm track:
     .sliding-button              the track — radius s20, background InversePrimary
     .sliding-button__background  the fill layer (z0)
     .sliding-button__slider      the draggable chip (z2), CustomSquircle base (.squircle), fill Brand, pinned to the start edge
     .sliding-button__start-slot  the icon inside the Slider (forward-arrow-right)
     .sliding-button__text        the StartText block (z3) — Title + Subtitle, fills the width (Track reserves s68), truncated
     .sliding-button__title       centred label, crosses to AlwaysDark
     .sliding-button__subtitle    centred second line
     .sliding-button__loader      the Loading spinner (z4), Loader size L (s32), inlined like Button
     .sliding-button__check       the Success glyph (z4)
     .sliding-button__close       the Error glyph (z4), close in AlwaysLight, s32
   States: Standard, On tap, On drag (--covered at 50%), Loading, Success,
   Error, Disabled, Skeleton. Motion consumes DS tokens only, named per the Figma
   Animation model (--transforming-state-fast + --pattern-scale-short-hide on tap,
   --curve-linear for the drag, --pattern-scale-medium-appear on the on-drag return,
   --component-fade-m-* for the result swaps, --pattern-color, --pattern-spin),
   declared and reduced-motion-zeroed in tokens/generated/motion.css.
   ──────────────────────────────────────────────── */

.sliding-button {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: var(--sp-s64);
  border-radius: var(--sp-s20);
  background: var(--background-inverse-primary);
  overflow: hidden;
  isolation: isolate;
  border: none;
  padding: var(--sp-s0);
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-y;
}

/* The fill layer — a named element so the anatomy has all six parts; it carries
   the track colour so the root stays a pure positioning context. */
.sliding-button__background {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--background-inverse-primary);
  border-radius: inherit;
  /* No transition by default: entering Loading must snap the fill to Brand, or
     the dark InversePrimary shows through the s4 border as a flash. The colour
     change is transitioned only in Error and on the Error return (below). */
}

/* The track morphs from InversePrimary toward Brand as the drag crosses the
   track: a Brand layer crossfades in, its opacity driven by --sb-bg-progress
   (0 → 1 mapped from 30% → 80% of the width by the drag handler). It returns to
   0 on release or a reverse drag, transitioning on Patterns/Color. */
.sliding-button__background::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--background-brand);
  opacity: var(--sb-bg-progress, 0);
  transition: opacity var(--pattern-color);
}
/* While dragging the morph follows the finger 1:1 — the transition is only for
   the animated return on release / reverse drag. */
.sliding-button--dragging .sliding-button__background::after { transition: none; }

/* StartText — the Title + Subtitle block, centred ABOVE the Slider (z3 > z2) so
   it stays visible as the Slider passes over it, crossing to AlwaysDark once the
   Slider covers it at 50% (§5). It fills the track width with the Track reserving
   s68 on each side (its own horizontal padding is s0); each line is single-line
   and truncates. Hidden in Loading, Success and Error (the result glyph takes the
   track). */
.sliding-button__text {
  position: relative;
  z-index: 3;
  align-self: stretch;
  margin-inline: var(--sp-s68);
  /* Horizontal padding s0 (the Track reserves s68 on each side); vertical s4 so
     the label keeps an s4 inset from the Track top/bottom — it never butts the
     edges when it grows past one line. */
  padding: var(--sp-s4) var(--sp-s0);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-s2);
  min-width: 0;
  pointer-events: none;
}
.sliding-button__title,
.sliding-button__subtitle {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-and-icon-inverse-primary);
  transition: color var(--pattern-color);
}
.sliding-button__title {
  font-family: var(--text-heading-heading4-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading4-size);
  font-weight: var(--text-heading-heading4-weight);
  line-height: var(--text-heading-heading4-line-height);
}
.sliding-button__subtitle {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}

/* ── Typography axes (custom base) — the Title and Subtitle text styles are
      replaceable defaults set on the root. Title: Heading 4 (default) · Heading 3
      · Promo Body. Subtitle: Compact Body (default) · Main Body. Family + size +
      weight + line-height move together, per the typography rule. ── */
.sliding-button--title-heading-3 .sliding-button__title {
  font-family: var(--text-heading-heading3-family), 'Noto Sans', sans-serif;
  font-size: var(--text-heading-heading3-size);
  font-weight: var(--text-heading-heading3-weight);
  line-height: var(--text-heading-heading3-line-height);
}
.sliding-button--title-promo-body .sliding-button__title {
  font-family: var(--text-body-promo-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-promo-body-size);
  font-weight: var(--text-body-promo-body-weight);
  line-height: var(--text-body-promo-body-line-height);
}
.sliding-button--subtitle-main-body .sliding-button__subtitle {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
}

/* Slider — the draggable chip, built on the CustomSquircle base (.squircle): the
   base carries the box model (radius s16, flex-centre, overflow, box-sizing) and
   this modifier pins it to the start edge, sizes it, and drives its behaviour. It
   hugs its icon at rest and carries the leading padding step on tap. */
.sliding-button__slider {
  position: absolute;
  top: var(--sp-s4);
  bottom: var(--sp-s4);
  left: var(--sp-s4);
  width: var(--sp-s56);
  height: auto;   /* top / bottom drive the height — override the squircle s48 default */
  z-index: 2;
  justify-content: flex-start;
  padding-inline-start: var(--sp-s16);
  padding-inline-end: var(--sp-s16);
  background: var(--background-brand);
  /* On tap the leading padding grows (Transforming/State/Fast); the fill moves on
     Patterns/Color. Width / left are NOT transitioned here — during a drag the
     Slider must track the finger 1:1. */
  transition:
    padding-inline-start var(--transforming-state-fast),
    background-color var(--pattern-color);
}

/* Width / left animate only when NOT dragging — the timed transitions are for
   the Success fill and the snap-back / Error return, never for finger tracking. */
.sliding-button:not(.sliding-button--dragging) .sliding-button__slider {
  transition:
    padding-inline-start var(--transforming-state-fast),
    background-color var(--pattern-color),
    width var(--pattern-scale-medium-appear),
    left var(--pattern-scale-medium-appear);
}

/* The icon travels with the Slider; on the brand fill it is AlwaysDark. */
.sliding-button__start-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s24);
  height: var(--sp-s24);
  flex-shrink: 0;
  color: var(--text-and-icon-always-dark);
  transition: opacity var(--component-fade-m-appear);
}
.sliding-button__start-slot svg { display: block; }
.sliding-button__start-slot svg path { fill: currentColor; }

/* Loader (Success/Error spinner) and Check — centred over the track, hidden by
   default, faded in per state. The Loader is an inline arc rotating on
   Patterns/Spin, reproduced like Button until Loader ships its own spec. */
.sliding-button__loader,
.sliding-button__check,
.sliding-button__close {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 4;
  width: var(--sp-s32);
  height: var(--sp-s32);
  transform: translate(-50%, -50%);
  color: var(--text-and-icon-always-dark);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--component-fade-m-appear);
}
/* All three result glyphs are s32 (Loader size L). The close glyph is AlwaysLight
   on the red fill; Loader and check stay AlwaysDark on the brand fill. */
.sliding-button__close { color: var(--text-and-icon-always-light); }
.sliding-button__loader svg { display: block; width: 100%; height: 100%; animation: var(--pattern-spin); }
.sliding-button__check svg,
.sliding-button__close svg { display: block; width: 100%; height: 100%; }
.sliding-button__check svg path,
.sliding-button__close svg path { fill: currentColor; }

/* ── On tap — the leading padding grows AND widens the Slider (s56 → s72) ── */
.sliding-button--on-tap { cursor: grabbing; }
.sliding-button--on-tap .sliding-button__slider {
  padding-inline-start: var(--sp-s32);
  width: var(--sp-s72);
}

/* ── On drag — the Slider covers the label past 50%, so it recolours ── */
.sliding-button--covered .sliding-button__title,
.sliding-button--covered .sliding-button__subtitle { color: var(--text-and-icon-always-dark); }

/* While dragging, the icon rides the leading edge — it travels in the drag
   direction as the Slider stretches, instead of staying pinned at the start. */
.sliding-button--drag-arrow .sliding-button__slider { justify-content: flex-end; }
.sliding-button--rtl.sliding-button--drag-arrow .sliding-button__slider { justify-content: flex-start; }

/* ── Loading / Success / Error — the Slider fills the track (keeping the s4
      inset), and the track fill under the s4 border matches the Slider so no
      seam shows. The morph layer is dropped in these states. ── */
.sliding-button--loading .sliding-button__slider,
.sliding-button--success .sliding-button__slider,
.sliding-button--error .sliding-button__slider {
  left: var(--sp-s4);
  right: var(--sp-s4);
  width: auto;
  padding-inline-start: var(--sp-s0);
  padding-inline-end: var(--sp-s0);
  justify-content: center;
}
.sliding-button--loading .sliding-button__background,
.sliding-button--success .sliding-button__background { background: var(--background-brand); }
.sliding-button--loading .sliding-button__background::after,
.sliding-button--success .sliding-button__background::after,
.sliding-button--error .sliding-button__background::after { opacity: 0; }
.sliding-button--loading .sliding-button__start-slot,
.sliding-button--success .sliding-button__start-slot { opacity: 0; }
.sliding-button--loading .sliding-button__title,
.sliding-button--loading .sliding-button__subtitle,
.sliding-button--success .sliding-button__title,
.sliding-button--success .sliding-button__subtitle { opacity: 0; }
.sliding-button--loading .sliding-button__loader { opacity: 1; }

/* ── Success — the check replaces the spinner over the full brand fill ── */
.sliding-button--success .sliding-button__loader { opacity: 0; }
.sliding-button--success .sliding-button__check { opacity: 1; }

/* ── Error — the fill and the track turn Accent/Red1 and a single close glyph
      (AlwaysLight, s32) appears; the label and the arrow are hidden, like Success ── */
.sliding-button.sliding-button--error .sliding-button__slider {
  background: var(--accent-red1);
  /* Loading → Error: the tint to Accent/Red1 eases in with the fadeM result swap. */
  transition: background-color var(--component-fade-m-appear);
}
.sliding-button--error .sliding-button__background {
  background: var(--accent-red1);
  transition: background-color var(--component-fade-m-appear);
}
.sliding-button--error .sliding-button__loader,
.sliding-button--error .sliding-button__start-slot,
.sliding-button--error .sliding-button__title,
.sliding-button--error .sliding-button__subtitle { opacity: 0; }
.sliding-button--error .sliding-button__close { opacity: 1; }

/* ── Error → Default: the close glyph fades out and the Slider retracts smoothly
      to the start edge on fadeM (never a hard state swap), the red fill fading out
      on the same token. The width is animated from a frozen px value in JS — CSS
      cannot transition to/from `width: auto`, which is why the retract is
      script-driven. ── */
.sliding-button.sliding-button--returning .sliding-button__slider {
  transition:
    width var(--component-fade-m-hide),
    background-color var(--component-fade-m-hide);
}
/* The track fill fades red → InversePrimary as the Slider retracts. */
.sliding-button--returning .sliding-button__background {
  transition: background-color var(--component-fade-m-hide);
}

/* ── Disabled — token swap, never opacity (component-authoring) ── */
.sliding-button--disabled {
  cursor: not-allowed;
  background: var(--surface-on-white);
}
.sliding-button--disabled .sliding-button__background { background: var(--surface-on-white); }
.sliding-button--disabled .sliding-button__slider { background: var(--surface-on-grey); }
.sliding-button--disabled .sliding-button__start-slot { color: var(--text-and-icon-disabled); }
.sliding-button--disabled .sliding-button__title,
.sliding-button--disabled .sliding-button__subtitle { color: var(--text-and-icon-disabled); }

/* ── Skeleton — the state hosts a Skeleton instance (.skeleton) filling the
      track shape (radius s20); the Slider, label, icon, result glyphs and the
      track fill are removed behind it. ── */
.sliding-button--skeleton { cursor: default; background: transparent; }
.sliding-button--skeleton .sliding-button__background,
.sliding-button--skeleton .sliding-button__slider,
.sliding-button--skeleton .sliding-button__text,
.sliding-button--skeleton .sliding-button__title,
.sliding-button--skeleton .sliding-button__subtitle,
.sliding-button--skeleton .sliding-button__loader,
.sliding-button--skeleton .sliding-button__check,
.sliding-button--skeleton .sliding-button__close { display: none; }
/* The hosted Skeleton — mounted in every instance, shown only in Skeleton. */
.sliding-button__skeleton {
  position: absolute;
  inset: 0;
  z-index: 5;
  border-radius: var(--sp-s20);
  display: none;
}
.sliding-button--skeleton .sliding-button__skeleton { display: block; }

/* ── Focus ring — follows the track, radius s20, inset so it never spills ── */
.sliding-button:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(var(--sp-s2) * -1);
  border-radius: var(--sp-s20);
}

/* ── RTL — the Slider rests at the end edge and the directional icon flips ── */
.sliding-button--rtl .sliding-button__slider { left: auto; right: var(--sp-s4); justify-content: flex-end; }
.sliding-button--rtl.sliding-button--loading .sliding-button__slider,
.sliding-button--rtl.sliding-button--success .sliding-button__slider,
.sliding-button--rtl.sliding-button--error .sliding-button__slider { left: var(--sp-s4); right: var(--sp-s4); }
.sliding-button--rtl .sliding-button__start-slot svg { transform: scaleX(-1); }

/* ══════════════════════════════════════════════════════════════════
   Floating Button — spec: specs/components/floating-button/floating-button.md (3.3.0)
   Figma [FloatingButton] 3.3 (node 16356:7288). One control on a Component
   axis: Squircle — an icon alone in a square — or Button, the square opened
   out to carry a label beside that icon. Height, radius, elevation and
   border hold across the change; only the width and the label arrive or
   leave, which is what keeps the two forms one component rather than two.
   Slots follow the DS-wide StartSlot / StartText / EndSlot semantics.

   Radius is s20 at both sizes, so L collapsed is Squircle L exactly (s56 at
   s20) while M collapsed is NOT Squircle M (s48 at s16) — the variant is
   named for the shape it reads as, not for the component. Nothing here is
   an instance of Squircle.
   ══════════════════════════════════════════════════════════════════ */

/* Motion: pushButton for the press, Transforming/State/Slow for the
   Squircle <-> Button opening (300ms — one step slower than State/Default,
   owner call 2026-08-05: the stretch wants more room than a state flip).
   Both come from tokens/generated/motion.css —
   declared once DS-wide and zeroed there under reduced motion, so this
   component neither redeclares them nor carries an override of its own.
   The press moves the scale alone: no style defines a pressed fill, so
   background-color is deliberately NOT in the transition — and neither is
   color, because Standard <-> Disabled is a swap, not an animation (owner
   decision, 2026-08-05).

   OPENING (spec §5): width travels on the state token via
   interpolate-size: allow-keywords, and the label collapses to zero width
   and opacity on the same token. One markup serves both forms — a closed
   control may keep StartText mounted; :not(--open) collapses it, which is
   also what keeps the icon truly centred in the square. Every animated
   piece (root width/padding/gap, slot padding, label width/padding/opacity)
   rides the SAME token, so the root's hug width equals the sum of its parts
   at every frame. Browsers without interpolate-size (and reduced motion)
   snap between the two forms — the correct degradation. */

.floating-button {
  /* The Indicator overlay pins to this box, so the root is the containing block. */
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-s0);
  padding: var(--sp-s0);
  border: none;
  border-radius: var(--sp-s20);
  background: var(--surface-floating);
  color: var(--text-and-icon-primary);
  /* Elevation, plus the edge as an INSET RING rather than a `border`.
     The hairline has no SP token (the scale runs s0 -> s2), so 1px is the
     DS-wide literal here, as on Squircle and Cell — but it cannot consume
     layout: Figma measures the s12 padding from the frame's bounds and draws
     the stroke inside them, so a real `border` pushes the icon to 17 and the
     label to 21 and breaks the 16 / 20 optical contract §3 states (measured).
     An inset shadow draws the same edge and stays out of the box model.
     Border/Transparent rather than no edge at all: it holds the control
     against a surface of any colour, which a floating element cannot assume,
     and it is the fallback where the elevation disappears into a dark
     background (floating-button-a11y.md § Contrast). */
  box-shadow: var(--shadow-s), inset 0 0 0 1px var(--border-transparent);
  cursor: pointer;
  white-space: nowrap;
  box-sizing: border-box;
  /* 56 <-> auto interpolates only with this; inherited by the label. */
  interpolate-size: allow-keywords;
  transition: transform var(--component-push-button-release),
              width var(--transforming-state-slow),
              padding var(--transforming-state-slow),
              gap var(--transforming-state-slow);
}

.floating-button:active {
  transform: scale(0.95);
  transition: transform var(--component-push-button-press),
              width var(--transforming-state-slow),
              padding var(--transforming-state-slow),
              gap var(--transforming-state-slow);
}

/* ── Sizes. Collapsed the width equals the height: the square comes from the
      size with s0 padding, not from padding around the icon. ── */
.floating-button--l { width: var(--sp-s56); height: var(--sp-s56); }
.floating-button--m { width: var(--sp-s48); height: var(--sp-s48); }

/* ── Component=Button: the square opened out. Width hugs the label, height
      and radius are untouched. ── */
.floating-button--open {
  width: auto;
  gap: var(--sp-s4);
  padding-inline: var(--sp-s12);
}

/* Closed, the label is collapsed rather than removed — zero width, zero
   padding, zero opacity — so one markup opens and closes on the state token
   and the icon centres in the square with StartText still mounted. */
.floating-button:not(.floating-button--open) .floating-button__start-text {
  width: 0;
  padding-inline: var(--sp-s0);
  opacity: 0;
}

/* ── Slots ── */
.floating-button__start-slot,
.floating-button__end-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--sp-s24);
  flex-shrink: 0;
  transition: padding var(--transforming-state-slow);
}
.floating-button__start-slot > *,
.floating-button__end-slot > * { width: var(--sp-s24); height: var(--sp-s24); display: block; }

/* Optical balance, the same contract the Button presets use (button.md §8):
   s4 on the outer edge of a side slot and s8 on StartText turn the s12 root
   padding into an effective s16 to an icon and s20 to a label — which is why
   a one-slot control still reads centred. Collapsed, both inner paddings are
   absent along with the label and the s24 icon centres on its own. */
.floating-button--open .floating-button__start-slot { padding-inline-start: var(--sp-s4); }
.floating-button--open .floating-button__end-slot   { padding-inline-end: var(--sp-s4); }

.floating-button__start-text {
  display: flex;
  flex-direction: column;
  /* Anchored to the reading edge, NOT centred: while the column's width
     animates, a centred row's left edge travels half the growth — the label
     visibly slides out from under the icon. Anchored, the glyphs hold still
     and the growing column REVEALS them (flex-start follows the text
     direction, so RTL anchors right). Trade-off, recorded in the spec: with
     two rows of different widths they now align to the edge, not mutually
     centred. */
  align-items: flex-start;
  gap: var(--sp-s2);
  padding-inline: var(--sp-s8);
  /* Lets a long label truncate rather than push the control wider. */
  min-width: 0;
  /* Clipped at all times, so nothing paints outside the column mid-flight. */
  overflow: hidden;
  /* The opening: the column's width and opacity travel together (spec §5). */
  transition: width var(--transforming-state-slow),
              padding var(--transforming-state-slow),
              opacity var(--transforming-state-slow);
}

/* Title is Heading 4 at BOTH sizes — unlike Button, the label does not step
   down with the size, because M differs from L in the room around it rather
   than in the weight of what it says (spec §7). A row holds one line and
   truncates to an ellipsis; a label that would wrap belongs outside. */
.floating-button__title-row {
  font-family: var(--text-heading-heading4-family);
  font-weight: var(--text-heading-heading4-weight);
  font-size: var(--text-heading-heading4-size);
  line-height: var(--text-heading-heading4-line-height);
  overflow: hidden;
  text-overflow: ellipsis;
}
.floating-button__subtitle-row {
  font-family: var(--text-body-compact-body-family);
  font-weight: var(--text-body-compact-body-weight);
  font-size: var(--text-body-compact-body-size);
  line-height: var(--text-body-compact-body-line-height);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Styles: fill and content per spec §6. Error reads as a tinted icon or
      label on the ordinary surface rather than a red block, so a destructive
      floating action stays legible without shouting. Inverse is the one style
      that changes the surface. ── */
.floating-button--primary { background: var(--surface-floating);         color: var(--text-and-icon-primary); }
.floating-button--error   { background: var(--surface-floating);         color: var(--text-and-icon-error); }
.floating-button--inverse { background: var(--background-inverse-primary); color: var(--text-and-icon-inverse-primary); }

/* ── Indicator: an overlay the size of the root pinning a s12 Indicator to
      the top-right corner. Off by default. It does not take the press, so it
      stays out of pointer events. ── */
.floating-button__indicator {
  position: absolute;
  inset: var(--sp-s0);
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  pointer-events: none;
}

/* Focus ring — floating-button-a11y.md § Focus. s2 in TextAndIcon/Primary at
   radius s20 so it follows the corner. Inset, so it stays inside the control
   rather than sitting on the elevation where the shadow would compete. */
.floating-button:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(var(--sp-s2) * -1);
  border-radius: var(--sp-s20);
}

/* Inverse takes its own ring colour: the default ring is the same primitive
   as the inverse FILL in Dark (both white) and a neighbouring grey in Light —
   invisible either way (a11y-reviewer catch, 2026-08-06). The style's content
   colour is the one token guaranteed to contrast with its fill. */
.floating-button--inverse:focus-visible {
  outline-color: var(--text-and-icon-inverse-primary);
}

/* ── Disabled: every Style collapses onto one appearance — a token swap, never
      opacity. Inverse loses its dark fill as well, so a disabled Inverse is a
      light control with disabled content, not a dark one. ── */
.floating-button:disabled,
.floating-button--disabled {
  background: var(--surface-floating);
  color: var(--text-and-icon-disabled);
  cursor: default;
}
.floating-button:disabled:active,
.floating-button--disabled:active { transform: none; }

/* ── Skeleton: the shape alone, filled by the Skeleton component. The border
      and the elevation stay — spec §6 gives both to every state. Collapsed it
      is the square; opened the width is fixed rather than hugging, since there
      is no label left to hug. ── */
.floating-button--skeleton {
  background: transparent;
  /* The placeholder is the whole shape, so the opened form's s12 padding goes
     away with the label it was holding — otherwise the Skeleton renders 24
     narrower than the control it stands in for (measured: 96 inside 120). */
  padding: var(--sp-s0);
  overflow: hidden;
  cursor: default;
}
/* L is 120. M's opened width is NOT captured from Figma yet (spec §4), so M
   keeps hugging rather than taking an invented number — token-usage forbids
   approximating a value that does not exist in the DS. When it is read, it
   joins this rule as .floating-button--skeleton.--open.--m. */
.floating-button--skeleton.floating-button--open.floating-button--l { width: var(--sp-s120); }
.floating-button--skeleton .skeleton {
  width: 100%;
  height: 100%;
  border-radius: var(--sp-s20);
}
.floating-button--skeleton:active { transform: none; }

/* ── RTL (opt-in) — the component carries its own RTL axis, off by default.
      Opt in with [dir="rtl"] on the root or .floating-button--rtl.
      NOTE: row-reverse applies ONLY to the class path — with [dir="rtl"] the
      rtl writing direction already reverses the flex row, and adding
      row-reverse on top double-flips back to LTR. The same holds for the
      Indicator: flex-end already resolves to the left edge under [dir="rtl"].
      The s12 root padding is symmetric, so it needs no mirroring. ── */
.floating-button--rtl:not([dir="rtl"]) { flex-direction: row-reverse; }
.floating-button--rtl:not([dir="rtl"]) .floating-button__indicator { justify-content: flex-start; }

/* ═════════════════════════════════════════════════════════
   DestinationButton — spec specs/components/destination-button/destination-button.md
   Figma [DestinationButton] (set 30339:368, branch ruoGXrpbw0vbfgl8bdfPcj).
   A locked customButton configuration: ONE size (s64 tall, full width), one
   fill (Surface/OnWhite), State = Standard | Skeleton — no Style axis and no
   Disabled form. Three slots on one row with space-between, both side slots
   s64 x s56: the modified-size IconContainer (an s32 glyph centred in the
   box) on the LEADING edge, the 3D service image on the TRAILING edge, and
   the two centred text rows between them. The sides were swapped and the
   end slot grew from s40 in the 2026-08-24 design update.
   ──────────────────────────────────────────────── */

.destination-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-s12);
  width: 100%;
  height: var(--sp-s64);
  border: none;
  border-radius: var(--sp-s20);
  background: var(--surface-on-white);
  color: var(--text-and-icon-primary);
  cursor: pointer;
  box-sizing: border-box;
  text-align: center;
  /* pushButton — motion-rules.md § 6.1. Only transform moves, so the press
     stays on the compositor. Reduced motion is zeroed at the token, so this
     component carries no override of its own. */
  transition: transform var(--component-push-button-release);
}

.destination-button:active {
  transform: scale(0.95);
  transition: transform var(--component-push-button-press);
}

/* ── Slots ──
   Both side slots are s64 x s56 inside the s64 row. The row declares NO
   padding — the slots centre on the row's cross axis, and the 4 of air above
   and below each is centring arithmetic (64 − 56), not an inset. flex-shrink: 0
   holds them at their size; the text column is what gives way when the label
   is long.

   The two slots differ only in what fills them. The start slot is a
   modified-size IconContainer: the BOX is stretched to s64 x s56 while the
   glyph keeps its s32 box, centred (Figma
   `modified-size-icon`). The end slot hands its whole box to the image, which
   is why the artwork reaches the trailing edge. */
.destination-button__start-slot,
.destination-button__end-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s64);
  height: var(--sp-s56);
  flex-shrink: 0;
}
/* The glyph keeps its own IconContainer box inside the stretched slot. */
.destination-button__start-slot > * {
  display: block;
  width: var(--sp-s32);
  height: var(--sp-s32);
}
/* ImgContainer — the artwork fills the box and keeps its aspect.

   The illustration is MIRRORED, in every variation: the Figma illustration
   container ships with its RTL toggle on, which turns the vehicle to face the
   text column instead of the row's trailing edge. As with a directional glyph
   the asset file is never edited — the mirror is a scaleX(-1) on the consuming
   component. It is baked into the artwork, not derived from the document's
   direction, so it does not flip a second time under [dir="rtl"]. */
.destination-button__end-slot > * {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform: scaleX(-1);
}

/* Text column — the rows are centred on each other and in the space between
   the two slots (spec § 7). min-width: 0 lets them truncate instead of
   pushing a slot out of the row. */
.destination-button__start-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-s2);
  min-width: 0;
}

.destination-button__title-row {
  font-family: var(--text-heading-heading2-family);
  font-weight: var(--text-heading-heading2-weight);
  font-size: var(--text-heading-heading2-size);
  line-height: var(--text-heading-heading2-line-height);
  letter-spacing: var(--text-heading-heading2-letter-spacing);
}

.destination-button__subtitle-row {
  font-family: var(--text-body-compact-body-family);
  font-weight: var(--text-body-compact-body-weight);
  font-size: var(--text-body-compact-body-size);
  line-height: var(--text-body-compact-body-line-height);
  letter-spacing: var(--text-body-compact-body-letter-spacing);
  color: var(--text-and-icon-secondary);
}

/* The row height is fixed, so both text rows stay on one line and truncate. */
.destination-button__title-row,
.destination-button__subtitle-row {
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Focus ring — destination-button-a11y.md § Focus. s2 in TextAndIcon/Primary at the
   row's own s20 radius, offset inward so a full-width row cannot push the
   ring past its container. */
.destination-button:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(var(--sp-s2) * -1);
  border-radius: var(--sp-s20);
}

/* ── Skeleton: the shape alone, filled by the Skeleton component. The row
      keeps its height and its width and drops the content it
      was holding, so the placeholder is the whole shape — the same way
      .button--skeleton and .floating-button--skeleton do it. ── */
.destination-button--skeleton {
  background: transparent;
  overflow: hidden;
  cursor: default;
}
.destination-button--skeleton .skeleton {
  width: 100%;
  height: 100%;
  border-radius: var(--sp-s20);
}
.destination-button--skeleton:active { transform: none; }

/* ── RTL — the row mirrors from the document's dir: the flex row reverses on
      its own and the row is symmetric (it declares no padding), so no class
      is needed. Nothing flips in place: the default start-slot icon is
      `search`, which is NOT on the icon-container.md § 3.5 directional list —
      a magnifier reads the same in both directions — and the end-slot
      illustration keeps the scaleX(-1) it ships with in every variation (the
      mirror belongs to the artwork, not to the direction). Swapping in a
      directional glyph (a chevron, forward-arrow-right) brings a directional
      scaleX(-1) back, at the instance, per that section. The mirrored
      composition is a variant of the Figma set (RTL: Off | On). ── */

/* ════════════════════════════════════════════════
   Notification component — spec: specs/components/notification/notification.md
   Transient toast pill sliding in from the top of the screen: one short
   message, optional leading icon, auto-dismiss 4 s, swipe-up dismiss, one on
   screen at a time. The root spans the full width and owns the s8 side
   margins; the pill (ContentContainer) carries the tone fill and radius s20
   at a fixed s56 height. Neutral inverts with the theme
   (Background/InversePrimary); Positive / Negative are accent fills with
   fixed-light content. Enter/exit consume Transforming/Enter/Exit Default
   directly — the HeaderAlert timing (owner, 2026-09-03: retimed from moveM
   400ms). Reduced motion is already zeroed at the token (motion.css), so the
   component ships no override of its own.
   ════════════════════════════════════════════════ */
.notification {
  width: 100%;
  padding-left: var(--sp-s8);
  padding-right: var(--sp-s8);
  box-sizing: border-box;
  /* Enter — Transforming/Enter/Default; the integrator (or the page demo)
     toggles the off-screen class, the component ships the timing. transform only. */
  transition: transform var(--transforming-enter-default);
}
/* The pill. Height FIXED at s56 (Figma: the container does not grow with a
   second line — spec § 8). s16 padding all round: the s24 icon + 2 x s16 = 56;
   a 2-line clamped text (32) centres within it, overflowing the content box
   symmetrically so the visual air compresses 16 → 12. In the 130/150 % scale
   modes the s56 token itself scales, making room for a third line. */
.notification__content {
  display: flex;
  align-items: center;
  gap: var(--sp-s12);
  height: var(--sp-s56);
  padding: var(--sp-s16);
  border-radius: var(--sp-s20);
  background: var(--background-inverse-primary);   /* Neutral — the base tone */
  color: var(--text-and-icon-inverse-primary);     /* children inherit via currentColor */
  box-sizing: border-box;
}
/* Tone modifiers — swap the fill and pin the content to fixed light */
.notification--positive .notification__content {
  background: var(--accent-green2);
  color: var(--text-and-icon-always-light);
}
.notification--negative .notification__content {
  background: var(--accent-red2);
  color: var(--text-and-icon-always-light);
}
/* Leading icon slot — an IconContainer: invisible box, s24 glyph painted via
   currentColor. Per-style default glyphs (warning / done / warning-tr —
   non-directional, never mirrored); Show Icon off removes the slot node. */
.notification__icon {
  width: var(--sp-s24);
  height: var(--sp-s24);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.notification__icon svg { width: 100%; height: 100%; }
/* Description — Compact Body. Clamped to 2 lines at 100 % scale with an
   ellipsis (the 130/150 % accessibility modes lift the clamp — spec § 7). */
.notification__description {
  flex: 1;
  min-width: 0;
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  word-break: break-word;
}
/* Exit — Transforming/Exit/Default: off-screen past the top edge (own height
   + the top gap). Removing the class re-enters with the enter timing above. */
.notification--off {
  transform: translateY(calc(-100% - var(--sp-s16)));
  transition: transform var(--transforming-exit-default);
}
/* RTL — the flex row mirrors from the document's dir on its own; the default
   glyphs are not on the icon-container.md § 3.5 directional list, so nothing
   flips in place. The mirrored composition is a variant of the Figma set
   (RTL: Off | On). */

/* ════════════════════════════════════════════════
   Snackbar component — spec: specs/components/snackbar/snackbar.md
   The actionable toast, sliding in from the BOTTOM edge on the Notification
   behaviour contract (4 s auto-dismiss, swipe dismiss, replace never stack).
   Root spans the full width and owns the s16 side margins; the pill carries
   the tone fill at radius s20 with s8 padding and FIXED heights — s56, or
   108 with the button docked below (8 + 40 + 8 + 40 + 12). Inform inverts
   with the theme; Custom takes an instance-set semantic pair. The
   action is the OPEN customButton base configured by the snackbar: s40, s12
   side padding, Heading 4 label (the .button base default), and the
   snackbar-owned pair Background/Primary + TextAndIcon/Primary — deliberately
   counter-phase to the inverse pill. Enter/exit consume Transforming
   Enter/Exit Default; reduced motion is zeroed at the token.
   ════════════════════════════════════════════════ */
.snackbar {
  width: 100%;
  padding-left: var(--sp-s16);
  padding-right: var(--sp-s16);
  box-sizing: border-box;
  /* Enter — the integrator (or the page demo) toggles the off-screen class,
     the component ships the timing. transform only. */
  transition: transform var(--transforming-enter-default);
}
/* The pill. s8 padding all round; the s40 content row centres in the fixed
   s56 (8 + 40 + 8). Container gaps are s4 — with the slots' own s8 insets
   they compose the visual 12s of the design (glyph→text, text→button). */
.snackbar__content {
  display: flex;
  align-items: center;
  gap: var(--sp-s4);
  height: var(--sp-s56);
  padding: var(--sp-s8);
  border-radius: var(--sp-s20);
  background: var(--background-inverse-primary);   /* Inform — the base tone */
  color: var(--text-and-icon-inverse-primary);     /* children inherit via currentColor */
  box-sizing: border-box;
}
/* Custom tone — the instance supplies a semantic pair via the two private
   vars (never raw values): style="--snackbar-fill: var(--…); --snackbar-content: var(--…)".
   Errors are NOT a snackbar tone — they surface as a Notification at the top
   of the screen, where they are seen (owner, 2026-09-04). */
.snackbar--custom .snackbar__content {
  background: var(--snackbar-fill, var(--background-inverse-primary));
  color: var(--snackbar-content, var(--text-and-icon-inverse-primary));
}
/* The icon + text row (Figma icon+text). Fills the width the button leaves. */
.snackbar__row {
  display: flex;
  align-items: center;
  gap: var(--sp-s4);
  flex: 1;
  min-width: 0;
  height: var(--sp-s40);
}
/* Leading icon slot — an IconContainer: s24 glyph in a s32 x s40 box with an
   s8 inline-start inset, painted via currentColor. Show Icon off removes it. */
.snackbar__icon {
  width: var(--sp-s32);
  height: var(--sp-s40);
  padding-inline-start: var(--sp-s8);
  box-sizing: border-box;
  flex-shrink: 0;
  display: flex;
  align-items: center;
}
.snackbar__icon svg { width: var(--sp-s24); height: var(--sp-s24); }
/* Description — Compact Body with its own s8 side insets (the Figma
   textContainer pads). Clamped to 2 lines at 100 % scale, centring within
   the fixed row instead of growing it; the 130/150 % modes lift the clamp
   and scale the height tokens. */
.snackbar__description {
  flex: 1;
  min-width: 0;
  padding-inline: var(--sp-s8);
  box-sizing: border-box;
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  word-break: break-word;
}
/* The action — the open customButton base (.button carries the Heading 4
   title and the pushButton press), configured by the snackbar: S geometry
   and the counter-phase colour pair. Declared after the Button block so the
   overrides win at equal specificity. */
.snackbar__button {
  height: var(--sp-s40);
  padding: 0 var(--sp-s12);
  border-radius: var(--sp-s10);   /* the instance overrides the base's s20 — at 40 tall that would read as a capsule */
  background: var(--background-primary);
  color: var(--text-and-icon-primary);
  flex-shrink: 0;
}
/* Button docked below — the COMPUTED state as much as the forced one: Aside
   is a preference, and the component relocates the button down when the
   message stops fitting 2 lines beside it (spec § 5; the reference measure
   lives on the preview page). The pill turns into a column — row, s8 gap,
   button aligned with the TEXT START (icon box s32 + gap s4 + the slot's own
   s8 inset = 44 from the content edge; Figma's indent36s composes with its
   container insets to the same rhythm) — and the fixed height becomes 108 by
   flow: 8 + 40 + 8 + 40 + (8 + 4). */
.snackbar--below .snackbar__content {
  height: auto;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-s8);
  padding-bottom: calc(var(--sp-s8) + var(--sp-s4));
}
.snackbar--below .snackbar__row { flex: 0 0 auto; }
.snackbar--below .snackbar__button {
  align-self: flex-start;
  margin-inline-start: calc(var(--sp-s32) + var(--sp-s4) + var(--sp-s8));
}
/* Exit — Transforming/Exit/Default: off-screen past the BOTTOM edge (own
   height + the bottom gap). Removing the class re-enters with the enter
   timing above. */
.snackbar--off {
  transform: translateY(calc(100% + var(--sp-s16)));
  transition: transform var(--transforming-exit-default);
}
/* RTL — the flex rows mirror from the document's dir on their own; the s8
   icon inset and the Below indent are logical (inline-start) and flip with
   it. warning-outline is not on the icon-container.md § 3.5 directional
   list, so nothing flips in place. The mirrored composition is a variant of
   the Figma set (RTL: Off | On). */

/* ════════════════════════════════════════════════
   ImageContainer primitive — spec: specs/primitives/image-container.md
   A clip-box for catalogue artwork/Lottie: the SOURCE is always the canonical
   file (illu/**, lottie/**), the COMPOSITION is a recipe keyed by
   (size-class × picture). Recipes are DATA mirrored from the Figma master set
   (hand-carried until the recipe sync script lands) — the numbers are artwork
   data and may exist ONLY in the recipe rows below (spec § 4). The container
   is an opaque boundary: consumers pick a size-class and a picture, never
   position the artwork themselves.
   ════════════════════════════════════════════════ */
.image-container {
  position: relative;
  display: block;
  overflow: hidden;
}
.image-container__artwork {
  position: absolute;
  /* Logical end-anchoring: the composition holds under RTL on its own */
  inset-inline-end: var(--img-end);
  bottom: var(--img-bottom);
  width: var(--img-box);
  height: var(--img-box);
}
.image-container__artwork img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The baked flip belongs to the ARTWORK (Figma bakes rotate180+flipY ≡
   scaleX(-1)). Under RTL every illustration mirrors with the layout
   (owner, 2026-09-11) — the baked flip inverts rather than doubles. */
.image-container__artwork--mirror { transform: scaleX(-1); }
[dir="rtl"] .image-container__artwork { transform: scaleX(-1); }
[dir="rtl"] .image-container__artwork--mirror { transform: none; }

/* Recipe rows — (size-class × picture) → {box, bottom, end}. Negative offsets
   bleed past the clipped edge. Source canvas for 3D/vehicle/full is 720×720,
   so box/720 is the render scale. */
.image-container--l[data-picture="economy-white"] { --img-box: 144px; --img-bottom: -25px; --img-end: 3px; }
.image-container--m[data-picture="economy-white"] { --img-box: 118px; --img-bottom: -28px; --img-end: -36px; }
.image-container--s[data-picture="economy-white"] { --img-box: 110px; --img-bottom: -28px; --img-end: -47px; }
.image-container--l[data-picture="Courier2"]      { --img-box: 140px; --img-bottom: -29px; --img-end: 1px; }

/* ════════════════════════════════════════════════
   DecorateContainer primitive — spec: specs/primitives/decorate-container.md
   A hug wrapper that floats up to two decorators above the hosted component
   (Slot#1). The container never affects the hosted component; decorators are
   absolute, anchored per the 3x3 matrix (default top right), offset by the
   instance props --dc-offset-x/y (SP tokens, negative allowed), and enter/exit
   on the Scale Short pattern from the decorator's own center. Logical inline
   anchoring mirrors RTL on its own; vertical never changes.
   ════════════════════════════════════════════════ */
.decorate-container { position: relative; display: inline-flex; }
.decorate-container__slot1 { display: inline-flex; }
.decorate-container__slot2,
.decorate-container__slot3 {
  position: absolute;
  display: inline-flex;
  z-index: 1;
  transition: transform var(--pattern-scale-short-appear);
  /* Default anchor: top right — the spec's default placement */
  inset-inline-end: var(--dc-offset-x, var(--sp-s0));
  top: var(--dc-offset-y, var(--sp-s0));
  /* Scale plays from the decorator's own center (owner, 2026-09-11) */
  transform-origin: center;
}
/* Anchor modifiers. Centering uses per-axis translate vars so h- and v-
   modifiers compose without overwriting each other; horizontal anchoring is
   logical, so RTL mirrors on its own. */
.decorate-container__slot2,
.decorate-container__slot3 { translate: var(--dc-tx, 0) var(--dc-ty, 0); }
.decorate-container__slot2--h-left,
.decorate-container__slot3--h-left {
  inset-inline-end: auto;
  inset-inline-start: var(--dc-offset-x, var(--sp-s0));
}
.decorate-container__slot2--h-center,
.decorate-container__slot3--h-center {
  inset-inline-end: auto;
  inset-inline-start: 50%;
  --dc-tx: -50%;
}
[dir="rtl"] .decorate-container__slot2--h-center,
[dir="rtl"] .decorate-container__slot3--h-center { --dc-tx: 50%; }
.decorate-container__slot2--v-bottom,
.decorate-container__slot3--v-bottom {
  top: auto;
  bottom: var(--dc-offset-y, var(--sp-s0));
}
.decorate-container__slot2--v-center,
.decorate-container__slot3--v-center {
  top: 50%;
  --dc-ty: -50%;
}
/* Exit — Scale Short hide; removing the class re-enters on the appear timing */
.decorate-container__slot2--off,
.decorate-container__slot3--off {
  transform: scale(0);
  transition: transform var(--pattern-scale-short-hide);
}

/* ════════════════════════════════════════════════
   ServiceCard component — spec: specs/components/service-card/service-card.md
   A service tile: fixed-size rounded card (Surface/OnWhite, radius s20), one
   caption title pinned to the top, the service's 3D illustration seated by an
   ImageContainer recipe in a full-bleed underlay. A locked customCard
   configuration promoted into its own component (the DestinationButton
   precedent). Sizes (minimums): L 160×160 (s160), M 160×76, S 76×76.
   Press = pushItem, inherited from the card family;
   reduced motion is zeroed at the token.
   ════════════════════════════════════════════════ */
.service-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: var(--sp-s160);
  min-height: var(--sp-s160);
  border: none;
  padding: var(--sp-s0);
  border-radius: var(--sp-s20);
  background: var(--surface-on-white);
  color: var(--text-and-icon-primary);        /* children inherit via currentColor */
  overflow: hidden;                            /* clips the underlay at the radius */
  cursor: pointer;
  text-align: start;
  transition: transform var(--component-push-item-release);
}
.service-card:active { transform: scale(0.95); transition: transform var(--component-push-item-press); }
/* Sizes are MINIMUMS (the Figma General contract): no explicit width/height,
   so a stretch context (a grid track, flex stretch) may grow the tile past its
   minimum. Each modifier carries its own full min pair. */
.service-card--l { min-width: var(--sp-s160); min-height: var(--sp-s160); }
.service-card--m { min-width: var(--sp-s160); min-height: var(--sp-s76); }
.service-card--s { min-width: var(--sp-s76); min-height: var(--sp-s76); }

/* The artwork underlay: absolute, stretch-anchored to all four edges — the
   slot sizes the ImageContainer, everything inside is the primitive's business */
.service-card__bottom {
  position: absolute;
  inset: 0;
  display: flex;
}
.service-card__bottom .image-container { flex: 1; }

/* StartText — pinned to the top, above the underlay. A column of up to two
   caption lines (title + optional subtitle, hidden by default) at gap s0;
   each line is a row that may carry up to two trailing slots (the Expanded
   mode, IconContainer s16 default) at gap s4. */
.service-card__title-row {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-s0);
  width: 100%;
  box-sizing: border-box;
  padding: var(--sp-s10) var(--sp-s12) var(--sp-s0);
}
.service-card__line {
  display: flex;
  align-items: center;
  gap: var(--sp-s4);
  min-width: 0;
}
.service-card__title,
.service-card__subtitle {
  min-width: 0;
  font-family: var(--text-caption-caption-family), 'Noto Sans', sans-serif;
  font-size: var(--text-caption-caption-size);
  font-weight: var(--text-caption-caption-weight);
  line-height: var(--text-caption-caption-line-height);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.service-card__subtitle { color: var(--text-and-icon-secondary); }

/* Skeleton — surface and radius hold; the title row carries a Skeleton
   caption line and the ImageContainer is REMOVED, not skeletonized */
.service-card--skeleton {
  cursor: default;
  transition: none;
}
.service-card--skeleton:active { transform: none; }
.service-card--skeleton .service-card__bottom { display: none; }
.service-card--skeleton .skeleton-text { width: var(--sp-s76); max-width: 100%; }

/* ════════════════════════════════════════════════
   BaseTextLine component — spec: specs/components/base-text-line/base-text-line.md (3.0.0)
   The core single-line input the text-field family is assembled on. A row:
   optional prefix · the input (placeholder + caret) · optional suffix, gap s4,
   no paddings of its own, Main Body throughout, five re-pointable colour slots
   (--base-text-line-*). Affixes never truncate — the input yields. Developer-
   facing (no Figma master — designers use the assembled fields). The Active
   affordance (border / underline / focus ring) AND the s44 touch target are
   the ASSEMBLED FIELD's duty — standalone use restores both (base-text-line-a11y.md).
   ════════════════════════════════════════════════ */
.base-text-line {
  --base-text-line-text: var(--text-and-icon-primary);
  --base-text-line-placeholder: var(--text-and-icon-secondary);
  --base-text-line-prefix: var(--text-and-icon-primary);
  --base-text-line-suffix: var(--text-and-icon-primary);
  --base-text-line-cursor: var(--text-and-icon-primary);
  display: flex;
  align-items: center;
  gap: var(--sp-s4);
  width: 100%;
  min-width: var(--sp-s0);
  font-family: var(--text-body-main-body-family);
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
  letter-spacing: var(--text-body-main-body-letter-spacing);
}

/* The editable line — hugs its content (field-sizing), so the suffix rides
   right after the text like the Figma TextContainer and Android's clamped
   offset; at overflow it shrinks and scrolls internally — the affixes never
   give way. Non-Chromium browsers ignore field-sizing and the input fills:
   the suffix then sits at the line end (recorded degradation). */
.base-text-line__input {
  flex: 0 1 auto;
  field-sizing: content;
  min-width: var(--sp-s0);
  padding: var(--sp-s0);
  border: none;
  background: transparent;
  outline: none; /* the host field draws the Active state — see the a11y spec */
  font: inherit;
  letter-spacing: inherit;
  color: var(--base-text-line-text);
  caret-color: var(--base-text-line-cursor);
  text-align: start;
}
.base-text-line__input::placeholder {
  color: var(--base-text-line-placeholder);
  opacity: 1; /* Firefox dims placeholders on its own */
}

/* Affixes — never wrap, never shrink: the input gives way */
.base-text-line__prefix,
.base-text-line__suffix {
  flex: 0 0 auto;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
}
.base-text-line__prefix { color: var(--base-text-line-prefix); }
.base-text-line__suffix { color: var(--base-text-line-suffix); }

/* Icon affix — the s24 glyph (spec; iOS ships s20 — recorded deviation) */
.base-text-line__prefix svg,
.base-text-line__suffix svg {
  width: var(--sp-s24);
  height: var(--sp-s24);
  display: block;
}

/* Alignment axis — left is the default; the hugged content group shifts as
   one, like the Figma alignment examples (text-align keeps the fallback
   browsers' filling input aligned too) */
.base-text-line--center { justify-content: center; }
.base-text-line--center .base-text-line__input { text-align: center; }
.base-text-line--right { justify-content: flex-end; }
.base-text-line--right .base-text-line__input { text-align: end; }

/* Forced LTR — an amount reads left-to-right in every locale; only the
   surrounding chrome mirrors. Inside RTL chrome the line still sits at the
   READING edge (right, under the mirrored label): only the character order
   stays LTR — alignment follows the layout (the PhoneField RTL staging). */
.base-text-line--ltr { direction: ltr; }
[dir="rtl"] .base-text-line--ltr { justify-content: flex-end; }

/* Disabled — a platform state (not in the Figma set): everything tints */
.base-text-line--disabled {
  --base-text-line-text: var(--text-and-icon-disabled);
  --base-text-line-placeholder: var(--text-and-icon-disabled);
  --base-text-line-prefix: var(--text-and-icon-disabled);
  --base-text-line-suffix: var(--text-and-icon-disabled);
}

/* ════════════════════════════════════════════════
   TextField component — spec: specs/components/text-field/text-field.md (3.2.0)
   The assembled field on the BaseTextLine core: an OnWhite box (radius s20,
   min-height s56 — both max ×130%) hosting a floating label + the input line
   between two icon slots, with an optional Description below. The border
   (Active/Error, s2 inside) fades in on Scale Medium, as does the label
   float 16/20 ⇄ 14/16. The whole box is one touch area (a <label>); slot
   buttons are their own targets. State classes: .text-field--filled (JS
   toggles on input), --error, --disabled, --skeleton, --placeholder-label.
   ════════════════════════════════════════════════ */
.text-field {
  --text-field-fill: var(--surface-on-white);
  --text-field-border: transparent;
  --text-field-label: var(--text-and-icon-secondary);
  --text-field-description: var(--text-and-icon-secondary);
  display: flex;
  flex-direction: column;
  width: 100%;
}

/* The box — the field surface and the one touch area */
.text-field__box {
  display: flex;
  align-items: center;
  min-height: var(--sp-s56);            /* max ×130% per the spec */
  padding: var(--sp-s0) var(--sp-s4) var(--sp-s0) var(--sp-s8);
  background: var(--text-field-fill);
  border-radius: var(--sp-s20);         /* max ×130% */
  box-shadow: inset 0 0 0 var(--sp-s2) var(--text-field-border);
  transition: box-shadow var(--pattern-scale-medium-appear);
  cursor: text;
}
/* Slots-off edges: the spec's s16 total. Left composes on its own
   (box s8 + start-text s8); the right steps s4 → s8 when Slot#2 is absent. */
.text-field__box:not(:has(.text-field__end-slot)) { padding-right: var(--sp-s8); }

/* Active — the border appears from transparency (Scale Medium) */
.text-field__box:focus-within { --text-field-border: var(--border-active); }

/* ── Slot#1 — s40 square, a DS icon ── */
.text-field__start-slot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s40);
  height: var(--sp-s40);
  color: var(--text-and-icon-primary);
}
.text-field__start-slot svg { width: var(--sp-s24); height: var(--sp-s24); display: block; }

/* ── StartText — the label + the input line ── */
.text-field__start-text {
  flex: 1 1 auto;
  min-width: var(--sp-s0);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--sp-s0) var(--sp-s8);
  overflow: hidden;                     /* long text hides — no truncation/ellipsis */
}
.text-field__label {
  font-family: var(--text-body-main-body-family);
  font-weight: var(--text-body-main-body-weight);
  font-size: var(--text-body-main-body-size);
  line-height: var(--text-body-main-body-line-height);
  color: var(--text-field-label);
  white-space: nowrap;
  /* font-size/line-height (and the line's height below) tween on the Scale
     Medium token — a RECORDED deviation from the compositor-only guidance
     (the Indicator width-glide precedent): Android interpolates the real
     font sizes for this exact float, and a transform scale would distort
     the glyphs (the iOS approximation). Reduced motion zeroes the token. */
  transition: font-size var(--pattern-scale-medium-appear), line-height var(--pattern-scale-medium-appear);
}
/* The input line collapses in rest-empty; it opens with focus or content */
.text-field .base-text-line {
  height: var(--sp-s0);
  overflow: hidden;
  transition: height var(--pattern-scale-medium-appear);
}
.text-field__box:focus-within .base-text-line,
.text-field--filled .base-text-line { height: var(--sp-s20); }
/* The floated (compact) label */
.text-field__box:focus-within .text-field__label,
.text-field--filled .text-field__label {
  font-size: var(--text-body-compact-body-size);
  line-height: var(--text-body-compact-body-line-height);
}

/* Placeholder-label mode — the label renders as the input's placeholder:
   the visible label is hidden (the input carries placeholder= + the name
   via aria-labelledby), the line never collapses, nothing floats. */
.text-field--placeholder-label .text-field__label {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.text-field--placeholder-label .base-text-line { height: var(--sp-s20); }

/* ── Slot#2 — one or two s48 squares at s0 apart ── */
.text-field__end-slot {
  flex: 0 0 auto;
  display: flex;
  gap: var(--sp-s0);
}
.text-field__slot-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s48);
  height: var(--sp-s48);
  padding: var(--sp-s0);
  border: none;
  background: transparent;
  border-radius: var(--sp-s24);
  color: var(--text-and-icon-primary);
  cursor: pointer;
}
.text-field__slot-button svg { width: var(--sp-s24); height: var(--sp-s24); display: block; }
.text-field__slot-button:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(-1 * var(--sp-s2));
}
/* The clear button — clear-filled in Secondary (3.2) */
.text-field__slot-button--clear { color: var(--text-and-icon-secondary); }

/* The loader replacing Slot#2 while waiting for the server — the Button
   spinner build ([Loader] 3.1 numbers: conic track, 20% band, capped head) */
.text-field__spinner {
  position: relative;
  width: var(--sp-s24);
  height: var(--sp-s24);
  flex-shrink: 0;
  border-radius: 50%;
  color: var(--text-and-icon-primary);
  background: conic-gradient(from 90deg, currentColor 0deg, transparent 337deg, transparent 360deg);
  -webkit-mask: radial-gradient(farthest-side, transparent 60%, #000 60%);
          mask: radial-gradient(farthest-side, transparent 60%, #000 60%);
  animation: var(--pattern-spin);
}
.text-field__spinner::after {
  content: '';
  position: absolute;
  top: 40%;
  right: 0;
  width: 20%;
  height: 20%;
  border-radius: 50%;
  background: currentColor;
}

/* ── Description — the helper line below the box ── */
.text-field__description {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-s4);
  padding: var(--sp-s8) var(--sp-s16) var(--sp-s0);
  font-family: var(--text-body-compact-body-family);
  font-weight: var(--text-body-compact-body-weight);
  font-size: var(--text-body-compact-body-size);
  line-height: var(--text-body-compact-body-line-height);
  color: var(--text-field-description);
}
.text-field__description svg {
  width: var(--sp-s16);
  height: var(--sp-s16);
  flex: 0 0 auto;                       /* aligns with the first line of long text */
  display: block;
}

/* ── Error — the red border persists until the next validation ── */
.text-field--error .text-field__box,
.text-field--error .text-field__box:focus-within { --text-field-border: var(--border-error); }
.text-field--error { --text-field-description: var(--text-and-icon-error); }

/* ── Disabled — everything tints, the fill stays ── */
.text-field--disabled {
  --text-field-label: var(--text-and-icon-disabled);
  --text-field-description: var(--text-and-icon-disabled);
}
.text-field--disabled .text-field__box { cursor: default; }
.text-field--disabled .text-field__start-slot,
.text-field--disabled .text-field__slot-button { color: var(--text-and-icon-disabled); }
.text-field--disabled .base-text-line {
  --base-text-line-text: var(--text-and-icon-disabled);
  --base-text-line-placeholder: var(--text-and-icon-disabled);
  --base-text-line-prefix: var(--text-and-icon-disabled);
  --base-text-line-suffix: var(--text-and-icon-disabled);
}

/* ── Skeleton — the whole box, no Description ── */
.text-field--skeleton .text-field__box {
  background: var(--skeleton-on-white);
  box-shadow: none;
  position: relative;
  overflow: hidden;
  cursor: default;
}
.text-field--skeleton .text-field__box > * { visibility: hidden; }
.text-field--skeleton .text-field__box::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0%, var(--skeleton-wave) 50%, transparent 100%);
  animation: var(--pattern-shimmer);
}
.text-field--skeleton .text-field__description { display: none; }

/* ════════════════════════════════════════════════
   PhoneField component — spec: specs/components/phone-field/phone-field.md (3.1.0)
   The TextField anatomy with locked phone content: the root carries BOTH
   classes (.text-field.phone-field) and every TextField contract applies
   unchanged. This block holds only the phone-specific parts: the country-
   code button (the Slot#1 geometry — s40 tap, s24 flag; the country LIST
   is outside the component) and the always-on LTR input row. The box uses
   an EXPLICIT for=→id label pairing — Slot#1 is a real <button> preceding
   the input, which would hijack the implicit association (the rule
   recorded on TextField; see phone-field-a11y.md).
   ════════════════════════════════════════════════ */
.phone-field__country-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-s40);
  height: var(--sp-s40);
  padding: var(--sp-s0);
  border: none;
  background: transparent;
  border-radius: var(--sp-s20);
  cursor: pointer;
}
.phone-field__country-button svg { width: var(--sp-s24); height: var(--sp-s24); display: block; }
.phone-field__country-button:focus-visible {
  outline: var(--sp-s2) solid var(--text-and-icon-primary);
  outline-offset: calc(-1 * var(--sp-s2));
}
/* The code prefix appears with input activity (the Figma staging: rest empty
   shows no code; active/filled show it) — the BaseTextLine host-owned
   visibility policy, decided here. */
.phone-field .base-text-line__prefix { display: none; }
.phone-field .text-field__box:focus-within .base-text-line__prefix,
.phone-field.text-field--filled .base-text-line__prefix { display: inline-flex; }

/* The flag is artwork — Disabled dims it (no token to re-point on a
   picture): opacity 0.5, the value BOTH platforms ship (iOS :237, Android
   :52) — deliberately not the Img-type 0.3. The buttons also carry the
   real disabled attribute. */
.text-field--disabled .phone-field__country-button svg { opacity: 0.5; }
