:root {
    --accent: #3b8ef3;

    /* One corner-radius scale for the whole site. Phones get the smaller
       step: the same radius eats a far bigger share of a narrow card, so a
       value that reads as a soft corner on desktop reads as a blob there. */
    --r-lg: 14px;     /* wide panels, media blocks */
    --r-md: 12px;     /* cards */
    --r-sm: 10px;     /* small inline media, thumbnails */
    --r-accent: 36px; /* the one oversized corner on .card-visit */
}
@media (min-width: 768px) {
    :root {
        --r-lg: 20px;
        --r-md: 14px;
        --r-sm: 12px;
        --r-accent: 60px;
    }
}

body {
    font-family: "Inter", serif;
    font-size: 16px;
    font-weight: 400;
    color: #454545;
    line-height: 1.4;
    overflow-x: hidden;
}

h1, h2, h3, h4, h5, h6 {
    font-family: "Lora", serif;
}

/* -- Process Slider -- */
.process-slider-wrapper,
.who-we-are {
    h3 {font-family: "Inter", serif;}
}

/* -- Visit Us -- */
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-30px); }
}
.animate-float {
  animation: float 5s ease-in-out infinite;
}
.delay-2000 {
  animation-delay: 2s;
}
.card-visit {
    border-radius: var(--r-lg) var(--r-accent) var(--r-lg) var(--r-lg);
}

/* -- FAQs/What We Do -- */
.heading-faq h2 {
    background: url('assets/shape-brush.svg') left bottom no-repeat;
}
.bg-overlay {
    background: linear-gradient(to right, rgba(0,0,0,0.8), rgba(0,0,0,0.6));
}
@keyframes bgZoom {
  0%   { transform: scale(1) translateY(0); }
  50%  { transform: scale(1.1) translateY(-10px); }
  100% { transform: scale(1) translateY(0); }
}

.bg-zoom {
  animation: bgZoom 12s ease-in-out infinite;
}

/* -- Blog -- */
.blog-slider {
    .slick-slide {
        opacity: 0.2;
        transform: scale(1);
        transition: all 0.4s ease;
    }
    .slick-center {
        opacity: 1 !important;
        transform: scale(1);
    }
    .slick-dots li {
        margin: 0;
        button:before {font-size: 10px;}
    }
    .slick-dots {
        bottom: -50px;
    }
    .slick-slide:has(+ .slick-center) {
        opacity: 1;
    }
    .slick-prev, .slick-next {
        z-index: 10;
    }
    .slick-prev:before,
    .slick-next:before {
        color: black;
        font-size: 20px;
    }
}
#blogPrev {margin-left: -75px;}
#blogNext {margin-right: -75px;}

/* -- Footer Section -- */
@keyframes gradientFlow {
    0%   { background-position: 0% center; }
    100% { background-position: 250% center; }
}

.contact-row:hover { color: #fff; transform: translateX(-4px); }
.contact-row:hover .contact-icon-wrap {
    background: rgba(59,142,243,0.15);
    border-color: var(--accent);
    box-shadow: 0 0 18px rgba(59,142,243,0.35);
}
.footer-divider {
    height: 1px;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5) 30%, rgba(59,142,243,0.25) 50%, rgba(255,255,255,0.5) 70%, transparent);
}
.gsap-fade { opacity: 0; }
.gsap-left { opacity: 0; transform: translateX(-40px); }
.gsap-up { opacity: 0; transform: translateY(30px); }

/* The starting positions above, and the opacity aos.css puts on [data-aos],
   are only safe while the library that animates them out of that state has
   actually loaded. GSAP and AOS both come from a CDN, so custom.js sets this
   class on <html> when either one is missing and the content is shown as-is
   rather than staying invisible forever. */
.no-aos [data-aos],
.no-gsap .gsap-fade,
.no-gsap .gsap-left,
.no-gsap .gsap-up {
    opacity: 1 !important;
    transform: none !important;
}

/* Marquee animation */
@keyframes marquee {
    0% {
        transform: translateX(0%);
    }
    100% {
        transform: translateX(-50%);
    }
}
.marquee {
    display: flex;
    width: max-content;
    animation: marquee 20s linear infinite;
}
.fade-mask {
    -webkit-mask-image: linear-gradient(to right, transparent, black 10%, black 90%, transparent);
    mask-image: linear-gradient(to right, transparent, black 10%, black 90%, transparent);
}
.marquee:hover {
  animation-play-state: paused;
}
.logos-section {
    padding: 2rem 0;
}
@media (min-width: 1024px) {
    .logos-section {
        padding: 3rem 0;
    }
}
.client-logo {
    width: auto;
    filter: grayscale(1);
    opacity: .7;
}
/* -- ===== Header ==== -- */
.glass {
    background: rgba(255, 255, 255, 0.08);
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.15);
}
.toggle-header {
    max-width: 100%;
    padding: 0;
    transition: all .3s;
    box-shadow: 0 1px 3px rgba(0,0,0,0.10);
    top: 0;
    .glass {
        background: white;
        border-radius: 0;
        border-top: 0;
        border-left: 0;
        border-right: 0;
    }
    .logo-main, #menuBtn, .nav-links a, button, #mobileMenu a, a[aria-label="Chat on WhatsApp"] {color: #333;}
    #mobileMenu {border-top: 1px solid #ddd;}
    svg path {fill: #333;}
    a[aria-label="Chat on WhatsApp"] {border-color: #d1d5db;}
}
.heading-hero {
    line-height: 1;
    font-weight: bold;
}
@keyframes borderFlow {
  0% { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
.animate-borderFlow {
  animation: borderFlow 3s linear infinite;
}
.toggle-header {
    #mobileMenu {
        margin-top: 0;
        padding: 0;
        background: white;
        &.opacity-100 {padding: 24px;}
    }
}
.process-slider .slick-slide {
  height: auto;
}
.process-slider .slick-track {
  display: flex !important;
}
.process-slider {
    .slick-slide > div {
        height: 100%;
    }
    .slick-dots {
        bottom: -50px;
        li {
            margin: 0;
            button {
                &:before {font-size: 10px;}
            }
        }
    }
}

@keyframes scroll {
  0% {
    transform: translateY(0);
    opacity: 1;
  }
  70% {
    transform: translateY(20px);
    opacity: 0.4;
  }
  100% {
    transform: translateY(0);
    opacity: 0;
  }
}
.animate-scroll {
  animation: scroll 1.8s infinite;
}
.nav-links {
    a {transition: all .3s;}
    a:hover,
    a.active {
        color: #00d3f3;
    }
}

/* -- Cube Animation -- */
.scene {
    position: relative;
    width: 100%;
    max-width: 540px;
    aspect-ratio: 540 / 460;
    /* Size the labels off the cube's own rendered width, not the viewport -
       the cube's actual room to breathe depends on the grid column it sits
       in, which isn't 1:1 with viewport width. */
    container-type: inline-size;
}
canvas {
    display: block;
    width: 100% !important;
    height: 100% !important;
}
.corner-label {
    position: absolute;
    /* Scales smoothly with the cube's width: wide enough to hold a full
       label on one line when there's room, small enough to wrap to two
       clean lines (never overlapping a neighbor) once the cube is tight. */
    font-size: clamp(11px, 3cqi, 16px);
    font-weight: 500;
    max-width: clamp(70px, 28cqi, 160px);
    text-align: center;
    line-height: 1.25;
    padding: 2px clamp(4px, 1.5cqi, 8px);
    pointer-events: none;
}
@media (min-width: 768px) {
    .wrap-cube {
        > div:nth-child(1) {
            order: 2;
            text-align: right;
            h2 {padding-right: 0;}
        }
        > div:nth-child(2) {
            order: 1;
        }
    }
}

/* -- Project Cards -- */
/* On touch devices there's no hover, so the title/category caption
   (normally revealed via group-hover) must stay visible, or the project
   is unidentifiable on mobile/tablet. */
@media (hover: none) {
    .project-card .group-hover\:opacity-100 {
        opacity: 1;
    }
}

/* -- Mobile card sliders (Services / Projects) -- */
/* Swipeable on small screens instead of a long vertical stack; JS
   unslicks back to the normal grid/list from md/sm up. Only one slide
   is ever shown at a time here, so (unlike the process/blog sliders)
   slides don't need forced equal heights - adaptiveHeight already
   resizes the list to fit whichever slide is current, and forcing
   height:100% on a slide's children fights that when a slide has more
   than one direct child (e.g. services' text + image columns). */
/* Slick sets display:block on every slide, which stops the grid `gap-10`
   from applying - so once the mobile slider initialises, a service's image
   sits flush against the last line of its copy. Put that spacing back as a
   margin, only while slicked (the class is dropped again from md up, where
   the real two-column grid and its gap take over). */
#servicesSlider.slick-initialized .slick-slide > * + * {
    margin-top: 2.5rem;
}
.mobile-slider.slick-initialized {
    .slick-slide {height: auto;}
    .slick-dots {
        position: static;
        margin-top: 20px;
        li {margin: 0;}
        button:before {font-size: 10px;}
    }
}
/* Hero animated background (canvas) */
.hero-canvas {
    display: block;
    width: 100%;
    height: 100%;
    pointer-events: none;
    background-color: #05060d;
}
/* Painted behind the canvas so the hero is never a blank box
   before the first frame, or if canvas is unavailable. */
.hero-canvas-fallback {
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(60% 55% at 62% 50%, rgba(59,142,243,0.22), transparent 70%),
        radial-gradient(45% 45% at 80% 25%, rgba(244,114,182,0.14), transparent 70%),
        #05060d;
}
.hero, .page-hero {
    background-color: #05060d;
}

/* -- Site Loader -- */
/* Plain CSS (not a Tailwind utility class) on purpose: this stylesheet is
   a blocking <link>, so the overlay is correctly positioned and opaque on
   the very first paint, before the Tailwind CDN script has had a chance
   to scan the DOM and inject its own rules. */
.site-loader {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: #05060d;
    transition: opacity .5s ease, visibility .5s ease;
}
.site-loader.is-hidden {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
}
/* The wait is the Pahadian name signing itself, once, in about 3.6
   seconds. The letterforms are Great Vibes (SIL Open Font License)
   converted to outlines and inlined, so the loader never waits on a
   webfont before it can draw - and hand-built SVG rather than a Lottie
   for the same reason: a player would have to download and execute
   before painting a frame, which is exactly what a loading screen
   cannot wait for. */
.site-loader-sign {
    position: relative;
    width: clamp(250px, 66vw, 430px);
    line-height: 0;
}
/* A static pool of light rather than a drop-shadow filter on the SVG: a
   filter would have to be re-applied on every frame of the writing, on
   top of re-rasterising the mask, which is the one thing that makes this
   stutter on a slow phone. */
.site-loader-sign::before {
    content: "";
    position: absolute;
    inset: -30% -10%;
    background: radial-gradient(closest-side, rgba(0, 212, 255, 0.16), rgba(0, 212, 255, 0) 100%);
    pointer-events: none;
}
.site-loader-sign svg {
    position: relative;
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
}
/* Paint - the ink gradient and the pen's white stroke - lives on the
   elements as presentation attributes, NOT here. A url(#id) written in an
   external stylesheet resolves against the stylesheet's own URL, so it
   would look for style.css#siteLoaderInkGrad and find nothing; an
   unresolvable paint server means the glyphs never paint. Keeping only the
   animation here also means a failure to load this file degrades to a
   static signature rather than a blank screen.

   Each pen stroke carries its own geometry and timing inline, in --l
   (its measured length, rounded up), --d (how long it takes to write),
   --t (when it starts) and --e (its speed curve). The dasharray is that
   measured length rather than pathLength, which some renderers handle
   unreliably - so if a pen path is edited, its --l has to be re-measured
   with getTotalLength() or the stroke will start part-drawn.

   The default curve eases in and out because a hand accelerates off the
   page and settles again at the end of every stroke; the long sweeps
   (the P, the h, the d's ascender, the n) override it with a flatter
   curve that holds speed through the middle. Both are what make this
   read as writing instead of a bar wiping across the letters. The last
   stroke lands at 3.605s - custom.js waits that long before it may
   uncover the page. */
.site-loader-pen > path {
    stroke-dasharray: var(--l);
    stroke-dashoffset: var(--l);
    animation: siteLoaderWrite var(--d) var(--e, cubic-bezier(.45, .06, .55, .95)) var(--t) forwards;
}
@keyframes siteLoaderWrite { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
    /* No writing motion: the finished signature is simply present. */
    .site-loader-pen > path { animation: none; stroke-dashoffset: 0; }
}
body.is-loading {
    overflow: hidden;
}

/* The eight service sections on /services are link targets (/services#seo
   and so on, from the home page service list and the schema @ids). The header
   is fixed, so a plain jump would hide the section's heading under it. */
.service-block {
    scroll-margin-top: 7rem;
}

/* ===========================================================================
   "Start your project" waiting character (.pc)

   Three complete renders of the same figure, stacked and cross-faded. All three
   were cropped from the same source frame to one shared 480x919 canvas (see
   index.html), so the body lands on identical pixels in every pose and only the
   arms change - which is what lets a plain opacity cross-fade read as the same
   person changing stance rather than as two pictures swapping.

     .pc          the layout box - the whole sheet, no overflow
     .pc-pose     one render each; .is-on is the visible one

   He stands still: no breathing, no bob, no drift, and he does not turn toward
   the pointer. Changing pose is the entire interaction, so every animation here
   is a plain CSS transition - the cross-fade and the glow - and custom.js only
   ever moves the .is-on class from one image to another.
   =========================================================================== */

/* The sheet is cropped tight to the widest pose (the thumbs-up arm), so the box
   below is exactly the artwork and no transparent margin becomes layout space.
   Because every pose shares that crop, none of them overflows the box. */
.pc {
    --pc-w: 146px;
    /* Breathing room either side of him, on top of the card padding on the left
       and the flex gap on the right. */
    --pc-gutter: 14px;
    position: relative;
    width: var(--pc-w);
    aspect-ratio: 480 / 919;
    flex-shrink: 0;
    margin-inline: var(--pc-gutter);
}
@media (min-width: 640px)  { .pc { --pc-w: 164px; --pc-gutter: 20px; } }
@media (min-width: 768px)  { .pc { --pc-w: 182px; --pc-gutter: 26px; } }
/* From here the card has stopped growing (max-w-[1100px]) but the row still has
   space to spare, so the gutter takes a share of it rather than letting it all
   pool in the empty stretch to the right of the paragraph. */
@media (min-width: 1024px) { .pc { --pc-w: 203px; --pc-gutter: 48px; } }
@media (min-width: 1280px) { .pc { --pc-w: 203px; --pc-gutter: 64px; } }

.pc-pose {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: auto;
    max-width: none;     /* Tailwind preflight sets img { max-width: 100% } */
    opacity: 0;
    pointer-events: none;
    user-select: none;
    -webkit-user-drag: none;
    z-index: 1;
    /* THE WHITE FLASH, AND WHY THE OUTGOING POSE IS DELAYED.

       Two stacked images at partial opacity do not add up to a solid figure.
       Their combined coverage is 1-(1-a)(1-b), so a plain cross-fade that has
       both at 0.5 halfway through only covers 75% - and the white card behind
       him shows straight through the figure as a pale wash. Slowing the fade
       down makes that worse, not better, because the dip lasts longer.

       There is no pair of curves that avoids it: holding coverage at ~1 needs
       the outgoing pose to still be at ~1 while the incoming one is only
       halfway. So it simply is. The incoming pose fades in on top over 0.5s on
       an ease-out curve that reaches near-solid early, and the outgoing one
       holds at full opacity underneath for 0.4s before dissolving. Coverage
       never drops, so nothing white can appear.

       What fades at the end is only the part of the old pose that the new one
       does not cover - the arm that moved - which is exactly the thing the
       change is about. */
    transition: opacity 0.5s cubic-bezier(0.4, 0, 0.6, 1) 0.4s;
}
/* The visible pose is lifted above the others so the incoming image is always
   the one on top, whichever direction the swap runs in. Without this the order
   in index.html would decide it: fading idle back in *underneath* an opaque
   waiting pose would show nothing at all until the waiting pose let go. */
.pc-pose.is-on {
    opacity: 1;
    z-index: 2;
    transition: opacity 0.5s cubic-bezier(0.25, 1, 0.5, 1);
}

/* The thumbs-up is an answer to something the visitor just did, so it swaps on
   a much shorter arc than the unprompted idle drift - a slow dissolve there
   would feel like lag on the hover rather than a reaction to it. The same
   hold-then-dissolve shape, just compressed. custom.js puts .is-excited on the
   stage before changing the pose, and takes it off after changing it back, so
   both directions get this timing. */
.pc.is-excited .pc-pose       { transition: opacity 0.26s cubic-bezier(0.4, 0, 0.6, 1) 0.2s; }
.pc.is-excited .pc-pose.is-on { transition: opacity 0.26s cubic-bezier(0.25, 1, 0.5, 1); }

/* Ambient warmth behind him. His own pixels are never filtered or tinted, so
   the artwork stays exactly as delivered. */
.pc-glow {
    position: absolute;
    left: 50%;
    top: 3%;
    margin-left: -95%;
    width: 190%;
    height: 46%;
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(20, 184, 166, 0.28), rgba(20, 184, 166, 0));
    opacity: 0;
    transition: opacity 0.45s ease;
    pointer-events: none;
}
.pc.is-attentive .pc-glow { opacity: 0.5; }
.pc.is-excited   .pc-glow { opacity: 1; }

/* "Get in touch" - lifts to meet the thumbs-up. */
.pc-trigger {
    box-shadow: 0 6px 16px -10px rgba(13, 148, 136, 0.9);
    transition: background-color 0.25s ease,
                box-shadow 0.35s ease,
                transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.pc-trigger:hover,
.pc-trigger:focus-visible {
    transform: translateY(-3px);
    box-shadow: 0 16px 28px -12px rgba(13, 148, 136, 0.85);
}
.pc-trigger:active { transform: translateY(-1px); }

/* Under reduced motion custom.js stops the idle/waiting cycle, so he simply
   stands there until the visitor does something. The pose still changes on hover
   - that is a state change rather than motion, and it is the whole point of the
   interaction - but it cross-fades without anything moving. */
@media (prefers-reduced-motion: reduce) {
    .pc-glow { transition: opacity 0.6s ease; }
    /* The cross-fade still happens - it is how one pose becomes the next - but
       it is shortened so it reads as a change of state rather than an effect. */
    .pc-pose { transition: opacity 0.2s ease 0.18s; }
    .pc-pose.is-on { transition: opacity 0.2s ease; }
    .pc-trigger,
    .pc-trigger:hover,
    .pc-trigger:focus-visible,
    .pc-trigger:active { transform: none; }
}
