/* ----------------------------------------------------------------
   Gapfy website - global layout and section styles.
   Brand tokens live in brand.css and drive everything in here.
   ---------------------------------------------------------------- */

html, body {
    margin: 0;
    padding: 0;
    background: var(--gapfy-bg);
    color: var(--gapfy-ink);
    font-family: var(--gapfy-font-sans);
    font-size: var(--gapfy-text-base);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

a {
    color: var(--gapfy-primary);
    text-decoration: none;
    transition: color var(--gapfy-dur-fast) var(--gapfy-ease-out);
}

a:hover {
    color: var(--gapfy-primary-strong);
}

h1, h2, h3, h4, h5, h6 {
    font-family: var(--gapfy-font-display);
    color: var(--gapfy-ink);
    margin: 0;
    line-height: 1.2;
    letter-spacing: -0.02em;
}

p {
    margin: 0;
}

/* --- Container ----------------------------------------------------- */

.site-container {
    width: 100%;
    max-width: var(--gapfy-container-max);
    margin: 0 auto;
    padding: 0 1.5rem;
    box-sizing: border-box;
}

/* --- Navbar -------------------------------------------------------- */

.site-navbar {
    position: sticky;
    top: 0;
    z-index: 60;
    background: var(--gapfy-navbar-bg);
    backdrop-filter: saturate(180%) blur(12px);
    height: var(--gapfy-header-height);
    display: flex;
    align-items: center;
    /* No bottom border at rest — the navbar floats over the hero with
       no hard line. Shadow appears only after the user scrolls past
       the small threshold (handled by navbar-scroll.js). */
    border-bottom: 1px solid transparent;
    transition: border-color var(--gapfy-dur-base) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out);
}

.site-navbar.is-scrolled {
    border-bottom-color: var(--gapfy-line);
    box-shadow: 0 6px 20px rgba(9, 30, 66, 0.06);
}

.site-navbar-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
}

.site-brand {
    display: inline-flex;
    align-items: center;
}

/* Official Gapfy horizontal lockup, inlined as SVG in the navbar (see
   SiteNavbar.razor) so the wordmark can follow the theme. The blue mark,
   the white "G" and the blue accent keep their fixed brand colours; only
   the wordmark paths use fill: currentColor, pinned here to --gapfy-ink
   so the text is dark navy in light mode and near-white in dark — and
   stays put regardless of the link's :hover colour. */
.site-brand-logo {
    height: 32px;
    width: auto;
    display: block;
    color: var(--gapfy-ink);
}

.site-brand-mark {
    width: 32px;
    height: 32px;
    border-radius: 8px;
    background: var(--gapfy-gradient-primary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #FFFFFF;
    font-weight: 700;
}

/* Real Gapfy mark (img) used by the navbar and footer brands. Replaces
   the CSS-drawn .site-brand-mark "G" square; the rounded corners come
   baked into the PNG itself. */
.site-brand-icon {
    width: 32px;
    height: 32px;
    border-radius: 8px;
    display: block;
}

.site-nav-links {
    display: flex;
    align-items: center;
    gap: 2rem;
    list-style: none;
    margin: 0;
    padding: 0;
}

.site-nav-link {
    color: var(--gapfy-ink-soft);
    font-weight: 500;
    font-size: var(--gapfy-text-sm);
    cursor: pointer;
    padding: 0.5rem 0;
    position: relative;
    background: none;
    border: none;
    font-family: inherit;
    transition: color var(--gapfy-dur-fast) var(--gapfy-ease-out);
}

.site-nav-link:hover,
.site-nav-link.active {
    color: var(--gapfy-primary);
}

/* Animated underline — Stripe / Linear style. Only on plain text
   nav links: NOT on dropdown triggers (chevron already does the job),
   NOT on the theme switcher (it's a circular icon button and the
   underline reads as a "circle with whiskers"), NOT on the culture
   switcher trigger (same circular-pill button reasoning). Uses
   transform: scaleX so the GPU does the work instead of width. */
.site-nav-link:not(.site-nav-dropdown-trigger):not(.theme-switcher):not(.culture-switcher-trigger)::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background: var(--gapfy-primary);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--gapfy-dur-base) var(--gapfy-ease-out);
    border-radius: 1px;
}

.site-nav-link:not(.site-nav-dropdown-trigger):not(.theme-switcher):not(.culture-switcher-trigger):hover::after,
.site-nav-link:not(.site-nav-dropdown-trigger):not(.theme-switcher):not(.culture-switcher-trigger).active::after {
    transform: scaleX(1);
}

.site-nav-dropdown {
    position: relative;
}

.site-nav-dropdown-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.site-nav-dropdown-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 50%;
    transform: translateX(-50%);
    min-width: 240px;
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    box-shadow: var(--gapfy-shadow-lg);
    padding: 0.75rem;
    display: none;
}

/* Invisible "bridge" that fills the 0.5rem gap between the trigger and the
   panel, so the cursor can travel from the button into the menu without
   leaving the .site-nav-dropdown hover area and closing the dropdown. */
.site-nav-dropdown-panel::before {
    content: "";
    position: absolute;
    top: -0.5rem;
    left: 0;
    right: 0;
    height: 0.5rem;
    background: transparent;
}

.site-nav-dropdown:hover .site-nav-dropdown-panel,
.site-nav-dropdown:focus-within .site-nav-dropdown-panel {
    display: block;
}

.site-nav-dropdown-item {
    position: relative;
    /* isolation establishes a stacking context so the gradient
       pseudo (z-index: -1) sits behind the text/icon but never
       slips behind the panel surface itself. */
    isolation: isolate;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    border-radius: var(--gapfy-radius);
    color: var(--gapfy-ink-soft);
    font-size: var(--gapfy-text-sm);
    font-weight: 500;
    transition: color var(--gapfy-dur-base) var(--gapfy-ease-out);
}

/* Gradient wash that fades in on hover. It lives in a pseudo so the
   gradient can animate (background-image itself is not transitionable —
   only the pseudo's opacity is). Soft brand-blue -> violet echoes the
   hero mesh, kept low-opacity so the title stays crisp. The rgba accent
   stops read the same over the light tint and the dark elevated surface,
   so no separate dark-mode rule is needed. */
.site-nav-dropdown-item::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: linear-gradient(120deg,
        rgba(61, 166, 221, 0.18) 0%,
        rgba(124, 108, 255, 0.14) 100%);
    opacity: 0;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out);
}

.site-nav-dropdown-item:hover,
.site-nav-dropdown-item:focus-visible {
    color: var(--gapfy-primary);
}

.site-nav-dropdown-item:hover::before,
.site-nav-dropdown-item:focus-visible::before {
    opacity: 1;
}

.site-nav-dropdown-item .material-symbols-outlined {
    color: var(--gapfy-primary);
    font-size: 20px;
}

/* Product brand icons inside the Products dropdown. object-fit keeps
   the non-square marks (Maestro is wider than tall) centred in a
   consistent 24px slot so the text column lines up across items.
   Top-aligned (not centred) so the icon sits next to the product
   title line instead of floating beside the description. */
.site-nav-product-icon {
    width: 24px;
    height: 24px;
    object-fit: contain;
    flex-shrink: 0;
    align-self: flex-start;
    margin-top: 1px;
}

/* Service icons — Services dropdown. The Products menu has real brand
   marks; Services only has generic function glyphs, which (being mono and
   vertically centred) read weightless and drift to the middle when a
   title wraps to two lines. Rather than box them in a tile, we paint the
   glyph itself with a brand gradient (background-clip: text) and top-align
   it — so it stays light and keeps the same ~24px footprint as the product
   icons. The per-service gradient walks the hero-mesh palette down the
   menu: blue (foundations) -> violet (people) -> magenta (launch), so the
   three read as one coherent brand arc.

   Base lives on .site-nav-dropdown-item .site-nav-service-icon — same
   (0,2,0) specificity as the generic glyph rule above, so it wins on
   source order. -webkit-text-fill-color is what reveals the gradient;
   where it is unsupported the glyph falls back to the solid
   --gapfy-primary from that rule, so the icon is never invisible. */
.site-nav-dropdown-item .site-nav-service-icon {
    align-self: flex-start;
    margin-top: 1px;
    font-size: 24px;
    line-height: 1;
    /* The global .material-symbols-outlined rule (further down) caps every
       glyph at a 1em box with overflow: hidden. That is harmless for the
       solid-fill icons elsewhere, but it clips a gradient glyph: the part
       of the shape that falls outside the 1em box gets no gradient (and is
       cut by overflow), so the icon renders sliced. Let this one size to
       its own glyph and overflow freely so the whole shape is painted. */
    width: auto;
    height: auto;
    overflow: visible;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

.site-nav-service-icon--consulting {
    background-image: linear-gradient(135deg, var(--gapfy-accent-sky) 0%, var(--gapfy-primary-strong) 100%);
}

.site-nav-service-icon--nearshore {
    background-image: linear-gradient(135deg, var(--gapfy-primary) 0%, var(--gapfy-accent-violet) 100%);
}

.site-nav-service-icon--delivery {
    background-image: linear-gradient(135deg, var(--gapfy-accent-violet) 0%, var(--gapfy-accent-magenta) 100%);
}

/* Navbar CTA — mirrors .btn-primary's gradient-on-hover treatment so
   the two principal CTAs (hero "Start a project" and navbar "Sign
   up free") share the same visual language. Solid brand blue at
   rest, accent gradient on hover, cross-faded via the ::before
   overlay (background-image gradients are not animatable). */
.site-nav-cta {
    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.625rem 1.25rem;
    border-radius: var(--gapfy-radius-pill);
    background-color: var(--gapfy-primary);
    color: #FFFFFF !important;
    font-weight: 600;
    font-size: var(--gapfy-text-sm);
    transition: transform var(--gapfy-dur-fast) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out);
    box-shadow: 0 2px 6px rgba(61, 166, 221, 0.3);
}

/* Accent gradient overlay — same opacity cross-fade as
   .btn-primary::before so both CTAs reveal the gradient identically. */
.site-nav-cta::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background-image: var(--gapfy-gradient-accent);
    opacity: 0;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out);
}

/* No translateY on the navbar CTA hover — the button sits inline with
   the other nav items, so a 1px lift creates visible misalignment
   with neighbours. The gradient reveal + shadow alone carry the hover
   feedback. The hero / page CTAs keep the lift via .btn-primary. */
.site-nav-cta:hover {
    box-shadow: 0 10px 24px rgba(124, 108, 255, 0.32);
}

.site-nav-cta:hover::before {
    opacity: 1;
}

.site-nav-mobile-toggle {
    display: none;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0.5rem;
    color: var(--gapfy-ink);
    z-index: 65;          /* above the drawer + backdrop so the close icon click reaches it */
    position: relative;
}

.site-nav-mobile-toggle .material-symbols-outlined {
    transition: transform var(--gapfy-dur-base) var(--gapfy-ease-out);
}

/* Swap the icon to "close" when the drawer is open. CSS-only so JS
   does not need to mutate the icon glyph. */
.site-navbar.has-mobile-open .site-nav-mobile-toggle .material-symbols-outlined {
    transform: rotate(90deg);
}

.site-navbar.has-mobile-open .site-nav-mobile-toggle .material-symbols-outlined::before {
    content: "close";
}

/* Backdrop — injected by mobile-nav.js, sits behind the drawer panel
   and dims the page underneath. Pointer events on so a click anywhere
   closes the drawer. */
.site-nav-mobile-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(9, 30, 66, 0.45);
    backdrop-filter: blur(2px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out);
    z-index: 55;
}

.site-navbar.has-mobile-open .site-nav-mobile-backdrop {
    opacity: 1;
    pointer-events: auto;
}

/* Lock body scroll while the drawer is open so flicks on the overlay
   do not bleed into the page beneath it. */
body.has-mobile-nav-open {
    overflow: hidden;
}

/* The navbar's backdrop-filter (the glass blur) establishes a containing
   block for position:fixed descendants. The mobile drawer (.site-nav-links)
   and the backdrop are fixed children of .site-navbar, so under that
   containing block their top/right/bottom/inset resolve against the ~72px
   header box instead of the viewport — the drawer renders short and the
   backdrop only covers the header strip, leaving the page visible beneath.
   Drop the filter while the drawer is open (the open drawer covers the
   navbar anyway, so the blur is not visible) so the overlay can size to the
   full viewport. Desktop and the closed-navbar blur are untouched. */
@media (max-width: 720px) {
    .site-navbar.has-mobile-open {
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }

    /* The drawer sets width: min(85vw, 360px) plus 1.5rem side padding.
       Without a global border-box reset that padding is added on top, so
       the drawer swells to ~97vw and the backdrop barely shows. border-box
       makes min(85vw, 360px) the total width as intended, leaving the
       backdrop strip visible to the left. */
    .site-nav-links {
        box-sizing: border-box;
    }
}

/* --- Theme switcher ----------------------------------------------- */
/* Compact icon-only button next to the culture switcher. The icon
   itself flips (sun vs moon) so the affordance is obvious without a
   text label; the aria-label / title carry the localized string. */

.theme-switcher {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    /* The element also carries .site-nav-link which sets padding:
       0.5rem 0. Without resetting that here the 38x38 box grows
       vertically by 16px and the "circle" turns into a vertical
       oval. box-sizing: border-box keeps the 38x38 exact even if a
       future change adds borders or padding. */
    padding: 0;
    box-sizing: border-box;
    border-radius: var(--gapfy-radius-pill);
    border: 1px solid var(--gapfy-line);
    color: var(--gapfy-ink-soft);
    transition: border-color var(--gapfy-dur-fast) var(--gapfy-ease-out),
                color var(--gapfy-dur-fast) var(--gapfy-ease-out),
                background var(--gapfy-dur-fast) var(--gapfy-ease-out);
}

.theme-switcher:hover {
    border-color: var(--gapfy-primary);
    color: var(--gapfy-primary);
    background: var(--gapfy-bg-tint);
}

/* --- Culture switcher --------------------------------------------- */

.culture-switcher {
    position: relative;
}

.culture-switcher-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    cursor: pointer;
    background: none;
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-pill);
    padding: 0.375rem 0.75rem;
    font-family: inherit;
    color: var(--gapfy-ink-soft);
    font-size: var(--gapfy-text-sm);
    font-weight: 600;
    transition: border-color var(--gapfy-dur-fast) var(--gapfy-ease-out),
                color var(--gapfy-dur-fast) var(--gapfy-ease-out);
}

.culture-switcher-trigger:hover {
    border-color: var(--gapfy-primary);
    color: var(--gapfy-primary);
}

.culture-switcher-menu {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    min-width: 220px;
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    box-shadow: var(--gapfy-shadow-lg);
    padding: 0.375rem;
    display: none;
    z-index: 70;
}

/* Same hover-bridge trick as .site-nav-dropdown-panel - fills the
   visible gap between the trigger and the menu so the cursor stays
   inside .culture-switcher while moving down. */
.culture-switcher-menu::before {
    content: "";
    position: absolute;
    top: -0.5rem;
    left: 0;
    right: 0;
    height: 0.5rem;
    background: transparent;
}

.culture-switcher:hover .culture-switcher-menu,
.culture-switcher:focus-within .culture-switcher-menu {
    display: block;
}

.culture-switcher-item {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--gapfy-radius);
    color: var(--gapfy-ink-soft);
    font-size: var(--gapfy-text-sm);
    font-weight: 500;
}

.culture-switcher-item:hover {
    background: var(--gapfy-bg-tint);
    color: var(--gapfy-primary);
}

.culture-switcher-item.is-active {
    color: var(--gapfy-primary);
    font-weight: 600;
}

/* Flag image rendered next to the culture label in the switcher menu.
   SVG sources live in wwwroot/img/flags/<iso2>.svg; size is fixed in CSS
   so each option lines up vertically regardless of the SVG's intrinsic
   viewBox aspect ratio. */
.culture-flag {
    display: inline-block;
    width: 22px;
    height: 16px;
    border-radius: 2px;
    object-fit: cover;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
    flex-shrink: 0;
}

/* Compact variant rendered inside the trigger button (next to the chevron),
   slightly smaller so the navbar height does not grow. */
.culture-flag--inline {
    width: 18px;
    height: 13px;
}

/* Legacy class kept so any consumer that still uses the text-badge layout
   does not break visually. Safe to delete once no template references it. */
.culture-switcher-flag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 20px;
    background: var(--gapfy-primary-soft);
    color: var(--gapfy-primary-strong);
    border-radius: 4px;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.05em;
}

/* --- Hero ---------------------------------------------------------- */

.hero {
    position: relative;
    padding: 7rem 0 5rem;
    background: var(--gapfy-bg);
    overflow: hidden;
}

/* Hero children must sit above any decorative layers. */
.hero > * {
    position: relative;
    z-index: 1;
}

.hero-grid {
    display: grid;
    grid-template-columns: 1.15fr 0.85fr;
    gap: 4rem;
    align-items: center;
}

/* Stagger entrance — eyebrow, title, subtitle and CTAs fade up with
   80ms steps so the eye lands on them in order. Mesh column gets its
   own slightly later beat. Keyframes are simple: opacity + translateY
   only, GPU-friendly, no layout reflow.
   Scoped to .hero specifically because .hero-eyebrow is reused inside
   .maestro-highlight as a styled badge — we don't want that one to
   animate on every render, only the hero's. */
@keyframes gapfy-rise-in {
    0%   { opacity: 0; transform: translateY(16px); }
    100% { opacity: 1; transform: translateY(0); }
}

.hero .hero-eyebrow,
.hero .hero-title,
.hero .hero-subtitle,
.hero .hero-actions,
.hero .hero-art {
    animation: gapfy-rise-in 700ms var(--gapfy-ease-out) both;
}

.hero .hero-eyebrow   { animation-delay: 0ms; }
.hero .hero-title     { animation-delay: 80ms; }
.hero .hero-subtitle  { animation-delay: 160ms; }
.hero .hero-actions   { animation-delay: 240ms; }
.hero .hero-art       { animation-delay: 320ms; }

/* Stagger the action buttons individually on top of the parent
   .hero-actions stagger so each CTA lands one beat after the next.
   The buttons inherit the parent fade-in (transform/opacity), then
   add a 40ms internal offset between them so the eye reads them in
   order. */
.hero .hero-actions > * {
    animation: gapfy-rise-in 700ms var(--gapfy-ease-out) both;
}
.hero .hero-actions > *:nth-child(1) { animation-delay: 280ms; }
.hero .hero-actions > *:nth-child(2) { animation-delay: 340ms; }
.hero .hero-actions > *:nth-child(3) { animation-delay: 400ms; }

/* Eyebrow — Stripe-style flat text, no pill. Small, dark ink with the
   icon inline. The element still ships the pill background when used
   inside .maestro-highlight (via inline style), so the scoping below
   (.hero .hero-eyebrow) keeps that override intact. */
.hero .hero-eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0;
    background: transparent;
    color: var(--gapfy-ink-soft);
    font-size: var(--gapfy-text-sm);
    font-weight: 600;
    letter-spacing: 0.01em;
    margin-bottom: 1.75rem;
}

.hero .hero-eyebrow .material-symbols-outlined {
    color: var(--gapfy-primary);
}

/* Title + subtitle — same gigantic font-size; title is bold ink with
   one inline word in an accent gradient, subtitle is muted. Together
   they read as one editorial unit, like the Stripe hero copy. */
.hero-title {
    font-size: clamp(2.25rem, 5vw, 4.25rem);
    font-weight: 800;
    color: var(--gapfy-ink);
    margin-bottom: 0.75rem;
    letter-spacing: -0.035em;
    line-height: 1.05;
}

/* Blazor's <FocusOnNavigate Selector="h1" /> in Routes.razor focuses the
   first h1 on every navigation for screen-reader accessibility. The
   browser then paints a default focus outline around the title (very
   visible because h1 is not normally focusable). Suppress the visual
   ring on hero titles but keep the programmatic focus intact. */
.hero-title:focus,
.hero-title:focus-visible,
.page-hero-title:focus,
.page-hero-title:focus-visible {
    outline: none;
}

.hero-title-accent {
    background: var(--gapfy-gradient-accent);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

/* Subtitle picks up the same heavy weight + tight tracking as the
   title for the editorial "second paragraph" feel, but drops the
   point size so a longer copy line still fits inside the column
   without breaking awkwardly. Stripe's hero is taller and wider,
   so they get away with same-size; ours stays at ~70% of the
   title for the same readability. */
.hero-subtitle {
    font-size: clamp(1.625rem, 3.4vw, 2.75rem);
    font-weight: 700;
    color: var(--gapfy-muted);
    max-width: 720px;
    margin-bottom: 2.5rem;
    letter-spacing: -0.025em;
    line-height: 1.15;
}

.hero-actions {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    margin-top: 1rem;
}

.hero-art {
    position: relative;
    min-height: 480px;
    border-radius: var(--gapfy-radius-xl);
    overflow: hidden;
    isolation: isolate;
}

/* Mesh — Stripe-style flowing gradient. Four large coloured blobs
   positioned in different quadrants, each blurred heavily and animated
   on a long, independent loop so the result feels organic and never
   syncs up. mix-blend-mode lets the colours bleed into each other the
   way the Stripe hero does. The base is white so the blend stays
   light; dark mode flips the base to the deep ink and reuses the same
   blobs. */
.hero-art--mesh {
    background: var(--gapfy-bg);
}

.hero-art--mesh::before {
    content: "";
    position: absolute;
    inset: -10%;
    z-index: 0;
    pointer-events: none;
    background:
        radial-gradient(45% 55% at 22% 30%, var(--gapfy-accent-violet) 0%, transparent 60%),
        radial-gradient(50% 50% at 78% 22%, var(--gapfy-accent-coral) 0%, transparent 55%),
        radial-gradient(55% 55% at 80% 78%, var(--gapfy-accent-magenta) 0%, transparent 60%),
        radial-gradient(50% 60% at 25% 80%, var(--gapfy-accent-sky) 0%, transparent 55%);
    filter: blur(48px);
    opacity: 0.85;
    mix-blend-mode: normal;
    animation: gapfy-mesh-drift-a 32s var(--gapfy-ease-in-out) infinite alternate;
    will-change: transform;
}

.hero-art--mesh::after {
    content: "";
    position: absolute;
    inset: -10%;
    z-index: 0;
    pointer-events: none;
    background:
        radial-gradient(40% 45% at 60% 45%, var(--gapfy-accent-violet) 0%, transparent 60%),
        radial-gradient(35% 40% at 40% 60%, var(--gapfy-accent-magenta) 0%, transparent 60%);
    filter: blur(56px);
    opacity: 0.55;
    mix-blend-mode: screen;
    animation: gapfy-mesh-drift-b 44s var(--gapfy-ease-in-out) infinite alternate;
    will-change: transform;
}

@keyframes gapfy-mesh-drift-a {
    0%   { transform: translate(0, 0) rotate(0deg) scale(1); }
    50%  { transform: translate(3%, -2%) rotate(4deg) scale(1.05); }
    100% { transform: translate(-2%, 4%) rotate(-3deg) scale(0.98); }
}

@keyframes gapfy-mesh-drift-b {
    0%   { transform: translate(0, 0) rotate(0deg) scale(1); }
    50%  { transform: translate(-4%, 3%) rotate(-5deg) scale(1.08); }
    100% { transform: translate(3%, -3%) rotate(6deg) scale(0.95); }
}

/* Dark mode keeps the blobs vibrant but softens their opacity so the
   navy background remains the dominant tone. */
:root.theme-dark .hero-art--mesh::before { opacity: 0.55; }
:root.theme-dark .hero-art--mesh::after  { opacity: 0.40; }

@media (prefers-color-scheme: dark) {
    :root:not(.theme-light):not(.theme-dark) .hero-art--mesh::before { opacity: 0.55; }
    :root:not(.theme-light):not(.theme-dark) .hero-art--mesh::after  { opacity: 0.40; }
}

/* --- Hero ecosystem — Gapfy core + four product satellites ----------
   Overlay on the mesh art frame: a central Gapfy hub wired out to the
   four products (Maestro, Harmony, Echo, Riff) with thin primary spokes,
   plus a delivery strip (Strategy to Run) along the bottom. Says "one
   platform, four products, end-to-end services" without repeating the
   dashboard shot used lower on the page.

   Interactive, but reusing the platform's own machinery: the .hero-eco
   panel carries data-tilt, so wwwroot/js/tilt-glow.js already binds it
   and writes --mx / --my (cursor) and --tilt-rx / --tilt-ry (<=4deg) the
   same way it does for the cards and the Maestro band — no new JS. The
   generic [data-tilt] rule supplies perspective + preserve-3d, so the
   nodes' translateZ depths parallax a touch as the panel tilts. Each node
   is split into three single-purpose layers — anchor (positions + depth),
   float (gentle drift), card (hover lift) — so float and hover never
   fight over `transform`. Cards sit on the brand surface tokens, so the
   whole motif flips light / dark with the site, and the prefers-reduced-
   motion reset (further down) drops every animation + the tilt. */
.hero-eco {
    position: absolute;
    inset: 0;
    z-index: 1;
}

/* Cursor-tracking glow — tilt-glow.js writes --mx / --my onto this
   [data-tilt] element; the halo follows the pointer and fades in only
   while the cursor is over the panel. Lowest layer, behind the nodes. */
.hero-eco-glow {
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    background: radial-gradient(340px circle at var(--mx, 50%) var(--my, 50%),
        rgba(61, 166, 221, 0.18) 0%, transparent 58%);
    opacity: 0;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out);
}

.hero-eco:hover .hero-eco-glow { opacity: 1; }

/* Spokes — drawn in primary so the products read as "wired into" Gapfy.
   preserveAspectRatio="none" stretches the 480x480 viewBox to the panel;
   the distortion is invisible at these stroke weights. */
.hero-eco-lines {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.hero-eco-lines line {
    stroke: var(--gapfy-primary);
    stroke-width: 1.25;
    stroke-opacity: 0.35;
    stroke-linecap: round;
}

.hero-eco-lines circle {
    fill: var(--gapfy-primary);
    fill-opacity: 0.55;
}

/* Anchor layer — only positions, centres and sets a depth (translateZ
   inside the [data-tilt] preserve-3d scene). No visuals; the card span
   carries those. Each product is a real link to its page. */
.hero-eco-node,
.hero-eco-core {
    position: absolute;
    transform: translate(-50%, -50%) translateZ(var(--ez, 0px));
    text-decoration: none;
    outline: none;
}

/* Keep the active node painting above the others while it grows. */
.hero-eco-node:hover,
.hero-eco-core:hover,
.hero-eco-node:focus-visible,
.hero-eco-core:focus-visible {
    z-index: 4;
}

.hero-eco-node--tl { left: 22%; top: 20%; --ez: 12px; }
.hero-eco-node--tr { left: 78%; top: 20%; --ez: 12px; }
.hero-eco-node--bl { left: 22%; top: 74%; --ez: 12px; }
.hero-eco-node--br { left: 78%; top: 74%; --ez: 12px; }
.hero-eco-core     { left: 50%; top: 46%; --ez: 30px; z-index: 2; }

/* Float layer — gentle vertical drift, offset per node so the set never
   syncs. A separate element so it never collides with the card's hover
   transform. The global prefers-reduced-motion reset stops it. */
.hero-eco-float {
    display: block;
    animation: gapfy-eco-float 7s var(--gapfy-ease-in-out) infinite alternate;
}

.hero-eco-node--tr .hero-eco-float { animation-duration: 8s;   animation-delay: -2s; }
.hero-eco-node--bl .hero-eco-float { animation-duration: 7.5s; animation-delay: -4s; }
.hero-eco-node--br .hero-eco-float { animation-duration: 8.5s; animation-delay: -1s; }
.hero-eco-core .hero-eco-float     { animation-duration: 9s;   animation-delay: -3s; }

@keyframes gapfy-eco-float {
    from { transform: translateY(0); }
    to   { transform: translateY(-7px); }
}

/* Card layer — the floating glass chrome on the elevated surface, and
   the only layer with a hover transition, so float + hover stay apart. */
.hero-eco-card {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.6rem 0.85rem;
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    box-shadow: var(--gapfy-shadow-lg);
    white-space: nowrap;
    transition: transform 280ms var(--gapfy-ease-out),
                box-shadow 280ms var(--gapfy-ease-out),
                border-color 280ms var(--gapfy-ease-out);
}

/* Hover + keyboard focus — the active card grows just a touch and lights
   up its cyan edge. The other nodes are left untouched. */
.hero-eco-node:hover .hero-eco-card,
.hero-eco-core:hover .hero-eco-card,
.hero-eco-node:focus-visible .hero-eco-card,
.hero-eco-core:focus-visible .hero-eco-card {
    transform: translateY(-3px) scale(1.08);
    border-color: var(--gapfy-primary);
    box-shadow: var(--gapfy-shadow-lg), 0 10px 26px -10px rgba(61, 166, 221, 0.45);
}

.hero-eco-node:focus-visible .hero-eco-card,
.hero-eco-core:focus-visible .hero-eco-card {
    outline: 2px solid var(--gapfy-primary);
    outline-offset: 3px;
}

/* Core reads as the hub: a permanent primary ring + a slightly larger
   mark and name. */
.hero-eco-card--core {
    border-color: var(--gapfy-primary);
    box-shadow: var(--gapfy-shadow-lg), 0 0 0 5px rgba(61, 166, 221, 0.12);
}

.hero-eco-logo { width: 26px; height: 26px; object-fit: contain; flex: none; }
.hero-eco-card--core .hero-eco-logo { width: 32px; height: 32px; }

.hero-eco-text { display: flex; flex-direction: column; line-height: 1.15; }
.hero-eco-name { font-size: 0.9rem; font-weight: 700; color: var(--gapfy-ink); }
.hero-eco-card--core .hero-eco-name { font-size: 0.98rem; }
.hero-eco-role { font-size: 0.72rem; font-weight: 500; color: var(--gapfy-muted); }

/* Delivery strip — the services dimension. One pill of stages on the
   neutral surface so it stays legible over any blob behind it. */
.hero-eco-flow {
    position: absolute;
    left: 50%;
    bottom: 5%;
    transform: translate(-50%, 0) translateZ(6px);
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.4rem 0.95rem;
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-pill);
    box-shadow: var(--gapfy-shadow);
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--gapfy-ink-soft);
    white-space: nowrap;
    max-width: 92%;
}

.hero-eco-flow-dot {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--gapfy-primary);
    flex: none;
}

/* Compact the motif on phones: the panel drops to 320px tall and the
   container narrows, so shrink the cards and drop the role line. */
@media (max-width: 720px) {
    .hero-eco-card { padding: 0.45rem 0.6rem; gap: 0.45rem; }
    .hero-eco-logo { width: 22px; height: 22px; }
    .hero-eco-card--core .hero-eco-logo { width: 26px; height: 26px; }
    .hero-eco-name { font-size: 0.8rem; }
    .hero-eco-card--core .hero-eco-name { font-size: 0.86rem; }
    .hero-eco-role { display: none; }
    .hero-eco-flow { font-size: 0.66rem; gap: 0.4rem; padding: 0.35rem 0.7rem; }
}

/* Legacy photo variant kept as a fallback if any page still uses it
   (Wave 2 plan calls for full replacement, but harmless if missed). */
.hero-art--photo {
    background: var(--gapfy-bg-tint);
}

.hero-art--photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    position: absolute;
    inset: 0;
}

/* --- Buttons ------------------------------------------------------- */

.btn-primary,
.btn-secondary,
.btn-ghost {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.875rem 1.5rem;
    border-radius: var(--gapfy-radius);
    font-weight: 600;
    font-size: var(--gapfy-text-base);
    cursor: pointer;
    border: 1px solid transparent;
    letter-spacing: -0.01em;
    transition: background var(--gapfy-dur-fast) var(--gapfy-ease-out),
                color var(--gapfy-dur-fast) var(--gapfy-ease-out),
                transform var(--gapfy-dur-fast) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out),
                border-color var(--gapfy-dur-fast) var(--gapfy-ease-out);
}

/* Primary CTA — solid brand blue at rest, gradient accent on hover.
   The gradient travels blue -> violet -> magenta so the hover state
   reads as an "energy reveal" rather than a flat darker shade.
   CSS cannot transition between two gradients (background-image is not
   an animatable property), so the accent gradient lives on a ::before
   overlay that cross-fades via opacity instead. The solid brand-blue
   background stays underneath, keeping the colour vibrant rather than
   fading through a muddy mid-tone. */
.btn-primary {
    position: relative;
    isolation: isolate;
    background-color: var(--gapfy-primary);
    color: #FFFFFF;
    box-shadow: 0 2px 4px rgba(61, 166, 221, 0.25);
}

/* Accent gradient overlay — transparent at rest, fades in on hover.
   border-radius: inherit clips it to the pill; z-index: -1 keeps it
   above the solid background but below the label. The gradient is
   sized to 220% and parked off to the right at rest, so on hover it
   both fades in (opacity) AND sweeps left into place (background-
   position) — a smooth "energy reveal" rather than a flat swap. */
.btn-primary::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background-image: var(--gapfy-gradient-accent);
    background-size: 220% 100%;
    background-position: 100% 0;
    opacity: 0;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out),
                background-position var(--gapfy-dur-slow) var(--gapfy-ease-out);
}

/* No translateY — the button stays put; the gradient reveal + the
   growing glow shadow carry the hover feedback on their own. */
.btn-primary:hover {
    color: #FFFFFF;
    box-shadow: 0 10px 24px rgba(124, 108, 255, 0.32);
}

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

.btn-secondary {
    background: var(--gapfy-bg-elevated);
    color: var(--gapfy-ink);
    border-color: var(--gapfy-line-strong);
}

.btn-secondary:hover {
    background: var(--gapfy-bg-soft);
    border-color: var(--gapfy-ink);
    color: var(--gapfy-ink);
}

.btn-ghost {
    background: transparent;
    color: var(--gapfy-primary);
    padding: 0.5rem 0.75rem;
}

.btn-ghost:hover {
    color: var(--gapfy-primary-strong);
}

/* --- Section base -------------------------------------------------- */

.section {
    padding: 6rem 0;
}

.section-tight {
    padding: 4rem 0;
}

/* Stripe-look removes the alternating section-soft background and lets
   all sections share the same surface. The class is kept so Razor
   markup does not have to change; it is just visually neutral now.
   Section rhythm is carried by the generous .section padding instead. */
.section-soft {
    background: transparent;
}

/* Section heading — Stripe-style editorial layout. Title is left-aligned
   and sits next to (or above on narrow screens) a subtitle in the same
   big size but muted. Eyebrow goes to plain primary-coloured text, no
   uppercase wall. Total feel: editorial, not marketing-template. */
.section-heading {
    text-align: left;
    max-width: none;
    margin: 0 0 3.5rem;
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
    gap: 2rem 3rem;
    align-items: end;
}

/* Headings without a subtitle should not leave a yawning empty column;
   collapse to a single column so the title can use the full width. */
.section-heading:not(:has(.section-subtitle)) {
    grid-template-columns: 1fr;
}

.section-eyebrow {
    display: block;
    grid-column: 1 / -1;
    color: var(--gapfy-primary);
    font-weight: 600;
    font-size: var(--gapfy-text-sm);
    letter-spacing: 0;
    text-transform: none;
    margin-bottom: 0.5rem;
}

.section-title {
    font-size: clamp(1.75rem, 3.6vw, 2.75rem);
    font-weight: 800;
    color: var(--gapfy-ink);
    margin: 0;
    letter-spacing: -0.03em;
    line-height: 1.1;
}

.section-subtitle {
    color: var(--gapfy-muted);
    font-size: clamp(1.125rem, 1.6vw, 1.375rem);
    line-height: 1.45;
    margin: 0;
}

@media (max-width: 880px) {
    .section-heading {
        grid-template-columns: 1fr;
        gap: 1rem;
    }
}

/* --- Competencies (4 icons) --------------------------------------- */

/* Competency block — Stripe-style "feature row": small coloured icon
   top-left, title underneath, supporting text below. Left-aligned to
   match the new section-heading direction. No card frame; the
   whitespace and grid alignment do the work. */
.competency-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 2.5rem 2rem;
}

.competency {
    text-align: left;
    padding: 0;
}

.competency-icon {
    width: 40px;
    height: 40px;
    margin: 0 0 1.25rem;
    border-radius: var(--gapfy-radius);
    background: var(--gapfy-primary-soft);
    color: var(--gapfy-primary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.competency-icon .material-symbols-outlined {
    font-size: 22px;
}

.competency-title {
    font-size: var(--gapfy-text-lg);
    font-weight: 700;
    color: var(--gapfy-ink);
    margin-bottom: 0.5rem;
    letter-spacing: -0.01em;
}

.competency-text {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-base);
    line-height: 1.5;
}

/* --- Service cards (6 cards) -------------------------------------- */

.service-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.5rem;
}

/* Card — calm at rest, expressive on hover (Stripe pattern). The
   resting state uses only a hairline border and no shadow; the
   hover lifts and adds the elevation. The mouse-tracked glow
   (added separately) does the rest. */
.service-card {
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    padding: 2.25rem;
    transition: border-color var(--gapfy-dur-base) var(--gapfy-ease-out),
                transform var(--gapfy-dur-base) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out);
    display: flex;
    flex-direction: column;
    position: relative;
}

.service-card:hover {
    border-color: var(--gapfy-line-strong);
    transform: translateY(-4px);
    box-shadow: 0 20px 40px rgba(9, 30, 66, 0.10);
}

.service-card-icon {
    width: 56px;
    height: 56px;
    border-radius: var(--gapfy-radius);
    background: var(--gapfy-primary-soft);
    color: var(--gapfy-primary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 1.25rem;
}

.service-card-icon .material-symbols-outlined {
    font-size: 28px;
}

/* Brand icon variant of the service-card icon slot (product cards on
   the Home grid and the Products hub). The tinted background square
   stays; the brand mark sits inside it. */
.service-card-icon img {
    width: 32px;
    height: 32px;
    object-fit: contain;
}

.service-card-media {
    width: 100%;
    height: 160px;
    margin: -2rem -2rem 1.25rem;
    width: calc(100% + 4rem);
    overflow: hidden;
    border-top-left-radius: var(--gapfy-radius-lg);
    border-top-right-radius: var(--gapfy-radius-lg);
    background: var(--gapfy-bg-tint);
}

.service-card-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform var(--gapfy-dur-slow) var(--gapfy-ease-out);
}

.service-card--with-image:hover .service-card-media img {
    transform: scale(1.04);
}

.service-card-title {
    font-size: var(--gapfy-text-xl);
    font-weight: 700;
    color: var(--gapfy-ink);
    margin-bottom: 0.5rem;
}

.service-card-text {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-sm);
    flex: 1;
    margin-bottom: 1.25rem;
}

.service-card-link {
    color: var(--gapfy-primary);
    font-weight: 600;
    font-size: var(--gapfy-text-sm);
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.service-card-link .material-symbols-outlined {
    font-size: 18px;
    transition: transform var(--gapfy-dur-base) var(--gapfy-ease-out);
}

.service-card:hover .service-card-link .material-symbols-outlined {
    transform: translateX(4px);
}

/* --- Values (8 cards) --------------------------------------------- */

/* Value cards follow the same Stripe pattern as the service cards:
   calm at rest (hairline border, no shadow), expressive on hover
   (lift + cursor-tracked glow, added by the glow rules further down).
   The icon chip is tinted at rest and flips to the vivid brand accent
   gradient on hover, so the whole row comes alive on interaction. */
.value-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1.5rem;
}

.value-card {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    text-align: left;
    gap: 0.875rem;
    padding: 1.75rem 1.5rem;
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    transition: border-color var(--gapfy-dur-base) var(--gapfy-ease-out),
                transform var(--gapfy-dur-base) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out);
}

.value-card:hover {
    border-color: var(--gapfy-line-strong);
    transform: translateY(-4px);
    box-shadow: 0 20px 40px rgba(9, 30, 66, 0.10);
}

.value-card-icon {
    width: 48px;
    height: 48px;
    border-radius: var(--gapfy-radius);
    background: var(--gapfy-primary-soft);
    color: var(--gapfy-primary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: transform var(--gapfy-dur-base) var(--gapfy-ease-out),
                background var(--gapfy-dur-base) var(--gapfy-ease-out),
                color var(--gapfy-dur-base) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out);
}

.value-card-icon .material-symbols-outlined {
    font-size: 26px;
}

/* Hover lights up the chip: it flips to the brand accent gradient, the
   icon turns white and lifts, with a soft violet halo underneath. */
.value-card:hover .value-card-icon {
    background: var(--gapfy-gradient-accent);
    color: #FFFFFF;
    transform: translateY(-2px) scale(1.05);
    box-shadow: 0 10px 22px rgba(124, 108, 255, 0.30);
}

.value-card-title {
    font-size: var(--gapfy-text-lg);
    font-weight: 700;
    color: var(--gapfy-ink);
    letter-spacing: -0.01em;
}

.value-card-text {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-sm);
    line-height: 1.5;
}

/* --- Maestro highlight section ------------------------------------ */

.maestro-highlight {
    position: relative;
    background: var(--gapfy-gradient-primary);
    color: #FFFFFF;
    border-radius: var(--gapfy-radius-xl);
    padding: 3.5rem;
    overflow: hidden;
    display: grid;
    grid-template-columns: 1.2fr 0.8fr;
    gap: 3rem;
    align-items: center;
}

.maestro-highlight h2 {
    color: #FFFFFF;
    font-size: clamp(1.75rem, 3vw, 2.5rem);
}

.maestro-highlight p {
    color: rgba(255, 255, 255, 0.9);
    font-size: var(--gapfy-text-lg);
    margin: 1rem 0 2rem;
}

/* Inside the Maestro highlight band the primary CTA inverts: white
   pill on the gradient background. Have to neutralise the background
   and suppress the accent ::before overlay so the global rule does
   not paint a gradient on top of the white. */
.maestro-highlight .btn-primary {
    background-color: #FFFFFF;
    background-image: none;
    color: var(--gapfy-primary-strong);
}

.maestro-highlight .btn-primary::before {
    content: none;
}

.maestro-highlight .btn-primary:hover {
    background-color: rgba(255, 255, 255, 0.94);
    color: var(--gapfy-primary-strong);
}

.maestro-highlight-art {
    background: rgba(255, 255, 255, 0.12);
    border-radius: var(--gapfy-radius-lg);
    padding: 1.5rem;
    backdrop-filter: blur(6px);
    border: 1px solid rgba(255, 255, 255, 0.2);
}

/* --- Footer -------------------------------------------------------- */
/* Stripe-style minimal footer: same surface as the page (not a dark
   anchor), separated only by a hairline border. Column labels are
   small muted text (no uppercase wall). Tightens the visual rhythm
   so the page feels editorial rather than enterprise. */

.site-footer {
    background: var(--gapfy-bg);
    color: var(--gapfy-ink-soft);
    padding: 5.5rem 0 2rem;
    border-top: 1px solid var(--gapfy-line);
}

.site-footer-grid {
    display: grid;
    /* 5 cols: brand wider, 4 link columns equal. The Razor markup
       renders brand + 4 link blocks, so the col count must match. */
    grid-template-columns: 1.6fr 1fr 1fr 1fr 1fr;
    gap: 3rem;
}

.site-footer h4 {
    color: var(--gapfy-ink);
    font-size: var(--gapfy-text-sm);
    font-weight: 600;
    letter-spacing: 0;
    text-transform: none;
    margin-bottom: 1.25rem;
}

.site-footer ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
}

.site-footer a {
    color: var(--gapfy-ink-soft);
    font-size: var(--gapfy-text-sm);
    transition: color var(--gapfy-dur-fast) var(--gapfy-ease-out);
}

.site-footer a:hover {
    color: var(--gapfy-primary);
}

/* Holds the inline Gapfy lockup (same SVG as the navbar). No text node
   any more, so the old font-size/weight/colour are gone; .site-brand-logo
   sizes the SVG (32px) and pins the wordmark to --gapfy-ink. */
.site-footer-brand {
    margin-bottom: 1rem;
    display: inline-flex;
    align-items: center;
}

.site-footer-tagline {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-sm);
    max-width: 340px;
    line-height: 1.55;
}

.site-footer-bottom {
    margin-top: 4rem;
    padding-top: 1.75rem;
    border-top: 1px solid var(--gapfy-line);
    display: flex;
    justify-content: space-between;
    align-items: center;
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-xs);
}

/* --- Page hero (inner pages) -------------------------------------- */

.page-hero {
    padding: 7rem 0 4rem;
    background: var(--gapfy-bg);
    position: relative;
    overflow: hidden;
}

/* Mesh accent — single soft blob in the top-right corner of each
   inner page hero. Mirrors the Home mesh palette but at a much
   lower intensity so it reads as a hint of brand colour without
   competing with the page title.
   No animation here: inner pages benefit from being calmer than
   the home; movement would be noise on a content-heavy page. */
.page-hero::before {
    content: "";
    position: absolute;
    top: -30%;
    right: -10%;
    width: 70%;
    height: 180%;
    z-index: 0;
    pointer-events: none;
    background:
        radial-gradient(55% 50% at 60% 30%, var(--gapfy-accent-violet) 0%, transparent 65%),
        radial-gradient(45% 50% at 80% 65%, var(--gapfy-accent-magenta) 0%, transparent 65%),
        radial-gradient(50% 50% at 35% 55%, var(--gapfy-accent-sky) 0%, transparent 65%);
    filter: blur(64px);
    opacity: 0.28;
}

:root.theme-dark .page-hero::before { opacity: 0.22; }

@media (prefers-color-scheme: dark) {
    :root:not(.theme-light):not(.theme-dark) .page-hero::before { opacity: 0.22; }
}

.page-hero > * {
    position: relative;
    z-index: 1;
}

.page-hero-inner {
    max-width: 820px;
}

/* Brand icon + title row on product page heroes (Maestro, Harmony,
   Echo, Riff): the product mark sits beside the big title. */
.page-hero-title-row {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    margin-bottom: 1.25rem;
}

.page-hero-icon {
    height: 56px;
    max-width: 84px;
    object-fit: contain;
    object-position: left center;
    flex-shrink: 0;
}

.page-hero-title-row .page-hero-title {
    margin-bottom: 0;
}

/* Secondary headline below the icon + title row (Maestro page: the
   original tagline keeps its copy, just steps down one level). */
.page-hero-tagline {
    font-size: clamp(1.5rem, 2.6vw, 2.25rem);
    font-weight: 700;
    letter-spacing: -0.025em;
    line-height: 1.2;
    margin: 0 0 1.25rem;
    color: var(--gapfy-ink);
    max-width: 720px;
}

.page-hero-eyebrow {
    color: var(--gapfy-primary);
    font-weight: 600;
    font-size: var(--gapfy-text-sm);
    text-transform: none;
    letter-spacing: 0;
    margin-bottom: 0.75rem;
    display: block;
}

.page-hero-title {
    font-size: clamp(2.25rem, 4.4vw, 3.75rem);
    font-weight: 800;
    margin-bottom: 1.25rem;
    letter-spacing: -0.035em;
    line-height: 1.05;
}

.page-hero-subtitle {
    color: var(--gapfy-muted);
    font-size: clamp(1.125rem, 1.6vw, 1.375rem);
    line-height: 1.45;
}

/* --- Prose (legal, content pages) --------------------------------- */

.prose {
    max-width: 760px;
    margin: 0 auto;
    color: var(--gapfy-ink-soft);
    font-size: var(--gapfy-text-base);
    line-height: 1.7;
}

.prose h2 {
    font-size: var(--gapfy-text-2xl);
    font-weight: 800;
    letter-spacing: -0.02em;
    margin: 2.75rem 0 1rem;
    color: var(--gapfy-ink);
}

.prose h3 {
    font-size: var(--gapfy-text-lg);
    font-weight: 700;
    letter-spacing: -0.01em;
    margin: 1.75rem 0 0.75rem;
    color: var(--gapfy-ink);
}

.prose p {
    margin-bottom: 1rem;
}

.prose ul {
    padding-left: 1.25rem;
    margin: 0 0 1rem;
}

.prose li {
    margin-bottom: 0.375rem;
}

/* --- Contact card -------------------------------------------------- */

.contact-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
    gap: 2.5rem;
    align-items: start;
}

@media (max-width: 960px) {
    .contact-layout {
        grid-template-columns: 1fr;
    }
}

.contact-card {
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    padding: 2.5rem;
    box-shadow: var(--gapfy-shadow-sm);
}

.contact-form {
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    padding: 2.75rem;
    box-shadow: var(--gapfy-shadow-sm);
}

/* Inputs inside the contact form — Stripe-style: generous padding,
   visible at-rest border, expressive 4px focus ring + 1.5px border
   recolour. Inputs come from Radzen so we target both the .rz-*
   classes and the underlying <input>/<textarea>. */
.contact-form .rz-textbox,
.contact-form .rz-textarea,
.contact-form input,
.contact-form textarea {
    font-size: var(--gapfy-text-base);
    padding: 0.75rem 0.875rem;
    border-radius: var(--gapfy-radius);
    border: 1.5px solid var(--gapfy-line);
    transition: border-color var(--gapfy-dur-fast) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out),
                background var(--gapfy-dur-fast) var(--gapfy-ease-out);
}

.contact-form .rz-textbox:hover:not(.rz-state-disabled):not(.rz-state-focused),
.contact-form .rz-textarea:hover:not(.rz-state-disabled):not(.rz-state-focused),
.contact-form input:hover:not(:focus),
.contact-form textarea:hover:not(:focus) {
    border-color: var(--gapfy-line-strong);
}

.contact-form-heading {
    font-size: var(--gapfy-text-2xl);
    margin: 0 0 0.5rem;
    color: var(--gapfy-ink);
}

.contact-form-subtitle {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-sm);
    margin: 0 0 1.75rem;
}

.contact-form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.25rem;
}

@media (max-width: 540px) {
    .contact-form-grid {
        grid-template-columns: 1fr;
    }
}

.contact-form-field {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.contact-form-field-full {
    grid-column: 1 / -1;
}

.contact-form-field label {
    font-weight: 600;
    color: var(--gapfy-ink);
    font-size: var(--gapfy-text-sm);
}

.contact-form-error {
    color: var(--gapfy-danger);
    font-size: var(--gapfy-text-xs, 0.8rem);
    min-height: 1rem;
}

/* Stripe-style focus ring on Radzen inputs in the contact form. The
   ring is 4px wide and 18% opacity so it reads clearly without
   shouting; border drops to brand blue so the active field is
   unambiguous. !important on both because Radzen ships hard-coded
   colour values on focused inputs we can't otherwise override. */
.contact-form .rz-textbox:not(.rz-state-disabled):focus,
.contact-form .rz-textarea:not(.rz-state-disabled):focus,
.contact-form .rz-textbox.rz-state-focused,
.contact-form .rz-textarea.rz-state-focused,
.contact-form input:focus,
.contact-form textarea:focus {
    outline: none;
    border-color: var(--gapfy-primary) !important;
    box-shadow: 0 0 0 4px rgba(61, 166, 221, 0.18) !important;
}

.contact-form-actions {
    margin-top: 1.5rem;
    display: flex;
    justify-content: flex-end;
}

.contact-form-captcha {
    margin-top: 1.5rem;
    display: flex;
    justify-content: flex-start;
    min-height: 65px;
}

.contact-row {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 1rem 0;
    border-bottom: 1px solid var(--gapfy-line);
}

.contact-row:last-child {
    border-bottom: none;
}

.contact-row .material-symbols-outlined {
    color: var(--gapfy-primary);
    font-size: 22px;
}

.contact-row-label {
    font-weight: 600;
    color: var(--gapfy-ink);
    margin-bottom: 0.25rem;
}

.contact-row-value {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-sm);
}

/* --- CTA strip ----------------------------------------------------- */

/* CTA strip — Stripe-style: keeps a soft tinted band so it reads as
   a distinct "next step" surface, but loses the hard borders and
   centers heavy text with a single accent CTA. */
.cta-strip {
    background: var(--gapfy-bg-soft);
    padding: 5rem 0;
    text-align: center;
}

.cta-strip h3 {
    font-size: clamp(1.75rem, 3vw, 2.5rem);
    font-weight: 800;
    letter-spacing: -0.025em;
    margin-bottom: 0.75rem;
}

.cta-strip p {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-lg);
    margin-bottom: 1.75rem;
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
}

/* --- Trusted-by strip --------------------------------------------- */
/* Narrow band of "social proof" logos placed near the hero. Matches
   the Atlassian/Confluence pattern of a single label above a row of
   greyscale partner logos. */

/* Trusted strip — Stripe-style: no border separator, no distinct
   background, just floats below the hero in the same surface. The
   gradient mask at the marquee edges does the visual fade so logos
   appear/disappear smoothly without needing a hard boundary. */
.trusted-strip {
    padding: 1.5rem 0 4rem;
    background: transparent;
    overflow: hidden;
}

.trusted-strip-label {
    text-align: center;
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-sm);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    margin-bottom: 1.5rem;
}

/* Marquee wrapper — clips the overflowing track and adds gradient
   masks at both edges so logos fade in/out instead of popping. The
   track inside (.trusted-strip-logos) holds the actual logos twice
   (rendered server-side in Home.razor) so the animation can loop
   seamlessly from -50% back to 0%. */
.trusted-strip-marquee {
    position: relative;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%);
}

.trusted-strip-logos {
    display: flex;
    gap: 4rem;
    align-items: center;
    width: max-content;
    animation: gapfy-marquee 40s linear infinite;
}

/* Pause on hover — gives users a chance to actually read a logo
   they want to look at. Standard expectation on marquee strips. */
.trusted-strip-marquee:hover .trusted-strip-logos {
    animation-play-state: paused;
}

@keyframes gapfy-marquee {
    0%   { transform: translateX(0); }
    100% { transform: translateX(-50%); }
}

/* Each partner logo sits on a light "chip" so the full-colour marks stay
   legible on BOTH the light (#FFFFFF) and dark (#0F1419) themes — the chip
   is always white, so dark-ink wordmarks (Brabous, CEDAE) never disappear
   on the dark theme, and CEDAE's white-background PNG blends in cleanly. */
.trusted-strip-logo {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 64px;
    padding: 0 1.75rem;
    background: #FFFFFF;
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    box-shadow: var(--gapfy-shadow-sm);
    opacity: 0.88;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out),
                box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out),
                transform var(--gapfy-dur-base) var(--gapfy-ease-out);
    flex-shrink: 0;
}

/* Per-logo optical normalisation: every chip is the same height, but the
   artwork inside scales by --logo-scale (set inline in Home.razor) off the
   34px base. Source marks differ in aspect ratio and baked-in padding, so a
   single fixed height makes tight wordmarks dominate and padded/stacked marks
   shrink — the scale closes that gap. max-width still caps very wide marks. */
.trusted-strip-logo img {
    height: calc(34px * var(--logo-scale, 1));
    width: auto;
    max-width: 190px;
    object-fit: contain;
    display: block;
}

/* Hover keeps the lift in opacity + shadow only — no translateY, so the
   logo chip stays put while still gaining presence on hover. */
.trusted-strip-logo:hover {
    opacity: 1;
    box-shadow: var(--gapfy-shadow);
}

/* --- Feature split (text + image alternating) --------------------- */
/* Atlassian-style "talking points" rows: short eyebrow, big punchy
   title, supporting paragraph and a list of bullet points, paired with
   a screenshot or illustration on the opposite side. Use the
   --reverse modifier to put the image on the left. */

.feature-split {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 5rem;
    align-items: center;
}

.feature-split + .feature-split {
    margin-top: 5rem;
}

.feature-split--reverse .feature-split-copy {
    order: 2;
}

.feature-split-copy .section-eyebrow {
    margin-bottom: 1rem;
}

.feature-split-title {
    font-size: clamp(1.875rem, 2.8vw, 2.5rem);
    font-weight: 900;
    color: var(--gapfy-ink);
    letter-spacing: -0.03em;
    line-height: 1.1;
    margin-bottom: 1.25rem;
}

.feature-split-text {
    color: var(--gapfy-muted);
    font-size: var(--gapfy-text-lg);
    line-height: 1.55;
    margin-bottom: 1.5rem;
}

.feature-split-list {
    list-style: none;
    padding: 0;
    margin: 0 0 1.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
}

.feature-split-list li {
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
    color: var(--gapfy-ink-soft);
    font-size: var(--gapfy-text-base);
}

.feature-split-list .material-symbols-outlined {
    color: var(--gapfy-primary);
    font-size: 20px;
    flex-shrink: 0;
    margin-top: 2px;
}

/* Feature-split media — the diagrams sit inside a Stripe-style frame:
   gradient halo behind (::before) and a soft outer shadow. No faux
   browser/Mac title bar — Gapfy ships Windows-first tools, so the
   Mac traffic-light chrome would be the wrong metaphor. The diagram
   fills the frame edge to edge; overflow: hidden clips it to the
   rounded corners. */
.feature-split-media {
    position: relative;
    border-radius: var(--gapfy-radius-lg);
    background: var(--gapfy-bg-elevated);
    border: 1px solid var(--gapfy-line);
    box-shadow: 0 30px 60px rgba(9, 30, 66, 0.12);
    aspect-ratio: 4 / 3;
    overflow: hidden;
    isolation: isolate;
}

/* Coloured halo bleeding behind the frame — reuses the accent mesh
   palette so the section feels stitched to the rest of the page. */
.feature-split-media::before {
    content: "";
    position: absolute;
    top: -20%;
    left: -10%;
    right: -10%;
    bottom: -20%;
    z-index: -1;
    background:
        radial-gradient(40% 50% at 30% 35%, var(--gapfy-accent-sky) 0%, transparent 65%),
        radial-gradient(40% 50% at 75% 70%, var(--gapfy-accent-violet) 0%, transparent 65%);
    filter: blur(60px);
    opacity: 0.35;
    pointer-events: none;
}

.feature-split-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    border-radius: var(--gapfy-radius-lg);
}

:root.theme-dark .feature-split-media::before { opacity: 0.30; }

@media (prefers-color-scheme: dark) {
    :root:not(.theme-light):not(.theme-dark) .feature-split-media::before { opacity: 0.30; }
}

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

@media (max-width: 1024px) {
    .hero-grid { grid-template-columns: 1fr; gap: 2.5rem; }
    .hero-art { min-height: 320px; }
    .competency-grid { grid-template-columns: repeat(2, 1fr); }
    .service-grid { grid-template-columns: repeat(2, 1fr); }
    .value-grid { grid-template-columns: repeat(2, 1fr); }
    .site-footer-grid { grid-template-columns: 1fr 1fr; }
    .maestro-highlight { grid-template-columns: 1fr; padding: 2.5rem; }
    .trusted-strip-logos { gap: 2.5rem; }
    .feature-split { grid-template-columns: 1fr; gap: 2.5rem; }
    .feature-split--reverse .feature-split-copy { order: 0; }
}

@media (max-width: 720px) {
    /* Drawer transforms the desktop horizontal nav into a full-height
       slide-in panel from the right. Stays in the DOM (no display
       toggling); just translates off-screen by default. JS adds
       .has-mobile-open on the <header> to slide it in. */
    .site-nav-links {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        width: min(85vw, 360px);
        flex-direction: column;
        align-items: stretch;
        justify-content: flex-start;
        gap: 0.5rem;
        padding: calc(var(--gapfy-header-height) + 1rem) 1.5rem 2rem;
        background: var(--gapfy-bg-elevated);
        box-shadow: var(--gapfy-shadow-lg);
        transform: translateX(100%);
        transition: transform var(--gapfy-dur-base) var(--gapfy-ease-out);
        overflow-y: auto;
        z-index: 60;
    }

    .site-navbar.has-mobile-open .site-nav-links {
        transform: translateX(0);
    }

    /* Dropdowns inside the drawer unfold permanently — no hover, no
       expand_more chevron animation. The panel just lays out the
       items in line. */
    .site-nav-dropdown-panel {
        position: static;
        display: block;
        transform: none;
        min-width: 0;
        box-shadow: none;
        background: var(--gapfy-bg-soft);
        border: 1px solid var(--gapfy-line);
        padding: 0.5rem;
        margin-top: 0.5rem;
    }

    .site-nav-dropdown-trigger {
        width: 100%;
        justify-content: space-between;
    }

    .site-nav-mobile-toggle { display: inline-flex; }
    .section { padding: 4rem 0; }
    .hero { padding: 5rem 0 3rem; }
    .competency-grid { grid-template-columns: 1fr 1fr; }
    .service-grid { grid-template-columns: 1fr; }
    .value-grid { grid-template-columns: 1fr; }
    .site-footer-grid { grid-template-columns: 1fr; gap: 2rem; }
    .site-footer-bottom { flex-direction: column; gap: 0.5rem; }
    .trusted-strip-logos { gap: 1.25rem; }
    .trusted-strip-logo { height: 56px; padding: 0 1.25rem; }
    .trusted-strip-logo img { height: calc(28px * var(--logo-scale, 1)); max-width: 150px; }
    .feature-split + .feature-split { margin-top: 3rem; }
}


/* --- Radzen Material theme bridge --------------------------------- */
/* Maps Radzen primary tokens to Gapfy brand so RadzenButton Primary,    */
/* selected dropdown items, focused inputs, etc. all use the logo blue.  */

:root {
    --rz-primary: #3FA9E1;
    --rz-primary-light: #5DB9E8;
    --rz-primary-lighter: #8DCBED;
    --rz-primary-dark: #2A8FC4;
    --rz-primary-darker: #1F7AAB;
    --rz-on-primary: #FFFFFF;
    --rz-primary-contrast: #FFFFFF;
    --rz-primary-background-color: #3FA9E1;
    --rz-primary-color: #FFFFFF;
}

/* Defensive override for older Radzen builds whose selectors hardcode  */
/* the color instead of reading the CSS variable.                       */
.rz-button.rz-primary,
.rz-button.rz-primary.rz-state-focused {
    background-color: var(--gapfy-primary);
    border-color: var(--gapfy-primary);
    color: #FFFFFF;
}

.rz-button.rz-primary:not(.rz-state-disabled):hover {
    background-color: var(--gapfy-primary-strong);
    border-color: var(--gapfy-primary-strong);
    color: #FFFFFF;
}

.rz-button.rz-primary:not(.rz-state-disabled):active {
    background-color: var(--gapfy-accent);
    border-color: var(--gapfy-accent);
    color: #FFFFFF;
}

/* --- Mouse-tracked glow + spotlight + tilt -------------------------- */
/* Companion to wwwroot/js/tilt-glow.js. The JS writes --mx and --my
   into the element style (percent values from the cursor inside the
   bounding box). The CSS uses those vars on a ::before pseudo
   containing a radial gradient that follows the cursor. Cards still
   have their own border + box-shadow hover transition from earlier —
   this is a layer on top, not a replacement. */

.service-card,
.download-card,
.value-card {
    --mx: 50%;
    --my: 50%;
    overflow: hidden;          /* keep the glow inside the rounded border */
}

.service-card::before,
.download-card::before,
.value-card::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out);
    background: radial-gradient(420px circle at var(--mx) var(--my),
                                rgba(61, 166, 221, 0.16),
                                transparent 45%);
    z-index: 0;
}

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

/* Card content already sits above the ::before via natural stacking
   when the pseudo has z-index 0, but ServiceCard's children render as
   plain blocks — force them to z-index 1 so the glow stays beneath
   the title / text / icon. */
.service-card > *,
.download-card > *,
.value-card > * {
    position: relative;
    z-index: 1;
}

/* When the service card carries an image (.service-card-media), the
   ::before glow is hidden beneath the media because the media is a
   child at z-index 1 covering the top of the card. Add a second
   layer (::after) that sits ABOVE the media using mix-blend-mode
   so the cursor-tracked glow blends into the photograph instead of
   being clipped under it. Title/text/link still need to read clearly
   though, so we lift every non-media child to z-index 3 — that puts
   the order media (1) -> glow-overlay (2) -> text (3). */
.service-card--with-image::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out);
    background: radial-gradient(380px circle at var(--mx) var(--my),
                                rgba(61, 166, 221, 0.30),
                                transparent 50%);
    mix-blend-mode: screen;
    z-index: 2;
}

.service-card--with-image:hover::after {
    opacity: 1;
}

.service-card--with-image > :not(.service-card-media) {
    z-index: 3;
}

/* Spotlight on the Maestro highlight band. Brighter, larger radius,
   uses white so it reads as a sheen on top of the blue gradient. */
[data-spotlight] {
    --mx: 50%;
    --my: 50%;
}

[data-spotlight]::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out);
    background: radial-gradient(600px circle at var(--mx) var(--my),
                                rgba(255, 255, 255, 0.22),
                                transparent 50%);
}

[data-spotlight]:hover::after {
    opacity: 1;
}

/* Make sure spotlight children stay above the ::after. The
   maestro-highlight already has its own positioning, so children
   render in natural document order; we just promote them with z-index. */
[data-spotlight] > * {
    position: relative;
    z-index: 1;
}

/* Tilt — opt-in via data-tilt. Stacking context + preserve-3d so the
   transform actually renders in 3D space. JS writes --tilt-rx and
   --tilt-ry; the transition smooths the snap back to flat on
   mouseleave. */
[data-tilt] {
    --tilt-rx: 0deg;
    --tilt-ry: 0deg;
    transform: perspective(900px) rotateX(var(--tilt-rx)) rotateY(var(--tilt-ry));
    transform-style: preserve-3d;
    transition: transform var(--gapfy-dur-base) var(--gapfy-ease-out);
}

/* Cards combine the existing -4px hover lift with the tilt rotation.
   Without this rule the card-level :hover transform overrides the
   tilt one (same specificity, last wins) and the tilt disappears
   the moment the cursor enters the card. */
.service-card[data-tilt]:hover,
.download-card[data-tilt]:hover {
    transform: translateY(-4px) perspective(900px) rotateX(var(--tilt-rx)) rotateY(var(--tilt-ry));
}

/* --- Page transition ----------------------------------------------- */
/* Companion to wwwroot/js/page-transition.js. The script flips
   .is-entering on <html> for one frame on every Blazor enhanced
   navigation; CSS catches it on <main> and runs a short fade-up. */

main {
    transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out),
                transform var(--gapfy-dur-base) var(--gapfy-ease-out);
}

html.is-entering main {
    opacity: 0;
    transform: translateY(8px);
}

/* --- Scroll reveal -------------------------------------------------- */
/* Companion to wwwroot/js/scroll-reveal.js. The hidden initial state
   only applies when JS has tagged <html> with .has-js, so a no-JS
   browser sees everything immediately (graceful degradation). The
   --reveal-delay CSS variable is set by JS on each child of a
   .reveal-stagger wrapper to chain the animations. */

.has-js .reveal {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity var(--gapfy-dur-slow) var(--gapfy-ease-out),
                transform var(--gapfy-dur-slow) var(--gapfy-ease-out);
    transition-delay: var(--reveal-delay, 0ms);
    will-change: opacity, transform;
}

.has-js .reveal.is-visible {
    opacity: 1;
    transform: translateY(0);
}

/* --- Material Symbols slot clamp ------------------------------------ */
/* Before the Material Symbols font finishes downloading, the browser
   renders the literal ligature names ("arrow_forward", "menu", etc.)
   in the fallback system font. A 13-char string at 18px is ~150px
   wide, which makes any button containing an icon balloon on first
   paint and then snap back when the font arrives.
   Clamping every icon span to a 1em x 1em slot with overflow hidden
   gives the layout a stable footprint immediately: the unstyled text
   is clipped, the eventual glyph fits perfectly. Pair with
   &display=block on the Google Fonts URL to also hide the unstyled
   chars during the block period. */
.material-symbols-outlined {
    width: 1em;
    height: 1em;
    overflow: hidden;
}

/* --- Service illustrations (inline, theme-aware line art) ----------- */
/* Used by ServiceIllustration.razor on the /services/* heroes and the
   Home services grid. The scene markup is written against the local
   --ink/--mut/--ln2/--soft/--a1/--a2/--a3 aliases; mapping them onto the
   brand tokens here means the line work follows the ink/grey ramp (and
   flips light/dark) while the accents stay on the brand violet, magenta
   and cyan in both themes. The knockout fills inside the art use --soft,
   so the surface they sit on must also be --gapfy-bg-soft (see the hero
   art frame and the card media below). */
.svc-illu {
    --ink: var(--gapfy-ink);
    --mut: var(--gapfy-muted);
    --ln2: var(--gapfy-line-strong);
    --soft: var(--gapfy-bg-soft);
    --a1: var(--gapfy-accent-violet);
    --a2: var(--gapfy-accent-magenta);
    --a3: var(--gapfy-primary);
    display: block;
    width: 100%;
    height: auto;
}

/* Card variant fills the fixed-height .service-card-media slot and is
   cropped by preserveAspectRatio="slice". */
.svc-illu--card {
    height: 100%;
}

/* Two-column hero on the /services/* pages: copy on the left, the
   illustration framed on the right. Pages that do not pass an
   IllustrationScene keep the single-column .page-hero-inner untouched. */
.page-hero--with-art .page-hero-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.95fr);
    gap: 3.5rem;
    align-items: center;
}

.page-hero-art {
    background: var(--gapfy-bg-soft);
    border: 1px solid var(--gapfy-line);
    border-radius: var(--gapfy-radius-lg);
    padding: 1.5rem;
    box-shadow: var(--gapfy-shadow);
}

@media (max-width: 880px) {
    .page-hero--with-art .page-hero-layout {
        grid-template-columns: 1fr;
        gap: 2.25rem;
    }
}

/* Line-art motif inside a Home service card: swap the blue photo tint
   for the neutral surface the art is drawn against, and let the inline
   SVG fill the media slot. */
.service-card-media--art {
    background: var(--gapfy-bg-soft);
}

/* --- Reduced motion ------------------------------------------------- */
/* Honour the OS / browser "prefers-reduced-motion: reduce" setting by
   disabling everything decorative: aurora drift on the hero, marquee
   in the trusted strip, hero stagger, scroll-reveals (Wave 2) and any
   tilt / spotlight added in Wave 3. Hover colour transitions are also
   shortened so the page is functionally identical but feels static.
   Canonical pattern recommended by WCAG 2.1 SC 2.3.3. */

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

    /* Carve-out: a colour / opacity cross-fade is NOT motion. WCAG 2.3.3
       targets movement (slides, parallax, drift), not a gradient that
       fades in. So keep the primary-CTA gradient reveal smooth — re-assert
       the opacity fade with higher specificity + !important so it beats the
       *::before reset above. Only the gradient's position sweep (the one
       genuinely-moving part) stays dropped, by leaving it off this list. */
    .btn-primary::before,
    .site-nav-cta::before {
        transition: opacity var(--gapfy-dur-base) var(--gapfy-ease-out) !important;
    }

    .btn-primary,
    .site-nav-cta {
        transition: background-color var(--gapfy-dur-base) var(--gapfy-ease-out),
                    color var(--gapfy-dur-base) var(--gapfy-ease-out),
                    box-shadow var(--gapfy-dur-base) var(--gapfy-ease-out),
                    border-color var(--gapfy-dur-base) var(--gapfy-ease-out) !important;
    }
}
