/* ============================================================================
   Diagnose Your Revenue Leak — diagnostic modal
   ----------------------------------------------------------------------------
   Self-contained. Everything is .vgd-* scoped so it cannot collide with the
   existing .diag-* lead-capture flow (flow.css), which keeps serving
   "Let's talk" until this module replaces it.

   Tokens below are the Figma values, taken from all three modes of the Smartt
   Advisory file: the mobile 390 board, the tablet boards at 900 and 1100, and
   the desktop boards at 1440 and 1920. Figma switches its whole scale at each
   breakpoint; we interpolate instead, using the same
   clamp(min, slope·vw + intercept, max) formula the live site uses, in two legs
   so every value lands exactly on the Figma number at 390, across the whole
   900-1100 tablet band, and at 1440.

   Layout has two real breakpoints. At 900 the sheet gains its 40px side inset,
   fills the height, and picks up the long lede and the third spec cell. At
   1100 the collapsed accordion becomes the two-column rail.
   ========================================================================== */

:root {
  /* ── Colour, Figma exactly ─────────────────────────────────────────── */
  --vgd-text:           #171717;   /* text-colour/text-primary */
  --vgd-text-2:         #4d4d4d;   /* text-colour/text-secondary */
  --vgd-text-invert:    #ffffff;   /* text-colour/text-primary-invert */
  --vgd-surface:        #fafafa;   /* surface-colour/surface-primary */
  --vgd-surface-2:      #ffffff;   /* surface-colour/surface-secondary */
  --vgd-surface-3:      #f2f2f2;   /* surface-colour/surface-tertiary */
  --vgd-surface-invert: #171717;   /* surface-colour/surface-invert-primary */
  --vgd-border:         #e0e0e0;   /* border-colour/border-tertiary */
  --vgd-border-2:       #ebebeb;   /* border-colour/border-secondary */
  --vgd-border-ink:     #171717;   /* border-colour/border-primary */
  /* Hover on a selectable card. Figma writes this one as a raw hex rather than
     a token (Concern Block, State=Hover), so it is copied literally. */
  --vgd-border-hover:   #8d8d8d;
  --vgd-scrim:          rgba(23, 23, 23, 0.4);

  /* ── Type and spacing, mobile → TABLET ─────────────────────────────────
     Figma has three modes, not two: mobile at 390, tablet across the 900-1100
     band, desktop at 1440. Tablet is a real mode with its own type scale, and
     it is not the midpoint of the other two — its spacing has already reached
     the desktop numbers while its type is still climbing, so a single 390→1440
     ramp misses it badly (67px display against Figma's 60, 48px screen padding
     against Figma's 40).

     So the ramp is in two legs. Everything here is anchored to hit mobile
     exactly at 390 and tablet exactly at 900, and every clamp is capped at its
     tablet value, which is what holds the whole 900-1100 band flat on the
     tablet mode with no extra rules. The second leg, tablet → desktop, is the
     min-width: 1100 block below and only has to restate what actually changes.

                             390    900   1440 */
  --vgd-display:    clamp(40px, 3.9216vw + 24.71px, 60px);  /* 40  →  60  →  80 */
  --vgd-h3:         clamp(28px, 0.7843vw + 24.94px, 32px);  /* 28  →  32  →  36 */
  --vgd-lh-h3:      36px;                                   /* 36  →  36  →  44 */
  --vgd-h4:         clamp(24px, 0.3922vw + 22.47px, 26px);  /* 24  →  26  →  28 */
  --vgd-lh-h4:      32px;                                   /*     →  32  →  36 */
  --vgd-eyebrow:    12px;                                   /* 12  →  12  →  14 */
  --vgd-lh-eyebrow: 20px;                                   /* 20  →  20  →  24 */
  --vgd-body:       16px;                                   /* 16  →  16  →  20 */
  --vgd-lh-body:    24px;                                   /* 24  →  24  →  28 */
  --vgd-lede:       clamp(18px, 0.3922vw + 16.47px, 20px);  /* 18  →  20  →  24 */
  --vgd-lh-lede:    clamp(26px, 0.3922vw + 24.47px, 28px);  /* 26  →  28  →  32 */
  --vgd-sm:         14px;                                   /* 14  →  14  →  16 */
  --vgd-caption:    12px;                                   /* 12  →  12  →  14 */
  /* H2 carries the gate completion deck and nothing else so far. No tablet
     board uses it, so the middle value is inferred rather than read: display
     and h3 both put the tablet mode on the exact midpoint of the size ramp
     (40→60→80, 28→32→36), so H2's is 32, and 40 is the midpoint of its two
     line heights. Replace both with the real numbers if a tablet board that
     uses H2 ever lands. */
  --vgd-h2:         clamp(24px, 1.5686vw + 17.88px, 32px);  /* 24  →  32  →  40 */
  --vgd-lh-h2:      clamp(32px, 1.5686vw + 25.88px, 40px);  /* 32  →  40  →  48 */

  /* Spacing is already at its desktop values by the tablet mode, so these
     stop climbing at 900 and the second leg leaves them alone. */
  --vgd-sp-100: clamp(4px,  0.7843vw +  0.94px,  8px);
  --vgd-sp-150: clamp(8px,  0.7843vw +  4.94px, 12px);
  --vgd-sp-200: clamp(12px, 0.7843vw +  8.94px, 16px);
  --vgd-sp-250: clamp(16px, 0.7843vw + 12.94px, 20px);
  --vgd-sp-300: clamp(16px, 1.5686vw +  9.88px, 24px);
  --vgd-sp-500: clamp(24px, 3.1373vw + 11.76px, 40px);

  /* The completion screen's own block gap, and the one value on this screen
     that runs DOWN as the viewport grows: the mobile board sets it from
     spacing/250 (16) and the desktop board writes a raw 12. It switches rather
     than ramps, at 900, on the same reading as everything else here — tablet
     spacing has already arrived at the desktop numbers. The row gap inside it
     goes the other way, 8 to 12, which is exactly --vgd-sp-150, so that one
     needs no token of its own. */
  --vgd-gate-gap: 16px;

  /* Screen padding: spacing/300 on mobile, spacing/500 at tablet, spacing/800
     on desktop. The only spacing that keeps moving past 900. */
  --vgd-pad-x: clamp(16px, 4.7059vw -  2.35px, 40px);
  --vgd-pad-y: clamp(24px, 7.8431vw -  6.59px, 64px);

  /* Fixed heights Figma sets outright, rather than deriving from padding. */
  --vgd-bar-h: clamp(64px, 1.5686vw + 57.88px, 72px);   /* 64 → 72 → 72 */
  --vgd-cta-h: clamp(48px, 3.1373vw + 35.77px, 64px);   /* 48 → 64 → 64 */
  --vgd-pill-h: 48px;   /* context-step select; 48 mobile → 52 desktop (below) */

  /* ── Reveal ─────────────────────────────────────────────────────────
     The reveal introduces error/rust and a few type roles the run does not
     use. These interpolate straight 390 → 1440 (single clamp) rather than in
     the two legs the shared scale uses: there is no tablet reveal board to
     anchor a mid point, so a monotonic ramp is the honest reading. */
  --vgd-error:          #cb2a2f;   /* surface/-text/-border error */
  --vgd-error-tint:     rgba(203, 42, 47, 0.12);   /* leaking bar track */
  --vgd-neutral-500:    #a1a1a1;   /* muted text on the black block */
  --vgd-divider-invert: rgba(255, 255, 255, 0.2);  /* #ffffff33 hairlines */
  --vgd-success:        #24c355;   /* the Next-steps dot */
  --vgd-edge:           #008816;   /* healthy: stamp, next-edge stat, softest gate mark + bar */
  --vgd-edge-tint:      #e0f0e3;   /* healthy: next-edge bar track */
  --vgd-rev-score:      80px;                                 /* stamp figure, fixed both boards */
  --vgd-rev-verdict:    clamp(40px, 1.524vw + 34.06px, 56px); /* 40 → 56 */
  --vgd-rev-offer:      clamp(32px, 2.286vw + 23.08px, 56px); /* 32 → 56 */
  --vgd-lg-body:        clamp(16px, 0.381vw + 14.51px, 20px); /* 16 → 20 */
  --vgd-lh-lg:          clamp(24px, 0.762vw + 21.03px, 32px); /* 24 → 32 */
  --vgd-stamp:          232px;   /* score stamp, fixed both boards */

  /* ── Radius and layout ────────────────────────────────────────────── */
  --vgd-radius:      clamp(12px, 0.7843vw + 8.94px, 16px); /* radius/lg-image */
  --vgd-radius-card: 8px;
  --vgd-rail-w:      394px;   /* only in play from 1100 up; see below */
  /* The sheet never reaches the top edge: a 40px dimmed strip is what keeps it
     reading as a modal rather than a page, at every width. The mobile boards
     run it full-bleed sideways; from the desktop layout up it is a flat 40px
     on all three edges and the sheet simply grows with the viewport. */
  --vgd-inset-top: 40px;
  --vgd-inset-x:   0px;

  /* ── The entrance ─────────────────────────────────────────────────────
     Taken from fluid.glass, which slides its quote panel with GSAP
     `duration: 1, ease: "power3.inOut"` and plays the same tween backwards to
     close. Ours travels up instead of in from the right, so the sheet runs
     translateY(100%) → 0 rather than xPercent 100 → 0. power3.inOut is
     easeInOutCubic; the bezier below is its standard CSS form and sits within
     1% of the real curve at its worst point, about 80% through the move.
     --vgd-slide has to stay in step with SLIDE_MS in ui.js. */
  --vgd-slide:      1000ms;
  --vgd-ease-slide: cubic-bezier(0.65, 0, 0.35, 1);
  /* The mobile accordion rides the same curve. It is shorter than the full
     slide because it starts from a tap rather than from nothing, and a full
     second between finger and answer reads as lag rather than as weight. The
     sheet's own height is not animated separately: on mobile the sheet is
     height:auto and bottom-anchored, so it simply tracks the panel and
     inherits this curve for free. */
  --vgd-slide-panel: 600ms;
  /* The title bar's handover between the launch layout and the flow layout:
     the back tile arrives, and the identity group travels from the left edge
     to the centre. Shorter again than the accordion, because it is a
     consequence of a tap rather than the thing the tap asked for. */
  --vgd-slide-bar: 500ms;
  /* One question leaving and the next arriving, on the same curve as the
     sheet. Deliberately lopsided: the outgoing half is short because it is
     the wait between the tap and the answer, and eighteen of those add up,
     while the incoming half can take its time because the new question is
     already readable before it settles. --vgd-step-out has to stay in step
     with STEP_OUT_MS in ui.js. The shift is small on purpose. The move only
     has to say which way the flow went; a full-width slide turns a diagnostic
     into a carousel. */
  --vgd-step-out:   160ms;
  --vgd-step-in:    320ms;
  --vgd-step-shift: 32px;
  /* The reveal bar-draw: each gate's fill sweeps from zero to its score once,
     alongside the count-up. Driven from JS so it degrades to a resting full bar
     when nothing is painting; see revealMotion. */
  --vgd-rev-draw:   900ms;
  /* The chain's own beats, which run behind the screen change rather than with
     it: the tile takes the ink first, then the line draws down from it, so the
     eye reads a cause and its consequence instead of two things moving at once.
     The tick is a single pass and never repeats; --vgd-rail-tick has to stay in
     step with RAIL_TICK_MS in ui.js. */
  --vgd-rail-fill:  280ms;
  --vgd-rail-run:   420ms;
  --vgd-rail-lag:   120ms;   /* the line waits this long for the tile */
  --vgd-rail-tick:  520ms;

  /* Widest the screen content ever gets. The 1440 board has no room to reach
     it (the column gives 712); the 1920 board shows the content held at this
     width and centred in the column rather than stretching. 842 is Figma's
     840 content box plus its two hairlines, so a bordered block lands on the
     same width at both boards. */
  --vgd-content-max: 842px;
}

/* The 40px side inset arrives with the tablet mode: the mobile boards run the
   sheet full-bleed sideways, both tablet boards and the desktop boards inset it
   40 on all three edges. */
@media (min-width: 900px) {
  :root {
    --vgd-inset-x:  40px;
    --vgd-gate-gap: 12px;
  }
}

/* ── Second leg: tablet → desktop ──────────────────────────────────────
   Only the tokens that actually keep moving past the tablet band. Anchored to
   hit tablet exactly at 1100 and desktop exactly at 1440, so it picks up
   continuously from where the first leg's caps left off. */
@media (min-width: 1100px) {
  :root {
    --vgd-display:    clamp(60px, 5.8824vw -  4.71px, 80px);
    --vgd-h3:         clamp(32px, 1.1765vw + 19.06px, 36px);
    --vgd-lh-h3:      clamp(36px, 2.3529vw + 10.12px, 44px);
    --vgd-h4:         clamp(26px, 0.5882vw + 19.53px, 28px);
    --vgd-lh-h4:      clamp(32px, 1.1765vw + 19.06px, 36px);
    --vgd-eyebrow:    clamp(12px, 0.5882vw +  5.53px, 14px);
    --vgd-lh-eyebrow: clamp(20px, 1.1765vw +  7.06px, 24px);
    --vgd-body:       clamp(16px, 1.1765vw +  3.06px, 20px);
    --vgd-lh-body:    clamp(24px, 1.1765vw + 11.06px, 28px);
    --vgd-lede:       clamp(20px, 1.1765vw +  7.06px, 24px);
    --vgd-lh-lede:    clamp(28px, 1.1765vw + 15.06px, 32px);
    --vgd-sm:         clamp(14px, 0.5882vw +  7.53px, 16px);
    --vgd-caption:    clamp(12px, 0.5882vw +  5.53px, 14px);
    --vgd-h2:         clamp(32px, 2.3529vw +  6.12px, 40px);
    --vgd-lh-h2:      clamp(40px, 2.3529vw + 14.12px, 48px);
    --vgd-pad-x:      clamp(40px, 7.0588vw - 37.65px, 64px);
    --vgd-pill-h:     52px;
    /* The rail appears at 1100 at 394 and widens to Figma's 470 by 1440. */
    --vgd-rail-w:     clamp(394px, 22.3529vw + 148.12px, 470px);
  }
}

/* ── Container ─────────────────────────────────────────────────────────
   Bottom-anchored cover. Figma has no sliver below the sheet on either
   board, so it runs flush to the bottom edge and only the top corners are
   rounded. Hidden until .is-open; display:none keeps it out of the tab
   order without needing inert.                                          */
.vgd-root {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: none;
  align-items: flex-end;
  justify-content: center;
  padding: var(--vgd-inset-top) var(--vgd-inset-x) 0;
  /* The sheet parks a full height below its resting place between states, so
     the root has to clip rather than let it hang past the viewport. */
  overflow: hidden;
}
.vgd-root.is-mounted { display: flex; }

/* The scrim is a pseudo-element rather than the root itself, so that fading it
   does not fade the sheet with it: the sheet is solid the whole way up and the
   whole way back down, and only the dimming behind it changes. It stays a
   pseudo deliberately, since a real element would become the click target and
   break the click-outside test, which compares against the root.

   Exactly the live .diag-overlay treatment, which is also what Figma
   specifies: rgba(23,23,23,0.4) over a 4px blur. Keep the two in step. */
.vgd-root::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--vgd-scrim);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  opacity: 0;
  /* Same curve and length as the slide, so the dimming and the sheet read as
     one move rather than two. */
  transition: opacity var(--vgd-slide) var(--vgd-ease-slide);
}
.vgd-root.is-open::before { opacity: 1; }

.vgd-sheet {
  /* Positioned so it paints above the absolutely-positioned scrim, which would
     otherwise sit on top of it: a positioned pseudo outranks a static sibling. */
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  max-height: 100%;
  overflow: hidden;
  background: var(--vgd-surface);
  border-radius: var(--vgd-radius) var(--vgd-radius) 0 0;
  font-family: 'Geist', sans-serif;
  color: var(--vgd-text);
  /* Proportional figures, which is what Figma draws and what the rest of the
     site uses. Geist's tabular one carries a full foot serif to fill the
     tabular advance; its proportional one is a bare stem with a flag, and that
     is the one on every board. This used to say font-feature-settings:'tnum',
     which was never a decision anyone recorded, and it changed the shape of
     every digit in the modal. If a counter ever needs figures that hold their
     width while they change, put tnum on that counter, not on the sheet. */
  /* Starts a full sheet-height below where it comes to rest, which is off the
     bottom of the viewport since the sheet is bottom-anchored. */
  transform: translateY(100%);
  transition: transform var(--vgd-slide) var(--vgd-ease-slide);
  will-change: transform;
}
.vgd-root.is-open .vgd-sheet { transform: translateY(0); }

/* Mobile sheet grows to its content rather than filling the viewport, which
   is what both mobile boards show. max-height is 100% of the padded root, so
   once the content would overflow it stops 40px below the top edge and the
   screen inside it scrolls instead. */
@media (max-width: 899px) {
  .vgd-sheet { height: auto; }
  /* Except on the completion beat, which centres its content vertically and so
     needs a height to centre inside. Its mobile board runs the sheet all the
     way to the cap rather than shrink-wrapping the block. */
  .vgd-sheet[data-screen="gateComplete"] { height: 100%; }
}

/* ── Title bar ─────────────────────────────────────────────────────────
   Figma sets the bar to a fixed height with the hairline drawn inside it:
   64 on the mobile board, 72 on desktop. Deriving the height from padding
   instead puts the hairline 1px low, because a CSS border adds to an auto
   height where Figma's stroke does not. Height wins; box-sizing is already
   border-box site-wide, so the hairline sits on the last row of the 72. */
/* Two layouts, and the bar travels between them rather than cutting.
   Launch: identity group hard left, no back tile, because there is nowhere to
   go back to. Every screen after: the back tile arrives and the identity group
   sits centred between the two 32px tiles.

   The back tile is absolutely positioned in BOTH states and the close tile is
   pushed right by an auto margin, so neither is ever load-bearing for the
   identity group's position. That is what makes the move measurable: the group
   is the only thing that has to travel, and it travels under a FLIP in
   applyBar() (ui.js), which needs its transform property free. So nothing here
   centres anything with a transform: the group centres by being stretched
   left-to-right with auto inline margins, and both tiles centre vertically by
   filling the bar's height and centring their own contents. */
.vgd-bar {
  position: relative;
  display: flex;
  align-items: center;
  flex-shrink: 0;
  height: var(--vgd-bar-h);
  padding: 0 var(--vgd-sp-300);
  background: var(--vgd-surface);
  border-bottom: 1px solid var(--vgd-border);
}
.vgd-bar-id {
  display: flex;
  align-items: center;
  gap: var(--vgd-sp-200);
  transition: transform var(--vgd-slide-bar) var(--vgd-ease-slide);
}
.vgd-bar[data-back="true"] .vgd-bar-id {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: max-content;
  margin-inline: auto;
}
.vgd-close { margin-left: auto; }
/* Fades and slides in on its own curve while the group travels. It is disabled
   rather than hidden when it has nowhere to go: a disabled button is out of the
   tab order and out of the focus trap's selector, but still paints, so it can
   fade rather than vanish. */
.vgd-back {
  position: absolute;
  left: var(--vgd-sp-300);
  top: 0;
  bottom: 0;
  margin: auto 0;
  opacity: 0;
  transform: translateX(-6px);
  pointer-events: none;
  transition: opacity var(--vgd-slide-bar) var(--vgd-ease-slide),
              transform var(--vgd-slide-bar) var(--vgd-ease-slide);
}
.vgd-bar[data-back="true"] .vgd-back {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
/* The title changes with the layout, so it is faded back in over the move
   rather than swapped under the founder's eye. */
.vgd-brand { transition: opacity var(--vgd-slide-bar) var(--vgd-ease-slide); }
.vgd-avatar {
  width: 32px;
  height: 32px;
  border-radius: var(--vgd-radius-card);
  object-fit: cover;
  display: block;
}
/* On the confirmation screen the title-bar avatar crossfades to a check tile
   (same idiom as the live site), since the run is complete. */
.vgd-bar-ava { position: relative; width: 32px; height: 32px; flex-shrink: 0; }
.vgd-bar-ava .vgd-avatar { transition: opacity 0.3s ease; }
.vgd-bar-check {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--vgd-surface-invert);
  border-radius: var(--vgd-radius-card);
  color: var(--vgd-text-invert);
  opacity: 0;
  transition: opacity 0.3s ease;
}
.vgd-bar-check svg { width: 18px; height: 18px; display: block; }
.vgd-sheet[data-screen="confirmation"] .vgd-bar-ava .vgd-avatar { opacity: 0; }
.vgd-sheet[data-screen="confirmation"] .vgd-bar-check { opacity: 1; }
.vgd-brand {
  font-weight: 500;
  font-size: var(--vgd-body);
  line-height: var(--vgd-lh-body);
}
/* Back and close are the same 32px tile. Both being the same width is also what
   centres the title between them: the bar is justify-between, so with equal
   side items the middle group lands dead centre without being told to. */
.vgd-icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  padding: 0;
  background: var(--vgd-surface-2);
  border: 1px solid var(--vgd-border);
  border-radius: var(--vgd-radius-card);
  color: var(--vgd-text);
  cursor: pointer;
  transition: background 160ms ease;
}
.vgd-icon-btn:hover { background: var(--vgd-surface); }
.vgd-icon-btn svg   { width: 19px; height: 19px; display: block; }
/* The back tile is removed outright on the launch screen, not just made
   invisible: with it gone the bar has two items, so justify-between puts the
   identity group hard left, which is what the launch board shows. */

/* ── Frame ─────────────────────────────────────────────────────────────
   The body sits in a gutter, and the content inside it is drawn as a frame
   open at the top and bottom: one hairline down each outer edge. The line
   across the top is the title bar's own bottom border, which runs the full
   width of the sheet; the frame must not add a second one under it. On
   desktop the divider between the column and the rail is the column's own
   right border, so there is never a double line there either. */
.vgd-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  padding: 0 var(--vgd-sp-300);
  background: var(--vgd-surface);
  overflow: hidden;
}
.vgd-frame {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}
.vgd-col {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--vgd-border);
  border-right: 1px solid var(--vgd-border);
}

/* Only the screen scrolls; the title bar, rail and action row stay put.
   Scrollbars are hidden on both scrollers: the sheet is a designed surface
   and a platform gutter cutting into the 64px padding reads as a bug. The
   regions still scroll by wheel, trackpad, touch and keyboard. */
.vgd-screen {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Stated, rather than left to compute. The step move below travels
     sideways, and an x-axis left to itself resolves to auto next to a
     scrolling y, which hands that travel a real horizontal scroll. hidden and
     not clip: paired with a scrolling axis, clip computes to hidden anyway,
     so naming clip would only describe the box wrongly. Either way the box
     stays scrollable from script and not by hand, which is what we want. */
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--vgd-pad-y) var(--vgd-pad-x);
  display: flex;
  flex-direction: column;
  gap: 24px;   /* both boards use 24 here, it is not on the mobile→desktop ramp */
}
.vgd-actions {
  flex-shrink: 0;
  padding: var(--vgd-sp-250) var(--vgd-pad-x);
  /* Boundary hairline, drawn inside. A real border-top would push the screen
     1px shorter and move every divider below it, which is how the stack
     drifted off the board. */
  box-shadow: inset 0 1px 0 var(--vgd-border);
}

/* Beyond the 1440 board the sheet keeps growing but the reading column does
   not: the 1920 board holds the content at --vgd-content-max and centres it
   in the column. Below that width these are no-ops. */
.vgd-screen > *,
.vgd-actions > * {
  width: 100%;
  max-width: var(--vgd-content-max);
  margin-inline: auto;
}

/* ── Question to question ───────────────────────────────────────────────
   The screen's blocks move as one, so the question, its counter and its five
   options travel together and the 24px gap between them never breathes. They
   are the direct children of the scroller rather than a wrapper because a
   wrapper would become the only flex child and collapse that gap.

   The move is the shell's, not a screen's: `data-step` on the scroller is set
   by `step()` in ui.js and nothing a screen renders knows about it. Both
   directions are spelled out rather than derived from a sign, because the
   entering half of a move runs the same way as the leaving half of the
   opposite one and reading them side by side is how that stays true.

   `entering` carries transition:none so arming the start position is a jump,
   not a move. Clearing the attribute is what runs the transition, which is the
   same forced-reflow trick the title bar's handover uses. */
.vgd-screen > * {
  transition: transform var(--vgd-step-in) var(--vgd-ease-slide),
              opacity   var(--vgd-step-in) var(--vgd-ease-slide);
}
.vgd-screen[data-step="leaving"] > *,
.vgd-screen[data-step="leaving-back"] > * {
  opacity: 0;
  transition-duration: var(--vgd-step-out);
}
.vgd-screen[data-step="leaving"] > *       { transform: translateX(calc(var(--vgd-step-shift) * -1)); }
.vgd-screen[data-step="leaving-back"] > *  { transform: translateX(var(--vgd-step-shift)); }

.vgd-screen[data-step="entering"] > *,
.vgd-screen[data-step="entering-back"] > * {
  opacity: 0;
  transition: none;
}
.vgd-screen[data-step="entering"] > *      { transform: translateX(var(--vgd-step-shift)); }
.vgd-screen[data-step="entering-back"] > * { transform: translateX(calc(var(--vgd-step-shift) * -1)); }

.vgd-screen,
.vgd-rail { scrollbar-width: none; }
.vgd-screen::-webkit-scrollbar,
.vgd-rail::-webkit-scrollbar { width: 0; height: 0; }

/* Headings take focus on every screen change so the new screen is announced.
   That is a programmatic move, not a keyboard one, and Chrome still paints a
   focus ring around the display type. Interactive elements are untouched. */
.vgd-sheet [tabindex="-1"]:focus { outline: none; }

/* ── Primary button ──────────────────────────────────────────────────
   Hover is the site's own two-part reveal, same curve and duration as
   .btn-primary-lg: the background darkens, the icon slides out to the right
   while its twin arrives from the left, and the label lifts away while its
   twin rises into its place. Mirrored here rather than borrowed from
   index.html, because this module has to stand on its own on any page.
   The 24px line is the site's button line, and it is what both label
   twins are clipped to. */
.vgd-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  height: var(--vgd-cta-h);
  padding: 0 var(--vgd-sp-250);
  background: var(--vgd-surface-invert);
  border: 0;
  border-radius: var(--vgd-radius-card);
  color: var(--vgd-text-invert);
  font-family: inherit;
  font-weight: 500;
  font-size: var(--vgd-body);
  line-height: 24px;
  cursor: pointer;
  transition: background 0.3s ease;
}
.vgd-cta:hover:not(:disabled) { background: #2e2e2e; }
/* 0.2 is Figma's disabled state on the question boards, not a guess. */
.vgd-cta:disabled { opacity: 0.2; cursor: not-allowed; }
/* The right arrow, after the label, on every CTA in the flow. Ruled
   2026-07-29, replacing the magnifying glass that led the button before it.
   Position is DOM order, so nothing here needs to know about it. */
.vgd-cta svg { width: 24px; height: 24px; display: block; }

.vgd-btn-icon-wrap {
  position: relative;
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  overflow: hidden;
}
.vgd-btn-icon {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.4s cubic-bezier(0.76, 0, 0.24, 1);
}
.vgd-btn-icon--out { transform: translateX(0); }
.vgd-btn-icon--in  { transform: translateX(-120%); }
.vgd-cta:hover:not(:disabled) .vgd-btn-icon--out { transform: translateX(120%); }
.vgd-cta:hover:not(:disabled) .vgd-btn-icon--in  { transform: translateX(0); }

.vgd-btn-label-wrap {
  position: relative;
  height: 24px;
  overflow: hidden;
}
.vgd-btn-label {
  display: block;
  white-space: nowrap;
  transition: transform 0.4s cubic-bezier(0.76, 0, 0.24, 1);
}
.vgd-btn-label--out { transform: translateY(0); }
.vgd-btn-label--in  { position: absolute; top: 0; left: 0; transform: translateY(110%); }
.vgd-cta:hover:not(:disabled) .vgd-btn-label--out { transform: translateY(-110%); }
.vgd-cta:hover:not(:disabled) .vgd-btn-label--in  { transform: translateY(0); }

/* Placing a booking: the button drops its arrow and a soft sheen sweeps across
   it. It stays at full strength while it works (overriding the 0.2 disabled
   fade), so it reads as busy rather than switched off. */
.vgd-cta.is-booking {
  position: relative;
  overflow: hidden;
}
.vgd-cta.is-booking:disabled { opacity: 1; cursor: progress; }
.vgd-cta.is-booking .vgd-btn-icon-wrap { display: none; }
.vgd-cta.is-booking::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.14), transparent);
  transform: translateX(-100%);
  animation: vgd-book-sheen 1.4s ease-in-out infinite;
  pointer-events: none;
}
@keyframes vgd-book-sheen {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .vgd-cta.is-booking::after { animation: none; }
}

/* ── Type helpers ──────────────────────────────────────────────────── */
.vgd-eyebrow {
  font-weight: 500;
  font-size: var(--vgd-eyebrow);
  line-height: var(--vgd-lh-eyebrow);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--vgd-text-2);
}
.vgd-display {
  font-weight: 500;
  font-size: var(--vgd-display);
  line-height: 1;
  letter-spacing: -0.01em;
}
.vgd-lede {
  font-weight: 400;
  font-size: var(--vgd-lede);
  line-height: var(--vgd-lh-lede);
  color: var(--vgd-text-2);
}

/* ── Breakpoint utilities ──────────────────────────────────────────────
   Two ledes ship because the two boards carry different strings, not
   different wrapping: mobile drops "Three minutes to see the real leak
   named." (blueprint 4.1). Only one is ever in the accessibility tree. */
.vgd-only-sm { display: none; }
.vgd-only-md { display: revert; }

/* Announced, never drawn. Used where an icon carries state that would
   otherwise be lost to a screen reader, such as the tick on a passed gate. */
.vgd-vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
@media (max-width: 899px) {
  .vgd-only-sm { display: revert; }
  .vgd-only-md { display: none; }
}

/* ── Launch screen ─────────────────────────────────────────────────── */
/* The two boards pick different tokens here rather than two ends of one ramp:
   spacing/300 on mobile, spacing/150 on desktop. Measured both ways, the
   eyebrow → display and display → lede gaps agree, and they account exactly
   for the 16px the mobile sheet was short. So it switches, it does not
   interpolate. */
.vgd-launch-head {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
@media (min-width: 900px) {
  .vgd-launch-head { gap: 12px; }
}
/* The headline breaks after "your" at every width. It used to wrap naturally
   below 900, which meant the break moved around as the display size climbed
   through the ramp. Holding it fixed keeps "Diagnose your" / "revenue leak."
   on two lines from the narrowest phone to the widest desktop. */
/* The lede breaks after "symptom." only once the rail is beside it. Both
   tablet boards run it as one wrapping paragraph, so the break belongs to the
   1100 breakpoint, not the 900 one. Same trick: the source carries a space
   after the <br>, so collapsing it still leaves a word gap. */
.vgd-launch-head .vgd-lede br { display: none; }
@media (min-width: 1100px) {
  .vgd-launch-head .vgd-lede br { display: inline; }
}

/* Spec grid + numbered steps read as one bordered block. Every hairline here,
   outer ring included, is an inset shadow rather than a border: Figma draws
   its strokes on the boundary between two rows, where they take no space,
   while a CSS border adds a pixel at every seam. Bordered, this block sat 5px
   taller than the board and pushed its own contents 1px off. */
.vgd-blocks {
  box-shadow: inset 0 0 0 1px var(--vgd-border);
  border-radius: var(--vgd-radius-card);
}
.vgd-spec {
  display: flex;
  box-shadow: inset 0 -1px 0 var(--vgd-border);
}
/* Labels hang from the top, values sit on the bottom, so the three numbers
   share a baseline however many lines a label takes. Both tablet boards show
   this: "QUESTIONS PER GATE" runs to two lines and the values still line up,
   with the extra space opening under the shorter labels. The 12px gap becomes
   a minimum rather than the spacing. */
.vgd-spec-cell {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 12px;   /* 12 on both boards, not on the mobile→desktop ramp */
  padding: var(--vgd-sp-300);
}
/* Across the tablet band the middle label is broken by hand, because Figma
   breaks it there even at 900 where it would otherwise fit with 20px to spare.
   From 1100 up it wraps on its own where it needs to and runs as one line on
   the desktop boards, so the break is only forced in the band. */
.vgd-spec-cell .vgd-eyebrow br { display: none; }
@media (min-width: 900px) and (max-width: 1099px) {
  .vgd-spec-cell .vgd-eyebrow br { display: inline; }
}
.vgd-spec-cell + .vgd-spec-cell { box-shadow: inset 1px 0 0 var(--vgd-border); }
.vgd-spec-value {
  font-weight: 500;
  font-size: var(--vgd-h3);
  line-height: var(--vgd-lh-h3);
}
/* Mobile drops the middle stat (Questions per gate), per blueprint 4.1. */
@media (max-width: 899px) {
  .vgd-spec-cell[data-optional] { display: none; }
}

.vgd-step {
  display: flex;
  align-items: flex-start;
  gap: var(--vgd-sp-200);
  padding: var(--vgd-sp-150) var(--vgd-sp-300);
}
.vgd-step + .vgd-step { box-shadow: inset 0 1px 0 var(--vgd-border); }
/* Shares the label's 24px line so the row is 48 tall on one line, not 52. */
.vgd-step-n {
  flex-shrink: 0;
  width: 24px;
  font-weight: 500;
  font-size: var(--vgd-eyebrow);
  line-height: 24px;
  letter-spacing: 2px;
  color: var(--vgd-text-2);
}
.vgd-step-t {
  font-weight: 500;
  font-size: var(--vgd-sm);
  line-height: 24px;
}

/* ── Progress bar ──────────────────────────────────────────────────────
   A 4px strip across the top of the reading column, under the title bar on
   desktop and under the collapsed chain on mobile. Being the column's first
   child gives both for free, because the rail is ordered above the column
   below 1100. It stops at the rail on desktop: the rail's own top is level
   with the column's, so the rail sits 4px higher than the reading content,
   exactly as the boards show.

   The track colour is the same hairline grey as the column's own borders, so
   the strip reads as continuous edge to edge even though the fill starts
   inside them. */
.vgd-progress {
  flex-shrink: 0;
  height: 4px;
  background: var(--vgd-border);
}
.vgd-progress-fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--vgd-surface-invert);
  /* Rides the step move rather than the panel's 600ms. The bar used to jump a
     sixth at a gate boundary, six times, and could take its time about it. It
     now moves an eighteenth per question, and the width is set at the swap, so
     matching the incoming half of the step lands the bar and the question it
     counts at the same moment. */
  transition: width var(--vgd-step-in) var(--vgd-ease-slide);
}

/* ── Question screen ───────────────────────────────────────────────────
   Header row, question, five option cards. Continue lives in the shared
   action row and is full width on every one of the 18, at every breakpoint,
   which is the point of there being no inline Back (blueprint 4.2). */
.vgd-q-head {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
/* Fixed 24px row at both breakpoints, so the 20px mobile eyebrow and the 24px
   desktop one both sit centred in the same box. */
.vgd-q-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--vgd-sp-200);
  height: 24px;
}
.vgd-q-where {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.vgd-q-sep {
  display: flex;
  flex-shrink: 0;
  color: var(--vgd-text-2);
}
.vgd-q-sep svg { width: 16px; height: 16px; display: block; }
/* H3, and unlike the display type it carries no negative tracking. */
.vgd-q {
  font-weight: 500;
  font-size: var(--vgd-h3);
  line-height: var(--vgd-lh-h3);
}

.vgd-opts {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
/* A real radio, with everything visible drawn on .vgd-opt-face, a SIBLING of
   the input rather than an ancestor of it. That shape is the point: it lets
   the selected state be pure CSS off :checked, so choosing never costs a
   re-render and there is no JS-held class that can drift out of step with what
   the radio group actually has selected.

   The label is only a positioning context and contributes no box of its own.
   The input is stretched across it, invisible but real, which gives the native
   arrow-key group and the single tab stop for free, makes the whole card the
   hit area, and puts the focus ring exactly round the card. */
.vgd-opt {
  position: relative;
  display: block;
  cursor: pointer;
}
.vgd-opt-input {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  border-radius: var(--vgd-radius-card);
  cursor: pointer;
}
.vgd-opt-input:focus-visible {
  outline: 2px solid var(--vgd-border-ink);
  outline-offset: 2px;
}

/* The hairline is an inset shadow, not a border, for the same reason as every
   other one in this file: Figma draws its stroke on the boundary where it
   takes no height, so a real border would make each card 70 tall against the
   board's 68 and push the stack down by 2px a row. */
.vgd-opt-face {
  display: flex;
  align-items: center;
  gap: var(--vgd-sp-200);
  padding: var(--vgd-sp-150) var(--vgd-sp-250);
  background: var(--vgd-surface-2);
  border-radius: var(--vgd-radius-card);
  box-shadow: inset 0 0 0 1px var(--vgd-border-2);
  transition: background 160ms ease, box-shadow 160ms ease;
}
/* Hover, per Figma's Concern Block State=Hover: the card's hairline goes to
   #8d8d8d and the radio's goes all the way to ink, while both fills stay put.
   The radio darkening is the tell that the whole card is the target, not just
   the label. Ordered before the selected rules on purpose: card hover and card
   selected have equal specificity, so source order is what keeps a selected
   card ink-bordered when the pointer is over it. */
.vgd-opt:hover .vgd-opt-face { box-shadow: inset 0 0 0 1px var(--vgd-border-hover); }
.vgd-opt:hover .vgd-opt-mark { box-shadow: inset 0 0 0 1px var(--vgd-border-ink); }
/* Selected: ink hairline over the tertiary surface. No new colour, and the box
   does not change size, so nothing below it moves when the choice changes. */
.vgd-opt-input:checked ~ .vgd-opt-face {
  background: var(--vgd-surface-3);
  box-shadow: inset 0 0 0 1px var(--vgd-border-ink);
}
.vgd-opt-mark {
  position: relative;
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--vgd-surface);
  box-shadow: inset 0 0 0 1px var(--vgd-border-2);
  transition: box-shadow 160ms ease;
}
/* 24px circle, 2px ring, 16px dot: that leaves a 2px gap, which is the tight
   ring the boards show rather than a conventional airy radio. */
.vgd-opt-mark::after {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--vgd-surface-invert);
  transform: scale(0);
  transition: transform 160ms var(--vgd-ease-slide);
}
.vgd-opt-input:checked ~ .vgd-opt-face .vgd-opt-mark {
  box-shadow: inset 0 0 0 2px var(--vgd-border-ink);
}
.vgd-opt-input:checked ~ .vgd-opt-face .vgd-opt-mark::after { transform: scale(1); }

.vgd-opt-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.vgd-opt-label {
  font-weight: 500;
  font-size: var(--vgd-sm);
  line-height: 24px;
}
.vgd-opt-desc {
  font-weight: 400;
  font-size: var(--vgd-caption);
  line-height: 20px;
  color: var(--vgd-text-2);
}

/* ── Gate completion screen ────────────────────────────────────────────
   A chapter turn, not a status report: the completion fact is stated once, in
   the tick-and-eyebrow row, and everything else is the gate's name and its
   why-line (blueprint 4.3). Both boards drop the progress strip here and leave
   the rail to carry position, so the screen router simply passes progress:null
   and the collapsed chain keeps its own bottom hairline.

   The block is centred in the screen with auto block margins rather than
   justify-content, and that is deliberate: an auto margin resolves to zero
   once the free space goes negative, so a block taller than the screen scrolls
   from its top edge instead of having it clipped away. */
.vgd-gate {
  margin-block: auto;
  display: flex;
  flex-direction: column;
  gap: var(--vgd-gate-gap);
}
/* The tick tile is the same 32px marker the rail uses for a passed gate, which
   is the point: the mark that has just appeared beside Intent in the chain is
   the mark standing next to the headline. */
.vgd-gate-mark {
  display: flex;
  align-items: center;
  gap: var(--vgd-sp-150);
}
.vgd-gate-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  background: var(--vgd-surface-invert);
  border-radius: var(--vgd-radius-card);
  color: var(--vgd-text-invert);
}
.vgd-gate-tile svg { width: 16px; height: 16px; display: block; }
.vgd-gate-why {
  font-weight: 500;
  font-size: var(--vgd-h2);
  line-height: var(--vgd-lh-h2);
  color: var(--vgd-text-2);
}

/* ── Context step ──────────────────────────────────────────────────────
   Eyebrow, H3 and a secondary subline the question screens do not carry, then
   three groups: an eyebrow label over a row of selectable pills. The head and
   the groups are direct children of the scroller, so they share the 24px gap
   and the step move like every other screen (blueprint 4.4).

   Desktop lays each group's pills in a single equal-width row, however many it
   holds. Below 1100 they fall to a two-column grid, and a group with an odd
   number of options lets its last pill span the full width, which is how the
   Later (five) and Hybrid (three) boards read; the four ARR bands stay a clean
   2×2. */
.vgd-ctx-head {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
/* body/sm-body, fixed 16/24 at both boards rather than on the type ramp. */
.vgd-ctx-sub {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-2);
}
.vgd-ctx-group {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
.vgd-pills {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;   /* 8 on both boards, not on the mobile→desktop ramp */
}
.vgd-pills > .vgd-pill:last-child:nth-child(odd) { grid-column: 1 / -1; }

@media (min-width: 1100px) {
  .vgd-pills {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
  }
  .vgd-pills > .vgd-pill:last-child:nth-child(odd) { grid-column: auto; }
}

/* Same radio-as-sibling shape as .vgd-opt: everything visible is drawn on the
   face, so the selected state is pure CSS off :checked and choosing never costs
   a re-render. The input is stretched invisibly over the whole pill, which
   gives the native arrow-key group, the single tab stop and the hit area. */
.vgd-pill {
  position: relative;
  display: block;
  cursor: pointer;
}
.vgd-pill-input {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  border-radius: var(--vgd-radius-card);
  cursor: pointer;
}
.vgd-pill-input:focus-visible {
  outline: 2px solid var(--vgd-border-ink);
  outline-offset: 2px;
}
/* Hairline is an inset shadow, not a border, for the same reason as every other
   one here: a real border would add a pixel the board does not have. */
.vgd-pill-face {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  height: var(--vgd-pill-h);
  padding: 0 var(--vgd-sp-200);
  background: var(--vgd-surface-2);
  border-radius: var(--vgd-radius-card);
  box-shadow: inset 0 0 0 1px var(--vgd-border);
  font-weight: 500;
  font-size: 16px;   /* body/sm-body - bold */
  line-height: 24px;
  color: var(--vgd-text);
  /* Every label is one line on both boards and each is narrower than its pill,
     so the padding is a minimum the centred text borrows from rather than a
     hard box: "Pre-seed / Seed" would otherwise wrap in the 136px Stage pill. */
  white-space: nowrap;
  transition: background 160ms ease, box-shadow 160ms ease, color 160ms ease;
}
.vgd-pill:hover .vgd-pill-face { box-shadow: inset 0 0 0 1px var(--vgd-border-hover); }
/* Selected: solid ink, inverted text. Ordered after hover so a selected pill
   stays inked under the pointer, same source-order rule as the options. */
.vgd-pill-input:checked ~ .vgd-pill-face {
  background: var(--vgd-surface-invert);
  box-shadow: inset 0 0 0 1px var(--vgd-border-ink);
  color: var(--vgd-text-invert);
}

/* ── Reveal ────────────────────────────────────────────────────────────
   The terminal screen (blueprint 4.5). It does not use the shared stepping
   chain, but it DOES use the shell's own two columns: the result content is the
   ordinary scroller and the scored chain is rendered into the rail slot (see
   render's railBody), so the frame's vertical hairlines run full height between
   them and the action row is the ordinary footer, exactly as the rest of the
   flow. On mobile the rail is hidden and a copy of the chain rides the scroll. */
.vgd-sheet[data-screen="reveal"] .vgd-rail,
.vgd-sheet[data-screen="booking"] .vgd-rail,
.vgd-sheet[data-screen="confirmation"] .vgd-rail {
  background: transparent;   /* the reveal/booking/confirmation rail sits on the body surface, not white */
}
/* The reveal, booking and confirmation inset evenly (16 mobile → 64 desktop, top
   included) and their blocks sit on the 16 → 24 gap, where the run's screens hold
   a flat 24. */
.vgd-sheet[data-screen="reveal"] .vgd-screen,
.vgd-sheet[data-screen="booking"] .vgd-screen,
.vgd-sheet[data-screen="confirmation"] .vgd-screen {
  padding: var(--vgd-pad-x);
  gap: var(--vgd-sp-300);
}
/* The cards clip to their rounded corners with overflow:hidden, which also
   makes them shrinkable flex items (auto min-height does not hold once overflow
   is not visible). As direct children of the scroller they must keep their
   content height and let the screen scroll, so pin the shrink. */
.vgd-sheet[data-screen="reveal"] .vgd-screen > *,
.vgd-sheet[data-screen="booking"] .vgd-screen > *,
.vgd-sheet[data-screen="confirmation"] .vgd-screen > * { flex-shrink: 0; }
/* The booking and confirmation boards draw no chain on mobile, as the reveal does not. */
@media (max-width: 1099px) {
  .vgd-sheet[data-screen="reveal"] .vgd-rail,
  .vgd-sheet[data-screen="booking"] .vgd-rail,
  .vgd-sheet[data-screen="confirmation"] .vgd-rail { display: none; }
}

/* Shared card: white surface, hairline drawn as an inset shadow (a border would
   add a pixel the board does not have), rounded, clipped so the inner section
   dividers meet the corner cleanly. */
.vgd-rev-card {
  background: var(--vgd-surface-2);
  border-radius: var(--vgd-radius-card);
  box-shadow: inset 0 0 0 1px var(--vgd-border);
  overflow: hidden;
}

/* ── Result card ─────────────────────────────────────────────────────── */
.vgd-rev-result-top {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);   /* eyebrow → content, 8 → 12 (board's 36−24) */
  padding: var(--vgd-sp-300);
}
.vgd-rev-verdict-row {
  display: flex;
  align-items: center;
  gap: var(--vgd-sp-500);
}
.vgd-rev-verdict {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
.vgd-rev-word {
  font-weight: 500;
  font-size: var(--vgd-rev-verdict);
  line-height: 1;
  letter-spacing: -0.01em;
}
.vgd-rev-sub {
  font-weight: 400;
  font-size: var(--vgd-lg-body);
  line-height: var(--vgd-lh-lg);
  color: var(--vgd-text-2);
}

/* The score stamp: three equal rounded squares rotated 0/30/60 make the
   rosette, exactly as Figma builds it. Fixed 232 on both boards. */
.vgd-rev-stamp {
  position: relative;
  width: var(--vgd-stamp);
  height: var(--vgd-stamp);
  flex-shrink: 0;
}
.vgd-rev-stamp-ring {
  position: absolute;
  inset: 13.4%;
  border: 1px solid var(--vgd-error);
  border-radius: 40px;
}
.vgd-rev-stamp-ring:nth-child(2) { transform: rotate(30deg); }
.vgd-rev-stamp-ring:nth-child(3) { transform: rotate(60deg); }
.vgd-rev-stamp-face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.vgd-rev-score {
  font-weight: 500;
  font-size: var(--vgd-rev-score);
  line-height: 1;
  color: var(--vgd-error);
  /* Tabular figures so the count-up holds its width as the digits change,
     the one counter the sheet gives tnum, per the note in build-status. */
  font-feature-settings: 'tnum';
}
.vgd-rev-stamp-cap { color: var(--vgd-text-2); }
/* Healthy chain: the whole rosette and its figure carry the value green, the
   green counterpart to the rust the leaking stamp wears. */
.vgd-rev-stamp.is-healthy .vgd-rev-stamp-ring { border-color: var(--vgd-edge); }
.vgd-rev-stamp.is-healthy .vgd-rev-score { color: var(--vgd-edge); }

/* Stat grid: one row on desktop, 2 + 1 on mobile. Cells are transparent so the
   card's own rounded ring is the outer border (drawing it a second time as a
   square frame left the corners ragged); the dividers between cells are inset
   shadows, the same way the launch spec grid does it, so nothing covers the
   ring and the corners stay clean. */
.vgd-rev-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  box-shadow: inset 0 1px 0 var(--vgd-border);   /* divider from the content above */
}
.vgd-rev-stat {
  padding: var(--vgd-sp-300);
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
.vgd-rev-stat + .vgd-rev-stat { box-shadow: inset 1px 0 0 var(--vgd-border); }
.vgd-rev-stat-v {
  font-weight: 500;
  font-size: var(--vgd-h4);
  line-height: var(--vgd-lh-h4);
}
.vgd-rev-stat.is-leak .vgd-rev-stat-v { color: var(--vgd-error); }
.vgd-rev-stat.is-edge .vgd-rev-stat-v { color: var(--vgd-edge); }

@media (max-width: 1099px) {
  .vgd-rev-stats { grid-template-columns: 1fr 1fr; }
  /* On its own second row the wide cell takes a top divider, not the left one
     the source-order rule would give it. */
  .vgd-rev-stat.is-wide { grid-column: 1 / -1; box-shadow: inset 0 1px 0 var(--vgd-border); }
}

/* Below the tablet width the stamp no longer sits beside the verdict: it
   centres on its own line and the word and subline stack under it, as the
   mobile board draws it. */
@media (max-width: 767px) {
  .vgd-rev-verdict-row { flex-direction: column; align-items: stretch; gap: var(--vgd-sp-300); }
  .vgd-rev-stamp { align-self: center; }
}

/* ── Understanding card ──────────────────────────────────────────────── */
.vgd-rev-understand {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);   /* eyebrow → heading, 12px per Figma */
  padding: var(--vgd-sp-300);
}
.vgd-rev-understand-body {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
.vgd-rev-h2 {
  font-weight: 500;
  font-size: var(--vgd-h2);
  line-height: var(--vgd-lh-h2);
}
.vgd-rev-lead {
  font-weight: 500;
  font-size: var(--vgd-lg-body);
  line-height: var(--vgd-lh-lg);
}
.vgd-rev-body {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-2);
}

/* ── Chain (reveal variant) ──────────────────────────────────────────────
   Head (eyebrow + title) and list. In the rail slot they are the rail's own
   direct children, so the rail's head→list gap (sp-500) and its head's
   eyebrow→title gap size them exactly as the shared stepping chain, and the two
   pixel-match when you toggle between context and reveal. */
.vgd-rev-chain-head {
  display: flex;
  flex-direction: column;
  gap: 12px;   /* eyebrow → title, matching .vgd-rail-head */
}
.vgd-rev-chain-title {
  font-weight: 500;
  font-size: var(--vgd-h4);
  line-height: var(--vgd-lh-h4);
}
.vgd-rev-chain-list { display: flex; flex-direction: column; }

/* The mobile copy is a bordered card in the scroll, carrying its own head→list
   gap since it is not in the rail. Desktop hides it; the rail slot shows the
   chain there instead. */
.vgd-rev-chain--inline {
  display: none;
  flex-direction: column;
  gap: var(--vgd-sp-500);
  background: var(--vgd-surface-2);
  border-radius: var(--vgd-radius-card);
  box-shadow: inset 0 0 0 1px var(--vgd-border);
  padding: var(--vgd-sp-300);
}
@media (max-width: 1099px) {
  .vgd-rev-chain--inline { display: flex; }
}

/* Rail crossfade layers (see crossfadeRail). The ghost mirrors the rail's own
   padding and column so the outgoing chain sits exactly where it did; the
   holder is in flow, inside that padding, and only needs the column. */
.vgd-rail-fade {
  position: absolute;
  inset: 0;
  padding: var(--vgd-pad-y) var(--vgd-pad-x);
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-500);
  pointer-events: none;
}
.vgd-rail-flow {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-500);
}

/* The eyebrow's clipped slide, the CTA label's own mechanism and timing: two
   words stacked in a one-line clip, the outgoing rising away as the incoming
   rises in. No fade. */
.vgd-rail-eyeswap { position: absolute; z-index: 2; }
.vgd-rail-eyeswap-clip {
  display: block;
  position: relative;
  height: var(--vgd-lh-eyebrow);
  overflow: hidden;
}
.vgd-rail-eyeswap-out,
.vgd-rail-eyeswap-in {
  display: block;
  white-space: nowrap;
  transition: transform 0.4s cubic-bezier(0.76, 0, 0.24, 1);
}
.vgd-rail-eyeswap-in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(110%);
}
.vgd-rail-eyeswap.is-swapping .vgd-rail-eyeswap-out { transform: translateY(-110%); }
.vgd-rail-eyeswap.is-swapping .vgd-rail-eyeswap-in  { transform: translateY(0); }

.vgd-rev-gate { display: flex; gap: 14px; align-items: flex-start; }
.vgd-rev-gate-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex-shrink: 0;
  width: 32px;
}
.vgd-rev-gate-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: var(--vgd-radius-card);
  background: var(--vgd-surface-invert);
  color: var(--vgd-text-invert);
}
.vgd-rev-gate-tile svg { width: 16px; height: 16px; display: block; }
.vgd-rev-gate.is-leak .vgd-rev-gate-tile { background: var(--vgd-error); }
.vgd-rev-gate.is-edge .vgd-rev-gate-tile { background: var(--vgd-edge); }
/* The connector between one gate and the next; the last gate has none. */
.vgd-rev-gate-spine {
  width: 1px;
  height: 32px;
  background: var(--vgd-surface-invert);
}
.vgd-rev-gate.is-last .vgd-rev-gate-spine { display: none; }

.vgd-rev-gate-main {
  flex: 1 1 auto;
  min-width: 0;
  height: 48px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding-top: 4px;
}
.vgd-rev-gate-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--vgd-sp-200);
  height: 24px;
}
.vgd-rev-gate-name {
  font-weight: 500;
  font-size: 16px;
  line-height: 24px;
}
.vgd-rev-gate.is-leak .vgd-rev-gate-name { color: var(--vgd-error); }
.vgd-rev-gate.is-edge .vgd-rev-gate-name { color: var(--vgd-edge); }
.vgd-rev-gate-score {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-2);
  font-feature-settings: 'tnum';
}

/* 4px track with hairline end-caps and a fill drawn to the gate's score. The
   fill rests at its final width (--w); the draw arms it to zero and releases,
   which is why width, not transform, carries the reveal motion. */
.vgd-rev-gate-bar {
  position: relative;
  height: 4px;
  width: 100%;
  background: var(--vgd-border);
  border-left: 1px solid var(--vgd-border);
  border-right: 1px solid var(--vgd-border);
}
.vgd-rev-gate.is-leak .vgd-rev-gate-bar { background: var(--vgd-error-tint); }
.vgd-rev-gate.is-edge .vgd-rev-gate-bar { background: var(--vgd-edge-tint); }
.vgd-rev-gate-fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 4px;
  width: var(--w);
  background: var(--vgd-surface-invert);
  transition: width var(--vgd-rev-draw) var(--vgd-ease-slide);
}
.vgd-rev-gate.is-leak .vgd-rev-gate-fill { background: var(--vgd-error); }
.vgd-rev-gate.is-edge .vgd-rev-gate-fill { background: var(--vgd-edge); }

/* ── Comparison card ──────────────────────────────────────────────────────
   Same treatment as the stat grid: transparent cells over the card's rounded
   ring, dividers drawn as inset shadows, so the corners stay clean. */
.vgd-rev-cmp-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.vgd-rev-cmp-cell {
  padding: var(--vgd-sp-300);
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
.vgd-rev-cmp-cell + .vgd-rev-cmp-cell { box-shadow: inset 1px 0 0 var(--vgd-border); }
.vgd-rev-cmp-n {
  font-weight: 500;
  font-size: var(--vgd-h3);
  line-height: var(--vgd-lh-h3);
  font-feature-settings: 'tnum';
}
.vgd-rev-cmp-cell.is-leak .vgd-rev-cmp-n { color: var(--vgd-error); }
.vgd-rev-cmp-g {
  font-weight: 500;
  font-size: var(--vgd-body);
  line-height: var(--vgd-lh-body);
}
.vgd-rev-cmp-cell .vgd-rev-gate-bar { margin-top: var(--vgd-sp-100); }
.vgd-rev-cmp-cell.is-leak .vgd-rev-gate-bar { background: var(--vgd-error-tint); }
.vgd-rev-cmp-cell.is-leak .vgd-rev-gate-fill { background: var(--vgd-error); }
.vgd-rev-cmp-note {
  padding: var(--vgd-sp-300);
  box-shadow: inset 0 1px 0 var(--vgd-border);   /* divider from the row above */
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-2);
}

/* ── Next-steps block ────────────────────────────────────────────────── */
.vgd-rev-next {
  background: var(--vgd-surface-invert);
  border-radius: var(--vgd-radius-card);
  overflow: hidden;
  color: var(--vgd-text-invert);
}
.vgd-rev-next-head {
  display: flex;
  align-items: center;
  gap: var(--vgd-sp-150);
  padding: var(--vgd-sp-250) var(--vgd-sp-300);
  border-bottom: 1px solid var(--vgd-divider-invert);
}
.vgd-rev-next-head .vgd-eyebrow { color: var(--vgd-neutral-500); }
.vgd-rev-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--vgd-success);
  flex-shrink: 0;
}
.vgd-rev-next-body {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-250);
  padding: var(--vgd-sp-300);
  border-bottom: 1px solid var(--vgd-divider-invert);
}
.vgd-rev-next-h {
  font-weight: 500;
  font-size: var(--vgd-rev-offer);
  line-height: 1;
}
.vgd-rev-next-sub {
  font-weight: 400;
  font-size: var(--vgd-lg-body);
  line-height: var(--vgd-lh-lg);
  color: var(--vgd-neutral-500);
}
.vgd-rev-next-foot {
  display: flex;
  align-items: center;
  gap: var(--vgd-sp-200);
  padding: var(--vgd-sp-250) var(--vgd-sp-300);
}
.vgd-rev-next-reassure {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-neutral-500);
}
.vgd-rev-next-foot .vgd-avatar {
  width: 32px;
  height: 32px;
  border-radius: var(--vgd-radius-card);
}

/* ── Sticky bar (in the action row) ──────────────────────────────────────
   The action row is the ordinary footer, already the content column's width
   with the frame's hairlines running through it, so the bar just puts identity
   and button at its two ends. */
.vgd-sheet[data-screen="reveal"] .vgd-actions {
  transition: transform var(--vgd-slide-panel) var(--vgd-ease-slide);
}
/* Tucks down out of the way once the real Next-steps CTA is on screen. The
   sheet clips it as it slides below the fold. */
.vgd-sheet[data-screen="reveal"] .vgd-actions.is-tucked {
  transform: translateY(120%);
}
/* Below 1100 the rail is hidden, so no frame hairline runs through the action
   row and it can leave the flow when tucked. Taking it out of flow lets the
   content column reclaim its (tall, ~124px) height instead of leaving an empty
   gap under the last block once the bar has slid away. */
@media (max-width: 1099px) {
  .vgd-sheet[data-screen="reveal"] .vgd-col { position: relative; }
  .vgd-sheet[data-screen="reveal"] .vgd-actions.is-tucked {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
  }
}
.vgd-rev-sticky {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--vgd-sp-300);
}
.vgd-rev-sticky-id {
  display: flex;
  align-items: center;
  gap: var(--vgd-sp-200);
  min-width: 0;
}
.vgd-rev-sticky-id .vgd-avatar {
  width: 32px;
  height: 32px;
  border-radius: var(--vgd-radius-card);
  flex-shrink: 0;
}
.vgd-rev-sticky-text {
  font-weight: 400;
  font-size: var(--vgd-sm);
  line-height: 24px;
  color: var(--vgd-text-2);
  min-width: 0;
}
.vgd-rev-sticky .vgd-cta { flex-shrink: 0; width: auto; }

@media (max-width: 767px) {
  .vgd-rev-sticky { flex-direction: column; align-items: stretch; gap: var(--vgd-sp-200); }
  .vgd-rev-sticky .vgd-cta { width: 100%; }
}

/* ── Reveal buttons ──────────────────────────────────────────────────
   The reveal's buttons are the primary button as-is (arrow, hover reveal and
   all); --invert is the white-on-black Next-steps block variant. */
.vgd-cta--invert {
  background: var(--vgd-surface-2);
  color: var(--vgd-text);
}
.vgd-cta--invert:hover:not(:disabled) { background: #ebebeb; }

/* Ghost CTA: the confirmation's "Add to calendar", a white button with an ink
   hairline. No arrow — it downloads rather than advancing (per the board). */
.vgd-cta--ghost {
  background: var(--vgd-surface-2);
  color: var(--vgd-text);
  box-shadow: inset 0 0 0 1px var(--vgd-border);
}
.vgd-cta--ghost:hover:not(:disabled) { background: var(--vgd-surface-3); }

/* ── The chain ─────────────────────────────────────────────────────────
   Desktop: a fixed right rail. Mobile: the same markup collapsed into an
   accordion pinned under the title bar, in the same place on every screen. */
.vgd-rail {
  flex-shrink: 0;
  width: var(--vgd-rail-w);
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-500);
  padding: var(--vgd-pad-y) var(--vgd-pad-x);
  background: var(--vgd-surface-2);
  border-right: 1px solid var(--vgd-border);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.vgd-rail-head {
  display: flex;
  flex-direction: column;
  gap: 12px;   /* the board sets the chain title 12 below its eyebrow */
}
.vgd-rail-title {
  font-weight: 500;
  font-size: var(--vgd-h4);
  line-height: var(--vgd-lh-h4);
}
.vgd-rail-toggle { display: none; }

.vgd-chain { display: flex; flex-direction: column; }
.vgd-link {
  display: flex;
  gap: 14px;
  align-items: flex-start;
}
/* The spine is the marker column: a numbered tile, then a 1px run down to the
   next tile. The last link has no run, so the spine terminates at Expansion. */
.vgd-link-spine {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex-shrink: 0;
  width: 32px;
}
.vgd-link-n {
  position: relative;   /* anchors the passed gate's visually-hidden "Done" */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  background: var(--vgd-surface);
  border: 1px solid var(--vgd-border-2);
  border-radius: var(--vgd-radius-card);
  font-weight: 500;
  font-size: var(--vgd-eyebrow);
  line-height: var(--vgd-lh-eyebrow);
  /* No tracking, though this is otherwise the eyebrow style. The label is a
     single digit, so tracking adds nothing between letters and only a trailing
     space, and CSS counts that space when it centres the inline box: it pushed
     the digit a pixel left of where the board draws it. Figma trims the same
     trailing space. Without it the ink lands within 0.02px of the board. */
}
/* The line below a tile is a track with the ink drawn over it, not a colour
   change: a colour cannot arrive from one end. The track keeps the resting
   hairline, the ::after is the ink, and it grows from the tile downwards. */
.vgd-link-run {
  position: relative;
  width: 1px;
  height: 32px;
  background: var(--vgd-border);
}
.vgd-link-run::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--vgd-surface-invert);
  transform: scaleY(0);
  transform-origin: top;
}
.vgd-link:last-child .vgd-link-run { display: none; }
.vgd-link-body {
  flex: 1 1 auto;
  min-width: 0;
  padding-top: 4px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.vgd-link-name {
  font-weight: 500;
  font-size: var(--vgd-sm);
  line-height: 24px;
}
.vgd-link-cap {
  font-weight: 400;
  font-size: var(--vgd-caption);
  line-height: 20px;
  color: var(--vgd-text-2);
}
/* Three states, all of them Figma's (8219:7194 shows all three in one rail).
   Upcoming is the resting tile above. Active and passed are both filled ink,
   and the only thing that separates them is what the tile holds: the gate's
   number while you are in it, a tick once it is behind you. So the fill is one
   rule for both, and only the glyph is swapped, in markup.

   The spine is dark from the top down through the *current* link's own run,
   which is the boards' reading rather than an assumption: the question boards
   show the active gate with the run below it filled, so the spine tracks how
   far down the chain you have got rather than stopping short at the tile.

   State is never carried by colour alone: the active link is marked
   aria-current, and a passed one says so in text inside the tile. */
.vgd-link.is-current .vgd-link-n,
.vgd-link.is-done .vgd-link-n {
  background: var(--vgd-surface-invert);
  border-color: var(--vgd-border-ink);
  color: var(--vgd-text-invert);
}
.vgd-link.is-done .vgd-link-run::after,
.vgd-link.is-current .vgd-link-run::after { transform: scaleY(1); }
.vgd-link-n svg { width: 16px; height: 16px; display: block; }

/* ── The chain's two faces ───────────────────────────────────────────────
   The digit and the tick occupy the same 32px tile, stacked, so one can give
   way to the other. The digit holds the layout because it is the one that is
   there first; the tick is taken out of flow on top of it. */
.vgd-link-digit { display: block; }
.vgd-link-tick {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.vgd-link-digit { opacity: 1; transform: none; }
.vgd-link-tick  { opacity: 0; transform: scale(0.6); }
.vgd-link.is-done .vgd-link-digit { opacity: 0; transform: scale(0.6); }
.vgd-link.is-done .vgd-link-tick  { opacity: 1; transform: none; }

/* ── The chain in motion ─────────────────────────────────────────────────
   Only ever with the document in front of someone. `applyRail` sets data-motion
   off when it is not, and off means every state lands outright: a transition
   started while nothing paints holds its first frame, which here would be a
   line stuck at nothing and a tile that never takes the ink.

   The tile fills first and the line follows it down, which is the whole point
   of the lag. Both are transitions rather than animations, so a state reached
   by stepping back unwinds along the same path it arrived by. */
.vgd-rail[data-motion="on"] .vgd-link-n {
  transition: background var(--vgd-rail-fill) var(--vgd-ease-slide),
              border-color var(--vgd-rail-fill) var(--vgd-ease-slide),
              color var(--vgd-rail-fill) var(--vgd-ease-slide);
}
.vgd-rail[data-motion="on"] .vgd-link-run::after {
  transition: transform var(--vgd-rail-run) var(--vgd-ease-slide) var(--vgd-rail-lag);
}
.vgd-rail[data-motion="on"] .vgd-link-digit,
.vgd-rail[data-motion="on"] .vgd-link-tick {
  transition: opacity var(--vgd-rail-fill) var(--vgd-ease-slide),
              transform var(--vgd-rail-fill) var(--vgd-ease-slide);
}

/* The one-shot when a gate is passed. A ring leaves the tile once and is gone;
   it is drawn on a pseudo-element so it can outgrow the tile without the tile
   having to give it room, and it never repeats because `applyRail` takes the
   class away again. Scale and opacity only, so it is composited and costs the
   step move nothing. */
.vgd-link-n::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid var(--vgd-border-ink);
  opacity: 0;
  pointer-events: none;
}
.vgd-link.is-ticking .vgd-link-n::after {
  animation: vgd-tick-ring var(--vgd-rail-tick) var(--vgd-ease-slide) forwards;
}
.vgd-link.is-ticking .vgd-link-tick {
  animation: vgd-tick-settle var(--vgd-rail-tick) var(--vgd-ease-slide);
}
@keyframes vgd-tick-ring {
  from { opacity: 0.4; transform: scale(1); }
  to   { opacity: 0;   transform: scale(1.7); }
}
/* Overshoots a little on the way in and settles back, which is what reads as
   the tick landing rather than merely appearing. */
@keyframes vgd-tick-settle {
  0%   { transform: scale(0.6); }
  55%  { transform: scale(1.12); }
  100% { transform: scale(1); }
}

/* Below 1100 there is no room for a 394px rail beside the reading column, so
   the chain collapses to the accordion pinned under the title bar. The 900
   board shows it collapsed, the 1100 board shows the rail. */
@media (max-width: 1099px) {
  .vgd-body  { padding: 0 var(--vgd-sp-300); }
  .vgd-frame { flex-direction: column; }
  .vgd-col   { border-left: 0; border-right: 0; }
  .vgd-screen {
    border-left: 1px solid var(--vgd-border);
    border-right: 1px solid var(--vgd-border);
  }
  .vgd-actions {
    border-left: 1px solid var(--vgd-border);
    border-right: 1px solid var(--vgd-border);
  }
  /* Below 1100 the column carries no borders of its own, so the strip draws
     its own. They are the track colour, so the join is invisible. */
  .vgd-progress {
    border-left: 1px solid var(--vgd-border);
    border-right: 1px solid var(--vgd-border);
  }

  .vgd-rail {
    order: -1;              /* sits directly under the title bar */
    width: auto;
    flex-shrink: 0;
    gap: 0;
    padding: 0;
    overflow: visible;
    border-left: 1px solid var(--vgd-border);
    border-right: 1px solid var(--vgd-border);
    /* Boundary hairline, inside, so the collapsed accordion is 56 not 57. */
    box-shadow: inset 0 -1px 0 var(--vgd-border);
  }
  /* Except where the progress strip is already the boundary. The mobile
     question boards show the collapsed chain running clean into the 4px
     track; keeping the hairline as well would stack them into a 5px band. */
  .vgd-sheet[data-progress="true"] .vgd-rail { box-shadow: none; }
  .vgd-rail-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vgd-sp-500);
    width: 100%;
    padding: var(--vgd-sp-250) var(--vgd-sp-300);
    background: none;
    border: 0;
    font-family: inherit;
    font-weight: 500;
    font-size: var(--vgd-body);
    line-height: var(--vgd-lh-body);
    color: var(--vgd-text);
    text-align: left;
    cursor: pointer;
  }
  .vgd-rail-toggle svg {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    transition: transform var(--vgd-slide-panel) var(--vgd-ease-slide);
  }
  .vgd-rail[data-open="true"] .vgd-rail-toggle svg { transform: rotate(180deg); }

  .vgd-rail-head    { display: contents; }
  .vgd-rail-eyebrow { display: none; }
  .vgd-rail-title   { display: none; }

  /* Grid-rows 0fr → 1fr animates the reveal without measuring height. The
     sheet is height:auto here and anchored to the bottom, so it grows and
     shrinks with this row: animating the row is what animates the sheet, and
     both land on the same curve as the sheet's own entrance. */
  .vgd-rail-panel {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--vgd-slide-panel) var(--vgd-ease-slide);
  }
  .vgd-rail[data-open="true"] .vgd-rail-panel { grid-template-rows: 1fr; }
  .vgd-rail-panel > .vgd-chain { overflow: hidden; min-height: 0; }
  /* Bottom spacing hangs off the last link rather than the list, and is never
     toggled. Toggling it with [data-open] changed the row's target height the
     instant the tap landed, so a collapse dropped 16px before it started
     travelling. Putting it on the list instead does not work either: overflow
     clips content, not an element's own padding, so a collapsed list still
     stood 16px tall. On the last link it is content, so the row clips it to
     nothing when closed and grows through it when open. */
  .vgd-chain { padding: 0 var(--vgd-sp-300); }
  .vgd-chain > .vgd-link:last-child { padding-bottom: var(--vgd-sp-300); }
}

@media (prefers-reduced-motion: reduce) {
  .vgd-root::before,
  .vgd-sheet,
  .vgd-rail-panel,
  .vgd-rail-toggle svg,
  .vgd-progress-fill,
  .vgd-opt-face,
  .vgd-opt-mark,
  .vgd-opt-mark::after,
  .vgd-pill-face,
  .vgd-rev-gate-fill,
  .vgd-sheet[data-screen="reveal"] .vgd-actions,
  .vgd-bar-id,
  .vgd-back,
  .vgd-brand { transition: none; }

  /* The button keeps its background change and drops the reveal. Both twins
     are pinned back to rest, at the same specificity as the hover rules they
     have to beat. */
  .vgd-btn-icon,
  .vgd-btn-label { transition: none; }
  .vgd-cta:hover:not(:disabled) .vgd-btn-icon--out  { transform: translateX(0); }
  .vgd-cta:hover:not(:disabled) .vgd-btn-icon--in   { transform: translateX(-120%); }
  .vgd-cta:hover:not(:disabled) .vgd-btn-label--out { transform: translateY(0); }
  .vgd-cta:hover:not(:disabled) .vgd-btn-label--in  { transform: translateY(110%); }

  /* `step()` already declines to animate under this query, so the screen never
     gets a data-step to begin with. These are the belt to that braces: a block
     pinned at rest cannot be left mid-move by a query that flips while a step
     is in flight. */
  .vgd-screen > * { transition: none; }
  .vgd-screen[data-step] > * { transform: none; opacity: 1; }

  /* Same arrangement for the chain: `applyRail` already declines to set
     data-motion under this query, so the transitions never attach and the tick
     class is never added. These pin the ends anyway, so nothing can be left
     part-way by a query that flips mid-move. */
  .vgd-rail .vgd-link-n,
  .vgd-rail .vgd-link-run::after,
  .vgd-rail .vgd-link-digit,
  .vgd-rail .vgd-link-tick { transition: none; animation: none; }
  .vgd-link.is-ticking .vgd-link-n::after { animation: none; opacity: 0; }
}

/* ════════════════════════════════════════════════════════════════════════
   Booking screen (blueprint 4.7)
   One long-scroll column: the sized-leak intro, the Cal.com month picker, the
   times a chosen date reveals, and the founder's details. The rail carries the
   reveal's scored chain (handled by the shared reveal/booking rules above).
   ════════════════════════════════════════════════════════════════════════ */

/* ── Intro ─────────────────────────────────────────────────────────────── */
.vgd-book-intro {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);   /* eyebrow → title → body */
}
.vgd-book-crumb {
  display: flex;
  align-items: center;
  gap: 6px;
}
.vgd-book-crumb-sep {
  display: inline-flex;
  color: var(--vgd-text-2);
}
.vgd-book-crumb-sep svg { width: 14px; height: 14px; display: block; }
.vgd-book-title {
  font-weight: 500;
  font-size: var(--vgd-h2);
  line-height: var(--vgd-lh-h2);
}
.vgd-book-body {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-2);
}

/* ── Cal.com month picker ──────────────────────────────────────────────── */
.vgd-book-cal {
  min-height: 355px;
  display: flex;
  flex-direction: column;
  gap: 24px;   /* below the head (month H4 + prev/next) → the grid */
  /* Matches the reveal cards on the previous step: 8px radius, sp-300 padding
     (16 mobile → 24 desktop). */
  padding: var(--vgd-sp-300);
  background: var(--vgd-surface-2);
  border: 1px solid var(--vgd-border);
  border-radius: var(--vgd-radius-card);
}
/* Loading / error / empty-day copy, centred while the card has no grid yet. */
.vgd-book-cal:not(:has(.vgd-cal-grid)) { align-items: center; justify-content: center; text-align: center; }
.vgd-cal-loading,
.vgd-cal-error,
.vgd-book-hint {
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
  color: var(--vgd-text-2);
}
.vgd-cal-error { color: var(--vgd-error); }
.vgd-cal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.vgd-cal-month {
  font-weight: 500;
  font-size: var(--vgd-h4);
  line-height: var(--vgd-lh-h4);
  color: var(--vgd-text);
}
.vgd-cal-nav { display: flex; gap: 8px; }
.vgd-cal-navbtn {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--vgd-surface-2);
  border: 1px solid var(--vgd-border);
  border-radius: 8px;
  color: var(--vgd-text);
  cursor: pointer;
  transition: background 0.15s ease, opacity 0.15s ease;
}
.vgd-cal-navbtn:hover:not(:disabled) { background: var(--vgd-surface-3); }
.vgd-cal-navbtn:disabled { opacity: 0.3; cursor: default; }
.vgd-cal-navbtn svg { width: 16px; height: 16px; display: block; }
.vgd-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
/* On desktop the day cells sit 12px apart, matching the times and field gaps. */
@media (min-width: 1100px) {
  .vgd-cal-grid { gap: 12px; }
}
.vgd-cal-wd {
  text-align: center;
  font-size: 12px;
  line-height: 20px;
  color: var(--vgd-text-2);
}
.vgd-cal-day {
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  color: var(--vgd-text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.vgd-cal-day.is-empty { visibility: hidden; cursor: default; }
.vgd-cal-day.is-disabled { color: #cdcdcd; cursor: default; }
.vgd-cal-day.is-available:hover { border-color: var(--vgd-border-hover); }
.vgd-cal-day.is-selected {
  background: var(--vgd-surface-invert);
  border-color: var(--vgd-surface-invert);
  color: var(--vgd-text-invert);
}

/* ── Available times ───────────────────────────────────────────────────── */
.vgd-book-times {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-200);
}
.vgd-book-pillgrid {
  display: grid;
  grid-template-columns: 1fr 1fr;   /* mobile: two per row */
  /* Matches the 16px between the form field boxes. The pills carry no focus-ring
     wrapper, so this is a flat 16px rather than the fields' 12px + 2px+2px. */
  gap: 16px;
}
@media (min-width: 768px) {
  .vgd-book-pillgrid { grid-template-columns: repeat(3, 1fr); }   /* desktop: three */
}
.vgd-book-pill {
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--vgd-surface-2);
  border: 1px solid var(--vgd-border);
  border-radius: 8px;
  font-family: inherit;
  font-size: 16px;
  color: var(--vgd-text);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.vgd-book-pill:hover { border-color: var(--vgd-border-hover); }
.vgd-book-pill.is-selected {
  background: var(--vgd-surface-invert);
  border-color: var(--vgd-surface-invert);
  color: var(--vgd-text-invert);
}
.vgd-book-tz {
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
  color: var(--vgd-text-2);
}

/* ── Details ───────────────────────────────────────────────────────────── */
.vgd-book-details {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-200);
}
.vgd-book-fields {
  display: flex;
  flex-direction: column;
  /* The live site's own field gap (flow.css `.diag-fields`, --sp-150 = 12px).
     Each field carries a 2px focus-ring wrapper, so 12px here reads as the
     Figma 16px between the visible field boxes. */
  gap: 12px;
}
/* The field is the live site's own input component (flow.css `.diag-field` /
   `.diag-input`): a 2px transparent wrapper that IS the focus/error ring. On
   focus the wrapper fills #d6d6d6 (a soft gutter) and the input border goes ink,
   so the focus reads as a ring rather than a hard black outline; on error the
   wrapper tints red. Kept identical to the live site so the two match. */
.vgd-field {
  padding: 2px;
  border-radius: 10px;
  background: transparent;
  transition: background 0.15s ease;
}
/* Email is wrapped with its validation message; the group is one field slot in
   the stack, with a tight gap to the message it reveals on a bad address. */
.vgd-field-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.vgd-field-error {
  display: none;
  padding: 0 12px;
  font-weight: 400;
  font-size: 14px;
  line-height: 16px;
  color: var(--vgd-error);
}
.vgd-field.is-error + .vgd-field-error { display: block; }
.vgd-input {
  width: 100%;
  height: 48px;
  padding: 12px;
  background: var(--vgd-surface-2);
  border: 1px solid var(--vgd-border-2);   /* #ebebeb */
  border-radius: 8px;
  font-family: inherit;
  font-weight: 400;
  font-size: 14px;
  line-height: 16px;
  color: var(--vgd-text);
  transition: border-color 0.15s ease;
}
.vgd-input::placeholder { color: var(--vgd-text); opacity: 1; }
.vgd-input:hover { border-color: #c9c9c9; }
.vgd-input:hover::placeholder { color: var(--vgd-text-2); }
.vgd-field:focus-within { background: #d6d6d6; }
.vgd-input:focus { outline: none; border-color: var(--vgd-text); }
.vgd-input:focus::placeholder { color: var(--vgd-text); }
.vgd-field.is-error { background: rgba(203, 42, 47, 0.12); }
.vgd-field.is-error .vgd-input { border-color: var(--vgd-error); }

/* Consent: a bordered box holding the native checkbox (visually replaced by the
   drawn mark) and the agreement line. The whole box is the label, so a tap
   anywhere toggles it; the box brightens once it is ticked, per the board. */
.vgd-consent {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--vgd-sp-200);          /* 16px, spacing/200 */
  padding: var(--vgd-sp-250);      /* 20px, spacing/250 */
  /* The text fields are inset 2px by their focus-ring wrapper, so the T&C is
     inset the same amount to keep its visible edges aligned with theirs. */
  margin-inline: 2px;
  background: var(--vgd-surface-2);
  border: 1px solid var(--vgd-border-2);   /* #ebebeb default, per the component */
  border-radius: var(--vgd-radius-card);   /* 8px */
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
/* Hover: the box border darkens to #8d8d8d and the checkbox outline sharpens to
   ink (see the mark rule below) — the component's Hover variant. */
.vgd-consent:hover { border-color: var(--vgd-border-hover); }   /* #8d8d8d */
/* Checked/Active: surface-3 tint + ink border. Higher specificity than :hover,
   so a checked box keeps its ink border on hover. */
.vgd-consent:has(.vgd-consent-box:checked) {
  background: var(--vgd-surface-3);
  border-color: var(--vgd-border-ink);
}
.vgd-consent-box {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}
.vgd-consent-mark {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--vgd-border-2);   /* #ebebeb, faint at rest */
  border-radius: 2px;                      /* radius/forms */
  background: var(--vgd-surface);          /* #fafafa */
  color: transparent;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.vgd-consent-mark svg { width: 16px; height: 16px; display: block; }
/* Hover: the checkbox outline sharpens to ink, per the board's hover state. */
.vgd-consent:hover .vgd-consent-mark { border-color: var(--vgd-border-ink); }
.vgd-consent-box:checked ~ .vgd-consent-mark {
  background: var(--vgd-surface-invert);
  border-color: var(--vgd-surface-invert);
  color: var(--vgd-text-invert);
}
.vgd-consent-box:focus-visible ~ .vgd-consent-mark {
  outline: 2px solid var(--vgd-border-ink);
  outline-offset: 2px;
}
.vgd-consent-tx {
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
  color: var(--vgd-text-2);
}
.vgd-book-link { color: inherit; text-decoration: underline; }

/* Off-screen honeypot: a real visitor never sees or fills it; a filled one is
   dropped server-side as a bot. */
.vgd-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.vgd-book-error {
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
  color: var(--vgd-error);
}

/* ════════════════════════════════════════════════════════════════════════
   Confirmation screen (blueprint 4.7)
   A clone of the live site's booking confirmation. Blocks stack on sp-500; the
   info and dark cards stack their rows with shared 1px borders (the live-site
   pattern), not the reveal's inset-shadow rings.
   ════════════════════════════════════════════════════════════════════════ */
.vgd-sheet[data-screen="confirmation"] .vgd-screen { gap: var(--vgd-sp-500); }

.vgd-conf-top {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-500);
}
.vgd-conf-photo {
  width: 120px;
  height: 120px;
  border-radius: 16px;
  object-fit: cover;
  display: block;
}
@media (max-width: 767px) {
  .vgd-conf-photo { width: 96px; height: 96px; }   /* 96 on mobile, per Figma */
}

/* When the modal closes it restores focus to the CTA that opened it (good for
   keyboard users). Show the focus ring ONLY for keyboard focus, never the
   programmatic restore after a tap/click — otherwise a blue outline lingers on
   the "Diagnose your revenue leak" button after closing. */
[data-vgd-open]:focus:not(:focus-visible) { outline: none; }
.vgd-conf-head {
  display: flex;
  flex-direction: column;
  gap: var(--vgd-sp-150);
}
.vgd-conf-h {
  font-weight: 500;
  /* The live site's own confirmation heading (flow.css .diag-h): 28/36 on mobile
     → 36/44 desktop, not the run's --vgd-h2. */
  font-size: clamp(28px, 0.762vw + 25.03px, 36px);
  line-height: clamp(36px, 0.762vw + 33.03px, 44px);
  letter-spacing: -0.01em;
}
.vgd-conf-sub {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-2);
}

/* ── Info card: stacked rows sharing a 1px border ──────────────────────── */
.vgd-conf-info { display: flex; flex-direction: column; }
.vgd-conf-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  background: var(--vgd-surface-2);
  border: 1px solid var(--vgd-border);
  border-top-width: 0;
}
.vgd-conf-row:first-child { border-top-width: 1px; border-radius: 8px 8px 0 0; }
.vgd-conf-row:last-child  { border-radius: 0 0 8px 8px; }
.vgd-conf-ico {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--vgd-surface-3);
  border-radius: 8px;
  color: var(--vgd-text);
}
.vgd-conf-ico svg { width: 20px; height: 20px; display: block; }
.vgd-conf-tx { display: flex; flex-direction: column; min-width: 0; }
.vgd-conf-lb { font-weight: 400; font-size: 14px; line-height: 20px; color: var(--vgd-text-2); }
.vgd-conf-vl { font-weight: 500; font-size: 16px; line-height: 24px; color: var(--vgd-text); }

/* ── Dark "What happens next" card ─────────────────────────────────────── */
.vgd-conf-next { display: flex; flex-direction: column; }
.vgd-conf-next-row {
  padding: 20px 24px;
  background: var(--vgd-surface-invert);
  border: 1px solid var(--vgd-divider-invert);
  border-top-width: 0;
}
.vgd-conf-next-row:first-child { border-top-width: 1px; border-radius: 8px 8px 0 0; }
.vgd-conf-next-row:last-child  { border-radius: 0 0 8px 8px; }
.vgd-conf-next-head {
  font-weight: 500;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-invert);
}
.vgd-conf-next-tt {
  font-weight: 500;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-text-invert);
  margin-bottom: 8px;
}
.vgd-conf-next-ds {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--vgd-neutral-500);
}
