@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(/fonts/jetbrains-mono-400-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(/fonts/jetbrains-mono-400-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url(/fonts/jetbrains-mono-500-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url(/fonts/jetbrains-mono-500-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 300; font-display: swap; src: url(/fonts/onest-300-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 300; font-display: swap; src: url(/fonts/onest-300-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 400; font-display: swap; src: url(/fonts/onest-400-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 400; font-display: swap; src: url(/fonts/onest-400-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 500; font-display: swap; src: url(/fonts/onest-500-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 500; font-display: swap; src: url(/fonts/onest-500-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 600; font-display: swap; src: url(/fonts/onest-600-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 600; font-display: swap; src: url(/fonts/onest-600-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 700; font-display: swap; src: url(/fonts/onest-700-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 700; font-display: swap; src: url(/fonts/onest-700-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 800; font-display: swap; src: url(/fonts/onest-800-cyrillic.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 800; font-display: swap; src: url(/fonts/onest-800-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


  /* jsdom при сборке статической версии портит clamp() внутри calc(): значение
     calc(90svh - var(--hdr-h)) превращается в мусор, браузер его отбрасывает,
     и первый экран рисуется другой высоты, а после запуска приложения скачет.
     Выносим такие размеры в переменные — в calc() остаётся var(), который переживает
     сериализацию без потерь. */
  :root { --hdr-h: clamp(62px,7vw,76px); --half-1: clamp(46px,4.6vw,60px); --half-2: clamp(44px,4.4vw,60px); }
  * { box-sizing: border-box; }
  html { scroll-behavior: smooth; }
  body { margin: 0; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; background: #FBFBF9; }
  ::selection { background: #16171B; color: #FBFBF9; }
  @keyframes stmk-ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
  @keyframes stmk-up { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: translateY(0); } }
  @keyframes stmk-rise { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
  @keyframes stmk-line { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  @keyframes stmk-drift { from { transform: translateX(-3%); } to { transform: translateX(3%); } }
  @keyframes stmk-zoom-in { from { transform: scale(1.16); } to { transform: scale(1); } }
  @keyframes stmk-scan-y { 0% { transform: translateY(-14vh); opacity: 0; } 12% { opacity: .8; } 88% { opacity: .8; } 100% { transform: translateY(100vh); opacity: 0; } }
  @keyframes stmk-wipe-l { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
  @keyframes stmk-wipe-r { from { clip-path: inset(0 0 0 100%); } to { clip-path: inset(0 0 0 0); } }
  @keyframes stmk-divider-draw { from { transform: scaleY(0); } to { transform: scaleY(1); } }
  @keyframes stmk-node-in { from { opacity: 0; transform: translate(-50%,-50%) scale(0.7); } to { opacity: 1; transform: translate(-50%,-50%) scale(1); } }
  @keyframes stmk-fade { from { opacity: 0; } to { opacity: 1; } }
  @keyframes stmk-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 0.1; } }
  @keyframes stmk-dash-flow { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -7.2; } }
  @keyframes stmk-choice-float { 0%,100%,60% { transform: translateY(0); } 68% { transform: translateY(22px); } 75% { transform: translateY(-4px); } 83% { transform: translateY(12px); } 90% { transform: translateY(0); } }
  @keyframes stmk-indicator-nudge { 0%,100%,60% { transform: translateY(0); } 68% { transform: translateY(-22px); } 75% { transform: translateY(4px); } 83% { transform: translateY(-12px); } 90% { transform: translateY(0); } }
  .gp-node { position:absolute; transform:translate(-50%,-50%); border-radius:50%; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; cursor:pointer; padding:6px; z-index:4; width:clamp(58px,9vw,88px); height:clamp(58px,9vw,88px); background:rgba(246,243,236,0.94); color:#14151A; border:1px solid rgba(255,255,255,0.25); box-shadow:0 6px 18px rgba(0,0,0,0.35); transition:width .28s cubic-bezier(.4,0,.2,1), height .28s cubic-bezier(.4,0,.2,1), background .28s, box-shadow .28s; }
  .gp-node.on { width:clamp(74px,11.5vw,108px); height:clamp(74px,11.5vw,108px); background:#B0542B; color:#F6F3EC; border-color:#B0542B; box-shadow:0 10px 30px rgba(176,84,43,0.42), 0 0 0 5px rgba(176,84,43,0.16); z-index:5; }
  .gp-node:hover { box-shadow:0 12px 32px rgba(0,0,0,0.4); }
  /* ===== GP 3D Control Model — live motion (GPU-composited: transform/opacity only) ===== */
  @keyframes gp3d-floatZ { 0%,100% { transform: translateZ(0); } 50% { transform: translateZ(7px); } }
  @keyframes gp3d-breathe { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
  @keyframes gp3d-sweep { from { transform: translate(-50%,-50%) translateZ(2px) rotate(0deg); } to { transform: translate(-50%,-50%) translateZ(2px) rotate(360deg); } }
  @keyframes gp3d-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
  @keyframes gp3d-spin-rev { from { transform: rotate(360deg); } to { transform: rotate(0deg); } }
  @keyframes gp3d-pedestal { 0%,100% { transform: translate(-50%,-50%) translateZ(1px) scale(1); opacity:.95; } 50% { transform: translate(-50%,-50%) translateZ(1px) scale(1.25); opacity:.5; } }
  @keyframes gp3d-ping {
    0%   { opacity: 0;   transform: translateZ(2px) scale(0.35); }
    2.5% { opacity: .85; }
    14%  { opacity: 0;   transform: translateZ(2px) scale(2.4); }
    100% { opacity: 0;   transform: translateZ(2px) scale(2.4); }
  }
  .gp3d-anim { will-change: transform, opacity; }
  /* An element that must keep its children in 3D may NOT hint a grouping property.
     Per the will-change spec a browser may treat `will-change: opacity` as though opacity
     were non-initial, and per CSS Transforms L2 a non-initial grouping property forces
     `transform-style` to compute to `flat` — which collapses the extruded cube faces into
     their top face. Chromium does exactly that; WebKit does not, so Safari looked correct
     while Chrome rendered the nodes flat. These wrappers only ever animate `transform`,
     so the opacity hint was useless anyway. Compound selector so it wins on specificity
     (0,2,0) regardless of source order — do not "simplify" it back to a single class. */
  .gp3d-anim.gp3d-solid { will-change: transform; }
  /* Rotating an SVG group about a point: a length-based `transform-origin` is resolved against
     whatever reference box the engine defaults `transform-box` to, and engines disagree — which
     is why these read correctly in one browser and orbit off-centre in another. Pin it instead.
     Only safe on groups whose OWN geometry box is already centred on the intended pivot (each of
     these holds concentric circles), so fill-box centre == the pivot. A group holding a single
     off-centre mark must NOT use this — rotate an HTML wrapper for that case. */
  .gp-spin-c { transform-box: fill-box; transform-origin: center; }

  /* ===== Developer credit in the footer =====
     The brief: present but never loud. At rest the mark is desaturated and dimmed to exactly the
     tone of the mono line it sits next to, so it reads as part of the footer's quiet bottom rule.
     On hover (or keyboard focus) it returns to full brand colour — white wordmark with the blue
     accent — which is what makes it noticeable without it ever competing with СТМК's own logo. */
  /* Font, size, letter-spacing and colour are inherited from the footer's bottom rule so the
     credit is indistinguishable from the mono items beside it — only the mark itself is styled. */
  /* ===== BUILT index — direction rows =====
     The photo stays hidden until the row is hovered, so the page reads as a quiet editorial index
     first and only reveals imagery on intent. Nothing moves on load. */
  .built-row .built-row-bg, .built-row .built-row-veil, .built-row .built-row-arrow {
    transition: opacity .5s cubic-bezier(.4,0,.2,1), transform .45s cubic-bezier(.4,0,.2,1); }
  .built-row:hover .built-row-bg { opacity:.30; }
  .built-row:hover .built-row-veil { opacity:1; }
  .built-row:hover .built-row-arrow { transform: translateX(8px); }

  /* ===== CITY — the drawing builds itself =====
     Setting-out axes first, then the outline traced by a moving pen, then slabs bottom-up,
     mullions, entrance, glazing, level marks and the dimension chains — the order a real sheet
     is drawn in. It retracts and repeats, which is the point: the section is still being drawn.
     Every step is stroke-dashoffset, opacity, transform or offset-distance, so the whole thing
     stays on the compositor.
     All the steps share one 13s period and differ only in delay, which is what keeps them in
     phase: with equal durations the offsets between them stay constant for every repeat. */
  @keyframes cty-592 { 0%{stroke-dashoffset:592} 12%{stroke-dashoffset:0} 86%{stroke-dashoffset:0} 95%{stroke-dashoffset:592} 100%{stroke-dashoffset:592} }
  @keyframes cty-180 { 0%{stroke-dashoffset:180} 7%{stroke-dashoffset:0}  86%{stroke-dashoffset:0} 93%{stroke-dashoffset:180} 100%{stroke-dashoffset:180} }
  @keyframes cty-178 { 0%{stroke-dashoffset:178} 7%{stroke-dashoffset:0}  86%{stroke-dashoffset:0} 93%{stroke-dashoffset:178} 100%{stroke-dashoffset:178} }
  @keyframes cty-162 { 0%{stroke-dashoffset:162} 7%{stroke-dashoffset:0}  86%{stroke-dashoffset:0} 93%{stroke-dashoffset:162} 100%{stroke-dashoffset:162} }
  @keyframes cty-fade { 0%{opacity:0} 8%{opacity:1} 86%{opacity:1} 94%{opacity:0} 100%{opacity:0} }
  @keyframes cty-scan { 0%{transform:translateY(0); opacity:0} 4%{opacity:.9} 34%{transform:translateY(-178px); opacity:0} 100%{transform:translateY(-178px); opacity:0} }
  /* The pen rides the outline path itself, on exactly the outline's duration, delay and easing,
     so the dot always sits on the tip of the line being drawn. */
  @keyframes cty-pen  { 0%{offset-distance:0%; opacity:0} 1%{opacity:1} 12%{offset-distance:100%; opacity:1} 14%{offset-distance:100%; opacity:0} 100%{offset-distance:100%; opacity:0} }

  .cty-axes  { animation: cty-fade 13s ease-out  .1s infinite both; }
  .cty-bub   { animation: cty-fade 13s ease-out  .6s infinite both; }
  .cty-outline { animation: cty-592 13s cubic-bezier(.5,0,.3,1) 1.1s infinite both; }
  .cty-pen   { offset-path: path('M120 232 V91 H114 V87 H138 V58 H130 V54 H290 V58 H282 V87 H306 V91 H300 V232');
               offset-rotate: 0deg;
               animation: cty-pen 13s cubic-bezier(.5,0,.3,1) 1.1s infinite both; }
  .cty-course{ animation: cty-fade 13s ease-out 2.7s infinite both; }
  .cty-f1 { animation: cty-180 13s ease-out 3.1s infinite both; }
  .cty-f2 { animation: cty-180 13s ease-out 3.5s infinite both; }
  .cty-f3 { animation: cty-180 13s ease-out 3.9s infinite both; }
  .cty-m   { animation: cty-162 13s ease-out 4.4s infinite both; }
  .cty-ent { animation: cty-162 13s ease-out 5.0s infinite both; }
  .cty-glass { animation: cty-fade 13s ease-out 5.4s infinite both; }
  .cty-lev   { animation: cty-178 13s ease-out 5.9s infinite both; }
  .cty-levlab{ animation: cty-fade 13s ease-out 6.3s infinite both; }
  .cty-chain { animation: cty-fade 13s ease-out 6.8s infinite both; }
  .cty-dim   { animation: cty-fade 13s ease-out 7.2s infinite both; }
  .cty-scan  { animation: cty-scan 13s cubic-bezier(.45,0,.3,1) 7.8s infinite both; }
  /* The sheet scales as one piece, so on a phone 7 user units land at ~6px and the 5-unit bay
     figures at ~4.5px — texture, not text. Below 620px the finest layer is dropped and the rest
     is stepped up, which keeps the drawing readable instead of merely decorative. */
  @media (max-width: 620px) {
    .cty-chain { display: none; }
    .cty-lab-lev, .cty-lab-dim { font-size: 9px; }
    .cty-lab-ax { font-size: 8.5px; }
  }




  /* ---- Экспертиза: альбом чертежей ----------------------------------------------------------
     Пять листов лежат стопкой в общем 3D-пространстве. Открытый лежит плашмя, пролистанные
     откинуты влево на петле по левому краю, следующие подложены со сдвигом вниз и в глубину.
     transform-origin по левому краю — иначе лист поворачивается вокруг центра и уходит
     сквозь стопку, а не переворачивается как страница. ---- */
  /* ---- Заглушка раздела «03»: лист в работе -------------------------------------------
     По листу идёт головка плоттера, и ровно за ней проявляется то, мимо чего она прошла:
     сначала рамка и сетка, потом оси, разрез, штриховка грунта, размерная цепочка и в
     конце штамп. Поле «ЛИСТ» остаётся пустым с мигающим курсором — это и есть сообщение
     «чертёж ещё не готов», сказанное не словами. Цикл 16 с.
     Всё на CSS: ни таймеров, ни скриптов, поэтому блок ничего не стоит и работает в
     предрендере. */
  @keyframes dws-stage { 0%{opacity:0} 3%{opacity:1} 88%{opacity:1} 96%,100%{opacity:0} }
  @keyframes dws-head  { 0%,6%{opacity:0; transform:translateX(0)} 9%{opacity:1}
                         62%{opacity:1; transform:translateX(1112px)}
                         67%,100%{opacity:0; transform:translateX(1112px)} }
  @keyframes dws-d1 { 0%,2%{stroke-dashoffset:100} 10%,96%{stroke-dashoffset:0} 100%{stroke-dashoffset:100} }
  @keyframes dws-d3 { 0%,10%{stroke-dashoffset:100} 24%,96%{stroke-dashoffset:0} 100%{stroke-dashoffset:100} }
  @keyframes dws-d4 { 0%,16%{stroke-dashoffset:100} 34%,96%{stroke-dashoffset:0} 100%{stroke-dashoffset:100} }
  @keyframes dws-d5 { 0%,30%{stroke-dashoffset:100} 40%,96%{stroke-dashoffset:0} 100%{stroke-dashoffset:100} }
  @keyframes dws-d6 { 0%,38%{stroke-dashoffset:100} 50%,96%{stroke-dashoffset:0} 100%{stroke-dashoffset:100} }
  @keyframes dws-d7 { 0%,46%{stroke-dashoffset:100} 58%,96%{stroke-dashoffset:0} 100%{stroke-dashoffset:100} }
  @keyframes dws-f2 { 0%,6%{opacity:0} 14%,90%{opacity:0.5} 96%,100%{opacity:0} }
  @keyframes dws-f3 { 0%,20%{opacity:0} 26%,90%{opacity:1} 96%,100%{opacity:0} }
  @keyframes dws-f6 { 0%,44%{opacity:0} 50%,90%{opacity:1} 96%,100%{opacity:0} }
  @keyframes dws-f7 { 0%,54%{opacity:0} 60%,90%{opacity:1} 96%,100%{opacity:0} }
  @keyframes dws-caret { 0%,58%{opacity:0} 59%{opacity:1} 64%{opacity:1} 65%{opacity:0}
                         70%{opacity:0} 71%{opacity:1} 76%{opacity:1} 77%{opacity:0}
                         82%{opacity:0} 83%{opacity:1} 88%{opacity:1} 89%,100%{opacity:0} }

  .dws-stage { animation: dws-stage 16s linear infinite both; }
  .dws-head  { transform-box: view-box; animation: dws-head 16s cubic-bezier(.45,0,.25,1) infinite both; }
  .dws-d1, .dws-d3, .dws-d4, .dws-d5, .dws-d6, .dws-d7 { stroke-dasharray: 100; stroke-dashoffset: 100; }
  .dws-d1 { animation: dws-d1 16s cubic-bezier(.4,0,.2,1) infinite both; }
  .dws-d3 { animation: dws-d3 16s cubic-bezier(.4,0,.2,1) infinite both; }
  .dws-d4 { animation: dws-d4 16s cubic-bezier(.4,0,.2,1) infinite both; }
  .dws-d5 { animation: dws-d5 16s cubic-bezier(.4,0,.2,1) infinite both; }
  .dws-d6 { animation: dws-d6 16s cubic-bezier(.4,0,.2,1) infinite both; }
  .dws-d7 { animation: dws-d7 16s cubic-bezier(.4,0,.2,1) infinite both; }
  .dws-f2 { animation: dws-f2 16s linear infinite both; }
  .dws-f3 { animation: dws-f3 16s linear infinite both; }
  .dws-f6 { animation: dws-f6 16s linear infinite both; }
  .dws-f7 { animation: dws-f7 16s linear infinite both; }
  .dws-caret { animation: dws-caret 16s steps(1,end) infinite both; }

  @media (prefers-reduced-motion: reduce) {
    /* Без движения лист показывается сразу собранным, а головка плоттера убирается. */
    .dws-stage, .dws-d1, .dws-d3, .dws-d4, .dws-d5, .dws-d6, .dws-d7,
    .dws-f2, .dws-f3, .dws-f6, .dws-f7, .dws-caret {
      animation: none !important; stroke-dashoffset: 0 !important; opacity: 1 !important; }
    .dws-f2 { opacity: 0.5 !important; }
    .dws-head { animation: none !important; opacity: 0 !important; }
  }

  /* Контактные ссылки не должны выглядеть как обычные синие ссылки: цвет наследуется,
     подчёркивание появляется только при наведении. */
  /* Слайдер проектной документации. Подложка — та же сетка 28×28, что и в слотах
     на страницах услуг: лист лежит на ней, как на подрезной доске. */
  .doc-stage { position:relative; background:var(--paper2); border:1px solid var(--line);
    border-radius:2px; padding:clamp(14px,2vw,26px); display:flex; align-items:center;
    justify-content:center; min-height:clamp(260px,42vw,560px);
    background-image:linear-gradient(rgba(22,23,27,0.03) 1px,transparent 1px),
                     linear-gradient(90deg,rgba(22,23,27,0.03) 1px,transparent 1px);
    background-size:28px 28px; }
  .doc-stage img { max-width:100%; max-height:clamp(240px,40vw,540px); width:auto; height:auto;
    display:block; background:#FCFCFA; box-shadow:0 2px 10px rgba(22,23,27,0.10), 0 1px 2px rgba(22,23,27,0.08); }
  /* Смена листа. Тонкая линия проходит по кадру, как головка плоттера, и лист
     проявляется следом за ней — коротко, 0,42 с, без отскоков и теней в движении.

     Пары -a / -b одинаковы намеренно. Элемент при листании остаётся тем же, меняется
     только адрес картинки, и если имя анимации не меняется, браузер считает, что
     перезапускать нечего: анимация так и висит в состоянии finished. Поэтому классы
     чередуются, а вместе с ними чередуется и имя — только тогда она играет заново. */
  @keyframes doc-in-a { from { opacity:0; transform:translate3d(var(--dx,20px),0,0) }
                        to   { opacity:1; transform:none } }
  @keyframes doc-in-b { from { opacity:0; transform:translate3d(var(--dx,20px),0,0) }
                        to   { opacity:1; transform:none } }
  @keyframes doc-scan-a { 0%{transform:translateX(var(--x0,-2%)); opacity:0} 14%{opacity:.85}
                          86%{opacity:.85} 100%{transform:translateX(var(--x1,102%)); opacity:0} }
  @keyframes doc-scan-b { 0%{transform:translateX(var(--x0,-2%)); opacity:0} 14%{opacity:.85}
                          86%{opacity:.85} 100%{transform:translateX(var(--x1,102%)); opacity:0} }
  .doc-sheet { will-change:opacity, transform; }
  .doc-sheet.sh-a { animation:doc-in-a .42s cubic-bezier(.22,.61,.36,1) both; }
  .doc-sheet.sh-b { animation:doc-in-b .42s cubic-bezier(.22,.61,.36,1) both; }
  .doc-sheet.rev { --dx:-20px; }
  .doc-scan { position:absolute; top:0; bottom:0; left:0; width:1px; opacity:0; z-index:2;
    background:linear-gradient(to bottom, transparent, var(--accent,#B0542B) 12%,
               var(--accent,#B0542B) 88%, transparent); pointer-events:none; }
  .doc-scan.sh-a { animation:doc-scan-a .5s cubic-bezier(.4,0,.2,1) both; }
  .doc-scan.sh-b { animation:doc-scan-b .5s cubic-bezier(.4,0,.2,1) both; }
  .doc-scan.rev { --x0:102%; --x1:-2%; }
  @media (prefers-reduced-motion: reduce) {
    .doc-sheet.sh-a, .doc-sheet.sh-b { animation:none; }
    .doc-scan { display:none; }
  }

  .doc-more { text-align:center; max-width:44ch; }
  .doc-bar { display:flex; align-items:center; gap:clamp(12px,1.6vw,22px); flex-wrap:wrap;
    margin-top:clamp(12px,1.6vw,18px); font-family:'JetBrains Mono',monospace; font-size:11px;
    letter-spacing:0.1em; color:var(--mut); text-transform:uppercase; }
  .doc-nav { width:36px; height:36px; border:1px solid var(--line); background:var(--paper);
    display:inline-flex; align-items:center; justify-content:center; cursor:pointer;
    font:inherit; font-size:15px; color:var(--ink); padding:0; border-radius:0; appearance:none; user-select:none; transition:border-color .2s, background .2s; }
  .doc-nav:hover { border-color:var(--ink); background:var(--paper2); }
  .doc-dl { display:inline-flex; align-items:center; gap:9px; margin-left:auto;
    font-family:'Onest',sans-serif; font-size:14px; letter-spacing:0; text-transform:none;
    font-weight:500; color:var(--paper); background:var(--ink); border:none;
    padding:11px 20px; border-radius:2px; text-decoration:none; }
  .doc-dl:hover { opacity:.88; }

  .ct-link { color: inherit; text-decoration: none; border-bottom: 1px solid transparent;
             transition: border-color .2s ease, opacity .2s ease; }
  .ct-link:hover { border-bottom-color: currentColor; }
  .ct-link:focus-visible { outline: 2px solid var(--accent,#B0542B); outline-offset: 3px; }

  .alb-host { perspective: 2200px; perspective-origin: 42% 44%; position: relative;
              overflow: hidden; isolation: isolate; }
  .alb-stack { position: absolute; inset: 8% 10%; transform-style: preserve-3d;
               transform: rotateX(12deg) rotateZ(-2deg); }
  .alb-sheet { position: absolute; inset: 0; background: #FCFCFA;
               border: 1px solid rgba(22,23,27,0.18); transform-origin: left center;
               backface-visibility: hidden; box-shadow: 0 18px 40px -28px rgba(22,23,27,0.5);
               transition: transform .72s cubic-bezier(.36,0,.14,1), opacity .5s ease; }
  .alb-sheet svg { width: 100%; height: 100%; display: block; }
  /* Штамп в правом нижнем углу — как на настоящем листе. */
  .alb-stamp { position: absolute; right: 0; bottom: 0; display: flex;
               border-top: 1px solid rgba(22,23,27,0.4); border-left: 1px solid rgba(22,23,27,0.4);
               font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: .08em;
               color: rgba(22,23,27,0.62); }
  .alb-stamp span { padding: 6px 10px; border-left: 1px solid rgba(22,23,27,0.18); }
  .alb-stamp span:first-child { border-left: 0; }

  @media (prefers-reduced-motion: reduce) { .alb-sheet { transition: none !important; } }

  /* ---- Схемы в статьях раздела «Экспертиза» ----------------------------------------------
     Каждая статья получает две схемы, объясняющие ровно то, о чём в ней речь. Все они
     построены на одном приёме: обводка вычерчивается через stroke-dashoffset, содержимое
     проступает по opacity, движение — только через transform. Никакой геометрии в JS.
     Все штрихуемые пути объявляют pathLength="100", поэтому один кейфрейм управляет
     линиями любой длины. Раскадровка внутри кейфреймов, а не в animation-delay: иначе
     слои гаснут вразнобой и картинка разваливается на последней секунде цикла. ---- */
  @keyframes fig-draw { 0%{stroke-dashoffset:100} 18%{stroke-dashoffset:0} 86%{stroke-dashoffset:0} 95%{stroke-dashoffset:100} 100%{stroke-dashoffset:100} }
  @keyframes fig-in-1 { 0%,16%{opacity:0} 24%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  @keyframes fig-in-2 { 0%,26%{opacity:0} 34%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  @keyframes fig-in-3 { 0%,36%{opacity:0} 44%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  @keyframes fig-in-4 { 0%,46%{opacity:0} 54%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  @keyframes fig-in-5 { 0%,56%{opacity:0} 64%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  .fig-draw { animation: fig-draw 14s cubic-bezier(.32,0,.16,1) infinite both; }
  .fig-1 { animation: fig-in-1 14s cubic-bezier(.32,0,.16,1) infinite both; }
  .fig-2 { animation: fig-in-2 14s cubic-bezier(.32,0,.16,1) infinite both; }
  .fig-3 { animation: fig-in-3 14s cubic-bezier(.32,0,.16,1) infinite both; }
  .fig-4 { animation: fig-in-4 14s cubic-bezier(.32,0,.16,1) infinite both; }
  .fig-5 { animation: fig-in-5 14s cubic-bezier(.32,0,.16,1) infinite both; }

  /* Коллизия: воздуховод наезжает на балку, пересечение подсвечивается, затем труба
     уходит вниз и отметка сменяется галочкой — то есть ошибка найдена и исправлена. */
  @keyframes fig-duct  { 0%,12%{transform:translate(-60px,0)} 26%{transform:translate(0,0)} 62%{transform:translate(0,0)} 74%{transform:translate(0,26px)} 100%{transform:translate(0,26px)} }
  @keyframes fig-clash { 0%,28%{opacity:0} 34%{opacity:1} 60%{opacity:1} 66%{opacity:0} 100%{opacity:0} }
  @keyframes fig-fixed { 0%,70%{opacity:0} 78%{opacity:1} 92%{opacity:1} 97%{opacity:0} 100%{opacity:0} }
  .fig-duct  { animation: fig-duct  14s cubic-bezier(.5,0,.2,1) infinite both; }
  .fig-clash { animation: fig-clash 14s ease-out infinite both; }
  .fig-fixed { animation: fig-fixed 14s ease-out infinite both; }

  /* Шкала LOD: бегунок идёт по шкале, элемент рядом набирает детализацию ступенями. */
  @keyframes fig-slider { 0%,8%{transform:translateX(0)} 26%{transform:translateX(150px)} 46%{transform:translateX(300px)} 66%{transform:translateX(450px)} 88%{transform:translateX(450px)} 100%{transform:translateX(450px)} }
  .fig-slider { animation: fig-slider 14s cubic-bezier(.45,0,.25,1) infinite both; }

  /* Модель первична: дверь двигают на модели — и она едет на всех трёх проекциях разом. */
  @keyframes fig-door { 0%,22%{transform:translateX(0)} 40%{transform:translateX(52px)} 74%{transform:translateX(52px)} 88%{transform:translateX(0)} 100%{transform:translateX(0)} }
  .fig-door { animation: fig-door 14s cubic-bezier(.4,0,.2,1) infinite both; }

  /* Инструменты: узлы загораются по очереди вокруг общей модели. */
  @keyframes fig-node { 0%,100%{opacity:.28} 8%{opacity:1} 22%{opacity:.28} }
  .fig-node { animation: fig-node 14s ease-in-out infinite both; }

  .fig-wrap { margin:clamp(30px,3.4vw,44px) 0 0; border:1px solid var(--line); border-radius:2px;
              background:var(--paper2); padding:clamp(16px,2vw,26px); }
  .fig-wrap svg { width:100%; height:auto; display:block; }
  .fig-cap { font-family:'JetBrains Mono',monospace; font-size:10.5px; letter-spacing:0.1em;
             text-transform:uppercase; color:var(--mut); margin-top:12px; }

  @media (prefers-reduced-motion: reduce) {
    /* Схемы остаются читаемыми: всё вычерчено, ничего не движется. */
    .fig-draw, .fig-1, .fig-2, .fig-3, .fig-4, .fig-5, .fig-clash {
      animation:none !important; stroke-dashoffset:0 !important; opacity:1 !important; }
    .fig-duct, .fig-slider, .fig-door, .fig-node { animation:none !important; transform:none !important; opacity:1 !important; }
    .fig-fixed { animation:none !important; opacity:0 !important; }
  }

  /* ---- Contacts: setting out the datum --------------------------------------------------
     Replaces the contact form, which had to go: collecting names and phone numbers means
     processing personal data. What is left is the idea a first conversation actually is —
     agreeing where zero is.
     The plate is set out in the order a real sheet is: registration marks in the corners,
     the two axes drawn outward from the centre, their bubbles (the same mark the favicon
     carries), the graduations and their figures, the control crosses, then the origin with
     a sight ring locking onto it. A total-station sweep turns over the whole thing, and the
     coordinate readout settles from dashes to a value once the origin has been found.
     12s cycle — long enough that each stage is legible instead of a flicker. Stroke, opacity
     and transform only: nothing here triggers layout, and the whole plate is one SVG.
     Every stroked path declares pathLength="100", so one keyframe drives all of them
     regardless of their real length — no one has to measure a path by hand. */
  /* Every stage carries its own start time INSIDE the keyframe rather than in
     animation-delay, and every one of them fades out on the same 86%→95% beat. That matters:
     with staggered animation-delay the layers also wipe at staggered times, so for a second
     near the end the axes had already gone while their graduations and figures still hung in
     the air — it read as a fault, not as an erase. One shared timeline, one clean wipe. */
  @keyframes ctd-frame-in { 0%{opacity:0} 4%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  @keyframes ctd-mid-in   { 0%,13%{opacity:0} 18%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  @keyframes ctd-late-in  { 0%,19%{opacity:0} 24%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }
  @keyframes ctd-last-in  { 0%,28%{opacity:0} 33%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }

  @keyframes ctd-axis-draw   { 0%{stroke-dashoffset:100} 14%{stroke-dashoffset:0} 86%{stroke-dashoffset:0} 95%{stroke-dashoffset:100} 100%{stroke-dashoffset:100} }
  @keyframes ctd-bubble-draw { 0%,12%{stroke-dashoffset:100} 24%{stroke-dashoffset:0} 86%{stroke-dashoffset:0} 95%{stroke-dashoffset:100} 100%{stroke-dashoffset:100} }
  @keyframes ctd-origin-draw { 0%,25%{stroke-dashoffset:100} 37%{stroke-dashoffset:0} 86%{stroke-dashoffset:0} 95%{stroke-dashoffset:100} 100%{stroke-dashoffset:100} }

  /* transform-box is load-bearing on both of these — without it Safari transforms about the
     SVG's own origin instead of the element's, and the ring flies off the plate. The sweep
     needs view-box (it turns about a named point); the ring needs fill-box (about itself). */
  @keyframes ctd-lock  { 0%,26%{transform:scale(2.6); opacity:0} 31%{opacity:.85} 45%{transform:scale(1); opacity:.85} 62%{transform:scale(1); opacity:0} 100%{transform:scale(1); opacity:0} }
  @keyframes ctd-sweep { 0%{transform:rotate(0deg)} 100%{transform:rotate(360deg)} }
  @keyframes ctd-pulse { 0%,100%{transform:scale(1); opacity:.9} 50%{transform:scale(1.7); opacity:.3} }

  /* the four control points are struck one after another, then hold faint */
  @keyframes ctd-c1 { 0%,21%{opacity:0} 24%{opacity:.9} 29%{opacity:.22} 86%{opacity:.22} 95%{opacity:0} 100%{opacity:0} }
  @keyframes ctd-c2 { 0%,24%{opacity:0} 27%{opacity:.9} 32%{opacity:.22} 86%{opacity:.22} 95%{opacity:0} 100%{opacity:0} }
  @keyframes ctd-c3 { 0%,27%{opacity:0} 30%{opacity:.9} 35%{opacity:.22} 86%{opacity:.22} 95%{opacity:0} 100%{opacity:0} }
  @keyframes ctd-c4 { 0%,30%{opacity:0} 33%{opacity:.9} 38%{opacity:.22} 86%{opacity:.22} 95%{opacity:0} 100%{opacity:0} }

  /* the readout is two stacked labels cross-fading: dashes while the station is still
     searching, the figure once it has locked onto the origin. */
  @keyframes ctd-seek  { 0%,40%{opacity:0} 44%{opacity:1} 56%{opacity:1} 60%{opacity:0} 100%{opacity:0} }
  @keyframes ctd-found { 0%,58%{opacity:0} 64%{opacity:1} 86%{opacity:1} 95%{opacity:0} 100%{opacity:0} }

  .ctd-frame     { animation: ctd-frame-in   12s ease-out infinite both; }
  .ctd-axis      { animation: ctd-axis-draw  12s cubic-bezier(.4,0,.2,1) infinite both; }
  .ctd-arrow     { animation: ctd-mid-in     12s ease-out infinite both; }
  .ctd-tick      { animation: ctd-mid-in     12s ease-out infinite both; }
  .ctd-bubble    { animation: ctd-bubble-draw 12s cubic-bezier(.4,0,.2,1) infinite both; }
  .ctd-bnum      { animation: ctd-late-in    12s ease-out infinite both; }
  .ctd-num       { animation: ctd-late-in    12s ease-out infinite both; }
  .ctd-origin    { animation: ctd-origin-draw 12s cubic-bezier(.4,0,.2,1) infinite both; }
  .ctd-dotwrap   { animation: ctd-last-in    12s ease-out infinite both; }
  .ctd-sweepwrap { animation: ctd-last-in    12s ease-out infinite both; }
  .ctd-c1        { animation: ctd-c1 12s ease-out infinite both; }
  .ctd-c2        { animation: ctd-c2 12s ease-out infinite both; }
  .ctd-c3        { animation: ctd-c3 12s ease-out infinite both; }
  .ctd-c4        { animation: ctd-c4 12s ease-out infinite both; }
  .ctd-dot       { transform-box: fill-box; transform-origin: center;
                   animation: ctd-pulse 2.8s ease-in-out infinite both; }
  .ctd-target    { transform-box: fill-box; transform-origin: center;
                   animation: ctd-lock  12s cubic-bezier(.3,0,.2,1) infinite both; }
  .ctd-sweep     { transform-box: view-box; transform-origin: 150px 96px;
                   animation: ctd-sweep 12s linear infinite both; }
  .ctd-read-a    { animation: ctd-seek  12s ease-out infinite both; }
  .ctd-read-b    { animation: ctd-found 12s ease-out infinite both; }

  /* ---- City: the handover strip --------------------------------------------------------
     A five-second levelling run that hands the visitor to Countryside. The fill is scaleX and
     the marker is translateX — both compositor-only. Animating width/left instead would force
     a layout on every frame of the run, which is exactly what a progress bar should not do.
     translateX(100%) on .ctyr-run works because the runner is inset:0, so its own width is the
     track's width; overflow:hidden on the track keeps it from widening the document, which is
     the bug the built-index marker had. */
  .ctyr-track { position:relative; height:26px; overflow:hidden; margin-top:12px; }
  .ctyr-base  { position:absolute; left:0; right:0; bottom:0; height:1px; background:var(--line); }
  .ctyr-ticks { position:absolute; left:0; right:0; bottom:0; height:6px;
                background-image:repeating-linear-gradient(90deg, rgba(22,23,27,0.26) 0 1px, transparent 1px 5%); }
  .ctyr-fill  { position:absolute; left:0; right:0; bottom:0; height:2px; background:var(--accent,#B0542B);
                transform-origin:left center; transform:scaleX(0);
                animation:ctyr-fill 5s linear forwards; }
  .ctyr-run   { position:absolute; inset:0; animation:ctyr-run 5s linear forwards; }
  .ctyr-mark  { position:absolute; left:0; bottom:0; width:1px; height:26px; background:var(--ink); }
  @keyframes ctyr-fill { to { transform:scaleX(1); } }
  @keyframes ctyr-run  { to { transform:translateX(100%); } }
  /* the countdown is five digits stacked in one cell, each shown for exactly its second —
     CSS cannot count, but it can hand the cell over on a timer. */
  /* inline-grid with every digit in the same cell: they stack, the cell sizes itself to the
     glyph, and it still sits on the text baseline. The obvious version — an inline-block with
     absolutely positioned digits — collapses to zero height, so the digit lands under the
     baseline and reads as a line break. */
  .ctyr-count { display:inline-grid; }
  .ctyr-n     { grid-area:1 / 1; opacity:0; animation:ctyr-n 1s steps(1,end) forwards; }
  @keyframes ctyr-n { 0%{opacity:1} 100%{opacity:0} }

  /* ---- Built index: a levelling line ---------------------------------------------------
     One graduated line with a survey marker running along it; each tick flicks up as the
     marker reaches it, because every tick's delay is its own position along the run. Thin
     on purpose — it belongs between two blocks of text, not on top of them. */
  .blt-mark { animation: blt-travel 7s linear infinite both; }
  @keyframes blt-travel { 0%{transform:translateX(0)} 100%{transform:translateX(100%)} }
  .blt-tick { transform-origin: bottom center; animation: blt-tick 7s linear infinite both; }
  @keyframes blt-tick {
    0%  { transform: scaleY(1);   opacity: .3; }
    3%  { transform: scaleY(2.4); opacity: 1; }
    12% { transform: scaleY(1);   opacity: .3; }
    100%{ transform: scaleY(1);   opacity: .3; }
  }

  /* ---- Built index: the two direction vignettes ----------------------------------------
     A distant elevation beside each row — the countryside villa, the city block — drawn the
     way a study sketch is: the silhouette is stroked in first, then the floors and glazing
     settle in behind it. Held visible for most of the loop so it reads as a drawing on the
     page rather than as something blinking for attention.
     Each silhouette declares pathLength="100", so one keyframe animates all of them without
     anyone having to measure a path. */
  @keyframes bltd-draw { 0%{stroke-dashoffset:100} 22%{stroke-dashoffset:0} 74%{stroke-dashoffset:0} 88%{stroke-dashoffset:100} 100%{stroke-dashoffset:100} }
  @keyframes bltd-soft { 0%{opacity:0} 30%{opacity:1} 74%{opacity:1} 88%{opacity:0} 100%{opacity:0} }
  /* 9s: drawn in two seconds, held for four, wiped, and straight round again — short enough
     that the loop is visible rather than a single draw followed by a long stare. */
  .bltd-line { animation: bltd-draw 9s cubic-bezier(.5,0,.3,1) infinite both; }
  .bltd-soft { animation: bltd-soft 9s ease-out .7s infinite both; }
  .bltd-city .bltd-line { animation-delay: 1s; }
  .bltd-city .bltd-soft { animation-delay: 1.7s; }

  @media (prefers-reduced-motion: reduce) {
    /* the plate still reads as a finished drawing standing still: everything drawn, the
       sweep and the sight ring simply absent, the readout showing the found value. */
    .ctd-frame, .ctd-axis, .ctd-arrow, .ctd-bubble, .ctd-bnum, .ctd-tick, .ctd-num,
    .ctd-origin, .ctd-dotwrap, .ctd-dot, .ctd-read-b {
      animation:none !important; stroke-dashoffset:0 !important; opacity:1 !important; transform:none !important; }
    /* the control points hold faint in the animation; at full strength they shout */
    .ctd-c1, .ctd-c2, .ctd-c3, .ctd-c4 { animation:none !important; opacity:.28 !important; }
    .ctd-target, .ctd-sweepwrap, .ctd-read-a { animation:none !important; opacity:0 !important; }
    /* the handover strip stops moving, but the redirect itself still runs — the wording and
       the two controls carry the meaning without the run. */
    .ctyr-fill { animation:none !important; transform:scaleX(1) !important; }
    .ctyr-run  { animation:none !important; }
    .ctyr-mark, .ctyr-count { display:none !important; }
    .cty, .cty-scan, .cty-pen { animation:none !important; stroke-dashoffset:0 !important; opacity:1 !important; }
    .cty-scan, .cty-pen { opacity:0 !important; }
    .blt-mark, .blt-tick { animation:none !important; opacity:1 !important; }
    .blt-tick { transform:none !important; opacity:.3 !important; }
    .bltd-line, .bltd-soft { animation:none !important; stroke-dashoffset:0 !important; opacity:1 !important; }
    .built-row .built-row-bg, .built-row .built-row-veil, .built-row .built-row-arrow { transition:none; }
  }

  /* On phones the wordmark is only ~71px wide, so the tagline is stepped down a size and tracked
     tighter — it must read as a subordinate descriptor, not as a second headline next to the logo.
     Below 360px even that is dropped: there the header needs the room more than the brand voice. */
  @media (max-width: 879px) {
    .logo-sub { font-size:8.5px !important; letter-spacing:0.12em !important; }
  }
  @media (max-width: 359px) { .logo-sub { display:none !important; } }

  .mvn-credit { display:inline-flex; align-items:center; gap:8px; text-decoration:none;
    color:inherit; white-space:nowrap; text-transform:uppercase;
    transition: color .35s cubic-bezier(.4,0,.2,1); }
  .mvn-credit img { height:11px; width:auto; display:block; opacity:.38; filter:grayscale(1);
    transition: opacity .35s cubic-bezier(.4,0,.2,1), filter .35s cubic-bezier(.4,0,.2,1); }
  .mvn-credit:hover, .mvn-credit:focus-visible { color:#9A9B93; }
  .mvn-credit:hover img, .mvn-credit:focus-visible img { opacity:1; filter:none; }
  .mvn-credit:focus-visible { outline:1px solid #5B5C56; outline-offset:5px; }
  @media (prefers-reduced-motion: reduce) { .mvn-credit, .mvn-credit img { transition:none; } }

  /* ===== BIM stack — glass slabs, each carrying a drawing sheet =====
     NOTE ON STRUCTURE: the discipline on/off state is an opacity fade, and `opacity` is a
     grouping property — an element with opacity < 1 is forced to `transform-style: flat`,
     which would collapse the slab's extruded edges back into its face. So the fade lives on
     the FACES (leaf elements with no 3D children of their own) while the slab wrapper stays
     fully opaque and keeps preserve-3d. Do not hoist the opacity up to the wrapper. */
  /* The flex parent centres the stack's LAYOUT box, but the projected slabs paint well outside it
     and hang low, so the optical centre needed lifting: it was overflowing the stage bottom while
     leaving ~52px dead above. translateY comes FIRST so it is a plain screen-space lift applied
     before the isometric rotations. Desktop only — the mobile stage is proportionally far tighter
     (225px tall vs a 394px projection) and the same lift drives the stack into the eyebrow label. */
  .bim-stack { transform: translateY(-15%) rotateX(58deg) rotateZ(-42deg); }
  @media (max-width: 879px) { .bim-stack { transform: rotateX(58deg) rotateZ(-42deg); } }
  @keyframes bim-glint { 0% { transform: translateX(-190%) skewX(-16deg); } 52%, 100% { transform: translateX(560%) skewX(-16deg); } }
  .bim-glint { animation: bim-glint 7.4s cubic-bezier(.55,0,.45,1) infinite both; will-change: transform; }
  @media (prefers-reduced-motion: reduce) { .bim-glint { animation: none !important; opacity: 0 !important; } }
  /* GP page: hairline row hover + scheme motion */
  .gps-row { transition: background .25s ease; }
  .gps-row:hover { background: #F6F5F0 !important; }
  @keyframes gpm-scan { from { transform: translateX(0); } to { transform: translateX(250px); } }
  @keyframes gpp-vertex { 0%,100% { opacity: .35; } 6% { opacity: 1; } 16% { opacity: .35; } }
  @keyframes gpw-ping { 0% { transform: scale(0.4); opacity: .9; } 70% { transform: scale(2.6); opacity: 0; } 100% { transform: scale(2.6); opacity: 0; } }
  /* Results block: rolling verification wave — one cell re-checks itself at a time */
  @keyframes gpr-blink { 0%,100% { opacity: 1; } 50% { opacity: .2; } }
  @keyframes gpr-cell { 0%, 12.5%, 100% { background-color: var(--paper); } 3%, 8% { background-color: rgba(176,84,43,0.07); } }
  @keyframes gpr-check {
    0%    { stroke-dashoffset: 0; }
    1.2%  { stroke-dashoffset: 14; }
    2.4%  { stroke-dashoffset: 14; }
    7%    { stroke-dashoffset: 0; }
    100%  { stroke-dashoffset: 0; }
  }
  @media (prefers-reduced-motion: reduce) { .gp3d-anim { animation: none !important; } }

  /* ===== BUILD PRINCIPLES — six instruments (transform / opacity / stroke-dashoffset only) =====
     Each cell carries its own micro-diagram of the principle it states. Structure strokes use
     currentColor so the whole figure inverts with the cell; accent parts read --bp-acc, which
     is re-pointed to the light tint on the dark hover state. */
  .bp-cell { --bp-acc:#B0542B; background:var(--paper); color:#16171B;
             transition:background .45s cubic-bezier(.4,0,.2,1), color .45s cubic-bezier(.4,0,.2,1); }
  .bp-cell:hover { background:#16171B; color:#FBFBF9; --bp-acc:#E08A5E; }
  .bp-no    { font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:0.1em; opacity:.5; }
  .bp-title { font-weight:600; font-size:clamp(17px,1.6vw,21px); letter-spacing:-0.01em; line-height:1.16; }
  .bp-desc  { font-size:13px; line-height:1.5; opacity:.6; }
  .bp-rule  { display:block; height:1px; background:currentColor; opacity:.22; transform:scaleX(0); transform-origin:left center;
              transition:transform .5s cubic-bezier(.4,0,.2,1), opacity .45s; }
  .bp-cell:hover .bp-rule { transform:scaleX(1); }
  .bp-a  { stroke:var(--bp-acc); transition:stroke .45s cubic-bezier(.4,0,.2,1); }
  .bp-af { fill:var(--bp-acc);   transition:fill   .45s cubic-bezier(.4,0,.2,1); }
  .bp-o  { transform-box:fill-box; transform-origin:center; }

  /* 01 · approved design — the approval check draws itself, holds, then wipes forward (never rewinds) */
  @keyframes bp-check { 0%{stroke-dashoffset:33} 18%{stroke-dashoffset:0} 76%{stroke-dashoffset:0} 90%{stroke-dashoffset:-33} 100%{stroke-dashoffset:-33} }
  /* 02 · deliveries — crates run the rail; the inspection gate flashes as each one passes it.
     A crate's centre crosses x=46 at 61% of its own travel; with crates staggered by -2s and -4s
     on a 6s cycle that lands the three flashes at 27.7% / 61% / 94.3%. */
  @keyframes bp-crate { 0%{transform:translateX(0)} 100%{transform:translateX(85px)} }
  @keyframes bp-gate  { 0%,24%{opacity:.42} 27.7%{opacity:1} 32%{opacity:.42} 57%{opacity:.42} 61%{opacity:1} 65%{opacity:.42} 90%{opacity:.42} 94.3%{opacity:1} 98%{opacity:.42} 100%{opacity:.42} }
  /* 03 · staged acceptance — treads are signed off in ascending order, then the reading clears */
  @keyframes bp-step  { 0%{opacity:0;transform:scale(.35)} 7%{opacity:1;transform:scale(1)} 74%{opacity:1;transform:scale(1)} 86%{opacity:0;transform:scale(1)} 100%{opacity:0;transform:scale(1)} }
  /* 04 · site & crews — outer nodes report in turn, the centre emits a coordination ring */
  @keyframes bp-node  { 0%{opacity:.4} 6%{opacity:1} 22%{opacity:.4} 100%{opacity:.4} }
  @keyframes bp-ring  { 0%{opacity:.75;transform:scale(.32)} 70%{opacity:0;transform:scale(2.1)} 100%{opacity:0;transform:scale(2.1)} }
  /* 05 · technology — layers are laid bottom-up, each flashing as it is placed, then curing back */
  @keyframes bp-layer { 0%{opacity:0;transform:translateY(7px)} 7%{opacity:1;transform:translateY(0)} 15%{opacity:.42;transform:translateY(0)} 82%{opacity:.42} 93%{opacity:0} 100%{opacity:0} }
  /* 06 · documentation — sheets stack, then the seal stamps down onto them */
  @keyframes bp-sheet { 0%{opacity:0;transform:translate(-5px,4px)} 9%{opacity:1;transform:translate(0,0)} 80%{opacity:1;transform:translate(0,0)} 92%{opacity:0} 100%{opacity:0} }
  @keyframes bp-seal  { 0%{opacity:0;transform:scale(1.75)} 6%{opacity:1;transform:scale(1)} 80%{opacity:1;transform:scale(1)} 92%{opacity:0;transform:scale(1)} 100%{opacity:0;transform:scale(1)} }

  .bp-f1  { animation:bp-check 6s cubic-bezier(.5,0,.3,1) .2s infinite both; }
  .bp-f2a { animation:bp-crate 6s linear      0s infinite both; }
  .bp-f2b { animation:bp-crate 6s linear     -2s infinite both; }
  .bp-f2c { animation:bp-crate 6s linear     -4s infinite both; }
  .bp-f2g { animation:bp-gate  6s linear      0s infinite both; }
  .bp-f3a { animation:bp-step 5.6s cubic-bezier(.3,1.4,.5,1) .30s infinite both; }
  .bp-f3b { animation:bp-step 5.6s cubic-bezier(.3,1.4,.5,1) .75s infinite both; }
  .bp-f3c { animation:bp-step 5.6s cubic-bezier(.3,1.4,.5,1) 1.20s infinite both; }
  .bp-f3d { animation:bp-step 5.6s cubic-bezier(.3,1.4,.5,1) 1.65s infinite both; }
  .bp-f4a { animation:bp-node 5.2s ease-in-out  0s   infinite both; }
  .bp-f4b { animation:bp-node 5.2s ease-in-out  .34s infinite both; }
  .bp-f4c { animation:bp-node 5.2s ease-in-out  .68s infinite both; }
  .bp-f4d { animation:bp-node 5.2s ease-in-out 1.02s infinite both; }
  .bp-f4r { animation:bp-ring 5.2s cubic-bezier(.2,.6,.35,1) 1.30s infinite both; }
  .bp-f5a { animation:bp-layer 6.4s cubic-bezier(.3,0,.2,1) .20s infinite both; }
  .bp-f5b { animation:bp-layer 6.4s cubic-bezier(.3,0,.2,1) .58s infinite both; }
  .bp-f5c { animation:bp-layer 6.4s cubic-bezier(.3,0,.2,1) .96s infinite both; }
  .bp-f5d { animation:bp-layer 6.4s cubic-bezier(.3,0,.2,1) 1.34s infinite both; }
  .bp-f5e { animation:bp-layer 6.4s cubic-bezier(.3,0,.2,1) 1.72s infinite both; }
  .bp-f6a { animation:bp-sheet 6s cubic-bezier(.3,0,.2,1) .20s infinite both; }
  .bp-f6b { animation:bp-sheet 6s cubic-bezier(.3,0,.2,1) .55s infinite both; }
  .bp-f6c { animation:bp-sheet 6s cubic-bezier(.3,0,.2,1) .90s infinite both; }
  .bp-f6s { animation:bp-seal  6s cubic-bezier(.3,1.5,.5,1) 1.35s infinite both; }
  @media (prefers-reduced-motion: reduce) {
    .bp-f1,.bp-f2a,.bp-f2b,.bp-f2c,.bp-f2g,.bp-f3a,.bp-f3b,.bp-f3c,.bp-f3d,
    .bp-f4a,.bp-f4b,.bp-f4c,.bp-f4d,.bp-f4r,.bp-f5a,.bp-f5b,.bp-f5c,.bp-f5d,.bp-f5e,
    .bp-f6a,.bp-f6b,.bp-f6c,.bp-f6s { animation:none !important; opacity:1 !important; transform:none !important; }
    .bp-f1 { stroke-dashoffset:0 !important; }
    .bp-f2a { transform:translateX(20px) !important; }
    .bp-f2b { transform:translateX(46px) !important; }
    .bp-f2c { transform:translateX(72px) !important; }
    .bp-f4r { opacity:.35 !important; }
  }
  /* ===== SCROLL CUE — "ГЛУБИНОМЕР" / depth gauge (GPU-composited: transform + opacity only) =====
     Geometry is welded to the markup. Gauge box 21x46 with overflow:hidden; graduations at
     y = 0 / 9 / 18 / 27 / 36; the index box is 24px tall at top:-24px, so translateY(N) puts
     its head exactly on y = N and translateY(0) hides it entirely above the clip.
     Cycle 2.4s at .42s delay:
       0-36%    head falls 0 -> 36px on cubic-bezier(.5,0,.5,1)   (41.7px/s avg, 83.3px/s peak)
       36-49%   holds on the datum (bottom major) while that graduation stays at full ink
       49-62%   releases downward 36 -> 52px, fading to 0 — exits through the fading axis
       62-64%   resets to y=0 while invisible (mark is clipped above the box; no loop flash)
       64-100%  still, 0.91s
     Motion windows land at 0.42-1.91s, 2.82-4.31s, 5.22-6.71s.
     TICK DELAYS are the numeric inverse of cubic-bezier(.5,0,.5,1) solved at p = y/36,
     minus the 2.5% keyframe attack (0.060s):
       y= 0  p=0.00  x=0.00000  ->  0.420 - 0.060 =  .360s
       y= 9  p=0.25  x=0.36453  ->  0.735 - 0.060 =  .675s
       y=18  p=0.50  x=0.50000  ->  0.852 - 0.060 =  .792s
       y=27  p=0.75  x=0.63547  ->  0.969 - 0.060 =  .909s
       y=36  p=1.00  x=1.00000  ->  1.284 - 0.060 = 1.224s
     If the 2.4s cycle, the 36% descent, the 36px travel or the easing is ever changed,
     ALL FIVE DELAYS MUST BE RE-SOLVED or the flashes desync from the head. */
  @keyframes stmk-cue-index {
    0%   { transform: translate3d(0, 0, 0);    opacity: 1; animation-timing-function: cubic-bezier(.25,.1,.25,1); }
    36%  { transform: translate3d(0, 36px, 0); opacity: 1; }
    49%  { transform: translate3d(0, 36px, 0); opacity: 1; animation-timing-function: cubic-bezier(.25,.1,.25,1); }
    62%  { transform: translate3d(0, 52px, 0); opacity: 0; }
    63%  { transform: translate3d(0, 0, 0);    opacity: 0; }
    64%  { transform: translate3d(0, 0, 0);    opacity: 1; }
    100% { transform: translate3d(0, 0, 0);    opacity: 1; }
  }
  @keyframes stmk-cue-major { 0% { opacity:.42; } 2.5% { opacity:1; }   17% { opacity:.42; } 100% { opacity:.42; } }
  @keyframes stmk-cue-minor { 0% { opacity:.26; } 2.5% { opacity:.86; } 15% { opacity:.26; } 100% { opacity:.26; } }
  @keyframes stmk-cue-datum { 0% { opacity:.42; } 2.5% { opacity:1; }   16% { opacity:1; } 30% { opacity:.42; } 100% { opacity:.42; } }
  .stmk-cue-index { animation: stmk-cue-index 2.4s linear .42s infinite both; will-change: transform, opacity; transform: translate3d(0,0,0); backface-visibility: hidden; }
  .stmk-cue-g1 { animation: stmk-cue-major 2.4s linear .360s infinite both; will-change: opacity; }
  .stmk-cue-g2 { animation: stmk-cue-minor 2.4s linear .675s infinite both; will-change: opacity; }
  .stmk-cue-g3 { animation: stmk-cue-major 2.4s linear .792s infinite both; will-change: opacity; }
  .stmk-cue-g4 { animation: stmk-cue-minor 2.4s linear .909s infinite both; will-change: opacity; }
  .stmk-cue-g5 { animation: stmk-cue-datum 2.4s linear 1.224s infinite both; will-change: opacity; }
  /* !important on color is required: it must beat the element's own inline color. */
  .stmk-cue:hover .stmk-cue-label { color: var(--ink) !important; transform: translateY(2px); }
  .stmk-cue:hover .stmk-cue-rule  { transform: scaleX(1.5); }
  .stmk-cue:focus-visible { outline: 1px solid var(--ink); outline-offset: 6px; }
  #hero { scroll-margin-top: clamp(62px,7vw,76px); }
  /* mirrors `const mobile = s.vw < 880` without touching the render scope */
  @media (max-width: 879px) { .stmk-cue-tag { display: none !important; } }
  @media (prefers-reduced-motion: reduce) {
    .stmk-cue-index, .stmk-cue-g1, .stmk-cue-g2, .stmk-cue-g3, .stmk-cue-g4, .stmk-cue-g5 { animation: none !important; }
    .stmk-cue-index { transform: translateY(36px); will-change: auto; }
    .stmk-cue-g5 { opacity: 1 !important; }
  }


                      @keyframes gpDashFlow { to { stroke-dashoffset: -12; } }
                      .gp-line-active { animation: gpDashFlow 0.8s linear infinite; }
                      .gp-line-idle   { animation: gpDashFlow 2.2s linear infinite; }
                    
body { --ink:#16171B; --ink2:#34353A; --mut:#86877F; --mut2:#9A9B92; --line:#E5E4DE; --line2:#EFEEE9; --paper:#FBFBF9; --paper2:#F3F2ED; --ink-soft:#52534F; font-family:'Onest',sans-serif; color:#16171B; }
x-dc { display: none !important; }
