/* Yugan Solutions — global component styles.
 *
 * These rules used to live in a ~30KB inline <style> block inside base.html,
 * which meant every single HTML response carried them again: never cached,
 * and a large share of each page's bytes were CSS rather than content. Served
 * as a file they are fetched once, cached with the rest of the static assets,
 * and every page's markup gets ~30KB lighter.
 *
 * Loaded AFTER tailwind.min.css in base.html so the cascade order is unchanged
 * from when this was inline.
 */

* {
    scroll-behavior: smooth;
}

html {
    overscroll-behavior: none;
}

body {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-y: none;
}

body {
    font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;
    background: #0D0B1A;
    color: #E5E7EB;
    /* Light-on-dark text renders optically thinner than the same weight
       dark-on-light. Plus Jakarta Sans is a variable font, so the correction
       is free: a hair over regular restores the stem weight the dark ground
       eats. Only text that sets no weight of its own inherits this, which is
       exactly the body and secondary copy that was reading faint — anything
       carrying `font-medium`, `font-semibold` etc. is untouched. */
    font-weight: 420;
}

/* ── Headings ──
   Roboto against Plus Jakarta Sans for everything else. It is kept to actual
   headings, so this is the single place it is applied: h1-h6 and nothing
   else. Non-heading display text (stat counters, buttons, labels) stays on
   Plus Jakarta Sans deliberately.

   Tracking is set per size rather than globally. Roboto is already tightly
   fitted, so it wants the negative tracking only where the type is large
   enough for the gaps to open up; at label sizes it is left alone. */
h1, h2, h3, h4, h5, h6 {
    font-family: 'Roboto', system-ui, 'Helvetica Neue', Arial, sans-serif;
    letter-spacing: -0.02em;
}

h4, h5, h6 {
    letter-spacing: 0;
}

/* Small headings — mostly the uppercase, wide-tracked column labels in the
   footer and sidebars. Roboto's regular weight is lighter than Plus Jakarta
   Sans' at the same nominal value, so these get the weight back to keep the
   heading/body distinction visible on a dark ground. */
h4, h5, h6,
h1.text-sm, h2.text-sm, h3.text-sm,
h1.text-xs, h2.text-xs, h3.text-xs {
    font-weight: 700;
}

.font-mono {
    font-family: 'Space Mono', monospace;
}

/* ── Small-text readability floor ──
   Measuring the rendered pages showed the secondary text was not, for the
   most part, failing on contrast ratio — it was failing on everything else
   that makes small light-on-dark type legible. The worst offenders were the
   micro-labels set in arbitrary pixel values: 9px, 10px and 11px at weight
   400, several of them monospaced and tracked out to 0.18em.

   Arbitrary Tailwind sizes bypass the theme scale, so they cannot be fixed
   in tailwind.config.js the way `text-xs` and `text-sm` were. Lifting them
   here keeps it to one place instead of 65 call sites, and the escaped
   selectors match the utilities exactly. Nothing grows more than 2px, so no
   layout shifts. */
.text-\[8px\],
.text-\[9px\] {
    font-size: 11px;
}

.text-\[10px\] {
    font-size: 11.5px;
}

.text-\[11px\] {
    font-size: 12px;
}

/* Space Mono is already wide; at label sizes the extra tracking these carry
   pushes the letters far enough apart to stop reading as words. Pulling it
   back is what makes them scannable again.

   Weight is deliberately left alone: only the 400 cut of Space Mono is
   self-hosted, so asking for a bolder one would get a synthesised faux-bold
   rather than a real weight. The size floor above and the lifted palette do
   the work instead. */
.font-mono.text-\[8px\],
.font-mono.text-\[9px\],
.font-mono.text-\[10px\],
.font-mono.text-\[11px\],
.font-mono.text-xs {
    letter-spacing: 0.12em;
}

/* Scrollbar */
::-webkit-scrollbar {
    width: 5px;
}

::-webkit-scrollbar-track {
    background: #0D0B1A;
}

::-webkit-scrollbar-thumb {
    background: #7C3AED;
    border-radius: 3px;
}

/* reCAPTCHA v3 — hide floating badge (legal notice shown inside forms via JS) */
.grecaptcha-badge { visibility: hidden !important; }

/* Gradient text */
.gradient-text {
    background: linear-gradient(135deg, #A855F7, #7C3AED, #F59E0B);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}

/*
 * Keeps the animated hero text visible after scrolling
 * on mobile and tablet browsers, especially Safari.
 */
.hero-scramble {
    display: inline-block;
    min-height: 1.05em;
    line-height: inherit;
    background-position: center;
    background-repeat: no-repeat;
    background-size: 100% 100%;
    isolation: isolate;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
    -webkit-transform: translateZ(0);
    transform: translateZ(0);
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
    will-change: transform;
}

.hero-scramble.scramble-repaint {
    -webkit-transform: translate3d(0, 0, 0.01px);
    transform: translate3d(0, 0, 0.01px);
}

/* Noise overlay */
.noise::before {
    content: '';
    position: absolute;
    inset: 0;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
    pointer-events: none;
    z-index: 1;
}

/* Nav glass */
.nav-glass {
    background: rgba(13, 11, 26, 0.85);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid rgba(124, 58, 237, 0.15);
}

/* ── Services dropdown (desktop: hover / keyboard focus) ── */
.nav-dropdown {
    position: relative;
}

/* The wrapper keeps a padding bridge so the pointer never crosses a
   dead gap between the trigger and the panel. */
.nav-dropdown-wrap {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    padding-top: 18px;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.22s ease, transform 0.22s ease, visibility 0s linear 0.22s;
}

.nav-dropdown:hover .nav-dropdown-wrap,
.nav-dropdown:focus-within .nav-dropdown-wrap {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
    transition: opacity 0.22s ease, transform 0.22s ease;
}

.nav-dropdown-wrap {
    transform: translateX(-50%) translateY(8px);
}

.nav-dropdown-panel {
    width: 560px;
    max-width: calc(100vw - 3rem);
    background: rgba(13, 11, 26, 0.97);
    border: 1px solid rgba(168, 85, 247, 0.22);
    border-radius: 14px;
    box-shadow: 0 28px 70px rgba(3, 2, 14, 0.6);
    backdrop-filter: blur(22px);
    -webkit-backdrop-filter: blur(22px);
    padding: 14px;
}

.nav-dropdown-item {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 10px 12px;
    border-radius: 10px;
    transition: background 0.22s ease;
}

.nav-dropdown-item:hover {
    background: rgba(124, 58, 237, 0.16);
}

.nav-dropdown-chevron {
    transition: transform 0.25s ease;
}

.nav-dropdown:hover .nav-dropdown-chevron,
.nav-dropdown:focus-within .nav-dropdown-chevron {
    transform: rotate(180deg);
}

/* ── Services accordion (mobile drawer: tap to open) ── */
.mobile-submenu {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.3s ease;
}

.mobile-submenu > div {
    overflow: hidden;
}

.mobile-submenu.open {
    grid-template-rows: 1fr;
}

.mobile-sub-toggle .mobile-sub-chevron {
    transition: transform 0.3s ease;
}

.mobile-sub-toggle[aria-expanded="true"] .mobile-sub-chevron {
    transform: rotate(180deg);
}

/* Cards */
.service-card {
    background: linear-gradient(145deg, rgba(124, 58, 237, 0.08), rgba(168, 85, 247, 0.04));
    border: 1px solid rgba(124, 58, 237, 0.2);
    transition: all 0.4s ease;
}

.service-card:hover {
    border-color: rgba(168, 85, 247, 0.6);
    transform: translateY(-8px);
    box-shadow: 0 20px 60px rgba(124, 58, 237, 0.25);
    background: linear-gradient(145deg, rgba(124, 58, 237, 0.15), rgba(168, 85, 247, 0.08));
}

/* Buttons */
.btn-primary {
    background: linear-gradient(135deg, #7C3AED, #A855F7);
    position: relative;
    overflow: hidden;
    transition: all 0.3s ease;
    -webkit-transform: translateZ(0);
    transform: translateZ(0);
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
    isolation: isolate;
}

.btn-primary::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(135deg, #A855F7, #7C3AED);
    opacity: 0;
    transition: opacity 0.3s ease;
}

.btn-primary:hover::before {
    opacity: 1;
}

.btn-primary:hover {
    box-shadow: 0 10px 40px rgba(124, 58, 237, 0.5);
    transform: translateY(-2px);
}

.btn-primary span {
    position: relative;
    z-index: 1;
}

.btn-outline {
    border: 1px solid rgba(168, 85, 247, 0.5);
    transition: all 0.3s ease;
}

.btn-outline:hover {
    border-color: #A855F7;
    background: rgba(168, 85, 247, 0.1);
    box-shadow: 0 0 20px rgba(168, 85, 247, 0.2);
}

/* Section tag */
.section-tag {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    background: rgba(124, 58, 237, 0.15);
    border: 1px solid rgba(168, 85, 247, 0.3);
    /* #A855F7 measured 4.41:1 against this pill's own background — just under
       the 4.5:1 AA needs at this size. #C084FC is the same purple the site
       already uses for inline links, and clears it at 6.6:1. */
    color: #C084FC;
    padding: 6px 16px;
    border-radius: 100px;
    /* 12px uppercase monospace was the smallest readable thing on the page and
       lost the argument; 13px matches the retuned `text-xs` step. */
    font-size: 0.8125rem;
    font-family: 'Space Mono', monospace;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* Floating WhatsApp */
.whatsapp-float {
    position: fixed;
    bottom: 80px;
    right: 24px;
    z-index: 999;
    width: 52px;
    height: 52px;
    background: #25D366;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 20px rgba(37, 211, 102, 0.4);
    transition: all 0.3s ease;
    animation: float 3s ease-in-out infinite;
}

.whatsapp-float:hover {
    transform: scale(1.1);
    box-shadow: 0 8px 30px rgba(37, 211, 102, 0.5);
}

/* Floating Call */
.call-float {
    position: fixed;
    bottom: 140px;
    right: 24px;
    z-index: 999;
    width: 52px;
    height: 52px;
    background: linear-gradient(135deg, #7C3AED, #A855F7);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 20px rgba(124, 58, 237, 0.45);
    transition: all 0.3s ease;
    animation: float 3s ease-in-out infinite;
}

.call-float:hover {
    transform: scale(1.1);
    box-shadow: 0 8px 30px rgba(124, 58, 237, 0.6);
}

/* Scroll to top */
.scroll-top {
    position: fixed;
    bottom: 24px;
    right: 24px;
    z-index: 999;
    width: 44px;
    height: 44px;
    background: linear-gradient(135deg, #7C3AED, #A855F7);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    pointer-events: none;
    transition: all 0.3s ease;
    box-shadow: 0 4px 20px rgba(124, 58, 237, 0.4);
}

.scroll-top.visible {
    opacity: 1;
    pointer-events: all;
}

.scroll-top:hover {
    transform: translateY(-3px);
    box-shadow: 0 8px 30px rgba(124, 58, 237, 0.5);
}

/* ── Scroll-triggered contact popup ── */
#scroll-popup {
    position: fixed;
    inset: 0;
    z-index: 9990;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    background: rgba(7, 5, 15, 0.72);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.3s ease, visibility 0s linear 0.3s;
}

#scroll-popup.open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.3s ease;
}

.scroll-popup-panel {
    position: relative;
    width: 100%;
    max-width: 460px;
    max-height: 88vh;
    overflow-y: auto;
    background: linear-gradient(145deg, #1A1530, #0D0B1A);
    border: 1px solid rgba(168, 85, 247, 0.28);
    border-radius: 14px;
    padding: 30px 26px 28px;
    box-shadow: 0 30px 80px rgba(3, 2, 14, 0.6);
    transform: translateY(16px) scale(0.97);
    transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}

#scroll-popup.open .scroll-popup-panel {
    transform: translateY(0) scale(1);
}

.scroll-popup-close {
    position: absolute;
    top: 14px;
    right: 14px;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: rgba(124, 58, 237, 0.18);
    border: 1px solid rgba(168, 85, 247, 0.3);
    color: #C2C9D4;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.25s ease;
}

.scroll-popup-close:hover {
    color: #fff;
    background: rgba(124, 58, 237, 0.35);
}

/* Mobile menu — left drawer */
#mobile-menu {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 75vw;
    max-width: 300px;
    background: rgba(13, 11, 26, 0.97);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-right: 1px solid rgba(124, 58, 237, 0.2);
    z-index: 9999;
    transform: translateX(-100%);
    transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
    overflow-y: auto;
    padding: 80px 20px 32px;
    display: flex !important;
    flex-direction: column;
    gap: 4px;
}

#mobile-menu.open {
    transform: translateX(0);
}

#mobile-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(2px);
    z-index: 9998;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.32s ease;
}

#mobile-overlay.open {
    opacity: 1;
    pointer-events: all;
}

/* Faint star scatter behind individual sections. Was a 40px lattice of
   purple dots; it is now the same dust tile the sky uses, spread thinner so a
   section sitting over the global star field gains texture rather than a
   second, competing starfield. See the star field block further down. */
.star-dust {
    background-image: var(--stars-dust);
    background-size: 260px 260px;
}

/* Form inputs */
.form-field {
    background: rgba(124, 58, 237, 0.08);
    border: 1px solid rgba(124, 58, 237, 0.25);
    color: #E5E7EB;
    transition: all 0.3s ease;
}

.form-field:focus {
    outline: none;
    border-color: #A855F7;
    background: rgba(124, 58, 237, 0.12);
    box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.1);
}

.form-field::placeholder {
    /* Placeholders are still hints, but at 3.9:1 they were unreadable against
       the field's dark fill. Tracks gray-500, so they stay clearly secondary
       while sitting well clear of the threshold. */
    color: #A9B1BF;
}

/* Blob */
.blob {
    border-radius: 62% 47% 82% 35% / 45% 45% 80% 66%;
    background: radial-gradient(ellipse, rgba(124, 58, 237, 0.3), rgba(168, 85, 247, 0.1), transparent);
    filter: blur(60px);
    contain: layout style;
    will-change: transform;
}

/* Animate on scroll */
.aos-init {
    opacity: 0;
    transform: translateY(30px);
    transition: opacity 0.7s ease, transform 0.7s ease;
}

.aos-animate {
    opacity: 1;
    transform: none;
}

/* ── Above-the-fold reveal ──
   Hero content must not wait for the IntersectionObserver. An element
   sitting at opacity:0 until JS runs is not counted as a contentful
   paint, which pushed LCP out by the entire script-execution window.
   Inside .hero-reveal the same motion runs on the CSS timeline, so the
   heading is painted as soon as the stylesheet is applied. */
.hero-reveal .aos-init {
    animation: heroReveal 0.7s ease both;
}

@keyframes heroReveal {
    from { opacity: 0; transform: translateY(30px); }
    to   { opacity: 1; transform: none; }
}

.hero-reveal .aos-init[data-delay="90"]  { animation-delay: 90ms; }
.hero-reveal .aos-init[data-delay="100"] { animation-delay: 100ms; }
.hero-reveal .aos-init[data-delay="130"] { animation-delay: 130ms; }
.hero-reveal .aos-init[data-delay="140"] { animation-delay: 140ms; }
.hero-reveal .aos-init[data-delay="150"] { animation-delay: 150ms; }
.hero-reveal .aos-init[data-delay="160"] { animation-delay: 160ms; }
.hero-reveal .aos-init[data-delay="170"] { animation-delay: 170ms; }
.hero-reveal .aos-init[data-delay="180"] { animation-delay: 180ms; }
.hero-reveal .aos-init[data-delay="200"] { animation-delay: 200ms; }
.hero-reveal .aos-init[data-delay="240"] { animation-delay: 240ms; }
.hero-reveal .aos-init[data-delay="250"] { animation-delay: 250ms; }
.hero-reveal .aos-init[data-delay="310"] { animation-delay: 310ms; }
.hero-reveal .aos-init[data-delay="380"] { animation-delay: 380ms; }

/* content-visibility: auto defers paint+layout of off-screen sections
   until the user scrolls near them — biggest single win for TBT/TTI */
.cv-auto {
    content-visibility: auto;
    contain-intrinsic-size: 0 600px;
}

/* Active nav link */
.nav-link {
    position: relative;
}

.nav-link::after {
    content: '';
    position: absolute;
    bottom: -4px;
    left: 0;
    width: 0;
    height: 2px;
    background: linear-gradient(90deg, #7C3AED, #A855F7);
    transition: width 0.3s ease;
}

.nav-link:hover::after, .nav-link.active::after {
    width: 100%;
}

.mobile-nav-link.active {
    color: #FFFFFF;
    background: rgba(124, 58, 237, 0.22);
    box-shadow: inset 3px 0 0 #A855F7;
}

.contact-nav-link.active {
    box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.75), 0 10px 40px rgba(124, 58, 237, 0.5);
}

/* Testimonial card */
.testimonial-card {
    background: linear-gradient(145deg, rgba(124, 58, 237, 0.08), rgba(13, 11, 26, 0.9));
    border: 1px solid rgba(124, 58, 237, 0.2);
}

/* Star rating */
.stars {
    color: #F59E0B;
}

/* ── FAQ accordions ──
   Native <details>, so every FAQ block on the site opens with zero
   JavaScript and its answer text is in the HTML for crawlers even when
   the panel is closed. Used by the home, services, projects, contact
   and service-detail pages. */
.faq-item summary { list-style: none; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::marker { content: ""; }

.faq-item .faq-chevron { transition: transform 0.3s ease; }
.faq-item[open] .faq-chevron { transform: rotate(180deg); }
.faq-item[open] { border-color: rgba(168, 85, 247, 0.45); }
.faq-item[open] summary .faq-question { color: #fff; }

/* Highlight line */
.highlight-line {
    width: 60px;
    height: 3px;
    background: linear-gradient(90deg, #7C3AED, #A855F7);
    border-radius: 2px;
}

/* Premium global interaction layer */
::selection {
    background: rgba(168, 85, 247, 0.35);
    color: #FFFFFF;
}

body {
    overflow-x: hidden;
}

/* ── Cursors ──
   The familiar Windows arrow and pointing hand, recoloured to the brand.
   Shape and behaviour stay native, so what is clickable is never in
   doubt: purple edge = normal, gold hand = you can click this.
   Every rule keeps the standard keyword as a fallback. */
:root {
    --cursor-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M1.5 1.5 L1.5 19.8 L6.1 15.4 L9.2 22.3 L12.3 20.9 L9.3 14.2 L15.8 14 Z' fill='%23ffffff' stroke='%237C3AED' stroke-width='1.6' stroke-linejoin='round'/%3E%3C/svg%3E") 2 2;
    --cursor-hand: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M8.2 2.4C8.2 1.4 9 .6 10 .6s1.8.8 1.8 1.8v7.8c0-.8.6-1.4 1.4-1.4s1.4.6 1.4 1.4v1c0-.7.6-1.3 1.3-1.3s1.4.6 1.4 1.3v1c0-.6.5-1.1 1.2-1.1.8 0 1.4.6 1.4 1.4v4.8c0 3.6-2.9 6.5-6.5 6.5h-1.7c-1.7 0-3.3-.7-4.4-2l-4.2-4.8c-.6-.7-.5-1.7.2-2.3.6-.5 1.6-.5 2.2.1l3 2.8Z' fill='%23ffffff' stroke='%23F59E0B' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E") 10 1;
}

body {
    cursor: var(--cursor-arrow), default;
}

a[href],
button,
summary,
label[for],
select,
[role="button"],
[data-magnetic],
input[type="submit"],
input[type="button"],
input[type="checkbox"],
input[type="radio"] {
    cursor: var(--cursor-hand), pointer;
}

/* Typing beats decoration — leave text fields on the native I-beam. */
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="url"],
input[type="search"],
input[type="password"],
input[type="number"],
textarea {
    cursor: text;
}

a[href]:disabled,
button:disabled,
[aria-disabled="true"],
[disabled] {
    cursor: not-allowed;
}

#site-header.is-scrolled .nav-glass {
    background: rgba(7, 5, 15, 0.9);
    border-bottom-color: rgba(168, 85, 247, 0.28);
    box-shadow: 0 16px 50px rgba(0, 0, 0, 0.24);
}

.nav-glass {
    transition: background 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
}

/* ── Ambient star field ───────────────────────────────────────────────────
   Replaces the 72px square grid that used to sit behind the whole site.

   The entire sky is tiled background-images on one element plus two
   pseudo-elements: no DOM nodes, no JavaScript, no image request, and
   nothing that can reflow. Each tile is an inline SVG, which is how the
   noise overlay and the brand cursors are already done here.

   Three tiles at three different sizes (200 / 340 / 560) rather than one:
   a single tile repeats into a visible lattice, which is precisely the
   regularity we are getting away from. Because the sizes do not divide into
   one another the combined pattern only truly repeats every few thousand
   pixels, and the size difference reads as depth — dense faint dust behind,
   fewer mid stars, a handful of bright ones with halos in front.

   Declared on :root because .star-dust reuses the same dust tile.

   ── Why every tile is wrapped in a dimming group ──
   Text sits over this, so the sky has a hard ceiling: a star is a patch of
   white laid over the page background, and where one lands behind a glyph it
   lifts the local background and eats the text's contrast.

   Solving each text colour in use for "still 4.5:1 with a star behind it"
   gives, in white alpha: #929BAA 0.158, #C084FC 0.175, #A9B1BF 0.229,
   #C2C9D4 0.297, #D1D5DB 0.329, #E5E7EB 0.377. The binding one is #929BAA
   at 0.158 — note that is NOT the colour with the lowest contrast on the
   bare background (#C084FC is), because how fast a colour loses contrast as
   the background lifts depends on its own luminance, not on where it
   started. Whole sky is held under 0.158.

   The tiles were drawn with peaks of 0.48 / 0.72 / 1.0 — three to six times
   over. Rather than hand-editing fifty-odd opacity attributes, each tile is
   wrapped in one `<g opacity>` that multiplies down the whole tile, so the
   factor is a single readable number per layer and the relative brightness
   inside each tile is preserved.

   The factors were picked by rasterising each tile and measuring its
   brightest pixel, not by multiplying the numbers: a circle smaller than a
   pixel antialiases well below its nominal alpha, and the rasteriser does
   not otherwise land where the arithmetic says it should. Measured peaks are
   0.114 dust, 0.145 mid, 0.149 bright, 0.149 twinkle — all under 0.158, so
   every text colour on the site holds 4.5:1 even in the worst case of a star
   core landing directly behind a glyph stem. A star at 0.15 still reads at
   1.5:1 against the page background, which for a point of light is plenty.

   Depth is now carried by star size and the halos rather than by alpha,
   which is the right way round anyway for something meant to sit behind
   words. */
:root {
    --stars-dust: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cg opacity='.35'%3E%3Cg fill='%23fff'%3E%3Ccircle cx='14' cy='9' r='.6' opacity='.40'/%3E%3Ccircle cx='37' cy='52' r='.5' opacity='.28'/%3E%3Ccircle cx='63' cy='21' r='.7' opacity='.46'/%3E%3Ccircle cx='88' cy='77' r='.55' opacity='.32'/%3E%3Ccircle cx='109' cy='14' r='.6' opacity='.38'/%3E%3Ccircle cx='131' cy='61' r='.5' opacity='.26'/%3E%3Ccircle cx='158' cy='33' r='.65' opacity='.44'/%3E%3Ccircle cx='183' cy='88' r='.55' opacity='.30'/%3E%3Ccircle cx='22' cy='101' r='.6' opacity='.36'/%3E%3Ccircle cx='48' cy='140' r='.5' opacity='.24'/%3E%3Ccircle cx='71' cy='118' r='.7' opacity='.48'/%3E%3Ccircle cx='96' cy='169' r='.55' opacity='.30'/%3E%3Ccircle cx='118' cy='132' r='.6' opacity='.40'/%3E%3Ccircle cx='144' cy='151' r='.5' opacity='.26'/%3E%3Ccircle cx='171' cy='113' r='.65' opacity='.42'/%3E%3Ccircle cx='191' cy='146' r='.55' opacity='.30'/%3E%3Ccircle cx='8' cy='63' r='.5' opacity='.25'/%3E%3Ccircle cx='55' cy='86' r='.6' opacity='.35'/%3E%3Ccircle cx='79' cy='45' r='.5' opacity='.27'/%3E%3Ccircle cx='126' cy='95' r='.6' opacity='.37'/%3E%3Ccircle cx='152' cy='74' r='.5' opacity='.25'/%3E%3Ccircle cx='177' cy='58' r='.6' opacity='.34'/%3E%3Ccircle cx='33' cy='178' r='.55' opacity='.29'/%3E%3Ccircle cx='100' cy='191' r='.5' opacity='.24'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");

    --stars-mid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='340' height='340'%3E%3Cg opacity='.17'%3E%3Cg fill='%23fff'%3E%3Ccircle cx='28' cy='44' r='1.1' opacity='.62'/%3E%3Ccircle cx='91' cy='17' r='.95' opacity='.50'/%3E%3Ccircle cx='146' cy='73' r='1.25' opacity='.72'/%3E%3Ccircle cx='203' cy='31' r='1' opacity='.55'/%3E%3Ccircle cx='259' cy='88' r='1.15' opacity='.66'/%3E%3Ccircle cx='312' cy='49' r='.95' opacity='.48'/%3E%3Ccircle cx='61' cy='129' r='1.2' opacity='.70'/%3E%3Ccircle cx='122' cy='166' r='1' opacity='.54'/%3E%3Ccircle cx='188' cy='140' r='1.1' opacity='.60'/%3E%3Ccircle cx='245' cy='197' r='.95' opacity='.47'/%3E%3Ccircle cx='301' cy='158' r='1.2' opacity='.68'/%3E%3Ccircle cx='39' cy='231' r='1' opacity='.52'/%3E%3Ccircle cx='97' cy='285' r='1.15' opacity='.64'/%3E%3Ccircle cx='163' cy='246' r='.95' opacity='.46'/%3E%3Ccircle cx='228' cy='299' r='1.1' opacity='.60'/%3E%3Ccircle cx='289' cy='262' r='1' opacity='.50'/%3E%3C/g%3E%3Ccircle cx='170' cy='320' r='1.2' fill='%23d8c9ff' opacity='.60'/%3E%3C/g%3E%3C/svg%3E");

    --stars-bright: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='560' height='560'%3E%3Cg opacity='.15'%3E%3Cg fill='%23fff' opacity='.07'%3E%3Ccircle cx='47' cy='88' r='5'/%3E%3Ccircle cx='163' cy='41' r='4'/%3E%3Ccircle cx='271' cy='152' r='5.5'/%3E%3Ccircle cx='388' cy='79' r='4'/%3E%3Ccircle cx='476' cy='211' r='5'/%3E%3Ccircle cx='92' cy='301' r='4.5'/%3E%3Ccircle cx='218' cy='398' r='5'/%3E%3Ccircle cx='344' cy='489' r='4'/%3E%3Ccircle cx='505' cy='412' r='4.5'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Ccircle cx='47' cy='88' r='1.7' opacity='.95'/%3E%3Ccircle cx='163' cy='41' r='1.4' opacity='.85'/%3E%3Ccircle cx='271' cy='152' r='1.9' opacity='1'/%3E%3Ccircle cx='388' cy='79' r='1.4' opacity='.82'/%3E%3Ccircle cx='92' cy='301' r='1.5' opacity='.88'/%3E%3Ccircle cx='218' cy='398' r='1.8' opacity='.95'/%3E%3Ccircle cx='344' cy='489' r='1.4' opacity='.80'/%3E%3C/g%3E%3Ccircle cx='476' cy='211' r='1.8' fill='%23e9dcff' opacity='.90'/%3E%3Ccircle cx='505' cy='412' r='1.5' fill='%23fcd9a0' opacity='.80'/%3E%3C/g%3E%3C/svg%3E");

    --stars-twinkle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cg opacity='.15'%3E%3Cg fill='%23fff'%3E%3Ccircle cx='66' cy='122' r='1.3'/%3E%3Ccircle cx='214' cy='58' r='1.1'/%3E%3Ccircle cx='351' cy='188' r='1.4'/%3E%3Ccircle cx='128' cy='327' r='1.2'/%3E%3Ccircle cx='299' cy='412' r='1.3'/%3E%3Ccircle cx='437' cy='291' r='1.1'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}

/* No `position` here on purpose: base.css loads after tailwind.min.css, so a
   position declaration would outrank the `absolute` / `fixed` utility every
   call site already carries and tear the layer off its container. */
.star-field {
    background-image: var(--stars-bright), var(--stars-mid), var(--stars-dust);
    background-size: 560px 560px, 340px 340px, 200px 200px;
    background-repeat: repeat;
}

/* ── The milky way ──
   Only on the one global layer in base.html, never on the per-section star
   fields: the same band at the same angle repeating down the page would read
   as a texture rather than as a sky.

   The band is one rotated pseudo-element carrying its own, denser scatter of
   stars plus a soft glow, the whole thing clipped by an elongated elliptical
   mask so it fades out at the edges instead of stopping. Deliberately no
   filter: blur() — a wide gradient is already soft, and a blurred layer the
   size of the viewport is the most expensive thing a phone GPU could be
   asked to do here (see the mobile paint budget note further down). */
.star-field--galaxy {
    overflow: hidden;
}

.star-field--galaxy::before {
    content: '';
    position: absolute;
    inset: -30%;
    transform: rotate(-24deg);
    background-image:
        var(--stars-mid),
        var(--stars-dust),
        /* The glow is a wash across a wide area rather than a pin-prick, so it
           lifts the background under whole paragraphs rather than single
           glyphs — held well under the star ceiling for that reason. */
        linear-gradient(90deg, transparent, rgba(214, 201, 255, 0.065) 22%, rgba(168, 85, 247, 0.055) 50%, rgba(214, 201, 255, 0.065) 78%, transparent);
    background-size: 280px 280px, 160px 160px, 100% 100%;
    -webkit-mask-image: radial-gradient(ellipse 70% 16% at 50% 50%, #000 0 42%, rgba(0, 0, 0, 0.35) 70%, transparent 100%);
    mask-image: radial-gradient(ellipse 70% 16% at 50% 50%, #000 0 42%, rgba(0, 0, 0, 0.35) 70%, transparent 100%);
    pointer-events: none;
}

/* A handful of stars that breathe. Opacity only, so it stays on the
   compositor and never repaints; the global prefers-reduced-motion rule
   further down settles it at its dim end. */
.star-field--galaxy::after {
    content: '';
    position: absolute;
    inset: 0;
    background-image: var(--stars-twinkle);
    background-size: 480px 480px;
    animation: starTwinkle 6.5s ease-in-out infinite;
    pointer-events: none;
}

@keyframes starTwinkle {
    0%, 100% { opacity: 0.3; }
    50%      { opacity: 0.95; }
}

/* Hero only: one dust layer drifting behind the static field, which is what
   sells the sky as something you are moving through. translateY on a child
   wrapper rather than background-position on the parent, so the browser can
   composite it on the GPU without repainting. The travel distance is exactly
   one tile height, so the loop is seamless. */
.star-field-inner {
    position: absolute;
    inset: -200px 0;
    background-image: var(--stars-dust);
    background-size: 200px 200px;
    will-change: transform;
    animation: starDrift 150s linear infinite;
}

@keyframes starDrift {
    from { transform: translateY(0); }
    to   { transform: translateY(200px); }
}

.perspective-stage {
    perspective: 1300px;
}

.preserve-3d {
    transform-style: preserve-3d;
}

.translate-z-1 {
    transform: translateZ(20px);
}

.translate-z-2 {
    transform: translateZ(42px);
}

.glass-panel {
    background: linear-gradient(145deg, rgba(35, 30, 61, 0.82), rgba(13, 11, 26, 0.64));
    border: 1px solid rgba(168, 85, 247, 0.2);
    box-shadow: 0 24px 70px rgba(3, 2, 14, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.07);
    backdrop-filter: blur(24px);
    -webkit-backdrop-filter: blur(24px);
}

.tech-border {
    position: relative;
}

.tech-border::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: 1px;
    background: linear-gradient(135deg, rgba(168, 85, 247, 0.7), rgba(124, 58, 237, 0.08), rgba(245, 158, 11, 0.34));
    -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    pointer-events: none;
}

.service-card,
.testimonial-card {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    transform-style: preserve-3d;
    will-change: transform;
}

.service-card::before,
.testimonial-card::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    opacity: 0;
    background: radial-gradient(420px circle at var(--pointer-x, 50%) var(--pointer-y, 50%), rgba(168, 85, 247, 0.18), transparent 42%);
    transition: opacity 0.35s ease;
    pointer-events: none;
}

.service-card:hover::before,
.testimonial-card:hover::before {
    opacity: 1;
}

.service-card.tilt-ready,
.testimonial-card.tilt-ready,
[data-tilt].tilt-ready {
    transition: transform 0.18s ease-out, border-color 0.35s ease, box-shadow 0.35s ease, background 0.35s ease;
}

.btn-primary::after {
    content: '';
    position: absolute;
    top: -30%;
    bottom: -30%;
    left: -42%;
    width: 34%;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.28), transparent);
    transform: skewX(-18deg);
    transition: transform 0.65s ease;
}

.btn-primary:hover::after {
    transform: translateX(470%) skewX(-18deg);
}

.scan-line {
    background: linear-gradient(90deg, transparent, rgba(168, 85, 247, 0.75), transparent);
    filter: blur(0.5px);
}

.ambient-orb {
    filter: blur(74px);
    will-change: transform;
    contain: layout style;
}

.code-window-grid {
    background-image: radial-gradient(rgba(168, 85, 247, 0.2) 1px, transparent 1px);
    background-size: 18px 18px;
}

.text-balance {
    text-wrap: balance;
}

/* ── Mobile paint budget ──
   The decorative background is the most expensive thing a phone GPU
   does here: several large blur() layers plus every frosted panel's
   backdrop-filter, each of which forces the compositor to snapshot and
   blur what sits behind it. These radii are already past the point of
   visible difference at phone size, so smaller ones look the same and
   cost a fraction. Desktop is untouched. */
@media (max-width: 767px) {
    .ambient-orb {
        filter: blur(38px);
        will-change: auto;
    }

    .blob {
        filter: blur(32px);
        will-change: auto;
    }

    /* Follows the pointer — there is no pointer on a touch screen.
       It was only opacity:0, which still costs a blurred layer. */
    #ambient-pointer {
        display: none;
    }

    .nav-glass,
    .glass-panel {
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
    }
}

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

/*
 * Touch devices do not need hover-only CTA animation.
 * Desktop hover behaviour remains unchanged.
 */
@media (hover: none), (pointer: coarse) {
    .btn-primary:hover {
        transform: none;
        box-shadow: none;
    }

    .btn-primary:hover::before {
        opacity: 0;
    }

    .btn-primary:hover::after {
        transform: skewX(-18deg);
    }
}
