:root {
  --ink: #0b0c0d;
  --paper: #f8f8f6;
  --muted: #a6a8a8;
  --green: #63e77d;
  --green-dark: #397a45;
  /* How far the footage comes up out of the near-black. Lives here because three
     rules need it — the pre-motion resting state, the revealed state and the
     mobile override — and they have to agree. */
  --hero-video-opacity: 0.5;
  /* Darkens the raw footage on top of --hero-video-opacity, independent of it —
     this is a flat scrim over the video itself, not the reveal-in blend against
     the near-black shell. Tune 0.35–0.7. */
  --hero-video-scrim: 0.55;
  /* Hero glow, appearance half. The motion half — period, travel and the angle
     both the drift and the band are aligned to — is at the top of
     HeroBackgroundVideo.jsx.

     Strength is what survives --hero-video-opacity, so the number here is
     roughly twice what lands on screen: 0.5 reads as about 0.25. */
  --glow-strength: 0.5;
  /* Its own token rather than a palette colour: the band is the only thing this
     hue serves, and it blends with `screen` against near-black, so it has to be
     light enough to lift the backdrop — --purple is far too dark for that. */
  --glow-color: #a06bff;
  --glow-band-length: 78%;
  --glow-band-width: 16%;
  /* Where the vertical fade reaches zero, measured into the glow layer from its
     own top and bottom. Has to land inside the crop .hero-video makes at 33% —
     the gap between the two is the room the drift needs. */
  --glow-fade: 25%;
  /* Hero <em>: a conic sweep through three cool stops, the first repeated at the
     end so the wheel closes on itself with no seam. */
  --em-pink: #ff7fc4;
  --em-sky: #6dd5fa;
  --em-mint: #7ff0d8;
  --em-stop-1: 0%;
  --em-stop-2: 33%;
  --em-stop-3: 66%;
  --em-stop-4: 100%;
  --em-sweep-x: 50%;
  --em-sweep-y: 50%;
  --em-sweep-period: 18s;

  /* Hero star. Size is a flat pixel value, so unlike the heading it does not
     follow the h1's clamp: on small screens the type shrinks under it and the
     star keeps its 90px. */
  --star-size: 90px;
  --star-rest-angle: 10deg;
  --star-tuck: -0.14em;
  --star-glow: #c9b6ff;
  --purple: #4b235e;
  --footer-ink: #1d1e22;
  --font-display: "Inter", Arial, Helvetica, sans-serif;
  --radius-lg: 58px;
  --radius-md: 22px;
  --max: 1200px;
  /* Nav and hero grid only — kept in sync on purpose (the nav sits directly
     above the hero, so the logo and the heading below it should line up).
     Every other section frame stays on --max. */
  --hero-max: 1400px;
  --motion-press: 110ms;
  --motion-fast: 180ms;
  --motion-hover: 280ms;
  --motion-reveal: 720ms;
  /* Long on purpose: the pill's knob crosses the full button width, so it needs
     travel time the 280ms hover tier cannot give it. */
  --motion-swap: 500ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-smooth: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-in: cubic-bezier(0.32, 0, 0.67, 0);
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  background: var(--ink);
}

body {
  margin: 0;
  color: #15191d;
  background: var(--ink);
  font-family: "Inter", Arial, Helvetica, sans-serif;
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
}

a {
  color: inherit;
  text-decoration: none;
}

button,
input,
textarea {
  font: inherit;
}

button,
a {
  -webkit-tap-highlight-color: transparent;
}

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
  outline: 3px solid var(--green);
  outline-offset: 4px;
}

html.motion-ready [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 28px, 0);
  transition:
    opacity var(--motion-reveal) var(--ease-out),
    transform var(--motion-reveal) var(--ease-out);
}

html.motion-ready [data-reveal="nav"] {
  transform: translate3d(0, -14px, 0);
}

html.motion-ready [data-reveal="media"] {
  transform: translate3d(0, 18px, 0) scale(0.965);
  transform-origin: center right;
}

html.motion-ready [data-reveal].is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
}

html.motion-ready .hero-video[data-reveal].is-visible {
  opacity: var(--hero-video-opacity);
}

/* Standalone crops now, not windows onto a shared sprite — the file is already
   the right frame, so it only needs to fill its box. */
.hero-shot {
  display: block;
  object-fit: cover;
  user-select: none;
  pointer-events: none;
}

.hero-shell {
  position: relative;
  isolation: isolate;
  min-height: 790px;
  color: #f7f7f4;
  background:
    radial-gradient(circle at 69% 36%, rgba(23, 28, 25, 0.42), transparent 24%),
    #050606;
  overflow: hidden;
}

.hero-video {
  position: absolute;
  top: 96px;
  left: 30%;
  z-index: 0;
  width: min(1078px, 77vw);
  height: 572px;
  overflow: hidden;
  opacity: var(--hero-video-opacity);
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 8%,
    #000 91%,
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 8%,
    #000 91%,
    transparent 100%
  );
  pointer-events: none;
}

.hero-video video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center right;
}

/* Flat scrim over the raw footage. Sits between the video and the glow band
   (below in DOM, both z-index:auto positioned so paint order follows DOM
   order) so it darkens the footage without muting the purple sweep on top. */
.hero-video-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, var(--hero-video-scrim));
  pointer-events: none;
}

/* Purple band sweeping along the footage. It sits inside .hero-video, so the
   wrapper's own opacity lands on top of --glow-strength, and the blend resolves
   against the video rather than the page — the wrapper's opacity makes it a
   stacking context, which keeps the effect off everything behind it.

   `screen` rather than `overlay`: overlay keys off the backdrop, and against
   the hero's near-black it collapses to 2 x backdrop x source, which is still
   near-black whatever colour goes in. Screen lifts a dark backdrop by roughly
   the source itself, which is the whole point of a glow. */
.hero-video-glow {
  position: absolute;
  /* Oversized so the drift never walks an edge into frame. */
  inset: -25%;
  opacity: var(--glow-strength);
  mix-blend-mode: screen;
  pointer-events: none;
  will-change: transform;
  /* The layer overhangs .hero-video by 25% on each side and the wrapper clips
     with overflow: hidden, so the band's falloff was being cut mid-ramp — at the
     bottom edge that reads as a hard horizontal line. The wrapper's own mask
     cannot help: it runs at 90deg and only softens left and right.

     Fading here rather than widening that mask keeps the video at full height —
     the video is the wrapper's other child and would fade with it. */
  mask-image: linear-gradient(
    180deg,
    transparent var(--glow-fade),
    #000 calc(var(--glow-fade) + 18%),
    #000 calc(100% - var(--glow-fade) - 18%),
    transparent calc(100% - var(--glow-fade))
  );
  -webkit-mask-image: linear-gradient(
    180deg,
    transparent var(--glow-fade),
    #000 calc(var(--glow-fade) + 18%),
    #000 calc(100% - var(--glow-fade) - 18%),
    transparent calc(100% - var(--glow-fade))
  );
}

/* The band is a separate layer purely so it can hold the rotation: the element
   above it animates transform, and a rotation parked there would be overwritten
   every frame. Length runs with the drift, width across it. */
.hero-video-glow::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse var(--glow-band-length) var(--glow-band-width) at 50% 50%,
    var(--glow-color) 0%,
    transparent 100%
  );
  transform: rotate(var(--glow-angle));
}

/* The client decides not to render this at all, but the server cannot know the
   preference — without this the first paint would flash a glow that JavaScript
   then removes. */
@media (prefers-reduced-motion: reduce) {
  .hero-video-glow {
    display: none;
  }
}

.hero-content {
  position: relative;
  z-index: 2;
}

.trusted,
.why-inner,
.audience-inner,
.plumrocket-inner {
  width: min(var(--max), calc(100% - 48px));
  margin-inline: auto;
}

/* The closing block carries the contact form, the map and the legal row side by
   side, so it runs on the same --max page frame as the nav and the sections. */
.footer-inner {
  position: relative;
  z-index: 1;
  width: min(var(--max), calc(100% - 48px));
  margin-inline: auto;
}

/* Flex, not a fixed-column grid: a grid with equal edge columns centers
   .nav-links on the *nav's* midpoint, which only matches the *visual*
   midpoint between the logo and the actions block when both happen to be the
   same width — they aren't (brand ~160px vs. actions ~195–275px depending on
   whether the icons are showing). Flex lets .nav-links claim whatever room is
   actually left between the other two and center itself in that, so it
   tracks the real gap at every width. */
.site-nav {
  position: relative;
  /* --hero-max, not --max: kept level with .hero-grid below it (see there),
     both wider than the 1200px every other section frame uses. */
  width: min(var(--hero-max), calc(100% - 48px));
  margin-inline: auto;
  height: 96px;
  display: flex;
  align-items: center;
}

.site-nav .brand {
  flex-shrink: 0;
}

/* flex: 1 claims the space left after .brand and .nav-actions take theirs;
   justify-content: center (existing rule below) then centers the links
   within that space rather than within the whole nav. min-width: 0 lets it
   actually shrink there instead of forcing the flex row wider. */
.nav-links {
  flex: 1 1 0%;
  min-width: 0;
}

/* Groups the CTA with the social pair so both sit as one unit at the nav's
   end. flex-shrink: 0 keeps the button from being squeezed once .nav-links
   starts claiming the space between it and the logo. */
.nav-actions {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: 16px;
}

/* Hidden below 1301px: at 190px edge columns there is only room for the CTA
   (see .site-nav) — the icons are the first thing to go under space pressure,
   not .nav-links, which is the part visitors actually navigate with. */
.nav-social {
  display: none;
  gap: 18px;
}

@media (min-width: 1301px) {
  .nav-social {
    display: flex;
  }
}

.nav-social-link {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  color: rgba(255, 255, 255, 0.3);
  transition: color var(--motion-fast) var(--ease-out);
}

.nav-social-link svg {
  width: 100%;
  height: 100%;
}

.nav-social-link:hover {
  color: var(--green);
}

/* Playground: fluid full-width nav, edge-anchored like trionn.com, used only
   by HeroSectionDraft.jsx to compare against the centered 1200px nav above —
   the live homepage's .site-nav is untouched. Flex instead of the 3-column
   grid so the logo | links | actions groups can space themselves out evenly
   at the wider measure. */
.site-nav--fullwidth {
  width: calc(100% - 96px);
  max-width: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Playground: shares the nav's own left/right margin above instead of the
   live homepage's centered max-width — matches a reference layout. Keeps the
   two-column copy + floating showcase split, just with a wider left column
   for the bigger heading. Not used on the live homepage. */
.hero-grid.hero-grid--fullwidth {
  width: calc(100% - 96px);
  max-width: none;
  /* BubbleMenu positions itself absolutely over the top of the hero instead
     of taking up flow height like the old nav did, so this reserves that
     96px with padding — the paragraph (pinned to the bottom of .hero-copy
     below) still tracks the bottom of the screen on any monitor height
     instead of sitting at a fixed 640px mark. */
  min-height: 100vh;
  padding-top: 96px;
  grid-template-columns: minmax(0, 960px) minmax(0, 1fr);
}

/* Playground: nudges the showcase cluster up 100px. Scoped to the fullwidth
   variant so the live homepage's cluster is untouched. transform is safe here
   (unlike on .hero-card) because parallax-floating's frame loop only ever
   writes to each card's own element, never to .hero-stage itself. */
.hero-grid--fullwidth .hero-stage {
  transform: translateY(-100px);
}

/* Stretched to the row's full height and split with space-between so the
   heading+button sit at the top and the paragraph lands near the bottom of
   the hero, matching the reference layout — instead of the live homepage's
   heading/paragraph/button stacked tight under each other. */
.hero-grid--fullwidth .hero-copy {
  max-width: 960px;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* .hero-shell clips overflow — without this the paragraph's own line-height
     pushes its last line past the viewport edge and off gets cut. */
  padding-bottom: 56px;
}

/* Platform logos pinned to the left of the bottom caption, at the caption's
   own line height rather than the paragraph's — two stacked text lines are
   taller than one icon row, so centering keeps the icons level with the
   caption block rather than its first line. */
.hero-caption {
  display: flex;
  align-items: center;
  gap: 16px;
}

.hero-caption-logos {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}

.hero-caption-logos img {
  width: 42px;
  height: 42px;
  filter: brightness(0) invert(1);
}

.hero-copy .hero-caption p {
  max-width: none;
  margin: 0;
}

/* Playground: light/dark switch next to the nav CTA in HeroSectionDraft.jsx —
   see ThemeToggle.jsx. Not used on the live homepage. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

.theme-toggle__track {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.18);
  border: 1.5px solid rgba(255, 255, 255, 0.3);
  transition: background var(--motion-fast) var(--ease-out);
}

.theme-toggle__thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: #fff;
  transition:
    transform var(--motion-fast) var(--ease-out),
    background var(--motion-fast) var(--ease-out);
}

.theme-toggle[aria-pressed="true"] .theme-toggle__track {
  background: var(--green);
  border-color: var(--green);
}

.theme-toggle[aria-pressed="true"] .theme-toggle__thumb {
  transform: translateX(20px);
  background: #102116;
}

/* Playground: light-theme state for the hero draft, toggled by
   ThemeToggle.jsx. Reuses the site's existing --paper/--ink tokens rather
   than inventing new colours. Only the text and switch swap — the background
   video keeps its own dark treatment, so the two sit side by side on purpose
   while the layout is being tested. */
.hero-shell.hero-shell--light {
  background: var(--paper);
  color: var(--ink);
}

.hero-shell.hero-shell--light .hero-copy h1 {
  color: var(--ink);
}

.hero-shell.hero-shell--light .theme-toggle__track {
  background: rgba(0, 0, 0, 0.1);
  border-color: rgba(0, 0, 0, 0.25);
}

.hero-shell.hero-shell--light .theme-toggle__thumb {
  background: #101215;
}

.hero-shell.hero-shell--light .theme-toggle[aria-pressed="true"] .theme-toggle__thumb {
  background: #123219;
}

/* Playground: React Bits SpecularButton, replaces the green btn-knob CTAs —
   used only by HeroSectionDraft.jsx. */
.specular-button {
  --sb-radius: 18px;
  --sb-tint: #ffffff;
  --sb-tint-opacity: 0;
  --sb-blur: 0px;
  --sb-text-color: #f5f5f5;

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  margin: 0;
  font-family: inherit;
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1;
  color: var(--sb-text-color);
  background: color-mix(in srgb, var(--sb-tint) calc(var(--sb-tint-opacity) * 100%), transparent);
  border-radius: var(--sb-radius);
  backdrop-filter: blur(var(--sb-blur));
  -webkit-backdrop-filter: blur(var(--sb-blur));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.04),
    0 8px 24px rgba(0, 0, 0, 0.25);
  cursor: pointer;
  outline: none;
  transition: transform 0.15s ease;
}

.specular-button:active {
  transform: scale(0.97);
}

.specular-button:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--sb-text-color) 60%, transparent);
  outline-offset: 3px;
}

.specular-button:disabled {
  opacity: 0.55;
  cursor: default;
}

.specular-button:disabled:active {
  transform: none;
}

.specular-button--sm {
  font-size: 0.85rem;
  padding: 10px 22px;
}

.specular-button--md {
  font-size: 1rem;
  padding: 14px 30px;
}

.specular-button--lg {
  font-size: 1.15rem;
  padding: 18px 40px;
}

.specular-button__fx {
  position: absolute;
  inset: -20px;
  pointer-events: none;
  z-index: 1;
}

.specular-button__fx canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.specular-button__label {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.specular-button__arrow {
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  color: var(--sb-text-color);
}

/* Playground: React Bits Beams WebGL background, filling the hero shell —
   used only by HeroSectionDraft.jsx. */
.hero-beams {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.beams-container {
  position: relative;
  width: 100%;
  height: 100%;
}

/* Playground: React Bits ASCIIText background, filling the gap between the
   caption paragraph (.hero-caption, bottom-left) and the circular badge below
   (bottom-right) — flush against the badge's left edge (its own 40px inset
   plus its 130px size) before being nudged 150px further left and scaled up
   20% then another 30%, used only by HeroSectionDraft.jsx. */
.hero-ascii-text {
  position: absolute;
  right: 320px;
  bottom: -30px;
  width: 406px;
  height: 500px;
  z-index: 1;
  pointer-events: none;
}

/* Playground: React Bits CircularText badge, top-right corner of the hero —
   used only by HeroSectionDraft.jsx. */
.hero-circular-text {
  position: absolute;
  bottom: 40px;
  right: 40px;
  z-index: 3;
  pointer-events: auto;
}

.circular-text {
  margin: 0 auto;
  border-radius: 50%;
  width: 130px;
  height: 130px;
  position: relative;
  font-weight: 900;
  color: #f7f7f4;
  text-align: center;
  cursor: pointer;
  transform-origin: 50% 50%;
  -webkit-transform-origin: 50% 50%;
}

.circular-text span {
  position: absolute;
  display: inline-block;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  font-size: 14px;
  letter-spacing: 0.02em;
  transition: all 0.5s cubic-bezier(0, 0, 0, 1);
}

.brand {
  display: inline-flex;
  width: 114px;
  overflow: hidden;
}

.brand-logo {
  display: block;
  width: 100%;
  height: auto;
}

.site-nav .brand {
  width: 160px;
}

.nav-links {
  display: flex;
  justify-content: center;
  gap: 54px;
  font-size: 16px;
  color: #b3b4b4;
}

.nav-links a {
  transition:
    color var(--motion-fast) var(--ease-out),
    transform var(--motion-fast) var(--ease-out);
}

.nav-links a:hover {
  color: var(--green);
  transform: translateY(-1px);
}

.nav-cta {
  border: 1.5px solid var(--green);
  color: var(--green);
}

.nav-cta .btn-knob {
  color: #102116;
  background: var(--green);
}

.nav-cta:hover {
  color: #102116;
  background: var(--green);
  box-shadow: 0 10px 28px rgba(99, 231, 125, 0.2);
}

.nav-cta:hover .btn-knob {
  color: var(--green);
  background: #102116;
}

.nav-burger {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 40px;
  height: 40px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
}

.nav-burger span {
  display: block;
  width: 100%;
  height: 2px;
  background: #fff;
}

.mobile-nav-close {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  background: transparent;
  color: #fff;
  cursor: pointer;
}

.mobile-nav {
  display: none;
}

body.nav-open {
  overflow: hidden;
}

/* Resting: ink on the card's own tint, because --green is a near-white green and
   would not hold an outline against it. Hovered: the green fill, where the
   colour finally has a dark label on top of it to carry the contrast. */
.card-cta {
  border: 1.5px solid #c8ccd5;
  color: #2c3e50;
}

/* Lighter than the 600 the shared button rule sets: the services cards carry a
   lot of CTAs at once, and at 600 the row of them competed with the titles. */
.services-panel .btn-label {
  font-weight: 500;
}

.card-cta .btn-knob {
  color: #2c3e50;
  background: #eaeff3;
}

.card-cta:hover,
.service-card:hover .card-cta {
  border-color: var(--green);
  color: #123219;
  background: var(--green);
  box-shadow: 0 14px 34px rgba(99, 231, 125, 0.2);
}

.card-cta:hover .btn-knob,
.service-card:hover .card-cta .btn-knob {
  color: var(--green);
  background: #123219;
}

.hero-grid {
  position: relative;
  top: 20px;
  min-height: 548px;
  /* --hero-max, level with .site-nav above it, not --max — the sections below
     stay at 1200px. The width is what buys both tracks their measure:
     splitting a narrower frame two ways left the heading 470px and wrapped it
     to six lines. margin-inline is set here rather than picked up from the
     shared section-frame rule, since this is no longer on that rule's
     selector list. */
  width: min(var(--hero-max), calc(100% - 48px));
  margin-inline: auto;
  display: grid;
  /* The copy takes the larger share, not the scene. Its measure is a hard floor
     — the longest h1 line is 561px at the 60px ceiling and the three manual line
     breaks are only the three rendered lines above that — while the scene has no
     floor at all and only reads smaller. At 1200px minus the gap there is no
     split that clears 561px for the copy and still leaves the scene the larger
     track, so the ratio goes the other way and both end up bigger than they were
     before the hero was widened.

     Neither track is a bare fraction, because a grid track's automatic minimum
     is its content: without a min the h1's longest unbreakable run pushes the
     left track past its share and squeezes the scene.

     The left min is a real floor rather than 0, and it is the measure the three
     lines need at the h1's *smallest* size: between 901px and about 943px of
     viewport the heading has bottomed out at its 45px minimum while the column
     is still shrinking, so it is the one band where the fraction alone leaves it
     short and it wraps to four. Below the floor the scene gives way instead —
     which is the right way round, since the heading has a measure and the scene
     only reads smaller. min(100%, …) so the floor cannot itself overflow. */
  grid-template-columns: minmax(min(100%, 448px), 1.1fr) minmax(0, 1fr);
  /* The columns used to touch: the leftmost card sat 4px from the copy's edge,
     which read as the scene crowding the heading even though nothing overlapped
     it at any width. */
  gap: clamp(24px, 4vw, 64px);
  align-items: center;
}

.hero-copy {
  position: relative;
  z-index: 2;
  padding-left: 8px;
  padding-bottom: 28px;
}

.hero-copy h1 {
  margin: 0;
  color: #f6f5f2;
  font-family: var(--font-display);
  font-size: clamp(45px, 4.2vw, 60px);
  font-weight: 600;
  line-height: 0.99;
  letter-spacing: -2.4px;
}

/* Playground: lighter/larger heading variant used only by HeroSectionDraft.jsx
   to compare against the live heading above — untouched here. */
.hero-copy h1.hero-heading--inter {
  font-family: "Inter", sans-serif;
  font-weight: 300;
  font-size: 90px;
}

/* Playground: pushes the heading and CTA further down in the fullwidth hero
   draft — live homepage's .hero-copy-top spacing is untouched. */
.hero-grid--fullwidth .hero-copy-top h1 {
  margin-top: 50px;
}

.hero-grid--fullwidth .hero-copy-top .hero-cta-wrap {
  margin-top: 30px;
}

/* Playground: strips the gradient-sweep "em" treatment so "ШІ-рішення" reads
   as plain heading text, same as "Розробка" — live homepage's em is untouched. */
.hero-copy h1.hero-heading--inter em {
  color: inherit;
  font-style: normal;
  font-weight: inherit;
  background: none;
  -webkit-background-clip: unset;
  background-clip: unset;
  -webkit-text-fill-color: inherit;
  animation: none;
}

.hero-copy h1 em,
.plumrocket-inner h2 em {
  color: var(--green);
  line-height: 1.1;
}

/* Only the hero's accent sits a notch under its heading — the h1 runs 600 and
   the lighter em is what separates the two halves of the line. The partner h2
   reads at a single weight like every other h2 on the page, so its em is left
   to inherit and the green alone marks the name out. */
.hero-copy h1 em {
  font-weight: 500;
}

.services-heading h2 em,
.audience-heading h2 em {
  color: inherit;
  font-style: normal;
  font-weight: inherit;
  line-height: 1.1;
}

/* Registered so the sweep's start angle can be interpolated at all — a plain
   custom property would step between keyframes instead of tweening, and there is
   no other way to turn a conic gradient: rotating the element would take the
   glyphs with it. */
@property --em-sweep-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

/* One full turn. The wheel's last stop is its first, so the loop closes with no
   seam and linear timing keeps it at a constant rate. */
@keyframes hero-em-sweep {
  to {
    --em-sweep-angle: 360deg;
  }
}

/* Gated on @supports because the effect needs transparent text to show through:
   where background-clip: text is not honoured the word would come out invisible,
   so browsers without it never leave the flat green above. */
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .hero-copy h1 em {
    /* Under the sweep in case a sub-pixel edge ever falls outside it. */
    background-color: var(--em-pink);
    background-image: conic-gradient(
      from var(--em-sweep-angle) at var(--em-sweep-x) var(--em-sweep-y),
      var(--em-pink) var(--em-stop-1),
      var(--em-sky) var(--em-stop-2),
      var(--em-mint) var(--em-stop-3),
      var(--em-pink) var(--em-stop-4)
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    animation: hero-em-sweep var(--em-sweep-period) linear infinite;
  }
}

/* Wins over the block above on source order. The gradient stays — it is the
   source's own static state, and dropping it would change the word's colour
   rather than only its motion. Just the wheel stops turning. */
@media (prefers-reduced-motion: reduce) {
  .hero-copy h1 em {
    animation: none;
  }
}

/* Sized off the heading's own font rather than a fixed pixel box, so it keeps
   its proportion to the word through the h1's clamp. The negative margin is what
   tucks it against the end of the word — it contributes no width of its own, so
   nothing either side is pushed around by it. */
/* Zero height on purpose. At 90px the star is half again the heading's line box,
   and an inline box that tall stretches the line it sits on — the h1 grew by a
   whole half-line the moment the size went flat. Keeping the box flat and
   hanging the artwork off it in a pseudo-element lets the star overflow the line
   instead of pushing it open, so the heading keeps its three even rows.

   What is left in the line is a zero-height box whose alignment point the
   artwork is centred on: vertical-align moves that point, and the star follows. */
.hero-star {
  position: relative;
  display: inline-block;
  width: var(--star-size);
  height: 0;
  margin-inline: var(--star-tuck);
  vertical-align: 20px;
  user-select: none;
  pointer-events: none;
}

.hero-star::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(var(--star-size) / -2);
  width: var(--star-size);
  height: var(--star-size);
  background-image: url("../img/hero-star.png");
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  /* Three radii rather than one: the tight pass reads as the star's own edge
     light, the wide one as the haze it throws, and the middle one keeps the two
     from separating into a ring. */
  filter: drop-shadow(0 0 2px color-mix(in srgb, var(--star-glow) 85%, transparent))
    drop-shadow(0 0 9px color-mix(in srgb, var(--star-glow) 55%, transparent))
    drop-shadow(0 0 24px color-mix(in srgb, var(--star-glow) 28%, transparent));
  transform: rotate(var(--star-rest-angle));
  transform-origin: 50% 50%;
}

/* A flat optical nudge, deliberately in pixels rather than em: it is closing a
   gap the star's tuck leaves at one size, not a proportion of the type. */
.hero-nudge-left {
  margin-inline-start: -10px;
}

/* Break points that only exist for the mobile reflow below — hidden here so
   desktop keeps its original three-line wrap untouched. */
.hero-break-show-mobile {
  display: none;
}

.hero-copy p {
  max-width: 455px;
  margin: 38px 0 0;
  color: #c5c6c5;
  font-size: 18px;
  line-height: 1.7;
}

.floating-root {
  position: absolute;
  inset: 0;
}

.floating-element {
  position: absolute;
  will-change: transform;
}

/* The zone the cards live in, not a box drawn around them: every card is placed
   as a percentage of this rectangle, so this is what bounds the arrangement and
   keeps it out of the copy's column. Overrides the absolute inset .floating-root
   sets, so it flows in the hero grid and takes its width from the track it sits
   in.

   Holding one proportion is what makes the arrangement resolution-independent:
   widths are shares of this rectangle's width and tops shares of its height, so
   both have to move together or the cluster changes shape between viewports.
   That also retires the --card-scale knob that stood here while the height was
   pinned — the cards now scale with the container and need no counter-weight. */
.hero-stage {
  position: relative;
  inset: auto;
  width: 100%;
  aspect-ratio: 523 / 403;
  z-index: 3;
  /* Not clipped: the glow has to cross the scene's edges. */
  overflow: visible;
  pointer-events: none;
}

/* Layout — left, top, width, aspect-ratio and z-index — arrives inline from the
   showcase array, so the number of cards and their arrangement live in JS only
   and no card needs a class of its own. Stacking is the array's z alone; the DOM
   order is top-to-bottom reading order and says nothing about layers.

   Nothing here may set transform: the frame loop in parallax-floating owns it. */
.hero-card {
  border-radius: 10px;
  /* On the wrapper rather than on .hero-card-frame, which clips its overflow: a
     glow drawn inside the clip would be sheared off exactly at the seams where
     cards overlap, which is the one place it has to be visible. */
  box-shadow:
    0 22px 52px rgba(4, 18, 9, 0.38),
    0 0 64px rgba(99, 231, 125, 0.1);
  /* Entry, breathing and parallax are composed together onto this wrapper by the
     animation frame in parallax-floating — including its opacity, which is why
     the resting 0.95 sits on the frame below and not here. */
  pointer-events: auto;
}

/* The frame owns the geometry; the image inside just fills it (see .hero-shot),
   so a replacement crop with different proportions cannot shift the layout. */
.hero-card-frame {
  width: 100%;
  height: 100%;
  overflow: hidden;
  /* Follows the wrapper, so the two radii can never drift apart. */
  border-radius: inherit;
  background: rgba(246, 246, 242, 0.88);
  /* Resting look. Kept off the wrapper so it never multiplies into the entry
     opacity the frame loop writes there. */
  opacity: 0.95;
}

.hero-card .hero-shot {
  width: 100%;
  height: 100%;
  transition:
    transform var(--motion-hover) var(--ease-out),
    filter var(--motion-hover) var(--ease-out);
}

/* Suppressed on a still scene (see the `still` prop in parallax-floating): the
   cards are meant to sit there without answering the pointer, and this zoom is
   the last thing that still did. Scoped rather than deleted so the flag remains
   the only switch — turning motion back on restores the hover with it. */
.floating-root:not([data-still]) .hero-card:hover .hero-shot {
  transform: scale(1.025);
  filter: saturate(1.04) contrast(1.02);
}

/* No transform is written on a still scene, so the compositor hint on every
   card is a promise nothing keeps — same reasoning as the reduced-motion block
   further down. */
.floating-root[data-still] .floating-element {
  will-change: auto;
}

/* Shared pill: the label sits left with the knob parked at the right edge, and
   on hover the two swap sides. Both paddings always total 80px, so the button
   never changes width and nothing around it reflows. */
.primary-button,
.nav-cta,
.card-cta,
.send-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: 48px;
  overflow: hidden;
  border-radius: 999px;
  padding: 0 56px 0 24px;
  font-size: 16px;
  font-weight: 600;
  white-space: nowrap;
  transition:
    padding var(--motion-swap) var(--ease-out),
    box-shadow var(--motion-hover) var(--ease-out),
    filter var(--motion-fast) var(--ease-out),
    color var(--motion-fast) var(--ease-out),
    background var(--motion-fast) var(--ease-out),
    transform var(--motion-fast) var(--ease-out);
}

/* Pointer-only: on touch, :hover sticks after the tap and would leave the knob
   stranded on the left. */
@media (hover: hover) {
  .primary-button:hover,
  .nav-cta:hover,
  .card-cta:hover,
  .send-button:hover:not(:disabled) {
    padding: 0 24px 0 56px;
  }

  .primary-button:hover .btn-knob,
  .nav-cta:hover .btn-knob,
  .card-cta:hover .btn-knob,
  .send-button:hover:not(:disabled) .btn-knob {
    /* 100% minus the knob's width plus its inset — flush on the left. */
    right: calc(100% - 44px);
    transform: rotate(45deg);
  }
}

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

.btn-knob {
  position: absolute;
  right: 4px;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  transition:
    right var(--motion-swap) var(--ease-out),
    transform var(--motion-swap) var(--ease-out),
    color var(--motion-fast) var(--ease-out),
    background var(--motion-fast) var(--ease-out);
}

.primary-button {
  margin-top: 43px;
  color: #123219;
  background: var(--green);
}

.primary-button .btn-knob {
  color: var(--green);
  background: #123219;
}

.primary-button:hover {
  filter: brightness(1.04);
  box-shadow: 0 14px 34px rgba(99, 231, 125, 0.2);
}

.primary-button:active,
.nav-cta:active,
.card-cta:active,
.send-button:active {
  transform: translateY(1px) scale(0.97);
  transition-duration: var(--motion-press);
  transition-timing-function: var(--ease-in);
}

html.motion-ready .primary-button[data-reveal].is-visible:active {
  transform: translateY(1px) scale(0.97);
}

.trusted {
  display: grid;
  justify-items: center;
  padding: 41px 0 38px;
  position: relative;
  top: 20px;
}

.trusted > p {
  margin: 0 0 14px;
  color: #777a79;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 1.8px;
}

.logo-marquee {
  width: min(100%, var(--max));
  overflow: hidden;
  cursor: grab;
  touch-action: pan-y;
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.logo-marquee.is-dragging {
  cursor: grabbing;
}

.logo-track {
  display: flex;
  width: max-content;
  gap: 52px;
  will-change: transform;
}

/* One group of the marquee. Both copies are content-sized and identical, which
   is what keeps the -50% keyframe seamless. */
.logo-wall {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 52px;
  height: 71px;
}

/* Capped on both axes rather than by height alone: the wordmarks range from
   very wide and thin to almost square, and a single fixed height would blow the
   thin ones out of scale. */
.logo-wall img {
  width: auto;
  height: auto;
  max-width: 104px;
  max-height: 34px;
  opacity: 0.55;
}

.services-section {
  --services-tail: 105px;
  --services-panel-gutter: 80px;
  --services-content-gutter: 160px;
  position: relative;
  z-index: 1;
  padding: 70px 0 105px;
  view-timeline-name: --services-expand;
  view-timeline-axis: block;
  background:
    linear-gradient(180deg, rgba(0, 0, 0, 0.88) 0, transparent 95px),
    linear-gradient(
      180deg,
      #43864d 0,
      #3d7a46 calc(100% - var(--services-tail)),
      #0b0c0d 100%
    );
}

.services-panel {
  position: relative;
  isolation: isolate;
  width: 100%;
  margin-inline: auto;
  /* Bottom is 84px + 50px: the paper panel is sized by this padding — its
     background is drawn by ::before at inset 0 — so the extra room under the
     card grid has to come from here rather than from a margin on the grid,
     which would fall outside the panel and land on the green instead. */
  padding: 58px 0 134px;
}

.services-panel::before {
  content: "";
  position: absolute;
  inset: 0 auto;
  left: 50%;
  z-index: -1;
  width: calc(100% - var(--services-panel-gutter));
  border-radius: var(--radius-lg);
  background: var(--paper);
  box-shadow: 0 20px 60px rgba(18, 44, 23, 0.22);
  transform: translateX(-50%);
}

.services-inner {
  width: min(var(--max), calc(100% - var(--services-content-gutter)));
  margin-inline: auto;
}

@keyframes services-panel-expand {
  from {
    width: min(888px, calc(100% - var(--services-panel-gutter)));
  }

  to {
    width: calc(100% - var(--services-panel-gutter));
  }
}

@keyframes services-content-expand {
  from {
    width: min(728px, calc(100% - var(--services-content-gutter)));
  }

  to {
    width: min(var(--max), calc(100% - var(--services-content-gutter)));
  }
}

@supports (animation-timeline: view()) {
  .services-panel::before {
    animation: services-panel-expand linear both;
    animation-timeline: --services-expand;
    animation-range: entry 8% cover 32%;
  }

  .services-inner {
    animation: services-content-expand linear both;
    animation-timeline: --services-expand;
    animation-range: entry 8% cover 32%;
  }
}

.services-heading {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
}

.services-heading h2,
.audience-heading h2,
.plumrocket-inner h2,
.faq-panel h2 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -1.1px;
}

.services-heading h2 {
  font-size: 40px;
  font-weight: 400;
  line-height: 1.02;
  white-space: nowrap;
}

.services-heading p {
  margin: 0;
  font-size: 18px;
  font-weight: 400;
  line-height: 1.35;
  white-space: nowrap;
}

/* Three columns: the fourth card fills row two's first slot, and the fifth —
   the wide one — spans the remaining two columns beside it. */
.services-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  margin-top: 64px;
}

.service-card {
  position: relative;
  display: flex;
  min-height: 320px;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  overflow: hidden;
  border-radius: 28px;
  padding: 32px;
  /* Keeps the tint overlay's negative z-index scoped to this card — without a
     stacking context of its own, a negative z-index child escapes to behind
     the nearest ancestor that has one, which here would be the grid or the
     panel behind it. */
  isolation: isolate;
  transition: box-shadow var(--motion-hover) var(--ease-out);
}

/* Sits behind the icon/title/copy/cta (all in-flow, so they paint above a
   negative-z-index sibling) and above the card's own gradient background.
   GSAP drives its opacity on hover — see ServiceCardHover.jsx — because the
   crossfade is between two separate layers (gradient background, flat tint),
   not a single property a CSS transition could animate between. */
.service-tint {
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0;
  pointer-events: none;
  background: var(--tint-color, transparent);
}

.service-icon {
  display: block;
  width: 44px;
  height: 44px;
  border-radius: 14px;
}

/* The glyphs fill their 24-unit viewBox edge to edge with no spare margin,
   so the hover nudges below would clip against the box otherwise. */
.service-icon svg {
  overflow: visible;
}

/* Micro-animation on hover, one move per icon, all plain CSS transforms —
   no JS needed since the shift amounts are copied straight from the source
   (see the WebIcon comment in page.jsx). Every sub-path transitions back on
   its own via this base rule when the card stops being hovered. */
.service-icon path {
  transition:
    transform var(--motion-hover) var(--ease-out),
    opacity var(--motion-hover) var(--ease-out),
    color var(--motion-hover) var(--ease-out);
}

/* code-xml: brackets spread apart, slash leans into it and turns green — the
   one moving part of the mark stays black at rest, like every icon here. */
.service-card:hover .icon-bracket-right {
  transform: translateX(3px);
}

.service-card:hover .icon-bracket-left {
  transform: translateX(-3px);
}

.service-card:hover .icon-slash {
  transform: scale(1.1);
  opacity: 0.7;
  color: var(--green);
}

/* gauge: needle swings toward the high end and turns green. */
.service-card:hover .icon-needle {
  transform: rotate(25deg);
  color: var(--green);
}

/* shield-check: both parts read as one solid mark at rest — the check stays
   fully drawn instead of hiding until hover, it just spins a beat after the
   shield and turns green. */
.service-card:hover .icon-shield-body {
  transform: scale(1.06);
}

.service-card:hover .icon-shield-check {
  transform: rotate(360deg);
  color: var(--green);
  transition-delay: 60ms;
}

/* sparkles: main mark flips in place (stays black), the two small ones spin
   off in opposite directions and turn green. */
.service-card:hover .icon-sparkle-main {
  transform: rotate(180deg) scale(1.1);
}

.service-card:hover .icon-sparkle-top {
  transform: rotate(-90deg);
  color: var(--green);
}

.service-card:hover .icon-sparkle-bottom {
  transform: rotate(90deg);
  color: var(--green);
}

/* plug-connected: the two halves pull apart, the sparks between them fade —
   reads as the connection breaking on hover. Only the upper half turns
   green, so the break reads as one side losing power, not both. */
.service-card:hover .icon-plug-upper {
  transform: translate(-2px, 2px);
  color: var(--green);
}

.service-card:hover .icon-plug-lower {
  transform: translate(2px, -2px);
}

.service-card:hover .icon-plug-leg {
  opacity: 0;
}

.service-card:hover {
  box-shadow: 0 18px 42px rgba(29, 54, 37, 0.14);
}

/* The reveal rule sets `transition` wholesale and outranks .service-card, so the
   shadow above had nothing to ease on and snapped in. Restated here with the
   reveal's own two properties kept intact. */
html.motion-ready .service-card[data-reveal] {
  transition:
    opacity var(--motion-reveal) var(--ease-out),
    transform var(--motion-reveal) var(--ease-out),
    box-shadow var(--motion-hover) var(--ease-out);
}

/* Each card washes from its own pastel tint at the top-left corner into white,
   one colour per card — no ring, no separate wash layer, the tint just is the
   card's background. */
.service-card.sky {
  --tint-color: #e3f0ff;
  background: radial-gradient(135% 150% at 8% -12%, #dbecff 0%, #ffffff 62%);
}

.service-card.amber {
  --tint-color: #fff7e8;
  background: radial-gradient(135% 150% at 8% -12%, #fff5e2 0%, #ffffff 62%);
}

.service-card.mint {
  --tint-color: #e3ffe3;
  background: radial-gradient(135% 150% at 8% -12%, #dbffdb 0%, #ffffff 62%);
}

.service-card.lilac {
  --tint-color: #f0eaff;
  background: radial-gradient(135% 150% at 8% -12%, #ece4ff 0%, #ffffff 62%);
}

.service-card.rose {
  --tint-color: #fad9ff;
  background: radial-gradient(135% 150% at 8% -12%, #f9cfff 0%, #ffffff 62%);
}

/* The 6th tile — a CTA/ad card, not a service, so it breaks from the pastel
   set on purpose to stand out as a promo among the five. Full supplied
   artwork (mockups included, per the reference), covering the tile and
   anchored top so the same crop line shows regardless of card height. */
.service-card.cta {
  background: url("../img/services/cta-bg.png") center top / cover no-repeat;
}

/* Pulls the whole text+button group down as one unit — the button's own
   margin-top: auto (see .service-card > .card-cta) would otherwise leave
   the heading flush at the top with all the slack pushed below it. Moving
   the auto margin to the heading instead keeps the button hard against the
   copy and leaves the artwork clear up top, per the reference. */
.service-card.cta h3 {
  margin-top: auto;
  color: #f8f8f6;
  font-size: 28px;
  font-weight: 500;
}

.service-card.cta .card-cta {
  margin-top: 0;
}

.service-card.cta h3 em {
  color: var(--green);
  font-style: normal;
}

.service-card.cta > p {
  color: rgba(248, 248, 246, 0.72);
}

/* Always shown in the green "hover" look — this card is the ad among the
   pastel ones, so its CTA reads as already-pressed rather than waiting for
   a hover to stand out. Desktop only (min-width matches the 900px breakpoint
   below): on mobile every other card's CTA drops the pill for a plain green
   text link, and this card follows suit there instead of fighting that
   simplification with a higher-specificity override. */
@media (min-width: 901px) {
  .service-card.cta .card-cta {
    border-color: var(--green);
    color: #123219;
    background: var(--green);
  }

  .service-card.cta .card-cta .btn-knob {
    color: var(--green);
    background: #123219;
  }

  .service-card.cta .card-cta:hover,
  .service-card.cta:hover .card-cta {
    filter: brightness(1.04);
    box-shadow: 0 14px 34px rgba(99, 231, 125, 0.2);
  }
}

.service-card h3 {
  margin: 0;
  color: #111827;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.5px;
  line-height: 1.25;
}

.service-card > p {
  margin: 0;
  color: #4b5563;
  font-size: 16px;
  line-height: 1.55;
}

/* Pushed to the bottom edge of the card so every button in a row lines up no
   matter how long the copy above it runs. Geometry, the knob and the swap all
   come from the shared pill above — only the placement is the card's own. */
.service-card > .card-cta {
  margin-top: auto;
}

.why-section {
  position: relative;
  z-index: 0;
  display: grid;
  grid-template-rows: auto 1fr;
  view-timeline-name: --why-section;
  view-timeline-axis: block;
  min-height: 905px;
  overflow: hidden;
  color: #f5f6f4;
  background:
    linear-gradient(
      180deg,
      rgba(11, 12, 13, 0.68) 0,
      rgba(11, 12, 13, 0.24) 235px,
      rgba(11, 12, 13, 0.18) 100%
    ),
    url("../img/bg_black.png") center top / cover no-repeat,
    #0b0c0d;
}

.why-inner {
  position: relative;
  z-index: 1;
  padding: 5px 0 0;
}

.section-kicker {
  margin: 0 0 44px;
  font-size: 16px;
  font-weight: 700;
}

.why-section blockquote {
  max-width: 780px;
  margin: 0;
  font-family: var(--font-display);
  font-size: 30px;
  font-style: italic;
  font-weight: 300;
  line-height: 1.42;
}

.advantages {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 40px;
  margin-top: 74px;
}

/* No ring any more — the glyph itself is the 40px mark. */
.advantage-mark {
  display: block;
  width: 40px;
  height: 40px;
  color: var(--green);
}

.advantages h3 {
  /* The 0.9 line-height crops the heading's own leading, so the gap to the copy
     has to be carried by the margin rather than inherited from the text box. */
  margin: 18px 0 22px;
  color: var(--green);
  font-family: var(--font-display);
  font-size: 38px;
  font-weight: 300;
  line-height: 0.9;
}

.advantages h3 span {
  display: block;
}

/* 15px rather than 16px is what squares all four copies at three lines each: at
   16px the first one ("З 2010 року…") runs to four, and it is the longest of the
   set, so it sets the ceiling. Below about 15px the three shorter ones collapse
   to two lines instead — the window where all four agree is roughly one pixel
   wide, which is why this is a flat value and not something rounder. */
.advantages p {
  margin: 0;
  color: #a7aaa8;
  font-size: 15px;
  line-height: 1.35;
}

.wordmark {
  position: relative;
  align-self: center;
  min-width: 0;
  overflow: hidden;
  color: rgba(255, 255, 255, 0.1);
  font-family: var(--font-display);
  font-size: clamp(240px, 17.4vw, 332px);
  font-style: italic;
  line-height: 0.82;
  white-space: nowrap;
  pointer-events: none;
}

.wordmark-track {
  display: flex;
  width: max-content;
  transform: translate3d(-8vw, 0, 0);
  will-change: transform;
}

@keyframes wordmark-scroll {
  from {
    transform: translate3d(16vw, 0, 0);
  }

  to {
    transform: translate3d(-70vw, 0, 0);
  }
}

@supports (animation-timeline: view()) {
  .wordmark-track {
    animation: wordmark-scroll linear both;
    animation-timeline: --why-section;
    animation-range: entry 5% exit 95%;
  }
}

.audience-reveal {
  min-height: 100dvh;
  padding: 98px 0 205px;
  border-radius: 58px 58px 0 0;
  background: var(--paper);
}

/* Stacked sections: the audience block pins while the Plumrocket section
   slides over it. Only engage where the section fits the viewport in full —
   once pinned it never scrolls again, so a taller section would clip. Gated
   to desktop widths only: on phones a tall CSS viewport (many run past
   880px in portrait) would pin the block anyway, freezing it mid-list while
   the next section immediately covers it — there's no room to pin against
   on a single-column scroll, so it just reads as stuck. */
@media (min-width: 769px) and (min-height: 880px) {
  .audience-reveal {
    position: sticky;
    top: 0;
    z-index: 0;
  }
}

.audience-sticky {
  display: contents;
}

/* Grid lives here rather than on .audience-heading so the CTA can sit in
   the header's row on desktop (grid-template-areas below) while still
   being free to drop to the very end of the stack on mobile — a plain
   child of .audience-heading it couldn't reorder independently of the
   copy it used to share a row with. */
.audience-inner {
  max-width: var(--max);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "heading cta" "list list";
  column-gap: 48px;
  row-gap: 70px;
  align-items: start;
}

.audience-heading {
  grid-area: heading;
}

.audience-heading-copy {
  display: grid;
  gap: 14px;
}

.audience-heading h2 {
  margin: 0;
  font-weight: 400;
  font-size: 40px;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: #111111;
}

.audience-heading p {
  margin: 0;
  font-size: 18px;
  line-height: 1.4;
  color: #2b2b2b;
}

/* Colors come from the shared .primary-button rule (this element carries
   both classes) — previously overridden here with a slightly different
   green/knob pair, which made this the one button on the site off-brand. */
.audience-cta {
  grid-area: cta;
  justify-self: end;
  margin-top: 0;
  font-size: 16px;
  font-weight: 700;
}

.audience-cta:focus-visible {
  outline-color: #123219;
}

.audience-list {
  grid-area: list;
}

/* ==============================================================
   BODY — tab list / preview panel, ported from
   audience-tabs-section.html (curtain-wipe mechanics borrowed from
   services-hover-demo.html). Runs on the site's own font tokens;
   only the #4ade80 accent color stays its own thing.
   ============================================================== */
.audience-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 400px);
  gap: 64px;
}

/* Stretched to the grid row's full height (set by .audience-preview's own
   aspect-ratio) and its rows spread out with space-between so the list
   spans edge-to-edge with the image instead of stopping short at its own
   content height. */
.audience-tabs {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-width: 0;
}

.audience-row {
  position: relative;
  display: block;
  width: 100%;
  border: 0;
  border-bottom: 1px solid #e2e2e2;
  background: none;
  text-align: left;
  cursor: pointer;
  padding: 0 0 26px;
  font: inherit;
  color: inherit;
}

.audience-row:focus-visible {
  outline: none;
}

/* Accent divider — grows left-to-right on activation (.line-in, anchored
   left) and erases left-to-right on deactivation: .line-out flips the
   anchor to `right` first, so the same width transition running back to 0
   erases from the left edge instead of just shrinking back from the right.
   Both classes are only ever set from setActive() in AudienceExperience.jsx,
   never from a row's own mouseleave/blur — see that file for why. */
.audience-row::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: 0;
  background: var(--row-accent, #4ade80);
  transition: width 0.5s ease;
}

.audience-row.line-out::after {
  left: auto;
  right: 0;
}

.audience-row.line-in::after,
.audience-row.is-active::after {
  width: 100%;
}

.audience-row-title {
  display: flex;
  align-items: center;
}

/* Flush left at rest — no reserved gutter. On hover/focus/active the dot
   grows in (width + margin animate from 0) and pushes the heading right to
   make room for it, instead of fading inside space already reserved. */
.audience-row-bullet {
  width: 0;
  height: 9px;
  margin-right: 0;
  border-radius: 999px;
  background: var(--row-accent, #4ade80);
  flex: 0 0 auto;
  overflow: hidden;
  opacity: 0;
  transition:
    width 0.3s ease,
    margin-right 0.3s ease,
    opacity 0.2s ease;
}

.audience-row:hover .audience-row-bullet,
.audience-row:focus-visible .audience-row-bullet,
.audience-row.is-active .audience-row-bullet {
  width: 9px;
  margin-right: 14px;
  opacity: 1;
}

.audience-row h3 {
  margin: 0;
  font-size: 22px;
  line-height: 1.15;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: #111827;
}

.audience-row-tags {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 16px;
  color: #3a3a3a;
  margin-top: 12px;
}

/* Only read on the mobile slider caption (below) — the desktop list already
   has the tags line and doesn't need the fuller copy too. */
.audience-row-copy {
  display: none;
}

.audience-preview {
  position: relative;
  overflow: hidden;
  background: #ececec;
  aspect-ratio: 400 / 512;
  width: 100%;
}

/* Curtain-wipe reveal — ported from services-hover-demo.html. Stacked
   layers: the incoming one clip-paths open bottom-to-top while sitting
   above the outgoing one, which gets no animation of its own, just a
   lower z-index so it's simply covered. */
.audience-preview-img {
  position: absolute;
  inset: 0;
  overflow: hidden;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

.audience-preview-img img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: var(--fit, contain);
  padding: var(--fit-pad, 0);
  transform: scale(1);
  transition: transform 1.5s ease;
}

.audience-preview-img.is-entering {
  z-index: 15;
  animation: audienceCurtainIn 0.5s ease forwards;
}

.audience-preview-img.is-leaving {
  z-index: 14;
}

@keyframes audienceCurtainIn {
  0% { clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0 100%); }
  100% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }
}

/* Prev/next pair for the mobile slider — hidden entirely on desktop, where
   the tab list drives selection instead. See the (max-width: 680px) block
   for the layout that turns .audience-body into that slider. */
.audience-mobile-nav {
  display: none;
}

.audience-nav-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 999px;
  border: 1px solid #d8d8d8;
  background: none;
  color: #141414;
  cursor: pointer;
  transition:
    background-color var(--motion-fast, 0.2s) ease,
    border-color var(--motion-fast, 0.2s) ease,
    color var(--motion-fast, 0.2s) ease;
}

.audience-nav-btn:hover,
.audience-nav-btn:focus-visible {
  background: #141414;
  border-color: #141414;
  color: #ffffff;
}

.audience-nav-btn:focus-visible {
  outline: 2px solid #141414;
  outline-offset: 2px;
}

.audience-nav-next svg {
  transform: scaleX(-1);
}

/* Holds the green section and the FAQ on one surface, so the scroll-driven
   colour swap carries from one into the other. Top rung of the stack: it covers
   the pinned audience section (z 0), the pinned form panel (z 1) and the map
   band (z 2), and it is the layer that peels away over the form. */
.partner-surface {
  position: relative;
  z-index: 3;
  border-radius: 0 0 58px 58px;
  background-color: #0d3b2c;
}

.plumrocket-section {
  position: relative;
  padding: 140px 24px 0;
  color: #eef6f0;
}

/* Toggled on in one frame by the scroll trigger. The surface and every piece
   of type invert together — the swap fires while the stats and CTA are still
   in view, so light text on a white surface must never survive a frame. */
.partner-surface.is-light {
  background-color: #ffffff;
}

.partner-surface.is-light .plumrocket-section {
  color: #0b0c0d;
}

.partner-surface.is-light .partner-copy {
  color: #3f4744;
}

.partner-surface.is-light .stats > div > span {
  color: #4e555a;
}

.partner-surface.is-light .stats div {
  border-left-color: rgba(11, 12, 13, 0.15);
}

/* Brand green stays as the accent but drops to the darker tone — the light
   one is unreadable on white. */
.partner-surface.is-light .plumrocket-inner h2 em,
.partner-surface.is-light .stats div:hover strong {
  color: var(--green-dark);
}

/* Pure spacing between the CTA and the FAQ, plus the anchor the colour swap
   triggers off. Its height no longer affects the swap — that fires off its
   top edge — so it is a flat value rather than viewport-relative. */
.partner-tail {
  height: 72px;
}

.plumrocket-inner {
  display: grid;
  /* minmax(0, 1fr) rather than the implicit auto column: .stats deliberately
     specifies a width wider than this container so it can break out and
     overflow symmetrically. An auto column would size itself to that width
     instead of clipping to it, dragging every other child (heading, CTA)
     off-centre along with it. */
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  max-width: 900px;
  margin: 0 auto;
}

.plumrocket-top {
  width: min(var(--max), calc(100vw - 48px));
  display: grid;
  grid-template-columns: minmax(280px, 400px) minmax(0, 1fr);
  align-items: center;
  gap: 64px;
}

.plumrocket-copy {
  text-align: left;
}

/* Nudges the heading + description up without touching the CTA below —
   a static transform rather than a margin so the SplitText lines
   PlumrocketMotion animates (its own transform, on the lines, not this
   outer box) and the button's own document-flow position stay untouched. */
.plumrocket-copy h2,
.plumrocket-copy .partner-copy {
  transform: translateY(-30px);
}

.plumrocket-inner h2 {
  font-size: 40px;
  font-weight: 400;
  text-align: left;
}

.partner-collage {
  position: relative;
  width: 100%;
  aspect-ratio: 700 / 339;
  margin: 0;
}

.collage-glow {
  position: absolute;
  left: 50%;
  top: calc(48% + 20px);
  z-index: 0;
  width: 108%;
  height: 130%;
  transform: translate(-50%, -50%);
  background: radial-gradient(
    circle,
    rgba(91, 178, 151, 0.85),
    rgba(91, 178, 151, 0) 68%
  );
  filter: blur(40px);
}

.collage-card {
  position: absolute;
}

.collage-accent-tl {
  left: 10%;
  top: 0;
  z-index: 1;
  width: 21.5%;
  height: 33%;
  border-radius: 4px;
  overflow: hidden;
  opacity: 0.48;
  box-shadow: 0 12px 26px rgba(6, 16, 11, 0.2);
}

.collage-accent-br {
  left: 79%;
  top: 59%;
  z-index: 1;
  width: 21%;
  height: 31%;
  border-radius: 12px;
  overflow: hidden;
  opacity: 0.48;
  box-shadow: -16px 12px 22px rgba(0, 0, 0, 0.16);
}

.collage-accent-tr {
  left: 68%;
  top: 2%;
  z-index: 3;
  width: 27.5%;
  height: 41%;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: -22px 17px 32px rgba(0, 0, 0, 0.25);
}

.collage-accent-tl img,
.collage-accent-br img,
.collage-accent-tr img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.collage-accent-bl {
  left: 0;
  top: 50%;
  z-index: 4;
  width: 25.5%;
  height: 40%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pr-logo-anim {
  width: 74.4%;
  aspect-ratio: 36 / 29;
  transform: translateX(50px);
}

.pr-logo-anim svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.collage-browser {
  left: 20.5%;
  top: 12.5%;
  z-index: 2;
  width: 67.5%;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 22px 48px rgba(4, 18, 9, 0.35);
}

.collage-browser img {
  display: block;
  width: 100%;
  height: auto;
}

/* Escapes .plumrocket-inner's 900px column: the big numerals need more room
   than the heading/paragraph above them, so this row alone breaks out to the
   section's full content measure. Centred the same way the 900px column is
   (both sit on the same section, symmetric padding), so no vw-offset math is
   needed — just a wider explicit width for the grid to overflow into. */
.stats {
  width: min(var(--max), calc(100vw - 48px));
  display: grid;
  /* Content-proportional, not an even 25/25/25/25 split: column two carries
     the longest label ("Використовують розширення Plumrocket") and needs the
     most room, column one just "18+" and needs the least. fr keeps the row
     filling the container exactly regardless of the ratio. */
  grid-template-columns: 21fr 27fr 26fr 26fr;
  gap: 0;
  margin-top: 53px;
  text-align: left;
}

.stats div {
  display: grid;
  align-content: start;
  gap: 14px;
  /* Same left inset in every column, divider or not — column one used to skip
     it and sit flush against the card edge while the rest were inset from
     their divider, which read as misaligned. */
  padding: 0 32px;
  border-left: 1px solid rgba(255, 255, 255, 0.2);
}

.stats div:first-child {
  border-left: none;
}

.stats div:last-child {
  padding-right: 0;
}

/* The one place on the site that is not on the body face: the counters' digits
   run Bodoni Moda. It sits on the <strong> rather than on .stat-value-live,
   because the hidden .stat-value-sizer inside it is what reserves the cell's
   width — split the face between the two spans and the live value would stop
   matching the space held for it. The labels beside them stay Inter. */
.stats strong {
  font-family: "Bodoni Moda", Georgia, serif;
  font-size: clamp(48px, 5vw, 88px);
  font-weight: 500;
  line-height: 1;
  /* Deliberately not transitioning colour: these flip with the surface, and
     an eased colour here would lag behind the hard cut. */
  transition: transform var(--motion-hover) var(--ease-out);
}

.stats div:hover strong {
  color: var(--green);
  transform: scale(1.035);
}

/* The counter stacks a hidden copy of the final value under the ticking one,
   so the cell is already at its widest and the digits never shuffle sideways
   while counting. */
.stat-value {
  display: inline-grid;
  justify-self: start;
  font-variant-numeric: tabular-nums;
}

.stat-value-sizer,
.stat-value-live {
  grid-area: 1 / 1;
  justify-self: start;
}

.stat-value-sizer {
  visibility: hidden;
}

/* Direct child only — the counter's own spans live inside the <strong> and
   must keep the display face, not the label size. */
.stats > div > span {
  color: rgba(255, 255, 255, 0.95);
  font-size: 14px;
  line-height: 1.35;
}

.partner-copy {
  max-width: 100%;
  margin: 22px 0 0;
  color: #bcd6c6;
  font-size: 18px;
  font-weight: 300;
  line-height: 1.5;
  text-align: left;
}

.plumrocket-copy .primary-button {
  margin-top: 32px;
}

/* No card — the questions sit straight on the swapped-to-white surface. On the
   same measure as every other content column, so the question rows line up with
   the nav, hero and stats above them rather than sitting in an inset well. */
.faq-panel {
  position: relative;
  width: min(var(--max), calc(100% - 48px));
  margin: 0 auto;
  padding: 40px 0 95px;
  color: #24303e;
}

.faq-label {
  display: table;
  margin: 0 auto;
  border-radius: 999px;
  padding: 8px 14px;
  color: #4e555a;
  background: #ececed;
  font-size: 16px;
  font-weight: 700;
}

.faq-panel h2 {
  margin-top: 22px;
  font-size: 40px;
  font-weight: 400;
  text-align: center;
}

.faq-list {
  margin-top: 65px;
}

.faq-item {
  border-bottom: 1px solid #dce0e3;
  transition: border-color var(--motion-fast) var(--ease-out);
}

.faq-item[data-state="open"] {
  border-color: rgba(57, 122, 69, 0.45);
}

.faq-trigger-header {
  display: flex;
}

.faq-trigger {
  position: relative;
  display: flex;
  flex: 1;
  align-items: center;
  width: 100%;
  border: none;
  background: none;
  cursor: pointer;
  text-align: left;
  color: inherit;
  padding: 47px 52px 47px 2px;
  font-size: 18px;
  font-weight: 700;
  transition: color var(--motion-fast) var(--ease-out);
}

.faq-trigger::after {
  content: "+";
  position: absolute;
  top: 50%;
  right: 4px;
  font-size: 24px;
  font-weight: 400;
  transform: translateY(-50%);
  transition: transform var(--motion-hover) var(--ease-out);
}

.faq-trigger[data-state="open"] {
  color: #397a45;
}

.faq-trigger[data-state="open"]::after {
  transform: translateY(-50%) rotate(45deg);
}

.faq-answer {
  overflow: hidden;
}

/* Matches the server-rendered data-state before GSAP mounts, so a closed
   row never flashes open pre-hydration. Height/fade past this point are
   owned by FaqAccordion.jsx (GSAP), not CSS. */
.faq-answer[data-state="closed"] {
  height: 0;
}

.faq-answer-inner {
  padding: 0 2px 22px;
}

.faq-answer-inner p {
  margin: 0;
  color: #747d84;
  font-size: 16px;
  line-height: 1.55;
}

/* Closing black block: the contact form and the footer share one surface and
   one measure, separated by hairlines rather than by colour changes. */
.site-footer {
  position: relative;
  /* Above the pinned audience section (z 0), below the partner surface (z 3)
     that slides over it. */
  z-index: 1;
  padding: 96px 0 0;
  color: #f2f3f5;
  background: var(--footer-ink);
}

.contact-block {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 80px;
  /* Stretch, not start: .contact-copy needs the full row height (set by the
     taller form) so .contact-direct's own margin-top: auto has a bottom to
     push down to. */
  align-items: stretch;
  padding-bottom: 80px;
}

.contact-copy {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.contact-kicker {
  margin: 0;
  color: #8b9199;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.16em;
}

.contact-copy h2 {
  margin: 0;
  color: #fff;
  font-size: 46px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.08;
  text-wrap: balance;
}

.contact-copy h2 em {
  color: #c9ced4;
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
}

.contact-intro {
  max-width: 400px;
  margin: 0;
  color: rgba(242, 243, 245, 0.62);
  font-size: 16px;
  line-height: 1.65;
  text-wrap: pretty;
}

/* margin-top: auto, not a flat value — .contact-copy now stretches to the
   form's height (see .contact-block), and this eats that slack to sit flush
   with the form's bottom edge instead of trailing right under the intro copy. */
.contact-direct {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: auto;
}

.contact-direct a,
.contact-direct address {
  display: flex;
  align-items: center;
  gap: 12px;
  color: #fff;
  font-size: 20px;
  font-style: normal;
  font-weight: 600;
  transition: color var(--motion-fast) var(--ease-out);
}

.contact-direct a:hover {
  color: #c9ced4;
}

.contact-direct-icon {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 1px solid rgba(255, 255, 255, 0.13);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
}


.contact-form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 20px;
  padding: 32px;
  background: rgba(255, 255, 255, 0.04);
}

.contact-form label {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.contact-form .form-wide {
  grid-column: span 2;
}

.contact-form label > span {
  font-size: 13px;
  font-weight: 700;
}

.contact-form input,
.contact-form textarea {
  width: 100%;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 10px;
  color: #fff;
  background: rgba(255, 255, 255, 0.07);
  font: inherit;
  font-size: 15px;
  outline: none;
  transition:
    background var(--motion-fast) var(--ease-out),
    border-color var(--motion-fast) var(--ease-out),
    box-shadow var(--motion-hover) var(--ease-out);
}

.contact-form input {
  height: 48px;
  padding: 0 16px;
}

.contact-form textarea {
  padding: 14px 16px;
  resize: vertical;
}

.contact-form input::placeholder,
.contact-form textarea::placeholder {
  color: rgba(242, 243, 245, 0.45);
}

.contact-form input:focus,
.contact-form textarea:focus {
  border-color: #fff;
  background: rgba(255, 255, 255, 0.1);
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.12);
}

.form-footer {
  grid-column: span 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.form-state {
  max-width: 260px;
  margin: 0;
  color: rgba(242, 243, 245, 0.5);
  font-size: 13px;
  line-height: 1.5;
}

.form-state.error {
  color: #ffb4b4;
}

.form-state.success {
  color: #9ef0b0;
}

.send-button {
  border: 0;
  color: #123219;
  background: var(--green);
  cursor: pointer;
  font-family: inherit;
}

.send-button .btn-knob {
  color: var(--green);
  background: #123219;
}

.send-button:hover:not(:disabled) {
  filter: brightness(1.04);
  box-shadow: 0 14px 34px rgba(99, 231, 125, 0.2);
}

.send-button:disabled {
  cursor: wait;
  opacity: 0.72;
}

.footer-main {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 80px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding: 56px 0;
}

.footer-brand {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.footer-logo {
  display: inline-flex;
  width: 168px;
}

.footer-logo img {
  display: block;
  width: 100%;
  height: auto;
}

.footer-tagline {
  max-width: 280px;
  margin: 0;
  color: rgba(242, 243, 245, 0.55);
  font-size: 15px;
  line-height: 1.6;
  text-wrap: pretty;
}

/* Brand marks closing the footer's left column. .footer-brand stretches to the
   grid row's full height (set by the taller map column beside it), so its flex
   items sit at the top with empty space below by default — margin-top: auto
   is what pushes this one to the bottom of that space instead of the 18px
   .footer-brand gap.

   No badge behind them any more: the delivered Facebook asset is itself a solid
   disc with the f knocked out, so ringing it in the contact list's bordered
   circle produced a circle inside a circle. The marks are drawn at their own
   48px instead, and the link is just the glyph. */
.footer-social {
  display: flex;
  gap: 12px;
  margin-top: auto;
}

.footer-social-link {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  color: rgba(255, 255, 255, 0.6);
  transition: color var(--motion-fast) var(--ease-out);
}

.footer-social-link svg {
  width: 100%;
  height: 100%;
}

.footer-social-link:hover {
  color: var(--green);
}

.footer-office {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* Overlays the map's top-left corner rather than sitting above the frame. The
   10px inset is MapLibre's own control padding, so it lines up with the zoom
   group across the top edge, and the panel reuses the exact fill those controls
   and the attribution already carry — all three read as one chrome family.
   The map has to stay draggable underneath: this is a caption, not a control,
   hence pointer-events. The max-width keeps it clear of the +/- group when the
   footer column gets narrow. */
.footer-office-label {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 3;
  max-width: calc(100% - 66px);
  margin: 0;
  border-radius: 8px;
  padding: 7px 12px;
  background: rgba(29, 30, 34, 0.72);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14);
  color: rgba(242, 243, 245, 0.7);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: normal;
  pointer-events: none;
}

.footer-office address {
  color: rgba(242, 243, 245, 0.7);
  font-size: 15px;
  font-style: normal;
  line-height: 1.6;
}

.footer-map {
  position: relative;
  height: 260px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.13);
  border-radius: 16px;
  background: #111315;
}

/* No filter any more: the tiles arrive dark from CARTO, so the repaint cost of
   inverting a live map on every tile load is gone. */
.footer-map-canvas {
  width: 100%;
  height: 100%;
}

.footer-map-pin {
  display: block;
  width: 14px;
  height: 14px;
  border: 2px solid var(--footer-ink);
  border-radius: 999px;
  background: var(--green);
  box-shadow: 0 0 0 5px rgba(99, 231, 125, 0.25);
}

/* MapLibre's own chrome, toned down to the footer's palette. */
.footer-map .maplibregl-ctrl-group {
  border-radius: 8px;
  background: rgba(29, 30, 34, 0.72);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14);
}

.footer-map .maplibregl-ctrl-group button + button {
  border-top-color: rgba(255, 255, 255, 0.14);
}

.footer-map .maplibregl-ctrl-group button span {
  filter: invert(1);
}

.footer-map .maplibregl-ctrl-attrib {
  background: rgba(29, 30, 34, 0.72);
}

.footer-map .maplibregl-ctrl-attrib a,
.footer-map .maplibregl-ctrl-attrib-button {
  color: rgba(242, 243, 245, 0.7);
}

.footer-map .maplibregl-canvas:focus-visible {
  outline: 3px solid var(--green);
  outline-offset: -3px;
}

.footer-map-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(29, 30, 34, 0) 45%,
    rgba(29, 30, 34, 0.82) 100%
  );
  pointer-events: none;
}

/* One centred line since the legal nav came out — the flex row that used to
   push the copyright and the links apart has nothing left to space. */
.footer-bottom {
  display: flex;
  align-items: center;
  justify-content: center;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  padding: 22px 0 34px;
  color: rgba(242, 243, 245, 0.45);
  font-size: 12px;
}

@media (max-width: 900px) {
  :root {
    --radius-lg: 38px;
  }

  .services-section {
    --services-content-gutter: 80px;
  }

  .site-nav {
    /* .nav-links and .nav-actions are hidden below — only .brand and
       .nav-burger are left, so space-between is all that's needed. */
    justify-content: space-between;
  }

  .nav-links,
  .site-nav .nav-actions {
    display: none;
  }

  .nav-burger {
    display: inline-flex;
  }

  .mobile-nav {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    padding: 16px 24px calc(56px + env(safe-area-inset-bottom, 0px));
    background: rgba(5, 6, 6, 0.98);
    backdrop-filter: blur(8px);
    /* Rest state only — the open/close shutter runs off a GSAP timeline
       (clip-path circle, centered on the burger) in MobileNav.jsx, not CSS. */
    clip-path: circle(0% at 100% 0%);
    pointer-events: none;
  }

  .mobile-nav-bar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex: none;
  }

  .mobile-nav-links {
    display: grid;
    gap: 4px;
    margin-top: 16px;
  }

  .mobile-nav-links a {
    padding: 10px 4px;
    color: #fff;
    font-family: var(--font-display);
    font-size: 40px;
  }

  .mobile-nav-divider {
    height: 1px;
    margin-top: auto;
    background: rgba(255, 255, 255, 0.12);
    transform-origin: 0% 50%;
  }

  .mobile-nav-contact {
    display: grid;
    gap: 12px;
    margin-top: 20px;
  }

  .mobile-nav-contact-mail {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--green);
    font-size: 17px;
    font-weight: 600;
  }

  .mobile-nav-contact a:not(.mobile-nav-contact-mail) {
    color: #fff;
    font-size: 15px;
    font-weight: 600;
  }

  .mobile-nav-contact address {
    color: #fff;
    font-style: normal;
    font-size: 15px;
  }

  /* Same treatment as .footer-social — the Facebook/Instagram marks are
     already filled glyphs, so no badge circle behind them here either. */
  .mobile-nav-social {
    display: flex;
    gap: 12px;
    margin-top: 4px;
  }

  .mobile-nav-social-link {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    color: rgba(255, 255, 255, 0.6);
    transition: color var(--motion-fast) var(--ease-out);
  }

  .mobile-nav-social-link svg {
    width: 100%;
    height: 100%;
  }

  .mobile-nav-social-link:hover {
    color: var(--green);
  }

  .mobile-nav-cta {
    justify-self: stretch;
    justify-content: center;
    margin-top: 24px;
  }

  .hero-grid {
    grid-template-columns: 1fr;
    padding-top: 48px;
  }

  .hero-copy {
    max-width: 650px;
  }

  .hero-copy h1 {
    font-size: clamp(42px, 8vw, 64px);
  }

  /* One column now, so the scene gets the full measure instead of the leftover
     beside the copy — much closer to the width it was drawn at. */
  .hero-grid {
    row-gap: 40px;
  }

  .logo-wall {
    gap: 39px;
  }

  .services-panel {
    padding: 48px 0 60px;
  }

  /* Too narrow for the 3-col block: two equal columns instead. */
  .services-grid {
    grid-template-columns: 1fr 1fr;
  }

  .advantages {
    grid-template-columns: 1fr 1fr;
  }

  /* Single column: heading copy, then the slider, then the CTA — the button
     drops below the slider (and its pagination arrows) instead of sitting
     next to the heading like it does on desktop. */
  .audience-inner {
    grid-template-columns: 1fr;
    grid-template-areas: "heading" "list" "cta";
  }

  .audience-body {
    grid-template-columns: 1fr;
    gap: 32px;
  }

  /* Left over from the inset-well version of this panel — it now runs on the
     same measure as every other column, so no side inset here either. */
  .faq-panel {
    padding-inline: 0;
  }

  .contact-block,
  .footer-main {
    gap: 48px;
  }

  .contact-copy h2 {
    font-size: 38px;
  }
}

/* Below this the scene is too narrow for the percentage arrangement to stay
   legible — the small cards shrink to thumbnails and the overlaps turn into
   clutter — so the layout is dropped entirely and the cards stack. The frame
   loop stops feeding parallax at the same width (COMPACT_QUERY in
   parallax-floating), so nothing is left driving them sideways. */
@media (max-width: 768px) {
  .hero-stage {
    aspect-ratio: auto;
    display: flex;
    flex-direction: column;
    gap: 18px;
  }

  /* !important is the cost of the layout being inline: these five properties are
     exactly the ones the showcase array writes onto each card, and the stack has
     to outrank them. aspect-ratio is deliberately not among them — it stays
     inline so every card keeps its own proportion in the stack. */
  .hero-card {
    position: static !important;
    left: auto !important;
    top: auto !important;
    width: 100% !important;
    z-index: auto !important;
  }
}

@media (max-width: 680px) {
  .site-nav,
  .hero-grid,
  .trusted,
  .why-inner,
  .audience-inner,
  .plumrocket-inner,
  .footer-inner {
    width: calc(100% - 76px);
  }

  .site-nav {
    height: 76px;
    padding-top: 10px;
  }

  .badge {
    --size: 83px;
    --offset: 16px;
  }

  .badge__label {
    font-size: 8px;
  }

  /* Smaller pill, same geometry: the knob shrinks with it so the swap keeps
     working instead of clipping. */
  .primary-button,
  .nav-cta,
  .card-cta,
  .send-button {
    height: 60px;
    padding: 0 52px 0 18px;
  }

  .primary-button:hover,
  .nav-cta:hover,
  .card-cta:hover,
  .send-button:hover:not(:disabled) {
    padding: 0 18px 0 52px;
  }

  .btn-knob {
    width: 40px;
    height: 40px;
  }

  .primary-button .btn-knob svg,
  .nav-cta .btn-knob svg,
  .send-button .btn-knob svg {
    width: 20px;
    height: 20px;
  }

  .primary-button:hover .btn-knob,
  .nav-cta:hover .btn-knob,
  .card-cta:hover .btn-knob,
  .send-button:hover:not(:disabled) .btn-knob {
    right: calc(100% - 44px);
  }

  .hero-shell {
    min-height: auto;
  }

  /* Dropped on mobile: at heading sizes this small the glow reads as clutter
     rather than an accent, and losing it lets "для" sit right after the word
     instead of leaving a tucked gap for an icon that is no longer there. */
  .hero-star {
    display: none;
  }

  /* Whole showcase drops on mobile — stacked one-per-row (see the 768px
     breakpoint above), five full-width cards ran the scene past 1000px tall
     and left a long empty run before the trusted logos, and the video card
     alone was still a second autoplaying stream next to the hero background
     video. The button now leads straight into the logo wall. Desktop's
     percentage arrangement is a different rule block entirely, so this
     cannot reach it. */
  .hero-stage {
    display: none;
  }

  .hero-video {
    top: 130px;
    left: 10%;
    width: 121%;
    max-width: none;
    height: 484px;
    opacity: var(--hero-video-opacity);
  }

  .hero-grid {
    min-height: 460px;
    padding-top: 0;
    align-items: start;
  }

  .hero-copy {
    padding-left: 0;
  }

  .hero-copy h1 {
    font-size: 49px;
    letter-spacing: -1.7px;
  }

  /* Reflows the heading to one word/phrase per line: "та" joins the line
     above instead of starting its own, while "для" and "бізнесу" each get a
     line of their own below. */
  .hero-break-hide-mobile {
    display: none;
  }

  .hero-break-show-mobile {
    display: block;
  }

  /* Only closes a gap when "для" trails the star on the same line — on its
     own line here, the same nudge just drags it left of the rest. */
  .hero-nudge-left {
    margin-inline-start: 0;
  }

  .hero-copy p {
    margin-top: 28px;
    font-size: 16px;
  }

  .primary-button {
    width: 100%;
    justify-content: space-between;
    margin-top: 30px;
    font-size: 16px;
  }

  .trusted {
    padding-bottom: 30px;
    overflow: hidden;
  }

  .trusted > p {
    font-size: 16px;
    text-align: center;
  }

  .logo-wall {
    gap: 29px;
    height: 59px;
  }

  .logo-wall img {
    max-width: 81px;
    max-height: 28px;
  }

  .services-section {
    --services-tail: 40px;
    --services-panel-gutter: 20px;
    --services-content-gutter: 100px;
    padding: 48px 0 40px;
  }

  .services-panel {
    width: 100%;
    padding: 42px 0 28px;
    border-radius: 34px;
  }

  .services-panel::before {
    animation: none;
  }

  .services-inner {
    animation: none;
  }

  .services-heading {
    gap: 12px;
    padding-inline: 8px;
  }

  .services-heading h2 {
    font-size: 32px;
    white-space: normal;
  }

  .services-heading p {
    font-size: 16px;
    white-space: normal;
  }

  .services-grid {
    grid-template-columns: 1fr;
    gap: 16px;
    margin-top: 42px;
  }

  .service-card {
    min-height: 300px;
  }

  .why-section {
    min-height: auto;
    grid-template-rows: auto auto;
  }

  .why-inner {
    padding: 0;
  }

  .why-section blockquote {
    font-size: 24px;
  }

  .advantages {
    grid-template-columns: 1fr;
    gap: 38px;
    margin-top: 55px;
  }

  .advantages article {
    max-width: 330px;
  }

  .wordmark {
    align-self: auto;
    margin-block: 60px;
    font-size: 84px;
  }

  .audience-reveal {
    padding: 72px 0;
    border-radius: 38px 38px 0 0;
  }

  .partner-surface {
    border-radius: 0 0 38px 38px;
  }

  .audience-heading h2 {
    font-size: 32px;
    line-height: 1.08;
  }

  .audience-inner {
    row-gap: 42px;
  }

  .audience-cta {
    justify-self: start;
  }

  .audience-row h3 {
    font-size: 25px;
  }

  /* Turns the hover-driven tab list + preview pair into a slider: image on
     top (order set on the grid items, no markup reorder needed), one row's
     text as the caption underneath, prev/next controls last. */
  .audience-body {
    gap: 20px;
  }

  .audience-preview {
    order: 1;
    touch-action: pan-y;
  }

  .audience-tabs {
    order: 2;
  }

  .audience-mobile-nav {
    order: 3;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 16px;
    margin-top: 4px;
  }

  /* Only the active row stays in the document — it doubles as the slider's
     caption, so the hover-list chrome (divider, bullet, line-in/out) that
     made sense for four stacked rows is stripped off. */
  .audience-row {
    display: none;
    border-bottom: 0;
    padding: 0;
    margin-bottom: 0;
    text-align: center;
  }

  .audience-row.is-active {
    display: block;
  }

  .audience-row::after {
    display: none;
  }

  .audience-row-title {
    justify-content: center;
  }

  .audience-row-bullet {
    display: none;
  }

  .audience-row-tags {
    margin-top: 8px;
  }

  .audience-row-copy {
    display: block;
    margin-top: 10px;
    font-size: 15px;
    line-height: 1.5;
    color: #3a3a3a;
  }

  .plumrocket-section {
    padding: 64px 0 0;
  }

  .partner-tail {
    height: 56px;
  }

  .plumrocket-inner h2 {
    max-width: 100%;
    font-size: 30px;
    line-height: 1.08;
  }

  .plumrocket-top {
    width: 100%;
    grid-template-columns: 1fr;
    gap: 0;
  }

  /* Image before copy on mobile — the reverse of the desktop grid's DOM
     order (copy, then collage), so it's an order swap, not a markup one. */
  .partner-collage {
    order: -1;
    margin-top: 0;
    margin-bottom: 28px;
  }

  .stats {
    width: 100%;
    max-width: 100%;
    grid-template-columns: 1fr 1fr;
    row-gap: 28px;
  }

  /* The dividers only make sense between left/right pairs on this 2-column
     wrap — first-child alone no longer covers the second row's left cell. */
  .stats div {
    padding: 0 20px;
    border-left: none;
  }

  .stats div:nth-child(even) {
    padding-left: 20px;
    border-left: 1px solid rgba(255, 255, 255, 0.2);
  }

  .partner-surface.is-light .stats div:nth-child(even) {
    border-left-color: rgba(11, 12, 13, 0.15);
  }

  .stats div:nth-child(odd) {
    padding-left: 0;
  }

  .stats strong {
    font-size: 42px;
  }

  .partner-copy {
    max-width: 100%;
    margin-top: 42px;
    font-size: 16px;
  }

  .faq-panel {
    width: calc(100% - 20px);
    margin-top: 0;
    padding: 32px 42px 40px;
  }

  .faq-panel h2 {
    font-size: 35px;
  }

  .faq-list {
    margin-top: 45px;
  }

  .site-footer {
    padding: 64px 0 0;
  }

  .contact-block,
  .footer-main {
    grid-template-columns: 1fr;
    gap: 40px;
  }

  .contact-block {
    padding-bottom: 48px;
  }

  .contact-copy h2 {
    font-size: 32px;
  }

  .contact-direct a {
    font-size: 17px;
  }

  .contact-form {
    grid-template-columns: 1fr;
    padding: 22px;
  }

  .contact-form .form-wide,
  .form-footer {
    grid-column: auto;
  }

  .footer-main {
    padding: 40px 0;
  }

  .send-button {
    width: 100%;
  }
}

/* Below three columns, a service card's own width is what constrains this
   button — the shared pill above (sized for the desktop grid) is wider than a
   single card's padding leaves room for, so it overflowed the card and got cut
   off by .service-card's overflow: hidden. Placed after both breakpoints above
   so it wins at every width in this range, not just one of them. */
@media (max-width: 900px) {
  .primary-button,
  .nav-cta,
  .card-cta,
  .send-button {
    height: 60px;
  }

  /* Drops the pill chrome for a plain green text link with a trailing arrow —
     lighter in the card than a full button. */
  .card-cta {
    height: auto;
    width: auto;
    padding: 0;
    border: none;
    background: none;
    box-shadow: none;
    color: #32cc4f;
    font-size: 16px;
    white-space: nowrap;
  }

  .card-cta:hover,
  .service-card:hover .card-cta {
    border: none;
    background: none;
    box-shadow: none;
    color: #32cc4f;
  }

  .card-cta .btn-knob {
    position: static;
    width: 20px;
    height: 20px;
    margin-left: 8px;
    background: none;
    color: #32cc4f;
  }

  .card-cta .btn-knob svg {
    width: 20px;
    height: 20px;
  }

  .card-cta:hover .btn-knob,
  .service-card:hover .card-cta .btn-knob {
    right: auto;
    transform: none;
    background: none;
    color: #32cc4f;
  }
}

/* Sticky bottom-right badge — independent of the in-flow "Обговорити проєкт"
   buttons, so it needs its own fixed stack rather than reusing
   .primary-button/.nav-cta. Above the partner surface's z-index:3 and the
   audience section's sticky panel so it never slips behind them. Rotation,
   the hover speed-up and the light/dark colour swap are StartProjectBadge.jsx
   — this only draws the glass circle and the two --ink themes. */
.badge {
  --size: 110px;
  --offset: 32px;
  --font: "Inter", Arial, Helvetica, sans-serif;

  /* Default theme = badge resting over a dark section → white ink. */
  --ink: #ffffff;
  --glass: rgba(255, 255, 255, 0.1);
  --edge: rgba(255, 255, 255, 0.3);
  --gloss: rgba(255, 255, 255, 0.55);
  --shade: rgba(0, 0, 0, 0.22);

  position: fixed;
  right: var(--offset);
  bottom: var(--offset);
  width: var(--size);
  height: var(--size);
  z-index: 60;

  display: grid;
  place-items: center;
  border-radius: 50%;
  text-decoration: none;
  color: var(--ink);
  transition: color 0.45s ease;
  -webkit-tap-highlight-color: transparent;
  cursor: pointer;
}

/* Badge over a LIGHT section → dark ink, denser glass. */
.badge.is-on-light {
  --ink: #101215;
  --glass: rgba(255, 255, 255, 0.42);
  --edge: rgba(0, 0, 0, 0.1);
  --gloss: rgba(255, 255, 255, 0.95);
  --shade: rgba(30, 50, 80, 0.14);
}

.badge__glass {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  overflow: hidden;
  background: var(--glass);
  border: 1px solid var(--edge);
  backdrop-filter: blur(14px) saturate(180%);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  box-shadow:
    inset 0 1px 0 var(--gloss),
    inset 0 -14px 24px -14px var(--gloss),
    0 14px 34px var(--shade);
  transition:
    background-color 0.45s ease,
    border-color 0.45s ease,
    box-shadow 0.45s ease;
}

@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .badge__glass {
    background: rgba(120, 130, 145, 0.38);
  }

  .badge.is-on-light .badge__glass {
    background: rgba(255, 255, 255, 0.78);
  }
}

.badge__sheen {
  position: absolute;
  inset: -30%;
  background: linear-gradient(
    115deg,
    transparent 34%,
    var(--gloss) 50%,
    transparent 66%
  );
  opacity: 0.5;
  transition: opacity 0.45s ease;
  will-change: transform;
}

.badge__ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.badge__ringText {
  font-family: var(--font);
  font-size: 18px; /* viewBox units — StartProjectBadge.jsx shrinks this if a run overflows its arc */
  font-weight: 700;
  fill: currentColor;
  fill-opacity: 0.45;
}

.badge__label {
  position: relative;
  text-align: center;
  font-size: 10px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  pointer-events: none;
}

.badge:focus-visible {
  outline: none;
}

.badge:focus-visible .badge__glass {
  box-shadow: 0 0 0 3px var(--green), inset 0 1px 0 var(--gloss);
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }

  .hero-video {
    visibility: hidden;
  }

  /* The frame loop bails out under reduced motion and clears its inline opacity,
     so this is what the cards rest at. */
  .hero-card-frame {
    opacity: 0.9;
  }

  .floating-element {
    will-change: auto;
  }

  .wordmark-track {
    animation: none;
    transform: translate3d(-8vw, 0, 0);
  }

  .services-panel::before {
    width: calc(100% - var(--services-panel-gutter));
  }

  .services-inner {
    width: min(var(--max), calc(100% - var(--services-content-gutter)));
  }

  .audience-preview-img.is-entering {
    animation: none;
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
  }

  .audience-preview-img.is-entering img {
    transform: scale(1);
  }
}

/* Playground: bento services section rendered after HeroSectionDraft on
   /hero only (see app/ServicesBento.jsx + ServicesBentoMotion.jsx). Tokens
   below are all pulled from the hero's own :root block and .hero-* rules —
   --green/--green-dark, --ease-out/--motion-*, the hero-card radius recipe
   (recoloured for a light surface — see the stack block further down for
   why). Does not touch any existing rule above. */

/* Scroll-stack: this panel rides up over .hero-shell and covers it
   (scroll-stacking-sections-brief.md, §1). .hero-shell's own sticky/z-index
   is added as a *new* rule further down, layered onto the existing
   .hero-shell block above rather than edited in place — every other
   hero-shell property (background, video, beams…) is untouched.
   Scoped under .stack (only present on the /hero draft route, see
   app/hero/page.jsx) — .hero-shell is also the real homepage hero's class
   (app/HeroSection.jsx), and an unscoped rule here made it sticky/overlapping
   on the live site too. */
.stack {
  position: relative;
}

.stack .hero-shell {
  position: sticky;
  top: 0;
  z-index: 1;
}

.services-bento {
  position: sticky;
  top: 0;
  z-index: 2;
  isolation: isolate;
  /* No overflow:hidden here: an ancestor with overflow other than visible
     becomes the containing block for its sticky descendants' scroll math,
     which would freeze .services-sticky-card in place instead of letting it
     pin against the real viewport as the page scrolls. Same reasoning is
     why nothing above this in the tree (.stack, .services-sticky-list,
     .services-sticky-item) may ever get a transform, even a transient one
     from a scroll-driven tween: a transform on a sticky element's ancestor
     forces it to be its containing block too, which breaks the stick. */
  min-height: 100svh;
  color: var(--ink);
  background: var(--paper);
  /* Same shadow shape as .hero-card's own recipe (52px blur, 22px spread),
     recoloured off --ink instead of the near-black rgba(4,18,9,…) that
     recipe uses — that value reads as a dirty smudge on a white surface,
     this is the same "lift" on a light one. */
  box-shadow: 0 -24px 60px rgba(11, 12, 13, 0.16);
}

/* Sits above every ServicesStickyList panel so it reads as one static
   heading the whole time the stack scrolls underneath it. Has to be
   `position: sticky`, not absolute: .services-bento is the LAST child of
   .stack and exactly fills its remaining height, so .services-bento's own
   sticky range is degenerate (its containing block's bottom edge, minus its
   own height, lands right back on its own static top — nowhere to travel),
   meaning .services-bento itself is never actually visually pinned for more
   than an instant. An absolutely positioned child anchored to it therefore
   scrolls away with the page instead of staying put. This h2's OWN sticky
   range doesn't have that problem — its containing block is .services-
   bento's content box, which (h2's own tiny box aside) is ~650vh tall, so
   there's ample room for it to stay stuck at `top` for virtually the whole
   scroll, independent of whatever .services-bento itself is doing.

   margin-bottom: -1.1em (matches line-height: 1.1, so it scales with the
   clamp()'d font-size) cancels this single-line heading's own flow-box
   height back to ~0, so it doesn't push .services-runway's (and so
   .services-sticky-card's) static top down — without that, .services-pin's
   own sticky pin hadn't engaged yet for that first sliver of scroll, and
   .services-bento's background showed through above the panel as a stray
   band behind the heading. The negative margin only removes this element's
   contribution to its PARENT's flow height; it doesn't change what the
   parent's content box is, so it doesn't shrink the sticky travel range
   above. */
.services-bento-heading {
  position: sticky;
  top: 148px;
  /* Above every .services-sticky-card (z-index 1-5, same .services-bento
     stacking context) so it stays put while the stack scrolls beneath it,
     instead of getting covered like everything else in the section. */
  z-index: 10;
  /* Horizontal centering is plain block auto-margins, NOT left:50% +
     translateX(-50%) — that pairing centers an absolutely-positioned box
     against its containing block's edges, but position:sticky (while not
     yet stuck) resolves `left` as a relative offset from the element's OWN
     static position instead, same as position:relative. The old left/
     transform pairing was therefore never actually centering this — it was
     off by roughly half the container's width. margin-left/right:auto on a
     width:max-content block sidesteps that entirely and doesn't touch the
     vertical (sticky) axis at all. */
  margin-left: auto;
  margin-right: auto;
  /* margin-top matches `top` and margin-bottom cancels (top + own rendered
     height) — net flow contribution to .services-bento stays ~0, same
     purpose as the old margin-bottom:-1.1em trick, just pinned to this
     fixed `top` instead of scaling off the clamp()'d font-size. */
  margin-top: 148px;
  margin-bottom: -179px;
  width: max-content;
  max-width: calc(100% - 96px);
  color: inherit;
  font-family: "Inter", sans-serif;
  font-weight: 300;
  font-size: 72px;
  letter-spacing: -0.4px;
  line-height: 1.1;
  text-align: center;
}

/* Round 5 rewrite — akaru.fr/expertises' actual DOM (not sticky-stack):
   a 920vh runway holds a single `position: sticky` pin exactly 100vh tall
   with `overflow: hidden`. All 5 panels sit `position: absolute; inset: 0`
   on top of each other inside that pin, same z-index — the next panel
   doesn't slide up over the last one, it's revealed in place by `clip-path`
   animating from `inset(100% 0 0)` (fully hidden) to `inset(0% 0 0)` (fully
   shown), a curtain rising from the bottom.

   920vh, not 650vh: each panel's 1.3vh reveal is followed by a 0.6vh dwell
   before the next panel's curtain starts climbing — (5 - 1) * 1.9vh reveal
   +dwell segments + 0.6vh for panel 0's own dwell + 100vh for the pin
   itself = 920vh. Without the dwell, the next curtain started the instant
   the previous one finished, so no panel ever held still long enough to
   read. See the rAF loop in ServicesStickyList.jsx (REVEAL/HOLD/SEG) that
   drives this and the media curtain off the same scroll math — if the
   panel count ever changes, this has to stay
   (panels.length - 1) * (REVEAL + HOLD) + HOLD + 100vh. */
.services-runway {
  position: relative;
  height: 920vh;
}

.services-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}

/* akaru.fr/expertises' .ExpertisesServicesItem: align-items:flex-end +
   justify-content:center pins the content row to the panel's bottom edge and
   centers it horizontally — no padding on the panel itself, every inset
   comes from padding-left inside the row's own columns instead (see
   .services-sticky-card-col--left/--right below), matching the reference's
   "no outer page margins" layout. */
.services-sticky-card {
  position: absolute;
  inset: 0;
  height: 100vh;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  overflow: hidden;
  color: #111827;
  /* --p (set per-frame by ServicesStickyList.jsx) is the single raw scroll
     progress for this panel, 0..1 — this curtain and the media's own (see
     .services-sticky-card-media) both derive from the same value, so they
     move in exact lockstep instead of two clip-paths drifting apart.
     Default 0 (fully hidden) covers the very first paint before the rAF
     loop's first frame runs. */
  clip-path: inset(calc((1 - var(--p, 0)) * 100%) 0% 0%);
  will-change: clip-path;
}

/* Base panel — always fully open; confirmed with the client as correct
   (the first screen shows its media immediately, no reveal). */
.services-sticky-card:first-child {
  --p: 1;
}

/* Flat swatch colors per panel (1 web, 2 speed, 3 support, 4 ai, 5 auto),
   picked from a supplied palette reference — replaces the old radial-gradient
   pastel wash (which faded each tone out to white) with a solid fill. */
.services-sticky-card.sky {
  background: #b7c5ff;
}

.services-sticky-card.amber {
  background: #fedf89;
}

.services-sticky-card.mint {
  background: #6ce9a6;
}

.services-sticky-card.lilac {
  background: #fda29b;
}

.services-sticky-card.rose {
  background: #e1d6ff;
}

/* akaru.fr/expertises' .itemContentWrap: three flex items, space-between.
   Left/right columns claim a fixed 33% each (.itemTitleWrap/.itemContent);
   the media column takes whatever's left (~34%, capped separately — see
   .services-sticky-card-col--center below), which is what actually produces
   the reference's near-edge-to-edge layout. No grid, no gap: the reference
   uses fixed flex-basis + padding-left inside each column instead of an
   even gutter, and the numbers below (padding-left, margin-right, etc.) are
   converted 1:1 from akaru's own CSS (`rem` there floats at 13.3332px @1920,
   i.e. ~0.694vw — so 5rem harmless-looks-like-80px is actually 3.47vw; using
   vw instead of rem/px keeps the conversion correct at any viewport width). */
.services-sticky-card-row {
  display: flex;
  justify-content: space-between;
  width: 100%;
}

/* Key principle (see task doc): no block here gets its own vertical
   coordinate. Both side columns are align-self:center against the row,
   whose own height is set by the media column (see --center below) — so
   they stay centered on the media automatically at any media size, with no
   per-panel or per-viewport tuning. */
.services-sticky-card-col--left {
  flex: 0 0 33%;
  width: 33%;
  align-self: center;
  display: flex;
  padding-left: 3.47vw;
  /* Shifts the title ~60px above the media's vertical center — an
     intentional asymmetry in the reference (compare akaru's own measured
     centers: media 482, heading 423), not a bug. Because this column is
     align-self:center'd on the ROW, adding margin-bottom here (instead of a
     fixed top offset) grows the column's own box downward, which pulls its
     *center* — and so the title sitting at the top of that box — upward.
     That keeps the offset relative to the row's actual height instead of a
     hardcoded pixel value. */
  margin-bottom: 6.25vw;
}

.services-sticky-card-index {
  flex: none;
  margin-right: 2.78vw;
  margin-top: 0.97vw;
  font-family: "Inter", sans-serif;
  font-size: 15px;
  font-weight: 900;
  letter-spacing: 0.02em;
  opacity: 0.45;
}

/* Plain center, no asymmetric offset — "права колонка центрована точно" in
   the reference measurements. */
.services-sticky-card-col--right {
  flex: 0 0 33%;
  width: 33%;
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  text-align: left;
  padding-left: 4.17vw;
}

/* akaru's .itemImageContainer: aspect-ratio:1/1, width:100%, max-width:35%,
   max-height:60vh — a container that's square by default but gets capped
   non-square whenever 60vh < 35vw (see the task doc's "Розмір медіа" table:
   1920x919 renders 672x551, i.e. height-capped, not square). flex-basis:35vw
   (not width:100%+max-width:35% against an ambiguous flex-item containing
   block) sidesteps a circular-sizing issue while producing the same result:
   flex-shrink:1 (the default) lets it yield to the ~34vw actually left over
   once both 33% side columns claim their share, same as the reference's own
   33/34/33 split — that 34% isn't hardcoded anywhere, it falls out of the
   flex math.

   align-self:flex-end (not the row's default stretch) pins this column's
   *bottom* edge to the row's bottom edge — which is the panel's bottom edge,
   since .services-sticky-card itself is align-items:flex-end — regardless of
   how the row's own height ends up being computed. That's the "низ боксу
   медіа = 919 рівно" criterion. */
.services-sticky-card-col--center {
  flex: 0 1 35vw;
  align-self: flex-end;
  aspect-ratio: 1 / 1;
  max-height: 60vh;
  overflow: hidden;
}

/* Its own curtain, independent of the panel's (see .services-sticky-card
   above) — a single clip-path on the panel can't give "half the media
   height at half the scroll" since the media only occupies the bottom
   portion of the viewport, not the full height the panel's curtain travels.
   Keyed off the same --p, so it reaches full reveal in exact lockstep with
   the panel curtain instead of finishing early. The side inset (25% -> 0%)
   is the reference's own touch: the media doesn't just rise from the
   bottom, it also widens from a centered 50%-wide strip out to full width
   as --p approaches 1. Default var(--p, 1) (not 0) — if this ever renders
   without --p set (panel 0 pre-hydration, reduced-motion's cleared inline
   style), it should show fully open, not hidden. */
.services-sticky-card-media {
  display: block;
  width: 100%;
  height: 100%;
  /* No-op for the flat-color placeholder <span>, but required once this is
     a real <img>: the container's own ratio isn't fixed (square up to
     ~1.9:1 depending on viewport height, see .services-sticky-card-col
     --center above) — cover fills the box on every ratio instead of
     stretching/distorting the source image. */
  object-fit: cover;
  clip-path: inset(
    calc((1 - var(--p, 1)) * 100%) calc((1 - var(--p, 1)) * 25%) 0%
  );
}

/* Real photos aren't shot yet for any panel — a full-size gradient stands
   in so the layout doesn't shift (CLS) once they land. Each tone gets a
   richer pair than its own panel wash (see .services-sticky-card.sky etc
   above), or the placeholder would nearly disappear against its own
   background. */
.services-sticky-card-media--placeholder {
  background: linear-gradient(135deg, #8ec5ff, #eaf4ff);
}

.services-sticky-card.amber .services-sticky-card-media--placeholder {
  background: linear-gradient(135deg, #ffc98a, #fff1d9);
}

.services-sticky-card.mint .services-sticky-card-media--placeholder {
  background: linear-gradient(135deg, #86e6a4, #e2fbe9);
}

.services-sticky-card.lilac .services-sticky-card-media--placeholder {
  background: linear-gradient(135deg, #c3a8ff, #f1eaff);
}

.services-sticky-card.rose .services-sticky-card-media--placeholder {
  background: linear-gradient(135deg, #ffa8d6, #ffe3f2);
}

.services-sticky-card h3 {
  margin: 0;
  font-family: "Inter", sans-serif;
  font-weight: 600;
  font-size: clamp(24px, 2.6vw, 34px);
  letter-spacing: -0.5px;
  line-height: 1.1;
  color: inherit;
}

.services-sticky-card p {
  margin: 0;
  max-width: 34ch;
  opacity: 0.75;
  font-size: 16px;
  line-height: 1.55;
}

/* akaru.fr/expertises' "Voir l'expertise" pill: transparent/outlined at
   rest, fills solid on hover while the trailing dot crossfades into an
   arrow — the circle itself never moves or resizes, only its content. */
.services-sticky-card-cta {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  height: 60px;
  padding: 0 6px 0 24px;
  border-radius: 999px;
  border: 1px solid #111827;
  background: transparent;
  color: #111827;
  font-family: "Inter", sans-serif;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--motion-hover) var(--ease-out),
    color var(--motion-hover) var(--ease-out);
}

.services-sticky-card-cta-dot {
  position: relative;
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: grid;
  place-items: center;
}

.services-sticky-card-cta-dot .dot,
.services-sticky-card-cta-dot .arrow {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: 50%;
  transition: opacity 0.2s ease;
}

.services-sticky-card-cta-dot .dot::before {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: currentColor;
}

.services-sticky-card-cta-dot .arrow {
  opacity: 0;
  background: #fff;
  color: #111827;
}

@media (hover: hover) {
  .services-sticky-card-cta:hover,
  .services-sticky-card-cta:focus-visible {
    background: #111827;
    color: #fff;
  }

  .services-sticky-card-cta:hover .services-sticky-card-cta-dot .dot,
  .services-sticky-card-cta:focus-visible .services-sticky-card-cta-dot .dot {
    opacity: 0;
  }

  .services-sticky-card-cta:hover .services-sticky-card-cta-dot .arrow,
  .services-sticky-card-cta:focus-visible .services-sticky-card-cta-dot .arrow {
    opacity: 1;
  }
}

/* Sticky-stack reads worse on a single-finger, fast mobile scroll (the
   "cover" cut is harder to track), so below 680px it drops back to a plain
   vertical list — same recommendation as the homepage's own sticky-stack
   plan. Three columns collapse to one, centred, in source order (index+
   title, then icon, then copy+cta). */
@media (max-width: 680px) {
  .services-runway {
    height: auto;
  }

  .services-pin {
    position: static;
    height: auto;
    overflow: visible;
  }

  .services-sticky-card {
    position: relative;
    inset: auto;
    height: auto;
    min-height: 100vh;
    padding: 96px 28px 64px;
    clip-path: none;
  }

  .services-bento-heading {
    font-size: 18px;
    top: 24px;
  }

  .services-sticky-card-row {
    grid-template-columns: 1fr;
    justify-items: center;
    gap: 24px;
    text-align: center;
  }

  .services-sticky-card-col--left,
  .services-sticky-card-col--right {
    align-items: center;
    text-align: center;
  }

  /* The desktop layout absolutely centers the media against this column so
     it can overflow the column symmetrically (see .services-sticky-card-
     media) — on the single-column mobile stack that would collapse this
     column to zero height and let the media overlap the copy+CTA below it,
     so both revert to normal flow here instead. */
  .services-sticky-card-col--center {
    position: static;
  }

  .services-sticky-card-media {
    position: static;
    top: auto;
    left: auto;
    transform: none;
    margin-inline: auto;
    max-width: 100%;
  }

  .services-sticky-card h3 {
    font-size: 26px;
  }

  .services-sticky-card p {
    max-width: 46ch;
    font-size: 16px;
  }
}

/* The rAF loop that drives the clip-path curtain (see ServicesStickyList.jsx)
   bails out entirely under reduced motion, so without this the panels would
   stay stuck in their default state (only the first one ever visible) —
   drop back to the same plain vertical list as the small-viewport fallback
   above instead. */
@media (prefers-reduced-motion: reduce) {
  .services-runway {
    height: auto;
  }

  .services-pin {
    position: static;
    height: auto;
    overflow: visible;
  }

  .services-sticky-card {
    position: relative;
    inset: auto;
    height: auto;
    min-height: 100vh;
    clip-path: none;
  }
}
