/* ==========================================================================
   header.module — PHASE-1.md task 1.3

   Translated from the chrome in docs/comps/design-foundation.html §4, plus the
   mobile nav the comp doesn't have (it hides the nav below 56em with nothing
   in its place).

   Tokens only, no raw hex, no !important, mobile-first (§4.7). Buttons and
   links come from base.css and are not redefined here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. The fixed dock

   .vz-header-dock is declared in templates/partials/header.html, not in
   module.html: it has to sit outside HubSpot's generated wrapper div, which is
   only as tall as the header itself. Its CSS lives here so all header styling
   is in one file.

   Fixed, not sticky. Sticky pins against the nearest scrolling ancestor and is
   silently cancelled by an `overflow` value on anything above it — and above
   this module sit .body-wrapper plus whatever dnd wrappers the portal emits,
   none of which this theme controls. Fixed resolves against the viewport and
   has no such ancestor dependency. The trade is that it takes no space in the
   flow, which .vz-header-spacer gives back (§1a).

   `position: fixed` on the dock does NOT change where the two fixed elements
   inside it resolve — a fixed ancestor is not a containing block for fixed
   descendants; only transform / filter / backdrop-filter are. That is why the
   frosted layers in §4a are pseudo-siblings of the bar's content rather than a
   filter on the bar or the dock: a backdrop-filter out here would re-anchor the
   skip link and the mobile nav panel to this box and collapse both.
   -------------------------------------------------------------------------- */
.vz-header-dock {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: var(--vz-z-sticky);
  /* The chrome arrives before the page does. base.css §7a's load ladder is the
     right timebase — the header is above the fold by definition, so there is
     nothing to scroll to — but not the right keyframe: vz-rise travels UP from
     below, and a bar dropping in from off the top edge is what reads as chrome
     settling rather than as content rising. 480ms, ahead of the hero's own
     ladder (60ms in) so the two are one entrance and not a race.

     `backwards` and NOT `both`. The distinction is load-bearing: with `both`
     the transform survives the animation, and a transformed ancestor makes
     `position: fixed` resolve against the dock instead of the viewport — which
     would break the skip link on focus AND the full-height mobile nav panel,
     both of which are fixed and both of which live in here. With `backwards`
     the transform exists only while the animation runs and the element returns
     to an untransformed box, so both are correct from ~480ms on. */
  animation: vz-header-drop 480ms var(--vz-ease-expo) backwards;
}

@keyframes vz-header-drop {
  from { opacity: 0; transform: translateY(-100%); }
}

/* Nothing is hidden without it (the keyframe animates FROM hidden, base.css §7)
   and there is nothing to cross-fade to, so removing it is the whole
   alternative — the same call every other entrance in this library makes.

   Declared BEFORE the @supports block below, which re-states this branch: the
   scroll-linked lift is position, not motion, and has to survive here. */
@media (prefers-reduced-motion: reduce) {
  .vz-header-dock { animation: none; }
}

/* The announcement bar scrolls away while the frosted bar stays pinned — the
   one thing sticky gave for free and fixed does not. Same arithmetic as the old
   sticky offset, now driven by the document's own scroll: the dock rises by the
   announcement's measured height over exactly the first --vz-announce-h pixels
   and then holds.

   `inset-block-start` and not a transform, deliberately. A transform on this
   element is precisely what §1 says would re-anchor the skip link and the
   mobile panel; offsetting a fixed box repositions it without ever making it a
   containing block. It is the dock's only out-of-flow subtree, so nothing in
   the document relayouts behind it.

   --vz-announce-h is measured in module.js. Unset (JS blocked, or no
   announcement configured) it resolves to 0px, the range is empty and the dock
   simply never moves — which is also the whole no-support fallback, since the
   spacer in §1a reserves the announcement's height either way. */
@supports (animation-timeline: scroll()) {
  @keyframes vz-header-dock-lift {
    to { inset-block-start: calc(-1 * var(--vz-announce-h, 0px)); }
  }

  .vz-header-dock {
    animation: vz-header-drop 480ms var(--vz-ease-expo) backwards,
               vz-header-dock-lift linear both;
    animation-timeline: auto, scroll(root block);
    animation-range: normal, 0px var(--vz-announce-h, 0px);
  }

  @media (prefers-reduced-motion: reduce) {
    .vz-header-dock {
      animation: vz-header-dock-lift linear both;
      animation-timeline: scroll(root block);
      animation-range: 0px var(--vz-announce-h, 0px);
    }
  }
}

/* --------------------------------------------------------------------------
   1a. The spacer — the flow space the fixed dock gave up

   An empty presentational box immediately after the dock, exactly as tall as
   the chrome, so the first section still starts below the header instead of
   behind it. Everything below it then scrolls under the frosted bar, which is
   the whole point of §4a.

   Two declarations rather than one because the fallback matters. With JS the
   measured --vz-announce-h is exact and tracks a dismissal to 0px. Without it
   the :has() branch still knows an announcement bar was rendered and holds a
   plausible one-line height back, so a no-JS page loses a few pixels of
   alignment rather than putting a headline behind the announcement.
   -------------------------------------------------------------------------- */
.vz-header-spacer { block-size: var(--vz-header-h); }

.vz-header-dock:has(.vz-announce) + .vz-header-spacer {
  block-size: calc(var(--vz-header-h) + var(--vz-announce-h, 2.75rem));
}

/* Both branches again, plus the contained pill's top offset (§4c). The pill
   hangs --vz-bar-inset below whatever is above it, so the chrome is that much
   taller and the flow space it gave up is that much bigger. Getting only one of
   the two branches would leave a page WITH an announcement bar short by 16px,
   which is a band of body background above the first section — the exact defect
   §1b and this spacer exist to remove.

   Four rules rather than two because :has() takes the specificity of its
   heaviest argument, so the two single-condition rules are both (0,3,0) and
   would be decided by source order alone on a page that is contained AND has an
   announcement. The double-:has() rule below is (0,4,0) and settles it outright;
   the two singles never both match anything else. */
.vz-header-dock:has(.vz-header--contained) + .vz-header-spacer {
  block-size: calc(var(--vz-header-h) + var(--vz-bar-inset));
}
.vz-header-dock:has(.vz-announce):has(.vz-header--contained) + .vz-header-spacer {
  block-size: calc(var(--vz-header-h) + var(--vz-bar-inset) + var(--vz-announce-h, 2.75rem));
}

/* THE CHROME ANNOUNCES ITSELF, so a first section can choose to run under it.

   The frosted bar is translucent by design (§4a) — but at the very top of a
   page it is translucent over the space the spacer reserved, which is the body's
   own background and nothing else. That reads as a solid white band above the
   design rather than as glass over it, and it is at its worst on the surface
   the bar most wants to be glass over: hero.module's lit wash (its §1b).

   The fix belongs to whichever section is first, not to the bar: it pulls
   itself up by exactly this height and pads its content back down by the same
   amount, so the surface starts at the top of the viewport and the bar has
   something to be translucent over from the first pixel.

   Published as a variable rather than read from --vz-header-h directly, and
   this file is where it has to happen: --vz-header-h is in theme.css and
   therefore always defined, so a section reading it would compensate for chrome
   that is not there — on a page built without this module, and in every
   module harness. This declaration only exists when the header does. It is the
   BAR's height, never the announcement's: the announcement is opaque content
   that scrolls away, so a first section must start below it, not behind it.

   --vz-bar-inset is declared HERE, on the root, rather than beside the rest of
   §4c on .vz-header--contained. A custom property never resolves upward, and
   both the contract below and the spacer in §1a need the number — so declaring
   it on the bar would leave those two reading a fallback while the pill read the
   real value, which is a 16px band of body background above the first section
   and no error anywhere. One declaration, three consumers. */
:root {
  --vz-bar-inset: var(--vz-space-s);
  --vz-chrome-h: var(--vz-header-h);
}

/* The contained bar is not flush with the top of its dock, so the height a first
   section pulls itself up by is the bar PLUS that gap. :has() rather than
   something module.js sets, for the same reason the declaration above is
   unconditional-when-the-module-is-present: it has to be right before a single
   frame of script has run, and it has to stay false on a page whose header is
   `full`. The same :has() §1a already depends on for the spacer. */
:root:has(.vz-header--contained) {
  --vz-chrome-h: calc(var(--vz-header-h) + var(--vz-bar-inset));
}

/* Set while the mobile panel is open. Scroll lock on the root, so the page
   behind the panel doesn't move. */
.vz-nav-locked { overflow: hidden; }

/* --------------------------------------------------------------------------
   2. Skip link — first focusable thing on the page

   Fixed rather than absolute on focus: the dock scrolls up out of view by the
   announcement bar's height, and an absolutely positioned skip link would ride
   up with it.
   -------------------------------------------------------------------------- */
.vz-header__skip {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.vz-header__skip:focus {
  position: fixed;
  inset-block-start: var(--vz-space-2xs);
  inset-inline-start: var(--vz-space-2xs);
  inline-size: auto;
  block-size: auto;
  clip-path: none;
  z-index: var(--vz-z-toast);
  padding: 0.6rem 0.9rem;
  border-radius: var(--vz-radius-s);
  background: var(--vz-color-ink-deep);
  color: var(--vz-color-white);
  font-size: var(--vz-step--1);
  font-weight: 700;
  text-decoration: none;
}
/* Lime, because the pill it lands on is ink — the same switch base.css makes
   on dark sections, and the same one §5 makes for the open nav panel. Stated
   here because base.css scopes its ring to .vz-dnd / .vz-section / .vz-btn /
   .vz-link and this is none of those: without it the first thing a keyboard
   visitor ever focuses falls back to whatever ring the browser draws. */
.vz-header__skip:focus-visible {
  outline: 2px solid var(--vz-color-accent);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   3. Announcement bar
   -------------------------------------------------------------------------- */
.vz-announce {
  background: var(--vz-color-ink-deep);
  color: var(--vz-color-white);
  font-size: var(--vz-step--1);
}

/* align-items:flex-start with no flex-wrap keeps the × on the first line at
   the top right however many lines the message wraps to. */
.vz-announce__inner {
  display: flex;
  align-items: flex-start;
  gap: var(--vz-space-s);
  padding-block: 0.65rem;
}

/* min-inline-size:0 lets the text wrap instead of shoving the button out. */
.vz-announce__text {
  flex: 1 1 auto;
  min-inline-size: 0;
  margin: 0;
}

/* Lime, not white — 13.6:1 on the dark surface, and it matches how base.css treats
   links on the ink surface. */
.vz-announce .vz-link { color: var(--vz-color-accent); }

/* 44px touch target, pulled back out of the flow with negative margins so it
   doesn't set the bar's height.

   -1rem is measured, not eyeballed: the × advance is 10.06px inside a 44px
   box, so hanging the box 16px past the content edge lands the glyph's right
   edge within a pixel of the menu button's bars in the row below. At -0.75rem
   the two sat 4px apart, which is visible when they are stacked. */
.vz-announce__dismiss {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  margin-block: -0.7rem;
  margin-inline-end: -1rem;
  padding: 0;
  border: 0;
  border-radius: var(--vz-radius-s);
  background: none;
  color: var(--vz-color-on-dark);
  font-family: var(--vz-font);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--vz-dur) var(--vz-ease);
}
.vz-announce__dismiss:hover { color: var(--vz-color-white); }

/* Lime for everything in the bar, the dismiss button and the inline link both.
   base.css only switches its ring to lime inside .vz-section--bg-ink, and this
   bar is chrome rather than a section — so the link was inheriting the
   light-surface ring and drawing #2844a6 on the navy, which is 2.05:1 and is a
   focus indicator a keyboard visitor cannot see. Lime is 13.6:1 here. */
.vz-announce :focus-visible {
  outline: 2px solid var(--vz-color-accent);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   4. The bar
   -------------------------------------------------------------------------- */
.vz-header {
  /* Explicit bar height — the mobile panel measures itself against it, and it
     keeps the bar from resizing as the logo or button copy changes. The value
     moved to theme.css when base.css started needing it too, for in-page anchor
     offsets; consumed here rather than redeclared so there is one number. */
  position: relative;   /* containing block for the frosted layers */

  /* One type style for everything that sits in the bar and reads as a control:
     the nav links and the button label are the same size and the same weight,
     so the row scans as one row rather than as small text next to a large
     pill. Declared once here and consumed in §7 and §8, because the two live in
     different breakpoints and would otherwise drift the first time either is
     touched.

     --vz-step--1, not --vz-step-0. Chrome is not body copy: at the body size
     the pill grows to ~50px inside a 76px bar and starts competing with the
     wordmark for the eye. 600 rather than base.css's 700 — at 15px in a light
     bar, four bold links read as four shouts. */
  --vz-bar-type-size: var(--vz-step--1);
  --vz-bar-type-weight: 600;

  /* How far the frosted layers hang below the bar to finish their fade (§4a). */
  --vz-glass-tail: 1.75rem;

  /* No fill of its own: §4a's layers are the bar's surface, and an opaque white
     underneath them would be the thing they blurred. The two branches at the
     end of §4a put this back. The border is declared transparent rather than
     absent so the progress rule keeps the same 1px to straddle in either
     state. */
  background: none;
  border-block-end: 1px solid transparent;
}
.vz-header__inner {
  /* Positioned so that it paints over the frosted layers, which are absolutely
     positioned and would otherwise cover it — a positioned box beats a static
     one whatever the source order says. No z-index, so no new stacking context
     and the fixed nav panel inside keeps painting against the dock's. */
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--vz-space-s);
  min-block-size: var(--vz-header-h);
}

/* Read progress, drawn along the bar's leading edge.

   A landing page is one long scroll with no other wayfinding — no nav
   highlight, no section index — and the bar is pinned in front of the visitor
   the whole way down, so this is the one place the page can say how far in
   somebody is without adding an element to the design. 2px, gutter to gutter,
   no layout, no hit area.

   TOP edge, not the bottom one it used to straddle. Two reasons, and the first
   is the §4a rewrite: the bottom is now a dissolve rather than a line, and a
   hard 2px rule laid across a gradient that exists to remove hard rules was the
   one element fighting the effect. The second is where it ends up — the
   announcement lifts away within the first 45px of scroll, so from the moment
   this bar has any progress to report its top edge IS the top edge of the
   viewport, which is where a reading indicator belongs. Above it before that
   point sits the ink announcement bar, and the rule is at ~0% there anyway.

   Brand blue and not lime, which §4.1 would suggest for an accent: lime on
   white is about 1.3:1 and this rule would be invisible. --vz-color-primary at
   4.46:1 is precisely the "rules, graphic fields, icons" role §4.1 reserves it
   for, and it is the identity colour — 127 uses on the live site.

   Declared only inside the @supports/no-preference pair, so with no scroll
   timeline (or under reduced motion) nothing is drawn at all. `scale: 0 1` as
   the base is what makes an inactive timeline — a document too short to scroll
   — degrade to nothing rather than to a full-width stripe claiming the page is
   finished. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes vz-header-progress {
      to { scale: 1 1; }
    }
    .vz-header::after {
      content: '';
      position: absolute;
      inset-block-start: 0;
      inset-inline: 0;
      block-size: 2px;
      /* Through the role variable rather than straight to --vz-color-primary:
         the bar inverts under dark sections (§4b) and blue on the dark surface
         is 3.92:1, which is not a read indicator anybody can follow. */
      background: var(--vz-bar-progress);
      transform-origin: 0 50%;
      scale: 0 1;
      pointer-events: none;
      animation: vz-header-progress linear both;
      animation-timeline: scroll(root block);
      transition: background-color var(--vz-dur) var(--vz-ease);
    }

    /* On the pill this rule is drawn ACROSS a curve, and 2px laid gutter to
       gutter would overhang both ends of it — the two places where the bar's
       own edge has already turned away underneath the rule. Inset by the
       radius, which on a pill is half the bar's height, so the track starts and
       stops where the top edge is actually straight. It is the honest shorter
       track, not a clipped one: 0% is still empty and 100% is still the whole
       of what is drawn.

       Not drawn inside .vz-header__glass instead, which is the other option the
       brief allows: the glass is display:none in three separate states (§4a's
       two fallbacks and the open panel) and a read indicator that disappears
       when a visitor asks their OS to reduce transparency is worse than a
       slightly shorter one.

       --vz-header-h and not --vz-radius-pill: the pill radius is 999px, which
       resolves against the box to half its height but does not report that
       number to calc(). The bar's own height is where that half comes from. */
    .vz-header--contained::after {
      inset-inline: calc(var(--vz-header-h) / 2);
      border-radius: 2px;
    }
  }
}

/* --------------------------------------------------------------------------
   4a. The frosted bar

   The bar is translucent and the page runs underneath it, so the blur is what
   keeps the row legible over whatever surface happens to be passing — this page
   alone scrolls white, cream, brand blue and deep navy under this box.

   PROGRESSIVE, and the four layers are why. One masked backdrop-filter fades
   the blurred image's OPACITY across the mask, compositing it over the sharp
   original: at 50% you get half of each, which is a double exposure and not a
   softer blur. Four layers of doubling radius, each masked to a shorter run
   than the last, blur the result of the one below them instead — so the top of
   the bar carries all four (≈15px) and the radius steps down to 1px and then to
   nothing past the bottom edge. The bar dissolves rather than ending on a line,
   which is the only reason a translucent header can go without a hard rule
   under it.

   --vz-glass-tail is that release, and it is why the stack hangs below the bar
   rather than filling it: the fade has to finish somewhere, and finishing
   exactly on the bar's edge would put the hardest step of the gradient on the
   one line the effect exists to remove. pointer-events:none because the tail
   sits over live page content.

   The layers are absolutely positioned children, NOT a filter on .vz-header or
   the dock. A backdrop-filter makes its element a containing block for fixed
   descendants, and the mobile nav panel and the skip link are both fixed and
   both live inside the bar; put the filter one level up and the panel resolves
   its `inset-block-end: 0` against a 64px box and collapses.

   Saturation rides on the widest layer only. On four stacked layers it would
   compound to 6x and turn the brand blue passing underneath into a cartoon.

   And it is a role variable rather than a constant, because §4b's matched
   states are the case where it has to come off. Once the tint IS the section's
   colour, compositing it over that section is an exact no-op at any alpha — so
   the bar would be invisible against the band, except that the saturation is
   not a no-op: #2844a6 saturated 1.6x is #1743e0, and it shows through in the
   tail where the tint's alpha has fallen away and the mask has not. That draws
   a brighter halo along the bottom of the tail, which is a band edge in the one
   place this whole section exists to remove one. Measured on brand blue, where
   it is obvious, and faintly present on ink for the same reason. It stays at
   1.6 on white and cream, where the thing underneath is real content the glass
   is meant to enrich rather than a flat colour it is meant to disappear into.
   -------------------------------------------------------------------------- */
.vz-header__glass {
  position: absolute;
  inset-block-start: 0;
  inset-block-end: calc(-1 * var(--vz-glass-tail));
  inset-inline: 0;
  pointer-events: none;
}

.vz-header__glass-band {
  position: absolute;
  inset: 0;
  -webkit-backdrop-filter: blur(var(--vz-glass-blur));
  backdrop-filter: blur(var(--vz-glass-blur));
  /* `black` here is a stencil, not a colour — a mask reads alpha only, and
     nothing of it is ever painted. The prefixed pair is for Safari before 15.4,
     which is also why the filter below carries one. */
  -webkit-mask-image: linear-gradient(to bottom, black var(--vz-glass-solid), transparent var(--vz-glass-fade));
  mask-image: linear-gradient(to bottom, black var(--vz-glass-solid), transparent var(--vz-glass-fade));
}

/* 2 / 4 / 8 / 16, so the top of the bar carries about 30px of blur. Measured
   against the thing that actually passes under this header: a .vz-display
   headline is 44px tall, and at the 15px the first pass used it stayed legible
   through the glass — a second, ghosted headline competing with the nav row.
   The radius has to beat the type it dissolves. */
.vz-header__glass-band:nth-child(1) {
  --vz-glass-blur: 2px;
  --vz-glass-solid: 72%;
  --vz-glass-fade: 100%;
  -webkit-backdrop-filter: blur(2px) saturate(var(--vz-glass-sat));
  backdrop-filter: blur(2px) saturate(var(--vz-glass-sat));
}
.vz-header__glass-band:nth-child(2) { --vz-glass-blur: 4px;  --vz-glass-solid: 52%; --vz-glass-fade: 80%; }
.vz-header__glass-band:nth-child(3) { --vz-glass-blur: 8px;  --vz-glass-solid: 34%; --vz-glass-fade: 62%; }
.vz-header__glass-band:nth-child(4) { --vz-glass-blur: 16px; --vz-glass-solid: 16%; --vz-glass-fade: 44%; }

/* The tint, painted last so it sits over the blur rather than under it — a
   pseudo-element is the box tree's final child, which is exactly the order
   glass needs.

   The alphas are not a preference. 80% is the floor anywhere the row's own text
   sits: white at 80% over this page's darkest surface lands the bar at #d0d0d0,
   which holds --vz-color-ink at about 9:1 and keeps the §4.1 body-text floor
   met over every surface the page scrolls past. It only drops below that in the
   tail, where nothing of the header's own is drawn.

   The COLOUR is --vz-glass-tint rather than white, and the alphas hold either
   way: §4b flips that variable to ink under dark sections, and the same three
   stops measured against ink give the wordmark 13-14:1 instead of the 1.04:1 a
   white tint leaves it at. One variable, because the ramp is the part that was
   measured and the part that must not drift. */
.vz-header__glass::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    color-mix(in oklab, var(--vz-glass-tint) 86%, transparent) 0%,
    color-mix(in oklab, var(--vz-glass-tint) 80%, transparent) 60%,
    color-mix(in oklab, var(--vz-glass-tint) 58%, transparent) 76%,
    transparent 100%);
}

/* Two ways back to the plain white bar, and both restore the hairline the
   dissolve replaced.

   The @supports branch is the honest fallback: without backdrop-filter the
   layers would tint but never blur, which is a washed-out bar over live text.
   The media query is the accessibility one — a visitor who has asked their OS
   to reduce transparency has asked for exactly this. Neither is a degraded
   header; the bar simply reads as the opaque chrome it was.

   Both go through §4b's --vz-bar-solid, so the opaque bar inverts too. It has
   to: the row's own colours come from the same role variables either way, and
   an opaque WHITE bar carrying the white nav text of the dark state is a header
   with nothing legible in it. Inverting is a colour change, not a transparency
   one — reduced-transparency has no quarrel with it. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .vz-header__glass { display: none; }
  .vz-header {
    background: var(--vz-bar-solid);
    border-block-end-color: var(--vz-bar-rule);
  }
}

@media (prefers-reduced-transparency: reduce) {
  .vz-header__glass { display: none; }
  .vz-header {
    background: var(--vz-bar-solid);
    border-block-end-color: var(--vz-bar-rule);
  }
}

/* And a third: while the mobile panel is up, the page behind is covered and
   scroll-locked, so there is nothing left for the bar to reveal — but the tail
   would still hang a fading white band over the top of the ink panel, which
   reads as a seam rather than as glass. */
.vz-header[data-vz-nav-open="true"] .vz-header__glass { display: none; }
.vz-header[data-vz-nav-open="true"] { background: var(--vz-bar-solid); }

/* --------------------------------------------------------------------------
   4b. Surface matching — the bar wears the colour of whatever is under it

   The wordmark is the reason. Its "sense" is --vz-color-accent (#d4ed8d), a
   colour §4.1 reserves for DARK surfaces, where it is 13.6:1. Over this bar it
   lands on none of the surfaces that number was measured against: the tint
   composites whatever is passing underneath into a variable light grey, and
   that grey sits within a couple of points of the lime's own luminance.
   Measured at the wordmark's vertical centre, where §4a's ramp resolves to 82%:

     under a white section    bar #ffffff   "sense" 1.29:1  <- the brand's own
     under cream  #fbf3ee     bar #fefdfc   "sense" 1.27:1     baseline
     under ink    #0c173b     bar #d4d6dc   "sense" 1.13:1
     under brand  #2844a6     bar #d9deef   "sense" 1.04:1  <- isoluminant

   Opacity cannot fix it, which is the finding that decided the approach. A
   fully opaque WHITE bar tops out at 1.29:1 — that is the lime-on-white ceiling
   — and 95% white only reaches 1.16:1 over ink. The surface has to change
   colour, not density.

   Not an AA failure: WCAG 1.4.3 and 1.4.11 both exempt logotypes. It is a craft
   defect and it is filed as one — the target here is "never worse than the
   wordmark on white", not 4.5:1.

   THE TINT IS NOW THE SECTION'S OWN COLOUR, not a light/dark polarity. Four
   states rather than two, and the reason is what the two-state version looked
   like over brand blue: the tint went to deep navy, so the bar read as a navy
   band laid across a blue section — the right polarity and the wrong colour,
   and the seam was the most visible thing in the fold. Tinting with the
   section's own token at §4a's same three alphas composites the bar to exactly
   that token where the section is underneath it, so there is no band at all;
   the blur is still doing all the work of keeping the row legible over moving
   content. Measured at the wordmark's vertical centre, where §4a's ramp
   resolves to 82% and the surface underneath is the section the tint came from:

     state     tint                     bar       row text     mark: "VIZ"/"sense"
     light     --vz-color-white         #ffffff   ink 15.43   4.46 /  1.29  <- baseline
     surface   --vz-color-surface       #fbf3ee   ink 14.07   4.07 /  1.18
     brand     --vz-color-primary-deep  #2844a6   wht  8.51   8.51 /  8.51  <- white mark
     ink       --vz-color-ink-deep      #0c173b   wht 17.48   3.92 / 13.57

   Measured off a rendered page by header-surface-check.mjs, not asserted here —
   that file prints this table on every run, so a token moving in the theme's
   own fields.json shows up as a different number rather than as a stale comment.

   The one number that moves the wrong way is cream's lime, 1.27 down to 1.18,
   and it is the honest cost of the change rather than a defect: 1.27 was an
   artifact of a near-WHITE bar sitting over a cream section, and 1.18 is simply
   what the wordmark's lime measures on cream — which is what it measures
   everywhere else on the page cream is used. The target above is met either way.

   BRAND BLUE IS THE STATE THAT NEEDS THE LOGO SWITCHED, and it is the only one.
   The wordmark's "VIZ" is --vz-color-primary (#3e6aff); on #2844a6 that is
   1.91:1 — two blues a few points of luminance apart, which is half a logo
   nobody can see. Neither opacity nor the tint can reach it, because the
   collision is between the mark and the surface it was asked to sit on. So on
   this one surface the mark goes to a solid white silhouette
   (brightness(0) makes every pixel black, invert(1) makes it white, and both
   leave alpha alone, so any wordmark an editor uploads works) at 8.5:1. It
   costs the lime "sense", which was legible there at 6.6:1 — a mono mark on the
   brand colour, which is what footer.module does on the same surface and for
   the same reason. Ink is deliberately left alone: the blue is 3.92:1 there,
   which is weak but visible, and the lime is at its 13.6:1 best.

   Everything the bar draws switches through the blocks below. Scattering
   [data-vz-surface] across §4, §6 and §8 would be nine selectors to keep in
   step; these are three, and the rules that consume them never learn the state
   exists. Which section is underneath is module.js's job (§3 there).

   THE CONTRACT, for anything built on this library later. The bar copies a
   section that carries .vz-section--bg-surface, --bg-brand or --bg-ink — which
   every module emits from its own `background` style field, so this needs no
   wiring per page or per template — and falls back to white for --bg-white,
   for anything else (hero.module's --bg-wash, whose top quarter is veiled to
   92% white anyway), and for a page with no JS. A section may also declare
   data-vz-surface-dark, which is read as `ink`: that attribute is for a section
   that is dark for some reason other than its background token — a full-bleed
   dark photograph, a surface painted through HubSpot's own dnd section styling.
   Emit it and the bar will find it.

   :not([data-vz-nav-open="true"]) is what suspends the whole thing while the
   mobile panel is up. The panel covers the page, so there is no surface left to
   answer to — and the bar above it is deliberately white (§4a above), where a
   white burger would be a menu button nobody can see.

   AND ALL OF IT IS THE FULL-WIDTH BAR'S. :not(.vz-header--contained) on the
   three state blocks below is what keeps the floating pill out of it, and it is
   the shape's own argument rather than a preference: the full bar spans the
   screen and has no edges of its own, so the only way it can avoid reading as a
   slab laid across the section is to become that section — which is the whole of
   the reasoning above. A pill is a discrete object floating over the page with a
   ring around it and body background on three sides; it is already legible AS a
   separate thing, so copying the band underneath buys nothing and costs the one
   material that says "floating": the white glass. It stays white over every
   surface, at §4a's measured alphas, which is the state every role in the block
   below this comment already declares.

   That leaves the wordmark's lime at the 1.29:1 it measures on white — the
   baseline this whole section targets ("never worse than on white") and not a
   regression from it — and leaves the pill's ink row at 15.43:1 and its deep-blue
   CTA at 8.51:1 over every band, since the surface under those numbers no longer
   moves. The one thing the pill gives up is the brand-blue silhouette: over a
   blue band its "VIZ" is the mark's own #3e6aff on white glass, which is the
   1.29/4.46 pair the brand ships with everywhere else, not the 1.91:1 collision
   that switch exists for — that collision only happens when the BAR turns blue.

   module.js §3 makes the same cut and skips the scroll listener entirely on a
   pill, so a page built that way does no surface work at all. The :not() here is
   the enforcement, not the optimisation: the state has to be right for a bar
   whose data-vz-surface arrives from anywhere else — a cached older module.js, a
   page that sets it by hand, the editor re-rendering markup around a script that
   has already run.
   -------------------------------------------------------------------------- */

/* Registered so the tint can be TRANSITIONED. An unregistered custom property
   is a token substitution and jumps between values; syntax:'<color>' makes it
   interpolable, and §4a's gradient recomputes each frame as it travels. Where
   @property is unsupported the plain declaration below still applies and the
   tint simply snaps to the same end state. */
@property --vz-glass-tint {
  syntax: '<color>';
  inherits: true;
  initial-value: #ffffff;
}

.vz-header {
  --vz-glass-tint:     var(--vz-color-white);
  --vz-bar-ink:        var(--vz-color-ink);
  --vz-bar-link-hover: var(--vz-color-primary-deep);
  --vz-bar-underline:  var(--vz-color-primary);
  --vz-bar-ring:       var(--vz-color-primary-deep);
  --vz-bar-progress:   var(--vz-color-primary);
  --vz-bar-solid:      var(--vz-color-white);
  --vz-bar-rule:       var(--vz-color-rule);
  --vz-bar-cta-bg:     var(--vz-color-primary-deep);
  --vz-bar-cta-ink:    var(--vz-color-white);
  --vz-bar-cta-hover:  var(--vz-color-primary-hover);
  --vz-bar-logo:       none;
  --vz-glass-sat:      1.6;

  /* Only the tint needs declaring here — every other role above is consumed by
     a property that carries its own transition (§4, §6, §8). --vz-dur and not
     something more luxurious, because all of them have to land together: a bar
     that is still darkening while the links have already gone white is 200ms of
     white on white. */
  transition: --vz-glass-tint var(--vz-dur) var(--vz-ease);
}

/* Cream. The lightest of the three matches, and the only one that changes
   nothing but the colour of the surface — ink on cream is 14.1:1, so every
   role the light state declared still holds and none is repeated here. */
.vz-header:not(.vz-header--contained)[data-vz-surface="surface"]:not([data-vz-nav-open="true"]) {
  --vz-glass-tint:     var(--vz-color-surface);
  --vz-bar-solid:      var(--vz-color-surface);
  --vz-glass-sat:      1;
}

/* The two dark surfaces share one inverted role set — §4.1's rule that the
   action colour on dark is lime and never the blue is the same on both — and
   differ only in the tint and the fill, which the block after this one gives
   brand blue. "dark" is here for the two-state contract this replaced: nothing
   emits it now, and a page still running an older module.js gets the ink state
   rather than a bar with no rule at all. */
.vz-header:not(.vz-header--contained):is([data-vz-surface="brand"], [data-vz-surface="ink"], [data-vz-surface="dark"]):not([data-vz-nav-open="true"]) {
  --vz-glass-tint:     var(--vz-color-ink-deep);
  --vz-bar-ink:        var(--vz-color-white);
  --vz-bar-link-hover: var(--vz-color-accent);
  --vz-bar-underline:  var(--vz-color-accent);
  --vz-bar-ring:       var(--vz-color-accent);
  --vz-bar-progress:   var(--vz-color-accent);
  --vz-bar-solid:      var(--vz-color-ink-deep);
  --vz-bar-rule:       var(--vz-color-rule-invert);
  --vz-bar-cta-bg:     var(--vz-color-accent);
  --vz-bar-cta-ink:    var(--vz-color-ink-deep);
  --vz-bar-cta-hover:  var(--vz-color-accent-hover);
  --vz-glass-sat:      1;
}

/* Brand blue. Same specificity as the block above — (0,4,0) both ways, since
   :is() and :not() both take the weight of their heaviest argument — so this
   wins on source order alone and must stay after it. */
.vz-header:not(.vz-header--contained)[data-vz-surface="brand"]:not([data-vz-nav-open="true"]) {
  --vz-glass-tint:     var(--vz-color-primary-deep);
  --vz-bar-solid:      var(--vz-color-primary-deep);
  --vz-bar-logo:       brightness(0) invert(1);
}

/* --------------------------------------------------------------------------
   4c. The contained pill — bar_style "contained", the opt-in shape

   Same bar, smaller box. The full-width bar is the default (fields.json), and it
   is the one that carries §4b's surface matching: this shape is always the same
   translucent white glass, over cream, over brand blue, over ink. §4b's closing
   paragraphs carry why — a pill is already a discrete object floating over the
   page, so it has nothing to gain by becoming the band underneath it and a
   material to lose by trying. Mechanically it is one thing: §4b's three state
   blocks are scoped :not(.vz-header--contained), so every role stays at the
   light-state value the base block declares and no rule below this line has to
   know the states exist.

   WHAT ELSE CHANGES IS THE EDGE. §4a's four bands exist so the bar can end
   in a dissolve rather than on a line: the blur radius steps down and the tint's
   alpha runs out --vz-glass-tail below the bar, and the whole point is that
   there is no edge to see. A pill is nothing but edge. So the tail goes to 0,
   the ramp collapses to one clipped layer, and a 1px ring draws the line the
   dissolve was avoiding — a ring OR a shadow and never both on one element
   (PHASE-1 §4.7 as amended by CR-9; video-embed.module §4 is the worked
   example), so there is no drop shadow under this pill at any width.

   WHERE THE RADIUS AND THE CLIP GO IS NOT A STYLE CHOICE. The clip lives on
   .vz-header__glass and never on .vz-header or the dock. §1 and §4a both spell
   out why a filter, a backdrop-filter or a transform cannot go on those two —
   each makes its element a containing block for fixed descendants, and the skip
   link and the full-height mobile panel are both fixed and both live inside the
   bar. An overflow clip is a different mechanism and a similar outcome: it does
   not re-anchor a fixed descendant, but it would visibly cut the panel off at
   the bar's own bottom edge. border-radius does neither, which is why the three
   opaque states at the end of this section are allowed to put one on .vz-header
   — that is the one place the glass is gone and something else has to be round.
   -------------------------------------------------------------------------- */
.vz-header--contained {
  /* The pill's ends land on .vz-container's content edge, so they line up with
     every headline on the page. The container is max-width:--vz-maxw +
     margin-inline:auto + padding-inline:--vz-gutter, so its content box is
     min(100%, maxw) - 2 x gutter — which is these three lines and nothing else.
     hero-harness.mjs measures that as alignDelta and asserts it at 0px. */
  inline-size: calc(100% - 2 * var(--vz-gutter));
  max-inline-size: calc(var(--vz-maxw) - 2 * var(--vz-gutter));
  margin-inline: auto;

  /* The float. Read from the root (§1) rather than declared here, because the
     spacer and --vz-chrome-h need the same number and neither can see this
     element. The margin collapses through the module's own wrapper div and
     stops at the dock, which is out of flow and therefore its own formatting
     context — so this is 16px of space inside the dock either way, with an
     announcement bar above it or without one. */
  margin-block-start: var(--vz-bar-inset);

  /* §4a's release, switched off. Zero rather than deleted: §4a's
     inset-block-end reads the variable in both states and this keeps one number
     in one place.

     Worth knowing before CR-11 touches it: hero.module §1b floors its top
     padding at --vz-space-xl BECAUSE of the 1.75rem tail — the eyebrow had to
     clear the end of the dissolve or it read as a line of type caught in the
     blur. With the tail at 0 that floor is over-generous in this state and
     entirely harmless, since it is open space above a fold rather than a
     collision. Changing it is CR-11's call, not this task's. */
  --vz-glass-tail: 0px;
}

/* The gutter is the right inset for a bar that spans the screen and too much
   for one already inset by it: at 360px the pill's content box is 40px narrower
   than the full bar's, and the nav-less configuration (wordmark + a full-size
   pill, nothing between them) is the tight fit §7 already had to buy room for.
   16px still clears the curve — a pill's leading edge is at its widest exactly
   where a vertically centred row sits — and gives the tight case its room back.
   Opens to 24px on a desktop pill, where 16px inside a 76px-tall bar reads mean. */
.vz-header--contained .vz-header__inner {
  padding-inline: clamp(var(--vz-space-s), 2.2vw, var(--vz-space-m));
}

/* The shape. overflow:clip so the one remaining backdrop-filter is cut to the
   curve — a backdrop-filter is not clipped by a parent's border-radius on its
   own — and border-radius:inherit on the children so the tint and its ring
   follow it too. */
.vz-header--contained .vz-header__glass {
  border-radius: var(--vz-radius-pill);
  overflow: clip;
}

/* Four bands down to one. The stack's whole job was the RAMP — four doubling
   radii, each masked to a shorter run than the last, so the blur dissolves
   towards a bottom edge that is not there. Clipped to a pill with no tail there
   is no run to ramp across: bands two to four would resolve to four copies of
   the same rectangle, four backdrop-filters deep, for an effect nobody can see.
   One layer at 14px is the same glass the top of the full bar carries (2+4+8+16
   composites to about 30px, but that is measured at the top of a ramp that has
   already spent half its strength by mid-bar) and one pass instead of four.

   Both mask stops at 100%, which is a mask that never fades: the pill is opaque
   glass to its own edge and the edge is the ring. */
.vz-header--contained .vz-header__glass-band:nth-child(n + 2) { display: none; }

.vz-header--contained .vz-header__glass-band:nth-child(1) {
  --vz-glass-solid: 100%;
  --vz-glass-fade: 100%;
  border-radius: inherit;
  -webkit-backdrop-filter: blur(14px) saturate(var(--vz-glass-sat));
  backdrop-filter: blur(14px) saturate(var(--vz-glass-sat));
}

/* The tint keeps §4a's measured alphas — 86% at the top and 80% where the row's
   own text sits, which is the floor white-over-the-darkest-surface needs to hold
   the §4.1 body-text ratio. The shape changed; the material did not, and
   thinning it "because a floating bar reads lighter" would be trading a measured
   number for a taste. What ends is the run to transparent: with no tail below
   the bar, a gradient reaching 0% alpha at 100% would make the bottom of the
   pill see-through.

   The ring rides on this pseudo-element rather than on the wrapper because the
   wrapper's ::after is the last box in the subtree — an inset shadow on the
   wrapper itself would be painted under its own tint and disappear. --vz-bar-rule
   is already §4b's role for exactly this line; on this shape it resolves to
   --vz-color-rule and stays there, since the pill never leaves the light state.
   A hairline of that weight is drawn over the pill's own white glass rather than
   over the band, so it reads the same over all four surfaces. */
.vz-header--contained .vz-header__glass::after {
  border-radius: inherit;
  background: linear-gradient(to bottom,
    color-mix(in oklab, var(--vz-glass-tint) 86%, transparent) 0%,
    color-mix(in oklab, var(--vz-glass-tint) 80%, transparent) 100%);
  box-shadow: inset 0 0 0 1px var(--vz-bar-rule);
}

/* The three states where §4a takes the glass away and .vz-header paints its own
   fill: no backdrop-filter support, a visitor who has asked their OS to reduce
   transparency, and the open mobile panel. The element carrying the shape is
   display:none in all three, so the shape moves here — radius and ring both,
   and the bottom hairline back to transparent, because a straight line across
   the underside of a pill is the seam this shape exists to not have.

   Declared after §4a's own three blocks, which they have to be: .vz-header--contained
   and .vz-header are both (0,1,0) and source order is what separates them. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .vz-header--contained {
    border-radius: var(--vz-radius-pill);
    border-block-end-color: transparent;
    box-shadow: inset 0 0 0 1px var(--vz-bar-rule);
  }
}

@media (prefers-reduced-transparency: reduce) {
  .vz-header--contained {
    border-radius: var(--vz-radius-pill);
    border-block-end-color: transparent;
    box-shadow: inset 0 0 0 1px var(--vz-bar-rule);
  }
}

/* The open panel. The pill stays a pill and stays inset — the alternative was
   springing it back to full width on open, and a bar that changes shape under
   the thumb that just tapped it is a worse answer than a rounded white bar
   floating over a full-width ink sheet, which is a composition rather than a
   defect. §4b's :not([data-vz-nav-open="true"]) still suspends surface matching
   underneath it, so this bar is the light state's white and its ring is the
   light state's rule. */
.vz-header--contained[data-vz-nav-open="true"] {
  border-radius: var(--vz-radius-pill);
  border-block-end-color: transparent;
  box-shadow: inset 0 0 0 1px var(--vz-bar-rule);
}

/* The states are in the selector list because the boilerplate re-types every
   anchor on hover, focus and active — base.css §5 has the full account. Here it
   is the broken-image case that suffers: the alt text would go from 700 ink to
   the link font's weight, underlined, the moment a pointer crossed it. */
.vz-header__logo,
.vz-header__logo:is(:hover, :focus, :active) {
  display: flex;
  align-items: center;
  margin-inline-end: auto;   /* everything else sits right */
  /* If the image ever fails to load, the alt text is what renders. Without
     this it arrives as underlined default-blue link text. Through the role
     variable so the fallback text inverts with the bar (§4b) rather than
     staying ink on an ink surface. */
  color: var(--vz-bar-ink);
  font-weight: 700;
  text-decoration: none;
  transition: color var(--vz-dur) var(--vz-ease);
}
/* Sized by height with width:auto, so a re-uploaded logo of any proportion
   stays undistorted.

   The filter is §4b's brand-blue case and is `none` in every other state. It
   goes through the role variable rather than being declared on a
   [data-vz-surface] selector for the reason §4b gives: the states are declared
   in one place and the rules that consume them never learn they exist. The
   transition is on `filter` and not on the variable — an unregistered custom
   property is a token substitution, but the substituted-into PROPERTY still
   transitions when its computed value changes, which is the same mechanism
   --vz-bar-cta-bg uses below. Interpolating from `none` is well defined: the
   missing value is filled in with the identity of the other side's function
   list, so this cross-fades rather than snapping. */
.vz-header__logo img {
  block-size: 2rem;
  inline-size: auto;
  max-inline-size: 55vw;
  filter: var(--vz-bar-logo);
  transition: filter var(--vz-dur) var(--vz-ease);
}

/* Below 30em the pill doesn't fit next to the wordmark and the menu button —
   it lives in the panel instead (see .vz-header__nav-cta). */
.vz-header__inner .vz-header__cta { display: none; }

/* The bar's own button. base.css owns the pill — shape, fill, hover, the press
   — and this narrows the type to the bar's, nothing else (§4's
   --vz-bar-type-*). Two classes deep so it beats .vz-btn's own font-size
   whichever order the portal emits the two stylesheets in, which is the same
   call footer.module and hero.module make.

   Dropping to --vz-step--1 takes the pill from ~50px to ~44px, which is exactly
   the touch floor and no lower — hence the explicit minimum rather than trust in
   the em padding, which would follow the type down the next time either
   number moves.

   The states are in the selector list for the reason base.css §5 sets out at
   length: the boilerplate re-types every anchor on hover, focus and active.
   Three deep rather than two, because base.css's own state rule for .vz-btn is
   (0,2,0) and this has to outrank it in every state as well as at rest. */
.vz-header .vz-header__cta,
.vz-header .vz-header__cta:is(:hover, :focus, :active) {
  font-size: var(--vz-bar-type-size);
  font-weight: var(--vz-bar-type-weight);
  min-block-size: 2.75rem;

  /* The fill, taken off .vz-btn--primary and onto §4b's role variables. It has
     to leave base.css's hands entirely: the bar inverts under dark sections and
     §4.1's rule there is not a preference — on dark surfaces the action colour
     is lime, never the blue, which is 3.92:1 on the dark surface. base.css still owns
     the shape, the leading and the press. */
  background: var(--vz-bar-cta-bg);
  color: var(--vz-bar-cta-ink);
  transition: background-color var(--vz-dur) var(--vz-ease),
              color var(--vz-dur) var(--vz-ease);
}
/* Same (0,3,0) as the states selector above and later in the file, which is
   what carries it — both are in here, so source order is ours to decide. The
   hover shade would otherwise be flattened by the rest fill. */
.vz-header .vz-header__cta:is(:hover, :focus) {
  background: var(--vz-bar-cta-hover);
}

/* --------------------------------------------------------------------------
   5. Nav — the ink panel below 56em, an inline row above it
   -------------------------------------------------------------------------- */
/* Fixed, not absolute, and pinned to the viewport bottom: the panel has to
   fill exactly the space under the bar whether the announcement bar is on
   screen or already scrolled away. Sized off the bar instead (100dvh minus the
   bar) it overhangs the bottom of the screen by the announcement's height,
   which is what pushed the button below the fold.

   --vz-nav-top is the bar's own bottom edge, published on open by module.js —
   the page is scroll-locked while the panel is up, so it can't go stale. The
   fallback is the correct value at the top of the page, which is where a
   no-JS render would sit if the toggle could open at all. */
.vz-header__nav {
  display: none;
  position: fixed;
  inset-block-start: var(--vz-nav-top, calc(var(--vz-announce-h, 0px) + var(--vz-header-h)));
  inset-block-end: 0;
  inset-inline: 0;
  overflow-y: auto;
  padding: var(--vz-space-l) var(--vz-gutter) var(--vz-space-2xl);
  background: var(--vz-color-ink-deep);
  opacity: 0;
  translate: 0 -0.5rem;
  transition: opacity var(--vz-dur) var(--vz-ease-expo),
              translate var(--vz-dur) var(--vz-ease-expo),
              display var(--vz-dur) allow-discrete;
}
/* Column, so the button can be anchored to the bottom of a full-height panel:
   navigation at the top, the action within thumb reach. Without it the panel
   is two-thirds empty ink. */
.vz-header[data-vz-nav-open="true"] .vz-header__nav {
  display: flex;
  flex-direction: column;
  row-gap: var(--vz-space-l);
  opacity: 1;
  translate: 0 0;
}
/* Entrance for a display:none → block change. Where @starting-style isn't
   supported the panel simply appears — no half-state. */
@starting-style {
  .vz-header[data-vz-nav-open="true"] .vz-header__nav {
    opacity: 0;
    translate: 0 -0.5rem;
  }
}

.vz-header__nav-list {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Full-width rules between rows. Never an inline-start stripe. */
.vz-header__nav-item + .vz-header__nav-item {
  border-block-start: 1px solid var(--vz-color-rule-invert);
}
/* States in the selector list, here and again in §8, for the reason base.css
   §5 sets out: the boilerplate re-types every anchor on hover, focus and
   active, at a specificity that beats a bare class. In this panel that meant a
   21px row dropping from 700 to the link font's weight and gaining an underline
   under the thumb. */
.vz-header__nav-link,
.vz-header__nav-link:is(:hover, :focus, :active) {
  display: block;
  padding-block: 0.85rem;
  color: var(--vz-color-white);
  font-size: var(--vz-step-1);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: var(--vz-lh-heading);
  text-decoration: none;
}

/* --------------------------------------------------------------------------
   5a. The current section — module.js §4's aria-current, in the panel

   The mark is lime here because the panel is ink: --vz-color-accent is §4.1's
   dark-surface action colour, 13.6:1 on --vz-color-ink-deep, and it is already
   what the panel's focus ring uses. White at 700 is the resting row, so the
   only thing that changes is the hue — no weight step, no box, and specifically
   no inline-start stripe, which §5's rule above rules out for the rows and
   which would be a second alignment to keep true against the full-width rules.

   Selected on the attribute alone, so there is no class for module.js to keep
   in step with it. Both states are listed for the reason §5 gives: the theme
   boilerplate re-types every anchor at (0,1,1) on hover and focus, so a bare
   (0,1,0) resting rule loses the pointer — (0,2,0) and (0,3,0) here clear it
   whichever order the two stylesheets load in.

   The underline is the second signal, and it is here rather than colour alone
   because 1.4.1 does not accept hue as the only carrier of a state. In the
   panel it is drawn at rest; in the inline row (§8) the retracted underline
   the row already declares is simply coloured in. */
.vz-header__nav-link[aria-current],
.vz-header__nav-link[aria-current]:is(:hover, :focus, :active) {
  color: var(--vz-color-accent);
  text-decoration: underline;
  text-decoration-color: currentColor;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.32em;
}

/* auto pushes it to the bottom of the panel; align-self stops the column's
   default stretch from turning the pill into a full-width block. */
.vz-header__nav .vz-header__nav-cta {
  margin-block-start: auto;
  align-self: start;
}

/* Rows and the button rise in sequence as the panel opens — one gesture, not
   a fade per element. Reuses base.css's vz-rise keyframes. */
.vz-header[data-vz-nav-open="true"] .vz-header__nav-item,
.vz-header[data-vz-nav-open="true"] .vz-header__nav-cta {
  animation: vz-rise 420ms var(--vz-ease-expo) backwards;
  animation-delay: calc(60ms + var(--vz-i, 0) * 45ms);
}

/* Lime focus ring on the ink panel, the same switch base.css makes on dark
   sections. The extra element selector keeps this ahead of base.css's
   .vz-btn:focus-visible whichever order the two files load in. */
.vz-header__nav a:focus-visible,
.vz-header__nav .vz-btn:focus-visible {
  outline: 2px solid var(--vz-color-accent);
  outline-offset: 3px;
}

/* --------------------------------------------------------------------------
   6. Menu button — two bars that become an X. Transform only.
   -------------------------------------------------------------------------- */
.vz-header__toggle {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  margin-inline-end: -0.6rem;
  padding: 0;
  border: 0;
  border-radius: var(--vz-radius-s);
  background: none;
  color: var(--vz-bar-ink);
  cursor: pointer;
  transition: color var(--vz-dur) var(--vz-ease);
}

/* The bar's two bare focusables — this button and the wordmark. Same reason as
   the skip link's ring: neither is a .vz-btn or a .vz-link, and neither sits
   inside a .vz-section, so base.css's ring never reaches either and both fell
   back to whatever the browser draws — a different colour and a different
   shape from every other focusable thing on the page.

   --vz-bar-ring, so the ring makes the same light/dark switch here that
   base.css makes on .vz-section--bg-ink. Deep blue on an inverted bar is 2.2:1
   and is a focus indicator a keyboard visitor cannot see. */
.vz-header__logo:focus-visible,
.vz-header__toggle:focus-visible {
  outline: 2px solid var(--vz-bar-ring);
  outline-offset: 3px;
  border-radius: 2px;
}

.vz-header__burger {
  position: relative;
  inline-size: 1.375rem;
  block-size: 0.75rem;
}
.vz-header__burger::before,
.vz-header__burger::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  block-size: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: translate var(--vz-dur) var(--vz-ease),
              rotate var(--vz-dur) var(--vz-ease);
}
.vz-header__burger::before { inset-block-start: 0; }
.vz-header__burger::after  { inset-block-end: 0; }

.vz-header[data-vz-nav-open="true"] .vz-header__burger::before {
  translate: 0 0.312rem;
  rotate: 45deg;
}
.vz-header[data-vz-nav-open="true"] .vz-header__burger::after {
  translate: 0 -0.312rem;
  rotate: -45deg;
}

/* --------------------------------------------------------------------------
   7. From 30em: the pill returns to the bar, the panel's copy stands down
   -------------------------------------------------------------------------- */
@media (min-width: 30em) {
  .vz-header__inner .vz-header__cta { display: inline-flex; }
  .vz-header__nav .vz-header__nav-cta { display: none; }
}

/* With no nav links there is no menu button and therefore no panel, so the
   bar keeps the button at every width. */
.vz-header--no-nav .vz-header__cta { display: inline-flex; }

/* That configuration is the one tight fit in the whole header: on a 360px
   screen the wordmark plus a full-size pill needs about 5px more than the
   content box, and the pill ends up eating into the right gutter while the
   left one stays put. Two points off the wordmark buys the room back. Scoped
   to this configuration — everywhere else the wordmark keeps its full size. */
.vz-header--no-nav .vz-header__logo img { block-size: 1.75rem; }
@media (min-width: 30em) {
  .vz-header--no-nav .vz-header__logo img { block-size: 2rem; }
}
/* And back onto §8's step at the desktop breakpoint. This third rung is not
   redundant: §8 sets the same size at (0,1,1) and this chain is (0,2,1), and a
   media query contributes NO specificity — so without it the 30em rung above
   beats §8 at every width from 56em up and a nav-less header keeps a 32px
   wordmark where every other header has a 38px one. Nothing chose that; the
   override two rules above exists only for the 360px fit its comment describes,
   and it was leaking to 1440px. Found by measuring the configuration rather
   than by reading it, and asserted at all three rungs in header-nav-check.mjs
   so the chain cannot drift away from §8 again. */
@media (min-width: 56em) {
  .vz-header--no-nav .vz-header__logo img { block-size: 2.375rem; }
}

/* --------------------------------------------------------------------------
   8. From 56em: the panel becomes the inline nav row (comp §4)
   -------------------------------------------------------------------------- */
@media (min-width: 56em) {
  /* --vz-header-h steps to 4.75rem here. The step is declared in theme.css
     alongside the base value, because base.css's scroll-padding on the root element has
     to read the same number and a custom property never resolves upward. */
  .vz-header__logo img { block-size: 2.375rem; max-inline-size: none; }

  /* The row's two groups have to be told apart. At the mobile gap the last nav
     link sat 16px from the button while the links sat 24px from each other —
     the action read as a fifth nav item that happened to have a fill. The
     button now sits further from its neighbour than any two links do, so the
     row reads as "navigation, then the thing to press". The wordmark's own
     margin-inline-end:auto absorbs the rest, so this only ever separates nav
     from button.

     Both gaps are fluid rather than fixed, and that is a fit constraint as much
     as rhythm: this row turns on at 896px, where it has to hold a wordmark, up
     to six links and a button in about 820px of content width, and it is the
     one place in the library where running out of room clips rather than wraps
     — the dock is fixed, so an overflowing row does not even extend the
     document's scroll width to reveal itself. Tight at the breakpoint, open by
     1264px, where there is room to breathe. */
  .vz-header__inner { gap: clamp(var(--vz-space-m), 2.6vw, var(--vz-space-l)); }

  .vz-header__nav,
  .vz-header[data-vz-nav-open="true"] .vz-header__nav {
    display: flex;
    flex-direction: row;   /* resets the panel's column */
    align-items: center;
    gap: var(--vz-space-m);
    position: static;
    inset: auto;
    overflow: visible;
    padding: 0;
    background: none;
    opacity: 1;
    translate: none;
    transition: none;
  }

  .vz-header__nav-list {
    display: flex;
    align-items: center;
    /* The inner half of the pair above — always narrower than the row's own
       gap, so the button never reads as one more link. */
    gap: clamp(var(--vz-space-s), 1.9vw, var(--vz-space-m));
  }
  .vz-header__nav-item + .vz-header__nav-item { border-block-start: 0; }

  /* Same size and same weight as the button beside them (§4's --vz-bar-type-*),
     so the row reads as one set of controls rather than as small links next to a
     large pill.

     The padding is a pointer target, not spacing: at 0.25rem the link was a
     25px band inside a 76px bar, which is a lot of bar that looks clickable and
     isn't. 0.55rem takes it to ~36px without touching the gaps, because the
     gaps are the flex row's.

     The underline is declared at rest and only coloured in on hover, so nothing
     about the box changes when the pointer arrives — no reflow, and the rule
     lands on the text's own offset rather than at the bottom of the padding. It
     is the same retract-don't-appear idea as base.css's .vz-link, run the other
     way round because a nav row underlined at rest is a nav row that looks like
     four broken links. */
  .vz-header__nav-link,
  .vz-header__nav-link:is(:hover, :focus, :active) {
    padding-block: 0.55rem;
    color: var(--vz-bar-ink);
    font-size: var(--vz-bar-type-size);
    font-weight: var(--vz-bar-type-weight);
    letter-spacing: normal;
    /* base.css's .vz-btn leading, restated rather than inherited: the panel's
       heading leading is what this rule is overriding, and leaving it would put
       the links and the button label on two different line boxes for no reason
       anyone could name. */
    line-height: 1.2;
    text-decoration: underline;
    text-decoration-color: transparent;
    text-decoration-thickness: 2px;
    text-underline-offset: 0.32em;
    transition: color var(--vz-dur) var(--vz-ease),
                text-decoration-color var(--vz-dur) var(--vz-ease);
  }
  .vz-header__nav-link:hover {
    color: var(--vz-bar-link-hover);
    text-decoration-color: var(--vz-bar-underline);
  }

  /* The current section, in the row (§5a is the panel's version of this).

     It is the hover state, held: the same two role variables, so the mark
     follows the bar's surface the way everything else in §4b does — deep blue
     with a blue rule on light and cream, lime with a lime rule on brand and
     ink — and there is no fifth colour to measure against four backgrounds.
     Reusing hover also means the row never gains a state it didn't have: the
     current link simply arrives already wearing the one the pointer produces.

     Hovering the current link is therefore a no-op, which is correct. Hovering
     a DIFFERENT one still reads, because that link changes and this one does
     not — the row is answering the pointer, and the mark it can't distinguish
     itself from is the one under the pointer anyway. */
  .vz-header__nav-link[aria-current],
  .vz-header__nav-link[aria-current]:is(:hover, :focus, :active) {
    color: var(--vz-bar-link-hover);
    text-decoration-color: var(--vz-bar-underline);
  }

  /* Off §5's fixed lime and onto the role variable: the panel below this
     breakpoint is always ink, but this row sits in a bar that inverts, so the
     ring has to follow the surface rather than the breakpoint. Resolves to the
     same deep blue §5 was overriding for, and to lime again when the bar is
     dark. */
  .vz-header__nav a:focus-visible,
  .vz-header__nav .vz-btn:focus-visible { outline-color: var(--vz-bar-ring); }

  /* The panel entrance belongs to the panel only. */
  .vz-header[data-vz-nav-open="true"] .vz-header__nav-item,
  .vz-header[data-vz-nav-open="true"] .vz-header__nav-cta { animation: none; }

  .vz-header__toggle { display: none; }
}

/* --------------------------------------------------------------------------
   9. Reduced motion — the panel and the X arrive instantly
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .vz-header__nav { transition: none; }
  .vz-header[data-vz-nav-open="true"] .vz-header__nav-item,
  .vz-header[data-vz-nav-open="true"] .vz-header__nav-cta { animation: none; }
  .vz-header__burger::before,
  .vz-header__burger::after { transition: none; }
  .vz-header__nav-link,
  .vz-announce__dismiss { transition: none; }

  /* The surface switch arrives instantly rather than crossfading. A colour
     crossfade would be a defensible reduced-motion alternative on its own, but
     not a partial one: §9 already stands the nav row's colour transition down,
     and a tint still travelling while the links have finished is exactly the
     white-on-white interval §4b's shared --vz-dur exists to prevent. All of it
     or none of it, and under this query it is none. */
  .vz-header,
  .vz-header__logo,
  .vz-header__logo:is(:hover, :focus, :active),
  .vz-header__toggle,
  .vz-header::after,
  .vz-header .vz-header__cta,
  .vz-header .vz-header__cta:is(:hover, :focus, :active) { transition: none; }
}
