/* Site-wide structural layout: nav, footer, page shell */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  /* More transparent than the site's general translucent token — a glass
     material needs to actually show (and let you see the warp in) what's
     behind it, rather than reading as a near-solid tinted bar. */
  background: rgb(255 255 255 / 45%);
  backdrop-filter: blur(var(--blur-nav));
  /* Hardcoded, not var(--blur-nav) — confirmed live (getComputedStyle,
     both the camelCase and getPropertyValue() forms) on a real Safari
     18.6/Intel Mac that this specific engine's -webkit-backdrop-filter
     fails to resolve a var() reference here: CSS.supports() reports the
     property/syntax as valid, background-color from this very rule
     computes correctly, yet this one property's computed value came back
     empty/"none" instead of a blur. Declaring a literal value first and
     letting a var()-based one after it win where supported (the pattern
     used for the fluid-glass rule below) does NOT work around this —
     cascade always resolves to the LAST same-specificity declaration
     regardless of whether it renders, and per spec, a var() reference
     that fails to resolve at computed-value time falls back to the
     property's initial value, not to an earlier sibling declaration. The
     only actual fix is to not use var() here at all. --blur-nav is 12px
     at every current breakpoint (css/tokens.css) with nothing planned to
     change that, so hardcoding it loses no real flexibility.

     This previously lived on a .site-header::before pseudo-element
     instead, isolated away from .site-header's own position:sticky and
     conditional classes on the theory that the combination itself was the
     point of failure — that turned out to be wrong (Safari's lack of
     support here is unconditional, isolating it changed nothing there)
     and cost a real regression in Chrome: backdrop-filter's job is to
     capture and blur whatever is genuinely behind the element it's
     applied to, and a generated pseudo-element child does not reliably
     resolve "behind" the same way a real element does across engines —
     which is what broke the animated fluid-glass warp there. Back on the
     real element directly, exactly as originally built and confirmed
     working in Chrome. */
  -webkit-backdrop-filter: blur(12px);
  /* Compositing hint — the backdrop content behind this element changes
     every scroll frame even though the filter's own value stays constant,
     so this stays expensive to recompute; keeping it promoted to its own
     GPU layer avoids the browser repromoting it mid-scroll. */
  will-change: backdrop-filter;
  /* Thin bright sheen along the top edge, like light catching a glass edge */
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 60%);
  /* Pulls the header into its own, constant-size View Transition group (see
     css/base.css) so it's excluded from the carousel slide entirely and
     stays put — unlike naming the variable-height <main> content, this
     can't visibly stretch/squash mid-transition, since the header's box is
     the same size on every page. */
  view-transition-name: site-header;
}

/* Floating pill treatment, mobile/tablet only (same 900px line the rest
   of the site treats as that split) — a 16px gap on top and (tablet)
   the sides reads as a distinct floating bar rather than a full-bleed
   strip, and border-radius past 50% of the bar's own height guarantees
   fully rounded ends at any --nav-height value, not just the current
   one. margin-inline rather than an explicit width: a sticky/static
   block already sizes to fill its container minus its own margins, so
   this doesn't need to duplicate the gap on both sides. */
@media (max-width: 900px) {
  .site-header {
    /* Named (rather than a bare 16px) so .nav__menu's mobile-only inset
       calc, further down, can reference the same value instead of
       hardcoding a second copy that could quietly drift out of sync —
       the menu needs to know how far below the true viewport top the
       header's own bottom edge sits, and this is half of that math. */
    --nav-top-gap: 16px;
    top: var(--nav-top-gap);
    margin-inline: 16px;
    border-radius: 999px;
  }
}

/* Fluid Glass (reactbits.dev/components/fluid-glass), approximated with an
   animated SVG turbulence/displacement filter (see initFluidGlass in
   js/main.js) layered on top of the plain blur above, so the content
   behind the bar visibly warps like it's seen through moving glass.
   Gated behind .has-fluid-glass — a class initFluidGlass() adds only on
   non-WebKit engines — rather than just trusting the cascade to fall
   back gracefully on its own: WebKit parses `url(#fluid-glass)` inside
   backdrop-filter as syntactically valid (so it wins the cascade over
   the plain-blur rule above, same as everywhere else), it just then
   fails to render *anything* for the property at paint time — not a
   graceful drop back to blur-only, but the header going completely flat
   with no backdrop effect at all. Scoping this rule to a class only
   added where it's confirmed to actually render guarantees WebKit never
   receives the declaration that would otherwise silently win and break
   it. */
.site-header.has-fluid-glass {
  backdrop-filter: blur(calc(var(--blur-nav) * 0.5)) url(#fluid-glass);
}

/* WebKit-only fallback for the rule above (see initFluidGlass in
   js/main.js, which is what actually injects this element — nothing to
   render here on engines that already get the real warp). Applies the
   same #fluid-glass filter as a plain `filter`, which WebKit does
   support, to a small decorative glow instead of the page content behind
   the bar (which only `backdrop-filter` can reach, and that's the one
   piece missing in this engine) — so Safari/iOS still get a rippling,
   shifting-light motion cue on the glass, just not a literal warp of
   what's scrolling underneath. Two radial blobs drifting in opposite
   phase read as light catching an uneven glass surface rather than a
   single blob sliding back and forth. z-index: -1 keeps it under
   .nav's real content (links/logo) while still painting above this
   element's own tinted background — see the stacking order note this
   relies on: a positioned child's negative-z layer paints right after
   its stacking-context parent's own background, before the parent's
   normal in-flow children. */
.site-header__glass-shimmer {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  opacity: 0.5;
  filter: url(#fluid-glass);
}

.site-header__glass-shimmer::before {
  content: "";
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(circle at 30% 50%, rgb(255 255 255 / 55%), transparent 60%),
    radial-gradient(circle at 70% 50%, rgb(255 255 255 / 35%), transparent 55%);
}

@media (prefers-reduced-motion: no-preference) {
  .site-header__glass-shimmer::before {
    animation: glass-shimmer-drift 12s ease-in-out infinite;
  }
}

@keyframes glass-shimmer-drift {
  0%, 100% { transform: translateX(-6%); }
  50% { transform: translateX(6%); }
}

.is-dark .site-header {
  background: rgb(0 0 0 / 40%);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 12%);
}

/* Desktop only, home page only: the header stays fully transparent while
   at rest — unscrolled, nothing inside it focused — so the hero glow
   (css/pages/home.css) shows through cleanly instead of getting cut off
   by a visible bar sitting right above it. It regains its normal glass
   background (base rule, the fluid-glass override, and the dark-mode
   variant above — this just wipes all three back to nothing rather than
   chasing each one individually) the moment there's an actual reason to
   need it: real content scrolling underneath, or a link genuinely
   gaining focus. has-scrolled is toggled by initHeaderScrollState in
   js/main.js; :focus-within needs no JS at all. Placed after every other
   .site-header background/backdrop-filter rule so it reliably wins the
   cascade for its one specific state. */
@media (min-width: 901px) {
  body[data-page="home"] .site-header {
    transition: background 300ms var(--ease), backdrop-filter 300ms var(--ease), box-shadow 300ms var(--ease);
  }

  body[data-page="home"] .site-header:not(.has-scrolled):not(:focus-within) {
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;
  }

  /* No glass to shimmer on while the bar itself is transparent (rule
     above) — same condition, so the shimmer disappears in lockstep with
     the tint/blur it's meant to live on top of. */
  body[data-page="home"] .site-header:not(.has-scrolled):not(:focus-within) .site-header__glass-shimmer {
    opacity: 0;
  }
}

/* macOS/iOS's system-wide "Reduce Transparency" accessibility setting makes
   Safari suppress backdrop-filter entirely, at the OS level — not a bug to
   route around, this is exactly what that setting is for, and Chrome/
   Firefox users can enable comparable OS-level settings too. Without this,
   anyone with it on would see whatever the affected rule's non-blurred,
   non-tinted computed background happens to be (transparent, on the home
   page's at-rest state above) rather than a deliberate, readable bar — so
   this swaps in a plain opaque background instead, site-wide, once the
   media feature confirms that preference. Repeats the home-page selector
   (rather than just a bare .site-header rule) to match that rule's own
   specificity and reliably override it; plain source order alone isn't
   enough when the selector being overridden is more specific than the
   one overriding it. */
@media (prefers-reduced-transparency: reduce) {
  .site-header,
  body[data-page="home"] .site-header:not(.has-scrolled):not(:focus-within) {
    background: #fff;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  .is-dark .site-header {
    background: #000;
  }

  .site-header__glass-shimmer {
    display: none;
  }
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  max-width: var(--content-max);
  height: var(--nav-height);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Mobile only (not tablet) — tighter side margin than the shared
   mobile/tablet rule above, paired with .nav's own fixed 16px padding
   below so the logo lands at a precise, deliberate 12+16=28px from the
   screen edge rather than the fluid --gutter value .page uses for
   everything else — which is also why .hero gets its own matching
   28px override (css/pages/home.css), so the hero paragraph's own left
   edge lines up with the logo above it instead of the two drifting
   apart at different widths within the mobile range. Placed after the
   base .site-header/.nav rules above (not just inside a narrower media
   query) since a media query alone doesn't out-specificity an
   unconditional rule of the same selector — only source order does. */
@media (max-width: 640px) {
  .site-header {
    margin-inline: 12px;
  }

  .nav {
    padding-inline: 16px;
  }
}

.nav__brand {
  font-size: var(--fs-h2);
  font-weight: 400;
  /* The Text Pressure long-press-and-drag gesture on touch (see
     initTextPressure in js/main.js) would otherwise race against iOS's
     own long-press callout (the "Open/Copy Link" menu) on this <a> — this
     turns that native callout off so holding the logo reliably triggers
     our own gesture instead. */
  -webkit-touch-callout: none;
}

/* Mobile and tablet: the wordmark's resting style matches the mobile
   index list's project-name treatment (.project-list__title in
   css/pages/home.css) — Poppins at body size/weight, plainer than the
   desktop nav's fs-h2/400 lockup. This is deliberately the *quiet* state:
   the periodic ASCII-art auto-play (initTextPressure in js/main.js)
   crossfades a Space Mono rendition on top of it every couple of minutes,
   so the contrast between this plain resting style and that effect is
   part of the reveal rather than something to blend away. */
@media (max-width: 900px) {
  .nav__brand {
    font-family: var(--font-body);
    font-size: var(--fs-body);
    font-weight: 300;
  }
}

/* Text Pressure (reactbits.dev/text-animations/text-pressure), approximated
   without a true variable font (Poppins isn't published as one): each
   character scales up continuously as the cursor nears it — the actual
   driver of the "pressure" feel — while font-weight snaps to a heavier
   static Poppins weight at close range for extra heft. Rest state (no
   hover) is untouched: scale(1), inherited weight 400, i.e. exactly the
   current look. See initTextPressure in js/main.js. */
.nav__brand__char {
  display: inline-block;
  /* Measured against the un-split rendering at this font/weight/size:
     splitting into isolated inline-block characters turned out to barely
     affect width on its own (the visible tightening was actually the
     space between "Orell" and "Inbrom" collapsing — fixed structurally in
     initTextPressure by leaving whitespace as a plain text node rather
     than its own span). This is a small deliberate touch of tracking on
     top of that natural spacing, standard practice for a UI wordmark. */
  letter-spacing: 0.01em;
  transform: scale(1);
  transition: transform 150ms var(--ease);
  will-change: transform;
}

.nav__menu {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.nav__link {
  font-size: var(--fs-h3);
  font-weight: 300;
}

.nav__toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  padding: 0.5rem;
}

.nav__toggle-bar {
  width: 22px;
  height: 2px;
  background: currentColor;
  transition: transform 250ms var(--ease), opacity 150ms var(--ease);
}

/* Cross (hamburger-react.netlify.app): top and bottom bars rotate to meet
   at center forming an X; the middle bar fades out. Keyed off the button's
   own aria-expanded state rather than a separate class. */
.nav__toggle[aria-expanded="true"] .nav__toggle-bar:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.nav__toggle[aria-expanded="true"] .nav__toggle-bar:nth-child(2) {
  opacity: 0;
}

.nav__toggle[aria-expanded="true"] .nav__toggle-bar:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* max-width: 900px, not 640px — mobile and tablet share the same
   hamburger + floating dropdown pattern rather than tablet showing the
   links inline, so there's an actual menu to open/close (and animate)
   at both sizes, matching how the rest of the site already treats
   "mobile and tablet" as one bucket up to this line. */
@media (max-width: 900px) {
  .nav__toggle {
    display: flex;
  }

  /* Solid background only while the hamburger menu is actually open
     (.nav-open, toggled in js/main.js) — matching the full-page menu's own
     background. White on light pages, black on the dark contact page.
     Closed, the header keeps its normal translucent/blur style at any
     width (the base .site-header / .is-dark .site-header rules, untouched
     here, already handle that).

     will-change: auto here matters, not just backdrop-filter: none — a
     will-change listing a property that (like backdrop-filter) can create
     a containing block makes the browser treat that as always active
     REGARDLESS of the property's actual current value, in at least
     Chromium and Firefox (confirmed: WebKit doesn't apply this rule at
     all, backdrop-filter or not). .nav__menu below is position:fixed and
     nested inside .site-header, so left with will-change still pointed at
     backdrop-filter, it was getting contained by .site-header's own
     (16px-inset, mobile/tablet-only) box in those two engines instead of
     sizing against the true viewport like it does in WebKit — a real,
     visible cross-browser mismatch in the open menu's height. Clearing
     both together while the menu is open removes the containing block
     entirely, so all engines agree. */
  .site-header.nav-open {
    background: #fff;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    will-change: auto;
    color: var(--color-ink);
  }

  .is-dark .site-header.nav-open {
    background: #000;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    will-change: auto;
    color: var(--color-paper);
  }

  /* Same idea as the home-page transparent state above: no tint/blur to
     shimmer on top of once the menu's solid background takes over. */
  .site-header.nav-open .site-header__glass-shimmer {
    opacity: 0;
  }

  /* Floats 8px below the navbar rather than flush against it, and is
     inset 12px on the remaining three sides — matching .site-header's
     own 12px side margin, so the panel reads as the same kind of
     floating card the navbar is, rather than a full-bleed sheet with a
     rounded top glued to an inset pill. The top offset needs
     --nav-top-gap (the header's own gap from the true viewport top,
     inherited from .site-header) added in as well as --nav-height —
     --nav-height alone is just the bar's own height, not how far down
     the page its bottom edge actually sits once it's floating. Explicit
     top+bottom (both part of the inset shorthand) rather than a separate
     height: calc(...) — with both edges pinned, the browser derives the
     height on its own, which is one fewer value to keep in sync by hand
     if any of these numbers change later. */
  /* Asymmetric easing on open vs close (the same "emphasized" pair used
     for the carousel page transitions in css/base.css, reused here for a
     consistent motion language site-wide): closing accelerates briskly
     away — dismissing something can afford to feel quicker than
     presenting it — while opening decelerates gently into place, with a
     touch more time to settle. A subtle scale alongside the existing
     translateY reads as the card actually arriving/receding rather than
     just fading, without being showy about it. */
  .nav__menu {
    position: fixed;
    inset: calc(var(--nav-top-gap) + var(--nav-height) + 8px) 12px 12px 12px;
    border-radius: 24px;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: var(--space-lg);
    padding: var(--space-lg) var(--gutter);
    background: #fff;
    transform-origin: top center;
    transform: scale(0.96) translateY(-8px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 250ms cubic-bezier(0.3, 0, 0.8, 0.15), transform 250ms cubic-bezier(0.3, 0, 0.8, 0.15);
  }

  .is-dark .nav__menu {
    background: #000;
    color: var(--color-paper);
  }

  .nav__menu.is-open {
    opacity: 1;
    transform: scale(1) translateY(0);
    pointer-events: auto;
    transition: opacity 350ms cubic-bezier(0.05, 0.7, 0.1, 1), transform 350ms cubic-bezier(0.05, 0.7, 0.1, 1);
  }

  /* Menu items are 3x the logo's font size */
  .nav__link {
    font-size: calc(var(--fs-h2) * 3);
  }

  /* Each link gets its own short entrance, staggered slightly after the
     panel itself starts opening — reads as a considered reveal rather
     than the whole panel (chrome and text together) arriving as one flat
     block. On close, the link's own transition duration/curve is set to
     match .nav__menu's exactly (250ms, same accelerate curve) so text and
     background reach fully invisible at the same instant — a mismatch
     here (links finishing faster or slower than the panel) reads as the
     text and the chrome dismissing as two separate, disconnected things
     instead of one cohesive close. */
  .nav__menu .nav__link {
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 250ms cubic-bezier(0.3, 0, 0.8, 0.15), transform 250ms cubic-bezier(0.3, 0, 0.8, 0.15);
  }

  .nav__menu.is-open .nav__link {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 350ms cubic-bezier(0.05, 0.7, 0.1, 1), transform 350ms cubic-bezier(0.05, 0.7, 0.1, 1);
  }

  .nav__menu.is-open .nav__link:nth-child(1) {
    transition-delay: 60ms;
  }

  .nav__menu.is-open .nav__link:nth-child(2) {
    transition-delay: 120ms;
  }
}

/* The open/close state itself (opacity + pointer-events) is functional,
   not just decorative, so it stays regardless of motion preference —
   only the transform/stagger/timing get pared back here, down to a
   near-instant cross-fade with no movement or delay. */
@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .nav__menu,
  .nav__menu.is-open,
  .nav__menu .nav__link,
  .nav__menu.is-open .nav__link {
    transform: none;
    transition-duration: 1ms;
    transition-delay: 0ms;
  }
}

/* Half the text-to-underline gap (base.css's .link-hover::after uses
   bottom: -0.15em) for the menu items specifically */
@media (max-width: 900px) {
  .nav__menu .nav__link::after {
    bottom: -0.075em;
  }
}

.page {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Fade Content on page load (reactbits.dev/animations/fade-content), applied
   to the whole page at once rather than staggered per section. The hidden
   starting state only applies once js-anim confirms JS is running (added
   synchronously in <head>, see js/main.js for the reveal), so content is
   never stuck invisible if JS is off. */
.js-anim .page {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 700ms var(--ease), transform 700ms var(--ease);
}

.js-anim .page.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Carousel prev/next arrivals already get a horizontal entrance from the
   View Transition slide (css/base.css) — skip this vertical translateY
   bump for them specifically, otherwise the two run at once and the
   content reads as entering diagonally from a bottom corner instead of
   cleanly from the left or right. */
.vt-next.js-anim .page,
.vt-prev.js-anim .page {
  opacity: 1;
  transform: none;
  transition: none;
}

.site-footer {
  margin-top: var(--space-2xl);
}

.footer__inner {
  max-width: var(--content-max);
  margin-inline: auto;
  padding: var(--space-md) var(--gutter);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
}

.footer__link {
  font-size: var(--fs-h3);
  font-weight: 300;
}
