/* ==========================================================================
   Visually Rendered Production — components.css
   Everything reusable across pages: chrome, buttons, cards, overlays, motion.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Scroll reveal
   The hidden state is scoped to .js so that if JavaScript fails to run the
   content is simply visible. Never trap content behind a script.
   -------------------------------------------------------------------------- */
.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.js [data-reveal='mask'] {
  transform: none;
  clip-path: inset(0 0 100% 0);
  transition:
    opacity var(--dur) var(--ease-out),
    clip-path var(--dur-slow) var(--ease-out);
}

.js [data-reveal='scale'] {
  transform: scale(1.06);
}

/* Directional entrances. Distance is modest on purpose: a long slide reads as
   a page still loading, a short one reads as craft. */
.js [data-reveal='left'] {
  transform: translate3d(-46px, 0, 0);
}

.js [data-reveal='right'] {
  transform: translate3d(46px, 0, 0);
}

@media (max-width: 54rem) {
  .js [data-reveal='left'],
  .js [data-reveal='right'] {
    transform: translate3d(0, 26px, 0);
  }
}

/* The hairline animates its own scale, so it must not also be translated. */
.js .draw-line[data-reveal] {
  opacity: 1;
  transform: scaleX(0);
}

.js .draw-line[data-reveal].is-in {
  transform: scaleX(1);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 0 0);
  will-change: auto;
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] {
    opacity: 1;
    transform: none;
    clip-path: none;
  }
}

/* Per-line text reveal — spans are wrapped by JS, never hard-coded */
.text-reveal__line {
  display: block;
  overflow: hidden;
}

.text-reveal__inner {
  display: block;
  transform: translate3d(0, 100%, 0);
  transition: transform var(--dur-slow) var(--ease-out);
  transition-delay: var(--line-delay, 0ms);
}

.is-in .text-reveal__inner,
.text-reveal.is-in .text-reveal__inner {
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .text-reveal__inner {
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   2. Buttons
   -------------------------------------------------------------------------- */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text);
  --btn-border: var(--border-strong);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
  padding: 0.95em 1.9em;
  min-block-size: 3rem;
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--btn-fg);
  background: var(--btn-bg);
  border: 1px solid var(--btn-border);
  border-radius: var(--radius-full);
  overflow: hidden;
  isolation: isolate;
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

/* Wipe fill — GPU transform only, no width/height animation */
.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--warm);
  transform: scaleY(0);
  transform-origin: bottom center;
  transition: transform var(--dur) var(--ease-out);
}

.btn:hover::before,
.btn:focus-visible::before {
  transform: scaleY(1);
  transform-origin: top center;
}

.btn:hover,
.btn:focus-visible {
  color: #0b0b0b;
  border-color: var(--warm);
}

.btn:active {
  transform: translateY(1px);
}

.btn--primary {
  --btn-bg: var(--warm);
  --btn-fg: #0b0b0b;
  --btn-border: var(--warm);
}

.btn--primary::before {
  background: var(--gold);
}

.btn--primary:hover,
.btn--primary:focus-visible {
  color: #0b0b0b;
  border-color: var(--gold);
}

.btn--gold {
  --btn-border: var(--gold-line);
  --btn-fg: var(--gold);
}

.btn--gold::before {
  background: var(--gold);
}

.btn__icon {
  inline-size: 1.05em;
  block-size: 1.05em;
  flex: none;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
}

/* These modifiers were used in the markup but never defined, which is why the
   contact buttons and note sat hard left under a centred heading. */
.btn-row--center {
  justify-content: center;
}

.btn-row--spaced {
  margin-block-start: var(--space-m);
}

/* Text link with an animated underline */
.link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  color: var(--text);
  padding-block-end: 2px;
}

.link::after {
  content: '';
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  block-size: 1px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur) var(--ease-out);
}

.link:hover::after,
.link:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left center;
}

/* --------------------------------------------------------------------------
   3. Header & navigation
   -------------------------------------------------------------------------- */
.site-header {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: var(--z-header);
  block-size: var(--header-h);
  display: flex;
  align-items: center;
  transition:
    background-color var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out),
    backdrop-filter var(--dur) var(--ease-out);
  border-block-end: 1px solid transparent;
}

/* Before the header goes solid it floats over the hero film. This keeps the
   wordmark and nav legible over a bright frame without darkening the film. */
.site-header::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: calc(var(--header-h) * 1.9);
  background: linear-gradient(180deg, rgba(9, 9, 9, 0.86), rgba(9, 9, 9, 0));
  pointer-events: none;
  z-index: -1;
  transition: opacity var(--dur) var(--ease-out);
}

.site-header.is-stuck::before {
  opacity: 0;
}

.site-header.is-stuck {
  background-color: rgba(9, 9, 9, 0.82);
  border-block-end-color: var(--border);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
}

.site-header__inner {
  inline-size: 100%;
  max-inline-size: var(--container-wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
}

.brand {
  display: inline-flex;
  align-items: center;
  flex: none;
  line-height: 0;
}

/* The complete supplied artwork — mark and strapline as one image, which is
   how the logo is meant to be used. It is sized to the header rather than the
   other way round, and the header is tall enough to carry it. */
.brand__logo {
  inline-size: clamp(10.5rem, 15vw, 13.5rem);
  block-size: auto;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.brand:hover .brand__logo,
.brand:focus-visible .brand__logo {
  opacity: 0.85;
}

.nav {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 2.4vw, 2.5rem);
}

.nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
  font-size: var(--step--1);
  font-weight: 450;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  padding-block: 0.4rem;
  transition: color var(--dur-fast) var(--ease-out);
}

.nav__link::after {
  content: '';
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: 1px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur) var(--ease-out);
}

.nav__link:hover,
.nav__link:focus-visible {
  color: var(--text);
}

.nav__link:hover::after,
.nav__link:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left center;
}

.nav__link[aria-current='page'] {
  color: var(--text);
}

.nav__link[aria-current='page']::after {
  transform: scaleX(1);
}

.header__cta {
  padding: 0.7em 1.4em;
  min-block-size: 2.6rem;
}

/* Hamburger — three lines that morph into a cross */
.nav-toggle {
  display: none;
  position: relative;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-full);
  flex: none;
}

.nav-toggle__bar {
  position: absolute;
  inset-inline-start: 50%;
  inline-size: 1.05rem;
  block-size: 1.5px;
  background: var(--text);
  border-radius: 2px;
  transition:
    transform var(--dur) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out);
}

.nav-toggle__bar:nth-child(1) {
  transform: translate(-50%, -5px);
}

.nav-toggle__bar:nth-child(2) {
  transform: translate(-50%, 0);
}

.nav-toggle__bar:nth-child(3) {
  transform: translate(-50%, 5px);
}

.nav-toggle[aria-expanded='true'] .nav-toggle__bar:nth-child(1) {
  transform: translate(-50%, 0) 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: translate(-50%, 0) rotate(-45deg);
}

@media (max-width: 60rem) {
  .nav-toggle {
    display: block;
  }

  .header__cta {
    display: none;
  }

  /* ------------------------------------------------------------------------
     The mobile sheet. One column, one rhythm: three page links set large with
     hairline separators, the two portfolio destinations as a chip row beneath
     the Portfolio link, then the project CTA and contact pinned to the foot.
     Earlier versions kept the desktop dropdown styling here, which produced
     three unrelated type sizes and a cramped indented list.
     ------------------------------------------------------------------------ */
  .nav {
    position: fixed;
    inset: 0;
    z-index: -1;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    padding: calc(var(--header-h) + 1.25rem) var(--gutter) max(1.5rem, env(safe-area-inset-bottom));
    background:
      radial-gradient(ellipse 90% 40% at 50% 0%, rgba(201, 169, 97, 0.06), transparent 70%),
      var(--bg);
    overflow-y: auto;
    clip-path: circle(0% at calc(100% - 3.2rem) 3.1rem);
    transition: clip-path 560ms var(--ease-in-out);
    pointer-events: none;
  }

  .nav.is-open {
    clip-path: circle(150% at calc(100% - 3.2rem) 3.1rem);
    pointer-events: auto;
    z-index: -1;
  }

  /* Links are VISIBLE at rest — the closed sheet is clipped to nothing, so
     hiding them again with opacity would be redundant. It was also fragile:
     an opacity-0 base + transition-to-visible means any engine hiccup leaves
     the open menu blank. The stagger is now an entrance animation, pure
     decoration on top of an already-working menu. */
  .nav__link {
    display: flex;
    align-items: center;
    inline-size: 100%;
    font-family: var(--font-display);
    font-size: var(--step-4);
    font-weight: 500;
    letter-spacing: -0.02em;
    text-transform: none;
    color: var(--text);
    padding-block: 0.85rem;
    border-block-end: 1px solid var(--border);
  }

  .nav.is-open .nav__link {
    animation: nav-item-in 460ms var(--ease-out) both;
    animation-delay: calc(var(--i, 0) * 60ms + 100ms);
  }

  @keyframes nav-item-in {
    from {
      opacity: 0;
      transform: translateY(14px);
    }
  }

  .nav__link::after {
    display: none;
  }

  .nav__link[aria-current='page'] {
    color: var(--gold);
  }

  /* Portfolio group: link keeps its separator; chips sit between it and About.
     Scoped as .nav .nav__item because the desktop dropdown's base rules sit
     later in the file and would otherwise win the cascade at equal
     specificity — which laid the chips out BESIDE the Portfolio link. */
  .nav .nav__item {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    inline-size: 100%;
  }

  .nav .nav__item .nav__link {
    border-block-end: 0;
  }

  .nav .nav__caret {
    display: none;
  }

  .nav .nav__item::after {
    display: none;
  }

  .nav__item .nav__menu {
    position: static;
    translate: none;
    transform: none;
    opacity: 1;
    visibility: visible;
    min-inline-size: 0;
    display: flex;
    gap: 0.5rem;
    padding: 0 0 1rem;
    margin: 0;
    background: none;
    border: 0;
    border-block-end: 1px solid var(--border);
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  .nav.is-open .nav__item .nav__menu {
    animation: nav-item-in 460ms var(--ease-out) both;
    animation-delay: 220ms;
  }

  .nav__item .nav__menu-link {
    flex: 1;
    justify-content: center;
    padding: 0.7rem 1rem;
    font-size: var(--step-0);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-full);
    color: var(--muted);
    background: var(--surface);
  }

  .nav__item .nav__menu-link:hover,
  .nav__item .nav__menu-link:focus-visible {
    color: var(--text);
    border-color: var(--gold-line);
    background: var(--surface);
  }

  /* Foot of the sheet: CTA, then the two direct lines. */
  .nav__meta {
    margin-block-start: auto;
    padding-block-start: var(--space-l);
    display: grid;
    gap: var(--space-2xs);
  }

  .nav.is-open .nav__meta {
    animation: nav-item-in 460ms var(--ease-out) both;
    animation-delay: 320ms;
  }

  .nav__cta {
    inline-size: 100%;
    margin-block-end: var(--space-s);
  }

  .nav__meta-link {
    display: inline-flex;
    align-items: center;
    min-block-size: 2.4rem;
    font-size: var(--step-0);
    color: var(--muted);
  }

  .nav__meta-link:hover,
  .nav__meta-link:focus-visible {
    color: var(--text);
  }
}

@media (min-width: 60.0625rem) {
  .nav__meta {
    display: none;
  }
}

/* Locks background scroll while the mobile nav or an overlay is open */
body.is-locked {
  overflow: hidden;
  touch-action: none;
}

/* --------------------------------------------------------------------------
   4. Footer
   -------------------------------------------------------------------------- */
.site-footer {
  border-block-start: 1px solid var(--border);
  background: var(--bg-elevated);
  padding-block: var(--space-2xl) var(--space-l);
  margin-block-start: var(--space-3xl);
}

.footer__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: var(--space-xl) var(--space-l);
  align-items: start;
}

.footer__title {
  font-family: var(--font-display);
  font-size: var(--step-3);
  letter-spacing: -0.025em;
  margin-block-end: var(--space-xs);
}

.footer__label {
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted-dim);
  margin-block-end: var(--space-2xs);
}

.footer__list {
  display: grid;
  gap: 0.55rem;
  list-style: none;
  padding: 0;
  color: var(--muted);
}

.footer__list a:hover,
.footer__list a:focus-visible {
  color: var(--text);
}

.footer__bottom {
  margin-block-start: var(--space-xl);
  padding-block-start: var(--space-m);
  border-block-start: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s) var(--space-m);
  justify-content: space-between;
  align-items: center;
  font-size: var(--step--1);
  color: var(--muted-dim);
}

.social {
  display: flex;
  gap: var(--space-2xs);
}

.social__link {
  display: grid;
  place-items: center;
  inline-size: 2.6rem;
  block-size: 2.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  color: var(--muted);
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.social__link:hover,
.social__link:focus-visible {
  color: var(--gold);
  border-color: var(--gold-line);
  transform: translateY(-2px);
}

.social__link svg {
  inline-size: 1.1rem;
  block-size: 1.1rem;
}

/* --------------------------------------------------------------------------
   5. Cards
   -------------------------------------------------------------------------- */
.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: var(--space-m);
  overflow: hidden;
  isolation: isolate;
  transition:
    transform var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out);
}

.card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--grad-card);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
}

.card:hover {
  transform: translateY(-4px);
  border-color: var(--border-strong);
}

.card:hover::before {
  opacity: 1;
}

.card__index {
  font-family: var(--font-display);
  font-size: var(--step--1);
  color: var(--gold);
  letter-spacing: 0.1em;
}

.card__title {
  margin-block: var(--space-xs) var(--space-2xs);
}

.card__text {
  color: var(--muted);
  font-size: var(--step-0);
}

/* --------------------------------------------------------------------------
   6. Infinite marquee — duplicated track, translated by exactly -50%
   -------------------------------------------------------------------------- */
.marquee {
  position: relative;
  overflow: hidden;
  padding-block: var(--space-m);
  border-block: 1px solid var(--border);
  /* Feather the edges so items do not pop at the boundary */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.marquee__track {
  display: flex;
  inline-size: max-content;
  will-change: transform;
  animation: marquee-scroll var(--marquee-duration, 42s) linear infinite;
}

.marquee__group {
  display: flex;
  align-items: center;
  gap: clamp(2rem, 5vw, 4.5rem);
  padding-inline-end: clamp(2rem, 5vw, 4.5rem);
  flex: none;
}

.marquee__item {
  display: inline-flex;
  align-items: center;
  gap: 0.9rem;
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 400;
  letter-spacing: -0.02em;
  color: var(--muted-dim);
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out);
}

.marquee__item::before {
  content: '';
  inline-size: 0.4rem;
  block-size: 0.4rem;
  border-radius: 50%;
  background: var(--gold);
  opacity: 0.55;
  flex: none;
}

.marquee:hover .marquee__item {
  color: var(--muted);
}

.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track {
  animation-play-state: paused;
}

@keyframes marquee-scroll {
  from {
    transform: translate3d(0, 0, 0);
  }

  to {
    transform: translate3d(-50%, 0, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .marquee__track {
    animation: none;
    transform: none;
  }

  .marquee {
    overflow-x: auto;
  }
}

/* --------------------------------------------------------------------------
   7. Custom cursor

   A precise dot at the true pointer position, and a thin ring that trails it
   by a fraction. That pairing is the one that actually works: the dot keeps
   clicking honest, the ring supplies the weight. Over media the ring fills and
   the dot inverts, so interactive things announce themselves without printing
   a word across the photograph. Everything is small — 24px, not 76px.
   -------------------------------------------------------------------------- */
.cursor {
  display: none;
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .js .cursor {
    display: block;
    position: fixed;
    inset-block-start: 0;
    inset-inline-start: 0;
    z-index: var(--z-cursor);
    pointer-events: none;
    opacity: 0;
    transition: opacity 180ms var(--ease-out);
  }

  .js .cursor.is-active {
    opacity: 1;
  }

  .cursor__ring,
  .cursor__dot {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    border-radius: 50%;
    will-change: transform;
  }

  .cursor__dot {
    inline-size: 5px;
    block-size: 5px;
    margin: -2.5px 0 0 -2.5px;
    background: var(--warm);
    transition: background-color 200ms var(--ease-out);
  }

  .cursor__ring {
    inline-size: 24px;
    block-size: 24px;
    margin: -12px 0 0 -12px;
    border: 1px solid rgba(237, 228, 214, 0.5);
    /* A very shallow breath, slow enough that you feel it rather than see it. */
    animation: cursor-breathe 3.6s var(--ease-in-out) infinite;
    transition:
      inline-size 260ms var(--ease-out),
      block-size 260ms var(--ease-out),
      margin 260ms var(--ease-out),
      background-color 260ms var(--ease-out),
      border-color 260ms var(--ease-out);
  }

  @keyframes cursor-breathe {
    0%, 100% { opacity: 0.75; }
    50% { opacity: 1; }
  }

  /* Over media, buttons and links. */
  .cursor.is-hover .cursor__ring {
    inline-size: 42px;
    block-size: 42px;
    margin: -21px 0 0 -21px;
    background: rgba(237, 228, 214, 0.14);
    border-color: var(--gold);
    animation-duration: 2.4s;
  }

  .cursor.is-hover .cursor__dot {
    background: var(--gold);
  }

  /* Hide the native cursor over the canvas, never over anything typed into
     or over a video, where the visitor needs the real transport controls. */
  .js.has-custom-cursor body {
    cursor: none;
  }

  .js.has-custom-cursor input,
  .js.has-custom-cursor textarea,
  .js.has-custom-cursor select,
  .js.has-custom-cursor video,
  .js.has-custom-cursor [contenteditable] {
    cursor: auto;
  }
}

/* --------------------------------------------------------------------------
   8. Loading screen — first visit only

   The mark, and a hairline that fills across the very bottom of the screen.
   It is deliberately quick: a loading screen is a courtesy while the first
   frames decode, not a title sequence. There is a hard 2.6s CSS failsafe
   underneath it so the page can never be held hostage by a script that did
   not run.
   -------------------------------------------------------------------------- */
.loader {
  display: none;
}

.js .loader {
  display: grid;
  position: fixed;
  inset: 0;
  z-index: var(--z-loader);
  place-items: center;
  background: var(--bg);
  transition:
    opacity 560ms var(--ease-out),
    visibility 0s linear 560ms;
  animation: loader-failsafe 700ms var(--ease-out) 2600ms forwards;
}

@keyframes loader-failsafe {
  to {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }
}

.js .loader.is-done {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.loader__inner {
  display: grid;
  justify-items: center;
  gap: var(--space-s);
  text-align: center;
  padding-inline: var(--gutter);
  animation: loader-in 900ms var(--ease-out) both;
}

@keyframes loader-in {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to { opacity: 1; transform: none; }
}

.loader__logo {
  inline-size: clamp(13rem, 34vw, 21rem);
  block-size: auto;
}

.loader__word {
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The bar sits on the bottom edge of the viewport, full width. */
.loader__bar {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  block-size: 2px;
  background: rgba(255, 255, 255, 0.07);
  overflow: hidden;
}

.loader__fill {
  display: block;
  block-size: 100%;
  inline-size: 100%;
  background: linear-gradient(90deg, var(--gold), var(--warm));
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
  transition: transform 320ms var(--ease-out);
}

/* If the script never runs, the bar still crosses and the screen still clears. */
.js .loader__fill {
  animation: loader-sweep 2.5s var(--ease-out) forwards;
}

.js .loader.is-primed .loader__fill {
  animation: none;
}

@keyframes loader-sweep {
  from { transform: scaleX(0.04); }
  to { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .loader__inner {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   9. Page transition veil — prevents the white flash between documents
   -------------------------------------------------------------------------- */
.veil {
  display: none;
}

.js .veil {
  display: block;
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-loader) - 1);
  background: var(--bg);
  opacity: 0;
  pointer-events: none;
  transition: opacity 340ms var(--ease-out);
}

.js .veil.is-visible {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .js .veil {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   10. Floating actions — contact on the right, back-to-top on the left
   Kept apart deliberately: two controls stacked in one corner get mis-tapped
   on a phone, and the one you hit by mistake is the one that navigates away.
   -------------------------------------------------------------------------- */
.actions {
  position: fixed;
  inset-block-end: clamp(1rem, 3vw, 2rem);
  inset-inline-end: clamp(1rem, 3vw, 2rem);
  z-index: var(--z-overlay);
  /* Side by side, not stacked — two pills in a column read as a dropdown that
     failed to close. */
  display: flex;
  align-items: center;
  gap: 0.6rem;
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  pointer-events: none;
  transition:
    opacity var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out);
}

.actions.is-visible {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.actions__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.8rem 1.25rem 0.8rem 1rem;
  background: rgba(20, 20, 20, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-full);
  box-shadow: var(--shadow-card);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.06em;
  white-space: nowrap;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.actions__btn:hover,
.actions__btn:focus-visible {
  border-color: var(--gold-line);
  background: rgba(26, 26, 26, 0.97);
  transform: translateY(-2px);
}

.actions__icon {
  inline-size: 1.3rem;
  block-size: 1.3rem;
  flex: none;
  color: var(--warm);
}

.actions__icon--wa {
  color: #25d366;
}

/* Staggered entrance so the two buttons arrive as a set, not a clump. */
.actions__btn:nth-child(1) { transition-delay: 0ms; }
.actions__btn:nth-child(2) { transition-delay: 60ms; }

.to-top {
  position: fixed;
  inset-block-end: clamp(1rem, 3vw, 2rem);
  inset-inline-start: clamp(1rem, 3vw, 2rem);
  z-index: var(--z-overlay);
  display: grid;
  place-items: center;
  inline-size: 3rem;
  block-size: 3rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-full);
  background: rgba(20, 20, 20, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--muted);
  box-shadow: var(--shadow-card);
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  pointer-events: none;
  transition:
    opacity var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.to-top.is-visible {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.to-top:hover,
.to-top:focus-visible {
  color: var(--gold);
  border-color: var(--gold-line);
  transform: translateY(-3px);
}

.to-top svg {
  inline-size: 1.1rem;
  block-size: 1.1rem;
}

@media (max-width: 34rem) {
  .actions__text {
    display: none;
  }

  .actions__btn {
    padding: 0.85rem;
  }

  .to-top {
    inline-size: 2.75rem;
    block-size: 2.75rem;
  }
}

/* --------------------------------------------------------------------------
   11. Media frame — shared by galleries, reel and lightbox
   -------------------------------------------------------------------------- */
.frame {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--surface);
  border-radius: var(--radius-m);
  border: 1px solid var(--border);
}

.frame img,
.frame video {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* Scale is animated, not width/height — stays on the compositor */
  transition: transform var(--dur-slow) var(--ease-out);
  backface-visibility: hidden;
}

.frame__veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(9, 9, 9, 0) 28%, rgba(9, 9, 9, 0.55) 58%, rgba(9, 9, 9, 0.94) 100%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
}

.frame__meta {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  padding: var(--space-s);
  display: grid;
  gap: 0.15rem;
  transform: translate3d(0, 12px, 0);
  opacity: 0;
  transition:
    opacity var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out);
}

.frame__title {
  font-family: var(--font-display);
  font-size: var(--step-0);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: #ffffff;
  /* A caption sits over whatever the photograph happens to be. The scrim
     handles most of it; the shadow covers a blown-out highlight landing
     exactly behind the text. */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 18px rgba(0, 0, 0, 0.6);
}

.frame__tag {
  font-size: 0.74rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #e8cd8e;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

.media-item:hover .frame img,
.media-item:focus-visible .frame img,
.media-item:hover .frame video {
  transform: scale(1.05);
}

.media-item:hover .frame__veil,
.media-item:focus-visible .frame__veil,
.media-item:hover .frame__meta,
.media-item:focus-visible .frame__meta {
  opacity: 1;
}

.media-item:hover .frame__meta,
.media-item:focus-visible .frame__meta {
  transform: none;
}

/* Skeleton shown until the image decodes */
.frame::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, #131313 30%, #1b1b1b 50%, #131313 70%);
  background-size: 220% 100%;
  animation: skeleton 1.4s var(--ease-in-out) infinite;
  opacity: 1;
  transition: opacity var(--dur) var(--ease-out);
  pointer-events: none;
}

.frame.is-loaded::after {
  opacity: 0;
  animation: none;
}

@keyframes skeleton {
  from {
    background-position: 130% 0;
  }

  to {
    background-position: -30% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .frame::after {
    animation: none;
  }
}

/* Play badge over video thumbnails */
.play-badge {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  transform: translate(-50%, -50%);
  inline-size: 3.6rem;
  block-size: 3.6rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(9, 9, 9, 0.55);
  border: 1px solid rgba(245, 245, 245, 0.4);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--text);
  transition:
    background-color var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out);
}

.play-badge svg {
  inline-size: 1.05rem;
  block-size: 1.05rem;
  margin-inline-start: 2px;
}

.media-item:hover .play-badge,
.media-item:focus-visible .play-badge {
  background: var(--warm);
  border-color: var(--warm);
  color: #0b0b0b;
  transform: translate(-50%, -50%) scale(1.06);
}

.duration {
  position: absolute;
  inset-block-end: 0.7rem;
  inset-inline-end: 0.7rem;
  padding: 0.22rem 0.55rem;
  font-size: 0.74rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--text);
  background: rgba(9, 9, 9, 0.7);
  border-radius: var(--radius-s);
}

/* --------------------------------------------------------------------------
   12. Filters
   -------------------------------------------------------------------------- */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-block-end: var(--space-l);
}

.filter {
  padding: 0.55em 1.15em;
  font-size: var(--step--1);
  font-weight: 450;
  letter-spacing: 0.06em;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.filter:hover,
.filter:focus-visible {
  color: var(--text);
  border-color: var(--border-strong);
}

.filter[aria-pressed='true'] {
  color: #0b0b0b;
  background: var(--warm);
  border-color: var(--warm);
}

.filter__count {
  font-variant-numeric: tabular-nums;
  opacity: 0.6;
  margin-inline-start: 0.4em;
}

/* --------------------------------------------------------------------------
   13. Overlays — lightbox and video modal share one shell
   -------------------------------------------------------------------------- */
.overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: rgba(6, 6, 6, 0.94);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--dur) var(--ease-out),
    visibility 0s linear var(--dur);
}

.overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.overlay__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  padding: var(--space-s) var(--gutter);
}

.overlay__counter {
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.14em;
  color: var(--muted);
}

.overlay__stage {
  position: relative;
  display: grid;
  place-items: center;
  padding-inline: clamp(0.75rem, 4vw, 4.5rem);
  min-block-size: 0;
}

/* The <picture> wrapper must not collapse to an inline box, or the image
   loses its centring and its max-height constraint on short viewports. */
.overlay__figure {
  display: grid;
  place-items: center;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
}

.overlay__media {
  max-inline-size: 100%;
  max-block-size: 100%;
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
  border-radius: var(--radius-s);
  opacity: 0;
  transform: scale(0.985);
  transition:
    opacity var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out);
}

.overlay__media.is-ready {
  opacity: 1;
  transform: none;
}

.overlay__foot {
  padding: var(--space-s) var(--gutter) var(--space-m);
  display: grid;
  gap: 0.2rem;
  text-align: center;
  min-block-size: 3.5rem;
}

.overlay__title {
  font-family: var(--font-display);
  font-size: var(--step-1);
  letter-spacing: -0.02em;
}

.overlay__tag {
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold);
}

.icon-btn {
  display: grid;
  place-items: center;
  inline-size: 2.9rem;
  block-size: 2.9rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-full);
  color: var(--text);
  background: rgba(20, 20, 20, 0.6);
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.icon-btn:hover,
.icon-btn:focus-visible {
  background: var(--warm);
  border-color: var(--warm);
  color: #0b0b0b;
}

.icon-btn:disabled {
  opacity: 0.3;
  pointer-events: none;
}

.icon-btn svg {
  inline-size: 1.05rem;
  block-size: 1.05rem;
}

.overlay__nav {
  position: absolute;
  inset-block-start: 50%;
  transform: translateY(-50%);
  z-index: 1;
}

.overlay__nav--prev {
  inset-inline-start: clamp(0.4rem, 1.5vw, 1.25rem);
}

.overlay__nav--next {
  inset-inline-end: clamp(0.4rem, 1.5vw, 1.25rem);
}

@media (max-width: 40rem) {
  .overlay__nav {
    inset-block-start: auto;
    inset-block-end: 0;
    transform: none;
  }
}

/* Video element inside the modal keeps its own controls */
.overlay__video {
  inline-size: min(100%, 74rem);
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: var(--radius-m);
}

/* --------------------------------------------------------------------------
   14. Empty state
   -------------------------------------------------------------------------- */
.empty {
  padding: var(--space-2xl) var(--space-m);
  text-align: center;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-m);
  color: var(--muted);
}

.empty__title {
  font-family: var(--font-display);
  font-size: var(--step-2);
  color: var(--text);
  margin-block-end: var(--space-2xs);
}

/* --------------------------------------------------------------------------
   15. Footer clearance for the floating WhatsApp button
   The button is fixed to the bottom-right. At the very bottom of the page it
   used to sit on top of the credit line. Reserving the space is more robust
   than moving the button, which visitors have learned to expect there.
   -------------------------------------------------------------------------- */
.site-footer {
  padding-block-end: 6.5rem;
}

.footer__bottom {
  align-items: flex-start;
}

.footer__credit {
  color: var(--muted);
}

.footer__credit a {
  color: var(--gold);
  border-block-end: 1px solid transparent;
  transition: border-color var(--dur-fast) var(--ease-out);
}

.footer__credit a:hover,
.footer__credit a:focus-visible {
  border-block-end-color: var(--gold);
}

.footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-m);
  margin-block-start: var(--space-s);
  font-size: var(--step--1);
}

.footer__legal a:hover,
.footer__legal a:focus-visible {
  color: var(--text);
}

@media (max-width: 46rem) {
  .footer__bottom {
    flex-direction: column;
  }
}

/* --------------------------------------------------------------------------
   16. Nav dropdown — Portfolio splits into Photos and Films
   -------------------------------------------------------------------------- */
.nav__item {
  position: relative;
  display: flex;
  align-items: center;
}

.nav__caret {
  display: inline-block;
  flex: none;
  inline-size: 0.6rem;
  block-size: 0.6rem;
  transition: transform var(--dur-fast) var(--ease-out);
  flex: none;
}

.nav__item.is-open .nav__caret {
  transform: rotate(180deg);
}

.nav__menu {
  position: absolute;
  inset-block-start: calc(100% + 0.85rem);
  inset-inline-start: 50%;
  translate: -50% 0;
  min-inline-size: 15rem;
  padding: 0.4rem;
  background: rgba(14, 14, 14, 0.96);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-m);
  box-shadow: var(--shadow-lift);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition:
    opacity var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out),
    visibility 0s linear var(--dur-fast);
  z-index: 2;
}

.nav__item:hover .nav__menu,
.nav__item.is-open .nav__menu {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

/* A bridge across the gap so the menu does not close as the pointer travels. */
.nav__item::after {
  content: '';
  position: absolute;
  inset-block-start: 100%;
  inset-inline: -1rem;
  block-size: 1rem;
}

.nav__menu-link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-s);
  padding: 0.7rem 0.85rem;
  border-radius: var(--radius-s);
  font-size: var(--step-0);
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted);
  transition:
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.nav__menu-link:hover,
.nav__menu-link:focus-visible {
  color: var(--text);
  background: rgba(255, 255, 255, 0.05);
}

.nav__menu-link span {
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  color: var(--muted-dim);
}

.nav__menu-link:hover span {
  color: var(--gold);
}



/* --------------------------------------------------------------------------
   17. Content protection
   Deterrents only — see docs/SECURITY.md. Nothing here can stop a determined
   visitor, and nothing here is allowed to break keyboard access or selection
   of ordinary text.
   -------------------------------------------------------------------------- */
/* Stills are made un-draggable and un-selectable. Video is deliberately NOT
   included: `pointer-events: none` on a <video> disables its own controls, so
   the visitor could not pause, scrub or jump to 0:20. Protecting a poster is
   worth a little friction; breaking the player is not. Right-click on video is
   still intercepted in protect.js, and controlslist blocks the download item. */
.protected img {
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  pointer-events: none;
}

.protected video {
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* The decorative hero loop has no controls and must never eat a click. */
.hero__media video {
  pointer-events: none;
}

/* A transparent layer over each frame so "save image as" lands on nothing. */
.frame::before,
.reel::before,
.hero__media::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: auto;
}

/* Only over the still image in the lightbox — never over the video modal,
   where it would swallow every click on the transport controls. */
.overlay__figure:not(:has(video))::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
}

/* --------------------------------------------------------------------------
   18. <picture> must fill its frame
   An <img> with `block-size: 100%` resolves against its containing block —
   which is the <picture>, not the frame. <picture> defaults to auto height, so
   the image silently collapsed to its natural aspect and left the rest of the
   frame empty and black. Every fixed-ratio container needs the wrapper to
   stretch too.
   -------------------------------------------------------------------------- */
.frame picture,
.rail__item picture,
.sequence__frame picture,
.brand-moment__art picture,
.portrait picture,
.hero__media picture {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}

/* --------------------------------------------------------------------------
   19. Touch target sizes
   WCAG 2.2 sets 24x24 CSS pixels as the minimum for a pointer target. Text
   links in the footer and the nav sheet were 22px tall — fine with a mouse,
   fiddly with a thumb. Padding the line box costs no layout and fixes it
   everywhere at once.
   -------------------------------------------------------------------------- */
.footer__list a,
.footer__legal a,
.nav__meta a,
.contact__note a,
.footer__credit a {
  display: inline-flex;
  align-items: center;
  min-block-size: 1.75rem;
}

/* The scroll cue was a 1px hairline wrapped in an anchor, so its hit area was
   1px wide. The line stays hairline; the target around it does not. */
.scroll-cue {
  display: grid;
  place-items: center;
  inline-size: 3rem;
  padding-block: 0.5rem;
}

@media (pointer: coarse) {
  /* Give every inline link in running text a little more room on touch. */
  .link,
  .channel,
  .social__link {
    min-block-size: 2.75rem;
  }

  .channel {
    padding-block: 1.1rem;
  }
}
