/* ==========================================================================
   Vagora Design System — component implementation
   v5.0 · Tier 3. Every colour, duration, radius and spacing value comes from
   a token — a literal in any of those four is a defect and CI fails on it.

   v5 is white-first and soft-geometry (see tokens.css header). Structure is
   whitespace: no construction hairlines, no frame, no shared-line grids.
   Lines survive only as UTILITY — table rows, the rare divider. Depth is
   frost (over imagery), lift (the inverted shadow) and float (overlays).
   Mirror OS keeps its v4 kit at the end of this file, under its own scope.
   ========================================================================== */

/* ── Base ──────────────────────────────────────────────────────────────── */
.vg-root, [data-theme] {
  font-family: var(--vg-font-sans);
  font-size: var(--vg-text-body);
  line-height: var(--vg-leading-body);
  color: var(--vg-text-primary);
  -webkit-font-smoothing: antialiased;
}
.vg-surface { background: var(--vg-surface-canvas); color: var(--vg-text-primary); }
.vg-num { font-variant-numeric: tabular-nums lining-nums; }

/* ── Type roles ────────────────────────────────────────────────────────────
   One face; size and tight leading carry the hierarchy. Weight tokens are
   semantic (--vg-w-*) so dark fields step them down without a component
   knowing. .vg-title-1..5 are Mirror-legacy aliases (Phase 3 retires them). */
.vg-display { font-size: var(--vg-text-display); line-height: var(--vg-leading-display); letter-spacing: var(--vg-tracking-display); font-weight: var(--vg-w-display); margin: 0; }
.vg-h1 { font-size: var(--vg-text-h1); line-height: var(--vg-leading-h1); letter-spacing: var(--vg-tracking-h1); font-weight: var(--vg-w-heading); margin: 0; }
.vg-h2 { font-size: var(--vg-text-h2); line-height: var(--vg-leading-h2); letter-spacing: var(--vg-tracking-h2); font-weight: var(--vg-w-heading); margin: 0; }
.vg-h3 { font-size: var(--vg-text-h3); line-height: var(--vg-leading-h3); letter-spacing: var(--vg-tracking-h3); font-weight: var(--vg-w-heading); margin: 0; }
.vg-h4 { font-size: var(--vg-text-h4); line-height: var(--vg-leading-h4); letter-spacing: var(--vg-tracking-h4); font-weight: var(--vg-w-heading); margin: 0; }
.vg-lede { font-size: var(--vg-text-lede); line-height: var(--vg-leading-lede); letter-spacing: var(--vg-tracking-lede); margin: 0; }
.vg-body { font-size: var(--vg-text-body); line-height: var(--vg-leading-body); letter-spacing: var(--vg-tracking-body); margin: 0; }
.vg-small { font-size: var(--vg-text-small); line-height: var(--vg-leading-small); letter-spacing: var(--vg-tracking-small); margin: 0; }
.vg-label { font-size: var(--vg-text-label); line-height: var(--vg-leading-label); font-weight: var(--vg-w-medium); letter-spacing: var(--vg-tracking-label); }
.vg-caption { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); color: var(--vg-text-secondary); }
.vg-eyebrow { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); font-weight: var(--vg-w-medium); letter-spacing: var(--vg-tracking-eyebrow); text-transform: uppercase; color: var(--vg-text-secondary); }
.vg-code { font-family: var(--vg-font-mono); font-size: var(--vg-text-caption); }
/* Mirror-legacy aliases */
/* The product vocabulary for the same four steps. It carried its OWN
   leadings (1.05 / 1.1 / 1.15 / 1.25) and dropped tracking entirely below
   title-3, so a 42px .vg-title-3 and a 42px .vg-h3 were two different
   typefaces' worth of difference on one system. Same step, same treatment,
   whichever class names it. */
.vg-title-1 { font-size: var(--vg-text-h1); line-height: var(--vg-leading-h1); letter-spacing: var(--vg-tracking-h1); font-weight: var(--vg-w-heading); margin: 0; }
.vg-title-2 { font-size: var(--vg-text-h2); line-height: var(--vg-leading-h2); letter-spacing: var(--vg-tracking-h2); font-weight: var(--vg-w-heading); margin: 0; }
.vg-title-3 { font-size: var(--vg-text-h3); line-height: var(--vg-leading-h3); letter-spacing: var(--vg-tracking-h3); font-weight: var(--vg-w-heading); margin: 0; }
.vg-title-4 { font-size: var(--vg-text-h4); line-height: var(--vg-leading-h4); letter-spacing: var(--vg-tracking-h4); font-weight: var(--vg-w-heading); margin: 0; }
.vg-title-5 { font-size: 1.25rem; line-height: 1.3; font-weight: var(--vg-w-heading); margin: 0; }

.vg-t-secondary { color: var(--vg-text-secondary); }
.vg-t-secondary-aa { color: var(--vg-text-secondary-aa); }
.vg-t-muted     { color: var(--vg-text-muted); }
.vg-t-tertiary  { color: var(--vg-text-tertiary); }
.vg-t-accent    { color: var(--vg-text-accent, var(--vg-text-primary)); }

/* ── Layout: container and section — whitespace is the structure ───────── */
/* The row: everything on a page aligns to this edge, or deliberately breaks
   it (.vg-bleed). Those are the only two options (§11.1). */
.vg-container { inline-size: var(--vg-row); margin-inline: auto; }
/* The Console keeps a cap: it is a TOOL, and a table stretched across a 27"
   monitor costs an operator eye travel all day. A poster has no such duty. */
.vg-container--wide { inline-size: auto; max-width: var(--vg-container-wide);
  margin-inline: auto; padding-inline: var(--vg-gutter); }
.vg-container--prose { inline-size: auto; max-width: var(--vg-container-prose);
  margin-inline: auto; padding-inline: var(--vg-gutter); }
.vg-section { padding-block: var(--vg-section); }
.vg-section--tight { padding-block: var(--vg-space-16); }
.vg-stack   { display: flex; flex-direction: column; gap: var(--vg-flow, var(--vg-space-3)); }
.vg-cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--vg-flow, var(--vg-space-4)); }

/* ── Focus (global contract — every interactive class, no exceptions) ──── */
.vg-btn:focus-visible, .vg-input:focus-visible, .vg-link:focus-visible,
.vg-navitem:focus-visible, .vg-chip:focus-visible, .vg-dock a:focus-visible,
.vg-iconbtn:focus-visible, .vg-card--interactive:focus-visible {
  outline: 2px solid var(--vg-border-focus);
  outline-offset: 2px;
}
.vg-dock a:focus-visible { outline-color: var(--vg-dock-fg); outline-offset: -2px; }

/* ── Buttons (§21) — 12px, never a pill with a content area ────────────── */
.vg-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--vg-space-2);
  block-size: 52px; padding-inline: var(--vg-space-8); border: 0;
  border-radius: var(--vg-radius-control);
  background: var(--vg-action-primary-bg); color: var(--vg-action-primary-fg);
  font: var(--vg-w-medium) var(--vg-text-label)/var(--vg-leading-control) var(--vg-font-sans);
  letter-spacing: var(--vg-tracking-control);
  text-decoration: none; cursor: pointer;
  transition: background-color var(--vg-dur-2) var(--vg-ease-standard),
              transform var(--vg-dur-2) var(--vg-ease-standard);
}
.vg-btn:hover, .vg-btn.is-hover   { background: var(--vg-action-primary-hover); }
.vg-btn:active, .vg-btn.is-pressed { background: var(--vg-action-primary-pressed); transform: scale(.98); }
.vg-btn--sm { block-size: 40px; padding-inline: var(--vg-space-5); }
.vg-btn--lg { block-size: 56px; padding-inline: var(--vg-space-10); font-size: var(--vg-text-small); }
/* Ghost — the secondary action. A quiet outline; on dark fields the Jeton
   white-ghost, translated. */
.vg-btn--ghost {
  background: transparent; color: var(--vg-text-primary);
  border: 1px solid var(--vg-border-default);
}
.vg-btn--ghost:hover, .vg-btn--ghost.is-hover { background: var(--vg-state-hover); border-color: var(--vg-text-primary); }
.vg-btn--ghost:active { background: var(--vg-state-pressed); transform: scale(.98); }
/* Tertiary — text only, padded rounded hover zone (Jeton's ghost link). */
.vg-btn--text {
  background: transparent; color: var(--vg-text-primary);
  block-size: auto; padding: var(--vg-space-2) var(--vg-space-3);
  border-radius: 8px;
}
.vg-btn--text:hover { background: var(--vg-state-hover); }
.vg-btn.is-disabled, .vg-btn[disabled] {
  background: var(--vg-state-disabled-bg); color: var(--vg-text-disabled);
  cursor: not-allowed; transform: none;
}
.vg-btn--ghost.is-disabled, .vg-btn--ghost[disabled] { background: transparent; border-color: var(--vg-border-subtle); }
.vg-iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 44px; block-size: 44px; border: 0; border-radius: var(--vg-radius-control);
  background: transparent; color: var(--vg-text-primary); cursor: pointer;
}
.vg-iconbtn:hover { background: var(--vg-state-hover); }

/* ── Links ─────────────────────────────────────────────────────────────── */
.vg-link { color: currentColor; text-decoration: underline; text-underline-offset: .18em; text-decoration-thickness: 1px; text-decoration-color: var(--vg-border-default); }
.vg-link:hover { text-decoration-color: currentColor; }
/* Standalone arrow link — the outcome carries an arrow that does not move. */
.vg-link--arrow { display: inline-flex; align-items: center; gap: var(--vg-space-2); font-weight: var(--vg-w-medium); text-decoration: none; padding: var(--vg-space-2) var(--vg-space-3); margin-inline: calc(-1 * var(--vg-space-3)); border-radius: 8px; }
.vg-link--arrow:hover { background: var(--vg-state-hover); }
.vg-link--arrow::after { content: "→"; }

/* ── Field & input (§22) — borderless, tinted, 16px ────────────────────── */
.vg-field { display: flex; flex-direction: column; gap: var(--vg-space-2); }
.vg-field__label { font-size: var(--vg-text-label); line-height: var(--vg-leading-label); letter-spacing: var(--vg-tracking-label); font-weight: var(--vg-w-medium); }
.vg-field__hint { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); color: var(--vg-text-secondary-aa); }
.vg-field__error { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); color: var(--vg-status-critical); }
.vg-input {
  block-size: 52px; padding-inline: var(--vg-space-5);
  border: 1px solid transparent; border-radius: var(--vg-radius-input);
  background: var(--vg-surface-alt); color: var(--vg-text-primary);
  font: 400 var(--vg-text-body)/var(--vg-leading-control) var(--vg-font-sans);
  letter-spacing: var(--vg-tracking-control);
  transition: background-color var(--vg-dur-2) var(--vg-ease-standard);
}
.vg-input::placeholder { color: var(--vg-text-secondary-aa); }
.vg-input:hover { background: color-mix(in srgb, var(--vg-ink) 7%, var(--vg-white)); }
.vg-input:focus-visible { outline: 2px solid var(--vg-border-focus); outline-offset: 0; }
.vg-input[aria-invalid="true"] { border-color: var(--vg-status-critical); }
textarea.vg-input { block-size: auto; padding-block: var(--vg-space-4); resize: vertical; }

/* ── Cards & surfaces (§25) — no borders, no default shadow ────────────── */
.vg-card {
  background: var(--vg-surface-alt); border-radius: var(--vg-radius-object);
  padding: var(--vg-space-8);
}
.vg-card--lg { border-radius: var(--vg-radius-object-lg); padding: clamp(28px, 3vw, 40px); }
/* The black card: a field as an object. Weights step down inside it. */
.vg-card--field {
  background: var(--vg-black); color: var(--vg-white);
  border-radius: var(--vg-radius-object-lg); padding: clamp(48px, 7vw, 96px);
}
/* Lifted — the inverted shadow: floats up from below (Jeton's signature,
   in neutral). For a WHITE card that must detach from the white canvas. */
.vg-card--lift { background: var(--vg-surface-canvas); box-shadow: var(--vg-shadow-lift); }
.vg-card--interactive { cursor: pointer; transition: transform var(--vg-dur-3) var(--vg-ease-standard); }
.vg-card--interactive:hover { transform: translateY(-2px); }
.vg-panel { background: var(--vg-surface-alt); border-radius: var(--vg-radius-object); padding: var(--vg-space-6); }

/* ── FROST (§13.1) — the panel over imagery ────────────────────────────────
   A translucent ink pane with backdrop blur: content stays legible over any
   photograph without a scrim gradient. Dark frost is the default (white
   text); light frost is for pale imagery. Requires a positioned ancestor
   containing the image. Radius follows the card class it is scaled to.    */
.vg-frost {
  background: var(--vg-frost-dark); color: var(--vg-white);
  --vg-text-primary: var(--vg-white);
  /* Frost sits on unpredictable photography, so its supporting text is
     FUNCTIONAL: 72% of primary, not the marketing 45% — at 45% over a live
     image the line disappears the moment the photo runs dark (§13.1). */
  --vg-text-secondary: color-mix(in srgb, var(--vg-text-primary) 72%, transparent);
  --vg-text-secondary-aa: var(--vg-text-secondary);
  --vg-text-tertiary: var(--vg-text-secondary);
  --vg-text-muted: var(--vg-text-secondary);
  --vg-w-heading: 500; --vg-w-display: 500; --vg-w-medium: 450;
  /* saturate() keeps the photo's colour alive through the blur — pure blur
     greys the backdrop and the glass goes dead. */
  -webkit-backdrop-filter: blur(var(--vg-frost-blur)) saturate(1.4);
  backdrop-filter: blur(var(--vg-frost-blur)) saturate(1.4);
  border-radius: var(--vg-radius-object); padding: var(--vg-space-6);
}
.vg-frost--light {
  background: var(--vg-frost-light); color: var(--vg-ink);
  --vg-text-primary: var(--vg-ink);
  --vg-w-heading: 600; --vg-w-display: 600; --vg-w-medium: 500;
}

/* ── Media (§16) — radius by scale; bleeds are square ──────────────────── */
.vg-media { display: block; inline-size: 100%; border-radius: var(--vg-radius-object); object-fit: cover; }
.vg-media--lg { border-radius: var(--vg-radius-object-lg); }
.vg-media--bleed { border-radius: 0; }

/* ── The dock (§24.1) — frosted ink pill, fixed bottom-centre ──────────── */
.vg-dock {
  position: fixed; inset-inline: 0; bottom: var(--vg-dock-inset); margin-inline: auto;
  inline-size: fit-content; display: flex; align-items: center; gap: var(--vg-space-1);
  padding: var(--vg-space-1-5);
  background: color-mix(in srgb, var(--vg-ink) 88%, transparent);
  -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px);
  border-radius: var(--vg-radius-pill);
  box-shadow: var(--vg-shadow-float);
  z-index: var(--vg-z-dock);
}
.vg-dock a {
  display: inline-flex; align-items: center; justify-content: center;
  block-size: 56px; padding-inline: var(--vg-space-5);
  border-radius: var(--vg-radius-pill);
  color: var(--vg-dock-fg); font: var(--vg-w-medium) var(--vg-text-label)/var(--vg-leading-control) var(--vg-font-sans);
  letter-spacing: var(--vg-tracking-control); text-decoration: none;
  transition: background-color var(--vg-dur-2) var(--vg-ease-standard);
}
.vg-dock a:hover { background: var(--vg-dock-hover); }
/* Active and hover share ONE value (§24.1). #1F1F1F was specified before the
   dock was frosted; against a blurred ink pill it read as a hard tile stuck to
   the bar, while the translucent white wash sits *in* the glass. The brand
   cell is a spy target like any other — it is active over the hero and
   inactive everywhere else, never permanently lit. */
.vg-dock a.is-active { background: var(--vg-dock-hover); }
.vg-dock__brand { inline-size: 56px; padding: 0; background: transparent; }
.vg-dock__brand img { block-size: 39px; display: block; }

/* On a phone the dock has to fit the row like everything else. Four cells at
   desktop padding measure 388px — wider than a 360px handset, so it ran off
   both edges (found by measuring the row change, not by looking). Tighter
   cells and a caption-scale label bring it inside the row at 360. */
@media (max-width: 460px) {
  .vg-dock { max-inline-size: var(--vg-row); }
  .vg-dock a { padding-inline: var(--vg-space-2); font-size: var(--vg-text-caption); }
  .vg-dock__brand { inline-size: 40px; }
  .vg-dock__brand img { block-size: 24px; }
}

/* The dock ARRIVES (§24.1). There is no top navigation, so the dock has to
   teach itself: it rises into place once, after the opening statement has
   been read, which is what makes a visitor look at it. Gated on
   `scripting: enabled` — never hide chrome behind a state only script clears
   — and reduced motion gets it in place immediately. */
@media (scripting: enabled) {
  .vg-dock--arrives { transform: translateY(calc(100% + var(--vg-space-8))); opacity: 0;
    transition: transform var(--vg-dur-5) var(--vg-ease-standard),
                opacity var(--vg-dur-4) var(--vg-ease-standard); }
  .vg-dock--arrives.is-in { transform: none; opacity: 1; }
}

/* ── Tags, chips, avatars — true capsules, so pills (§12.1) ────────────── */
.vg-tag {
  display: inline-flex; align-items: center; block-size: 28px; padding-inline: var(--vg-space-3);
  border-radius: var(--vg-radius-pill); background: var(--vg-surface-alt);
  color: var(--vg-text-secondary-aa); font-size: var(--vg-text-caption); font-weight: var(--vg-w-medium);
}
.vg-chip {
  display: inline-flex; align-items: center; gap: var(--vg-space-1-5); block-size: 32px;
  padding-inline: var(--vg-space-4); border-radius: var(--vg-radius-pill);
  border: 1px solid var(--vg-border-default); background: transparent;
  color: var(--vg-text-primary); font-size: var(--vg-text-caption); font-weight: var(--vg-w-medium);
  cursor: pointer;
}
.vg-chip.is-selected { background: var(--vg-ink); border-color: var(--vg-ink); color: var(--vg-white); }
.vg-chip.is-disabled, .vg-chip[disabled] { color: var(--vg-text-disabled); border-color: var(--vg-border-subtle); cursor: not-allowed; }
.vg-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 40px; block-size: 40px; border-radius: var(--vg-radius-circle);
  background: var(--vg-surface-alt); color: var(--vg-text-primary);
  font-size: var(--vg-text-caption); font-weight: var(--vg-w-medium); overflow: hidden;
}
.vg-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }

/* ── Status pills — the surviving colour, Console and pills only ───────── */
.vg-pill { display: inline-flex; align-items: center; line-height: var(--vg-leading-control); letter-spacing: var(--vg-tracking-control); gap: var(--vg-space-1-5); block-size: 26px; padding-inline: var(--vg-space-3); border-radius: var(--vg-radius-pill); font-size: var(--vg-text-caption); font-weight: var(--vg-w-medium); }
.vg-pill::before { content: ""; inline-size: 6px; block-size: 6px; border-radius: var(--vg-radius-circle); background: currentColor; }
.vg-pill--live     { background: var(--vg-status-success-bg);  color: var(--vg-status-success); }
.vg-pill--syncing  { background: var(--vg-status-info-bg);     color: var(--vg-status-info); }
.vg-pill--offline  { background: var(--vg-status-neutral-bg);  color: var(--vg-status-neutral); }
.vg-pill--degraded { background: var(--vg-status-warning-bg);  color: var(--vg-status-warning); }
.vg-pill--error    { background: var(--vg-status-critical-bg); color: var(--vg-status-critical); }

/* ── Banner ────────────────────────────────────────────────────────────── */
.vg-banner { padding: var(--vg-space-4) var(--vg-space-5); border-radius: var(--vg-radius-object); background: var(--vg-surface-alt); font-size: var(--vg-text-small); line-height: var(--vg-leading-small); letter-spacing: var(--vg-tracking-small); }
.vg-banner--warning  { background: var(--vg-status-warning-bg);  color: var(--vg-status-warning); }
.vg-banner--critical { background: var(--vg-status-critical-bg); color: var(--vg-status-critical); }
.vg-banner--success  { background: var(--vg-status-success-bg);  color: var(--vg-status-success); }

/* ── Data table (§26) — the one place lines are structural ─────────────── */
.vg-table-wrap { background: var(--vg-surface-canvas); border-radius: var(--vg-radius-object); overflow: hidden; border: 1px solid var(--vg-border-subtle); }
.vg-table { inline-size: 100%; border-collapse: collapse; }
.vg-table th {
  text-align: start; block-size: 44px; padding-inline: var(--vg-space-5);
  font: var(--vg-w-medium) var(--vg-text-caption)/var(--vg-leading-control) var(--vg-font-sans);
  letter-spacing: var(--vg-tracking-control);
  color: var(--vg-text-secondary-aa); background: var(--vg-surface-alt); white-space: nowrap;
}
.vg-table td { block-size: 56px; padding-inline: var(--vg-space-5); font-size: var(--vg-text-small); line-height: var(--vg-leading-control); letter-spacing: var(--vg-tracking-control); border-block-start: 1px solid var(--vg-border-subtle); }
.vg-table tr:hover td { background: var(--vg-state-hover); }

/* ── Metric (§26.2) — space-separated, never boxed, never ruled ────────── */
.vg-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--vg-space-8); }
.vg-metric { display: flex; flex-direction: column; gap: var(--vg-space-2); }
.vg-metric__label { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); font-weight: var(--vg-w-medium); color: var(--vg-text-secondary-aa); }
.vg-metric__value { font-size: 2.375rem; line-height: 1.05; letter-spacing: -0.018em; font-weight: var(--vg-w-medium); font-variant-numeric: tabular-nums lining-nums; }
.vg-metric__value--display { font-size: var(--vg-text-metric); line-height: 1; letter-spacing: var(--vg-tracking-display); }
.vg-metric__delta { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); }
.vg-metric__delta--up   { color: var(--vg-status-success); }
.vg-metric__delta--down { color: var(--vg-status-critical); }

/* ── Console navigation (§24.2) — light shell, full-width rows ─────────── */
.vg-sidebar { box-sizing: border-box; inline-size: 264px; padding-block: var(--vg-space-3); background: var(--vg-surface-canvas); border-inline-end: 1px solid var(--vg-border-subtle); display: flex; flex-direction: column; }
.vg-navitem { display: flex; align-items: center; gap: var(--vg-space-3); block-size: 40px; padding-inline: var(--vg-space-6); color: var(--vg-text-secondary-aa); font: var(--vg-w-medium) var(--vg-text-small)/var(--vg-leading-control) var(--vg-font-sans); letter-spacing: var(--vg-tracking-control); text-transform: none; text-decoration: none; cursor: pointer; position: relative; border-radius: 0; }
.vg-navitem:hover, .vg-navitem.is-hover { background: var(--vg-state-hover); color: var(--vg-text-primary); }
.vg-navitem.is-active, .vg-navitem.is-selected { background: var(--vg-state-selected); color: var(--vg-text-primary); }
.vg-navitem.is-active::before, .vg-navitem.is-selected::before { content: ""; position: absolute; inset-inline-start: 0; inset-block: 0; inline-size: 2px; background: var(--vg-ink); }
.vg-navgroup { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); font-weight: var(--vg-w-medium); letter-spacing: var(--vg-tracking-eyebrow); text-transform: uppercase; color: var(--vg-text-secondary-aa); padding: var(--vg-space-4) var(--vg-space-6) var(--vg-space-2); }

/* ── Empty state ───────────────────────────────────────────────────────── */
.vg-empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--vg-space-3); max-inline-size: 360px; margin-inline: auto; padding-block: var(--vg-space-12); }
.vg-empty__body { font-size: var(--vg-text-small); line-height: var(--vg-leading-small); letter-spacing: var(--vg-tracking-small); color: var(--vg-text-secondary-aa); }
.vg-empty__actions { margin-block-start: var(--vg-space-3); }

/* ── Overlays (§28) — white, 24px, float shadow ────────────────────────── */
.vg-scrim { position: fixed; inset: 0; background: var(--vg-scrim); z-index: var(--vg-z-scrim); }
.vg-modal { background: var(--vg-surface-overlay); border-radius: var(--vg-radius-object-lg); box-shadow: var(--vg-shadow-float); padding: var(--vg-space-8); max-inline-size: 480px; z-index: var(--vg-z-modal); }
.vg-modal__foot { display: flex; justify-content: flex-end; gap: var(--vg-space-3); margin-block-start: var(--vg-space-8); }

/* ── Divider — utility, not construction. Use sparingly. ───────────────── */
.vg-divider { block-size: 1px; background: var(--vg-border-subtle); border: 0; margin: 0; }

/* ── Loading ───────────────────────────────────────────────────────────── */
.vg-skeleton { border-radius: var(--vg-radius-control); background: linear-gradient(90deg, var(--vg-surface-alt) 25%, color-mix(in srgb, var(--vg-ink) 9%, var(--vg-white)) 37%, var(--vg-surface-alt) 63%); background-size: 400% 100%; animation: vg-shimmer 1.4s ease infinite; }
.vg-spinner { inline-size: 20px; block-size: 20px; border-radius: var(--vg-radius-circle); border: 2px solid var(--vg-border-subtle); border-block-start-color: var(--vg-text-primary); animation: vg-rotate .8s linear infinite; }
@keyframes vg-rotate { to { transform: rotate(360deg); } }
@keyframes vg-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
@keyframes vg-indeterminate { 0% { inset-inline-start: -40%; } 100% { inset-inline-start: 100%; } }

/* ══════════════════════════════════════════════════════════════════════════
   MARKETING PATTERNS (§42.1) — the composition layer.

   The atoms above (buttons, cards, type roles) let you BUILD a marketing
   page; they do not MAKE one. Polish on a page is density of content types,
   not decoration — a hero that carries support, a feature that shows the
   product, proof that has a face and a name, questions answered before they
   are asked. Everything below is a slot to fill, and a page that leaves a
   slot empty should delete it, not ship it hollow (§11.5).

   None of these introduces a colour, a radius or a shadow: they are
   arrangements of the existing system.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Hero (§42.1) — a statement that carries its support ───────────────────
   The headline alone is a title card, not a hero. The pattern is: eyebrow
   (optional) → statement → one lede → actions → a support row that answers
   "and why should I believe you" in one line. */
.vg-hero { padding-block: clamp(96px, 14vh, 180px) clamp(64px, 8vw, 112px); }
.vg-hero__lede { margin-block-start: var(--vg-space-6); max-inline-size: 46ch; font-size: var(--vg-text-lede); line-height: var(--vg-leading-lede); letter-spacing: var(--vg-tracking-lede); color: var(--vg-text-secondary); }
.vg-hero__actions { margin-block-start: var(--vg-space-10); display: flex; flex-wrap: wrap; gap: var(--vg-space-3); }
.vg-hero__support { margin-block-start: var(--vg-space-8); display: flex; flex-wrap: wrap; align-items: center; gap: var(--vg-space-3) var(--vg-space-6); font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); color: var(--vg-text-secondary); }
.vg-hero--center { text-align: center; }
.vg-hero--center .vg-hero__lede { margin-inline: auto; }
.vg-hero--center .vg-hero__actions,
.vg-hero--center .vg-hero__support { justify-content: center; }

/* ── Intro (§42.1) — the opening sequence ─────────────────────────────────
   One black stage, three beats: the statement is read, the dock rises into
   it (the page has no top navigation, so the dock has to introduce itself),
   the statement clears and the film takes the whole viewport.

   Every beat is opacity plus an ≤8px rise — §14 holds even here. Nothing
   loops except the film. The whole thing is gated on `scripting: enabled`
   and collapsed by `prefers-reduced-motion`, where the film is simply
   there from the first frame. */
/* The stage opens on the CANVAS, not on black: white is the primary
   surface, so the site starts there and the film is what turns it dark. */
.vg-intro { position: relative; block-size: 100svh; overflow: hidden;
  background: var(--vg-surface-canvas); color: var(--vg-text-primary); }
.vg-intro__film { position: absolute; inset: 0; opacity: 0;
  transition: opacity var(--vg-dur-6) var(--vg-ease-standard); }
.vg-intro__film video, .vg-intro__film img {
  inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
.vg-intro__statement { position: absolute; inset: 0; display: grid; place-items: center;
  text-align: center; padding-inline: var(--vg-gutter);
  transition: opacity var(--vg-dur-5) var(--vg-ease-standard),
              transform var(--vg-dur-5) var(--vg-ease-standard); }
/* The card and the dock sit on ONE baseline — both are chrome floating on
   the film, and two floating objects at different heights read as a mistake.
   --vg-dock-inset is the shared value; change it once, both move. */
.vg-intro__foot { position: absolute; inset-inline: 0;
  inset-block-end: var(--vg-dock-inset); padding-inline: var(--vg-gutter);
  display: flex; align-items: flex-end; justify-content: flex-end;
  gap: var(--vg-space-6); opacity: 0;
  transition: opacity var(--vg-dur-5) var(--vg-ease-standard); }
.vg-intro.is-playing .vg-intro__film,
.vg-intro.is-playing .vg-intro__foot { opacity: 1; }
.vg-intro.is-playing .vg-intro__statement { opacity: 0; transform: translateY(-8px);
  pointer-events: none; }

@media (scripting: enabled) {
  .vg-intro__statement { opacity: 0; transform: translateY(8px); }
  /* :not(.is-playing) — this rule lives after the is-playing rule and would
     otherwise win the cascade and keep the statement on top of the film. */
  .vg-intro.is-stated:not(.is-playing) .vg-intro__statement { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .vg-intro__statement, .vg-intro__film, .vg-intro__foot { transition: none; }
}

@media (max-width: 640px) {
  /* The card and the dock are both bottom-anchored; on a narrow screen the
     card is full-row-width and the two collide. The card steps up above the
     dock rather than shrinking — the dock is chrome and always wins the
     bottom edge. */
  .vg-intro__foot {
    inset-block-end: calc(var(--vg-dock-inset) + var(--vg-dock-h) + var(--vg-space-4)); }
  .vg-intro__foot .vg-frost { max-inline-size: none; }
}


/* ── Showcase (§42.1) — a sticky visual, the argument scrolling past it ────
   The pattern for a product with more capabilities than a page has room for.
   One pinned plate carries the product while the reasons move; the plate
   cross-fades to the frame belonging to whichever reason is being read. It
   is NOT scroll-jacking: the page scrolls at the speed the reader chose. */
/* The picture column is sized BY THE PICTURE, not by half the row. At 1fr
   1fr the plate floated centred in a half-row track and landed 309px from
   the words it belongs to — the law of proximity means the reader reads that
   distance as "unrelated". An `auto` track puts the plate on the row edge,
   aligned with the section heading, and the gap becomes the only separation. */
/* BALANCE, then position — in that order (§11.1).
   Spanning the whole row, a 16:9 plate took 72% of it and the words 28%: the
   picture stopped being half of a pair and became a backdrop with a caption.
   The stage is therefore capped at a width where the two halves are
   comparable (1.35 : 1), and a capped symmetric object is CENTRED — a block
   that no longer reaches the right margin has no business claiming the left
   one. This is the documented exception to the single left edge, and it is
   granted for balance, not to hide dead space. */
.vg-showcase {
  /* The stage's geometry is declared, not just laid out, so the plate's
     HEIGHT is computable in CSS — which is what lets the pin be centred
     (see .vg-showcase__col). 0.5745 is 1.35/2.35, the picture's share of the
     tracks; 0.5625 is 9/16. The row is written in vw here rather than 100%
     because a percentage cannot be used in `top`; that overstates it by the
     scrollbar width, which moves the pin line by about 2px. */
  --stage-gap: clamp(32px, 3vw, 64px);
  --stage-w: min(calc(100vw - 2 * max(3vw, 20px)), max(1400px, 72vw));
  --plate-h: calc((var(--stage-w) - var(--stage-gap)) * 0.5745 * 0.5625);

  display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: var(--stage-gap); align-items: start;
  max-inline-size: min(100%, max(1400px, 72vw)); margin-inline: auto; }
/* Pinned to a line that CENTRES the plate, without wrapping it in a
   viewport-tall box. The obvious way to centre a sticky plate — a 100svh
   column with the picture centred inside it — also pushes the picture
   (100svh − plate)/2 below the section copy before it ever pins, which is
   the half-screen gap this section spent three revisions removing. Solving
   for the line instead gives both: in flow the plate sits where the layout
   put it, directly under the copy, and it pins dead centre. */
.vg-showcase__col { position: sticky; block-size: auto;
  top: max(var(--vg-space-6), calc(50svh - var(--plate-h) / 2)); }
/* 16:9. The panel's own 9:16 was the more literal choice, but a portrait
   crop of a portrait mirror shows the mirror and nothing around it — no
   store, no room, no customer standing back from it. The capability being
   sold is what happens in the room, so the plate is landscape and the plate
   width is driven by the row, not by the viewport height. */
.vg-showcase__media { position: relative; inline-size: 100%; aspect-ratio: 16 / 9;
  max-block-size: 72svh; border-radius: var(--vg-radius-object-lg);
  overflow: hidden; background: var(--vg-surface-alt); }
.vg-showcase__plate { position: absolute; inset: 0; inline-size: 100%; block-size: 100%;
  object-fit: cover; opacity: 0;
  transition: opacity var(--vg-dur-5) var(--vg-ease-standard); }
.vg-showcase__plate.is-shown { opacity: 1; }
/* The runway, at BOTH ends, and it is not decoration:

   Without the lead-in, step one is already at the top of the pinned column
   when the section arrives — it appears fully formed while every other step
   rises into view.

   Without the run-out, the sticky column releases the moment the grid ends,
   which is the moment the LAST step is still below centre: its text halts
   mid-screen, the plate unpins, and the pair separates. The tail keeps the
   grid alive long enough for the final step to reach the middle and for both
   to leave together. Anything under ~22svh reintroduces the fault. */
/* No leading pad: the plate now sits in flow under the section copy rather
   than centred in a viewport-tall box, so step one is already close to it.
   The run-out stays — without it the last step halts before centre (§42.1). */
.vg-showcase__steps { padding-block: 0 26svh; }
.vg-showcase__steps > * + * { margin-block-start: clamp(72px, 14vh, 140px); }
.vg-showcase__step { min-block-size: 56svh; display: flex; flex-direction: column;
  justify-content: center; max-inline-size: 44ch; }
/* The FIRST step is the one the reader meets before any scrolling has
   happened, so it has to be composed against the plate rather than against
   the viewport. Giving it a box exactly the plate's height puts its centred
   content on the plate's centre line — the two arrive as a pair. Every step
   after it is centred against the viewport instead, because by then the
   plate is pinned there. */
.vg-showcase__step:first-child { min-block-size: var(--plate-h); }

/* Retail impact — the commercial half of a capability, set apart. A feature
   without its consequence is a spec sheet; this block is what a buyer reads. */
.vg-impact { margin-block-start: var(--vg-space-8); padding-block-start: var(--vg-space-6);
  border-block-start: 1px solid var(--vg-border-subtle); }
.vg-impact__label { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption);
  letter-spacing: var(--vg-tracking-caption); font-weight: var(--vg-w-medium);
  color: var(--vg-text-secondary); }
/* The impact line is the same voice as the paragraph above it, so it is the
   same size. It was one step down, which made a five-part block out of a
   three-part one for no gain in meaning (§9.7). */
.vg-impact__body { margin-block-start: var(--vg-space-3); font-size: inherit;
  line-height: inherit; letter-spacing: inherit; max-inline-size: 46ch; }

@media (max-width: 980px) {
  .vg-showcase { grid-template-columns: minmax(0, 1fr); }
  .vg-showcase__col { position: static; block-size: auto; margin-block-end: var(--vg-space-10); }
  /* Height-first sizing overflows a narrow row: 78svh of an 844px handset is
     658px, and at 9:16 that is a 370px-wide plate inside a 320px row. On one
     column the plate is width-first and lets its height follow the ratio. */
  .vg-showcase__media { max-block-size: none; }
  .vg-showcase__step { min-block-size: 0; }
  .vg-showcase__steps > * + * { margin-block-start: clamp(56px, 10vw, 96px); }
}

/* ── Split (§42.1) — text beside a visual, alternating down the page ───────
   The workhorse of a product page: one idea, one picture, repeated with the
   sides swapped so the eye re-enters from the other margin each time. */
/* Sized like the stage (§42.1), for the same reason: at 1fr 1fr the visual
   floated centred in a half-row track while the words sat at the far edge of
   the other, so the pair read as two unrelated objects. Both tracks are
   content-sized, the pair is capped and centred, and the gap is the only
   thing between them. */
/* BOTH tracks are `auto`. Naming one of them the picture column only works
   if the picture is always the first child — and `--flip` reorders, so in the
   unflipped split the body took the picture's track and the picture sat in a
   42ch track with 42px of slack beside it. Content-sized tracks are
   order-proof. */
.vg-split { display: grid; grid-template-columns: auto auto;
  gap: clamp(32px, 3vw, 64px); align-items: center; justify-content: center;
  max-inline-size: min(100%, max(1100px, 62vw)); margin-inline: auto; }
/* A DEFINITE width, so the `auto` track is sized by the picture rather than
   by the longest line of its caption — which silently made one split's track
   462px against the other's 356px and threw the centring off by 21px. */
.vg-split__visual { inline-size: 420px; max-inline-size: 100%; }
.vg-split__body { max-inline-size: 42ch; }
.vg-split__body > * + * { margin-block-start: var(--vg-space-5); }
.vg-split--flip .vg-split__visual { order: -1; }
.vg-split + .vg-split { margin-block-start: clamp(72px, 9vw, 128px); }

/* ── Feature card with a visual (§25) — the hollow-card cure ───────────────
   A card whose only content is a number and two lines is 27% full: a hole
   with a border-radius. Give it something to hold. */
.vg-card__media { display: block; inline-size: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--vg-radius-input); background: var(--vg-surface-alt); margin-block-end: var(--vg-space-6); }
.vg-card__index { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); font-weight: var(--vg-w-medium); color: var(--vg-text-secondary); font-variant-numeric: tabular-nums; }
.vg-card__body { margin-block-start: var(--vg-space-3); font-size: var(--vg-text-small); line-height: var(--vg-leading-small); letter-spacing: var(--vg-tracking-small); color: var(--vg-text-secondary); }

/* ── Logo wall (§42.1) — proof at a glance ────────────────────────────────
   Marks are set in the secondary weight so the wall reads as a texture, not
   as a competing brand row. Never a carousel: a moving logo cannot be read. */
.vg-logowall { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--vg-space-8) var(--vg-space-10); }
.vg-logowall__item { font-size: var(--vg-text-h4); line-height: var(--vg-leading-h4); letter-spacing: var(--vg-tracking-h4); font-weight: var(--vg-w-medium); color: var(--vg-text-secondary); }

/* ── Steps (§42.1) — a numbered process, separated by air ─────────────────── */
/* Tracks are CAPPED, not fractional. On an uncapped row three 1fr tracks
   became 560px wide and their paragraphs ran to 80 characters — a third
   longer than the eye wants to track. Capping the track fixes the measure
   AND keeps the columns near each other; capping only the paragraph would
   have left the columns far apart with short text rattling inside them. */
/* The cap is in `ch`, not px, so a track holds the same number of CHARACTERS
   at every width. A pixel cap stops scaling the moment the ladder does not:
   at 2560 a 380px track with 21px type is a 40-character column adrift in a
   2400px row. `ch` here resolves against the grid's own (fluid) body size. */
/* ── Band (§42.1) — a dark act, and the only way to build one ─────────────
   The page carries two of these. They were the same component wearing two
   different sets of inline overrides — 160 vs 180px padding, a 66px heading
   against an 88px one, 42ch tracks against 46ch, 56 vs 72px gaps — and a
   reader had no way to know they belonged to the same site. The generous set
   won; the values live here now and neither section overrides anything. */
.vg-band { padding-block: clamp(96px, 12vw, 180px); }
.vg-band__steps { margin-block-start: clamp(64px, 9vw, 128px); }

/* Each step now leads with a picture, and that changes how the track is
   capped. §9.4 says cap the TRACK, not the paragraph — true while a track
   holds nothing but words. A picture wants the whole track and a paragraph
   wants 46ch, so the two measures separate: the tracks fill the row (1fr)
   and the caps move onto the title and the body. Left at 46ch the tracks
   stopped 717px short of the right gutter on a 2560 screen — three plates
   floating inside an edge we drew, which is §11.5's accidental emptiness. */
.vg-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: clamp(40px, 4vw, 72px); }
/* Titles and bodies wrap to different line counts per column, so the impact
   rule — an actual drawn line — landed at three different heights and read as
   carelessness. The columns are flex, the rule is pushed to the floor, and it
   runs straight across the row at every width without the copy having to be
   cut to a line count that only holds at one viewport.
   The body absorbs the slack rather than the rule taking `margin-top:auto` —
   auto would win over .vg-impact's own space-8 and let the rule touch the
   paragraph in whichever column happens to be the tallest. */
.vg-steps > * { display: flex; flex-direction: column;
  font-size: var(--vg-text-small); line-height: var(--vg-leading-small);
  letter-spacing: var(--vg-tracking-small); }
.vg-steps .vg-step__body { flex: 1 0 auto; }
/* block-size:auto is load-bearing. The markup carries width/height attributes
   so the browser reserves the box before the file arrives, and the UA maps
   height="1080" to an actual used height — which beats aspect-ratio and
   rendered a 413 x 1080 portrait crop of a 16:9 photograph. */
.vg-step__media { display: block; inline-size: 100%; block-size: auto; aspect-ratio: 16 / 9;
  object-fit: cover; border-radius: var(--vg-radius-object);
  background: var(--vg-surface-alt); margin-block-end: var(--vg-space-8); }
.vg-step__title { margin-block-start: var(--vg-space-4); font-size: var(--vg-text-h4);
  line-height: var(--vg-leading-h4); letter-spacing: var(--vg-tracking-h4);
  font-weight: var(--vg-w-heading); max-inline-size: 20ch; }
.vg-step__body { margin-block-start: var(--vg-space-3); font-size: var(--vg-text-small);
  line-height: var(--vg-leading-small); letter-spacing: var(--vg-tracking-small);
  color: var(--vg-text-secondary); max-inline-size: 46ch; }

/* ── Testimonial (§42.1) — a quote with a person attached ──────────────────
   An unattributed quote is copywriting; an attributed one is proof. The
   attribution row carries a mark or avatar, a name, and a role. */
.vg-testimonial__quote { font-size: var(--vg-text-h3); line-height: var(--vg-leading-h3); letter-spacing: var(--vg-tracking-h3); font-weight: var(--vg-w-heading); max-inline-size: 24ch; margin: 0; }
.vg-testimonial__by { margin-block-start: var(--vg-space-8); display: flex; align-items: center; gap: var(--vg-space-4); }
.vg-testimonial__who { font-size: var(--vg-text-small); line-height: var(--vg-leading-small); letter-spacing: var(--vg-tracking-small); font-weight: var(--vg-w-medium); }
.vg-testimonial__role { font-size: var(--vg-text-small); line-height: var(--vg-leading-small); letter-spacing: var(--vg-tracking-small); color: var(--vg-text-secondary); }

/* ── FAQ (§42.1) — answered, not hidden ───────────────────────────────────
   The answers are the argument. Putting them behind a toggle asks the reader
   to work for the thing we most want read, and on a page selling to buyers
   every collapsed answer is an objection left standing.

   It still has to LOOK like an FAQ: one centred column, question above its
   own answer, hairline between pairs. A two-column grid turns it into a
   comparison table — the eye starts reading across rows instead of down
   pairs, and the pairing that makes it an FAQ is what gets lost. Centred
   measure, left-aligned text: centred text would slow the reading of the
   one block on the page a buyer reads word for word. */
.vg-faq { max-inline-size: 68ch; margin-inline: auto; }
/* Rules go BETWEEN pairs, never around the set. A leading rule separates the
   list from its own heading and a trailing one closes a box that is not
   there — the same "hairline ending in space" fault the frame died of (§12.5).
   The first item loses its top padding with the rule it was clearing. */
.vg-faq__item { padding-block: clamp(28px, 3.5vw, 44px); }
.vg-faq__item + .vg-faq__item { border-block-start: 1px solid var(--vg-border-subtle); }
.vg-faq__item:first-child { padding-block-start: 0; }
.vg-faq__item:last-child { padding-block-end: 0; }
.vg-faq__q { font-size: var(--vg-text-h4); font-weight: var(--vg-w-heading);
  letter-spacing: var(--vg-tracking-h4); line-height: var(--vg-leading-h4);
  max-inline-size: 34ch; margin: 0; text-wrap: balance; }
.vg-faq__a { margin: var(--vg-space-5) 0 0; font-size: var(--vg-text-body);
  line-height: var(--vg-leading-body); letter-spacing: var(--vg-tracking-body);
  color: var(--vg-text-secondary);
  /* 54ch, not 62: `ch` is the width of "0", which in Switzer is wider than
     the average lowercase letter — a 62ch box measured 72 real characters. */
  max-inline-size: 54ch; }

/* ── Bleed (§11.5) — a section that leaves the container ──────────────────
   The container is the page's default, not its only setting. A band that
   runs edge to edge is how a page changes register: the eye reads the loss
   of the margin as a change of scale, which is the whole cinematic trick. */
.vg-bleed { padding-inline: var(--vg-gutter); }
.vg-bleed > * { max-inline-size: none; }

/* ── CTA (§42.1) — the closing ask at full scale ──────────────────────────
   Boxing the final ask in a rounded card reads as a template block dropped
   at the end of a page. Full bleed, display type, and the action under it:
   the ask should be the largest thing on the page after the opening.

   THE CLOSE AND THE FOOTER ARE ONE FIELD, not two full-bleed sections that
   happen to touch. Both carry data-surface="dark", the CTA drops its bottom
   padding and the footer its top, so the seam does not exist and the page
   ends on a single black plate. Black at the end is a curtain, not a second
   act — §42.1 bans a second dark ARGUMENT, and this is not one. It also
   gives the dock, which is an ink pill, its own ground to sit on. */
/* The close owns ALL of the field's air, top and bottom, and the footer adds
   none. Split between the two rules the ask sat 216px below the field's top
   edge and 88px above the signature, which read as sinking rather than as a
   composition. One symmetric value, one place to change it. */
.vg-cta { text-align: center; padding-block: clamp(120px, 15vw, 220px); }
/* max-content on the shared column is what makes the button the heading's
   width: the heading's line break is explicit, so its max-content IS its
   longest line, and the button then takes 100% of that. Matching them by
   hand would hold at exactly one viewport — the heading is fluid (52->80px)
   and the button's label is not. */
.vg-cta__ask { inline-size: max-content; max-inline-size: 100%; margin-inline: auto; }
/* 64px, not the --lg 56 (§21.1). This button is not a large button, it is a
   BLOCK the width of the heading — 321px at 1440, 457 at 2560 — and at 56px
   a block that wide reads as a bar rather than as a control. The size stays
   scoped here so the system's 40/52/56 ladder is not bent by one placement. */
.vg-cta__actions .vg-btn { inline-size: 100%; block-size: 64px; }
/* H1: one step above every section's H2 and one below the hero's display,
   because the page's last ask should out-rank a section heading. What matters
   is that it is a STEP and not a bespoke clamp — the earlier one-off ran
   48-176px and at 2560 stood 176 against every other heading's 80, which
   reads as a different page rather than as emphasis. */
.vg-cta__head { font-size: var(--vg-text-h1); line-height: var(--vg-leading-h1);
  letter-spacing: var(--vg-tracking-h1); font-weight: var(--vg-w-heading);
  margin: 0; max-inline-size: 13ch; margin-inline: auto; text-wrap: balance; }
.vg-cta__actions { margin-block-start: clamp(40px, 4vw, 64px); display: flex; flex-wrap: wrap; gap: var(--vg-space-3); justify-content: center; }

/* The address, reachable three ways (§42.1). A site with no form has one
   channel, so that channel cannot be a single mailto: a desktop with no mail
   client configured swallows the click and the visitor leaves with nothing.
   The address is therefore PRINTED, selectable, a link, and copyable — read
   it, click it, or take the copy. The button is the same object in both
   states so the row does not reflow when the label changes to "Copied". */
.vg-cta__mail { margin-block-start: clamp(32px, 3.5vw, 56px);
  display: inline-flex; align-items: center; gap: var(--vg-space-4);
  flex-wrap: wrap; justify-content: center; }
.vg-cta__addr { font-size: var(--vg-text-lede); color: var(--vg-text-primary);
  line-height: var(--vg-leading-lede); letter-spacing: var(--vg-tracking-lede);
  text-decoration: none;
  border-block-end: 1px solid var(--vg-border-default);
  padding-block-end: 3px; transition: border-color var(--vg-dur-2) var(--vg-ease-standard); }
.vg-cta__addr:hover { border-block-end-color: var(--vg-text-primary); }
.vg-cta__addr:focus-visible { outline: 2px solid var(--vg-border-focus); outline-offset: 4px; border-radius: 2px; }
/* Reserve the WIDER label. "Copy" measures 78px and "Copied" 90, so without
   this the address shifts 12px left the moment you click it — the one frame
   where the user is looking straight at it. 7ch is the longer word in the
   `0`-width units `ch` actually means (§9.4), plus .vg-btn--sm's padding. */
.vg-cta__copy { min-inline-size: calc(7ch + var(--vg-space-5) * 2); }

/* ── Footer (§42.1) — a signature, not a sitemap ──────────────────────────
   Every link this footer used to carry was either already in the dock or a
   page that does not exist; a column of links to nowhere is worse than no
   column. What is left is a signature: the mark, and who made it.

   THE SIGNATURE ROW SHARES THE DOCK'S AXIS. The dock is fixed at
   --vg-dock-inset from the bottom and is --vg-dock-h tall, so a row of
   exactly --vg-dock-h sitting on exactly --vg-dock-inset has the same
   centreline by construction — no magic number, and it stays true when
   either token moves. Scrolled to the end, the mark, the dock and the
   colophon read as one line.

   The mark is the HORIZONTAL LOCKUP — brandmark plus wordmark — inside a row
   of exactly --vg-dock-h sitting on exactly --vg-dock-inset. That row has the
   dock's centreline by construction, so the lockup, the pill and the colophon
   all centre on one line with no magic number, and it stays true when either
   token moves.

   42px is a DOCUMENTED EXCEPTION to §7.4's 64px minimum, decided 28 Aug 2026,
   this placement only. The reason is proportion: the lockup's mark is its full
   placed height, so at 64px it stood 64 against the dock brandmark's 39 on the
   same line and read as the larger of two copies of one object. At 42 they
   match. The cost is measured, not assumed — the MIRROR descriptor is 10px at
   64, 7px at 52, and at 42 its strokes fall under one device pixel at 1x and
   render as grey rather than as letters. It resolves on a 1.5x display and
   above. 52px is the floor at which it survives at 1x.
   One thing improves: clear space is 48.7% of the placed height (§7.3), so it
   drops 31.2px -> 20.5px against the --vg-dock-inset below the row, and is
   now satisfied from 1281px up rather than from 1920. */
.vg-footer { padding-block: 0 var(--vg-dock-inset);
  padding-inline: var(--vg-gutter); }
.vg-footer__sign { display: flex; align-items: center; justify-content: space-between;
  gap: var(--vg-space-6); block-size: var(--vg-dock-h); }
.vg-footer__sign img { block-size: 42px; inline-size: auto; display: block; }
/* Never centre this with flex or grid: both wrap the text run and the <a> as
   separate anonymous items and drop the whitespace between them, so "website
   by Aqib" renders "website byAqib". The row's own align-items does it. */
.vg-footer__colophon { font-size: var(--vg-text-caption); line-height: var(--vg-leading-caption); letter-spacing: var(--vg-tracking-caption); color: var(--vg-text-secondary-aa); margin: 0; }
.vg-footer__colophon a { color: var(--vg-text-primary); font-weight: var(--vg-w-medium);
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-thickness: from-font; }
.vg-footer__colophon a:focus-visible { outline: 2px solid var(--vg-border-focus); outline-offset: 3px; border-radius: 2px; }

/* ── Device frame (§42.1, §16) — the product, shown ────────────────────────
   The single strongest polish signal on a product page is the product. The
   Mirror is a 9:16 panel in a thin dark bezel; the frame is drawn from the
   ink token so it is the hardware, not a decorative border. Compose a real
   Mirror surface inside it — the [data-theme="mirror"] scope works here. */
.vg-device { position: relative; aspect-ratio: 9 / 16; border-radius: var(--vg-radius-object-lg); padding: var(--vg-space-2); background: var(--vg-ink); box-shadow: var(--vg-shadow-float); overflow: hidden; }
.vg-device__screen { block-size: 100%; inline-size: 100%; border-radius: var(--vg-radius-input); overflow: hidden; position: relative; background: var(--vg-black); }
.vg-device__screen > img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }

/* ── Marquee (§42.1) — a slow band of proof; stops for reduced motion ────── */
.vg-marquee { overflow: hidden; display: flex; gap: var(--vg-space-10);
  -webkit-mask-image: var(--vg-mask-edge-fade); mask-image: var(--vg-mask-edge-fade); }
.vg-marquee__track { display: flex; gap: var(--vg-space-10); flex: none; animation: vg-marquee 42s linear infinite; }
@keyframes vg-marquee { to { transform: translateX(-100%); } }

/* ── Reveal (§14) — the motion the system describes and nothing demonstrated
   Opacity plus an 8px rise: single-property-preference, no overshoot, one
   pass. The `is-in` class is set by an IntersectionObserver.
   The hidden state is gated on `scripting: enabled`, because the class that
   reveals it can only ever be added by script: ungated, a browser with
   JavaScript off would render the entire page at opacity 0. A browser too old
   for the query ignores the block and shows the content, which is the same
   correct failure. NEVER hide content behind a state only script can clear. */
@media (scripting: enabled) {
  .vg-reveal { opacity: 0; transform: translateY(8px);
    transition: opacity var(--vg-dur-4) var(--vg-ease-standard),
                transform var(--vg-dur-4) var(--vg-ease-standard); }
  .vg-reveal.is-in { opacity: 1; transform: none; }
}

@media (max-width: 860px) {
  /* minmax(0, 1fr), not 1fr: a track's automatic minimum is its item's
     min-content, and the visual carries a definite 420px width — so a plain
     1fr floors at 420 and runs off a 390px screen instead of shrinking. */
  .vg-split { grid-template-columns: minmax(0, 1fr); }
  .vg-split__visual { inline-size: 100%; }
  .vg-split--flip .vg-split__visual { order: 0; }
}

/* The signature row shares its line with a CENTRED dock, so it only works
   while the two ends clear the pill. Below that width the row stacks and
   the footer pads past the dock instead — the breakpoint is measured from
   where the colophon actually reaches the pill, not guessed. */
/* The signature row shares its line with a CENTRED dock, so it only holds
   while both ends clear the pill — below ~900px the colophon runs into it.
   Below the breakpoint the block stacks AND centres, and the page ends on
   the same centred axis the dock sits on. */
@media (max-width: 900px) {
  .vg-footer { padding-block-end: calc(var(--vg-dock-inset) * 2 + var(--vg-dock-h)); }
  /* Flex, not grid: a single auto grid track is sized to its widest item and
     then placed by justify-CONTENT, so justify-items:center centred the mark
     inside a 191px track that was itself still sitting on the left edge. */
  .vg-footer__sign { flex-direction: column; align-items: center; text-align: center;
    gap: var(--vg-space-6); block-size: auto; }
}

/* ── Mirror deltas ─────────────────────────────────────────────────────────
   REFERENCE RENDERING ONLY. The shipped Mirror is a Unity application on
   Windows; these rules exist so the Mirror can be specified and reviewed in
   a browser alongside every other surface. Unity consumes the same values
   through the generated token export (§38.5). Nothing here runs in store.

   Scale and input only: Mirror keeps the role radii (§30.1) and inherits the
   shadow removal from the mirror theme tokens. Apply .vg-mirror on the root.

   Type steps to the 2 m floor — the smallest text on the Mirror is 28 px,
   because at 1.5–2 m a 20 px cap height measures 8.2 mm against a 10 mm
   comfortable minimum. */
.vg-mirror .vg-title-1 { font-size: 60px; }
.vg-mirror .vg-body    { font-size: 38px; }
.vg-mirror .vg-label   { font-size: 32px; }
.vg-mirror .vg-caption { font-size: var(--vg-mirror-text-min); }
.vg-mirror .vg-card, .vg-mirror .vg-panel { box-shadow: none; }

/* Gesture target, not a button. Sized from sensor jitter, never from a
   fingertip: 160 px default, 120 px floor, 48 px between neighbours. */
.vg-mirror .vg-btn {
  min-block-size: var(--vg-mirror-target);
  min-inline-size: var(--vg-mirror-target);
  padding-inline: var(--vg-space-10);
  font-size: 38px;
}
.vg-mirror .vg-btn--sm { min-block-size: var(--vg-mirror-target-min); min-inline-size: var(--vg-mirror-target-min); font-size: 32px; }
.vg-mirror .vg-strip { display: flex; gap: var(--vg-mirror-target-gap); }

/* ── Gesture input (Mirror) ────────────────────────────────────────────────
   A gesture interface has no cursor, no hover and no tap. It needs three
   things the touch model never did: a visible pointer, a dwell indicator
   that shows selection accruing, and an explicit hand-tracking state so the
   customer is never left wondering whether the screen can see them. */

/* ── Mirror: discrete air-gesture kit ──────────────────────────────────────
   There is no pointer and no dwell-on-target. Rows are cycled with swipe
   gestures; the confirmation device is a progressive underline the customer
   watches fill. Everything below serves that model. */

/* Progressive confirmation — the animated form of the tab indicator (§24.3),
   at Mirror scale (--vg-mirror-rule). The active option gets a faint track so
   the customer can see how far there is to go; the filled portion is the stroke —
   the region's one indicator (§30.2.2). Inactive options carry no rule at all.

   THE STROKE TRACES THE EDGE OF THE THING BEING SELECTED. For a text option
   that edge is the bottom rule below the label (there is no perimeter worth
   tracing, and a box around a label would make it look like a button — the
   one thing this interface must never suggest). For an OBJECT — a colour
   swatch, a scene tile — the edge is its own outline, so the stroke travels
   round it: see .vg-trace. Same device, same colour, same duration, same
   meaning; it follows the geometry of what it is committing. A rule floating
   under a row of five swatches names the row, not the swatch. */
.vg-prog { display: flex; flex-direction: column; gap: var(--vg-space-3); }
/* At Mirror scale the rule must sit CLOSE to the label it belongs to. Two things
   pushed it away: this gap, and .vg-opt's 120 px block floor wrapping a 38 px
   label in a 120 px box. The floor is right for a block the customer looks at as
   an object (a tile); for a text option in a row the label IS the block, and
   legibility comes from type size while separation comes from the row gap. So a
   text option drops both floors — see .vg-opt--text. */
.vg-prog__track {
  block-size: var(--vg-mirror-rule);
  background: var(--vg-border-subtle);
  position: relative;
}
.vg-prog__track::after {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 0;
  inline-size: var(--vg-prog, 0%);
  background: var(--vg-border-accent);
}

/* Carousel pagination (.vg-dots) — POSITION ONLY. One dot per item, the active
   one white and a size up, the rest muted. Nothing on it animates and nothing
   fills: position and countdown are different questions, and answering both
   with one bar (an earlier draft's .vg-steps) put the countdown where the eye
   was not — the customer reads the label, so the timer belongs under the label.

   Rounded because this is the one pattern a first-time customer already owns
   from carousels and onboarding: it is borrowed convention, and borrowed
   convention is worth more here than internal novelty. Dots appear ONLY when
   the row is a window onto more items than fit — a row showing everything it
   has does not need a map of itself. */
.vg-dots { display: flex; gap: var(--vg-space-2); align-items: center; }
.vg-dots__dot {
  inline-size: var(--vg-mirror-dot); block-size: var(--vg-mirror-dot); flex: none;
  border-radius: var(--vg-radius-circle);
  background: var(--vg-text-muted);
}
.vg-dots__dot.is-active {
  inline-size: calc(var(--vg-mirror-dot) + var(--vg-space-1-5));
  block-size: calc(var(--vg-mirror-dot) + var(--vg-space-1-5));
  background: var(--vg-text-primary);
}

/* Browse cue (.vg-handcue) — how the browse gesture is taught, and the only
   navigation instruction in the product.

   TWO LINES: an icon, then the direction and what it does.

       [<-- o]        [o -->]
       Swipe left     Swipe right
       (Next)         (Prev)

   THE ICON IS A COMPLETE GESTURE, read in one glance: a filled dot where the
   hand starts, a shaft, and an arrowhead where it finishes. Earlier drafts tried
   a bare taper (ambiguous — the comet convention and the arrowhead convention
   disagree about which end is the destination) and then a hand label with no
   mark at all (which names who acts but never what to do). An origin dot plus a
   real arrowhead has neither problem: there is a beginning, and there is a
   direction, and nothing about it can be read backwards.

   THE LABEL NAMES THE DIRECTION, not the hand. Naming the direction is
   sufficient, and this is worth understanding rather than assuming: only the
   RIGHT hand can sweep leftward across the body, and only the LEFT hand can
   sweep rightward, so direction plus "inward" already determines the hand. The
   recogniser keys on hand and inward travel (§30.2.1); the glass only has to
   say which way. That is one fewer thing to read at 2 m.

   POSITION does the rest: the cue for each direction sits on the side of the
   screen that motion starts from, so the mark and the movement agree. */
.vg-handcue { display: flex; flex-direction: column; gap: var(--vg-space-2); }
.vg-handcue--end { align-items: flex-end; text-align: end; }
.vg-handcue__icon { display: block; margin-block-end: var(--vg-space-1); }
.vg-handcue__do {
  font-size: 32px; font-weight: var(--vg-w-medium);
  color: var(--vg-text-secondary); white-space: nowrap;
}
/* The outcome, subordinated on all three axes the type system offers at once:
   one step DOWN the Mirror scale (32 -> 28 px, which is the floor — it can go no
   quieter without becoming unreadable at 2 m), one step DOWN the text ladder
   (secondary -> tertiary), and back to regular weight. Lower case does the rest:
   an abbreviated gloss in parentheses reads as an aside, where Title Case would
   read as a second label competing with the first. The hand is the instruction;
   this is the consequence, and it should be available without being loud. */
.vg-handcue__does {
  font-size: var(--vg-mirror-text-min); color: var(--vg-text-tertiary);
  font-weight: var(--vg-weight-regular);
}

/* The browse row's footer: the two hand cues at the outer edges with the
   pagination between them. One line carries where you are, how many there are,
   and which hand does what. */
/* The two cues sit at the outer edges; the pagination is centred on the PANEL,
   not in the space left between them. Centring it between two labels of unequal
   width would put it off-centre, and — worse — a long row would push the wider
   cue past the 88 px inset, because three items sharing one line only fit while
   the middle one stays small. Absolute centring decouples them: the cues own the
   edges, the dots own the middle, and neither moves when the other changes.

   The arithmetic sets a real limit. Across 904 px of usable band, with cues about
   326 px wide and a --vg-space-6 clearance, the dots have 244 px before they
   touch one: at 14 px with --vg-space-2 gaps that is ELEVEN dots. A row longer
   than eleven shows a window of eleven with the active dot centred, so the
   customer still reads position without the row becoming uncountable — a
   pagination row nobody can count at a glance has stopped being a position
   indicator anyway. */
.vg-browsebar {
  position: relative;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--vg-space-6);
}
.vg-browsebar > .vg-dots {
  position: absolute; inset-inline-start: 50%; transform: translateX(-50%);
}

/* Progressive stroke that traces an OBJECT's own outline (§30.2.2). Rendered
   as an SVG overlay so the dash geometry is exact on both a circle and a
   rounded rectangle: pathLength is normalised to 100, so --vg-prog is a
   percentage in both cases and one animation drives either shape.

   Two strokes, always: a faint TRACK on the full outline and the filled portion
   over it. Without the track the customer sees how far the timer has come but
   not how far it has to go, which is the whole reason the device exists.

   Both start at TOP CENTRE and travel CLOCKWISE. A clock is the one shared
   mental model for a closing timer, and every geometry in the system obeys it:
   a ring closes like a clock face, a perimeter like a clock with corners.

   The stroke sits --vg-space-3 OUTSIDE the object's edge. It was briefly moved
   onto the edge; that is wrong for this content. The accent has to frame colour
   samples and photographic scene plates, and a coloured stroke touching either interferes
   with the one thing the tile exists to show — a stroke against a camel
   swatch or a graded beach plate shifts how both read. The gap keeps the
   indicator legible as an indicator and keeps the sample honest. This is the
   one place a separated stroke beats a contiguous one, and the reason is the
   content, not the geometry. */
.vg-trace { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.vg-trace__track {
  fill: none; stroke: var(--vg-border-subtle);
  stroke-width: var(--vg-mirror-rule);
}
.vg-trace__line {
  fill: none; stroke: var(--vg-border-accent);
  stroke-width: var(--vg-mirror-rule); stroke-linecap: butt;
  stroke-dasharray: 100; stroke-dashoffset: calc(100 - var(--vg-prog, 0));
}

/* Depleting timer — a deadline, not a selection. The distinction is carried by
   direction: a track that FILLS means "this is about to be chosen"; a track
   that EMPTIES means "this screen is about to go". Two behaviours the customer
   must not confuse, so they never share a direction. Neutral, never the stroke colour:
   nothing is being selected, so nothing here is the accent. */
.vg-deplete { block-size: var(--vg-mirror-rule); background: var(--vg-border-subtle);
  position: relative; }
.vg-deplete::after {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 0;
  inline-size: var(--vg-remain, 100%); background: var(--vg-text-secondary);
}

/* An option in a swipe row. Not a button — nothing is pressed. The active one
   is named by contrast and by its progressive underline, never by a fill
   or a coloured border. */
.vg-opt {
  display: inline-flex; align-items: center; justify-content: center;
  min-inline-size: var(--vg-mirror-target); min-block-size: var(--vg-mirror-target-min);
  padding-inline: var(--vg-space-8);
  color: var(--vg-text-tertiary);
  border-radius: var(--vg-radius-control);
  border: var(--vg-border-width-hair) solid transparent;
}
.vg-opt.is-active { color: var(--vg-text-primary); }
.vg-opt--text {   /* a label in a row, not a block: type and gap carry it */
  min-inline-size: 0; min-block-size: 0; padding-inline: 0;
}

/* Gesture affordance — an edge-anchored plate naming a raise gesture, placed
   on the SIDE of the hand it responds to. The render is mirrored, so a
   customer raising their right hand watches their own hand rise toward the
   right-hand plate: the "whose left?" ambiguity dissolves spatially instead of
   being explained in words. Square where it meets the frame, eased on its free
   corners only (§12.1 anchored edges). No shadow — it is part of the frame. */
.vg-affordance {
  position: absolute; inset-block-start: 0;
  display: flex; flex-direction: column; gap: var(--vg-space-2);
  padding: var(--vg-space-8);
  background: var(--vg-surface-default);
  border: var(--vg-border-width-hair) solid var(--vg-border-default);
}
.vg-affordance--left  { inset-inline-start: 0; border-inline-start: 0; border-block-start: 0;
  border-end-end-radius: var(--vg-radius-object); }
.vg-affordance--right { inset-inline-end: 0; border-inline-end: 0; border-block-start: 0;
  border-end-start-radius: var(--vg-radius-object); align-items: flex-end; text-align: end; }
.vg-affordance__hold {   /* fills over --vg-mirror-hold while the hand is up */
  align-self: stretch;
  block-size: var(--vg-mirror-rule); background: var(--vg-border-subtle);
  position: relative; margin-block-start: var(--vg-space-3);
}
.vg-affordance__hold::after {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 0;
  inline-size: var(--vg-hold, 0%); background: var(--vg-border-accent);
}
/* A completed hold stays in the ACCENT, and so does the label beside it. An
   earlier draft turned both teal on the §8.4 reading that confirmation is teal;
   that misread the object. This rule is not a status message — it is the
   terminal frame of a progressive stroke, and every progressive stroke in the
   product runs one colour from 0% to 100% (§30.2.2) — white, since v5. Switching colour at the moment of
   completion breaks the one device the customer has learned to watch. §8.4's
   teal governs status surfaces — toasts, banners, scan results — not the
   indicator itself. */
.vg-affordance__hold.is-done::after { background: var(--vg-border-accent); }

/* Garment preview — the neighbours in the row, cropped by the screen edge. A
   partially visible object is the strongest "there is more that way" signal
   there is, and at the periphery it cannot compete with the person.
   NO LABEL. A name on a plate this small, this far into the periphery, cannot
   be read at 2 m — and a name the customer squints at costs more attention
   than the shape it was meant to explain. The garment is shown; only the one
   they are wearing is named. */
.vg-preview {
  position: absolute; display: flex; flex-direction: column; gap: var(--vg-space-3);
  background: var(--vg-surface-default);
  border: var(--vg-border-width-hair) solid var(--vg-border-subtle);
  padding: var(--vg-space-4);
}
.vg-preview--prev { inset-inline-start: -60px; border-inline-start: 0;
  border-start-end-radius: var(--vg-radius-object); border-end-end-radius: var(--vg-radius-object); }
.vg-preview--next { inset-inline-end: -60px; border-inline-end: 0;
  border-start-start-radius: var(--vg-radius-object); border-end-start-radius: var(--vg-radius-object); }
.vg-preview__plate { flex: 1; background: var(--vg-surface-raised);
  border-radius: var(--vg-radius-object); }

/* Size availability. Communicates stock; it is not a control. Available sizes
   are boxed and legible; unavailable sizes take --vg-text-disabled and lose
   the box — never opacity (§32.1). */
.vg-sizes { display: flex; gap: var(--vg-space-3); align-items: center; }
.vg-size {
  display: inline-flex; align-items: center; justify-content: center;
  min-inline-size: 72px; block-size: 72px;
  border-radius: var(--vg-radius-control);
  border: var(--vg-border-width-hair) solid var(--vg-border-default);
  color: var(--vg-text-primary);
}
.vg-size.is-unavailable { border-color: transparent; color: var(--vg-text-disabled); }

/* Colour tile and scene tile — the SAME OBJECT with different content, so the
   same geometry: --vg-radius-object, identical dimensions, identical stroke.

   An earlier draft made the colour a circle, on the argument that a colour
   sample has no orientation and roundness is its convention. That argument
   loses to two better ones. First, a round tile and a square tile appearing in
   the same anatomy, one screen apart, reads as two different systems — and
   "same row, same shape" is a stronger gestalt than the swatch convention.
   Second, a rectangle of the same nominal size shows roughly 27% more colour
   area, and at 2 m colour judgement is area-limited. The circle is withdrawn
   from §12.1's meaning-bearing list. */
.vg-swatch, .vg-scene {
  position: relative; flex: none;
  border-radius: var(--vg-radius-object);
  border: var(--vg-border-width-hair) solid var(--vg-border-subtle);
}
.vg-swatch .vg-trace, .vg-scene .vg-trace { inset: calc(-1 * var(--vg-space-3)); }
.vg-swatch { border-color: var(--vg-border-strong); }

/* Bottom sheet — summoned, anchored, temporary. Square where it meets the
   frame, eased on its free top corners; no shadow, because it is anchored
   rather than floating (§12.2). */
.vg-sheet {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  background: var(--vg-surface-raised);
  border-block-start: var(--vg-border-width-hair) solid var(--vg-border-default);
  border-start-start-radius: var(--vg-radius-object);
  border-start-end-radius: var(--vg-radius-object);
}

/* Withdrawn in 4.1: .vg-launch and all its overrides.
   The Mirror boots into a SPLASH — the placed mark, revealed, resolving into the
   attract screen by itself (§30.1.1). It needs no class of its own: no buttons,
   no type below the Mirror floor, no operator chrome. Two earlier drafts built
   an operator console here and then a pitch screen, and both needed exemptions
   from the Mirror legibility rules to exist. Deleting the surface deletes the
   exemptions, which is the better outcome: there is now NO Mirror surface with a
   button and NO Mirror surface outside the 28 px floor, unconditionally.
   The operator console is deferred; when it is built it gets its own review. */

/* ── Scan (Mirror) ─────────────────────────────────────────────────────────
   The barcode / QR scanner is a first-class input, not an accessory: it is
   how a garment in the customer's hand becomes a garment on the screen, and
   it is the accessible path when gesture is not available to someone. */
.vg-scan { display: flex; align-items: center; gap: var(--vg-space-4);
  padding: var(--vg-space-6); border-radius: var(--vg-radius-object);
  border: var(--vg-border-width-hair) dashed var(--vg-border-strong);
  font-size: 38px; color: var(--vg-text-secondary); }
.vg-scan--found  { border-style: solid; border-color: var(--vg-status-success); color: var(--vg-text-primary); }
.vg-scan--unknown{ border-style: solid; border-color: var(--vg-status-warning); color: var(--vg-text-primary); }

/* Withdrawn in 4.1: .vg-capture (camera status indicator) and .vg-handstate.
   There is no persistent status of any kind on the Mirror — the privacy
   disclosure is one sentence on the idle screen, before anything starts
   (§30.4). Do not reintroduce either class.

   Session countdown — the visible clearing count on the picks handoff after
   the customer walks away (--vg-mirror-reset). Live count, tabular figures,
   secondary contrast minimum: privacy copy is never set in muted. */
.vg-countdown { display: flex; align-items: center; gap: var(--vg-space-6); color: var(--vg-text-secondary); }
.vg-countdown__num { font-variant-numeric: tabular-nums lining-nums; color: var(--vg-text-primary); font-weight: var(--vg-w-medium); }


/* ── Utilities ─────────────────────────────────────────────────────────── */
.vg-visually-hidden {
  position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.vg-skip-link {
  position: absolute; inset-block-start: -100%; inset-inline-start: var(--vg-space-4);
  padding: var(--vg-space-2) var(--vg-space-4); background: var(--vg-surface-raised);
  border: var(--vg-border-width-hair) solid var(--vg-border-default);
  border-radius: var(--vg-radius-structure); z-index: var(--vg-z-tooltip);
}
.vg-skip-link:focus { inset-block-start: var(--vg-space-4); }

/* ── Reduced motion — non-negotiable ──────────────────────────────────────
   Implemented at the token layer so the policy in §14.4 is exact:
   · transform-based entrances, staggers and long reveals collapse to instant
   · opacity-only state fades at dur-1/dur-2 are RETAINED — they carry state
     legibility and do not trigger vestibular discomfort
   · looping indicators become static: the indeterminate bar parks at 30%
     (label it with aria-busy text), skeletons stop shimmering, spinners stop */
@media (prefers-reduced-motion: reduce) {
  :root, [data-theme] {
    --vg-dur-3: 1ms; --vg-dur-4: 1ms; --vg-dur-5: 1ms; --vg-dur-6: 1ms;
    --vg-stagger: 0ms;
    --vg-distance-sm: 0px; --vg-distance-md: 0px; --vg-distance-lg: 0px;
  }
  .vg-skeleton, .vg-progress--indeterminate .vg-progress__fill,
  .vg-spinner, .vg-btn.is-loading svg, .vg-btn.is-loading .vg-spinner {
    animation: none;
  }
  .vg-toggle, .vg-toggle::after { transition-duration: 1ms; }
  /* The marquee stops dead; the reveal resolves to its final state rather
     than animating to it — the content must never be the thing withheld. */
  .vg-marquee__track { animation: none; }
  .vg-reveal { opacity: 1; transform: none; transition: none; }
  html { scroll-behavior: auto; }
}

/* ── Geometry — v4.0 ──────────────────────────────────────────────────────
   Radius is applied by role, never by eye. A component that needs a corner
   picks the role token that describes what it is; it never picks a number.
   The one computed guardrail: no radius may exceed 1/8 of the element's
   shortest side — if it does, step down one role. Where role and elevation
   disagree, elevation wins (§12.2). Checked by render_check.py.            */

/* Avatars are square (§12.1) — identity imagery is content, not a control.
   Where a circular avatar arrives from an external source, it is masked to
   the square, never re-rounded. */
.vg-avatar { inline-size: 32px; block-size: 32px; background: var(--vg-surface-overlay);
  border: var(--vg-border-width-hair) solid var(--vg-border-default);
  border-radius: var(--vg-radius-structure); overflow: hidden; flex: none; }
.vg-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }

/* ── Brand assets ─────────────────────────────────────────────────────────
   The logo is never rebuilt in HTML/CSS — embed the released SVGs from
   assets/brand/logo/ (§7). Dark artwork on dark/mirror themes, light
   artwork on light theme. Sizing floors are in §7.4. */
.vg-logo { display: inline-flex; flex: none; }
.vg-logo img, .vg-logo svg { display: block; block-size: 100%; inline-size: auto; }
