/* ========== HEADER ========== */
/* position:fixed takes header out of the flow entirely (unlike the
   previous sticky), so it no longer inherits .page's padding as an
   ancestor - it needs its own padding, matching .page's mobile padding
   (22px top / 28px sides) exactly, to keep the logo/nav at the same
   spot on screen. */
header {
  position: fixed;
  top: var(--header-top-offset);
  left: 0;
  width: 100%;
  z-index: 20;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--muted);
  padding: 22px 28px 0;
}

/* Fade zone for the fixed header: content scrolling underneath dissolves
   via blur instead of getting cut off by a hard edge or darkened by a
   tint. No background/color here on purpose - backdrop-filter blur is
   the only effect, and mask-image fades its opacity from full (top) to
   none (bottom of the transition band), so it reads as content
   optically defocusing rather than a shaded bar.
   top:0 + height:calc(100% + 180px): covers the header's own full height
   (100%, already responsive per breakpoint via header's own padding)
   PLUS the 180px transition band below it - not just that band alone.
   180px (was 52px) gives the mask-image gradient far more vertical room to
   ramp up gradually, so scrolling content is already visibly blurring well
   before it reaches the header's bottom edge, instead of only in the last
   few pixels right before it slides underneath.
   Header has no opaque background, so without this the header's own
   rectangle was a blur-free gap content could scroll through untouched
   before hitting the band underneath, hence the hard overlap with the
   logo/nav.
   top is negated --header-top-offset (not 0): header itself now starts
   --header-top-offset below the viewport top, but this blur zone must
   still start at the actual viewport top (y=0), or that sliver above
   the header would show unblurred content scrolling past. */
header::after {
  content: "";
  position: absolute;
  top: calc(-1 * var(--header-top-offset));
  left: 0;
  right: 0;
  height: calc(100% + 180px);
  pointer-events: none;
  /* Negative, not positive: keeps this behind header's own non-positioned
     children (logo/nav/text paint above it per normal stacking order)
     while the header subtree as a whole still sits above the scrolling
     .hero/.sections content, via header's own z-index:20. */
  z-index: -1;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
}

.logo-link {
  display: block;
  /* Anchors .logo-shine (desktop-only, see the @media (min-width: 768px)
     block below) - absolute inset:0 there needs a positioned ancestor
     to align exactly over .logo-img. Harmless on mobile too, where
     .logo-shine has no styling at all (untargeted bare <span>, zero
     size, fully inert). */
  position: relative;
}

/* opacity/transition mirror .rc-logo's own rest/hover pattern (see
   css/footer.css) for visual consistency between the two logos. */
.logo-img {
  display: block;
  height: 25px;
  width: auto;
  opacity: 0.55;
  transition: opacity 0.3s ease;
}

.logo-link:hover .logo-img {
  opacity: 1;
}

.header-mid {
  display: none;
}

.header-right {
  display: flex;
  gap: 18px;
  align-items: center;
}

.socials, .nav {
  display: flex;
  gap: 12px;
}

.socials a, .nav a {
  color: var(--muted);
  text-decoration: none;
  transition: color 0.2s;
}
.socials a:hover, .nav a:hover { color: #fff; }

/* Mobile-only: shrinks the VORTICOSO logo ~20% from its 35px base -
   read too large relative to the nav/contact text at narrow widths.
   Desktop/tablet keep the unchanged 35px (set explicitly again in the
   @media (min-width: 768px) block below). */
@media (max-width: 767px) {
  .logo-img {
    height: 23px; /* was 28px - another ~18% down, so it stops crowding
      WORK/ABOUT/RC + LI/TW/SC/YT on the same row at narrow iPhone widths
      (header's own justify-content: space-between left zero slack
      between the two at 28px and 375px wide - see git history). */
  }

  /* .logo-img above is dead/orphaned CSS now (no element carries that
     class since the metallic hover-fx markup change - see index.html)
     but left in place rather than deleted, matching "don't touch
     anything else." .logo-sizer is the element that actually needs this
     same mobile-narrower size today, for the exact same reason. */
  .logo-sizer {
    height: 23px;
  }

  /* The logo alone wasn't enough: at exactly 375px (a real, common iPhone
     width - SE/12/13 mini) the nav+socials group still didn't fit on one
     row even at the smaller logo above, and being flex children with the
     default flex-shrink:1, they silently compressed instead of visibly
     overflowing - each nav <a>'s own "+ WORK"-style text then wrapped
     mid-string (the "+" stranded on its own line, the word dropping to a
     second line). Tightening .nav/.socials' own internal gap (12px -> 6px,
     desktop-shared base value, overridden here rather than changed at the
     source) removes enough width pressure to keep every item on one line
     down to 375px even with .header-right's own gap enlarged below -
     desktop (768px+) keeps the original 12px, untouched. */
  .socials, .nav {
    gap: 6px;
  }

  /* .header-right's own gap is the seam BETWEEN the two groups (.nav vs
     .socials - two distinct containers, not a shared flat list; see
     index.html), not inside either one - a first attempt at 10px read as
     barely different from the 8px/6px used *inside* each group, so the
     two groups never read as visually separate, just as one longer run
     of tighter-spaced links. 28px (roughly the width of one link) makes
     that seam unambiguous. The .nav/.socials tightening above is what
     keeps this fitting on one line down to 375px without it. */
  .header-right {
    gap: 28px;
  }

  /* TW (Twitter/X) hidden on mobile only - element stays in the DOM
     (desktop still shows it via the @media (min-width: 768px) tier,
     untouched). display:none removes it from the flex layout entirely,
     so .socials' own gap simply closes up around LI/SC/YT with no
     leftover slot - not visibility:hidden, which would leave its space
     reserved. */
  .socials a:nth-child(2) {
    display: none;
  }
}

/* ========== DESKTOP / TABLET ========== */
@media (min-width: 768px) {
  header {
    display: grid;
    /* minmax(0, 1fr), not bare 1fr: 1fr alone carries an implicit
       minmax(auto, 1fr) floor, so a column can never shrink below its
       own content's min-content width. Between 768px and ~900px
       column 4 (nav+socials, "+ WORK + ABOUT + RC" / "LI TW SC YT")
       needed ~192px minimum and took it from the other 3 columns
       instead of sharing evenly, wrapping "AVAILABLE FOR FREELANCE"
       onto an extra line and dropping nav/socials onto separate rows -
       confirmed via testing with every script on the page disabled, so
       purely this track-sizing default, unrelated to any JS. minmax(0,
       1fr) removes that auto floor, letting all 4 columns shrink
       together and stay equal-width down through that range; the
       actual text inside each column still wraps on its own via
       normal inline layout exactly as before, only the column-width
       distribution changes. */
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: 24px;
    align-items: start;
    font-size: 11px;
    padding: 28px 48px 0;
  }

  .logo-img { height: 35px; }

  /* CSS Grid's default justify-items:stretch (header's own grid, above)
     otherwise stretches .logo-link to fill its whole 1fr column
     (~314px) instead of hugging its actual content width (~144px,
     .logo-img's own intrinsic size at height:35px) - invisible before
     .logo-shine existed (a replaced element like <img> with width:auto
     sizes to its own intrinsic width regardless of an oversized
     containing block, so .logo-img always rendered at its correct
     144px regardless), but .logo-shine's inset:0 below sizes to
     .logo-link's own (stretched) box, not .logo-img's - without this,
     the shine layer/mask would render ~2x too wide and misaligned. */
  .logo-link {
    justify-self: start;
  }

  /* Desktop-only shine-sweep layer for js/header-entrance.js's final
     swipe glow, run once at the very end of the header entrance
     sequence. inset:0 on a .logo-link (position:relative, see base
     rule) with no padding of its own lines this up exactly over
     .logo-img, matching its size 1:1.
     - background: a thin diagonal bright band (115deg) flanked by
       transparent - the classic "shine" gradient - on a background-size
       3x the layer's own width, so the band can sit fully off-screen
       left/right at the animation's start/end instead of clipping
       abruptly at the edges.
     - mask-image: the logo's OWN png as the mask source - the browser
       reads its alpha channel, so the gradient only ever paints where
       the logo's lettering is actually opaque, never spilling into the
       transparent space around/between the letters.
     - background-position-x starts at 150% (fully off-screen, JS
       animates it down to -150% for a left-to-right sweep) and opacity
       starts at 0 - both are also this layer's permanently-idle rest
       state if JS never runs (fails to load, etc.): fully invisible, no
       artifact. Opacity (not just position) now carries the sweep's
       own intensity curve too - js/header-entrance.js animates it
       0 -> 1 -> 0 with its own rise/fall easing, independent of the
       position tween, rather than a flat/constant visibility riding
       along with the position. */
  .logo-shine {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0;
    background: linear-gradient(115deg, transparent 40%, rgba(255,255,255,0.6) 50%, transparent 60%);
    background-size: 300% 100%;
    background-repeat: no-repeat;
    background-position-x: 150%;
    -webkit-mask-image: url('../images/vrtz_logo_01.png');
    mask-image: url('../images/vrtz_logo_01.png');
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
  }

  /* display:contents unwraps this div so its two children
     (contact / direction blocks) - AND now the yt-widget, a 3rd child,
     see index.html/css/header.css's own #ytWidget rule below - become
     direct grid items in their own right, landing in columns 2/3(/3
     again for the widget) of the header grid on their own. */
  .header-mid {
    display: contents;
  }
  /* :not(.yt-widget) - the widget brings its own text-align/line-height
     via css/yt-audio-widget.css; without this exclusion it would
     inherit this rule's line-height:1.55 into .yt-title/.yt-sub, which
     never had to account for that spacing before (the widget used to
     live outside .header-mid entirely). */
  .header-mid > div:not(.yt-widget) {
    text-align: left;
    line-height: 1.55;
  }
  .header-mid strong {
    color: #fff;
    font-weight: 400;
    display: block;
    margin-bottom: 1px;
  }

  /* grid-column/grid-row on the FREELANCE div - defensive, kept even
     though the widget no longer shares this column (see .nav #ytWidget
     below for its current home): matches exactly where this div
     already auto-places on its own, so it's a no-op either way, and
     removing it isn't necessary for anything below to work. */
  .header-mid > div:nth-child(2) {
    grid-column: 3;
    grid-row: 1;
  }

  /* 4th grid column: social block flush left, WRK/ABT flush right */
  .header-right {
    justify-content: space-between;
    gap: 0;
  }
}

/* THE SECRETS Radio widget (js/yt-audio-widget.js,
   css/yt-audio-widget.css) - a direct child of .header-right (see
   index.html's own comment there for why NOT .nav: .nav gets a
   one-shot clip-path curtain-reveal from js/header-entrance.js at page
   load, and that clip-path's own final "open" value still clips any
   position:absolute descendant extending outside .nav's own box - a
   real, confirmed-by-testing bug, not a hypothetical). position:relative
   here anchors the widget's own position:absolute below - doesn't
   affect .header-right's own flex-row layout otherwise (relative
   positioning with no top/left offset never moves the element itself). */
.header-right {
  position: relative;
}
/* position:absolute removes the widget from .header-right's own flex
   row entirely, so nav/socials' own spacing is completely unaffected by
   its presence, regardless of the widget's own width (auto/compact
   while minimized, 300px once opened - see css/yt-audio-widget.css).
   left:0 lines its own left edge up with .header-right's own left edge -
   which is exactly .nav's own left edge too (nav is header-right's
   flush-left child via justify-content:space-between, no padding on
   either), and exactly "+ WORK"'s own left edge in turn (nav's first
   child) - directly below it, not the group's center or any other
   link. top:100% + margin-top clears the nav/socials row's own text
   before the widget starts - header-right's own height matches nav's
   exactly (align-items:center over equal-height children), so this
   lands at the identical y position .nav's own top:100% would have. */
#ytWidget {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 6px;
}

/* Mobile: previously the widget lived inside .header-mid, which is
   display:none on mobile (base rule, above) - it inherited that
   invisibility for free. Now that it lives inside .nav (visible on
   mobile, carefully fitted to one row across many earlier rounds of
   work - see that section's own comments), this explicit rule is what
   keeps that same mobile-hidden behavior in place; without it the
   widget would suddenly render on top of the tightly-fit WORK/ABOUT/RC
   + LI/TW/SC/YT row for the first time, an unrelated regression this
   move should not introduce. position:absolute already means it can't
   affect that row's own flex fit even so, but the intent has always
   been desktop-only for this feature. */
@media (max-width: 767px) {
  /* Visible on mobile, same anchor as desktop: under + WORK
     (absolute top:100% left:0 relative to .header-right) */
  #ytWidget {
    display: block !important;
    position: absolute !important;
    top: 100% !important;
    left: 0 !important;
    right: auto !important;
    bottom: auto !important;
    margin-top: 6px !important;
    z-index: 10050;
    max-width: calc(100vw - 24px);
  }
}

@media (min-width: 1100px) {
  /* Matches .page's padding growth at this breakpoint - header is fixed
     (out of .page's flow) so it needs the same values mirrored directly. */
  header {
    padding: 32px 56px 0;
  }
}

/* Pre-existing bug, separate from the grid-template-columns fix above:
   even with all 4 header columns equal width (repeat(4, minmax(0,
   1fr))), .nav ("+ WORK + ABOUT + RC") and .socials ("LI TW SC YT")
   together need ~192-234px to sit on one row at their base 12px gap/
   11px font-size (inherited from header) - more than their equal 1/4
   share of the header's own width provides anywhere from 768px up to
   ~1150-1199px, so each wraps internally (a single link's own "+" and
   its word landing on separate lines - min-width:auto on a flex item
   permits wrapping at its own text's word boundaries once shrunk that
   far). Confirmed via testing to be present identically whether
   js/header-hover.js runs or not - unrelated to that feature.
   Only fix scoped to this exact range: 1200px+ already fits with the
   base 12px/11px values (confirmed by testing - untouched, no rule
   here reaches it) and below 768px .nav/.socials use the flex, not
   grid, header layout entirely (a different, already-tuned gap:6px
   override - css/header.css's own base rules above, untouched).
   gap:6px/font-size:7px here is the largest (least-shrunk) combination
   that still keeps both on one line at this range's own tightest point
   (768px, where the shared column is narrowest) - confirmed by testing
   smaller reductions were unnecessary and gap:7px or font-size:7.5px+
   both still wrapped at 768px. */
@media (min-width: 768px) and (max-width: 1199px) {
  .nav,
  .socials {
    gap: 6px;
    font-size: 7px;
  }
}

/* ========== LOGO HOVER FX (metallic gold/brass) ========== */
/* CSS-only. Replaces an earlier cyan/magenta neon-glow version (removed
   in full: that one's .logo-base transition/filter/animation,
   .logo-sweep, and the logo-pulse/logo-sweep-anim keyframes are gone,
   not just hidden). .logo-hover-fx is the same dedicated wrapper nested
   INSIDE .logo-link (see index.html) as before - independent
   position:relative/display:inline-block, so nothing here touches
   .logo-link's own rules or any other existing selector. */
.logo-hover-fx {
  position: relative;
  display: inline-block;
  /* Without this, .logo-hover-fx (an inline-level box) sits on its
     surrounding inline formatting context's baseline inside .logo-link
     (display:block, untouched) - which reserves a few extra px below it
     for font descenders, inflating .logo-link's own measured height by
     ~4px (39px instead of the logo's real 35px) even though nothing
     visible actually grew. vertical-align:top removes that baseline
     allowance without changing the display:inline-block the effect
     needs. */
  vertical-align: top;
  /* perspective belongs on the PARENT of whatever receives the 3D
     rotateX/rotateY (.logo-tilt below, driven by js/logo-tilt.js) for
     that transform to render with real foreshortening instead of a
     flat, unconvincing skew. */
  perspective: 300px;
}

/* Mask source: the logo's own png - its alpha channel keeps every layer
   below confined to the actual letterforms, never spilling into the
   transparent space around/between them. Same file .logo-shine (the
   separate, JS-driven, load-time swipe - untouched, still present in
   index.html) already uses as its own mask. */
.logo-hover-fx {
  --logo-mask: url('../images/vrtz_logo_01.png');
}

/* .logo-sizer (the real <img>, src/alt unchanged) is the visible logo
   at rest again - opacity restored to 0.55, the ORIGINAL pre-effects
   value .logo-img's own base rule used (still further up this file,
   now orphaned since no element carries that class anymore - .logo-img
   itself was never edited, only stopped being referenced). Crossfades
   to 0 on hover (see .logo-hover-fx:hover .logo-sizer below) as
   .metal-fill crossfades in over it, via the matching transition on
   both - no keyframe animation on either side of that swap, so neither
   direction can snap.
   height/width: unrelated to the opacity fix, still needed regardless -
   without it this <img> renders at the source PNG's own real natural
   size (400x150) instead of the previously-rendered 144x35. */
.logo-sizer {
  display: block;
  height: 35px;
  width: auto;
  opacity: 0.55;
  transition: opacity 0.45s ease;
}

/* Mobile logo — MUST be after base .logo-sizer or cascade loses to height:35px */
@media (max-width: 767px) {
  img.logo-sizer {
    height: 25px !important;
    width: auto !important;
  }
}

/* Rigid tilted surface: .metal-fill + the two specular layers below all
   sit inside this one wrapper, so js/logo-tilt.js only ever has to set
   ONE transform (on .logo-tilt itself) to tilt all three together,
   rather than repeating it per layer. position:absolute + inset:0
   matches .metal-fill's own old sizing exactly (relative to
   .logo-hover-fx, same as before this wrapper existed), and being
   absolutely positioned itself, .logo-tilt is already a valid
   containing block for its own absolutely-positioned children below -
   no separate position:relative needed on top of that.
   transition (not just letting JS snap the value) is what makes the
   tilt itself glide back to flat on mouseleave instead of cutting -
   js/logo-tilt.js's own mousemove updates happen far more often than
   this transition's duration, so continuous tracking still reads as
   instant/direct, only the final return-to-rest is eased. */
.logo-tilt {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transition: transform 0.3s ease-out;
  will-change: transform;
}

/* Invisible at rest (opacity:0) - only .logo-sizer (the plain logo)
   shows until hover. Hovering crossfades this in over 0.45s (matching
   .logo-sizer's own fade-out above) while metal-shift starts its
   separate, continuous background-position drift - the only motion
   kept from the previous, non-tilting version of this effect. */
.metal-fill {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: linear-gradient(115deg,
    #4a4a4d 0%, #d4d4d7 10%, #f8f8fa 18%, #8c8c90 28%,
    #2c2c2e 40%, #e2e2e5 52%, #ffffff 60%, #5c5c60 70%,
    #b0b0b4 82%, #3a3a3c 100%);
  background-size: 240% 100%;
  background-position: 30% 0;
  -webkit-mask-image: var(--logo-mask, none);
  mask-image: var(--logo-mask, none);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  filter:
    drop-shadow(0 1px 0 rgba(210,210,215,0.9))
    drop-shadow(0 2px 0 rgba(170,170,175,0.87))
    drop-shadow(0 3px 0 rgba(130,130,135,0.83))
    drop-shadow(0 4px 0 rgba(90,90,95,0.78))
    drop-shadow(0 5px 0 rgba(60,60,64,0.72))
    drop-shadow(0 8px 7px rgba(0,0,0,0.6));
  transition: opacity 0.45s ease, background-position 0.6s ease, filter 0.4s ease;
}

.logo-hover-fx:hover .logo-sizer {
  opacity: 0;
}
/* filter here (a single, simpler drop-shadow) still overrides the
   richer multi-layer bevel set on .metal-fill's own base rule above the
   instant hover starts, same as it already did before this color swap -
   untouched, wasn't part of what this update asked for. Flagging it:
   the new stacked bevel is consequently never actually visible while
   hovering (the only time .metal-fill is opacity:1 at all) - let me
   know if this hover override should be updated to match, or removed,
   if that's not the intended result. */
.logo-hover-fx:hover .metal-fill {
  opacity: 1;
  filter: drop-shadow(0 3px 4px rgba(0,0,0,0.75));
}
/* No more auto-looping animation here - js/logo-tilt.js now drives
   .metal-fill's own background-position directly from the cursor, same
   as .spec-1/.spec-2, so all three layers move together under direct
   mouse control instead of the gold fill drifting on its own timer. */

/* Two mouse-reactive specular highlight layers - js/logo-tilt.js sets
   each one's own background-position-x directly (per-frame, on
   mousemove), independent of .logo-tilt's rotateX/rotateY transform
   above. Only their opacity (visible/hidden) is CSS-driven, via the
   hover rule below. */
.spec-1,
.spec-2 {
  position: absolute;
  inset: 0;
  background-size: 300% 100%;
  -webkit-mask-image: var(--logo-mask, none);
  mask-image: var(--logo-mask, none);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.spec-1 {
  background-image: linear-gradient(-70deg, transparent 44%, rgba(255,255,255,0.98) 50%, transparent 56%);
  mix-blend-mode: overlay;
}
.spec-2 {
  background-image: linear-gradient(-70deg, transparent 47%, rgba(210,230,255,0.65) 50%, transparent 53%);
  mix-blend-mode: screen;
}

.logo-hover-fx:hover .spec-1,
.logo-hover-fx:hover .spec-2 {
  opacity: 1;
}

/* Inner edge/liseré: two copies of the SAME logo-shape mask layered on
   one white fill - one held at a fixed "center" position, one offset by
   --edge-pos (js/logo-tilt.js, driven by cursor position relative to
   the logo's own center). mask-composite: subtract (layer1 MINUS
   layer2) leaves visible only the thin sliver where the offset copy no
   longer overlaps the fixed one - a ring a few px wide that shifts
   toward whichever edge the offset leans, reading as an inner rim
   catching the light as the cursor moves. -webkit-mask-composite:
   source-out is the older/WebKit compositing-model equivalent for the
   same two-layer setup - support for either varies by engine version,
   flagged for a render check (see the request this came from). */
.edge {
  position: absolute;
  inset: 0;
  background: #ffffff;
  -webkit-mask-image: var(--logo-mask, none), var(--logo-mask, none);
  mask-image: var(--logo-mask, none), var(--logo-mask, none);
  -webkit-mask-size: contain, contain;
  mask-size: contain, contain;
  -webkit-mask-repeat: no-repeat, no-repeat;
  mask-repeat: no-repeat, no-repeat;
  -webkit-mask-position: center, center;
  mask-position: center, var(--edge-pos, center);
  mask-composite: subtract;
  -webkit-mask-composite: source-out;
  opacity: 0;
  transition: opacity 0.45s ease;
  pointer-events: none;
}
.logo-hover-fx:hover .edge {
  opacity: 1;
}

/* 4 fixed cross-hair "glints" - each one's own opacity/scale is driven
   entirely by js/logo-tilt.js from how close the cursor's horizontal
   position is to that glint's own trigger zone (glint1X..glint4X/
   glint4XRight below), not by :hover alone - so they default to
   opacity:0 here and are never touched by a plain hover rule. */
.glint {
  position: absolute;
  width: 46px;
  height: 2px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.95) 50%, transparent);
  transform: translate(-50%, -50%);
  opacity: 0;
  pointer-events: none;
  mix-blend-mode: screen;
}
.glint::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2px;
  height: 46px;
  background: linear-gradient(180deg, transparent, rgba(255,255,255,0.95) 50%, transparent);
  transform: translate(-50%, -50%);
}
.glint::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 14px;
  height: 14px;
  background: radial-gradient(circle, rgba(255,255,255,0.9) 0%, transparent 70%);
  transform: translate(-50%, -50%);
}
.glint-1 { left: 58%; top: 10%; }
.glint-2 { left: 94%; top: 78%; width: 56px; height: 1.5px; }
.glint-2::before { width: 1.5px; height: 56px; }
.glint-2::after { width: 14px; height: 14px; opacity: 0.55; }
.glint-3 { left: 52%; top: 4%; width: 62px; height: 1px; }
.glint-3::before { width: 1px; height: 62px; }
.glint-3::after { width: 10px; height: 10px; }
.glint-4 { left: 4%; top: 18%; width: 82px; height: 2px; }
.glint-4::before { width: 2px; height: 82px; }
.glint-4::after { width: 18px; height: 18px; }

/* ========== HEADER BLOCK HOVER FILL (blocks 2/3/4) ========== */
/* Full-viewport-height solid layer that sweeps in left-to-right on
   hover over the contact / freelance / nav+socials blocks, matching
   the solid-black column treatment shown in
   reference/layout-_sub-page_ref.png. Markup (.header-hover-fill) is
   created and inserted by js/header-hover.js as a DIRECT CHILD of
   <header> itself (its first child) - deliberately NOT nested inside
   .header-mid's own div children (confirmed by testing: those 2 divs
   are among js/header-entrance.js's staggerGroups targets, and that
   script's onComplete only clearProps's `transform`, not the
   `clip-path: inset(0% 0% 0% 0%)` it also animates - so every page
   load leaves that exact clip-path permanently inline on both divs.
   Mathematically a no-op clip (0 inset on every side) but clip-path
   still clips to the ELEMENT'S OWN border box regardless, silently
   truncating any descendant - including an absolutely/fixed-positioned
   one meant to extend to 100vh - to that div's own ~52px content
   height. Not our animation to alter (see the task's own instruction
   not to touch header-entrance.js), so the fill lives outside that
   subtree entirely instead).
   Desktop-only: these 3 blocks only exist as real grid columns once
   header switches to display:grid at 768px+ (css/header.css's own base
   rule) - mobile stacks .header-right in a flex row with no equivalent
   column to size against, and .header-mid is display:none there
   entirely (js/header-hover.js also no-ops below 768px, so this rule
   being desktop-scoped is belt-and-suspenders, not load-bearing).
   position:fixed, direct child of <header>: left/width/top are set
   inline by JS (js/header-hover.js) from the corresponding block's own
   live getBoundingClientRect() - the real current grid column, not a
   value guessed here.
   Stacking order (bottom to top): header::after's blur layer, THEN
   this fill, THEN the block's own text - requires 2 different
   z-index tiers, not just 1, because z-index is an integer and there
   is no integer between header::after's -1 and the fill's own
   requirement to sit above it: any negative z-index (however close to
   0) still paints as part of the SAME "negative z-index" paint tier,
   which is a single block always painted as a whole relative to
   non-negative content, so tying/beating header::after there was never
   possible without changing ITS z-index too (not allowed - see below).
   Instead this fill uses z-index:0 (paints in the "0/auto" tier,
   always after/above the ENTIRE negative tier header::after belongs to
   - z-index:-1 on header::after is untouched, still exactly what the
   task asked to leave alone) while the 3 target blocks below get
   z-index:1 (paints after/above the "0/auto" tier this fill is in).
   Net order: header::after(-1) < fill(0) < block text(1) - matches the
   requested stacking exactly, and neither number touches
   header::after's own -1. */
@media (min-width: 768px) {
  /* background: 20% opaque black, not solid - and backdrop-filter blurs
     whatever's visible through it (hero imagery, .sec content, etc.),
     same principle as the yt-widget's own translucent panel
     (css/yt-audio-widget.css's own .yt-widget-inner). blur(16px) is
     that exact same value, reused as asked rather than a new one -
     checked visually at this much larger (100vh-tall) surface and it
     still reads as a clean, coherent blur, no adjustment needed. */
  .header-hover-fill {
    position: fixed;
    top: 0;
    height: 100vh;
    background: rgba(0, 0, 0, 0.2);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
    pointer-events: none;
    z-index: 0;
    will-change: transform;
  }

  /* Promotes the 3 target blocks' own text above this fill (see the
     z-index tier explanation above) - position:relative is what lets
     z-index apply at all; .header-right already carries
     position:relative from its own pre-existing, untouched rule
     further up this file, so only its z-index is new here. Harmless
     side effect, checked: this also gives .header-right its own local
     stacking context, which re-scopes #ytWidget's z-index:999
     (css/yt-audio-widget.css) to compare only within .header-right's
     own children (nav/widget/socials) instead of the whole header -
     those are the only elements it ever visually overlaps anyway, so
     the widget still renders above nav/socials exactly as before. */
  .header-mid > div:nth-child(1),
  .header-mid > div:nth-child(2),
  .header-right {
    position: relative;
    z-index: 1;
  }

  /* Opens with transform-origin:left (scaleX 0->1 sweeps its right edge
     rightward, left edge pinned) - js/header-hover.js flips
     transform-origin to "right" right before removing this class on
     mouseleave, so the SAME transition (same duration/easing) then
     shrinks scaleX 1->0 with its right edge now pinned instead: the
     left edge is what moves, and it moves rightward too, so closing
     reads as the sweep continuing in the same direction rather than
     reversing. */
  .header-hover-fill.is-open {
    transform: scaleX(1);
  }

  /* Blocks 2/3's fills only (js/header-hover.js adds this modifier
     class to whichever configs opt into mergeHoverZone - currently
     both - block 4's fill stays plain .header-hover-fill,
     pointer-events:none, untouched) - makes the whole black area
     itself part of the same hover zone as the trigger and the overlay
     content below (enlarged text on block 2, the logo on block 3), so
     moving the mouse straight down from the small trigger through the
     fill toward that content never crosses a non-interactive gap that
     would otherwise close it. See js/header-hover.js's own mouseenter/
     mouseleave counter for how the 3 zones (trigger + this fill +
     .header-hover-content) combine. */
  .header-hover-fill--interactive {
    pointer-events: auto;
  }

  /* Blocks 2/3 only enlarged-content overlay (block 2: name/email/
     phone; block 3: the vrtz_logo_02.png mark) - js/header-hover.js
     creates each as ANOTHER direct child of <header> (like the fill),
     appended LAST (not prepended like the fills) so it paints above
     both the fill (z:0 tier) and the block's own promoted text (z:1
     tier, same DOM-order tie-break rule) without needing a higher
     z-index number. left/width/padding-left are set inline by JS. left
     matches the fill's own (block's rect.left minus LEFT_PAD, see
     js/header-hover.js), and padding-left is set to that exact same
     LEFT_PAD value - not a separate guessed number - so the padded
     content edge lands back at precisely the ORIGINAL block's own
     rect.left, i.e. the same x its compact text itself starts at
     today, left-aligned to match it exactly rather than centered or
     offset. bottom:10vh anchors it a fixed 10% of the CURRENT viewport
     height up from the bottom, auto-sizing to its own content; being a
     vh unit this tracks any window height with zero JS math, on load
     or live resize alike, and both blocks share this exact rule so
     their 2 overlays land at the identical vertical height when
     compared side by side. pointer-events:auto (was none on block 2's
     text originally) + no user-select rule anywhere in this codebase
     targets it (checked - only the yt-widget's own drag handle sets
     user-select:none, unrelated) - together that's what makes block
     2's text normally selectable/copyable by click-drag, same as any
     other page text (irrelevant for block 3's plain image, but shared
     harmlessly since both use this one rule).
     clip-path mask-reveal, not opacity - sweeps left-to-right on open
     and closes in that SAME direction rather than reversing, exactly
     like the black fill's own scaleX/transform-origin technique above,
     just expressed through clip-path's independent left/right insets
     instead (js/header-hover.js's own CLIP_* constants/comments have
     the full mechanics) - reused because animating opacity here would
     have been "a simple fade," which this was explicitly asked not to
     be, while clip-path never scales/distorts the pixels the way the
     fill's own scaleX would if reused directly on text/a logo.
     transition-delay 150ms (chosen, not specified) - starts the
     content sweep a beat after the black fill's own (which has no
     delay) so the motion reads as the black surface arriving first and
     the branding following into the space it just opened, rather than
     both sweeping in perfectly simultaneously; applied to both open
     AND close for simplicity (one rule, not two asymmetric ones) - on
     close this reads as the black fill leading the retreat too, which
     stays visually coherent with the same "fill leads" logic. */
  .header-hover-content {
    position: fixed;
    bottom: 10vh;
    display: flex;
    flex-direction: column;
    gap: 6px;
    text-align: left;
    pointer-events: auto;
    clip-path: inset(0 100% 0 0%);
    transition: clip-path 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 150ms;
    z-index: 1;
  }
  /* font-family matches .title (css/hero.css) - the hero's own big
     "Visual-System Interface" headline - var(--font-display) resolves
     to 'Space Grotesk' (css/base.css), reused as-is rather than a new
     value. font-weight:700 on the name mirrors .title's own weight too
     ("si pertinent" - relevant for this, the "gros texte" the request
     points at); the smaller detail lines below it keep the default
     weight, same as before, since .title has no equivalent secondary-
     text weight to match against. line-height on both (0.92/1.05, down
     from 1.05/unset-normal) is the tighter interlignage requested -
     "FRANÇOIS"/"DEVAUCHELLE" (the name wraps to 2 lines) sit closer
     together, and the email/phone lines below tighten to match, on top
     of the container's own gap shrinking (12px -> 6px) between the 3
     text blocks overall. */
  .header-hover-content .hhc-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 56px;
    line-height: 0.92;
    color: #fff;
    /* Tighter than the 0.02em this would otherwise inherit from `header`
       (css/header.css's own base rule, line 18) - set here (not on the
       shared .header-hover-content parent) so the em resolves against
       THIS element's own 56px font-size, not the 10px it would inherit
       from `header` if set further up. Scoped to this class only - the
       base header rule, and every other header element that relies on
       it, stays untouched. */
    letter-spacing: -0.01em;
  }
  .header-hover-content .hhc-line {
    font-family: var(--font-display);
    font-size: 20px;
    line-height: 1.05;
    color: var(--muted);
    /* Same reasoning as .hhc-name above - resolved against this
       element's own 20px font-size. */
    letter-spacing: -0.01em;
  }
  /* Block 3's logo mark - width fixed, height:auto preserves the
     source PNG's own 262x256 aspect ratio (via its width/height HTML
     attributes, js/header-hover.js) so it never looks stretched. 90px
     wide: sized to stay clearly visible without approaching the
     narrowest column this fill ever renders at (~150px net width at
     the 768px breakpoint, css/header.css's own grid-template-columns)
     while still leaving comfortable margin either side. */
  .header-hover-content .hhc-logo {
    display: block;
    width: 90px;
    height: auto;
  }

  /* Self-fade for whichever block is CURRENTLY hovered - its own
     compact text dims (block 2, fully - see .header-hover-content
     above, its enlarged overlay shows the same info in front of it) or
     partially (block 3/nav - block 3's own overlay is just the logo,
     near the bottom, not a replacement for its compact text near the
     top) while its own fill is open, back to opacity:1 on mouseleave.
     The other 2 blocks are never touched by this at all. The actual
     0/0.35/1 opacity
     VALUES are set as inline styles directly by js/header-hover.js, not
     via a toggled class: js/header-entrance.js (untouched, not this
     feature's animation) leaves a permanent inline `opacity: 1` on
     these exact elements after its own one-shot load tween - its
     onComplete clearProps's `transform` but not `opacity` (same
     lingering-inline-style pattern already found and documented on
     .header-mid's own clip-path, above) - which would out-rank any
     plain class-based opacity rule here regardless of specificity.
     This transition rule (no value, just the timing) is what actually
     makes those inline-style CHANGES animate smoothly instead of
     snapping - a plain opacity ease, not the elastic curve above: a
     bouncy overshoot reads right for the fill's own sweep/retreat
     motion, but would look like a glitch on a simple dim/undim of
     static text, so a flat ease is the visually-coherent choice here.
     .socials is deliberately NOT in this selector list (js/header-hover.js
     never sets its opacity either) - it never triggers this fill and
     must stay completely unaffected by it, same as #ytWidget. */
  .header-mid > div,
  .nav {
    transition: opacity 0.35s ease;
  }
}
