/* ==========================================================================
   Fonts
   ========================================================================== */

@font-face {
  font-family: "Satoshi";
  src: url("fonts/Satoshi-Variable.woff2") format("woff2");
  font-weight: 300 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Satoshi";
  src: url("fonts/Satoshi-VariableItalic.woff2") format("woff2");
  font-weight: 300 900;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Sora";
  src: url("fonts/sora-v17-latin-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   Tokens
   ========================================================================== */

:root {
  /* Color */
  --midnight: #000000;
  --black-cat: #050014;
  --white: #ffffff;
  --off-white: #fffefd;
  --mint-pale: #e5f8f5;
  --blue-deep: #0225b7;
  --periwinkle: #5d5cff;
  --periwinkle-soft: #6671fc;
  --green-pale: #d1ffc2;
  --shark-gray: #4f4e56;
  --periwinkle-link: #3f3ffc;

  --gradient-blue: linear-gradient(135deg, #ffffff 0%, #668fff 100%);
  --gradient-green: linear-gradient(135deg, #ffffff 0%, #b2ff99 100%);

  --link-color: var(--blue-deep);

  /* Type */
  --font-heading: "Sora", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-body: "Satoshi", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Layout */
  --site-max-width: 1600px;
  --site-padding: clamp(1.25rem, 4vw, 3rem);
  --header-height: 5.5rem;

  /* Motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ==========================================================================
   Reset / base
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* Guards against any stray element (even a sub-pixel amount) pushing past
     the viewport's right edge and producing a horizontal scrollbar — this
     never clips intentional full-bleed content, since those still span
     exactly 100% of this same width; it only cuts off unintentional
     overflow, which should never be scrollable to begin with. */
  overflow-x: hidden;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--off-white);
  color: var(--black-cat);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: var(--link-color);
  text-decoration: none;
}

a:hover,
a:focus-visible {
  text-decoration: underline;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-heading);
  font-weight: 400;
  margin: 0;
  line-height: 1.1;
}

p {
  margin: 0;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

:focus-visible {
  outline: 2px solid var(--periwinkle);
  outline-offset: 3px;
}

.container {
  max-width: var(--site-max-width);
  margin-inline: auto;
  padding-inline: var(--site-padding);
}

.full-bleed {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* <site-header>/<site-footer> (see partials.js) are unstyled custom element
   tags wrapping the real, semantic <header>/<footer> markup they inject —
   display:contents removes the wrapper from the box tree entirely, so it
   contributes nothing to layout and can't interfere with the header's
   sticky positioning or the footer's normal-flow placement. */
site-header,
site-footer {
  display: contents;
}

/* ==========================================================================
   Header / nav
   ========================================================================== */

/* Mechanics match wolffolins.com's own header (inspected directly — see
   PR notes), not a guess: .site-header is a plain sticky box with a
   *constant* height that never animates. It centers its one child via flex
   instead of margin-inline:auto, and has pointer-events:none so the empty
   space around the floating pill (once narrow) doesn't block clicks on the
   hero underneath it — .nav-wrap re-enables pointer-events for itself. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--header-height);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  pointer-events: none;
}

/* The one piece of this whole mechanism that actually animates on scroll:
   its WIDTH, between the site's own fluid content column (matching
   .container — see tokens above) and a narrower centered band once
   scrolled. Nothing about its position ever changes — `top` is a constant
   1rem gap from the header's own top edge, in both states — which is what
   keeps the logo/links pixel-fixed as the page scrolls; only the box
   around them narrows. Centering comes from .site-header's own
   justify-content:center, not margin, so this only ever needs to animate
   one property. */
.nav-wrap {
  position: relative;
  top: 1rem;
  display: flex;
  align-items: center;
  pointer-events: auto;
  width: calc(100% - 2 * var(--site-padding));
  max-width: calc(var(--site-max-width) - 2 * var(--site-padding));
  transition: width 0.65s cubic-bezier(0.23, 1, 0.32, 1);
}

/* Menu-open gets the same narrow treatment as scrolled (even at the very
   top of the page) so opening it always shows as a compact floating panel,
   never a full-bleed bar — matching the original design intent here, just
   reached through the simpler width-based mechanism above. */
.site-header.is-scrolled .nav-wrap,
.site-header.menu-open .nav-wrap {
  width: min(40ch, calc(100% - 2rem));
}

@media (min-width: 769px) {
  .site-header.is-scrolled .nav-wrap,
  .site-header.menu-open .nav-wrap {
    width: clamp(64ch, 70%, 96ch);
  }
}

/* The pill's visible white background — a real, always-present element
   (not a pseudo-element gated behind a class), so its opacity fade is a
   plain transition on a box that already exists rather than a value
   appearing to "pop in" the instant a class first makes the box exist.
   Blur + translucency stand in for a shadow entirely; the reference site
   uses no box-shadow here at all. */
.nav-bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: 1.75rem;
  background: rgba(255, 254, 253, 0.9);
  border: 1px solid rgba(79, 78, 86, 0.3);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  opacity: 0;
  transition: opacity 0.2s ease-out;
}

.site-header.is-scrolled .nav-bg,
.site-header.menu-open .nav-bg {
  opacity: 1;
}

/* Always rounded, at the same radius, whether scrolled or not — there's no
   radius animation anywhere in this system. Height is likewise constant
   except while the mobile menu is open, when JS sets it explicitly (see
   script.js) to reveal .nav__mobile-links below the row. */
.nav {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 3.5rem;
  padding-inline: 1rem;
  border-radius: 1.75rem;
  overflow: hidden;
  transition: height 0.3s cubic-bezier(0, 0, 0.2, 1), padding-inline 0.3s cubic-bezier(0, 0, 0.2, 1);
}

/* A little extra breathing room once the nav is actually pill-shaped —
   at the full-width unscrolled state the 1rem base padding reads fine
   against the wide bar, but against the pill's own rounded edge it read
   as too tight. */
.site-header.is-scrolled .nav,
.site-header.menu-open .nav {
  padding-inline: 1.5rem;
}

.nav__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  height: 3.5rem;
}

.nav__logo-link {
  display: flex;
  align-items: center;
}

.nav__logo-full {
  height: 42px;
  width: auto;
  display: block;
}

/* Hidden by default (desktop and tablet keep the full lockup even when
   scrolled); the mobile breakpoint below is what actually reveals it. */
.nav__logo-icon {
  display: none;
}

.icon-mark {
  fill: var(--black-cat);
}

/* The eyes shift left on hover, then ease back. transform values on an
   inlined SVG's children are resolved in the SVG's own 210x173 user-unit
   space, not final on-screen pixels — the nav logo renders at ~42px tall,
   roughly a 4.1x scale-down, so translating by the visually-intended 5px
   needs ~20.6 user-units here to still read as 5px on screen. The footer
   logo is bigger (see .footer__logo), so the same 20.6 user-units reads as
   a proportionally larger shift there — same motion, scaled with the mark. */
.icon-eye {
  transition: transform 0.7s var(--ease);
}

.nav__logo-link:hover .icon-eye,
.footer__logo-link:hover .icon-eye {
  transform: translateX(-20.6px);
}

.nav__links {
  display: flex;
  align-items: center;
  gap: 2.0625rem;
}

.nav__links a {
  position: relative;
  display: inline-block;
  color: var(--shark-gray);
  text-decoration: none;
  font-size: 1.0625rem;
  font-weight: 500;
}

.nav__links a:hover,
.nav__links a:focus-visible {
  color: var(--periwinkle-link);
}

.nav__toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 2.25rem;
  height: 2.25rem;
}

/* :not(.visually-hidden) excludes the toggle's hidden "Menu" label — this
   selector's specificity (one class + one element) otherwise beats
   .visually-hidden's own width/height (one class alone), which blew that
   label up to a large visible-sized box instead of the intended 1x1px. */
.nav__toggle span:not(.visually-hidden) {
  display: block;
  width: 100%;
  height: 2px;
  background: var(--black-cat);
  /* Not var(--ease) — that token is a punchy curve that reaches ~100% early
     then flattens out, which reads as a harsh start and a wobbly landing.
     This curve is deliberately front-loaded (fast start) with a long, soft
     tail that keeps easing all the way to zero velocity, rather than
     stopping with any perceptible speed left — that abrupt full-stop is
     what reads as a jitter/bounce right at the end. */
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.nav__toggle[aria-expanded="true"] span:first-child {
  transform: translateY(3.5px) rotate(45deg);
}

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

/* Mobile links, contained in the pill */

/* No gap and no padding-block here — spacing lives on each `a` itself (see
   below) so that adjacent links sit flush against each other with zero dead
   space between them. That dead space would otherwise show as a white gap
   the hover fill can't reach, since the fill only ever covers its own link's
   box. */
.nav__mobile-links {
  display: flex;
  flex-direction: column;
}

/* The fill/text-color/arrow motion here is driven entirely in JS via the Web
   Animations API (see script.js) rather than CSS :hover + transition — it
   needs to read each element's *current* on-screen value and animate from
   there, so that moving quickly between adjacent links interrupts and
   reverses smoothly instead of snapping. These rules are just the static
   layout/positioning; they hold no hover-triggered values themselves. */
.nav__mobile-links a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* Vertical padding lives directly on the link (not a container gap) so the
     hover fill, which covers this box's full padding area, reaches all the
     way to the next link's box with no seam. Horizontal padding bleeds out
     to the pill's true edges (negative margin cancels the pill's own 1.5rem
     padding-inline, which is what's in force whenever these links are
     actually visible — see .site-header.menu-open .nav above) so the fill
     has no white showing at either side. */
  padding: 0.7rem 1.5rem;
  margin-inline: -1.5rem;
  overflow: hidden;
  font-family: var(--font-heading);
  font-size: 2.25rem;
  text-decoration: none;
}

/* Extra breathing room at the very top and bottom of the list, applied to
   the first/last link's own padding rather than the container so the last
   link's hover fill still reaches the pill's bottom edge — where .nav's
   overflow:hidden and rounded corners clip it into the pill's shape,
   fulfilling the "fills the entire bottom area" requirement. */
.nav__mobile-links a:first-child {
  padding-top: 1.25rem;
}

.nav__mobile-links a:last-child {
  padding-bottom: 1.5rem;
}

/* Parked one full width off-screen to the left (clipped by the link's own
   overflow:hidden). JS slides it to translateX(0) to cover the link on
   hover, then all the way to translateX(100%) — off through the right
   edge — on hover-out, so the highlight always travels the same direction
   and never reverses back the way it came. */
.nav__mobile-link-fill {
  position: absolute;
  inset: 0;
  background: var(--periwinkle);
  transform: translateX(-100%);
  z-index: 0;
  pointer-events: none;
}

.nav__mobile-link-text {
  position: relative;
  z-index: 1;
  color: var(--black-cat);
}

.nav__mobile-link-arrow {
  position: relative;
  z-index: 1;
  width: 1.625rem;
  height: 1.3rem;
  flex-shrink: 0;
  color: var(--black-cat);
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.85rem 1.75rem;
  border: 2px solid transparent;
  border-radius: 4px;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 1.125rem;
  line-height: 1.2;
  text-decoration: none;
  overflow: hidden;
  transition: color 0.35s var(--ease), border-color 0.35s var(--ease);
}

.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease);
  z-index: -1;
}

.btn:hover,
.btn:focus-visible {
  text-decoration: none;
}

.btn:hover::before,
.btn:focus-visible::before {
  transform: scaleX(1);
}

/* A button that's also a scroll-reveal element (e.g. .statement__cta below)
   carries both .btn's hover transitions and .reveal's entrance transition on
   the very same element. `transition` is a shorthand — whichever selector
   wins the cascade replaces the WHOLE list, it doesn't merge with a lower-
   priority one — so without this higher-specificity combined rule, .reveal's
   own transition (declared later in this file, see Scroll reveal) would
   silently drop .btn's color/border-color transitions entirely, leaving
   hover changes to snap with no animation at all. This is the actual list in
   effect for any element carrying both classes: entrance keeps .reveal's own
   opacity/transform timing. Hover itself is asymmetric: entering is quick,
   leaving is a slower ease-in fade (rather than a held pause that then
   snaps into a transition) — overridden to the quick version on hover
   below. */
.reveal.btn {
  transition: opacity 0.7s var(--ease) 0.12s, transform 0.7s var(--ease) 0.12s, color 0.3s ease-in,
    border-color 0.3s ease-in;
}

.reveal.btn:hover,
.reveal.btn:focus-visible {
  transition: opacity 0.7s var(--ease) 0.12s, transform 0.7s var(--ease) 0.12s, color 0.2s var(--ease),
    border-color 0.2s var(--ease);
}

.btn--on-light {
  background: transparent;
  color: var(--black-cat);
  border: 2px solid var(--black-cat);
}

.btn--on-light::before {
  background: var(--periwinkle);
  /* Fades in place rather than the base .btn::before left-to-right slide.
     Fading out (hover-off) is slower with an ease-in curve — overridden to
     a quicker fade-in on hover below — so releasing reads as a gradual fade
     rather than a held pause that then snaps into a transition. */
  transform: none;
  opacity: 0;
  transition: opacity 0.3s ease-in;
}

.btn--on-light:hover,
.btn--on-light:focus-visible {
  color: var(--white);
  border-color: var(--periwinkle);
}

.btn--on-light:hover::before,
.btn--on-light:focus-visible::before {
  opacity: 1;
  transition: opacity 0.2s var(--ease);
}

.btn--on-dark {
  background: transparent;
  color: var(--green-pale);
  border-color: var(--green-pale);
}

.btn--on-dark::before {
  background: var(--green-pale);
}

.btn--on-dark:hover,
.btn--on-dark:focus-visible {
  color: var(--black-cat);
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  position: relative;
  /* Fills exactly the viewport height remaining below the header, so the
     hero's bottom edge lands at the bottom of the browser on page load —
     matching the reference site. `svh` (small viewport height) is used
     over plain `vh` so mobile browser chrome doesn't cause the hero to
     overflow past the visible area; the `vh` line is a fallback for
     browsers that don't support `svh`. */
  height: calc(100vh - var(--header-height));
  height: calc(100svh - var(--header-height));
  min-height: 26rem;
  overflow: hidden;
}

.hero__slide {
  position: absolute;
  inset: 0;
  transform: translateX(100%);
  transition: transform 0.7s var(--ease);
  display: flex;
  align-items: flex-end;
}

/* Without an explicit size, a <video> renders at its own intrinsic pixel
   dimensions (e.g. 1920x1080) as a flex item here — left-aligned by the
   flex default and clipped by .hero's overflow:hidden, so the visible crop
   sits toward the video's left edge instead of its center. Filling the
   slide's box and letting object-fit crop the overflow keeps it centered
   and full-bleed at any viewport size instead. */
.hero__slide-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.hero__slide.is-active {
  transform: translateX(0);
}

.hero__slide.is-leaving {
  transform: translateX(-100%);
}

.hero__slide--blue {
  background: var(--gradient-blue);
}

.hero__slide--green {
  background: var(--gradient-green);
}

.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(5, 0, 20, 0.55) 0%,
    rgba(5, 0, 20, 0) 42%
  );
}

.hero__content {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--site-padding);
  padding-bottom: 3rem;
  padding-right: 12rem;
  color: var(--white);
  z-index: 1;
  /* Text swaps via a quick fade, independent of the background slide's
     transform — it should never appear to slide along with the image. */
  transition: opacity 0.18s var(--ease);
}

.hero__content.is-fading {
  opacity: 0;
}

.hero__eyebrow {
  display: block;
  font-size: 1.1rem;
  opacity: 0.85;
  margin-bottom: 0.5rem;
}

.hero__title {
  font-size: clamp(1.3rem, 2.75vw, 1.95rem);
  max-width: 20ch;
}

.hero__title-link {
  color: inherit;
  text-decoration: none;
}

.hero__title-link:hover,
.hero__title-link:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.08em;
}

.hero__meta {
  position: absolute;
  right: var(--site-padding);
  bottom: 3rem;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1rem;
  color: var(--white);
}

.hero__counter {
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  opacity: 0.9;
}

#hero-current {
  display: inline-block;
  transition: transform 0.2s var(--ease), opacity 0.2s var(--ease);
}

#hero-current.is-ticking-out {
  transform: translateY(-60%);
  opacity: 0;
}

#hero-current.is-ticking-in {
  transform: translateY(60%);
  opacity: 0;
  transition: none;
}

.hero__controls {
  display: flex;
  gap: 0.75rem;
}

@media (hover: hover) and (pointer: fine) {
  .hero__controls {
    display: none;
  }
}

.hero__control {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--white);
}

.hero__control:hover,
.hero__control:focus-visible {
  background: rgba(255, 255, 255, 0.15);
}

.hero__control svg {
  width: 1.1rem;
  height: 1.1rem;
}

/* Hover-driven prev/next/view zones — mouse + trackpad only; touch devices
   keep the always-visible controls above instead. */

.hero__zones {
  display: none;
}

@media (hover: hover) and (pointer: fine) {
  .hero__zones {
    display: flex;
    position: absolute;
    inset: 0;
    z-index: 1;
  }

  .hero__zone {
    flex: 1 1 33.333%;
    height: 100%;
    cursor: none;
  }

  .hero__zone-label {
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -50%);
    color: var(--green-pale);
    font-family: var(--font-heading);
    font-size: clamp(1.75rem, 3.4vw, 2.75rem);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    z-index: 2;
    transition: opacity 0.2s var(--ease);
  }

  .hero__zone-label.is-visible {
    opacity: 1;
  }

  .hero__zone-label svg {
    display: block;
    width: 1.61em;
    height: 1.57em;
    fill: currentColor;
  }
}

/* ==========================================================================
   Selected Work
   ========================================================================== */

.statement {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 1.5rem;
  padding-block: 5rem 3rem;
}

.statement__text {
  grid-column: span 11;
  font-family: var(--font-heading);
  font-size: clamp(2.25rem, 5.1vw, 3.75rem);
  line-height: 1.15;
}

/* Always its own line regardless of viewport width — not left to natural
   wrapping — since .statement__rest picks up right where it left off. */
.statement__role {
  display: block;
  color: var(--periwinkle);
  font-size: clamp(2rem, 4.6vw, 3.4rem);
}

.statement__line-mask {
  display: block;
  overflow: hidden;
}

.statement__line {
  display: block;
  opacity: 0;
  transform: translateY(100%);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}

.statement__line.is-revealed {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .statement__line {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

.statement__cta {
  grid-column: span 11;
  justify-self: start;
  margin-top: 2rem;
}

.work {
  padding-block: 3rem 5rem;
}

.work__divider {
  border: none;
  border-top: 2px solid var(--shark-gray);
  margin: 0 0 2.5rem;
}

.work__heading {
  font-size: clamp(1.125rem, 1.6vw, 1.375rem);
  margin-bottom: 2.5rem;
}

.work-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 1.5rem;
  row-gap: 2.5rem;
}

.project-card--large {
  grid-column: span 7;
}

.project-card--medium {
  grid-column: span 5;
}

.project-card--full {
  grid-column: span 12;
}

.project-card__link {
  display: block;
  position: relative;
  color: inherit;
  text-decoration: none;
}

.project-card__hover-label {
  display: none;
}

@media (hover: hover) and (pointer: fine) {
  .project-card__link:hover {
    cursor: none;
  }

  .project-card__hover-label {
    display: block;
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -50%);
    color: var(--green-pale);
    font-family: var(--font-heading);
    font-size: clamp(1.25rem, 2vw, 1.75rem);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    z-index: 2;
    transition: opacity 0.2s var(--ease);
  }

  .project-card__hover-label.is-visible {
    opacity: 1;
  }

  .project-card__hover-label svg {
    display: block;
    width: 1.61em;
    height: 1.57em;
    fill: currentColor;
  }
}

.project-card__link:hover,
.project-card__link:focus-visible {
  text-decoration: none;
}

.project-card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  margin-bottom: 1.25rem;
  transition: transform 0.4s var(--ease);
}

.project-card--full .project-card__media {
  aspect-ratio: 21 / 9;
}

.project-card__link:hover .project-card__media,
.project-card__link:focus-visible .project-card__media {
  transform: scale(1.015);
}

.project-card__title {
  font-size: 1.375rem;
  margin-bottom: 0.35rem;
}

.project-card__tagline {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.project-card__tag {
  background: var(--off-white);
  color: var(--black-cat);
  padding: 0.25rem 0.65rem;
  border: 1px solid rgba(79, 78, 86, 0.4);
  border-radius: 4px;
  font-size: 0.85rem;
  line-height: 1.3;
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer {
  background: var(--off-white);
  color: var(--midnight);
  /* Bottom padding must comfortably exceed .reveal's translateY(48px) offset —
     it's the last section on the page, and a reveal-animated element can
     never be scrolled into view if its pre-reveal transform pushes it past
     the document's actual scrollable height. */
  padding-block: 4rem 5rem;
  border-top: 1px solid var(--midnight);
}

.site-footer a {
  display: inline-flex;
  align-items: center;
  color: var(--midnight);
}

.footer__arrow {
  flex-shrink: 0;
  width: 0.6rem;
  height: 0.5rem;
  margin-right: 0.5rem;
  transition: transform 0.2s var(--ease);
}

.site-footer a:hover .footer__arrow,
.site-footer a:focus-visible .footer__arrow {
  transform: translateX(3px);
}

.footer__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
  padding-bottom: 3rem;
  margin-bottom: 2rem;
  border-bottom: 1px solid var(--midnight);
}

.footer__logo {
  height: 3.75rem;
  width: auto;
}

.footer__link-columns {
  display: flex;
  align-items: flex-start;
  gap: 5.5rem;
}

.footer__col {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.footer__col a:hover,
.footer__col a:focus-visible {
  color: var(--periwinkle-link);
  text-decoration: none;
}

.footer__bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  font-size: 0.875rem;
  opacity: 0.85;
}

.footer__legal {
  display: flex;
  gap: 1.5rem;
}

.footer__back-to-top {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  color: var(--midnight);
  transition: color 0.3s var(--ease);
}

.footer__back-to-top:hover,
.footer__back-to-top:focus-visible {
  color: var(--periwinkle-link);
}

.footer__back-to-top-icon {
  flex-shrink: 0;
  width: 2rem;
  height: 1.94rem;
  transform: rotate(-90deg);
}

.footer__back-to-top-icon path {
  stroke: currentColor;
  stroke-width: 0.3;
}

/* Two animations handing off at the same instant: a single rise, then a
   continuous gentle bounce that repeats for as long as it's hovered —
   without ever replaying that initial rise. Driven by a JS-toggled class
   (not :hover directly) so hover-off can ease back to rest smoothly instead
   of snapping — CSS animations, unlike transitions, don't animate their own
   removal, so :hover alone can't produce a graceful return. */
.footer__back-to-top-icon.is-bouncing {
  animation-name: footer-rise-once, footer-gentle-bounce;
  animation-duration: 0.9s, 1.4s;
  animation-delay: 0s, 0.9s;
  animation-iteration-count: 1, infinite;
  animation-fill-mode: forwards, none;
  animation-timing-function: linear, linear;
}

/* translateY comes before rotate() so the movement happens in true
   screen-space up/down — with rotate() first, the translate would happen in
   the icon's own (already-rotated) local axis and move sideways instead.
   Easing matches constant-acceleration ("gravity") physics — the same
   easeOutQuad/easeInQuad pair as footer-gentle-bounce below, for a
   continuous decelerate-then-accelerate motion rather than the earlier
   "expo" curves, which pinned both control points at the target value and
   produced a snap-then-hang look instead of smooth motion. */
@keyframes footer-rise-once {
  0% {
    transform: translateY(0) rotate(-90deg);
    animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  45% {
    transform: translateY(-60%) rotate(-90deg);
    animation-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53);
  }
  100% {
    transform: translateY(0) rotate(-90deg);
  }
}

@keyframes footer-gentle-bounce {
  0%,
  100% {
    transform: translateY(0) rotate(-90deg);
    animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  50% {
    transform: translateY(-20%) rotate(-90deg);
    animation-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53);
  }
}

/* ==========================================================================
   Scroll reveal
   ========================================================================== */

.reveal {
  opacity: 0;
  transform: translateY(48px) scale(0.97);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}

.reveal.is-revealed {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.work-grid .reveal:nth-child(2n) {
  transition-delay: 0.1s;
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   Responsive
   ========================================================================== */

@media (max-width: 1024px) {
  .project-card--large,
  .project-card--medium {
    grid-column: span 6;
  }
}

@media (max-width: 768px) {
  .statement__text,
  .statement__cta {
    grid-column: span 12;
  }

  .nav__links {
    display: none;
  }

  .nav__toggle {
    display: flex;
  }

  /* Logo is tuned down slightly for mobile. */
  .nav__logo-full {
    height: 39.9px;
  }

  /* Icon-only mark replaces the full lockup once the mobile pill engages
     (scrolled or menu open) — the narrow pill doesn't have room for the
     wordmark. Same height as .nav__logo-full above and the same 173-tall
     viewBox in both SVGs, so the swap is a pure width change: the mark's
     vertical position stays pixel-identical, nothing jumps when the pill
     kicks in. The eye hover shift is inert on real touch devices (no
     hover), but the markup keeps the same .icon-eye class either way so
     it still works from a mouse/trackpad on a narrow viewport. */
  .nav__logo-icon {
    display: none;
    height: 39.9px;
    width: auto;
  }

  .site-header.is-scrolled .nav__logo-full,
  .site-header.menu-open .nav__logo-full {
    display: none;
  }

  .site-header.is-scrolled .nav__logo-icon,
  .site-header.menu-open .nav__logo-icon {
    display: block;
  }

  .hero__content {
    padding-right: var(--site-padding);
    padding-bottom: 6rem;
  }

  .hero__meta {
    left: var(--site-padding);
    right: var(--site-padding);
    bottom: 1.5rem;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }

  .work {
    padding-block: 3.5rem;
  }

  .work-grid {
    row-gap: 2.5rem;
  }

  .project-card--large,
  .project-card--medium,
  .project-card--full {
    grid-column: span 12;
  }

  /* Footer's own bottom padding is sized for desktop (see .site-footer's
     comment on why it must comfortably exceed .reveal's 48px offset) — 5rem
     read as an oversized gap once the mobile layout below is tightened up,
     so it's pulled in here. 4rem/3rem still clears the 48px reveal offset
     with room to spare. */
  .site-footer {
    padding-block: 3rem 3rem;
  }

  .footer__top {
    flex-direction: column;
    padding-bottom: 2rem;
    margin-bottom: 1.25rem;
  }

  /* Smaller than the desktop mark (see .footer__logo) — at full size it
     dominates the narrow column it now sits above. */
  .footer__logo {
    height: 2.75rem;
  }

  /* Three columns — nav links, social links, back-to-top — spaced evenly
     across the full row instead of clustered to the left with a fixed gap. */
  .footer__link-columns {
    width: 100%;
    justify-content: space-between;
    gap: 1rem;
  }

  /* Copyright + legal links tightened into a stacked block instead of the
     wrapping row used on desktop, which left an uneven gap when "Privacy
     Policy" / "Terms & Conditions" wrapped onto their own line. */
  .footer__bottom {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }
}

/* ==========================================================================
   Legal pages (privacy.html, terms.html)
   --------------------------------------------------------------------------
   Long-form prose, which the base reset deliberately strips down: `p` has no
   margin and `ul` has no markers or indent site-wide, since almost every
   other block on the site lays those out itself. Everything here is scoped
   under .legal so restoring normal document flow for legal copy can't leak
   into the nav, footer, or work grid.

   Sora ships as a single 400-weight file (see @font-face at the top), so
   headings here stay at 400 and lean on size and letter-spacing for
   hierarchy rather than asking for a bold that would be synthesized.
   ========================================================================== */

.legal-hero {
  padding-top: calc(var(--header-height) + clamp(2rem, 6vw, 4.5rem));
  padding-bottom: clamp(2rem, 5vw, 3.5rem);
  background: linear-gradient(160deg, var(--off-white) 0%, var(--mint-pale) 100%);
  border-bottom: 1px solid rgba(5, 0, 20, 0.12);
}

.legal-hero__eyebrow {
  font-family: var(--font-heading);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--blue-deep);
  margin-bottom: 1rem;
}

.legal-hero__title {
  font-size: clamp(2.25rem, 1.6rem + 3vw, 3.5rem);
  letter-spacing: -0.02em;
  margin-bottom: 1.25rem;
}

.legal-hero__dates {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  font-size: 0.875rem;
  color: #4a4a5c;
}

.legal-hero__dates strong {
  font-weight: 500;
  color: var(--black-cat);
}

.legal {
  padding-top: clamp(2.5rem, 6vw, 4rem);
  padding-bottom: clamp(4rem, 8vw, 6rem);
}

/* Narrower than --site-max-width on purpose: the work grid wants the full
   1600px, a column of legal text does not. ~68ch keeps line length in the
   readable range at every viewport. */
.legal__body {
  max-width: 68ch;
  margin-inline: auto;
  line-height: 1.7;
}

.legal__body p {
  margin-bottom: 1rem;
}

.legal__body ul,
.legal__body ol {
  list-style: revert;
  padding-left: 1.25rem;
  margin-bottom: 1rem;
}

.legal__body li {
  margin-block: 0.35rem;
}

/* Nested list inside the service-provider bullet in section 6. */
.legal__body li > ul {
  margin-top: 0.5rem;
  margin-bottom: 0.5rem;
}

.legal__body h2 {
  font-size: clamp(1.25rem, 1.1rem + 0.6vw, 1.6rem);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin-bottom: 0.85rem;
}

.legal__body h3 {
  font-size: 1.0625rem;
  margin-top: 1.75rem;
  margin-bottom: 0.5rem;
}

.legal__body > section {
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
  /* Clears the sticky header when a table-of-contents link jumps here. */
  scroll-margin-top: calc(var(--header-height) + 1rem);
}

.legal__divider {
  border: 0;
  border-top: 1px solid rgba(5, 0, 20, 0.12);
  margin-block: clamp(2rem, 5vw, 3rem);
}

/* --- Summary card --- */

.legal-summary {
  padding: 1.5rem clamp(1.25rem, 3vw, 2rem);
  background: var(--mint-pale);
  border-left: 3px solid var(--periwinkle);
  border-radius: 3px;
  margin-bottom: clamp(2rem, 5vw, 3rem);
}

.legal-summary h2 {
  font-size: 1rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.legal-summary__note {
  font-size: 0.9375rem;
  margin-top: 1rem;
  margin-bottom: 0;
}

/* --- Table of contents --- */

.legal-toc h2 {
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #4a4a5c;
  margin-bottom: 0.75rem;
}

.legal-toc ol {
  list-style: none;
  padding-left: 0;
  columns: 2;
  column-gap: 2rem;
  font-family: var(--font-heading);
  font-size: 0.9rem;
}

.legal-toc li {
  margin-bottom: 0.45rem;
  break-inside: avoid;
}

.legal-toc a {
  color: var(--black-cat);
}

.legal-toc a:hover,
.legal-toc a:focus-visible {
  color: var(--blue-deep);
}

/* --- Tables --- */

/* Wide tables scroll inside their own container rather than pushing the page
   sideways — html has overflow-x:hidden, so an unwrapped table would simply
   be clipped and unreachable on narrow screens. */
.legal-table {
  overflow-x: auto;
  margin-bottom: 1.25rem;
  -webkit-overflow-scrolling: touch;
}

.legal-table table {
  border-collapse: collapse;
  width: 100%;
  min-width: 34rem;
  font-size: 0.9375rem;
}

.legal-table caption {
  text-align: left;
  font-size: 0.8125rem;
  color: #4a4a5c;
  padding-bottom: 0.5rem;
}

.legal-table th,
.legal-table td {
  text-align: left;
  vertical-align: top;
  padding: 0.7rem 0.85rem;
  border-bottom: 1px solid rgba(5, 0, 20, 0.12);
}

.legal-table th {
  font-family: var(--font-heading);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: var(--mint-pale);
}

.legal-table tbody tr:last-child td {
  border-bottom: 0;
}

/* --- Pre-launch placeholders --- */

/* Highlights every value still to be confirmed before this page goes live.
   Delete this rule once the last .legal-fill span is gone, so a stray one
   can never ship looking like ordinary copy. */
.legal-fill {
  background: var(--green-pale);
  padding: 0.05em 0.4em;
  border-radius: 2px;
  font-family: var(--font-heading);
  font-size: 0.9em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* --- Contact block --- */

.legal-contact {
  background: var(--black-cat);
  color: var(--white);
  border-radius: 3px;
  padding: clamp(1.5rem, 4vw, 2.25rem);
}

.legal-contact h2 {
  color: var(--white);
}

.legal-contact a {
  color: var(--green-pale);
}

.legal-contact a:hover,
.legal-contact a:focus-visible {
  color: var(--mint-pale);
}

.legal-contact address {
  font-style: normal;
  line-height: 1.8;
}

.legal-contact__terms {
  margin-top: 1rem;
  margin-bottom: 0;
}

.legal-contact .legal-fill {
  color: var(--black-cat);
}

@media (max-width: 700px) {
  /* Two columns of contents entries get too narrow to scan here. */
  .legal-toc ol {
    columns: 1;
  }
}

/* Long all-caps disclaimer blocks in terms.html. The caps are conventional in
   contract drafting (they mark the clause as conspicuous), but caps destroy
   word-shape cues and get hard to read at body size, so these are set a notch
   smaller with looser tracking and line height to keep them scannable rather
   than shouted. */
.legal-caps {
  font-size: 0.875rem;
  letter-spacing: 0.015em;
  line-height: 1.75;
}
