/* ====================================================================
   motion.css — the motion and finish layer, shared by both trees.

   Load order. This file is LAST in both <head>s: after style.css on the
   English tree, after style.rtl.css + fa.css on the Persian one. Being
   last is what lets a plain, low-specificity selector here beat a theme
   rule of equal weight, which two sections below rely on.

   ONE FILE, BOTH DIRECTIONS. Every rule here is either direction-neutral
   or carries an explicit [dir="rtl"] companion. So:

       DO NOT run this file through rtlcss.

   style.rtl.css is a generated artifact; this one is hand-written, and
   flipping it would double-flip the two rules that are already handled.

   Three rules the file keeps, and that anything added to it must keep:

     1. No CDN, no @import, no external host of any kind. The Persian
        tree serves visitors inside Iran, where Google Fonts, jsDelivr,
        unpkg and cdnjs are all unreachable. The only URL below is an
        inline data: URI.

     2. Nothing hides content unless the thing that would reveal it is
        known to work. Every rule that starts an element at opacity 0
        sits inside an @supports block for the feature that animates it
        back. A browser without the feature never runs the rule, so it
        never has content stuck invisible — the failure mode of every
        scroll-animation library that ships without this guard.

     3. prefers-reduced-motion: reduce turns off everything that moves
        by itself. style.css honours that preference in 44 places and
        this file is not going to be the exception.

        One deliberate exception, section 1: the reading-progress bar
        moves only in direct response to the user's own scrolling, in
        lockstep with it, like a scrollbar. It starts nothing, it
        cannot surprise anyone, and removing the only affordance that
        says "how far down am I" is not an accessibility win.

   Nothing in this file is required for the page to be usable. It is all
   finish. If any of it misbehaves, deleting the two <link>/<script>
   lines that load motion.css and motion.js restores the site exactly.
   -------------------------------------------------------------------- */

:root {
  /* The brand colours the effects below draw from.

     Do NOT read --bs-primary here. It still holds the theme's stock
     #3f78e0 blue, but the site overrides every place that colour would
     show: .text-primary is #02a7a4 at style.css:11265, and .puls-btn
     carries the same teal at the tail of the file. The variable is
     stale; the teal is what a visitor actually sees, so that is the
     brand.

     The gold is the hero's own gradient overlay, hardcoded at
     style.css:39117 with no variable of its own. */
  --zt-teal: #02a7a4;
  --zt-teal-light: #3fd0cd;
  --zt-gold: #a5882c;
}

/* --------------------------------------------------------------------
   1. Reading progress — scroll-driven, zero JavaScript.

      animation-timeline: scroll() drives an animation from the document
      scroll position instead of from a clock. The browser does it on the
      compositor, so the bar tracks the scroll without a scroll listener
      and without a frame of lag.

      body::before is free: nothing in style.css or plugins.css uses it.

      z-index 9000 is chosen to sit between the two things it must not
      get wrong — above the sticky navbar (1040) and the back-to-top
      widget (1010), below the offcanvas menu and its backdrop (9999),
      which must stay on top of everything while it is open.

      RTL: the bar has to fill from the side the reader starts on, so
      transform-origin and the gradient both flip. This is the first of
      the two hand-flipped rules mentioned in the header.
   -------------------------------------------------------------------- */
@supports (animation-timeline: scroll()) {
  body::before {
    content: "";
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    height: 2px;
    z-index: 9000;
    pointer-events: none;
    background: linear-gradient(to right, var(--zt-teal), var(--zt-gold));
    transform: scaleX(0);
    transform-origin: left center;
    animation: zt-progress linear both;
    animation-timeline: scroll(root block);
  }

  [dir="rtl"] body::before {
    background: linear-gradient(to left, var(--zt-teal), var(--zt-gold));
    transform-origin: right center;
  }

  @media print {
    body::before {
      display: none;
    }
  }
}

@keyframes zt-progress {
  to {
    transform: scaleX(1);
  }
}

/* --------------------------------------------------------------------
   2. Scroll-driven reveals — animation-timeline: view().

      view() ties an animation to the element's own passage through the
      viewport rather than to the page scroll. Same compositor path as
      section 1, same zero JavaScript.

      2a. The hero contents drift up and fade as the hero scrolls away.
          The range is `exit`: it opens the moment the hero's top edge
          reaches the top of the viewport — that is, at scroll 0, since
          the hero starts there — and closes when the hero is fully
          past. Stopping at 70% means the text is gone a little before
          the photo is, which reads as depth rather than as a fade.

      2b. [data-reveal] is an opt-in for anything else. It is NOT
          applied anywhere yet; it exists so new sections have a
          CSS-only reveal that does not need scrollCue.

          :not([data-cue]) is load-bearing. scrollCue (plugins.js:10877,
          initialised in theme.js:324) reveals elements by assigning
          element.style.animationName directly from the data-cue value.
          An element carrying both attributes would have two animations
          fighting over opacity and transform, and scrollCue's inline
          style would win the cascade — the element would flicker and
          then land wherever scrollCue put it. One mechanism per
          element; this guard enforces it.
   -------------------------------------------------------------------- */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .video-wrapper .video-content {
      animation: zt-hero-exit linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 70%;
    }

    [data-reveal]:not([data-cue]) {
      animation: zt-reveal linear both;
      animation-timeline: view();
      animation-range: entry 15% cover 30%;
    }
  }
}

@keyframes zt-hero-exit {
  to {
    opacity: 0;
    transform: translateY(-2.5rem);
  }
}

@keyframes zt-reveal {
  from {
    opacity: 0;
    transform: translateY(1.5rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------
   3. Cross-document View Transitions.

      navigation: auto makes the browser hold the outgoing page's
      rendered frame, load the next one, and animate between them. Real
      navigations throughout: separate documents, separate URLs, full
      server-rendered HTML. Nothing about it is an SPA, so nothing about
      it changes what a crawler sees.

      It applies only to same-origin navigations, which is exactly the
      right scope here — the portfolio download (it carries `download`,
      so it is not a navigation) and the social links in the footer
      (target="_blank") are both left alone without needing an opt-out.

      Both documents have to opt in for the transition to happen. They
      do: this file is linked from both <head>s.

      WHY NO view-transition-name ON THE HEADER. The obvious next step
      is to name the navbar so it stays put while the content crossfades.
      Do not. theme.js:51 hands the navbar to Headhesive, which CLONES
      it into .navbar-clone once the user scrolls. From that moment the
      page has two .navbar elements. A view-transition-name that matches
      more than one element is not a partial failure — it aborts the
      whole transition, silently, and only for visitors who had scrolled.
      The root-level animation below needs no names and cannot hit this.
   -------------------------------------------------------------------- */
@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation: zt-vt-out 140ms cubic-bezier(0.4, 0, 1, 1) both;
}

::view-transition-new(root) {
  animation: zt-vt-in 260ms cubic-bezier(0, 0, 0.2, 1) both;
}

@keyframes zt-vt-out {
  to {
    opacity: 0;
  }
}

@keyframes zt-vt-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* The transition pseudo-elements live in their own tree and are not
   reachable from the rules further down, so reduced motion has to be
   answered here. Switching the animations off does not disable the
   transition — the browser still does the swap, it just happens in one
   frame, which is what the preference asks for. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* --------------------------------------------------------------------
   4. Hero aurora — @property plus a drifting gradient.

      Custom properties are strings to the animation engine: --x: 18%
      going to --x: 62% jumps at the halfway mark rather than sweeping.
      @property is what fixes that. Declaring the syntax as <percentage>
      tells the engine the value is a number with a unit, and it
      interpolates. That is the whole technique; without these five
      declarations the block below stutters instead of drifting.

      COMPOSITED WITH THE PHOTO, NOT REPLACING IT. header-img.jpg is
      244 KB and this layer would stand on its own, but whether the
      office photograph appears on the front page is a brand decision,
      not a performance one. To drop it, add `aurora-only` to the hero
      <section> in resources/views/layout/home.blade.php and
      resources/views/fa/templates/home.blade.php — the rule is at the
      end of this section. Nothing else has to change.

      Stacking inside .video-wrapper, which is position: relative with
      overflow: hidden:

          <img>                     z-index auto  — the photograph
          ::before (this)           z-index 1     — the aurora
          ::after (style.css:39105) z-index 1     — the gold overlay,
                                                    later in paint order
                                                    so it tints the
                                                    aurora too
          .video-content            z-index 2     — the copy

      mix-blend-mode: screen means the aurora only ever lightens, so it
      lifts the photograph instead of veiling it.

      border-radius: inherit tracks .page-frame's 1rem corners at
      >= 768px (style.css:22855) without repeating the breakpoint.

      The animation is gated to >= 768px as well as to reduced-motion.
      A continuously repainting blend layer is a real battery cost on a
      phone, and at that width the hero is small enough that the drift
      is close to imperceptible. Narrow viewports get the same aurora,
      standing still.
   -------------------------------------------------------------------- */
@property --zt-aurora-x {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 18%;
}

@property --zt-aurora-y {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 24%;
}

@property --zt-aurora-x2 {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 82%;
}

@property --zt-aurora-y2 {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 76%;
}

@property --zt-aurora-spin {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.video-wrapper.bg-overlay::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: inherit;
  mix-blend-mode: screen;
  opacity: 0.5;
  background:
    radial-gradient(38rem 26rem at var(--zt-aurora-x) var(--zt-aurora-y),
      rgba(63, 208, 205, 0.42), transparent 68%),
    radial-gradient(34rem 24rem at var(--zt-aurora-x2) var(--zt-aurora-y2),
      rgba(165, 136, 44, 0.40), transparent 70%),
    conic-gradient(from var(--zt-aurora-spin) at 50% 130%,
      rgba(2, 167, 164, 0.26), transparent 32%,
      rgba(165, 136, 44, 0.24) 62%, rgba(2, 167, 164, 0.26));
}

@media (prefers-reduced-motion: no-preference) and (min-width: 768px) {
  .video-wrapper.bg-overlay::before {
    animation:
      zt-aurora-drift 26s ease-in-out infinite alternate,
      zt-aurora-turn 64s linear infinite;
  }
}

@keyframes zt-aurora-drift {
  to {
    --zt-aurora-x: 62%;
    --zt-aurora-y: 44%;
    --zt-aurora-x2: 36%;
    --zt-aurora-y2: 58%;
  }
}

@keyframes zt-aurora-turn {
  to {
    --zt-aurora-spin: 360deg;
  }
}

/* Opt-in: the aurora alone, no photograph. Not applied anywhere. */
.video-wrapper.aurora-only > img {
  display: none;
}

.video-wrapper.aurora-only {
  background-color: #151a22;
}

/* --------------------------------------------------------------------
   5. Film grain.

      A single feTurbulence tile, desaturated by feColorMatrix and
      inlined as a data: URI — around 300 bytes, no request, no CDN. It
      breaks up the flat fills the theme uses everywhere and is the
      cheapest thing on this page that reads as expensive.

      Fixed rather than scrolling: grain that moves with the content
      looks like a texture printed on it, grain that stays still looks
      like the screen. The second is the one that works.

      Opacity 0.035 is meant to be invisible when you look for it. If it
      is visible, it is too strong.

      z-index as in section 1 — over the navbar, under the offcanvas.
      body::after is free, same as body::before.
   -------------------------------------------------------------------- */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9000;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

@media print {
  body::after {
    display: none;
  }
}

/* --------------------------------------------------------------------
   6. Blur-up on load — the CSS half. motion.js is the other half.

      An image that has not arrived yet is blurred and very slightly
      oversized; when it decodes, it sharpens and settles. The scale is
      there so the blur does not show a soft edge against the layout.

      motion.js only ever adds .zt-blur-up to images that were still
      loading when it ran, so a cached image is never blurred and then
      unblurred for no reason.

      NOT APPLIED TO THE HERO. style.css:39142 gives .video-wrapper img
      a deliberate -webkit-filter: blur(4px). Chrome treats that as an
      alias of `filter`, so the .zt-loaded rule below — being later in
      the cascade — would resolve it to blur(0) and quietly delete the
      theme's effect. motion.js excludes .video-wrapper img for exactly
      this reason; if that exclusion is ever removed, the hero photo
      goes sharp on Chrome and stays soft on Firefox.
   -------------------------------------------------------------------- */
img.zt-blur-up {
  filter: blur(14px);
  transform: scale(1.04);
  transition: filter 0.65s ease, transform 0.65s ease;
}

img.zt-blur-up.zt-loaded {
  filter: blur(0);
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  img.zt-blur-up,
  img.zt-blur-up.zt-loaded {
    filter: none;
    transform: none;
    transition: none;
  }
}

/* --------------------------------------------------------------------
   7. Magnetic buttons — the CSS half. motion.js sets --zt-mx/--zt-my.

      The button leans a few pixels toward the cursor while the cursor
      is near it. Roughly a kilobyte between the two files, no library.

      Gated on (hover: hover) and (pointer: fine) so it never runs on
      touch, where there is no cursor to lean toward and where the
      transform would only appear after the tap has already happened.

      SPECIFICITY IS THE POINT OF THE :not(.btn-link). style.css:27027
      is `.btn:not(.btn-link):hover { transform: translateY(-0.15rem) }`,
      at (0,3,0). The selector below matches it structurally and adds
      the attribute, reaching (0,4,0), so it wins on weight rather than
      on load order. The -0.15rem is folded back into the translate so
      the theme's hover lift is preserved rather than replaced.

      Two transition durations on purpose: 0.12s while the pointer is
      over the button, so the lean tracks; 0.25s on the rule that also
      applies once it leaves, so the button eases back instead of
      snapping.
   -------------------------------------------------------------------- */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .btn[data-zt-magnetic] {
    transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.25, 1);
  }

  .btn[data-zt-magnetic]:not(.btn-link):hover {
    transform: translate(var(--zt-mx, 0px), calc(var(--zt-my, 0px) - 0.15rem));
    transition-duration: 0.12s;
  }
}

/* --------------------------------------------------------------------
   8. Home hero (.hero-main)

   Everything below is scoped under .hero-main, which one section on each
   home page carries — layout/home.blade.php and fa/templates/home.blade.php
   — and nothing else does. The hero is the only part of the site whose
   layout lives here rather than in style.css, because style.css is the
   stock theme sheet and this replaces a piece of it.

   What it changes against the theme's stock hero, and why:

     - The gold overlay goes. bg-overlay-gradient paints
       linear-gradient(120deg, #a5882c, #6b5400) at style.css:39117,
       and gold against the brand teal is a clash that forces every
       accent in the hero to fall back to white. The scrim here is
       near-black with one teal corner instead.

     - The photograph is pushed further back. It is a blurred stock
       code editor carrying no information, so it earns its place as
       texture, not as subject: the same blur the theme applies, plus
       desaturation and a drop in brightness. The blur is restated
       rather than inherited. style.css:39142 declares only
       -webkit-filter, and a bare `filter` here would replace it
       outright in Chrome, which aliases the two.

     - Type hierarchy inverts. The promise becomes the one h1; the
       brand name drops to an eyebrow. Two h1 elements on one page was
       the old markup's doing, not a design decision.

   Direction is handled by hand, as everywhere else in this file, and
   this section has the most of it: the scrim angle, the arrow travel,
   the title's leading and the Latin-only letter-spacing all flip.
   -------------------------------------------------------------------- */

/* -- Box ------------------------------------------------------------- */

/* The theme centres a hero by absolutely positioning .video-content at
   height:100% (style.css:39148). That works while the content is two
   lines and two buttons. This hero is six blocks tall, and an absolutely
   positioned box cannot push its section open however tall it gets — so
   it overflowed at both ends at once, sliding under the overlaid navbar
   at the top and past the frame's rounded corner at the bottom.

   Padding alone would not have fixed it; the box had no room to give.
   Putting the content back in flow and centring the section itself makes
   min-vh-80 a floor instead of a ceiling: short viewport, the section
   grows; tall viewport, the content sits centred exactly as before. */
.video-wrapper.hero-main {
  display: flex;
  align-items: center;
}

.hero-main .video-content {
  position: relative;
  height: auto;
  /* Top clears the transparent navbar the hero sits underneath; bottom
     clears the scroll cue, which is positioned against this edge. */
  padding-block: 7rem 4.25rem;
  padding-inline: 1.25rem;
}

/* -- Background ------------------------------------------------------ */

/* The scrim. Overrides .video-wrapper.bg-overlay:after (style.css:39105)
   on weight, (0,3,0) against the theme's (0,2,0), not on load order.
   z-index drops to 0 so it paints under the aurora rather than over it:
   both pseudo-elements sat at z-index 1, and ::before loses that tie on
   DOM order, which is why the aurora was invisible under an 0.82 black.
   The photo is z-index auto and comes first among the 0s, so the order
   is photo, scrim, grid, aurora, text. */
.video-wrapper.hero-main.bg-overlay::after {
  z-index: 0;
  opacity: 1;
  background:
    linear-gradient(180deg, rgba(8, 14, 18, 0.58) 0%, rgba(8, 14, 18, 0.88) 100%),
    linear-gradient(115deg, rgba(2, 167, 164, 0.34) 0%, rgba(8, 14, 18, 0) 58%);
}

[dir="rtl"] .video-wrapper.hero-main.bg-overlay::after {
  background:
    linear-gradient(180deg, rgba(8, 14, 18, 0.58) 0%, rgba(8, 14, 18, 0.88) 100%),
    linear-gradient(245deg, rgba(2, 167, 164, 0.34) 0%, rgba(8, 14, 18, 0) 58%);
}

/* Now that it is no longer buried, the aurora can carry the hero. */
.video-wrapper.hero-main.bg-overlay::before {
  opacity: 0.8;
}

.video-wrapper.hero-main > img {
  filter: blur(5px) saturate(0.45) brightness(0.6);
  transform: scale(1.06);  /* Blur softens the edges; the scale hides them. */
}

/* A third background layer, on its own element because .video-wrapper's
   two pseudo-elements are both spent. It is aria-hidden and carries no
   content, so there is nothing to lose if it never renders. */
.hero-main .hero-fx {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}

/* Graph paper. The mask fades the lines out well before they reach the
   text or the frame's rounded corners, so it reads as depth rather than
   as a grid sitting on top of the page. */
.hero-main .hero-fx::before {
  content: "";
  position: absolute;
  inset: -2.5rem;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.055) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
  background-size: 2.5rem 2.5rem;
  -webkit-mask-image: radial-gradient(115% 85% at 50% 42%, #000 0%, transparent 70%);
  mask-image: radial-gradient(115% 85% at 50% 42%, #000 0%, transparent 70%);
}

/* A slow light sweep. Wide and weak on purpose: it should read as the
   surface catching light, not as a banner animation. */
.hero-main .hero-fx::after {
  content: "";
  position: absolute;
  inset-block: -30%;
  inset-inline-start: -55%;
  width: 50%;
  background: linear-gradient(100deg,
    transparent,
    rgba(63, 208, 205, 0.13) 45%,
    rgba(255, 255, 255, 0.05) 56%,
    transparent);
  transform: rotate(9deg);
  opacity: 0;
}

/* Light enters from the side the reader starts on, so the whole thing
   mirrors. Spelled out as a second set of keyframes rather than driven
   by a custom property: a var that fails to resolve inside a keyframe
   invalidates the whole transform, which would park this band off-screen
   and leave no symptom to chase. */
[dir="rtl"] .hero-main .hero-fx::after {
  transform: rotate(-9deg);
}

@media (prefers-reduced-motion: no-preference) {
  .hero-main .hero-fx::before {
    animation: zt-hero-grid 20s linear infinite;
  }

  .hero-main .hero-fx::after {
    animation: zt-hero-sweep 14s ease-in-out infinite;
  }

  [dir="rtl"] .hero-main .hero-fx::after {
    animation-name: zt-hero-sweep-rtl;
  }
}

/* Exactly one cell of travel, so the loop closes with no visible seam. */
@keyframes zt-hero-grid {
  to { transform: translate3d(2.5rem, 2.5rem, 0); }
}

@keyframes zt-hero-sweep {
  0%, 10%  { transform: translateX(0) rotate(9deg); opacity: 0; }
  30%, 70% { opacity: 1; }
  100%     { transform: translateX(300%) rotate(9deg); opacity: 0; }
}

@keyframes zt-hero-sweep-rtl {
  0%, 10%  { transform: translateX(0) rotate(-9deg); opacity: 0; }
  30%, 70% { opacity: 1; }
  100%     { transform: translateX(-300%) rotate(-9deg); opacity: 0; }
}

/* -- Eyebrow -------------------------------------------------------- */

.hero-main .hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 1rem;
  padding: 0.36rem 0.85rem;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 2rem;
  background: rgba(255, 255, 255, 0.06);
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.72);
}

/* Persian has no case, and fa.css neutralises letter-spacing across the
   tree because tracking breaks the joins between Arabic-script letters. */
[dir="rtl"] .hero-main .hero-eyebrow {
  letter-spacing: 0;
  text-transform: none;
  font-size: 0.8125rem;
}

.hero-main .hero-eyebrow::before {
  content: "";
  flex: none;
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--zt-teal-light);
  box-shadow: 0 0 0 0 rgba(63, 208, 205, 0.55);
}

@media (prefers-reduced-motion: no-preference) {
  .hero-main .hero-eyebrow::before {
    animation: zt-hero-pulse 2.6s ease-out infinite;
  }
}

@keyframes zt-hero-pulse {
  70%  { box-shadow: 0 0 0 0.5rem rgba(63, 208, 205, 0); }
  100% { box-shadow: 0 0 0 0 rgba(63, 208, 205, 0); }
}

/* -- Title and subtitle --------------------------------------------- */

/* The upper bound is what needed cutting, not the lower one. 5.2vw with a
   4rem ceiling meant the heading grew with the window until it was 64px
   on a laptop — display type on a page whose own h2 elements are 40px. */
.hero-main .hero-title {
  margin: 0 0 0.9rem;
  font-size: clamp(1.7rem, 3.4vw, 2.85rem);
  font-weight: 700;
  line-height: 1.16;
  letter-spacing: -0.015em;
  color: #fff;
  text-wrap: balance;
}

[dir="rtl"] .hero-main .hero-title {
  letter-spacing: 0;
  line-height: 1.45;  /* Arabic script sits taller; 1.16 collides. */
  font-size: clamp(1.5rem, 3vw, 2.5rem);
}

.hero-main .hero-title .hero-accent {
  color: var(--zt-teal-light);
}

.hero-main .hero-sub {
  max-width: 36rem;  /* rem, not ch: the ch unit is unreliable across scripts. */
  margin: 0 auto 1.75rem;
  font-size: clamp(0.9375rem, 1.05vw, 1.0625rem);
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.74);
  text-wrap: pretty;
}

[dir="rtl"] .hero-main .hero-sub {
  max-width: 38rem;
  line-height: 2;  /* fa.css sets 2 for body text; the hero should match. */
}

/* -- Calls to action ------------------------------------------------- */

.hero-main .hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem 1.15rem;
}

/* No `transform` here, on purpose. The theme lifts every button on hover
   at style.css:27027 with the same (0,3,0) weight this selector carries,
   so declaring transform would win the tie and cancel both that lift and
   the magnetic offset from section 7. */
.hero-main .btn-brand {
  padding: 0.68rem 1.55rem;
  border: 1px solid var(--zt-teal);
  background-color: var(--zt-teal);
  color: #fff;
  font-size: 0.9375rem;
  font-weight: 600;
  box-shadow: 0 0.6rem 1.5rem rgba(2, 167, 164, 0.28);
}

.hero-main .btn-brand:hover,
.hero-main .btn-brand:focus-visible {
  border-color: #019492;
  background-color: #019492;
  color: #fff;
  box-shadow: 0 1rem 2.25rem rgba(2, 167, 164, 0.42);
}

.hero-main .hero-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.15rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
}

.hero-main .hero-link:hover,
.hero-main .hero-link:focus-visible {
  border-bottom-color: var(--zt-teal-light);
  color: var(--zt-teal-light);
}

.hero-main .hero-arrow {
  display: inline-block;
  transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.25, 1);
}

/* The glyph itself is written per language in the template, U+2192 in
   the English hero and U+2190 in the Persian one, so only the travel
   direction needs flipping here. */
.hero-main .hero-link:hover .hero-arrow,
.hero-main .hero-link:focus-visible .hero-arrow {
  transform: translateX(0.3rem);
}

[dir="rtl"] .hero-main .hero-link:hover .hero-arrow,
[dir="rtl"] .hero-main .hero-link:focus-visible .hero-arrow {
  transform: translateX(-0.3rem);
}

/* -- Proof bar ------------------------------------------------------- */

.hero-main .hero-proof {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.5rem 2.1rem;
  max-width: 34rem;
  margin: 1.9rem auto 0;
  padding: 1.1rem 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.13);
  list-style: none;
}

.hero-main .hero-proof li {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: 0.8125rem;
  color: rgba(255, 255, 255, 0.62);
}

.hero-main .hero-proof b {
  font-size: 1.05rem;
  font-weight: 700;
  color: #fff;
  font-variant-numeric: lining-nums tabular-nums;
}

/* -- Entrance -------------------------------------------------------- */

/* Guarded once, and that is enough. A visitor who asked for less motion
   gets the finished hero with no animation at all, which matters here
   more than elsewhere: `both` with a `from` of opacity 0 would otherwise
   leave the headline invisible for them. Plain keyframe animations are
   supported by every browser this site runs in, so there is no third
   state in which the content stays hidden. */
@media (prefers-reduced-motion: no-preference) {
  .hero-main .hero-eyebrow,
  .hero-main .hero-title,
  .hero-main .hero-sub,
  .hero-main .hero-actions,
  .hero-main .hero-proof {
    animation: zt-hero-rise 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
  }

  .hero-main .hero-title   { animation-delay: 0.09s; }
  .hero-main .hero-sub     { animation-delay: 0.18s; }
  .hero-main .hero-actions { animation-delay: 0.27s; }
  .hero-main .hero-proof   { animation-delay: 0.36s; }
}

@keyframes zt-hero-rise {
  from { opacity: 0; transform: translateY(1.1rem); }
  to   { opacity: 1; transform: none; }
}

/* -- Scroll cue ------------------------------------------------------ */

/* Positioned against .video-content's bottom edge, which is why that
   element carries 4.25rem of bottom padding — the cue has to sit clear
   of the proof bar without leaving the section. */
.hero-main .hero-cue {
  position: absolute;
  inset-block-end: 1.25rem;
  inset-inline: 0;
  width: 1.25rem;
  margin-inline: auto;
  color: rgba(255, 255, 255, 0.42);
}

.hero-main .hero-cue::before {
  content: "";
  display: block;
  width: 0.55rem;
  height: 0.55rem;
  margin: 0 auto;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}

@media (prefers-reduced-motion: no-preference) {
  .hero-main .hero-cue {
    animation: zt-hero-cue 2.2s ease-in-out infinite;
  }
}

@keyframes zt-hero-cue {
  0%, 100% { transform: translateY(0);      opacity: 0.42; }
  50%      { transform: translateY(0.35rem); opacity: 0.85; }
}

/* -- Narrow ---------------------------------------------------------- */

@media (max-width: 991.98px) {
  .hero-main .video-content {
    /* The navbar collapses to a bar with a burger, so less to clear. */
    padding-block: 5.5rem 3.5rem;
  }
}

@media (max-width: 575.98px) {
  .hero-main .hero-cue { display: none; }
  .hero-main .video-content { padding-block: 5rem 2.5rem; }
  .hero-main .hero-proof { gap: 0.4rem 1.4rem; margin-top: 1.5rem; }
  .hero-main .hero-actions { gap: 0.5rem 0.9rem; }
}

/* --------------------------------------------------------------------
   9. Hero code stream (.hero-code)

   Replaces the hero photograph with source code being typed, on both
   home pages. motion.js holds the typing; this section holds everything
   it looks like.

   The photograph it replaces is a blurred stock shot of a code editor:
   a picture of the thing, bought, static, and carrying no information
   once blurred. This is the thing itself, and it moves.

   One block of code, full width. The hero is about fourteen hundred
   pixels wide, so the pane fits ~142 monospace characters at the size
   set below; the source in motion.js is written to reach ~137 at its
   longest. That pairing is the whole of why this does not read as thin.
   Getting there by splitting the pane into two narrow columns was tried
   and looked like two files pasted side by side — a seam down the middle
   of a background is a thing the eye finds, and finding it is the
   opposite of what a background is for. So: one column, long lines.

   The two settings that hold it are the font-size clamp and the
   horizontal padding, and they work against each other. Lower the font
   and each line covers less of the width; raise the padding and there is
   less width to cover. Either one, moved far, reopens the empty band on
   the right that this arrangement exists to close.

   Blurred, but only as far as it takes to stop the code competing with
   the headline. At 4px word shapes survive and words do not, which is
   the whole of what this layer is for: legible code behind a headline
   is two things asking to be read at once, and the headline loses. What
   should come through is the shape of code — ragged line lengths, a dim
   gutter, indentation steps, coloured runs, and a caret travelling left
   to right.

   Cost is bounded on purpose, because this paints roughly thirty times
   a second directly under the fold:

     - The blur sits on .hero-code, which is inset: 0 and therefore a
       fixed area, rather than on the stream inside it, which grows as
       lines arrive. Blur cost scales with area.
     - contain: layout paint keeps the repaint inside the pane instead
       of letting it invalidate the section around it.
     - motion.js stops the loop when the tab is hidden or the hero
       scrolls out of view, and never starts it below 768px or under
       prefers-reduced-motion — in those cases the pane is filled once
       and left alone. The result still looks right; it just holds
       still.

   Direction is pinned to ltr and the gutter uses physical properties,
   both against the grain of the rest of this file. Code is left to
   right in every language. On /fa the page is rtl around this pane and
   the pane must not follow it: a right-aligned gutter with lines
   running the other way is not what a Persian developer's editor looks
   like either.
   -------------------------------------------------------------------- */

.hero-code {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  contain: layout paint;

  /* flex-end stands the text on the floor of the pane, so each new line
     arrives at a fixed baseline and pushes the older ones up and out
     through the top — what a terminal does, and what an editor does with
     the caret on the last line. Growing down from the top would move the
     caret, and the caret is the part the eye follows. */
  display: flex;
  align-items: flex-end;

  /* The side padding is half of the width budget — see the note above
     before widening it. The bottom padding lifts the line being typed
     clear of the section edge; without it the caret rides the boundary
     between the hero and whatever follows it, which reads as an overflow
     rather than as a background. */
  padding: 0 clamp(1rem, 3.5vw, 4.5rem) clamp(1.5rem, 5vh, 4rem);

  /* Never inherited from <html dir="rtl">. See the note above. */
  direction: ltr;
  text-align: left;

  filter: blur(4px);

  /* Fades in from under the navbar, which overlays the top of the hero
     and would otherwise have a hard line of code sliding under it. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 20%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 20%, #000 100%);
}

.hero-code-stream {
  flex: 1 1 0;

  /* A pre line is as wide as its longest line, and without this that
     width becomes the flex base — the block would then be sized by its
     one longest line and stop filling the pane on narrow viewports. */
  min-width: 0;

  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", "Courier New", monospace;

  /* The other half of the width budget. A vw unit rather than a fixed
     size keeps the characters-per-line roughly constant as the viewport
     changes, which is the number that matters here; 1.05vw puts the
     longest source line at ~96% of the pane from 1280 to 1600px. The
     19px cap is what it takes to keep that ratio at 1920 — a lower one
     reopens the empty band on the right at exactly the width most
     desktops are. The 13px floor is legibility as texture: below it the
     blur turns the lines into a grey wash with no code in it. */
  font-size: clamp(0.8125rem, 1.05vw, 1.1875rem);
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.82);
  opacity: 0.78;

  /* Narrow, because lines are meant to reach both edges now: enough to
     stop a clipped line ending on a cut, not enough to eat the last
     words of the long ones. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 3%, #000 97%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, #000 3%, #000 97%, transparent 100%);
}

/* pre, not pre-wrap: indentation is half of what makes this read as
   code, and a wrapped line would reflow every line below it mid-type. A
   line longer than the pane is clipped, which is also what an editor
   without soft wrap does. */
.hero-code-line {
  white-space: pre;
  overflow: hidden;
}

/* The gutter. Two digits wide and cycling with the source, so it never
   grows a third digit and shifts the column. */
.hero-code-line::before {
  content: attr(data-n);
  display: inline-block;
  width: 1.75rem;
  margin-right: 1rem;
  text-align: right;
  color: rgba(255, 255, 255, 0.24);
}

/* Two hues and three greys. Keywords teal, literals and type names gold
   — the two colours the rest of the hero already uses — and everything
   else a neutral. A full editor palette would put five unrelated hues
   behind a headline that has exactly one accent. */
.hero-code-line .t-key { color: #55d3d0; }
.hero-code-line .t-cls,
.hero-code-line .t-str,
.hero-code-line .t-num { color: #dcb268; }
.hero-code-line .t-fn  { color: rgba(255, 255, 255, 0.96); }
.hero-code-line .t-var { color: rgba(255, 255, 255, 0.86); }
.hero-code-line .t-pun { color: rgba(255, 255, 255, 0.5); }
.hero-code-line .t-com { color: rgba(255, 255, 255, 0.34); }

/* The one part meant to survive the blur. Everything else dissolves into
   texture; this stays a discrete point of teal light moving along a
   line, and it is what makes the pane read as being written rather than
   as a still of some code. Hence the glow — a flat block would blur into
   the line it sits on. */
.hero-code-caret {
  display: inline-block;
  width: 0.5rem;
  height: 1.05em;
  vertical-align: text-bottom;
  background: var(--zt-teal-light);
  box-shadow: 0 0 0.75rem rgba(63, 208, 205, 0.9);
}

@media (prefers-reduced-motion: no-preference) {
  .hero-code-caret {
    animation: zt-code-caret 1.05s steps(1, end) infinite;
  }
}

@keyframes zt-code-caret {
  50% { opacity: 0; }
}

/* -- The section around it ------------------------------------------- */

/* No photograph underneath, so the base colour is the only thing behind
   the code and it has to be the ink itself. */
.video-wrapper.hero-main.hero-code-bg {
  background-color: #070d11;
}

/* The scrim, which paints directly over the code, is what decides how
   much of it comes through. Over the photograph it was one slope, heavy
   at the bottom to bury a busy image; here that put its heaviest point
   exactly where the code is densest and where the carets sit. So it is
   a band instead: lightest at the top and bottom, heaviest across the
   middle, which is the only part of the pane the headline occupies. */
.video-wrapper.hero-main.hero-code-bg.bg-overlay::after {
  background:
    linear-gradient(180deg,
      rgba(7, 13, 17, 0.34) 0%,
      rgba(7, 13, 17, 0.7) 40%,
      rgba(7, 13, 17, 0.7) 72%,
      rgba(7, 13, 17, 0.52) 100%),
    linear-gradient(115deg, rgba(2, 167, 164, 0.3) 0%, rgba(7, 13, 17, 0) 58%);
}

[dir="rtl"] .video-wrapper.hero-main.hero-code-bg.bg-overlay::after {
  background:
    linear-gradient(180deg,
      rgba(7, 13, 17, 0.34) 0%,
      rgba(7, 13, 17, 0.7) 40%,
      rgba(7, 13, 17, 0.7) 72%,
      rgba(7, 13, 17, 0.52) 100%),
    linear-gradient(245deg, rgba(2, 167, 164, 0.3) 0%, rgba(7, 13, 17, 0) 58%);
}

/* The graph paper stays — it is the layer the hero was liked for, and
   dropping it because the code arrived gave up more than it saved. What
   it does cost is that grid and code are both fine light ruling at a
   similar weight, so over this variant the lines come down by about a
   third and the two read as two depths rather than as one ruling the
   other.

   Only the colour changes. The cell size is what zt-hero-grid travels
   in one iteration, so a different cell here would open a seam in the
   loop. */
.hero-code-bg .hero-fx::before {
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.038) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.038) 1px, transparent 1px);
}

/* Below 992px the lines are longer than the pane and most of them clip.
   That is the intended reading rather than a failure: an editor without
   soft wrap does the same, and the alternative — shrinking the text
   until 137 characters fit on a phone — is a grey wash. The horizontal
   fade above is what makes a clipped line end in something other than a
   cut. */

/* Narrow: fewer, larger lines. The gutter is the first thing to go —
   at this width it costs a fifth of the line and reads as a grey stripe
   rather than as line numbers. */
@media (max-width: 575.98px) {
  .hero-code { padding-inline: 0.75rem; }
  .hero-code-stream { line-height: 1.8; }
  .hero-code-line::before { display: none; }
}

/* --------------------------------------------------------------------
   10. Footer (.footer-fx)

   The footer this decorates is `background: #21262c !important` and
   nothing else (style.css:38994). Flat, and on a page that opens with a
   hero carrying a drifting grid, an aurora, film grain and code being
   typed, that flatness reads as the site giving up two thirds of the way
   down rather than as restraint.

   WHERE IT APPLIES. Everywhere. .footer-fx is on
   components/{app-partials,fa}/footer.blade.php, which the two site
   layouts and the two home templates render, so this section decorates
   all 86 English pages and the whole Persian tree. That is a change from
   when it was written: it lived behind $preview on two staging URLs
   until it was approved. Anything below that would only hold for the
   home page has to be checked against the other 84 — which is what the
   note on the corner radius immediately below is about.

   SQUARE AND ROUNDED, BOTH. .page-frame gives `border-radius: 1rem` to
   sections and the footer at >=768px (style.css:22849), and it is on
   exactly two view files — layout/home.blade.php and
   fa/templates/home.blade.php. Every other page's footer is square and
   full-bleed. So nothing below may assume a corner radius: `overflow:
   hidden` on the footer clips each layer to whatever radius the footer
   happens to have, 1rem or 0, and that is the entire adaptation.

   THE LAYERS:

     footer::before             the top hairline
     footer::after              the square grid
     .footer-fx-layer::before   the corner glow
     .footer-mark::before       the wordmark

   An element has two pseudo-elements, which is the only reason the two
   decoration spans exist in the markup at all: four layers do not fit on
   footer::before and footer::after alone.

   There used to be more. An animated hourglass sat on a third span,
   .footer-sand, and was removed on request. If something like it comes
   back, note what the four above have in common and it did not: every
   one of them is painted once and then costs nothing.

   HEIGHT. 657px at 1440px wide when this was first written, which the
   footer had not earned: 200 of those were py-13/py-md-15 padding and
   144 more were the CTA band's. It is ~527px now, 20% less, from six
   cuts and no lost content — CTA padding 5vw -> 3.6vw (-40), the widget
   container py-13 py-md-15 -> py-12 py-md-13 (-40), the forced <br> out
   of the copyright line (-24), mb-4 -> mb-3 three times (-15), social
   circles 2.5rem -> 2.1rem (-8), logo 160 -> 150 (-3). Remember the root
   font-size here is 20px (--bs-root-font-size, style.css:180), so every
   rem below is worth a quarter more than it looks.

   ALL OF THEM SIT AT z-index: -1, under `isolation: isolate` on the
   footer. Inside a stacking context, negative-z children paint after the
   context root's own background and before its in-flow content — so the
   layers land between #21262c and the text with no z-index needed on any
   container, and the isolate keeps that local instead of letting -1
   escape under the page.

   ONE THING MOVES, AND ONLY UNDER A CURSOR. No rule below animates by
   itself, and there is no prefers-reduced-motion block in this section
   because the footer's one motion is JS-driven: the follower in
   motion.js drifts the glow after the cursor while it is over the
   footer, slowly, and leaves it wherever the cursor left. That module
   gates itself on (hover: hover) and (pointer: fine) and on
   prefers-reduced-motion, and it cancels its own frame loop the moment
   the glow settles — so a page left alone still costs nothing, which is
   the bargain every layer here was admitted under. The rule below only
   consumes two variables; with motion.js absent or declined they
   resolve to 0 0 and the glow is exactly the static picture it was.

   Everything else is the finished state, not an unfinished one. Every
   remaining layer has an obvious animation available — the grid could
   drift, the hairline could sweep — and a footer with four things
   moving in it is not a finished footer, it is a screen saver. Unlike
   the hero, this is on all 86 pages and sits at the bottom of every one
   of them, where a permanent compositor job buys nothing and costs a
   repaint for as long as the tab is open.

   DIRECTION. No [dir="rtl"] rule below, on purpose. The two layers that
   are not symmetric — the glow and the wordmark — are placed with
   inset-inline-start/-end, which resolve per direction on their own. The
   hairline gradient is a palindrome. The grid is a grid.
   -------------------------------------------------------------------- */

/* `clip` after `hidden`, and both on purpose. They clip identically, but
   overflow: hidden makes the footer a scroll container and overflow: clip
   does not — and a scroll container is what animation-timeline: view()
   resolves against. Put [data-reveal] from section 2 on a footer widget
   under `hidden` and its timeline would be a box that never scrolls, so
   the animation would sit at progress 0 forever and the widget would
   stay at the opacity 0 its keyframes start from. That is the exact
   failure this file's rule 2 exists to prevent, arriving through the
   back door.

   Declared as a pair rather than as clip alone because a browser that
   does not know `clip` drops that line and keeps `hidden` — clipping
   still happens; it just brings the scroll container with it. Nothing in
   the footer uses view() today, so on those browsers this costs nothing
   at all. */
footer.footer-fx {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  overflow: clip;
}

/* The seam between the page and the footer. The theme draws no line here
   at all, and one at full strength would read as a rule; faded to
   nothing at both ends it reads as light catching an edge. Symmetric, so
   it needs no RTL companion. */
footer.footer-fx::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 1px;
  z-index: -1;
  background: linear-gradient(to right,
    rgba(2, 167, 164, 0) 0%,
    rgba(2, 167, 164, 0.55) 50%,
    rgba(2, 167, 164, 0) 100%);
}

/* The hero's graph paper, carried down. Same 2.5rem cell, so the two
   read as one ruling the page is drawn on rather than as two different
   textures — which is the whole argument for putting a grid here at all.
   The cells will not line up with the hero's, since each is anchored to
   its own box and a whole page separates them; at this weight that is
   not something the eye can check.

   Fading downward, and gone by 85%, because the bottom of the footer
   belongs to the wordmark and two textures at once there would make mud.

   Static, unlike the hero's. See the note above on what moves. */
footer.footer-fx::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.042) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.042) 1px, transparent 1px);
  background-size: 2.5rem 2.5rem;
  -webkit-mask-image: linear-gradient(to bottom,
    #000 0%, rgba(0, 0, 0, 0.4) 48%, transparent 85%);
  mask-image: linear-gradient(to bottom,
    #000 0%, rgba(0, 0, 0, 0.4) 48%, transparent 85%);
}

/* The extra pair of pseudo-elements. Nothing of its own. */
.footer-fx-layer {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

/* Teal light in the logo corner, the same colour the hero's aurora and
   the CTA button are.

   Sized to put its bright middle roughly on the logo rather than in the
   corner: 30x24rem is 600x480px at this site's 20px root, offset so the
   centre lands around (120px, 280px) from the corner — which is where
   the logo sits once the CTA band above it is accounted for. A glow
   centred on a corner is a vignette; centred on something, it is light.

   inset-inline-start, so /fa gets it over the corner the Persian logo
   actually sits in without a second rule. */
.footer-fx-layer::before {
  content: "";
  position: absolute;
  inset-block-start: 2rem;
  inset-inline-start: -9rem;
  width: 30rem;
  height: 24rem;
  background: radial-gradient(50% 50% at 50% 50%,
    rgba(2, 167, 164, 0.2) 0%,
    rgba(2, 167, 164, 0.08) 42%,
    rgba(2, 167, 164, 0) 72%);
  /* Written by the glow follower in motion.js: 0 0 until a cursor
     visits the footer, then the offset that puts the glow's centre
     under the cursor's last position. No transition on purpose — the
     easing lives in the follower's own frame loop, and a transition
     here would smooth it twice. */
  transform: translate3d(var(--zt-glow-x, 0px), var(--zt-glow-y, 0px), 0);
}

/* The brand, set enormous and cut off by the bottom edge. Two things
   make it work rather than one: the size, which takes it past reading
   into shape, and the cut, which says it is a surface the footer is
   standing on rather than a word someone forgot to delete.

   At 4.5% white on #21262c the contrast with the background is about 11
   of 255 — a form you notice after the content, which is the correct
   order for it to be noticed in.

   The em on inset-block-end resolves against the clamp above it, so the
   slice stays proportional at every width — but how big that slice is
   depends entirely on the font, which is why font-family is pinned here
   and is not inherited.

   Where the cut lands is arithmetic. With line-height: 1 the baseline
   sits at (1 - (ascent + descent)) / 2 + ascent from the top of the box,
   and inset-block-end: -0.32em puts the footer's bottom edge at 0.68em
   from that same top. Manrope's ascent is 1.066em and its descent
   0.300em, so its baseline is at 0.883em — 0.203em below the cut, which
   against a cap height of 0.720em is 28.2% of every letter gone. That is
   the intended look and it is a large number, not a rounding error.

   Inherit the family instead and the number changes completely. Ravi
   FaNum, which /fa sets on :root, has ascent 0.900 and descent 0.650:
   baseline at 0.625em, which is above the cut, so the Persian ZAMAN was
   rendering whole with clear air underneath — a word parked at the
   bottom of the footer rather than a mark running off the edge.

   Tuning a second inset for RTL would not have fixed it either. Ravi
   FaNum's hhea ascent (0.900) and OS/2 usWinAscent (1.000) disagree, and
   browsers choose between those two by platform, so any value tuned on
   one would be off by ~0.05em — 12px at the 240px top of the clamp — on
   another. Manrope's hhea, OS/2 win and OS/2 typo metrics all agree, so
   pinning it is the only version of this that renders the same
   everywhere. It also drops the faux-bold: Ravi FaNum ships 400 and 300
   only, so font-weight: 800 was being synthesised.

   The cost is one @font-face on the Persian side, in fa.css, because
   style.rtl.css has the Manrope faces stripped out of it. It is the
   latin subset that is already shipped and already cached on the English
   tree, and if this file is ever deleted the face goes unmatched and is
   never fetched — so the promise at the top of this file still holds.

   The word arrives through content: attr(data-mark) rather than sitting
   between the tags. Two reasons, one of them load-bearing: this file
   states at the top that deleting it restores the site exactly, and a
   wordmark written the obvious way would survive that deletion as the
   literal word ZAMAN in body text at the bottom of the footer. An empty
   span survives it as nothing. The other is that attr() keeps the word
   in the Blade components, where somebody editing the footer can find
   it, instead of hardcoding brand text in a stylesheet.

   Both locales pass ZAMAN. The Persian footer does not translate this
   one string, and the reason is in the comment there: at 200px and 4.5%
   this is a shape, cut off by the bottom edge, and Latin capitals have
   the flat cap line and flat baseline that a cut like that needs. There
   is no equivalent in a connected script with descenders and dots. */
.footer-mark {
  position: absolute;
  inset-block-end: -0.32em;
  inset-inline-start: clamp(0.75rem, 4vw, 5rem);
  z-index: -1;
  font-family: "Manrope", sans-serif;
  font-size: clamp(4rem, 13vw, 12rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.04em;
  color: rgba(255, 255, 255, 0.045);
  white-space: nowrap;
  user-select: none;
  -webkit-user-select: none;
}

.footer-mark::before {
  content: attr(data-mark);
}

/* -- The content the layers sit behind -------------------------------- */

/* The closing call to action. The footer is the last thing a visitor
   sees and the previous one asked them to do nothing, so the page ended
   on a list of links. This is a .container, so its divider stops where
   the columns stop instead of running the full bleed — a rule that lines
   up with the content reads as structure, one that does not reads as a
   border. 0.08 white is the divider the theme already uses on dark
   (.wrapper-border.bg-dark, style.css:38992). */
.footer-fx .footer-cta {
  padding-block: clamp(1.75rem, 3.6vw, 2.6rem);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.footer-cta-title {
  font-size: clamp(1.375rem, 2.4vw, 1.875rem);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: #fff;
}

.footer-cta-text {
  color: rgba(255, 255, 255, 0.6);
}

/* Social icons as filled circles on hover. The theme gives these a
   -0.15rem lift and a 0.2s transition already (style.css:28563), with
   its own reduced-motion guard, so this adds the shape and the colour
   and inherits the movement rather than restating it.

   The icon stays white because it has to: .social.social-white a i is
   `color: #fff !important` at style.css:29126. Which is the treatment
   that was wanted anyway — white on teal — so the fill goes on the
   anchor and the !important is left alone.

   :focus-visible matched alongside :hover. These are the only outbound
   links in the footer and a keyboard user should see the same thing a
   mouse user does. */
.footer-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background-color: rgba(255, 255, 255, 0.04);
}

.footer-social a:hover,
.footer-social a:focus-visible {
  border-color: var(--zt-teal);
  background-color: var(--zt-teal);
  box-shadow: 0 0.5rem 1.1rem rgba(2, 167, 164, 0.32);
}

/* -- Narrow ----------------------------------------------------------- */

/* One rule, and it is an inset, because that is all the narrow footer
   needs: nothing here is sized in pixels, nothing here moves, and the
   glow is a soft radial that does not care how wide it is.

   The wordmark's own inset is clamp(0.75rem, 4vw, 5rem), and 4vw is the
   wrong middle term below 768px. There the columns have stacked and the
   footer is roughly three times as tall, so the mark reads as a label
   under the last widget rather than a watermark behind the block — and
   4vw walks it inward, away from the edge that is cropping it, which is
   the one place it should not be. Pinning it to the floor of the clamp
   parks it on the container's own inline padding instead. */
@media (max-width: 767.98px) {
  .footer-mark { inset-inline-start: 0.75rem; }
}

/* --------------------------------------------------------------------
   11. Grainy glass — PREVIEW ONLY.

      The look: a translucent, blurred panel with a fine noise tile laid
      on top of it. Glassmorphism on its own reads slick and slightly
      cheap, because a blur is perfectly smooth and nothing real is. The
      grain is what breaks that up and makes the surface read as a
      material rather than as a filter. The noise is the point, not the
      blur.

      NO COLOURED FIELD BEHIND THE CARDS. The first version put soft
      teal and gold blobs behind both card groups — the reference image
      has them, and glass over a flat fill has nothing to refract. It
      was looked at on the previews and rejected: the bands read as a
      stain across two otherwise white sections rather than as depth.
      They are gone, and they should not come back without a new reason.

      What that costs is honest to state: over the flat sections these
      groups sit on, the blur has nothing to work with, so the panels
      read as a lit edge, a soft drop and the grain — the texture, not
      the refraction. Only the hero bar, over the aurora in section 4,
      is glass in the full sense. The Technologies band takes that to
      its conclusion and drops the translucency altogether, which is
      what lets it keep its own grey exactly; see its own note below.

      So the backdrop-filter on the two card groups currently buys
      nothing but costs a blurred backdrop per card per frame. It is
      kept because it starts working the moment a card sits on something
      that is not flat, which is the likely direction of travel. If that
      never happens, dropping those two declarations is free.

      WHERE IT LANDS. Four surfaces, and the restraint is deliberate —
      not every card on the page. The hero proof bar, dark glass over
      the aurora and the closest thing on this site to the reference;
      the nine Solutions cards; the four Success Numbers cards; and the
      Technologies band. Between them they cover the dark case, the
      white case and the mid-grey case, which is what a look has to
      survive.

      All four live on the two home pages and, as of this writing,
      nowhere else — checked in a browser rather than assumed. Nine
      industry pages carry `id="solutions"` and sixteen technology pages
      carry `.counter-wrapper`, but neither puts a `.card` inside, so
      none of them matches. That is why these rules are plain selectors
      and not scoped to anything.

      It follows that adding a `.card` inside `#solutions` or a
      `.counter-wrapper` on some other page silently opts that page into
      this treatment. That is the intended behaviour — the look should be
      the site's, not the home page's — but it is worth knowing before
      wondering where a frosted card came from.

      Reviewed behind $preview on /preview/home and /fa/preview before
      it was promoted here; the two staging routes are back to rendering
      exactly what the live pages render. To take the look off the site,
      delete this section: it is one contiguous block and touches no
      markup, which is also why there is no class to remove.

      The file's three rules hold. No external anything: the grain is
      the same kind of inline feTurbulence data: URI as section 5, at a
      higher frequency and on a smaller tile. Nothing is hidden: every
      rule that thins a fill sits inside an @supports for
      backdrop-filter, so a browser without it renders those cards
      exactly as the live site does today — and the one rule that thins
      nothing, the Technologies band, is deliberately outside that gate
      because it does not depend on the feature. And nothing added here
      moves, so there is nothing for reduced-motion to switch off.

      Direction: nothing here is asymmetric. With the coloured field
      gone — it was the one rule that had to follow the card column,
      which sits right in English and left in Persian — every remaining
      rule is a box, a border or a tile, and reads the same both ways.
      That is why this section has no [dir="rtl"] companion at all.
   -------------------------------------------------------------------- */
:root {
  /* One tile, three uses. 96px is small enough that the grain reads as
     texture rather than as a repeat, and baseFrequency 0.9 puts the
     speckle just under a pixel pair at 2x. */
  --zt-grain-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");

  /* The same turbulence with the speckle pulled apart: feComponentTransfer
     at slope 2, intercept -0.5 doubles contrast around mid grey. Used by
     the Technologies band, which is a wide empty surface and wanted more
     texture than the tile above gives at any opacity.

     WHY A SECOND TILE RATHER THAN `filter: contrast(2)` ON THE FIRST.
     The CSS filter was the obvious way to avoid duplicating 400 bytes,
     and it was tried and measured against this one on the real page.
     It is worse on both counts that matter: less grain (pixel spread 13
     against 21) and four times the colour drift (+4 against +1 on a
     #eaebec ground), because contrast() clips in sRGB after the tile is
     composited rather than shaping the noise before it. Boosting inside
     the SVG keeps the distribution centred, which is the whole reason
     the band can take this much grain without going pale. Do not
     "simplify" this back to a filter. */
  --zt-grain-tile-strong: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='2' intercept='-0.5'/%3E%3CfeFuncG type='linear' slope='2' intercept='-0.5'/%3E%3CfeFuncB type='linear' slope='2' intercept='-0.5'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");

  --zt-glass-blur: 18px;
  /* Pushes whatever is behind the panel past full saturation, so a
     half-opaque white fill lifts the colour under it instead of washing
     it to grey.

     Worth knowing that on the two sections these cards currently sit on
     this does nothing, and neither does the blur: both grounds are a
     flat fill, and blurring a flat fill returns the same flat fill. It
     is kept because it costs nothing to keep and starts working the
     moment a card sits on something that is not flat — which is most of
     the site the day this rolls out past the home page. If the
     treatment is ever promoted and the cards still land only on flat
     bands, drop the backdrop-filter from the two card rules: it is a
     real per-frame cost buying nothing. */
  --zt-glass-sat: 1.6;
}

/* A blurred backdrop is a real per-frame cost and a phone is where it
   is felt. The look survives a smaller radius; it does not survive a
   scrolling page that drops frames. */
@media (max-width: 767.98px) {
  :root {
    --zt-glass-blur: 10px;
  }
}

@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {

  /* -- The panels ----------------------------------------------------- */

  /* Two !importants, both forced by the theme and neither optional.
     .bg-soft-yellow is `background-color: #fff8ee !important`
     (style.css:24149) and .card[class*=bg-] is
     `box-shadow: none !important` (style.css:31464). The Solutions cards
     carry both classes, so a translucent fill and a lit edge cannot be
     declared on weight alone. The Success Numbers cards need neither;
     they share the rule because the material is the same.

     The box-shadow is three effects at once: the lit top edge every
     real pane of glass has, a hairline contact shadow, and a wide soft
     drop that lifts the panel off the bloom. */
  #solutions .card,
  .counter-wrapper .card {
    position: relative;
    isolation: isolate;
    border: 1px solid rgba(255, 255, 255, 0.62);
    border-radius: 0.9rem;
    background-color: rgba(255, 255, 255, 0.46) !important;
    -webkit-backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
    backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.70),
      0 1px 2px rgba(8, 60, 130, 0.05),
      0 14px 30px -14px rgba(8, 60, 130, 0.22) !important;
  }

  /* The hero bar: the same material read for a dark ground. The fill is
     a wash of white rather than a sheet of it, and what it blurs is the
     aurora from section 4 — the one place on this site that already
     looks like the reference.

     Section 8 gives this element a `border-top` and `padding: 1.1rem 0
     0`, a rule rather than a box. Both are restated here instead of
     patched, so the box is defined in one place. */
  .hero-main .hero-proof {
    position: relative;
    isolation: isolate;
    max-width: 36rem;
    padding: 1.1rem 1.6rem;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 1.1rem;
    background-color: rgba(255, 255, 255, 0.07);
    -webkit-backdrop-filter: blur(var(--zt-glass-blur)) saturate(1.3);
    backdrop-filter: blur(var(--zt-glass-blur)) saturate(1.3);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.18),
      0 20px 44px -22px rgba(0, 0, 0, 0.75);
  }

  /* -- The grain ------------------------------------------------------ */

  /* What separates this from ordinary glassmorphism. It goes ON the
     panel, clipped to its corners, over the text as well — that is how
     the reference reads, and at these opacities it costs nothing in
     legibility.

     The blend mode changes with the ground and the opacity follows it.
     `overlay` on a light panel darkens and lightens around a mid grey,
     which is the neutral speckle wanted there; on the dark hero the
     same mode blows the light half of the noise out to near white, so
     that one takes `soft-light`, which stays gentle over dark and needs
     more opacity to register at all.

     The `isolation: isolate` on each panel above is what keeps these
     blends from reaching past their own card into the page behind. */
  #solutions .card::after,
  .counter-wrapper .card::after,
  .hero-main .hero-proof::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    background-image: var(--zt-grain-tile);
    background-size: 96px 96px;
  }

  #solutions .card::after,
  .counter-wrapper .card::after {
    opacity: 0.20;
    mix-blend-mode: overlay;
  }

  .hero-main .hero-proof::after {
    opacity: 0.32;
    mix-blend-mode: soft-light;
  }
}

/* -- The Technologies band -------------------------------------------- */

/* The fourth surface, and the one that keeps its own colour.

   OUTSIDE THE @SUPPORTS ABOVE, ON PURPOSE. Nothing here is translucent,
   so nothing here needs backdrop-filter — and gating it on a feature it
   does not use would switch it off in browsers that could render it
   perfectly well.

   The grey is the point: .bg-soft-dark is `#eaebec !important`
   (style.css:23444), and the ask was to keep it. So this rule never
   mentions background-color at all. Thinning the fill would have been
   the obvious way to make the band read as glass, and it is the wrong
   one: the band sits on a near-white section, so any alpha below 1
   lightens it — at 0.55 the composite lands near #f3f3f3, visibly paler
   than the grey it is meant to preserve. Compensating by darkening the
   base colour works only until something behind it changes, at which
   point the grey silently drifts. Leaving the fill alone cannot drift.

   What is left is the finish, which since the coloured fields came out
   is what the treatment actually is: a lit top edge, a soft drop, and
   the grain. No radius either — the theme already rounds this band to
   20px and the grain inherits it.

   Both trees carry `id="technologies"` on this section and nothing else
   on the site does; it is `d-none d-lg-block`, so none of this reaches a
   phone. The section owns neither pseudo-element already — checked, not
   assumed. */
#technologies {
  position: relative;
  isolation: isolate;
  border: 1px solid rgba(255, 255, 255, 0.55);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    0 1px 2px rgba(8, 60, 130, 0.04),
    0 22px 48px -22px rgba(8, 60, 130, 0.20);
}

/* Same blend as the light cards, but the high-contrast tile and a lot
   more of it. The band is a wide, mostly empty surface — the one place
   on the page where a flat fill reads as flat — so it is where the grain
   has the most to do and the most room to do it in.

   The numbers behind 0.65, measured on the rendered page over an empty
   stretch of the band: pixel spread goes 6 -> 21, which is the grain
   becoming something you can see rather than something you can only
   find; mean colour moves from #eaebec to #ebeced, one step in one
   channel. That second number is the constraint. The brief on this band
   was to keep its grey, and `overlay` on a ground this light only ever
   pushes lighter, so strength here is bounded by drift, not by taste —
   the plain tile at full opacity drifts +3 and still shows less grain
   than this does. */
#technologies::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0.65;
  mix-blend-mode: overlay;
  background-image: var(--zt-grain-tile-strong);
  background-size: 96px 96px;
}

/* Paper has a grain of its own. */
@media print {
  #solutions .card::after,
  .counter-wrapper .card::after,
  .hero-main .hero-proof::after,
  #technologies::after {
    display: none;
  }
}

/* A forced-colours user has asked the browser to throw the palette out.
   Translucency and noise are the two things that survive that worst. */
@media (forced-colors: active) {
  #solutions .card::after,
  .counter-wrapper .card::after,
  .hero-main .hero-proof::after,
  #technologies::after {
    display: none;
  }

  #solutions .card,
  .counter-wrapper .card,
  .hero-main .hero-proof {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* --------------------------------------------------------------------
   12. Grainy glass, service pages.

      The same material as section 11, applied to the one page shape the
      site has 39 copies of. Everything it needs — the two grain tiles,
      the blur and saturation amounts — is already declared on :root up
      there; this section adds surfaces, not vocabulary. If section 11 is
      ever deleted, this one has to go with it or it loses its variables.

      THE SCOPE IS LOAD-BEARING ON THE CARDS AND ABSENT ON THE BAND,
      and the split is the thing to understand before editing here.

        * The card and bloom rules are scoped and must stay scoped.
          `.card.shadow-lg` and `#deliverables` are also on the nine
          purpose pages and, for the card, the sixteen technology pages.
          Unscope those and they land on pages nobody designed them for.
          Section 11 could drop its scope on promotion; these cannot.
        * The title band rules carry no page class at all, on purpose.
          Every `<section class="wrapper bg-soft-primary">` in the
          codebase is a page title band — eleven of them, four branches
          of layout/main-layout and seven Persian templates — so there
          is no page type to exclude and nothing a scope would buy. That
          treatment belongs to the site, not to one page family, and a
          twelfth band added later inherits it without an edit here.

      The class comes from the page's own type rather than from this
      treatment: layout/main-layout writes `page-{$type}` and fa/layout
      writes the family from the page key, so a service page is
      `page-service` in both trees whether or not any CSS cares. It is a
      description of the page, not a switch for a stylesheet, which is
      why it is safe for other sections to start using it.

      WHY ONE SAMPLE WAS ENOUGH TO REVIEW IT. All 39 English service
      pages carry the same five section ids and the same ten
      `.card.shadow-lg` — one summary card above the fold and nine
      deliverables below — and the Persian side renders every service
      through a single template. So the treatment is expressed against
      structure the pages share, and reviewing it on Website Development
      reviewed it on all of them. The two preview routes still exist and
      now render exactly what the live pages render.

      THE GREY BAND IS NOT A PANEL. On an inner page the block behind
      the title is two elements, not one: <header class="wrapper
      bg-soft-primary"> and the <section> under it, both #f3f3f3, reading
      as a single field. Giving either a lit edge or a drop shadow draws
      a line through the middle of that field. So the band gets grain and
      nothing else — no border, no shadow, no radius, and its colour
      untouched, the same decision the Technologies band came to for the
      same reason.

      The `section` in that selector is doing the other half of the job:
      it excludes the <header>, whose grain layer would otherwise sit
      over the open navigation dropdowns.
   -------------------------------------------------------------------- */

/* -- The cards ------------------------------------------------------- */

/* The box every card on the page gets: radius, lit edge, soft drop. The
   fill is not set here — it is added further down, inside the section's
   one @supports, and only once each card has something behind it worth
   refracting. Order of events, because it explains the shape of the
   rest of this section:

     1. All ten cards got the section 11 recipe verbatim, fill included.
        On flat white it looked exactly like an opaque fill, as it must:
        blurring a flat colour returns that colour.
     2. The summary card broke. It is pulled up to straddle the line
        where the grey band meets the white body, and a translucent fill
        showed that line straight through the middle of it — grey above,
        white below, indistinguishable from a rendering fault.
     3. So the fill came off everywhere, leaving finish and grain.
     4. Then a bloom went in behind the deliverables grid, and the grey
        band's bottom edge was dissolved into the body. Both grounds
        stopped being flat, which is what earns the fill back — this
        time on grounds where the blur has something to do.

   The lesson worth keeping: translucency is not the look, it is a
   technique that pays off only over something worth seeing. Adding it
   to a flat ground costs a blurred backdrop per card per frame and
   returns nothing.

   `!important` on the shadow because .shadow-lg is
   `box-shadow: ... !important` (style.css:8656) and all ten cards carry
   it. Nothing else here needs one. */
:is(.page-service, .page-purpose, .page-tech) .card.shadow-lg {
  position: relative;
  isolation: isolate;
  border: 1px solid rgba(255, 255, 255, 0.62);
  border-radius: 0.9rem;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.70),
    0 1px 2px rgba(8, 60, 130, 0.05),
    0 14px 30px -14px rgba(8, 60, 130, 0.22) !important;
}

:is(.page-service, .page-purpose, .page-tech) .card.shadow-lg::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0.20;
  mix-blend-mode: overlay;
  background-image: var(--zt-grain-tile);
  background-size: 96px 96px;
}

/* -- The bloom behind the summary card -------------------------------- */

/* The lobe that lights the summary card, moved down here from the title
   band. `.blog.single` is the theme's own wrapper around that card and
   the only thing on the page that identifies it.

   TEAL HERE, GOLD BEHIND THE DELIVERABLES GRID — the two blooms are
   deliberately not the same and this is under review. The grid's lobe
   was teal first and was changed to gold precisely so a wash of the
   site's accent colour would not repeat down 39 pages; this one sits
   above the fold on a much smaller area, so it can carry the accent
   without that risk. If the split reads as indecision rather than as
   hierarchy, make them both gold — this is the one to change, not the
   grid's.

   The alpha is 0.10 and it is low on purpose. Teal reads stronger than
   gold at the same value on a near-white ground, and this lobe is seen
   through a translucent card rather than beside it, which concentrates
   it further: at 0.18 the card interior measured 14 points of blue over
   red, enough that the panel looked tinted rather than lit. 0.10 halves
   that. The number to change if it still reads as colour instead of as
   light is this one, not the radius. */

   The inset is negative so the glow spreads past the card on every side
   and the card sits in the middle of it rather than on its edge — the
   point is a lit object, not a tinted rectangle. Wider than it is tall
   for the same reason the card is.

   SIZED IN PERCENTAGES, AND THAT IS THE WHOLE FIX FOR A REAL BUG.
   This was `36rem 20rem` first, and it left a hard horizontal line below
   the card where the glow stopped: 20rem of vertical radius did not
   reach its transparent stop before the box ran out, so the box edge cut
   the gradient mid-colour. A fixed radius cannot be made safe here
   either, because the box height follows the card, and the card's height
   follows how long that page's summary sentence is — it differs across
   all 39 services. Percentages resolve against the box itself, so the
   fade always completes inside it: 55% vertical radius reaching
   transparent at 72% puts the last visible colour at about 40% of the
   way out from the centre, with the edge at 50%. No page can clip it.

   The insets stay inside the section's own bottom padding on purpose.
   This layer is absolutely positioned, so extending it further would
   paint it over the intro text below rather than behind it. */

   NO NEGATIVE Z-INDEX, DELIBERATELY. The obvious way to put a layer
   behind a sibling is z-index: -1, and here it would fail in a way that
   is easy to misread: nothing on the path up from this element
   establishes a stacking context, so a negative layer would escape to
   the root and paint underneath the white section background — visible
   nowhere, with no error to explain it. Instead both are positioned at
   auto and DOM order decides: ::before is the first child, the card
   comes after, so the card paints on top. That also keeps the glow
   inside the card's backdrop root, which is what lets the card's blur
   pick it up. Adding `isolation` or a z-index to this wrapper to "tidy
   it up" would break that second part. */
:is(.page-service, .page-purpose) .blog.single {
  position: relative;
}

:is(.page-service, .page-purpose) .blog.single::before {
  content: "";
  position: absolute;
  inset: -5rem -3rem -6rem;
  pointer-events: none;
  background: radial-gradient(62% 55% at 50% 50%,
    rgba(2, 167, 164, 0.10), transparent 72%);
}

/* -- The bloom behind the deliverables grid --------------------------- */

/* The coloured field, brought back — and it works here for the reason it
   did not work on the home page.

   There it sat behind a single column of cards running down one side of
   an otherwise white section, so it read as a stain across the section
   rather than as depth, and it was rejected. This grid is a symmetric
   3x3 block centred in its own band, which gives the light an obvious
   place to come from: behind the middle of the grid. Same technique,
   composition it actually suits.

   ONE CENTRED LOBE, IN THE BRAND GOLD. The teal was tried first and
   worked, but it is the wrong colour to spend here: teal is already the
   site's accent everywhere — links, buttons, the hero — and a wash of it
   behind a full-width grid on 39 pages tips the whole site green. The
   gold is the other half of the palette and is used sparingly enough
   that it reads as light rather than as theme.

   Composition was settled by rendering three on the page: this single
   centred lobe; a version with two smaller lobes at the lower left and
   right; and a diagonal pair. The two-lobe version is the one to avoid —
   the lobes land where the band clips them, so they read as smudges in
   the corners rather than as light, which is the same failure that got
   the home page field rejected. The diagonal pair looks good but is not
   mirror-symmetric, so it would need a [dir="rtl"] companion to keep the
   Persian pages matching, for no gain over this.

   Symmetric about the centre line, so it reads identically flipped and
   this section stays free of direction rules entirely.

   CENTRED ON THE CARDS, NOT ON THE BAND. The grid does not sit in the
   middle of its own section — the eyebrow and heading take the top
   third, so the nine cards run from 28% to 89% of the box and their
   midpoint is at 59%, measured on the rendered page rather than
   guessed. The lobe is centred there, so the brightest point lands in
   the middle of the thing it is lighting.

   That replaced an earlier version anchored at 42% and run flush to the
   top edge, so the colour began exactly on the hairline above the grid.
   The two cannot both be had: a symmetric radial centred at 59% is 473px
   from the top edge and 329px from the bottom, so any radius large
   enough to reach the top is large enough to be cut off at the bottom —
   which is the clipped-gradient edge this file has had to fix twice
   already. Centred wins; the colour now fades up out of nothing below
   the hairline instead of starting on it.

   Sized in percentages for the same reason as the other two lobes, and
   here the margin is worth stating: at a 50% vertical radius reaching
   transparent at 72%, the colour ends 289px from the centre, with the
   nearer edge 329px away. It fades out inside the box on every side, so
   there is no edge to see at any content length.

   z-index -1 against an isolated parent keeps the field behind the grid
   but inside the band, and the bottom inset keeps it clear of this
   section's own hairline. */
:is(.page-service, .page-purpose) #deliverables {
  position: relative;
  isolation: isolate;
}

:is(.page-service, .page-purpose) #deliverables::before {
  content: "";
  position: absolute;
  inset: 0 0 2rem;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(48% 50% at 50% 59%,
    rgba(165, 136, 44, 0.16), transparent 72%);
}

/* The two places on a service page where the glass is glass rather than
   finish, because they are the two with something behind them worth
   refracting: the nine deliverables cards over their bloom, and the
   summary card over the faded band.

   The summary card was opaque until the band's bottom edge was
   dissolved — see the note on that layer for why, and why it can stop
   being opaque now. `.blog.single` is the theme's own wrapper around it
   and is the only thing on the page that identifies it.

   Gated, because these are the only rules in section 12 that depend on
   backdrop-filter. Without it the cards stay opaque, which is a finished
   look rather than a broken one. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  :is(.page-service, .page-purpose) #deliverables .card.shadow-lg,
  :is(.page-service, .page-purpose) .blog.single .card.shadow-lg {
    background-color: rgba(255, 255, 255, 0.46);
    -webkit-backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
    backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
  }
}

/* -- The grey band --------------------------------------------------- */

/* `section` is load-bearing: the <header> above shares both classes, and
   a grain layer on it would sit over the open navigation dropdowns. The
   band below the header is the one with nothing on top of it. */
section.wrapper.bg-soft-primary {
  position: relative;
  isolation: isolate;
}

/* Depth in the band, and the band's bottom edge dissolved.

   Two jobs in one layer, because they are the same layer: a soft grey
   lobe behind the title, and a fade from transparent to the body's own
   background across the bottom third, which is the more interesting
   half.

   THE LOBE HERE IS GREY, NOT GOLD. It was gold first, and gold in this
   position put the page's warmest point above its brightest object —
   the white card below was left looking like a hole punched through a
   tinted field rather than the thing the eye should land on. Moving the
   gold down behind the card and leaving neutral depth up here inverts
   that: the band recedes, the card is lit. The grey is the theme's own
   ink at a low alpha rather than a picked colour, so it darkens without
   tinting and cannot clash with anything the band is ever given.

   THE LOBE IS SIZED IN PERCENTAGES FOR THE SAME REASON THE GOLD ONE IS.
   At a fixed radius it did not reach transparent before this section's
   top edge, and that edge is where the <header> above ends — the two
   share a colour and read as one field, so the clip drew a horizontal
   line across the middle of it. Measured against the untreated page,
   which is flat to within one level there, the step was eight. Sizing
   against the box makes the fade finish inside it whatever the title
   wraps to, which on 39 pages it does differently.

   WHAT THE FADE IS FOR. The summary card is pulled up by `mt-n13` so it
   straddles the line where this grey band meets the white body — that
   overlap is the theme's own idea and it is a good one. But a hard step
   running behind a card is exactly what stopped that card from being
   glass: a translucent fill showed the step straight through the middle
   of it, grey above and white below, which read as a rendering fault.
   Fading the band out before the step happens removes the step instead
   of hiding from it, and the card can then be what everything else on
   this page is. What shows through it now is a smooth vertical gradient,
   which is what frosted glass over a gradient is supposed to look like.

   The fade colour is the body's --bs-light and not a hardcoded white,
   so it tracks the theme rather than drifting away from it the first
   time that variable moves.

   Paint order: this is ::before at z-index -1 and the grain is ::after
   on top, so the noise runs over the fade as well and the whole band
   keeps one texture. A negative z-index still paints above the element's
   own background-color, which is what lets a child lighten its parent. */
section.wrapper.bg-soft-primary::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(var(--bs-light-rgb), 0) 62%,
      rgb(var(--bs-light-rgb)) 100%),
    radial-gradient(58% 55% at 50% 48%,
      rgba(30, 34, 40, 0.07), transparent 72%);
}

/* 0.75, and that is the ceiling rather than a preference.

   This band is #f3f3f3 where the Technologies band is #eaebec, and
   `overlay` has less room the lighter the ground gets, so the same tile
   cannot reach the same strength here: measured on the page, 0.75 gives
   a pixel spread of 15 against that band's 21. Pushing further buys
   almost nothing and costs the colour — 0.9 gives 17 and 1.0 gives 19,
   and both drift the mean off #f3f3f3. 0.75 is the most grain this
   ground takes while still measuring as exactly the theme's grey. */
section.wrapper.bg-soft-primary::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.75;
  mix-blend-mode: overlay;
  background-image: var(--zt-grain-tile-strong);
  background-size: 96px 96px;
}

/* Paper has a grain of its own. */
@media print {
  :is(.page-service, .page-purpose) #deliverables::before,
  :is(.page-service, .page-purpose) .blog.single::before,
  :is(.page-service, .page-purpose, .page-tech) .card.shadow-lg::after,
  section.wrapper.bg-soft-primary::before,
  section.wrapper.bg-soft-primary::after {
    display: none;
  }
}

@media (forced-colors: active) {
  :is(.page-service, .page-purpose) #deliverables::before,
  :is(.page-service, .page-purpose) .blog.single::before,
  :is(.page-service, .page-purpose, .page-tech) .card.shadow-lg::after,
  section.wrapper.bg-soft-primary::before,
  section.wrapper.bg-soft-primary::after {
    display: none;
  }

  :is(.page-service, .page-purpose) #deliverables .card.shadow-lg,
  :is(.page-service, .page-purpose) .blog.single .card.shadow-lg {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* --------------------------------------------------------------------
   13. Inner page entrances.

      Motion, where section 12 is finish. Kept apart from it so either
      can be deleted without the other, and split the same way: 13a is
      scoped to the page types that have the grids it animates, 13b is
      not scoped at all because every inner page has a title band. See
      section 12 on why that split is deliberate.

      WHAT THESE PAGES HAD BEFORE: one entrance, on one element — the
      intro photograph, which carries the theme's own
      `data-cue="zoomIn"`. The title, the summary card, the nine
      deliverables cards and the advantages list all simply appeared.
      Meanwhile the home page has a staged hero entrance (section 8) and
      this file has carried a general-purpose scroll reveal since it was
      written (section 2b, `[data-reveal]`) that nothing in the project
      has ever used. The machinery existed; the page was not wired to it.

      NO NEW KEYFRAMES. Both effects below reuse animations already in
      this file — zt-reveal from section 2 and zt-hero-rise from section
      8 — so the site keeps one vocabulary of movement instead of
      growing a second one per page type.

      THE INTRO PHOTOGRAPH IS LEFT ALONE. Section 2b's selector carries
      `:not([data-cue])` for a reason spelled out there: scrollCue sets
      element.style.animationName inline, so an element driven by both
      mechanisms flickers and lands wherever scrollCue put it. Nothing
      below selects that element. If a service page ever gains another
      data-cue, the same care applies.
   -------------------------------------------------------------------- */

/* -- 13a. The deliverables grid assembles ----------------------------- */

/* Scroll-driven, not time-driven, and that is the point. view() ties
   each card's reveal to its own passage through the viewport, so the
   grid builds as the reader arrives at it and un-builds if they scroll
   back up. The theme's scrollCue plays on a clock once triggered, which
   means scrolling past quickly leaves cards finishing their entrance
   after the reader has already gone by.

   The stagger is done with animation-range, not animation-delay. A
   view() timeline has no clock for a delay to act on — progress is a
   function of position — so the way to make the middle card arrive
   after the first is to start its range later. Each column's window is
   pushed 8% further down the entry phase than the one before.

   DIRECTION COMES FREE. The stagger keys off nth-child, which is DOM
   order, and the grid is laid out by the same DOM order — so the
   cascade runs left-to-right in English and right-to-left in Persian
   without a [dir="rtl"] rule, following the reading direction in both.
   This is the one place in sections 12 and 13 where direction matters
   at all, and it resolves itself.

   At the xl breakpoint the grid is three across and the three rules
   below map onto its columns exactly. Below that it collapses to two
   and then one, where the 3n pattern no longer lines up with the
   layout — the cards then arrive in a rolling order rather than a
   columnar one, which is not wrong, just different, and not worth a
   media query to control.

   Guarded twice, as everything animated in this file is: on the feature
   that drives it, because zt-reveal starts at opacity 0 and a browser
   without view() would leave nine cards permanently invisible; and on
   reduced-motion. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    :is(.page-service, .page-purpose) #deliverables .card.shadow-lg {
      animation: zt-reveal linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 28%;
    }

    :is(.page-service, .page-purpose) #deliverables .col-xl-4:nth-child(3n + 2) .card.shadow-lg {
      animation-range: entry 18% cover 36%;
    }

    :is(.page-service, .page-purpose) #deliverables .col-xl-4:nth-child(3n + 3) .card.shadow-lg {
      animation-range: entry 26% cover 44%;
    }
  }
}

/* -- 13b. The title band arrives -------------------------------------- */

/* The same three-beat entrance the home hero gets in section 8, on the
   three things at the top of an inner page: the title, whatever follows
   it, and the summary card below them. Same keyframes, same easing,
   same spacing between beats, so arriving on an inner page from the
   home page feels like the same site rather than a different one.

   `h1 + *` RATHER THAN A LIST OF PAGE TYPES. The site has two shapes of
   title band and this selector does not need to know which it is on.
   Service, purpose and industry pages wrap the title in `.post-header`
   and follow it with a tagline `<p>`; the company pages — about,
   contact, legals, engagement models — put the `<h1>` straight into the
   column and follow it with a breadcrumb `<nav>`. The second beat is
   "whatever comes after the heading" in both, so one rule covers eleven
   bands across both trees, and a twelfth added later is covered without
   anyone remembering to come back here.

   THE SUMMARY CARD IS IN THIS SEQUENCE ON PURPOSE, even though it sits
   in a different section of the markup. It is above the fold at every
   width, so animating the title above it and leaving it static reads as
   an entrance that did not finish. It is the third beat, not a separate
   idea. It needs no scope of its own: `.blog.single` exists only on the
   service and purpose pages.

   Guarded on reduced-motion only, and that is deliberate rather than an
   oversight — the same argument section 8 records. These are plain
   keyframe animations with no feature behind them to detect, supported
   by every browser this site runs in, so there is no third state in
   which `both` leaves the content stuck at opacity 0. A reader who asks
   for less motion gets the finished page with nothing moving, which is
   the outcome that matters most here: the title is the first thing on
   the page. */
@media (prefers-reduced-motion: no-preference) {
  section.wrapper.bg-soft-primary h1,
  section.wrapper.bg-soft-primary h1 + *,
  .blog.single {
    animation: zt-hero-rise 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
  }

  section.wrapper.bg-soft-primary h1 + * { animation-delay: 0.09s; }
  .blog.single                           { animation-delay: 0.18s; }
}

/* --------------------------------------------------------------------
   14. Purpose pages — the two things they do not share.

      A purpose page is a near-relative of a service page rather than a
      copy: same summary card straddling the same grey band, same
      nine-card #deliverables grid, same #intro and #cta. So instead of a
      second copy of sections 12 and 13, those two select
      `:is(.page-service, .page-purpose)` and cover both. Two page types
      wearing one design should be one set of rules; keeping two is how
      they drift apart.

      WHAT DID NOT NEED SAYING TWICE, because the existing rules already
      landed correctly on the differences:

        * #deliverables sits on bg-gray (#f6f7f9) here rather than
          bg-light. The bloom and the translucent cards read the same on
          it; nothing needed a second value.
        * The three #results cards, which service pages do not have, are
          picked up by section 12's card rule — it selects every
          `.card.shadow-lg` on the page — so they get the radius, the lit
          edge, the drop and the grain for free.

      Which leaves two gaps, both below: those cards' fill, and their
      entrance.
   -------------------------------------------------------------------- */

/* The three #results cards go translucent too — with no bloom added
   behind them, because they already have something behind them.

   This looked at first like the case the section 12 note argues
   against: a flat white band, where a translucent fill costs a blurred
   backdrop per card per frame and returns the same white. It is not.
   The theme puts two decorations in this band at z-index 0, underneath
   the cards — a pale blue circle bottom-right and a yellow dot grid
   top-left — and the outer cards overlap both. Opaque, the cards cut
   those shapes off at a hard edge. Translucent, the shapes carry
   through and the panels read as glass laid over them, which is the
   one thing glass is actually for.

   So the rule here is the same as the deliverables one minus the
   bloom: the decorations are the thing worth seeing, and a coloured
   field on top of them would only muddy what is already there. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .page-purpose #results .card.shadow-lg {
    background-color: rgba(255, 255, 255, 0.46);
    -webkit-backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
    backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
  }
}

@media (forced-colors: active) {
  .page-purpose #results .card.shadow-lg {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* The #results grid reveals like the #deliverables grid above it.
   Without this it would be the only card group on the page that just
   appears, immediately after one that assembles — the inconsistency
   reads as something broken rather than as restraint.

   Three across at md and up, so the same 3n stagger and the same
   ranges as 13a. Guarded identically, and for the same reason:
   zt-reveal starts at opacity 0. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .page-purpose #results .card.shadow-lg {
      animation: zt-reveal linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 28%;
    }

    .page-purpose #results .col-md-4:nth-child(3n + 2) .card.shadow-lg {
      animation-range: entry 18% cover 36%;
    }

    .page-purpose #results .col-md-4:nth-child(3n + 3) .card.shadow-lg {
      animation-range: entry 26% cover 44%;
    }
  }
}


/* --------------------------------------------------------------------
   15. Technology pages.

      The #about grid: six cards, one per selling point, on every one of
      the sixteen technology pages and on the single Persian template
      that renders them all. Same card material as sections 12 and 14 —
      those rules name .page-tech too — over a bloom in the page's own
      colour rather than the brand gold.

      WHY THE COLOUR IS READ FROM THE MARKUP AND NOT FROM $cover.
      Each technology page already passes a slug to the layout for its
      header image, and reaching for it here was the obvious move. It is
      wrong on three of the sixteen: Android ships under cover=kotlin,
      Go under cover=golang, iOS under cover=swift. Those pages would
      have been painted in another technology colour, quietly and only
      on three pages out of sixteen — the kind of thing that survives a
      review because nobody opens all of them.

      The icon colour class is the ground truth instead: it is what
      actually tints the icons in these very cards, so the bloom cannot
      disagree with what it sits behind. :has() reads it off the section
      without any template needing to pass anything new, which also
      means the Persian template — which builds the same class from its
      own slug key — is covered by the same rules with no second wiring.

      THE MAP BELOW WAS GENERATED FROM style.css, NOT TRANSCRIBED.
      Each row is the fill that .icon-color-{slug} already sets on its
      SVG paths, converted to an RGB triple so one alpha can be applied
      to all of them. If a technology is added, add a row; if none
      matches, the fallback on #about keeps the brand gold rather than
      leaving a bloom with no colour.

      :has() is supported everywhere this site runs. Where it is not,
      the fallback row still applies and the section gets a gold bloom
      instead of a tinted one, which is a different look and not a
      broken one.
   -------------------------------------------------------------------- */

.page-tech #about {
  position: relative;
  isolation: isolate;
  /* Brand gold, for a technology with no row in the map below. */
  --zt-tech-rgb: 165, 136, 44;
}

.page-tech #about:has(.icon-color-android) { --zt-tech-rgb: 230, 119, 73; }  /* #e67749 */
.page-tech #about:has(.icon-color-angular) { --zt-tech-rgb: 222, 0, 45; }  /* #de002d */
.page-tech #about:has(.icon-color-flutter) { --zt-tech-rgb: 69, 210, 253; }  /* #45d2fd */
.page-tech #about:has(.icon-color-go) { --zt-tech-rgb: 0, 174, 216; }  /* #00aed8 */
.page-tech #about:has(.icon-color-ios) { --zt-tech-rgb: 255, 87, 34; }  /* #ff5722 */
.page-tech #about:has(.icon-color-java) { --zt-tech-rgb: 84, 131, 162; }  /* #5483a2 */
.page-tech #about:has(.icon-color-javascript) { --zt-tech-rgb: 233, 209, 17; }  /* #e9d111 */
.page-tech #about:has(.icon-color-laravel) { --zt-tech-rgb: 239, 83, 63; }  /* #ef533f */
.page-tech #about:has(.icon-color-nodejs) { --zt-tech-rgb: 83, 158, 67; }  /* #539e43 */
.page-tech #about:has(.icon-color-python) { --zt-tech-rgb: 54, 115, 166; }  /* #3673a6 */
.page-tech #about:has(.icon-color-reactjs) { --zt-tech-rgb: 97, 218, 251; }  /* #61dafb */
.page-tech #about:has(.icon-color-reactnative) { --zt-tech-rgb: 97, 218, 251; }  /* #61dafb */
.page-tech #about:has(.icon-color-rubyonrails) { --zt-tech-rgb: 204, 0, 0; }  /* #cc0000 */
.page-tech #about:has(.icon-color-vuejs) { --zt-tech-rgb: 65, 184, 131; }  /* #41b883 */
.page-tech #about:has(.icon-color-woocommerce) { --zt-tech-rgb: 162, 105, 203; }  /* #a269cb */
.page-tech #about:has(.icon-color-wordpress) { --zt-tech-rgb: 70, 68, 66; }  /* #464442 */

/* Centred on the grid and sized in percentages, for the reasons the
   #deliverables bloom records: the fade has to finish inside the box on
   every page, and these six cards wrap to different heights depending
   on how long each selling point runs. */
.page-tech #about::before {
  content: "";
  position: absolute;
  inset: 0 0 2rem;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(48% 45% at 50% 65%,
    rgba(var(--zt-tech-rgb), 0.16), transparent 72%);
}

/* Translucent over that bloom, like the other grids that have one. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .page-tech #about .card.shadow-lg {
    background-color: rgba(255, 255, 255, 0.46);
    -webkit-backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
    backdrop-filter: blur(var(--zt-glass-blur)) saturate(var(--zt-glass-sat));
  }
}

/* Three across at xl, so the same stagger and ranges as 13a. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .page-tech #about .card.shadow-lg {
      animation: zt-reveal linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 28%;
    }

    .page-tech #about .col-md-4:nth-child(3n + 2) .card.shadow-lg {
      animation-range: entry 18% cover 36%;
    }

    .page-tech #about .col-md-4:nth-child(3n + 3) .card.shadow-lg {
      animation-range: entry 26% cover 44%;
    }
  }
}

@media print {
  .page-tech #about::before { display: none; }
}

@media (forced-colors: active) {
  .page-tech #about::before { display: none; }

  .page-tech #about .card.shadow-lg {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
