/* Виправлення, дописані поверх основного масиву: побрейкпоінтні
   правки й правила під конкретні сторінки. Мають лишатись
   останніми — саме на це вони й розраховані. */

/* Було: transition: transform 0.2s linear. Це і давало «гумову» ходу —
   привід переписує transform щокадру, і кожен запис стартує новий
   200-мілісекундний перехід від проміжної позиції, якої елемент так
   і не досягає. Плавність тепер дає scrub у ScrollTrigger. */
.the-width-400vh-scrollable-div {
  will-change: transform;
}

/* Стрічку партнерів можна гортати пальцем (js/partners-rail.js). pan-y
   лишає браузеру вертикальну прокрутку сторінки, а горизонтальні жести
   віддає нам — без цього браузер або з'їдав би змах, або обробляв би
   його з затримкою на розпізнавання. Кнопок гортання тут немає: їх
   сховано нижче 992px. */
.the-overflow-hidden-mask {
  touch-action: pan-y;
}

/* Native :hover is superseded by .js-hover below (JS is the single source
   of truth so scroll- and mouse-triggered zoom can never disagree). These
   two rule sets have equal specificity (two classes each), so .js-hover
   MUST come after in source order to win whenever both are simultaneously
   true — which happens on every normal real-mouse hover, since native
   :hover fires a frame before our rAF-driven .js-hover catches up. */
.showcase__image:hover {
  filter: grayscale(30%);
  transform: none;
}

.gallery-image-wide:hover {
  filter: grayscale(30%);
  transform: none;
}

.gallery-image-wrapper.mobile:hover {
  z-index: 1;
}

.showcase__image.js-hover {
  filter: grayscale(0%);
  transform: scale(1.02);
}

.gallery-image-wide.js-hover {
  filter: grayscale(0%);
  transform: scale(1.01);
}

.gallery-image-wrapper.mobile.js-hover {
  z-index: 999;
}

/* The .active states for the "Чому ми?" timeline dots layer a colorful
   gradient background-image on top of the black background-color, so the
   dot renders as a rainbow, not solid black. Same rule, positioned after
   the original so it wins the cascade tie — just strips the image, the
   black background-color underneath already does the rest. */
.process-dot.active {
  background-image: none;
}

.process-dot-inner.active {
  background-image: none;
}

.process-dot-dot.active {
  background-image: none;
}

/* Mobile hamburger icon: replaced a Lottie-rendered icon (loaded async
   from a JSON file, data-autoplay:0, so its first frame came from an
   interaction) with a plain CSS icon. On some real devices the Lottie SVG never
   visibly renders even though the button itself is present and still
   opens the menu correctly (couldn't reproduce the exact failure locally,
   but a dependency-free icon can't have this failure mode at all).
   Toggled via the same class the old nav script already added and removed
   on open/close — no interaction logic touched. Пізніше прибрано й сам
   прихований елемент Lottie разом з JSON-файлом: він нічого не показував,
   але браузер однаково тягнув його на кожній сторінці окремим запитом. */

.css-hamburger-icon {
  width: 26px;
  height: 20px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  flex: none;
}

.css-hamburger-icon span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background-color: #000;
  border-radius: 2px;
  transition: top 0.25s ease, transform 0.25s ease, opacity 0.2s ease;
}

.css-hamburger-icon span:nth-child(1) {
  top: 0;
}

.css-hamburger-icon span:nth-child(2) {
  top: 9px;
}

.css-hamburger-icon span:nth-child(3) {
  top: 18px;
}

/* At the ≤479px breakpoint, the original set .gallery-grid to align-
   items:center in flex-direction:column mode instead of the default
   stretch. Its <a> children have no width of their own (no text content,
   only class="w-inline-block" — the id-based link-block-16 class exists
   on just the first one, not a reliable selector), so with stretch
   disabled they collapse to 0 width and every gallery photo disappears.
   Restoring full width on the flex children is the minimal fix — doesn't
   touch the original align-items declaration in case it's relied on
   elsewhere. */
@media screen and (max-width: 479px) {
  .gallery-showcase-grid > a {
    width: 100%;
  }
}

/* At ≤479px, .hero-copy-column (hero heading/subtitle/button wrapper) is
   position:absolute with a fixed width narrower than the viewport and no
   centering of its own — it sits flush against the left edge, so its
   text-align:center children end up centered within an off-center box
   instead of the actual screen. Re-centers the box itself; every child
   already uses position:static so none of them depend on this box's own
   left offset. Also widens it — at its original ~230px width "DAV
   CONSTRUCTIVE" (shortened from the original two-line heading, which this
   width was sized for) wraps to two lines; 90% comfortably fits it on
   one. */
@media screen and (max-width: 479px) {
  .hero-copy-column {
    left: 50%;
    transform: translateX(-50%);
    width: 90%;
  }
}

/* about.html hero: .about-hero__image is a deliberately oversized/scaled photo (the
   same kind of intentional parallax bleed used elsewhere on the site), but
   unlike those, none of its ancestors clip the overflow — it was pushing
   the whole page's scrollWidth past the viewport, causing real horizontal
   scroll on every phone. Contains it at the source instead of guessing
   which breakpoint needs it; clipping a deliberately-oversized decorative
   image can't make it look wrong. */
.about-hero__inner {
  overflow: hidden;
}

/* .navbar is position:fixed with width:100% — for fixed elements, % width
   resolves against the viewport (window.innerWidth), not against <body>.
   On real phones (and this dev tool) those two differ by the scrollbar
   gutter / viewport calculation, so the navbar rendered ~18px wider than
   the actual visible page — pushing the hamburger button that same 18px
   past the right edge while the phone icon on the left stayed flush,
   breaking their matching margins. Pinning both edges instead of relying
   on a percentage width is the standard, robust fix — resolves correctly
   regardless of any viewport/body width discrepancy. */
.navbar {
  left: 0;
  right: 0;
  width: auto;
}

/* .process-item-desc's hardcoded 200px width at ≤479px runs ~7px past the
   card's own overflow:hidden boundary, clipping the last sliver of the
   longest description lines. A fixed px width only accounted for one
   test width (375px) and still clipped on narrower phones like the
   360px Galaxy S8+ — using calc(100vw - Npx) instead scales with the
   actual viewport so it stays safely inside the card at any width,
   rather than needing a fresh guess per device. */
@media screen and (max-width: 479px) {
  .process-item-desc, .process-item-desc.process__desc--alt, .process-item-desc.process__desc--alt.process__desc--tablet {
    width: calc(100vw - 195px);
  }
}

/* about.html contact CTA: at ≤479px .contact-right-content-wrap switches
   to width:auto (shrink-to-fit its content) instead of a definite width.
   margin:auto (inherited from the 767px rule) doesn't reliably center a
   shrink-to-fit flex item, so it renders wider than its own parent
   (.contact-row) and overflows to the right — 32px gap on the left, only
   ~5px on the right, plus the contact form itself runs noticeably
   off-center. Giving it a definite 100% width makes it fill its parent
   properly, so the existing centering behaves as intended. */
@media screen and (max-width: 479px) {
  .contact-right-content-wrap {
    width: 100%;
  }
}

/* Same shrink-to-fit-vs-centered problem one level deeper: fixing the wrap
   above still left its own child, .contact-right-from-wrap (the actual
   form), auto-sized and centered by the same flex row — it rendered 338px
   inside its now-296px parent, 21px overflow on both sides. Chasing the
   bug down to its own child rather than assuming one fix covers the whole
   card. Same fix, same reason: a definite width instead of shrink-to-fit. */
@media screen and (max-width: 479px) {
  .contact-right-from-wrap {
    width: 100%;
  }
}

/* One more level down still: the submit button's value text ("ДОЛУЧИТИСЬ
   ДО КОМАНДИ") can't wrap — form controls render their value as a single
   unbreakable line — so with the base rule's 40px side padding it forces
   a ~302px box regardless of the button's container, 42px wider than the
   260px the card actually has at ≤479px, sticking out past the card's
   right edge. A definite 100% width plus smaller side padding gives the
   text enough room inside the card instead of pushing past it. */
@media screen and (max-width: 479px) {
  .about-form__submit {
    width: 100%;
    padding-left: 15px;
    padding-right: 15px;
  }
}

/* index.html final CTA ("zakluk") background block: hardcoded to
   width:393px at ≤479px — the exact width of the device the designer was
   previewing on (iPhone 14/15 Pro) instead of a relative unit. Harmless at
   393px and wider, but on any narrower phone (e.g. the 360px Galaxy S8+)
   it pushes 33px past the section's edge and creates real page-level
   horizontal scroll. width:100% fills the section at any width instead. */
@media screen and (max-width: 479px) {
  .process-visual-wrap {
    width: 100%;
  }
}

/* Same "hardcoded to the designer's own preview width" bug, one breakpoint
   up: at the 767px (tablet) breakpoint, .contact-row itself is pinned to
   width:603px — presumably exactly what it measured on whatever tablet
   canvas it was styled on. Fine at 603px+ but on a narrower tablet/
   foldable (tested at 550px) it overflows its own parent (.about__contact-inner,
   486px there) by 117px, dragging both stacked children (the info column
   and the form column, both width:100% of this row) out along with it.
   Scoped to exactly the 480–767px gap so it can't touch the ≤479px rule
   above (which already correctly resets this to width:auto). */
@media screen and (min-width: 480px) and (max-width: 767px) {
  .contact-row {
    width: 100%;
  }
}

/* .contact-row's own sibling child carries the identical bug independently
   at this same breakpoint: width:603px with margin:auto — the auto margin
   can't center a fixed-width box that's wider than its parent, so it
   overflows to the right (0 left / 117px right at 550px), same shape as
   the ≤479px .contact-right-content-wrap bug fixed above, just one
   breakpoint over. Its sibling .contact-left-content-wrap already uses
   width:100% here — bringing this one in line with it. */
@media screen and (min-width: 480px) and (max-width: 767px) {
  .contact-right-content-wrap {
    width: 100%;
  }
}

/* index.html hero: .hero-title.hero-title-text ("DAV CONSTRUCTIVE") is
   hardcoded to width:700px from the base (desktop) rule. Nothing between
   the base rule and ≤479px ever narrows it — the ≤991px and ≤767px rules
   only adjust margins — so across the whole 480–767px tablet gap it holds
   at a fixed 700px and (with this section's 8px left margin at ≤767px)
   overflows any viewport under 708px, e.g. 158px past the edge at 550px.
   ≤479px already sidesteps this by swapping in a different, narrower
   heading entirely; this range needs its own fix. A calc() width scales
   with the viewport instead of assuming one fixed tablet size, and the
   text wraps cleanly to "DAV" / "CONSTRUCTIVE" on two lines — matching
   the two-line logo in the navbar — rather than clipping or scrolling. */
@media screen and (min-width: 480px) and (max-width: 767px) {
  .hero-title.hero-title-text {
    width: calc(100vw - 48px);
  }
}

/* contact.html hero paragraph: same story again, third time this exact
   shape of bug turns up. Base rule hardcodes width:539.297px; the only
   override is at ≤479px (width:auto), so 480–767px is left holding the
   fixed value — 21px overflow at 550px (32px left margin + 539px width vs
   a 550px viewport). Ordinary wrapping text, so width:100% is enough on
   its own here (no calc() needed like the unbreakable heading/button
   cases above) — confirmed the paragraph still reads as a normal
   3-line block, not unnaturally narrow. */
@media screen and (min-width: 480px) and (max-width: 767px) {
  .contact__lead.contact-change {
    width: 100%;
  }
}

/* index.html service-card contact modals (all 4 — .modal-wrapper.opt/gen/
   efect/zone share this same class, see HANDOFF.md §7 for how they're
   opened/matched): the popup's own backdrop box is hardcoded to
   width:350px at ≤479px. Invisible down to 350px, but the narrowest real
   phones (320px, e.g. Galaxy Fold/older iPhone SE) are narrower than
   that — the box (and, since it's positioned via right:0 off this same
   box, the × close button with it) sits 30px past the right edge,
   putting the close button off-screen. width:100% keeps it filling
   exactly whatever the modal's own space is, close button included, at
   any width from 320px up — confirmed no overflow and the close button
   back in the visible top-right corner at 320px.

   Follow-up (§101): user asked for a 10px margin from the screen edges
   instead of flush-edge-to-edge. The base rule already sets both
   `left:0` and `right:0` (confirmed via getComputedStyle, not just the
   `inset` shorthand) — with left, right, AND a definite width all
   specified on an absolutely positioned box, the spec has `right` win
   out and get silently ignored/recomputed from the other three, so
   simply changing `width` to `calc(100% - 20px)` here would have kept
   `left:0` and shifted the whole 20px gap onto the right side only, not
   split evenly. Setting `left:10px; right:10px` and letting `width` go
   back to `auto` sidesteps that entirely — with both offsets pinned and
   width auto, the browser computes the box's width as whatever's left
   between them, which is symmetric by construction. */
@media screen and (max-width: 479px) {
  .service-modal-panel.back-blur.model-size1 {
    width: auto;
    left: 10px;
    right: 10px;
  }
}

/* index.html "Чому ми?" process timeline (.process-timeline): each
   .process-item is its own independent CSS Grid (grid-template-columns:
   1fr minmax(10rem,.35fr) 1fr), and the two 1fr side columns size off
   that row's own content — different description-text lengths per row,
   plus which side (icon vs. Text) lands in column 1 flipping by row via
   .process-item-block.align-right{order:9999}. Result: the "1fr" columns
   compute to different actual pixel widths row-to-row, so the center
   column (and the .process-dot inside it) lands at a different absolute X
   per row instead of sitting on one straight line — confirmed via
   getComputedStyle: grid-template-columns alternated between "250px 160px
   201.203px" and "201.203px 160px 250px" row-to-row at 768px. The
   original's fix for this — abandon grid-based centering for .process-
   item-center, switch to position:absolute pinned to a fixed inset from
   the row's own left edge — already exists, but only at ≤767px (same
   tablet-gap shape as the fixes above: the real target device, iPad Mini,
   is exactly 768px and falls just outside it). Copied verbatim rather
   than reduced to the minimum diff — tried a smaller version (just
   .process-item-center's own position:absolute, leaving .process-item as
   display:grid) first, but removing the center dot from grid flow without
   also removing the *row* from grid collapses the two remaining grid
   children together, losing the gap the center column used to reserve.
   The ≤767px flex-based layout doesn't have that problem, so this is that
   same block, unmodified, widened to 768–991px. Separately, .process-
   line-ray-2 (the thin decorative vertical line that visually threads
   through all 5 dots, one per .process-timeline, not per-row) is its OWN
   element, absolutely positioned via inset:0%+margin:auto — i.e. Centered
   at 50% of .process-timeline's width, not tied to the grid/flex dot column
   at all. First pass at this fix only copied the .process-item* rules
   above and missed it, so the line stayed pinned at dead-center (x=384 at
   768px) while the now-correctly-aligned dots sat in their flex position
   near the left (~x=160) — a straight line cutting through the middle of
   the text content instead of running through the dots. The original
   ≤767px block repositions this line too (margin-left: calc(2.5rem +
   2px), overriding the auto-centering with a fixed left offset matching
   .process-item's own padding-left:5rem) — copied that rule in as well.
   .process-timeline-fill (the colored progress-fill line drawn on top) needs no
   rule of its own: it's a plain flex child of .process-timeline-track with no
   position of its own, so it follows automatically wherever its parent
   moves. Verified live at 768px: line and all 5 dots now sit on the same
   X. Third pass, same day: user reported description text and icon/number
   placement still inconsistent row-to-row. Root cause was the same
   "missed a sibling selector" shape as the .process-timeline-track miss
   above, just with more siblings at once — cross-checked this time by
   reading the *entire* ≤767px block for every .process-item* selector
   (not just the ones already known to be broken) and diffing against the
   live DOM's actual class list (via
   [...document.querySelectorAll('.process-timeline *')] and collecting
   .classList), rather than fixing one reported symptom at a time. Found
   and copied in the remaining gaps: .process-item-desc's .process__desc--
   alt/.process__desc--tablet variants (used on alternating rows whose
   content is the .align-right/order:9999 side) are right-aligned in the
   base desktop rule — makes sense there, text hugging the center dot
   column in a two-column layout — but the original ≤767px block flips it
   to text-align:left, since in a single-column stacked layout right-
   aligned text reads as randomly-indented rather than deliberate. Also
   copied ≤767px's .process-item-icon-frame sizing, .process-item-title
   font-size, .process-item-desc's own font-size, and — the fix for
   "numbers in different places" — .process-item-number {display:none}:
   the original's mobile answer to the number label's position flipping
   between before/after the icon per row (same .align-right swap that
   affects the dot) isn't to make the order consistent, it's to hide the
   number at this width entirely. Confirmed via the DOM diff that nothing
   else in .process-timeline's live class list (.process-dot/-dot/-inner,
   .process-item-number-wrap, .process-line-hide-top/bottom) has any
   ≤767px override left unapplied. */
@media screen and (min-width: 768px) and (max-width: 991px) {
  .process-item {
    grid-column-gap: 1rem;
    grid-row-gap: 1rem;
    grid-template-columns: 1fr minmax(10rem, .35fr);
    padding-left: 5rem;
    display: flex;
    position: relative;
    overflow: hidden;
  }

  .process-item.active {
    grid-template-columns: .5fr minmax(10rem, .4fr) 1.5fr;
    grid-auto-flow: row;
    display: flex;
    grid-column-gap: 1rem;
    grid-row-gap: 1rem;
  }

  .process-item-center {
    width: 3.5rem;
    position: absolute;
    inset: 0% auto 0% 1rem;
    overflow: hidden;
  }

  .process-item-block {
    justify-content: center;
    align-items: flex-start;
    padding-top: 1rem;
    padding-bottom: 1rem;
  }

  .process-item-block.align-right {
    text-align: left;
    order: 9999;
  }

  .process-item-content {
    grid-column-gap: .25rem;
    grid-row-gap: .25rem;
    text-align: left;
    justify-content: center;
  }

  .process-timeline-track {
    margin-left: calc(2.5rem + 3.5px);
  }

  .process-item-desc {
    font-size: .875rem;
  }

  .process-item-desc.process__desc--alt, .process-item-desc.process__desc--alt.process__desc--tablet {
    text-align: left;
  }

  .process-item-icon-frame {
    border-radius: 1.2rem;
    width: 4rem;
    height: 4rem;
  }

  .process-item-number {
    display: none;
  }

  .process-item-title {
    font-size: 1rem;
  }
}

/* about.html "career" section (id="career"): the "Круті проекти не
   робляться на одинці" heading (.contact-t) was a fairly bright, saturated
   yellow (#f4c542). First attempt swapped it for #d4af37 ("metallic gold")
   — too muted/dull once seen live, user asked for brighter; landed on
   #ffc72c (amber-gold). User later gave an exact hex directly: #FFD600
   (a pure, saturated yellow) — current value, no more iterating on this
   unless asked again. Only property touched. */
.contact-t {
  color: #FFD600;
}

/* Navbar + footer nav links ("Головна"/"Проекти"/"Про Нас"/"Контакти"):
   underline on hover, requested directly. Navbar's 4 links each have
   their own auto-generated class (.link, .header__link-projects/10/11 —
   same 4 classes on every page, confirmed via grep) wrapping .link-box >
   .link-text, with no underline element built in, so this adds a plain
   text-decoration on the inner .link-text on hover of the parent link.
   Footer's 4 links share one class (.footer__link) and already carry a
   built-in .link-underline bar div per link — but its own :hover rule
   targets the 1.5px bar itself (near-impossible to actually hover) and
   its transform stays translate(-100%) in both states, so it's
   permanently off-screen regardless — a pre-existing broken interaction
   in the export, left as-is per project convention (never edit the
   export's original rules). Simplest correct fix: same text-decoration
   approach as the navbar, targeting the 4 footer text-block classes
   (text-block-17/18/19/20, confirmed identical across pages) under
   .footer__link:hover — matches the navbar treatment instead of trying to
   repair the dormant bar animation. */
.link:hover .link-text, .header__link-projects:hover .link-text, .header__link-about:hover .link-text, .header__link-career:hover .link-text, .header__link-services:hover .link-text, .footer__link:hover .footer__link-text {
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* contact.html: user asked to remove the background photo entirely and
   keep the section plain white at every width, not just below 1440px
   where it already rendered white by default (transparent, falling
   through to the page's own white canvas). .contact's only background
   rule in the whole stylesheet is scoped to min-width:1440px
   (background-image: linear-gradient(#fffc,#fffc), url(...blueprint
   watermark...)) — overridden here unconditionally so it's gone at every
   width (confirmed .contact is used only on this one page, via grep, so
   no page-scoping needed).
   Follow-up: background-color was originally an explicit #fff (not left
   transparent) so this didn't depend on the browser's blank-canvas
   default — but that made it opaque, which turned out to fully cover
   body's own background (this element spans the entire page height, so
   nothing of body's background was ever visible on this page). Once the
   later sitewide body-texture rule was added, .contact silently blocked
   it here specifically. Switched to transparent now that there's
   something on body worth showing through — background-image:none above
   still unconditionally keeps the watermark gone regardless of color,
   so this doesn't reopen that original fix. */
.contact {
  background-image: none;
  background-color: transparent;
}

/* Mobile hamburger dropdown (.header__menu — same class also serves as
   the plain horizontal desktop nav, display:flex with no background
   there, so this fix is scoped to ≤991px where it becomes the full-width
   dropdown panel): three separate breakpoint rules (≤991px, ≤767px,
   ≤479px) each painted it solid opaque white — two via background-
   color:#fff, the ≤479px one via background-color:#fff0 (transparent)
   layered under an opaque background-image:linear-gradient(#fff,#fff),
   same net solid-white result. User wants it to match the real navbar's
   (.navbar) own look exactly: background-color:#fff9 (~60%-opacity white)
   + backdrop-filter, matching .navbar's own blur value at each breakpoint
   — blur(5px) here, dropping to blur(4px) alongside .navbar's own ≤479px
   rule (see that rule elsewhere in this file). Getting blur to render
   here at all (as opposed to matching a specific value) took multiple
   rounds of misdiagnosis — worth knowing before touching this again.
   Every hero photo on this site is put through a GSAP/ScrollTrigger
   scroll-linked zoom, and TWO separate things about that zoom each
   independently break backdrop-filter's ability to sample through the
   photo in this environment: (1) `will-change: transform`, which GSAP
   sets only while the photo is actively mid-tween, and (2) the scale
   `transform` itself (e.g. `matrix(1.07,0,0,1.07,0,0)`), which stays
   applied at whatever value the tween last reached even after GSAP clears
   will-change once it settles. Both were independently confirmed
   sufficient on their own to fully break the blur — an earlier round
   tested with will-change removed at a moment the transform hadn't
   reached a large value yet and wrongly concluded transform didn't
   matter; it does. Neither is fixable from CSS alone (every page uses a
   different, uniquely auto-generated class for its own hero photo) —
   js/menu-blur-fix.js scans for both while the dropdown is open, on
   whatever currently overlaps it on screen, and restores everything the
   instant the dropdown closes, so normal scroll performance and the zoom
   effect itself are unaffected the rest of the time. See that file's own
   comment for the full detail. With that in place, one value per
   breakpoint (matching .navbar's own) is safe across the entire ≤991px
   range — no width tiering needed. */
@media screen and (max-width: 991px) {
  .header__menu {
    background-color: #fff9;
    background-image: none;
    -webkit-backdrop-filter: blur(5px);
    backdrop-filter: blur(5px);
  }
}

@media screen and (max-width: 479px) {
  .header__menu {
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
  }
}

/* Home page "Про DAV" heading above this text was removed entirely (user
   request), making .hero__note-strong ("Одна команда. Один проект. Жодного «це
   не наш розділ»") the top element of this block instead of a secondary
   line under a 64px heading. Sized it up from the original 24px
   (--padding-pd-06) to 32px
   (--padding-pd-07) so it reads as its own lead-in
   statement rather than looking small relative to the 18px body copy
   below it. pd-07 isn't a new value invented for this — it's the same
   token already used for .contact-t's accent heading elsewhere on the
   site. .hero__note-strong is only ever used in this one place (confirmed via
   grep across all pages), so this can't affect anything else. */
.hero__note-strong {
  font-size: var(--padding-pd-07);
}

/* This same hero-text block: user wants the gap above the text (image
   bottom -> "Одна команда...") to visually match the gap below it (last
   line -> next section), instead of the bottom reading as noticeably
   bigger. The bottom gap is .hero-wrapper's own padding-bottom (a *fluid*
   clamp(2rem,5vw,5rem) at base/tablet, flat 0 at <=479px in the
   original). The top gap looks like it's simply .hero-text-bottom-
   wrapper-2's margin-top (4rem stepping down to 3rem at <=479px) but it
   isn't only that: .hero-wrapper has no padding-top/border-top, so that
   child margin COLLAPSES with .hero-wrapper's own margin-top — which the
   original sets to 0 at base, -2rem at <=991px, and +3rem at <=479px.
   Collapsing two margins together takes the largest positive minus the
   largest negative, not a simple sum, so the *effective* top gap per tier
   is: base max(0,64)=64px; <=991px down to 480px, 64+(-32)=32px
   (confirmed live — this is not the same as the child's own raw 64px
   margin-top, which is what a first pass at this fix wrongly assumed);
   <=479px max(48,48)=48px. Matched .hero-wrapper's padding-bottom to
   those three effective values so top and bottom stay equal at every
   width, not just the one (~1280px) where the original fluid clamp()
   happened to coincide with the unadjusted child margin. (The last
   paragraph's own trailing margin-bottom is zeroed separately, inline on
   that specific element — see index.html — so it doesn't add on top of
   this.) */
.hero-wrapper {
  padding-bottom: 6rem;
}

@media screen and (max-width: 991px) {
  .hero-wrapper {
    padding-bottom: 2rem;
  }
}

/* Desktop-only bump to 6rem (user request) — top gap needs the matching
   change on .hero__note-wrapper's own margin-top (the original
   base value, 4rem, untouched until now), scoped to min-width:992px
   specifically so tablet (480-991px, which inherits this same base value
   unless overridden — its own effective gap is handled separately via
   .hero-wrapper's ≤991px padding-bottom above) and mobile (≤479px, which
   already has its own authored 3rem override) aren't affected by a
   desktop-only change. */
@media screen and (min-width: 992px) {
  .hero__note-wrapper {
    margin-top: 6rem;
  }
}

@media screen and (max-width: 479px) {
  .hero-wrapper {
    padding-bottom: 3rem;
  }
}

/* Same fix as above, missing piece: matching .hero-wrapper's own
   padding-bottom to the top gap wasn't the whole story — the *next*
   section, .services-section ("Послуги"), has its own separate
   margin-top:4rem (64px, flat at every breakpoint, confirmed no
   ≤991/≤767 variant exists for it) stacking on top of that padding, with
   nothing equivalent added to the top side. That's the extra 64px that
   was still making the bottom read as bigger even after the previous
   fix. .services-section is used exactly once, only on this page (confirmed
   via grep), so zeroing its margin-top entirely and letting
   .hero-wrapper's own (already-correct, per-breakpoint) padding-bottom
   be the sole source of bottom spacing is safe and exact — no separate
   breakpoint handling needed here since the 4rem being removed was flat
   across all of them to begin with. */
.services-section {
  margin-top: 0;
}

/* Same fix, one more layer down: even with .services-section's own margin-top
   zeroed above, 480-991px width still showed an extra, unaccounted
   32px gap that didn't correspond to any margin/padding on .services-section
   or .services__background (the gray "Послуги" band) themselves —
   both genuinely compute to 0 there. Root cause: two levels deeper,
   .services-container.layout-container (centers this section's actual content) has
   its own margin-top:2rem at exactly this breakpoint range only (margin-
   top is 0/auto at every other breakpoint, which is why 1280px and
   390px both tested clean and only the 480-991px range didn't). Neither
   .services-section nor .services__background has any padding-top or
   border-top to contain that margin, so it collapses all the way out
   past both of their own top edges — invisible extra whitespace that
   doesn't belong to anything one would think to check first.
   display:flow-root on the gray band establishes a new block formatting
   context, which contains the collapse completely (the 2rem becomes
   real internal spacing between the band's own top edge and its
   content, where it visually belongs) without adding any extra height
   of its own — cleaner than a padding-top hack for this exact problem. */
.services__background {
  display: flow-root;
}

/* Full-page audit (user request, then narrowed): the gap between each of
   the five main content sections on index.html — .projects-section ("Проекти"),
   .gallery-section ("Галерея"), .the-height-400vh-section ("Партнери"),
   .why-us-section ("Чому ми?"), .process-size ("Як це працює?"), in that DOM
   order, with .zakluk as an unlabeled connector div directly between the
   last two — should read as 6rem, matching what §28 established for the
   hero-text block. Deliberately scoped to *only* these five sections'
   own boundaries, not anything before .projects-section or after .process-size,
   and not anything inside any of them — an earlier pass here also
   touched .services-section/.scroll-wrapper (the "Послуги" section, before
   "Проекти") on the assumption that "every section gap" meant the whole
   page; corrected after the user clarified the request was specifically
   about the five sections shown, nothing else. Every fix scoped to
   min-width:992px (desktop) only — mobile/tablet have their own,
   untouched responsive spacing, and a flat 6rem gap would look
   disproportionate on a narrow screen anyway, same reasoning as the
   hero-text block's own smaller values below 992px.
   .projects-section/.gallery-section/.process-size each just had a single plain
   margin-top (8rem, 8rem, 4rem respectively) with no desktop-tier
   override to conflict with; .zakluk had no rule at all at this
   breakpoint. .the-height-400vh-section (a scroll-pin container — fixed
   150vh height, position:relative, holds a position:sticky child) also
   just needed a plain margin-top added.

   The .the-height-400vh-section -> .why-us-section boundary needed a
   different approach: that container's *own* trailing margin-bottom is
   a separate, pre-existing negative value (-4rem) that does not behave
   like an ordinary collapsed margin against .why-us-section's margin-top —
   empirically (binary-searched live, since the numbers didn't match
   standard collapse math and weren't worth fully reverse-engineering):
   raising .why-us-section's margin-top from 0 up to 128px changes the
   rendered gap *not at all* (stays 64px flat), then from ~130px upward
   it responds 1:1. 160px (10rem) is the value that lands the actual
   gap at exactly 96px — not a "6rem+dead-zone" derivation, just the
   verified number. If this section's internal structure changes later
   and this stops producing 96px, re-verify empirically the same way
   rather than assuming the dead-zone size is still 128px. */
@media screen and (min-width: 992px) {
  .projects-section {
    margin-top: 6rem;
  }

  .gallery-section {
    margin-top: 6rem;
  }

  .zakluk {
    margin-top: 6rem;
  }

  .the-height-400vh-section {
    margin-top: 6rem;
  }

  .why-us-section {
    margin-top: 10rem;
  }
}

/* "Партнери" logo marquee (inside .the-height-400vh-section's sticky
   pin): the gap between the "Партнери" subtitle and the visible logo
   row, and the gap on the *other* side of the row before the sticky
   content ends, are two ends of one fixed budget — .the-overflow-
   hidden-mask has height:100% (auto-fills whatever's left of
   .the-sticky-div's 100vh after .heading-wrap.partner-head's own
   height), so mask_height - block_height (~492px at typical heights)
   has to be split between "before" and "after" the row somehow.

   Two earlier attempts pushed the row up toward the heading
   (align-items:flex-end + a large margin-bottom, first a flat px value
   — wrong, since the mask's leftover space scales with *viewport
   height* via the 100vh chain, not just width like everything else on
   this page — then a calc(100vh - 472px) formula that correctly held
   ~64px at every height tested). Both were reasonable answers to "logos
   closer to the heading" taken literally, but pushing nearly all the
   dead space to the *lower* side of the row had a side effect neither
   attempt accounted for: during a real (not JS-jumped) scroll past the
   point where .the-sticky-div releases, that large lower dead zone
   momentarily coexists on screen with .why-us-section ("Чому ми?") already
   scrolling into view, reproducible via genuine scroll input — looks
   exactly like a broken, oversized gap even though every static
   measurement of the settled layout was correct.

   Fixed properly this time by asking what "closer to the heading"
   should have meant in the first place: the row centered in its
   available band, not shoved against one edge. Switched
   .partners-marquee-track's align-items from flex-end to center —
   splits the ~492px budget evenly (~246px each side) instead of
   assigning nearly all of it to one side. Still needed one more piece:
   align-items centers an item's *margin box*, not its content box, and
   .a-block/.partner-logo-block's original margin-bottom:15% (the same rule
   from the very first version of this fix, ~270px in this layout) was
   still there, so centering the margin box just re-skewed the split
   almost exactly like the old flex-end result (measured 91px/381px,
   not the expected ~246px/246px). Zeroed margin-bottom alongside the
   align-items change — margin-left/margin-right (the horizontal
   spacing between logos in the marquee) are untouched. This also
   shrinks the lower dead zone from ~430-530px down to ~246px, which
   directly narrows the window where the release-transition overlap
   from above can happen. */
@media screen and (min-width: 992px) {
  .partners-marquee-track {
    align-items: center;
  }

  .a-block,
  .partner-logo-block {
    margin-bottom: 0;
  }
}

/* "Чому ми?" (.why-us-section) subtitle -> first stats card: user wanted
   ~4rem, was ~160px (10rem). .process-layout (the flex column holding
   the heading, an empty spacer div, and the stats cards) has
   row-gap:5rem applied uniformly between its 3 children. The middle
   child, .process-timeline-alt, is a 0-height positioning anchor for an
   absolutely-positioned decorative line (.process-timeline-alt-track) — it
   contributes no visible content, so the perceived gap is exactly
   2x the row-gap (160px), not something separately settable. Halving
   row-gap to 2rem lands the perceived total at exactly 64px (2x2rem)
   without touching .process-timeline-alt itself (which would risk breaking
   the decorative line's positioning anchor). .process-layout is used
   exactly once on this page. */
@media screen and (min-width: 992px) {
  .process-layout {
    row-gap: 2rem;
  }
}

/* Gap above "Партнери" itself, distinct from the gallery-section -> .the-
   height-400vh-section gap fixed earlier (§29): that external gap only
   exists *before* .the-sticky-div engages — once scrolled far enough
   that it pins to the viewport top, the space between the top of the
   screen and the "Партнери" heading is governed entirely by
   .heading-wrap.partner-head's own margin-top (8rem, unrelated to the
   external gap), which is what a screenshot taken once the pin has
   engaged actually shows. Reduced to 6rem to match the same convention
   used everywhere else on this page. .heading-wrap.partner-head is
   used exactly once on this page. */
@media screen and (min-width: 992px) {
  .heading-wrap.partner-head {
    margin-top: 6rem;
  }
}

/* "Zakluk" hero building photo (.cta-building-image.color): object-fit:cover (set
   elsewhere in this file) crops this image to fill .process-visual-wrap's box.
   Default object-position (50% 50%, centered) crops evenly off both
   sides — with this specific source photo, since the building's own
   content already runs close to both edges of the file, an even crop
   still left a thin sliver of empty margin before the building reached
   the container's right edge. Anchored object-position to the right
   instead, so cover crops only from the left (into the trees, which
   have more forgiving empty space around them) and the building's own
   right edge lines up exactly with the container's right edge, per
   request. .cta-building-image is used exactly once on this page. */
.cta-building-image.color {
  object-position: 100% 50%;
}

/* Sitewide (user request): plain white body areas felt flat, wanted a
   subtle texture that "adds volume" without being noticeable. The actual
   <body> tag carries its own `.page` class (not a bare `body` element),
   which already declares its own background-image (a translucent white
   wash over a repeating banner photo, original rule ~line 2434) — a class
   selector beats a plain `body` element selector on specificity, so a
   bare `body{background-image:...}` rule here is silently ignored no
   matter where it sits in source order. Re-declaring `.page` here (same
   specificity, later in source order -> wins) layers a fine straight-line
   grid ON TOP of the existing background instead of fighting it: two
   repeating-linear-gradients, one per axis, each a 1px near-invisible
   line every 80px, listed first (background layers paint first-on-top) so
   the grid sits above the original wash+ photo. Kept deliberately faint
   (~5% black) and generously spaced (80px, not a dense graph-paper grid)
   so it reads as quiet structure/depth at a glance rather than a visible
   pattern — matches the architectural/ blueprint language already
   established for the preloader (HANDOFF.md §18) without competing with
   it. Background-position/-repeat/-size restate the original two layers'
   values unchanged (position/repeat/ size are independent properties from
   background-image and don't cascade together — without restating them,
   CSS would cycle the original 2-value lists across the new 4 layers,
   silently forcing the image layer's repeat-x/contain onto one of the new
   grid layers). */
.page {
  background-image:
    repeating-linear-gradient(0deg, #0000 0px, #0000 79px, #0000000d 80px),
    repeating-linear-gradient(90deg, #0000 0px, #0000 79px, #0000000d 80px),
    linear-gradient(#fffc, #fffc),
    url("../images/chatgpt-image-22-yyul-2026-h-16-16-07.webp");
  background-position: 0 0, 0 0, 0 0, 50% 0;
  background-repeat: repeat, repeat, repeat, repeat-x;
  background-size: auto, auto, auto, contain;
}

/* Follow-up (user request): "make the body background on every page" —
   turns out the rule above ONLY ever reached index.html. Checked all 31
   pages' actual <body class="..."> and found SEVEN different values, not
   one shared "body" class as assumed: `body` (index.html only), `body-2`
   (about.html), `five-hotel`, `fomich-town`, `hotels-change` (25 pages —
   the large majority), `le-meandre`, and contact.html's <body> has no
   class at all. None of those six other class names have any rule of
   their own anywhere in this stylesheet (confirmed by grep — zero hits
   each, except .page-about's own unrelated margin/mobile-blur rules,
   neither of which touches background-image) — so nothing there competes
   for specificity the way index.html's own `.page` rule does.
   Deliberately a SEPARATE rule from `.page` above, not a broadened
   selector on it (e.g. `body, .page {...}`) — that rule's declaration
   also carries index.html's own pre-existing wash+banner-photo background
   (kept from before this texture existed), which is specific to that
   page's own page-background setting and was never meant to spread to the
   other 30 pages. This rule repeats ONLY the two grid gradients, via a
   plain `body` element selector, which reaches every page's real <body>
   tag regardless of its class. On index.html both this rule and `.page`
   above match; `.page` wins on specificity so index.html's own combined
   result (grid+wash+photo) is unchanged. On every other page, this is the
   only rule targeting background-image on body, so they get the grid
   texture alone — which is exactly the original intent, since none of
   them had a wash/photo to begin with. Also adding background-color:#fff
   here, found while chasing a follow-up bug report ("контакти" page
   rendered solid black once .contact below stopped covering the whole
   page). Root cause is a second, later `body{}` rule (~line 2390) that
   sets `background-color: var(--f5f5f7)` — and `--f5f5f7` is declared
   with no value at all (`--f5f5f7: ;`, see the §40 HANDOFF note on the
   same variable elsewhere), so that var() is invalid and the property
   falls back to its initial value, transparent — NOT the pale gray the
   variable name implies. That rule beats line 222's `body{background-
   color:#fff}` on source order (same specificity, comes later), so body's
   real background-color has apparently been transparent on every page all
   along, sitewide, independent of anything this session touched — just
   never visible before because page content or a wrapper like .contact
   happened to be opaque everywhere real users would look. This
   environment's browser also happens to render `prefers-color-
   scheme:dark`, which is what turned "transparent" into visible black
   instead of a real browser's usual white canvas. Fixing here (same
   selector as the texture rule above, later than the buggy line-2390 one)
   rather than editing the original rule in place. */
body {
  background-color: #fff;
  background-image:
    repeating-linear-gradient(0deg, #0000 0px, #0000 79px, #0000000d 80px),
    repeating-linear-gradient(90deg, #0000 0px, #0000 79px, #0000000d 80px);
}

/* Follow-up (user request): the "Чому ми?" section and the partner-logos
   marquee both sat on their own opaque white background-color, fully
   hiding the .page texture above instead of letting it show through like
   the rest of the page. Three selectors were the actual opaque layers
   (checked live via getComputedStyle, not guessed from source — the
   original .the-height-400vh-section/.the-sticky-div/.the-overflow-
   hidden-mask and .a-block/.partner-logo-block were already transparent,
   so left alone): .why-us-section is process-section's own wrapper ("Чому
   ми?"); the marquee needed two separate layers cleared (.the-
   width-400vh-scrollable-div, the horizontally-scrolling strip itself,
   and .partners-marquee-track, each row's own content wrapper). One
   unconditional rule here beats every viewport, including the original's
   own narrower breakpoint variants of the same selectors, since media-
   scoped rules get no cascade priority over an unconditional one at equal
   specificity — only source order decides, and this file loads last. */
.why-us-section,
.the-width-400vh-scrollable-div,
.partners-marquee-track {
  background-color: transparent;
}

/* Mobile top-hero photo, full history (index.html, "фото поганої якості
   на моб версії" → then "розмір фото має бути на весь розмір телефона").
   Three rounds: 1) Shrunk the photo's box (via its real selector,
   `.parallax.hero-photo.hero__photo--full` — all three classes together, not any one
   alone, which is what the original actually used everywhere for this
   element; a bare `.hero__photo--full` silently loses to that 0-3-0 combo, caught by
   re-measuring after the first attempt did nothing) to cut down a ~2.4x
   object-fit:cover upscale of the loaded 800x359 srcset candidate. Fixed
   the blur, but shrank the hero well below "full phone screen" —
   explicitly rejected, would've looked like "half photo half white". 2)
   Kept the box small but fixed a knock-on bug that surfaced along the way
   (button pushed out from over the photo, viewport-height- fragile svh
   sizing) — not relevant anymore now that round 3 reverts the box size,
   but see HANDOFF.md §45 if this specific failure shape (svh box vs.
   Fixed-height overlay content) comes up elsewhere. 3) Real fix: the
   actual problem was never the box being tall, it was that a 359px-tall
   SOURCE image was being asked to cover a much taller box — any photo
   this size will look soft stretched that far, regardless of box height.
   Traced to the <img>'s own `sizes` attribute (`sizes="(max-width:1873px)
   100vw, 1873px"`, in index.html itself, not this stylesheet) — declares
   width-only need, so the browser's srcset picked a small candidate sized
   for the narrow mobile viewport WIDTH, with no way to know object-
   fit:cover would need much more resolution than that once cropped into a
   tall box. Changed `sizes` to a flat `"1873px"` — always loads the full
   source (1873x840, ~190KB) regardless of viewport, which is what
   actually fixes sharpness. With that in place, the box can go back to
   full height without the blur returning: covering even a ~900px-tall box
   from an 840px-tall source is close to 1:1, no meaningful upscale.
   Height itself changed again one round later — see the next comment
   block below, this paragraph only concerns the sizes-attribute/sharpness
   fix, which is still current. */

/* Mobile top-hero text treatment (index.html, user request: try white
   text, arrange it more clearly centered). Scoped to <=479px only —
   desktop wasn't reported as an issue and already reads fine as dark
   text on the lighter part of the photo.
   White needs a legibility aid here: this text sits over the photo's
   lighter sky-gradient area (not the darker building portion further
   down), so plain white-on-light would be low-contrast. Used a soft
   dark text-shadow rather than a background scrim behind the text —
   keeps the photo itself untouched, reads as a natural hero-text
   treatment rather than adding a new visual layer.
   .hero-copy-column's width was computed at 337.5px out of a 375px
   viewport (99% of the space after this element's own margins) —
   already text-align:center underneath, but with so little slack on
   either side, centered and left-aligned were nearly indistinguishable.
   Capping max-width + auto margins gives the centering real, visible
   breathing room instead of just being technically true. */
@media screen and (max-width: 479px) {
  .hero-copy-column {
    max-width: 300px;
    margin-left: auto;
    margin-right: auto;
  }
}

/* The white-text + dark-scrim treatment that briefly lived here (a
   follow-up to the paragraph above, once text-shadow alone stopped being
   enough) was fully reverted at explicit user request — back to the
   original dark heading/button color at every width, and the
   `hero-photo-scrim` div removed from index.html along with it (a scrim
   whose only purpose was propping up white text is just a pointless
   darkening of the photo once the text goes back to dark). Height stays
   `47rem` below — that part was about the photo's own display size, not
   the text-color question, and wasn't part of what got reverted. Full
   history of the white-text attempt, if it's ever wanted again, is in
   HANDOFF.md rather than kept live here. */
@media screen and (max-width: 991px) {
  .parallax.hero-photo.hero__photo--full {
    height: 47rem;
  }
}

/* .navbar is fixed at a constant 65px on every breakpoint (checked via
   getComputedStyle at 375/840/1440px) and sits on top of the page, so
   jumping to any same-page anchor (e.g. #career) lands with that much of
   the target's top hidden behind it. scroll-padding-top reserves the gap
   sitewide, for this link and any future anchor link alike. */
html {
  scroll-padding-top: 65px;
}

/* The original rule (line ~10408) puts backdrop-filter:blur(2px) on
   .page-about (about.html only) at >=1440px. Backdrop-filter creates a
   new containing block for any fixed-position descendant, same as
   transform does — so at that width the whole page's .navbar stopped
   being fixed to the viewport and scrolled away with the body instead,
   which is why it read as "missing" while scrolling on a wide screen. The
   blur itself is a barely-visible 2px effect on the body background;
   dropping it is the fix, not a visible loss. */
@media screen and (min-width: 1440px) {
  .page-about {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* "Партнери" logo strip (index.html) is a scroll-pin: vertical scroll
   through #sec01 (.the-height-400vh-section) drives a transform:translateX
   on .the-width-400vh-scrollable-div while .the-sticky-div holds it pinned
   to the viewport — see js/partners-rail.js, which owns both that movement
   and these buttons. .the-sticky-div
   itself (not the mask) is the buttons' parent, so they sit beside the
   logo row instead of on top of it: .the-overflow-hidden-mask is
   narrowed below (992px+) to less than the sticky div's own width,
   which — because .the-sticky-div is display:flex; align-items:center —
   centers the now-narrower mask automatically and opens up genuine empty
   side margins within the same 1404px max-width box for the buttons to
   sit in, at any desktop width, not just very wide ones.
   Vertical centering can't be a flat top:50% here, since .the-sticky-div
   also contains the "Партнери" heading above the row — centering against
   its full height would land the buttons too high (this was tried once
   already, see the removed comment this replaced). js/partners-rail.js
   sets --partner-btn-top from the mask's actual measured
   position instead, so it stays correct regardless of viewport height or
   how tall the heading happens to wrap to.
   Desktop only (>=992px, this page's own breakpoint for "desktop"
   elsewhere): below that, scrolling the page *is* the interaction
   (a vertical swipe through the pinned section already drives the same
   horizontal transform), so a button would be redundant there.
   No button chrome (circle/background/border/shadow) by request — just
   the bare arrow, sized up and given more room to breathe instead. The
   reserved side margin grew to match (132px -> 224px total) so the
   bigger, further-out icon still clears the (now-wider) mask edge by a
   real gap, not just a few px. */
@media screen and (min-width: 992px) {
  .the-overflow-hidden-mask {
    width: calc(100% - 224px);
  }
}

.partner-scroll-btn {
  position: absolute;
  top: var(--partner-btn-top, 50%);
  z-index: 250;
  transform: translateY(-50%);
  color: #000000a6;
  background: none;
  border: none;
  padding: 8px;
  justify-content: center;
  align-items: center;
  display: flex;
  cursor: pointer;
  transition: transform 0.2s, color 0.2s;
}

.partner-scroll-btn:hover {
  color: #000;
  transform: translateY(-50%) scale(1.12);
}

.partner-scroll-prev {
  left: 28px;
}

.partner-scroll-next {
  right: 28px;
}

@media screen and (max-width: 991px) {
  .partner-scroll-btn {
    display: none;
  }
}

/* index.html, 320px (iPhone SE) only: the "Одна команда..." paragraph
   (.hero__note.hero__note--compact.hero__note--third, text starting "Найдорожчі
   помилки...") read visibly heavier/bigger than the reference paragraph
   in "Послуги" (.service-card-text-alt) right below it on the same page —
   not actually a font-weight difference (both compute to 400), it's
   font-size: 18px here (from .hero__note--compact.hero__note--third's own
   font-size:var(--about-125), i.e. 1.125rem) against the reference's
   16px, with a proportionally looser line-height masking some of the
   size gap at normal reading distance but not in a close-up screenshot.
   Matched to the reference's 16px, scoped to this one breakpoint only
   per explicit request — not touched at 480px+, where this wasn't
   reported as an issue. */
@media screen and (max-width: 479px) {
  .hero__note.hero__note--compact.hero__note--third {
    font-size: 16px;
  }
}

/* index.html: the "Одна команда..." strong text (.hero__note-strong) read
   too heavy next to "Послуги" (.section-title.services-heading-size) — real,
   measurable difference: .hero__note-strong is font-weight:700 (a plain
   <strong>'s browser-default bold, never overridden by either of its
   two font-size rules above) against "Послуги"'s 600, which stays 600
   at every one of its own breakpoints (checked — none of its media-
   query overrides touch font-weight, only font-size/position/margin).
   First matched to 600 scoped to <=479px only, per how the issue was
   first reported; a follow-up asked for it "всюди" (everywhere) — moved
   out of that media query to apply unconditionally, since "Послуги"
   itself is never anything but 600 regardless of width. */
.hero__note-strong {
  font-weight: 600;
}

/* index.html, 320px only, same element again: lines read as stuck
   together ("наліплено одне на одне"). Real cause: .hero__note-strong sets no
   line-height of its own, so it inherits its parent .hero__note.size-
   aboutustext-copy-copy's line-height — which resolves to a fixed
   19.2px, sized for *the paragraph's own* 16px font-size (140% of 16px
   rounds close to that once this breakpoint's own 1rem font-size
   override applies to the parent). A computed px line-height doesn't
   rescale for a child with a bigger font-size when inherited, so
   .hero__note-strong's actual 32px text was rendering inside a 19.2px line
   box — less than the font-size itself, guaranteed to look like
   overlapping lines. Given its own explicit, unitless line-height
   instead of a px value specifically so it scales off *its own*
   font-size rather than being vulnerable to the same inherited-px trap
   again if the font-size here ever changes.
   1.25 (40px) was the first value tried and read as too loose once seen
   live ("занадто сильно") — brought down to 1.1 (35.2px), still enough
   room that ascenders/descenders on adjacent lines don't touch. */
@media screen and (max-width: 479px) {
  .hero__note-strong {
    line-height: 1.1;
  }
}

/* index.html, 320px only: manual break, moved once already (originally
   right after "Жодного" — moved to right after "Один проект." instead,
   per a follow-up request, so the two sentences "Одна команда. Один
   проект." share the first line and "Жодного «це не наш розділ»" is
   the whole second line, rather than splitting mid-phrase). A plain <br>
   can't be conditional by itself, so it's tagged with a class and
   suppressed above this breakpoint instead — at 480px+ this line wraps
   differently anyway (and at 32px, its own unmodified size there, a
   break forced at either position would leave the far side of the line
   nearly empty). */
@media screen and (min-width: 480px) {
  .mobile-line-break {
    display: none;
  }
}

/* This 20px <=479px override existed only to make "Одна команда. Один
   проект." and "Жодного «це не наш розділ»" each fit their own line
   inside a 300px-wide container without wrapping mid-phrase (see the
   git history for the original measurement). Once "Жодного «це не наш
   розділ»" was removed from the heading entirely, the constraint this
   size was solving for no longer exists — a follow-up request to size
   the remaining text back up "proportionally to the site's fonts"
   confirmed live that its own unconditional 32px (pd-07, the same value
   already used at every other breakpoint) fits this container cleanly:
   getClientRects() on the text showed it wrapping naturally at the
   sentence boundary — "Одна команда." / "Один проект." — with no manual
   <br> needed at all. Removed this override rather than picking a new
   fixed value, so the heading is genuinely one size everywhere again. */

/* index.html, 320px only: "Ми рахуємо навантаження. Ви рахуєте
   прибуток." (.hero__note.hero__note--compact.hero__note--third, the second of
   two sentences sharing that class — the first, "Найдорожчі помилки...",
   is untouched here) went through an earlier fix (shrunk to 12px so it
   fit on one line — see HANDOFF.md if that specific measurement work is
   ever relevant again) before the user decided, after seeing it live,
   that the sentence just doesn't belong at this width at all ("замалий
   екран") — replaced with display:none rather than layered on top of
   the size fix, since a hidden element's font-size is moot.
   That attribute selector alone had the same specificity as a
   single class (0,0,1,0) — loses outright to the element's own 3-class
   .hero__note.hero__note--compact.hero__note--third combo (0,0,3,0) regardless
   of source order, so it's combined with that same combo here (0,0,4,0)
   rather than relied on by itself. */
@media screen and (max-width: 479px) {
  /* Ховаємо третій абзац під заголовком на вузьких екранах. Раніше цей
     селектор чіплявся за атрибут інтеракцій, якого вже немає.
     Тепер добираємось позицією в обгортці: вона стабільна й не залежить
     від чужої системи. */
  .hero__note-wrapper .hero__note.hero__note--compact.hero__note--third:last-of-type {
    display: none;
  }
}

/* The scroll-rotated crane section (index.html, between hero and
   "Послуги") was removed entirely at explicit request — full detail on
   what it was is in HANDOFF.md §59-60 if it's ever wanted back, not kept
   live here. */

/* index.html, 320px only: the "Партнери" logo row read as sitting too
   close to "Чому ми?" below it — same root cause as the desktop version
   of this exact gap (HANDOFF.md §29), just never carried over to this
   breakpoint: .the-height-400vh-section (the whole "Партнери" scroll-pin
   wrapper) has an unconditional margin-bottom:-4rem, and at this width
   .why-us-section's own original margin-top (6rem, line ~14303) wasn't
   enough to counteract it and still leave real clearance before "Чому
   ми?"'s own text (which sits well inside its box, not flush with the
   box's own top edge). Unlike the desktop case, this margin pair *does*
   respond 1:1 to changes here (checked live: +104px on margin-top moved
   the rendered gap by +104px, no dead zone) — so this was tuned by
   watching the actual screenshot rather than fighting non-standard
   collapse math a second time. 160px (10rem) is the value that reads as
   evenly balanced against the ~96px gap already above the logo row
   (between the "Партнери" subtitle and the row itself), not a formula. */
@media screen and (max-width: 479px) {
  .why-us-section {
    margin-top: 160px;
  }
}

/* index.html, 320px only: "Зв'язатись" (.text-medium-alt.link, the
   underlined link right before the zakluk/hero-photo section) rendered
   at a flat 32px on every screen size — its own font-size is
   clamp(2rem, 2vw, 1.4rem), where the min (2rem) is already bigger than
   the max (1.4rem). A clamp with min > max always resolves to the min,
   per spec, so the 2vw preferred value and 1.4rem max never actually do
   anything here — this was effectively a fixed 32px the whole time, not
   a real responsive clamp, at every width including this one. Confirmed
   this exact 2-class combo is used exactly once on this page. Sized
   down to 20px, checked live against the surrounding stat-card text
   before picking that number. */
@media screen and (max-width: 479px) {
  .text-medium-alt.link {
    font-size: 20px;
  }
}

/* Footer, 320px only: the gap above "Ми в соцмережах" (and above
   "Контакти" too — same grid, same gap) made the footer read as too long.
   .footer-links-wrapper.tab-footer (the real combo used for this grid —
   the bare .footer-links-wrapper's own 767px-and-below override to 1.6rem
   never actually applies, it loses on specificity to this 2-class combo's
   unconditional 8rem, same shape of trap as elsewhere in this file) has
   grid-row-gap:8rem (128px) with no breakpoint override anywhere that
   touches row-gap specifically — the 479px block for this combo only ever
   touched text-align/width. Footer markup is identical on every page
   (shared component), so this fixes the same visual gap everywhere the
   footer appears at this width, not just index.html — confirmed live that
   the grid is a single column at this breakpoint, so it's only row-gap
   that's visually doing anything here. */
@media screen and (max-width: 479px) {
  .footer-links-wrapper.tab-footer {
    grid-row-gap: 48px;
  }
}

/* about.html, 320px only: .about-hero__image ("Чому обирають нас?" hero photo) is
   a wide office-interior shot (1280x721 source) with object-fit:cover
   inside a narrow, tall mobile box — at this width the rendered box is
   only ~347px wide against an effective scaled image width over 1400px,
   so the default centered object-position:50% 50% only ever shows a
   ~24%-wide sliver of the photo's horizontal center (the walkway/table),
   missing the actual desks with people at them, which sit in the left
   and right thirds of the source image (confirmed by opening the full
   source directly). Shifted to object-position:20% 50% to bring the
   left-side workstations into that crop instead — checked both
   directions live (80% for the right-side desks looked comparably good)
   before picking this one; either was a reasonable answer, this wasn't
   the only "correct" choice. .about-hero__image confirmed used only on this
   page. */
@media screen and (max-width: 479px) {
  .about-hero__image {
    object-position: 20% 50%;
  }
}

/* about.html, 320px only: "Долучитись до команди" (.about-form__submit, the
   career-form submit button) was clipping — measured live (canvas text
   metrics against the button's own real padding) at the original 16px
   the label needs ~222px but the button's available inner width is only
   190px at this breakpoint (button is width:100%, padding already
   reduced to 15px/side by the existing ≤479px rule, but font-size was
   never touched there). 13px was the first size with real margin below
   that 190px ceiling (14px still didn't fit; 13.5px barely did).
   .about-form__submit confirmed used only on this page. */
@media screen and (max-width: 479px) {
  .about-form__submit {
    font-size: 13px;
  }
}

/* Every project/object page (26 pages sharing .page-hero__inner, the hero
   wrapper), 320px only: .page-hero__title is position:absolute with
   inset:0% 0% auto (left:0, right:0 from the base rule) — zero
   horizontal padding, so long single-word titles overflow both edges
   with no way to wrap around them. Confirmed live: "ВАРШАВСЬКА" (the
   widest single word across every page's heading, checked via canvas
   measureText against all of them, "CONFERENCE" from grand-spa-resort-
   conference.html a close second) renders ~338px wide at the existing
   3rem, wider than the 320px viewport itself, so padding alone can't
   fix it — the word has no break opportunity to wrap on. Scoped to
   .page-hero__inner (not the bare .page-hero__title) so about.html's
   "ЧОМУ ОБИРАЮТЬ НАС?", which reuses the same class in a different
   section (.about-hero__inner), is untouched, since that wasn't reported.
   left/right:10px gives the requested edge clearance; font-size dropped
   to 2.5rem so "ВАРШАВСЬКА" (~282px measured at this size) still clears
   the resulting 300px available width with margin to spare. */
@media screen and (max-width: 479px) {
  .page-hero__inner .page-hero__title {
    left: 10px;
    right: 10px;
    font-size: 2.5rem;
  }
}

/* index.html only, 320-479px: the old navbar script baked a current-page
   class onto the navbar brand link on whichever exported page IS its own
   link target — since the brand always links to index.html, only
   index.html's own file has class="brand w-nav-brand w--current"
   (confirmed via grep: 0 other pages). .brand.is-current has its own rule
   at this breakpoint (width:auto; padding-left:0), different from plain
   .brand's (width:90px, padding-left:10px inherited from the base .w-nav-
   brand framework class) — letting the child .logo <img> (unconditionally
   width:90px in its own rule at every breakpoint) render at its full 90px
   instead of being boxed into an 80px content area, ~12.5% bigger in both
   dimensions (measured live: 90x45.66 vs 80x40.59 everywhere else). This
   is original, untouched behaviour, not introduced by any fix this
   session — it just never showed up in this session's own testing because
   every test navigation used a ?nocache= query param, which made the old
   "is this link the current page" check stop matching and silently drop w
   --current, masking the real behavior every time until a genuinely clean
   load (no query string) exposed it. Restoring plain .brand's own
   width/padding-left values on the .is-current variant, scoped to this
   breakpoint only, makes the homepage's logo match every other page's
   (confirmed via live inline-style test before writing this rule, then
   re-verified against the real served page afterward: 152.5/80/40.59,
   pixel-identical to a non-current page). .brand.is-current has its own
   separate rules at other breakpoints too (base, >=992px) — deliberately
   left untouched here, since only this breakpoint was reported. */
@media screen and (max-width: 479px) {
  .brand.is-current {
    width: 90px;
    padding-left: 10px;
  }
}

/* index.html "Послуги" cards, landscape phones: .card has an
   unconditional width:22.5rem (base) or width:300px (<=991px) — always a
   fixed px value, never proportional, same "one hardcoded preview width"
   shape documented repeatedly elsewhere in this file (see §17 in
   HANDOFF.md). .services-cards-desktop does get flex-flow:wrap at
   <=991px, so the fixed 300px card CAN wrap instead of overflowing — but
   2x300px+16px gap (616px) is wider than the actual available content
   width at a short landscape phone (measured 603px at 667px viewport), so
   it wraps to a single column, leaving ~45% of the row empty next to
   every card and roughly doubling the section's scroll length for no
   reason (confirmed live: 4 cards stacked, identical x, ~336px apart
   vertically). Scoped to (max-width:991px) and (orientation:landscape)
   specifically — NOT a plain max-width range — because landscape phones
   and portrait tablets overlap heavily in raw width (a landscape phone
   can be 900px+ wide) but never overlap in orientation (a portrait device
   is never orientation:landscape by definition), so this reaches exactly
   "phone turned sideways" widths without touching the already-working
   portrait tablet layout that shares the same <=991px width range. Real
   device landscape widths run roughly 568px (iPhone SE) to 932px (iPhone
   14 Pro Max) — checked both ends live before shipping this, not just the
   one width the report screenshot happened to show: at 568px (narrowest)
   each card is ~244px wide and its content still doesn't overflow the
   existing fixed 320px card height (confirmed via scrollHeight <=
   clientHeight on all 4 cards' real copy, worst case ~72px of margin to
   spare); at 932px (widest) cards are ~426px, still a reasonable, non-
   stretched card width. Calc(50% - 8px) (half the container minus half
   the 1rem/16px gap) was used instead of a second fixed px guess
   specifically so it scales correctly across that whole width range
   instead of only being right at one specific device's width. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .services-cards-desktop {
    flex-flow: wrap;
  }

  .card {
    width: calc(50% - 8px);
  }
}

/* "Партнери" logo row, landscape phones: user reported "half the logos
   not visible" — real clipping, not an exaggeration. Root cause has two
   independent parts, both only matter when the viewport is SHORT (this
   whole component is a chain of vh-based sizing — see the extensive
   history above this comment — so it was only ever tuned against tall
   portrait viewports and normal desktop heights, never a short landscape
   phone):

   1. .heading-wrap.partner-head's margin-top is 8rem (128px) at this
      width (neither the <=479px nor >=992px override reaches 480-991px
      landscape) — on a ~320-430px-tall viewport that alone eats a
      third-plus of the whole .the-sticky-div (100vh). .the-overflow-
      hidden-mask's own height is genuinely "100% of whatever's left"
      (confirmed live, matches the existing comment above about this
      same mechanic), so a huge heading margin-top directly starves the
      mask down to less than the logo's own height.

   2. Even with more room, .a-block/.partner-logo-block's margin-bottom:15%
      (measured live: 112.5px-137.5px across the widths checked) still
      breaks it — percentage margins resolve against the CONTAINING
      BLOCK'S WIDTH per spec, not height, so this stayed large regardless
      of how little vertical room there was. .partners-marquee-track
      is align-items:center (confirmed live at this width, not just the
      >=992px case already documented above), which centers the item's
      full MARGIN BOX — with margin-bottom this much bigger than the
      margin-box's own content, centering pulls the actual visible logo
      upward, out through the TOP of the mask's box, where the mask's
      own overflow:hidden (that's its whole purpose) clips it. This is
      the exact same shape of bug the existing >=992px-only rule below
      already fixes on desktop (align-items:center + margin-bottom:0) —
      just never extended to this width/orientation combination.

   Fixed by applying that same, already-proven combination here: zero
   .a-block/.partner-logo-block's margin-bottom (align-items:center itself
   doesn't need restating, already active at this width), and cut
   .heading-wrap.partner-head's margin-top down from 128px to 12px —
   picked by testing the actual narrowest real landscape phone (568x320,
   iPhone SE) live, not guessed: 32px left only ~7.6px of clearance
   between the logo and the mask's own edge at that height, uncomfortably
   tight for a value that has to hold across real font-rendering
   variance; 12px gives ~27.6px of real breathing room there instead,
   confirmed still comfortable at 932x430 (iPhone 14 Pro Max landscape,
   the widest real case) too, where slack is ~137px. Screenshot-verified
   at both ends: heading/subtitle sit with a normal, uncramped gap before
   the logo row, every visible logo in the strip renders complete with no
   top or bottom clipping — this component's own horizontal marquee
   strip still shows a partial next-logo poking in at the mask's right
   edge, same as every other breakpoint; that's normal marquee behavior,
   not part of this bug. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .heading-wrap.partner-head {
    margin-top: 12px;
  }

  .a-block,
  .partner-logo-block {
    margin-bottom: 0;
  }
}

/* Gallery (.gallery-section), landscape phones: user reported big gaps
   between photos and asked specifically for 2-per-row with 10px side
   margins — a concrete direction, not just "fix the spacing." Root cause:
   at <=767px, the original rule abandons the grid entirely (display:flex;
   flex-flow:column) and stacks all 9 photos in one column with grid-row-
   gap:4rem (64px, still applies as a flex row-gap alias) between each — a
   deliberate, reasonable choice for narrow PORTRAIT phones, just never
   intended for a wide-but-short landscape one, same "tuned for portrait
   only" shape as the two fixes above. The <=991px rule directly above it
   in the cascade (2 columns, .75fr 1fr) never gets a chance to apply at
   landscape-phone widths, since <=767px is more specific and wins first.
   Rebuilding this as a clean, uniform 2-column grid (not reusing the
   <=991px rule's own 2-column attempt, since .75fr 1fr is an uneven
   masonry-style split, not the plain "two even photos per row" that was
   asked for) needed one non-obvious step: the base rule's `grid-template`
   shorthand bakes in `grid-template-areas` (an 11-row x 7-column ASCII
   layout, one named area per photo, each photo's own grid-column/-row
   individually spans anywhere from 1 to 7 of those columns) — setting
   `grid-template-columns` alone, live-tested first, was silently ignored
   and still computed 3 uneven columns, because an active grid-template-
   areas keeps asserting its own column count independent of a longhand
   grid-template-columns override sitting next to it. `grid-template:
   none` clears the areas (and rows) in one shot, and only after that does
   a plain `grid-template-columns: 1fr 1fr` behave as expected. Each
   photo's own individual grid-column/grid-row span (inherited from the
   base rule, e.g. `span 4 / span 4`) also needed resetting to `span 1 /
   span 1` via `.gallery-grid > a` (the same safe, already-used-elsewhere
   child selector as the existing <=479px Local Addition above) — left as-
   is, several would still try to span more columns than now exist and
   distort the row count. 10px side margins: the grid's own parent
   (.about__container, confirmed via grep used exactly once on this page)
   has 32px of padding on each side that's shared with this section's own
   heading above the grid — rather than shrinking that container's padding
   (would also pull the heading in, not asked for), pulled just the grid
   22px past it on each side (margin-left/-right: -22px), landing the
   grid's own edge at exactly 10px from the viewport regardless of screen
   width. Inter-photo gap left at 8px (this rule's own pre-existing grid-
   column-gap/grid-row-gap value from the base rule, not changed) — the
   user's "10px" was about the outer edges specifically ("від боків"), not
   the gap between photos. Follow-up round found this rule was silently
   NOT working, on a fresh re-check with a working screenshot: `display`
   computed to `flex` (the first pass never actually wrote `display:grid`
   into this rule despite testing it inline before shipping — a real gap
   between what was tested and what was written, caught on re-
   verification, not assumed fixed) and even after adding that, the
   container still auto-generated 9 implicit columns (one per photo,
   single row) instead of the intended 2. Two more real, independent bugs,
   found by testing each property change live one at a time rather than
   guessing: (1) `.gallery-grid > a`'s `grid-column`/`grid-row` reset was
   losing to the per-photo placement, which turned out to be set via
   per-card `#<id> { grid-area: span N / span N / ... }` selectors
   (confirmed via grep — a real rule at the <=479px block, and an
   unconditional/base one for every photo) — an ID selector's specificity
   always beats a class selector regardless of media-query nesting or
   source order, so this needed `!important` to actually win, not a class-
   selector rewrite (there's no selector shape short of matching each ID
   individually that beats an ID on specificity alone). (2) Even with
   items correctly forced to `span 1/span 1`, the grid still rendered 9
   columns — `grid-auto-flow` computed to `column` (inherited from the
   <=767px rule, line ~13121, never reset by this rule) combined with no
   explicit row count meant every item needed its own new implicit column
   to be placed at all. Added an explicit `grid-auto-flow: row` reset.
   Same round, user also asked for the photos themselves to be square —
   added `aspect-ratio: 1` on `.gallery-grid > a` (also needs `!important`
   for the same ID-selector-specificity reason as the grid-column/-row
   reset above), which now correctly derives the square's height from
   whatever the column's own resolved width is at any real landscape
   width, rather than hardcoding one number that would only be right at
   one screen size. Verified live end to end after all of the above:
   exactly 2 explicit columns of 319.5px each at 667px width, every
   photo's own rect 319.5x319.5 (true square, not just visually close),
   grid still spans edge-to-edge at 10px from the viewport as before. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .gallery-showcase-grid {
    display: grid;
    grid-template: none;
    grid-template-columns: 1fr 1fr;
    grid-auto-flow: row;
    row-gap: 8px !important;
    margin-left: -22px;
    margin-right: -22px;
  }

  .gallery-showcase-grid > a {
    grid-column: span 1 / span 1 !important;
    grid-row: span 1 / span 1 !important;
    aspect-ratio: 1 !important;
  }

  .gallery-showcase-grid > a:last-child {
    grid-column: span 2 / span 2 !important;
    aspect-ratio: auto !important;
    height: 320px !important;
  }
}

/* "Проекти" category list (.projects-section), landscape phones: the last row
   ("Торгово-розважальні центри") was entirely invisible — user said "a
   line disappeared," and that's literally accurate, not an exaggeration.
   Confirmed via document.elementFromPoint at the text's own live
   coordinates: it hit BODY, not the title div — genuinely clipped, not
   a color/opacity/animation problem (checked and ruled out both: the
   GSAP scroll-reveal on this row fires and settles at opacity:1 well
   before the row scrolls into view, same as the other three rows).

   Each `.project-item` row (.project-content inside it) is sized
   `height:8vh` — on a normal-height phone or desktop this is generous
   (8vh is 70-90px+ on most real viewports), but on a short landscape
   phone (320-430px tall) it shrinks to just 26-34px. Every row's actual
   title text renders measurably below its own 8vh row box regardless of
   viewport (confirmed identical on all 4 rows, so this vertical offset
   itself isn't the landscape-specific bug — tried and ruled out both
   `.project-content`'s line-height:74px and `.project-item`'s
   padding-top/-bottom:2.4rem directly as the cause, live, before finding
   the real one) — normally harmless, since each row's overflow just
   bleeds into the next row's own space, which is empty there anyway.
   The outer list wrapper (`.projects-section .container`, confirmed via grep
   to structurally differ from the footer's own identically-classed
   `.container` only by being scoped under `.projects-section` here) has
   `overflow:hidden` with `height:auto` at this width — auto-sizing
   itself to the rows' own LAYOUT height (8vh x 4), which doesn't account
   for that per-row visual overflow. Every row but the last can overflow
   into next-row space and stay inside that auto height; the *last* row
   has no next row to bleed into, so on a short viewport its overflow is
   the first (and only) one to actually exceed the container's own
   bottom edge and get clipped.

   Fixed with the direct, minimal counter to the actual failure mode —
   `overflow: visible` on `.projects-section .container` only, not a deeper fix
   for why titles render below their own row box in the first place
   (real, but harmless everywhere except this one edge case, and not
   worth risking a wider change to something already working correctly
   for 3 of 4 rows and at every non-landscape breakpoint). Scoped to
   `.projects-section .container` rather than the bare `.container` class,
   since that exact class combo is also reused by this page's footer
   (confirmed via grep, 2 hits total on index.html) — this reaches only
   the Проекти list, not the footer. Verified via elementFromPoint at
   both 667x375 and 932x430 (the same width range checked for the other
   two fixes above): hit-tests the title div correctly post-fix at both,
   and a full screenshot at 667px confirms all 4 rows now render with
   identical divider-line styling and no visible gap or shift where the
   previously-clipped row appears. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .projects-section .container {
    overflow: visible;
  }
}

/* "Проекти" list, follow-up to the fix above: once the last row stopped
   being clipped, the user immediately flagged the divider lines
   themselves as wrong ("не рівно", lines above the text instead of
   below it) — a real, separate bug this same overflow discovery
   exposed rather than fixed. Root cause: `.project-item`'s own
   `border-bottom` sits at ITS OWN box edge (`height:8vh`), but — same
   mechanic established while diagnosing the clipped row above — every
   row's actual text renders about 26px below that box's own bottom
   edge, identically on all 4 rows. A border tied to the box therefore
   lands 26px *before* that same row's own text ends, which reads as
   "the line for row N sits above row N's text" — technically it's
   between row N's box and row N's overflowing text, but visually it's
   indistinguishable from "the previous row's line, sitting oddly high
   above the next line of text." Fixed by giving `.project-item`
   `padding-bottom:26px` (measured live to close the gap exactly:
   confirmed the box's own bottom edge now lands at the *same* pixel
   as its own title's bottom on all 4 rows, 0px gap, not just closer) —
   the border now renders exactly where a divider between two rows
   visually belongs, right after each row's own text. Not re-scoped
   from the fix above since it's the same root mechanic on the same
   element family; kept as a separate rule for a clean diff/revert unit
   if only one of the two ever needs undoing. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .project-item {
    padding-bottom: 26px;
  }
}

/* "Послуги" cards, landscape phones (follow-up to the 2-column fix
   above): user pointed specifically at "Оптимізація проектів" — its
   text wasn't laid out like the other cards. First hypothesis was
   wrong and worth recording so it isn't retried: this card's paragraph
   had three plain, unconditional `<br>` tags (index.html only, no
   `.mobile-line-break` class, unlike every other manual break added
   this session) — looked like the obvious cause, but adding the class
   (still kept, a harmless independent improvement — the text now
   wraps on natural word boundaries, "на будівництво." no longer
   orphaned on its own short line) barely changed the paragraph's
   rendered height, since this copy is simply longer than "Генеральне
   проектування"'s and needs about the same number of wrapped lines
   with or without the manual breaks.

   Real cause: measured all 4 cards' own paragraph heights live —
   3 of 4 (sharing class `.service-card-text`) already land at an identical
   157px; only "Генеральне проектування" (the one card using the
   distinct `.service-card-text-alt` class, shorter copy) comes in at
   134px, 23px short, which is exactly why its button alone sat higher
   than the rest.

   First fix attempt here was `.card{display:flex;flex-direction:
   column;justify-content:space-between}`, pinning every button to the
   card's own bottom edge — worked, but the user explicitly rejected
   the mechanism on review: buttons should stay exactly where they
   were (immediately below their own paragraph, original small gap),
   only the TEXT should be adjusted so the buttons end up level as a
   side effect. Replaced with the more targeted fix: `min-height:157px`
   on `.service-card-text-alt` alone (scoped further to `.services-cards-desktop
   .service-card-text-alt`, since that exact class also exists — unrelated,
   confirmed via grep — on a second, `display:none`-at-this-breakpoint
   duplicate block elsewhere on the page, same "-copy" duplicate-section
   pattern seen earlier with `.services-cards-mobile`). This reserves
   the extra 23px the short paragraph doesn't visually need without
   touching the button at all — verified live: both first-row buttons
   land at an identical top (1334px) with the paragraph-to-button gap
   itself unchanged from its original value, not stretched. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .services-cards-desktop .service-card-text-alt {
    min-height: 157px;
  }
}

/* Same "Послуги" card button-alignment bug, found again during a full
   iPad audit — this time in PORTRAIT at 810×1080 (iPad 9.7"), not
   landscape. Root cause is identical to the landscape fix just above
   (`.service-card-text-alt`, the "Генеральне проектування" card's shorter
   copy, renders shorter than its `.service-card-text` row-mates, pushing its
   button up out of line) — confirmed live: `.service-card-text-alt` measured
   134px vs `.service-card-text`'s 156.79px at this width, a 22.4px gap
   matching the exact button misalignment seen. The underlying cause
   (short vs. long copy in a 2-column grid) is driven by card WIDTH, not
   orientation, so it can occur in portrait too wherever the grid is still
   2 columns — confirmed this is genuinely portrait-range-specific and not
   already covered: at 375×812 (phone portrait) `.services-cards-desktop`
   itself is `display:none` in favor of the single-column `.cards-wrapper-
   copy-copy` duplicate (no row-mate exists to misalign with, so no bug
   possible there); at 768×1024 (already audited earlier this session,
   confirmed clean via screenshot) the narrower column width happens to
   wrap the short copy enough lines to naturally reach the same height —
   no gap, nothing to fix. 810px is 42px wider, just enough for the short
   copy to drop a wrapped line and fall short. Reused the exact same
   157px value as the landscape rule (not a coincidence — it's the same
   component, same target: match `.service-card-text`'s own real height,
   measured at 156.79px here, effectively identical to the landscape
   measurement). Kept as a separate rule (not merged into the landscape
   one by dropping its orientation qualifier) to match this file's own
   established convention for this exact pair of fixes — a clean
   diff/revert unit if only one orientation ever needs undoing. */
@media screen and (max-width: 991px) and (orientation: portrait) {
  .services-cards-desktop .service-card-text-alt {
    min-height: 157px;
  }
}

/* "Чому ми?" stats timeline (.process-item, the same component with an
   extensive history documented earlier in this file — dot alignment, line
   positioning, specificity traps): user wanted the description text
   closer to its own number/heading, and wrapped in 2 lines instead of 3,
   using "$22,4 млн+" as the example but confirmed this affects all 5 stat
   rows equally. Gap: `.process-item-desc`'s own `margin-top:10px` (base
   rule) was the full gap; cut to 4px. Line count took real digging —
   three independent things had to be fixed together, found by testing one
   change at a time rather than guessing all three up front: (1) At this
   width (<991px but >=768px is a separate, unrelated fix — see the
   768-991px block earlier in this file), `.process-item-desc` has a
   hardcoded `width:250px` from a `<=991px` Local Addition made in an
   earlier round (line ~11650) — too narrow for most of these 5
   descriptions to fit in 2 lines regardless of anything else. Tried
   switching `.process-item` to the existing 768-991px flex-based layout
   first (this project's own established "check if the original already
   solved this elsewhere first" pattern) — display mode changed correctly
   but the width problem persisted, because… (2) …`.process-item-
   content`'s parent uses `align-items:flex-start` (not the flex default
   of stretch), so children size to their own intrinsic width regardless
   of how much room the parent actually has — confirmed live that giving
   the row's flex children `flex-grow` moved pixels between the icon and
   text columns, but `.process-item-desc`'s own rendered width never
   budged, because the width was coming from (1) above the whole time,
   several DOM levels down, not from the row's own flex distribution at
   all. Fixed by setting `.process-item-desc`'s width directly, 320px —
   matches this element's own pre-existing base-rule `max-width:20rem`, so
   it's a ceiling that already existed in principle, just never reached at
   this width; confirmed via canvas text measurement across all 5 real
   description strings (not just the reported one) that 320px fits every
   one of them in exactly 2 lines. (3) One specific row ("Успішно
   реалізованих концепцій...") kept measuring as 3 lines at any width
   tried, including well past what the text should need — turned out to
   have the exact same unconditional `<br>` oversight already found and
   fixed once this session on the "Оптимізація проектів" card: a plain
   `<br>`, no `.mobile-line-break` class, forcing an early break
   regardless of container width. Fixed the same way. (The "3 lines"
   reading for this one row was itself briefly a false alarm even after
   the fix — a hidden `<br>` still splits its surrounding text into
   separate DOM text nodes, and `Range.getClientRects()` returns one rect
   per text node even when two adjacent rects are visually on the same
   line; had to cross-check by grouping rects by their own `top` value;
   still confirms out.) Verified with the grouped-by-top-position method
   above across all 5 real description strings, not just the one shown:
   every one confirmed at exactly 2 real visual lines. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .process-item-desc, .process-item-desc.process__desc--alt, .process-item-desc.process__desc--alt.process__desc--tablet {
    width: 320px;
    margin-top: 4px;
  }
}

/* "Зв'язатись" CTA link (.text-medium-alt.link — the same element
   with the broken clamp() landmine documented earlier in this file)
   directly followed by the "zakluk" hero photo section, with the photo
   sitting right under the link's own underline — measured live at 1px
   of actual gap. `.zakluk` gets `margin-top:6rem` at >=992px (a Local
   Addition further up this file) but nothing at all below that, and
   neither the link's own wrapper nor `.zakluk` itself has any other
   margin/padding contributing space at this width — confirmed both
   compute to 0px. Added a landscape-scoped `margin-top`, not reusing
   the desktop's 6rem (96px) — proportionate for a full-width desktop
   flow but excessive relative to the modest gaps used elsewhere this
   round on a short viewport; 32px verified live to produce a real,
   visible 33px separation between the link and the photo. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .zakluk {
    margin-top: 32px;
  }
}

/* Gallery follow-up (round 4 of this landscape series): user flagged two
   more real things once the 2-column square grid was actually working —
   large gaps between rows, and the odd 9th photo left alone and small.

   Row gap: `row-gap` computed to 64px, not the 8px this rule already
   uses for `column-gap` — the earlier fix never touched `row-gap`
   specifically, so the `<=767px` block's own `grid-row-gap:4rem` (still
   valid, since that's a real property this rule never overrode) kept
   winning. Set explicitly to 8px (0.5rem, matching the user's own
   number and this rule's existing column gap).

   Last photo: with 9 photos in 2 columns, the last one lands alone in
   its own row. `:last-child` spans both columns to fill that row
   instead of leaving it half-empty — but naively doing that also
   inherits this rule's `aspect-ratio:1 !important` from the shared
   `> a` selector, and a square that's 2 columns wide comes out ~647px
   tall, wildly taller than every other row (confirmed live before
   catching this). Overrode `aspect-ratio:auto` and a fixed
   `height:320px` on just the last photo specifically, matching every
   other row's own height — reads as a wide closing banner photo rather
   than an oversized outlier. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .gallery-showcase-grid {
    row-gap: 8px !important;
  }

  .gallery-showcase-grid > a:last-child {
    grid-column: span 2 / span 2 !important;
    aspect-ratio: auto !important;
    height: 320px !important;
  }
}

/* "Zakluk" hero building photo (.process-visual-wrap/.cta-building-image.color — the
   same element §33, way earlier in this file, tuned object-position
   for on desktop), landscape phones: user asked directly why the photo
   looked cropped when the real photo isn't. Real, measurable cause:
   `.process-visual-wrap` and `.cta-building-image.color` both get `height:70vh` at
   `<=991px` — only 262.5px on a 375px-tall landscape viewport, well
   short of the ~360px this specific box would need at its own 667px
   rendered width to show the 1536x828 source at native proportions.

   First attempt raised the box to a fixed 320px, cutting the crop from
   ~27% down to ~11% — a real improvement, shipped, but the very next
   round's screenshot showed the rooftop terrace (the most detail-dense
   part of this specific photo) still visibly cut at the top, and the
   user was explicit that they wanted the whole photo, not just less
   heavily cropped. A fixed height can only ever approximate the source's
   own proportions at exactly the one viewport width it was measured
   against — switched to `aspect-ratio: 1536 / 828` (the source's own
   real pixel dimensions) instead of any height value at all, which
   makes the box's height *exactly* match what `object-fit:cover` needs
   to render the full image with zero cropping, at any landscape width
   this rule covers, not just 667px. Confirmed live: at 667px wide the
   box computes to exactly 360px tall (667 * 828/1536 = 359.6, matching
   to the pixel) and a screenshot shows the entire building, roofline
   and all, with clean margin above and below — not just "less cropped."
   Both the wrapper and the image need the same override, same reason
   as before: `.cta-building-image.color` carries its own independent `height:70vh`
   at this breakpoint, not merely inheriting the wrapper's. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .process-visual-wrap,
  .cta-building-image.color {
    height: auto !important;
    aspect-ratio: 1536 / 828 !important;
  }
}

/* "Одна команда. Один проект." heading (.hero__note-strong), landscape
   phones: user wanted the line break back after "Один проект." — this
   exact `<br class="mobile-line-break">` already exists in the HTML
   (added earlier this session for the 320px-portrait pass), but
   `.mobile-line-break`'s own sitewide rule hides it at any width
   >=480px, which covers every real landscape phone width. Scoped to
   `.hero__note-strong .mobile-line-break` specifically (confirmed via grep
   that `.hero__note-strong` is used exactly once on this page) rather than
   touching the shared `.mobile-line-break` rule itself, which would
   have also re-shown every OTHER conditional break on the page added
   for the 320px pass (the Послуги card copy, the process-item-desc fix
   earlier this round) — those are correctly meant to stay hidden once
   there's enough width, which landscape phones have; this one heading
   is the only one where the user explicitly asked for the break back
   at this width. Verified live: 2 real visual lines ("Одна команда.
   Один проект." / "Жодного «це не наш розділ»"), confirmed via
   screenshot the break lands exactly where asked. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .hero__note-strong .mobile-line-break {
    display: inline !important;
  }
}

/* Same heading, immediate follow-up: restoring the break above made the
   line-overlap bug from earlier in this file (the <=479px "наліплено
   одне на одне" entry, same element) recur at this width too — the
   exact same root cause, just never triggered before because there was
   only ever one line to show here until the break above brought back a
   second one. `.hero__note-strong` sets no line-height of its own at this
   breakpoint, so it inherits a fixed, non-rescaling px value sized for
   a different, smaller font-size context — measured live: line-height
   computed to 24px against this element's own 32px font-size, guaranteed
   overlap. Reused the exact same fix already proven at <=479px:
   `line-height:1.1` (unitless, scales off its own font-size rather than
   inheriting a fixed px value) — 1.1 was the value that already survived
   a "занадто сильно"/too-loose rejection at 1.25 in that earlier round,
   so reused directly here rather than re-deriving it. Verified live:
   35px of real spacing between the two lines at this element's 32px
   landscape font-size, no overlap. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .hero__note-strong {
    line-height: 1.1;
  }
}

/* contact.html, landscape phones — first page in this landscape series
   that isn't index.html: the "У вас ідея. У нас — команда." heading sat
   partly under the fixed navbar. `.navbar` is fixed and 65px tall at
   every breakpoint (confirmed live, same value used sitewide — see
   §50's `scroll-padding-top:65px` fix for the same number used
   elsewhere); `.container.contact.layout-container`'s own `margin-top` is
   only 2rem (32px) at `<=991px`, so the heading's own top landed 33px
   *into* the navbar's own box, not just close to it. Raised to 80px —
   clears the navbar with a real ~15px of breathing room, not just
   flush against it. Scoped to this page's own `.container.contact`
   combo (not the bare `.container`, which is also the footer's class
   on this and every other page, confirmed earlier this series) so nothing
   outside this specific heading's own wrapper is touched. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .container.contact {
    margin-top: 80px;
  }
}

/* Same bug, found again during the iPad audit — this time in PORTRAIT
   (768×1024 and presumably the whole portrait tablet range), which the
   landscape-only rule just above never reached: the plain `<=991px` rule
   (no orientation) only gives `.container.contact` `margin-top:2rem`
   (32px), same insufficient value, same 65px fixed navbar, same
   overlap — confirmed live, heading's own rect landed at `top:32px`,
   squarely inside the navbar's box, reading as faint "ghosted" text in a
   screenshot (the navbar's own translucent background showing through
   over the heading, not an opacity bug on the heading itself — confirmed
   `opacity:1` via `getComputedStyle`). Reused the exact same `80px`
   value as the landscape fix (same component, same 65px navbar, same
   target clearance) rather than re-deriving it.

   Deliberately floored at `min-width:480px`, NOT the same bare `<=991px`
   as the landscape rule above — this Local Additions section is appended
   after the original `<=479px` block earlier in this file, so a bare
   `<=991px` rule here would beat that earlier, narrower, already-tested
   phone-portrait rule on source order alone and push phone portrait
   (confirmed live at 375×812, part of this same session's earlier
   320-479px audit) down by 80px it was never missing — caught via a
   regression check at 375px after shipping the first version of this
   rule (heading moved from a previously-fine position to `top:176`),
   not caught before shipping. Floor matches this file's existing
   `480-767px` tablet-gap convention (§17-shape) exactly. */
@media screen and (min-width: 480px) and (max-width: 991px) and (orientation: portrait) {
  .container.contact {
    margin-top: 80px;
  }
}

/* Same page, immediate follow-up: "У вас ідея. У нас — команда." was
   wrapping across 3 lines with awkward mid-phrase breaks. Cause: a
   `<=991px` Local Addition from an earlier round set `.font-size-
   medium{width:245px}` — a single-class rule, tuned for a narrower
   portrait context, that happens to also match this exact heading
   (full class list `font-size-medium margin-bottom-1rem contact-
   change-copy`). At this landscape width the heading's real available
   space is 603px (confirmed live: its own column is full-width, not
   narrowed by any sibling), so a 245px cap was forcing 3 lines out of
   text that fits on one at 472px measured width.

   `.font-size-medium` alone is reused twice more on this same page for
   unrelated text ("Контакти", "Довірте свою ідею тим, хто вміє її
   реалізувати.") — confirmed via grep neither was reported as broken,
   so widened only the full 3-class combo specifically rather than the
   bare single class, leaving those other two untouched. Verified live:
   full heading renders as one real visual line at 603px width. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .font-size-medium.margin-bottom-1rem.contact__intro {
    width: 100%;
  }
}

/* Same page, one more follow-up: the "Ім'я" field sat 83px below the
   subtitle text, well past a normal paragraph-to-form gap. Not a
   margin/padding issue — `.contact-form-wrapper` has a fixed
   `height:448px` (unrelated to this landscape series, an existing
   value from an earlier round) and `justify-content:space-between`,
   which spreads whatever leftover space exists between its two
   children (`.contact-form-column-left`, the heading+subtitle;
   `.contact-form-column-right`, the form) along the vertical axis.
   With both columns' own natural content well under 448px combined at
   this breakpoint's smaller heading/font sizes, `space-between`
   manufactured an 83px gap out of that leftover space rather than
   packing the two columns together. Overrode to `justify-content:
   flex-start`, which stacks both columns tight against each other —
   confirmed live: gap drops to 16px, a normal paragraph-to-field
   spacing, not the wrapper's own fixed height being fought over.
   `.contact-form-wrapper` confirmed via grep used exactly once on this
   page, so no risk of touching an unrelated layout by scoping to it
   directly rather than a more specific combinator. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .contact-form-wrapper {
    justify-content: flex-start;
  }
}

/* Every project/object page (the same 26-page `.page-hero__inner` hero
   already touched earlier in this landscape series for the heading
   overflow fix), landscape phones: user reported the hero photo "very
   big vertically, need smaller" — measured live before touching
   anything: `.page-hero`/`.page-hero__image` compute to `height:68rem`
   (1088px) at this width — the base/unconditional value, with no
   override between the base rule and the existing `<=479px` block
   (which itself only drops to `47rem`/752px, still taller than this
   entire landscape viewport on its own). On a 375px-tall viewport,
   1088px is roughly 3x the visible screen.

   First attempt used a fixed 320px, matching this round's height
   convention from the zakluk-photo/gallery fixes — shipped, but the
   very next report was the opposite complaint: "не перегібай" (don't
   overcorrect), the photo should be genuinely full-screen for this
   orientation, not just smaller. Fixed-320px was a step in the right
   direction but landed short of what was actually asked. Switched to
   `100svh` instead — a real full-screen hero that's always exactly the
   current viewport's height, at any real landscape height (tested
   320px, 375px, 430px), rather than one fixed number that happens to
   be close at only one of them. (`svh` over `vh` specifically to avoid
   mobile browser chrome resize jumps — same reasoning as this
   project's other viewport-height decisions.)

   Full-height also meant the heading's positioning couldn't stay a
   fixed px value either — `margin-top:120px` (right for a 320px
   section) would sit too high relative to a 430px one. Used
   `margin-top:37.5vh` instead (vh, not a %, deliberately — percentage
   margins on this project have repeatedly turned out to resolve
   against the containing block's WIDTH per spec, not height, so a %
   value here would not scale with the section's own height at all;
   `vh` is viewport-height-relative and unaffected by that). 37.5vh was
   picked by testing the original fixed-px pairing's own ratio
   (120px / 320px section = 0.375) and reapplying it as a viewport
   fraction, then verified live at all three tested heights that the
   heading stays comfortably inside the photo with real margin above
   and below at each: 320px section -> heading 120-293; 375px section
   -> heading 141-256; 430px section -> heading 161-276. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .page-hero, .page-hero__image {
    height: 100svh !important;
  }

  .page-hero__inner .page-hero__title {
    margin-top: 37.5vh;
  }
}

/* Same hero, immediate follow-up: user reported "gray smear" over the
   content section right below the hero photo — a real bug, not a
   description of the intended dark scrim. `.page-hero__scrim` (this
   specific page's own instance of a 5-way-shared selector, `.page-
   hero__scrim` through `.page-hero__scrim` — the editor's own auto-
   numbering for the same component reused across different page exports,
   confirmed via grep all five share one rule) is the hero's own darkening
   overlay (`background-image: linear-gradient(#0000004d, #0000004d)`, a
   flat 30% black tint over the photo for text legibility) — `position:
   absolute`, and its own `height:68rem` (1088px) was never touched by the
   two fixes above, which only resized the photo and repositioned the
   heading. With the photo now `100svh` (375-430px depending on device)
   but the overlay still 1088px tall, the overlay extended hundreds of
   pixels past the photo's own new bottom edge, tinting the real content
   section underneath — exactly the "gray smear on the content" report.
   Added all five shared class names (matching the original rule's own
   selector list, so this reaches whichever variant any given object page
   actually uses, not just this one page's `-41`) to the same `100svh`
   height as the photo itself. Verified live: overlay height now matches
   the photo's exactly (375px at this viewport), content section below
   renders at full brightness, no tint bleeding past the hero. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .page-hero__scrim {
    height: 100svh !important;
  }
}

/* Homepage top hero photo (.hero-image-wrapper.about-photo.hero__photo-box wrapping
   .parallax.hero-photo.hero__photo--full) — same root shape as the object-page hero
   above and the "zakluk" photo earlier in this file, a different
   component entirely (this is the very first section on index.html, full
   width, text overlaid on top rather than a two-column layout — confirmed
   live via getBoundingClientRect, wrapper and image both span the full
   736px width at this breakpoint, not a side-by-side split). The un-
   scoped `@media (max-width:991px)` rule further up this file (no
   orientation qualifier, tuned for narrow portrait phones) sets
   `.parallax.about- photo-copy.hero__photo--full{height:47rem}` (752px) — reasonable
   for a tall narrow portrait screen, but at a landscape phone width this
   same fixed 752px applies regardless of the actual (much shorter)
   viewport height: confirmed live at 736×414 (iPhone 8 Plus landscape)
   the section rendered at a full 752px against a 414px viewport, nearly
   double, forcing a big scroll past the fold before any other content
   became visible — exactly the "задовге по вертикалі" report. `.hero-
   image-wrapper.about-photo.hero__photo-box` itself has no explicit height at this
   breakpoint (`auto`, inherited from the base rule), so it simply follows
   the image's own height — no wrapper override needed, only the image.
   Fixed the same way as the object-page hero: `height:100svh`, so the
   section now matches the real viewport height at any landscape phone
   size instead of one fixed number tuned for a taller, narrower screen.
   The `.parallax` class's own scroll-linked transform (confirmed via the
   interaction data and the image's live rect being ~9.5% larger than its
   box with a matching negative offset — deliberate parallax headroom)
   reads this base height at runtime, so it isn't touched or broken by
   this change. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .parallax.hero-photo.hero__photo--full {
    height: 100svh;
  }
}

/* Same homepage hero, immediate follow-up: with the photo now a real
   100svh (previous fix), the heading/subtext/button block (.hero-copy-column,
   position:absolute, offset purely via margin-top since it has no
   top/left of its own) still used the un-scoped `@media (max-width:991px)`
   rule's `margin-top:12rem` (192px) — sized for a tall narrow portrait
   phone, where that much of the sky is available above the building. At
   736×414 landscape the building's own roofline sits much higher in the
   now-short frame; measured precisely by drawing the rendered photo to an
   off-screen canvas and scanning several columns for the sky-to-silhouette
   brightness drop (not eyeballed) — the tower's peak lands at
   `imgRect.top + 0.358 * imgRect.height` ≈ 144px into the section. At the
   inherited 192px margin, the heading's own box (57.6px tall) landed
   fully below that line, crossing through the building's upper floors —
   exactly the reported overlap. Tested candidate margin-top values live
   (83px landed the heading 3px above the roofline, too tight; 65px
   touched the navbar's own bottom edge with zero gap) before settling on
   `72px`: heading top sits 7px below the navbar, its bottom lands ~14px
   above the measured roofline, confirmed via a matching screenshot —
   "DAV CONSTRUCTIVE" reads cleanly against open sky, no longer crossing
   the building. Scoped to landscape only, same reasoning as the photo fix
   above: the un-scoped 991px rule still governs portrait phones, where
   12rem has never been reported as a problem. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  .hero-copy-column {
    margin-top: 72px;
  }
}

/* Same hero, smallest landscape phone tested this round: iPhone SE
   landscape (568×320). "DAV CONSTRUCTIVE" (.hero-title.hero-title-text) wraps
   to two lines here — not itself a bug in isolation (a separate, existing
   `@media (min-width:480px) and (max-width:767px)` rule, no orientation
   qualifier, deliberately narrows this heading to `calc(100vw - 48px)` and
   *wants* a two-line "DAV" / "CONSTRUCTIVE" wrap in that width range, to
   match the navbar's own two-line logo — see the comment just above that
   rule). The problem is specific to this width combined with a landscape
   phone's short height: two lines of a 64px heading (115px tall) plus the
   subtext and button left almost no room in a 320px-tall viewport — button
   bottom measured at 317px, 3px from the edge, with the subtext visually
   crowding the button above it. Confirmed via canvas `measureText` (same
   font/weight/letter-spacing as rendered) that "DAV CONSTRUCTIVE" needs
   ≈604px at the base 64px size but only 619px is available at 667px width
   (iPhone 8 landscape) — fits fine there, no wrap, so this is genuinely
   specific to narrower landscape phones, not the whole 480–767px landscape
   range. Reduced `.hero-title.hero-title-text` to `54px` (measured to fit
   "DAV CONSTRUCTIVE" in ≈510px against the 520px available at 568px width,
   confirmed live: single line, height drops from 115px to 48.6px) — kept
   `max-width:600px` as the cutoff specifically so this doesn't reach 667px
   (iPhone 8 landscape), which already renders correctly under the existing
   64px rule and wasn't reported as broken. Verified live: heading single
   line, button bottom now at 250.6px (69px of clear space below it, was
   3px), screenshot-confirmed no more overlap. */
@media screen and (max-width: 600px) and (orientation: landscape) {
  .hero-title.hero-title-text {
    font-size: 54px;
  }
}

/* New device bucket, first round: iPad mini landscape (1024×768). This
   width sits in a genuine gap between this file's other breakpoints —
   above the ≤991px rules (both the plain one and this session's own
   landscape-scoped ones) but below the existing `min-width:1280px` rule —
   so `.hero-copy-column` was still falling through to the bare, unscoped
   base rule (`margin-top:14rem`, 224px), tuned for desktop and never
   revisited for this tablet-landscape gap. User asked specifically to
   raise the heading/subtext/button block by 4rem here, and to touch only
   this resolution. Scoped to `min-width:992px and max-width:1279px and
   orientation:landscape` — deliberately a width band, not an exact
   1024px match, since standard iPad and iPad mini landscape share the
   identical 1024×768 CSS pixel size and are indistinguishable to a media
   query; the band's own edges (992px, 1280px) line up with this file's
   existing breakpoints on both sides, so it can't bleed into the ≤991px
   phone rules or the ≥1280px desktop rule. `224px - 4rem(64px) = 160px`.
   Verified live via a temporary inline override before committing
   (screenshot-confirmed the block now sits higher, comfortably clear of
   the photo's own upper third) — not yet re-checked at other widths
   inside this same band (e.g. iPad Pro's own landscape sizes are wider
   and fall outside 992–1279px, so shouldn't be reachable, but this is
   this session's first fix at this bucket and hasn't had a dedicated
   leak-check pass the way the phone breakpoints have). */
@media screen and (min-width: 992px) and (max-width: 1279px) and (orientation: landscape) {
  .hero-copy-column {
    margin-top: 160px;
  }
}

/* "Послуги" cards, same 992–1279px gap bucket, found during the same
   iPad audit: at 1080×810 one card ("Генеральне проектування") rendered
   at 172px wide with its paragraph wrapped to 336px tall, while the other
   three sat at a uniform 273px/157px — a ~200px button misalignment, far
   bigger than the paragraph-height-only bug fixed twice above.

   Root cause is a different mechanism entirely: `.services-cards-desktop` is
   `display:flex` with no `flex-wrap` in this width range (the only two
   rules that ever add `flex-wrap:wrap` are scoped to `<=991px`, one
   plain and one landscape-specific — both stop at 991px) and `.card`'s
   only width in this range is the base, unscoped `width:22.5rem` (360px).
   Four 360px cards plus 3×1rem gaps need 1488px; this bucket's own
   container measured only 1016px wide at 1080×810 — far short, forcing
   flex-shrink. Because flex-shrink's "resolve flexible lengths" algorithm
   freezes any item that hits its own min-content floor and dumps the
   *remaining* negative space onto whichever items can still shrink, and
   each card's min-content floor depends on its own copy's longest
   unbreakable run, the four (differently-worded) cards don't shrink
   equally — three froze at a shared min-content floor (273px) and the
   fourth absorbed all the leftover shrinkage alone, collapsing to 172px
   and wrapping its paragraph 2 extra lines. This is inherently
   copy-content-dependent and not a stable layout regardless of the exact
   numbers measured here.

   Fixed the same way already proven twice in this file for the exact
   same underlying shape (`<=991px` plain, and `<=991px` landscape,
   both above): give the wrapper `flex-wrap` and the cards an explicit,
   *equal*, container-relative width, which sidesteps the min-content
   freeze cascade entirely (two per row, each card's own share is fixed
   by the formula, never negotiated against its neighbors' copy).
   `calc(50% - 8px)` matches the landscape-phone rule's own approach
   (half the container minus half the 1rem/16px gap) rather than the
   `<=991px` plain rule's fixed `300px`, since this bucket spans a wide
   992–1279px range (unlike a single fixed px, this scales correctly at
   both ends). Not scoped to `orientation:landscape` — unlike the hero
   margin fix just above, this bug is pure flexbox-width math with no
   dependency on orientation, so both landscape and portrait devices
   landing in this width band need the same fix. Verified live at
   1080×810: all 4 cards equal width, buttons aligned; at 1280×900
   (just outside this bucket, on the existing `min-width:1280px` desktop
   side) cards were already equal at 304px each without this rule,
   confirming the fix is scoped to exactly the gap that needed it. */
@media screen and (min-width: 992px) and (max-width: 1279px) {
  .services-cards-desktop {
    flex-flow: wrap;
  }

  .card {
    width: calc(50% - 8px);
  }
}

/* Immediate follow-up to the fix just above: once the cards were forced
   to equal widths (2 per row) in this 992–1279px bucket, the *other*
   card-alignment bug (`.service-card-text-alt` rendering shorter than its
   `.service-card-text` row-mates — the same root cause fixed twice already,
   for `<=991px landscape` and `<=991px portrait`) surfaced here too, for
   the same reason it didn't matter before this rule existed: with cards
   now genuinely 2-per-row same-width, "Генеральне проектування"'s
   shorter copy is short enough to leave its button 45px above its row
   partner's. Measured live at 1080×810: `.service-card-text-alt` 112px vs.
   `.service-card-text` 157px — same 157px target as the other two instances
   of this fix, not a coincidence, same component. Not orientation-scoped,
   matching the flex-wrap fix just above it (both address the same
   992–1279px width bucket, and neither mechanism depends on aspect
   ratio). */
@media screen and (min-width: 992px) and (max-width: 1279px) {
  .services-cards-desktop .service-card-text-alt {
    min-height: 157px;
  }
}

/* User-directed fine-tuning, index.html hero, explicitly scoped to
   exactly 768×1024 (iPad mini / iPad 9.7" portrait, the two devices that
   share this CSS width) and nowhere else, per an explicit request not to
   touch any other resolution. `.hero-copy-column` (heading/subtext/button
   block) raised 2rem and nudged 1rem left from the plain `<=991px`
   values (`margin-top:12rem`→`10rem`, `margin-left:2rem`→`1rem`) — a
   pure positioning preference, not a bug fix. Used an exact
   `min-width:768px and max-width:768px` band rather than the broader
   `480–991px` portrait bucket used elsewhere in this file, since this is
   a specific aesthetic adjustment only verified to look right at this
   one width, not a general fix known to hold across the whole bucket. */
@media screen and (min-width: 768px) and (max-width: 768px) and (orientation: portrait) {
  .hero-copy-column {
    margin-top: 160px;
    margin-left: 16px;
  }
}

/* Same page, same exact-768×1024 scope: "Одна команда. Один проект.
   Жодного «це не наш розділ»" (.hero__note-strong) was wrapping mid-phrase
   ("Жодного «це не" / "наш розділ»") — user wants a clean break after
   "Один проект." instead. Same fix shape already proven twice in this
   file for this exact element (the `<=479px` original and the
   landscape-phone round just above): show the existing
   `<br class="mobile-line-break">` (hidden by the sitewide rule at
   `>=480px`) via `.hero__note-strong .mobile-line-break{display:inline}`,
   plus `line-height:1.1` on `.hero__note-strong` itself, since — confirmed
   live, identical to the landscape case — this element inherits a fixed
   24px line-height against its own 32px font-size at this breakpoint,
   which overlaps the two lines without the unitless override. */
@media screen and (min-width: 768px) and (max-width: 768px) and (orientation: portrait) {
  .hero__note-strong .mobile-line-break {
    display: inline !important;
  }

  .hero__note-strong {
    line-height: 1.1;
  }
}

/* "Партнери" -> "Чому ми?", exactly 768×1024: user wanted "Чому ми?"
   higher up / the gap after the visible logo row tighter. This section
   (.the-height-400vh-section) is a scroll-pin — .the-sticky-div stays
   fixed to the viewport while the user scrolls through this element's own
   height, and an interaction reads that scroll distance to drive
   `translateX` on the 7090px-wide logo track (.the-width-400vh-
   scrollable-div, confirmed via `scrollWidth`, same full 31-logo strip as
   the desktop version measured in HANDOFF §53). Two other approaches were
   tried first and reverted as unsafe: (1) shrinking `.the-overflow-
   hidden-mask`'s own height directly — confirmed live the logo's position
   inside the mask does not move in response (script-driven, independent
   of the mask's CSS height), so this only clipped the logo instead of
   recentering it; (2) an aggressive height cut to 120vh — confirmed live
   this measurably truncates how much of the logo strip passive scroll can
   ever reach before the pin releases. Important context before touching
   this again: even the ORIGINAL, unmodified 150vh only ever reaches about
   46% of the strip via passive scroll at this width (`translateX` maxed
   at -2933px of the -6322px needed for the last logo, measured live) —
   this is not a regression, it's how this component already behaves at
   this breakpoint. The prev/next buttons that let desktop users reach the
   rest of the strip (js/partners-rail.js, see HANDOFF §51-56) are
   explicitly `>=992px`-only, so below that width scroll has always been a
   partial, not complete, way to see this row — full traversal was never
   available here to begin with. Given that, a moderate height reduction
   (150vh -> 130vh, about 13%) was chosen as a real compromise, not a full
   fix: it measurably brings "Чому ми?" closer/higher as asked, while
   keeping the passive-scroll coverage close to its own already-partial
   baseline rather than cutting it sharply. Not a mask/logo re-centering
   fix (ruled out above as unsafe) — purely shortens the pin's own
   footprint. */
@media screen and (min-width: 768px) and (max-width: 768px) and (orientation: portrait) {
  .the-height-400vh-section {
    height: 130vh;
  }
}

/* Same "Партнери" pin, direct follow-up — this replaces an earlier
   attempt at this same request that turned out to be wrong. The first
   attempt shrank `.the-sticky-div` itself (`height:580px` +
   `overflow:hidden`) to crop the mask's empty lower portion from the
   outside; it looked correct in this environment's own testing tool
   (verified live via `elementFromPoint`, screenshots at two scroll
   depths) but the user's own real-device screenshot (Safari/iPadOS)
   afterward showed the identical, unfixed dead zone — so whatever this
   environment's tool was reporting as "fixed" did not reflect the real
   page. Reverted that rule entirely rather than dig further into why the
   two disagreed, since a completely different, already-*proven* fix for
   this exact bug turned up in this same file while investigating (see the
   `<=991px and orientation:landscape` rule below, `.a-block, .partner-
   logo-block{margin-bottom:0}`) — this is that exact bug, just at a
   width/orientation combination that rule never reached. Root cause,
   already fully diagnosed once for landscape (see that comment for the
   full derivation) and just as true in portrait, since nothing about it
   is orientation-specific: `.a-block`/`.partner-logo-block` carry
   `margin-bottom:15%` from the original export — percentage margins
   resolve against the containing block's WIDTH per spec, not height, so
   this stays large (~115px at 768px width) regardless of the mask's own
   height. `.partners-marquee-track` is `align-items: center` at this
   width (from the plain, original `<=991px` rule — confirmed active, no
   override needed for that part), which centers each logo's full MARGIN
   BOX. With margin-bottom this much bigger than the margin box's own
   visible content, centering pulls the actual logo upward inside the
   mask, leaving a large empty band below it before the mask's own bottom
   edge — exactly the dead zone in every screenshot this round, on both
   testing tools. Fixed by reusing the exact same, already-shipped
   combination as the landscape rule: zero `.a-block`/`.partner-logo-
   block`'s `margin-bottom`. Nothing else needed touching — `align-
   items:center` was already correct, `.the-overflow-hidden-mask`'s own
   height was never actually the problem in the first place. */
@media screen and (min-width: 768px) and (max-width: 768px) and (orientation: portrait) {
  .a-block,
  .partner-logo-block {
    margin-bottom: 0;
  }
}

/* contact.html, exactly 768×1024: user wants the heading/subtitle broken
   into 5 specific lines: "У вас ідея." / "У нас — команда." / "Залиште
   заявку." / "Зателефонуємо, розберемо задачу," / "запропонуємо
   рішення." Both paragraphs already carry exactly the right `<br>` tags
   in the HTML for this split (`.font-size-medium.margin-bottom-1rem
   .contact__intro`: one `.mobile-line-break` after "ідея.";
   `.contact__lead.contact-change`: one `.mobile-line-break` after "заявку.",
   plus one *unconditional* `<br>` — always visible at every width —
   after "задачу," which already produces that specific split's last
   break for free) — they just needed showing at this width, same
   sitewide `.mobile-line-break{display:none}` at `>=480px` pattern
   already worked around elsewhere in this file. Verified live via
   screenshot: exactly the 5 lines asked for, in order. */
@media screen and (min-width: 768px) and (max-width: 768px) and (orientation: portrait) {
  .contact__lead.contact-change .mobile-line-break, .font-size-medium.margin-bottom-1rem.contact__intro .mobile-line-break {
    display: inline !important;
  }
}

/* Object/project pages (.section__container — the shared text-content wrapper
   around "FOMICH GROUP" / "Проектувальник:" / "Сектор:" /
   "Характеристики:" / the body paragraphs, confirmed via grep used on
   27 pages, the whole object-page family), exactly 768×1024: text sat
   completely flush against both screen edges (0px), user wants a real
   10px side margin. `.section__container` and its whole ancestor chain up to
   the page's own outer container had zero padding/margin at this width
   (confirmed live, every level in the chain computed `left:0`) — added
   directly on `.section__container` itself, the innermost wrapper common to
   all of this content, rather than a page-specific element, so the fix
   reaches every object page through the shared class, not just this one
   page's own markup. */
@media screen and (min-width: 768px) and (max-width: 768px) and (orientation: portrait) {
  .section__container {
    padding-left: 10px;
    padding-right: 10px;
  }
}

/* about.html "чому обирають нас" 6-item grid (.choose-us-content, 3
   columns), exactly 768×1024: "Досвід масштабних проектів" was the only
   one of the 6 titles wrapping to 3 lines (measured height 81px vs 54px
   for the other 2-line titles), reading as uneven against its row-mates.
   Its own wrapper's side padding (`.about__intro .about__container` — scoped to
   `.about__intro` specifically, not the bare `.about__container`, since that
   exact class is also reused on index.html for an unrelated section,
   confirmed via grep — 32px/2rem) was the only lever available (the grid
   itself and its cells have no padding of their own). Reduced to 12px:
   confirmed live this is enough for the one 3-line title to drop to 2,
   matching its row-mates, without needing to touch font-size or the
   grid's own column count/gap. */
@media screen and (min-width: 768px) and (max-width: 768px) and (orientation: portrait) {
  .about__intro .about__container {
    padding-left: 12px;
    padding-right: 12px;
  }
}

/* New round, new exact-width target: iPad Pro 10.5" landscape (1112×834)
   specifically — user moved to this resolution and asked for 4 more
   user-directed adjustments, all explicitly "only at this resolution."
   Same convention as the 768×1024 round above: every rule here uses
   `min-width:1112px and max-width:1112px and orientation:landscape`, an
   exact-width band, not a general bucket. */

/* index.html "Послуги" cards: "Генеральне проектування"'s description
   (.service-card-text-alt, the short-copy card already touched by earlier
   rounds for a different reason) rendered in 4 lines at this width; user
   wants 7. Measured via `Range.getClientRects()` grouped by `top` (this
   file's own established way to count real visual lines, not just
   height/line-height division) across a range of candidate widths —
   260px lands exactly on 7 lines, stable across a 250-270px window
   (not a hair-trigger boundary value). */
@media screen and (min-width: 1112px) and (max-width: 1112px) and (orientation: landscape) {
  .services-cards-desktop .service-card-text-alt {
    width: 260px;
  }
}

/* contact.html heading: "У вас ідея. У нас — команда." was wrapping at
   whatever point the text naturally overflowed ("У вас ідея. У нас —" /
   "команда."), not the clean "У вас ідея." / "У нас — команда." split
   the existing `.mobile-line-break` in the HTML already provides —
   same fix shape as several rounds above, just a new width. */
@media screen and (min-width: 1112px) and (max-width: 1112px) and (orientation: landscape) {
  .font-size-medium.margin-bottom-1rem.contact__intro .mobile-line-break {
    display: inline !important;
  }
}

/* about.html "чому обирають нас" grid: at this wider width, the
   *opposite* problem from the 768px round — several titles now fit on
   one line instead of two, and user wants all 6 uniformly at 2 lines.
   Tested a single shared width first (temporarily, live) and confirmed
   no value works: "Увага до деталей" (short, 3 words) refuses to wrap to
   2 lines until the column is narrow enough that "Ефективні інженерні
   рішення" and "Досвід масштабних проектів" (longer titles) overflow to
   3 — there's no single width where all 6 land on exactly 2 at once, a
   genuine conflict from the varying copy lengths, not a value that was
   just hard to find. Only 2 of the 6 needed fixing to begin with
   ("Оптимізація бюджету", "Увага до деталей" — the other 4 were already
   2 lines at this width's natural column size), so fixed with a manual
   break in each (`<br class="ipadpro-break">` in about.html, matching
   this file's established `.mobile-line-break` pattern — hidden by
   default everywhere, shown only in this exact band), rather than
   fighting the whole grid's width for a fix only 2 cards actually need. */
.ipadpro-break {
  display: none;
}

@media screen and (min-width: 1112px) and (max-width: 1112px) and (orientation: landscape) {
  .ipadpro-break {
    display: inline;
  }
}

/* Object/project pages (.section__container, same shared 27-page class fixed
   for 768×1024 earlier): text sat flush against both edges again at
   this width (that earlier fix doesn't reach 1112px) — same 10px side
   margin, same reasoning, new exact-width band. */
@media screen and (min-width: 1112px) and (max-width: 1112px) and (orientation: landscape) {
  .section__container {
    padding-left: 10px;
    padding-right: 10px;
  }
}

/* index.html top hero, phone only (≤479px — the same breakpoint every
   existing custom fix for this exact block already uses, e.g. The .hero-
   copy-column re-centering above). Three user-directed changes: 1. Remove
   the "DAV CONSTRUCTIVE" heading entirely on phone. The original ≤479px
   rule already swaps .hero-title.hero-title-text out for .heading-26
   (display:block) as the phone-sized heading — this just turns that
   swapped-in heading off too, leaving only the paragraph and button.
   Desktop/tablet untouched: .hero-title.hero-title-text is what's shown
   everywhere above 479px, and this rule doesn't touch it. 2. Bold the
   paragraph ("Генпроектування повного циклу...") — was inheriting font-
   weight:400 from the base (desktop) rule at every breakpoint; only
   overriding it here keeps desktop/tablet at 400. 3. Button moved down,
   given a blurred/translucent background, and sized up slightly: -
   margin-top adds real space below the paragraph (beyond the paragraph's
   own existing 1rem margin-bottom). - .btn-primary-inner.btn-primary-inner--white already
   has a transparent background (#fff0) with just a border — adding
   backdrop-filter blurs whatever's behind it (hero gradient/photo) and a
   light white tint makes the blur visibly read as "glass", not just
   invisible. - height/padding/font-size bumped modestly (~15%) for "трохи
   збільши"; height must be overridden alongside padding since the base
   rule hardcodes height:50px (a fixed height, not min-height), so padding
   alone would just overflow/clip instead of growing the box. */
@media screen and (max-width: 479px) {

  .hero-subtitle.hero-subtitle-text {
    font-weight: 700;
  }

  /* §94's margin-top nudge wasn't enough — user wants the button pinned
     at the bottom of the screen, not just further from the paragraph.
     .hero-copy-column is position:absolute with no explicit height (it
     auto-shrinks to wrap its in-flow children), so there's nothing for
     a "bottom:0" child to anchor against. Giving it an explicit height
     equal to #about-top's own height at this breakpoint (47rem) minus
     its own top offset (margin-top:6rem) makes its box reach exactly
     to the hero section's bottom edge — anchored to the hero's own
     fixed-rem dimensions, not 100vh, so it stays correct regardless of
     viewport height (mobile browser chrome, taller/shorter phones)
     instead of drifting past the hero into the next section. The
     paragraph above stays position:static and renders exactly as
     before — height only extends the box's bottom, it doesn't move
     its top. */
  .hero-copy-column {
    height: calc(47rem - 6rem);
  }

  .btn-primary.white {
    position: absolute;
    left: 50%;
    bottom: 1rem;
    transform: translateX(-50%);
    margin-top: 0;
    width: 270px;
  }

  /* Widened further at user request, and this also fixes a real layout
     bug caught while doing it: the button's text ("Дивитися проекти")
     was wrapping to 2 lines, not because 34px padding was too tight,
     but because shrink-to-fit width for an absolutely-positioned box
     with left:50%/width:auto is computed from `left`, before the
     transform that re-centers it ever runs (transform is a paint-time
     effect, invisible to layout) — so the available width the browser
     wrapped text against was ~half of .hero-copy-column's own width
     (~150px), not the real ~300px box. An explicit width sidesteps
     shrink-to-fit entirely instead of fighting it with more padding. */
  .btn-primary-inner.btn-primary-inner--white {
    height: 58px;
    width: 100%;
    padding-left: 34px;
    padding-right: 34px;
    padding-top: 16px;
    padding-bottom: 16px;
    font-size: 17px;
    background-color: #ffffff26;
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
  }
}

/* index.html top hero, phone only (≤479px), third follow-up in this same
   round: remove all hero text, replace the pill button with a bare white
   down-chevron, and swap the background photo — all phone-only, per the
   same "нічого не змінювалось" requirement as every earlier round on this
   block. */
@media screen and (max-width: 479px) {
  /* 1. Remove the remaining hero text entirely — .heading-26 ("DAV
     CONSTRUCTIVE") was already hidden in the previous round; this hides
     the paragraph too, leaving just the photo and the arrow. */
  .hero-subtitle.hero-subtitle-text {
    display: none;
  }

  /* 2. Photo swap. The <img> (.parallax.hero-photo.hero__photo--full) is the SAME
     element used at every breakpoint via srcset — there's no separate
     mobile <picture>/<source>, so its `src` can't be swapped for phone
     only without touching desktop/tablet too. Instead: hide the shared
     <img> here and paint the new photo as a background-image on its
     wrapper (.hero-image-wrapper.about-photo.hero__photo-box) instead —
     desktop/tablet still render the original <img> untouched. The
     wrapper's own height is normally `auto`, derived from the now-hidden
     image (which was `height:47rem` at this breakpoint) — with the image
     gone that would collapse to 0, so the wrapper needs the same 47rem
     given back explicitly, matching what #about-top (.hero-section)
     itself is already fixed to at this breakpoint (original rule), so the
     section's overall height is unaffected either way. New photo
     optimized to WebP (2.28MB PNG → 225KB) via the same cwebp pipeline
     used for the rest of this project's images, at quality 82. */
  .parallax.hero-photo.hero__photo--full {
    display: none;
  }

  .hero-image-wrapper.about-photo.hero__photo-box {
    height: 47rem;
    background-image: url("../images/mobile-hero-laptop-concrete.webp");
    background-size: cover;
    background-position: center;
  }

  /* 3. Button → bare white arrow. Hides the pill/text/blur box from the
     previous round entirely (its sizing/padding rules above are now
     moot, left in place rather than deleted for the round-by-round
     history) and renders a single down-chevron instead, matching the
     shape the user referenced — inline SVG data-URI (no new asset file
     needed), same lucide-style stroke convention already used elsewhere
     on this site (e.g. the navbar phone icon: round caps/joins,
     stroke-based, no fill). .btn-primary.white keeps its existing
     bottom-pinned position from the previous round (position:absolute;
     left:50%; bottom:2.5rem; transform:translateX(-50%)) — only its size
     shrinks to fit the arrow instead of the old pill. */
  .btn-primary-inner.btn-primary-inner--white {
    display: none;
  }

  .btn-primary.white {
    width: 144px;
    height: 90px;
    opacity: 0.55;
  }

  .btn-primary.white::after {
    content: "";
    width: 100%;
    height: 100%;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    animation: phone-arrow-bounce 6s ease-in-out infinite;
  }
}

/* Shared by both phone hero arrows below (portrait just above, landscape
   further down): a PERIODIC double-bounce — two hops (a bigger one, then
   a smaller settling one), then a long pause before repeating — per
   "стрибки подвійні має робити, періодичність 6 секунд" (double jumps,
   6-second period). Amplitude (28px/14px) scaled up along with the 3x
   arrow size increase (144×90, was 48×30) so the bounce still reads as
   proportionate, not tiny relative to the new glyph. Animates the
   pseudo-element's own transform, not .btn-primary.white's (which
   already uses transform:translateX(-50%) for centering — piling a
   second, unrelated transform meaning onto the same property on the
   same element would require re-deriving the centering offset in every
   keyframe; animating the child instead sidesteps that entirely, since
   its centering comes from the parent's flexbox alignment, not its own
   transform). Defined once, unscoped, since a @keyframes block with no
   selector referencing it has no effect by itself — safe outside any
   media query. */
@keyframes phone-arrow-bounce {
  0%, 8%, 100% {
    transform: translateY(0);
  }
  4% {
    transform: translateY(28px);
  }
  12% {
    transform: translateY(14px);
  }
  16% {
    transform: translateY(0);
  }
}

/* Same three changes as the portrait round above (no text, photo swap,
   pill → bare white arrow), now extended to landscape phones at the
   user's explicit request ("розширення коли телефон повертають
   горизонтально, зроби так само"). Reuses `max-width:991px and
   orientation:landscape` — the established landscape-phone bucket this
   whole file already uses for every other §71-§90 landscape fix, not a
   new convention — rather than portrait's `max-width:479px`, since
   landscape phone widths (568-926px) all exceed 479px. */
@media screen and (max-width: 991px) and (orientation: landscape) {
  /* At this width .heading-26 is already `display:none` (its swap-in
     only ever triggers at <=479px, which no real landscape phone
     reaches) — .hero-title.hero-title-text is the heading actually showing
     here (confirmed live), so that's the one to hide. */
  .hero-title.hero-title-text {
    display: none;
  }

  .hero-subtitle.hero-subtitle-text {
    display: none;
  }

  /* Photo swap, same technique and same reasoning as the portrait block:
     hide the shared <img>, paint the new photo as this wrapper's own
     background instead. Height matches what the image was already
     rendering at here (100svh, from §86's landscape-hero-height fix) —
     without restating it explicitly the wrapper's auto height would
     collapse to 0 once its only child is hidden, same collapse risk as
     the portrait version. */
  .parallax.hero-photo.hero__photo--full {
    display: none;
  }

  .hero-image-wrapper.about-photo.hero__photo-box {
    height: 100svh;
    background-image: url("../images/mobile-hero-laptop-concrete.webp");
    background-size: cover;
    background-position: center;
  }

  /* .hero-copy-column's text-era layout used a plain margin-left offset
     (no horizontal centering), which was fine while it held visible
     text spanning almost the full width — but with all its children
     now hidden except a small arrow, centering it via left:50%/
     transform (same mechanism the portrait rule already uses) is needed
     so the arrow itself lands centered rather than off to one side.
     margin-left:0 avoids that inherited offset stacking on top of the
     transform-based centering. Height reaches from this breakpoint's
     own existing top offset (margin-top:72px, §87 — left unchanged, not
     restated) down to the bottom of the 100svh hero, giving the
     bottom-pinned arrow a real box to anchor against, same reasoning as
     the portrait rule's `calc(47rem - 6rem)`. Also needs an explicit
     `width` now, for a reason specific to this round: with every child
     hidden except the (now also position:absolute) button, .head-slog-
     div has no in-flow content left to shrink-wrap around, so `width:
     auto` shrink-to-fit resolves to a hard 0 regardless of available
     space — confirmed live (rect width:0, collapsing the button inside
     it to 0 too). `90%` matches the exact value the portrait rule
     already uses for the same "just needs to be nonzero" reason — the
     precise number doesn't matter for how the button lands, since both
     this box and the button inside it center via left:50%/transform
     independently; any nonzero width keeps that math working. */
  .hero-copy-column {
    left: 50%;
    margin-left: 0;
    transform: translateX(-50%);
    width: 90%;
    height: calc(100svh - 72px);
  }

  .btn-primary-inner.btn-primary-inner--white {
    display: none;
  }

  .btn-primary.white {
    position: absolute;
    left: 50%;
    bottom: 0.5rem;
    transform: translateX(-50%);
    width: 144px;
    height: 90px;
    margin-top: 0;
    opacity: 0.55;
  }

  .btn-primary.white::after {
    content: "";
    width: 100%;
    height: 100%;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    animation: phone-arrow-bounce 6s ease-in-out infinite;
  }
}

/* index.html "Послуги" service-card contact modals (all 4 — opt/gen/
   efect/zone, see HANDOFF.md §7 for how they're opened/matched),
   reported as "все не рівно" (everything is uneven) at phone width. Root
   cause: a stale leftover from an earlier session's fix, not a new bug.
   The original ≤479px rule sets `.service-modal-panel.back-blur. Model-
   size1` (the modal card) to `width:350px` and its child `.div- block-6`
   (wraps the modal's own heading/subtitle — confirmed via grep this class
   is used nowhere else on the page) to `width:310px` — 310 is exactly 350
   minus the card's 40px total horizontal padding, so at the time this
   first shipped, the heading and the card's content box lined up exactly.
   A prior session's fix (search this file for "close button being off-
   screen" if that comment ever gets pruned) widened the CARD itself to
   `width:100%` so its close button wouldn't go off-screen on very narrow
   phones, but never updated this one child to match — .form-wrap (the
   form below it) has no hardcoded width of its own, so it naturally
   stretches to 100% of the new, wider card, while .service-modal-row
   stayed frozen at the old 310px, leaving a ~40px gap on the right of the
   heading/subtitle only. Confirmed live (390px viewport): .form-wrap
   spans the full 350px content box (20px padding each side, symmetric),
   .service-modal-row spans only 310px (still 20px on the left, 60px on
   the right) — exactly the lopsided look in the report. `width:100%`
   brings it in line with its sibling, the same fix shape already used on
   the card itself. */
@media screen and (max-width: 479px) {
  .service-modal-row {
    width: 100%;
  }
}

/* Same modals, same round, exact same bug shape one level deeper: the
   single-line inputs (Ім'я → .home-form__name, Телефон → also .text-
   field-5-copy, Email → .home-form__phone) are hardcoded to width:310px
   in the original ≤479px rule, the same stale "sized for the old 350px
   card" leftover as .service-modal-row above — while .textarea
   (Повідомлення) and .submit-btn.btm4 (the submit button) have no
   hardcoded width in that same rule, so they already stretch to 100% of
   the current (wider) card and were the correct reference all along.
   Confirmed via grep both classes are used nowhere else on the site (only
   inside these 4 modals' forms), so fixing them broadly is safe.
   Width:100% brings the three narrow fields in line with the textarea and
   submit button below them, matching this file's now three-times-
   repeated fix for this exact shape of bug (card, heading, now inputs). */
@media screen and (max-width: 479px) {
  .home-form__phone, .home-form__name {
    width: 100%;
  }
}

/* projects.html: new category filter bar, left-aligned directly above the
   project grid (per request — "фільтр (зліва) одразу над фото першого
   об'єкту"). UI only for now, no actual filtering yet — the user will
   supply which project belongs to which category in a later round;
   js/project-filter.js just toggles which button looks active so it
   already behaves like a real filter control. Styled to match the
   site's existing pill-button language (.btn-primary-inner /
   .btn-primary-inner.btn-primary-inner--white on index.html's hero: rounded-full,
   Open Sans, #010101 as both the filled and the outlined-state color) —
   inactive buttons echo the outlined white variant, the active one
   echoes the plain filled variant, so switching state reads as the same
   kind of "selected vs. not" toggle already used elsewhere on the site. */
.project-filter-bar {
  flex-flow: row wrap;
  justify-content: center;
  align-items: center;
  gap: .75rem;
  width: 100%;
  display: flex;
}

.project-filter-btn {
  color: #010101;
  cursor: pointer;
  background-color: #fff0;
  border: 1px solid #00000033;
  border-radius: 99999px;
  padding: .75rem 1.5rem;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  font-weight: 500;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease;
}

.project-filter-btn:hover {
  border-color: #00000080;
}

.project-filter-btn.is-active {
  color: #fff;
  background-color: #010101;
  border-color: #010101;
}

/* Cards hidden by js/project-filter.js when their data-category doesn't
   match the selected filter. .project-card is a CSS Grid item
   (.projects-grid.is-all) — display:none removes it from the grid
   flow entirely so the remaining visible cards reflow to fill the gap,
   rather than leaving a blank slot. */
.property-hidden {
  display: none;
}

/* projects.html: user reported the sitewide body texture (grid pattern,
   see the `body{background-image:...}` rule and its own HANDOFF §39/§40
   history) wasn't visible anywhere on this page. Root cause: `.section-
   15.accent` — the section wrapping the entire filter+grid content,
   confirmed via a full-page opaque-background scan (every element over
   200×100px checked programmatically, not eyeballed) — carries its own
   solid `background-color:#fff` from the original export, sized to the
   full page height (measured live at 1920px tall, i.e. The whole visible
   page), painting over the texture completely. `.projects__accent`
   (without `.accent`) is reused elsewhere without this problem — only
   this modifier, and only on this page (confirmed via grep: `.section-
   15.accent` appears in no other page's HTML), so scoping the override to
   the compound selector can't affect anything else. Also clearing
   `.project-card`'s own `background-color:#fff` — each project
   thumbnail has `padding:1rem` around its image (not `object-fit:cover`
   filling the whole card), so that white was showing as a solid matte
   border around every photo instead of the texture. */
.projects__accent.accent, .project-card {
  background-color: transparent;
}

/* about.html "чому обирають нас" 6-card grid: reported as not vertically
   centered within its section (flanked by a photo above and another
   below). Measured live at 4 widths spanning every breakpoint bucket,
   not guessed: the gap BELOW the grid (to the next section's photo) is
   a rock-steady 96px at every width tested (375/700/1200/1600px) — that
   side has no responsive overrides anywhere in this component. The gap
   ABOVE varies by bucket because `.why-choose-us-wrapper`/`.choose-us-
   content`'s margin-top (they collapse together, larger one wins) AND,
   at 480–991px specifically, `.about__container`'s own separate margin-top
   (32px, easy to miss — confirmed via getComputedStyle on all three
   elements together, not assumed from the class names alone) all
   contribute: ≤479px measured 64px, 480–991px measured 112px, 992–
   1439px already measured exactly 96px (left untouched, nothing to
   fix), ≥1440px measured 128px. Overriding whichever of margin-top
   values is the larger one (the one actually winning the collapse) at
   each mismatched bucket brings the top gap to the same fixed 96px the
   bottom already has — a real 96/96 match confirmed live via
   getBoundingClientRect at all 4 widths after this change, not just
   computed style. */
@media screen and (max-width: 479px) {
  .why-choose-us-wrapper {
    margin-top: 6rem;
  }
}
@media screen and (min-width: 480px) and (max-width: 991px) {
  .choose-us-content {
    margin-top: 4rem;
  }
}
@media screen and (min-width: 1440px) {
  .choose-us-content {
    margin-top: 6rem;
  }
}

/* projects.html: shown by js/project-filter.js only when the selected
   category has zero tagged cards (currently "Торгово-розважальні
   центри"/"Комерційні приміщення" — no objects assigned to those
   categories yet, see HANDOFF.md §104-105). Deliberately keyed off
   "zero results", not the specific category names, so this message
   automatically stops appearing on its own once real objects get
   tagged into these categories later — no code change needed then.

   Rebuilt to a full spec (§116) replacing the original plain "Скоро
   тут з'являться проекти" placeholder — that copy was explicitly
   rejected ("не писати 'Скоро'/'Незабаром'") in favor of a real
   conversion path: eyebrow + headline + copy (category name filled in
   by JS) + two mandatory CTAs + a full-width dashed placeholder card
   that's itself a link, all sized to land at ~480-560px total height
   right under the filter row with no gap before the text. */
.project-empty-state {
  width: 100%;
  padding-top: 1rem;
  display: none;
  animation: project-empty-fade-in .6s ease-out both;
}

@media (prefers-reduced-motion: reduce) {
  .project-empty-state {
    animation: none;
  }
}

@keyframes project-empty-fade-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.project-empty-eyebrow {
  color: #a2a8ad;
  text-transform: uppercase;
  margin-bottom: .75rem;
  font-family: Open Sans, sans-serif;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .18em;
}

.project-empty-title {
  color: #010101;
  margin-bottom: 1rem;
  font-family: Open Sans, sans-serif;
  font-size: clamp(26px, 3.4vw, 40px);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -.025em;
}

.project-empty-desc {
  color: #6b7075;
  max-width: 62ch;
  margin-bottom: 2rem;
  font-family: Open Sans, sans-serif;
  font-size: clamp(15px, 1.6vw, 17px);
  line-height: 1.6;
}

.project-empty-actions {
  flex-flow: row wrap;
  align-items: center;
  gap: 1.5rem;
  margin-bottom: 3.5rem;
  display: flex;
}

.project-empty-btn-primary {
  color: #fff;
  background-color: #010101;
  border-radius: 999px;
  padding: 16px 32px;
  font-family: Open Sans, sans-serif;
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
  display: inline-block;
  transition: transform .2s ease, background-color .2s ease;
}

.project-empty-btn-primary:hover {
  transform: translateY(-2px);
}

.project-empty-btn-secondary {
  color: #010101;
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  font-family: Open Sans, sans-serif;
  font-size: 15px;
  font-weight: 600;
  text-decoration: underline;
}

.project-empty-card {
  text-align: center;
  border: 1.5px dashed #0a0a0a;
  border-radius: 14px;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 220px;
  text-decoration: none;
  display: flex;
  transition: background-color .25s ease, transform .25s ease;
}

.project-empty-card span {
  color: #0a0a0a;
  font-family: Open Sans, sans-serif;
  font-size: 21px;
  font-weight: 600;
  transition: color .25s ease;
}

.project-empty-card:hover {
  background-color: #010101;
  transform: translateY(-3px);
}

.project-empty-card:hover span {
  color: #fff;
}

.project-empty-btn-primary:focus-visible,
.project-empty-btn-secondary:focus-visible,
.project-empty-card:focus-visible,
.project-filter-btn:focus-visible {
  outline: 2px solid #010101;
  outline-offset: 4px;
}

/* Second row of the SAME single global filter as the category row above
   (see js/project-filter.js — one activeFilter value shared across both
   rows, not two AND-combined dimensions; a card can carry several
   work-type tags and surface under any one of them individually).
   Reuses .project-filter-bar's own flex/gap rule so both rows read as
   one cohesive filter UI — same gap value as the buttons within a row,
   so the row-to-row spacing doesn't stand out from the button-to-button
   spacing. */
.project-worktype-wrap {
  margin-top: calc(.75rem - 4rem);
  width: 100%;
}

.project-worktype-bar {
  flex-flow: row wrap;
  justify-content: center;
  align-items: center;
  gap: .75rem;
  width: 100%;
  display: flex;
}

/* Mobile-only (<900px) filter UI: the two wrapped button rows (still
   used at >=900px, unchanged) are replaced below 900px by a single
   toggle button — reads "Усі об'єкти" by default, or the active
   filter's own name once one is picked — that opens a bottom-sheet
   modal listing all 9 filter values (5 category + 4 work type) as one
   flat, single-select list. §137 tried keeping the two rows and just
   wrapping them so nothing needed a swipe — rejected once seen live,
   too much button clutter competing for space above the fold on an
   actual phone screen. Hidden entirely at >=900px via the media query
   below; the toggle/modal markup exists in the DOM at every width but
   only becomes visible under it. */
.project-mobile-filter-toggle {
  display: none;
  color: #010101;
  cursor: pointer;
  background-color: #fff0;
  border: 1px solid #00000033;
  border-radius: 99999px;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  width: 100%;
  padding: .875rem 1.5rem;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  font-weight: 500;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease;
}

.project-mobile-filter-toggle.is-filtered {
  color: #fff;
  background-color: #010101;
  border-color: #010101;
}

/* Підпис на кнопці — один рядок. Найдовша пара, яку він може показати,
   це «Громадські · Генпроектування»: коли обрано обидві осі, підписи
   беруться скороченими (SHORT_NAMES у project-filter.js), і вона влазить
   на 390px. Обрізання — страховка на випадок дуже вузького екрана або
   збільшеного системного шрифту, щоб підпис не розпирав кнопку і не
   зіштовхував шеврон за її межі. min-width: 0 обов'язковий: без нього
   flex-елемент не дає себе стиснути нижче ширини вмісту. */
.project-mobile-filter-toggle-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-mobile-filter-chevron {
  flex: none;
  transition: transform .2s ease;
}

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

.project-mobile-filter-backdrop {
  z-index: 5000;
  background-color: #00000066;
  opacity: 0;
  transition: opacity .25s ease;
  pointer-events: none;
  position: fixed;
  inset: 0;
}

.project-mobile-filter-backdrop.is-open {
  opacity: 1;
  pointer-events: auto;
}

.project-mobile-filter-modal {
  z-index: 5001;
  background-color: #fff;
  border-radius: 1.25rem 1.25rem 0 0;
  max-height: 80vh;
  padding: .5rem 1.25rem calc(1.5rem + env(safe-area-inset-bottom));
  box-shadow: 0 -16px 40px #00000024;
  transform: translateY(100%);
  transition: transform .3s ease;
  overflow-y: auto;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
}

.project-mobile-filter-modal.is-open {
  transform: translateY(0);
}

.project-mobile-filter-modal-header {
  justify-content: space-between;
  align-items: center;
  padding: 1rem 0 .5rem;
  display: flex;
}

.project-mobile-filter-modal-title {
  color: #010101;
  font-family: Open Sans, sans-serif;
  font-size: 1.0625rem;
  font-weight: 700;
}

.project-mobile-filter-close {
  color: #010101;
  cursor: pointer;
  background: none;
  border: none;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  display: flex;
}

.project-mobile-filter-option {
  color: #010101;
  cursor: pointer;
  text-align: left;
  background-color: #fff0;
  border: none;
  border-radius: .75rem;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: .875rem .75rem;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  font-weight: 500;
  display: flex;
  transition: background-color .15s ease;
}

.project-mobile-filter-option:active {
  background-color: #0000000d;
}

.project-mobile-filter-option.is-active {
  font-weight: 700;
}

.project-mobile-filter-option.is-active:after {
  content: "✓";
  margin-left: .75rem;
}

.project-mobile-filter-divider {
  background-color: #0000001a;
  height: 1px;
  margin: .5rem 0;
}

@media screen and (max-width: 899px) {
  .project-empty-card {
    height: 140px;
  }

  .project-empty-card span {
    font-size: 17px;
  }

  .project-empty-actions {
    flex-flow: column;
    align-items: stretch;
    gap: 1rem;
  }

  .project-empty-btn-primary,
  .project-empty-btn-secondary {
    text-align: center;
    width: 100%;
  }

  /* Two-row layout hidden below 900px, single toggle shown instead. */
  .project-filter-bar,
  .project-worktype-wrap {
    display: none;
  }

  .project-mobile-filter-toggle {
    display: flex;
    /* .vertical-wrapper's own row-gap (4rem, see §135) is what pushes the
       card grid this far below the toggle — on mobile the toggle is the
       only visible sibling above the grid, so this margin only affects
       that one gap, matching it to the ~31px already sitting above the
       toggle (navbar height vs. the wrapper's own margin-top, unrelated
       to row-gap and left untouched). */
    margin-bottom: calc(31px - 4rem);
  }
}

/* Project pages' photo gallery lightbox (js/photo-lightbox.js) — a full
   replacement for the export's own built-in lightbox, which renders
   visibly broken (ghosted, overlapping images, no real full-screen view)
   at every width, and whose mobile pinch-to-zoom used to corrupt the
   page's own layout. Touch-action:none lives on .custom-photo-lightbox-
   image specifically (not this whole full-viewport overlay) — that's the
   only element pinch/pan/double-tap handlers are attached to, so it's the
   only one that needs to opt out of the browser's own native touch
   gesture handling. Scoping it that tightly, rather than to the whole
   overlay, also turned out to matter for tap-to-close on the backdrop
   area actually working reliably. */
.photo-phone img {
  cursor: pointer;
}

.custom-photo-lightbox-backdrop {
  z-index: 999997;
  background-color: #000000e6;
  opacity: 0;
  visibility: hidden;
  transition: opacity .25s ease;
  position: fixed;
  inset: 0;
}

.custom-photo-lightbox-backdrop.is-open {
  opacity: 1;
  visibility: visible;
}

.custom-photo-lightbox {
  z-index: 999998;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  justify-content: center;
  align-items: center;
  padding: 1.5rem;
  transition: opacity .25s ease;
  display: flex;
  position: fixed;
  inset: 0;
}

.custom-photo-lightbox.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

.custom-photo-lightbox-image {
  object-fit: contain;
  max-width: 100%;
  max-height: 100%;
  border-radius: 4px;
  user-select: none;
  -webkit-user-drag: none;
  touch-action: none;
  /* The zoomToward() math in photo-lightbox.js assumes the transform
     scales from the image's own top-left corner, not the CSS default
     (50% 50%) — keeps the translate/scale relationship simple enough to
     solve directly for "keep this point under the finger" on every
     pinch/double-tap, instead of needing a center-relative correction
     term as well. */
  transform-origin: 0 0;
}

.custom-photo-lightbox-close,
.custom-photo-lightbox-prev,
.custom-photo-lightbox-next {
  z-index: 999999;
  color: #fff;
  cursor: pointer;
  background-color: #ffffff1a;
  border: none;
  border-radius: 50%;
  justify-content: center;
  align-items: center;
  width: 44px;
  height: 44px;
  opacity: 0;
  visibility: hidden;
  transition: opacity .2s ease, background-color .2s ease;
  display: flex;
  position: fixed;
}

.custom-photo-lightbox-close:hover,
.custom-photo-lightbox-prev:hover,
.custom-photo-lightbox-next:hover {
  background-color: #ffffff33;
}

.custom-photo-lightbox-close.is-open,
.custom-photo-lightbox-prev.is-open,
.custom-photo-lightbox-next.is-open {
  opacity: 1;
  visibility: visible;
}

.custom-photo-lightbox-close {
  top: 1.5rem;
  right: 1.5rem;
}

.custom-photo-lightbox-prev {
  top: 50%;
  left: 1.5rem;
  transform: translateY(-50%);
}

.custom-photo-lightbox-next {
  top: 50%;
  right: 1.5rem;
  transform: translateY(-50%);
}

.custom-photo-lightbox-counter {
  z-index: 999999;
  color: #fff;
  text-align: center;
  background-color: #00000080;
  border-radius: 999px;
  padding: .375rem .875rem;
  font-family: Open Sans, sans-serif;
  font-size: .875rem;
  opacity: 0;
  visibility: hidden;
  transition: opacity .2s ease;
  position: fixed;
  bottom: 1.5rem;
  left: 50%;
  transform: translateX(-50%);
}

.custom-photo-lightbox-counter.is-open {
  opacity: 1;
  visibility: visible;
}

@media screen and (max-width: 479px) {
  .custom-photo-lightbox-prev,
  .custom-photo-lightbox-next {
    width: 38px;
    height: 38px;
  }

  .custom-photo-lightbox-close {
    top: 1rem;
    right: 1rem;
  }

  .custom-photo-lightbox-prev {
    left: .5rem;
  }

  .custom-photo-lightbox-next {
    right: .5rem;
  }
}

/* ============================================================
   Breadcrumbs + page header
   Added for SEO (schema.org BreadcrumbList has a visible
   counterpart) and accessibility. Typography deliberately
   mirrors the existing site scale: Open Sans, .section-title sizes
   for the title, --color--text-color-light-gray for inactive
   crumbs, so the block reads as native to the design.
   ============================================================ */

.breadcrumbs {
  font-family: Open Sans, sans-serif;
  font-size: .875rem;
  line-height: 1.4;
}

.breadcrumbs__list {
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
}

.breadcrumbs__item {
  align-items: center;
  gap: .5rem;
  margin: 0;
  padding: 0;
  display: flex;
}

.breadcrumbs__item + .breadcrumbs__item:before {
  content: "/";
  color: var(--color--text-color-light-gray);
}

.breadcrumbs__link {
  color: var(--color--text-color-light-gray);
  text-decoration: none;
  transition: color .2s ease;
}

.breadcrumbs__link:hover,
.breadcrumbs__link:focus-visible {
  color: var(--000);
  text-decoration: underline;
}

.breadcrumbs__current {
  color: var(--000);
  font-weight: 600;
}

/* Light variant, for placement over dark imagery */

.page-header {
  flex-direction: column;
  gap: 1rem;
  display: flex;
}

.page-header__title {
  color: var(--000);
  margin-top: 0;
  margin-bottom: 0;
  font-family: Open Sans, sans-serif;
  font-size: 2rem;
  font-weight: 600;
  line-height: 120%;
}

/* Pages whose first content block sits directly under the fixed navbar
   need the same 6rem clearance the existing first blocks already use. */
.page-header.has-navbar-offset {
  margin-top: 6rem;
}

.page-header__subtitle {
  color: var(--color-c1-75);
  max-width: 46rem;
  margin-top: 0;
  margin-bottom: 0;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  line-height: 160%;
}

/* Standalone breadcrumbs (object pages) sit inside the regular content
   column, so they carry their own bottom rhythm. */
.breadcrumbs-row,
/* Same column, but on the service pages it now wraps only the page header --
   breadcrumbs were removed from everything except the object pages. */
.page-header-row {
  width: 100%;
  max-width: 74rem;
  margin-left: auto;
  margin-right: auto;
  padding-top: 2rem;
}

@media screen and (max-width: 991px) {
  .page-header__title {
    font-size: 1.9rem;
  }
}

@media screen and (max-width: 767px) {
  .page-header__title {
    font-size: 1.5rem;
  }

  .breadcrumbs {
    font-size: .8125rem;
  }
}

/* ============================================================
   Service pages (/services/<slug>/)
   Built from the same tokens as the rest of the site: Open Sans,
   --000 for headings, --color-c1-75 for body copy,
   the 74rem content column and the 6rem vertical rhythm.
   ============================================================ */

.service-section {
  width: 100%;
  padding-top: 3rem;
}

.service-section__title {
  margin-top: 0;
  margin-bottom: 2rem;
}

.service-grid {
  grid-column-gap: 2rem;
  grid-row-gap: 2rem;
  grid-template-columns: 1fr 1fr;
  display: grid;
}

.service-item__title {
  color: var(--000);
  margin-top: 0;
  margin-bottom: .5rem;
  font-family: Open Sans, sans-serif;
  font-size: 1.1rem;
  font-weight: 700;
  line-height: 130%;
}

.service-item__text,
.service-step__text {
  color: var(--color-c1-75);
  margin-top: 0;
  margin-bottom: 0;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  line-height: 160%;
}

/* Стрічка, а не третій поспіль двоколонковий список. На сторінці послуги
   три секції поспіль мали однакову будову — заголовок і дві колонки, — і
   сторінка читалась однією нотою від верху до низу. Етапи для цього
   найкращий кандидат: вони послідовні, їх пʼять, і в рядок вони читаються
   як хід роботи, а не як ще один перелік. auto-fit, щоб число колонок
   бралося з числа кроків, а не було зашите. */
.service-steps {
  counter-reset: service-step;
  grid-column-gap: 1.5rem;
  grid-row-gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
}

.service-step {
  counter-increment: service-step;
  border-top: 1px solid var(--color-c1-10);
  padding-top: 1rem;
}

.service-step__title {
  color: var(--000);
  margin-top: 0;
  margin-bottom: .5rem;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  font-weight: 700;
  line-height: 130%;
}

/* Номер став над назвою, а не перед нею: у вузькій колонці стрічки він
   інакше відриває перше слово на власний рядок. Регістр той самий, що в
   переліку напрямів і в номерах питань: 11px, 700, letter-spacing 1px,
   табличні цифри. */
.service-step__title:before {
  content: "0" counter(service-step);
  color: #09061159;
  letter-spacing: 1px;
  margin-bottom: .5rem;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  display: block;
}

.service-projects {
  grid-column-gap: 1.5rem;
  grid-row-gap: 1.5rem;
  grid-template-columns: 1fr 1fr 1fr;
  display: grid;
}

.service-project {
  color: var(--000);
  text-decoration: none;
  display: block;
}

.service-project__img {
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 4px;
  width: 100%;
  height: auto;
  display: block;
}

.service-project__name {
  margin-top: .75rem;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  font-weight: 600;
  display: block;
}

.service-project:hover .service-project__name {
  text-decoration: underline;
}

.service-cta {
  border: 1px solid var(--color-c1-10);
  border-radius: 1.2rem;
  flex-flow: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 1.5rem;
  padding: 2rem;
  display: flex;
}

.service-cta__text {
  color: var(--000);
  max-width: 34rem;
  margin: 0;
  font-family: Open Sans, sans-serif;
  font-size: 1.25rem;
  line-height: 140%;
}

.service-cta__btn {
  float: none;
  margin-left: 0;
  margin-right: 0;
}

@media screen and (max-width: 767px) {
  .service-grid,
  .service-steps {
    grid-template-columns: 1fr;
  }

  .service-projects {
    grid-template-columns: 1fr;
  }

  .service-cta {
    flex-direction: column;
    align-items: flex-start;
    padding: 1.5rem;
  }

  .service-cta__text {
    font-size: 1.1rem;
  }
}

/* Cards on the /services/ hub: same block as .service-item, but the whole
   card is the link. */
.service-item--link {
  color: var(--000);
  border: 1px solid var(--color-c1-10);
  border-radius: 1.2rem;
  padding: 1.5rem;
  text-decoration: none;
  transition: border-color .2s ease, background-color .2s ease;
  display: block;
}

.service-item--link:hover,
.service-item--link:focus-visible {
  border-color: var(--color-c1-40);
  background-color: var(--color-c1-04);
}

.service-item__more {
  color: var(--000);
  margin-top: 1rem;
  font-family: Open Sans, sans-serif;
  font-size: .875rem;
  font-weight: 600;
  display: inline-block;
}

.service-item--link:hover .service-item__more {
  text-decoration: underline;
}

/* ============================================================
   Hero showcase (home page)
   Five real project renders cross-fading behind the headline, each
   clickable through to its own page, with a drafting overlay in the
   same line language the preloader already uses. Built by
   js/hero-showcase.js; everything here is additive and the old hero
   photo is simply hidden on this page.
   ============================================================ */

/* The section used to take its height from the hero photo; that element is
   hidden now, so the height moves onto the section itself. */
.page .hero-section {
  /* single source of truth for the left edge: the copy is padded by it and the
     drafting rules are positioned on it, so they always line up */
  --hero-gutter: 2rem;
  /* the rule reads as a dimension line, so the copy sits inside it rather
     than butting against it -- shapes (buttons) especially looked glued on */
  --hero-indent: 1.25rem;
  background-color: #0b0b0d;
  min-height: 100svh;
  display: flex;
  align-items: center;
  position: relative;
  overflow: hidden;
}

/* the original static photo is replaced by the showcase stage */
.page .hero-image-wrapper.about-photo.hero__photo-box {
  display: none;
}

.hero-stage {
  z-index: 0;
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.hero-slide {
  background-position: 50%;
  background-repeat: no-repeat;
  background-size: cover;
  opacity: 0;
  position: absolute;
  inset: 0;
  transform: scale(1.06);
  transition: opacity 1.5s ease, transform 7s linear;
}

.hero-slide.is-active {
  opacity: 1;
  transform: scale(1);
}

/* Renders are bright at the top and dark at the bottom, so the scrim is
   weighted to the left and bottom where the copy sits, rather than flattening
   the whole image. */
.hero-scrim {
  background-image: linear-gradient(75deg, #0b0b0dfa 0%, #0b0b0dcc 32%, #0b0b0d4d 58%, #0b0b0d1a 100%),
                    linear-gradient(to top, #0b0b0de6 0%, #0b0b0d00 42%);
  position: absolute;
  inset: 0;
}

.hero-draft {
  pointer-events: none;
  position: absolute;
  inset: 0;
}

.hero-draft__v,
.hero-draft__h {
  background-color: #ffffff2b;
  position: absolute;
}

.hero-draft__v {
  top: 0;
  bottom: 0;
  width: 1px;
  transform: scaleY(0);
  transform-origin: top;
  animation: 1.5s cubic-bezier(.22, 1, .36, 1) .35s forwards hero-rule-v;
}

.hero-draft__v--l { left: var(--hero-gutter); }
.hero-draft__v--r { right: var(--hero-gutter); }

.hero-draft__h {
  left: 0;
  right: 0;
  bottom: 22%;
  height: 1px;
  transform: scaleX(0);
  transform-origin: 0;
  animation: 1.7s cubic-bezier(.22, 1, .36, 1) .55s forwards hero-rule-h;
}

.hero-draft__node {
  background-color: #0000;
  border: 1px solid #ffffff5c;
  border-radius: 50%;
  width: 9px;
  height: 9px;
  position: absolute;
  bottom: 22%;
  opacity: 0;
  animation: .5s ease 1.5s forwards hero-node-in;
  transform: translate(-50%, 50%);
}

.hero-draft__node--l { left: var(--hero-gutter); }
.hero-draft__node--r { left: auto; right: var(--hero-gutter); transform: translate(50%, 50%); }

.hero-draft__tick {
  background-color: #ffffff4d;
  width: 15px;
  height: 1px;
  position: absolute;
  opacity: 0;
  animation: .5s ease 1.35s forwards hero-node-in;
}

.hero-draft__tick--l { top: 28%; left: var(--hero-gutter); transform: translateX(-50%); }
.hero-draft__tick--r { top: 46%; right: var(--hero-gutter); transform: translateX(50%); }

@keyframes hero-rule-v { to { transform: scaleY(1); } }
@keyframes hero-rule-h { to { transform: scaleX(1); } }
@keyframes hero-node-in { to { opacity: 1; } }

/* ---- copy sits above the stage ----
   It was absolutely positioned with a 15rem top margin so it could be placed
   on the old photo. The block is taller now (two CTAs + the slide HUD), so it
   goes back into normal flow and the section's own flex centring places it. */
.page .hero-slogan-block.hero-slogan-wrap {
  z-index: 2;
  width: 100%;
  position: relative;
}

.page .hero-copy-column {
  z-index: 2;
  box-sizing: border-box;
  width: 100%;
  max-width: 46rem;
  height: auto;
  min-height: 0;
  margin: 0;
  padding-left: calc(var(--hero-gutter) + var(--hero-indent));
  padding-right: var(--hero-gutter);
  position: static;
  transform: none;
}

@media screen and (min-width: 1280px) {
  .page .hero-section { --hero-gutter: 5rem; }
}

@media screen and (min-width: 1440px) {
  .page .hero-section { --hero-gutter: 8rem; }
  .page .hero-copy-column { max-width: 52rem; }
}

.page .hero-title.hero-title-text, .page .hero-subtitle.hero-subtitle-text {
  color: var(--white);
}

/* Optical alignment: at 64px the "D" carries ~5.75px of left side bearing, so
   its box sitting on the same x as the subtitle and the buttons still reads as
   indented. Measured 5.75/64 = .09em, expressed in em so it holds at every
   breakpoint instead of only the one it was measured at. */
.page .hero-title.hero-title-text {
  margin-left: -.09em;
}

.page .hero-subtitle.hero-subtitle-text {
  /* was an inline style="max-width:min(500px,85vw)" straight from the export,
     which beat every stylesheet rule -- 85vw is 331px on a 390px phone, so the
     line ran to the edge no matter what the media query asked for */
  color: var(--color-c2-80);
  max-width: 500px;
}

/* ---- the two calls to action ----
   The export's own hero button is superseded by this pair, so it is hidden at
   every width rather than only on phones -- it was still ghosting through
   above the new buttons on desktop. */
.page .hero-section .btn-primary.white {
  display: none;
}

.hero-actions {
  flex-wrap: wrap;
  align-items: center;
  gap: .75rem;
  margin-top: 2rem;
  display: flex;
}

.hero-btn {
  color: #0b0b0d;
  cursor: pointer;
  background-color: var(--white);
  border: 1px solid #fff;
  border-radius: 999px;
  align-items: center;
  gap: .625rem;
  height: 52px;
  padding: 0 1.5rem;
  font-family: Open Sans, sans-serif;
  font-size: .9375rem;
  font-weight: 600;
  text-decoration: none;
  transition: transform .25s cubic-bezier(.22,1,.36,1), background-color .25s ease, color .25s ease;
  display: inline-flex;
  position: relative;
  overflow: hidden;
}

.hero-btn--ghost {
  color: var(--white);
  background-color: #0000;
  border-color: #ffffff5c;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.hero-btn:hover,
.hero-btn:focus-visible {
  transform: translateY(-2px);
}

.hero-btn--ghost:hover,
.hero-btn--ghost:focus-visible {
  color: #0b0b0d;
  background-color: var(--white);
  border-color: #fff;
}

.hero-btn__arrow {
  transition: transform .3s cubic-bezier(.22,1,.36,1);
  display: inline-flex;
}

.hero-btn:hover .hero-btn__arrow {
  transform: translate(3px, -3px);
}

/* ---- caption chip + slide ticks ---- */
/* The ticks tell you which project the caption is naming, so they belong next
   to it. space-between across a full-width row put them at the far edge of a
   1512px screen, a whole viewport away from the name they refer to -- the eye
   had to travel to connect the two. Stacked directly under the caption now. */
.hero-hud {
  flex-direction: column;
  align-items: flex-start;
  gap: 1.125rem;
  margin-top: 3rem;
  display: flex;
}

.hero-chip {
  color: var(--white);
  border-left: 1px solid #ffffff40;
  flex-direction: column;
  gap: .125rem;
  margin-left: calc(-1 * var(--hero-indent));
  padding-left: var(--hero-indent);
  text-decoration: none;
  transition: border-color .25s ease, transform .25s ease;
  display: flex;
}

.hero-chip:hover {
  border-left-color: #fff;
  transform: translateX(3px);
}

.hero-chip__eyebrow {
  color: var(--color-c2-60);
  letter-spacing: .12em;
  text-transform: uppercase;
  font-family: Open Sans, sans-serif;
  font-size: .625rem;
}

.hero-chip__name {
  font-family: Open Sans, sans-serif;
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.2;
}

.hero-chip__kind {
  color: var(--color-c2-60);
  font-family: Open Sans, sans-serif;
  font-size: .8125rem;
}

.hero-chip.is-in .hero-chip__name,
.hero-chip.is-in .hero-chip__kind {
  animation: .5s ease both hero-chip-in;
}

@keyframes hero-chip-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

.hero-ticks {
  gap: .5rem;
  display: flex;
}

/* Sitting on the render rather than at the screen edge, they need a touch
   more presence to stay readable, and a bigger hit area than 3px. */
.hero-ticks {
  margin-left: calc(-1 * var(--hero-indent));
  padding-left: var(--hero-indent);
}

.hero-tick {
  cursor: pointer;
  background-color: #0000;
  border: none;
  border-radius: 0;
  width: 38px;
  height: 14px;
  padding: 0;
  position: relative;
  overflow: visible;
}

.hero-tick:before {
  content: "";
  background-color: #ffffff47;
  border-radius: 999px;
  height: 2px;
  position: absolute;
  inset: 6px 0 auto;
}

.hero-tick:hover:before {
  background-color: #ffffff8f;
}

.hero-tick__fill {
  background-color: var(--white);
  border-radius: 999px;
  height: 2px;
  display: block;
  position: absolute;
  inset: 6px 0 auto;
  transform: scaleX(0);
  transform-origin: 0;
}

.hero-tick.is-active .hero-tick__fill {
  animation: 5.2s linear forwards hero-tick-fill;
}

@keyframes hero-tick-fill {
  to { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-slide { transition: opacity .3s linear; transform: none; }
  .hero-draft__v { animation: none; transform: scaleY(1); }
  .hero-draft__h { animation: none; transform: scaleX(1); }
  .hero-draft__node, .hero-draft__tick { animation: none; opacity: 1; }
  .hero-tick.is-active .hero-tick__fill { animation: none; transform: scaleX(1); }
  .hero-chip.is-in .hero-chip__name,
  .hero-chip.is-in .hero-chip__kind { animation: none; }
}

@media screen and (max-width: 991px) {
  .hero-hud { margin-top: 2.25rem; }
}

@media screen and (max-width: 767px) {
  .page .hero-section {
    --hero-gutter: 1.25rem;
    --hero-indent: 1rem;
    /* 100vh on a phone is measured with the address bar HIDDEN, so the section
       overshoots and the next one peeks in by however tall that bar is -- which
       differs between iOS and Android, hence "fine on my iPhone, short on his
       Samsung". svh is the small viewport height, i.e. address bar VISIBLE, so
       the section fits on every handset. vh first as the fallback. */
    height: auto;
    min-height: 100vh;
    min-height: 100svh;
  }

  /* A vertical wash: the copy runs the height of the screen here, unlike
     desktop where it sits in the lower left, so the corner-weighted scrim left
     the headline and subtitle over bright sky. */
  .page .hero-section .hero-scrim {
    background-image: linear-gradient(#0b0b0de0 0%, #0b0b0d94 34%, #0b0b0d99 52%, #0b0b0de6 82%, #0b0b0df2 100%);
  }

  .page .hero-copy-column {
    box-sizing: border-box;
    flex-direction: column;
    width: 100%;
    max-width: none;
    min-height: 100vh;
    min-height: 100svh;
    margin: 0;
    padding-top: 5.5rem;
    padding-right: var(--hero-gutter);
    padding-bottom: 2rem;
    padding-left: calc(var(--hero-gutter) + var(--hero-indent));
    display: flex;
    position: static;
    transform: none;
  }

  .page .hero-title.hero-title-text {
    text-align: left;
    width: auto;
    max-width: 13rem;
    margin: auto 0 .625rem;
    font-size: 1.875rem;
    line-height: 105%;
    display: block;
  }

  .page .hero-subtitle.hero-subtitle-text {
    color: #fffffff2;
    text-align: left;
    width: auto;
    max-width: 17rem;
    margin: 0;
    font-size: .9375rem;
    line-height: 148%;
    display: block;
  }

  .hero-actions { gap: .5rem; margin-top: 1.5rem; margin-bottom: auto; }
  .hero-btn { height: 46px; padding: 0 1.125rem; font-size: .875rem; }

  /* pinned to the bottom, so the space between the pitch and the caption is
     whatever this particular phone has spare rather than a fixed gap */
  .hero-hud {
    gap: .875rem;
    padding-top: 2rem;
  }

  .hero-tick { width: 30px; }
}

@media screen and (max-width: 379px) {
  .page .hero-title.hero-title-text { font-size: 1.75rem; }
  .page .hero-subtitle.hero-subtitle-text { font-size: .875rem; }
  .hero-btn { height: 44px; padding: 0 1rem; font-size: .8125rem; }
}

/* ============================================================
   Navbar + burger menu: more see-through
   The blur is deliberately left exactly as it was (5px desktop /
   4px phone, plus the fixed-ancestor workaround in menu-blur-fix.js).
   Only the white behind it changes: 45%, down from the export's 60%.

   45% is the floor, not a taste call. The nav text is black and the
   hero slides run dark right under the bar; blended over the darkest
   frame the contrast works out at 5.15:1, while 40% drops to 4.37:1
   and fails WCAG AA. Going more transparent than this needs the nav
   text to change colour too.
   ============================================================ */
.navbar{
  background-color: #ffffff73;
}

/* Only below 992px, where .header__menu is the drop-down panel. On desktop it
   is an inline container sitting INSIDE the bar, so giving it a background of
   its own stacked a second 45% layer over the navbar's and showed up as a
   brighter rectangle behind the links. The export left it transparent there
   for exactly this reason. */
@media screen and (max-width: 991px) {
  .header__menu {
    background-color: #ffffff73;
    background-image: none;
  }
}

/* ============================================================
   "Зв'язатись" — one visual language for one action
   The service cards, the "Чому ми?" section and the burger menu
   all offer the same action and looked like three unrelated
   controls. They now share the site's own pill vocabulary:
   outlined pill for the in-page CTAs, solid pill for the menu,
   which mirrors .btn-primary-inner's existing secondary/primary
   split rather than inventing a new one.
   ============================================================ */

/* --- shared: outlined pill --------------------------------- */
/* Border was #00000080 — a 50% hairline that read as a dotted
   suggestion rather than an edge. 70% puts the boundary at 9:1
   against white while staying lighter than the text itself. */
.service-card-cta,
.page-link-block {
  border: 1px solid #090611b3;
  transition: background-color .22s ease, color .22s ease, border-color .22s ease, transform .22s ease;
}

.service-card-cta:hover,
.service-card-cta:focus-visible {
  color: var(--white);
  background-color: #090611;
  border-color: #090611;
}

/* --- "Чому ми?": was a 32px underlined text link ------------ */
.page-link-block {
  box-sizing: border-box;
  color: #090611;
  background-color: #0000;
  border-radius: 1.2rem;
  align-items: center;
  gap: .5rem;
  height: 52px;
  padding: 0 1.625rem;
  text-decoration: none;
  display: inline-flex;
}

/* its underline is animated on hover; the pill has its own
   hover now, so the line is hidden rather than left half-driving
   a control it no longer belongs to */
.page-link-block .page-link-underline {
  display: none;
}

.page-link-block .text-medium-alt {
  color: inherit;
  padding: 0;
  font-family: Open Sans, sans-serif;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1;
}

.page-link-block:hover,
.page-link-block:focus-visible {
  color: var(--white);
  background-color: #090611;
  border-color: #090611;
}

/* --- burger menu: solid pill, the primary weight ------------ */
.nav-cta {
  display: none;
}

@media screen and (max-width: 991px) {
  .nav-cta {
    box-sizing: border-box;
    color: var(--white);
    background-color: #090611;
    border: 1px solid #090611;
    border-radius: 999px;
    justify-content: center;
    align-items: center;
    gap: .5rem;
    width: calc(100% - 3rem);
    height: 52px;
    margin: .25rem auto 1.25rem;
    font-family: Open Sans, sans-serif;
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    transition: background-color .22s ease, color .22s ease;
    display: flex;
  }

  .nav-cta:hover,
  .nav-cta:focus-visible {
    color: #090611;
    background-color: #0000;
  }

  .nav-cta__arrow {
    transition: transform .25s cubic-bezier(.22, 1, .36, 1);
  }

  .nav-cta:hover .nav-cta__arrow {
    transform: translate(2px, -2px);
  }
}

/* The CTA banner's content sat 99px high: the container is a fixed 798px and
   its content stacked from the top, leaving 232px above and 430px below.
   Centring inside the container centres it in the section too, since the
   section's own 96px padding is already symmetric. */
.cta-banner-container {
  flex-direction: column;
  justify-content: center;
  display: flex;
}

/* ============================================================
   .visually-hidden
   Standard sr-only: the element stays in the DOM with its text, is
   read by screen readers and counted by search engines, but takes up
   no space and cannot be seen.

   Deliberately NOT display:none or visibility:hidden -- both remove
   the element from the accessibility tree, which would undo the whole
   point of keeping the h1. clip-path does the hiding in modern
   browsers; the legacy clip is kept for older ones. white-space:nowrap
   stops the 1px box from forcing odd line breaks.
   ============================================================ */
.visually-hidden {
  clip: rect(0 0 0 0);
  -webkit-clip-path: inset(50%);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  position: absolute;
  overflow: hidden;
}

/* A .page-header whose only child is the hidden title still occupied space:
   .visually-hidden is position:absolute, so the wrapper collapses to 0px --
   but a 0px flex item in .vertical-wrapper (gap: 64px) still takes a 64px
   gap, leaving a band of empty page where the heading used to be.

   display:contents makes the wrapper generate no box at all, so it stops
   being a flex item and the gap goes with it. The h1 itself stays in the
   DOM and in the accessibility tree -- which display:none on the wrapper
   would have destroyed, taking the h1 down with it.

   Scoped by :has so it only fires when nothing visible is left inside: the
   service pages keep a visible subtitle in the same wrapper and are
   untouched. */
.page-header:not(:has(> :not(.visually-hidden))) {
  display: contents;
}

/* ============================================================
   Дві дрібниці мобільної версії
   ============================================================ */

/* Рядки категорій у «Проекти». Підкреслення намальоване бордером на
   .project-item, і горизонтальний padding стоїть на тому ж елементі —
   тому лінія виступала на 11px з кожного боку відносно тексту (при
   375px це padding: 0 3vw). Знімаємо саме цей відступ, щоб початок
   тексту збігався з початком лінії. Вертикальні відступи й сама лінія
   лишаються як були. */
@media screen and (max-width: 991px) {
  .project-item {
    padding-left: 0;
    padding-right: 0;
  }
}

/* Просвіт між словом і лінією. Нижче 480px .project-content переходить
   у flex і рядок тексту заходив на 3px ЗА лінію — слова торкались її
   знизу. Відступ відсуває лінію, лишаючи під словом чисте поле.
   Тільки для телефонних ширин: уже на 768px просвіт був 26px, тобто
   там нічого правити не треба. */
@media screen and (max-width: 479px) {
  .project-item {
    padding-bottom: 14px;
  }
}

/* Перенос, який діє лише на вузькому екрані. На десктопі перше речення
   підзаголовка «Проекти» вміщається в один рядок, і розрив після «до»
   там був би недоречний. */
.br-phone { display: none; }

@media screen and (max-width: 767px) {
  .br-phone { display: inline; }
}

/* Підзаголовок «Галерея». Там примусовий перенос стоїть УСЕРЕДИНІ
   речення, після «що»: на широкому екрані він ділить фразу навпіл, а
   на вузькому лишає в середині рядок із двох слів. На телефоні даємо
   тексту переноситись самому.

   Переноси в «Проектах» і «Партнерах» навмисно не чіпаємо — там вони
   стоять МІЖ реченнями, і це доречно на будь-якій ширині. */
@media screen and (max-width: 767px) {
  .gallery-section .subheading-text br {
    display: none;
  }
}

/* ============================================================
   FAQ, варіант 3 — «Сцена»

   Тут змінено не ефект, а структуру уваги.

   Чому люди перестають клацати акордеон: кожне відкриття штовхає
   сторінку вниз. Ти губиш місце, де читав, і після другого питання
   кидаєш. Клік має ціну — і люди перестають платити.

   Тут ціни немає. Усі відповіді лежать ОДНА НА ОДНІЙ у тій самій
   комірці сітки, тож висота блоку не змінюється взагалі. Сторінка не
   рухається ні на піксель, хоч 20 разів перемикай. Досліджувати
   безкоштовно — і тому досліджують.

   Плюс чотири прийоми:
   1. Перша відповідь відкрита одразу. Людина бачить нагороду ДО того,
      як щось зробила. Порожня сцена такого не дає.
   2. Наведення показує відповідь, не фіксуючи вибір — безкоштовна
      дегустація. Весь список можна прочитати, жодного разу не
      клікнувши, і саме тому хочеться клікати.
   3. Картка вирівняна по центру правої колонки, а вся секція
      вміщається в один екран — щоб перемкнути й прочитати, не треба
      нікуди рухатись.
   4. Лічильник «переглянуто 3 з 8» — незавершений список тисне на
      завершення (ефект Зейгарнік).

   Читабельність — окремим пріоритетом: 17px, інтерліньяж 1.75,
   рядок обмежений, повний контраст. Нічого не приглушено й не розмито.
   ============================================================ */

.faq3 {
  --ink: #090611;
  --bronze: #b7a27a;
  --rule: #09061120;
  --card: #f5f5f7;
  --n: 8;                 /* кількість питань, використовується в сітці */
  margin-top: 6rem;
  /* Нижній відступ потрібен саме тут, а не лише в CTA. У CTA темний фон,
     тож його власний padding-top теж темний — без цього поля остання
     лінія переліку впиралась би просто в чорний блок, без жодного
     білого просвіту. 6rem — той самий крок, яким розділені всі секції
     сторінки. */
  margin-bottom: 6rem;
  padding: 0 1.25rem;
}

.faq3-head {
  max-width: 760px;
  margin: 0 auto 3.25rem;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.faq3-head > * { margin: 0; }
.faq3-head .subheading-text,
.faq3-head .subheading-text strong { text-align: center; display: block; }

/* --- лічильник ---------------------------------------------------- */
.faq3-counter {
  align-items: center;
  gap: .7rem;
  margin-top: .6rem;
  display: flex;
  font-family: "Open Sans", sans-serif;
  font-size: 11px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: #09061180;
}

/* Саме число велике: воно і є показник прогресу, і має ловитись оком
   без пошуку. Підпис і знаменник лишаються дрібними, щоб не сперечатись
   із ним за увагу. */
.faq3-counter__now {
  color: var(--bronze);
  letter-spacing: -.5px;
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.faq3-counter__of {
  color: #09061166;
  letter-spacing: 0;
  font-size: 14px;
  font-weight: 600;
}

.faq3-counter__track { background-color: var(--rule); width: 132px; height: 3px; display: block; overflow: hidden; }
.faq3-counter__fill {
  background-color: var(--bronze);
  transform-origin: left;
  width: 100%; height: 100%;
  display: block;
  transform: scaleX(0);
  transition: transform .45s cubic-bezier(.4, 0, .2, 1);
}
.faq3-counter__done { color: var(--bronze); font-weight: 700; }

/* --- сітка --------------------------------------------------------
   Ліва колонка — питання, права — сцена. Усі відповіді розміщені в
   ОДНІЙ області (колонка 2, усі рядки), тобто лежать одна на одній.
   Через це область завжди така, як найвища з них, і перемикання
   не змінює висоту нічого. */
.faq3-grid { max-width: 1180px; margin: 0 auto; }

@media screen and (min-width: 900px) {
  .faq3-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr);
    grid-template-rows: repeat(var(--n), auto);
    column-gap: 3.5rem;
  }
  .faq3-q { grid-column: 1; }
  .faq3-a { grid-column: 2; grid-row: 1 / -1; align-self: start; }
}

/* --- питання ------------------------------------------------------ */
.faq3-q {
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  text-align: left;
  background: none;
  border: 0;
  border-bottom: 1px solid #0906111a;
  border-radius: 12px 12px 0 0;
  width: 100%;
  /* місце під стрілку зарезервоване в УСІХ рядках, щоб вибір не
     переверстовував текст і не міняв висоту рядка */
  padding: 1.4rem 2.6rem 1.4rem 1.15rem;
  align-items: baseline;
  gap: 1rem;
  display: flex;
  position: relative;
  font-family: "Open Sans", sans-serif;
  color: #090611b3;
  transition: color .25s ease, background-color .25s ease;
}

.faq3-q:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }

/* Бронзова риска ліворуч — знак «зараз читаємо це». Рухається лише
   transform, тож нічого не переверстовується. */
.faq3-q:before {
  content: "";
  background-color: var(--bronze);
  border-radius: 3px;
  width: 3px;
  position: absolute;
  top: .5rem; bottom: .5rem; left: 0;
  transform: scaleY(0);
  transform-origin: center;
  transition: transform .3s cubic-bezier(.4, 0, .2, 1);
}

/* Стрілка вказує туди, де з'явилась відповідь. Позиція абсолютна, тож
   поява нічого не зсуває. */
.faq3-q:after {
  content: "→";
  color: var(--bronze);
  font-size: 15px;
  line-height: 1;
  position: absolute;
  top: 50%;
  right: 1rem;
  opacity: 0;
  transform: translate(-6px, -50%);
  transition: opacity .28s ease, transform .28s cubic-bezier(.4, 0, .2, 1);
}

.faq3-q__no {
  color: #09061159;
  letter-spacing: 1px;
  flex: none;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transition: color .25s ease;
}

/* Читабельність переліку.
   Було: усі вісім питань напівжирні (600) при інтерліньяжі 1.45 — вони
   зливались в одну сіру масу, бо жоден не виділявся на тлі інших і
   рядки всередині питання стояли надто щільно.
   Стало: звичайна вага, більший кегль і більший інтерліньяж. Виділяє
   не жирність, а заливка вибраного рядка — і саме тому решта може
   лишатись спокійною. */
.faq3-q__text {
  font-size: 1.06rem;
  font-weight: 500;
  line-height: 1.55;
}

/* Наведення лише повідомляє, що рядок натискний. Відкриває тільки клік. */
@media (hover: hover) {
  .faq3-q:not(.is-selected):hover { color: var(--ink); background-color: #09061108; }
  .faq3-q:not(.is-selected):hover .faq3-q__no { color: var(--bronze); }
}

/* Вибране питання стає ТИМ САМИМ матеріалом, що й картка з відповіддю:
   та сама заливка, той самий радіус. Око читає рядок і картку як один
   об'єкт, а не як два незалежні. Кольорової риски збоку для цього було
   мало. Змінюються лише колір, фон і межа — жодна властивість не
   впливає на розкладку, тож висота не рухається. */
.faq3-q.is-selected {
  color: var(--ink);
  background-color: var(--card);
  border-radius: 12px;
  border-bottom-color: #0000;
}

.faq3-q.is-selected:before { transform: scaleY(1); }
.faq3-q.is-selected:after { opacity: 1; transform: translate(0, -50%); }
.faq3-q.is-selected .faq3-q__no { color: var(--bronze); }
/* Жирність на вибраному навмисно НЕ вмикається: заливка, риска, номер
   і стрілка вже роблять його очевидним, а зміна ваги могла б перекинути
   питання на зайвий рядок і зрушити висоту. */

/* Прочитане лишається позначеним: номер уже не сіріє назад. Список
   поступово наливається бронзою, і одразу видно, які питання ще не
   чіпав — це той самий тиск незавершеності, тільки просто в переліку,
   а не лише в лічильнику. */
.faq3-q.is-read .faq3-q__no { color: var(--bronze); }
.faq3-q.is-read { color: #090611e0; }

/* --- сцена з відповіддю ------------------------------------------- */
.faq3-a {
  transition: opacity .32s ease, transform .32s cubic-bezier(.4, 0, .2, 1);
}

@media screen and (min-width: 900px) {
  .faq3-a {
    opacity: 0;
    visibility: hidden;
    transform: translateY(10px);
  }
  /* Картка коротша за перелік питань, тож вирівнюється по центру
     колонки — так композиція врівноважена, а не притиснута догори. */
  .faq3-grid .faq3-a { align-self: center; }
  .faq3-a.is-shown { opacity: 1; visibility: visible; transform: none; }
}

.faq3-a__card {
  background-color: var(--card);
  border-radius: 19.2px;
  padding: 2.5rem 2.75rem 2.75rem;
  position: relative;
  overflow: hidden;
}

/* Великий бляклий номер — як номер аркуша на кресленні. Прив'язує
   картку до питання ліворуч і дає їй власне обличчя. */
.faq3-a__no {
  color: #0906110f;
  letter-spacing: -4px;
  position: absolute;
  top: .4rem; right: 1.5rem;
  font-family: "Open Sans", sans-serif;
  font-size: 96px;
  font-weight: 700;
  line-height: 1;
  pointer-events: none;
  user-select: none;
}

/* Риска прокреслюється зверху картки, коли відповідь приходить:
   маленьке очікування і маленька винагорода. */
.faq3-a__rule {
  background-color: var(--bronze);
  width: 44px;
  height: 2px;
  margin-bottom: 1.4rem;
  display: block;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .45s cubic-bezier(.4, 0, .2, 1) .12s;
}

.faq3-a.is-shown .faq3-a__rule { transform: scaleX(1); }

.faq3-a__q {
  color: var(--ink);
  margin: 0 0 .9rem;
  font-family: "Open Sans", sans-serif;
  max-width: 80%;          /* щоб не заходити під великий номер */
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.35;
  position: relative;
}

/* Читабельність — окремий пріоритет, а не побічний ефект:
   17px, інтерліньяж 1.75, повний контраст, рядок не задовгий. */
/* Кнопка у відповіді. Візуально — та сама пігулка, що й кнопки
   «Зв'язатись» у послугах: радіус 19.2px, тонка рамка, на наведення
   заливається чорним. Клас власний, щоб не успадкувати чужі правила. */
.faq3-a__cta {
  color: var(--ink);
  background-color: #0000;
  border: 1px solid #090611b3;
  border-radius: 19.2px;
  align-items: center;
  gap: .45rem;
  margin-top: 1.6rem;
  padding: .75rem 1.5rem;
  display: inline-flex;
  position: relative;
  font-family: "Open Sans", sans-serif;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: color .25s ease, background-color .25s ease, border-color .25s ease;
}

.faq3-a__cta:after { content: "↗"; font-size: 13px; }

@media (hover: hover) {
  .faq3-a__cta:hover {
    color: #fff;
    background-color: var(--ink);
    border-color: var(--ink);
  }
}

.faq3-a__cta:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.faq3-a__text {
  color: #090611e6;
  max-width: 58ch;
  margin: 0;
  font-family: "Open Sans", sans-serif;
  font-size: 17px;
  line-height: 1.75;
  position: relative;
}

/* --- вузький екран: звичайний акордеон ---------------------------
   Двох колонок нема, і відповідь мусить бути під пальцем, поруч зі
   своїм питанням. Розмітка та сама — сітка просто вимикається. */
@media screen and (max-width: 899px) {
  .faq3 { margin-top: 4rem; margin-bottom: 4rem; }
  .faq3-head { margin-bottom: 2.25rem; }

  .faq3-q { border-bottom: 0; padding: 1.1rem 2.2rem 1.1rem 1rem; }
  .faq3-q:after { content: "↓"; right: .7rem; }
  .faq3-q.is-selected { border-radius: 12px 12px 0 0; }
  .faq3-q__text { font-size: 1rem; }

  .faq3-a {
    height: 0;
    overflow: hidden;
    border-bottom: 1px solid var(--rule);
    transition: height .32s cubic-bezier(.4, 0, .2, 1);
  }

  .faq3-a__card {
    background-color: #0000;
    border-radius: 0;
    padding: 0 .5rem 1.5rem 1rem;
  }

  .faq3-a__no { display: none; }
  .faq3-a__q { display: none; }        /* питання вже видно поруч */
  .faq3-a__text { font-size: 16px; line-height: 1.7; }
  .faq3-counter__track { width: 96px; }
}

/* без JS усі відповіді просто розгорнуті й читабельні */
.faq3:not(.is-ready) .faq3-a { height: auto; opacity: 1; visibility: visible; transform: none; }
.faq3:not(.is-ready) .faq3-a__rule { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .faq3-a, .faq3-a__rule, .faq3-q, .faq3-q:before, .faq3-counter__fill { transition: none; }
}

/* ------------------------------------------------------------------ */
/* Картки об'єктів на /projects/ — розмір комірки в сітці по класу.    */
/*                                                                     */
/* Редактор видавав це чотирма списками з 28 id-селекторів, по       */
/* одному id на кожну картку. Перевірено: у списку рівно ті самі 28    */
/* карток, що є в розмітці, один-в-один — тобто правило і так          */
/* застосовується до всіх без винятку. Дублюємо його по класу, щоб     */
/* додавання нового об'єкта не вимагало правити чотири списки          */
/* селекторів (а отже й піднімати ?v= на всіх 36 сторінках щоразу).    */
/* Оголошення ті самі, а id-селектори специфічніші за класові, тож для */
/* наявних карток нічого не змінюється — вони й далі виграють.         */
/* .project-card існує лише тут, у єдиному .projects-grid */
/* на projects.html, тож ширше це нічого не зачіпає.                   */
.projects-grid > .project-card {
  grid-area: span 2 / span 1 / span 2 / span 1;
}

@media screen and (max-width: 991px) {
  .projects-grid > .project-card { grid-row: span 1 / span 1; }
}

@media screen and (max-width: 767px) {
  .projects-grid > .project-card { grid-row: span 1 / span 1; }
}

@media screen and (max-width: 479px) {
  .projects-grid > .project-card { grid-row: span 1 / span 1; }
}

/* ══════════════════════════════════════════════════════════════════
   Шапка: мобільне меню
   ──────────────────────────────────────────────────────────────────
   Портал — фіксований контейнер у кінці <body>, куди на час відкриття
   переїжджає меню. Він потрібен по суті: меню виїжджає з-за верхнього
   краю і проходить крізь смугу шапки, а прикрити його самою шапкою не
   можна — вона напівпрозора з backdrop-filter, і меню було б крізь неї
   видно. Обрізає портал. Він же накриває екран і ловить клік повз меню.

   На рівні <body>, а не всередині шапки, він і був раніше: шапка
   position:fixed відрізає backdrop-filter від вмісту сторінки в Blink,
   і попередня задача вже виносила оверлей сюди саме через це.
   ══════════════════════════════════════════════════════════════════ */

@media screen and (max-width: 991px) {
  .navbar .header__menu { display: none; }
  .menu-button { display: block; }
}

.nav-portal {
  display: none;
  position: fixed;
  top: 65px;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 999999;
  overflow: hidden;
}

.nav-portal.is-open { display: block; }

.nav-portal .header__menu {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  min-width: 200px;
  text-align: center;
  /* Стартова позиція така сама, як була: висота меню плюс висота шапки —
     воно ховається повністю за верхнім краєм обрізки, а не просто на
     свою висоту. */
  transform: translateY(calc(-100% - 65px));
  transition: transform 400ms ease;
}

.nav-portal .header__menu.is-open { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .nav-portal .header__menu { transition: none; }
}

/* Колір відкритої кнопки раніше давало правило самого компонента меню.
   На вигляд це не впливає — смужки бургера мають власний background, —
   але лишати обчислене значення іншим без потреби не варто. */
.menu-button.is-open { color: #fff; }

.menu-button.is-open .css-hamburger-icon span:nth-child(1) { top: 9px; transform: rotate(45deg); }
.menu-button.is-open .css-hamburger-icon span:nth-child(2) { opacity: 0; }
.menu-button.is-open .css-hamburger-icon span:nth-child(3) { top: 9px; transform: rotate(-45deg); }

/* Шапка їде вгору при прокрутці вниз і повертається при прокрутці вгору.
   Клас перемикає js/header-hide.js, а рух і його тривалість — тут:
   лінійно, 500мс, на всю власну висоту. Обидва числа зняті з оригіналу
   покадровим записом, а не вибрані на смак. */
.navbar {
  transition: transform 0.5s linear;
}

.navbar.is-hidden {
  transform: translateY(-100%);
}

/* ═══════════════════════════════════════════════════════════════════
   Секція «Проекти» на головній: перелік категорій
   ═══════════════════════════════════════════════════════════════════

   Було: рядок під курсором отримував ледь помітну сіру заливку. Вона
   читалась як «поле вводу підсвітилось», а не як «ось цей пункт». П'ять
   рядків із однаковою вагою так і лишались п'ятьма однаковими рядками.

   Стало: наведення розводить перелік на два плани. Решта рядків гасне
   до 30% чорного, а той, що під курсором, лишається на повну — вибір
   видно периферійним зором, ще не читаючи назв. Далі рядок
   домальовується, і все рухається в один бік, зліва направо, як лінія
   на кресленні:

     ——  Готелі                                                   →
     ↑   ↑                                                        ↑
     риска-виноска    назва відступає               стрілка «веде туди»
     (бронза)         на 44px                       (бронза)

   плюс нижня лінія рядка з волосяної стає двопіксельною чорною і
   прокреслюється від лівого краю до правого за 0.55s.

   Бронза #b7a27a — не нова фарба: нею вже набрані заголовки кроків у
   «Чому ми» (.process-item-title) і вся акцентика переліку питань
   (.faq3 --bronze). Чорна лінія — структура, бронза — позначки на ній.

   Чому не так, як у списку питань (там заливка + зміна кольору): там
   рядок РОЗКРИВАЄТЬСЯ на місці, і заливка показує, що питання й картка
   з відповіддю — один об'єкт. Тут рядок КУДИ ВЕДЕ. Різна дія — різний
   відгук; стрілка й виноска саме про перехід.

   Анімуються тільки transform, opacity і колір — жодна властивість не
   змінює розкладку, висота переліку стоїть на місці протягом усієї
   анімації.

   Межі застосування:
   - (hover: hover) and (pointer: fine) — на тачі :hover залипає після
     тапу, і рядок лишався б підсвіченим до наступного тапу деінде;
   - min-width: 992px для домальовування — нижче .project-item стає
     display:block і position:static, тобто абсолютні псевдоелементи
     втратили б систему координат. Згасання решти рядків працює на
     будь-якій ширині, воно нічого не позиціонує.
   - :is(:hover, :has(:focus-visible)) — той самий стан для клавіатури.
     Список у :is() пробачливий: браузер без :has() відкине саме цей
     аргумент, а не все правило разом. */

@media (hover: hover) and (pointer: fine) {
  /* Гасити рядки через opacity не можна: .project-item — це елемент з
     data-motion="reveal-up", і після появи на ньому лишається інлайновий
     `opacity: 1` від GSAP, який б'є будь-яке правило з таблиці стилів.
     Тому приглушуємо кольором. Заразом це точніше: рамка знизу бере колір
     з currentColor, тож текст і лінію можна вести одним значенням, але
     оголошуємо їх обидва явно, щоб перехід не залежав від того, як
     браузер інтерполює currentColor. */
  .project-list > .project-item {
    transition: color .4s cubic-bezier(.22, .61, .36, 1),
                border-bottom-color .4s cubic-bezier(.22, .61, .36, 1);
  }

  .project-list > .project-item .project-title {
    transition: color .4s cubic-bezier(.22, .61, .36, 1);
  }

  .project-list:is(:hover, :has(:focus-visible)) > .project-item {
    color: #0202024d;
    border-bottom-color: #0202024d;
  }

  .project-list:is(:hover, :has(:focus-visible)) > .project-item .project-title {
    color: #0202024d;
  }

  /* Ті самі ваги селекторів, що й у двох правилах вище, — рядок під
     курсором повертається на повну за порядком у файлі. */
  .project-list > .project-item:is(:hover, :has(:focus-visible)) {
    color: #020202;
    border-bottom-color: #020202;
  }

  .project-list > .project-item:is(:hover, :has(:focus-visible)) .project-title {
    color: var(--000);
  }
}

@media (hover: hover) and (pointer: fine) and (min-width: 992px) {
  /* Риска-виноска ліворуч. Стоїть рівно там, де в спокої починалась
     назва, тож на око вона не з'являється «збоку», а виїжджає з-під
     тексту, який саме звільнив це місце. */
  .project-item .project-content:before {
    content: "";
    background-color: #b7a27a;
    width: 28px;
    height: 2px;
    position: absolute;
    top: 50%;
    left: 0;
    transform: translateY(-50%) scaleX(0);
    transform-origin: 0 50%;
    transition: transform .4s cubic-bezier(.22, .61, .36, 1);
  }

  .project-item:is(:hover, :has(:focus-visible)) .project-content:before {
    transform: translateY(-50%) scaleX(1);
  }

  /* Назва відступає рівно на риску (28px) плюс міжслівний просвіт.
     Скорочення transition переписує весь список, тож колір із блоку вище
     доводиться повторити тут разом із transform. */
  .project-list > .project-item .project-title {
    transition: color .4s cubic-bezier(.22, .61, .36, 1),
                transform .45s cubic-bezier(.22, .61, .36, 1);
  }

  .project-item:is(:hover, :has(:focus-visible)) .project-title {
    transform: translateX(44px);
  }

  /* Стрілка — flex-елемент, а не абсолютний: .project-content уже стоїть
     на space-between, тож вона сама сідає до правого краю. Місце під неї
     зайняте завжди, тож поява нічого не зсуває. */
  .project-content:after {
    content: "→";
    color: #b7a27a;
    opacity: 0;
    font-size: 20px;
    line-height: 1;
    transform: translateX(-14px);
    transition: opacity .4s ease, transform .45s cubic-bezier(.22, .61, .36, 1);
  }

  .project-item:is(:hover, :has(:focus-visible)) .project-content:after {
    opacity: 1;
    transform: none;
  }

  /* Лінія під рядком. Власну рамку не чіпаємо — двопіксельна чорна
     накладається зверху й прокреслюється зліва направо; bottom:-1px, бо
     абсолютні координати рахуються від padding-box, а рамка на 1px
     нижче. Найдовший рух у зв'язці: риска й назва вже стали на місце,
     коли лінія ще дочерчується до правого краю. */
  .project-item:after {
    content: "";
    background-color: #020202;
    height: 2px;
    position: absolute;
    right: 0;
    bottom: -1px;
    left: 0;
    transform: scaleX(0);
    transform-origin: 0 50%;
    transition: transform .55s cubic-bezier(.22, .61, .36, 1);
  }

  .project-item:is(:hover, :has(:focus-visible)):after {
    transform: scaleX(1);
  }
}

/* Кому рух заважає — той самий поділ на два плани, але без анімації:
   стани лишаються, переходи знімаються. */
@media (prefers-reduced-motion: reduce) {
  .project-list > .project-item,
  .project-list > .project-item .project-title,
  .project-item .project-content:before,
  .project-content:after,
  .project-item:after {
    transition: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   Навбар на телефоні: логотип посередині і бургер, що не стрибає
   ═══════════════════════════════════════════════════════════════════

   Тільки ≤479px: лише там у ряду три елементи — іконка телефона, лого,
   бургер. Від 480 до 991 іконка телефона схована, лого стоїть ліворуч, і
   цих проблем немає (заміряно).

   Що було не так. Ряд — flex, а в лого margin-left/right: auto, тобто
   воно центрується не в екрані, а в тому, що лишилось між сусідами. А
   сусіди різні: слот телефона 64px, бургер — 36px (сам лише padding,
   бо іконка в ньому позиціонована абсолютно). Звідси й перекіс: центр
   лого на 214 при центрі екрана 195, іконка телефона за 21px від лівого
   краю, а бургер — за 5px від правого, майже впритул.

   Далі гірше: .menu-button.is-open задає ширину 64px, і в мить відкриття
   меню бургер стає 64 замість 36. Автомаржі перераховуються — і лого
   стрибає на 14px уліво, рівно в центр. Саме це й видно як «відскакує
   до середини».

   Обидва слоти тепер однакові 64x64 у будь-якому стані, а лого — це вся
   ширина свого блоку без відступу зліва. Тоді автомаржі центрують його
   точно в екрані й перерахунок при відкритті нічого не зсуває: 195 при
   центрі екрана 195, і закрито, і відкрито. Іконки при цьому стоять на
   21px і 19px від країв — різниця лише через різну ширину самих значків
   (22 і 26px), обидва центровані в однакових слотах.

   64x64 заразом лікує зону натискання: 36x36 було менше за 44px, які
   вважаються мінімумом для пальця.

   Не регресія: на попередній збірці ті самі 214/195 і той самий стрибок
   на 14px — заміряно. */

@media screen and (max-width: 991px) {
  .menu-button,
  .menu-button.is-open {
    width: 64px;
    height: 64px;
    min-width: 0;
    min-height: 0;
    margin: 0;
    padding: 0;
  }

  /* Обидва варіанти разом: на головній у бренда є .is-current зі своїми
     значеннями (див. коментар вище про w--current), і якби ми правили
     лише один із них, лого на головній стояло б інакше, ніж на решті
     сторінок. */
  .brand,
  .brand.is-current {
    width: auto;
    height: auto;
    padding-left: 0;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   Мобільне меню: пункти по центру, а не на 3px правіше
   ═══════════════════════════════════════════════════════════════════

   Заміряно на 375 і 430px: логотип, контейнер меню й рамка кожного
   пункту стоять рівно в центрі екрана (Δ 0.0), а текст усіх п'яти
   пунктів — на +3.0px правіше. Однакове число на п'яти словах різної
   ширини (53.2…66.5px) означає систематичний зсув, а не те, що кожне
   слово центрується саме по собі: тоді Δ розкидало б довкола нуля.

   Складається з двох доданків. Рядок — flex із justify-content: center,
   тож центрується не рамка, а вміст: padding-left: 16px при
   padding-right: 0 дає центр вмісту на +8.0. А .link-box має
   margin-right: 10px, і центрування рахує групу «текст + маржа», тож
   текст відтягує назад на пів маржі — −5.0. Разом +3.0.

   Ширше 480px те саме, тільки нерівномірно: на 767 «Головна» стоїть на
   +4.5 (у .link.is-current свій padding-left: 9px), а решта на +8.0 —
   тобто перший пункт ще й на 3.5px не збігається з іншими. Тому правило
   охоплює весь діапазон, де меню відкривається бургером, до 991px.

   Один селектор із двох частин, бо padding-left роздано п'яти класам
   пунктів у чотирьох правилах: .header__menu > a (0,1,1) перекриває
   .link і .header__link-* (0,1,0), але програє .link.is-current та
   .header__link-about.is-current (0,2,0) — для них потрібна друга
   частина, .header__menu > a.is-current (0,2,1). Вертикальні відступи
   не чіпаємо: саме вони дають рядку висоту 64px.

   Обнуляємо, а не дзеркалимо: пункт займає всю ширину панелі, вміст у
   ньому центрований, тож симетричний padding дав би рівно те саме, лише
   з зайвим числом у файлі.

   Не наша регресія: на попередній збірці ті самі +3.0. Видимим це стало
   після того, як логотип у шапці став рівно по центру — доти він сам
   стояв на +5.0, і розбіжність між ним і меню була 2px. */

@media screen and (max-width: 991px) {
  .header__menu > a,
  .header__menu > a.is-current {
    padding-left: 0;
    padding-right: 0;
  }

  .header__menu .link-box {
    margin-right: 0;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   Футер: ряд соцмереж
   ═══════════════════════════════════════════════════════════════════

   Було: два рядки «іконка + назва + @нік», кожен зі своїм .link-underline,
   який нікуди не їхав (наведення не міняло ні кольору, ні ширини — звірено
   заміром). Читалось як ще один перелік посилань поряд із «Сторінками» й
   «Контактами», хоча це не перелік, а чотири позначки.

   Стало: рівний ряд із чотирьох іконок, без підписів.

   Іконки inline-SVG, а не <img>, з двох причин. Перша: <img> не
   перефарбувати на наведенні — тільки filter'ом, а це підбір навпомацки.
   Друга: inline-SVG у проєкті вже є (іконка телефона в шапці, стрілки
   кнопок), тож нічого нового не заводимо. Сімейство теж не нове — те саме
   Lucide, що й у шапки: viewBox 24, stroke-width 2, круглі кінці. Через це
   всі чотири мають однакову товщину ліній і однаковий розмір, і стоять в
   одному ряду з монолінійними іконками пошти, телефона й локації вище.

   Розміри: значок 24px у зоні натискання 44x44 — мінімум для пальця.
   Проміжок 8px = --padding-pd-02, той самий крок, яким набрані контактні
   рядки футера.

   margin-left: -10px — оптичне вирівнювання, а не свавілля: значок
   центрований у своїх 44px, тобто його ліва грань на 10px правіше за край
   колонки, і без зсуву ряд стояв би сходинкою до заголовка «Ми в
   соцмережах» і до посилань сусідніх колонок. -10 = (44 - 24) / 2.

   Наведення: колір на бронзу #b7a27a (нею вже набрані заголовки кроків у
   «Чому ми», акцентика переліку питань і риски в переліку категорій) плюс
   підйом на 2px. Тривалість і крива — .25s cubic-bezier(.4, 0, .2, 1), ті
   самі, що в переліку питань. Рухається тільки transform і колір, розкладка
   не змінюється. */

.footer__socials {
  display: flex;
  gap: var(--padding-pd-02);
  margin-top: var(--padding-pd-02);
  margin-left: -10px;
}

.footer__social {
  color: var(--000);
  border-radius: 12px;
  justify-content: center;
  align-items: center;
  width: 44px;
  height: 44px;
  display: flex;
  transition: color .25s cubic-bezier(.4, 0, .2, 1),
              transform .25s cubic-bezier(.4, 0, .2, 1);
}

.footer__social-glyph {
  display: block;
}

@media (hover: hover) {
  .footer__social:hover {
    color: #b7a27a;
    transform: translateY(-2px);
  }
}

/* Клавіатурі — той самий стан плюс видима рамка. Обведення тим самим
   прийомом, що в переліку питань (.faq3-q:focus-visible). */
.footer__social:focus-visible {
  color: #b7a27a;
  outline: 2px solid var(--000);
  outline-offset: 2px;
  transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .footer__social {
    transition: color .25s cubic-bezier(.4, 0, .2, 1);
  }

  .footer__social:hover,
  .footer__social:focus-visible {
    transform: none;
  }
}

/* Примітка під рядом іконок. margin-top: 54px у site.css підбирався під
   старий блок із двох високих рядків; із компактним рядом він лишав під
   іконками діру вдвічі більшу за проміжок над ними. 16px + власний gap
   колонки (16px) дають 32px — рівно стільки ж, скільки між заголовком і
   рядом, тож вузол читається симетрично. */
.footer__note {
  margin-top: var(--padding-pd-04);
}

/* ═══════════════════════════════════════════════════════════════════
   Картки послуг на головній: посилання «Детальніше»
   ═══════════════════════════════════════════════════════════════════

   Було: у картки одна дія — «Зв'язатись», яка відкриває форму. Тобто
   людині, що прочитала опис і хоче подробиць, одразу пропонують писати.
   Середньої сходинки не було, хоч сторінки послуг існують із серпня.

   Кнопку не чіпаємо: це головна дія, і модалки привʼязані до тексту
   заголовка картки через тригер a.service-card-cta.btn[href$="#"]
   (js/service-modals.js). Нове посилання має інший клас і адресу, що
   закінчується слешем, тож під цей селектор не потрапляє — перевірено.

   Стрілка й її рух узяті з наявного прийому сайту: .hero-btn__arrow
   їде на 3px тією ж кривою cubic-bezier(.22, 1, .36, 1). Тут вона
   горизонтальна, бо посилання веде на свою сторінку, а не назовні —
   діагональна ↗ у героя саме про зовнішній перехід. Колір наведення —
   бронза #b7a27a, та сама, що в переліку категорій і в іконках футера. */

.service-card-more {
  color: #090611;
  align-items: center;
  gap: .5rem;
  margin-left: var(--padding-pd-03);
  font-family: Open Sans, sans-serif;
  text-decoration: none;
  transition: color .25s cubic-bezier(.22, 1, .36, 1);
  display: inline-flex;
}

.service-card-more__arrow {
  transition: transform .3s cubic-bezier(.22, 1, .36, 1);
  display: inline-flex;
}

@media (hover: hover) {
  .service-card-more:hover {
    color: #b7a27a;
  }

  .service-card-more:hover .service-card-more__arrow {
    transform: translateX(3px);
  }
}

.service-card-more:focus-visible {
  color: #b7a27a;
  outline: 2px solid #090611;
  outline-offset: 3px;
  border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .service-card-more__arrow {
    transition: none;
  }

  .service-card-more:hover .service-card-more__arrow {
    transform: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   Сторінки послуг: шапка, фото в картках, цифри, кнопка
   ═══════════════════════════════════════════════════════════════════

   Шапка. Заголовок сторінки досі був visually-hidden — існував для
   пошукових систем і не існував для ока, тож сторінка починалась просто
   з абзацу, і людина не бачила назви того, куди прийшла. Тепер він
   видимий, а поруч стоїть зображення.

   Це навмисно НЕ герой на весь екран, як на сторінках обʼєктів. Взято
   композицію, яку сайт уже використовує саме для цієї теми: блок
   «Послуги» на головній — заголовок ліворуч, зображення праворуч на 40%
   ширини, кутик 1.2rem, знебарвлення 20%, що зникає при наведенні. Тобто
   людина, яка клікнула «Послуги», потрапляє на продовження того самого
   блоку, а не в чужий формат. Значення взяті звідти ж
   (.services__columns / .services-header-media / .services-header-image),
   крім висоти: 50svh — це зріст секції на головній, для шапки сторінки
   зависоко, тут 22rem. */

.service-hero {
  /* Ті самі 6rem, що давав .page-header.has-navbar-offset: шапка сайту
     фіксована на 65px, і без цього відступу верх сторінки заїжджає під неї.
     Хлібних крихт тут навмисно немає — їх зняли зі сторінок послуг 24
     серпня (коміт 81850f8) разом із BreadcrumbList, лишивши тільки на
     сторінках обʼєктів. */
  justify-content: space-between;
  align-items: flex-start;
  gap: 4rem;
  margin-top: 6rem;
  display: flex;
}

.service-hero__text {
  flex-direction: column;
  gap: 1rem;
  width: 50%;
  display: flex;
}

/* Заголовок сторінки тепер видимий — і на хабі, і на чотирьох сторінках
   послуг, — тож і масштаб має бути сторінковий, а не секційний: 2rem це
   розмір .section-title, яким набрані заголовки ВСЕРЕДИНІ сторінки. Беремо
   ту саму шкалу, що й решта великих заголовків сайту. Селектор чіпляється
   за відсутність visually-hidden, щоб не зачепити projects.html, де
   заголовок навмисно лишається схованим. */
.page-header__title:not(.visually-hidden) {
  font-size: var(--size--heading-2);
  line-height: 110%;
}

@media screen and (max-width: 991px) {
  .page-header__title:not(.visually-hidden) {
    font-size: var(--size--heading-2-tablet);
  }
}

@media screen and (max-width: 479px) {
  .page-header__title:not(.visually-hidden) {
    font-size: var(--size--heading-2-mobile);
  }
}

/* Волосяна лінія під шапкою сторінки послуги: відділяє вступ від першої
   секції так само, як лінії відділяють рядки переліків. */
.page-header:not(.visually-hidden) + .service-section,
.page-header-row + .service-section {
  border-top: 1px solid var(--color-c1-10);
  margin-top: 3rem;
}

/* Праворуч від заголовка — не картинка, а зміст сторінки: чотири напрями
   з номерами. Це хаб, тобто перехрестя, і найкорисніше, що може стояти в
   його шапці, — перелік того, куди звідси можна піти. Заразом він працює
   як ярлик: не треба гортати чотири високі картки, щоб побачити всі
   чотири назви, а на телефоні вони видно одразу під заголовком.

   Нумерація набрана тим самим регістром, що й номери питань у переліку
   (.faq3-q__no) і кроки на сторінках послуг: 11px, 700, letter-spacing
   1px, табличні цифри. Стрілка й її рух — ті самі, що в «Детальніше» на
   картках послуг на головній. */

.service-index {
  border-top: 1px solid var(--color-c1-10);
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* У шапці хаба перелік ділить рядок із заголовком, тож займає свою колонку;
   на сторінках послуг («Інші напрями») він стоїть сам і йде на всю ширину. */
.service-hero .service-index {
  width: 40%;
}

.service-index__item {
  border-bottom: 1px solid var(--color-c1-10);
}

.service-index__link {
  color: var(--000);
  align-items: baseline;
  gap: 1rem;
  padding: .9rem 0;
  font-family: Open Sans, sans-serif;
  text-decoration: none;
  transition: color .25s cubic-bezier(.22, 1, .36, 1);
  display: flex;
}

.service-index__no {
  color: #09061159;
  letter-spacing: 1px;
  flex: none;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transition: color .25s cubic-bezier(.22, 1, .36, 1);
}

.service-index__name {
  flex: 1;
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 130%;
}

.service-index__arrow {
  color: #09061159;
  flex: none;
  align-self: center;
  transition: transform .3s cubic-bezier(.22, 1, .36, 1), color .25s ease;
}

@media (hover: hover) {
  .service-index__link:hover {
    color: #b7a27a;
  }

  .service-index__link:hover .service-index__no,
  .service-index__link:hover .service-index__arrow {
    color: #b7a27a;
  }

  .service-index__link:hover .service-index__arrow {
    transform: translateX(3px);
  }
}

.service-index__link:focus-visible {
  outline: 2px solid #090611;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .service-index__link:hover .service-index__arrow {
    transform: none;
  }
}

@media screen and (max-width: 991px) {
  .service-hero {
    flex-direction: column;
    align-items: flex-start;
    gap: 2rem;
  }

  .service-hero__text,
  .service-hero .service-index {
    width: 100%;
  }
}

/* Фото в картці. Кожна картка показує обʼєкт, який уже стоїть першим у
   секції «Реалізовані проекти» на сторінці цієї ж послуги, — картка й
   сторінка кажуть про той самий проєкт. Знебарвлення те саме, що в
   шапці, і зникає при наведенні на всю картку, а не на саме зображення:
   картка — одне посилання, тож і реагувати має цілком. */

.service-item__media {
  border-radius: .8rem;
  margin-bottom: 1.25rem;
  overflow: hidden;
}

.service-item__image {
  aspect-ratio: 16 / 10;
  object-fit: cover;
  width: 100%;
  filter: grayscale(20%);
  transition: filter .5s, transform .5s cubic-bezier(.22, 1, .36, 1);
  display: block;
}

@media (hover: hover) {
  .service-item--link:hover .service-item__image {
    filter: grayscale(0%);
    transform: scale(1.03);
  }
}

/* Цифри. Не нові дані: це той самий блок «Чому ми» з головної, дослівно.
   Тут він доречніший, бо саме на цій сторінці людина вирішує, чи має з
   вами справу. Бронза #b7a27a — колір, яким набрані ті самі числа на
   головній (.process-item-title). */

.service-stats {
  grid-template-columns: repeat(4, 1fr);
  gap: 2rem;
  margin: 0;
  padding: 2rem 0 0;
  border-top: 1px solid var(--color-c1-10);
  display: grid;
}

.service-stats__item {
  margin: 0;
}

.service-stats__value {
  color: #b7a27a;
  margin: 0 0 .5rem;
  font-family: Open Sans, sans-serif;
  font-size: 2rem;
  font-weight: 700;
  line-height: 110%;
}

.service-stats__label {
  color: var(--color-c1-75);
  max-width: 22rem;
  margin: 0;
  font-family: Open Sans, sans-serif;
  font-size: .875rem;
  line-height: 150%;
}

@media screen and (max-width: 991px) {
  .service-stats {
    grid-template-columns: 1fr 1fr;
  }
}

@media screen and (max-width: 479px) {
  .service-stats {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }
}

/* Кнопка заклику. .back-link дає їй рамку й радіус, але не дає полів,
   тож вона рендерилась як підкреслений текст у тонкій коробці. Беремо
   вигляд кнопки, який у сайту вже є для світлих секцій — .faq3-a__cta
   у переліку питань: прозорий фон, рамка #090611b3, радіус 19.2px,
   14px/600, і заливка чорним при наведенні. */

.service-cta__btn {
  color: var(--000);
  float: none;
  background-color: #0000;
  border: 1px solid #090611b3;
  border-radius: 19.2px;
  align-items: center;
  gap: .45rem;
  margin-left: 0;
  margin-right: 0;
  padding: .75rem 1.5rem;
  font-family: Open Sans, sans-serif;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: color .25s ease, background-color .25s ease, border-color .25s ease;
  display: inline-flex;
}

@media (hover: hover) {
  .service-cta__btn:hover {
    color: #fff;
    background-color: #090611;
    border-color: #090611;
  }
}

.service-cta__btn:focus-visible {
  outline: 2px solid #090611;
  outline-offset: 3px;
}

/* ═══════════════════════════════════════════════════════════════════
   Шапка сторінки послуги: фото обʼєкта під заголовком
   ═══════════════════════════════════════════════════════════════════

   Смуга на всю ширину: фото проєкту, затемнення, білий текст. Обʼєкт не
   випадковий — це той самий проєкт, який стоїть першим у секції
   «Реалізовані проекти» на цій же сторінці, тобто шапка показує роботу,
   зроблену саме цією послугою.

   Затемнення d9 (85%) — не на око. Заміряно так само, як для банера
   заклику: сторінка малюється з прозорим текстом, зчитуються справжні
   пікселі під заголовком і підзаголовком. На всіх чотирьох сторінках і на
   обох ширинах найгірша точка дає 10.95:1 при порозі AA 4.5 — тобто одного
   значення вистачає, окремого мобільного не треба. Я очікував, що на
   телефоні доведеться темнити глибше; замір показав, що ні.

   Відступи, на відміну від затемнення, побрейкпоінтні: 9rem зверху — це
   місце під фіксовану шапку плюс повітря на десктопі, на телефоні стільки
   не потрібно. */

.service-headband {
  background-color: #0f1b22;
  background-position: 0 0, 50% 40%;
  background-size: auto, cover;
  background-repeat: no-repeat, no-repeat;
  width: 100%;
}

.service-headband .page-header.has-navbar-offset {
  margin-top: 0;
  padding-top: 9rem;
  padding-bottom: 5rem;
}

.service-headband .page-header__title,
.service-headband .page-header__subtitle {
  color: #fff;
}

.service-headband + .service-section {
  margin-top: 3rem;
}

.service-headband--generalne {
  background-image: linear-gradient(#0f1b22d9, #0f1b22d9), url("../images/img-84402.webp");
}

.service-headband--optymizatsiya {
  background-image: linear-gradient(#0f1b22d9, #0f1b22d9), url("../images/img-94319-jpg.webp");
}

.service-headband--efektyvni {
  background-image: linear-gradient(#0f1b22d9, #0f1b22d9), url("../images/img-2-1-1-1-1.webp");
}

.service-headband--optymalne {
  background-image: linear-gradient(#0f1b22d9, #0f1b22d9), url("../images/bar-6.webp");
}

@media screen and (max-width: 991px) {
  .service-headband .page-header.has-navbar-offset {
    padding-top: 7rem;
    padding-bottom: 3.5rem;
  }
}



/* Хаб теж отримує смугу. Тут під нею перелік напрямів, тож його волосяні
   лінії й номери переводяться на світлі: 10% чорного на темному фото не
   видно. Знімок міський, з висоти — сторінка про всі чотири напрями
   одразу, і жоден окремий обʼєкт її б не представляв. */
.service-headband--hub {
  background-image: linear-gradient(#0f1b22d9, #0f1b22d9), url("../images/duc-1.jpg");
}

.service-headband .service-hero {
  margin-top: 0;
  padding-top: 9rem;
  padding-bottom: 5rem;
}

.service-headband .page-header__title,
.service-headband .page-header__subtitle,
.service-headband .service-index__link {
  color: #fff;
}

.service-headband .service-index,
.service-headband .service-index__item {
  border-color: #ffffff33;
}

.service-headband .service-index__no,
.service-headband .service-index__arrow {
  color: #ffffff8c;
}

/* ═══════════════════════════════════════════════════════════════════
   Сторінки послуг: поля від краю екрана
   ═══════════════════════════════════════════════════════════════════

   Заміряно: на 768 і 1024 текст стояв рівно за 0px від краю. Причина в
   тому, що .page-header-row має лише max-width: 74rem, а .section__container
   отримує бічні поля тільки нижче 768px (1rem) і має два разові правила
   рівно на 768 і 1112. Між 768 і 1184 не лишалось нічого, а на 1184+
   поля дає вже max-width, тобто випадково.

   Тепер поле задане явно й однакове на всіх ширинах: 2rem від 480px і
   вище, 20px на телефоні. На телефоні 10 з тих 20 дає власний відступ
   .sections (margin: 0 10px нижче 480px), тож усередині додаємо 10 —
   в сумі та сама відстань, що й від 480. Смуга-шапка при цьому лишається
   на всю ширину, а відступ отримує текст усередині неї. */

.page-header-row,
.service-section .section__container {
  padding-left: 2rem;
  padding-right: 2rem;
}

/* .page-header-row сама несе max-width: 74rem, тож поле додалось би до
   центрувальних полів і на 1280 текст стояв би за 80px замість 48. Даємо
   їй ширину «колонка + два поля», щоб сама колонка лишалась 74rem, як у
   секцій нижче, а поле працювало тільки там, де колонка ще не впирається
   в межу. */
.page-header-row {
  max-width: calc(74rem + 4rem);
}

@media screen and (max-width: 479px) {
  .page-header-row,
  .service-section .section__container {
    padding-left: 10px;
    padding-right: 10px;
  }
}

/* Телефон у шапці між 480 і 991. Заміряно: нижче 992px кнопку з номером
   ховають три правила (991, 767, 479), а іконку телефона показує лише
   правило нижче 480px — тобто в цьому проміжку в шапці немає ні номера,
   ні іконки взагалі.

   Кнопку з номером тут пробували і прибрали: ряд шапки йде на
   space-between, і навіть з margin-left: auto кнопка не притискається до
   бургера — заміряно на 768px, auto забирає 182px, а між кнопкою і
   бургером лишається ще 166px, які розподіляє space-between. Номер
   зависає в порожнечі посеред ряду. Тож на всю ширину 480–991 показуємо
   ту саму компактну іконку, що й на телефоні: три елементи в ряду,
   логотип посередині, нічого не висить. */
@media screen and (min-width: 480px) and (max-width: 991px) {
  /* Колір задаємо явно. Нижче 480px іконка стає чорною через
     filter: grayscale() на .code-embed — це знебарвлює синій колір
     посилання. Тут робимо це чесно: svg намальований stroke="currentColor",
     тож достатньо кольору на самому посиланні. */
  .header__phone-icon {
    color: #090611;
    justify-content: center;
    align-items: center;
    width: 64px;
    height: 64px;
    display: flex;
  }

  .header__phone-icon .code-embed {
    justify-content: center;
    align-items: center;
    width: 22px;
    display: flex;
  }
}

/* Смуга на телефоні максимально низька, наскільки дозволяє фіксована шапка
   (65px) і повітря під нею. Це не косметика: чим вища смуга при тій самій
   ширині, тим вужчий шматок кадру лишається видимим. Заміряно на 390px —
   при 6.5/3rem від фото було видно 46–60% ширини.

   Правило стоїть у кінці файлу навмисно: у хаба своє оголошення
   .service-headband .service-hero з такою самою вагою, і перекрити його
   можна тільки порядком. */
@media screen and (max-width: 479px) {
  .service-headband .page-header.has-navbar-offset,
  .service-headband .service-hero {
    padding-top: 5.5rem;
    padding-bottom: 2rem;
  }

  /* Рядки переліку на хабі теж трохи щільніші — кожні 4 рядки по 0.2rem
     це ще 13px висоти смуги, тобто ще шматок кадру. */
  .service-headband .service-index__link {
    padding-top: .7rem;
    padding-bottom: .7rem;
  }
}

/* Смуга-шапка на телефоні йде від краю до краю.
   Заважав власний відступ .sections — нижче 480px вона має margin: 0 10px,
   і смуга, лежачи всередині, лишала по 10px білого з боків. Відʼємним
   відступом це не обійти: там-таки стоїть overflow: hidden, який його
   обріже. Тому знімаємо сам відступ, але тільки на сторінках, де смуга є —
   :has() дає це зробити без окремого класу на <body>. Ті 10px, які
   .sections давала тексту, одразу повертаються полями всередині, щоб
   відстань до краю лишилась тією самою — 20px. */
@media screen and (max-width: 479px) {
  main.sections:has(.service-headband) {
    margin-left: 0;
    margin-right: 0;
  }

  main.sections:has(.service-headband) .page-header-row,
  main.sections:has(.service-headband) .service-section .section__container {
    padding-left: 20px;
    padding-right: 20px;
  }
}
