/* ==========================================================================
   FONTS — Helvetica Neue (local files, Font/)
   ========================================================================== */

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueUltraLight.otf') format('opentype');
  font-weight: 100;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueUltraLightItalic.otf') format('opentype');
  font-weight: 100;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueThin.otf') format('opentype');
  font-weight: 200;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueThinItalic.otf') format('opentype');
  font-weight: 200;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueLight.otf') format('opentype');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueLightItalic.otf') format('opentype');
  font-weight: 300;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueRoman.otf') format('opentype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueItalic.ttf') format('truetype');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueMedium.otf') format('opentype');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueMediumItalic.otf') format('opentype');
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueBold.otf') format('opentype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueBoldItalic.otf') format('opentype');
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueHeavy.otf') format('opentype');
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueHeavyItalic.otf') format('opentype');
  font-weight: 800;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueBlack.otf') format('opentype');
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Helvetica Neue';
  src: url('Font/HelveticaNeueBlackItalic.otf') format('opentype');
  font-weight: 900;
  font-style: italic;
  font-display: swap;
}

/* ==========================================================================
   RESET
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  margin: 0;
  padding: 0;
  border: none;
  outline: none;
  box-shadow: none;
  -webkit-text-size-adjust: 100%;
  /* safety net against any stray horizontal overflow (a full-bleed
     mobile element a fraction of a px too wide, an --u rounding
     difference between this environment and a real device, etc.)
     showing up as a sliver strip on the right and a genuine horizontal
     scroll gutter — reported live on her phone, not reproducible in this
     session's own test viewport, so this clamps it regardless of the
     exact source rather than chasing one specific element. */
  overflow-x: hidden;
}

body {
  margin: 0;
  padding: 0;
  border: none;
  outline: none;
  box-shadow: none;
  min-height: 100vh;
  /* .social pins a section SHORTER than the viewport (890u, unlike
     .gallery/.stories which are taller) — while it's pinned+fixed, the
     leftover viewport strip below its own box isn't covered by anything
     and fell through to the browser's default white. Every section on the
     page is --beige-100 or darker anyway, so this is a safe blanket
     backdrop rather than a fix scoped to .social specifically. */
  background-color: var(--beige-100);
  font-family: 'Helvetica Neue', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body.is-preloading {
  overflow: hidden;
}

@media (hover: hover) and (pointer: fine) {
  body.has-custom-cursor,
  body.has-custom-cursor a,
  body.has-custom-cursor button {
    cursor: none;
  }
}

ul,
ol {
  list-style: none;
}

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
}

/* ==========================================================================
   SPECIAL TEXT SYMBOLS
   Оборачивать декоративные символы вида "(" в свой span,
   когда по макету скобка идёт курсивом, а остальной текст — regular.
   ========================================================================== */

.italic-symbol {
  font-style: italic;
}

/* ==========================================================================
   DESIGN TOKENS (з Figma Dev Mode, "Artefact", node 1:893)
   ========================================================================== */

:root {
  /* fluid unit — 1920px макет = 100vw, все розміри масштабуються пропорційно */
  --u: calc(100vw / 1920);

  /* 4-колонкова сітка: margin 50 / gutter 10 (у fluid-одиницях) */
  --grid-margin: calc(var(--u) * 50);
  --grid-gutter: calc(var(--u) * 10);
  --grid-col: calc((100vw - (2 * var(--grid-margin)) - (3 * var(--grid-gutter))) / 4);
  --col-1: var(--grid-margin);
  /* left-column correction: logo/eyebrow/H1 shifted -15u to balance the
     measured physical left/right margin discrepancy */
  --col-1-left: calc(var(--col-1) - var(--u) * 15);
  --col-2: calc(var(--grid-margin) + var(--grid-col) + var(--grid-gutter));
  --col-3: calc(var(--grid-margin) + 2 * (var(--grid-col) + var(--grid-gutter)));
  --col-4: calc(var(--grid-margin) + 3 * (var(--grid-col) + var(--grid-gutter)));

  --beige-100: #EDE9DD;
  --beige-80: rgba(237, 233, 221, 0.8);
  --beige-60: rgba(237, 233, 221, 0.6);
  --beige-40: rgba(237, 233, 221, 0.4);
  --blue: #B1B8CA;
  --olive-100: #9A9671;
  --olive-80: rgba(154, 150, 113, 0.8);
  --olive-60: rgba(154, 150, 113, 0.6);
  --brown-700: #5D4033;
  --preloader-bg: radial-gradient(circle at center, rgba(217, 224, 242, 1) 0%, rgba(147, 154, 172, 1) 100%);
  --preloader-fg: #2B3D2D;
}

/* ==========================================================================
   PRELOADER — "The Focusing Logo"
   Повноекранний оверлей із окремих SVG-літер логотипу. Літери проявляються
   зі стагером (opacity 0 → 1, blur 20px → 0) через GSAP, після чого весь
   оверлей плавно згасає, відкриваючи статичний Hero.
   ========================================================================== */

.preloader {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  border: none;
  outline: none;
  box-shadow: none;
  background: var(--preloader-bg);
}

.preloader__logo {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 6);
}

/* letters animate purely via this CSS keyframe (staggered per-letter with
   the --i custom property set inline in index.html) — script.js never
   sets opacity/filter on these elements, so nothing fights the animation;
   it only times a delayed call off the same durations to know when the
   hold + fade-out should start */
.preloader__logo span {
  display: inline-flex;
  align-items: center;
  animation: animateBlur 2.0s ease-in-out 3; /* EXACTLY 3 PASSES */
  animation-delay: calc(0.12s * var(--i));
  animation-fill-mode: forwards;
  will-change: opacity, filter;
}

.preloader__logo img {
  display: block;
  height: calc(var(--u) * 84);
  width: auto;
}

@keyframes animateBlur {
  0%, 20% {
    filter: blur(0px);
    opacity: 1;
  }
  40% {
    filter: blur(16px);
    opacity: 0.3;
  }
  60%, 100% {
    filter: blur(0px);
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .preloader__logo span {
    animation: none;
  }
}

/* No preloader screen on mobile at all, per her explicit request — this is
   CSS, not just initPreloader()'s isMobile branch in script.js (which
   already skipped the letter animation and jumped straight to
   runHeroEntrance()): scripts are `defer`, so the browser paints the
   static HTML — body.is-preloading + this full-viewport overlay — before
   any JS runs and removes it, which was still a visible flash on a slower
   phone/connection. Hiding it here means it never paints at all, not just
   "removed quickly." body.is-preloading's overflow:hidden is neutralized
   too so a locked scroll can't survive the same window. JS still runs
   (preloader.remove() + runHeroEntrance()) and is harmless/redundant here. */
@media (max-width: 768px) {
  .preloader {
    display: none;
  }

  body.is-preloading {
    overflow: visible;
  }
}

/* ==========================================================================
   HERO SECTION
   Усі розміри/позиції зведені у fluid-одиниці --u, де 1920px макета = 100vw,
   тому композиція масштабується пропорційно на будь-якій ширині екрана.
   ========================================================================== */

.hero {
  position: relative;
  width: 100%;
  aspect-ratio: 1920 / 1080;
  overflow: hidden;
  margin: 0;
  border: none;
  outline: none;
  box-shadow: none;
  background-color: #2f4a3d;
  color: var(--beige-100);
}

.hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --- decorative background labels + ARTEFACT ghost text --- */

.hero__label {
  position: absolute;
  z-index: 6;
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  margin: 0;
  font-size: calc(var(--u) * 20);
  line-height: 1.5;
  color: var(--olive-60);
  white-space: nowrap;
  pointer-events: none;
}

.hero__label--craft { left: calc(var(--u) * 569); top: calc(var(--u) * 158); }
.hero__label--texture { left: calc(var(--u) * 284); top: calc(var(--u) * 256); }
.hero__label--touch { left: calc(var(--u) * 1034); top: calc(var(--u) * 266); }
.hero__label--process { left: calc(var(--u) * 1403); top: calc(var(--u) * 403); }
.hero__label--materiality { left: calc(var(--u) * 227); top: calc(var(--u) * 834); }
.hero__label--rhythm { left: calc(var(--u) * 569); top: calc(var(--u) * 981); }

/* img/artefact-blur.png, positioned behind the vase (z-index 1 < vase's 2).
   Base top (141u) is the original Pixel Perfect position; the requested
   "shift up 15px" is folded into that offset (141 - 15 = 126) via `top`
   rather than a CSS `transform`, because this element is also a live
   gsap.quickTo() parallax target (script.js) — a static `transform` here
   would be overwritten the instant the parallax quickTo first touches x/y,
   silently losing the shift. Same visual result, no conflict with parallax. */
.hero__ghost-img {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 420);
  top: calc(var(--u) * 126);
  width: calc(var(--u) * 1367.744);
  height: auto;
  display: block;
  pointer-events: none;
}

/* --- top navigation --- */

.hero__logo {
  position: absolute;
  z-index: 7;
  left: var(--col-1-left);
  top: calc(var(--u) * 30);
  width: calc(var(--u) * 200);
  height: calc(var(--u) * 28);
}

.hero__logo img {
  display: block;
  width: 100%;
  height: 100%;
}

.hero__links {
  position: absolute;
  z-index: 7;
  left: calc(var(--u) * 965);
  top: calc(var(--u) * 26);
  width: calc(var(--u) * 729);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  font-size: calc(var(--u) * 24);
  font-weight: 400;
  line-height: 1;
  color: var(--beige-80);
  white-space: nowrap;
}

.hero__links a {
  position: relative;
  display: block;
  transition: color 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

.hero__links a:hover {
  color: var(--beige-100);
}

.hero__link-text {
  display: inline-block;
}

/* matches olgaprudka.com's own nav-link underline (read straight from
   their live stylesheet 2026-08-07, .link::after on their Projects/About/
   Contact) — hidden at rest, draws in left-to-right on hover, retracts
   back to the right on mouseleave. 0.5s, cubic-bezier(0.215, 0.61, 0.355,
   1) — NOT this project's usual signature curve, deliberately, per her
   explicit "as on the reference" request.

   Track+line split (same as .hero__btn-line-track/.hero__btn-line): the
   TRACK holds the fixed height:1px and never gets a transform; only the
   inner LINE (inset:0 of the track) gets scaleX animated. Putting the
   scaleX transform directly on a height:1px element (the previous,
   single-span version) made the browser re-rasterize that hairline at
   whatever fractional width the transition was mid-flight at, which
   visibly jittered the line's THICKNESS while it animated — same family
   of sub-pixel/dpr issue as the project's other hairline-vs-transform
   gotchas (BLOCK_STARTER_KIT.md's hover-scale counter-scale case), just
   triggered by scaleX instead of the whole card scaling. The wrapping
   track is never transformed, so its 1px height is never subject to that
   rounding at all. */
.hero__link-line-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--u) * -6);
  height: 1px;
  overflow: hidden;
}

.hero__link-line {
  position: absolute;
  inset: 0;
  background: var(--beige-100);
  transform-origin: right;
  transform: scaleX(0);
  transition: transform 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
}

.hero__link:hover .hero__link-line {
  transform-origin: left;
  transform: scaleX(1);
}

.hero__menu-btn {
  position: absolute;
  z-index: 7;
  left: calc(var(--u) * 1807);
  top: calc(var(--u) * 26);
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  background: none;
  border: none;
  cursor: pointer;
  font-size: calc(var(--u) * 24);
  font-weight: 400;
  line-height: 1;
  color: var(--beige-80);
  white-space: nowrap;
}

.hero__menu-label {
  display: block;
}

.hero__menu-line-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--u) * -6);
  height: 1px;
  overflow: hidden;
}

.hero__menu-line {
  position: absolute;
  inset: 0;
  background: var(--beige-100);
}

/* --- vase scene --- */

.hero__vase-scene {
  position: absolute;
  z-index: 2;
  inset: 0;
  pointer-events: none;
}

.hero__table {
  position: absolute;
  left: calc(var(--u) * 835);
  top: calc(var(--u) * 895);
  width: calc(var(--u) * 809);
  height: calc(var(--u) * 186);
  object-fit: cover;
}

.hero__vase-intro {
  position: absolute;
  left: calc(var(--u) * 856);
  top: calc(var(--u) * 150);
  width: calc(var(--u) * 654);
  height: calc(var(--u) * 907);
  transform-origin: 50% 100%;
}

.hero__vase {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  will-change: transform;
}

/* --- left content: eyebrow + H1 --- */

.hero__content {
  position: absolute;
  z-index: 3;
  left: var(--col-1-left);
  /* 305u base position from an earlier pass, moved down 12u this pass */
  top: calc(var(--u) * 317);
  width: calc(var(--u) * 1087);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.hero__title-wrap {
  position: relative;
  width: 100%;
}

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

/* top edge matches the top edge of hero-title.png exactly (both flush at
   the title-wrap's own top) — this is the Pixel Perfect stage value; the
   eyebrow's short width and the image's right-aligned "ЗВ'ЯЗОК" keep them
   from ever overlapping horizontally */
.hero__eyebrow {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 6);
  display: block;
  margin: 0;
  font-size: calc(var(--u) * 30);
  font-weight: 400;
  letter-spacing: calc(var(--u) * -0.6);
  text-transform: none;
  color: var(--beige-100);
  white-space: nowrap;
}

/* split into .word spans by splitWords() — .italic-symbol keeps its own
   gap to the word since inline-flex/gap no longer applies per word */
.hero__eyebrow .italic-symbol {
  margin-right: 10px;
}

.hero__eyebrow .word {
  display: inline-block;
  will-change: opacity;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- description + CTA --- */

/* verified against Figma node 1:904 (get_metadata + get_design_context):
   x:1423 y:654 width:446, font-size 48, Helvetica Neue Regular (400),
   leading-none (line-height:1). Figma's own markup joins "відроджуємо" +
   "традиції майстрів" as one space-separated run that wraps naturally at
   this width, then a hard <br> before "у цифрову епоху" — which carries
   no color override and simply inherits the paragraph's own
   rgba(237,233,221,0.6), rather than being its own line span. */
.hero__description {
  position: absolute;
  z-index: 3;
  left: calc(var(--u) * 1423);
  top: calc(var(--u) * 654);
  width: calc(var(--u) * 446) !important;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif !important;
  font-size: calc(var(--u) * 48) !important;
  font-weight: 400 !important;
  line-height: 1.0 !important;
  color: rgba(237, 233, 221, 0.60);
}

.hero__description-line--1 { color: #EDE9DD !important; }
.hero__description-line--2 { color: rgba(237, 233, 221, 0.80) !important; }

.hero__btn {
  position: absolute;
  z-index: 7;
  left: calc(50% + var(--u) * 598.5);
  top: calc(var(--u) * 972);
  /* centering handled via GSAP xPercent: -50 (script.js) so entrance
     tweens on `y`/`opacity` never clobber the transform */
  display: inline-flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  text-decoration: none;
  color: var(--beige-100);
  font-size: calc(var(--u) * 36);
  line-height: 1.1;
}

body.is-preloading .hero__btn {
  pointer-events: none;
}

.hero__btn-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--u) * 32);
  line-height: 1;
  color: var(--blue);
  overflow: hidden;
  height: 1em;
}

.hero__btn-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.hero__btn-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.hero__btn-plus-char--out {
  transform: translateY(0%);
}

.hero__btn-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .hero__btn-plus-char--out {
  transform: translateY(-100%);
}

.js-line-hover:hover .hero__btn-plus-char--in {
  transform: translateY(0%);
}

.hero__btn-line-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--u) * -8);
  height: 1px;
  overflow: hidden;
}

.hero__btn-line {
  position: absolute;
  inset: 0;
  background: var(--beige-100);
}

/* --- scroll-down arrow --- */

/* shares .hero__btn's top + line-box height so the arrow centers on the
   exact same horizontal band as the "обрати курс +" text */
/* left-anchored, NOT `right: var(--grid-margin)`. `--u` derives from 100vw,
   which includes the scrollbar, while a `right` offset resolves against the
   containing block, which does not — so a right-anchored 50u margin landed
   the arrow at 1855u while every left-positioned element on the site lines
   up on the working field's 1870u edge. Anchoring from the left instead
   (1870u - the arrow's own 16u width) puts it flush with that edge and
   keeps it there whether or not a scrollbar is present.

   top is 988u, not the 972u of .hero__btn: the two boxes are the same
   height and were already flush, but the 24u arrow glyph sits centred in
   its 40u box, so the VISIBLE arrow ended 16u above the button's
   underline. +16u drops the glyph's baseline onto that same line (the bob
   keyframe then lifts it from there). */
.hero__scroll {
  position: absolute;
  z-index: 7;
  left: calc(var(--u) * 1854);
  top: calc(var(--u) * 988);
  height: calc(var(--u) * 36 * 1.1);
  display: inline-flex;
  align-items: center;
  color: var(--beige-100);
}

.hero__scroll-arrow {
  width: calc(var(--u) * 16);
  height: calc(var(--u) * 24);
}

.hero__scroll-tail {
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: calc(var(--u) * 1.4);
  height: calc(var(--u) * 16);
  background: currentColor;
  transform-origin: bottom;
  transform: translateX(-50%) scaleY(0);
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) {
  .hero__scroll-tail {
    transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
  }
  .hero__scroll:hover .hero__scroll-tail {
    transform: translateX(-50%) scaleY(1);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .hero__scroll-arrow {
    animation: scrollArrowBob 1.8s ease-in-out infinite;
  }
}

/* bobs UP, not down. The arrow's resting position is aligned with the CTA's
   underline, so a downward bob spent half its cycle sitting below that line
   and read as "the arrow dropped". Lifting instead keeps the aligned
   position as the lowest point it ever reaches. */
@keyframes scrollArrowBob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(calc(var(--u) * -6)); }
}

/* ==========================================================================
   HERO — MOBILE (Figma node 978:863, file 6tA1k2sLpFm5U7cLn9p9rZ, 375px
   reference, plain px per the mobile-phase convention — see
   BLOCK_STARTER_KIT.md's "Mobile version"). Background, vase photo, H1
   title (5 separately-exported line images, each pre-blurred in Figma so
   no CSS blur is needed), logo, eyebrow, description and CTA all reuse
   the existing desktop markup/classes (`.hero__eyebrow`,
   `.hero__description*`, `.hero__btn*`) — only position/size are
   overridden here, no new elements needed since those are real text, not
   exported images. Nav links + decorative labels/ghost-text stay hidden
   (not present in the mobile macket). Still missing: scroll arrow.

   Mobile assets (mob-*.png/svg in img/) are exported at 3x, i.e. dividing
   their natural pixel size by 3 reproduces the Figma frame's own px value
   exactly (confirmed: mob-vase.png 942x2481 / 3 = 314x827, matching node
   997:1780's own x:61 y:-13 w:314 h:827) — so all the px values below are
   taken straight from Figma metadata, not eyeballed.

   Scoped to the single existing 768px breakpoint for now (matching the
   menu overlay's own precedent) rather than splitting 480/768, since only
   the 375px phone frame + assets exist so far. Revisit once she supplies a
   768px tablet frame for Hero specifically. */

.hero__bg-mobile,
.hero__vase-mobile,
.hero__title-mobile {
  display: none;
}

/* ".hero__logo img" (a class+type selector, specificity 0,1,1) already
   sets display:block on any img inside .hero__logo — a plain single-class
   ".hero__logo-img--mobile { display:none }" (specificity 0,1,0) loses to
   it regardless of source order, so both logos would render stacked even
   on desktop. Matching it as a descendant of .hero__logo (0,2,0) instead
   wins cleanly. */
.hero__logo .hero__logo-img--mobile {
  display: none;
}

@media (max-width: 768px) {
  .hero {
    aspect-ratio: unset;
    height: 812px;
  }

  /* not in the mobile macket at all — decorative desktop-only elements */
  .hero__label,
  .hero__ghost-img,
  .hero__links,
  .hero__vase-scene,
  .hero__scroll {
    display: none;
  }

  /* .hero__content wraps the eyebrow (real text, kept) + .hero__title-img
     (desktop PNG, replaced by .hero__title-mobile above) */
  .hero__content {
    position: absolute;
    z-index: 3;
    left: 0;
    top: 0;
    width: 100%;
  }

  .hero__title-img {
    display: none;
  }

  .hero__eyebrow {
    position: absolute;
    left: 20px;
    top: 183px;
    font-size: 16px;
    font-weight: 400;
    letter-spacing: -0.32px;
    line-height: 1.1;
    color: var(--beige-100);
    white-space: nowrap;
  }

  /* Figma text-[24px] leading-[1.1], x:20 y:591 w:225 — desktop rule uses
     !important on width/font-size/font-weight/line-height (guards against
     a global reset elsewhere), so the override needs it too or it loses
     despite coming later in the cascade. */
  .hero__description {
    display: block;
    left: 20px;
    top: 591px;
    width: 225px !important;
    font-size: 24px !important;
    font-weight: 400 !important;
    line-height: 1.1 !important;
  }

  /* Figma: PrimaryBtn left:calc(50%-77px) + translate-x(-50%), top:704px,
     text-[24px], gap-[28px] — both label and plus icon share the same
     24px (no desktop-style 32/36 downscale on the plus glyph). Centered
     with a plain CSS transform since no mobile entrance timeline exists
     yet (desktop centers via GSAP xPercent so an entrance tween on x/y
     never clobbers it — not needed until this button gets mobile motion). */
  /* left:20px, NOT centered — matches metadata (symbol "primary-btn" x:20
     y:704 w:181 h:28), same left edge as the eyebrow/H1/description
     column. The React/Tailwind reference code's "-translate-x-1/2 +
     left-calc(50%-77px)" combo reads like centering but isn't ground
     truth; get_metadata's raw x/y is. */
  .hero__btn {
    display: inline-flex;
    left: 20px;
    top: 704px;
    gap: 28px;
    font-size: 24px;
  }

  .hero__btn-plus {
    font-size: 24px;
  }

  /* base rule is bottom:calc(var(--u)*-8) — at mobile widths --u shrinks
     to ~0.2 (it's still the 1920-reference fluid unit, unaffected by the
     "fixed px, not --u" mobile convention unless a block explicitly
     overrides it), so the line was landing ~1.5px under the text instead
     of a real gap. Fixed px here, sized off the same ratio desktop uses
     between its own gap and font-size (~8u under a 36u CTA, ≈0.22) so it
     reads consistently with the rest of the project instead of just
     reusing the literal (broken) desktop value. */
  .hero__btn-line-track {
    bottom: -6px;
  }

  .hero__bg {
    display: none;
  }

  .hero__bg-mobile {
    display: block;
    position: absolute;
    z-index: 0;
    left: 0;
    top: -51px;
    width: 375px;
    height: 864px;
    object-fit: cover;
  }

  .hero__vase-mobile {
    display: block;
    position: absolute;
    z-index: 2;
    left: 61px;
    top: -13px;
    width: 314px;
    height: 827px;
    object-fit: cover;
    pointer-events: none;
  }

  /* logo: fixed 150x21 box (Figma header frame, x:20 y:22), same <a> as
     desktop — only the image inside swaps */
  .hero__logo {
    left: 20px;
    top: 22px;
    width: 150px;
    height: 21px;
  }

  .hero__logo .hero__logo-img--desktop {
    display: none;
  }

  .hero__logo .hero__logo-img--mobile {
    display: block;
    width: 100%;
    height: 100%;
  }

  /* H1: 5 separately-exported line images (Figma "Frame 2147229672",
     x:20 y:219 w:320, rows at y 0/49/98/147/196 i.e. 45px row + 4px gap —
     stacked with the same flex gap rather than each row's absolute y,
     since the exported PNGs are cropped tightly to their (blurred) glyph
     bounds, not a fixed 45px line box, and would drift out of alignment
     if positioned against the designed row centers instead.

     Widths are natural-px / 2, NOT / 3 — corrected after she flagged the
     H1 rendering too small. The vase/bg assets really are 3x (942/314 and
     1125/375 both land on an exact 3.0), but these 5 PNGs export at 2x:
     dividing by 2 lands within a few px of Figma's own designed row width
     (line1 "зв'язок" -> 219.5 vs metadata's 211; line4 "мистецтво" -> 309
     vs 305), the small remainder being blur bleed (largest on line3
     "через", which also has the biggest designed blur, 4px vs 2.5px). */
  .hero__title-mobile {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    position: absolute;
    z-index: 3;
    left: 20px;
    top: 219px;
    width: 320px;
    pointer-events: none;
  }

  .hero__title-mobile img {
    display: block;
    height: auto;
  }

  .hero__title-mobile img:nth-child(1) { width: 219.5px; }
  .hero__title-mobile img:nth-child(2) { width: 258px; }
  .hero__title-mobile img:nth-child(3) { width: 166.5px; }
  .hero__title-mobile img:nth-child(4) { width: 309px; }
  .hero__title-mobile img:nth-child(5) { width: 326.5px; }

  /* line-spacing halved per her request. The old flex gap:4px (Figma's
     own designed row gap) wasn't actually what read as "the gap" — each
     PNG also carries its own blur-bleed padding above/below the glyph
     (measured via the same alpha-bbox canvas scan as the "мова глини"
     fix), so the real glyph-to-glyph whitespace at gap:4 ranged ~10.5 to
     15.5px across the 5 rows, not a flat 4. Halving each row's OWN
     measured gap (not just retargeting the flex gap value) needed a
     different margin per row since bleed differs per image (biggest on
     line 4 "мистецтво", whose own designed blur/height is also the
     largest) — switched to gap:0 + explicit per-row margin-top. */
  .hero__title-mobile img:nth-child(2) { margin-top: -1.25px; }
  .hero__title-mobile img:nth-child(3) { margin-top: -2.75px; }
  .hero__title-mobile img:nth-child(4) { margin-top: -3.75px; }
  .hero__title-mobile img:nth-child(5) { margin-top: -2.5px; }
}

/* ==========================================================================
   SCROLL-UP BUTTON — shared component (not block-prefixed on purpose, it's
   reused identically across 6 different sections: .gallery/.steps/.stories/
   .pricing/.social/.footer — the ones her anchor nav links point to, plus
   the footer). Same visual recipe as .hero__scroll, arrow mirrored to point
   up. left:1854u reused verbatim (same scrollbar-safety reasoning as
   .hero__scroll's own comment — it doesn't depend on section height/width,
   only the reference viewport, so every host section can share one value).
   bottom-anchored rather than top, since these 6 sections are all different
   heights — bottom:50u (the project's standard margin unit, close to
   .hero__scroll's own ~52u gap from Hero's bottom edge) keeps it in the
   same visual corner regardless. One shared dark color (#2B3D2D) since all
   six host sections share data-cursor-theme="light" (light bg/dark text),
   unlike Hero itself.
   ========================================================================== */

.scroll-up {
  position: absolute;
  z-index: 7;
  left: calc(var(--u) * 1854);
  bottom: calc(var(--u) * 50);
  height: calc(var(--u) * 36 * 1.1);
  display: inline-flex;
  align-items: center;
  color: #2B3D2D;
}

.scroll-up-arrow {
  width: calc(var(--u) * 16);
  height: calc(var(--u) * 24);
}

/* below the arrow (not above, like .hero__scroll-tail) — the tail extends
   the shaft backward from the arrowhead, and this arrow points up */
.scroll-up-tail {
  position: absolute;
  left: 50%;
  top: 100%;
  width: calc(var(--u) * 1.4);
  height: calc(var(--u) * 16);
  background: currentColor;
  transform-origin: top;
  transform: translateX(-50%) scaleY(0);
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) {
  .scroll-up-tail {
    transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
  }
  .scroll-up:hover .scroll-up-tail {
    transform: translateX(-50%) scaleY(1);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .scroll-up-arrow {
    animation: scrollArrowBob 1.8s ease-in-out infinite;
  }
}

/* ==========================================================================
   LINE-HOVER MICRO-INTERACTION
   Idle line sits fully visible. On hover it collapses toward its right
   edge, then redraws from its left edge, in one sequential sweep. Applied
   to the menu button underline and the CTA button underline.
   ========================================================================== */

.hero__menu-line--out,
.hero__btn-line--out {
  transform-origin: 0 0;
  transform: scaleX(1);
}

@keyframes heroLineSweep {
  0%, 0.1%   { transform-origin: 100% 100%; transform: scaleX(1); }
  49.9%      { transform-origin: 100% 100%; transform: scaleX(0); }
  50%, 50.1% { transform-origin: 0 0;       transform: scaleX(0); }
  100%       { transform-origin: 0 0;       transform: scaleX(1); }
}

@media (hover: hover) and (pointer: fine) {
  .js-line-hover:hover .hero__menu-line--out,
  .js-line-hover:hover .hero__btn-line--out {
    animation: heroLineSweep 1.1s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  }
}

@media (prefers-reduced-motion: reduce) {
  .js-line-hover:hover .hero__menu-line--out,
  .js-line-hover:hover .hero__btn-line--out {
    animation: none;
  }
}

/* ==========================================================================
   SECTION INNER — scroll-lag carrier
   One per section (except .hero, which keeps its own arrangement for
   stacking-context reasons). The scroll-lag drift animates `y` on THIS
   element while each section's entrance timeline animates `y` on the
   individual children — two separate elements, so the two never overwrite
   each other's transform. inset:0 makes it exactly co-extensive with its
   section, so every child's absolute left/top keeps resolving against the
   same box it did before.
   ========================================================================== */

.section-inner {
  position: absolute;
  inset: 0;
}

/* ==========================================================================
   CRAFT SECTION — "2_мистецтво" (Figma node 978:1223, file
   6tA1k2sLpFm5U7cLn9p9rZ). Same fluid --u system + position:relative /
   absolutely-positioned-children structure as .hero. Entrance is
   ScrollTrigger-driven (script.js: cacheCraftRefs/setInitialCraftStates/
   runCraftEntrance) — plain fade/rise only, no pin/scrub/curtain between
   sections. As with .hero, the pre-reveal hidden states (opacity 0 etc.)
   are set entirely via GSAP on load, not authored here in CSS.
   ========================================================================== */

.craft {
  position: relative;
  width: 100%;
  aspect-ratio: 1920 / 1224;
  overflow: hidden;
  margin: 0;
  background-color: var(--beige-100);
  color: #2B3D2D;
}

/* --- staggered 3-line heading; the visible lines are plain text, the
   real <h2> (index.html) carries the full sentence hidden, same split
   as .hero__title-img (visible) + its hidden <h1> --- */

.craft__heading-line {
  position: absolute;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 96);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -4.8);
  text-transform: uppercase;
  color: #2B3D2D;
  white-space: nowrap;
}

.craft__heading-line--1 { left: calc(var(--u) * 50); top: calc(var(--u) * 150); }
.craft__heading-line--2 { left: calc(var(--u) * 468); top: calc(var(--u) * 251); }
.craft__heading-line--3 { left: calc(var(--u) * 398); top: calc(var(--u) * 352); }

/* --- img-art-1: stacked close-up pair, only the first visible; the
   second sits ready at opacity 0 for a later hover-crossfade --- */

.craft__art-1 {
  position: absolute;
  left: calc(var(--u) * 980);
  /* +2px fixed offset: the heading shares this same 150u top, but its
     text has its own font-metric leading space above the visible glyph
     top, so without this the image reads as sitting slightly higher
     than the heading */
  top: calc(var(--u) * 150 + 2px);
  width: calc(var(--u) * 199);
  height: calc(var(--u) * 273);
  overflow: hidden;
}

/* height/top: oversized + centered so the scroll-linked yPercent:-15
   drift (script.js: initCraftImageParallax()) never exposes empty space
   at the frame's bottom edge — the drift only ever moves the image
   upward from its resting position, so the bottom buffer (25%) has to
   cover the full drift distance (15% of a 150%-tall image = 22.5% of
   the frame's own height), with a small margin to spare. */
.craft__art-1-img {
  position: absolute;
  left: 0;
  top: -25%;
  width: 100%;
  height: 150%;
  object-fit: cover;
}

.craft__art-1-img--b {
  opacity: 0;
}

/* --- body copy --- */

.craft__body {
  position: absolute;
  left: calc(50% - var(--u) * 492);
  top: calc(var(--u) * 494);
  width: calc(var(--u) * 359);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 36);
  font-weight: 400;
  line-height: 1;
  color: #2B3D2D;
}

/* --- "free вебинар +" link: same text+plus layout convention as
   .hero__btn, including its plus-swap hover (minus the line-sweep —
   the underline here is a static border-bottom by design, not the
   line-sweep mechanism). Its Figma left value is the widget's
   horizontal CENTER, not its left edge (node 978:1230: x:470.5 width:270
   → center 605.5, exactly 50% - 354.5u) — same center-anchor +
   translateX(-50%) trick .hero__btn uses via GSAP xPercent:-50, done in
   plain CSS here since this element's own position never animates. --- */

.craft__cta {
  position: absolute;
  left: calc(50% - var(--u) * 354.5);
  top: calc(var(--u) * 894);
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding-bottom: calc(var(--u) * 8);
  border-bottom: 1px solid #2B3D2D;
  color: #2B3D2D;
  text-decoration: none;
  font-family: 'Helvetica Neue', sans-serif;
}

.craft__cta-text {
  font-size: calc(var(--u) * 36);
  font-weight: 400;
  line-height: 1;
}

.craft__cta-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--u) * 32);
  font-weight: 400;
  line-height: 1;
  overflow: hidden;
  height: 1em;
}

.craft__cta-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.craft__cta-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.craft__cta-plus-char--out {
  transform: translateY(0%);
}

.craft__cta-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .craft__cta-plus-char--out {
  transform: translateY(-100%);
}

.js-line-hover:hover .craft__cta-plus-char--in {
  transform: translateY(0%);
}

/* --- large ceramics photo --- */

.craft__art-2-frame {
  position: absolute;
  left: calc(var(--u) * 1423);
  top: calc(var(--u) * 494);
  width: calc(var(--u) * 422);
  height: calc(var(--u) * 580);
  overflow: hidden;
}

/* height/top: same oversized + centered buffer as .craft__art-1-img,
   for the same reason — covers the scroll-linked yPercent:-15 drift
   without exposing empty space at the frame's bottom edge. */
.craft__art-2 {
  position: absolute;
  left: 0;
  top: -25%;
  width: 100%;
  height: 150%;
  object-fit: cover;
  display: block;
}

/* --- img-art-1 / img-art-2: plain hover zoom (script.js:
   initCraftImageHover(), gated behind (hover:hover) and (pointer:fine)
   in script.js itself, not here) + a scroll-linked vertical drift
   (initCraftImageParallax()) on the INNER image only — the outer frame
   (.craft__art-1/.craft__art-2-frame) never scales or moves, it stays a
   fixed box and only clips the inner image via its own overflow:hidden
   (set above). No CSS transition on the inner images: both the hover
   scale and the scroll-drift are driven directly via GSAP inline styles
   every frame, and a competing CSS transition on the same property
   would fight it — same reasoning as .menu-overlay__close-icon-bar's
   own "transition: none". --- */

/* --- right-side three-tier text block, same multi-tier color-span
   convention as .hero__description (base color = lowest-opacity tier,
   line--1/--2 override for the higher tiers) --- */

.craft__side-text {
  position: absolute;
  left: calc(var(--u) * 1423);
  top: calc(var(--u) * 290);
  width: calc(var(--u) * 446);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 48);
  font-weight: 400;
  line-height: 1;
  color: rgba(43, 61, 45, 0.4);
}

.craft__side-text-line--1 { color: rgba(43, 61, 45, 0.8); }
.craft__side-text-line--2 { color: rgba(43, 61, 45, 0.6); }

/* ==========================================================================
   CRAFT — MOBILE (Figma node 978:898, file 6tA1k2sLpFm5U7cLn9p9rZ, 375px
   reference, plain px per the mobile-phase convention). Pixel Perfect
   pass only, no motion yet — matches how every desktop block was built
   (layout first, motion as its own separate pass). Verified against
   get_metadata's raw x/y/w/h, not the get_design_context React/Tailwind
   reference code, after the Hero pass showed that code's
   translate-x-1/2-style "centering" isn't reliable ground truth (the CTA
   button here uses the same pattern and is genuinely left-aligned at
   x:25, not centered — checked before trusting it this time). Two content
   pieces (.craft__heading-mobile, .craft__side-text-mobile) turned out to
   have real differences from their desktop counterparts — different line
   breaks and different color tiers, not just resized — so they're new
   elements rather than mobile overrides of the desktop ones; see each
   element's own HTML comment in index.html for specifics. .craft__body
   and .craft__cta reuse the desktop elements as-is (same content, same
   mechanism), just repositioned/resized. */

.craft__heading-mobile,
.craft__art-1-mobile,
.craft__art-2-mobile,
.craft__side-text-mobile {
  display: none;
}

@media (max-width: 768px) {
  /* 80px breathing room top and bottom, per her explicit revision — the
     raw Figma frame (1236 tall) had ~0px at the top and ~1px at the
     bottom. Implemented as +80 on every element's own top (not padding on
     .craft, which is absolutely-positioned children with no padding
     contribution) plus a matching +80 on the section's own height. */
  .craft {
    aspect-ratio: unset;
    height: 1355px;
  }

  .craft__heading-line,
  .craft__art-1,
  .craft__art-2-frame,
  .craft__side-text {
    display: none;
  }

  .craft__heading-mobile {
    display: block;
    position: absolute;
    z-index: 3;
    left: 9px;
    top: 80px;
    width: 343px;
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 500;
    font-size: 46px;
    line-height: 1;
    letter-spacing: -1.38px;
    text-align: right;
    text-transform: uppercase;
    color: #2B3D2D;
  }

  /* same source photo as the desktop hover-crossfade pair
     (craft-art-1a.png) — she hasn't supplied a separate mobile crop for
     this block yet, so this reuses it with object-fit:cover rather than
     the exact Figma export crop. Swap src if/when she sends one. */
  .craft__art-1-mobile {
    display: block;
    position: absolute;
    z-index: 2;
    left: 195px;
    top: 238px;
    width: 158px;
    height: 217px;
    object-fit: cover;
  }

  .craft__art-2-mobile {
    display: block;
    position: absolute;
    z-index: 2;
    left: 20px;
    top: 587px;
    width: 335px;
    height: 440px;
    object-fit: cover;
  }

  .craft__side-text-mobile {
    display: block;
    position: absolute;
    z-index: 3;
    left: 20px;
    top: 495px;
    width: 335px;
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    color: #2B3D2D;
  }

  .craft__side-text-mobile-line--1 { color: #2B3D2D; }
  .craft__side-text-mobile-line--2 { color: rgba(43, 61, 45, 0.8); }
  .craft__side-text-mobile-line--3 { color: rgba(43, 61, 45, 0.6); }

  /* reuses the desktop element/content as-is — Figma's mobile spec has
     the same "ви навчитеся..." copy, just a different color (0.8 here vs
     desktop's solid #2B3D2D) and position/size */
  .craft__body {
    left: 20px;
    top: 1067px;
    width: 233px;
    font-size: 20px;
    line-height: 1.06;
    letter-spacing: -0.4px;
    color: rgba(43, 61, 45, 0.8);
  }

  /* left:20px, matching .craft__body/.craft__art-2-mobile's own left
     edge — she asked for it explicitly (the raw Figma metadata actually
     reads x:25 here, 5px off that column; overridden per her direct
     instruction rather than the macket). NOT centered either way —
     desktop's .craft__cta uses a center-anchor (left:50%-354.5u +
     transform:translateX(-50%)), so that transform has to be explicitly
     cancelled or it still applies on top of the new left value (same
     lesson as the Hero CTA). */
  /* top:1247px — shifted up 40px along with .craft__body when the
     image-to-body gap was tightened from 80 to 40, so the body-to-cta gap
     (53px) and the cta-to-section-bottom gap (80px, from item 2) both
     stay exactly what they were, rather than the 40px just reopening as
     extra dead space between body and cta. */
  /* padding-bottom 2px -> 6px: matches Figma's literal pb-[2px] but read
     as sitting too tight against the text — same fix/ratio as the Hero
     underline-tracks above (~0.22-0.25 of the 24px CTA font, matching
     desktop's own gap/font-size relationship project-wide). */
  .craft__cta {
    left: 20px;
    top: 1247px;
    transform: none;
    gap: 28px;
    padding-bottom: 6px;
  }

  .craft__cta-text {
    font-size: 24px;
  }

  .craft__cta-plus {
    font-size: 24px;
  }
}

/* ==========================================================================
   CLAY SECTION — "3_мова глини" (Figma node 978:1237, file
   6tA1k2sLpFm5U7cLn9p9rZ). Same fluid --u system as .hero/.craft.
   Pixel Perfect only for now — no entrance/scroll effects yet.
   ========================================================================== */

.clay {
  position: relative;
  width: 100%;
  aspect-ratio: 1920 / 1080;
  overflow: hidden;
  margin: 0;
  background-color: #c9beae;
  color: var(--beige-100);
}

/* Exactly the section's own size — no oversizing. Matches rejouice.com's
   own reel markup, whose video element is 100% of its container while the
   drift runs yPercent -50 -> +50 (script.js: initClayVideoParallax()).
   That pairing is self-covering: at every scroll position the half of the
   element left uncovered is the half that is off-screen anyway, so the
   visible part of the section is never bare. An earlier 200%-tall version
   here needed a smaller ±25% drift to stay covered, which halved the
   visible movement and was why the effect read as absent. */
.clay__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* mova-clay.png: pre-rendered title asset (blur is baked into the PNG by
   design, not a CSS effect — matches Figma exactly). The 1707×268 export
   has asymmetric transparent padding around the glyphs from the blur bleed
   (measured via alpha-bbox: content sits at x:[27,1682] y:[21,231] inside
   the full canvas) — sizing/positioning off the full image would misalign
   the glyphs from Figma's actual frame (978:1244: 846×117 at 50,883).
   Scaled + shifted so the glyph content itself (not the padded canvas)
   lands on the frame's left edge, matching .clay__word's left:50u so
   "МОВА" and "ваші" sit flush on the same line. */
.clay__title {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 36.2);
  top: calc(var(--u) * 872.3);
  width: calc(var(--u) * 872.6);
  height: auto;
  display: block;
  pointer-events: none;
}

/* --- "ваші руки оживляють у форму та наповнюють її історією": each word
   individually positioned per Figma (978:1247), not a flex/justify line —
   per her explicit instruction to match the exact absolute coordinates. --- */

.clay__word {
  position: absolute;
  z-index: 1;
  top: calc(var(--u) * 807);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1;
  color: var(--beige-80);
  white-space: nowrap;
}

.clay__word--vashi { left: calc(var(--u) * 50); }
.clay__word--ruky { left: calc(var(--u) * 141); }
.clay__word--ozhivlyayut { left: calc(var(--u) * 315); }
.clay__word--u { left: calc(var(--u) * 604); }
.clay__word--formu { left: calc(var(--u) * 672); }
.clay__word--ta { left: calc(var(--u) * 904); }
.clay__word--napovnyuyut { left: calc(var(--u) * 970); }
.clay__word--ii { left: calc(var(--u) * 1347); }
.clay__word--istoriyeyu { left: calc(var(--u) * 1424); }

.clay__caption {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 1424);
  top: calc(var(--u) * 876);
  width: calc(var(--u) * 446);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 0.98;
  color: #2B3D2D;
}

/* --- play/pause toggle: hit area is a centered circle (Figma "btn-next"
   978:3108's own 150×150), not the full video — per her request the
   cursor should only morph into the glass bubble near the middle of the
   block, not anywhere over the footage. FEEDBACK is the existing custom
   cursor morphing into its .cursor__media glass bubble (same
   olive-tinted blur used for the "далі" hover state elsewhere) — her
   call to make this "more interesting" than a permanently-visible static
   circle. script.js: initClayVideoToggle() keeps this element's
   data-cursor-label in sync with play state; initCustomCursor()'s
   existing .js-cursor-media wiring (previously unused anywhere on the
   site) reads it on hover. --- */
.clay__video-toggle {
  position: absolute;
  z-index: 2;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: calc(var(--u) * 150);
  height: calc(var(--u) * 150);
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: none;
  cursor: pointer;
}

/* ==========================================================================
   CLAY — MOBILE (Figma node 978:908, file 6tA1k2sLpFm5U7cLn9p9rZ, 375px
   reference, plain px per the mobile-phase convention). Pixel Perfect
   pass only, no motion/toggle button yet — the mobile macket shows no
   play/pause control, and its interaction (custom-cursor label morph on
   hover) doesn't translate to touch anyway, so it's left out rather than
   forced in. .clay__bg (the background video) needs no mobile override at
   all — it's already `inset:0; width/height:100%; object-fit:cover`,
   so it's full-bleed at any breakpoint as-is. Node coordinates in Figma
   were given relative to the whole page (y offset +2219px) since this
   node is a "contents" group, not a bounded frame in the exported
   React/Tailwind code — converted to section-local px using
   get_metadata's frame origin (0, 2219) before using them below. */

.clay__vignette-mobile,
.clay__body-mobile,
.clay__title-mobile {
  display: none;
}

@media (max-width: 768px) {
  .clay {
    aspect-ratio: unset;
    height: 812px;
  }

  .clay__title,
  .clay__word {
    display: none;
  }

  /* not shown in the mobile macket (no such node in 978:908) but she
     wants the play/pause cursor interaction kept on mobile too — reuses
     the exact same element/mechanism as desktop (js-cursor-media +
     data-cursor-label, initClayVideoToggle() in script.js), just a fixed
     px hit-circle instead of desktop's --u-scaled 150u (which would
     shrink to ~29px at mobile's --u and be too small to reliably hover). */
  .clay__video-toggle {
    width: 120px;
    height: 120px;
  }

  .clay__vignette-mobile {
    display: block;
    position: absolute;
    z-index: 0;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }

  .clay__body-mobile {
    display: block;
    position: absolute;
    z-index: 1;
    left: 20px;
    top: 50px;
    width: 335px;
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    text-align: justify;
    color: var(--beige-100);
  }

  /* reuses the desktop element/content as-is (same "як тисячолітнє..."
     copy already in index.html) — just repositioned/resized; color
     already matches (#2B3D2D on both). */
  .clay__caption {
    left: 21px;
    top: 552px;
    width: 269px;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
  }

  /* мова/глини: widths are natural-px / 2, same lesson as the Hero H1
     lines — a small overshoot past Figma's own 207/228px row widths is
     expected blur bleed (bigger on "мова", which also has the bigger
     designed blur: 7px vs "глини"'s 6px). Flex-stacked with the row's own
     rendered height (not a fixed absolute y per line) for the same
     reason the Hero title used a flex gap instead of hard-coded tops:
     the exported PNGs are cropped to their (blurred) glyph bounds, not a
     fixed line box.

     left:11px — 10px left of .clay__caption's own 21px, per her explicit
     follow-up request (the blurred glyph itself reads visually inset
     from its crop edge, so a literal edge-to-edge match with the caption
     still looked off-center to her). */
  .clay__title-mobile {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    position: absolute;
    z-index: 1;
    left: 11px;
    top: 632px;
    pointer-events: none;
  }

  .clay__title-mobile img {
    display: block;
    height: auto;
  }

  .clay__title-mobile img:nth-child(1) { width: 230px; }
  .clay__title-mobile img:nth-child(2) {
    width: 241px;
    /* measured both PNGs' real alpha bounding box (canvas/pixel scan, not
       eyeballed) rather than guessing: at gap:0 the visible whitespace
       between "мова"'s glyph-bottom and "глини"'s glyph-top was ~16px
       (each export has several px of transparent blur-bleed padding above
       and below the actual glyph, so a 0 flex-gap still leaves real dead
       space). Halved per her request -> pull this image up 8px. */
    margin-top: -8px;
  }
}

/* ==========================================================================
   MATERIAL SECTION — "4_глина це" (Figma node 978:1257, file
   6tA1k2sLpFm5U7cLn9p9rZ). Same fluid --u system as the other sections.
   Pixel Perfect only for now — no entrance/scroll effects yet.

   All three text layers share the same optical centre (960.5u) but are
   positioned with explicit left+width rather than translateX(-50%): these
   are all future GSAP targets, and a static CSS transform here would be
   wiped the moment a tween touches x/y — the same trap .hero__ghost-img
   already hit in this project.
   ========================================================================== */

/* Full viewport height — the block always fills whatever screen the site is
   opened on. svh rather than vh so collapsing mobile browser chrome doesn't
   resize the box mid-scroll.
   Because the height no longer scales with --u, the three text layers below
   are anchored to the section's CENTRE instead of fixed `top` values (their
   offsets are the original Figma distances from 993/2 = 496.5u), so the
   composition keeps its exact internal spacing at any window height.
   The empty margin this leaves around the text is intentional breathing
   room; what previously made it read as a void was .material and .gallery
   sharing one beige — the darker #DDD8C8 below now separates them. */
.material {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  margin: 0;
  /* a shade darker than --beige-100 (#EDE9DD), which the gallery below
     keeps — now that the divider line is gone, this is what separates the
     two beige blocks from each other */
  background-color: #DDD8C8;
  color: #2B3D2D;
  /* no bottom border: removed at her request once the gallery block landed
     below — both sections are beige, and the divider read as an unwanted
     seam rather than a boundary */
}

/* decorative oversized echo of .material__text sitting behind it. Figma's
   Layer blur 10 corresponds to CSS blur(5px) — the two use different
   scales, and 5px is what reproduces the mockup. */
.material__echo {
  position: absolute;
  z-index: 0;
  left: calc(var(--u) * 104);
  top: calc(50% - var(--u) * 289.02); /* Figma y 207.48, i.e. 289.02u above the 496.5u centre */
  width: calc(var(--u) * 1713);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 100);
  line-height: 0.98;
  letter-spacing: calc(var(--u) * -2);
  text-align: center;
  color: #2B3D2D;
  opacity: 0.1;
  filter: blur(5px);
  pointer-events: none;
}

.material__label {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 890);
  top: calc(50% - var(--u) * 298.5); /* Figma y 198 */
  width: calc(var(--u) * 141);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1;
  letter-spacing: calc(var(--u) * -0.6);
  text-align: center;
  color: rgba(43, 61, 45, 0.8);
}

/* literal px gap, matching the .italic-symbol convention used elsewhere */
.material__label .italic-symbol:first-child { margin-right: 10px; }
.material__label .italic-symbol:last-child { margin-left: 10px; }

.material__text {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 280);
  top: calc(50% - var(--u) * 228.5); /* Figma y 268 */
  width: calc(var(--u) * 1361);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 80);
  line-height: 0.98;
  letter-spacing: calc(var(--u) * -1.6);
  text-align: center;
  color: #2B3D2D;
}

/* same descending-opacity tiers as .craft__side-text / .hero__description */
.material__tier-2 { color: rgba(43, 61, 45, 0.8); }
.material__tier-3 { color: rgba(43, 61, 45, 0.6); }

/* splitWordsDeep() wraps every word in these; inline-block is required or
   the echo's horizontal scatter (a transform) would not apply at all */
.material__echo .word,
.material__text .word {
  display: inline-block;
  will-change: opacity, transform;
}

/* ==========================================================================
   MATERIAL — MOBILE (Figma node 978:919, file 6tA1k2sLpFm5U7cLn9p9rZ, 375px
   reference, plain px per the mobile-phase convention). Checked carefully
   before assuming reuse (per her explicit "be careful" — craft/clay both
   turned out to have real content/color differences from desktop): this
   block's content is a genuine exact match — same three elements
   (.material__label "( глина )", .material__text, .material__echo), same
   sentence, same tier-2/tier-3 (0.8/0.6) color split at the same word
   boundaries, verified span-by-span against get_design_context's output,
   not assumed from the screenshot alone. So this is a pure CSS override
   of the existing desktop elements, no new markup, and it inherits the
   desktop entrance (runMaterialEntrance()) for free — no JS changes
   needed either.

   .material itself needs no override: `height:100vh/100svh` (no
   `aspect-ratio`/`--u` at the container level) already adapts to any
   viewport, mobile included.

   left values are literal px, NOT left:50%+transform:translateX(-50%),
   for .material__text specifically: setInitialMaterialStates() does
   `gsap.set(materialText, { y: 12 })` directly on this element (not just
   its words) — GSAP compiles y into an inline `transform`, which would
   silently wipe out a CSS translateX(-50%) the instant that gsap.set()
   runs (inline styles always beat stylesheet ones), same class of bug as
   the Hero/Craft CTAs. Sidestepped by hand-computing each element's
   centered left offset instead of centering via transform — convenient
   since Figma's own raw x already IS that computed value almost exactly
   (e.g. .material__text: (375-326)/2 = 24.5 ≈ metadata's x:25).
   .material__label has no such conflict (only its child .word spans get
   a GSAP tween, never the label element itself), so it safely keeps the
   left:50%/transform centering trick. */

@media (max-width: 768px) {
  .material__echo {
    left: -23px;
    top: 158px;
    width: 420px;
    font-size: 42px;
    line-height: 0.98;
    letter-spacing: -0.84px;
    filter: blur(2.5px);
  }

  .material__text {
    left: 25px;
    top: 206px;
    width: 326px;
    font-size: 32px;
    line-height: 1.1;
    letter-spacing: -0.64px;
  }

  .material__label {
    left: 50%;
    top: 150px;
    width: auto;
    transform: translateX(-50%);
    font-size: 16px;
    letter-spacing: -0.32px;
  }
}

/* ==========================================================================
   GALLERY SECTION — "5_галерея робіт" (Figma node 978:1261, file
   6tA1k2sLpFm5U7cLn9p9rZ). Same fluid --u system as the other sections.
   Pixel Perfect only — no entrance/carousel behaviour yet.
   ========================================================================== */

/* 1240u, not Figma's frame height of 1230: the olive strip sits at y1194
   with a height of 46, so it ends at 1240 and overflowed the frame by 10u
   in the mockup itself.

   Height is set in `--u` rather than via `aspect-ratio` on purpose. `--u`
   is derived from 100vw, which INCLUDES the scrollbar, while aspect-ratio
   resolves against the element's own content width, which does not — so an
   aspect-ratio box ends up ~10px shorter than the 1240u coordinate space
   its children are positioned in, and the bottom band gets clipped by
   exactly the scrollbar's width. Sizing the box in the same unit as its
   contents removes the mismatch. */
.gallery {
  position: relative;
  width: 100%;
  height: calc(var(--u) * 1153);
  overflow: hidden;
  margin: 0;
  background-color: var(--beige-100);
  color: #2B3D2D;
}

/* --- heading group (Figma frame "H" at 51,237; its children are placed
   here in absolute section coordinates rather than inside a wrapper, so
   nothing needs a transform that a later GSAP tween could clobber) --- */

.gallery__eyebrow {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 51);
  top: calc(var(--u) * 150);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.5;
  color: #2B3D2D;
  white-space: nowrap;
}

/* u-based, not the 10px literal the Hero eyebrow uses: Figma builds this
   gap out of typed spaces at mixed sizes (one 30px + two 18px), so it
   scales with the type rather than being a fixed pixel inset */
.gallery__eyebrow .italic-symbol {
  margin-right: calc(var(--u) * 18.3);
}

.gallery__title {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 508); /* Figma: frame x51 + text x457 */
  top: calc(var(--u) * 150);
  width: calc(var(--u) * 685);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 500;
  font-size: calc(var(--u) * 96);
  line-height: 1;
  letter-spacing: calc(var(--u) * -1.92);
  text-transform: uppercase;
}

.gallery__title-a { color: var(--olive-100); }
.gallery__title-b { color: var(--olive-80); }

.gallery__lead {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 508);
  top: calc(var(--u) * 382); /* Figma frame y237 + text y232, shifted -87 */
  width: calc(var(--u) * 294);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.24);
  color: #2B3D2D;
  opacity: 0.8;
}

/* --- category filter --- */

/* gap 35.05u, not Figma's 40u: "Декор" has to line up with the left edge of
   the "дозвольте цим образам" note (965u) while the two gaps stay equal,
   and "Ліплення" has to start on the title/lead column (508u). Solved from
   the items' MEASURED widths (145 / 242 / 97.2), which come out a few units
   narrower than Figma reports: 508 + 145 + 242 + 2*35.05 = 965. The earlier
   32.4 was solved against stale widths and left Декор 6.3u short. */
.gallery__filter {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 508);
  top: calc(var(--u) * 586);
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 35.05);
}

.gallery__filter-item {
  position: relative;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  font-family: 'Helvetica Neue', sans-serif;
  /* Roman (400), not the mockup's Medium (500) — her call */
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.9);
  text-transform: uppercase;
  /* all three share the brand green now — the mockup dimmed the middle one,
     but that dimming turned out to mean "selected", which is expressed by
     .is-active below instead of being baked into a position */
  color: #2B3D2D;
  white-space: nowrap;
  cursor: pointer;
}

.gallery__filter-text {
  display: inline-block;
}

/* selected state — the 60% she specified */
.gallery__filter-item.is-active {
  opacity: 0.6;
}

/* EXACT same LOCKED recipe as .hero__link-line-track/.hero__link-line and
   .footer__menu-link-line-track/.footer__menu-link-line — see
   feedback_nav_underline_settings_locked.md. Track (fixed height:1px,
   never transformed) + inner line (gets the scaleX transform) so the
   hairline's thickness never jitters mid-transition; hidden at rest,
   draws in left-to-right on hover, retracts back to the right on leave;
   olgaprudka.com's own values (0.5s, cubic-bezier(0.215, 0.61, 0.355, 1)),
   not this project's usual signature curve — deliberate. */
.gallery__filter-line-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--u) * -6);
  height: 1px;
  overflow: hidden;
}

.gallery__filter-line {
  position: absolute;
  inset: 0;
  background: #2B3D2D;
  transform-origin: right;
  transform: scaleX(0);
  transition: transform 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
}

.gallery__filter-item:hover .gallery__filter-line {
  transform-origin: left;
  transform: scaleX(1);
}

/* --- olive strip the captions sit on. Sits at z-index 0 so the cards'
   captions (z-index 1) read on top of it rather than being covered --- */

.gallery__bar {
  isolation: isolate;
  position: absolute;
  z-index: 0;
  left: 0;
  top: calc(var(--u) * 1107);
  width: 100%;
  height: calc(var(--u) * 46);
  background-color: var(--olive-100);
}

/* Same film grain .philosophy carries — see the note above .philosophy::before.
   This strip and block 6 are both --olive-100 and meet at the section
   boundary, so a grained olive against a flat one put a visible seam there.
   `overlay` against the mid-grey tile also lifts the base by ~0.4 levels, so
   matching the texture matters for the mean colour as well as the noise.
   `isolation: isolate` above keeps the blend inside this strip. */
.gallery__bar::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grain, none);
  background-repeat: repeat;
  mix-blend-mode: overlay;
}

/* --- horizontal carousel track. Starts at -867u and runs 3654u wide (the
   original 6 real cards' -409u/2738u, both grown by one card-pitch, 458u,
   in each direction) so scrolling to either end stop reveals a looping
   clone card instead of empty background — her "по колу" spec, 2026-08-04.
   The original -409u/2738u left the LAST 49u of GALLERY_TRAVEL's ±458u
   scrub exposing raw page background at both extremes (measured: the real
   cards' 818u of overhang was 49u short of the 916u the scrub travels
   through). One prepended clone of card 6 and one appended clone of card 1
   (both aria-hidden, tabindex="-1" — decorative repeats, not second real
   controls) fill exactly that shortfall with real content, with margin to
   spare (409u of overhang left at each extreme instead of -49u). Because
   the shift is symmetric (+458u prepended is compensated by -458u on
   `left`), the 6 real cards' on-screen positions — and the note panel's
   card-pitch alignment with them — are completely unchanged; only what's
   visible past the old edges changed. .gallery's own overflow:hidden crops
   the whole thing to the mockup's framing. --- */

.gallery__track {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * -867);
  top: calc(var(--u) * 659);
  width: calc(var(--u) * 3654);
  height: calc(var(--u) * 494);
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 10);
}

.gallery__card {
  flex: 0 0 auto;
  width: calc(var(--u) * 448);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 10);
  /* Figma's component carries pb-10; content ends at 484u and the instance
     measures 494u. Invisible on its own, but it keeps the card box matching
     the mockup for whatever the motion pass hangs off it. */
  padding-bottom: calc(var(--u) * 10);
  margin: 0;
}

/* a <button> (each work opens a lightbox) — so it needs the control reset
   before it can behave like the frame it used to be */
.gallery__photo {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
}

/* +1px, and bottom:auto so the height wins over the inset: at scale 1 the
   image and its frame are the same fractional size (a 448u card lands on
   something like 203.2px), and the rounding could leave a hairline of the
   beige page showing along the bottom edge as the hover scale settled back.
   One spare pixel removes it; the crop shift is invisible. */
.gallery__photo img {
  position: absolute;
  top: 0;
  right: 0;
  bottom: auto;
  left: 0;
  width: 100%;
  height: calc(100% + 1px);
  object-fit: cover;
  display: block;
}

/* the first photo is cropped lower than centre in Figma (the export sits
   at top:-61.89% inside a 177.82%-tall box); 79.5% is that same slice
   expressed as an object-position, so the cup stays framed as designed */
/* --first is the real card 1 (no longer :first-child in the DOM now that a
   loop clone is prepended before it — see .gallery__track's comment);
   --clone-first is that same card's own clone at the end of the track.
   Same crop for both, since --clone-first is a literal copy of card 1. */
.gallery__card--first .gallery__photo img,
.gallery__card--clone-first .gallery__photo img {
  object-position: center 79.5%;
}

.gallery__caption {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  color: var(--beige-100);
}

.gallery__marks {
  position: absolute;
  z-index: 1;
  right: calc(var(--u) * 10);
  top: calc(var(--u) * 10);
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 10);
  color: #2B3D2D;
}

/* the typographic "+" the CTA buttons use, not the old design's SVG glyph
   (four stroked segments with a gap in the middle). 28u is chosen so the
   rendered character reads at roughly the 16u the SVG box occupied. */
.gallery__mark {
  display: block;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 28);
  line-height: 1;
}

/* --- the "txt" card sitting between the 3rd and 4th work --- */

/* Absolutely placed rather than a flex item in .gallery__track, because the
   track now scrubs horizontally (initGalleryCarousel) and this note must
   not travel with it. It occupies the same 448u slot it used to hold in the
   row, at the same 659u the cards start on, and sits above the track so the
   cards pass behind it. */
.gallery__note {
  position: absolute;
  z-index: 2;
  left: calc(var(--u) * 965);
  top: calc(var(--u) * 659);
  width: calc(var(--u) * 448);
  height: calc(var(--u) * 494);
}

/* the beige panel from the mockup — a gap punched through the card row for
   the text to sit in.

   Height is the photo height (448u), not the full 494u card slot: the slot
   runs past 1107u, where .gallery__bar starts, so a full-height panel cut a
   notch through the olive caption strip. Cropped here it stops flush with
   the bottom of the photos beside it and leaves the strip unbroken.

   Left edge pulled 10px out (literal px) so the copy reads with a 10px
   inset from it while the text itself stays on 965u, aligned with "ДЕКОР"
   above. The RIGHT edge stays on the slot line rather than travelling with
   it: the panel occupies exactly one card slot, so a card lands squarely
   behind it as the row scrubs past, and a panel translated bodily left
   would leave a 10px strip of that card's photo showing past its right
   edge. The 10px it gains on the left falls in the gap between two cards,
   which is this same beige — invisible either way.

   z-index 0 with the paragraph lifted to 1, rather than the panel sitting at
   -1 under unpositioned text: a negative z-index puts the panel on its own
   layer behind its parent, and inside a pinned (position:fixed, composited)
   section that layer's edge landed between device pixels and drew a hairline
   along the top of the panel. Same paint order, no separate layer. */
.gallery__note::before {
  content: '';
  position: absolute;
  z-index: 0;
  /* Overshoot the top by a whole device pixel instead of trying to land on
     659u exactly. The panel and the photos beside it share that line, so at
     any fractional --u the card that parks behind the panel rounded up one
     pixel earlier than the panel did and a hairline of its photo showed
     above the panel's top edge, running the panel's full width. The pixel it
     gains is beige on beige — .gallery's own background — so nothing above
     changes; the height compensates so the bottom edge stays on 1107u. */
  top: -1px;
  left: -10px;
  right: 0;
  height: calc(var(--u) * 448 + 1px);
  background-color: var(--beige-100);
}

/* The note's box already starts on the same 659u line as the cards, but a
   line box is not ink: with line-height 1 the first visible pixel of this
   36u type sits 11.4u below the box top, so optically the paragraph hung
   below the photo beside it. Pulled up by exactly that measured offset
   (canvas TextMetrics: half-leading + fontAscent - actualAscent) so the
   text's top edge reads flush with the photo's. */
.gallery__note p {
  position: relative;
  z-index: 1;
  width: calc(var(--u) * 356);
  margin: calc(var(--u) * -11.4) 0 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1;
  color: rgba(43, 61, 45, 0.8);
}

/* The mockup's circular "next" control (node 978:1273) is deliberately gone
   from both the markup and here: that circle became the glass cursor, so
   keeping a static one on the page duplicated it. */

/* ==========================================================================
   PHILOSOPHY SECTION — "6_філософія" (Figma node 978:1287, file
   6tA1k2sLpFm5U7cLn9p9rZ). Olive field that the gallery above flows into.

   Height is set in --u for the same reason as .gallery: --u derives from
   100vw (scrollbar included) while aspect-ratio resolves against the
   element's own content width (scrollbar excluded), so an aspect-ratio box
   ends up short of the coordinate space its children live in.

   Inner layout is flex, following how the frame is built in Figma, rather
   than the absolute-in-u approach the earlier sections use — the two
   groups are absolutely placed, everything inside them flows.
   ========================================================================== */

/* --- film grain over the olive field, to match the mockup. Measured off
   the Figma render of node 978:1287: a bare patch of background has a
   per-channel standard deviation of ~3.4 levels at full resolution against
   the flat #9A9671, i.e. a very fine roughly-monochrome noise, not a
   texture. The tile itself is generated once in a canvas by
   initOliveGrain() (same approach as the glass cursor's normal map) so the
   amplitude is an exact number rather than an opacity guessed against an
   feTurbulence.

   `overlay` against a mid-grey tile leaves the base colour untouched and
   only applies the deviations. `isolation: isolate` keeps that blend inside
   the section instead of letting it reach the sections above. The pseudo
   element paints under .section-inner (both are positioned, and it comes
   first in DOM order), so the grain sits on the background only and never
   over the photographs. --- */

.philosophy {
  isolation: isolate;
  position: relative;
  width: 100%;
  height: calc(var(--u) * 1762);
  overflow: hidden;
  /* .gallery's own bottom edge lands 0.4px above this box's top in exact
     math — close enough that under fractional dpr the two edges can round
     to different device pixels and let a hairline of the (transparent)
     page background show through between them. A 2px real overlap, pulled
     up over .gallery's trailing pixels, can't produce a gap at any
     rounding: this section's own background paints over them regardless. */
  margin: -2px 0 0;
  background-color: var(--olive-100);
}

.philosophy__group {
  position: absolute;
  left: calc(var(--u) * 50);
  right: calc(var(--u) * 50);
}

.philosophy__group--top {
  top: calc(var(--u) * 150);
  height: calc(var(--u) * 614);
}

.philosophy__group--bottom {
  top: calc(var(--u) * 864);
  height: calc(var(--u) * 748);
}

/* --- shared pieces --- */

.philosophy__eyebrow {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.5;
  white-space: nowrap;
}

/* same mixed-size typed spaces as the gallery eyebrow, so the gap scales
   with the type rather than sitting at a fixed pixel inset */
.philosophy__eyebrow .italic-symbol {
  margin-right: calc(var(--u) * 18.3);
}

.philosophy__eyebrow--light { color: var(--beige-100); }
.philosophy__eyebrow--dark { color: #2B3D2D; }

.philosophy__title {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 500;
  font-size: calc(var(--u) * 96);
  line-height: 1;
  letter-spacing: calc(var(--u) * -1.92);
  text-transform: uppercase;
}

.philosophy__title--dark { color: #2B3D2D; }
.philosophy__title--light { color: var(--beige-100); }
.philosophy__title-dim { color: rgba(43, 61, 45, 0.8); }
.philosophy__title-dim-light { color: rgba(237, 233, 221, 0.8); }

.philosophy__head {
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 20);
  align-items: flex-start;
  width: calc(var(--u) * 752);
}

.philosophy::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* set by initOliveGrain(); `none` keeps the section clean if JS is off */
  background-image: var(--grain, none);
  background-repeat: repeat;
  mix-blend-mode: overlay;
}

.philosophy__img {
  position: relative;
  overflow: hidden;
}

/* 150%/-25% rather than a flush fit: these images get the craft section's
   hover-zoom + scroll-drift treatment (initMediaImageParallax), and the
   drift needs spare image above and below the frame or the -15% yPercent
   would expose the frame's bottom edge. The drift eats 22.5% of the frame
   (15% of an image 1.5x its height) against 25% of overhang. These are
   craft's own numbers: this block ran shallower (125%/-12.5% at -7) until
   she asked for block 2's effect exactly. */
.philosophy__img img {
  position: absolute;
  top: -25%;
  right: 0;
  bottom: auto;
  left: 0;
  width: 100%;
  height: 150%;
  object-fit: cover;
  display: block;
}

/* --- top group --- */

.philosophy__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  height: calc(var(--u) * 448);
}

.philosophy__imgh {
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 162);
  flex: 0 0 auto;
}

.philosophy__img--main {
  flex: 0 0 auto;
  width: calc(var(--u) * 448);
  height: calc(var(--u) * 448);
}

.philosophy__note {
  flex: 0 0 auto;
  width: calc(var(--u) * 295);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
}

.philosophy__note--dark { color: #2B3D2D; }

/* Figma crops this one specifically: the export sits at top:-13.85% inside
   a 150%-tall box, i.e. the visible slice starts above centre. Expressed as
   an object-position so it survives any container size. */
.philosophy__img--p1 {
  position: absolute;
  left: calc(var(--u) * 915);
  top: calc(var(--u) * 319);
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
}

.philosophy__img--p1 img {
  object-position: center 27.7%;
}

/* --- bottom group --- */

.philosophy__col-left {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 753);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 90);
  /* Figma right-aligns the 295 image inside this 753 column */
  align-items: flex-end;
}

.philosophy__col-left .philosophy__head {
  width: 100%;
}

.philosophy__img--p2 {
  flex: 0 0 auto;
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
}

.philosophy__col-right {
  position: absolute;
  left: calc(var(--u) * 915);
  top: 0;
  width: calc(var(--u) * 905);
  /* stands in for Figma's 120-tall transparent spacer div */
  padding-top: calc(var(--u) * 120);
}

.philosophy__row-lower {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 163);
  height: calc(var(--u) * 628);
}

/* the wrapper is 295 wide but its paragraph is 356 — the text deliberately
   overhangs its column in the mockup, so nothing here may clip it */
.philosophy__lead-wrap {
  flex: 0 0 auto;
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 628);
  position: relative;
}

.philosophy__lead {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 356);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1.1;
  color: var(--beige-100);
}

.philosophy__img-frame {
  position: relative;
  flex: 0 0 auto;
  width: calc(var(--u) * 447);
  height: calc(var(--u) * 628);
}

.philosophy__img--p3 {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 181);
  width: calc(var(--u) * 447);
  height: calc(var(--u) * 447);
}

/* ==========================================================================
   GALLERY + PHILOSOPHY — MOBILE (Figma node 1388:2152, containing 978:923
   "5_галерея" and 978:984 "6_філософія", file 6tA1k2sLpFm5U7cLn9p9rZ,
   375px reference, plain px). Built together per her request. Both
   source frames are themselves auto-layout (flex/gap), not absolute
   positioning — unlike blocks 1-4, these use flex here too rather than
   forcing an absolute-in-px layout onto content that wasn't designed
   that way (same reasoning desktop's own .philosophy already used for
   the same reason).

   .gallery-mobile__row-wrap/.gallery-mobile__row is a plain native
   horizontal scroll — no JS, no loop-clones, no scrub. Desktop's track
   has an infinite-loop scrub carousel; replicating that on mobile is a
   motion-pass decision she hasn't made yet, not attempted here.

   Card 3 of 5 in the gallery row is a placeholder (see its HTML comment
   in index.html) — Figma's own metadata marks that specific instance
   unmodified ("symbol", not "instance" like the other four), meaning she
   hasn't finished assigning it a real photo/caption yet.

   Neither .gallery nor .philosophy gets a hardcoded mobile height like
   blocks 1-4 did — both sections' real content is longer than one
   viewport and driven by flex layout, not a fixed Figma frame size
   matched to absolute children, so `height:auto` lets it size itself
   instead of clipping at a guessed number. */

.gallery-mobile,
.philosophy-mobile,
.steps-mobile {
  display: none;
}

@media (max-width: 768px) {
  .gallery,
  .philosophy {
    height: auto;
    overflow: visible;
  }

  /* .steps's own override lives further down this file, right after
     .steps's desktop rule block (line ~3578) instead of here — same
     selector, same specificity (both plain `.steps`), so source order
     alone decides the winner and the desktop rule sits BELOW this point
     in the file. Putting the override here first cost a real bug: the
     desktop height/overflow silently won at every viewport width,
     clipping the mobile carousel at ~317px with nothing visibly wrong in
     the markup itself. Not a mistake worth repeating for .gallery/
     .philosophy above, whose own desktop rules both sit EARLIER than this
     block already. */

  /* .section-inner is position:absolute;inset:0 by default (it's the
     scroll-lag drift target elsewhere) — that takes it out of normal
     flow, so the flex content inside it never propagates a real height
     up to .gallery/.philosophy above, and `height:auto` on those
     collapses to 0 despite genuinely-visible content. Safe to put back
     in flow here specifically: both are already excluded from scroll-lag
     (they're meant to read as one continuous surface). .steps > .section-inner's
     equivalent override is next to its height/overflow fix below, for the
     same source-order reason. */
  .gallery > .section-inner,
  .philosophy > .section-inner {
    position: static;
  }

  .gallery__eyebrow,
  .gallery__title,
  .gallery__lead,
  .gallery__filter,
  .gallery__track,
  .gallery__note,
  .gallery__bar,
  .steps__heading,
  .steps__list,
  .scroll-up {
    display: none;
  }

  /* padding-top restored to 80px per her explicit instruction
     (2026-08-05) — literal px, not derived from Figma's auto-layout this
     time. padding-bottom removed: her updated Figma frame now ends flush
     with the card row's own caption (frame height 808, no trailing
     space) — see the note above .gallery-mobile__row for where that
     bottom space actually belongs now (moved to
     .philosophy-mobile__block--1's padding-top, so it's olive-colored
     like the mockup instead of bleeding this section's beige). */
  .gallery-mobile {
    display: block;
    position: relative;
    padding: 80px 0 0;
  }

  .gallery-mobile__eyebrow {
    margin: 0 0 10px;
    padding: 0 20px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.5;
    letter-spacing: -0.32px;
    color: #2B3D2D;
  }

  .gallery-mobile__eyebrow .italic-symbol {
    margin-right: 10px;
  }

  .gallery-mobile__title {
    margin: 0 0 20px;
    padding: 0 20px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 500;
    font-size: 46px;
    line-height: 1;
    letter-spacing: -1.38px;
    text-transform: uppercase;
  }

  .gallery-mobile__title-a { color: var(--olive-100); }
  .gallery-mobile__title-b { color: var(--olive-80); }

  /* width:210 — her "+20px" request (173->193) still wrapped 7 lines;
     she then showed the actual Figma selection box, whose wrap is 5
     lines ("кожна робота тут" / "— це унікальна" / "історія, створена" /
     "руками наших" / "студентів"). Tested every width live and 200-220px
     all reproduce exactly that wrap — 210 picked as the middle of that
     confirmed range, this screenshot is the real spec now. */
  .gallery-mobile__lead {
    margin: 0 0 60px;
    padding: 0 20px;
    width: 210px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    color: #2B3D2D;
  }

  .gallery-mobile__filter {
    margin: 0 0 40px;
    padding: 0 20px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: flex-start;
  }

  /* is-active = 0.6 opacity, NOT full brightness — matches
     .gallery__filter-item.is-active's own convention exactly (checked,
     not assumed the intuitive way round). Underline is hover-only per
     her request (removed the always-on default) — gated behind
     (hover:hover)/(pointer:fine) so it doesn't get stuck "on" on a real
     touch device with no hover state to leave. */
  .gallery-mobile__filter-item {
    margin: 0;
    padding: 0;
    border: none;
    background: none;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 500;
    font-size: 20px;
    line-height: 1.1;
    text-transform: uppercase;
    text-decoration: none;
    text-underline-position: from-font;
    color: #2B3D2D;
    cursor: pointer;
  }

  @media (hover: hover) and (pointer: fine) {
    .gallery-mobile__filter-item:hover {
      text-decoration: underline;
    }
  }

  .gallery-mobile__filter-item.is-active {
    opacity: 0.6;
  }

  /* Reverted 2026-08-05 back to her desktop .gallery__bar mechanism (one
     continuous olive strip behind the whole track), per her explicit
     instruction — the previous per-caption-background approach (each card
     painting its own bg) was a deliberate fix for a real bug (see below)
     but it also meant the 10px gaps BETWEEN cards, and the inset before
     card 1, showed the section's beige instead of olive, which doesn't
     match the mockup. This time the strip's size comes from
     .gallery-mobile__track's own `width:max-content` (exactly as wide as
     its real content) instead of a guessed/assumed number, so it can't
     repeat the old bug (a stray block sized off an assumed card height
     with no hard cap, see the incident this replaced below). data-lenis-prevent
     on the row so Lenis (which listens page-wide for scroll/wheel/touch)
     can't swallow a horizontal swipe/drag before it reaches this nested
     scroller. */
  /* native scrollbar hidden — same fix as .steps-mobile__row (her request
     landed on that block first, applied here too for consistency: both
     rows are the same carousel mechanism and leaving one with a visible
     scrollbar next to one without would itself read as a bug).

     -webkit-overflow-scrolling:touch deliberately removed (2026-08-07,
     from all 6 mobile carousel rows — gallery/steps/stories/pricing/cta-
     stats/social): she reported the page freezing for several seconds
     whenever a scroll gesture passed over ANY of these rows, on a real
     phone. That property is the old iOS async-momentum hint — on affected
     Safari versions a nested `overflow-x` element carrying it can "capture"
     the touch for its own inertial deceleration, leaving the outer page
     scroll unresponsive until the row's own momentum finishes (a well-
     documented iOS quirk, not specific to this codebase). Modern Safari
     already gives `overflow-x:auto` native momentum without this hint, so
     removing it costs nothing. If reintroducing it anywhere, retest this
     exact symptom on a real iOS device first. */
  .gallery-mobile__row {
    position: relative;
    overflow-x: auto;
    /* pan-x alone blocks native vertical panning for any touch that starts
       over the row — a real jerk when a vertical swipe happens to begin
       here, same bug documented on .stories-mobile__row (see there);
       pan-x pan-y lets both directions through, the row's own
       overflow-x:auto still owns the horizontal gesture. */
    touch-action: pan-x pan-y;
    cursor: grab;
    padding-left: 20px;
    margin-bottom: 0;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .gallery-mobile__row::-webkit-scrollbar {
    display: none;
  }

  .gallery-mobile__row.is-dragging {
    cursor: grabbing;
  }

  .gallery-mobile__track {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: max-content;
  }

  /* her desktop .gallery__bar, ported: one strip behind the whole track
     instead of each card's own background. left:-20px cancels the row's
     own padding-left so the strip reaches the true canvas edge on the
     left (matching desktop's edge-to-edge bar); right:0 stops it exactly
     at the track's own max-content edge — flush with the last card, not a
     guessed width. top:320px is the real, fixed photo height (not an
     assumed card height), so it can't drift out of sync with the cards
     the way the old incident bar did. */
  .gallery-mobile__bar {
    isolation: isolate;
    position: absolute;
    z-index: 0;
    left: -20px;
    right: 0;
    top: 320px;
    height: 46px;
    background-color: var(--olive-100);
  }

  /* same film grain as desktop's .gallery__bar (and .philosophy/.cta/
     .footer) — see initOliveGrain() in script.js, sets --grain globally on
     the root so no new JS is needed here. */
  .gallery-mobile__bar::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: var(--grain, none);
    background-repeat: repeat;
    mix-blend-mode: overlay;
  }

  .gallery-mobile__card {
    position: relative;
    z-index: 1;
    flex: 0 0 auto;
    width: 320px;
    display: flex;
    flex-direction: column;
    margin: 0;
  }

  .gallery-mobile__photo {
    display: block;
    position: relative;
    width: 320px;
    height: 320px;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    text-align: inherit;
    overflow: hidden;
  }

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

  .gallery-mobile__marks {
    position: absolute;
    z-index: 1;
    right: 10px;
    top: 10px;
    display: flex;
    gap: 10px;
    color: #2B3D2D;
  }

  .gallery-mobile__mark {
    font-family: 'Helvetica Neue', sans-serif;
    font-size: 16px;
    line-height: 1;
  }

  /* No background of its own now — sits on .gallery-mobile__bar (the
     shared strip behind the whole track). height:46px matches the bar
     exactly (her desktop .gallery__bar height, ported to a literal px);
     flex+center gives clean vertical centering without re-deriving padding
     from the font metrics. */
  .gallery-mobile__caption {
    margin: 0;
    padding: 0;
    width: 320px;
    height: 46px;
    display: flex;
    align-items: center;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    color: var(--beige-100);
  }

  /* --- philosophy --- */

  .philosophy__group {
    display: none;
  }

  .philosophy-mobile {
    display: block;
    position: relative;
  }

  /* padding-top/gaps below are computed from Figma's actual absolute
     y-coordinates (checked via get_metadata), not generic round numbers:
     block--1: eyebrow y127, title y161 (34 after eyebrow top), img y289
     (36 after title), note y664 (40 after img). block--2 starts at y884,
     44px after block--1's own content ends at 840 (664 note-top + its
     176 height). */
  .philosophy-mobile__block {
    position: relative;
    padding: 0 20px;
  }

  /* Re-derived again after her 2026-08-05 Figma update (get_metadata,
     node 1398:2238, fileKey 6tA1k2sLpFm5U7cLn9p9rZ) — the previous "85px"
     note above this rule was itself already stale. Current real numbers,
     group-local: gallery frame 0-808, ends flush with the card row's own
     caption (no trailing space in Figma). Philosophy frame starts at
     group-y 808 (flush, 0 overlap — .gallery-mobile no longer carries any
     trailing padding/margin, both zeroed out, so this section starts
     right where the beige one ends visually too). Eyebrow sits at local
     y:127 within the philosophy frame -> group-y 935. Target gap from
     gallery's real bottom to the eyebrow = 935 - 808 = 127px, carried
     entirely as THIS padding-top so the gap reads as olive (matching the
     mockup) instead of the beige .gallery-mobile used to bleed into it. */
  .philosophy-mobile__block--1 {
    padding-top: 127px;
  }

  .philosophy-mobile__block--2 {
    padding-top: 44px;
    padding-bottom: 80px; /* not specified past this group in Figma — matches the site's usual section-end spacing */
  }

  .philosophy-mobile__eyebrow {
    margin: 0 0 10px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.5;
    letter-spacing: -0.32px;
  }

  .philosophy-mobile__eyebrow .italic-symbol { margin-right: 10px; }

  .philosophy-mobile__eyebrow--light { color: var(--beige-100); }
  .philosophy-mobile__eyebrow--dark { color: #2B3D2D; }

  .philosophy-mobile__title {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 500;
    font-size: 46px;
    line-height: 1;
    letter-spacing: -1.38px;
    text-transform: uppercase;
  }

  .philosophy-mobile__block--1 .philosophy-mobile__title {
    margin-bottom: 36px;
  }

  .philosophy-mobile__title--dark { color: #2B3D2D; }
  .philosophy-mobile__title--light { color: var(--beige-100); }
  .philosophy-mobile__title-dim { color: rgba(43, 61, 45, 0.8); }
  .philosophy-mobile__title-dim-light { color: rgba(237, 233, 221, 0.8); }

  .philosophy-mobile__img {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    margin: 0;
  }

  .philosophy-mobile__block--1 .philosophy-mobile__img {
    margin-bottom: 40px;
  }

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

  .philosophy-mobile__note,
  .philosophy-mobile__lead {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
  }

  .philosophy-mobile__note {
    width: 240px;
    color: #2B3D2D;
  }

  /* absolute, not flex — a space-between row was tried first (eyebrow+
     title as one flex item, lead as the other) since Figma's lead
     visually sits beside the title, but head-2's rendered width + a
     fixed 200px lead + gap already exceeded the 335px column, and
     flex-shrink:0 on both just pushed lead past the section's right
     edge. Figma's own x:156 isn't actually a flex relationship to the
     title's width — it's an independent fixed position.
     top:159, NOT 115: an absolutely-positioned child's containing block
     is the padding EDGE of its nearest positioned ancestor, i.e.
     block--2's own border-box top (840, where block--1's content ends)
     — NOT the content-box top after its 44px padding-top has been
     applied (884, which is only where IN-FLOW children like the eyebrow
     start). 999 (Figma) - 840 (block's real absolute-positioning origin)
     = 159. First pass used 884 as the origin by mistake and landed 44px
     high. */
  .philosophy-mobile__lead {
    position: absolute;
    left: 156px;
    top: 159px;
    width: 200px;
    color: var(--beige-100);
  }

  /* img follows eyebrow+title in normal flow (unlike lead, so its
     padding-top DOES apply normally here, no origin confusion). Title
     renders as ONE line at this width — "мова творця" fits, unlike
     block 1's explicitly-<br>-broken 2-line title — so the flow position
     before this margin lands at 964 (measured live, not just summed from
     font-metrics on paper after the lead miscalculation above). Margin
     covers the rest of the way to Figma's y:1239. */
  .philosophy-mobile__block--2 .philosophy-mobile__img {
    margin-top: 275px;
  }

  /* --- steps (node 978:997 "7_кроки") ---
     Same root-has-zero-horizontal-padding convention as .gallery-mobile:
     the row needs to bleed edge-to-edge for the carousel, so every OTHER
     child insets itself with its own `padding: 0 20px` instead of relying
     on the section wrapper. Carousel is a plain native horizontal scroll,
     same mechanism as .gallery-mobile__row/__track, per her explicit
     "як у 5 блоці" spec — no bar (desktop's step cards have no background
     strip to port, unlike the gallery). */
  .steps-mobile {
    display: flex;
    flex-direction: column;
    gap: 80px;
    padding: 80px 0;
  }

  .steps-mobile__top {
    display: flex;
    flex-direction: column;
    gap: 40px;
    width: 100%;
  }

  .steps-mobile__heading {
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding: 0 20px;
  }

  /* Image title, per her explicit request (2026-08-05) — her own
     mob-keramika-1/2/3.png exports, not text + CSS. Confirmed by opening
     the files: img 2 ("народжується") carries a real soft-focus blur BAKED
     INTO the raster; img 1/3 are crisp. That's exactly why the earlier
     text version was left unblurred (get_design_context showed a Figma
     `blur` value on all three text layers, but this project's hard rule is
     no LIVE CSS blur anywhere) — the blur was always meant to ship as a
     flat image, same category of exception as the footer's soft-focus
     logo, not a CSS filter to avoid or a Figma value to replicate. Each
     img is full column width with its own natural aspect ratio (139:84,
     537:184, 447:84 — img 1/3 already share img 2's per-line glyph scale,
     no rescaling needed the way desktop's own two-image heading required). */
  .steps-mobile__title {
    display: flex;
    flex-direction: column;
    width: 100%;
  }

  .steps-mobile__title-img {
    display: block;
    height: auto;
  }

  /* NOT all width:100% — the three PNGs share one consistent internal
     font-scale (checked their real pixel dimensions: 139x84, 537x184,
     447x84 — img 1/3 are already single-line at the SAME glyph height as
     one line of img 2's two), so stretching each independently to fill
     the column would blow "ЯК" up ~2.4x bigger than its actual designed
     size relative to the other two lines. img 2 ("народжується") is the
     widest and sets the column's full width; img 1/3 are sized down from
     their own natural width by that exact same ratio (335/537 = 0.6238),
     not guessed. */
  .steps-mobile__title-img:nth-child(1) {
    width: 87px;
  }

  .steps-mobile__title-img:nth-child(2) {
    width: 335px;
  }

  .steps-mobile__title-img:nth-child(3) {
    width: 279px;
  }

  .steps-mobile__subheading {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    color: var(--olive-100);
  }

  /* margin, not padding: the image itself must render edge-to-edge within
     the inset box (a padding would shrink the visible photo further). aspect
     [295/295] is really 1:1 — 335px column minus these 20px margins each
     side = 315px... kept as a plain aspect-ratio square instead of Figma's
     "295" literal so it stays exactly the column width minus its own
     margins at any width, not a hardcoded number that only matches at
     exactly 375px viewport. Overscan crop (150.27%/-42.42%) copied from
     Figma verbatim — same technique as .gallery-mobile__card--first's old
     desktop counterpart, a wrapper + absolutely-oversized img instead of a
     plain object-position, because the crop isn't just off-center, it's a
     genuine scale+offset. */
  .steps-mobile__art {
    position: relative;
    width: auto;
    margin: 0 20px;
    aspect-ratio: 1 / 1;
    overflow: hidden;
  }

  .steps-mobile__art-img {
    position: absolute;
    left: 0;
    top: -42.42%;
    width: 100%;
    height: 150.27%;
    object-fit: cover;
    display: block;
  }

  /* native scrollbar hidden per her request — the .js-cursor-media hint
     (data-cursor-label="гортай" in the markup) is the replacement
     affordance instead, not the browser's own scroll UI. */
  .steps-mobile__row {
    position: relative;
    overflow-x: auto;
    /* pan-x pan-y, not pan-x alone — same vertical-scroll-jerk fix as
       .gallery-mobile__row/.stories-mobile__row, see the comment there. */
    touch-action: pan-x pan-y;
    cursor: grab;
    padding-left: 20px;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .steps-mobile__row::-webkit-scrollbar {
    display: none;
  }

  .steps-mobile__row.is-dragging {
    cursor: grabbing;
  }

  .steps-mobile__track {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: max-content;
  }

  .steps-mobile__card {
    flex: 0 0 auto;
    width: 325px;
    display: flex;
    flex-direction: column;
    gap: 40px;
    margin: 0;
  }

  /* Number/word hover swap, ported from desktop's .step__label (same
     structure: -left group holds the opening paren + a crossfading
     rest/hover pair, -spacer is a flex-grow:0->1 push that shoves the
     closing paren out to the card's own right edge, recomputed by flexbox
     itself regardless of word width). The actual :hover trigger lives in
     the (hover:hover)/(pointer:fine) block further down — base rules here
     are just sizing/layout, always active, so the number itself always
     renders even on a real touch device that will never fire :hover. */
  .steps-mobile__label {
    margin: 0;
    display: flex;
    align-items: center;
    width: 100%;
    color: #2B3D2D;
  }

  .steps-mobile__label-left {
    display: flex;
    align-items: center;
    gap: 5px;
  }

  .steps-mobile__label-paren {
    display: block;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 100;
    font-size: 36px;
    line-height: 1;
    letter-spacing: -0.72px;
  }

  .steps-mobile__label-value {
    position: relative;
    display: block;
  }

  .steps-mobile__label-value-rest {
    display: block;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    white-space: nowrap;
  }

  .steps-mobile__label-value-hover {
    position: absolute;
    left: 0;
    bottom: 0;
    opacity: 0;
    pointer-events: none;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    white-space: nowrap;
  }

  .steps-mobile__label-spacer {
    flex-grow: 0;
    flex-shrink: 0;
  }

  @media (hover: hover) and (pointer: fine) {
    .steps-mobile__label-value-rest,
    .steps-mobile__label-value-hover {
      transition: opacity 0.85s cubic-bezier(0.65, 0, 0.35, 1);
    }

    .steps-mobile__label-spacer {
      transition: flex-grow 0.75s cubic-bezier(0.65, 0, 0.35, 1);
    }

    .steps-mobile__card:hover .steps-mobile__label-spacer {
      flex-grow: 1;
    }

    .steps-mobile__card:hover .steps-mobile__label-value-rest {
      opacity: 0;
      transition-duration: 0.25s;
    }

    .steps-mobile__card:hover .steps-mobile__label-value-hover {
      opacity: 1;
      transition-duration: 0.85s;
    }

    /* dragging the row under a stationary mouse can slide a card under the
       cursor and fire its :hover uninvited — same class of bug documented
       for desktop's .is-scrolling guard. Force the swap back off for the
       duration of an active drag rather than porting the full JS guard. */
    .steps-mobile__row.is-dragging .steps-mobile__card:hover .steps-mobile__label-spacer {
      flex-grow: 0;
      transition: none;
    }

    .steps-mobile__row.is-dragging .steps-mobile__card:hover .steps-mobile__label-value-rest {
      opacity: 1;
      transition: none;
    }

    .steps-mobile__row.is-dragging .steps-mobile__card:hover .steps-mobile__label-value-hover {
      opacity: 0;
      transition: none;
    }
  }

  .steps-mobile__body {
    display: flex;
    flex-direction: column;
    gap: 20px;
    width: 100%;
  }

  /* pre-wrap, not desktop's plain pre — a 325px mobile card can't hold the
     same manually-spaced-out caption on one line the way desktop's much
     wider column can (this is Figma's own choice for this node too:
     whitespace-pre-wrap, checked in get_design_context, not copied from
     desktop by assumption). Preserves the multi-space manual justification
     while still wrapping instead of overflowing the card. */
  .steps-mobile__caption {
    margin: 0;
    width: 100%;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    color: var(--olive-100);
    white-space: pre-wrap;
    overflow-wrap: break-word;
  }

  .steps-mobile__text {
    margin: 0;
    width: 100%;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    color: #2B3D2D;
    overflow-wrap: break-word;
  }
}

/* ==========================================================================
   CUSTOM CURSOR — glassmorphic "Далі" cursor
   Small trailing dot by default; expands into a glass circle with a label
   when hovering images/video/portfolio-style media.
   ========================================================================== */

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10000;
  width: 0;
  height: 0;
  pointer-events: none;
  will-change: transform;
}

.cursor__dot {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(var(--u) * 10);
  height: calc(var(--u) * 10);
  margin: calc(var(--u) * -5) 0 0 calc(var(--u) * -5);
  border-radius: 50%;
  background: var(--beige-100);
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease, background 0.2s ease;
}

/* toggled by script.js (initCustomCursor) based on which
   [data-cursor-theme] section the cursor is currently over */
body.cursor-on-light .cursor__dot {
  background: #2B3D2D;
}

/* the filter lives in an inline <svg> that must not take up layout */
.glass-defs {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}

/* --- the glass bubble. Matches Figma's Glass effect on node 978:1272
   (Refraction 44 / Depth 73 / Dispersion 35 / Frost 7 / Light -50° at 0%).

   Two things the old `backdrop-filter: blur(8px)` got wrong: Frost 7 is
   nearly zero in Figma's own units (its shader scales the slider by
   0.1, so 7 -> 0.007), and Light at 0% intensity means there is no
   specular highlight at all — the bright rim in the mockup comes purely
   from refraction bending the backdrop, not from a light source. So:
   almost no blur, almost no tint, and the whole character comes from
   the rim.

   Layered so nothing can break: the box-shadow bevel and the 1.5px blur
   below are the complete look everywhere, and the SVG refraction is a
   second `backdrop-filter` declaration that simply overrides the first
   in browsers that parse url() inside it (Chrome/Edge). Where it does
   not parse (Safari, Firefox) the earlier declaration survives the
   cascade and the bubble degrades to frost + bevel rather than to
   nothing. --- */
.cursor__media {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--u) * 110);
  height: calc(var(--u) * 110);
  margin: calc(var(--u) * -55) 0 0 calc(var(--u) * -55);
  border-radius: 50%;
  /* near-clear now that the frost is gone; just enough sand/olive to keep
     the beige label legible over a bright photo */
  background: rgba(154, 150, 113, 0.16);
  backdrop-filter: blur(1.5px);
  -webkit-backdrop-filter: blur(1.5px);
  backdrop-filter: blur(1.5px) url(#glass-refraction);
  border: 1px solid rgba(237, 233, 221, 0.22);
  /* Depth 73 — the inward bevel. Ring of light on the inside edge plus a
     soft dark seat behind it, which is what reads as glass thickness. */
  box-shadow:
    inset 0 0 0 1px rgba(237, 233, 221, 0.30),
    inset 0 0 calc(var(--u) * 16) calc(var(--u) * 7) rgba(237, 233, 221, 0.10),
    inset 0 0 calc(var(--u) * 10) calc(var(--u) * 2) rgba(43, 61, 45, 0.10);
  opacity: 0;
  transform: scale(0.4);
  transition: transform 0.55s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.35s ease;
}

.cursor__media-label {
  font-family: 'Helvetica Neue', Arial, sans-serif;
  font-size: calc(var(--u) * 24);
  font-weight: 400;
  letter-spacing: calc(var(--u) * 0.2);
  color: var(--beige-100);
}

.cursor.is-media .cursor__dot {
  opacity: 0;
  transform: scale(0);
}

.cursor.is-media .cursor__media {
  opacity: 1;
  transform: scale(1);
}

@media (hover: none) and (pointer: coarse) {
  .cursor {
    display: none;
  }
}

/* ==========================================================================
   MOTION
   Vase entrance settles into an infinite GSAP "breathe" float (script.js).
   Everything else in the master timeline runs once on load and rests in
   its final state.
   ========================================================================== */

.js-parallax {
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .hero__scroll-arrow {
    animation: none !important;
  }
}

/* ==========================================================================
   MENU TRIGGER ICON — mobile only
   .hero__menu-btn shows text "меню" + underline-hover on desktop/tablet
   (same line-hover mechanism as .hero__link/.hero__btn), and this icon
   instead on mobile. On desktop/tablet, "меню" now stays visible the
   whole time (open or closed) exactly like the logo and nav links —
   .menu-overlay__close-icon below is a separate, additional control
   inside the overlay, not a replacement for it. This mobile icon's X is
   built from the two bars in icon-menu.svg: each is re-centered onto the
   shared 18,18 midpoint (translateX) before rotating ±45deg about it, so
   the two originally-separate dashes become one crossing X. Hovering the
   closed mobile icon previews that same X; once open, body.menu-is-open
   forces it regardless of hover. (.menu-overlay__close-icon-bar below is
   plain HTML, not SVG, and has its own geometry/rules — see its own
   comment further down.)
   ========================================================================== */

.hero__menu-icon {
  display: none;
  width: calc(var(--u) * 36);
  height: calc(var(--u) * 36);
  align-items: center;
  justify-content: center;
}

.hero__menu-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

.hero__menu-icon-bar {
  /* without this, transform-origin resolves against each bar's OWN tiny
     fill-box (e.g. bar--1 spans only x:0-15.5674, y:17-19) rather than
     the shared SVG center — "18px 18px" would then land way outside that
     bbox, flinging the rotated bar off its intended arc and clipping
     most of it against the SVG's own default overflow:hidden. view-box
     makes 18px 18px resolve against the actual 0 0 36 36 viewBox instead */
  transform-box: view-box;
  transform-origin: 18px 18px;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .hero__menu-btn:hover .hero__menu-icon-bar--1 {
    transform: rotate(45deg) translateX(10.2163px);
  }
  .hero__menu-btn:hover .hero__menu-icon-bar--2 {
    transform: rotate(-45deg) translateX(-10.2163px);
  }
}

body.menu-is-open .hero__menu-icon-bar--1 {
  transform: rotate(45deg) translateX(10.2163px);
}

body.menu-is-open .hero__menu-icon-bar--2 {
  transform: rotate(-45deg) translateX(-10.2163px);
}

/* --- close icon: lives inside #menu-overlay, positioned exactly where
   .hero__menu-btn ("меню") sits in the header, so it reads as that
   control turning into this icon. Its two bars
   (.menu-overlay__close-icon-bar--1/--2) are plain HTML <span> elements,
   not SVG — CSS position/transform-origin is unambiguous for a plain
   HTML box (unlike SVG's fill-box vs view-box ambiguity, which caused a
   persistent "jumps left" bug on open regardless of how that version was
   patched). .menu-overlay__close-icon is the positioning context for
   both spans. GSAP animates the hamburger→X morph automatically as part
   of the menu opening (script.js); resting state is the hamburger.
   Desktop/tablet only — hidden on mobile alongside the other desktop-
   only overlay content, since mobile keeps its own header icon. Part of
   the overlay's existing .js-menu-stagger reveal, no separate mechanism. --- */

.menu-overlay__close-icon {
  position: absolute;
  left: calc(var(--u) * 1807);
  top: calc(var(--u) * 26);
  width: calc(var(--u) * 36);
  height: calc(var(--u) * 36);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  color: #2B3D2D;
}

.menu-overlay__close-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* transform-box: view-box is required — without it, transform-origin
   resolves against each bar's own tiny fill-box (e.g. bar--1 spans only
   x:0-15.5674, y:17-19) instead of the shared 36x36 SVG canvas, and "18
   18" lands way outside that bbox, flinging the bar off its intended arc.
   Resting hamburger (no rotation) — hovering (JS mouseenter/mouseleave,
   script.js) animates these bars to/from the X by writing the `transform`
   string directly every frame, not via GSAP's rotate shorthand and not
   via a CSS transition — both were tried and didn't reliably respect this
   origin during the actual animated tween. */
.menu-overlay__close-icon-bar {
  transform-box: view-box;
  transform-origin: 18px 18px;
}

/* ==========================================================================
   MENU OVERLAY — full-screen curtain reveal (same clip-path technique as
   the Hero's own curtain, but its own snappier ease and reusable open/
   close functions since it can be triggered repeatedly). z-index sits
   above all Hero content but below the custom cursor (10000), so the
   cursor keeps tracking on top of the overlay.
   ========================================================================== */

.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: color-mix(in srgb, var(--blue) 98%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
}

body.menu-is-open .menu-overlay {
  pointer-events: auto;
}

body.menu-is-open {
  overflow: hidden;
}

/* --- desktop/tablet content (Figma node 978:1686, 1920px reference) --- */

.menu-overlay__logo {
  position: absolute;
  left: var(--col-1-left);
  top: calc(var(--u) * 34);
  width: calc(var(--u) * 200);
  height: calc(var(--u) * 28);
}

.menu-overlay__logo img {
  display: block;
  width: 100%;
  height: 100%;
}

.menu-overlay__social {
  list-style: none;
  display: flex;
  flex-direction: column;
}

.menu-overlay__social--desktop {
  position: absolute;
  left: calc(var(--u) * 1270);
  top: calc(var(--u) * 446);
  gap: calc(var(--u) * 20);
}

.menu-overlay__social-link {
  position: relative;
  display: inline-flex;
  font-family: 'Helvetica Neue', Arial, sans-serif;
  font-size: calc(var(--u) * 64);
  font-weight: 400;
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -1.28);
  color: #2B3D2D;
  text-decoration: none;
}

.menu-overlay__contact {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 843);
  width: calc(var(--u) * 1820);
}

.menu-overlay__contact-col {
  position: absolute;
  top: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 10);
}

.menu-overlay__contact-col:nth-child(1) { left: 0; }
.menu-overlay__contact-col:nth-child(2) { left: calc(var(--u) * 565); }
.menu-overlay__contact-col:nth-child(3) { left: calc(var(--u) * 1220); }

.menu-overlay__contact-label {
  font-family: 'Helvetica Neue', Arial, sans-serif;
  font-size: calc(var(--u) * 24);
  font-weight: 400;
  color: rgba(43, 61, 45, 0.6);
}

.menu-overlay__contact-value {
  font-family: 'Helvetica Neue', Arial, sans-serif;
  font-size: calc(var(--u) * 40);
  font-weight: 400;
  line-height: 1.1;
  color: #2B3D2D;
}

.menu-overlay__mobile-strip,
.menu-overlay__mobile-nav,
.menu-overlay__mobile-bottom {
  display: none;
}

/* --- social link hover: label dims + shifts right, an italic "(" fades
   in at the label's original left edge to reveal itself in the gap the
   shift opens up. The paren is absolutely positioned (left:0, top left
   auto so it keeps its normal inline vertical alignment) so it takes no
   space in the layout at rest — the resting label sits flush at x:0
   either way, lining up with .menu-overlay__contact-col's left edge.
   Paren + label are independent siblings (not parent/child) so each can
   reach its own opacity — a child's opacity is otherwise capped by its
   parent's, since opacity forms a compositing group. */

.menu-overlay__social-paren {
  position: absolute;
  left: 0;
  opacity: 0;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.menu-overlay__social-text {
  display: inline-block;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.menu-overlay__social-link:hover .menu-overlay__social-paren {
  opacity: 0.6;
}

.menu-overlay__social-link:hover .menu-overlay__social-text {
  opacity: 0.6;
  /* em-based so the shift scales with font-size — flat px only cleared
     the "(" glyph's own width at one of the two sizes this class is used
     at (64px desktop vs 20px mobile); 0.5em covers the glyph's width at
     either, plus a flat 10px gap on top */
  transform: translateX(calc(0.5em + 10px));
}

/* ==========================================================================
   STEPS SECTION — "7_кроки" (Figma node 978:1310, file
   6tA1k2sLpFm5U7cLn9p9rZ). Pixel Perfect + first motion pass together.
   ========================================================================== */

.steps {
  position: relative;
  width: 100%;
  height: calc(var(--u) * 1623);
  overflow: hidden;
  /* .philosophy's bottom edge lands at the exact same fractional y as this
     section's top in exact math (confirmed: both compute to the identical
     sub-pixel value) — the same precondition documented for the
     gallery/philosophy seam. Under fractional devicePixelRatio the two
     edges can still round to different device pixels and let the
     (transparent) page background show through as a hairline. Same fix:
     a real 2px overlap no rounding combination can defeat. */
  margin: -2px 0 0;
  background-color: var(--beige-100);
  color: #2B3D2D;
}

.steps__heading {
  position: absolute;
  left: calc(var(--u) * 50);
  right: calc(var(--u) * 50);
  top: calc(var(--u) * 150);
  height: calc(var(--u) * 400);
}

.steps__heading-imgs {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: calc(var(--u) * 2);
  width: 100%;
}

.steps__heading-img {
  display: block;
}

/* h-step-1.png carries both "ЯК" and "народжується" already laid out
   full-width (left/right flush, per Figma's justify-between row) — sized
   to the container edge-to-edge so there is no rounding guesswork. */
.steps__heading-img--1 {
  width: 100%;
  height: auto;
}

/* h-step-2.png ("кераміка") is the shorter second line, right-aligned by
   the parent's align-items:flex-end. Sized off the SAME px-per-u scale as
   line 1 (source 3703px -> 1817u, so 1773px -> ~870u) so both lines read
   at one consistent type size instead of each filling its own box. */
.steps__heading-img--2 {
  width: calc(var(--u) * 870);
  height: auto;
  /* pulled 20px closer to line 1 per her review passes (10px, then another 10px) */
  margin-top: -20px;
}

.steps__subheading {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--u) * 370);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1;
  text-align: right;
  color: var(--olive-100);
  /* preserves the manually-typed multi-space gaps between words (Figma's
     own way of spreading this line across the full width) */
  white-space: pre;
}

.steps__subheading .word {
  display: inline-block;
  will-change: opacity;
}

.steps__list {
  position: absolute;
  left: calc(var(--u) * 50);
  right: calc(var(--u) * 50);
  top: calc(var(--u) * 650);
  height: calc(var(--u) * 823);
}

/* --- photo: same oversized-image + hover-scale + scroll-drift treatment
   as .craft__art-1 (mediaImageGroups() in script.js), her explicit spec
   ("такие же еффекты как в 2 блоке") --- */

.steps__art {
  position: absolute;
  left: 0;
  top: calc(var(--u) * -276);
  width: calc(var(--u) * 377);
  height: calc(var(--u) * 518);
  overflow: hidden;
}

.steps__art-img {
  position: absolute;
  left: 0;
  top: -25%;
  width: 100%;
  height: 150%;
  object-fit: cover;
}

.step {
  position: absolute;
  top: 0;
  width: calc(var(--u) * 450);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 60);
}

/* :hover re-evaluates purely from where content sits under a stationary
   cursor — scrolling card 1 up past a cursor that never moved was enough to
   trigger its hover. initStepHoverGuard() in script.js toggles this class on
   <body> for the duration of any scroll (native scroll event, works whether
   Lenis or the browser itself is driving it), which blocks hover from
   engaging until scrolling has actually stopped. */
.is-scrolling .step {
  pointer-events: none;
}

/* same scroll-hover guard, block 13's carousel: the track scrubs cards
   horizontally under a stationary cursor as the pin drives the page
   scroll, so a card sliding under the pointer fires a real mouseenter on
   .social__photo (initSocialPhotoHover() listens for it directly, not via
   CSS :hover) — scale(1.05) + cursor-drift kick in uninvited, reading as
   the photo "jerking/swapping." Confirmed 2026-08-07, most noticeable on
   whichever card happens to end up under wherever her mouse rests while
   scrolling. Reuses the existing global .is-scrolling toggle
   (initStepHoverGuard() in script.js already sets/clears it on ANY
   scroll, not just over .step) — no new JS needed. */
.is-scrolling .social__photo {
  pointer-events: none;
}

/* same guard, the big hero photo above the carousel row: it's static (not
   sliding under the cursor like the cards), but scrolling the PAGE past a
   stationary cursor resting on top of it fires the same uninvited
   mouseenter (initMediaImageHover()) → scale(1.05) kick right as the block
   enters view — a second, distinct contributor to the same "jerks on
   entrance" report alongside the clearProps fix in runSocialEntrance(). */
.is-scrolling .social__hero-photo {
  pointer-events: none;
}

/* same guard, block 7's own big left photo (.steps__art): identical setup
   to .social__hero-photo above — a static, non-clickable photo registered
   in mediaImageGroups() for hover-scale, so scrolling it past a
   stationary cursor while the section's entrance timeline settles fires
   the same uninvited mouseenter -> scale(1.05) kick, reading as a jerk
   right as the elements land. Confirmed 2026-08-08, same root cause as
   block 13's, same fix — no new JS, initStepHoverGuard() already toggles
   .is-scrolling globally on ANY scroll. */
.is-scrolling .steps__art {
  pointer-events: none;
}

.step--1 {
  right: 0;
}

.steps__row {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 490);
  width: calc(var(--u) * 1365);
  display: flex;
  justify-content: space-between;
}

.steps__row .step {
  position: relative;
}

/* override the shared .scroll-up bottom:50u — her spec 2026-08-09: align
   its own bottom edge with cards 2/3's bottom edge (measured live,
   getBoundingClientRect, not derived from the cards' own content-driven
   height in CSS). */
.steps .scroll-up {
  bottom: calc(var(--u) * 145.5);
}

/* --- "(N)" / hover swap: same family as .craft__cta-plus-char's swap — a
   plain CSS-transition hover, not GSAP, per the project's convention that
   simple hover interactions live in CSS while GSAP is reserved for
   scroll/entrance work. The spacer's flex-grow is what pushes the closing
   paren out to the card's right edge: flexbox recomputes the remaining
   space itself, so this works regardless of how wide the label text is,
   with no manual pixel measurement needed.

   The number/word swap is a pure opacity crossfade between two stacked,
   independently-sized spans (.step__label-value-rest/-hover) rather than a
   JS textContent swap animated by a font-size transition. That combination
   used to jitter: a static webfont (Helvetica Neue's separate weight
   files, not a variable font) can't actually interpolate font-weight, so
   the paren's weight change snapped instead of easing, and the JS content
   swap landed at the START of the font-size transition, so the box jumped
   to the full word's width at the small size before growing — two visible
   jumps stacked on one hover. Removing the paren weight change and the
   font-size animation (replaced by two statically-sized crossfading
   spans) removes both jump sources; only continuous, GPU-cheap opacity
   and flex-grow are now animated, on one shared slower/softer ease. */

.step__label {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  margin: 0;
  color: #2B3D2D;
}

.step__label-left {
  display: flex;
  align-items: center;
  gap: 5px;
}

.step__label-spacer {
  flex-grow: 0;
  flex-shrink: 0;
  /* the project's signature fast-start/slow-settle hover curve read wrong
     on this specific crossfade — her call was to go back to a symmetric
     ease-in-out (smooth start AND smooth end) instead, which is what this
     component used originally */
  transition: flex-grow 0.75s cubic-bezier(0.65, 0, 0.35, 1);
}

.step:hover .step__label-spacer {
  flex-grow: 1;
}

.step__label-paren {
  display: block;
  font-weight: 100;
  line-height: 1;
  font-size: calc(var(--u) * 64);
  letter-spacing: calc(var(--u) * -1.28);
}

.step__label-value {
  position: relative;
  display: block;
}

/* The crossfade duration is direction-aware, not per-element: whichever span
   is DISAPPEARING always takes the fast 0.25s, whichever is APPEARING always
   takes the slow 0.85s. Giving each element one fixed duration regardless of
   direction (the original approach) only worked for the hover-IN direction —
   on hover-OUT it was backwards (the reappearing number used the fast
   duration, the disappearing word the slow one), so the number visibly
   popped back in before the word had finished fading out. Base rules below
   cover hover-OUT (returning to rest); `.step:hover` overrides the duration
   (not the whole `transition` shorthand, so the shared easing curve stays
   one declaration) for hover-IN. Easing is a symmetric ease-in-out (smooth
   start AND smooth end) — the project's usual fast-start/slow-settle hover
   curve was tried here first and didn't read right on this crossfade. */
.step__label-value-rest {
  display: block;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1.1;
  letter-spacing: 0;
  white-space: nowrap;
  transition: opacity 0.85s cubic-bezier(0.65, 0, 0.35, 1);
}

.step__label-value-hover {
  position: absolute;
  left: 0;
  /* bottom-aligned against the container's own bottom (set by
     .step__label-value-rest, the only normal-flow child) so the two stay on
     the same baseline through the crossfade even if their sizing ever
     diverges again */
  bottom: 0;
  opacity: 0;
  pointer-events: none;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.8);
  white-space: nowrap;
  transition: opacity 0.25s cubic-bezier(0.65, 0, 0.35, 1);
}

.step:hover .step__label-value-rest {
  opacity: 0;
  transition-duration: 0.25s;
}

.step:hover .step__label-value-hover {
  opacity: 1;
  transition-duration: 0.85s;
}

.step__body {
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 18);
}

.step__caption {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 32);
  line-height: 1.1;
  color: var(--olive-100);
  white-space: pre;
}

.step__text {
  margin: 0;
  width: 100%;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  color: #2B3D2D;
}

/* .steps's mobile-width override, placed HERE (right after its own
   desktop rule block) rather than up with .gallery/.philosophy's — same
   selector `.steps`, same specificity as the desktop rule directly above,
   so source order alone decides which wins, and a media-query condition
   being true does NOT grant extra precedence over a later same-specificity
   rule outside any media query. Putting this fix earlier in the file (where
   .gallery/.philosophy's own overrides safely live, since THEIR desktop
   rules sit even earlier) silently lost to the desktop `height:calc(var(--u)*1623);
   overflow:hidden` above every time — the mobile carousel had completely
   correct markup and geometry (confirmed live) but was clipped to ~317px
   tall with nothing visibly wrong in the DOM. Same fix for
   .steps > .section-inner, for the identical reason. */
@media (max-width: 768px) {
  .steps {
    height: auto;
    overflow: visible;
  }

  .steps > .section-inner {
    position: static;
  }
}

/* --- mobile-only breakpoint (first in the project, scoped to the menu
   overlay only) — Figma node 1189:2267, 375px reference, plain px --- */

@media (max-width: 768px) {
  .menu-overlay__logo,
  .menu-overlay__close-icon,
  .menu-overlay__social--desktop,
  .menu-overlay__contact {
    display: none;
  }

  /* was #2B3D2D — a hard-edged dark-green band against the overlay's own
     blue-tinted background (.menu-overlay, var(--blue) at 98%) read as two
     separate things snapping in one after another as the curtain revealed
     them, not one continuous sheet. Matching the overlay's own blue
     removes that seam. */
  /* align-items:center previously put the logo at top:15.5px (centered in
     the 52px strip) — 6.5px off from .hero__logo's own top:22px, so it
     visibly shifted between the Hero header and the open menu even though
     it's meant to read as the same fixed element. flex-start + an
     explicit 22px top padding matches .hero__logo's position exactly
     (both measured from the viewport top, since both .hero and this strip
     start flush at y:0). */
  .menu-overlay__mobile-strip {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    height: 52px;
    padding: 22px 20px 0;
    background: var(--blue);
  }

  .menu-overlay__mobile-logo {
    display: block;
    width: 150px;
    height: 21px;
  }

  .menu-overlay__mobile-nav {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 80px 20px 0;
    font-family: 'Helvetica Neue', Arial, sans-serif;
    font-size: 32px;
    font-weight: 400;
    color: #2B3D2D;
  }

  .menu-overlay__mobile-link {
    color: inherit;
    text-decoration: none;
  }

  .menu-overlay__mobile-bottom {
    display: block;
    margin: 184px 20px 80px;
  }

  .menu-overlay__mobile-contact {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  .menu-overlay__mobile-mail,
  .menu-overlay__mobile-phone {
    color: rgba(43, 61, 45, 0.6);
  }

  .menu-overlay__mobile-mail {
    font-size: 16px;
  }

  .menu-overlay__mobile-phone {
    font-size: 32px;
  }

  .menu-overlay__social--mobile {
    margin-top: 40px;
    gap: 10px;
  }

  .menu-overlay__social--mobile .menu-overlay__social-link {
    font-size: 20px;
    line-height: normal;
    letter-spacing: -0.4px;
  }

  /* Revised: the Hero mobile macket (node 978:863, "btn-menu" x:305 y:24
     w:50) shows the plain "меню" text link at rest, same as desktop/tablet
     — not the hamburger icon. Icon-only was a blanket "mobile = icon"
     rule that predates having this macket to check against. Keep the icon
     for the OPEN state specifically: mobile still has no separate close
     button, so this same control needs to read as an X once the overlay
     is up.

     position:absolute, NOT fixed — she explicitly wants "меню" to live
     only in the Hero header and scroll away with it, same as desktop
     already behaves (.hero__menu-btn is a body-level sibling with no
     positioned ancestor, so absolute resolves against the page's initial
     containing block — right:20/top:24 lands it in the header at scroll
     0, then it scrolls normally with the rest of the page). This does
     mean the trigger is only reachable from the top of the page on
     mobile — worth flagging in case she wants a persistent trigger
     later, but this is what she asked for twice now.
     `body.menu-is-open { overflow:hidden }` (below) locks page scroll
     the instant the overlay opens, so the button (now showing as the X)
     stays visually put for the whole time it's open regardless — this
     was fixed's whole reason for existing, and absolute covers it too as
     long as that lock stays in place. */
  .hero__menu-btn {
    position: absolute;
    left: auto;
    top: 24px;
    right: 20px;
    font-size: 20px;
    letter-spacing: -0.4px;
    color: var(--beige-80);
    z-index: 9001;
  }

  /* same fix as .hero__btn-line-track above — base rule's
     bottom:calc(var(--u)*-6) collapses to ~-1.2px at mobile widths,
     reading as sitting right against the text instead of a real gap.
     Fixed px, same ~0.25 gap/font-size ratio as desktop's own 6u/24u. */
  .hero__menu-line-track {
    bottom: -5px;
  }

  /* Real bug found while checking this per her request: openMenu()/
     closeMenu() toggle body.menu-is-open synchronously (before the
     curtain tween even starts), so a plain `display` swap here snapped
     "меню" -> X in a single frame right at the tap, well before the
     curtain had covered that corner — read as a jump/jitter on the one
     control she was looking at. Fixed by keeping both label and icon
     permanently in the flow (icon absolutely centered over the label so
     it doesn't add height) and crossfading opacity instead of swapping
     display. */
  .hero__menu-icon {
    display: inline-flex;
    width: 36px;
    height: 36px;
    position: absolute;
    inset: 0;
    margin: auto;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .hero__menu-label,
  .hero__menu-line-track {
    transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1);
  }

  body.menu-is-open .hero__menu-icon {
    opacity: 1;
    pointer-events: auto;
  }

  body.menu-is-open .hero__menu-label,
  body.menu-is-open .hero__menu-line-track {
    opacity: 0;
    pointer-events: none;
  }
}

/* ==========================================================================
   POP-UP — course booking, tariff 1 "перший дотик" (Figma node 1386:1198,
   file 6tA1k2sLpFm5U7cLn9p9rZ, "pop-up-1-tarif"). Group A from
   project_artefact_popup_modals_scope.md — first of 3 course-booking
   variants (beige theme, 3-field form: ім'я/телефон/ел-пошта). Triggered
   by .pricing__card--basic .pricing__card-cta via [data-popup-open]. Same
   curtain mechanism as the other 2 popups, see openPopup()/closePopup()/
   initPopups() in script.js. No success screen yet (same as the other 2
   popups when first built) — submit only reportValidity()s.

   Heading images (img/pop-up-h-1.png, img/pop-up-h-2.png) are her own
   pre-rendered exports of the Figma text layers' blur-[12px] effect — NOT
   live CSS blur (off-limits project-wide, same exception precedent as the
   footer wordmark). Verified real alpha transparency at all 4 corners
   (not an opaque-background false positive — see the pricing-title PNG
   gotcha in BLOCK_STARTER_KIT.md) and confirmed both PNGs are flat 2x
   exports of their own Figma text node via a pixel-scan bounding box
   (PowerShell System.Drawing, alpha>15 threshold): dividing each image's
   measured ink bbox by 2 lands almost exactly on the node's own Figma
   width (1008u/1820u), confirming the scale factor rather than guessing
   it. Position/size below = (natural px ÷ 2) for size, offset by minus
   half the measured bleed margin so the image's blurred ink sits where
   Figma's original sharp text frame was, not the image's own canvas edge.
   ========================================================================== */

.popup-tariff-1 {
  position: fixed;
  inset: 0;
  z-index: 9100;
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
  background-color: var(--beige-100);
}

body.popup-is-open .popup-tariff-1 {
  pointer-events: auto;
}

.popup-tariff-1__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 50);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

.popup-tariff-1__heading {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 320);
}

.popup-tariff-1__title-img {
  position: absolute;
  left: calc(var(--u) * -10);
  top: calc(var(--u) * -24);
  width: calc(var(--u) * 1046);
  height: auto;
  display: block;
}

.popup-tariff-1__subtitle-img {
  position: absolute;
  left: calc(var(--u) * -24);
  top: calc(var(--u) * 107);
  width: calc(var(--u) * 1854);
  height: auto;
  display: block;
}

.popup-tariff-1__note {
  position: absolute;
  left: calc(var(--u) * 910);
  top: calc(var(--u) * 279);
  width: calc(var(--u) * 452);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 18);
  line-height: 1.5;
  color: #2B3D2D;
}

.popup-tariff-1__note .italic-symbol { margin-right: 10px; }

.popup-tariff-1__bottom {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 529.5);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 306);
}

.popup-tariff-1__txt {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 447);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  color: #2B3D2D;
  opacity: 0.6;
}

.popup-tariff-1__form-wrap {
  position: absolute;
  left: calc(var(--u) * 915);
  top: 0;
  width: calc(var(--u) * 905);
  height: calc(var(--u) * 306);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 100);
}

.popup-tariff-1__form {
  display: flex;
  flex-direction: column;
}

.popup-tariff-1__field {
  position: relative;
  display: flex;
  align-items: center;
  height: calc(var(--u) * 56);
}

/* line lives on its own element, not the input's own border — same
   recipe as .footer__subscribe-field/.popup-free-lesson__field (a CSS
   border anti-aliases at a visibly different weight than the flat
   height:1px lines everywhere else on the site). */
.popup-tariff-1__input {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  margin: 0;
  padding: calc(var(--u) * 6) 0 calc(var(--u) * 8);
  border: none;
  background: transparent;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 18);
  line-height: 1.1;
  color: #2B3D2D;
  outline: none;
}

.popup-tariff-1__input::placeholder {
  color: rgba(43, 61, 45, 0.4);
}

/* the "absurd transition delay" autofill fix — confirmed final version
   (round 5 on this project, see project_artefact_popup_modals_scope.md),
   background-agnostic unlike box-shadow-repaint or autocomplete="off". */
.popup-tariff-1__input:-webkit-autofill,
.popup-tariff-1__input:-webkit-autofill:hover,
.popup-tariff-1__input:-webkit-autofill:focus {
  -webkit-text-fill-color: #2B3D2D;
  transition: background-color 600000s 0s, color 600000s 0s;
}

.popup-tariff-1__field-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: #2B3D2D;
  pointer-events: none;
}

/* --- CTA: same static-underline recipe as every other CTA on the site
   (.craft__cta, .pricing__card-cta, .popup-free-lesson__cta, ...) — 8u
   padding-bottom, not Figma's literal pb-[2px] (that value was explicitly
   rejected project-wide already, see BLOCK_STARTER_KIT.md/
   project_artefact_status.md). --- */
.popup-tariff-1__cta {
  /* .popup-tariff-1__form-wrap is a column flex container with the default
     align-items:stretch, which was stretching this CTA's box (and its
     border-bottom with it) to the full 905u form width — align-self:
     flex-start opts it back out to shrink-wrap its own content, same as
     every other CTA on the site (.craft__cta/.pricing__card-cta use
     inline-flex specifically so this can't happen; this one sits inside a
     stretching flex parent instead, so it needs the explicit override). */
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding-bottom: calc(var(--u) * 8);
  border-bottom: 1px solid #2B3D2D;
  text-decoration: none;
}

.popup-tariff-1__cta-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1;
  color: #2B3D2D;
  white-space: nowrap;
}

.popup-tariff-1__cta-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1;
  overflow: hidden;
  height: 1em;
  color: #2B3D2D;
}

.popup-tariff-1__cta-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.popup-tariff-1__cta-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.popup-tariff-1__cta-plus-char--out {
  transform: translateY(0%);
}

.popup-tariff-1__cta-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .popup-tariff-1__cta-plus-char--in {
  transform: translateY(0%);
}

.js-line-hover:hover .popup-tariff-1__cta-plus-char--out {
  transform: translateY(-100%);
}

/* --- close button: no underline in Figma (unlike the CTA), just a
   text+icon pair — same "quiet" opacity-dim-on-hover recipe as
   .popup-free-lesson__close, just recolored dark for this light popup. --- */
/* wrapper carries the position AND the entrance stagger (.js-popup-stagger,
   fade+rise-from-below like every other group) — the button itself must
   never get that class directly, or GSAP's tween leaves an inline opacity
   that permanently blocks the button's own :hover rule (no clearProps is
   ever called). Real bug already hit once on this exact button, see
   project_artefact_popup_modals_scope.md. */
.popup-tariff-1__close-wrap {
  position: absolute;
  left: calc(var(--u) * 1644);
  top: calc(var(--u) * 945);
}

.popup-tariff-1__close {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding: 0 0 2px;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  color: #2B3D2D;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-tariff-1__close:hover {
    opacity: 0.6;
  }
}

.popup-tariff-1__close-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  white-space: nowrap;
}

.popup-tariff-1__close-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--u) * 30);
  height: calc(var(--u) * 30);
}

.popup-tariff-1__close-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ==========================================================================
   POP-UP — course booking, tariff 2 "мова глини" (Figma node 1386:1234,
   file 6tA1k2sLpFm5U7cLn9p9rZ, "pop-up-2-tarif"). Group A, second of 3
   course-booking variants — same layout skeleton as .popup-tariff-1, but
   OLIVE theme: bg var(--olive-100), body text/field labels/lines/close all
   light beige, CTA text/border stays DARK green #2B3D2D on purpose — same
   convention .pricing__card--featured already uses for its own olive card
   (dark CTA reads as the accent against the olive, not an inversion bug).
   data-cursor-theme="dark" (not "light") to match every other olive/dark
   section (.cta, .hero, ...) — the custom cursor renders light over dark.

   Content box is offset left:55u (not 50u) and close-btn/txt/form-wrap
   x-values also differ slightly from tariff-1/3's own numbers (910u form
   start here vs 915u there, 910u form width vs 905u) — read straight off
   this node's own Figma metadata, not copied from the other 2 variants;
   every popup in this group has small per-design metric differences.

   Heading images (pop-up-h-5.png, pop-up-h-6.png) verified with the same
   pixel-scan technique as the other 2 popups' own images (real alpha
   transparency at all 4 corners, ~2x export confirmed: 810/1607=0.504 and
   1820/3603=0.505 against this node's own Figma text widths). Close
   button intentionally does NOT get .js-popup-stagger — giving GSAP's
   stagger tween direct control of an element's inline opacity permanently
   blocks that element's own CSS :hover rule (no clearProps was ever
   called), a real bug found and fixed on tariff-1/tariff-3's close
   buttons; this one skips it from the start. Same reason align-self:
   flex-start is on the CTA from the start, not added after the fact.
   ========================================================================== */

.popup-tariff-2 {
  position: fixed;
  inset: 0;
  z-index: 9100;
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
  background-color: var(--olive-100);
}

body.popup-is-open .popup-tariff-2 {
  pointer-events: auto;
}

.popup-tariff-2__content {
  position: absolute;
  left: calc(var(--u) * 55);
  top: calc(var(--u) * 50);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

.popup-tariff-2__heading {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 320);
}

.popup-tariff-2__title-img {
  position: absolute;
  left: calc(var(--u) * -10);
  top: calc(var(--u) * -13);
  width: calc(var(--u) * 837);
  height: auto;
  display: block;
}

.popup-tariff-2__subtitle-img {
  position: absolute;
  left: calc(var(--u) * -24);
  top: calc(var(--u) * 104);
  width: calc(var(--u) * 1843.5);
  height: auto;
  display: block;
}

.popup-tariff-2__note {
  position: absolute;
  left: calc(var(--u) * 1063);
  top: calc(var(--u) * 282);
  width: calc(var(--u) * 359);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 18);
  line-height: 1.1;
  color: #2B3D2D;
}

.popup-tariff-2__note .italic-symbol { margin-right: 10px; }

.popup-tariff-2__bottom {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 529.5);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 306);
}

.popup-tariff-2__txt {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 447);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  color: var(--beige-100);
  opacity: 0.6;
}

.popup-tariff-2__form-wrap {
  position: absolute;
  left: calc(var(--u) * 910);
  top: 0;
  width: calc(var(--u) * 910);
  height: calc(var(--u) * 306);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 100);
}

.popup-tariff-2__form {
  display: flex;
  flex-direction: column;
}

.popup-tariff-2__field {
  position: relative;
  display: flex;
  align-items: center;
  height: calc(var(--u) * 56);
}

.popup-tariff-2__input {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  margin: 0;
  padding: calc(var(--u) * 6) 0 calc(var(--u) * 8);
  border: none;
  background: transparent;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 18);
  line-height: 1.1;
  color: var(--beige-100);
  outline: none;
}

.popup-tariff-2__input::placeholder {
  color: var(--beige-40);
}

.popup-tariff-2__input:-webkit-autofill,
.popup-tariff-2__input:-webkit-autofill:hover,
.popup-tariff-2__input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--beige-100);
  transition: background-color 600000s 0s, color 600000s 0s;
}

.popup-tariff-2__field-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--beige-100);
  pointer-events: none;
}

/* CTA text/border stays dark green even on this olive background — same
   accent convention as .pricing__card--featured .pricing__card-cta. */
.popup-tariff-2__cta {
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding-bottom: calc(var(--u) * 8);
  border-bottom: 1px solid #2B3D2D;
  text-decoration: none;
}

.popup-tariff-2__cta-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1;
  color: #2B3D2D;
  white-space: nowrap;
}

.popup-tariff-2__cta-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1;
  overflow: hidden;
  height: 1em;
  color: #2B3D2D;
}

.popup-tariff-2__cta-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.popup-tariff-2__cta-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.popup-tariff-2__cta-plus-char--out {
  transform: translateY(0%);
}

.popup-tariff-2__cta-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .popup-tariff-2__cta-plus-char--in {
  transform: translateY(0%);
}

.js-line-hover:hover .popup-tariff-2__cta-plus-char--out {
  transform: translateY(-100%);
}

/* same wrapper split as .popup-tariff-1__close-wrap — see its comment. */
.popup-tariff-2__close-wrap {
  position: absolute;
  left: calc(var(--u) * 1644);
  top: calc(var(--u) * 945);
}

.popup-tariff-2__close {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding: 0 0 2px;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  color: var(--beige-100);
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-tariff-2__close:hover {
    opacity: 0.6;
  }
}

.popup-tariff-2__close-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  white-space: nowrap;
}

.popup-tariff-2__close-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--u) * 30);
  height: calc(var(--u) * 30);
}

.popup-tariff-2__close-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ==========================================================================
   POP-UP — course booking, tariff 3 "професійний рівень" (Figma node
   1386:1251, file 6tA1k2sLpFm5U7cLn9p9rZ, "pop-up-3-tarif"). Group A,
   third of 3 course-booking variants — same beige theme + layout as
   .popup-tariff-1 (see its own comment block for the full rationale:
   heading images are her pre-rendered blur exports, not live CSS blur;
   CTA uses align-self:flex-start inside the stretching form-wrap column,
   same fix applied from the start here). Triggered by .pricing__card--pro
   .pricing__card-cta via [data-popup-open]. Only real structural diff:
   4 fields, not 3 (adds "коротко про ваш досвід / ідею", left optional —
   not marked required, unlike the other 3 fields), so .popup-tariff-3__bottom/
   __form-wrap/__form are all taller than tariff-1's (224u form vs 168u,
   362u bottom vs 306u) — verified against this node's own metadata, not
   assumed identical to tariff-1's numbers.

   Same pixel-scan verification as tariff-1's own images: both PNGs
   confirmed real alpha transparency at all 4 corners and land within
   ~0.1% of an exact 2x export of their Figma text node (scale computed
   from measured ink bbox ÷ Figma's own text width — 1374/2745=0.5005 and
   1820/3633=0.5011), same PowerShell/System.Drawing technique. Bleed
   margins differ from tariff-1's own two images (different glyphs blur
   differently) — each image's offset below is its OWN measured bleed,
   not copied from tariff-1. ========================================================================== */

.popup-tariff-3 {
  position: fixed;
  inset: 0;
  z-index: 9100;
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
  background-color: var(--beige-100);
}

body.popup-is-open .popup-tariff-3 {
  pointer-events: auto;
}

.popup-tariff-3__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 50);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

.popup-tariff-3__heading {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 320);
}

.popup-tariff-3__title-img {
  position: absolute;
  left: calc(var(--u) * -10);
  top: calc(var(--u) * -24);
  width: calc(var(--u) * 1406.5);
  height: auto;
  display: block;
}

.popup-tariff-3__subtitle-img {
  position: absolute;
  left: calc(var(--u) * -24);
  top: calc(var(--u) * 114);
  width: calc(var(--u) * 1853);
  height: auto;
  display: block;
}

.popup-tariff-3__note {
  position: absolute;
  left: calc(var(--u) * 910);
  top: calc(var(--u) * 287);
  width: calc(var(--u) * 475);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 18);
  line-height: 1.1;
  color: #2B3D2D;
}

.popup-tariff-3__note .italic-symbol { margin-right: 10px; }

.popup-tariff-3__bottom {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 501.5);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 362);
}

.popup-tariff-3__txt {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 447);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  color: #2B3D2D;
  opacity: 0.6;
}

.popup-tariff-3__form-wrap {
  position: absolute;
  left: calc(var(--u) * 915);
  top: 0;
  width: calc(var(--u) * 905);
  height: calc(var(--u) * 362);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 100);
}

.popup-tariff-3__form {
  display: flex;
  flex-direction: column;
}

.popup-tariff-3__field {
  position: relative;
  display: flex;
  align-items: center;
  height: calc(var(--u) * 56);
}

.popup-tariff-3__input {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  margin: 0;
  padding: calc(var(--u) * 6) 0 calc(var(--u) * 8);
  border: none;
  background: transparent;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 18);
  line-height: 1.1;
  color: #2B3D2D;
  outline: none;
}

.popup-tariff-3__input::placeholder {
  color: rgba(43, 61, 45, 0.4);
}

.popup-tariff-3__input:-webkit-autofill,
.popup-tariff-3__input:-webkit-autofill:hover,
.popup-tariff-3__input:-webkit-autofill:focus {
  -webkit-text-fill-color: #2B3D2D;
  transition: background-color 600000s 0s, color 600000s 0s;
}

.popup-tariff-3__field-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: #2B3D2D;
  pointer-events: none;
}

/* align-self:flex-start — .popup-tariff-3__form-wrap is a column flex
   container (default align-items:stretch), which would otherwise stretch
   this CTA's border-bottom to the full 905u form width. Same real bug
   already fixed on .popup-tariff-1__cta, applied from the start here. */
.popup-tariff-3__cta {
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding-bottom: calc(var(--u) * 8);
  border-bottom: 1px solid #2B3D2D;
  text-decoration: none;
}

.popup-tariff-3__cta-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1;
  color: #2B3D2D;
  white-space: nowrap;
}

.popup-tariff-3__cta-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1;
  overflow: hidden;
  height: 1em;
  color: #2B3D2D;
}

.popup-tariff-3__cta-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.popup-tariff-3__cta-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.popup-tariff-3__cta-plus-char--out {
  transform: translateY(0%);
}

.popup-tariff-3__cta-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .popup-tariff-3__cta-plus-char--in {
  transform: translateY(0%);
}

.js-line-hover:hover .popup-tariff-3__cta-plus-char--out {
  transform: translateY(-100%);
}

/* same wrapper split as .popup-tariff-1__close-wrap — see its comment. */
.popup-tariff-3__close-wrap {
  position: absolute;
  left: calc(var(--u) * 1644);
  top: calc(var(--u) * 945);
}

.popup-tariff-3__close {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding: 0 0 2px;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  color: #2B3D2D;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-tariff-3__close:hover {
    opacity: 0.6;
  }
}

.popup-tariff-3__close-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  white-space: nowrap;
}

.popup-tariff-3__close-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--u) * 30);
  height: calc(var(--u) * 30);
}

.popup-tariff-3__close-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ==========================================================================
   POP-UP — shared course-booking success screen (Figma node 1386:1216,
   file 6tA1k2sLpFm5U7cLn9p9rZ, "pop-up-confirm"). Group A's shared final
   screen, opened by all 3 tariff forms (.popup-tariff-1/2/3) on a valid
   submit — see the wiring in initPopups(). Beige bg per this node's own
   Figma data (checked directly — the plain heading has no blur-[12px]
   class at all here, unlike the 3 tariff headings, so it's live CSS text
   with 3 opacity tiers, same recipe as .popup-subscription__word, not a
   pre-rendered image). The 2 rounded-rect photo slots use her real
   exports (popup-final-1.png/popup-final-2.png), identified by opening
   both files and matching against the Figma screenshot's actual photo
   content, not by filename order. Figma's own layer tree has the
   top-right photo duplicated (2 separate node ids, byte-identical
   position/image) — rendered once here, not twice.
   ========================================================================== */

.popup-confirm {
  position: fixed;
  inset: 0;
  z-index: 9100;
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
  background-color: var(--beige-100);
}

body.popup-is-open .popup-confirm {
  pointer-events: auto;
}

.popup-confirm__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 50);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

.popup-confirm__heading {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 991);
}

.popup-confirm__line {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
  text-transform: uppercase;
  color: #2B3D2D;
  white-space: nowrap;
}

.popup-confirm__line--1 { opacity: 1; }
.popup-confirm__line--2 { opacity: 0.8; }
.popup-confirm__line--3 { opacity: 0.6; }

.popup-confirm__photo-top {
  position: absolute;
  left: calc(var(--u) * 1525);
  top: 0;
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
  overflow: hidden;
}

.popup-confirm__photo-bottom {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 685);
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
  overflow: hidden;
}

.popup-confirm__photo-top img,
.popup-confirm__photo-bottom img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-confirm__photo-top:hover img,
  .popup-confirm__photo-bottom:hover img {
    transform: scale(1.05);
  }
}

.popup-confirm__note {
  position: absolute;
  left: calc(var(--u) * 915);
  top: calc(var(--u) * 484);
  width: calc(var(--u) * 905);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -1.28);
  color: var(--olive-100);
}

/* bottom:0 (not a top offset) — her catch 2026-08-08: this block read as
   disconnected/floating above .popup-confirm__close-wrap, which sits at
   945u/content height 980u i.e. already flush with the content box's own
   bottom edge. Anchoring this block to the same bottom edge puts both
   groups at the same visual "level" instead of leaving dead space
   between the social links row and закрити. */
.popup-confirm__social {
  position: absolute;
  left: calc(var(--u) * 915);
  bottom: 0;
  width: calc(var(--u) * 512);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 40);
}

.popup-confirm__social-label {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  color: var(--olive-100);
  opacity: 0.6;
}

.popup-confirm__social-links {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 40);
}

/* hover mechanism verbatim from .menu-overlay__social-link/
   .footer__social-link — see either's own comment for the full
   reasoning. Text dims + shifts right; an italic "(" hidden at rest
   (no layout space, position:absolute) fades in to fill the gap the
   shift opens up. Font-size lives on the <a> itself so the paren
   inherits it (her catch on the footer version — a paren with no
   font-size of its own renders at the page's base size instead). */
.popup-confirm__social-link {
  position: relative;
  display: inline-flex;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1;
  color: #2B3D2D;
  text-decoration: none;
  white-space: nowrap;
}

.popup-confirm__social-paren {
  position: absolute;
  left: 0;
  opacity: 0;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.popup-confirm__social-text {
  display: inline-block;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-confirm__social-link:hover .popup-confirm__social-paren {
    opacity: 0.6;
  }

  .popup-confirm__social-link:hover .popup-confirm__social-text {
    opacity: 0.6;
    transform: translateX(calc(0.5em + 10px));
  }
}

/* same wrapper/stagger split as .popup-tariff-1__close-wrap — see its
   comment. Dark green, matching this screen's own beige bg (not olive). */
.popup-confirm__close-wrap {
  position: absolute;
  left: calc(var(--u) * 1644);
  top: calc(var(--u) * 945);
}

.popup-confirm__close {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding: 0 0 2px;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  color: #2B3D2D;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-confirm__close:hover {
    opacity: 0.6;
  }
}

.popup-confirm__close-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  white-space: nowrap;
}

.popup-confirm__close-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--u) * 30);
  height: calc(var(--u) * 30);
}

.popup-confirm__close-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ==========================================================================
   POP-UP — "free lesson" lead magnet (Figma node 1386:1135, file
   6tA1k2sLpFm5U7cLn9p9rZ, "pop-up-free lesson"). First of 3 popup
   mechanics scoped 2026-08-04 (see project_artefact_popup_modals_scope.md)
   — this is Group B, the free-lesson lead magnet. Triggered by
   .free-lesson__cta (block 8's "free вебинар" button) via
   [data-popup-open]. Same fixed full-viewport + clip-path curtain
   mechanism as .menu-overlay, just its own brown theme — see
   initPopups()/openPopup()/closePopup() in script.js.

   Content sits on a 1920x1080 reference frame, same --u system as every
   desktop block, inset by the site's own 50u --grid-margin.
   ========================================================================== */

.popup-free-lesson {
  position: fixed;
  inset: 0;
  z-index: 9100;
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
}

body.popup-is-open {
  overflow: hidden;
}

body.popup-is-open .popup-free-lesson {
  pointer-events: auto;
}

.popup-free-lesson__bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background-color: var(--brown-700);
  pointer-events: none;
}

/* Figma's own "gradient" group here is the same asset as block 8's own
   background (4 heavily Gaussian-blurred rotated ellipses) — reusing
   .free-lesson__gradient's already-approved radial-gradient approximation
   verbatim (real blur filters are off-limits project-wide) rather than
   re-deriving it from scratch. */
.popup-free-lesson__gradient {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse 70% 85% at 55% 62%, rgba(126, 100, 84, 0.55) 0%, rgba(126, 100, 84, 0) 70%),
    radial-gradient(ellipse 60% 75% at 82% 52%, rgba(100, 71, 55, 0.5) 0%, rgba(100, 71, 55, 0) 70%),
    radial-gradient(ellipse 45% 55% at 60% 88%, rgba(91, 64, 50, 0.55) 0%, rgba(91, 64, 50, 0) 70%),
    radial-gradient(ellipse 55% 60% at 6% 4%, rgba(0, 0, 0, 0.22) 0%, rgba(0, 0, 0, 0) 65%);
}

.popup-free-lesson__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 50);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

/* --- heading, same typography as .free-lesson__word (128u, uppercase,
   -3.84u tracking) — this popup's own opacity tiers differ slightly from
   block 8's (её "до" is 80% here, not 40%), so it's its own class set,
   not a shared one, per the starter kit's no-cross-block-reuse rule. --- */

.popup-free-lesson__line {
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(var(--u) * 128);
  margin: 0;
}

.popup-free-lesson__line--1 { top: 0; }
.popup-free-lesson__line--2 { top: calc(var(--u) * 128); }
.popup-free-lesson__line--3 { top: calc(var(--u) * 256); }

.popup-free-lesson__word {
  position: absolute;
  top: 0;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
  text-transform: uppercase;
  white-space: nowrap;
}

.popup-free-lesson__word--nohe { left: 0; color: var(--beige-100); }
.popup-free-lesson__word--gotovi { left: 0; color: var(--beige-80); }
.popup-free-lesson__word--do { left: calc(var(--u) * 1177); color: var(--beige-80); }
.popup-free-lesson__word--povno { left: 0; color: var(--beige-60); }
.popup-free-lesson__word--course { left: calc(var(--u) * 1418); color: var(--beige-60); }

.popup-free-lesson__mark {
  position: absolute;
  right: 0;
  top: 0;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
  color: var(--beige-100);
}

/* --- email field: same "line lives on its own element, not the input's
   own border" recipe as .footer__subscribe-field (a CSS border anti-
   aliases at a visibly different weight than the flat height:1px lines
   everywhere else on the site). Figma has this line fully opaque at rest
   (not dimmed like the footer's), so there's no separate hover/focus
   state to add here — it's already the "engaged" look. --- */

.popup-free-lesson__field {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 444);
  width: calc(var(--u) * 905);
}

.popup-free-lesson__input {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  margin: 0;
  padding: calc(var(--u) * 14) 0 calc(var(--u) * 16);
  border: none;
  background: transparent;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  color: var(--beige-100);
  outline: none;
}

.popup-free-lesson__input::placeholder {
  color: var(--beige-40);
}

/* same background-agnostic autofill fix as .footer__subscribe-input — see
   its own comment for why (box-shadow-repaint and autocomplete="off" were
   both tried first and neither actually worked against a gradient bg). */
.popup-free-lesson__input:-webkit-autofill,
.popup-free-lesson__input:-webkit-autofill:hover,
.popup-free-lesson__input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--beige-100);
  transition: background-color 600000s 0s, color 600000s 0s;
}

.popup-free-lesson__field-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--beige-100);
  pointer-events: none;
}

/* --- course-image row + "( отримайте... )" text/CTA column --- */

.popup-free-lesson__images {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 685);
  height: calc(var(--u) * 295);
  display: flex;
  gap: calc(var(--u) * 10);
}

.popup-free-lesson__img {
  flex: 0 0 auto;
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
  overflow: hidden;
}

.popup-free-lesson__img img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

/* her call 2026-08-07: "add at least a little movement" to the popup
   photos — plain CSS hover-scale (the frame's own overflow:hidden already
   crops it), no JS needed since there's no scroll happening inside a
   fixed-position popup to also parallax against. */
@media (hover: hover) and (pointer: fine) {
  .popup-free-lesson__img:hover img {
    transform: scale(1.05);
  }
}

/* her call 2026-08-07: level with the photo's own top edge (685u), not
   Figma's original 60u-lower offset (745u) — raised to align with
   .popup-free-lesson__images. */
/* height:295u + space-between (not gap) — her call 2026-08-07: the
   отримати/закрити row should sit level with the photo's own BOTTOM edge
   (295u tall, same as .popup-free-lesson__images), not just trail 9u below
   the text block. */
.popup-free-lesson__info {
  position: absolute;
  left: calc(var(--u) * 915);
  top: calc(var(--u) * 685);
  width: calc(var(--u) * 905);
  height: calc(var(--u) * 295);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
}

.popup-free-lesson__txt {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -2);
  color: var(--beige-100);
}

.popup-free-lesson__txt-paren { margin-right: 12px; }
.popup-free-lesson__txt-paren--close { margin-right: 0; margin-left: 40px; }

.popup-free-lesson__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
}

/* --- CTA: identical recipe to .free-lesson__cta (static underline, not
   the sweep — same reasoning as pricing's CTA underline, see
   project_artefact_block10_pricing.md), just as a real <button> now. --- */
.popup-free-lesson__cta {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding-bottom: calc(var(--u) * 8);
  border-bottom: 1px solid var(--beige-100);
}

.popup-free-lesson__cta-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1.1;
  color: var(--beige-100);
  white-space: nowrap;
}

/* two-line icon swap — same recipe as .free-lesson__cta-plus verbatim. */
.popup-free-lesson__cta-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1;
  overflow: hidden;
  height: 1em;
  color: var(--beige-100);
}

.popup-free-lesson__cta-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.popup-free-lesson__cta-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.popup-free-lesson__cta-plus-char--out {
  transform: translateY(0%);
}

.popup-free-lesson__cta-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .popup-free-lesson__cta-plus-char--in {
  transform: translateY(0%);
}

.js-line-hover:hover .popup-free-lesson__cta-plus-char--out {
  transform: translateY(-100%);
}

/* --- close button: no underline in Figma (unlike the CTA), just a
   text+icon pair — a plain opacity dim on hover instead, same feedback
   .faq__toggle already uses for a similar "quiet" control. --- */
.popup-free-lesson__close {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding: 0 0 2px;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  color: var(--beige-100);
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-free-lesson__close:hover {
    opacity: 0.6;
  }
}

.popup-free-lesson__close-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  white-space: nowrap;
}

.popup-free-lesson__close-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--u) * 30);
  height: calc(var(--u) * 30);
}

.popup-free-lesson__close-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ==========================================================================
   POP-UP — free-lesson lead-magnet success screen (Figma node 1386:1176,
   file 6tA1k2sLpFm5U7cLn9p9rZ, "pop-up-free lesson-confirmation"). Group
   B's missing second half — .popup-free-lesson is the form, this is the
   success screen it was always meant to lead to (see
   project_artefact_popup_modals_scope.md). Same brown theme + background
   gradient as .popup-free-lesson (identical asset, reused verbatim — see
   its own comment for why the gradient is a radial-gradient
   approximation, not a real blur filter). No CTA button on this screen —
   just the promo code, a video thumbnail, and close.

   Video thumbnail (img/popup-free-lesson-confirm-video.png) is a genuinely
   NEW photo (hands on the wheel), not a repeat of
   img/free-lesson-course.png (the vase) — confirmed by opening both files
   and comparing, not assumed from either filename. Play icon is the exact
   lucide/play SVG downloaded from this node's own Figma asset (never
   hand-drawn), recolored to currentColor so it inherits beige-100 like
   every other inlined icon on the site. ========================================================================== */

.popup-free-lesson-confirm {
  position: fixed;
  inset: 0;
  z-index: 9100;
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
}

body.popup-is-open .popup-free-lesson-confirm {
  pointer-events: auto;
}

.popup-free-lesson-confirm__bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background-color: var(--brown-700);
  pointer-events: none;
}

.popup-free-lesson-confirm__gradient {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse 70% 85% at 55% 62%, rgba(126, 100, 84, 0.55) 0%, rgba(126, 100, 84, 0) 70%),
    radial-gradient(ellipse 60% 75% at 82% 52%, rgba(100, 71, 55, 0.5) 0%, rgba(100, 71, 55, 0) 70%),
    radial-gradient(ellipse 45% 55% at 60% 88%, rgba(91, 64, 50, 0.55) 0%, rgba(91, 64, 50, 0) 70%),
    radial-gradient(ellipse 55% 60% at 6% 4%, rgba(0, 0, 0, 0.22) 0%, rgba(0, 0, 0, 0) 65%);
}

.popup-free-lesson-confirm__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 50);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

.popup-free-lesson-confirm__heading {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 384);
}

.popup-free-lesson-confirm__line {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
  text-transform: uppercase;
  color: var(--beige-100);
}

.popup-free-lesson-confirm__line--2 { color: var(--beige-80); }
.popup-free-lesson-confirm__line--3 { color: var(--beige-60); }

.popup-free-lesson-confirm__lesson {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 508);
  width: calc(var(--u) * 905);
}

.popup-free-lesson-confirm__note {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -1.28);
  color: var(--beige-100);
}

/* absolute, NOT a margin-top flow offset — .lesson's own 177u in Figma is
   an offset from ITS top-left, not a gap after the note paragraph's real
   rendered height. A margin-top here added 177u AFTER whatever height the
   note wrapped to (~116u), landing the video ~115u too low and badly
   misaligned with .popup-free-lesson-confirm__right's own bottom-flush
   promo-code/close row — her catch 2026-08-08, confirmed live (video
   bottom was overflowing past the content box's own bottom edge). */
.popup-free-lesson-confirm__video {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 177);
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
  overflow: hidden;
  appearance: none;
  -webkit-appearance: none;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

.popup-free-lesson-confirm__video-el {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-free-lesson-confirm__video:hover .popup-free-lesson-confirm__video-el {
    transform: scale(1.05);
  }
}

.popup-free-lesson-confirm__right {
  position: absolute;
  left: calc(var(--u) * 915);
  top: calc(var(--u) * 685);
  width: calc(var(--u) * 458);
}

.popup-free-lesson-confirm__promo-label {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  color: var(--beige-100);
  opacity: 0.6;
}

.popup-free-lesson-confirm__promo-code {
  margin: calc(var(--u) * 138) 0 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -1.28);
  color: var(--beige-100);
}

.popup-free-lesson-confirm__note .italic-symbol,
.popup-free-lesson-confirm__promo-code .italic-symbol {
  margin-right: 10px;
}

/* wrapper carries the position AND the entrance stagger — the button
   itself must never get .js-popup-stagger directly, see
   feedback_nav_underline_settings_locked.md's sibling lesson on
   .popup-tariff-1__close-wrap for why (GSAP leaves an inline opacity that
   blocks :hover otherwise). */
.popup-free-lesson-confirm__close-wrap {
  position: absolute;
  left: calc(var(--u) * 1644);
  top: calc(var(--u) * 945);
}

.popup-free-lesson-confirm__close {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding: 0 0 2px;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  color: var(--beige-100);
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-free-lesson-confirm__close:hover {
    opacity: 0.6;
  }
}

.popup-free-lesson-confirm__close-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  white-space: nowrap;
}

.popup-free-lesson-confirm__close-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--u) * 30);
  height: calc(var(--u) * 30);
}

.popup-free-lesson-confirm__close-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ==========================================================================
   POP-UP — footer newsletter confirmation (Figma node 1386:1114, file
   6tA1k2sLpFm5U7cLn9p9rZ, "pop-up-subscription"). Group C — confirmation
   screen only, no form of its own (real form is .footer__subscribe-form,
   in the FOOTER section below). Same curtain mechanism as .popup-free-
   lesson, see openPopup()/closePopup()/initPopups() in script.js.

   Two of the 3 bottom-row photo slots are opacity-0 in Figma itself (not
   just an unfinished placeholder like the other popup) — genuinely only
   1 photo here, matches her explicit "only the leftmost photo" call on
   the free-lesson popup too.
   ========================================================================== */

.popup-subscription {
  position: fixed;
  inset: 0;
  z-index: 9100;
  clip-path: inset(0% 0% 100% 0%);
  pointer-events: none;
  overflow: hidden;
  background-color: var(--blue);
}

body.popup-is-open .popup-subscription {
  pointer-events: auto;
}

.popup-subscription__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 50);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

.popup-subscription__top {
  position: relative;
  width: 100%;
  height: calc(var(--u) * 512);
}

.popup-subscription__heading {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--u) * 1068);
  height: calc(var(--u) * 512);
}

/* absolute, not flex+space-between — the first pass used space-between,
   which pushed "ЗА"/"НА" flush to the line's far right edge (~905u/896u
   worth of gap) instead of Figma's real, much narrower gap (ЗА sits at
   533u, НА at 896u — neither is edge-flush). Match the literal metadata
   left values per word, same convention .popup-free-lesson's heading
   already uses, not a flexible layout trick. */
.popup-subscription__line {
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(var(--u) * 128);
  margin: 0;
}

.popup-subscription__line--1 { top: 0; }
.popup-subscription__line--2 { top: calc(var(--u) * 128); }
.popup-subscription__line--3 { top: calc(var(--u) * 256); }
.popup-subscription__line--4 { top: calc(var(--u) * 384); }

.popup-subscription__word {
  position: absolute;
  top: 0;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
  text-transform: uppercase;
  white-space: nowrap;
}

.popup-subscription__word--dakuyu { left: 0; }
.popup-subscription__word--za { left: calc(var(--u) * 533); }
.popup-subscription__word--pidpysku { left: 0; }
.popup-subscription__word--na { left: calc(var(--u) * 896); }
.popup-subscription__word--lysty { left: 0; }
.popup-subscription__word--tvorchyh { left: 0; }

.popup-subscription__line--1 .popup-subscription__word { color: #2B3D2D; }
.popup-subscription__line--2 .popup-subscription__word { color: rgba(43, 61, 45, 0.8); }
.popup-subscription__line--3 .popup-subscription__word { color: rgba(43, 61, 45, 0.6); }
.popup-subscription__line--4 .popup-subscription__word { color: rgba(43, 61, 45, 0.4); }

.popup-subscription__photo-top {
  position: absolute;
  left: calc(var(--u) * 1525);
  top: 0;
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
  overflow: hidden;
}

.popup-subscription__photo-top img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-subscription__photo-top:hover img {
    transform: scale(1.05);
  }
}

.popup-subscription__bottom {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 685);
  width: calc(var(--u) * 1515);
  height: calc(var(--u) * 295);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}

.popup-subscription__images {
  display: flex;
  gap: calc(var(--u) * 10);
}

.popup-subscription__img {
  flex: 0 0 auto;
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
  overflow: hidden;
}

.popup-subscription__img img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .popup-subscription__img:hover img {
    transform: scale(1.05);
  }
}

.popup-subscription__info {
  width: calc(var(--u) * 600);
  height: calc(var(--u) * 295);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
}

.popup-subscription__txt {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -2);
  color: var(--beige-100);
}

/* --- close: same underline+plus CTA recipe as .craft__cta (dark green on
   a light background, unlike .popup-free-lesson's beige-on-brown), just
   this popup's own class names. --- */
.popup-subscription__close {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  padding-bottom: calc(var(--u) * 8);
  border-bottom: 1px solid #2B3D2D;
}

.popup-subscription__close-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1;
  color: #2B3D2D;
  white-space: nowrap;
}

.popup-subscription__close-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1;
  overflow: hidden;
  height: 1em;
  color: #2B3D2D;
}

.popup-subscription__close-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.popup-subscription__close-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.popup-subscription__close-plus-char--out {
  transform: translateY(0%);
}

.popup-subscription__close-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .popup-subscription__close-plus-char--in {
  transform: translateY(0%);
}

.js-line-hover:hover .popup-subscription__close-plus-char--out {
  transform: translateY(-100%);
}

/* ==========================================================================
   FREE LESSON SECTION — "8_free lesson" (Figma node 978:1326, file
   6tA1k2sLpFm5U7cLn9p9rZ). Pixel Perfect only for now — no entrance or
   hover motion wired yet, verstka pass.
   ========================================================================== */

.free-lesson {
  position: relative;
  width: 100%;
  height: calc(var(--u) * 1207);
  overflow: hidden;
  /* same fractional-devicePixelRatio hairline gotcha as every other block
     seam so far (5/6, 6/7) — .steps is beige, this section is dark brown,
     so a 1px gap here would be far more visible than most. */
  margin: -2px 0 0;
  background-color: #5E4133;
}

/* Figma's "gradient" group is 4 heavily Gaussian-blurred (feGaussianBlur
   100-250) rotated ellipses composited together — a real blur filter,
   which is off-limits here (project hard rule). Approximated instead with
   layered radial-gradients in the same 4 hues at roughly the same screen
   positions: no filter, same soft warm diagonal wash. Flag to her if the
   fit needs to be tighter — the exact Figma asset route (pre-baked blur,
   same precedent as mova-clay.png) is the fallback if this reads too flat. */
.free-lesson__gradient {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse 70% 85% at 55% 62%, rgba(126, 100, 84, 0.55) 0%, rgba(126, 100, 84, 0) 70%),
    radial-gradient(ellipse 60% 75% at 82% 52%, rgba(100, 71, 55, 0.5) 0%, rgba(100, 71, 55, 0) 70%),
    radial-gradient(ellipse 45% 55% at 60% 88%, rgba(91, 64, 50, 0.55) 0%, rgba(91, 64, 50, 0) 70%),
    radial-gradient(ellipse 55% 60% at 6% 4%, rgba(0, 0, 0, 0.22) 0%, rgba(0, 0, 0, 0) 65%);
}

.free-lesson__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 150);
  width: calc(var(--u) * 1820);
  height: calc(var(--u) * 980);
}

.free-lesson__heading {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: calc(var(--u) * 402);
}

.free-lesson__line {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: calc(var(--u) * 128);
  margin: 0;
}

.free-lesson__line--2 {
  top: calc(var(--u) * 128);
}

.free-lesson__line--3 {
  top: calc(var(--u) * 256);
  height: calc(var(--u) * 146);
}

.free-lesson__word {
  position: absolute;
  top: 0;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
  text-transform: uppercase;
  white-space: nowrap;
}

.free-lesson__word--nohe {
  left: 0;
  color: var(--beige-100);
}

.free-lesson__mark {
  position: absolute;
  right: 0;
  top: 0;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
  color: var(--beige-100);
}

.free-lesson__word--gotovi {
  left: 0;
  color: var(--beige-80);
}

.free-lesson__word--do {
  left: calc(var(--u) * 1177);
  color: var(--beige-40);
}

.free-lesson__word--povno {
  left: 0;
  color: var(--beige-60);
}

.free-lesson__word--course {
  right: 0;
  color: var(--beige-60);
}

.free-lesson__eyebrow {
  position: absolute;
  left: calc(var(--u) * 568);
  top: calc(var(--u) * 77);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1;
  letter-spacing: calc(var(--u) * -0.6);
  color: var(--beige-100);
  white-space: nowrap;
}

.free-lesson__media {
  position: absolute;
  left: calc(var(--u) * 458);
  top: calc(var(--u) * 612);
  width: calc(var(--u) * 295);
  height: calc(var(--u) * 295);
  overflow: hidden;
}

/* oversized + centered, same as .craft__art-1-img: covers the scroll-linked
   yPercent:-15 drift (mediaImageGroups() in script.js) without exposing
   empty space at the frame's bottom edge. */
.free-lesson__media-img {
  position: absolute;
  left: 0;
  top: -25%;
  width: 100%;
  height: 150%;
  object-fit: cover;
}

.free-lesson__txt {
  position: absolute;
  left: calc(var(--u) * 914);
  top: calc(var(--u) * 612);
  width: calc(var(--u) * 580);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  /* Figma has 0.9 leading per line PLUS a 7u gap between lines; merged into
     one line-height now that it's a single <p> with <br> (needed so
     splitWords() can stagger every word — see runFreeLessonEntrance() in
     script.js) instead of 3 separate <p> lines. */
  line-height: 1;
  letter-spacing: calc(var(--u) * -1.28);
  color: var(--beige-100);
}

/* small fixed gaps beside italic parens are literal px, not --u, by
   project convention */
.free-lesson__txt-paren {
  margin-right: 12px;
}

.free-lesson__txt-paren--close {
  margin-right: 0;
  margin-left: 40px;
}

.free-lesson__cta {
  position: absolute;
  left: calc(var(--u) * 918.5);
  top: calc(var(--u) * 865);
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  /* same 8u drop as .craft__cta / .hero__btn-line-track — her Figma spec
     literally says pb-[2px], but that reads too tight against the text
     compared to every other CTA underline on the site; matching the
     established convention instead, per her explicit correction. */
  padding-bottom: calc(var(--u) * 8);
  border-bottom: 1px solid var(--beige-100);
}

.free-lesson__cta-label {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 36);
  line-height: 1.1;
  color: var(--beige-100);
  white-space: nowrap;
}

/* two-line icon swap — same recipe as .craft__cta-plus verbatim (track +
   two stacked chars, one hidden below), just this block's own 40u size. */
.free-lesson__cta-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 40);
  line-height: 1;
  overflow: hidden;
  height: 1em;
  color: var(--beige-100);
}

.free-lesson__cta-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.free-lesson__cta-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.free-lesson__cta-plus-char--out {
  transform: translateY(0%);
}

.free-lesson__cta-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .free-lesson__cta-plus-char--out {
  transform: translateY(-100%);
}

.js-line-hover:hover .free-lesson__cta-plus-char--in {
  transform: translateY(0%);
}

/* --- free-lesson mobile (node 978:1019 "8_free lesson") ---
   Hidden on desktop by default; the @media block below both reveals this
   and hides the desktop absolute layout, same two-part mechanism as
   .steps-mobile (see BLOCK_STARTER_KIT.md / project_artefact_status.md for
   why the override has to live AFTER .free-lesson's own desktop height/
   overflow rule above, not up with the other mobile blocks' shared
   display:none list — same-specificity, same-selector, source order wins,
   and an earlier override would silently lose to the desktop rule sitting
   later in the file). Fully self-contained: doesn't touch any other
   block's selectors. */
.free-lesson-mobile {
  display: none;
}

@media (max-width: 768px) {
  .free-lesson {
    height: auto;
    overflow: visible;
  }

  .free-lesson > .section-inner {
    position: static;
  }

  .free-lesson__gradient,
  .free-lesson__content {
    display: none;
  }

  .free-lesson-mobile {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 80px;
    position: relative;
    overflow: hidden;
    padding: 80px 20px;
    background-color: #5E4133;
  }

  /* same "real blur filter is off-limits" situation as desktop's own
     .free-lesson__gradient (see its comment above) — this mobile frame's
     "gradient" group is the same Gaussian-blurred rotated-ellipse asset,
     just viewed through a taller/narrower window. Approximated the same
     way: layered radial-gradients, same hues, no filter, repositioned for
     a portrait layout (warmth concentrated near the heading/top, corners
     darkened). Flag to her if the fit needs to be tighter. */
  .free-lesson-mobile__gradient {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
      radial-gradient(ellipse 90% 30% at 50% 12%, rgba(126, 100, 84, 0.55) 0%, rgba(126, 100, 84, 0) 70%),
      radial-gradient(ellipse 80% 25% at 85% 38%, rgba(100, 71, 55, 0.5) 0%, rgba(100, 71, 55, 0) 70%),
      radial-gradient(ellipse 70% 30% at 10% 68%, rgba(91, 64, 50, 0.55) 0%, rgba(91, 64, 50, 0) 70%),
      radial-gradient(ellipse 60% 25% at 90% 95%, rgba(0, 0, 0, 0.22) 0%, rgba(0, 0, 0, 0) 65%);
  }

  .free-lesson-mobile__top {
    display: flex;
    flex-direction: column;
    gap: 60px;
    width: 100%;
    position: relative;
  }

  .free-lesson-mobile__heading {
    display: flex;
    flex-direction: column;
    width: 100%;
  }

  .free-lesson-mobile__line {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 70px;
    line-height: 1;
    letter-spacing: -2.1px;
    text-transform: uppercase;
    white-space: nowrap;
  }

  /* opacity read straight off this frame's own text fills — mobile's
     shade-per-line doesn't mirror desktop's 1:1 ("до" is 80 here, 40 on
     desktop), so these aren't copied from .free-lesson__word--*. */
  .free-lesson-mobile__line--100 { color: var(--beige-100); }
  .free-lesson-mobile__line--80 { color: var(--beige-80); }
  .free-lesson-mobile__line--60 { color: var(--beige-60); }

  .free-lesson-mobile__line--last {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    width: 100%;
    color: var(--beige-60);
  }

  .free-lesson-mobile__mark {
    color: var(--beige-40);
  }

  .free-lesson-mobile__media {
    width: 100%;
    height: 400px;
  }

  .free-lesson-mobile__media-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .free-lesson-mobile__bottom {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 60px;
    width: 100%;
    position: relative;
    color: var(--beige-100);
  }

  .free-lesson-mobile__txt {
    display: flex;
    flex-direction: column;
    gap: 6px;
    width: 100%;
  }

  .free-lesson-mobile__txt-line {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 32px;
    line-height: 1.1;
    letter-spacing: -0.64px;
  }

  .free-lesson-mobile__txt-line .italic-symbol {
    font-size: 30px;
  }

  .free-lesson-mobile__txt-paren {
    margin-right: 10px;
  }

  .free-lesson-mobile__txt-paren--close {
    margin-right: 0;
    margin-left: 10px;
  }

  .free-lesson-mobile__cta {
    display: flex;
    align-items: center;
    gap: 28px;
    /* 6px, not Figma's literal pb-[2px] — same correction already applied
       to every desktop CTA underline (.craft__cta, .hero__btn-line-track,
       ...) and to .craft__cta's own mobile variant; this one and
       .pricing-mobile__card-cta were missed when mobile was built, found
       2026-08-07 auditing every CTA underline for a consistent offset. */
    padding-bottom: 6px;
    border-bottom: 1px solid var(--beige-100);
  }

  .free-lesson-mobile__cta-label {
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    color: var(--beige-100);
    white-space: nowrap;
  }

  /* two-line icon swap — same recipe as .free-lesson__cta-plus verbatim,
     just this block's own mobile 24px size. */
  .free-lesson-mobile__cta-plus {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1;
    overflow: hidden;
    height: 1em;
    color: var(--beige-100);
  }

  .free-lesson-mobile__cta-plus-track {
    position: relative;
    display: block;
    line-height: 1;
  }

  .free-lesson-mobile__cta-plus-char {
    display: block;
    transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .free-lesson-mobile__cta-plus-char--out {
    transform: translateY(0%);
  }

  .free-lesson-mobile__cta-plus-char--in {
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(100%);
  }

  .js-line-hover:hover .free-lesson-mobile__cta-plus-char--out {
    transform: translateY(-100%);
  }

  .js-line-hover:hover .free-lesson-mobile__cta-plus-char--in {
    transform: translateY(0%);
  }
}

/* ==========================================================================
   STORIES SECTION — "9_історії учнів" (Figma node 978:1359, file
   6tA1k2sLpFm5U7cLn9p9rZ). Pixel Perfect only for now — no entrance or
   hover motion wired yet, verstka pass.
   ========================================================================== */

.stories {
  position: relative;
  width: 100%;
  height: calc(var(--u) * 1620);
  overflow: hidden;
  /* same fractional-devicePixelRatio hairline gotcha as every other block
     seam so far — see BLOCK_STARTER_KIT.md. */
  margin: -2px 0 0;
  background-color: var(--beige-100);
  color: #2B3D2D;
}

/* --- heading group (Figma frame "H" at 50,150) --- */

.stories__eyebrow {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 150);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.5;
  color: #2B3D2D;
  white-space: nowrap;
}

.stories__eyebrow .italic-symbol {
  margin-right: calc(var(--u) * 18.3);
}

.stories__intro {
  position: absolute;
  z-index: 1;
  left: calc(var(--u) * 508);
  top: calc(var(--u) * 150);
  width: calc(var(--u) * 685);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 40);
}

.stories__title {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 500;
  font-size: calc(var(--u) * 96);
  line-height: 1;
  letter-spacing: calc(var(--u) * -1.92);
  text-transform: uppercase;
}

.stories__title-a { color: #2B3D2D; }
.stories__title-b { color: rgba(43, 61, 45, 0.8); }

.stories__lead {
  margin: 0;
  width: calc(var(--u) * 294);
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.24);
  color: #2B3D2D;
  opacity: 0.8;
}

/* --- horizontal carousel track — same mechanism as .gallery__track
   (pinned section + scrubbed x + a Lenis-driven settle, see
   initStoriesCarousel()/initStoriesSettle() in script.js; never
   ScrollTrigger's `snap`, project hard rule). Two .stories__page blocks
   sit side by side, each one page-width (1820u, the same content width
   .stories__grid used to be before it became the wide track) plus one
   grid-margin's worth of gap between them. Panning is the ONLY thing that
   changed from the single-page build — each .stories__page still holds
   the exact two-row grid from before, unmodified. --- */

.stories__grid {
  position: absolute;
  z-index: 1;
  left: var(--grid-margin);
  top: calc(var(--u) * 490);
  width: calc(var(--u) * 1820 * 2 + var(--u) * 50);
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 50);
}

/* --- one page: two rows of four grid slots each. Every slot that has no
   photo of its own (row1 slot2, row2 slot1, row2 slot3) is a
   .stories__slot — a pure click-to-reveal target, empty until its paired
   card is clicked. Built off the shared --grid-margin/--grid-gutter tokens
   rather than --col-1..4, since every slot here is an equal 1/4 share of
   the row — no asymmetric columns. --- */

.stories__page {
  flex: 0 0 calc(var(--u) * 1820);
  display: flex;
  flex-direction: column;
  gap: var(--grid-gutter);
}

.stories__row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--grid-gutter);
}

.stories__card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 10);
}

.stories__label {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 20);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.24);
  color: #2B3D2D;
  transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

/* hidden while a NEIGHBOUR's review is open on top of this card's own
   photo (cards 2/3 only) — toggled from initStoriesReviews() */
.stories__label.is-hidden {
  opacity: 0;
}

/* the typographic "+" — same convention as .gallery__mark (Helvetica Neue
   glyph standing in for the old design's SVG icon), per her instruction to
   match block 5 exactly. */
.stories__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 28);
  line-height: 1;
}

/* button, not div — same reset .gallery__photo uses, so it can be a real
   .js-cursor-media hover/click target */
.stories__photo {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
}

/* +1px/bottom:auto — same hairline-on-hover-scale-settle fix as
   .gallery__photo img (see that rule's comment) */
.stories__photo img {
  position: absolute;
  top: 0;
  right: 0;
  bottom: auto;
  left: 0;
  width: 100%;
  height: calc(100% + 1px);
  object-fit: cover;
  display: block;
}

/* --- click-to-reveal review panels ---
   A .stories__slot is a dedicated empty grid cell reserved for one card's
   review (row1 slot2, row2 slot1, row2 slot3). Cards 2/3 have no spare
   cell, so per her spec their .stories__review lives INSIDE the
   NEIGHBOURING .stories__card instead, absolutely positioned over that
   sibling's own photo — see the index.html comment above .stories__grid
   for the exact wiring. Either way the panel is positioned/sized the same:
   opaque, full width, starting at the photo's own top edge (36u = the
   label row's 26u height + the card's 10u internal gap, same offset used
   for the row-2 quote before this became click-triggered) so it reads as
   replacing the picture, never floating over the "курс / ..." label. */
.stories__slot {
  position: relative;
}

.stories__review {
  position: absolute;
  top: calc(var(--u) * 36);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  margin: 0;
  padding: calc(var(--u) * 20);
  background-color: var(--beige-100);
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  line-height: 1.1;
  color: #2B3D2D;
  opacity: 0;
  pointer-events: none;
  /* direction-aware duration: fast out, slow in (same convention as block
     7's card crossfade) — the base rule is the CLOSE speed, .is-open
     overrides it with the slower OPEN speed */
  transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.stories__review.is-open {
  opacity: 1;
  pointer-events: auto;
  transition-duration: 0.5s;
}

.stories__review-text {
  margin: 0;
  max-width: calc(var(--u) * 355);
  font-size: calc(var(--u) * 30);
  letter-spacing: calc(var(--u) * -0.3);
  opacity: 0.6;
  font-style: normal;
}

.stories__review-author {
  margin: calc(var(--u) * 20) 0 0;
  font-size: calc(var(--u) * 24);
  letter-spacing: calc(var(--u) * -0.24);
  opacity: 0.8;
  font-style: normal;
}

/* --- stories mobile (node 978:1048 "9_історії учнів") ---
   Same two-part override mechanism as .steps-mobile/.free-lesson-mobile:
   base display:none here, real reveal + desktop-hide inside the @media
   block below, placed AFTER .stories's own desktop height/overflow rule
   above so source order doesn't let the earlier one silently lose (see
   BLOCK_STARTER_KIT.md). Fully self-contained. */
.stories-mobile {
  display: none;
}

@media (max-width: 768px) {
  .stories {
    height: auto;
    overflow: visible;
  }

  .stories > .section-inner {
    position: static;
  }

  .stories__eyebrow,
  .stories__intro,
  .stories__grid,
  .scroll-up {
    display: none;
  }

  /* bottom padding trimmed 140→82px (2026-08-06) so the visible gap down
     to block 10 (pricing) reads as exactly 80px — pricing-mobile's own
     top padding was removed to 0 (see its rule), and the -2px full-bleed
     seam guard on .pricing eats 2px, so 82-2=80. Verified via
     getBoundingClientRect between .stories-mobile__row and
     .pricing-mobile__header. Top padding (140px, gap up to block 9/
     free-lesson) is untouched. */
  .stories-mobile {
    display: flex;
    flex-direction: column;
    gap: 60px;
    padding: 140px 20px 82px;
  }

  .stories-mobile__title {
    display: flex;
    flex-direction: column;
    gap: 20px;
    width: 100%;
  }

  .stories-mobile__eyebrow {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.5;
    color: #2B3D2D;
  }

  .stories-mobile__eyebrow .italic-symbol {
    margin-right: 10px;
  }

  .stories-mobile__intro {
    display: flex;
    flex-direction: column;
    gap: 16px;
    width: 100%;
  }

  .stories-mobile__heading {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 500;
    font-size: 46px;
    line-height: 1;
    letter-spacing: -1.38px;
    text-transform: uppercase;
  }

  .stories-mobile__heading-a { color: #2B3D2D; }
  .stories-mobile__heading-b { color: rgba(43, 61, 45, 0.8); }

  .stories-mobile__lead {
    margin: 0;
    width: 172px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    color: #2B3D2D;
    opacity: 0.8;
  }

  /* native scrollbar hidden — same convention as .steps-mobile__row /
     .gallery-mobile__row.

     Right-edge treatment reverted to plain peek, her later call: the
     mask-image fade tried first (softening the truncated next card to the
     section's beige instead of a hard crop) read as an unwanted blur/soft
     edge. margin-right:-20px cancels .stories-mobile's own 20px side
     padding so the row bleeds to the true viewport edge on the right, same
     as .gallery-mobile__row/.steps-mobile__row's own convention (padding-
     left inset, no right constraint) — the next card now peeks further in
     naturally, no mask needed.

     touch-action: pan-x alone (the same value gallery-mobile/steps-mobile
     use) blocks native vertical panning for any touch that starts over the
     element — fine for those two, whose rows are short, but this row's
     cards run ~600px tall and cover most of the viewport, so a vertical
     swipe anywhere over the carousel silently failed to scroll the page.
     pan-x pan-y allows both; the row's own overflow-x:auto still owns the
     horizontal gesture, vertical now bubbles to the page normally. */
  .stories-mobile__row {
    position: relative;
    overflow-x: auto;
    touch-action: pan-x pan-y;
    cursor: grab;
    margin-right: -20px;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .stories-mobile__row::-webkit-scrollbar {
    display: none;
  }

  .stories-mobile__row.is-dragging {
    cursor: grabbing;
  }

  .stories-mobile__track {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: max-content;
  }

  .stories-mobile__card {
    flex: 0 0 auto;
    width: 320px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
  }

  .stories-mobile__meta {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
  }

  .stories-mobile__label {
    display: flex;
    align-items: center;
    gap: 20px;
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    color: #2B3D2D;
  }

  .stories-mobile__mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1;
  }

  .stories-mobile__photo {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    overflow: hidden;
  }

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

  .stories-mobile__text {
    display: flex;
    flex-direction: column;
    gap: 40px;
    width: 100%;
  }

  .stories-mobile__quote {
    margin: 0;
    width: 100%;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    color: #2B3D2D;
    opacity: 0.6;
    overflow-wrap: break-word;
  }

  .stories-mobile__author {
    margin: 0;
    width: 100%;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    color: #2B3D2D;
    opacity: 0.8;
  }
}

/* ==========================================================================
   PRICING SECTION — "10_тарифи" (Figma node 978:1381, file
   6tA1k2sLpFm5U7cLn9p9rZ). Verstka + motion pass.

   BLUR: Figma renders the main heading words and all three card titles
   with real blur baked into the glyphs. Project hard rule bans CSS blur
   anywhere, so — same precedent as mova-clay.png (block 3) and
   h-step-1/2.png (block 7) — every blurred piece of type is a pre-rendered
   PNG exported straight from Figma instead of a `filter: blur()`. Each
   image is positioned CENTRED on the original Figma text node's bounding-
   box centre (not its top-left/width), because Figma's blur export
   expands the canvas around the glyphs by an amount that isn't reliably
   symmetric — centring on the source centre is the cheap version of the
   alpha-bbox measuring mova-clay.png's comment describes, good enough
   since these assets are static (never scattered/animated individually).

   CARD LAYOUT: every .pricing__card-* is positioned with literal
   left/top offsets per element (not flex/gap) inside each card, because
   the three cards' internal spacing isn't uniform — card 2's larger
   title pushes its price block down by a different amount than cards
   1/3's shared spacing. Only the outer row (.pricing__cards) uses flex
   (align-items: flex-end + gap), which is exactly how Figma bottom-
   aligns the three different-height cards against each other.
   ========================================================================== */

/* 1441u is Figma's own cards-flush-to-bottom height; +150u is her explicit
   request for breathing room below the cards that Figma's spec didn't have. */
.pricing {
  position: relative;
  overflow: hidden;
  height: calc(var(--u) * (1441 + 150));
  /* full-bleed seam guard, same fix as every other block boundary on the
     site — see BLOCK_STARTER_KIT.md's "full-bleed seam hairline gotcha" */
  margin: -2px 0 0;
  background-color: var(--beige-100);
  color: #2B3D2D;
}

.pricing__header {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 150);
  width: calc(var(--u) * 1821);
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 20);
}

.pricing__heading {
  position: relative;
  width: 100%;
  height: calc(var(--u) * 308);
}

.pricing__eyebrow {
  position: absolute;
  left: 0;
  top: 0;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.5;
  color: #2B3D2D;
  white-space: nowrap;
}

.pricing__eyebrow .italic-symbol { margin-right: 10px; }

.pricing__heading-word {
  position: absolute;
  display: block;
  /* word--3's true width (2091u) exceeds .pricing__heading's own 1821u —
     override the sitewide `img { max-width: 100% }` reset, which was
     silently capping it to the container width and squashing the blur
     bleed on "гончарства" */
  max-width: none;
  pointer-events: none;
}

/* All three heading words share ONE scale (s = 0.50277, i.e. k = 1.989
   canvas-px per Figma unit), derived from the one blur-FREE reference we
   have — "у" in h-price-3.png isn't blurred, so its alpha bbox is a clean,
   distortion-free measurement: bbox width 189px for a node that's 95u
   wide -> k = 189/95 = 1.989. Using a per-image scale derived from each
   word's OWN blurred bbox height (the first pass) put "оберіть" (blur 12)
   and "шлях" (blur 5) at visibly different apparent font sizes, because
   heavier blur inflates the measured bbox height without changing the
   true glyph size — her catch. One shared k fixes that: every word is now
   the same true size, only the blur halo differs.

   Every word is positioned by LEFT/TOP-EDGE alignment: left = node.x -
   bbox.minX*s, top = node.y - bbox.minY*s — not bbox-centring — so the
   actual glyph ink lands exactly on Figma's node position regardless of
   how much (possibly asymmetric) padding her export left around it. */
.pricing__heading-word--1 {
  /* h-price-1.png, canvas 1306×326, bbox min (22,22); node 978:1391 at
     (306, 0) within .pricing__heading */
  left: calc(var(--u) * 294.9);
  top: calc(var(--u) * -11.06);
  width: calc(var(--u) * 656.6);
  height: auto;
}

.pricing__heading-word--2 {
  /* h-price-2.png, canvas 923×264, bbox min (19,8); node 978:1390 at
     (1364, 0) */
  left: calc(var(--u) * 1354.4);
  top: calc(var(--u) * -4.02);
  width: calc(var(--u) * 464.1);
  height: auto;
}

/* "у" + "світ гончарства" together — h-price-3.png, canvas 3693×347, bbox
   min (0,27); node 978:1385 at (0, 154). True width at this shared scale
   is 1857.1u, which bleeds ~36u past .pricing__heading's own 1821u right
   edge — i.e. past the site's standard 50u right margin (her catch #2).
   clip-path trims exactly that overshoot instead of shrinking the image
   (which would also shrink the "у"/"світ" glyphs out of the shared
   scale) or clipping the whole heading box (which would also cut the
   other two words' harmless vertical blur bleed). */
.pricing__heading-word--3 {
  left: 0;
  top: calc(var(--u) * 140.4);
  width: calc(var(--u) * 1857.1);
  height: auto;
  clip-path: inset(0 calc(var(--u) * 36.1) 0 0);
}

/* two lines, each spread edge-to-edge — Figma spreads every word to an
   explicit manual x offset; text-align-last:justify reproduces the same
   "fills the full width" look without hardcoding 17 per-word positions
   that would only be exactly right at the 1920 reference width. */
.pricing__lead {
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 10);
  width: 100%;
}

.pricing__lead-line {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.2;
  letter-spacing: calc(var(--u) * -0.3);
  color: rgba(43, 61, 45, 0.8);
  text-align: justify;
  text-align-last: justify;
}

.pricing__lead-line .word {
  display: inline-block;
  will-change: opacity;
}

.pricing__cards {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 650);
  width: calc(var(--u) * 1819);
  display: flex;
  align-items: flex-end;
  gap: calc(var(--u) * 10);
}

/* Hover: the whole card lifts 2px and grows 1% as one unit — background,
   border and every child scale together, no separate wrapper or per-child
   transform. A per-child version (`.pricing__card > *` scaling instead of
   the card itself) was tried in between and reverted — her explicit call,
   she wants the card's own box moving, not just its contents. */
.pricing__card {
  position: relative;
  width: calc(var(--u) * 600);
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .pricing__card:hover {
    transform: translateY(-2px) scale(1.01);
  }
}

.pricing__card--basic {
  height: calc(var(--u) * 732);
  background-color: rgba(154, 150, 113, 0.2);
}

.pricing__card--featured {
  height: calc(var(--u) * 791);
  background-color: var(--olive-100);
}

.pricing__card--pro {
  height: calc(var(--u) * 702);
  /* 1.5px, not 1px: at her machine's devicePixelRatio (1.5), ANY
     border-width from 0 up to 1.3px rounds down to the exact same
     rendered thickness (1 device pixel ≈ 0.667 CSS px) — confirmed live by
     testing 0.5px/1px/1.2px/1.3px side by side, all identical. That's why
     the earlier "make it 1px like everywhere else" change was invisible to
     her. 1.4px is the first value that actually rounds up to a full 2
     device pixels; 1.5px sits safely past that threshold. If a future
     hairline border/line looks unchanged after a width tweak, suspect this
     same DPR rounding before assuming the CSS didn't apply. */
  border: 1.5px solid rgba(154, 150, 113, 0.2);
}

.pricing__card-title {
  position: absolute;
  z-index: 1;
  display: block;
  pointer-events: none;
}

.pricing__card-subtitle,
.pricing__card-format,
.pricing__card-result {
  position: absolute;
  z-index: 1;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
}

.pricing__card-subtitle {
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.24);
}

.pricing__card-subtitle .italic-symbol { font-style: italic; margin-right: 10px; }

.pricing__card-price {
  position: absolute;
  z-index: 1;
  margin: 0;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 5);
  font-family: 'Helvetica Neue', sans-serif;
  color: #2B3D2D;
}

.pricing__card-price-currency,
.pricing__card-price-period {
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.5;
}

.pricing__card-price-value {
  font-weight: 500;
  line-height: 1;
}

.pricing__card-format {
  font-size: calc(var(--u) * 18);
  line-height: 1.5;
  white-space: nowrap;
}

.pricing__card-result {
  font-size: calc(var(--u) * 24);
  line-height: 1.2;
  letter-spacing: calc(var(--u) * -0.24);
}

/* CTA — the underline is a STATIC line, always visible, no hover sweep.
   An earlier version used the shared heroLineSweep hover-animation (like
   .hero__btn's), but the animation itself was the bug she flagged (jitter)
   and didn't suit these cards — she wants the line always there, doing
   nothing on hover. The plus-icon swap is untouched and still animates on
   hover via the same .js-line-hover:hover trigger (see
   .pricing__card-cta-plus-char rules below) — that part was never the
   complaint, only the line's hover animation was. */
.pricing__card-cta {
  position: absolute;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: calc(var(--u) * 36);
  text-decoration: none;
  font-family: 'Helvetica Neue', sans-serif;
}

.pricing__card-cta-line-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--u) * -8);
  height: 1px;
  overflow: hidden;
  /* counter-scales against the card's own hover scale(1.01) below, so the
     line's RENDERED thickness never changes even mid-transition — a
     transform on an ancestor visually scales everything painted inside it
     (a 1px line really does render as ~1.01px once the card scales up),
     and a hairline is thin enough that this tiny stretch reads as
     trembling/jitter while the transition is running, since sub-pixel
     rendering has to keep re-resolving a constantly-changing fractional
     thickness. Same transition duration/easing as .pricing__card:hover so
     both scales interpolate in lockstep and actually cancel out
     throughout the animation, not just at the two end states. */
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .pricing__card:hover .pricing__card-cta-line-track {
    transform: scale(calc(1 / 1.01));
  }
}

.pricing__card-cta-line {
  position: absolute;
  inset: 0;
  background: currentColor;
}

.pricing__card-cta-label {
  font-size: calc(var(--u) * 36);
  font-weight: 400;
  line-height: 1.1;
  white-space: nowrap;
}

.pricing__card-cta-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  line-height: 1;
  overflow: hidden;
  height: 1em;
}

.pricing__card-cta-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.pricing__card-cta-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.pricing__card-cta-plus-char--out { transform: translateY(0%); }
.pricing__card-cta-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .pricing__card-cta-plus-char--out { transform: translateY(-100%); }
.js-line-hover:hover .pricing__card-cta-plus-char--in { transform: translateY(0%); }

/* --- card 1: перший дотик (basic) --- */
/* her export (img/h-1price.png, canvas 653×153, bbox min (32,32)), same
   shared scale s=0.50277 as the main heading (see .pricing__heading-word
   comment) and same LEFT/TOP-EDGE alignment — her catch #4: the title was
   previously bbox-CENTRED, which visibly drifted it right of the
   subtitle/price/etc below (all flush at left:40u). Edge-aligning puts
   the actual "перший дотик" glyph ink at the same 40u left edge as
   everything else in the card: left = node.x(40) - bbox.minX(32)*s. */
.pricing__card--basic .pricing__card-title { left: calc(var(--u) * 23.9); top: calc(var(--u) * 103.9); width: calc(var(--u) * 328.3); }
.pricing__card--basic .pricing__card-subtitle { left: calc(var(--u) * 40); top: calc(var(--u) * 168); width: calc(var(--u) * 520); color: rgba(43, 61, 45, 0.6); }
.pricing__card--basic .pricing__card-price { left: calc(var(--u) * 40); top: calc(var(--u) * 234); }
.pricing__card--basic .pricing__card-price-value { font-size: calc(var(--u) * 104); letter-spacing: calc(var(--u) * -2.08); }
.pricing__card--basic .pricing__card-format { left: calc(var(--u) * 40); top: calc(var(--u) * 405); width: calc(var(--u) * 335); color: rgba(43, 61, 45, 0.6); }
.pricing__card--basic .pricing__card-result { left: calc(var(--u) * 40); top: calc(var(--u) * 452); width: calc(var(--u) * 520); color: rgba(43, 61, 45, 0.8); }
.pricing__card--basic .pricing__card-result-label { color: rgba(43, 61, 45, 0.6); }
.pricing__card--basic .pricing__card-cta { left: calc(var(--u) * 40); top: calc(var(--u) * 570); color: rgba(43, 61, 45, 0.8); }
.pricing__card--basic .pricing__card-cta-plus { font-size: calc(var(--u) * 40); }

/* --- card 2: мова глини (featured, olive bg — light text) --- */
/* placeholder pending her h-2price.png export (see index.html's block
   comment). pricing-title-2.png is a tight MCP-render crop at native
   scale (its 492×85 canvas maps ~1:1 to Figma units, unlike her own
   manual exports which are a different pipeline entirely) — used at its
   native size, left/top-edge aligned to node 978:1448's own position
   (40, 100 within the card). Do NOT rescale this to the node's declared
   77u height like a first pass tried — that assumption doesn't hold for
   this render pipeline and only shrinks the title for no reason.

   `left` is 24u, not 40u: pricing-title-2.png was exported FLATTENED onto
   an opaque olive fill matching the card's own background, not with real
   alpha transparency (unlike h-1price.png, which is genuinely transparent
   — that's what "bbox min is 0,0" was checking before, the wrong signal
   for this asset). Measured directly (canvas pixel scan for the first
   column whose color differs from the image's own corner pixel, not the
   alpha channel): visible ink starts 16px into the 492px-wide asset. Left
   is set to 40u - 16u = 24u so the VISIBLE glyph lines up with the
   subtitle's 40u, the same compensation h-1price.png's 23.9u already does
   for its own (smaller, ~16u) baked-in margin below. */
.pricing__card--featured .pricing__card-title { left: calc(var(--u) * 24); top: calc(var(--u) * 100); width: calc(var(--u) * 492); }
.pricing__card--featured .pricing__card-subtitle { left: calc(var(--u) * 40); top: calc(var(--u) * 187); width: calc(var(--u) * 520); color: rgba(43, 61, 45, 0.6); }
.pricing__card--featured .pricing__card-price { left: calc(var(--u) * 40); top: calc(var(--u) * 257); }
.pricing__card--featured .pricing__card-price-value { font-size: calc(var(--u) * 140); letter-spacing: calc(var(--u) * -4.2); }
.pricing__card--featured .pricing__card-format { left: calc(var(--u) * 40); top: calc(var(--u) * 473); width: calc(var(--u) * 335); color: #2B3D2D; text-align: center; }
.pricing__card--featured .pricing__card-result { left: calc(var(--u) * 40); top: calc(var(--u) * 516); width: calc(var(--u) * 520); color: var(--beige-100); }
.pricing__card--featured .pricing__card-result-label { color: rgba(237, 233, 221, 0.6); }
.pricing__card--featured .pricing__card-cta { left: calc(var(--u) * 40); top: calc(var(--u) * 634); color: #2B3D2D; }
.pricing__card--featured .pricing__card-cta-plus { font-size: calc(var(--u) * 40); }

/* --- card 3: професійний рівень (transparent, hairline border) --- */
/* placeholder pending her h-3price.png export — same native-scale MCP
   render as card 2's (see that comment): used at its real 449×77 size,
   left/top-edge aligned to node 978:1454's position (40, 90). Reverted
   from an earlier pass that incorrectly rescaled this down to the node's
   declared 38u height and made the title visibly smaller than card 1's —
   her catch.

   Same flattened-background export as card 2's title (see that comment):
   pricing-title-3.png's visible ink starts 15px into its 449px width, so
   `left` is 40u - 15u = 25u to line the glyph up with the subtitle's 40u.
   `top` is nudged an extra literal -13px on top of the 90u position — her
   explicit correction, applied in two rounds (-6px, then another -7px on
   top of that) because the title kept sitting too low/close to the
   subtitle beneath it. */
.pricing__card--pro .pricing__card-title { left: calc(var(--u) * 25); top: calc(calc(var(--u) * 90) - 13px); width: calc(var(--u) * 449); }
.pricing__card--pro .pricing__card-subtitle { left: calc(var(--u) * 40); top: calc(var(--u) * 138); width: calc(var(--u) * 520); color: rgba(43, 61, 45, 0.6); }
.pricing__card--pro .pricing__card-price { left: calc(var(--u) * 40); top: calc(var(--u) * 214); }
.pricing__card--pro .pricing__card-price-value { font-size: calc(var(--u) * 96); letter-spacing: calc(var(--u) * -1.92); }
.pricing__card--pro .pricing__card-format { left: calc(var(--u) * 40); top: calc(var(--u) * 375); width: calc(var(--u) * 410); color: rgba(43, 61, 45, 0.6); }
.pricing__card--pro .pricing__card-result { left: calc(var(--u) * 40); top: calc(var(--u) * 422); width: calc(var(--u) * 520); color: rgba(43, 61, 45, 0.8); }
.pricing__card--pro .pricing__card-result-label { color: rgba(43, 61, 45, 0.6); }
.pricing__card--pro .pricing__card-cta { left: calc(var(--u) * 40); top: calc(var(--u) * 540); color: rgba(43, 61, 45, 0.8); }

/* --- pricing mobile (node 978:1081 "10_тарифи") ---
   Same two-part override mechanism as .stories-mobile/.steps-mobile: base
   display:none here, real layout inside the @media block below, placed
   AFTER .pricing's own desktop height/overflow rule so source order can't
   silently lose (see BLOCK_STARTER_KIT.md). Fully self-contained.

   See index.html's block comment for the asset-scale/content-bug notes —
   not repeated here. */
.pricing-mobile {
  display: none;
}

@media (max-width: 768px) {
  .pricing {
    height: auto;
    overflow: visible;
  }

  .pricing > .section-inner {
    position: static;
  }

  .pricing__header,
  .pricing__cards,
  .scroll-up {
    display: none;
  }

  /* Top stays 0 — the gap to block 9 above is owned by .stories-mobile's
     own bottom padding, see its comment.

     Bottom: REVERSED 2026-08-07, her explicit call — she wants a real 80px
     BEIGE gap after the cards, distinct from .cta-mobile's own 80px olive
     top padding (not the same gap read twice). The single-shared-gap
     convention tried on 2026-08-06 (see [[project-artefact-mobile-small-
     fixes-batch1]]) read as "no gap at all" on her real phone — a same-
     colour continuation into an already-olive stat card didn't register as
     intentional whitespace the way a colour change does. Total visual
     space at this boundary is now genuinely 160px (80 beige + 80 olive),
     not 80. */
  .pricing-mobile {
    display: flex;
    flex-direction: column;
    gap: 32px;
    padding: 0 0 80px;
  }

  .pricing-mobile__header {
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 0 20px;
  }

  .pricing-mobile__eyebrow {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.5;
    color: #2B3D2D;
  }

  .pricing-mobile__eyebrow .italic-symbol {
    margin-right: 10px;
  }

  .pricing-mobile__heading {
    position: relative;
    width: 100%;
    height: 138px;
  }

  .pricing-mobile__heading-line {
    position: absolute;
    display: block;
    max-width: none;
    pointer-events: none;
  }

  /* mob-h-price-1.png, canvas 688×101, bbox min (10,11); line node
     978:1086 at (0,0) within .pricing-mobile__heading, shared scale
     s=0.5 (see index.html comment) */
  .pricing-mobile__heading-line--1 {
    left: -5px;
    top: -5.5px;
    width: 344px;
    height: auto;
  }

  /* mob-h-price-2.png, canvas 307×101, bbox min (16,14); line node
     978:1087 at (0,46) */
  .pricing-mobile__heading-line--2 {
    left: -8px;
    top: 39px;
    width: 153.5px;
    height: auto;
  }

  /* mob-h-price-3.png, canvas 609×101, bbox min (10,12); line node
     978:1088 at (0,92) */
  .pricing-mobile__heading-line--3 {
    left: -5px;
    top: 86px;
    width: 304.5px;
    height: auto;
  }

  .pricing-mobile__row {
    position: relative;
    overflow-x: auto;
    touch-action: pan-x pan-y;
    cursor: grab;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .pricing-mobile__row::-webkit-scrollbar {
    display: none;
  }

  .pricing-mobile__row.is-dragging {
    cursor: grabbing;
  }

  .pricing-mobile__track {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    width: max-content;
  }

  /* each card is the full 375px mobile frame width, edge to edge — no
     side inset, unlike the header's 20px padding above (that's what the
     Figma frame actually shows: the row bleeds full-bleed while the
     eyebrow/heading stay inset). */
  /* overflow:hidden matters beyond the visual edge: card 3's title image
     (mob-pricing-title-3.png) has a wider transparent canvas margin than
     cards 1/2's, and its un-clipped box bleeds ~75px past the card's own
     right edge — since it's the LAST card in the track, that bleed
     inflated .pricing-mobile__row's scrollWidth and left extra empty
     scroll space after card 3. Clipping at the card boundary fixes both
     the stray scroll space and keeps every card visually self-contained
     (card 3 already reads as a bounded box via its border). */
  /* ALIGNMENT: her explicit call — title/subtitle/price/format and the
     CTA must land on the same absolute Y across all 3 cards, not follow
     Figma's own per-card centering (which put the featured card's title
     ~40px lower than the other two, since it's a taller card with much
     bigger top padding, and the row bottom-aligns them). Two mechanisms:
     (1) every card gets the SAME title/price/format gaps (26px/46px/7px,
     dropping featured's own slightly different 28px/44px), so identical
     content produces identical cumulative offsets; (2) the featured
     card's padding-top is basic/pro's 35px + exactly the 40px height
     difference (566 vs 526) it pokes up by in the bottom-aligned row —
     that 40px is what makes its title start 40px higher on the page
     than the other two would otherwise, so adding it back to padding-top
     cancels it out. Confirmed via getBoundingClientRect: both land at
     the same 379px from viewport top. The CTA is bottom-pinned instead
     (margin-top:auto, see .pricing-mobile__card-cta) rather than relying
     on identical gaps all the way down, since result-text length still
     varies per card (card 3's format wraps 2 lines) — bottom-pinning
     makes CTA alignment immune to that instead of needing yet another
     hardcoded compensation. */
  .pricing-mobile__card {
    flex: 0 0 auto;
    box-sizing: border-box;
    width: 375px;
    display: flex;
    flex-direction: column;
    padding: 35px 18px 60px;
    overflow: hidden;
  }

  .pricing-mobile__card--basic {
    height: 526px;
    background-color: rgba(154, 150, 113, 0.2);
  }

  .pricing-mobile__card--featured {
    height: 566px;
    padding-top: 75px;
    background-color: var(--olive-100);
  }

  .pricing-mobile__card--pro {
    height: 526px;
    /* 1.5px not 1px — same dpr-1.5 rounding cliff as desktop's card 3
       border, see project memory / BLOCK_STARTER_KIT.md */
    border: 1.5px solid rgba(154, 150, 113, 0.2);
  }

  .pricing-mobile__card-info {
    display: flex;
    flex-direction: column;
  }

  /* same 26px/46px gap on ALL 3 cards now — see the ALIGNMENT comment
     on .pricing-mobile__card above. */
  .pricing-mobile__card-title-group {
    display: flex;
    flex-direction: column;
    gap: 16px;
    margin-bottom: 26px;
  }

  /* reserves the source text node's own 38px height so the subtitle's
     16px gap below lands in the right place — a LAYOUT height only, not
     a clip. It must NOT carry overflow/mask: a blurred title's canvas is
     taller than this box (blur bleeds above AND below the glyph), and an
     earlier version here had overflow:hidden, which clipped that bleed
     off at the bottom on every card — most visibly on card 3's taller
     asset, read as "the title is cut off." Card 3's title is ALSO wider
     than its column (measured ~418px in the site's real Helvetica Neue
     48px/500 against the ~338px column) and does still get clipped
     horizontally, but that clip now happens up at .pricing-mobile__card's
     own overflow:hidden (the card's outer edge, a generous boundary) —
     not here, and with no vertical side effect. */
  .pricing-mobile__card-title-slot {
    position: relative;
    width: 100%;
    height: 38px;
  }

  .pricing-mobile__card-title {
    position: absolute;
    display: block;
    max-width: none;
    pointer-events: none;
  }

  /* mob-pricing-title-1.png, canvas 629×129, bbox min (19,19); title node
     at (0,0) within .pricing-mobile__card-title-slot */
  .pricing-mobile__card--basic .pricing-mobile__card-title {
    left: -9.5px;
    top: -9.5px;
    width: 314.5px;
  }

  /* mob-pricing-title-2.png — she re-exported this file 2026-08-06 18:47
     (replacing the original 4x-scale export flagged in project memory),
     canvas 653×111, bbox min (19,16) max (637,93). Her explicit call
     2026-08-06: this title should read BIGGER — width set directly to
     her requested 321px (scale = 321/653 = 0.4916, close to the 0.5
     cards 1/3 use — the earlier 246.2px sizing, derived from matching a
     real-Helvetica-Neue width measurement, undersized it). left/top
     re-derived at this scale from the same bbox: left = -19*0.4916,
     top = -16*0.4916. */
  .pricing-mobile__card--featured .pricing-mobile__card-title {
    left: -9.34px;
    top: -7.87px;
    width: 321px;
  }

  /* mob-pricing-title-3.png — she re-exported this file 2026-08-06 with a
     shortened title ("професійний", dropped "рівень"), new canvas
     581×129, alpha bbox min (19,13) max (568,108). Re-measured from
     scratch per project convention (never carry over a sibling/previous
     asset's scale): real "професійний" width in the site's Helvetica
     Neue 48px/500/-2.4px is 273.75px vs. the asset's own bbox width
     549px = 0.4986 ≈ clean 2x export, same convention as cards 1/2's
     natural scale (the old asset's 0.3088 downsize was a one-off CSS
     override for the previous, longer two-word title and doesn't apply
     to this new asset). left/top from the same bbox at s=0.5:
     left = -19*0.5, top = -13*0.5. */
  .pricing-mobile__card--pro .pricing-mobile__card-title {
    left: -9.5px;
    top: -6.5px;
    width: 290.5px;
  }

  .pricing-mobile__card-subtitle {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.1;
    letter-spacing: -0.16px;
  }

  .pricing-mobile__card-subtitle .italic-symbol {
    font-style: italic;
    margin-right: 10px;
  }

  .pricing-mobile__card--basic .pricing-mobile__card-subtitle,
  .pricing-mobile__card--pro .pricing-mobile__card-subtitle {
    color: rgba(43, 61, 45, 0.6);
  }

  .pricing-mobile__card--featured .pricing-mobile__card-subtitle {
    color: var(--beige-100);
  }

  .pricing-mobile__card-price {
    margin: 0 0 46px;
    display: flex;
    align-items: center;
    gap: 3px;
    font-family: 'Helvetica Neue', sans-serif;
    color: #2B3D2D;
  }

  .pricing-mobile__card-price-currency,
  .pricing-mobile__card-price-period {
    font-weight: 400;
    font-size: 16px;
    line-height: 1.1;
  }

  .pricing-mobile__card-price-value {
    font-weight: 500;
    font-size: 90px;
    line-height: 1;
    letter-spacing: -2.7px;
  }

  .pricing-mobile__card-result-group {
    display: flex;
    flex-direction: column;
    gap: 7px;
  }

  /* min-height reserves 2 lines even though only card 3's format text
     ("...Формат: офлайн/онлайн") actually wraps to 2 — cards 1/2's
     shorter one-line format would otherwise leave the result paragraph
     (and everything below it) ~18px higher than card 3's, undoing part
     of the alignment work above. */
  .pricing-mobile__card-format {
    margin: 0;
    min-height: 36px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.1;
  }

  .pricing-mobile__card--basic .pricing-mobile__card-format,
  .pricing-mobile__card--pro .pricing-mobile__card-format {
    color: rgba(43, 61, 45, 0.6);
  }

  .pricing-mobile__card--featured .pricing-mobile__card-format {
    color: #2B3D2D;
  }

  .pricing-mobile__card-result {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
  }

  .pricing-mobile__card--basic .pricing-mobile__card-result {
    color: rgba(43, 61, 45, 0.8);
  }

  .pricing-mobile__card--basic .pricing-mobile__card-result-label {
    color: rgba(43, 61, 45, 0.6);
  }

  .pricing-mobile__card--featured .pricing-mobile__card-result {
    color: var(--beige-100);
  }

  .pricing-mobile__card--featured .pricing-mobile__card-result-label {
    color: rgba(237, 233, 221, 0.6);
  }

  /* card 3 is read verbatim off Figma: the WHOLE result line (label +
     body) is one uniform olive tone, no separate muted-label/opaque-body
     split like cards 1/2 — not an oversight, see index.html's comment. */
  .pricing-mobile__card--pro .pricing-mobile__card-result,
  .pricing-mobile__card--pro .pricing-mobile__card-result-label {
    color: rgba(154, 150, 113, 0.8);
  }

  /* align-self:flex-start is required — .pricing-mobile__card is a
     flex column with default align-items:stretch, so without this the
     CTA (a flex item there) stretches to the card's full content width
     and its border-bottom (the underline) runs the full card width
     instead of hugging just the label+icon, like every other CTA on the
     site.

     margin-top:auto bottom-pins the CTA to the card's own padding-box
     instead of letting it flow directly after .pricing-mobile__card-info
     — this is what actually guarantees "все кнопки на одном рівні": the
     3 cards already share the same bottom edge (.pricing-mobile__track's
     align-items:flex-end), so a bottom-pinned CTA lands on the same
     absolute Y on all 3 regardless of any residual content-height
     difference above it (result-text length still varies per card even
     after the .pricing-mobile__card-format min-height fix). Relying on
     identical gaps all the way down would still leave the CTA position
     hostage to exact text length; this doesn't. */
  .pricing-mobile__card-cta {
    display: flex;
    align-items: center;
    align-self: flex-start;
    gap: 28px;
    margin-top: auto;
    /* 6px, not Figma's literal pb-[2px] — same fix as .free-lesson-mobile__cta
       above, see its comment. */
    padding-bottom: 6px;
    border-bottom: 1px solid #2B3D2D;
    text-decoration: none;
    font-family: 'Helvetica Neue', sans-serif;
    color: #2B3D2D;
  }

  .pricing-mobile__card-cta-label {
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    white-space: nowrap;
  }

  .pricing-mobile__card-cta-plus {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1;
    overflow: hidden;
    height: 1em;
  }

  .pricing-mobile__card-cta-plus-track {
    position: relative;
    display: block;
    line-height: 1;
  }

  .pricing-mobile__card-cta-plus-char {
    display: block;
    transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .pricing-mobile__card-cta-plus-char--out { transform: translateY(0%); }
  .pricing-mobile__card-cta-plus-char--in {
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(100%);
  }

  .js-line-hover:hover .pricing-mobile__card-cta-plus-char--out { transform: translateY(-100%); }
  .js-line-hover:hover .pricing-mobile__card-cta-plus-char--in { transform: translateY(0%); }
}

/* ==========================================================================
   CTA SECTION — "11_СТА"
   ========================================================================== */

.cta {
  isolation: isolate;
  position: relative;
  overflow: hidden;
  height: calc(var(--u) * 1281);
  /* full-bleed seam guard vs. .pricing above — see BLOCK_STARTER_KIT.md's
     "full-bleed seam hairline gotcha" */
  margin: -2px 0 0;
  background-color: var(--olive-100);
}

/* same film-grain tile as .philosophy / .gallery__bar — both are flat
   --olive-100 fields per Figma, see the note above .philosophy::before */
.cta::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grain, none);
  background-repeat: repeat;
  mix-blend-mode: overlay;
}

.cta__top {
  position: absolute;
  left: calc(var(--u) * 50);
  right: calc(var(--u) * 50);
  top: calc(var(--u) * 150);
}

/* --- heading: 4 lines at 100/80/60/40% opacity, literal green #2B3D2D
   (the project's one standing dark-text literal). Figma positions the
   second word of rows 3-4 with an absolute offset independent of the
   first word's rendered width (row 1 is a plain flex gap instead) — kept
   as absolute offsets here rather than justify-content so the gap matches
   exactly regardless of glyph width.

   Each row is also a mask (overflow:hidden) around its own -row-inner —
   the kasiasiwosz.com "line" reveal (see BLOCK_STARTER_KIT.md's sibling
   reference note): the inner is what actually slides up + fades in
   (runCtaEntrance), the row just clips it so it reads as emerging from
   behind the line rather than sliding in from off-section. --- */
.cta__heading {
  position: relative;
  width: calc(var(--u) * 1009);
  margin: 0;
  display: flex;
  flex-direction: column;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  text-transform: uppercase;
  white-space: nowrap;
}

.cta__heading-row {
  position: relative;
  overflow: hidden;
  height: calc(var(--u) * 128);
  font-size: calc(var(--u) * 128);
  line-height: 1;
  letter-spacing: calc(var(--u) * -3.84);
}

.cta__heading-row-inner {
  position: relative;
  display: flex;
  align-items: center;
  height: 100%;
}

.cta__heading-row--1 .cta__heading-row-inner { gap: calc(var(--u) * 78); }
.cta__heading-row--1 { color: #2B3D2D; }
.cta__heading-row--2 { color: rgba(43, 61, 45, 0.8); }
.cta__heading-row--3 { color: rgba(43, 61, 45, 0.6); }
.cta__heading-row--4 { color: rgba(43, 61, 45, 0.4); }

.cta__heading-row--3 .cta__heading-word--b { position: absolute; left: calc(var(--u) * 563); top: 0; }
.cta__heading-row--4 .cta__heading-word--b { position: absolute; left: calc(var(--u) * 378); top: 0; }

.cta__lead {
  position: absolute;
  left: 0;
  top: calc(var(--u) * 552);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -1.28);
  white-space: nowrap;
  color: var(--beige-100);
}

.cta__lead-dim { color: var(--beige-80); }

/* clickable video, not a passive photo — cursor-bubble treatment
   (js-cursor-media, "стоп"/"грати" via initCtaVideoToggle) instead of the
   shared mediaImageGroups() hover-scale+parallax recipe, same distinction
   BLOCK_STARTER_KIT.md draws for .gallery__photo. Plain hover-scale only,
   no scroll-drift. */
.cta__media {
  display: block;
  position: absolute;
  left: calc(var(--u) * 1443);
  top: calc(var(--u) * 97);
  width: calc(var(--u) * 377);
  height: calc(var(--u) * 518);
  overflow: hidden;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .cta__media:hover { transform: scale(1.03); }
}

.cta__media-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: calc(100% + 1px);
  object-fit: cover;
  display: block;
}

/* --- advantages / stats row --- */

.cta__stats {
  position: absolute;
  left: calc(var(--u) * 50);
  right: calc(var(--u) * 50);
  top: calc(var(--u) * 944);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}

.cta__stat {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.cta__stat-number {
  display: flex;
  align-items: flex-start;
  margin: 0 0 calc(var(--u) * -20);
  line-height: 1;
  text-transform: uppercase;
  color: var(--beige-80);
}

.cta__stat-value {
  display: inline-block;
  /* right-aligned: runCtaCounters() locks this to its final width via an
     inline min-width (see setInitialCtaStates()) before the count starts,
     so the digits don't reflow the stats row mid-count — right-aligning
     the text within that reserved box keeps it flush against the
     +/% symbol throughout instead of leaving a gap that only closes at
     the very end */
  text-align: right;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 174);
  line-height: 1;
  letter-spacing: calc(var(--u) * -5.22);
}

.cta__stat-symbol {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 500;
  font-size: calc(var(--u) * 96);
  line-height: 1;
  letter-spacing: calc(var(--u) * -1.92);
}

.cta__stat-label {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  white-space: nowrap;
  color: var(--beige-80);
}
.pricing__card--pro .pricing__card-cta-plus { font-size: calc(var(--u) * 32); }

/* --- cta mobile (node 978:1110 "11_СТА") ---
   Same two-part override mechanism as every other -mobile block: base
   display:none here, real layout inside the @media block below. Verstka
   only — no entrance motion yet (desktop's runCtaEntrance/runCtaCounters
   aren't touched or ported).

   Heading keeps desktop's 4-tier opacity scheme (100/80/60/40% on
   #2B3D2D) but as plain inline spans, not desktop's per-row
   overflow:hidden mask — that structure exists on desktop only to support
   the kasiasiwosz "line" scroll reveal, which mobile doesn't have yet.

   Stats row: Figma's "advantages" frame is 798px wide against a 375px
   frame (measured via get_metadata, node 978:1116) — same horizontal
   overflow shape as every other mobile card row on this site, so it's
   built as the same drag-scroll .cta-mobile__stats-row/-track pattern
   (initDragScrollRow) rather than trying to squeeze 4 stats into one
   screen width. Per-stat padding (0 60px, first stat 0 on the left)
   reproduces Figma's own box widths exactly: item 1's box is padding-free
   (203px = its content width exactly), items 2-4 each have symmetric
   60px padding around their number+symbol group — confirmed against the
   metadata's absolute x/width values for all 4 "advantages" children. */
.cta-mobile {
  display: none;
}

@media (max-width: 768px) {
  .cta {
    height: auto;
    overflow: visible;
  }

  .cta > .section-inner {
    position: static;
  }

  .cta__top,
  .cta__stats {
    display: none;
  }

  /* top padding restored — her call (2026-08-06): the gap up to block 10
     should read as OLIVE (this block's own background), not beige, so it
     lives here rather than on .pricing-mobile's bottom (now 0, see its
     comment). 82px not 80 — .cta carries the site's usual -2px full-bleed
     seam guard (see BLOCK_STARTER_KIT.md), which pulls this section up
     2px; +2 here cancels it so the MEASURED gap (getBoundingClientRect,
     .pricing-mobile's last card to .cta-mobile__title) lands on exactly
     80px, same compensation already applied to .stories-mobile's bottom
     padding for the block 9/10 seam. */
  /* bottom 82px not 80 — same -2px seam-guard compensation as the top
     padding above, this time for .faq's own margin:-2px seam vs. THIS
     section (block 12 built 2026-08-06); confirmed via
     getBoundingClientRect between .cta-mobile's last child and
     .faq-mobile__header. */
  .cta-mobile {
    display: flex;
    flex-direction: column;
    gap: 60px;
    padding: 82px 20px 82px;
  }

  .cta-mobile__title {
    width: 335px;
  }

  .cta-mobile__heading {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 40px;
    line-height: 1;
    letter-spacing: -0.8px;
    text-transform: uppercase;
  }

  .cta-mobile__heading-tier--1 { color: #2B3D2D; }
  .cta-mobile__heading-tier--2 { color: rgba(43, 61, 45, 0.8); }
  .cta-mobile__heading-tier--3 { color: rgba(43, 61, 45, 0.6); }
  .cta-mobile__heading-tier--4 { color: rgba(43, 61, 45, 0.4); }

  .cta-mobile__media-group {
    display: flex;
    flex-direction: column;
    gap: 20px;
    width: 337px;
  }

  .cta-mobile__art {
    position: relative;
    width: 100%;
    aspect-ratio: 422.45 / 580.55;
    overflow: hidden;
  }

  .cta-mobile__art-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .cta-mobile__lead {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 32px;
    line-height: 1.1;
    letter-spacing: -0.64px;
    color: var(--beige-100);
  }

  .cta-mobile__lead-dim {
    color: var(--beige-80);
  }

  .cta-mobile__stats-row {
    position: relative;
    overflow-x: auto;
    touch-action: pan-x pan-y;
    cursor: grab;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .cta-mobile__stats-row::-webkit-scrollbar {
    display: none;
  }

  .cta-mobile__stats-row.is-dragging {
    cursor: grabbing;
  }

  .cta-mobile__stats-track {
    display: flex;
    width: max-content;
  }

  .cta-mobile__stat {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    flex: 0 0 auto;
    padding: 0 60px;
  }

  .cta-mobile__stat:first-child {
    padding-left: 0;
  }

  .cta-mobile__stat-number {
    display: flex;
    align-items: flex-start;
    margin: 0 0 -20px;
    line-height: 1;
    text-transform: uppercase;
    color: var(--beige-80);
  }

  .cta-mobile__stat-value {
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 100px;
    line-height: 1;
    letter-spacing: -3px;
  }

  .cta-mobile__stat-symbol {
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 70px;
    line-height: 1;
    letter-spacing: -2.1px;
  }

  .cta-mobile__stat-label {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    letter-spacing: -0.4px;
    text-align: right;
    white-space: nowrap;
    color: var(--beige-80);
  }
}

/* ==========================================================================
   FAQ SECTION — "12_FAQ" (Figma node 978:1468, file 6tA1k2sLpFm5U7cLn9p9rZ)
   5-item accordion, all items closed by default, each toggles independently
   (her call — not single-open). Only question 02 had answer copy authored
   in Figma; 01/03/04/05 were supplied separately and aren't in the mockup.
   Height is intentionally NOT a fixed --u constant like every other block:
   opening an item must grow the section, so .section-inner here breaks from
   the site-wide position:absolute;inset:0 rule (see below) and the list
   sits in normal flow instead. Not opted into the site-wide scroll-lag
   system for the same reason — see BLOCK_STARTER_KIT.md.
   ========================================================================== */

.faq {
  position: relative;
  background-color: var(--beige-100);
  /* full-bleed seam guard vs. .cta above — see BLOCK_STARTER_KIT.md's
     "full-bleed seam hairline gotcha" */
  margin: -2px 0 0;
  /* symmetric with .faq__header's own 150u top offset — the Figma frame's
     bottom gap only worked out to 88u because that mockup had question 02
     pinned open; with every item closed by default that measurement isn't
     meaningful, so this is a plain 150u to match the top instead */
  padding-bottom: calc(var(--u) * 150);
}

/* deviates from the global .section-inner rule on purpose — see the block
   comment above. display:flow-root establishes its own block-formatting
   context so .faq__list's margin-top doesn't collapse through it and
   .faq's own top edge — without it, that 529u margin "leaked" upward and
   pushed the whole section down away from .cta instead of creating space
   for the header inside it. */
.faq > .section-inner {
  position: static;
  display: flow-root;
}

.faq__header {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(var(--u) * 150);
  width: calc(var(--u) * 1114);
}

.faq__eyebrow {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.5;
  color: #2B3D2D;
}

.faq__eyebrow .italic-symbol {
  margin-right: 10px;
}

.faq__heading {
  margin: calc(var(--u) * 20) 0 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 500;
  font-size: calc(var(--u) * 96);
  line-height: 1;
  letter-spacing: calc(var(--u) * -2.88);
  text-transform: uppercase;
}

.faq__heading-a { color: #2B3D2D; }
.faq__heading-b { color: rgba(43, 61, 45, 0.8); }

.faq__lead {
  position: absolute;
  left: calc(var(--u) * 611);
  top: calc(var(--u) * 201);
  width: calc(var(--u) * 294);
  margin: 0;
  display: flex;
  flex-direction: column;
}

.faq__lead-line {
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  color: rgba(43, 61, 45, 0.8);
}

/* the missing 50u left/right inset here was the root cause of the lead
   paragraph misalignment too: .faq__lead's 611u offset is measured from
   .faq__header's own left edge (already inset 50u), while .faq__question's
   611u offset is measured from .faq__list's left edge — with no inset on
   the list, that put the two 611u marks 50u apart instead of on the same
   vertical line */
.faq__list {
  margin: calc(var(--u) * 529) calc(var(--u) * 50) 0;
  border-top: 1.5px solid rgba(43, 61, 45, 0.6);
}

.faq__item {
  border-bottom: 1.5px solid rgba(43, 61, 45, 0.6);
}

.faq__row-heading {
  margin: 0;
  font-weight: 400;
}

.faq__row {
  display: flex;
  width: 100%;
  align-items: flex-end;
  justify-content: space-between;
  gap: calc(var(--u) * 40);
  padding: calc(var(--u) * 40) 0 calc(var(--u) * 10);
  border: 0;
  background: none;
  cursor: pointer;
  text-align: left;
  font-family: 'Helvetica Neue', sans-serif;
  color: #2B3D2D;
}

.faq__row-left {
  display: flex;
  align-items: flex-end;
  min-width: 0;
}

.faq__num {
  flex: 0 0 calc(var(--u) * 611);
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.24);
}

.faq__num .italic-symbol {
  margin: 0 10px;
}

/* no size difference between open/closed on purpose — she tried the
   Figma-authored 40px->48px bump (first as font-size, then as a transform
   to fix the resulting jitter) and asked for it removed outright: the
   question stays one constant size, only the answer copy gets a reveal. */
.faq__question {
  max-width: calc(var(--u) * 750);
  font-size: calc(var(--u) * 40);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.8);
  font-weight: 400;
  transition: color 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

/* hover affordance on the row, same signature easing as every other hover
   transition on the site — question text and the "( +/- )" toggle both dim
   to 60% opacity, purely pointer feedback, independent of the open/close
   state itself. A scale on the toggle was tried first and rejected (read as
   jittery/twitchy on hover) — plain opacity is calmer. */
@media (hover: hover) and (pointer: fine) {
  .faq__row:hover .faq__question {
    color: rgba(43, 61, 45, 0.6);
  }

  .faq__row:hover .faq__toggle {
    opacity: 0.6;
  }
}

.faq__toggle {
  display: flex;
  align-items: flex-end;
  flex-shrink: 0;
  font-size: calc(var(--u) * 40);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.8);
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.faq__toggle .italic-symbol {
  margin: 0 10px;
}

/* "+"/"-" swap: same two-stacked-spans translateY recipe as every other
   glyph-swap on this site (see .craft__cta-plus-char), just triggered by
   the .is-open class instead of :hover */
.faq__toggle-track {
  position: relative;
  display: inline-block;
  overflow: hidden;
  width: calc(var(--u) * 24);
  height: calc(var(--u) * 44);
  vertical-align: bottom;
}

/* width:100% + text-align:center (not left:0 alone) — "+" and "-" aren't
   the same glyph width in Helvetica Neue, so left-anchoring them let "+"
   sit centered by coincidence while "-" read as shifted left */
.faq__toggle-char {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  text-align: center;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.faq__toggle-char--plus { transform: translateY(0%); }
.faq__toggle-char--minus { transform: translateY(100%); }
.faq__item.is-open .faq__toggle-char--plus { transform: translateY(-100%); }
.faq__item.is-open .faq__toggle-char--minus { transform: translateY(0%); }

/* auto-height accordion reveal: grid-template-rows 0fr -> 1fr, not a JS
   height measurement — CSS-native and correct for every item regardless of
   its answer's length. The signature hover curve (fast-start/slow-end) read
   too abrupt for a reveal this size — switched to the project's OTHER
   established easing, the symmetric ease-in-out from the block 7 card
   crossfade (smooth start AND end, not just the end), with a longer
   duration for a calmer, less snappy motion. */
.faq__answer-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.8s cubic-bezier(0.65, 0, 0.35, 1);
}

.faq__item.is-open .faq__answer-wrap {
  grid-template-rows: 1fr;
}

.faq__answer-inner {
  overflow: hidden;
  min-height: 0;
}

/* the padding lives here, one level below .faq__answer-inner, on purpose:
   overflow:hidden only zeroes out an element's own CONTENT-based minimum
   size for grid track sizing, not its padding — padding on .faq__answer-
   inner itself would keep the closed row 24+40=64u tall instead of
   collapsing to 0 */
.faq__answer-body {
  padding: calc(var(--u) * 24) 0 calc(var(--u) * 40) calc(var(--u) * 611);
}

.faq__answer-body p {
  max-width: calc(var(--u) * 598);
  margin: 0 0 calc(var(--u) * 20);
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  color: rgba(43, 61, 45, 0.6);
}

.faq__answer-body p:last-child {
  margin-bottom: 0;
}

@media (prefers-reduced-motion: reduce) {
  .faq__answer-wrap,
  .faq__toggle-char,
  .faq__toggle,
  .faq__question,
  .faq__num {
    transition: none;
  }
}

/* --- faq mobile (node 978:1137 "12_FAQ") ---
   Same two-part override mechanism as every other -mobile block: base
   display:none here, real layout inside the @media block below.
   Accordion mechanism (grid-template-rows 0fr->1fr, padding one level
   below the overflow:hidden element — see BLOCK_STARTER_KIT.md's own
   "grid-template-rows 0fr-collapse trap" note) is copied from desktop's
   .faq__answer-wrap exactly, just under faq-mobile__* names — see
   index.html's block comment for why this isn't a shared-class reuse.

   Top padding: REVERSED 2026-08-07 (was removed on purpose 2026-08-06,
   see [[project-artefact-mobile-small-fixes-batch1]]) — her explicit call,
   she wants a real 80px BEIGE gap here distinct from .cta-mobile's own
   80px olive bottom padding, not the same gap read once. The literal
   Figma value (80px, matching the header's own 80px-from-top inset) turns
   out to be correct as-is; the "would double the gap" reasoning that
   removed it was based on a shared-gap convention she has since rejected
   (a same-colour continuation didn't read as a gap on her real phone).
   Bottom padding (80px) is unrelated, unchanged, still a plain symmetric
   guess pending block 13's own mobile pass. */
.faq-mobile {
  display: none;
}

@media (max-width: 768px) {
  .faq__header,
  .faq__list {
    display: none;
  }

  .faq-mobile {
    display: flex;
    flex-direction: column;
    gap: 60px;
    padding: 80px 20px;
  }

  .faq-mobile__header {
    display: flex;
    flex-direction: column;
    width: 335px;
  }

  .faq-mobile__eyebrow {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.5;
    color: #2B3D2D;
  }

  .faq-mobile__eyebrow .italic-symbol {
    margin-right: 10px;
  }

  .faq-mobile__heading {
    margin: 20px 0 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 500;
    font-size: 46px;
    line-height: 1;
    letter-spacing: -0.92px;
    text-transform: uppercase;
  }

  .faq-mobile__heading-a { color: #2B3D2D; }
  .faq-mobile__heading-b { color: rgba(43, 61, 45, 0.8); }

  .faq-mobile__lead {
    margin: 10px 0 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 20px;
    line-height: 1.1;
    color: rgba(43, 61, 45, 0.8);
  }

  .faq-mobile__list {
    border-top: 1.5px solid rgba(43, 61, 45, 0.6);
  }

  .faq-mobile__item {
    border-bottom: 1.5px solid rgba(43, 61, 45, 0.6);
  }

  .faq-mobile__row-heading {
    margin: 0;
    font-weight: 400;
  }

  .faq-mobile__row {
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: 100%;
    padding: 24px 0 10px;
    border: 0;
    background: none;
    cursor: pointer;
    text-align: left;
    font-family: 'Helvetica Neue', sans-serif;
    color: #2B3D2D;
    /* kills the default blue/gray flash mobile browsers put on tapped
       buttons — she caught it live (2026-08-06), no other button on the
       site has hit this yet since none are this big a tap target */
    -webkit-tap-highlight-color: transparent;
  }

  .faq-mobile__row-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
  }

  .faq-mobile__num {
    font-size: 16px;
    line-height: 1.1;
    letter-spacing: -0.16px;
  }

  .faq-mobile__num .italic-symbol {
    margin: 0 6px;
  }

  .faq-mobile__question {
    max-width: 300px;
    font-size: 24px;
    line-height: 1.1;
    letter-spacing: -0.48px;
    font-weight: 400;
  }

  .faq-mobile__toggle {
    display: flex;
    align-items: flex-start;
    flex-shrink: 0;
    font-size: 24px;
    line-height: 1.1;
    letter-spacing: -0.48px;
  }

  .faq-mobile__toggle .italic-symbol {
    margin: 0 6px;
  }

  .faq-mobile__toggle-track {
    position: relative;
    display: inline-block;
    overflow: hidden;
    width: 15px;
    height: 27px;
    vertical-align: bottom;
  }

  .faq-mobile__toggle-char {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    text-align: center;
    transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .faq-mobile__toggle-char--plus { transform: translateY(0%); }
  .faq-mobile__toggle-char--minus { transform: translateY(100%); }
  .faq-mobile__item.is-open .faq-mobile__toggle-char--plus { transform: translateY(-100%); }
  .faq-mobile__item.is-open .faq-mobile__toggle-char--minus { transform: translateY(0%); }

  .faq-mobile__answer-wrap {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.8s cubic-bezier(0.65, 0, 0.35, 1);
  }

  .faq-mobile__item.is-open .faq-mobile__answer-wrap {
    grid-template-rows: 1fr;
  }

  .faq-mobile__answer-inner {
    overflow: hidden;
    min-height: 0;
  }

  .faq-mobile__answer-body {
    padding: 0 0 24px;
  }

  .faq-mobile__answer-body p {
    margin: 0 0 16px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 18px;
    line-height: 1.1;
    color: rgba(43, 61, 45, 0.6);
  }

  .faq-mobile__answer-body p:last-child {
    margin-bottom: 0;
  }

  @media (prefers-reduced-motion: reduce) {
    .faq-mobile__answer-wrap,
    .faq-mobile__toggle-char {
      transition: none;
    }
  }
}

/* ==========================================================================
   SOCIAL SECTION — "13_соц мережі" (Figma node 978:1536)
   ========================================================================== */

/* min-height:100vh, not a flat 890u — her catch, live on video: while the
   carousel is pinned (a long scroll-locked hold, unlike a section you
   simply scroll past), a section shorter than the real viewport shows a
   big dead beige gap below the cards for the ENTIRE pin duration, not
   just a passing seam. Same root cause and same fix shape as .footer's
   min-height:100vh, but the content here (header+content, both
   absolutely positioned) is also re-centered vertically — see the
   `calc(50% - …)` tops below — rather than pinned to top/bottom, since
   this is one visual cluster, not a top-group/bottom-group footer split. */
.social {
  position: relative;
  min-height: 100vh;
  /* full-bleed seam guard vs. .faq above — see BLOCK_STARTER_KIT.md's
     "full-bleed seam hairline gotcha" */
  margin: -2px 0 0;
  background-color: var(--beige-100);
  color: #2B3D2D;
}

/* --- heading group (Figma frame "Н" at 50,0) --- */

/* Cluster (.social__header + .social__content) is 740u tall total
   (content's own bottom, 292+448u hero-photo, is the tallest point).
   Centering it as one unit: header's top = 50% of .social's real height
   minus half the cluster (370u); .social__content keeps its original
   292u offset FROM the header, just added on top of that same centred
   start instead of literal 0. */
.social__header {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(50% - var(--u) * 370);
  width: calc(var(--u) * 1819);
}

.social__heading-row {
  display: flex;
  align-items: center;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 500;
  font-size: calc(var(--u) * 96);
  line-height: 1;
  letter-spacing: calc(var(--u) * -2.88);
  text-transform: uppercase;
  white-space: nowrap;
}

/* top row: two words at the frame's own edges (Figma's "justify-between") */
.social__heading-row--top {
  justify-content: space-between;
  color: #AAB1C3;
}

/* bottom row: fixed 386u gap between the two words (Figma's own value, not
   a justify — the row is narrower than the frame and left-aligned) */
.social__heading-row--bottom {
  justify-content: flex-start;
  gap: calc(var(--u) * 386);
  color: var(--blue);
}

/* --- content row (Figma frame "content" at 50,292) --- */

.social__content {
  position: absolute;
  left: calc(var(--u) * 50);
  top: calc(50% - var(--u) * 78);
  width: calc(var(--u) * 1819);
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 10);
}

.social__left {
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 10);
  width: calc(var(--u) * 904);
}

.social__hero-photo {
  position: relative;
  flex: 0 0 auto;
  width: calc(var(--u) * 448);
  height: calc(var(--u) * 448);
  overflow: hidden;
}

/* +1px/bottom:auto — same hairline-on-hover-scale-settle fix as
   .gallery__photo img */
.social__hero-photo img {
  position: absolute;
  top: 0;
  right: 0;
  bottom: auto;
  left: 0;
  width: 100%;
  height: calc(100% + 1px);
  object-fit: cover;
  object-position: center 65%;
  display: block;
}

.social__text {
  flex: 0 0 auto;
  width: calc(var(--u) * 388);
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 30);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.3);
  color: rgba(43, 61, 45, 0.8);
}

/* --- carousel. Native horizontal scroll (overflow-x + hidden scrollbar),
   width fits exactly 3 cards (295*3 + 10*2 = 905u) so it reads flush with
   the mockup at rest; initSocialCarousel() layers wheel-redirect + pointer
   drag on top so a plain mouse wheel/drag pages through the other 4 cards
   too, without the .gallery/.stories full-page pin. --- */

/* mask, not the moving part — same split as .gallery (section is the mask)
   / .stories (.stories__grid is the mask); here it's this one child instead
   of the whole section, since only the card row scrubs, not the header. */
.social__carousel {
  position: relative;
  flex: 0 0 auto;
  width: calc(var(--u) * 905);
  height: calc(var(--u) * 295);
  overflow: hidden;
}

.social__track {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  align-items: flex-start;
  gap: calc(var(--u) * 10);
}

.social__card {
  flex: 0 0 auto;
  width: calc(var(--u) * 295);
}

/* override the shared .scroll-up bottom:50u — align with
   .social__hero-photo's own bottom edge (the big left photo), not cards
   2/3 like .steps uses. Measured live as the DIRECT pixel gap between the
   two elements at the same instant (not derived from .social's own
   getBoundingClientRect() — .social is a pinned section, so its bounding
   rect reads differently depending on scroll/pin state, which produced a
   wrong 619.5u the first time this was computed that way). Re-measured
   2026-08-11 against .section-inner's own bottom edge (79.5u was still
   63.85px too low — the earlier pass measured a stale/pre-pin state).
   Her follow-up: raise it one more arrow-height (36*1.1 = 39.6u) above
   that flush position, leaving a one-arrow-height gap between the
   arrow's own bottom edge and the photo's bottom edge: 176.4 + 39.6 = 216. */
.social .scroll-up {
  bottom: calc(var(--u) * 216);
}

/* button, not div — same reset .gallery__photo uses, so it can be a real
   .js-cursor-media hover/click target */
.social__photo {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
}

/* covers both the photo cards and the one video card (978:1551 in the
   mockup) — same +1px/bottom:auto hairline fix as .gallery__photo img.
   pointer-events:none so a click always resolves on the button, never gets
   stolen by the media element itself. */
.social__photo img,
.social__photo video {
  position: absolute;
  top: 0;
  right: 0;
  bottom: auto;
  left: 0;
  width: 100%;
  height: calc(100% + 1px);
  object-fit: cover;
  display: block;
  pointer-events: none;
}

.social__card:nth-child(1) .social__photo img { object-position: center 58%; }
.social__card:nth-child(4) .social__photo img { object-position: 55% 68%; }
.social__card:nth-child(5) .social__photo img { object-position: 45% 50%; }
.social__card:nth-child(6) .social__photo img { object-position: 35% 40%; }
.social__card:nth-child(7) .social__photo img { object-position: 40% 72%; }

.social-mobile {
  display: none;
}

@media (max-width: 768px) {
  /* .social uses min-height:100vh + absolute-centered content on desktop
     (see the block comment above .social__header) — normal flow content
     needs the section to just size itself instead, same fix every other
     pinned/centered section got for its own mobile pass. */
  .social {
    min-height: 0;
    height: auto;
    overflow: visible;
  }

  .social > .section-inner {
    position: static;
  }

  .social__header,
  .social__content,
  .scroll-up {
    display: none;
  }

  /* padding-top:0 is her actual mobile frame value (checked via
     get_design_context, not the usual 80px default) — the visible gap
     above this block is entirely FAQ-mobile's own 80px bottom padding. */
  .social-mobile {
    display: flex;
    flex-direction: column;
    gap: 60px;
    padding: 0 0 80px 20px;
  }

  /* intro wraps heading+text in their OWN 20px gap (her explicit request)
     — separate from .social-mobile's own outer 60px gap, which now only
     applies between this whole intro block and .social-mobile__images. */
  .social-mobile__intro {
    display: flex;
    flex-direction: column;
    gap: 20px;
    width: 335px;
  }

  /* gap:0 (was 20px) — her explicit request: the heading's two wrapped
     <p> rows (4 visual lines total) should read as one continuous block
     with the SAME tight leading between rows 2/3 (the gap between the two
     <p> elements) as rows 1/2 and 3/4 (each row's own natural line-height
     wrap), not a distinct larger gap. */
  .social-mobile__header {
    display: flex;
    flex-direction: column;
    gap: 0;
    width: 335px;
  }

  .social-mobile__heading-row {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 500;
    font-size: 40px;
    line-height: 1;
    letter-spacing: -0.8px;
    text-transform: uppercase;
  }

  /* blue/600 — a shade darker than the project's own --blue (blue/500),
     used here specifically per her mobile frame; not worth a new custom
     property for a single one-off row. */
  .social-mobile__heading-row--top { color: #A0A6B6; }
  .social-mobile__heading-row--bottom { color: var(--blue); }

  .social-mobile__text {
    margin: 0;
    width: 335px;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    color: #2B3D2D;
  }

  .social-mobile__images {
    display: flex;
    flex-direction: column;
    gap: 40px;
    width: 100%;
  }

  .social-mobile__hero-photo {
    position: relative;
    width: 335px;
    height: 400px;
    overflow: hidden;
  }

  /* +1px/bottom:auto — same hairline-on-settle fix as .social__hero-photo img */
  .social-mobile__hero-photo img {
    position: absolute;
    top: 0;
    right: 0;
    bottom: auto;
    left: 0;
    width: 100%;
    height: calc(100% + 1px);
    object-fit: cover;
    display: block;
  }

  /* peek+scroll row, no pin/scrub — same mechanism as .gallery-mobile__row/
     .stories-mobile__row/.pricing-mobile__row. No left-padding to cancel
     here: .social-mobile__images is already inset by .social-mobile's own
     padding-left, and has no padding-right, so the row bleeds to the true
     right edge on its own, matching her mobile frame (its own "images"
     frame is w-full while the hero photo above stays a fixed 335px). */
  .social-mobile__row {
    position: relative;
    width: 100%;
    overflow-x: auto;
    touch-action: pan-x pan-y;
    cursor: grab;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .social-mobile__row::-webkit-scrollbar {
    display: none;
  }

  .social-mobile__row.is-dragging {
    cursor: grabbing;
  }

  .social-mobile__track {
    display: flex;
    align-items: center;
    gap: 10px;
    width: max-content;
  }

  .social-mobile__card {
    flex: 0 0 auto;
  }

  /* button, not div — same reset .social__photo uses, so it can be a real
     .js-cursor-media target (harmless on touch: the cursor module already
     gates on hover:hover+pointer:fine before doing anything). */
  .social-mobile__photo {
    display: block;
    position: relative;
    width: 162px;
    height: 162px;
    overflow: hidden;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    text-align: inherit;
    cursor: pointer;
  }

  /* covers both photo cards and the video card — same +1px/bottom:auto
     hairline fix as .social__photo img/video. */
  .social-mobile__photo img,
  .social-mobile__photo video {
    position: absolute;
    top: 0;
    right: 0;
    bottom: auto;
    left: 0;
    width: 100%;
    height: calc(100% + 1px);
    object-fit: cover;
    display: block;
    pointer-events: none;
  }

  /* same per-photo crop fine-tuning as desktop's own .social__card
     nth-child rules, matched by ASSET not by position (mobile's card
     order differs from desktop's) — cards 4-7 here are the exact same
     social-5/6/7/8.jpg files desktop already fine-tuned. */
  .social-mobile__card:nth-child(3) .social-mobile__photo img { object-position: center 58%; }
  .social-mobile__card:nth-child(4) .social-mobile__photo img { object-position: 55% 68%; }
  .social-mobile__card:nth-child(5) .social-mobile__photo img { object-position: 45% 50%; }
  .social-mobile__card:nth-child(6) .social-mobile__photo img { object-position: 35% 40%; }
  .social-mobile__card:nth-child(7) .social-mobile__photo img { object-position: 40% 72%; }
}

/* ==========================================================================
   FOOTER — "14_футер" (Figma node 978:1556), the last block
   ========================================================================== */

/* flex-col + a single 200u gap reproduces the Figma frame's own auto-layout
   (pt 50 / top-group 341 / gap 200 / bottom-group ~221 / pb 150) exactly,
   so no fixed section height is set — it sizes itself, same reasoning as
   .faq opting out of the .section-inner/fixed-height pattern. isolation:
   isolate contains .footer::before's mix-blend-mode, same as every other
   grained section (.philosophy/.gallery__bar/.cta). */
/* min-height:100vh — her catch: as the LAST section, an intrinsic ~960u
   height (this design's own Figma frame height, scaled off viewport WIDTH)
   left it well short of viewport HEIGHT on any normal screen (measured
   441px tall against an 897px viewport at one test size) — the page
   simply ended mid-screen instead of the footer covering it. min- (not a
   flat height) so real overflow on a narrow/tall viewport still grows the
   box instead of clipping. justify-content:space-between then pins
   .footer__top to the top edge and .footer__bottom to the bottom edge of
   that full-height box, with `gap` as a floor in case the viewport is
   short enough that space-between would otherwise crush them together. */
.footer {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: calc(var(--u) * 200);
  padding: calc(var(--u) * 50) calc(var(--u) * 48);
  /* full-bleed seam guard vs. .social above — see BLOCK_STARTER_KIT.md's
     "full-bleed seam hairline gotcha" */
  margin: -2px 0 0;
  background:
    radial-gradient(60% 55% at 78% 12%, rgba(217, 224, 242, 0.55), transparent 70%),
    radial-gradient(55% 60% at 12% 88%, rgba(147, 154, 172, 0.45), transparent 70%),
    var(--blue);
  color: #2B3D2D;
}

/* same film-grain tile as .philosophy/.gallery__bar/.cta — see the note
   above .philosophy::before. Reuses the SAME --grain custom property
   initOliveGrain() already sets globally; no new JS needed. Paints ABOVE
   .footer's own background but BELOW .footer__top/.footer__bottom because
   those are given position:relative below, landing in the same
   z-index:auto paint bucket as this ::before and coming after it in DOM
   order. */
.footer::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grain, none);
  background-repeat: repeat;
  mix-blend-mode: overlay;
}

.footer__top,
.footer__bottom {
  position: relative;
}

.footer__top {
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 100);
}

/* width 907u, not the full row — her catch: this now shares its right
   edge with .footer__logo below it (same 907u column, both starting flush
   at the section's left padding) instead of stretching across the whole
   footer width like it did originally. */
.footer__menu {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: calc(var(--u) * 907);
  margin: 0;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
}

.footer__menu-link {
  position: relative;
  display: block;
  text-decoration: none;
  color: inherit;
  white-space: nowrap;
}

.footer__menu-link-text {
  display: inline-block;
}

/* EXACT same recipe as .hero__link-line-track/.hero__link-line — confirmed
   final with her 2026-08-07, DO NOT CHANGE without her asking again (see
   feedback_nav_underline_settings memory): olgaprudka.com's own nav-link
   underline values (0.5s, cubic-bezier(0.215, 0.61, 0.355, 1), origin
   flips right->left on hover), split into a track (fixed height:1px,
   never transformed) + inner line (gets the scaleX transform) so the
   hairline's thickness never jitters mid-transition. */
.footer__menu-link-line-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--u) * -6);
  height: 1px;
  overflow: hidden;
}

.footer__menu-link-line {
  position: absolute;
  inset: 0;
  background: #2B3D2D;
  transform-origin: right;
  transform: scaleX(0);
  transition: transform 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
}

.footer__menu-link:hover .footer__menu-link-line {
  transform-origin: left;
  transform: scaleX(1);
}

/* .footer__social is positioned absolutely at the exact same left offset
   as .footer__copyright (1371u from the padded content edge, both measured
   off Figma) rather than flexed with space-between — her catch: with
   space-between the social column's left edge only matched the copyright
   text's left edge by coincidence of content width, and drifted out of
   alignment. An explicit height keeps this row's own layout height equal
   to the (taller) social column now that it's out of flow. */
.footer__subscribe-row {
  position: relative;
  height: calc(var(--u) * 214);
}

/* width 907u — same column as .footer__menu/.footer__logo below it. The
   input grows to fill whatever's left after the heading and button take
   their natural width (flex:1 chained through .footer__subscribe-form ->
   .footer__subscribe-field), so the row's right edge lands flush on that
   same 907u line regardless of the heading's actual rendered width. */
.footer__subscribe {
  display: flex;
  align-items: flex-end;
  gap: calc(var(--u) * 21);
  width: calc(var(--u) * 907);
}

.footer__subscribe-heading {
  flex-shrink: 0;
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -1.28);
  color: #2B3D2D;
  white-space: nowrap;
}

.footer__subscribe-form {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: flex-end;
  gap: calc(var(--u) * 40);
  padding-bottom: calc(var(--u) * 8);
}

/* the line lives on its own element (.footer__subscribe-line), not as the
   input's own border — a CSS border anti-aliases at a visibly different
   weight than the flat height:1px boxes every other line on the site uses
   (.hero__link-underline, .hero__btn-line, …), which is what made it read
   as a different thickness. */
.footer__subscribe-field {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

.footer__subscribe-input {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  margin: 0;
  padding: 0 0 calc(var(--u) * 8);
  border: none;
  background: transparent;
  font-family: 'Helvetica Neue', sans-serif;
  font-size: calc(var(--u) * 24);
  line-height: 1;
  color: #2B3D2D;
  outline: none;
}

.footer__subscribe-input::placeholder {
  color: rgba(43, 61, 45, 0.4);
}

/* Chrome autofill highlight, take 2 — her report 2026-08-07 showed it on
   THIS field too, plus autocomplete="off" alone (tried on the popup's own
   email field) doesn't reliably stop Chrome for an <input type="email">
   it recognises as a saved value, and a box-shadow repaint (tried first)
   can only match a FLAT background colour — both this footer field and
   the free-lesson popup's sit on a gradient, so neither fix actually
   worked. This is the version that's background-agnostic: an absurdly
   long transition delay on the two properties Chrome's autofill forces
   (background-color, color) means that "change" is still technically
   queued 600000s from now — visually the field never appears to update
   at all, regardless of what's behind it. -webkit-text-fill-color (not
   color — autofill's forced text colour only answers to this) keeps the
   real text visible immediately, not deferred with everything else. */
.footer__subscribe-input:-webkit-autofill,
.footer__subscribe-input:-webkit-autofill:hover,
.footer__subscribe-input:-webkit-autofill:focus {
  -webkit-text-fill-color: #2B3D2D;
  transition: background-color 600000s 0s, color 600000s 0s;
}

.footer__subscribe-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: rgba(43, 61, 45, 0.4);
  pointer-events: none;
  transition: background-color 0.3s ease;
}

/* hover is a subtle darken, distinct from (and lighter than) focus's full
   solid line — "не перегружений" was her spec */
.footer__subscribe-field:hover .footer__subscribe-line {
  background: rgba(43, 61, 45, 0.6);
}

.footer__subscribe-input:focus ~ .footer__subscribe-line {
  background: #2B3D2D;
}

/* plus-icon swap — verbatim .hero__btn-plus recipe, see
   BLOCK_STARTER_KIT.md's "Two-line icon swap". height:1em (not a fixed
   20u box) — Figma's own "+" frame is a tight ink-bounds auto-layout hug
   around a 40u glyph, not a safe CSS box; a fixed 20u height with
   overflow:hidden clipped the glyph in half. */
.footer__subscribe-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 1em;
  padding: 0;
  border: none;
  background: none;
  font-size: calc(var(--u) * 40);
  line-height: 1;
  color: rgba(43, 61, 45, 0.8);
  overflow: hidden;
  cursor: pointer;
}

.footer__subscribe-plus-track {
  position: relative;
  display: block;
  line-height: 1;
}

.footer__subscribe-plus-char {
  display: block;
  transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
}

.footer__subscribe-plus-char--out {
  transform: translateY(0%);
}

.footer__subscribe-plus-char--in {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}

.js-line-hover:hover .footer__subscribe-plus-char--out {
  transform: translateY(-100%);
}

.js-line-hover:hover .footer__subscribe-plus-char--in {
  transform: translateY(0%);
}

/* positioned to match .footer__copyright's left edge exactly — see the
   comment above .footer__subscribe-row */
.footer__social {
  position: absolute;
  left: calc(var(--u) * 1371);
  top: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: calc(var(--u) * 20);
}

/* hover mechanism verbatim from .menu-overlay__social-link — see that
   rule's own comment for the full reasoning. Text dims + shifts right;
   an italic "(" hidden at rest (no layout space, position:absolute)
   fades in to fill the gap the shift opens up. Font size lives on the
   <a> itself (matching .menu-overlay__social-link exactly), not just on
   the text span — her catch: the paren had no font-size rule of its own,
   so it inherited the page's base size instead of the link's 64u and
   rendered tiny/mispositioned. Inheriting from the parent is what keeps
   paren and text the same size. */
.footer__social-link {
  position: relative;
  display: inline-flex;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 64);
  line-height: 0.9;
  letter-spacing: calc(var(--u) * -1.28);
  text-decoration: none;
  color: #2B3D2D;
}

.footer__social-link-paren {
  position: absolute;
  left: 0;
  opacity: 0;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.footer__social-link-text {
  display: inline-block;
  white-space: nowrap;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.footer__social-link:hover .footer__social-link-paren {
  opacity: 0.6;
}

.footer__social-link:hover .footer__social-link-text {
  opacity: 0.6;
  transform: translateX(calc(0.5em + 10px));
}

.footer__bottom {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: calc(var(--u) * 466);
}

.footer__contacts-logo {
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 38);
  width: calc(var(--u) * 905);
}

/* must stay on one line, same width as the logo below it — her catch: the
   defensive flex-wrap was the bug, it was wrapping onto two lines whenever
   the real Helvetica Neue metrics ran a hair wider than Figma's own
   measurement. nowrap + slightly tightened gaps (down from Figma's 107/54)
   keep it on one line with margin to spare. */
.footer__contacts {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: calc(var(--u) * 40);
}

.footer__contacts-group {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 90);
}

.footer__contact-item {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: calc(var(--u) * 10);
}

.footer__contact-label {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 18);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.36);
  color: rgba(43, 61, 45, 0.6);
  white-space: nowrap;
}

.footer__contact-value {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  letter-spacing: calc(var(--u) * -0.24);
  color: #2B3D2D;
  white-space: nowrap;
}

/* Her own export (img/logo-footer.png), soft-focus/grain baked in on
   purpose — see the section-level comment in index.html above .footer.
   -25.2%/-3.54% inset reproduces the Figma frame's own oversize (969x191
   image inside a 905x127 box) so the blurred edges have room to fall off
   without a hard clip. */
.footer__logo {
  position: relative;
  width: calc(var(--u) * 905.002);
  height: calc(var(--u) * 126.998);
}

.footer__logo-img {
  position: absolute;
  left: -3.54%;
  top: -25.2%;
  width: 107.08%;
  height: 150.4%;
  max-width: none;
  display: block;
}

.footer__copyright {
  margin: 0;
  font-family: 'Helvetica Neue', sans-serif;
  font-weight: 400;
  font-size: calc(var(--u) * 24);
  line-height: 1.1;
  color: #2B3D2D;
  white-space: nowrap;
}

.footer-mobile {
  display: none;
}

@media (max-width: 768px) {
  /* .footer's own padding is fluid --u, meant for .footer__top/__bottom —
     left alone it'd still apply a small residual amount around
     .footer-mobile on top of ITS OWN fixed padding below. Zeroed here so
     .footer-mobile owns 100% of its own spacing, same reasoning every
     other -mobile block's parent section gets (they just don't need this
     explicit reset since their padding lives on .section-inner instead).
     min-height:0 (was 100vh, the desktop last-section fix) — her catch:
     with a single flex child and no bottom-anchored sibling to fill the
     rest on mobile, min-height:100vh just left a large dead gap of footer
     background BELOW .footer-mobile's own 80px padding-bottom, so the
     real visual gap under the copyright line read as ~200px, not the 80px
     her mobile frame actually specifies. */
  .footer {
    padding: 0;
    min-height: 0;
  }

  .footer__top,
  .footer__bottom,
  .scroll-up {
    display: none;
  }

  .footer-mobile {
    display: flex;
    flex-direction: column;
    gap: 100px;
    padding: 40px 20px 80px;
  }

  .footer-mobile__top {
    display: flex;
    flex-direction: column;
    gap: 60px;
    width: 335px;
  }

  .footer-mobile__subscribe {
    display: flex;
    flex-direction: column;
    width: 335px;
  }

  .footer-mobile__subscribe-heading {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 32px;
    line-height: 1.1;
    letter-spacing: -0.64px;
    color: #2B3D2D;
  }

  .footer-mobile__subscribe-form {
    display: flex;
    align-items: flex-end;
    gap: 24px;
    width: 280px;
    height: 50px;
  }

  .footer-mobile__subscribe-field {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
  }

  .footer-mobile__subscribe-input {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    margin: 0;
    padding: 0 0 8px;
    border: none;
    background: transparent;
    font-family: 'Helvetica Neue', sans-serif;
    font-size: 24px;
    line-height: 1;
    color: #2B3D2D;
    outline: none;
  }

  .footer-mobile__subscribe-input::placeholder {
    color: rgba(43, 61, 45, 0.4);
  }

  .footer-mobile__subscribe-line {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: rgba(43, 61, 45, 0.4);
    pointer-events: none;
    transition: background-color 0.3s ease;
  }

  .footer-mobile__subscribe-field:hover .footer-mobile__subscribe-line {
    background: rgba(43, 61, 45, 0.6);
  }

  .footer-mobile__subscribe-input:focus ~ .footer-mobile__subscribe-line {
    background: #2B3D2D;
  }

  /* plus-icon swap — verbatim .footer__subscribe-btn recipe, mobile's own
     32px (matches this frame's "+" glyph size, not desktop's 40u) */
  .footer-mobile__subscribe-btn {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 1em;
    padding: 0;
    border: none;
    background: none;
    font-size: 32px;
    line-height: 1;
    color: rgba(43, 61, 45, 0.8);
    overflow: hidden;
    cursor: pointer;
  }

  .footer-mobile__subscribe-plus-track {
    position: relative;
    display: block;
    line-height: 1;
  }

  .footer-mobile__subscribe-plus-char {
    display: block;
    transition: transform 0.65s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .footer-mobile__subscribe-plus-char--out {
    transform: translateY(0%);
  }

  .footer-mobile__subscribe-plus-char--in {
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(100%);
  }

  .js-line-hover:hover .footer-mobile__subscribe-plus-char--out {
    transform: translateY(-100%);
  }

  .js-line-hover:hover .footer-mobile__subscribe-plus-char--in {
    transform: translateY(0%);
  }

  .footer-mobile__social {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  /* hover mechanism verbatim from .footer__social-link — text dims+shifts
     right, italic "(" fades in at the left edge the shift opens up. */
  .footer-mobile__social-link {
    position: relative;
    display: inline-flex;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 24px;
    line-height: 1.1;
    text-decoration: none;
    color: #2B3D2D;
  }

  .footer-mobile__social-link-paren {
    position: absolute;
    left: 0;
    opacity: 0;
    transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .footer-mobile__social-link-text {
    display: inline-block;
    white-space: nowrap;
    transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .footer-mobile__social-link:hover .footer-mobile__social-link-paren {
    opacity: 0.6;
  }

  .footer-mobile__social-link:hover .footer-mobile__social-link-text {
    opacity: 0.6;
    transform: translateX(calc(0.5em + 10px));
  }

  .footer-mobile__bottom {
    display: flex;
    flex-direction: column;
    gap: 60px;
    width: 335px;
  }

  .footer-mobile__contacts {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.1;
    color: #2B3D2D;
  }

  .footer-mobile__contacts p {
    margin: 0;
  }

  .footer-mobile__logo-group {
    display: flex;
    flex-direction: column;
    gap: 40px;
    width: 100%;
  }

  /* img/mob-logo-footer.png (swapped 2026-08-06, was the .svg export) —
     re-measured from scratch per the project's own rule for any swapped
     asset (never carry over a sibling/previous file's scale). Alpha-scanned
     the PNG directly: canvas 718x142, ink bbox ~679x104 starting at
     (23,20) — a small, tight blur-feather margin (~3% each side), nothing
     like desktop's own logo-footer.png (25%/3.5% overscale margin), so the
     old SVG's -3.54%/-25.2% inset-and-overscale trick doesn't apply here.
     Just sized to the asset's own natural aspect ratio instead (335 * 142/718
     ≈ 66px tall) — no inset/overscale needed since this export's margin is
     already the intended framing. */
  .footer-mobile__logo {
    width: 335px;
  }

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

  .footer-mobile__copyright {
    margin: 0;
    font-family: 'Helvetica Neue', sans-serif;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.1;
    color: #2B3D2D;
  }
}

@media (prefers-reduced-motion: reduce) {
  .footer__menu-link-line,
  .footer__social-link-paren,
  .footer__social-link-text,
  .footer__subscribe-plus-char,
  .footer-mobile__social-link-paren,
  .footer-mobile__social-link-text,
  .footer-mobile__subscribe-plus-char {
    transition: none;
  }
}
