/* ============================================================
   STACKVEL SOLUTIONS — ANIMATIONS
   Enhanced micro-interactions, entrance choreography,
   smooth hover lifts, image zoom transitions, and the
   reduced-motion contract.
   ============================================================ */

/* ============================================================
   1. CORE KEYFRAMES
   ============================================================ */
@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(22px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fadeInDown {
  from { opacity: 0; transform: translateY(-16px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fadeInRight {
  from { opacity: 0; transform: translateX(24px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes fadeInLeft {
  from { opacity: 0; transform: translateX(-24px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes pulseGlow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(11, 95, 208, 0.28); }
  50%      { box-shadow: 0 0 0 8px rgba(11, 95, 208, 0); }
}

@keyframes subtleFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}

@keyframes dropdownFadeSlide {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

/* ============================================================
   2. HERO ENTRANCE
   Choreographed single-run sequence for hero section across all pages.
   Headings reveal smoothly line-by-line from behind a mask;
   surrounding kickers, descriptions, actions & stats slide up.
   ============================================================ */

/* The eyebrow pill on the inner pages. The home page hero used to
   share this rule; its whole sequence is now in section 9, because
   it runs on load rather than on render and it travels from the
   right rather than up. */
.hero__eyebrow {
  animation: fadeInUp 0.65s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.06s both;
}

/* With JavaScript the headline entrance is the per-line mask reveal */
.js .hero__headline.reveal-text {
  animation: none;
  opacity: 1;
}

html:not(.js) .hero__headline {
  animation: fadeInUp 0.8s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.16s both;
}

/* All Subpages Hero (.page-hero) */
.page-hero .breadcrumbs {
  animation: fadeInUp 0.6s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.05s both;
}

.page-hero .hero__eyebrow,
.page-hero .hero__kicker,
.page-hero .section-eyebrow {
  animation: fadeInUp 0.65s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.12s both;
}

/* When JavaScript is active, the page hero title line-mask reveal handles the entrance */
.js .page-hero .page-hero__title.reveal-text,
.js .page-hero h1.reveal-text {
  animation: none;
  opacity: 1;
}

html:not(.js) .page-hero .page-hero__title,
html:not(.js) .page-hero h1 {
  animation: fadeInUp 0.8s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.20s both;
}

.page-hero .error-code {
  animation: fadeInUp 0.8s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.15s both;
}

.page-hero .page-hero__desc,
.page-hero .page-hero__lead,
.page-hero p:not(.hero__kicker):not(.hero__eyebrow) {
  animation: fadeInUp 0.8s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.32s both;
}

.page-hero .page-hero__actions,
.page-hero .btn {
  animation: fadeInUp 0.75s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.44s both;
}

.page-hero .hero__stats,
.page-hero .page-hero__meta,
.page-hero .page-hero__tags {
  animation: fadeInUp 0.75s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.54s both;
}

/* ============================================================
   3. ENHANCED HOVER & INTERACTION EFFECTS
   ============================================================ */

/* --- Buttons --- */
.btn {
  transition: background-color 0.25s ease, color 0.25s ease,
              border-color 0.25s ease, box-shadow 0.25s ease,
              transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn-primary:hover {
  box-shadow: 0 8px 24px rgba(11, 95, 208, 0.32);
}

.btn-secondary:hover {
  box-shadow: 0 6px 18px rgba(16, 30, 67, 0.08);
}

/* Subtle button text micro-movement */
.btn span:not(.btn-disc__disc),
.btn .btn__text {
  display: inline-block;
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.btn:hover span:not(.btn-disc__disc),
.btn:hover .btn__text {
  transform: translateX(1px);
}

/* Arrow slide on hover */
.btn svg,
.btn-arrow svg,
.link-arrow svg {
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.btn:hover svg,
.btn-arrow:hover svg,
.link-arrow:hover svg {
  transform: translateX(4px);
}

/* --- Service, Project & Feature Cards --- */
.service-card,
.work-card,
.case-row,
.case-card,
.feature,
.benefit-card,
.industry-card,
.challenge-card,
.value-item {
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
              box-shadow 0.35s ease,
              border-color 0.3s ease;
}

.service-card:hover,
.work-card:hover,
.case-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 20px 48px rgba(16, 30, 67, 0.12), 0 4px 12px rgba(11, 95, 208, 0.06);
  border-color: rgba(11, 95, 208, 0.35);
}

.feature:hover,
.benefit-card:hover,
.industry-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 14px 36px rgba(16, 30, 67, 0.09);
  border-color: rgba(11, 95, 208, 0.25);
}

.case-row:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 40px rgba(16, 30, 67, 0.09);
}

/* --- Smooth Image Hover Transitions ---

   Deliberately NOT applied to .work-card__frame. That frame holds a
   full-page capture and scrolls it on hover, which is the effect the
   home page describes in words ("walk down the page the way a visitor
   would see it"). A scale() here overrode that translate and turned a
   walkthrough into a nudge, so the frame keeps its own rule in
   main.css and this only covers the images that really do just zoom. */
.media img,
.media > picture > img,
.service-card__media img,
.case-row__media .media img {
  transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1),
              filter 0.4s ease;
}

.service-card:hover .service-card__media img,
.case-row:hover .case-row__media .media img,
a:hover > .media img,
a:focus-visible > .media img {
  transform: scale(1.045);
}

/* A quiet scrim that deepens on hover, so a photograph behind a card
   link reads as interactive rather than as wallpaper. One overlay
   spec, used everywhere an image sits inside a link. */
.media { position: relative; overflow: hidden; }

.media::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
              rgba(10, 19, 48, 0) 40%,
              rgba(10, 19, 48, 0.28) 100%);
  opacity: 0;
  transition: opacity 0.4s ease;
}

.service-card:hover .service-card__media::after,
.case-row:hover .case-row__media .media::after,
a:hover > .media::after,
a:focus-visible > .media::after { opacity: 1; }

/* The service card's own picture frame joins the same system */
.service-card__media { position: relative; overflow: hidden; }

.service-card__media::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
              rgba(10, 19, 48, 0) 45%,
              rgba(10, 19, 48, 0.26) 100%);
  opacity: 0;
  transition: opacity 0.4s ease;
}

/* --- Navigation & Dropdown Animations --- */
.nav-link {
  position: relative;
  transition: color 0.2s ease;
}

.nav-link::after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: 50%;
  width: 0;
  height: 2px;
  background: var(--c-primary);
  border-radius: var(--radius-full, 9999px);
  transform: translateX(-50%);
  transition: width 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}

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

.nav-item:hover .nav-dropdown,
.nav-item:focus-within .nav-dropdown,
.nav-dropdown.is-open {
  animation: dropdownFadeSlide 0.25s cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* --- Statistics Smooth Tabular Display --- */
.hero__stat-value,
.stat-card__value {
  font-variant-numeric: tabular-nums;
}

/* --- Mobile Menu Interactive Drawer --- */
.mobile-menu {
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.25s ease;
}

.mobile-nav-link {
  transition: color 0.2s ease, padding-left 0.2s ease;
}

.mobile-nav-link:hover {
  padding-left: var(--space-2, 0.5rem);
}

/* ============================================================
   4. REDUCED MOTION CONTRACT
   Strictly honour user preferences: eliminate motion, keep layout
   crisp, readable and fully accessible.
   ============================================================ */
@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;
  }

  .hero__eyebrow,
  .hero__kicker,
  .hero__headline,
  .hero__subheadline,
  .hero__actions,
  .hero__stats,
  .page-hero .breadcrumbs,
  .page-hero .hero__eyebrow,
  .page-hero .hero__kicker,
  .page-hero .section-eyebrow,
  .page-hero .page-hero__title,
  .page-hero h1,
  .page-hero .error-code,
  .page-hero .page-hero__desc,
  .page-hero .page-hero__lead,
  .page-hero p,
  .page-hero .page-hero__actions,
  .page-hero .btn,
  .page-hero .hero__stats,
  .page-hero .page-hero__meta,
  .page-hero .page-hero__tags {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .reveal,
  .stagger-children > *,
  .media-reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  .btn:hover,
  .btn:active,
  .service-card:hover,
  .work-card:hover,
  .case-row:hover,
  .case-card:hover,
  .feature:hover,
  .benefit-card:hover,
  .industry-card:hover,
  .platform-card:hover,
  .challenge-card:hover,
  .value-item:hover,
  .legal-nav__link:hover {
    transform: none !important;
  }

  .btn:hover span:not(.btn-disc__disc),
  .btn:hover .btn__text,
  .btn:hover svg,
  .btn-arrow:hover svg,
  .link-arrow:hover svg,
  .nav-dropdown__item a:hover,
  .mobile-nav-link:hover {
    transform: none !important;
  }

  /* scaleX(0), not none: this rail is hidden at rest and drawn in on
     hover. `none` would leave it drawn on every card. */
  .service-card:hover::after { transform: scaleX(0); }

  .link-arrow:hover { gap: var(--space-2); }
  .mobile-nav-link:hover { padding-left: 0; }

  .nav-item:hover .nav-dropdown,
  .nav-item:focus-within .nav-dropdown,
  .nav-dropdown.is-open { animation: none !important; }

  .service-card:hover .service-card__media img,
  .work-card:hover .work-card__frame img,
  .case-row:hover .case-row__media img,
  .work-card__frame:hover img {
    transform: none !important;
  }

  .work-card__frame::after { opacity: 0 !important; }
}

/* ============================================================
   5. CONSISTENCY PASS
   The effects the rest of the site already had, applied to the
   components that had been left out of them, plus the focus
   treatment that makes every one of them reachable by keyboard.
   ============================================================ */

/* --- Nav dropdown items arrive in sequence ------------------ */
.nav-dropdown__item {
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.22s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
              transform 0.22s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}

.nav-item:hover .nav-dropdown__item,
.nav-item:focus-within .nav-dropdown__item,
.nav-dropdown.is-open .nav-dropdown__item {
  opacity: 1;
  transform: translateY(0);
}

.nav-dropdown__item:nth-child(1) { transition-delay: 20ms; }
.nav-dropdown__item:nth-child(2) { transition-delay: 40ms; }
.nav-dropdown__item:nth-child(3) { transition-delay: 60ms; }
.nav-dropdown__item:nth-child(4) { transition-delay: 80ms; }
.nav-dropdown__item:nth-child(5) { transition-delay: 100ms; }
.nav-dropdown__item:nth-child(6) { transition-delay: 120ms; }

/* A dismissed panel must not animate its rows back in behind the
   pointer, so the delay is dropped the moment it closes. */
.nav-dropdown.is-dismissed .nav-dropdown__item {
  transition-delay: 0ms;
  opacity: 0;
}

/* --- Mobile drawer: rows follow the panel in ---------------- */
.mobile-menu__nav > *,
.mobile-menu__footer > * {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.3s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
              transform 0.3s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}

.mobile-menu.is-open .mobile-menu__nav > *,
.mobile-menu.is-open .mobile-menu__footer > * {
  opacity: 1;
  transform: translateY(0);
}

.mobile-menu.is-open .mobile-menu__nav > *:nth-child(1) { transition-delay: 60ms; }
.mobile-menu.is-open .mobile-menu__nav > *:nth-child(2) { transition-delay: 100ms; }
.mobile-menu.is-open .mobile-menu__nav > *:nth-child(3) { transition-delay: 140ms; }
.mobile-menu.is-open .mobile-menu__nav > *:nth-child(4) { transition-delay: 180ms; }
.mobile-menu.is-open .mobile-menu__nav > *:nth-child(5) { transition-delay: 220ms; }
.mobile-menu.is-open .mobile-menu__nav > *:nth-child(6) { transition-delay: 260ms; }
.mobile-menu.is-open .mobile-menu__nav > *:nth-child(7) { transition-delay: 300ms; }
.mobile-menu.is-open .mobile-menu__nav > *:nth-child(8) { transition-delay: 340ms; }
.mobile-menu.is-open .mobile-menu__footer > * { transition-delay: 380ms; }

/* --- Cards that were outside the hover system --------------- */
.platform-card,
.note,
.legal-nav__link,
.capability-item,
.stat-card {
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
              box-shadow 0.3s ease,
              border-color 0.25s ease,
              background-color 0.2s ease;
}

.capability-item:hover,
.stat-card:hover {
  border-color: var(--c-border-accent);
  box-shadow: var(--shadow-md);
}

/* --- Footer social marks pick up the card lift -------------- */
.footer-social__link {
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),
              background-color 0.25s ease,
              border-color 0.25s ease,
              color 0.25s ease;
}

.footer-social__link:hover { transform: translateY(-2px); }

/* --- Table of contents links ------------------------------- */
.legal-toc__list a { transition: color 0.2s ease, border-color 0.2s ease; }

/* --- Keyboard focus, stated once --------------------------- */
.btn:focus-visible,
.nav-link:focus-visible,
.footer-nav__link:focus-visible,
.footer-legal a:focus-visible,
.legal-nav__link:focus-visible,
.legal-toc__list a:focus-visible,
.service-card:focus-visible,
.work-card__frame:focus-visible,
.benefit-card:focus-visible,
.card-link:focus-visible,
.note__summary:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}

.page-hero .btn:focus-visible,
.hero .btn:focus-visible { outline-color: #fff; }

/* ============================================================
   6. REDUCED MOTION — the section 5 additions
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .nav-dropdown__item,
  .mobile-menu__nav > *,
  .mobile-menu__footer > * {
    opacity: 1 !important;
    transform: none !important;
    transition-delay: 0ms !important;
  }

  .footer-social__link:hover,
  .capability-item:hover,
  .stat-card:hover,
  .platform-card:hover { transform: none !important; }

  .footer-nav__link::after { transition: none !important; }
}

/* ============================================================
   7. TOUCH DEVICES
   Hover is a pointer idea. On a touch screen the "hover" state
   sticks after a tap, so the lifts are simply not offered.
   ============================================================ */
/* ============================================================
   GEOMETRY RESET — the lift and the shadow

   Two situations need this, for different reasons, so the query is
   a list rather than a single condition.

   `hover: none` is the touch case: a tap leaves :hover applied to
   whatever was tapped until something else is, so a card keeps its
   lifted shadow while every card around it sits flat.

   `max-width: 1024px` is the narrow case, and it is not the same
   thing. A desktop browser resized to phone width — or Chrome's
   responsive mode with touch emulation off — still reports a fine
   pointer, so none of the touch rules match, while the grids have
   already stacked to a single column with row-gap 0. A 4px lift
   plus a shadow reaching 4px above the box and 32px below then
   runs straight into the cards either side of it.

   Only geometry is reset here. Colour feedback on hover is left
   alone at narrow widths: with a real mouse it is wanted, and it
   cannot overlap anything. The colour resets stay in the touch-only
   block further down.
   ============================================================ */
@media (hover: none), (max-width: 1024px) {

  /* ----------------------------------------------------------
     A tap on a touch screen leaves :hover applied to whatever was
     tapped until something else is tapped. Any hover state that
     changes a lasting property therefore sticks — which is what put
     a card in the middle of the home page's service list wearing a
     lifted shadow, a blue title and a highlighted border while every
     card around it sat flat.

     Resetting `transform` alone was not enough: that stopped the
     card moving, but the shadow, the border and the title colour all
     stayed. Each component is put back to its own resting value
     below, because there is no CSS keyword for "the value this would
     have had without the hover".
     ---------------------------------------------------------- */

  /* Nothing lifts */
  .btn:hover,
  .btn:active,
  .feature:hover,
  .feature-item:hover,
  .service-card:hover,
  .work-card:hover,
  .work-card__frame:hover,
  .case-card:hover,
  .case-row:hover,
  .benefit-card:hover,
  .industry-card:hover,
  .challenge-card:hover,
  .value-item:hover,
  .capability-item:hover,
  .stat-card:hover,
  .platform-card:hover,
  .process-card:hover,
  .solution-item:hover,
  .sector:hover,
  .sector__caps > li:hover,
  .sector__cta .link-arrow:hover,
  .tech-badge:hover,
  .legal-nav__link:hover,
  .note:hover,
  .card:hover,
  .footer-social__link:hover,
  .sv-chat__launcher:hover { transform: none; }

  /* The resting shadow each of these goes back to. Every value below
     was read out of the engine at 360, 700, 768 and 1024px and is the
     same at all four — not inferred from the source order, which is
     how an earlier version of this block got six of them wrong.
     `.service-card`, `.benefit-card`, `.note` and `.process-card` in
     particular are declared with `var(--shadow-card)` and then reset
     to none by a later rule, so the declaration is not the answer. */

  /* rest with no shadow */
  .feature:hover,
  .feature-item:hover,
  .service-card:hover,
  .work-card:hover,
  .case-card:hover,
  .case-row:hover,
  .benefit-card:hover,
  .industry-card:hover,
  .value-item:hover,
  .capability-item:hover,
  .platform-card:hover,
  .process-card:hover,
  .solution-item:hover,
  .tech-badge:hover,
  .legal-nav__link:hover,
  .note:hover,
  .faq-item:hover { box-shadow: none; }

  /* rest on the standard card shadow */
  .challenge-card:hover,
  .stat-card:hover { box-shadow: var(--shadow-card); }

  /* and the two with a shadow of their own */
  .sector:hover { box-shadow: 0 4px 20px rgba(16, 30, 67, 0.03); }
  .work-card__frame:hover { box-shadow: 0 1px 2px rgba(16, 30, 67, 0.05); }

}

@media (hover: none) {
  /* Borders, grouped by the value each actually rests on — measured,
     for the same reason as the shadows above. The four components
     with a zero-width border are not listed: their border colour
     cannot show. */
  .feature-item:hover,
  .capability-item:hover,
  .stat-card:hover,
  .solution-item:hover,
  .legal-nav__link:hover,
  .faq-item:hover { border-color: var(--c-border); }

  .service-card:hover,
  .benefit-card:hover,
  .platform-card:hover,
  .process-card:hover,
  .note:hover,
  .work-card__frame:hover { border-color: var(--hairline); }

  .challenge-card:hover,
  .tech-badge:hover { border-color: var(--c-border-strong); }
  .sector:hover { border-color: rgba(16, 30, 67, 0.07); }
  .sector__caps > li:hover { border-color: rgba(16, 30, 67, 0.05); }

  /* Backgrounds */
  .value-item:hover { background: transparent; }
  .platform-card:hover { background: var(--c-surface-soft); }
  .feature-item:hover { background: var(--c-surface-soft); }
  .tech-badge:hover,
  .legal-nav__link:hover { background: var(--c-white); }
  .sector__caps > li:hover { background: rgba(246, 248, 252, 0.7); }

  /* Headings stay the heading colour rather than turning blue */
  .feature:hover .feature__title,
  .service-card:hover .service-card__title { color: var(--c-heading); }

  /* Icon chips keep their resting fill */
  .service-card:hover .service-card__icon {
    background: var(--c-white);
    border-color: var(--c-border);
    box-shadow: var(--shadow-sm);
  }
  .service-card:hover .service-card__icon svg { color: var(--c-primary); transform: none; }

  .feature-item:hover .feature-item__icon {
    background: var(--c-white);
    border-color: var(--c-border);
  }

  .value-item:hover .value-item__icon {
    background: var(--c-primary-tint);
    border-color: var(--c-border);
  }
  .value-item:hover .value-item__icon svg { transform: none; }

  .capability-item:hover .capability-item__icon {
    background: var(--c-primary-tint);
    border-color: var(--c-border-soft);
  }

  /* Arrows, rules and other small travelling parts */
  .btn:hover span:not(.btn-disc__disc),
  .btn:hover svg,
  .btn-arrow:hover svg,
  .link-arrow:hover svg,
  .feature:hover .feature__link::after,
  .feature:hover::after,
  .service-card:hover .service-card__link svg,
  .btn-disc:hover .btn-disc__disc,
  .sector__cta .link-arrow:hover svg,
  .process-step:hover .process-step__num { transform: none; }

  .link-arrow:hover { gap: var(--space-2); }
  .service-card:hover .service-card__link { gap: var(--space-2); }
  .mobile-nav-link:hover { padding-left: 0; }
  .sector-rail__item a:hover { padding-left: var(--space-4); }

  /* A touch laptop is wider than 1024px, so the desktop bar and the
     carousel arrows are both on screen and both tappable. Neither
     should be left in a hover state by the tap that opened them. */
  /* The dropdown is NOT reset. Its resting transform is
     translateX(-50%), which is what centres the panel under its
     trigger — setting it to none would throw the panel half its own
     width to the left. Opening on tap is also the only way to reach
     the submenu on a touch laptop, so it is wanted behaviour rather
     than a stuck state; navigation.js closes it on an outside tap.
     Only the parts that travel purely for decoration are stilled. */
  .nav-item:hover .nav-dropdown__item { transform: none; }
  .nav-item:hover .nav-link svg { transform: none; }
  .nav-dropdown__item a:hover { transform: none; }

  /* Its resting transform is translateY(-50%), which is what centres
     it on the track — so this restates the other four properties and
     leaves transform alone. Resetting it to none here would drop the
     arrow half its own height. */
  .testimonials__arrow:hover {
    background: var(--c-white);
    border-color: var(--hairline);
    box-shadow: 0 6px 20px rgba(16, 30, 67, .10);
    color: var(--c-heading);
  }

  /* Images do not zoom without a pointer to drive the zoom */
  .service-card:hover .service-card__media img,
  .case-row:hover .case-row__media .media img,
  .sector:hover .sector__media img { transform: none; }

  .service-card:hover .service-card__media::after,
  .case-row:hover .case-row__media .media::after { opacity: 0; }

  /* The work-card walkthrough needs a pointer to drive it; on touch
     the capture stays at the top of the frame, where the useful part
     of a website screenshot is anyway, and the badge is always shown
     because there is no hover to reveal it. */
  .work-card__frame:hover img { transform: translateY(0); }
  .work-card__frame .work-card__live { opacity: 1; transform: translateY(0); }
}

/* ============================================================
   8. FOOTER RISE

   The footer lines come up from below and fade in as it scrolls
   into view, column by column and row by row, in reading order.

   Two details worth knowing before changing this:

   1. It animates `translate`, not `transform`. They are separate
      properties, so the rise composes with the hover lifts the
      social marks and the footer CTA already have rather than
      overriding them — a `transform` keyframe with fill-mode
      `both` holds its end value forever and would kill both.

   2. It is a keyframe animation rather than a transition, so it
      never has to restate the `transition` list each of these
      elements already carries for its own hover.

   The hidden start state is scoped to `.js`, so with scripting
   off the footer is simply there, as everywhere else on the site.
   ============================================================ */
@keyframes footerRise {
  from { opacity: 0; translate: 0 16px; }
  to   { opacity: 1; translate: 0 0; }
}

/* The lines that move. Every one is a leaf — nothing here contains
   anything else on this list, so nothing travels twice. */
.js .footer-rise :is(
  .site-logo,
  .footer-brand__desc,
  .footer-brand__note,
  .footer-social__link,
  .footer-col__title,
  .footer-nav__link,
  .footer-contact-item,
  .footer-contact__cta,
  .footer-copyright,
  .footer-legal a
) { opacity: 0; }

.footer-rise.is-visible :is(
  .site-logo,
  .footer-brand__desc,
  .footer-brand__note,
  .footer-social__link,
  .footer-col__title,
  .footer-nav__link,
  .footer-contact-item,
  .footer-contact__cta,
  .footer-copyright,
  .footer-legal a
) {
  animation: footerRise 0.62s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both;
  animation-delay: calc(var(--fr-col, 0ms) + var(--fr-item, 0ms));
}

/* --- Column offset ------------------------------------------
   Set on the four columns and inherited by everything inside
   them, so each column starts a beat after the one to its left. */
.footer-top > *:nth-child(1) { --fr-col: 0ms; }
.footer-top > *:nth-child(2) { --fr-col: 70ms; }
.footer-top > *:nth-child(3) { --fr-col: 140ms; }
.footer-top > *:nth-child(4) { --fr-col: 210ms; }

/* --- Brand column ------------------------------------------- */
.footer-brand .site-logo          { --fr-item: 0ms; }
.footer-brand__desc               { --fr-item: 60ms; }
.footer-brand__note               { --fr-item: 110ms; }
.footer-social__link:nth-child(1) { --fr-item: 160ms; }
.footer-social__link:nth-child(2) { --fr-item: 200ms; }
.footer-social__link:nth-child(3) { --fr-item: 240ms; }

/* --- Link and contact columns -------------------------------
   The heading leads, then its rows follow it down the column. */
.footer-col__title { --fr-item: 0ms; }

.footer-nav__link:nth-child(1) { --fr-item: 55ms; }
.footer-nav__link:nth-child(2) { --fr-item: 90ms; }
.footer-nav__link:nth-child(3) { --fr-item: 125ms; }
.footer-nav__link:nth-child(4) { --fr-item: 160ms; }
.footer-nav__link:nth-child(5) { --fr-item: 195ms; }
.footer-nav__link:nth-child(6) { --fr-item: 230ms; }
.footer-nav__link:nth-child(7) { --fr-item: 265ms; }
.footer-nav__link:nth-child(8) { --fr-item: 300ms; }

.footer-contact-item:nth-child(1) { --fr-item: 55ms; }
.footer-contact-item:nth-child(2) { --fr-item: 95ms; }
.footer-contact-item:nth-child(3) { --fr-item: 135ms; }
.footer-contact-item:nth-child(4) { --fr-item: 175ms; }
.footer-contact__cta              { --fr-item: 220ms; }

/* --- Bottom bar ---------------------------------------------
   Its own reveal, so it arrives when the reader reaches it
   rather than having already played further up the page. */
.footer-copyright            { --fr-item: 0ms; }
.footer-legal a:nth-child(1) { --fr-item: 55ms; }
.footer-legal a:nth-child(2) { --fr-item: 90ms; }
.footer-legal a:nth-child(3) { --fr-item: 125ms; }
.footer-legal a:nth-child(4) { --fr-item: 160ms; }
.footer-legal a:nth-child(5) { --fr-item: 195ms; }

/* ------------------------------------------------------------
   Reduced motion. The global rule at the top of section 4 cuts
   the duration but not the delay, which would leave the lines
   popping in one at a time over half a second — the staccato
   version of the thing the setting asks us not to do.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .js .footer-rise :is(
    .site-logo,
    .footer-brand__desc,
    .footer-brand__note,
    .footer-social__link,
    .footer-col__title,
    .footer-nav__link,
    .footer-contact-item,
    .footer-contact__cta,
    .footer-copyright,
    .footer-legal a
  ) {
    animation: none !important;
    animation-delay: 0ms !important;
    opacity: 1 !important;
    translate: none !important;
  }
}

/* ============================================================
   9. HOME HERO — LOAD SEQUENCE

   Two motions, opposed. The hand reaches in from off the left
   edge (its keyframes live with the rest of the hand, in main.css
   section 68); the copy arrives from the right, one element at a
   time, over about two seconds.

   Everything here waits for `is-loaded` on <html>, which main.js
   adds as the page loader starts to lift. Before that the elements
   sit at opacity 0. That gate is the whole point: without it the
   sequence runs while the loader is still covering the page and is
   finished before the visitor sees anything.

   main.js sets `is-loaded` unconditionally, and again on a 1600ms
   timer, so a failure anywhere upstream cannot leave the hero
   blank. The hidden states are all scoped to `.js` as well, so
   with scripting off the hero is simply there.
   ============================================================ */
/* Named for the side the copy used to arrive from. The hand now
   reaches in from the right, so the words come the other way to meet
   it; the name is kept because it is referenced a dozen times below
   and the direction is one number. */
@keyframes copyInRight {
  from { opacity: 0; transform: translate3d(-42px, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* --- Held until the loader lifts ---------------------------- */
.js:not(.is-loaded) .hero__kicker,
.js:not(.is-loaded) .hero__lead-in,
.js:not(.is-loaded) .hero__subheadline,
.js:not(.is-loaded) .hero__actions,
.js:not(.is-loaded) .hero__stat { opacity: 0; }

/* The headline is a masked per-line reveal, not a slide. reveal.js
   marks it visible on the first frame, so it has to be pushed back
   under the line's start state until the gate opens — hence the
   specificity here, which has to beat `.reveal-text.is-visible`. */
.js:not(.is-loaded) .hero__headline .line > span {
  transform: translateY(115%);
  opacity: 0;
}

/* --- The sequence ------------------------------------------- */
.js.is-loaded .hero__kicker {
  animation: copyInRight 0.85s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.15s both;
}

/* the heading's three lines, wiping up out of their masks */
.js.is-loaded .hero__headline.is-visible .line:nth-child(1) > span { transition-delay: 0.28s; }
.js.is-loaded .hero__headline.is-visible .line:nth-child(2) > span { transition-delay: 0.37s; }
.js.is-loaded .hero__headline.is-visible .line:nth-child(3) > span { transition-delay: 0.46s; }
.js.is-loaded .hero__headline.is-visible .line:nth-child(4) > span { transition-delay: 0.55s; }

.js.is-loaded .hero__lead-in {
  animation: copyInRight 0.9s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.58s both;
}

.js.is-loaded .hero__subheadline {
  animation: copyInRight 0.9s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.68s both;
}

.js.is-loaded .hero__actions {
  animation: copyInRight 0.9s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 0.80s both;
}

.js.is-loaded .hero__stat {
  animation: copyInRight 0.85s var(--ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both;
}
.js.is-loaded .hero__stat:nth-child(1) { animation-delay: 0.90s; }
.js.is-loaded .hero__stat:nth-child(2) { animation-delay: 0.97s; }
.js.is-loaded .hero__stat:nth-child(3) { animation-delay: 1.04s; }

/* The counters in the stats row are animated by reveal.js on an
   IntersectionObserver, which fires while these are still at
   opacity 0. Nothing to fix — the numbers simply finish counting
   at about the moment the row arrives. */

/* ------------------------------------------------------------
   9.1  Reduced motion

   Nothing travels and nothing is staggered. The gate still has to
   be released, or the hero would stay at opacity 0 for a visitor
   who asked only for less movement, not for a blank page.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .js .hero__kicker,
  .js .hero__lead-in,
  .js .hero__subheadline,
  .js .hero__actions,
  .js .hero__stat,
  .js:not(.is-loaded) .hero__kicker,
  .js:not(.is-loaded) .hero__lead-in,
  .js:not(.is-loaded) .hero__subheadline,
  .js:not(.is-loaded) .hero__actions,
  .js:not(.is-loaded) .hero__stat {
    animation: none !important;
    animation-delay: 0ms !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .js:not(.is-loaded) .hero__headline .line > span,
  .js.is-loaded .hero__headline.is-visible .line > span {
    transform: none !important;
    opacity: 1 !important;
    transition-delay: 0ms !important;
  }

}

