/* ============================================================
   HERO — sticky cinematic scroll region

   Structure:
     .hero-scroll   tall scroll region, gives the sequence its runway
       .hero-sticky   one viewport tall, pinned while scrolling through
         .hero-canvas   the frame sequence
         .hero          the text layer, on top

   The height of .hero-scroll is what decides how much physical
   scrolling the cinematic takes. It is a variable so the pacing can
   be retuned without touching JavaScript — the script derives
   everything from the element's measured box.
   ============================================================ */

:root {
  /* 460vh, not 400vh — and the extra 60 is not padding.
     ------------------------------------------------------------
     The sticky pane eats one viewport, so the usable travel is
     height - 100vh. At 400vh that was 300vh mapped across 110
     frames: ~25px of scroll per frame, about four frames to a
     wheel notch.

     The slogan now leaves before the sequence starts, and it needs
     travel of its own. Taking that out of the existing 400 would
     leave the cinematic 240vh — ~20px per frame, five frames a
     notch, a 25% coarser step than what the non-uniform frame
     sampling was tuned to produce. Adding the runway instead keeps
     the sequence's pacing bit-for-bit unchanged.

     Below 768px and under reduced motion the whole region collapses
     to 100svh (see .hero-scroll--static), so none of this height
     exists on a phone. */
  --hero-scroll-height: 460vh;

  /* 60vh of that is the slogan's exit. A trackpad flick moves
     400-800px; 60vh lands inside that band on a normal screen, so
     one ordinary gesture plays the animation start to finish.
     Shorter and a single flick skips past it unseen; longer and the
     user scrolls a full screen watching text leave, which reads as
     being stuck. */
  --hero-text-runway: 60vh;

  /* The laptop sits slightly below centre in the source framing, so
     the crop is biased upward to keep it in view on wide screens. */
  --hero-frame-position: center 45%;
}

.hero-scroll {
  position: relative;
  height: var(--hero-scroll-height);
  background-color: var(--c-bg);
}

/* Reduced motion, or a screen too small to be worth 2.4MB of
   frames: the sequence never starts, so the runway would just be
   empty scrolling. Collapse it to a single screen. */
.hero-scroll--static {
  height: 100svh;
}

.hero-sticky {
  position: sticky;
  top: 0;
  height: 100svh;

  /* The panel's margin is a BORDER, not padding and not an inset on
     each child.

     An absolutely positioned element resolves against its
     containing block's PADDING box, so padding here would be
     ignored by the canvas and the scrim, and insetting each child
     individually does not work either: a <canvas> is a replaced
     element, so with `inset` set but no width/height it falls back
     to its intrinsic 1280x720 instead of filling. Measured on the
     first attempt — the panel came out 402px short and shifted 9px
     sideways rather than inset.

     A border moves the padding box itself, so every absolutely
     positioned child inherits the inset for free and none of them
     needs to know about it.

     Colour is --rgb-surface rather than --c-bg because .hero-scroll
     pins --c-bg to the media colour for everything sitting on
     footage, and this border is the one part of the hero that is
     deliberately NOT footage. On dark the two resolve to the same
     value and the border is 0 anyway. */
  border: var(--hero-inset) solid rgb(var(--rgb-surface));
  /* Outer radius, so the INNER edge — which is what `overflow: clip`
     rounds the footage to — lands on --hero-radius exactly. */
  border-radius: calc(var(--hero-radius) + var(--hero-inset));
  background-color: rgb(var(--rgb-surface));
  overflow: clip;
  display: flex;
  flex-direction: column;
}

/* ------------------------------------------------------------
   FRAME SEQUENCE
   ------------------------------------------------------------ */

.hero-canvas,
.hero-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}

/* The canvas is a replaced element, so it crops with object-fit.
   The poster is a <div> and crops with background-size — the two
   properties are not interchangeable, but the geometry they
   produce here is identical, and both read the same position
   token so the framing cannot drift apart. */
.hero-canvas {
  object-fit: cover;
  object-position: var(--hero-frame-position);
}

/* The poster carries the hero until the first frame has decoded,
   and stays as the whole visual on mobile and reduced motion —
   which is why it has to follow the theme on its own rather than
   waiting for the sequence: below 768px the sequence never starts,
   so the poster IS the hero. */
.hero-poster {
  z-index: 0;
  background-image: url('../../hero/hero-poster.webp');
  background-size: cover;
  background-position: var(--hero-frame-position);
  background-repeat: no-repeat;
}

:root[data-theme='light'] .hero-poster {
  background-image: url('../../hero/hero-poster-light.webp');
}

.hero-canvas {
  z-index: 1;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease);
}

.hero-canvas.is-ready {
  opacity: 1;
}

/* ------------------------------------------------------------
   TEXT LAYER

   Sits above the sequence for the whole pin. The reference design
   keeps its hero copy legible over the footage rather than
   animating it out, and the copy is the SEO-bearing content, so it
   stays real HTML on top rather than living inside the frames.
   ------------------------------------------------------------ */

.hero {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Legibility floor. The sequence runs from near-black to a bright
   UI mockup at the end, so the copy cannot rely on the footage
   staying dark — this keeps contrast stable across the whole
   timeline without flattening the image. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  inset: 0;
  background-image:
    linear-gradient(to bottom,
      rgb(var(--rgb-media) / 0.72) 0,
      rgb(var(--rgb-media) / 0.30) 28%,
      rgb(var(--rgb-media) / 0.30) 62%,
      rgb(var(--rgb-media) / 0.85) 100%);
}

/* Grid, not flex-with-auto-margins. Two rows — a 1fr that centres
   the slogan and an auto that holds the bottom bar — so the slogan
   sits at the true centre of the space above the bar. Stacking
   `margin: auto` on both elements instead splits the free space
   three ways and lands the slogan a sixth of a screen high.

   The other reason is robustness: if the slogan is ever removed the
   1fr row simply collapses and the bar stays at the bottom. A
   layout that breaks when an element is deleted is a trap.

   Top padding clears the fixed hamburger, which floats over this
   section rather than sitting in a bar above it. */
.hero__inner {
  flex: 1;
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: center;
  gap: var(--sp-xl);
  padding-block: clamp(5.5rem, 13vh, 8rem) var(--sp-2xl);
}

/* ------------------------------------------------------------
   THE MARK, ABOVE THE SENTENCE

   The slogan used to be the largest thing on the page and did the
   work of a masthead. It is half the size now, so the logo takes
   that job and the h1 goes back to being a sentence.

   `justify-self: center` rather than a margin pair: the parent is
   the same centred grid the slogan sits in, and mixing the two
   centring methods in one column is how they drift apart.
   ------------------------------------------------------------ */
/* One grid item holding both — see the note in index.html. */
.hero__lead {
  justify-self: center;

  /* `start`, not the row's default centring: the block belongs in
     the upper part of the frame, above the laptop in the footage
     rather than across it. The row is still 1fr, so the space it
     gives up falls BELOW the block where the scroll cue lives, not
     above it where the fixed controls are. */
  align-self: start;

  display: flex;
  flex-direction: column;
  align-items: center;
}

/* THE SIZE IS A LEGIBILITY FLOOR, NOT A PREFERENCE.

   The file is 1962px wide and carries real detail at that size:
   hairline bars, the feather cuts in the wings, and a second
   wordmark ("e-commerce") set much smaller than the first. At the
   140px this used to render at, all three collapse — the sub-
   wordmark in particular stops being letters and becomes a dark
   smudge under the name.

   Worth recording, because the first diagnosis of that smudge was
   wrong. It was read as black fills in the SVG, and 31 fills there
   ARE dark. But measured on a light background, only THREE pixels
   of the entire mark are actually achromatic — the other ~1,270
   dark pixels are deep violet and read correctly as shading.
   Nothing was wrong with the colours; the mark was being drawn too
   small to resolve. */
.hero__logo {
  inline-size: clamp(210px, 30vw, 420px);
  block-size: auto;
  margin-block-end: clamp(var(--sp-md), 2.5vh, var(--sp-xl));

  /* NO DROP-SHADOW. It was here to lift the mark off the footage
     and it did the opposite.

     Measured: on dark, --rgb-shadow is black and the shadow lands
     on a near-black frame, so it is invisible and buys nothing. On
     light it is rgb(16,24,40) at 55%, blurred 24px and offset 8px
     down — and the mark is not a solid shape. It is a wordmark with
     gaps: between the feathers of each wing, inside the letters,
     between the vertical bars. The shadow fills every one of those
     gaps with a dark blur, which is exactly the "black lines and
     dots" this was reported as.

     The violet already clears the page unaided in both themes
     (#A332D7 is 4.9:1 on the light panel), so nothing replaces it.

     Recorded because two earlier diagnoses were wrong: the dark
     fills inside the SVG (only 3 pixels of the mark are actually
     achromatic) and the render scale (0.1-0.2% of the mark goes
     muddy at every size from 140px to 420px). Neither was it. */

  /* Rides the slogan's own exit. --hero-out is written to
     .hero-scroll by hero-scroll.js and inherits down, so the mark
     leaves with the sentence instead of hanging there after it. */
  opacity: calc(1 - var(--hero-out, 0));
  transform: translateY(calc(var(--hero-out, 0) * -1.15em))
             scale(calc(1 - var(--hero-out, 0) * 0.22));
}

/* ------------------------------------------------------------
   SLOGAN  —  the page's <h1>

   The company line, and the only heading the hero carries. It is
   real text rather than part of the frames because it is the
   document's h1 and the sentence a search engine has to find.
   ------------------------------------------------------------ */

.hero__slogan {
  margin: 0;
  justify-self: center;
  text-align: center;
  max-inline-size: 16ch;

  font-size: var(--fs-slogan);
  font-weight: var(--fw-black);
  line-height: var(--lh-display);
  letter-spacing: 0;              /* Persian letterforms connect */

  /* Depth, to replace the size that came off.

     ONE filter here rather than one per word: nine separate
     drop-shadows would each re-render on every frame of the shine,
     and the words sit on a single line-box anyway, so filtering the
     composited result looks the same for a ninth of the work.

     It has to be `filter`, not `text-shadow`. The glyphs are filled
     with `transparent` so the gradient can show through the clip —
     a text-shadow would paint the glyph shape BEHIND that
     transparent fill and read as a muddy second colour inside the
     letters instead of a glow outside them. drop-shadow works off
     the composited alpha, which is exactly the clipped gradient.

     Two passes: a wide cool halo for the lift, and a tight dark one
     so the sentence still separates when the frame sequence reaches
     the bright screen at the end of the camera move.

     The two passes live in --hero-slogan-filter rather than here,
     because the light theme now has its own footage and needs the
     polarity reversed — a light halo with a soft dark drop. Keeping
     the value in the token file puts both versions side by side
     instead of splitting them across two stylesheets. */
  filter: var(--hero-slogan-filter);
}

/* Word-level spans, never per-character: Persian letters join, and
   an element boundary inside a word breaks the shaping — the same
   reason the loader tagline is split this way. Words that carry a
   ZWNJ (می‌کنیم, هیچ‌کس) stay whole inside one span. */
.hero__word {
  display: inline-block;

  /* The sheen and the metal ramp, clipped to the glyphs.
     ------------------------------------------------------------
     Clipped per WORD rather than across the whole sentence, because
     each word carries its own transform and a transformed child is
     not reliably part of its parent's text-clip region.

     Visually it costs nothing: --grad-slogan runs at 172deg, eight
     degrees off vertical, and every word spans the full height of
     its line — so a per-word ramp and a per-line one paint almost
     the same pixels. The sweep is recovered by delaying each word's
     shine, which reads as a highlight travelling along the
     sentence rather than one gradient sliding under it. */
  background-image:
    linear-gradient(100deg,
      transparent 38%,
      var(--hero-shine) 50%,
      transparent 62%),
    var(--grad-slogan);
  background-size: 200% 100%, 100% 100%;
  background-position: 200% 0, 0 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;

  /* Clipping to the glyphs cuts Persian descenders (ج ح ع م ی) at
     the text box. The padding gives them room; the matching
     negative margin takes that room back out of the layout, so the
     line rhythm set by --lh-display is untouched. */
  padding-block: 0.14em;
  margin-block: -0.14em;

  /* Entrance and exit are driven by two independent numbers that
     meet in ONE transform below, so neither can clobber the other
     if the user scrolls while the words are still arriving. */
  --wo: clamp(0, calc((var(--hero-out, 0) - var(--i) * 0.05) * 2.4), 1);

  opacity: calc(var(--hero-in, 1) * (1 - var(--wo)));
  transform:
    perspective(var(--perspective))
    translateY(calc((1 - var(--hero-in, 1)) * 0.8em + var(--wo) * -1.15em))
    rotateX(calc((1 - var(--hero-in, 1)) * -55deg + var(--wo) * 65deg))
    scale(calc(1 - var(--wo) * 0.22));
}

/* Registered so they interpolate as numbers. Unregistered custom
   properties animate discretely — the entrance would snap rather
   than tween.

   BOTH initial values describe the RESTING, FULLY VISIBLE state,
   and that is the whole point of the polarity.

   A registered property's initial-value beats any var() fallback,
   so `var(--hero-in, 1)` above is dead code the moment @property
   is supported — an initial-value of 0 would therefore hold the h1
   at opacity 0 on every path where the animation never attaches:
   scripts blocked, the <head> failsafe withdrawing `.js`, a slow
   page where that 2s timer wins the race. Measured: with `.js`
   absent the sentence computed to opacity 0 and vanished.

   Starting at 1 inverts that. `animation-fill-mode: both` still
   pins the words to the `from` state (0) through their delay, so
   the entrance is unchanged when it runs — but with no animation
   at all the h1 is simply there, which is the only acceptable
   failure for the page's heading. */
@property --hero-in  { syntax: '<number>'; initial-value: 1; inherits: false; }
@property --hero-out { syntax: '<number>'; initial-value: 0; inherits: true;  }

@keyframes hero-word-in { from { --hero-in: 0; } to { --hero-in: 1; } }

/* Sweeps in the first fifth of the cycle and rests for the other
   four. The jump back at 100% happens with the band already off the
   left edge, so nothing bright is over the glyphs when it resets. */
@keyframes hero-shine {
  0%   { background-position:  200% 0, 0 0; }
  20%  { background-position: -100% 0, 0 0; }
  100% { background-position: -100% 0, 0 0; }
}

/* Scoped to `.js` for the same reason every entrance on this site
   is: the animation's first frame is opacity 0, and if the scripts
   never run the h1 must still be on the page. */
.js .hero__word {
  animation:
    hero-word-in 0.85s var(--ease-out) calc(var(--i) * 0.075s + 0.2s) both,
    hero-shine   5s    var(--ease)     calc(var(--i) * 0.09s  + 1.6s) infinite;
}

/* Held at the first frame while the entry loader is up, so the
   sentence performs for the viewer instead of behind a black
   screen. `is-loading` is set by loader.js and dropped when it
   dismisses; on a page with no loader it is never set and the
   animation simply runs on load. */
.js.is-loading .hero__word {
  animation-play-state: paused;
}

/* Once the slogan has cleared, the shine is repainting glyphs at
   opacity 0 for the rest of the scroll. One attribute flip from the
   same place that hides the scroll cue turns it off. */
.hero-scroll[data-hero-text='gone'] .hero__word {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .js .hero__word { animation: none; }
  .hero__word     { opacity: 1; transform: none; }
}

/* The bottom bar and its "مشاهده نمونه‌کارها" button are gone. The
   hero now carries the slogan and nothing else, so the frame
   sequence plays with one sentence over it rather than a sentence
   and a call to action competing for the same screen. Nothing
   below relies on .hero__foot; the rule went with the markup
   instead of being left behind as a selector that matches nothing.

   The section is still reachable — the menu links to it, and the
   scroll cue below points the way. */

/* ---- scroll cue ----------------------------------------------
   The hero now occupies four viewports; without a hint the pinned
   first screen can read as a page that has finished. Fades out as
   soon as the sequence starts moving. */
.hero__cue {
  position: absolute;
  inset-inline: 0;
  inset-block-end: var(--sp-md);
  z-index: 3;

  display: flex;
  justify-content: center;
  pointer-events: none;

  font-size: var(--fs-xs);
  letter-spacing: 0;
  /* Not --c-text-dim: .hero-scroll pins that to the media ink for
     everything sitting on footage, and this one is not — at 95% of
     the hero's height it lands inside the light theme's edge
     dissolve, where light ink would be invisible. */
  color: var(--c-hero-cue);
  transition: opacity var(--dur-base) var(--ease);
}

.hero-scroll[data-hero-cue='hidden'] .hero__cue {
  opacity: 0;
}

@media (max-width: 767px) {
  :root {
    --hero-scroll-height: 380vh;
    --hero-text-runway: 50vh;
  }

  /* Framing on mobile portrait:
     16:9 canvas with object-fit: contain prevents any horizontal cropping
     of the MacBook chassis or screen content. Scale 1.18 balances frame width
     to give the laptop presence on phone screens while preserving full margins. */
  .hero-canvas {
    object-fit: contain;
    object-position: center 58%;
    transform: scale(1.18);
    transform-origin: center 58%;
  }

  .hero-poster {
    background-size: contain;
    background-position: center 58%;
    transform: scale(1.18);
    transform-origin: center 58%;
  }

  .hero__inner {
    padding-block: clamp(4.2rem, 11vh, 5.5rem) var(--sp-xl);
    gap: var(--sp-lg);
  }

  .hero__lead {
    align-self: start;
  }

  .hero__logo {
    inline-size: clamp(170px, 46vw, 250px);
    margin-block-end: clamp(0.75rem, 2vh, 1.25rem);
  }

  .hero__slogan {
    max-inline-size: 18ch;
  }

  /* Scroll cue placed cleanly above the floating dock navigation */
  .hero__cue {
    display: flex;
    inset-block-end: calc(var(--dock-h) + var(--dock-gap) + 14px);
    font-size: 0.78rem;
    opacity: 0.85;
  }
}

