/* ============================================================
   МОЙ — Evgeny Mylnikov · Editorial Portfolio
   Colours sampled from author mockups:
     beige  #FCF2E7   ·   grey-blue  #A9C1DB
   Gradient lives ONLY on the first screen; the page resolves to
   solid grey-blue below it. Single red signal accent.
   ============================================================ */

/* ---------- Fonts ----------
   Every face below is a file actually supplied with the project. Read out of
   each TTF's own name table, they are:

     ObjectSans-Heavy.ttf                 PP Object Sans Black   (one cut)
     AdventPro_SemiExpanded-Light.ttf     Advent Pro SemiExpanded Light
     AdventPro_Expanded-Light.ttf         Advent Pro Expanded Light
     AdventPro_ExtraExpanded-ExtraBold.ttf  … ExtraExpanded ExtraBold
     AdventPro_Expanded-Black.ttf         Advent Pro Expanded Black

   That inventory decides the roles, because the cuts are not interchangeable:
   PP Object Sans ships as a single black display cut and cannot set body copy;
   the SemiExpanded Light cut is the narrowest and the only one comfortable at
   17px, and it was the ONE supplied file the old CSS never declared at all.
   Montserrat (Google) is the dropdown face, which is a stated requirement.

   The UltraExpanded cuts that used to drive "Advent Pro Display" are gone from
   the system: that was the reference_old choice, not this design's. */
@font-face {                                     /* role: display  */
  font-family: "PP Object Sans";
  src: url("../assets/fonts/ObjectSans-Heavy.ttf") format("truetype");
  font-weight: 900; font-style: normal; font-display: swap;
}
@font-face {                                     /* role: body     */
  font-family: "Advent Pro Text";
  src: url("../assets/fonts/AdventPro_SemiExpanded-Light.ttf") format("truetype");
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {                                     /* role: meta/nav */
  font-family: "Advent Pro";
  src: url("../assets/fonts/AdventPro_Expanded-Light.ttf") format("truetype");
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Advent Pro";
  src: url("../assets/fonts/AdventPro_ExtraExpanded-ExtraBold.ttf") format("truetype");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Advent Pro";
  src: url("../assets/fonts/AdventPro_Expanded-Black.ttf") format("truetype");
  font-weight: 900; font-style: normal; font-display: swap;
}


/* ---------- Tokens ---------- */
:root {
  --beige:  #FCF2E7;   /* warm cream (top of hero) */
  --blue:   #A9C1DB;   /* grey-blue (everything below the fold) */
  --blue-2: #C7D6E6;   /* lighter blue used inside gradient mid */

  --ink: #20242B;                 /* soft near-black slate (mockup text) */
  --ink-50: rgba(32, 36, 43, 0.55);
  --ink-30: rgba(32, 36, 43, 0.30);
  --ink-12: rgba(32, 36, 43, 0.12);
  --line:   rgba(32, 36, 43, 0.22);

  --accent: #FF3B30;              /* signal red */
  --white-circle: rgba(255, 255, 255, 0.96);

  /* ---- TYPOGRAPHY ROLES ----
     One token per role; nothing outside this block names a font family, so a
     role is re-pointed in exactly one line and every page follows.

     Montserrat carries headings and the dropdown/UI. Body and meta stay on
     Inter — that is what this implementation has always set them to
     (--font-ui was "Inter, …" and .body-text/.meta both resolved through it),
     and Inter has been in the Google Fonts link the whole time. Body is
     deliberately NOT the heading face. The local Advent Pro / PP Object Sans
     files remain declared below but are assigned to no role. */
  --font-heading: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-body:    "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-meta:    "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-menu:    "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* aliases kept so existing rules keep resolving; not separate roles */
  --font-ui:      var(--font-meta);
  --font-display: var(--font-heading);

  --meta-size: 0.75rem;
  --meta-track: 0.12em;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* ---- SPACING ----
     Shared, so moving the navigation is one edit rather than four. */
  --gutter: clamp(25px, 4.2vw, 60px);        /* page side gutter            */
  --pad-y:  clamp(20px, 3vh, 34px);
  --nav-bottom: clamp(48px, 6vh, 72px);      /* footer/nav offset from base */
  --pod-btn: 52px;                           /* nav circle diameter         */
  --pod-gap: 7px;                            /* clear space between circles */
  --pod-pad: 10px;                           /* capsule inset               */

  /* design-system spacing scale (reference 8px base) */
  --sp-2: 12px; --sp-3: 16px; --sp-4: 24px; --sp-6: 40px;
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; overscroll-behavior-x: none; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; background: var(--blue); }
body {
  font-family: var(--font-ui);
  color: var(--ink);
  background: transparent;          /* grey-blue ground lives on <html>; gradient layers above it */
  overflow-x: hidden;
  cursor: none;
}
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: none; }
::selection { background: var(--accent); color: #fff; }

/* Hero gradient — anchored to the TOP of the document (scrolls away),
   one viewport tall, melting from beige into the grey-blue ground. */
.hero-gradient {
  position: absolute; top: 0; left: 0; right: 0;
  height: 100vh; height: 100svh;
  z-index: -1; pointer-events: none;
  background: linear-gradient(180deg,
    var(--beige) 0%,
    #F4EFE9 22%,
    var(--blue-2) 64%,
    var(--blue) 98%);
}
/* contacts is solid blue in the mockup — no gradient there */
body.view-contacts .hero-gradient { opacity: 0; }

/* ---------- Typography helpers ---------- */
/* role: heading — Montserrat Medium. Size and line-height are unchanged. */
.display {
  font-family: var(--font-heading);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.0;
  color: var(--ink);
}
.meta {
  font-family: var(--font-ui);
  font-weight: 400;
  text-transform: uppercase;
  font-size: var(--meta-size);
  letter-spacing: var(--meta-track);
  line-height: 1;
  color: var(--ink);
}
/* role: body — Inter, the face this implementation has always used for copy */
.body-text {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(0.95rem, 1.18vw, 1.0625rem);
  line-height: 1.47;
  opacity: 0.82;
  text-wrap: pretty;
}

/* ---------- Media containers ---------- */
.media { position: relative; overflow: hidden; background: rgba(32,36,43,0.08); }
.media img, .media video {
  width: 100%; height: 100%; object-fit: cover; display: block; will-change: transform;
}
.media-h { width: 38vw; max-width: 600px; aspect-ratio: 16 / 10; }

/* ============================================================
   CUSTOM CURSOR
   ============================================================ */
.cursor { position: fixed; top: 0; left: 0; z-index: 9999; pointer-events: none; }
.cursor-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--ink);
  transform: translate(-50%, -50%); transition: opacity 0.25s var(--ease);
  opacity: 0; /* hidden until the first pointermove — never flashes at (0,0) */
}
body.cursor-live .cursor-dot { opacity: 1; }
.cursor-ring {
  position: fixed; top: 0; left: 0; z-index: 9998; pointer-events: none;
  width: 86px; height: 86px; border-radius: 50%;
  background: var(--white-circle);
  transform: translate(-50%, -50%) scale(0);
  display: grid; place-items: center; will-change: transform;
}
.cursor-ring .sign {
  font-family: var(--font-ui); font-weight: 300; font-size: 26px; line-height: 1;
  color: var(--accent); transform: scale(0); transition: transform 0.3s var(--ease);
}
.cursor-ring.is-open .sign { transform: scale(1); }
.cursor-ring.connect { background: var(--accent); }
.cursor-ring.connect .sign { color: #fff; }
body.cursor-hidden .cursor-dot { opacity: 0; }

/* thin light ring variant used over imagery (WebGL hover) */
.cursor-ring.thin { background: transparent; box-shadow: inset 0 0 0 1.4px rgba(255,255,255,0.9); }
.cursor-ring.thin .sign { color: #fff; }

@media (hover: none) {
  body { cursor: auto; }
  .cursor, .cursor-ring { display: none; }
}

/* ============================================================
   GLOBAL CHROME — header + footer
   ============================================================ */
.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 200;
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--pad-y) var(--gutter); pointer-events: none;
}
.site-header > * { pointer-events: auto; }

.logo { width: 50px; height: auto; display: block; }
.logo img { width: 100%; height: auto; }

/* icon button — white circle blooms under the cursor; icon turns red */
.icon-btn {
  position: relative; width: 44px; height: 44px;
  display: grid; place-items: center; border-radius: 50%;
}
.icon-btn .bloom {
  position: absolute; inset: 0; border-radius: 50%; background: #fff;
  transform: translate(var(--mx, 0px), var(--my, 0px)) scale(0); opacity: 0;
  transition: transform 0.35s var(--ease), opacity 0.3s var(--ease);
  z-index: 0; pointer-events: none;
}
.icon-btn .ico { position: relative; z-index: 1; transition: color 0.3s var(--ease); color: var(--ink); }
.icon-btn.mag-on .bloom { transform: translate(var(--mx, 0px), var(--my, 0px)) scale(1); opacity: 1; }
.icon-btn.mag-on .ico { color: var(--accent); }
body.icon-hover .cursor-dot { opacity: 0; }

/* kebab menu icon — three vertical dots */
.menu-dots { display: flex; flex-direction: column; gap: 4px; align-items: center; }
.menu-dots span { width: 4.5px; height: 4.5px; border-radius: 50%; background: currentColor; display: block; }

/* close (x) — two crossed strokes */
.x-ico { width: 19px; height: 19px; position: relative; }
.x-ico i {
  position: absolute; top: 50%; left: 0; width: 100%; height: 1.5px; background: currentColor;
}
.x-ico i:first-child { transform: translateY(-50%) rotate(45deg); }
.x-ico i:last-child  { transform: translateY(-50%) rotate(-45deg); }

/* ---------- bottom footer rail ---------- */
.site-footer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 200;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding: 0 var(--gutter) var(--nav-bottom); pointer-events: none;
}
/* on the home works page the footer sits at the BOTTOM OF THE PAGE, not pinned
   to the screen — it scrolls away with the gallery and only reappears at the end */
.view.home { position: relative; }
.view.works { position: relative; }
.site-footer[data-footer="home"].docked { position: absolute; }
.site-footer > * { pointer-events: auto; }
.site-footer .footer-link { align-self: center; }
.site-footer > .footer-link:first-child { justify-self: start; }
.site-footer > .footer-link:last-child,
.site-footer .footer-right { justify-self: end; grid-column: 3; }

.footer-link { position: relative; }
.footer-link:hover { color: var(--accent); }
.site-footer .footer-link.meta { font-weight: 500; letter-spacing: 0.1em; }

/* center column: label stays on the footer baseline; cue hangs BELOW it
   (absolute) so it never pushes the label out of alignment. */
.footer-center { position: relative; grid-column: 2; display: flex; justify-content: center; align-items: center; }

/* thin vertical cue under "All works" — hangs below the label, stays in view */
.scroll-cue { position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%); width: 1px; height: clamp(34px, 4.4vh, 46px); background: var(--line); overflow: hidden; transition: opacity 0.4s var(--ease); }
.scroll-cue::after {
  content: ""; position: absolute; top: -100%; left: 0; width: 100%; height: 100%;
  background: var(--accent); animation: cue 2.4s var(--ease) infinite;
}
@keyframes cue { 0% { top: -100%; } 60%,100% { top: 100%; } }

/* ============================================================
   SCROLL INDICATORS — ux_redesign/line.svg, inlined
   ------------------------------------------------------------
   The indicator IS the file. index.html and project.html carry its four
   shapes at their authored coordinates; nothing below draws a line, a dot or
   a ring. These rules do two things only: carry over the file's own <style>
   block — which cannot be inlined as-is, because .st0/.st1/.st2 would leak
   into the whole document — and give the <svg> a size.

   line.svg's palette, verbatim:
     .st0  stroke #fff,    stroke-width .5  ->  .ind-track / .ind-end
     .st1  stroke #ff3600, stroke-width 1   ->  .ind-run
     .st2  fill   #ff3600                   ->  .ind-start
   #ff3600 is the file's own orange and is deliberately NOT var(--accent):
   this colour belongs to the indicator, not to the project palette.
   The one departure from the file: the white parts carry the run's 1px weight
   instead of .5 — at .5 the unfilled remainder read as a hairline next to the
   orange and all but vanished on the page. Colours and geometry are untouched.

   ONE geometry, two orientations. The horizontal copy is the same four
   shapes inside a rotate(-90) group, so the two cannot drift apart, and a
   single JS helper drives every copy by moving .ind-run's head. */
.ind { display: block; overflow: visible; }
.ind-track, .ind-run { fill: none; stroke-miterlimit: 10; }
.ind-track { stroke: #fff; stroke-width: 1px; }
.ind-run   { stroke: #ff3600; stroke-width: 1px; }
.ind-start { fill: #ff3600; }
.ind-end   { fill: none; stroke: #fff; stroke-width: 1px; }
/* The end ring is where the run goes, not a second thing that lights up when
   the run arrives. .ind-fill is the SAME circle laid over the white one, drawn
   as one dash that BEGINS at the point the line lands on and runs clockwise
   from there — the line's own head, continued around the circle. Both dash
   values come straight out of setIndicator() as a function of the scroll
   fraction, so there is no timed transition anywhere in the indicator: the arc
   advances, stops and unwinds at exactly the speed the page scrolls.

   The orange settles in the ring after exactly one lap. When the home band
   wraps 1 -> 0, the completed ring returns to white immediately; no white
   tail travels around it.
   stroke-linecap must stay butt: a round cap would overhang the contact point
   and the arc would no longer start exactly where the line ends. */
.ind-fill {
  fill: none; stroke: #ff3600; stroke-width: 1px; stroke-linecap: butt;
  stroke-dasharray: 0 100;   /* empty until the head reaches the ring */
}
/* closed: for the moment the arc is a full lap the white ring underneath takes
   the run's colour too, so the two coincident strokes cannot leave an
   antialiased fringe between them */
.ind.is-full .ind-end { stroke: #ff3600; }

/* vertical — 1:1 with the file's viewBox, so the run is exactly as long as
   line.svg draws it (212.72), not the shorter clamp this replaced */
.ind-v { width: 11.28px; height: 212.72px; }
/* horizontal — same aspect; it only scales down when the footer column is too
   narrow to seat it, and height follows width so the caps never distort */
.ind-h { width: min(276.54px, 31.2vw); height: auto; }

/* ============================================================
   VIEW ROUTER (SPA)
   ============================================================ */
.view { display: none; }
.view.is-active { display: block; }
.view:not(.is-active) { display: none !important; }
.about.is-active, .contacts.is-active { display: flex; }

body.lock { overflow: hidden; height: 100vh; }

/* ============================================================
   HOME — hero (two-row horizontal marquee) + vertical all-works
   ============================================================ */
.home-hero {
  position: relative; height: 100vh; height: 100svh; overflow: clip;
}
.gallery { position: absolute; inset: 0; }
.g-row {
  position: absolute; left: 0; top: 14%; display: flex; align-items: flex-start;
  gap: clamp(46px, 4.6vw, 104px); white-space: nowrap; will-change: transform;
}

/* Лента живёт на GPU: без will-change субпиксельный сдвиг строки и покадровая
   запись --ix заставляют перерисовывать слой на главном потоке, и медленное
   движение читается как подрагивание. Три правила ниже — из исходного
   index.html; были ошибочно удалены как мёртвые на ЭТАПЕ 5 и возвращены
   2026-08-07 после визуальной проверки владельцем. */
.g-item { position: relative; flex: 0 0 auto; will-change: transform; backface-visibility: hidden; }
.g-item .media { will-change: transform; }
.home-hero { touch-action: pan-y; }
.home-hero .gallery { touch-action: none; }
/* scattered vertical stagger across the single band (matches mockup) */
.gp-1 { transform: translateY(9vh) translateX(var(--ix,0px)); }
.gp-2 { transform: translateY(28vh) translateX(var(--ix,0px)); }
.gp-3 { transform: translateY(18vh) translateX(var(--ix,0px)); }
.gp-4 { transform: translateY(-2vh) translateX(var(--ix,0px)); }
.gp-5 { transform: translateY(15vh) translateX(var(--ix,0px)); }

/* scale(1.015) на изображении ленты снят 2026-08-07: под ним canvas ripple
   рисует текстуру в натуральном масштабе, и при наведении картинка скачком
   уменьшалась на 1,5%. В исходном index.html этого правила нет, а от
   волосяной линии на кромке защищает will-change: transform выше. */
.g-item .media img { backface-visibility: hidden; }
.g-item:hover { z-index: 20; }

.g-caption {
  display: flex; gap: 12px; align-items: baseline; margin-top: var(--sp-4);
  text-align: left; opacity: 0; transform: translateY(8px);
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease); white-space: nowrap;
}
.g-item:hover .g-caption { opacity: 1; transform: translateY(0); }
.g-caption .idx { color: var(--accent); }

/* compact, varied sizes (matches mockup proportions) */
.gi-h  { width: clamp(210px, 18.5vw, 300px); aspect-ratio: 16/10; }
.gi-v  { width: clamp(168px, 14.5vw, 225px); aspect-ratio: 3/4; }
.gi-c  { width: clamp(222px, 20.5vw, 312px); aspect-ratio: 4/3; }
.gi-sq { width: clamp(205px, 18.5vw, 318px); aspect-ratio: 1/1; }
.gi-w  { width: clamp(275px, 26vw, 458px); aspect-ratio: 16/9; }

/* ============================================================
   ALL WORKS — sparse editorial catalogue (à la collectionparis)
   12-col grid · 2–3 images per band · lots of air. Each item is
   placed via inline vars: --c (col start) --s (col span)
   --ar (aspect) --dy (vertical stagger). Caption sits in-flow
   below the image: "N°01 Name", small grotesque, no effects.
   ============================================================ */
/* --cat-h is the single source of truth for the catalogue page height.
   js/main.js reads it back from here; the two must not drift apart. */
.home-works { position: relative; padding: 0; --cat-h: 174; height: calc(var(--cat-h) * 1vw); overflow: hidden; }

/* The old .blur-btn chevrons are gone: Project now uses the SAME .nav-pod and
   .icon-btn components as All-works, so there is no project-local button
   implementation left to style here. */
.project-footer .footer-right { display: flex; align-items: center; gap: clamp(20px, 3vw, 44px); }
.project-footer .footer-fwd { flex: none; }

/* ---------------- floating glass nav pod (footer centre) ----------------
   The one footer-centre button on EVERY page — home, All-works and project
   alike. One capsule, four fixed slots: Contacts | All works | scroll | About.

   Only the slot WIDTH animates, never a scale. .footer-center already
   centres the capsule, so growing it can never shift its position.
   In state "top" the capsule is 52 + 2*10 = 72px — the exact footprint of
   the button it replaces, so nothing around it moves.

   Hover is NOT reimplemented: every button is a plain .icon-btn carrying the
   existing .bloom span, so the white circle and the accent icon come from
   the rules above and from initMagneticIcons(). The slots deliberately do
   NOT clip: the bloom travels up to MAXOFF px outside its button and must
   stay whole — see the opacity timings below, which keep the button out of
   sight while its slot is still narrow enough for it to overhang. */
.nav-pod {
  display: flex; align-items: center;
  padding: var(--pod-pad) calc(var(--pod-pad) - var(--pod-gap) / 2);
  border-radius: 999px;
  /* glass: the blur has to actually blur, so it is deep; saturate keeps the
     colour behind it alive instead of turning the panel into grey mud */
  background: rgba(255,255,255,0.34);
  -webkit-backdrop-filter: blur(30px) saturate(1.5);
          backdrop-filter: blur(30px) saturate(1.5);
  border: 1px solid rgba(255,255,255,0.30);
  /* Weight under the glass — deliberately light. The panel should read as a
     pane resting just above the page, not as a card pressed onto it, so both
     drop layers are roughly 40% weaker and pulled tighter than before
     (0.28/0.16 -> 0.16/0.10, 38px/14px -> 28px/10px spread). The inset
     highlight along the top edge is part of the glass, not the weight, and
     is unchanged. backdrop-filter is untouched. */
  box-shadow: 0 12px 28px -18px rgba(32,36,43,0.16),
              0 4px 10px -6px rgba(32,36,43,0.10),
              inset 0 1px 0 rgba(255,255,255,0.30);
}
.pod-slot {
  flex: none; display: grid; place-items: center;
  width: 0; opacity: 0; visibility: hidden;
  /* closing: the button must be GONE before its slot narrows enough to let it
     overhang a neighbour. Fading first is not sufficient — --ease front-loads
     the width — so the width also waits out the fade. Measured: closing two
     slots at once used to overlap the centre circle by 15px. */
  transition: width 0.46s var(--ease) 0.06s, opacity 0.10s var(--ease),
              visibility 0s linear 0.52s;
}
/* Which slots are open is decided by JS, per page and per scroll state, and
   written as .is-open. It used to be a hard-coded list of [data-state] pairs,
   which could only ever describe one page; the pod now serves Home, All works,
   About and Project from this single rule. */
.pod-slot.is-open {
  width: calc(var(--pod-btn) + var(--pod-gap));
  opacity: 1; visibility: visible;
  /* opening: --ease front-loads the width, so by 0.16s the slot is already
     ~86% wide; only then does the button fade in, and it never overhangs */
  transition: width 0.46s var(--ease), opacity 0.24s var(--ease) 0.16s,
              visibility 0s;
}
.nav-pod .pod-btn {
  width: var(--pod-btn); height: var(--pod-btn);
  /* the UA button padding survives the global reset; with border-box it
     shrank the content box and pushed the widest icon off centre */
  padding: 0;
  /* the reference artwork fills the circles with the page blue itself, so
     they read a shade DARKER than the lighter capsule around them */
  background: var(--blue);
  /* NO shadow on the circles, in any state. Not softened — absent. The
     capsule carries all of the component's weight; anything here reads as a
     halo ringing every button. The :hover/:focus/:active/.mag-on rules below
     never reintroduce one. */
  box-shadow: none;
  cursor: pointer;
  /* the tint below fades in; same 0.3s curve the bloom already used */
  transition: background 0.3s var(--ease);
}
.nav-pod .pod-btn:hover,
.nav-pod .pod-btn:focus,
.nav-pod .pod-btn:focus-visible,
.nav-pod .pod-btn:active { box-shadow: none; outline: none; }
/* an inline <svg> sits on the text baseline, which pushed every icon 2px
   above the centre of its button; block removes the line box entirely */
.nav-pod .pod-btn svg { display: block; }
/* the dots keep the source artwork framing from ux_redesign/down_buttons.svg
   (its circle bounding box) and are scaled to the circle, so their size and
   position match the reference exactly with no hand re-cropping */
.nav-pod [data-slot="works"] svg { width: var(--pod-btn); height: var(--pod-btn); }
.nav-pod .pod-btn .ico { color: #fff; }
/* The icon stays WHITE on hover. .icon-btn.mag-on .ico turns every other icon
   button on the site accent; inside the pod that is overridden here, so the
   only thing hover changes is the circle behind it. */
.nav-pod .pod-btn.mag-on .ico { color: #fff; }

/* ---- hover inside the panel ----
   Everywhere else on the site .icon-btn blooms a white disc under the cursor.
   Inside the pod that disc is wrong: the circles ARE the component, so a shape
   appearing over them reads as a different control. The bloom element is not
   deleted (it is the existing hover machinery, with the existing timing) — it
   is turned into a very soft DARK tint, so the circle stays exactly where it is
   and simply settles a shade deeper — enough to read as hover, not enough to
   contrast. The white icon sits above it and stays fully legible.

   transform:none is the other half: the bloom normally rides the magnetic
   offset, which is invisible while it is a full-bleed disc but would drag a
   visible blob off-centre. */
.nav-pod .pod-btn .bloom {
  background: rgba(32,36,43,0.10);
  transform: none;
}
.nav-pod .pod-btn.mag-on .bloom { transform: none; opacity: 1; }
/* keyboard users get the same darkening the pointer produces */
.nav-pod .pod-btn:focus-visible .bloom { opacity: 1; }
/* the third slot is ONE button: at the end of the page its chevron flips
   down -> up, on the same 0.45s curve the old footer chevron used */
.nav-pod [data-slot="scroll"] svg { transition: transform 0.45s var(--ease); }
/* the chevron follows data-dir, which JS writes from the live scroll direction
   (and pins to "up" at the bottom of the page) — so it can flip mid-scroll
   rather than only at the very end */
.nav-pod[data-dir="up"] [data-slot="scroll"] svg { transform: rotate(180deg); }

.cat-grid {
  position: relative;
  width: 100%; height: 100%;
  column-gap: clamp(14px, 1.6vw, 30px);
  row-gap: clamp(60px, 9vh, 130px);
}
.cat-item {
  position: absolute;
  left: var(--l, 0); top: var(--t, 0); width: var(--w, 20%);
  will-change: transform;
  margin-top: 0;
  display: block; cursor: none;
}

/* reveal: image un-clips from the bottom, photo settles from a slight zoom */
.cat-item .media {
  display: block; position: relative; width: 100%; aspect-ratio: var(--ar, 3/4);
  overflow: hidden; background: rgba(20, 24, 40, 0.05);
}
.cat-item .media img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}

/* hover — slow, quiet zoom (reference style: no scrims, no overlays) */
html.ready .cat-item.in .media img { transition: transform 1s var(--ease); }
.cat-item:hover .media img { transform: scale(1.05); }

/* ---- HOVER-TAG (All Works) ----
   Single overlay node (sibling of .cat-grid) that follows the cursor while
   it sits over a catalogue image. Two stacked lines: name (bold display) +
   meta (YEAR · STILL/MOTION). Foreground is driven by --tag-fg, which is
   mirrored onto .tag-hover from data-bright on the hovered .cat-item
   (1 = light photo -> ink text, 0 = dark photo -> white text). Avoiding
   per-frame getImageData — the catalog reuses a per-card attribute that
   the human owner set at markup time. */
.tag-hover {
  position: fixed; left: 0; top: 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 0; margin: 0;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(13px, 0.95vw, 17px);
  line-height: 1.05;
  letter-spacing: 0.01em;
  color: var(--tag-fg, var(--ink));
  text-align: left;
  white-space: nowrap;
  pointer-events: none;
  z-index: 9996;            /* under .cursor (9999) and .cursor-ring (9998), above all site content */
  opacity: 0;
  visibility: hidden;
  will-change: transform, opacity;
  transform: translate3d(-9999px, -9999px, 0);  /* off-screen until first show */
  transition: opacity 0.18s var(--ease);
}
.tag-hover.is-on { opacity: 1; visibility: visible; }
.tag-hover .tag-l1 { font-weight: 700; }
.tag-hover .tag-l2 {
  font-family: var(--font-meta);
  font-weight: 500;
  font-size: var(--meta-size);
  letter-spacing: var(--meta-track);
  text-transform: uppercase;
  opacity: 0.85;
}

/* per-card foreground switch (mirrored onto .tag-hover by initHoverTag()
   in main.js, because the tag is a sibling of .cat-grid and CSS custom
   properties do not cross sibling boundaries). "bright" describes the
   photo, not the text. dark photo -> white text, light photo -> ink. */
.tag-hover[data-bright="0"] { --tag-fg: #fff; }          /* dark photo  -> white */
.tag-hover[data-bright="1"] { --tag-fg: var(--ink); }    /* light photo -> ink   */

/* touch / coarse pointer devices: no hover, no tag */
@media (hover: none) {
  .tag-hover { display: none; }
}

/* ============================================================
   ABOUT
   ============================================================ */
.about {
  min-height: 100vh; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; padding: 13vh var(--gutter) clamp(130px, 18vh, 210px);
}
.about .portrait { width: clamp(300px, 30vw, 440px); aspect-ratio: 16 / 10; margin-bottom: 40px; }
.about h1 { font-size: clamp(2.06rem, 3.93vw, 3.51rem); margin: 0 0 24px; max-width: 20ch; line-height: 1.02; letter-spacing: -0.01em; }
.about .bio { max-width: 56ch; }
.about .signoff { margin-top: 40px; color: var(--accent); }

/* ============================================================
   CONTACTS
   ============================================================ */
.contacts {
  min-height: 100vh; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; padding: var(--gutter);
}
.contacts h1 {
  font-size: clamp(1.78rem, 3.18vw, 2.57rem); letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 500; font-family: var(--font-heading); margin: 0 0 16px; color: #fff;
}
/* the sub is body copy, so it now carries .body-text and inherits the shared
   body role wholesale — family, weight 300, size clamp, line-height and the
   0.82 opacity every other page's copy uses. Only what is genuinely
   page-specific stays here: the white ink and the gap above the field. */
.contacts .sub {
  color: #fff; margin-bottom: clamp(40px, 7vh, 72px);
}
.mail-field {
  position: relative; width: min(620px, 80vw);
  display: flex; align-items: center; gap: 16px;
  border-bottom: 1px solid rgba(255,255,255,0.4); padding: 16px 2px;
}
.mail-field::before { content: ""; width: 1px; height: 22px; background: var(--ink); }
.mail-field input {
  flex: 1; border: 0; background: none; outline: none;
  font-family: var(--font-ui); font-weight: 400; text-transform: uppercase; letter-spacing: 0.06em;
  font-size: clamp(0.875rem, 1.05vw, 1rem); color: var(--ink); cursor: none;
}
.mail-field input::placeholder { color: var(--ink); opacity: 0.85; }
.mail-underline { position: absolute; left: 0; bottom: -1px; height: 2px; background: #fff; width: 0; transition: width 0.5s var(--ease); }
.mail-field.filled .mail-underline, .mail-field:focus-within .mail-underline { width: 55%; }

.dots-indicator { position: fixed; left: 50%; bottom: 8vh; transform: translateX(-50%); display: flex; gap: 18px; z-index: 200; }
.dots-indicator .d { width: 22px; height: 22px; border: 1px solid rgba(255,255,255,0.55); border-radius: 50%; transition: all 0.4s var(--ease); }
.dots-indicator .d.on { background: #fff; border-color: #fff; }

/* ============================================================
   PROJECT PAGE
   ============================================================ */
.project { position: relative; }

/* right-side section indicator — vertical line only, sitting on the SAME
   vertical axis as the header menu button above it (button centre =
   gutter + half its 44px width) */
.side-rail {
  position: fixed; right: calc(var(--gutter) + 22px); top: 50%; transform: translate(50%, -50%);
  z-index: 150; display: flex; flex-direction: column; align-items: center;
  gap: clamp(14px, 1.8vh, 22px); pointer-events: none;
}
/* the rail holds the inline .ind-v SVG; it is styled once, up top, and is the
   same element on All-works and on Project. .side-rail is translate(50%)
   against a `right` anchor, which centres its box on that anchor whatever the
   box is wide, so the run stays on the header menu button's axis. */
/* the All-works rail only shows while that view is active */
#worksRail { display: none; }
body.on-works #worksRail[data-scrollable="yes"] { display: flex; }

/* An indicator is an answer to "is there anything to scroll", not a per-page
   decoration. syncScrollIndicators() writes data-scrollable from the measured
   content-vs-viewport size, so a short page simply never grows one and a page
   that stops being scrollable (resize, orientation, content shrinking) drops
   it again. No page names, no manual flags — including the rule above, which
   only adds "and this view is the one on screen". */
.side-rail[data-scrollable="no"] { display: none; }
.ind[data-scrollable="no"] { display: none; }

/* HERO ----------------------------------------------------- */
.project-hero { padding: 16vh var(--gutter) 0; display: flex; flex-direction: column; align-items: center; }
.project-hero .media-h { width: 46vw; max-width: 620px; }

.video-frame { position: relative; cursor: none; }
.hero-slides { position: absolute; inset: 0; }
.hero-slides .hs {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity 0.75s var(--ease);
}
.hero-slides .hs.is-on { opacity: 1; }
.hero-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 70px; height: 70px; display: grid; place-items: center;
  font-family: var(--font-ui); font-weight: 300; font-size: 32px; line-height: 1;
  color: #fff; opacity: 0; pointer-events: none; z-index: 3;
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.4); transition: opacity 0.4s var(--ease);
}
.hero-nav.prev { left: 14px; } .hero-nav.next { right: 14px; }
.video-frame.hovering .hero-nav { opacity: 0.45; }
.video-frame.hovering .hero-nav.active { opacity: 0.92; }
.hero-nav.connect { opacity: 0; }

/* static circular arrow button (bottom-right of hero, per mockup) */
.hero-cta {
  position: absolute; right: 20px; bottom: 20px; z-index: 4;
  width: 54px; height: 54px; border-radius: 50%; display: grid; place-items: center;
  border: 1px solid rgba(255,255,255,0.7); color: #fff; pointer-events: none;
  transition: background 0.4s var(--ease), transform 0.5s var(--ease);
}
.hero-cta svg { width: 22px; height: 22px; }
.video-frame:hover .hero-cta { background: rgba(255,255,255,0.14); transform: scale(1.06); }

/* count indicator like home — white track, red fill, NO % */
.slider-progress { margin: 24px auto 0; display: flex; align-items: center; gap: 16px;
  font-family: var(--font-ui); font-weight: 400; text-transform: uppercase;
  font-size: var(--meta-size); letter-spacing: var(--meta-track); }
.slider-progress .ends { color: var(--ink-50); }
.slider-progress .bar { width: clamp(180px, 24vw, 340px); height: 1px; background: var(--line); position: relative; }
.slider-progress .bar i { position: absolute; left: 0; top: 0; height: 1px; background: var(--accent); width: 25%; transition: width 0.6s var(--ease); }

.project-title { text-align: center; margin: 40px 0 0; }
.project-title .pt {
  font-family: var(--font-heading); font-weight: 500; text-transform: uppercase; letter-spacing: 0.1em;
  font-size: clamp(0.98rem, 1.64vw, 1.31rem); line-height: 1.45; color: var(--ink);
}
.project-desc { text-align: center; max-width: 50ch; margin: 24px auto 0; }

/* SECTION: text-above / images / text-below (per mockup) ---- */
.proj-section { max-width: 1200px; margin: clamp(120px, 18vh, 220px) auto; padding: 0 calc(var(--gutter) + 2vw); position: relative; }
/* the three-frame section sits centred in the viewport, copy beneath */
.sec-tri { min-height: 80vh; display: flex; flex-direction: column; justify-content: center; }
/* copy sits BELOW the images, centred like the hero description */
.ps-foot   { max-width: 50ch; margin: 8vh auto 0; text-align: center; }

/* SEC 1 copy — the lower lines fade into the canvas; they are text that
   doesn't fit yet and is revealed on scroll (JS scrubs --reveal 0 -> 1). */
.ps-foot.ps-reveal {
  --reveal: 1; /* full text if JS / scroll is unavailable */
  -webkit-mask-image: linear-gradient(180deg, #000 calc(10% + var(--reveal) * 98%), transparent calc(42% + var(--reveal) * 80%));
          mask-image: linear-gradient(180deg, #000 calc(10% + var(--reveal) * 98%), transparent calc(42% + var(--reveal) * 80%));
}

/* SEC 1 — two portraits (hand · veil statue) + light-blue panel behind */
.cluster { position: relative; min-height: clamp(420px, 58vh, 640px); }
.cluster .media { position: absolute; cursor: none; }
.cl-1 { left: 23%; top: 0;    width: 21vw; max-width: 290px; aspect-ratio: 3/4; z-index: 2; }
.cl-2 { left: 47%; top: 9vh;  width: 15vw; max-width: 215px; aspect-ratio: 3/4; z-index: 3; }
.cl-panel { left: 56%; top: 3vh; width: 21vw; max-width: 300px; aspect-ratio: 4/5; background: var(--blue-2); position: absolute; z-index: 1; }

/* SEC 2 — three landscape frames in a descending staircase */
.tri { position: relative; min-height: clamp(320px, 34vh, 470px); }
.tri .media { position: absolute; cursor: none; }
.tri-1 { left: 18%; top: 0;    width: 21vw; max-width: 300px; aspect-ratio: 3/2; z-index: 1; }
.tri-2 { left: 44%; top: 3vh;  width: 21vw; max-width: 300px; aspect-ratio: 16/9; z-index: 2; }
.tri-3 { left: 60%; top: 14vh; width: 24vw; max-width: 340px; aspect-ratio: 16/9; z-index: 3; }

.cluster .media img, .tri .media img { transition: transform 0.7s var(--ease); }
.cluster .media:hover img, .tri .media:hover img { transform: scale(1.05); }

/* NEXT PROJECT footer block ------------------------------- */
.next-project {
  position: relative; min-height: 70vh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center; overflow: hidden; margin-top: 14vh;
}
.next-project .np-bg { position: absolute; inset: 0; z-index: 0; }
.next-project .np-bg img { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.7); }
.next-project .np-bg::after { content: ""; position: absolute; inset: 0; background: rgba(10,10,15,0.35); }
.next-project .np-inner { position: relative; z-index: 1; color: #fff; }
.next-project .np-eyebrow { font-family: var(--font-heading); font-size: clamp(1.31rem, 2.81vw, 2.24rem); opacity: 0.85; }
.next-project .np-name {
  font-family: var(--font-heading); font-weight: 500; letter-spacing: -0.02em;
  font-size: clamp(2.43rem, 7.01vw, 6.08rem); margin-top: 8px; line-height: 1.0;
}
.next-project .np-name .dash { color: var(--accent); }

/* footer flips to light ink over the dark next-project band */
.site-footer.on-dark .footer-link { color: #fff; }
.site-footer.on-dark::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 150px;
  background: linear-gradient(to top, rgba(8,8,14,0.55), transparent); z-index: -1; pointer-events: none;
}

/* ============================================================
   FULLSCREEN LIGHTBOX (project image gallery)
   ============================================================ */
.lightbox {
  position: fixed; inset: 0; z-index: 600; background: rgba(12,13,16,0.97);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; visibility: hidden; transition: opacity 0.45s var(--ease), visibility 0.45s;
}
/* closed: skip rendering the slides entirely, so their lazy images are not
   fetched on initial load (ISSUE-031). Not applied to .open, so the fade in
   and the slide transitions are untouched. */
.lightbox:not(.open) { content-visibility: hidden; }
.lightbox.open { opacity: 1; visibility: visible; }
.lb-stage { position: relative; width: min(78vw, 1100px); height: min(82vh, 760px); }
.lb-slide {
  position: absolute; inset: 0; opacity: 0; transform: scale(1.04);
  transition: opacity 0.6s var(--ease), transform 0.8s var(--ease);
}
.lb-slide.on { opacity: 1; transform: scale(1); }
.lb-slide img { width: 100%; height: 100%; object-fit: contain; }
.lb-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 64px; height: 64px; border-radius: 50%; display: grid; place-items: center;
  color: #fff; font-weight: 300; font-size: 28px; cursor: none; z-index: 2;
  background: rgba(255,255,255,0.08); transition: background 0.3s var(--ease);
}
.lb-nav:hover { background: rgba(255,255,255,0.18); color: var(--accent); }
.lb-prev { left: -84px; } .lb-next { right: -84px; }
.lb-count { position: absolute; bottom: -46px; left: 0; right: 0; display: flex; align-items: center; justify-content: center;
  gap: 14px; color: #fff; font-size: var(--meta-size); letter-spacing: var(--meta-track); text-transform: uppercase; }
.lb-count .bar { width: 220px; height: 1px; background: rgba(255,255,255,0.3); position: relative; }
.lb-count .bar i { position: absolute; left: 0; top: 0; height: 1px; background: var(--accent); }
.lb-close { position: fixed; top: var(--pad-y); right: var(--gutter); z-index: 610; }
.lb-close .ico { color: #fff; }

/* ============================================================
   MENU OVERLAY
   ============================================================ */
.menu-overlay {
  position: fixed; inset: 0; z-index: 300; background: var(--blue);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(12px, 2.2vh, 24px);
  opacity: 0; visibility: hidden; transition: opacity 0.5s var(--ease), visibility 0.5s;
}
.menu-overlay.open { opacity: 1; visibility: visible; }
.menu-overlay a {
  font-family: var(--font-menu); font-weight: 500; letter-spacing: -0.02em;
  font-size: clamp(2.6rem, 8vw, 6.5rem); line-height: 1.0; position: relative;
}
.menu-overlay a .num { position: absolute; top: 6px; left: -34px; font-family: var(--font-menu); font-size: 0.75rem; letter-spacing: 0.1em; color: var(--accent); }
.menu-overlay a:hover { color: var(--accent); }
.menu-close { position: fixed; top: var(--pad-y); right: var(--gutter); z-index: 310; }
/* white-circle bloom + red icon on hover (same as header buttons, but
   guaranteed on hover \u2014 not only on magnetic proximity) */
.menu-close:hover .bloom { transform: translate(var(--mx, 0px), var(--my, 0px)) scale(1); opacity: 1; }
.menu-close:hover .ico { color: var(--accent); }

/* reveal helper */
.reveal { will-change: opacity, transform; }
html.ready .reveal { opacity: 0; transform: translateY(26px); transition: opacity 0.9s var(--ease), transform 0.9s var(--ease); }
html.ready .reveal.in { opacity: 1; transform: none; }
/* Parallax elements: GSAP owns their transform every frame — a transform
   transition here makes the browser "chase" each value = micro-jitter and
   masks the speed differences. Only fade them in; leave transform to GSAP. */
html.ready .reveal[data-scroll-speed] { transition: opacity 0.9s var(--ease); transform: none; }

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width: 1024px) {
  .project-hero .media-h { width: 70vw; }
  .cl-1 { left: 6%; width: 30vw; } .cl-2 { left: 46%; width: 22vw; } .cl-panel { left: 56%; width: 30vw; }
  .tri-1 { width: 34vw; } .tri-2 { left: 38%; width: 32vw; } .tri-3 { left: 58%; width: 34vw; }
  .ps-foot { margin-left: auto; margin-right: auto; }
  .lb-prev { left: 8px; } .lb-next { right: 8px; }
  .g-row { top: 22%; }

}

@media (max-width: 760px) {
  :root { --meta-size: 0.6875rem; --gutter: 25px; }
  /* one compact band on small screens */
  .g-row { gap: clamp(40px, 9vw, 80px); top: 14%; }
  .gi-h { width: 56vw; } .gi-v { width: 44vw; } .gi-c { width: 56vw; } .gi-sq { width: 52vw; } .gi-w { width: 64vw; }
  .gp-1 { transform: translateY(6vh) translateX(var(--ix,0px)); } .gp-2 { transform: translateY(22vh) translateX(var(--ix,0px)); }
  .gp-3 { transform: translateY(14vh) translateX(var(--ix,0px)); } .gp-4 { transform: translateY(-2vh) translateX(var(--ix,0px)); } .gp-5 { transform: translateY(12vh) translateX(var(--ix,0px)); }



  /* project clusters collapse to a readable stack */
  .proj-section { margin: 12vh auto; }
  .ps-foot { max-width: none; margin: 5vh 0 0; }
  .ps-foot.ps-reveal { -webkit-mask-image: none; mask-image: none; }
  .cluster, .tri { min-height: 0; display: flex; flex-direction: column; gap: 5vh; align-items: center; }
  .cluster .media, .tri .media { position: relative; left: auto; top: auto; width: 80vw; max-width: none; margin: 0; }
  .cl-panel { display: none; }

  .about .portrait { width: 76vw; }
  .about h1 { font-size: clamp(1.59rem, 7.01vw, 2.24rem); }

  .lb-stage { width: 90vw; height: 70vh; }
  .lb-prev { left: 4px; } .lb-next { right: 4px; }
  .lb-nav { width: 48px; height: 48px; font-size: 22px; }

}

@media (max-width: 480px) {
  .gi-h { width: 72vw; } .gi-v { width: 56vw; } .gi-c { width: 72vw; } .gi-sq { width: 64vw; } .gi-w { width: 80vw; }
  .footer-link.meta { font-size: 0.625rem; }
}

@media (max-height: 780px) {
  .about { justify-content: flex-start; padding-top: 11vh; }
  .about .portrait { margin-bottom: 26px; width: clamp(280px, 26vw, 380px); }
  .about h1 { margin-bottom: 18px; }
}

@media (hover: none) {
  .g-caption { opacity: 1 !important; transform: none !important; }
  body { cursor: auto; }
  button, a, input { cursor: pointer; }
  input { cursor: text; }
}
