/* ---------- Tokens ---------- */
:root{
  /* One background tone for the whole page — no second shade anywhere. */
  --bg: #050608;
  --text: #edeef2;
  --muted: #7d8698;
  --faint: #4a5162;
  --accent: #2f6fed;
  --accent-soft: rgba(47,111,237,.30);
  --border: rgba(255,255,255,.09);
  --border-strong: rgba(255,255,255,.20);
  /* Solid twin of --border over --bg. Used where two hairlines meet in the
     same pixel: alpha would compound there and read brighter than one line. */
  --hairline: #1c1c1e;

  --font-display: 'Vazirmatn', sans-serif;
  --font-mono: 'IBM Plex Mono', monospace;

  /* Slow expo-out curve — luxury motion settles, it does not bounce. */
  --ease: cubic-bezier(.16, 1, .3, 1);
}

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

/* The page must only ever scroll vertically. Two things are needed for that,
   and the sideways drag on phones came from having neither:
   - the rule has to sit on <html> as well. On <body> alone the root element is
     still free to scroll, so a decorative bleed could pan the whole page.
   - `clip` rather than `hidden`. `hidden` makes the element a scroll *container*
     — it merely hides the scrollbar, and touch gestures can still pan it. `clip`
     creates no scroll container at all, so the drag is impossible.
   `hidden` is kept first as a fallback for engines without `clip`. */
html, body{
  overflow-x: hidden;
  overflow-x: clip;
  overscroll-behavior-x: none;   /* kills the horizontal rubber-band on iOS */
  /* The decisive fix for mobile Safari: `overflow-x: hidden`/`clip` stop the
     page from *having* horizontal scroll room, but WebKit has a long-standing
     bug where a finger-drag can still pan the page sideways regardless.
     touch-action tells the browser which gesture directions to honor at all —
     pan-y allows vertical drag and pinch-zoom, and simply drops any sideways
     component of a drag instead of turning it into a scroll. */
  touch-action: pan-y;
}
body{
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-display);
  font-weight: 300;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
h1, h2, p, ul{ margin: 0; padding: 0; }
ul{ list-style: none; }
a{ color: inherit; text-decoration: none; }
img{ display: block; max-width: 100%; }
button{ font: inherit; color: inherit; background: none; border: none; cursor: pointer; }

:focus-visible{
  outline: 1px solid var(--accent);
  outline-offset: 4px;
}

/* A hairline divider used inside labels — the small editorial detail
   that carries most of the "expensive" feeling. */
.rule{
  display: inline-block;
  width: 2rem;
  height: 1px;
  background: var(--border-strong);
  vertical-align: middle;
}

/* NOTE: Persian script is cursive — its letters join. Applying letter-spacing
   to Farsi text visually breaks those joins. Tracking is therefore only ever
   applied to Latin/mono runs, never to Farsi. */
.eyebrow{
  display: flex;
  align-items: center;
  gap: .75rem;
  font-size: .8rem;
  font-weight: 400;
  color: var(--muted);
  margin-bottom: 2rem;
}
.eyebrow__num{
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}

.accent{ color: var(--accent); font-weight: 400; }

.section-heading{
  font-size: clamp(1.75rem, 4vw, 2.75rem);
  font-weight: 300;
  line-height: 1.3;
}
.section-sub{
  margin-top: .9rem;
  color: var(--muted);
  font-size: 1rem;
}

/* ---------- Preloader ----------
   Covers the font swap, which is very visible in Persian because the fallback
   face looks nothing like Vazirmatn. It waits on the fonts and nothing else,
   with a hard cap in JS — no fake progress. */
.preload{
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--bg);
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 2.25rem;
  transform: translateY(0);
  transition: transform .9s var(--ease) .2s;
  /* Failsafe: if the CDN scripts fail, main.js never runs and nothing would
     remove this. Auto-clear it so a script error can never trap the page. */
  animation: preload-failsafe 0s linear 2.5s forwards;
}
.preload__mark{
  width: clamp(4.5rem, 14vw, 7rem);
  height: auto;
  opacity: 0;
  /* Kept shorter than MIN_SHOW in main.js so the mark is fully in before it lifts. */
  animation: preload-mark-in .55s var(--ease) forwards;
}
.preload__rule{
  width: 5rem;
  height: 1px;
  background: var(--border);
  position: relative;
  overflow: hidden;
}
/* Swept with transform rather than inset: transform stays on the compositor,
   an inset animation repaints every frame. */
.preload__rule::after{
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 100%;
  background: var(--border-strong);
  transform: translateX(-100%);
  animation: preload-sweep 1.3s var(--ease) infinite;
}
.preload.is-done{ transform: translateY(-101%); }
.preload.is-done .preload__mark,
.preload.is-done .preload__rule{
  opacity: 0;
  transition: opacity .3s var(--ease);
}

@keyframes preload-mark-in{ to{ opacity: 1; } }
@keyframes preload-sweep{ to{ transform: translateX(100%); } }
@keyframes preload-failsafe{ to{ transform: translateY(-101%); } }

/* ---------- Grain overlay ---------- */
.grain{
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  opacity: .045;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------- Nav ---------- */
.nav{
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.6rem clamp(1.5rem, 7vw, 7rem);
  transition: transform .7s var(--ease);
}
.nav--hidden{ transform: translateY(-130%); }
/* Mark only — the name is carried by the hero, so repeating it here just
   crowds the corner. aria-label keeps it announced for screen readers. */
.nav__brand{
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-inline-start: -6px;   /* optical alignment with the text below */
}
.nav__brand img{ width: 40px; height: auto; }
.nav__links{
  display: flex;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  font-size: .85rem;
  color: var(--muted);
}
.nav__links a{ transition: color .5s var(--ease); }
.nav__links a:hover{ color: var(--text); }

/* Menu extras — only ever shown inside the mobile panel. */
.nav__num, .nav__glow, .nav__note, .nav__close{ display: none; }

/* Hamburger — hidden until the mobile breakpoint. Two hairlines of unequal
   length (the asymmetry is the detail), squaring up as they cross into an X. */
.nav__toggle{
  display: none;
  width: 44px;
  height: 44px;
  margin-inline-end: -10px;   /* keeps the bars optically aligned to the edge */
  place-content: center;
  justify-items: end;
  gap: 7px;
}
.nav__toggle-bar{
  width: 24px;
  height: 1px;
  background: var(--text);
  transition: transform .5s var(--ease), width .5s var(--ease);
}
.nav__toggle-bar:nth-child(2){ width: 15px; }
body.menu-open .nav__toggle-bar{ width: 24px; }
body.menu-open .nav__toggle-bar:nth-child(1){ transform: translateY(4px) rotate(45deg); }
body.menu-open .nav__toggle-bar:nth-child(2){ transform: translateY(-4px) rotate(-45deg); }

/* ---------- Hero ---------- */
.hero{
  position: relative;
  min-height: 100svh;
  display: grid;
  align-content: center;
  overflow: hidden;
  padding: 8rem clamp(1.5rem, 7vw, 7rem) 9rem;
}

/* Fixed rather than living inside the hero: the hero clipped it, so only the
   hero had atmosphere while every section below sat on flat black. Pinning it
   to the viewport means one continuous background runs the whole page. */
.bg{
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
main{ position: relative; z-index: 1; }

.blob{
  position: absolute;
  border-radius: 50%;
  filter: blur(120px);
  will-change: transform;
}
.blob--a{
  width: 55vw; height: 55vw;
  top: 0;
  inset-inline-end: -8%;
  background: radial-gradient(circle, var(--accent-soft) 0%, transparent 70%);
  animation: drift-a 26s ease-in-out infinite alternate;
}
.blob--b{
  width: 42vw; height: 42vw;
  bottom: -12%;
  inset-inline-start: 0;
  background: radial-gradient(circle, rgba(47,111,237,.16) 0%, transparent 70%);
  animation: drift-b 32s ease-in-out infinite alternate;
}
@keyframes drift-a{
  from{ transform: translate(0,0) scale(1); }
  to{ transform: translate(-5%, 7%) scale(1.12); }
}
@keyframes drift-b{
  from{ transform: translate(0,0) scale(1); }
  to{ transform: translate(6%, -5%) scale(1.08); }
}
/* Mobile drift: translate only. Scaling a composited layer forces a repaint,
   moving one does not. */
@keyframes drift-flat{
  from{ transform: translate(0,0); }
  to{ transform: translate(-5%, 7%); }
}

.hero__content{
  position: relative;
  z-index: 1;
  max-width: 40rem;
}
.hero__label{
  display: flex;
  align-items: center;
  gap: .75rem;
  font-size: .85rem;
  color: var(--muted);
  margin-bottom: 2rem;
}
.hero__name{
  font-size: clamp(2.4rem, 7.5vw, 5.2rem);
  font-weight: 300;
  line-height: 1.2;
}
.hero__lens-wrap{ position: relative; display: inline-block; }

/* Clipped, chromatically-split clone revealed under the cursor — the page's
   one signature moment. Everything else is kept quiet so this lands. */
.hero__lens-copy{
  position: absolute;
  inset: 0;
  color: var(--text);
  pointer-events: none;
  filter: url(#glassLens);
  clip-path: circle(0px at 50% 50%);
  opacity: 0;
  transition: opacity .4s var(--ease);
}
.hero__lens-copy.is-active{ opacity: 1; }

.hero__tagline{
  margin-top: 1.4rem;
  font-size: clamp(1.05rem, 1.9vw, 1.35rem);
  color: var(--muted);
}

.hero__meta{
  position: absolute;
  bottom: clamp(2rem, 5vw, 3.5rem);
  inset-inline-start: clamp(1.5rem, 7vw, 7rem);
  z-index: 1;
  font-size: .78rem;
  line-height: 1.9;
}
.hero__meta .muted{ color: var(--muted); }

.hero__scroll{
  position: absolute;
  bottom: clamp(2rem, 5vw, 3.5rem);
  inset-inline-end: clamp(1.5rem, 7vw, 7rem);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .7rem;
  font-size: .72rem;
  color: var(--muted);
}
.hero__scroll-line{
  width: 1px;
  height: 3rem;
  background: var(--border);
  position: relative;
  overflow: hidden;
}
/* transform, not top — same reason as the preloader sweep. */
.hero__scroll-line::after{
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 100%;
  background: linear-gradient(to bottom, transparent, var(--border-strong));
  transform: translateY(-100%);
  animation: scroll-tick 3.2s var(--ease) infinite;
}
@keyframes scroll-tick{
  0%{ transform: translateY(-100%); }
  55%{ transform: translateY(100%); }
  100%{ transform: translateY(100%); }
}

/* ---------- Shared section frame ---------- */
.bio, .projects, .contact{
  max-width: 68rem;
  margin-inline: auto;
  padding-inline: clamp(1.5rem, 7vw, 7rem);
}

/* ---------- Bio ---------- */
/* Bio now follows the hero directly, so its top padding is kept tight —
   the full-height hero above it already provides the breathing room. */
.bio{
  position: relative;
  padding-block: clamp(3rem, 7vh, 5rem) clamp(5rem, 11vh, 8rem);
}
.bio__decor{
  position: absolute;
  top: 2rem;
  inset-inline-end: -2rem;
  color: var(--accent);
  opacity: .07;
  pointer-events: none;
}
.bio__text{
  position: relative;
  font-size: clamp(1.15rem, 2.4vw, 1.65rem);
  line-height: 2;
  font-weight: 300;
  max-width: 46rem;
}

/* ---------- Projects ---------- */
.projects{ padding-block: clamp(5rem, 11vh, 8rem); }
/* Fixed at three columns rather than auto-fit: auto-fit will happily drop to
   two, which leaves three cards sitting 2-over-1 and looking lopsided. Three
   across, or one per row below the breakpoint — never two. */
.projects__grid{
  margin-top: 3.5rem;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
}
/* Hairline grid, not rounded boxes — solid rules read considered,
   dashed borders read unfinished.
   Each card draws its own hairline rather than the container painting one
   behind the gaps: a container background also fills any *empty* cell in an
   incomplete row, which showed up as a grey block. Neighbouring shadows meet
   inside the 1px gap and read as a single shared line. */
.project-card{
  /* transparent, not a solid fill — a fill would punch an opaque block out of
     the page-wide glow, which is what made the cards read as a second shade */
  background: transparent;
  box-shadow: 0 0 0 1px var(--hairline);
  padding: 3.5rem 1.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  color: var(--faint);
  font-size: .9rem;
  transition: background .6s var(--ease), color .6s var(--ease);
}
/* A faint tint rather than a different background colour. :hover sticks after
   a tap on touch screens, and a solid second shade left a grey block behind. */
@media (hover: hover){
  .project-card:hover{ background: rgba(255,255,255,.02); color: var(--muted); }
}
.project-card__icon{ display: inline-flex; }

/* ---------- Contact ---------- */
.contact{ padding-block: clamp(5rem, 11vh, 8rem) 4rem; }
.contact__grid{
  margin-top: 3.5rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 3rem 1.5rem;
}
.contact__cell{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .9rem;
  text-align: center;
}
.contact__body{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .35rem;
  min-width: 0;
}
.contact__btn{
  width: 4rem;
  height: 4rem;
  border: 1px solid var(--border);
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--text);
  transition: border-color .6s var(--ease), color .6s var(--ease), transform .6s var(--ease);
}
.contact__btn:hover{
  border-color: var(--accent);
  color: var(--accent);
  transform: translateY(-3px);
}
.contact__label{ font-size: .85rem; }
.contact__value-row{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  color: var(--faint);
  min-width: 0;
}
.contact__value{
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .02em;
  unicode-bidi: plaintext;
  /* On a 320px screen the email is wider than its row. Wrapping keeps it
     readable — the page clips overflow, so without this it would be cut off. */
  overflow-wrap: anywhere;
  min-width: 0;
}
.contact__copy{
  display: inline-flex;
  padding: .25rem;
  color: inherit;
  transition: color .5s var(--ease);
}
.contact__copy:hover{ color: var(--text); }
.contact__copy.is-copied{ color: var(--accent); }

.footer{
  margin-top: 5rem;
  padding-top: 2rem;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: .6rem;
  color: var(--faint);
  font-size: .8rem;
}

/* ---------- Scroll reveals (GSAP toggles .is-visible) ---------- */
[data-reveal]{
  opacity: 0;
  transform: translateY(14px);
}
[data-reveal].is-visible{
  opacity: 1;
  transform: none;
  transition: opacity 1.1s var(--ease), transform 1.1s var(--ease);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce){
  .blob{ animation: none; }
  .hero__scroll-line::after{ animation: none; }
  [data-reveal]{ opacity: 1; transform: none; }
  [data-reveal].is-visible{ transition: none; }
}

/* ---------- Responsive ---------- */

@media (max-width: 720px){
  /* --- Nav: hamburger + full-screen panel --- */
  .nav__toggle{ display: grid; }

  .nav__links{
    position: fixed;
    inset: 0;
    z-index: 45;              /* under .nav (50) so the close button stays on top */
    background: var(--bg);
    overflow: hidden;         /* clips the glow to the panel */
    flex-direction: column;
    justify-content: center;
    align-items: stretch;
    gap: 0;
    padding-inline: clamp(1.5rem, 7vw, 7rem);
    color: var(--text);
    transform: translateY(-100%);
    visibility: hidden;
    /* Visibility is delayed on close so the panel finishes sliding out first,
       and flipped instantly on open. Keeps links out of the tab order while shut. */
    transition: transform .7s var(--ease), visibility 0s linear .7s;
  }
  body.menu-open .nav__links{
    transform: none;
    visibility: visible;
    transition: transform .7s var(--ease), visibility 0s;
  }

  /* An explicit bordered X reads as "close" far more plainly than two
     hairlines crossing. The hamburger steps aside while the panel is open
     so there is only ever one close control. */
  body.menu-open .nav__toggle{ display: none; }
  .nav__close{
    display: grid;
    place-items: center;
    position: absolute;
    top: 1.6rem;
    inset-inline-end: calc(clamp(1.5rem, 7vw, 7rem) - 10px);
    z-index: 2;
    width: 44px;
    height: 44px;
    border: 1px solid var(--border-strong);
    border-radius: 50%;
    color: var(--text);
    transition: border-color .5s var(--ease), color .5s var(--ease);
  }
  .nav__close:active{ border-color: var(--accent); color: var(--accent); }

  /* Numbered rows on hairlines — the same editorial device the sections use. */
  .nav__links a{
    position: relative;
    z-index: 1;
    display: flex;
    align-items: baseline;
    gap: 1.1rem;
    font-size: 2rem;
    font-weight: 300;
    padding-block: 1.15rem;
    border-bottom: 1px solid var(--border);
  }
  .nav__links a:first-of-type{ border-top: 1px solid var(--border); }
  .nav__num{
    display: block;
    font-size: .8rem;
    color: var(--faint);
    font-variant-numeric: tabular-nums;
  }
  .nav__note{
    display: block;
    position: absolute;
    bottom: clamp(2rem, 7vw, 3rem);
    inset-inline-start: clamp(1.5rem, 7vw, 7rem);
    z-index: 1;
    font-size: .8rem;
    color: var(--muted);
  }
  /* Carries the accent into the menu so it is not a flat black slab. */
  .nav__glow{
    display: block;
    position: absolute;
    bottom: -25%;
    inset-inline-start: -30%;
    width: 95vw;
    height: 95vw;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(47,111,237,.30) 0%, transparent 70%);
    filter: blur(65px);
    pointer-events: none;
  }

  /* --- Hero --- */
  .hero{ padding-bottom: 8.5rem; }

  /* A radial gradient is already soft, so the blur filter on top of it was
     redundant — and because the drift also scales, that blur had to be
     re-rasterised across a ~1400px surface every single frame. Dropping the
     filter and the scale is the single biggest mobile win here.
     One blob instead of two halves the compositing work again. */
  .blob{ filter: none; }
  .blob--a{
    width: 115vw; height: 115vw;
    top: -8%;
    inset-inline-end: -30%;
    background: radial-gradient(circle,
      rgba(47,111,237,.30) 0%,
      rgba(47,111,237,.13) 38%,
      transparent 68%);
    animation: drift-flat 26s ease-in-out infinite alternate;
  }
  .blob--b{ display: none; }

  /* A fixed, blend-mode layer must be re-composited over the whole viewport
     on every scroll frame. Plain alpha costs a fraction of that. */
  .grain{ mix-blend-mode: normal; opacity: .03; }
  .hero__meta{ font-size: .74rem; }
  .hero__scroll{ padding: .5rem 1rem; }   /* the hairline alone is untappable */

  /* --- Sections: tighter rhythm on a small screen --- */
  /* Pulled flush with the section edge. The desktop bleed hangs into the page
     margin, which a phone does not have — there it hung off the viewport and
     was what dragged the page sideways. */
  .bio__decor{ width: 200px; inset-inline-end: 0; }
  .bio__decor svg{ width: 200px; height: 200px; }
  .projects__grid{ margin-top: 2.5rem; grid-template-columns: 1fr; }
  .project-card{ padding: 2.5rem 1.25rem; }

  /* --- Contact: stacked rows read better than a cramped grid --- */
  .contact__grid{
    grid-template-columns: 1fr;
    gap: 0;
    margin-top: 2.5rem;
    border-top: 1px solid var(--border);
  }
  .contact__cell{
    flex-direction: row;
    align-items: center;
    text-align: start;
    gap: 1rem;
    padding-block: .6rem;
    border-bottom: 1px solid var(--border);
  }
  .contact__btn{ width: 3rem; height: 3rem; flex-shrink: 0; }
  .contact__body{ align-items: flex-start; flex: 1; }
  .contact__value-row{ justify-content: flex-start; }
  .contact__value{ font-size: .8rem; }
  /* 44px is the iOS minimum touch target — the icon stays small, the hit area does not. */
  .contact__copy{
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    padding: 0;
    flex-shrink: 0;
  }

  .footer{ margin-top: 3rem; }
}
