/* ==========================================================================
   hero.module

   An asymmetric named-line grid: the copy sits inside the container column and
   the media bleeds to the viewport edge.

   Surface and vertical padding come from base.css's .vz-section--bg-* /
   --pad-* classes; the eyebrow is .vz-kicker, the headline .vz-display, the
   subhead .vz-lead and both actions .vz-btn. This file owns composition only.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. The grid

   Five named lines reproduce .vz-container's geometry as grid tracks, so a
   child can run to the viewport edge without nesting a full-bleed hack inside
   a centred box.

   The content tracks total (--vz-maxw - 2 * --vz-gutter), not --vz-maxw:
   .vz-container is border-box, so its *content* is maxw minus two gutters, and
   tracks totalling maxw start the headline one gutter left of every container
   section below it. Below that width the outer minmax() floors take over and
   both sides fall back to one gutter, exactly as .vz-container does, so the two
   agree at every viewport.

   THE 46/54 SPLIT IS A PERCENTAGE OF THE GRID, NOT A LENGTH. Grid distributes
   free space to length tracks equally until each hits its growth limit, so
   534px and 626px tracks in 944px of space both come out at 472 — equal halves
   at exactly the desktop widths most visitors are on. A percentage track
   resolves against the grid's inline size and stays proportional all the way up.

   `100%` and not `100vw`: the section's box excludes the scrollbar and the
   viewport unit does not, and the outer tracks are already at their gutter
   floor with nowhere to absorb the 15px difference.
   -------------------------------------------------------------------------- */
.vz-hero {
  --vz-hero-content: min(
    calc(100% - 2 * var(--vz-gutter)),
    calc(var(--vz-maxw) - 2 * var(--vz-gutter))
  );
  --vz-hero-copy-w:  calc(var(--vz-hero-content) * 0.46);
  --vz-hero-media-w: calc(var(--vz-hero-content) * 0.54);
  display: grid;
  row-gap: var(--vz-pad-s);
  grid-template-columns:
    [full-start] minmax(var(--vz-gutter), 1fr)
    [content-start] minmax(0, var(--vz-hero-copy-w))
    [mid] minmax(0, var(--vz-hero-media-w))
    [content-end] minmax(var(--vz-gutter), 1fr)
    [full-end];
  /* Containing block for the two light layers below, and the clip that keeps
     them in the band: each is 6% oversized at the far end of its drift. */
  overflow: clip;
  isolation: isolate;
}

/* The section's own vertical padding, re-declared as a custom property so the
   media can cancel exactly as much of it as it needs to sit flush on the bottom
   seam. Three mappings rather than overriding .vz-section--pad-*, so the
   editor's choice stays authoritative. */
.vz-hero.vz-section--pad-s { --vz-hero-pad-block: var(--vz-pad-s); }
.vz-hero.vz-section--pad-m { --vz-hero-pad-block: var(--vz-pad-m); }
.vz-hero.vz-section--pad-l { --vz-hero-pad-block: var(--vz-pad-l); }

/* Nothing to show at all — every field blank, or a rejected fields.json, which
   leaves every module.* lookup empty. Without this the hero still paints its
   surface and padding as a band of colour containing nothing. :empty cannot be
   used, because the HubL branches leave whitespace behind. */
.vz-hero:not(:has(.vz-hero__copy > *)):not(:has(.vz-hero__media, .vz-hero__bg, .vz-hero__form)) { display: none; }

/* --------------------------------------------------------------------------
   1a. The lit surface — .vz-section--bg-wash, and the two light layers

   The one surface this module owns, and the reason it is not in base.css with
   the four flat backgrounds: it is not a colour, it is a fold. Three
   brand-coloured glows placed for this composition, drifting on two slow loops,
   under a veil that hands the top of the band to the chrome. Only this module's
   fields.json offers the value and only this file knows how to paint it.

   Blue upper-left behind the headline, coral on the seam where the copy meets
   the photograph, lime low under the actions — the brand's own three colours
   over the cream surface it already ships. No hue is introduced that the theme
   did not already have.

   The lights are spread across the full width rather than banked into the copy
   column, because the photograph is held off the section's top and bottom edges
   and the surface shows in the strips over and under it. The picture then
   floats on the light instead of ending it. Below 64em the same three positions
   land in one column, with the lime moved up under the actions.

   The background-image sits on a pseudo-element because the section's
   background-color belongs to base.css on five of the six surfaces, and
   because the load fade and the reduced-motion branch need a layer to switch.

   LILAC IS NOT LIT, and that is a decision rather than an omission. The two
   :is() lists below name wash, brand and ink and are deliberately left at
   three: the lights are three brand hues laid over a NEUTRAL surface, and
   --vz-atm-lime is 1.56:1 on lilac while --vz-atm-coral is 1.34:1 — two of the
   three would be invisible and the third would tint a band that already has a
   hue of its own. Lilac behaves like White and Cream here: a flat colour with
   no ::before, no ::after and no veil.
   -------------------------------------------------------------------------- */
.vz-hero.vz-section--bg-wash {
  background-color: var(--vz-color-surface);
  color: var(--vz-color-ink);
}

/* Two layers, not one, and the reason is the motion: lights that drift together
   read as one texture sliding around. Split across the two pseudo-elements —
   blue and lime on ::before, coral on ::after — they pass each other instead,
   and the colour where they overlap changes as they go.

   Both are absolutely positioned so neither is a grid item; a pseudo-element on
   a grid container otherwise takes a track of its own.

   THE VEIL RIDES ON THE FIRST LAYER. A background-image list paints its first
   layer on top, so the fade over the top quarter is declared first and sits
   above the glows, and the colour begins under the bar rather than at the top
   of the screen. --vz-atm-veil rather than white, so the dark surfaces fade
   from their own colour. It is on the still layer deliberately — on a drifting
   one the blend under the bar would slide with it. */
.vz-hero:is(.vz-section--bg-wash, .vz-section--bg-brand, .vz-section--bg-ink)::before,
.vz-hero:is(.vz-section--bg-wash, .vz-section--bg-brand, .vz-section--bg-ink)::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.vz-hero:is(.vz-section--bg-wash, .vz-section--bg-brand, .vz-section--bg-ink)::before {
  background-image:
    linear-gradient(to bottom, var(--vz-atm-veil) 0%, transparent 25%),
    radial-gradient(58rem 46rem at -6% 6%, var(--vz-atm-blue), transparent 72%),
    radial-gradient(52rem 40rem at var(--vz-atm-lime-x, 18%) var(--vz-atm-lime-y, 66%), var(--vz-atm-lime), transparent 68%);
}

.vz-hero:is(.vz-section--bg-wash, .vz-section--bg-brand, .vz-section--bg-ink)::after {
  background-image:
    radial-gradient(48rem 42rem at 68% -14%, var(--vz-atm-coral), transparent 70%);
}

/* The dark surfaces re-declare the three tokens and inherit the placement. Deep
   navy takes light differently from cream: the blue all but vanishes over a
   surface already that hue, while coral and lime read twice as strong, so the
   balance inverts rather than scales. */
.vz-hero:is(.vz-section--bg-brand, .vz-section--bg-ink) {
  --vz-atm-blue:  color-mix(in oklab, var(--vz-color-primary) 20%, transparent);
  --vz-atm-coral: color-mix(in oklab, var(--vz-color-accent-warm) 9%, transparent);
  --vz-atm-lime:  color-mix(in oklab, var(--vz-color-accent) 8%, transparent);
  --vz-atm-veil:  color-mix(in oklab, var(--vz-color-ink-deep) 55%, transparent);
}

/* Brand blue takes less light than the navy, and it is contrast arithmetic
   rather than taste: #2844a6 is mid-luminance, so the lime eyebrow and ghost
   link clear 4.5:1 on it with little to spare (6.6:1) and every point of
   luminance the glows add comes off that margin. The navy starts at 13.6:1 and
   can afford four times as much light. */
.vz-hero.vz-section--bg-brand {
  --vz-atm-blue:  color-mix(in oklab, var(--vz-color-primary) 11%, transparent);
  --vz-atm-coral: color-mix(in oklab, var(--vz-color-accent-warm) 7%, transparent);
  --vz-atm-lime:  color-mix(in oklab, var(--vz-color-accent) 6%, transparent);
  --vz-atm-veil:  color-mix(in oklab, var(--vz-color-primary-deep) 55%, transparent);
}

/* The light goes out where a photograph takes the surface. `wash` stays a legal
   value under layout="centered_bg", but three brand glows painted over a
   photograph is a colour cast on someone's picture, and it lands between the
   image and the type whose contrast this composition guarantees.

   Removed on [data-vz-surface-dark] rather than on the layout class, because
   that attribute means "a photograph is actually here" — a centered_bg hero
   with no media picked is an ordinary coloured band and keeps its light.

   `content: none` rather than `display: none`, so the pseudo-elements are never
   generated: a suppressed layer that still exists is a box the compositor
   carries, and the measured cost of an idle animated layer is high. */
.vz-hero[data-vz-surface-dark]::before,
.vz-hero[data-vz-surface-dark]::after { content: none; }

/* Content rides above both layers. */
.vz-hero__copy,
.vz-hero__media,
.vz-hero__form {
  position: relative;
  z-index: 1;
}

/* --------------------------------------------------------------------------
   1b. Under the chrome

   The header's bar is translucent and blurred because the page scrolls under
   it — but at the top of the document it was glass over the flow space the
   spacer reserves, which is the body background. On a lit hero that is a solid
   white band above the design, with the fold's light stopping a bar's height
   short of the top of the screen.

   So the first section starts at the top of the viewport: it pulls itself up by
   the bar's height and pads its content back down by the same amount, leaving
   every line of copy where it was with the surface behind the glass.

   Three things this deliberately is not:

   Not a change to the spacer, which also reserves the announcement bar —
   opaque content that scrolls away, and a hero starting behind that would have
   its eyebrow hidden until the visitor scrolled.

   Not unconditional. --vz-chrome-h is declared in header.module's CSS and
   nowhere else, so a page without the chrome resolves it to 0px and this rule
   does nothing.

   Not every hero. `:first-child` is the gate: pulling up a hero with something
   above it would slide it over that section's last line. If the selector stops
   matching, the hero sits below the chrome as it always did — the failure mode
   is the old design, not a broken one.
   -------------------------------------------------------------------------- */
.dnd-section:first-child .vz-hero {
  margin-block-start: calc(-1 * var(--vz-chrome-h, 0px));
  /* Not chrome + a full section padding: the bar's height is already open
     space, so adding the section's full padding under it stacks two kinds of
     air. Half of it puts the copy where the eye expects the fold to start.

     The floor is not arbitrary. The frosted bar does not end on a line — its
     blur hangs --vz-glass-tail (1.75rem) below the bar and dissolves there —
     and half the compact padding on a phone would land the eyebrow 4px past the
     end of that dissolve, reading as type caught in the blur. */
  padding-block-start: calc(
    var(--vz-chrome-h, 0px) + max(var(--vz-space-xl), var(--vz-hero-pad-block, 0px) * 0.5)
  );
}

/* --------------------------------------------------------------------------
   2. Copy column
   -------------------------------------------------------------------------- */
.vz-hero__copy {
  grid-column: content-start / content-end;
  /* A grid item's default min-width is min-content, so a track capped at
     minmax(0, copy width) is still pushed wider by an unbreakable word. This
     half bounds the box; the rule below bounds the text. */
  min-inline-size: 0;
}

/* A 29-character unbroken headline widened the page by 286px at 360 and 41px at
   834 before this rule — a document-wide horizontal scrollbar, from one field
   an editor is free to type into. */
.vz-hero .vz-hero__eyebrow,
.vz-hero .vz-hero__heading,
.vz-hero .vz-hero__subtext { overflow-wrap: break-word; }

/* With no image the copy would run the full 1160px content width at a 90px
   display size. Capping it keeps the hero asymmetric — deliberate whitespace
   rather than a missing column. */
.vz-hero:not(:has(.vz-hero__media)) .vz-hero__copy { max-inline-size: 42rem; }

/* Declared under .vz-hero (0,2,0) rather than on the bare modifier:
   .vz-display sets `margin: 0` at (0,1,0) and theme-overrides.css sets margins
   on bare `p`. The extra class settles both whatever the stylesheet order. */
.vz-hero .vz-hero__eyebrow { margin-block: 0 var(--vz-space-m); }
.vz-hero .vz-hero__heading { margin-block: 0 var(--vz-space-m); }
.vz-hero .vz-hero__subtext { margin-block: 0 var(--vz-space-l); }

/* The h1 gets the step above the scale's top. .vz-display is step 4, which is
   also where every h2 tops out, so the fold's headline and a mid-page section
   heading were within a few pixels of each other. Step 5 is 1.24x step 4 at the
   cap. Tracking tightens by a hundredth as the size grows, stopping at -0.03em,
   off the -0.04em floor where letters start to touch. */
.vz-hero .vz-hero__heading {
  font-size: var(--vz-step-5);
  letter-spacing: -0.03em;
}

/* Two smaller h1s, for the headline that isn't three words: at 90px a long one
   fills the copy column with four lines and pushes the actions off the fold.

     l  step 5              44 -> 90px   the rule above; the designed size
     m  half a step down    40 -> 81px   the clamp below
     s  step 4              40 -> 72px   .vz-display's own size

   Half a step is exact — the scale's ratio is 1.24, so 1.11 is its square root.
   `s` needs no new number at all: it is the display step, i.e. the h1 with this
   module's own opinion removed. All three still outrank every section heading
   (every .vz-h2 caps at 44px, so even `s` is 1.64x it), which is the constraint
   on how far the ramp can go.

   `m` holds step 5's growth window so it tracks the default rather than
   crossing it. The three converge on a phone, which is correct rather than a
   clamp to fix — at 360 the whole ramp spans 5.7px, because a 360px column has
   no room to express the difference. The control earns its keep from ~768 up.

   Both rules are (0,2,0), identical to the rule they override, so they win on
   source order and must stay below it. */
.vz-hero .vz-hero__heading--m {
  font-size: clamp(2.5rem, 1.74rem + 4.10vw, 5.0625rem);
  letter-spacing: -0.025em;
}
.vz-hero .vz-hero__heading--s {
  font-size: var(--vz-step-4);
  letter-spacing: var(--vz-tracking-display);
}

/* The subhead is two points larger than .vz-lead, written as an addition so the
   token stays the source of the size — a second hand-written clamp here would
   stop tracking the scale the next time it moves. calc() applies to the
   resolved value, so both ends travel together: 21 -> 23.67px at the floor,
   24 -> 26.67px at the cap.

   It stays an addition rather than becoming step 2, which is 26–32px: at wide
   widths the two differ by over 5px and at narrow ones step 2 overtakes this.
   Step 2 would also make the hero's subhead a heading-scale size while every
   other lead on the site stays at step 1.

   The ceiling is the headline, not the scale: the ratio is 3.37x at 1440 and
   1.97x at 360, and the phone end is the tight one to re-measure before this
   is nudged again.

   `balance`, not `pretty`: `pretty` only rescues the last line from an orphan,
   where a lead under a word-by-word headline needs every line evened. Both are
   hints, so a browser without support falls back to normal wrapping. */
/* 18px, AND IT IS DECLARED TWICE ON PURPOSE — base.css §4a's two spellings,
   for §4a's reason, applied to the one module §4a deliberately exempts.

   THE SIZE ABOVE NEVER REACHED THE PAGE. This rule read
   `calc(var(--vz-step-1) + 2pt)` and computed to 26.67px on the wrapper, and
   the editor's own paragraph inside it rendered at 16px in every one of the six
   layouts — measured, not reasoned about. `p { font-size: 1rem }` in
   theme-overrides is an ELEMENT rule at (0,0,1) and beats anything a richtext
   <p> INHERITS from its wrapper. base.css §4a fixes exactly this for every
   other module's intro and lists hero's subhead as EXEMPT ON PURPOSE, so
   nothing ever gave this one a descendant-level rule and the whole argument
   above — the +2pt addition, the 3.37x ratio against the headline, why it is
   not step 2 — has been describing a size no reader has seen.

   That argument is kept above rather than deleted because it is the record of
   what was intended, and it is worth knowing the intent was ~24-26px if this
   number is ever revisited. What ships now is a flat 18px, Ariel's call: up
   from the 16px that was actually rendering, down from the 26.67px that was
   not. One number, and it lands.

   The descendant list is (0,2,1) and the wrapper (0,2,0); both beat the element
   rule whatever order the portal emits the stylesheets in. Only font-size is
   taken here — the colour, the measure and the balance all still apply. */
.vz-hero .vz-hero__subtext,
.vz-hero .vz-hero__subtext :is(p, ul, ol, li) {
  font-size: 1.125rem;
}

.vz-hero .vz-hero__subtext {
  text-wrap: balance;
}

/* The subhead steps back so the headline can step forward. Light surfaces only:
   on brand and ink the section is already --vz-color-on-dark and a muted ink
   would be unreadable.

   A hero-local mix rather than --vz-color-ink-muted, which is ink at 72% —
   6.23:1 on flat cream, but 3.88:1 against the lit surface's composited worst
   case, under the 4.5:1 floor. At 84% it reads 9.1:1 flat and 5.66:1 lit.

   Lilac joins the list and the mix is unchanged: 84% ink toward WHITE is
   #3f4347, which measures 4.97:1 on lilac. That clears the floor with less
   room than the other three have, and it is why the value is not re-mixed
   toward the band — a hero-local mix against --vz-color-lilac would drift from
   the one number this rule is allowed to have. Note this is a hero-local
   colour and NOT --vz-color-ink-muted, so base.css §3's re-route of that token
   does not reach it; the 4.97 above is the real figure. */
.vz-hero:is(.vz-section--bg-white, .vz-section--bg-surface, .vz-section--bg-wash, .vz-section--bg-lilac) .vz-hero__subtext {
  color: color-mix(in oklab, var(--vz-color-ink) 84%, var(--vz-color-white));
}

/* subtext is richtext, so the editor's own blocks come through. Collapse the
   outer edges only; interior rhythm stays theirs. */
.vz-hero .vz-hero__subtext > :first-child { margin-block-start: 0; }
.vz-hero .vz-hero__subtext > :last-child  { margin-block-end: 0; }

.vz-hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--vz-space-m) var(--vz-space-l);
}

/* --------------------------------------------------------------------------
   3. Media column

   On a phone the image runs edge to edge and finishes flush against whatever
   section follows, which is what makes the stats bar read as continuous with
   the hero rather than as a box under it.

   It gets there with a negative bottom margin equal to the section's own
   padding, not by zeroing that padding: a grid item's negative margin shortens
   its row while the box still paints, so the image's bottom edge lands on the
   section's whichever padding the editor picked.

   overflow: clip is load-bearing, not tidiness — the reveal below scales the
   image to 1.03, which on a full-bleed element is 1.5% of the viewport past
   each edge and a horizontal scrollbar on every page load.
   -------------------------------------------------------------------------- */
.vz-hero__media {
  grid-column: full-start / full-end;
  overflow: clip;
  margin-block-end: calc(-1 * var(--vz-hero-pad-block, 0px));
}

/* The frame is where the aberration's blend group ends, and it is why the clip
   and the radius stay on .vz-hero__media rather than moving here. A blended
   layer is alpha-composited, so where source and backdrop are both partially
   transparent — every antialiased pixel of a rounded corner — a dark multiplied
   copy leaves a rim along the curve. Isolating on a square, unclipped element
   keeps the blend opaque, and the parent's rounded clip then cuts an
   already-opaque result: one antialiased edge instead of two.

   It must not gain an overflow of its own: the drift runs the image 5% larger
   than this box, which is what covers the frame at both ends of the travel. */
.vz-hero__frame {
  position: relative;
  isolation: isolate;
}

.vz-hero .vz-hero__media img {
  display: block;
  inline-size: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

/* --------------------------------------------------------------------------
   4. 64em: the copy field beside a full-height media slab

   The separation between copy and media is padding on the copy column, not
   column-gap on the grid: a gap goes between every pair of the four tracks and
   comes out of the 1fr outer tracks first, pulling the content tracks off
   .vz-container's alignment by tens of pixels. Padding on the item leaves the
   track maths untouched.

   64em rather than 62em: the copy column is 46% of the content width, and below
   1024px a display headline in ~430px beside a portrait image leaves the
   subhead at about 30 characters a line.
   -------------------------------------------------------------------------- */
@media (min-width: 64em) {
  .vz-hero {
    align-items: center;
    row-gap: 0;
  }

  /* The lime moves with the layout: under the actions on a phone, where the
     copy ends two thirds down, and below the picture across two columns, where
     it lifts the strip above the next section's seam. */
  .vz-hero {
    --vz-atm-lime-x: 34%;
    --vz-atm-lime-y: 112%;
  }

  .vz-hero:has(.vz-hero__media) .vz-hero__copy {
    grid-column: content-start / mid;
    padding-inline-end: clamp(var(--vz-space-l), 5vw, var(--vz-space-3xl));
  }

  .vz-hero__media {
    grid-column: mid / full-end;
    margin-block-end: 0;
  }

  /* Radius on the leading corners only: the trailing edge is the viewport.

     It sits on the clip box, not on the image. `overflow: clip` clips to the
     rounded shape, so the corners hold whatever the image is doing — and the
     drift keeps the image permanently 5% larger than this box, which put its
     own corners outside the clip and squared them off. */
  .vz-hero__media {
    border-start-start-radius: var(--vz-radius);
    border-end-start-radius: var(--vz-radius);
  }

  /* Portrait, capped in height so the hero stays one screenful. The cap wins
     over the ratio above ~1024px, so the crop widens gradually rather than the
     image growing to 900px tall at 1440; object-fit: cover absorbs it. */
  .vz-hero .vz-hero__media img {
    aspect-ratio: 4 / 5;
    max-block-size: 40rem;
  }
}

/* --------------------------------------------------------------------------
   4a. layout="split" — an even 50/50, and the picture stays on the page

   The ratio alone would not be a second layout: 46/54 against 50/50 is eight
   percent of a column, and an editor picking between two dropdown entries could
   not tell which they had chosen.

   What makes them two compositions is where the picture ends. media_right's
   photograph runs past the container and off the right edge of the screen, so
   the fold has no right-hand boundary — which is why this is a named-line grid
   rather than a container with two columns. `split` holds the picture inside
   the page: both columns end on the container's line and the media carries
   --vz-radius on all four corners.

   The ratio is still a percentage of the grid, for the reason above: written as
   lengths, `split` and media_right would be the same layout at every desktop
   width under 1240px.

   Below 64em the picture is a full-width block inside the container rather than
   a bleeding one, so it also loses the negative bottom margin — that exists to
   land a bleeding image on the section's seam, and pulling a contained, rounded
   picture over the seam would hang it off the band it belongs to.
   -------------------------------------------------------------------------- */
.vz-hero--split {
  --vz-hero-copy-w:  calc(var(--vz-hero-content) * 0.5);
  --vz-hero-media-w: calc(var(--vz-hero-content) * 0.5);
}

.vz-hero--split .vz-hero__media {
  grid-column: content-start / content-end;
  border-radius: var(--vz-radius);
  margin-block-end: 0;
}

@media (min-width: 64em) {
  /* mid / content-end, not mid / full-end — the one line of difference from the
     media_right layout, and the one a visitor can see. */
  .vz-hero--split .vz-hero__media { grid-column: mid / content-end; }
}

/* --------------------------------------------------------------------------
   4b. layout="centered_bg" — the only place in the library where type sits on
       an arbitrary photograph

   Which means contrast cannot be asserted from tokens. Everywhere else the
   background is a known value and the ratio is computed; here it is whatever an
   editor uploaded, and the floor has to hold against a picture nobody has seen.
   The scrim is what makes that possible, and its strength is measured against
   the LIGHTEST region of the shipped default image rather than its average —
   the average is the number that reads fine and ships a headline nobody can
   read over the one bright patch of wall behind it.

   62% is what the rendered page needs, and it is higher than the source file
   predicts. Sampling the default image says 58% (worst tile composited to
   luminance 0.166, white type at 4.85:1); screenshotting the real page and
   reading the pixels behind the subhead says 58% delivers 4.44:1, under the
   floor at every width. object-fit crops the source, the browser resamples it,
   and the JPEG's chroma reconstruction is its own. The binding floor is the
   subhead's 4.5:1, not the h1's 3:1, since it takes the same scrim at a
   smaller size.

   The gradient is not decoration either: 78% at the top edge, easing to the 62%
   floor across the middle third where the copy sits, closing at 76%. The top is
   what the frosted bar is glass over, and the bottom is the seam with whatever
   section follows — a hard edge between a photograph and a flat colour unless
   the picture is already on its way down. Nowhere is it weaker than the floor.

   overlay="none" is an escape hatch and its help text says so: for artwork
   already dark across the whole frame, with the editor owning the check.
   -------------------------------------------------------------------------- */
/* Two deliberate absences here.

   `position: relative` goes on the ATTRIBUTE, not on the layout class and not
   unconditionally. `overflow: clip` does not create a containing block, so
   without this the picture sizes itself against the initial containing block —
   the whole document — and crops a 6000px-tall box. Scoping to
   [data-vz-surface-dark] lands it on exactly the heroes whose glow layers have
   already been removed, so it cannot re-anchor them.

   And .vz-hero__bg carries no grid-column and no grid-row. An absolutely
   positioned child of a grid container is positioned against its grid AREA when
   one is given, and that area is inside the section's block padding — a band of
   the section's own colour above and below a photograph meant to be the
   surface. With no placement it falls back to the padding box. */
.vz-hero[data-vz-surface-dark] { position: relative; }

.vz-hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: clip;
}

/* Doubled selector: base.css's `.vz-section img { height: auto }` is (0,1,1)
   and beats a block-size set on a single class. */
.vz-hero .vz-hero__bg img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: center;
}

.vz-hero__bg--scrim::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: linear-gradient(
    to bottom,
    color-mix(in oklab, var(--vz-color-ink-deep) 78%, transparent) 0%,
    color-mix(in oklab, var(--vz-color-ink-deep) 62%, transparent) 34%,
    color-mix(in oklab, var(--vz-color-ink-deep) 62%, transparent) 68%,
    color-mix(in oklab, var(--vz-color-ink-deep) 76%, transparent) 100%
  );
}

/* One column at every width, centred, so there is no breakpoint here.

   THE HEADLINE AND THE SUBHEAD DO NOT WANT THE SAME CAP, and for one column
   they used to share one. 46rem was chosen for the SUBHEAD — about 62
   characters at its size, inside the 65–75ch ceiling — and applying it to the
   column applied it to the h1 as well. At the display step that is only three
   or four words a line: measured, a six-word headline broke to FOUR lines at
   both 1280 and 1440 and stood the section at 891px.

   So the column takes the headline's cap and the subhead re-declares its own.
   Measured on a rendered page at 768/1024/1280/1440, the same six-word
   headline: 46rem 4 lines · 52rem 3 · 62rem 3 at 1440 but 2 at 1280, which is
   the worst of the options because it changes between two desktop widths ·
   68rem 2 lines at both. 68rem it is. Below about 1200px .vz-container is
   narrower than the cap and takes over, so this self-limits and there is no
   breakpoint to add — at 768 the headline is 707px wide either way.

   The subhead keeps 46rem exactly, with margin-inline:auto because a centred
   box needs centring as well as centred text. Nothing else in the column moves:
   the eyebrow and the actions are centred on their own axes below. */
.vz-hero.vz-hero--centered_bg .vz-hero__copy {
  max-inline-size: 68rem;
  justify-self: center;
  text-align: center;
}

.vz-hero--centered_bg .vz-hero__subtext {
  max-inline-size: 46rem;
  margin-inline: auto;
}

.vz-hero--centered_bg .vz-hero__copy .vz-kicker,
.vz-hero--centered_bg .vz-hero__actions { justify-content: center; }

.vz-hero--centered_bg .vz-hero__subtext { margin-inline: auto; }

/* Without a floor the section is as tall as its copy, and a full-bleed
   photograph 260px high reads as a banner rather than an opening. svh rather
   than vh so a phone's collapsing address bar does not make the picture jump;
   capped in rem so a short laptop screen still shows what is below. */
@media (min-width: 48em) {
  .vz-hero--centered_bg[data-vz-surface-dark] { min-block-size: min(36rem, 82svh); }
}

/* The type goes white on the attribute rather than on the layout — same
   condition as the suppressed glows: a photograph is here. A centered_bg hero
   with no picture keeps whatever its Background field says.

   base.css flips .vz-kicker and .vz-btn--ghost to lime for its two dark section
   classes; this surface has no such class, so the same flips are repeated here
   from the same token rather than a fourth colour being introduced.

   Pure white for the subhead, not --vz-color-on-dark: white at 82% is correct
   on a flat dark fill but costs a point of contrast over a photograph, taking
   the subhead from 4.85:1 to 3.57:1 at the same scrim. Hierarchy is carried by
   size and weight in this branch instead. */
.vz-hero[data-vz-surface-dark] { color: var(--vz-color-white); }
.vz-hero[data-vz-surface-dark] .vz-hero__heading,
.vz-hero[data-vz-surface-dark] .vz-hero__subtext { color: var(--vz-color-white); }
.vz-hero[data-vz-surface-dark] .vz-kicker,
.vz-hero[data-vz-surface-dark] .vz-btn--ghost { color: var(--vz-color-accent); }

/* --------------------------------------------------------------------------
   4c. layout="embedded_form" — the form takes the picture's column

   The column is 27rem, which is form.module's own measured value: a form does
   not improve past about that — the fields get longer, the labels stay where
   they are, and the eye travels further between them. Two modules on one page
   with two different form widths would be the library disagreeing with itself
   where a visitor compares them directly. 62em rather than 64em for the same
   reason: copy beside a fixed 27rem column can stack later than copy beside a
   54% media track.

   The tracks are reshaped, not replaced. Setting the two custom properties
   moves the `mid` line and leaves every guarantee of the grid intact; a second
   grid-template-columns would have to re-derive all of it.

   :has(.vz-hero__form) gates the two-up, because an unselected form is one
   column rather than a missing one — without it the copy sits in a narrow track
   with a 27rem hole beside it, which is how the module looks the moment after
   an editor drags it onto a page.

   The separation is padding on the copy column rather than column-gap, for the
   reason given above.
   -------------------------------------------------------------------------- */
/* Stacked, the form takes the copy column's cap rather than the full content
   width. Without it the two disagree by 156px at 900px, which reads as the form
   having escaped the column the copy is in. */
.vz-hero__form {
  grid-column: content-start / content-end;
  max-inline-size: 42rem;
}

@media (min-width: 62em) {
  .vz-hero--embedded_form:has(.vz-hero__form) {
    --vz-hero-media-w: 27rem;
    --vz-hero-copy-w:  calc(var(--vz-hero-content) - 27rem);
    align-items: center;
    row-gap: 0;
  }
  .vz-hero--embedded_form:has(.vz-hero__form) .vz-hero__copy {
    grid-column: content-start / mid;
    padding-inline-end: clamp(var(--vz-space-l), 5vw, var(--vz-space-3xl));
  }
  .vz-hero--embedded_form:has(.vz-hero__form) .vz-hero__form {
    grid-column: mid / content-end;
  }
}

/* The panel is the surface the fields are on. .vz-card-surface is applied in
   module.html, so the fill, radius and the panel's own `color` come from the
   primitive — which is also why this module flips none of base.css's form role
   variables: the fields are on white on all five backgrounds, which is what the
   :root defaults are for.

   The elevation is the raised token, not the primitive's hairline, which reads
   as nearly nothing on `white` — one of the five backgrounds an editor can
   pick, and a card that disappears on one surface is not a card.

   The inset is re-sized for a column: the primitive's padding is a fraction of
   the VIEWPORT, and this is a fixed 27rem column, so at 1440 it took 57.6px a
   side off a 432px card and left the fields 317px. Same expression card-grid
   uses on the same primitive; the fields go to 368px.

   The extra class is specificity — .vz-card-surface sets both padding and
   box-shadow at (0,1,0). */
.vz-hero .vz-hero__form.vz-card-surface {
  padding: clamp(var(--vz-space-m), 2.4vw, var(--vz-space-l));
  box-shadow: var(--vz-shadow-raised);
}

/* --------------------------------------------------------------------------
   4d. layout="trust_logos" — borrowed marks, under the actions

   THE COLUMNS ARE 50/50 HERE, NOT THE SECTION'S 46/54, and that is the whole
   of the difference from media_right's geometry. This layout IS media_right
   plus a wrapped logo row — it inherits the media at `mid / full-end`, so the
   picture still bleeds to the viewport edge, which is the look it is chosen
   for. What it does not inherit is the ratio.

   Measured at 1440 before this: `mid` sat at 674 while the viewport midpoint
   was 720, so the picture crossed the centre line by 46px, took 53% of the
   width and left the headline 462px. On a layout whose copy column also has to
   carry a row of logos UNDER the actions, that column is the tightest in the
   library and the 8 points matter more here than anywhere else.

   Overriding the two custom properties is all it takes, exactly as
   `.vz-hero--split` does above — the grid re-derives from them. media_right's
   own 46/54 is deliberately untouched: it is the module's DEFAULT layout and
   the live Go Out. Do Good hero uses it, and unlike a field default a CSS
   change reaches a published page immediately.
   -------------------------------------------------------------------------- */
.vz-hero--trust_logos {
  --vz-hero-copy-w:  calc(var(--vz-hero-content) * 0.5);
  --vz-hero-media-w: calc(var(--vz-hero-content) * 0.5);
}

/* --------------------------------------------------------------------------
   4d, continued — the marks themselves

   Wrapped flex, not a grid, because of what happens to six logos of six aspect
   ratios: a grid gives each an equal cell, so a wide wordmark and a square
   badge are padded to the same box and the row reads as a table. Flex lets each
   keep its own width, which is also why the cap is on height.

   Greyscale is the house treatment for marks that are not ours: six brands at
   full saturation under a headline compete with it, where in grey they read as
   one band of evidence and come back to themselves under the pointer. The lift
   is 2px — enough to answer, not enough to be a feature.

   The row is .vz-enter--4, the same rung as the actions above it, rather than a
   fifth rung of its own.
   -------------------------------------------------------------------------- */
.vz-hero__logos {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--vz-space-m) var(--vz-space-l);
  margin-block: var(--vz-space-xl) 0;
  padding: 0;
  list-style: none;
}

/* Rows added but no images picked yet. The HubL guard catches the no-rows case
   and cannot catch this one — an image field with an empty src is still an
   object — so the list is emitted and removed here. */
.vz-hero__logos:not(:has(.vz-hero__logo)) { display: none; }

.vz-hero__logo { display: flex; }

/* Doubled ancestor again for `.vz-section img { height: auto }` at (0,1,1).
   Both axes auto with a cap on the block axis preserves each mark's own ratio
   while holding the row to one optical height. */
.vz-hero .vz-hero__logo img {
  display: block;
  inline-size: auto;
  block-size: auto;
  max-block-size: 2.125rem;
  /* The height cap does the work; this is a runaway guard only. At 9rem it was
     binding on two of four test marks and the row came back at four different
     heights. 14rem clears a 6.5:1 wordmark at the capped height and still stops
     a 2000x40 banner from taking the row. */
  max-inline-size: 14rem;
  filter: grayscale(1);
  opacity: 0.66;
  transition: filter var(--vz-dur-slow) var(--vz-ease),
              opacity var(--vz-dur-slow) var(--vz-ease),
              translate var(--vz-dur) var(--vz-ease);
}

.vz-hero .vz-hero__logo img:hover {
  filter: grayscale(0);
  opacity: 1;
  translate: 0 -2px;
}

/* On the dark surfaces the marks are lifted rather than dimmed: 66% of a dark
   logo on near-black is a smudge. */
.vz-hero:is(.vz-section--bg-brand, .vz-section--bg-ink) .vz-hero__logo img,
.vz-hero[data-vz-surface-dark] .vz-hero__logo img { opacity: 0.82; }

/* --------------------------------------------------------------------------
   5. Motion

   This is the one module that keeps the load ladder: it is the fold, so its
   copy stays on .vz-enter while everything below moved to .vz-reveal. On a
   scroll timeline the hero would be at 100% progress before a visitor saw it
   and the entrance would never happen.

   The image keeps the one effect that is this module's own — a wipe from the
   leading edge, which makes the bleed read as the image arriving rather than as
   a block fading in. It runs from hidden with `backwards` fill, so the image is
   visible by default if the animation never fires.
   -------------------------------------------------------------------------- */
@keyframes vz-hero-reveal {
  from {
    opacity: 0;
    clip-path: inset(0 0 0 18%);
    transform: scale(1.03);
  }
}
.vz-hero .vz-hero__media img {
  animation: vz-hero-reveal 900ms var(--vz-ease-expo) 120ms backwards;
}

/* The drift: ±2% of the image's own height, a few pixels a second at reading
   speed. The composition's idea is that the image is not in the column, and a
   photograph travelling at a different rate to the copy beside it is that idea
   in motion rather than in layout.

   It costs a permanent 1.05 crop, which is what gives the travel somewhere to
   go — 2.5% of overflow either side against 2% of travel leaves the frame
   covered at both ends, so the bleed never opens a gap.

   `translate` and `scale` are the independent transform properties, not
   `transform`, so they compose with the wipe's own scale instead of replacing
   it: the wipe stays on the clock while the drift runs on the scroll timeline.

   Redeclared rather than added to, because CSS cannot append to an animation
   list. Outside this block the declaration above stands unchanged.
   -------------------------------------------------------------------------- */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes vz-hero-drift {
      from { translate: 0 -2%; }
      to   { translate: 0 2%; }
    }
    .vz-hero .vz-hero__media img {
      scale: 1.05;
      animation: vz-hero-reveal 900ms var(--vz-ease-expo) 120ms backwards,
                 vz-hero-drift linear both;
      animation-timeline: auto, view();
      animation-range: normal, cover 0% cover 100%;
    }
  }
}

/* --------------------------------------------------------------------------
   6. The fold arriving, and two small things that reward looking

   The light comes up with the page: one 1.4s fade on the atmosphere layer,
   slower than every entrance in the load ladder and starting under them, so the
   copy rises into a surface that is still brightening. It animates from
   transparent with `backwards`, so a renderer running no animation paints the
   lit surface directly.

   The kicker's coral rule draws itself from its leading edge, on the shared
   vz-wipe-x keyframe the card grid and quote block use for their hairlines. It
   lands just after the eyebrow's own entrance.

   The primary action catches the light: in a lit field the button blooms rather
   than merely darkens. Hover and focus-visible both, since a keyboard visitor
   is looking at the same control. Scoped to .vz-hero__actions so no other
   button inherits a glow it was not designed for.
   -------------------------------------------------------------------------- */
@keyframes vz-atm-in {
  from { opacity: 0; }
}
.vz-hero::before,
.vz-hero::after {
  animation: vz-atm-in 1400ms var(--vz-ease) 60ms backwards;
}

/* The light breathes. Two loops, deliberately mismatched — 34s and 43s, the
   second reversed — so the pair never returns to the same arrangement twice in
   a visit; a shared duration is what makes ambient motion read as a loop.

   Slow enough not to be watchable: the coral travels about 3% of the section in
   ten seconds, under a pixel a second.

   `translate` and `scale`, so both layers stay on the compositor, and both are
   inside the section's `overflow: clip`, so a layer 6% oversized never widens
   the page. Redeclared with the entrance rather than added to it. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes vz-atm-drift-a {
    50% { translate: 2.5% -1.5%; scale: 1.06; }
  }
  @keyframes vz-atm-drift-b {
    50% { translate: -3% 2%; scale: 1.05; }
  }
  /* `will-change` because these two are the only permanently-animating boxes on
     the page and they are the size of the fold: without it each step repaints
     two full-section gradient stacks on the main thread, 38ms a frame against
     21ms with. Inside the no-preference block, so a visitor who asked for less
     motion is not charged two layers of GPU memory for nothing. */
  .vz-hero::before {
    will-change: transform;
    animation: vz-atm-in 1400ms var(--vz-ease) 60ms backwards,
               vz-atm-drift-a 34s var(--vz-ease) infinite alternate;
  }
  .vz-hero::after {
    will-change: transform;
    animation: vz-atm-in 1400ms var(--vz-ease) 60ms backwards,
               vz-atm-drift-b 43s var(--vz-ease) infinite alternate reverse;
  }
}

/* The headline arrives a word at a time. Each word carries its own --vz-i from
   the HubL loop, the same indexed-stagger pattern base.css uses for repeater
   rows. 70ms apart: a quarter of a second for the four-word headline this is
   built around, and under a second for a pathological twelve-word one.

   The blur is what makes it read as focus pulling rather than as text sliding.
   0.12em rather than a pixel value, so it scales with the type.

   inline-block is required — a transform does nothing to an inline box — and is
   why module.html emits a real space between the words. */
@keyframes vz-hero-word {
  from {
    opacity: 0;
    transform: translateY(0.32em);
    filter: blur(0.12em);
  }
}
.vz-hero .vz-hero__word {
  display: inline-block;
  animation: vz-hero-word 760ms var(--vz-ease-expo)
             calc(140ms + var(--vz-i, 0) * 70ms) backwards;
}

.vz-hero .vz-hero__eyebrow::before {
  transform-origin: left center;
  animation: vz-wipe-x var(--vz-dur-slow) var(--vz-ease-expo) 240ms backwards;
}

/* The picture answers the cursor as a lens — chromatic aberration in the
   brand's two poles rather than the usual red/cyan.

   A channel split, not a tint: each pseudo-element paints the same photograph
   MULTIPLIED by one brand colour, and the pair composites back over the
   untouched image with `lighten`.

     ::before   image x --vz-color-primary     #3e6aff — blue 1.00, red 0.24
     ::after    image x --vz-color-accent-warm #ea8161 — red 0.92, blue 0.38

   `lighten` is what makes it free where nothing is happening: a multiplied copy
   is darker than its source wherever the two align, so across flat areas the
   layers contribute nothing and surface only at edges — which is where a lens
   fringes.

   The split is differential magnification (1 - k on the blue copy, 1 + k on the
   coral), because blue refracts hardest and focuses short. That puts separation
   at zero in the centre and maximum in the corners: a uniform offset reads as
   printing misregistration, a radial one reads as glass. --vz-media-d (0 at the
   picture's centre, 1 at its frame) drives k, and the pointer's direction rides
   on `transform` as a few pixels of lateral pull.

   The trigger is the whole fold, and stillness puts it back: module.js listens
   on the section, and --vz-media-charge decays on a half-life once movement
   stops. That is why there is no pointerleave branch — leaving and stopping are
   the same thing to a decay. Charge is WHETHER, distance is HOW MUCH.

   `transform` and not `translate`, which the scroll drift already uses; the two
   compose, so the copies keep travelling with the image they are copies of.
   Same reason --vz-ca-base picks up the permanent 1.05 crop.

   The layers sit on .vz-hero__frame because a blend needs an opaque backdrop
   and the media box's rounded corners do not have one. Both are gated on
   module.js having published --vz-media-img: no script, no layers. The whole
   effect is behind prefers-reduced-motion — two copies of a 700px picture
   sliding against each other is large-area movement. */
@media (prefers-reduced-motion: no-preference) {
  .vz-hero__media {
    /* Dispersion, 0..1: how much the lens is being used, times how far off its
       axis. Both come from module.js and default to 0, so an unarmed page
       computes this to nothing. The 0.18 is a floor under the DISTANCE term
       only, never under the whole expression — "returns to the original image"
       has to mean zero. */
    --vz-ca: calc(var(--vz-media-charge, 0) * (0.18 + 0.82 * var(--vz-media-d, 0)));
    /* The radial term: 1% of the frame between the two copies at the edges,
       ~7px across a 700px picture. It scales with the image because it is a
       magnification, not an offset. 0.7% was only findable if you knew to look;
       1.4% stopped reading as a lens and started reading as a misprint. */
    --vz-ca-spread: calc(var(--vz-ca) * 0.010);
    /* The lateral term. A length, so it does not scale with the image: the tilt
       is the same four pixels at any size. */
    --vz-ca-shift: calc(var(--vz-ca) * 0.26rem);
    --vz-ca-base: 1;
  }

  .vz-hero__frame::before,
  .vz-hero__frame::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    opacity: 0;
    /* cover + center matches the img's object-fit and object-position, and the
       two boxes are the same box, so the copies register exactly. */
    background-image: linear-gradient(var(--vz-ca-tint), var(--vz-ca-tint)), var(--vz-media-img);
    background-blend-mode: multiply;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    scale: var(--vz-ca-base);
    transform: none;
    transition: opacity 380ms var(--vz-ease),
                scale 260ms var(--vz-ease),
                transform 260ms var(--vz-ease),
                mix-blend-mode 0s linear 380ms;
    /* Its own declaration rather than `allow-discrete` inside the shorthand
       above: an unknown keyword invalidates the whole shorthand and would take
       the opacity fade with it, where an unknown property is simply dropped.
       Where it lands the layers go out blended; where it does not they flip to
       normal composition for the length of the fade. */
    transition-behavior: allow-discrete;
  }

  /* Without a source there is nothing to disperse: an unset --vz-media-img
     makes background-image invalid at computed-value time, so it resolves to
     `none` and the layers paint nothing. The no-JS path is silent by
     construction rather than by a guard. */
  .vz-hero__frame::before { --vz-ca-tint: var(--vz-color-primary); }
  .vz-hero__frame::after  { --vz-ca-tint: var(--vz-color-accent-warm); }

  /* The scroll drift, on the copies. Declared under the same condition that
     gives the img its permanent 1.05 crop, so the three stay one image.

     THE RANGE HAS TO STAY IN STEP WITH THE IMAGE'S. These are three elements
     running one motion, and a range changed on the img alone would leave the
     copies at a different point on the timeline — a double exposure rather than
     an aberration. */
  @supports (animation-timeline: view()) {
    .vz-hero__frame::before,
    .vz-hero__frame::after {
      --vz-ca-base: 1.05;
      animation: vz-hero-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }

  /* THE BLEND MODES ARE DECLARED ON THE LIT STATE, NOT ON THE LAYERS, and that
     is a performance fix. A blended box costs its stacking context a
     main-thread composite every frame whether or not it is visible — opacity 0
     does not exempt it. Measured: 38ms a frame with these declared at rest
     against 16.7ms without, on a page where nothing was moving. Declared here
     they exist only while a cursor is over the photograph.

     The dispersion lives here too, so the resting state is the two copies at
     dead register and the exit is the fringes converging back into the picture
     rather than a light switching off. */
  .vz-hero__media[data-vz-lit] .vz-hero__frame::before,
  .vz-hero__media[data-vz-lit] .vz-hero__frame::after {
    opacity: 1;
    mix-blend-mode: lighten;
    transition: opacity 380ms var(--vz-ease),
                scale 260ms var(--vz-ease),
                transform 260ms var(--vz-ease);
  }

  .vz-hero__media[data-vz-lit] .vz-hero__frame::before {
    scale: calc(var(--vz-ca-base) - var(--vz-ca-spread));
    transform: translate(calc(var(--vz-media-nx, 0) * var(--vz-ca-shift) * -1),
                         calc(var(--vz-media-ny, 0) * var(--vz-ca-shift) * -1));
  }
  .vz-hero__media[data-vz-lit] .vz-hero__frame::after {
    scale: calc(var(--vz-ca-base) + var(--vz-ca-spread));
    transform: translate(calc(var(--vz-media-nx, 0) * var(--vz-ca-shift)),
                         calc(var(--vz-media-ny, 0) * var(--vz-ca-shift)));
  }

  /* A touch more contrast for the fringes to hold onto, and a touch more colour
     with it. Small, because the aberration is doing the work. */
  .vz-hero .vz-hero__media img {
    transition: filter 500ms var(--vz-ease);
  }
  .vz-hero__media[data-vz-lit] img {
    filter: saturate(1.06) contrast(1.06);
  }
}

/* Blue on the light surfaces, lime on the dark ones: the halo is the button's
   own colour, which is what makes it read as light on the control rather than
   as a shadow under it.

   Left at --vz-color-primary on lilac, where that blue is 2.22:1 against the
   band. No floor applies: this is a 34%-alpha glow behind a button, decorative
   by construction and carrying nothing. It is also the only value that still
   reads as light there — the band's own re-routed button colour is darker than
   the band, and a halo darker than what it sits on is a shadow. */
.vz-hero { --vz-hero-halo: var(--vz-color-primary); }
.vz-hero:is(.vz-section--bg-brand, .vz-section--bg-ink) { --vz-hero-halo: var(--vz-color-accent); }

.vz-hero__actions .vz-btn:not(.vz-btn--ghost) {
  transition: background-color var(--vz-dur) var(--vz-ease),
              color var(--vz-dur) var(--vz-ease),
              transform var(--vz-dur) var(--vz-ease),
              box-shadow var(--vz-dur-slow) var(--vz-ease);
}
.vz-hero__actions .vz-btn:not(.vz-btn--ghost):hover,
.vz-hero__actions .vz-btn:not(.vz-btn--ghost):focus-visible {
  box-shadow: 0 0.55rem 1.6rem color-mix(in oklab, var(--vz-hero-halo) 34%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .vz-hero .vz-hero__media img,
  .vz-hero::before,
  .vz-hero::after,
  .vz-hero .vz-hero__word,
  .vz-hero .vz-hero__eyebrow::before { animation: none; }
  .vz-hero__actions .vz-btn:not(.vz-btn--ghost) { transition: none; }
  /* The transitions go, the hover state stays: a visitor who asked for less
     motion still gets the colour back under the pointer, without the fade. */
  .vz-hero .vz-hero__logo img { transition: none; }
  .vz-hero .vz-hero__logo img:hover { translate: none; }
  /* The headline is still one heading — the word spans are markup, not motion,
     and only the animation is dropped. */
}

/* The atmosphere prints as three bands of ink-heavy colour behind the headline
   if the visitor asked for backgrounds, and the lens has no meaning on paper. */
@media print {
  .vz-hero::before,
  .vz-hero::after,
  .vz-hero__frame::before,
  .vz-hero__frame::after { display: none; }
  .vz-hero .vz-hero__word { animation: none; }
}
