/* ============================================================
   V LINK — MOTION LAYER v3.6

   Loaded after main.css. Everything here is presentation for the
   motion kit in assets/js/motion-kit.js:

     · image reveal, parallax and drift
     · the gold sheen that crosses a photo as it lands
     · pointer-tracked card lighting
     · the spacing utilities the rhythm pass relies on

   Nothing in this sheet hides content on its own. Entrance states
   are set by GSAP at runtime, so a failed script or a blocked CDN
   still leaves a complete, readable page.
   ============================================================ */

/* ---------- 1. IMAGE MOTION ------------------------------- */

/*
 * Every photo is a two-part rig: the frame clips, the image moves
 * inside it. Scale 1.12 at rest is the parallax headroom — it lets
 * the image travel vertically without ever exposing an edge.
 */
.vl-media,
.vl-hero__bg,
.vl-banner__bg,
.vl-steprow__thumb,
.vl-page-hero__media,
.vl-spotlight__media,
.vl-figure__media,
.vl-portrait__photo {
  overflow: hidden;
  isolation: isolate;
}

.vl-anim-img {
  will-change: transform;
  backface-visibility: hidden;
  transform-origin: center center;
}

/*
 * main.css transitions transform and filter on these images for the
 * CSS-only hover zoom. Once the kit owns the inline transform, that
 * transition fights every frame GSAP writes — the tween ends up
 * chasing a value the browser is still interpolating. GSAP drives
 * both properties now, so the transition comes off.
 *
 * Specificity beats `.vl-media img` deliberately; without the class
 * present (no JS) the original transition still applies.
 */
.vl-media .vl-anim-img,
.vl-anim-frame .vl-anim-img { transition: none; }

/* The frame stops compositing work once the reveal has played. */
.vl-anim-frame.is-revealed .vl-anim-img { will-change: auto; }

/*
 * Gold sheen: a single diagonal pass across the photo as it arrives.
 * Injected by the kit, removed from the DOM when it finishes, so it
 * never sits in the paint tree idle.
 */
.vl-sheen {
  position: absolute;
  inset: -20% -60%;
  z-index: 3;
  pointer-events: none;
  transform: translateX(-120%) rotate(8deg);
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(210, 172, 95, 0) 30%,
    rgba(210, 172, 95, 0.22) 48%,
    rgba(245, 242, 234, 0.3) 52%,
    rgba(210, 172, 95, 0) 70%,
    transparent 100%
  );
  mix-blend-mode: screen;
  opacity: 0;
}

/*
 * A hairline that draws itself across the bottom of a frame as the
 * photo settles — the detail that reads as "composed", not "loaded".
 */
.vl-anim-frame::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 4;
  width: 100%;
  height: 1px;
  background: linear-gradient(90deg, var(--gold), rgba(210, 172, 95, 0));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1.2s var(--ease-out) 0.25s;
  pointer-events: none;
}
.vl-anim-frame.is-revealed::before { transform: scaleX(1); }

/* The tint layer already owns ::before on these two. */
.vl-media--tint.vl-anim-frame::before { display: none; }

/* Hover: the frame lifts, the photo pushes past it. */
.vl-media {
  transition: transform var(--dur-slow) var(--ease-spring),
              box-shadow var(--dur-slow) var(--ease-out);
}
a:hover > .vl-media,
.vl-card:hover .vl-media {
  transform: translateY(-4px);
  box-shadow: 0 1.5rem 3rem -1rem rgba(0, 0, 0, 0.75);
}

/* ---------- 2. POINTER-TRACKED CARD LIGHT ----------------- */

/*
 * --mx/--my are written by the kit on pointermove. The gradient is
 * painted only while the pointer is inside the card, so a page of
 * cards costs nothing until one is touched.
 */
[data-spotlight] {
  position: relative;
}
[data-spotlight]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
  background: radial-gradient(
    22rem 22rem at var(--mx, 50%) var(--my, 50%),
    rgba(210, 172, 95, 0.13),
    rgba(210, 172, 95, 0.04) 38%,
    transparent 68%
  );
}
[data-spotlight].is-lit::after { opacity: 1; }

/* Edge catch-light: the border warms toward the pointer. */
[data-spotlight].is-lit {
  border-color: rgba(210, 172, 95, 0.3);
}

/* ---------- 1b. PARTICLE FIELD ----------------------------- */

/*
 * The connected-dot field sits behind the whole site at half strength,
 * so it reads as texture in the black rather than as a foreground
 * graphic. Fixed, so it stays put while the page scrolls past it.
 *
 * pointer-events are off: the canvas covers the viewport, and anything
 * that covers the viewport must not be able to swallow a click. The
 * cursor interaction is driven from a window listener instead.
 */
#vl-particles {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
}
#vl-particles canvas { display: block; }

/*
 * Page content is lifted into its own layer above the field. Without
 * this the canvas would paint over it — a positioned element at
 * z-index 0 outranks non-positioned boxes, whatever the source order.
 *
 * The header (90), menu (95), progress bar (89) and skip link (200)
 * already carry z-index values well clear of the field, so the existing
 * scale is left alone.
 */
#main,
.vl-footer {
  position: relative;
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  /* The field stays — it is drawn once, static — but quieter. */
  #vl-particles { opacity: 0.28; }
}

/* ---------- 1c. FRACTAL BLOOM (hero only) ------------------ */

/*
 * The branching tree that grows out of the bottom of a hero. Half
 * strength, so it reads as structure behind the headline rather than
 * as artwork competing with it.
 *
 * Sits above the hero photograph (it is inserted after it in the DOM)
 * and below the content wrapper, which carries its own z-index.
 */
.vl-fractal {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  overflow: hidden;
}
.vl-fractal canvas {
  display: block;
  width: 100%;
  height: 100%;
  /*
   * Screen keeps the branches readable over a photograph without
   * needing a scrim: on the black heroes it behaves like normal
   * compositing, and over the hero image it lightens rather than
   * disappearing into it.
   */
  mix-blend-mode: screen;
}

@media (prefers-reduced-motion: reduce) {
  .vl-fractal { opacity: 0.3; }
}

/* ---------- 1c2. ICONS ------------------------------------- */

/*
 * One grid, one weight. Icons inherit colour from their surroundings,
 * so the "gold only under the cursor" rule needs no icon-specific
 * overrides — colouring the parent carries the icon with it.
 */
.vl-icon {
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
  display: block;
  color: inherit;
  vector-effect: non-scaling-stroke;
}
.vl-icon--lg { width: 1.75rem; height: 1.75rem; }
.vl-icon--sm { width: 1rem; height: 1rem; }

/*
 * Impact card icon: sits opposite the number, quiet until the card is
 * under the cursor. A ring rather than a filled chip, to stay in the
 * same hairline language as the borders around it.
 */
.vl-impact-card__icon {
  position: absolute;
  top: 1.5rem;
  right: 1.5rem;
  z-index: 3;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--line-soft);
  border-radius: 50%;
  color: var(--muted-dim);
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              transform var(--dur-slow) var(--ease-spring);
}
.vl-impact-card:hover .vl-impact-card__icon,
.vl-impact-card:focus-visible .vl-impact-card__icon {
  color: var(--gold);
  border-color: rgba(210, 172, 95, 0.45);
  transform: scale(1.08);
}

/* Contact list: icons replace the old ✦ glyphs. */
.vl-contactlist li {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.vl-contactlist .vl-icon {
  color: var(--muted-dim);
  transition: color var(--dur-base) var(--ease-out);
}
.vl-contactlist li:hover .vl-icon { color: var(--gold); }

/* ---------- 1d. PHOTOGRAPH GRID ---------------------------- */

.vl-gallery {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(var(--s-5), 2.5vw, var(--s-8));
}
@media (min-width: 700px) {
  .vl-gallery--2 { grid-template-columns: repeat(2, 1fr); }
  .vl-gallery--3 { grid-template-columns: repeat(2, 1fr); }
  .vl-gallery--4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1000px) {
  .vl-gallery--3 { grid-template-columns: repeat(3, 1fr); }
  .vl-gallery--4 { grid-template-columns: repeat(4, 1fr); }
}
.vl-gallery figure { margin: 0; }
.vl-gallery figcaption {
  margin-top: var(--s-3);
  color: var(--muted);
  font-size: 0.83rem;
  line-height: 1.5;
}

/* ---------- 2b. THE ROUTE ---------------------------------- */

/*
 * The four homepage stages are links, so they carry a pointer, a hover
 * state and a focus ring. Hovering one also fills the connecting line
 * up to that stage — the component's own metaphor, driven by
 * --route-p, which motion-kit sets from the hovered index.
 */
.vl-route__item {
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out),
              transform var(--dur-slow) var(--ease-spring);
}
.vl-route__item > span {
  transition: border-color var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out),
              transform var(--dur-slow) var(--ease-spring);
}
.vl-route__item h3 { transition: color var(--dur-base) var(--ease-out); }
.vl-route__item p  { transition: color var(--dur-base) var(--ease-out); }

.vl-route__item:hover,
.vl-route__item:focus-visible {
  border-color: rgba(210, 172, 95, 0.55);
  background: rgba(210, 172, 95, 0.06);
  transform: translateX(0.4rem);
}
.vl-route__item:hover > span,
.vl-route__item:focus-visible > span {
  border-color: var(--gold);
  background: var(--gold);
  color: var(--gold-ink);
  transform: scale(1.06);
}
.vl-route__item:hover h3,
.vl-route__item:focus-visible h3 { color: var(--gold); }
.vl-route__item:hover p,
.vl-route__item:focus-visible p { color: var(--fg); }

/* Focus ring on the row itself, not on the text inside it. */
.vl-route__item:focus-visible { outline-offset: 4px; }

/*
 * Progress fill. The base line stays at its resting opacity underneath;
 * this overlay is the travelled part of the route.
 */
.vl-route__line::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  width: 100%;
  height: var(--route-p, 0%);
  background: linear-gradient(var(--cyan), var(--gold));
  transition: height var(--dur-slow) var(--ease-out);
}

/* ---------- 3. SECTION ENTRANCE SCAFFOLD ------------------ */

/*
 * The kit tags each section shell so its heading block, body and
 * media can arrive on separate beats. These only carry compositing
 * hints — the transforms themselves are GSAP's.
 */
[data-motion-section] { position: relative; }
[data-motion-item] { will-change: transform, opacity, filter; }
[data-motion-item].is-motion-done { will-change: auto; filter: none; }

/* ---------- 4. RHYTHM UTILITIES --------------------------- */

/*
 * Card and comparison grids sat at a fixed ~1rem gutter regardless of
 * viewport: tight at desktop widths, loose on a phone. One fluid step
 * off the scale instead, so columns breathe as the page widens.
 */
.vl-grid,
.vl-compare {
  gap: clamp(var(--s-5), 2.5vw, var(--s-8));
}

/*
 * Section head → body was three different steps depending on the page.
 * One rung, applied everywhere.
 */
.vl-head { margin-bottom: clamp(var(--s-8), 5vw, var(--s-11)); }
.vl-head .vl-display { margin-top: var(--s-6); }
.vl-head .vl-lead    { margin-top: var(--s-6); }
.vl-head .vl-lead    { max-width: 62ch; }

/*
 * Line length. Editorial body copy running the full 1240px container
 * is past the comfortable 65–75 character range.
 */
.vl-lead,
.vl-card__text,
.vl-row__text { max-width: 68ch; }

/* ---------- 5. MOTION PREFERENCES ------------------------- */

@media (prefers-reduced-motion: reduce) {
  .vl-anim-img {
    transform: none !important;
    clip-path: none !important;
    opacity: 1 !important;
    filter: none !important;
  }
  .vl-anim-frame::before { transition: none; transform: scaleX(1); }
  .vl-sheen { display: none; }
  [data-spotlight]::after { display: none; }
  a:hover > .vl-media,
  .vl-card:hover .vl-media { transform: none; }
  [data-motion-item] {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
  }
}

/* ---------- 6. IMPACT CARD PHOTOGRAPHY --------------------- */

/*
 * A photograph behind the card's words.
 *
 * Held well back at rest — dimmed, desaturated, and under a scrim that
 * is opaque where the text sits and clears toward the top corner. The
 * card has to read as typography with a photograph behind it, not as a
 * photograph with text on top; the copy is the point.
 *
 * It lifts on hover, in the same "the cursor brings it to life" language
 * the rest of the site uses.
 */
.vl-impact-card__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}
.vl-impact-card__bg img,
.vl-impact-card__bg picture {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.vl-impact-card__bg img {
  opacity: 0.3;
  filter: grayscale(0.45) brightness(0.62) contrast(1.05);
  transform: scale(1.04);
  transition: opacity var(--dur-slow) var(--ease-out),
              filter var(--dur-slow) var(--ease-out),
              transform 1.2s var(--ease-out);
}

/* The scrim: solid under the copy, clearing toward the top corner. */
.vl-impact-card__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(0deg, rgba(0, 0, 0, 0.94) 12%, rgba(0, 0, 0, 0.6) 52%, rgba(0, 0, 0, 0.35) 100%),
    radial-gradient(120% 90% at 85% 8%, rgba(210, 172, 95, 0.1), transparent 60%);
}

/* Everything above the photograph. */
.vl-impact-card.has-bg::before,
.vl-impact-card.has-bg > *:not(.vl-impact-card__bg) { position: relative; z-index: 2; }
.vl-impact-card.has-bg .vl-impact-card__num { position: absolute; z-index: 2; }

.vl-impact-card:hover .vl-impact-card__bg img,
.vl-impact-card:focus-visible .vl-impact-card__bg img {
  opacity: 0.45;
  filter: grayscale(0.15) brightness(0.78) contrast(1.06);
  transform: scale(1.09);
}

@media (prefers-reduced-motion: reduce) {
  .vl-impact-card__bg img { transform: none; transition: none; }
}

/*
 * Figure card: no title, no text — the artwork is the message. It rests
 * at half visibility and comes to full under the cursor, the same "the
 * pointer brings it to life" rule the rest of the site follows. No
 * scrim and no grade, both of which exist to protect copy that is not
 * there, and the picture fills the card rather than being cropped to a
 * caption's proportions.
 */
.vl-impact-card.is-figure {
  padding: 0;
  overflow: hidden;
  /* The card takes the artwork's own 4:5 shape, so the whole picture
     shows with nothing cropped away. min-height is what forced the old
     captioned proportion; it has to go, not just be overridden. */
  min-height: 0;
  aspect-ratio: 4 / 5;
}
.vl-impact-card.is-figure .vl-impact-card__bg { inset: 0; }
.vl-impact-card.is-figure .vl-impact-card__bg::after { display: none; }

/* A figure card's title sits over the artwork, top left, out of the
   way of the drawing. Muted until the cursor arrives, like every other
   label on the site. */
.vl-impact-card.is-figure .vl-impact-card__label {
  position: absolute;
  top: clamp(1rem, 2.4vw, 1.6rem);
  left: clamp(1rem, 2.4vw, 1.6rem);
  z-index: 2;
  font: 700 clamp(1.1rem, 1.7vw, 1.45rem)/1 var(--font-display);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fg);
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.85);
  transition: color var(--dur-base) var(--ease-out);
}
.vl-impact-card.is-figure:hover .vl-impact-card__label,
.vl-impact-card.is-figure:focus-within .vl-impact-card__label { color: var(--gold); }
.vl-impact-card.is-figure .vl-impact-card__bg img {
  opacity: 0.5;
  filter: none;
  object-fit: contain;
  object-position: center;
  transform: none;
}
.vl-impact-card.is-figure:hover .vl-impact-card__bg img,
.vl-impact-card.is-figure:focus-within .vl-impact-card__bg img {
  opacity: 1;
  filter: none;
  transform: none;
}


/* ---------- 7. SECTION FIGURE: LEADS → ONE MEETING --------- */

/*
 * Puts the diagram beside the section headline instead of under it.
 * Below 1000px there is no room for two columns, so it stacks and the
 * figure sits under the copy at a reduced size.
 */
.vl-head--figure {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(var(--s-7), 4vw, var(--s-10));
  align-items: center;
}
@media (min-width: 1000px) {
  .vl-head--figure { grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.7fr); }
}
.vl-head--figure .vl-head__text > * + * { margin-top: var(--s-6); }

.vl-head__figure {
  justify-self: center;
  width: 100%;
  max-width: 22rem;
}
.vl-fig {
  width: 100%;
  height: auto;
  display: block;
  color: var(--muted-dim);
}
/* Stacked under the copy on tablet and phone, the diagram is the same
   width as the text column and its labels shrink with it. Cap the
   width and lift the label size so it stays readable. */
@media (max-width: 999px) {
  .vl-head__figure { max-width: 20rem; margin-top: var(--s-4); }
  .vl-fig__label { font-size: 13px; }
}

.vl-fig__label {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.14em;
  fill: var(--muted-dim);
  stroke: none;
}
.vl-fig__label--gold { fill: var(--gold); }

/*
 * One 9-second loop, read left to right:
 *   records arrive and stack up      0 – 30%
 *   most are rejected and fade       30 – 55%
 *   one passes the screen            45 – 70%
 *   the meeting lands and ticks      65 – 100%
 *
 * Each record is delayed by its own --i, so they arrive in sequence
 * rather than as a block.
 */
.vl-fig__lead {
  opacity: 0;
  animation: vl-fig-lead 9s var(--ease-out) infinite;
  animation-delay: calc(var(--i) * 0.14s);
}
@keyframes vl-fig-lead {
  0%        { opacity: 0; transform: translateX(-14px); }
  10%, 34%  { opacity: 0.85; transform: translateX(0); }
  48%       { opacity: 0.12; transform: translateX(10px); }
  60%, 100% { opacity: 0; transform: translateX(10px); }
}

.vl-fig__screen { opacity: 0; animation: vl-fig-screen 9s var(--ease-out) infinite; }
@keyframes vl-fig-screen {
  0%, 14%   { opacity: 0; }
  26%, 94%  { opacity: 1; color: var(--gold); }
  100%      { opacity: 0; }
}

.vl-fig__pass {
  opacity: 0;
  color: var(--gold);
  animation: vl-fig-pass 9s var(--ease-out) infinite;
}
@keyframes vl-fig-pass {
  0%, 44%  { opacity: 0; transform: translateX(-26px); }
  56%      { opacity: 1; transform: translateX(0); }
  68%      { opacity: 1; transform: translateX(76px); }
  76%, 100%{ opacity: 0; transform: translateX(96px); }
}

.vl-fig__meeting {
  opacity: 0;
  color: var(--gold);
  animation: vl-fig-meeting 9s var(--ease-spring) infinite;
  transform-origin: 282px 108px;
}
@keyframes vl-fig-meeting {
  0%, 60%   { opacity: 0; transform: scale(0.9); }
  72%, 96%  { opacity: 1; transform: scale(1); }
  100%      { opacity: 0; transform: scale(1); }
}

/* The tick draws itself rather than appearing. */
.vl-fig__tick {
  stroke-dasharray: 46;
  stroke-dashoffset: 46;
  animation: vl-fig-tick 9s var(--ease-out) infinite;
}
@keyframes vl-fig-tick {
  0%, 74%   { stroke-dashoffset: 46; }
  84%, 100% { stroke-dashoffset: 0; }
}

@media (prefers-reduced-motion: reduce) {
  /*
   * The diagram still has to explain itself without motion, so every
   * stage is simply shown at once rather than hidden.
   */
  .vl-fig__lead,
  .vl-fig__screen,
  .vl-fig__pass,
  .vl-fig__meeting,
  .vl-fig__tick { animation: none; opacity: 1; transform: none; stroke-dashoffset: 0; }
  .vl-fig__lead { opacity: 0.6; }
  .vl-fig__pass { opacity: 0; }
  .vl-fig__screen, .vl-fig__meeting { color: var(--gold); }
}

/* ---------- 8. ROUTE STAGE PHOTOGRAPHY --------------------- */

/*
 * These rows are ~90px tall, so a full-bleed photograph shows a thin
 * horizontal slice and reads as smudge. The photograph takes the
 * right-hand end of the row instead, masked so it dissolves into the
 * black well before it reaches the copy.
 *
 * It is painted by ::after rather than an <img>: the row is a grid, and
 * an absolutely positioned grid child is laid out against its own grid
 * area — the number column — instead of the row. A pseudo-element is
 * not a grid item, so it spans the row as intended.
 */
.vl-route__item.has-bg::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 40%;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  background-image: var(--stage-bg);
  background-size: cover;
  background-position: center;
  /* Flat artwork, not a photograph: it is already drawn in the site's
     palette on the row's own ground, so it needs no desaturating and
     no crop — contain keeps the whole scene inside the row. */
  background-size: contain;
  background-position: right center;
  background-repeat: no-repeat;
  opacity: 0.55;
  filter: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 45%, #000 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 45%, #000 100%);
  transition: opacity var(--dur-slow) var(--ease-out),
              filter var(--dur-slow) var(--ease-out);
}

/* Number, copy and the connecting line stay above the photograph. */
.vl-route__item.has-bg > * { position: relative; z-index: 1; }

.vl-route__item.has-bg:hover::after,
.vl-route__item.has-bg:focus-visible::after {
  opacity: 1;
  filter: none;
}

/* On a phone the row is all copy; the photograph would only crowd it. */
@media (max-width: 700px) {
  .vl-route__item.has-bg::after { display: none; }
}

/* ---------- 9. SECTION FIGURE: QUEUE → NAMED OWNER --------- */

/*
 * Drawn under "no faceless queue — a person owns the meeting". One
 * 12s loop, deliberately slow: it sits beside body copy and a quick
 * cycle in the corner of the eye is a distraction, not an aid.
 *
 * Every stage holds its finished state until the loop restarts, so a
 * reader who glances at any moment sees a coherent picture rather
 * than a half-drawn one.
 */
.vl-fig2-wrap { margin-top: 2.25rem; max-width: 40rem; }
.vl-fig2 { width: 100%; height: auto; display: block; }

.vl-fig2 text {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: 0.14em;
  fill: var(--muted);
}

/* the queue: unnamed, and it stays that way */
.vl-fig2__ghost {
  fill: rgba(242, 237, 227, 0.22);
  animation: vlFig2Wait 12s var(--ease) infinite;
  animation-delay: calc(var(--i) * 0.12s);
}

/* the one who steps out is gold, and gets a name plate */
.vl-fig2__owner { fill: var(--gold); opacity: 0; animation: vlFig2Step 12s var(--ease) infinite; }
.vl-fig2__badge rect { fill: none; stroke: var(--gold); stroke-width: 1.2; }
.vl-fig2__rule { stroke: var(--gold); stroke-width: 2; stroke-linecap: round; opacity: 0.75; }

.vl-fig2__thread {
  stroke: var(--gold);
  stroke-width: 1.2;
  fill: none;
  opacity: 0;
  stroke-dasharray: 220;
  stroke-dashoffset: 220;
  animation: vlFig2Thread 12s var(--ease) infinite;
}

.vl-fig2__duty rect { fill: none; stroke: rgba(255, 255, 255, 0.12); stroke-width: 1; }
.vl-fig2__duty { opacity: 0.35; animation: vlFig2Duty 12s var(--ease) infinite; animation-delay: calc(2.6s + var(--i) * 1.5s); }
.vl-fig2__tick {
  stroke: var(--gold);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  stroke-dasharray: 34;
  stroke-dashoffset: 34;
  animation: vlFig2Tick 12s var(--ease) infinite;
  animation-delay: calc(1.9s + var(--i) * 1.3s);
}

@keyframes vlFig2Wait {
  /* Opacity only. A CSS transform here would override each figure's
     own position and stack the whole queue on one spot. */
  0%, 2%    { opacity: 0; }
  7%, 100%  { opacity: 1; }
}
@keyframes vlFig2Step {
  0%, 5%    { opacity: 0; transform: translate(-26px, 0); }
  13%, 99%  { opacity: 1; transform: translate(0, 0); }
  100%      { opacity: 0; transform: translate(-26px, 0); }
}
@keyframes vlFig2Thread {
  0%, 11%   { opacity: 0; stroke-dashoffset: 220; }
  20%, 99%  { opacity: 0.7; stroke-dashoffset: 0; }
  100%      { opacity: 0; stroke-dashoffset: 220; }
}
@keyframes vlFig2Duty {
  0%, 3%    { opacity: 0.35; }
  9%, 84%   { opacity: 1; }
  100%      { opacity: 0.35; }
}
@keyframes vlFig2Tick {
  0%, 2%    { stroke-dashoffset: 34; }
  8%, 88%   { stroke-dashoffset: 0; }
  100%      { stroke-dashoffset: 34; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold the finished picture: the diagram still says what it says. */
  .vl-fig2__ghost,
  .vl-fig2__owner,
  .vl-fig2__thread,
  .vl-fig2__duty,
  .vl-fig2__tick { animation: none; opacity: 1; transform: none; stroke-dashoffset: 0; }
}

@media (max-width: 760px) {
  /* These are viewBox units: the drawing scales down with the column,
     so 11px would land under 7px on a phone screen. */
  .vl-fig2 text { font-size: 19px; letter-spacing: 0.1em; }
}


/* ---- Figure cards: shape follows the column width ---------------- */
@media (min-width: 560px) and (max-width: 819px) {
  /* Two-up on a tablet the 4:5 portrait is still tall but no longer
     absurd; full-width on a phone it needs to be shorter again. */
  .vl-impact-card.is-figure { aspect-ratio: 1 / 1; }

  /* An odd last card would sit alone against an empty half-row. Centre
     it across both columns at one column's width: it keeps the square
     the artwork was cut for, and the row reads as balanced rather than
     half-empty. Widening it instead would letterbox the picture. */
  .vl-impact-grid > *:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    justify-self: center;
    width: calc(50% - 0.5rem);
  }
}
@media (max-width: 559px) {
  .vl-impact-card.is-figure { aspect-ratio: 5 / 6; }
}

/* ---------- 10. THE PANELS BESIDE THE FIT DISC -------------------- */

/*
 * Hidden until the disc is pressed. They sit either side of the orbit,
 * outside its circle, at half strength — present enough to read, quiet
 * enough that the disc stays the subject.
 *
 * `visibility` is animated alongside opacity so nothing under them is
 * clickable while they are away.
 */
.vl-orbit-aside {
  position: absolute;
  top: 50%;
  width: min(20rem, 30vw);
  aspect-ratio: 1;
  transform: translateY(-50%) scale(0.96);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  color: var(--muted);
  transition: opacity 0.45s var(--ease), transform 0.55s var(--ease-out), visibility 0.45s;
}
.vl-orbit-aside--left  { right: calc(100% + clamp(1rem, 4vw, 4rem)); }
.vl-orbit-aside--right { left:  calc(100% + clamp(1rem, 4vw, 4rem)); }
.vl-orbit-aside svg { width: 100%; height: 100%; display: block; }

.vl-screen-orbit.is-rushing .vl-orbit-aside {
  opacity: 0.5;
  visibility: visible;
  transform: translateY(-50%) scale(1);
}

.vl-aside__label {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: 0.16em;
  fill: var(--gold);
  opacity: 0.9;
}
.vl-aside__bar { fill: currentColor; opacity: 0.3; }
.vl-aside__bar--gold { fill: var(--gold); opacity: 0.75; }
.vl-aside__head { fill: var(--cyan); opacity: 0.65; }
.vl-aside__dot { fill: var(--gold); }

/*
 * The animations only run while the panels are shown. Declaring them
 * on the hidden state would burn frames on something nobody can see.
 */
.vl-screen-orbit.is-rushing .vl-aside__record {
  animation: vlAsideDrift 2.6s var(--ease-out) infinite;
  animation-delay: calc(var(--i) * 0.18s);
}
.vl-screen-orbit.is-rushing .vl-aside__answer {
  animation: vlAsideAnswer 2.6s var(--ease-out) infinite;
  animation-delay: calc(var(--i) * 0.2s);
}
.vl-screen-orbit.is-rushing .vl-aside__slot {
  animation: vlAsideSlot 2.6s var(--ease-out) infinite;
}

/* Raw records jitter and fade: unordered, interchangeable, going nowhere. */
@keyframes vlAsideDrift {
  0%, 100% { opacity: 0.35; transform: translateX(0); }
  45%      { opacity: 0.85; transform: translateX(6px); }
}

/* Answers land one after another, and hold. */
@keyframes vlAsideAnswer {
  0%, 10%  { opacity: 0; transform: translateX(-8px); }
  30%, 92% { opacity: 1; transform: translateX(0); }
  100%     { opacity: 0; transform: translateX(-8px); }
}

/* The agreed slot arrives last. */
@keyframes vlAsideSlot {
  0%, 55%  { opacity: 0; }
  72%, 94% { opacity: 1; }
  100%     { opacity: 0; }
}

/* No room beside the circle on a tablet or phone — and nothing is lost:
   the chips below say the same thing in words. */
@media (max-width: 1100px) {
  .vl-orbit-aside { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .vl-screen-orbit.is-rushing .vl-aside__record,
  .vl-screen-orbit.is-rushing .vl-aside__answer,
  .vl-screen-orbit.is-rushing .vl-aside__slot { animation: none; opacity: 1; transform: none; }
}

/* ---------- 11. FIGURE: KICKOFF → A REPEATING RHYTHM -------------- */

/*
 * One 15s loop under the implementation timeline. Slow on purpose: it
 * sits beneath body copy, and a fast cycle in the corner of the eye is
 * a distraction rather than an explanation.
 *
 * Every station holds its lit state once reached, so the picture builds
 * up rather than flickering — a reader glancing at any moment sees how
 * far the run has got, not a single blinking dot.
 */
.vl-fig3-wrap { margin-top: clamp(2.5rem, 5vw, 4rem); }
.vl-fig3 { width: 100%; height: auto; display: block; overflow: visible; }

.vl-fig3__track {
  stroke: rgba(255, 255, 255, 0.12);
  stroke-width: 2;
}
.vl-fig3__run {
  stroke: var(--gold);
  stroke-width: 2;
  stroke-dasharray: 820;
  stroke-dashoffset: 820;
  animation: vlFig3Run 15s var(--ease-out) infinite;
}

.vl-fig3__ring  { fill: none; stroke: rgba(255, 255, 255, 0.14); stroke-width: 1.4; }
.vl-fig3__pip   { fill: rgba(255, 255, 255, 0.22); }
.vl-fig3__glyph { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; color: var(--muted-dim); }
.vl-fig3__slot  { fill: currentColor; stroke: none; }
.vl-fig3__label {
  font: 500 12px/1 var(--font-mono);
  letter-spacing: 0.14em;
  fill: var(--muted-dim);
}

/* Each station lights as the token reaches it and stays lit. */
.vl-fig3__station { animation: vlFig3Reach 15s linear infinite; animation-delay: calc(var(--i) * 2.6s); }
@keyframes vlFig3Reach {
  0%, 1%   { opacity: 0.45; }
  4%, 92%  { opacity: 1; }
  100%     { opacity: 0.45; }
}
.vl-fig3__station .vl-fig3__pip,
.vl-fig3__station .vl-fig3__ring,
.vl-fig3__station .vl-fig3__glyph,
.vl-fig3__station .vl-fig3__label { transition: none; }

.vl-fig3__station { --lit: 0; }
.vl-fig3__station .vl-fig3__pip { animation: vlFig3Pip 15s linear infinite; animation-delay: calc(var(--i) * 2.6s); }
@keyframes vlFig3Pip {
  0%, 1%  { fill: rgba(255, 255, 255, 0.22); }
  4%, 92% { fill: var(--gold); }
  100%    { fill: rgba(255, 255, 255, 0.22); }
}
.vl-fig3__station .vl-fig3__glyph { animation: vlFig3Glyph 15s linear infinite; animation-delay: calc(var(--i) * 2.6s); }
@keyframes vlFig3Glyph {
  0%, 1%  { color: var(--muted-dim); }
  4%, 92% { color: var(--gold); }
  100%    { color: var(--muted-dim); }
}
.vl-fig3__station .vl-fig3__label { animation: vlFig3Label 15s linear infinite; animation-delay: calc(var(--i) * 2.6s); }
@keyframes vlFig3Label {
  0%, 1%  { fill: var(--muted-dim); }
  4%, 92% { fill: var(--fg); }
  100%    { fill: var(--muted-dim); }
}

/* The token runs the track, pausing on each station. */
.vl-fig3__token { fill: var(--gold); animation: vlFig3Token 15s var(--ease) infinite; }
@keyframes vlFig3Token {
  0%           { transform: translateX(0);    opacity: 0; }
  2%           { transform: translateX(0);    opacity: 1; }
  14%, 17%     { transform: translateX(205px); }
  31%, 34%     { transform: translateX(410px); }
  48%, 51%     { transform: translateX(615px); }
  65%, 94%     { transform: translateX(820px); opacity: 1; }
  100%         { transform: translateX(820px); opacity: 0; }
}
@keyframes vlFig3Run {
  0%        { stroke-dashoffset: 820; }
  65%, 94%  { stroke-dashoffset: 0; }
  100%      { stroke-dashoffset: 0; }
}

/* On a phone the run is drawn small; lift the labels so they hold. */
@media (max-width: 760px) {
  .vl-fig3__label { font-size: 20px; letter-spacing: 0.08em; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold the finished run: every station reached, the track complete. */
  .vl-fig3__run,
  .vl-fig3__token,
  .vl-fig3__station,
  .vl-fig3__station * { animation: none; }
  .vl-fig3__run   { stroke-dashoffset: 0; }
  .vl-fig3__token { transform: translateX(820px); }
  .vl-fig3__pip   { fill: var(--gold); }
  .vl-fig3__glyph { color: var(--gold); }
  .vl-fig3__label { fill: var(--fg); }
}

/* ---------- 12. FOR DEVELOPERS — HERO ARTWORK -------------------- */

/*
 * The drawing behind the headline. Motion is deliberately small: this
 * sits under type, so anything that moves far or fast competes with the
 * words. One slow 14s cycle, low amplitude, nothing that flashes.
 */
.vl-page-hero__media--art { inset: 0; opacity: 0.9; }
.vl-devhero {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

/* Bought attention: many faint marks, drifting, arriving nowhere. */
.vl-devhero .noise circle {
  fill: #f2ede3;
  transform-box: fill-box;
  transform-origin: center;
  animation: vlDevDrift 9s ease-in-out infinite;
}
.vl-devhero .noise circle:nth-child(3n)   { animation-delay: -1.4s; animation-duration: 11s; }
.vl-devhero .noise circle:nth-child(3n+1) { animation-delay: -3.8s; animation-duration: 13s; }

/* The managed path draws itself, then holds. */
.vl-devhero .path {
  stroke-dasharray: 1180;
  stroke-dashoffset: 1180;
  animation: vlDevDraw 14s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
}

/* A short segment travels the same route: one buyer making the trip. */
.vl-devhero .comet {
  stroke-dasharray: 26 1154;
  stroke-dashoffset: 1180;
  animation: vlDevComet 14s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

.vl-devhero .ring-outer,
.vl-devhero .arrival,
.vl-devhero .tick-ring { transform-box: fill-box; transform-origin: center; }
.vl-devhero .ring-outer { animation: vlDevBreathe 14s ease-in-out infinite; }
.vl-devhero .arrival    { animation: vlDevArrive 14s ease-out infinite; }
.vl-devhero .tick-ring  { animation: vlDevArrive 14s ease-out infinite; animation-delay: 0.8s; }
.vl-devhero .tick {
  stroke-dasharray: 34;
  stroke-dashoffset: 34;
  animation: vlDevTick 14s ease-out infinite;
  animation-delay: 0.9s;
}
.vl-devhero .windows rect { animation: vlDevWindow 14s ease-in-out infinite; }
.vl-devhero .windows rect:nth-child(2n) { animation-delay: -4s; }
.vl-devhero .windows rect:nth-child(3n) { animation-delay: -7s; }

@keyframes vlDevDrift {
  0%, 100% { opacity: 0.10; transform: translateY(0); }
  50%      { opacity: 0.22; transform: translateY(-6px); }
}
@keyframes vlDevDraw {
  0%       { stroke-dashoffset: 1180; }
  42%, 92% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 1180; }
}
@keyframes vlDevComet {
  0%, 6%    { stroke-dashoffset: 1180; opacity: 0; }
  12%       { opacity: 1; }
  46%       { stroke-dashoffset: 26; opacity: 1; }
  54%, 100% { stroke-dashoffset: 26; opacity: 0; }
}
@keyframes vlDevBreathe {
  0%, 100% { opacity: 0.35; transform: scale(0.98); }
  55%      { opacity: 0.6;  transform: scale(1); }
}
@keyframes vlDevArrive {
  0%, 44%  { opacity: 0; transform: scale(0.9); }
  56%, 94% { opacity: 1; transform: scale(1); }
  100%     { opacity: 0; transform: scale(0.9); }
}
@keyframes vlDevTick {
  0%, 50%  { stroke-dashoffset: 34; }
  62%, 94% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 34; }
}
@keyframes vlDevWindow {
  0%, 100% { opacity: 0.12; }
  40%, 60% { opacity: 0.30; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold the arrived state: path complete, buyer confirmed. */
  .vl-devhero .noise circle,
  .vl-devhero .path,
  .vl-devhero .comet,
  .vl-devhero .ring-outer,
  .vl-devhero .arrival,
  .vl-devhero .tick-ring,
  .vl-devhero .tick,
  .vl-devhero .windows rect { animation: none; }
  .vl-devhero .path { stroke-dashoffset: 0; }
  .vl-devhero .comet { opacity: 0; }
  .vl-devhero .arrival, .vl-devhero .tick-ring { opacity: 1; }
  .vl-devhero .tick { stroke-dashoffset: 0; }
}

/* ---------- 13. FIGURE: LEAD STALLS, MEETING PROGRESSES ---------- */

/*
 * Two lanes running the same 13s cycle at the same time, because the
 * point is the comparison: the top token stops at the first unanswered
 * question while the bottom one is still going.
 */
.vl-fig4-wrap { margin-top: clamp(2.5rem, 5vw, 4rem); }
.vl-fig4 { width: 100%; height: auto; display: block; overflow: visible; }

.vl-fig4__label {
  font: 500 12px/1 var(--font-mono);
  letter-spacing: 0.16em;
  fill: var(--muted-dim);
}
.vl-fig4__label--gold { fill: var(--gold); opacity: 0.9; }

.vl-fig4__rail { stroke: rgba(255, 255, 255, 0.10); stroke-width: 2; }

/* The lead gets as far as the first gate, and no further. */
.vl-fig4__gone {
  stroke: rgba(242, 237, 227, 0.35);
  stroke-width: 2;
  stroke-dasharray: 250;
  stroke-dashoffset: 250;
  animation: vlFig4Gone 13s cubic-bezier(0.3, 0.7, 0.4, 1) infinite;
}
.vl-fig4__gate circle { fill: none; stroke: rgba(255, 255, 255, 0.16); stroke-width: 1.5; }
.vl-fig4__cross {
  stroke: #c0623f;
  stroke-width: 2.4;
  stroke-linecap: round;
  opacity: 0;
  animation: vlFig4Cross 13s ease-out infinite;
}
.vl-fig4__token { fill: rgba(242, 237, 227, 0.55); }
.vl-fig4__token--stall { animation: vlFig4Stall 13s cubic-bezier(0.3, 0.7, 0.4, 1) infinite; }

/* The meeting clears every checkpoint. */
.vl-fig4__run {
  stroke: var(--gold);
  stroke-width: 2;
  stroke-dasharray: 880;
  stroke-dashoffset: 880;
  animation: vlFig4Run 13s cubic-bezier(0.3, 0.7, 0.4, 1) infinite;
}
.vl-fig4__check circle { fill: none; stroke: rgba(210, 172, 95, 0.35); stroke-width: 1.5; }
.vl-fig4__tick {
  stroke: var(--gold);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 22;
  stroke-dashoffset: 22;
  animation: vlFig4Tick 13s ease-out infinite;
  animation-delay: calc(var(--i) * 1.55s);
}
.vl-fig4__token--run { fill: var(--gold); animation: vlFig4RunToken 13s cubic-bezier(0.3, 0.7, 0.4, 1) infinite; }

@keyframes vlFig4Gone {
  0%       { stroke-dashoffset: 250; }
  18%, 92% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 250; }
}
@keyframes vlFig4Stall {
  0%       { transform: translateX(0);     opacity: 0; }
  3%       { opacity: 0.55; }
  18%      { transform: translateX(250px); }
  /* and there it sits, for the rest of the cycle */
  22%, 92% { transform: translateX(250px); opacity: 0.55; }
  100%     { transform: translateX(250px); opacity: 0; }
}
@keyframes vlFig4Cross {
  0%, 20%  { opacity: 0; }
  26%, 92% { opacity: 0.9; }
  100%     { opacity: 0; }
}
@keyframes vlFig4Run {
  0%       { stroke-dashoffset: 880; }
  72%, 92% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 880; }
}
@keyframes vlFig4RunToken {
  0%       { transform: translateX(0);     opacity: 0; }
  3%       { opacity: 1; }
  18%      { transform: translateX(250px); }
  36%      { transform: translateX(460px); }
  54%      { transform: translateX(670px); }
  72%, 92% { transform: translateX(880px); opacity: 1; }
  100%     { transform: translateX(880px); opacity: 0; }
}
@keyframes vlFig4Tick {
  0%, 14%  { stroke-dashoffset: 22; }
  22%, 78% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 22; }
}

@media (max-width: 760px) {
  .vl-fig4__label { font-size: 20px; letter-spacing: 0.08em; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold the outcome: one lane stopped at the first gate, one arrived. */
  .vl-fig4 * { animation: none !important; }
  .vl-fig4__gone { stroke-dashoffset: 0; }
  .vl-fig4__run  { stroke-dashoffset: 0; }
  .vl-fig4__cross { opacity: 0.9; }
  .vl-fig4__tick { stroke-dashoffset: 0; }
  .vl-fig4__token--stall { transform: translateX(250px); }
  .vl-fig4__token--run   { transform: translateX(880px); }
}

/* ---------- 14. BACKDROP: THE SHEET THAT NEVER FILLS ------------- */

/*
 * Sits behind the "ordinary lead" column at half visibility. Drawn in
 * currentColor at low alpha so it reads as texture under the copy, not
 * as a picture competing with it — the words stay the subject.
 */
.vl-compare__col { position: relative; overflow: hidden; }
.vl-compare__col > *:not(.vl-sheet):not(.vl-booked) { position: relative; z-index: 1; }

.vl-sheet {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0.5;
  color: rgba(242, 237, 227, 0.28);
  pointer-events: none;
  mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
}
.vl-sheet svg { width: 100%; height: 100%; display: block; }

.vl-sheet__grid { stroke-width: 1; opacity: 0.45; }
.vl-sheet__filled { fill: currentColor; opacity: 0.55; }

/* The cells that should hold budget, relevance, intent: a dash, and
   nothing else. They flicker as the cursor passes and stay blank. */
.vl-sheet__blank {
  stroke-width: 2;
  stroke-linecap: round;
  opacity: 0.25;
  animation: vlSheetBlank 9s ease-in-out infinite;
  animation-delay: calc(var(--i) * 0.35s);
}

/* The cursor, sweeping rows and finding nothing to read. */
.vl-sheet__scan {
  fill: currentColor;
  opacity: 0.07;
  animation: vlSheetScan 9s cubic-bezier(0.5, 0, 0.2, 1) infinite;
}

@keyframes vlSheetBlank {
  0%, 100% { opacity: 0.18; }
  50%      { opacity: 0.42; }
}
@keyframes vlSheetScan {
  0%       { transform: translateY(0);     opacity: 0; }
  8%       { opacity: 0.09; }
  30%      { transform: translateY(48px); }
  55%      { transform: translateY(96px); }
  80%      { transform: translateY(144px); opacity: 0.09; }
  100%     { transform: translateY(144px); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .vl-sheet__blank, .vl-sheet__scan { animation: none; }
  .vl-sheet__scan { opacity: 0; }
}

/* ---------- 15. BACKDROP: THE CALENDAR THAT FILLS IN ------------- */

/*
 * The mirror of the empty sheet: same weight, same half visibility, but
 * gold rather than pale, because this column is the outcome rather than
 * the problem. Kept at the same opacity so neither side of the
 * comparison looks louder than the other.
 */
.vl-booked {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0.5;
  color: rgba(210, 172, 95, 0.34);
  pointer-events: none;
  mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
}
.vl-booked svg { width: 100%; height: 100%; display: block; }

.vl-booked__brief rect { fill: none; stroke: none; }
.vl-booked__line rect { fill: currentColor; opacity: 0.4; }
.vl-booked__brief > rect { stroke: currentColor; opacity: 0.5; }

/* Each line of the brief is answered in turn. */
.vl-booked__tick {
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  stroke-dasharray: 22;
  stroke-dashoffset: 22;
  animation: vlBookedTick 11s ease-out infinite;
  animation-delay: calc(var(--i) * 0.9s);
}

.vl-booked__cal { opacity: 0.5; fill: none; }
.vl-booked__cell { fill: currentColor; opacity: 0.12; }

/* The slot is taken, and then confirmed. */
.vl-booked__cell--slot {
  opacity: 0.12;
  animation: vlBookedSlot 11s ease-out infinite;
  animation-delay: 4.6s;
}
.vl-booked__confirm circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  opacity: 0;
  animation: vlBookedRing 11s ease-out infinite;
  animation-delay: 5.4s;
}
.vl-booked__check {
  stroke: currentColor;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  stroke-dasharray: 30;
  stroke-dashoffset: 30;
  animation: vlBookedCheck 11s ease-out infinite;
  animation-delay: 5.8s;
}

@keyframes vlBookedTick {
  0%, 4%   { stroke-dashoffset: 22; }
  12%, 88% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 22; }
}
@keyframes vlBookedSlot {
  0%, 2%   { opacity: 0.12; }
  10%, 88% { opacity: 0.85; }
  100%     { opacity: 0.12; }
}
@keyframes vlBookedRing {
  0%, 2%   { opacity: 0; }
  10%, 88% { opacity: 0.85; }
  100%     { opacity: 0; }
}
@keyframes vlBookedCheck {
  0%, 2%   { stroke-dashoffset: 30; }
  12%, 88% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 30; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold the booked state: brief answered, slot taken, confirmed. */
  .vl-booked__tick, .vl-booked__cell--slot,
  .vl-booked__confirm circle, .vl-booked__check { animation: none; }
  .vl-booked__tick, .vl-booked__check { stroke-dashoffset: 0; }
  .vl-booked__cell--slot { opacity: 0.85; }
  .vl-booked__confirm circle { opacity: 0.85; }
}

/* ---------- 16. BACKDROPS: THE SIX READINESS INPUTS -------------- */

/*
 * One drawing per card, sitting behind the heading at 80% visibility.
 * The strokes themselves are low-alpha, so 80% of a quiet drawing still
 * reads as backdrop rather than as a picture with a heading on top.
 */
.vl-readiness-grid article { position: relative; overflow: hidden; }
.vl-readiness-grid article > *:not(.vl-ready-art) { position: relative; z-index: 1; }

.vl-ready-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 1;
  /* Full-strength gold. These sit below the heading rather than behind
     it, so nothing is competing with type and the linework can read at
     its own weight. */
  color: var(--gold);
  pointer-events: none;
  display: grid;
  place-items: center;
}
.vl-ready-art svg { width: min(78%, 15rem); height: auto; display: block; }

/* Shared vocabulary across the six. */
.vl-ready-art .ra-frame { fill: none; stroke: currentColor; stroke-width: 2; }
.vl-ready-art .ra-line,
.vl-ready-art .ra-item,
.vl-ready-art .ra-slot { fill: currentColor; }
.vl-ready-art .ra-pip  { fill: currentColor; }
.vl-ready-art .ra-flap { stroke: currentColor; stroke-width: 2; }
.vl-ready-art .ra-marks path { stroke: currentColor; stroke-width: 2; stroke-linecap: round; opacity: .85; }

/* 01 — the brief writes itself, line by line. */
.ra-line { opacity: 0; animation: raLine 9s ease-out infinite; animation-delay: calc(var(--i) * .5s); }
@keyframes raLine {
  0%, 4%   { opacity: 0; transform: translateX(-6px); }
  14%, 86% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: translateX(-6px); }
}

/* 02 — the documents are stamped. */
.ra-doc { opacity: .8; animation: raDoc 9s ease-out infinite; animation-delay: calc(var(--i) * .4s); }
.ra-seal { fill: none; stroke: currentColor; stroke-width: 2; opacity: 0; animation: raSeal 9s ease-out infinite; animation-delay: 1.1s; }
.ra-seal-check {
  fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 30; stroke-dashoffset: 30; animation: raCheck 9s ease-out infinite; animation-delay: 1.4s;
}
@keyframes raDoc  { 0%,6%{opacity:.35} 18%,86%{opacity:1} 100%{opacity:.35} }
@keyframes raSeal { 0%,10%{opacity:0;transform:scale(.9)} 22%,86%{opacity:1;transform:none} 100%{opacity:0;transform:scale(.9)} }
@keyframes raCheck{ 0%,12%{stroke-dashoffset:30} 24%,86%{stroke-dashoffset:0} 100%{stroke-dashoffset:30} }
.vl-ready-art--1 svg { overflow: visible; }
.ra-seal, .ra-doc { transform-box: fill-box; transform-origin: center; }

/* 03 — the presenter answers, one question then the next. */
.ra-head, .ra-body { fill: currentColor; opacity: .95; }
.ra-bubble { opacity: 0; animation: raBubble 9s ease-out infinite; animation-delay: calc(var(--i) * .9s); transform-box: fill-box; transform-origin: left center; }
@keyframes raBubble {
  0%, 6%   { opacity: 0; transform: scale(.92); }
  18%, 84% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: scale(.92); }
}

/* 04 — the kit is packed. */
.ra-item { opacity: 0; animation: raItem 9s cubic-bezier(.3,.7,.4,1) infinite; animation-delay: calc(var(--i) * .45s); }
@keyframes raItem {
  0%, 4%   { opacity: 0; transform: translateY(-14px); }
  16%, 86% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: translateY(-14px); }
}

/* 05 — the reply lands well inside the day. */
.ra-dial { fill: none; stroke: currentColor; stroke-width: 2; opacity: .9; }
.ra-sweep {
  stroke: currentColor; stroke-width: 3; stroke-linecap: round;
  transform-box: fill-box; transform-origin: 50% 100%;
  animation: raSweep 9s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes raSweep {
  0%       { transform: rotate(0deg); }
  30%, 86% { transform: rotate(96deg); }
  100%     { transform: rotate(0deg); }
}

/* 06 — the slot is kept: on time, and about the buyer. */
.ra-slot { opacity: .4; animation: raSlot 9s ease-out infinite; animation-delay: .6s; }
.ra-ring {
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-dasharray: 190; stroke-dashoffset: 190;
  animation: raRing 9s ease-out infinite; animation-delay: .9s;
}
@keyframes raSlot { 0%,6%{opacity:.35} 18%,86%{opacity:1} 100%{opacity:.35} }
@keyframes raRing { 0%,8%{stroke-dashoffset:190} 30%,86%{stroke-dashoffset:0} 100%{stroke-dashoffset:190} }

@media (prefers-reduced-motion: reduce) {
  /* Hold each drawing finished. */
  .vl-ready-art * { animation: none !important; }
  .ra-line, .ra-item, .ra-bubble { opacity: 1; transform: none; }
  .ra-doc { opacity: 1; }
  .ra-seal { opacity: 1; }
  .ra-seal-check, .ra-ring { stroke-dashoffset: 0; }
  .ra-slot { opacity: 1; }
  .ra-sweep { transform: rotate(96deg); }
}


/* ---- Readiness cards: heading at the top, artwork below ---------- */

/*
 * The number and heading sit together at the top of the card, and the
 * drawing takes the space underneath at full strength. Previously the
 * heading sat a third of the way down with the drawing behind it, which
 * left the top of each card empty and the two competing for the middle.
 */
.vl-readiness-grid article {
  display: grid;
  align-content: start;
  gap: 0.6rem;
}
.vl-readiness-grid h3 { margin-top: 0; }

.vl-ready-art {
  /* Clear of the heading block, then centred in what is left. */
  inset: 6.5rem 0 0;
  opacity: 1;
}
.vl-ready-art svg { width: min(86%, 15rem); }

@media (max-width: 719px) {
  .vl-ready-art { inset: 5.5rem 0 0; }
}

/* ---------- 17. FIGURE: TWO OWNERS, ONE LINE -------------------- */

/*
 * A 12s loop under the ownership columns. The two sides fill in step
 * with each other, so neither reads as the senior partner, and the
 * divider is the only element that never changes — the point being that
 * work lands on one side or the other and nothing is shared.
 */
.vl-fig5-wrap { margin-top: clamp(2.5rem, 5vw, 4rem); }
.vl-fig5 { width: 100%; height: auto; display: block; overflow: visible; }

.vl-fig5__label {
  font: 500 12px/1 var(--font-mono);
  letter-spacing: 0.16em;
  fill: var(--muted-dim);
}
.vl-fig5__label--dev { fill: var(--muted-dim); }

.vl-fig5__divide {
  stroke: rgba(255, 255, 255, 0.16);
  stroke-width: 1.5;
  stroke-dasharray: 6 8;
}
.vl-fig5__node {
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.6;
  opacity: 0.5;
  transform-box: fill-box;
  transform-origin: center;
  animation: vlFig5Node 12s ease-in-out infinite;
}

.vl-fig5__task rect:first-of-type { fill: none; stroke: rgba(255, 255, 255, 0.12); stroke-width: 1.2; }
.vl-fig5__task circle { fill: currentColor; }
.vl-fig5__bar { fill: currentColor; opacity: 0.5; }

.vl-fig5__task--left  { color: var(--gold); }
.vl-fig5__task--right { color: var(--cyan); }

/*
 * Each task slides in from its own outer edge. Left arrives from the
 * left, right from the right: nothing is ever seen crossing the middle.
 */
.vl-fig5__task--left {
  opacity: 0;
  animation: vlFig5Left 12s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
  animation-delay: calc(var(--i) * 0.55s);
}
.vl-fig5__task--right {
  opacity: 0;
  animation: vlFig5Right 12s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
  animation-delay: calc(var(--i) * 0.55s + 0.28s);
}

@keyframes vlFig5Left {
  0%, 3%   { opacity: 0; transform: translateX(-46px); }
  16%, 92% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: translateX(-46px); }
}
@keyframes vlFig5Right {
  0%, 3%   { opacity: 0; transform: translateX(46px); }
  16%, 92% { opacity: 1; transform: none; }
  100%     { opacity: 0; transform: translateX(46px); }
}
@keyframes vlFig5Node {
  0%, 100% { opacity: 0.35; transform: scale(0.94); }
  50%      { opacity: 0.7;  transform: scale(1.06); }
}

@media (max-width: 760px) {
  .vl-fig5__label { font-size: 20px; letter-spacing: 0.08em; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold both sides filled, the line intact. */
  .vl-fig5 * { animation: none !important; }
  .vl-fig5__task { opacity: 1; transform: none; }
  .vl-fig5__node { opacity: 0.5; }
}

/* ---------- 19. FIGURE: MEASURES, NOT NOISE --------------------- */

/*
 * A 13s loop under the success-framework cards. The noise side never
 * resolves — that is the point of it — while the three measures fill in
 * turn and hold, so a glance at any moment shows how far the reading
 * has got rather than a single blinking element.
 */
.vl-fig6-wrap { margin-top: clamp(2.5rem, 5vw, 4rem); }
.vl-fig6 { width: 100%; height: auto; display: block; overflow: visible; }

.vl-fig6__label {
  font: 500 12px/1 var(--font-mono);
  letter-spacing: 0.16em;
  fill: var(--muted-dim);
}
.vl-fig6__label--gold { fill: var(--gold); opacity: 0.9; }
.vl-fig6__cap {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: 0.12em;
  fill: var(--muted-dim);
}

/* Noise: it moves, it never adds up. */
.vl-fig6__noise { color: rgba(242, 237, 227, 0.4); }
.vl-fig6__dot {
  animation: vlFig6Dot 4.5s ease-in-out infinite;
  animation-delay: calc(var(--i) * 0.21s);
  transform-box: fill-box;
  transform-origin: center;
}
.vl-fig6__flat { stroke: rgba(242, 237, 227, 0.18); stroke-width: 2; }

.vl-fig6__divide { stroke: rgba(255, 255, 255, 0.12); stroke-width: 1.5; stroke-dasharray: 6 8; }
.vl-fig6__base   { stroke: rgba(210, 172, 95, 0.5); stroke-width: 2; }

/* Measures: each fills from its own base and stays. */
.vl-fig6__well { fill: rgba(255, 255, 255, 0.04); }
.vl-fig6__fill {
  fill: var(--gold);
  opacity: 0.85;
  transform-box: fill-box;
  transform-origin: center bottom;
  transform: scaleY(0);
  animation: vlFig6Fill 13s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
  animation-delay: calc(var(--i) * 1.5s);
}
.vl-fig6__tick {
  stroke: var(--gold);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  stroke-dasharray: 34;
  stroke-dashoffset: 34;
  animation: vlFig6Tick 13s ease-out infinite;
  animation-delay: calc(0.5s + var(--i) * 1.5s);
}
.vl-fig6__measure .vl-fig6__cap { animation: vlFig6Cap 13s ease-out infinite; animation-delay: calc(var(--i) * 1.5s); }

@keyframes vlFig6Dot {
  0%, 100% { opacity: 0.28; transform: translateY(0); }
  50%      { opacity: 0.6;  transform: translateY(-4px); }
}
@keyframes vlFig6Fill {
  0%, 4%   { transform: scaleY(0); }
  22%, 92% { transform: scaleY(1); }
  100%     { transform: scaleY(0); }
}
@keyframes vlFig6Tick {
  0%, 8%   { stroke-dashoffset: 34; }
  24%, 92% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: 34; }
}
@keyframes vlFig6Cap {
  0%, 8%   { fill: var(--muted-dim); }
  24%, 92% { fill: var(--fg); }
  100%     { fill: var(--muted-dim); }
}

@media (max-width: 760px) {
  .vl-fig6__label { font-size: 20px; }
  .vl-fig6__cap { font-size: 17px; letter-spacing: 0.06em; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold the reading: all three measures full, the noise still noise. */
  .vl-fig6 * { animation: none !important; }
  .vl-fig6__fill { transform: scaleY(1); }
  .vl-fig6__tick { stroke-dashoffset: 0; }
  .vl-fig6__cap  { fill: var(--fg); }
}
