/* Storefront-only styles.
 *
 * The storefront loads styles.css whole and adds this file on top: the design system is
 * inherited, not restated (docs/design-system.md). Everything here is a shape the two
 * application surfaces have no equivalent of — a site header instead of a sidebar, a
 * marketing hero, a pricing card, a documentation column.
 *
 * No token is redefined. A second palette would diverge from the first, and a diverged
 * storefront is one that reads as a different product from the one behind the login.
 *
 * Sizes come from content, never from a fixed width: model ids, tariff titles and
 * translated copy all vary in length (AGENTS.md). Logical properties throughout, for the
 * same reason the panel uses them.
 */

/* The application surfaces are a shell with its own scroll containers. This one is an
   ordinary document, so it scrolls as one and the header sticks. */
body.public {
  display: block;
  overflow: auto;
  background: var(--bg);
}

/* 1. Header ============================================================== */

.site-head {
  position: sticky;
  inset-block-start: 0;
  z-index: 60;
  border-block-end: 1px solid var(--line);
  /* Translucent rather than solid: the hero's mark scrolls under it, and a solid bar
     would cut the page in two at a line nothing else explains. The fallback matters —
     without backdrop-filter the bar must still be opaque enough to read against. */
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: blur(14px);
}

.site-head-inner {
  display: flex;
  align-items: center;
  gap: 22px;
  max-width: var(--content-max);
  margin-inline: auto;
  padding: 12px clamp(16px, 4vw, 40px);
}

/* The lockup is a link here, unlike in the panel where it is a label: on a storefront
   the mark is how a visitor gets back to the front page. */
.site-head .brand {
  flex: none;
  text-decoration: none;
  color: inherit;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-inline-end: auto;
  min-width: 0;
}

.site-nav a {
  padding: 7px 11px;
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.site-nav a:hover {
  color: var(--text);
  background: var(--surface-hover);
}

/* The current section is marked by weight and colour together, not by colour alone:
   colour as the only marker fails for a reader who cannot distinguish it. */
.site-nav a[aria-current="page"] {
  color: var(--text);
  font-weight: 560;
  background: var(--surface-2);
}

.site-head-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

/* The live verdict, as a link to its own page. Three states, and each is marked by the
   dot's colour *and* by its text — "operational" next to a green dot is redundant on
   purpose, because the dot alone is not readable to everyone. */
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  font-size: 12px;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
}

.status-chip:hover {
  border-color: var(--line-strong);
  color: var(--text);
}

.status-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: var(--text-faint);
  flex: none;
}

.status-chip.operational {
  border-color: var(--ok-line);
  color: var(--ok);
}

.status-chip.operational .status-dot {
  background: var(--ok);
}

.status-chip.degraded {
  border-color: var(--warn-line);
  color: var(--warn);
}

.status-chip.degraded .status-dot {
  background: var(--warn);
}

.status-chip.down {
  border-color: var(--danger-line);
  color: var(--danger);
}

.status-chip.down .status-dot {
  background: var(--danger);
}

/* The honest fourth state: the status request failed, so the chip says
   "неизвестно" instead of keeping a stale verdict or lying "operational".
   Neutral, not warn: an unanswered request is not an observed outage. */
.status-chip.unknown {
  border-color: var(--neutral-line);
  color: var(--text-muted);
}

.status-chip.unknown .status-dot {
  background: var(--text-faint);
}

@keyframes public-health-pulse {
  0%, 100% { opacity: .58; transform: scale(.86); }
  50% { opacity: 1; transform: scale(1); }
}

.status-dot[data-icon-state="health"] {
  transform-origin: center;
  animation: public-health-pulse 4.5s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .status-dot[data-icon-state="health"] { animation: none; }
}

/* The burger only exists below the width where the nav stops fitting. */
.nav-toggle {
  display: none;
}

/* 2. Page frame ========================================================== */

.site-main {
  max-width: var(--content-max);
  margin-inline: auto;
  padding: clamp(28px, 5vw, 56px) clamp(16px, 4vw, 40px) 72px;
  /* The main region is focusable so a route change can move focus into it. Without
     removing the ring the outline would flash on every navigation. */
  outline: none;
}

/* A page's own heading block. Wider max-width than the panel's, because this copy is
   prose meant to be read rather than a label above a table. */
.page-intro {
  max-width: 760px;
  margin-block-end: 34px;
}

.page-intro h1 {
  font-family: var(--font-display);
  font-size: clamp(26px, 3.4vw, 36px);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

.page-intro p {
  margin-block-start: 12px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-muted);
}
.page-intro {
  position: relative;
  padding-block-end: 22px;
  border-block-end: 1px solid var(--line);
}

.page-intro::before {
  content: "PUBLIC / SIGNAL";
  display: block;
  margin-block-end: 12px;
  color: var(--text-faint);
  font: 500 10px/1 var(--font-mono);
  letter-spacing: 0.12em;
}

.page-intro::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-end: -1px;
  width: 84px;
  height: 1px;
  background: var(--accent);
}

 /* 3. Hero ================================================================ */

/* 3. Hero ================================================================ */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: center;
  margin-block-end: clamp(40px, 6vw, 72px);
}

.hero-copy h1 {
  font-family: var(--font-display);
  font-size: clamp(32px, 5vw, 54px);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.06;
}

.hero-copy > p {
  margin-block-start: 18px;
  max-width: 560px;
  font-size: clamp(15px, 1.5vw, 17px);
  line-height: 1.6;
  color: var(--text-muted);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-block-start: 28px;
}

/* The animated mark, at the size the login uses it. Square by aspect-ratio rather than
   by a height, so it scales with the column instead of overflowing it. */
.hero-mark {
  position: relative;
  aspect-ratio: 1;
  width: 100%;
  max-width: 420px;
  margin-inline: auto;
  /* Once per visit: the mark is the object the page is built around, so it
     arrives as a surface rather than popping in at full size. Spring is reserved
     for overlay objects (drawer, modal, toast); this uses the default curve. */
  animation: mark-arrive var(--dur-base) var(--ease);
}

.hero-mark .orbit-mark {
  width: 100%;
  height: 100%;
}

/* The pad under the hero mark, both themes: the dark one grew out of the chrome
   render's own reflections, the light one is drawn for it (the graphite object
   over a near-white panel gathers nothing behind itself). Same variable the
   login uses, so the two stages cannot drift apart. */
.hero-mark::before {
  content: "";
  position: absolute;
  inset: 8%;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    var(--orbit-pad, rgba(139, 150, 165, 0.16)) 0%,
    transparent 70%
  );
  pointer-events: none;
}

/* 4. Feature and pricing cards =========================================== */

.card-grid {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

.info-card {
  display: grid;
  gap: 9px;
  align-content: start;
  padding: 20px 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  transition: border-color var(--dur-fast) var(--ease);
}

.info-card:hover {
  border-color: var(--line-strong);
}

.info-card > svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.info-card h3 {
  font-size: 14.5px;
  font-weight: 560;
}

.info-card p {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

/* A tariff as a customer chooses it: the name, the promise, and what it does not say.
   No price on the card — a tariff is a set of vendors, and its price is per model. */
.tariff-card {
  display: grid;
  gap: 12px;
  align-content: start;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.tariff-card h3 {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 500;
}

.tariff-card .promise {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

/* Периодные планы на витрине. Карточки приходят из /api/public/offers: цена периода
   у подписки, суточная квота у FREE. Это не чекаут: кнопка ведёт в аккаунт, списание
   происходит в кабинете. Featured — акцент, а не второй продукт. */
.plan-grid {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  align-items: stretch;
}

.plan-card {
  display: grid;
  gap: 14px;
  align-content: start;
  padding: 26px 28px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  transition: border-color var(--dur-fast) var(--ease);
  position: relative;
  overflow: hidden;
}

.plan-card:hover {
  border-color: var(--line-strong);
}

/* Featured is weight, not a second product: the ok tokens mark the recommended
   period without a glow or a second surface. */
.plan-card.featured {
  border-color: var(--ok-line);
  background: var(--ok-soft);
}

.plan-card.featured:hover {
  border-color: var(--ok);
}

.plan-card h3 {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -0.012em;
}

.plan-price {
  font-family: var(--font-mono);
  font-size: clamp(22px, 3vw, 28px);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.plan-period {
  font-size: 13px;
  color: var(--text-muted);
}

.plan-card ul {
  display: grid;
  gap: 8px;
  margin: 4px 0 2px;
  padding: 0;
  list-style: none;
}

.plan-card li {
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--text-muted);
}

.plan-disclaimer {
  margin-block-start: 14px;
  max-width: 720px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-faint);
}

@keyframes mark-arrive {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.97);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Plan pop-up ------------------------------------------------------------- */

/* The pricing page's fine print (§14.6 p.6). Opened rarely, so the motion gate
   permits a real entrance: --ease-spring is the curve for surfaces that arrive
   as physical objects, and a dialog is one. Only transform and opacity move. */
.plan-details-btn {
  justify-self: start;
  padding: 6px 2px;
  border: none;
  background: none;
  font-size: 13px;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.plan-details-btn:hover {
  color: var(--text);
}

#plan-pop {
  width: min(460px, calc(100vw - 40px));
  padding: 26px 28px 22px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
}

#plan-pop[open] {
  animation: plan-pop-in var(--dur-base) var(--ease-spring);
}

#plan-pop::backdrop {
  background: color-mix(in srgb, var(--bg) 62%, transparent);
}

.plan-pop-close {
  position: absolute;
  top: 14px;
  right: 14px;
}

#plan-pop h3 {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 500;
}

.plan-pop-price {
  margin: 0 0 14px;
  font-size: 15px;
  color: var(--text-muted);
}

.plan-pop-points {
  display: grid;
  gap: 8px;
  margin: 0 0 16px;
  padding: 0;
  list-style: none;
}

.plan-pop-points li {
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--text-muted);
}

#plan-pop .plan-disclaimer {
  margin: 0;
}

@keyframes plan-pop-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.96);
  }
}

@media (prefers-reduced-motion: reduce) {
  #plan-pop[open] {
    animation: none;
  }
}

/* 5. Section rhythm ====================================================== */

.site-section {
  margin-block-start: clamp(40px, 6vw, 72px);
}

.site-section > h2 {
  font-family: var(--font-display);
  font-size: clamp(20px, 2.4vw, 26px);
  font-weight: 500;
  letter-spacing: -0.015em;
  margin-block-end: 8px;
}

.site-section > h2 + p {
  max-width: 720px;
  margin-block-end: 22px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-muted);
}

/* 6. Enquiry layout (shared with the old calculator grid) ================ */

.calc {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(260px, 0.7fr);
  gap: 20px;
  align-items: start;
}

.calc-form {
  display: grid;
  gap: 14px;
  padding: 20px 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}

/* The answer, large enough to be the point of the page. */
.calc-result {
  display: grid;
  gap: 10px;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  position: sticky;
  inset-block-start: calc(var(--topbar-h) + 18px);
}

.calc-note {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-faint);
}

/* 7. Documentation ======================================================= */

/* Prose measured for reading: 74 characters is the upper end of comfortable, and the
   panel has no equivalent because it has no prose. */
.doc-layout {
  display: grid;
  gap: 32px;
  align-items: start;
}

.doc-toc {
  display: none;
}

.doc-toc a {
  display: block;
  padding: 6px 0;
  font-size: 13px;
  color: var(--text-muted);
  text-decoration: none;
}

.doc-toc a:hover {
  color: var(--text);
}

.doc-body h2 {
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 500;
  margin-block: 34px 10px;
  scroll-margin-block-start: calc(var(--topbar-h) + 16px);
}

.doc-body h2:first-child {
  margin-block-start: 0;
}

.doc-body {
  max-width: 74ch;
  min-width: 0;
  width: 100%;
  overflow-wrap: anywhere;
}

.doc-body h3 {
  font-size: 15px;
  font-weight: 560;
  margin-block: 24px 8px;
}

.doc-body p,
.doc-body li {
  font-size: 14.5px;
  line-height: 1.68;
  color: var(--text-muted);
}

.doc-body p {
  margin-block-end: 12px;
}

.doc-body ul {
  margin-block-end: 14px;
  padding-inline-start: 20px;
  display: grid;
  gap: 6px;
}

.doc-body li {
  list-style: disc;
}

.doc-body strong {
  color: var(--text);
  font-weight: 560;
}

.doc-body code {
  font-family: var(--font-mono);
  font-size: 0.92em;
  padding: 1px 5px;
  border-radius: var(--radius-xs);
  background: var(--surface-2);
  color: var(--text);
}

/* A copyable snippet. The button sits inside the box rather than above it, so the
   relationship needs no label. */
.snippet {
  position: relative;
  margin-block-end: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.snippet pre {
  margin: 0;
  padding: 16px 18px;
  /* The one place a horizontal scroll is correct: a shell command must not be wrapped,
     because a wrapped command copied by eye is a broken command. */
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--text);
  white-space: pre;
}

.snippet .copy-btn {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
}

/* 8. Status ============================================================== */

.status-hero {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  margin-block-end: 20px;
}

.status-hero .status-dot {
  width: 12px;
  height: 12px;
}

.status-hero strong {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 500;
}

.status-hero p {
  margin-block-start: 4px;
  font-size: 13.5px;
  color: var(--text-muted);
}

.status-components {
  display: grid;
  gap: 10px;
  margin-block-end: 24px;
}

.component-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.component-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.component-info strong {
  font-size: 14px;
  font-weight: 560;
}

.component-desc {
  font-size: 12.5px;
  color: var(--text-muted);
}

.component-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 560;
}

.component-status.operational {
  color: var(--ok);
}

/* 8a. Enquiry form ======================================================= */

/* The three steps beside the form. A numbered list rather than three cards: the order is the
   content — nothing here happens before the step above it — and cards would present them as
   independent choices. */
.step-list {
  margin: 0;
  padding-inline-start: 20px;
  display: grid;
  gap: 8px;
}

.step-list li {
  list-style: decimal;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

/* The message field sizes to its rows attribute, and resizing is left to the visitor: a
   fixed height truncates a long enquiry into a scrollbar three lines tall. Vertical only,
   because horizontal resizing breaks out of the form's column. */
#lead-message {
  resize: vertical;
  min-height: 90px;
}

/* 9. Model catalogue ===================================================== */

/* Панель над таблицей: поиск, лаба, порядок и счётчик. Своя строка, а не внутри
   .page-heading, потому что счётчик стоит рядом с полями, а у заголовка нет слота под
   пару. */
.catalogue-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-block-end: 16px;
}

/* Поиск получает вдвое больший запас роста: подстрока — основной способ найти имя, а
   два селектора рядом читаются целиком и в своей ширине. Все три сжимаются до 150px,
   после чего перенос ставит их столбиком — на 390px иначе получается три обрезанных
   поля в одну строку. */
.catalogue-bar .field {
  flex: 1 1 180px;
  min-width: 150px;
}

.catalogue-bar .field:first-child {
  flex: 2 1 240px;
}

.catalogue-count {
  font-size: 12.5px;
  color: var(--text-faint);
  white-space: nowrap;
}

.lab-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 8px;
  margin-block: 14px 20px;
}

.lab-chip {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  min-width: 0;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-muted);
  text-align: start;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), transform var(--dur-instant) var(--ease);
}

.lab-chip:hover,
.lab-chip.active {
  border-color: var(--accent);
  color: var(--text);
}

.lab-chip:active { transform: translateY(1px); }

.lab-chip svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linejoin: bevel;
}

.lab-chip span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}

.lab-chip b {
  font: 500 11px var(--font-mono);
  color: var(--text-faint);
}


/* 10. Footer ============================================================= */

.site-foot {
  border-block-start: 1px solid var(--line);
  background: var(--surface);
}

.site-foot-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  justify-content: space-between;
  max-width: var(--content-max);
  margin-inline: auto;
  padding: 28px clamp(16px, 4vw, 40px);
}

.site-foot-brand {
  display: grid;
  gap: 8px;
  max-width: 420px;
}

.site-foot-brand p {
  font-size: 12.5px;
  line-height: 1.55;
}

.site-foot-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  align-content: start;
}

.site-foot-nav a {
  font-size: 13px;
  color: var(--text-muted);
  text-decoration: none;
}

.site-foot-nav a:hover {
  color: var(--text);
}

/* 11. Narrow widths ====================================================== */

@media (min-width: 901px) {
  .doc-layout {
    grid-template-columns: minmax(140px, 180px) minmax(0, 1fr);
  }

  .doc-toc {
    display: grid;
    gap: 2px;
    position: sticky;
    inset-block-start: calc(var(--topbar-h) + 16px);
  }
}

/* 900px is where the two-column layouts stop having room for the second column, not a
   device: the same breakpoint the panel uses for the same reason. */
@media (max-width: 900px) {
  .hero,
  .calc {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The mark leads on a narrow screen, but as a small crest above the copy — not the
     full column square it is on a wide screen. A 260px reserved block above the
     headline pushed the product's own name below the fold on a 390px phone and left
     a near-empty region as the first thing a visitor saw. */
  .hero-mark {
    order: -1;
    max-width: 120px;
    margin-inline: 0;
  }

  .calc-result {
    position: static;
  }
}

/* Below 800px the horizontal nav no longer fits beside the lockup and the actions. It
   becomes a panel the burger opens, positioned under the header rather than over the
   page: a full-screen overlay for five links is a heavier gesture than the content. */
@media (max-width: 800px) {
  .nav-toggle {
    display: inline-flex;
  }

  .site-nav {
    position: absolute;
    inset-inline: 0;
    inset-block-start: 100%;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: 10px clamp(16px, 4vw, 40px) 14px;
    border-block-end: 1px solid var(--line);
    background: var(--bg);
  }

  .site-nav.open {
    display: flex;
  }

  .site-head-inner {
    position: relative;
  }

  .site-nav a {
    padding: 10px 12px;
    font-size: 14.5px;
  }

  /* The theme toggle and the status chip stay; the chip's text is the shortest of the
     three states, so it survives the narrowest width without truncation. */
  .site-head-actions {
    margin-inline-start: auto;
  }
}

/* Below 520px the header runs out of room, and it overflowed rather than adapting: five
   controls plus the lockup measured 441px inside a 420px viewport, which gave the whole
   document a horizontal scrollbar — on every page, because the header is on every page.

   The status chip is what goes first: the nav has become a dropdown and its «Статус»
   entry leads to the same verdict, while the lockup is the product's own name — with
   the chip gone the wordmark fits again at 390px and the first viewport names the
   product instead of only drawing its mark. */
@media (max-width: 520px) {
  .site-head-inner {
    gap: 12px;
    flex-wrap: wrap;
  }

  .site-head-actions {
    gap: 6px;
  }

  /* The status chip goes, not the wordmark: at 390px the five controls measured 355px
     inside 358px of free width, and the document scrolled sideways by 25px on every
     page. The chip is the one control that is redundant here rather than merely nice —
     the nav has become a dropdown, and its «Статус» entry leads to the same verdict in
     full words. The alternative — keeping the chip and hiding its text — would leave a
     coloured dot as the only marker of a three-state answer, which is exactly what the
     design system forbids. Same trade the portal makes with its balance chip below
     700px. */
  .status-chip {
    display: none;
  }

  .catalogue-bar {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: end;
  }

  .catalogue-bar .field {
    min-width: 0;
    margin-bottom: 0;
  }

  .catalogue-bar .field:first-child {
    grid-column: 1 / -1;
  }

  .catalogue-count {
    justify-self: end;
    align-self: center;
  }

}

/* 12. Motion ============================================================= */

/* Everything in this section is gated on `.motion`, which public.js sets on <html> before
   the first page renders. Without it the storefront is the document it was: a reveal that
   starts an element at `opacity: 0` and waits for a script to reveal it would leave a
   scripting-blocked visitor on a blank page. The class is the guard, not a feature flag —
   nothing here checks it twice.

   The frequency table in `.cursor/rules/interface-motion.mdc` decides what may move at all.
   A storefront section is read once per visit, which is the row the budget lives in; the
   header, which is on every page and always visible, gets nothing but a one-pixel progress
   line whose purpose is stated below. */

/* Reveal on entry. Purpose: explanation — a page of six sibling cards gives no reading
   order, and arriving in sequence says which comes first. Only `opacity` and `transform`,
   so a reveal is a compositor job and a section entering costs no layout.

   The delay is a custom property rather than an `nth-child` ladder: the same rule then
   serves a three-card grid and a nine-row list, and public.js sets the index once per
   observed element. */
.motion [data-reveal] {
  opacity: 0;
  transform: translateY(14px);
}

.motion [data-reveal][data-shown] {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-reveal) var(--ease) var(--reveal-delay, 0ms),
    transform var(--dur-reveal) var(--ease) var(--reveal-delay, 0ms);
}

/* The heading's two lines are blocks whether or not anything animates: the markup carries a
   pair of spans instead of the <br> it used to, and a pair of inline spans would render as
   one line. Only the mask is conditional. */
.hero-copy h1 .line {
  display: block;
}

/* The hero heading, once per visit, per line. Each line is masked by its own box and rises
   into it: the words arrive as one object per line rather than as a block fading in, which
   is the difference between a page that starts and a page that appears.

   `overflow: clip` rather than `hidden`, because `hidden` would make each line a scroll
   container and clip the descender of «у» a pixel early. */
.motion .hero-copy h1 .line {
  overflow: clip;
}

.motion .hero-copy h1 .line > span {
  display: block;
  animation: line-rise var(--dur-reveal) var(--ease) both;
  animation-delay: var(--reveal-delay, 0ms);
}

@keyframes line-rise {
  from {
    opacity: 0;
    /* Its own height, so the line starts exactly outside its mask and no distance has to be
       guessed against a clamped font size. */
    transform: translateY(100%);
  }
}

/* The scroll line. Purpose: state — a storefront page is between two and five screens tall,
   and how much of one is left is otherwise unknowable without dragging the scrollbar. One
   pixel, no colour of its own, and driven by a transform: it is the cheapest honest answer.
   `--scroll` is written by public.js on <html> from one rAF-throttled listener. */
.scroll-line {
  position: absolute;
  inset-block-end: -1px;
  inset-inline: 0;
  height: 1px;
  background: var(--accent);
  opacity: 0.5;
  transform: scaleX(var(--scroll, 0));
  transform-origin: 0 50%;
  pointer-events: none;
}

/* Hover on a card. Purpose: response — a card that is a link target and a card that is not
   look identical at rest, and a border that sharpens under the pointer is the whole of
   the difference a visitor needs. No lift: depth is surface and line, not shadow
   (design-system §3), and these cards sit in flow.

   The sheen is the same trick the reference uses: a gradient large enough to travel inside
   its own box, masked to the border ring so only the outline lights, and paused until the
   pointer is on the card. A paused animation costs nothing; a running one costs a
   background-position interpolation on one composited element. */
.info-card,
.plan-card,
.tariff-card {
  position: relative;
  transition: border-color var(--dur-fast) var(--ease);
}

@media (hover: hover) {
  .info-card:hover,
  .plan-card:hover,
  .tariff-card:hover {
    border-color: var(--line-strong);
  }

  .info-card:hover::after,
  .plan-card:hover::after,
  .tariff-card:hover::after {
    opacity: 1;
    animation-play-state: running;
  }
}

.info-card::after,
.plan-card::after,
.tariff-card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  padding: 1px;
  opacity: 0;
  pointer-events: none;
  background:
    linear-gradient(115deg, transparent 35%, var(--accent-line) 50%, transparent 65%);
  background-size: 300% 300%;
  /* Ring only: the fill is excluded, so the gradient lights the border and never washes the
     text underneath it. */
  mask:
    linear-gradient(#000 0 0) content-box exclude,
    linear-gradient(#000 0 0);
  transition: opacity var(--dur-fast) var(--ease);
  animation: card-sheen 2.2s linear infinite paused;
}

@keyframes card-sheen {
  from {
    background-position: 15% 15%;
  }
  to {
    background-position: 85% 85%;
  }
}

/* The trailing icon on a call to action. Purpose: response, and it is the arrow's own
   direction — two pixels along the axis it points. The leading icon on `.btn.secondary`
   is not moved: an icon that labels does not travel. */
@media (hover: hover) {
  .site-main .btn > svg:last-child {
    transition: transform var(--dur-fast) var(--ease);
  }

  .site-main .btn:hover > svg:last-child {
    transform: translateX(2px);
  }
}

/* The copy button's confirmation. The icon already swaps to a check; the pop is what makes
   the swap read as "done" rather than as a redraw. Once per press, so `@keyframes` is
   correct here — but the press repeats, so the attribute is removed and re-added by
   public.js and the animation restarts from zero, which is the behaviour wanted. */
.copy-btn[data-copied] {
  animation: copy-pop var(--dur-base) var(--ease-spring);
}

@keyframes copy-pop {
  50% {
    transform: scale(1.14);
  }
}

/* The hero's frame. Purpose: none — this is decoration, and it is allowed here for the one
   reason the design system allows any: the hero is the single surface on the storefront
   whose job is the product's register rather than a value on screen. It moves nothing.

   The vignette pulls the corners toward the page background so the mark sits in a space
   instead of on a rectangle. The grain is a static SVG turbulence tile at two percent: it
   removes the flat-gradient banding that a large soft radial shows on an 8-bit panel. */
.hero {
  position: relative;
  isolation: isolate;
}

.hero::before,
.hero::after {
  content: "";
  position: absolute;
  /* Vertically past the hero's box, because a vignette with a visible top edge is a
     rectangle; horizontally flush with it, because the hero sits 40px from the document
     edge and a horizontal bleed measured in percent put 42px of gradient outside the
     viewport — which the browser counts as scrollable width and draws a scrollbar for, on
     every page, the exact regression the 520px header rules exist to record. The radial
     stop below fades out at 58%, so nothing is cut. */
  inset-block: -10%;
  inset-inline: 0;
  z-index: -1;
  pointer-events: none;
}

.hero::before {
  background: radial-gradient(120% 90% at 68% 45%, var(--surface-2) 0%, transparent 58%);
  opacity: 0.7;
}

.hero::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  opacity: 0.05;
  mix-blend-mode: overlay;
}

/* The mark drifts against the scroll at a twentieth of it and dims as it leaves. Purpose:
   spatial coherence — the hero is the one place on the page with a foreground and a
   background, and nothing else on the storefront claims depth.

   The fade is on the mark alone and never on the copy: text that dims while it is still
   readable is text the visitor loses mid-sentence, which is the failure mode this kind of
   effect is known for. The mark carries no information, so it is the only thing here that
   may leave.

   Both values come from properties one listener writes on <html>. Under reduced motion
   public.js writes neither, and the fallbacks — `0px` and `1` — park the mark at rest at
   full opacity, so the calm variant needs no second rule. */
.hero-mark {
  transform: translate3d(0, calc(var(--sy, 0px) * 0.05), 0);
  opacity: var(--hero-out, 1);
}

/* Section headings stay while their section passes. Purpose: orientation — the storefront's
   long pages (docs, models) scroll past the heading that says what is being read.
   Translucent rather than solid so the content does not appear to end at a line, matching
   the header's own treatment; the fallback background is opaque enough to read against
   where `backdrop-filter` is missing.

   The offset is the header's height plus a hairline, not less: the header is `position:
   sticky` at `z-index: 60` and this is at 2, so a heading pinned any higher is a heading
   drawn underneath the nav. Measured — at `--topbar-h` minus 24px the pinned heading sat
   23px inside the header's box and its cap line was hidden.

   Only from 901px. Below that the header is already 62px of a short viewport, and a second
   pinned band would leave a phone reading through a slot. */
@media (min-width: 901px) {
  .site-section > h2 {
    position: sticky;
    inset-block-start: calc(var(--topbar-h) + 1px);
    z-index: 2;
    padding-block: 6px;
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: blur(10px);
  }
}

/* The footer wordmark. Decoration, on the one surface where the page has already ended:
   nothing below it is read, and a visitor who reached it has finished. Clipped by the
   footer, never selectable, never announced. */
.site-foot {
  position: relative;
  overflow: hidden;
}

.foot-ghost {
  position: absolute;
  inset-block-end: -0.24em;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  z-index: 0;
  font-family: var(--font-display);
  font-size: clamp(88px, 19vw, 260px);
  font-weight: 500;
  line-height: 0.78;
  letter-spacing: -0.04em;
  color: var(--text);
  opacity: 0.05;
  white-space: nowrap;
  user-select: none;
  pointer-events: none;
}

.site-foot-inner {
  position: relative;
  z-index: 1;
}

/* Route change. Purpose: spatial coherence — the storefront replaces `#page` wholesale, and
   a cross-fade of a quarter of a second says the address changed rather than that the
   document blinked. The panel deliberately has none of this (its section switch is the
   operator's most frequent action); a visitor changes storefront page a handful of times.

   Named on the two boxes that actually differ. Without a name the whole root cross-fades,
   which would fade the header and the footer as well — they did not change. */
.site-main {
  view-transition-name: page;
}

::view-transition-old(page),
::view-transition-new(page) {
  animation-duration: var(--dur-base);
  animation-timing-function: var(--ease);
}

::view-transition-old(page) {
  animation-name: vt-out;
}

::view-transition-new(page) {
  animation-name: vt-in;
}

@keyframes vt-out {
  to {
    opacity: 0;
    transform: translateY(-6px);
  }
}

@keyframes vt-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* Reduced motion. The blanket rule in styles.css already collapses every duration here, so
   this block only handles the two cases where collapsing is not enough: a reveal that never
   arrives and a sheen frozen mid-travel.

   The reveal is not merely instant — the pre-state is dropped entirely, so an element is
   visible even if its observer never fires. The lift on hover stays: it is two pixels under
   the pointer, not a surface crossing the viewport, and losing it would cost the one signal
   that says a card is interactive. */
@media (prefers-reduced-motion: reduce) {
  .motion [data-reveal] {
    opacity: 1;
    transform: none;
  }

  .motion .hero-copy h1 .line > span {
    animation: none !important;
  }

  .info-card::after,
  .plan-card::after,
  .tariff-card::after {
    display: none;
  }
}

/* 13. Free key check ===================================================== */

/* The form is a column, not a grid: three fields where each hint is a sentence, and a
   two-column layout would put a hint beside the wrong input on the first narrow width. */
.check-form {
  display: grid;
  gap: 18px;
  max-width: 620px;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.check-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}

.check-note {
  font-size: 12.5px;
}

.check-output {
  margin-block-start: 24px;
  max-width: 720px;
}

.check-progress {
  display: grid;
  gap: 10px;
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}

.check-report {
  display: grid;
  gap: 16px;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/* The verdict carries its own border colour and nothing else. No filled banner: a red
   block over somebody else's vendor reads as an accusation, and what we have is one run of
   observations (docs/upstream-audit.md, «Находка, а не приговор»). */
.check-verdict {
  display: grid;
  gap: 6px;
  padding-inline-start: 14px;
  border-inline-start: 2px solid var(--line-strong);
}

.check-verdict strong {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 500;
}

.check-verdict p {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

.check-verdict[data-verdict="pass"] {
  border-inline-start-color: var(--ok);
}

.check-verdict[data-verdict="risk"] {
  border-inline-start-color: var(--warn);
}

/* An error and "no conclusion" share a colour on purpose: both mean we have nothing to
   report, and giving the failure a danger colour would suggest a finding about the
   vendor. */
.check-verdict[data-verdict="error"],
.check-verdict[data-verdict="inconclusive"],
.check-verdict[data-verdict="unknown"] {
  border-inline-start-color: var(--line-strong);
}

.check-findings {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.check-findings li {
  display: grid;
  gap: 3px;
  padding: 11px 0;
  border-block-start: 1px solid var(--line-soft);
}

.check-finding-title {
  font-size: 13.5px;
  font-weight: 560;
}

.check-finding-detail {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-muted);
  /* A vendor's own text, which regularly arrives as one unbroken line. */
  overflow-wrap: anywhere;
}

/* Severity colours the title, and it is never the only marker: the finding's own words say
   what happened, which is what the four accessibility signals require (docs/design-system.md
   §8). A coloured row would also read as a verdict about the vendor rather than as one
   observation among several. */

.check-findings li[data-severity="fail"] .check-finding-title {
  color: var(--danger);
}

.check-findings li[data-severity="warn"] .check-finding-title {
  color: var(--warn);
}

.check-disclaimer {
  font-size: 12.5px;
  line-height: 1.55;
}

/* 14. Физика и оптика ===================================================== */
/* Раздел закреплён `docs/design-system.md` §10 и `SYSTEM_RULES.md` D-8. До него витрина
   не имела ни одного `:active`: отклик появлялся на `click`, то есть после отпускания,
   и нажатие читалось как «ничего не произошло». */

/* §10.1 — отклик возникает на нажатии. Сдвиг на один пиксель и смена поверхности
   вместо масштабирования: карточки витрины крупные, и уменьшение целой карточки на
   касание читается как «уехала», а не как «нажата». */
.site-nav a:active,
.site-foot-nav a:active,
.doc-toc a:active,
.status-chip:active,
.plan-details-btn:active {
  background: var(--surface-3);
  transform: translateY(1px);
  transition-duration: var(--dur-instant);
}

.info-card:active,
.plan-card:active,
.tariff-card:active {
  transform: translateY(1px);
  transition-duration: var(--dur-instant);
}

/* Строка каталога — тоже цель нажатия: по ней открывается карточка модели. */
.model-row:active {
  background: var(--surface-2);
  transition-duration: var(--dur-instant);
}

/* §10.5 — граница плавающей обвязки с содержимым: затухание вместо линии в 1px. Линия
   разрезала страницу там, где ничего не заканчивается; маска показывает, что содержимое
   уходит под материал. Линия возвращается ниже — там, где материал становится плотным
   и затухание перестаёт читаться. */
.site-head {
  border-block-end: 0;
  box-shadow: 0 1px 0 0 color-mix(in srgb, var(--line-soft) 60%, transparent),
    0 12px 24px -20px color-mix(in srgb, #000 60%, transparent);
}

/* §10.7 — плотный материал, когда пользователь просил меньше прозрачности. Размытие
   снимается целиком: полупрозрачный слой без размытия — это просто мутный фон. */
@media (prefers-reduced-transparency: reduce) {
  .site-head {
    background: var(--bg);
    backdrop-filter: none;
    border-block-end: 1px solid var(--line);
    box-shadow: none;
  }

  .plan-card,
  .info-card,
  .tariff-card {
    backdrop-filter: none;
  }
}

/* §10.7 — больше контраста: почти сплошной фон и явная граница вместо мягких линий,
   потому что мягкая линия при повышенном контрасте не читается вовсе. */
@media (prefers-contrast: more) {
  .site-head {
    background: var(--bg);
    backdrop-filter: none;
    border-block-end: 1px solid var(--line-strong);
    box-shadow: none;
  }

  .plan-card,
  .info-card,
  .tariff-card,
  .status-chip,
  .plan-details-btn {
    border-color: var(--line-strong);
  }

  .site-nav a[aria-current="page"] {
    outline: 1px solid var(--line-strong);
    outline-offset: -1px;
  }
}

/* §10.1 + §10.7 — уменьшенное движение не отменяет отклик: перелёты и сдвиги убираются,
   смена поверхности остаётся, иначе нажатие снова становится незаметным. */
@media (prefers-reduced-motion: reduce) {
  .site-nav a:active,
  .site-foot-nav a:active,
  .doc-toc a:active,
  .status-chip:active,
  .plan-details-btn:active,
  .info-card:active,
  .plan-card:active,
  .tariff-card:active {
    transform: none;
  }

  /* Карточки двигались на один пиксель и больше ничем не отвечали, поэтому без движения
     нажатие переставало читаться вовсе. Поверхность и линия — отклик, который не двигает
     ничего на экране. */
  .info-card:active,
  .plan-card:active,
  .tariff-card:active {
    background: var(--surface-2);
    border-color: var(--line-strong);
  }
}
