/* ==========================================================================
   CodeVani — custom.css

   Deliberately small. Tailwind handles layout, spacing, type and colour.
   This file only carries what utilities genuinely cannot express:

     1. Theme tokens (the RGB channels Tailwind's semantic colours resolve to)
     2. Global element defaults + Lenis hooks
     3. Split-text mask geometry for GSAP reveals
     4. The custom cursor
     5. Film grain + media treatment
     6. A few stateful components (menu, accordion, marquee edges)
     7. Reduced-motion and coarse-pointer fallbacks
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Theme tokens
   Space-separated RGB channels so Tailwind can apply its own alpha values.
   -------------------------------------------------------------------------- */

:root {
  --gutter: clamp(1.25rem, 1rem + 3.2vw, 5rem);

  /* Dark is the default presentation. */
  --c-bg: 5 5 5;
  --c-bg-alt: 11 11 11;
  --c-surface: 19 19 19;
  --c-surface-2: 29 29 29;
  --c-line: 44 44 44;
  --c-line-strong: 78 78 78;
  --c-text: 247 245 242;
  --c-text-soft: 201 198 193;
  --c-text-muted: 124 124 124;
  --c-inverse: 247 245 242;
  --c-inverse-text: 5 5 5;

  --media-brightness: 1;
  color-scheme: dark;
}

[data-theme="light"] {
  --c-bg: 247 245 242;
  --c-bg-alt: 239 236 231;
  --c-surface: 226 223 218;
  --c-surface-2: 201 198 193;
  --c-line: 214 210 204;
  --c-line-strong: 160 156 150;
  --c-text: 5 5 5;
  --c-text-soft: 61 61 61;
  --c-text-muted: 88 88 88;
  --c-inverse: 5 5 5;
  --c-inverse-text: 247 245 242;

  --media-brightness: 0.94;
  color-scheme: light;
}

/* --------------------------------------------------------------------------
   2. Global defaults + Lenis
   -------------------------------------------------------------------------- */

html {
  -webkit-text-size-adjust: 100%;
  /* Lenis owns scrolling; native smooth scroll would fight it. */
  scroll-behavior: auto;
  /* Reserve the scrollbar track permanently. The preloader and the menu both
     lock scrolling with `overflow: hidden`, which would otherwise drop the
     scrollbar, widen the padding box, and shift every right-anchored element
     sideways the instant the lock is released. */
  scrollbar-gutter: stable;
}

body {
  font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

/* Required Lenis hooks. */
html.lenis,
html.lenis body {
  height: auto;
}

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis.lenis-stopped {
  overflow: hidden;
}

::selection {
  background-color: rgb(var(--c-inverse));
  color: rgb(var(--c-inverse-text));
}

/* Keyboard-only focus ring. */
:focus {
  outline: none;
}

:focus-visible {
  outline: 2px solid rgb(var(--c-text));
  outline-offset: 4px;
}

/* Balanced headings and pretty paragraph rag, applied globally so no utility
   has to repeat it. */
h1, h2, h3, h4, h5, h6 {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

/* The supplied logo is light letterforms baked onto a solid black plate, so it
   cannot simply sit on the page — the plate has to be knocked out.
   Dark theme: `screen` drops pure black to transparent, keeping the letters.
   Light theme: invert first (white plate, dark letters), then `multiply` drops
   the white. The result reads as clean artwork on either background without
   ever touching the source file. */
.logo-mark {
  mix-blend-mode: screen;
  transition: filter 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

[data-theme="light"] .logo-mark {
  filter: invert(1);
  mix-blend-mode: multiply;
}

/* Thin neutral scrollbar — the page still needs one on desktop. */
@supports selector(::-webkit-scrollbar) {
  ::-webkit-scrollbar { width: 9px; height: 9px; }
  ::-webkit-scrollbar-track { background: rgb(var(--c-bg)); }
  ::-webkit-scrollbar-thumb {
    background: rgb(var(--c-line-strong));
    border: 3px solid rgb(var(--c-bg));
    border-radius: 999px;
  }
  ::-webkit-scrollbar-thumb:hover { background: rgb(var(--c-text-muted)); }
}

/* --------------------------------------------------------------------------
   3. Split-text scaffolding
   Markup is generated by animation.js. These rules define the mask geometry so
   every reveal is a pure transform — no layout thrash, no reflow.
   -------------------------------------------------------------------------- */

.split .line {
  display: block;
  overflow: hidden;
  /* Breathing room for descenders inside the mask, cancelled by the margin so
     the visual rhythm of the type is untouched. */
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}

/* Same reasoning as .media__inner — GSAP handles promotion per tween, so these
   stay unpromoted at rest. A long page has dozens of lines. */
.split .line__inner,
.split .char {
  display: inline-block;
}

.split .line__inner {
  display: block;
}

.split .word {
  display: inline-block;
  white-space: pre;
}

/* Pre-animation states are scoped to .js so a no-JS visitor reads everything. */
.js .split[data-reveal="pending"] .line__inner {
  transform: translate3d(0, 105%, 0);
}

.js [data-fade="pending"] {
  opacity: 0;
}

.js [data-clip="pending"] {
  clip-path: inset(0 0 100% 0);
}

/* The hero rule draws itself in. The hero timeline is deliberately deferred
   until the preloader curtain lifts, so without a starting state here the rule
   would paint at full width, get revealed, then snap back to zero. */
.js [data-hero-rule] {
  transform: scaleX(0);
  transform-origin: left center;
}

/* Words that dim then light up as the manifesto scrolls past. */
.js .word-dim {
  color: rgb(var(--c-text) / 0.16);
}

/* --------------------------------------------------------------------------
   4. Custom cursor
   Two independent lerped layers: a small dot that tracks fast and a ring that
   trails. cursor.js writes only the translate on .cursor__dot / .cursor__ring;
   every size, colour and label change below is pure CSS state, so the two never
   fight over the transform property.
   -------------------------------------------------------------------------- */

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9000;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}

.cursor.is-active {
  opacity: 1;
}

/* The dot and ring are positioned at the origin and moved wholly by the
   translate that cursor.js writes each frame. Size is animated from the
   centre, so width/height changes read as a scale without any reflow jitter. */
.cursor__dot,
.cursor__ring {
  position: absolute;
  top: 0;
  left: 0;
  margin-top: -0.5px;
  margin-left: -0.5px;
  border-radius: 999px;
  will-change: transform;
}

.cursor__dot {
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  background-color: rgb(var(--c-text));
  transition: opacity 0.3s ease, background-color 0.6s ease;
}

.cursor__ring {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  margin: -19px 0 0 -19px;
  border: 1px solid rgb(var(--c-text) / 0.55);
  /* Position is written to the independent `translate` property by cursor.js,
     which leaves `transform` free for the press-state scale below. */
  transform: scale(1);
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
    width 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    margin 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    background-color 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    border-color 0.45s ease, opacity 0.3s ease;
}

/* The label rides inside the ring, so it needs no positioning of its own. */
.cursor__label {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgb(var(--c-inverse-text));
  opacity: 0;
  transform: scale(0.72);
  transition: opacity 0.3s ease, transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Hovering a link or button: the ring contracts onto the dot and fills. */
.cursor[data-state="link"] .cursor__ring {
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  background-color: rgb(var(--c-text));
  border-color: rgb(var(--c-text));
}

.cursor[data-state="link"] .cursor__dot {
  opacity: 0;
}

/* Hovering media: the ring opens up and carries a label. */
.cursor[data-state="media"] .cursor__ring {
  width: 88px;
  height: 88px;
  margin: -44px 0 0 -44px;
  background-color: rgb(var(--c-text));
  border-color: transparent;
}

.cursor[data-state="media"] .cursor__dot {
  opacity: 0;
}

.cursor[data-state="media"] .cursor__label {
  opacity: 1;
  transform: scale(1);
}

.cursor[data-state="hidden"] {
  opacity: 0;
}

/* Press feedback — the ring dips inward for the duration of the click. */
.cursor[data-pressed="true"] .cursor__ring {
  transform: scale(0.82);
}

/* Hide the native cursor only where the custom one is actually running. */
html.has-custom-cursor,
html.has-custom-cursor * {
  cursor: none !important;
}

/* --------------------------------------------------------------------------
   5. Media treatment
   One consistent neutral grade, plus fine grain to tie all imagery to a single
   photographic stock.
   -------------------------------------------------------------------------- */

.media {
  position: relative;
  overflow: hidden;
  background-color: rgb(var(--c-surface));
}

.media img,
.media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.05) brightness(var(--media-brightness));
}

/* Film grain. Deliberately NOT mix-blend-mode: overlay — its backdrop is the
   transforming .media__inner sibling, so blending would force the browser to
   re-read and re-composite the backdrop every frame on every image on the
   page, which defeats compositor-only animation and is the single most
   expensive thing this design could do. Plain alpha reads near-identically at
   this opacity and costs nothing per frame. */
.media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.055;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Inner wrapper that GSAP scales for the parallax/zoom effect. No will-change:
   gsap.config({ force3D: true }) promotes this to its own layer for the
   duration of the tween and releases it afterwards, which is what we want.
   Declaring it here instead would pin ~25 layers in GPU memory permanently. */
.media__inner {
  width: 100%;
  height: 100%;
}

/* --------------------------------------------------------------------------
   6. Stateful components
   -------------------------------------------------------------------------- */

/* Animated underline for text links — draws in from the left. */
.link-line {
  position: relative;
  display: inline-block;
}

.link-line::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -0.14em;
  width: 100%;
  height: 1px;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.link-line:hover::after,
.link-line:focus-visible::after,
.link-line[aria-current="true"]::after {
  transform: scaleX(1);
}

/* Nav links get a reversed variant: underline present, retracts on hover. */
.link-line--on::after {
  transform: scaleX(1);
}

.link-line--on:hover::after {
  transform: scaleX(0);
  transform-origin: right;
}

/* Solid button whose fill wipes upward on hover. Two stacked labels keep the
   text crisp instead of blending through the fill. */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  overflow: hidden;
  isolation: isolate;
}

.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: rgb(var(--c-text));
  transform: translate3d(0, 101%, 0);
  transition: transform 0.55s cubic-bezier(0.16, 1, 0.3, 1);
}

.btn:hover::before,
.btn:focus-visible::before {
  transform: translate3d(0, 0, 0);
}

.btn:hover,
.btn:focus-visible {
  color: rgb(var(--c-bg));
}

/* Inverse button sits on a light surface. */
.btn--inverse::before {
  background-color: rgb(var(--c-bg));
}

.btn--inverse:hover,
.btn--inverse:focus-visible {
  color: rgb(var(--c-text));
}

/* Fullscreen mobile menu. */
.menu-panel {
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.8s cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
}

.menu-panel[data-open="true"] {
  clip-path: inset(0 0 0 0);
  pointer-events: auto;
}

/* Menu toggle: two bars that cross into an X. */
.menu-toggle__bar {
  display: block;
  width: 22px;
  height: 1px;
  background-color: currentColor;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1),
    opacity 0.3s ease;
}

.menu-toggle__bar + .menu-toggle__bar {
  margin-top: 6px;
}

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

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:last-child {
  transform: translateY(-3.5px) rotate(-45deg);
}

/* Accordion — height is animated from a measured value set by main.js so the
   transition stays on a single property. */
.accordion__panel {
  height: 0;
  overflow: hidden;
  transition: height 0.6s cubic-bezier(0.16, 1, 0.3, 1),
    opacity 0.4s ease;
  opacity: 0;
}

.accordion__item[data-open="true"] .accordion__panel {
  opacity: 1;
}

.accordion__icon::before,
.accordion__icon::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background-color: currentColor;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.accordion__icon::after {
  transform: rotate(90deg);
}

.accordion__item[data-open="true"] .accordion__icon::after {
  transform: rotate(0deg);
}

/* Marquee track — duplicated content, translated by exactly -50%. */
.marquee {
  display: flex;
  overflow: hidden;
  user-select: none;
}

.marquee__track {
  display: flex;
  flex: none;
  align-items: center;
  min-width: 100%;
  will-change: transform;
}

.marquee--reverse .marquee__track {
  animation-direction: reverse;
}

.marquee:hover .marquee__track {
  animation-play-state: paused;
}

/* Soft edge fade so marquees dissolve rather than cut at the viewport. */
.marquee--faded {
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

/* Horizontal rail dragged by pointer (testimonials). */
.rail {
  cursor: grab;
}

.rail.is-dragging {
  cursor: grabbing;
}

.rail__track {
  will-change: transform;
}

/* Work-card hover: the whole composition lifts a touch and the image drifts. */
.work-card:hover .media__inner {
  transform: scale(1.045);
}

.work-card .media__inner {
  transition: transform 1.1s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Service row hover: the row's index and title shift right in sequence. */
.service-row__shift {
  transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1),
    color 0.4s ease;
}

.service-row:hover .service-row__shift {
  transform: translateX(clamp(0.5rem, 1.5vw, 1.75rem));
}

/* The floating image that follows the pointer across the services list. */
.service-preview {
  position: absolute;
  top: 0;
  left: 0;
  width: clamp(180px, 20vw, 300px);
  opacity: 0;
  pointer-events: none;
  will-change: transform;
  transition: opacity 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

.service-preview.is-visible {
  opacity: 1;
}

/* Scroll lock, used by the preloader and the fullscreen menu. Lenis is stopped
   at the same time; this only stops the native fallback. */
body[data-locked="true"] {
  overflow: hidden;
}

/* Preloader curtain. The explicit starting clip-path gives GSAP a value to
   animate from on the very first frame. */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 8000;
  background-color: rgb(var(--c-bg));
  clip-path: inset(0 0 0% 0);
}

.preloader[data-done="true"] {
  pointer-events: none;
}

/* Scroll cue: a hairline whose highlight travels downward on a loop. */
.scroll-cue {
  position: relative;
  width: 1px;
  height: 72px;
  overflow: hidden;
  background-color: rgb(var(--c-line-strong));
}

.scroll-cue::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: rgb(var(--c-text));
  animation: scroll-hint 2.4s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

@keyframes scroll-hint {
  0% { transform: translateY(-100%); }
  100% { transform: translateY(100%); }
}

/* Vertically-set label used along section edges. */
.vertical-text {
  writing-mode: vertical-rl;
  text-orientation: mixed;
}

/* --------------------------------------------------------------------------
   7. Fallbacks
   Motion is a layer, not the content — removing it must lose nothing.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .js .split[data-reveal="pending"] .line__inner,
  .js [data-clip="pending"],
  .js [data-fade="pending"],
  .js [data-hero-rule] {
    transform: none;
    clip-path: none;
    opacity: 1;
  }

  .js .word-dim {
    color: inherit;
  }

  .cursor {
    display: none !important;
  }

  .marquee__track {
    animation: none !important;
  }

  .scroll-cue::after {
    animation: none !important;
  }
}

/* No custom cursor on touch — and never hide the native one there. */
@media (hover: none), (pointer: coarse) {
  .cursor {
    display: none !important;
  }

  html.has-custom-cursor,
  html.has-custom-cursor * {
    cursor: auto !important;
  }
}
