/* ============================
   Stealtify — Global Styles

   Design tokens mirror the Android app (Material 3 Dark, Surface family):
   dark neutral surfaces + a single plum accent. No blue, no neon.
   The --stx-* block is the source of truth; the semantic variables below
   it map onto those tokens, so re-theming means editing one block.
   ============================ */

/* ─── Inter, served from here ───
   The site used to pull this from Google Fonts: two preconnects and a
   render-blocking stylesheet to a third party, in front of every page, on a
   product whose whole claim is that it reports nothing about you anywhere. A
   request to fonts.gstatic.com hands over the reader's address and user agent
   as surely as a tracker would, and it did so before the first paint, so
   nobody could have opted out of it. It also cost the page two DNS lookups and
   two TLS handshakes on a host that had nothing else to give it.

   One variable file per subset covers 400 through 700, so the four weights the
   site uses are four instances of one download rather than four downloads. The
   unicode-range lines are what keep that honest: a browser fetches a subset
   only once a character in its range is actually on the page, so a Russian
   reader gets cyrillic and latin and never sees the other two.

   Ranges are copied verbatim from what Google's own css2 endpoint serves for
   Inter, so the split is the same one those files were cut for. If the font is
   ever regenerated, they have to be re-copied together — a range that does not
   match its file silently leaves characters to the fallback. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/inter-cyrillic.woff2?v=20260827a') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/inter-cyrillic-ext.woff2?v=20260827a') format('woff2');
    unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/inter-latin.woff2?v=20260827a') 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: 'Inter';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/inter-latin-ext.woff2?v=20260827a') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
    /* ─── App design tokens (Material 3 Dark, Surface family) ─── */
    --stx-surface-dark:            #1A1A1F; /* surface / page background */
    --stx-surface-variant-dark:    #24242B; /* surfaceVariant — inline examples, table heads */
    --stx-surface-container-dark:  #1F1F25; /* primaryContainer — cards */
    --stx-surface-container-high:  #2A2A31; /* secondaryContainer — card hover */
    --stx-tertiary-container:      #272433; /* tertiaryContainer — chips, badges */
    --stx-accent-plum:             #3A1840; /* brand accent — plum / deep magenta */
    --stx-accent-plum-deep:        #1A0020; /* deep plum, gradient end */
    --stx-on-tertiary:             #E6D6EA; /* text on plum surfaces */

    /* Derived from the accent above — the raw plum is too dark to read as text
       on a dark background, so headings and links use these lighter tints. */
    --stx-accent-plum-light:       #C9A6D4;
    --stx-accent-plum-mid:         #9B6BA8;
    --stx-accent-plum-soft:        #6E3577;

    /* ─── Amber ───
       The resin, not the metal: orange rather than gold, and built from
       translucent layers so the dark surface reads through it like light
       through a chip of amber.

       SCOPE: the icon plates, the download card, and — since 2026-08-20, on
       request — the "read more" chips at the foot of each block. Plum remains
       the accent for everything else: structure, interaction, states, the
       hero, and every link that is not one of those ten chips.

       The chips were tried in plum twice first and stayed quiet both times,
       which is the argument for the widening: plum cannot separate from a page
       that is already plum. Amber now marks the two things a reader is meant
       to act on — take the app, or go deeper — and nothing else.

       Do not widen this further without being asked to. */
    /* Cognac, not honey. Hue sits at 21-26 degrees across the whole ramp;
       gold lives around 45 and is what the earlier version kept reading as.
       Nothing here is a cream or a pale tint — the lightest stop is already a
       ginger, so the piece never looks yellow. */
    --stx-amber-glow:              #F2A868; /* light through the piece — gloss, glyph top */
    --stx-amber-lit:               #E68C46;
    --stx-amber:                   #DA7328; /* core ginger */
    --stx-amber-core:              #B85A1C; /* saturated rust, chip middle */
    --stx-amber-dark:              #3E1B08; /* deep end of the chip */

    /* Translucent layers. The alphas are what make it read as resin instead of
       paint — over the card surface they composite to #533322 / #41281e /
       #2d1d18, and the glyph is checked against all three. */
    --stx-amber-face:              rgba(184, 90, 28, 0.34);
    --stx-amber-body:              rgba(138, 58, 16, 0.32);
    --stx-amber-base:              rgba(62, 27, 8, 0.44);
    --stx-amber-edge:              rgba(218, 115, 40, 0.45);
    --stx-amber-halo:              rgba(184, 90, 28, 0.22);

    /* Chip and glyph run the same direction and stay in separate bands, so the
       glyph never lands on a chip stop it cannot beat. Worst of the nine
       combinations: 3.47:1, above the 3:1 non-text minimum. */
    --gradient-amber-chip: linear-gradient(150deg, var(--stx-amber-face) 0%, var(--stx-amber-body) 48%, var(--stx-amber-base) 100%);
    --gradient-amber-glyph: linear-gradient(150deg, var(--stx-amber-glow) 0%, var(--stx-amber-lit) 45%, var(--stx-amber) 100%);

    /* ─── Semantic layer ─── */
    --bg-primary: var(--stx-surface-dark);
    --bg-secondary: var(--stx-surface-container-dark);
    --bg-card: var(--stx-surface-container-dark);
    --bg-card-hover: var(--stx-surface-container-high);
    --bg-inset: var(--stx-surface-variant-dark);
    --border-color: rgba(255, 255, 255, 0.07);
    --border-hover: rgba(230, 214, 234, 0.22);

    --text-primary: #F2EFF4;
    --text-secondary: #A9A3B4;
    /* Lightened twice. #7C7688 gave 3.95:1 on --stx-surface-dark and failed
       WCAG AA outright; #8F899C fixed that on the page background but was
       measured against the wrong surface — almost everything this colour is
       used on sits on a card or an inset, which are lighter than the page, and
       there it fell back to 4.86:1 and 4.57:1. A hair over the AA line is not
       the same as legible: at 12–13px, which is the size of every label this
       token dresses, it read as greyed-out rather than quiet, and that is the
       "make the small captions more noticeable" report.

       Measured 2026-08-20 over CDP across the whole home page. Contrast, old
       → new, on the three surfaces this lands on:
         --bg-inset  (#242 42B-ish, the lightest)  4.57 → 5.54
         --bg-card                                 4.86 → 5.89
         page background                           5.14 → 6.23
       Still a clear step below --text-secondary (6.19–7.09), so the two tiers
       stay distinguishable — this lifts the floor, it does not merge them. */
    --text-muted: #9E98AD;

    --accent-primary: var(--stx-accent-plum-light);
    --accent-secondary: var(--stx-on-tertiary);
    --accent-glow: rgba(58, 24, 64, 0.55);

    /* CTAs and large fills: plum → deep plum. */
    --gradient-main: linear-gradient(135deg, var(--stx-accent-plum) 0%, var(--stx-accent-plum-deep) 100%);
    /* Text set with background-clip needs the light tints to stay legible. */
    --gradient-text: linear-gradient(135deg, var(--stx-on-tertiary) 0%, var(--stx-accent-plum-light) 55%, var(--stx-accent-plum-mid) 100%);
    --gradient-subtle: linear-gradient(135deg, rgba(58, 24, 64, 0.55), rgba(39, 36, 51, 0.55));

    /* Soft, neutral shadows — no blue tint. */
    --shadow-card: 0 8px 28px rgba(0, 0, 0, 0.38);
    --shadow-cta: 0 6px 24px var(--accent-glow);

    --font-family: 'Inter', 'Segoe UI', 'SF Pro Text', Roboto, system-ui, sans-serif;
    --font-mono: 'SF Mono', 'Fira Code', 'Consolas', ui-monospace, monospace;

    --radius-sm: 10px;
    --radius-md: 14px;
    --radius-lg: 18px;  /* cards: 16–20px */
    --radius-xl: 20px;

    --transition: 0.18s cubic-bezier(0.4, 0, 0.2, 1);
    --transition-fast: 0.16s cubic-bezier(0.4, 0, 0.2, 1);
}

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

html {
    scroll-behavior: smooth;
    /* The navbar is fixed, so an anchor that scrolls its target to y=0 parks it
       underneath the bar: the heading is off screen and the reader lands on the
       middle of the section with no idea what they are looking at. This is the
       height of the bar plus a little air, and the browser subtracts it from
       every anchor jump — the menu, the footer, and a link arriving from
       another page alike. */
    scroll-padding-top: 96px;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

body {
    font-family: var(--font-family);
    background: var(--stx-surface-dark);
    color: var(--text-primary);
    line-height: 1.6;
    overflow-x: hidden;
}

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

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

.container {
    max-width: 1400px;
    margin: 0 auto;
    padding: 0 40px;
}

/* ============================
   Navigation
   ============================ */

/* The bar must not change height when .scrolled is toggled, or the whole page
   appears to jump — so padding is fixed and only the backdrop layer changes. */
/* Uneven padding on purpose. The bar is fixed to the top of the window, so its
   top edge is the edge of the screen, and 14px of it under a 52px mark left the
   logo touching the browser chrome — which reads as the row of links being out
   of alignment rather than as the bar being short. 22px above and 14px below
   sits the block optically in the middle of what the eye takes for the header.
   The mobile bar was never the problem and keeps the even padding below. */
.navbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    padding: 22px 0 14px;
}

/* The backdrop is a pseudo-element, NOT the .navbar box itself.
   backdrop-filter establishes a containing block for fixed-position
   descendants, and the mobile menu overlay (.nav-links) is a descendant: with
   the filter on .navbar, the overlay's inset:0 resolved against the bar instead
   of the viewport, so it only ever covered the header strip and page content
   showed through it. Keeping the filter on ::before leaves .navbar a plain
   positioned box, and the overlay covers the viewport again.
   The border lives here too, so it cannot affect the header's height. */
.navbar::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    border-bottom: 1px solid transparent;
    background-color: rgba(26, 26, 31, 0);
    /* Explicit property list: `all` would animate layout properties too. */
    transition: background-color var(--transition),
                border-color var(--transition),
                box-shadow var(--transition);
}

/* No backdrop-filter here, deliberately. A backdrop filter on a fixed bar is
   re-read and re-blurred from the page behind it on every frame of every
   scroll, and this bar is 92% opaque — the blur was deciding under a twelfth
   of each pixel. Raised to 0.96 and dropped: the bar looks the same and the
   scroll costs a full-width readback less. */
.navbar.scrolled::before {
    background-color: rgba(26, 26, 31, 0.96);
    border-bottom-color: var(--border-color);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}

.nav-container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    max-width: 1400px;
    margin: 0 auto;
    padding: 0 40px;
}

.nav-logo {
    display: flex;
    align-items: center;
    gap: 10px;
    font-weight: 700;
    font-size: 1.25rem;
    transition: var(--transition);
}

.nav-logo:hover {
    opacity: 0.8;
}

.logo-icon {
    width: 52px;
    height: 52px;
    object-fit: contain;
    /* Мягкое плам-свечение */
    box-shadow: 0 0 16px 2px rgba(201, 166, 212, 0.16);
    border-radius: 16px;
    /* Фон-подложка */
    background: linear-gradient(135deg, rgba(110, 53, 119, 0.35) 0%, rgba(39, 36, 51, 0.55) 100%);
    padding: 6px;
}

.logo-text {
    background: var(--gradient-text);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}

.nav-links {
    display: flex;
    align-items: center;
    gap: 32px;
}

.nav-links a {
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--text-secondary);
    transition: var(--transition);
    position: relative;
}

.nav-links a:hover {
    color: var(--text-primary);
}

.nav-links a::after {
    content: '';
    position: absolute;
    bottom: -4px;
    left: 0;
    width: 0;
    height: 2px;
    background: var(--gradient-text);
    border-radius: 1px;
    transition: var(--transition);
}

.nav-links a:hover::after {
    width: 100%;
}

/* Language Switcher */
.lang-switcher {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border-radius: 100px;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
    font-family: var(--font-family);
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
    transition: var(--transition);
    white-space: nowrap;
}

.lang-switcher:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-hover);
    color: var(--text-primary);
}



.nav-right-mobile {
    display: none;
    align-items: center;
    gap: 12px;
}

/* Padded out to a finger, not to a cursor. Measured 2026-08-20 at 360, 390,
   430 and 768: the button was 32×24, which is the three bars plus 4px of
   padding and nothing else. That is the bare floor of WCAG 2.5.8 and it is the
   one control on a phone that has to be hit before anything else on the page
   can be reached — the language button beside it is already 59×44.

   The bars keep their own size; only the box around them grows, and it is
   centred so the mark does not move. */
.nav-toggle {
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 4px;
    min-width: 44px;
    min-height: 44px;
}

.nav-toggle span {
    width: 24px;
    height: 2px;
    background: var(--text-primary);
    border-radius: 2px;
    transition: var(--transition);
}

/* ============================
   Buttons
   ============================ */

.btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 12px 24px;
    border-radius: var(--radius-md);
    font-family: var(--font-family);
    font-size: 0.95rem;
    font-weight: 600;
    cursor: pointer;
    transition: var(--transition);
    border: none;
    white-space: nowrap;
}

.btn-primary {
    background: var(--gradient-main);
    color: var(--stx-on-tertiary);
    border: 1px solid var(--stx-accent-plum-soft);
    box-shadow: var(--shadow-cta);
}

.btn-primary:hover {
    box-shadow: 0 8px 32px var(--accent-glow);
    transform: translateY(-2px);
    filter: brightness(1.25);
}

.btn-secondary {
    background: var(--bg-card);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
}

.btn-secondary:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-hover);
    transform: translateY(-2px);
}

.btn-large {
    padding: 16px 32px;
    font-size: 1.05rem;
    border-radius: var(--radius-lg);
}

/* ============================
   Hero Section
   ============================ */

.hero {
    /* Declared here, not on .hero-graphic, because the FAQ panel keeps time
       with it too and is not a descendant of the graphic. The phase table that
       explains what the beat is divided into lives on .hero-graphic. */
    --hero-beat: 3.2s;
    position: relative;
    min-height: 100vh;
    display: flex;
    /* Centred, and it stays centred. Top-aligning both axes was tried as a way
       of stopping the first screen moving when an answer opens, and it does
       stop it — by moving the headline up half a screen and leaving the hero
       bottom-heavy, which is a worse trade than the thing it fixed. The height
       is meant to adjust; what it must not do is jump, and that is handled
       where it belongs: the answer's height is animated, and the details is
       closed before the inline height is released so there is no frame where
       the answer is full-height and still open. */
    align-items: center;
    justify-content: center;
    padding: 120px 0 80px;
    overflow: hidden;
}

.hero-bg {
    position: absolute;
    inset: 0;
    z-index: 0;
}

/* ─── The floor of the hero ───
   The second glow sits low and to the right with its centre below the fold, so
   the bottom of the section is the lightest plum on the page — and the section
   under it is the flat page background. That edge read as a seam straight
   across the viewport. This washes the last 300px back down to exactly the
   colour the next section starts at, so there is no line to see.

   Between the glows and the graphic, which is the whole trick: it has to cover
   the light — that is what the seam was made of — and it must not cover the
   routes, which run along the bottom of the frame and were being wiped out
   with it. Inside .hero-bg it is the last child, so it paints over the two
   glows; .hero-graphic then lifts itself over it with a z-index, and the lines
   come back.

   Same RGB at every stop, only the alpha moves: a gradient that fades to
   transparent through a *different* colour greys the middle of the ramp and
   trades one visible edge for two. The stops are weighted late because the eye
   catches the start of a ramp long before its end. */
.hero-bg::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 300px;
    z-index: 0;
    pointer-events: none;
    background: linear-gradient(to bottom,
                rgba(26, 26, 31, 0) 0%,
                rgba(26, 26, 31, 0.28) 38%,
                rgba(26, 26, 31, 0.72) 68%,
                rgba(26, 26, 31, 0.95) 87%,
                var(--stx-surface-dark) 100%);
}

/* will-change on a pair of static elements, which looks wrong and is not: the
   two glows are 1250x720 and 1200x1100 with a 120px blur on them, and without
   a layer of their own that blur is re-rasterised every time anything in the
   hero repaints — which, with the graphic animating, is every frame. Promoted,
   they are rasterised once and only composited afterwards. */
.hero-glow {
    position: absolute;
    border-radius: 50%;
    filter: blur(120px);
    will-change: transform;
}

/* Sits behind the headline, not the top-right corner. The eye goes to the
   brightest thing on a dark screen before it reads anything, so the lightest
   patch of the hero is the one the h1 stands on. */
.hero-glow-1 {
    width: 1250px;
    height: 720px;
    top: 2%;
    left: -280px;
    background: linear-gradient(135deg, rgba(110, 53, 119, 0.5) 0%, rgba(58, 24, 64, 0.28) 100%);
}

/* Counterweight, low and far right, so the left does not read as lit by
   accident. Kept dimmer than the first — it is ambience, not a focal point. */
.hero-glow-2 {
    width: 1200px;
    height: 1100px;
    bottom: -380px;
    right: -240px;
    background: var(--stx-accent-plum);
    opacity: 0.2;
}



/* v2: two columns — the pitch on the left, the FAQ on the right — instead of
   one centred 800px block. The width and the auto margins come from .container
   on the same element now; the explicit width is needed because .hero is a
   flex container and an auto-width flex item would shrink to its content. */
/* The gap is not breathing room, it is the channel the supergraphic runs
   through, and it has to be wider than the hub: the app's rings reach 58 units
   either side of centre, and at 120px the FAQ panel was sitting on top of them.
   240px clears the circle by about 30 units at every width worth tuning for.

   The FAQ is pushed off the container grid to buy that room — it starts later
   and runs to the container edge, so the two columns are not mirror images.
   That asymmetry is the point, not a rounding error. */
.hero-content {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 1620px;
    display: grid;
    /* The copy column carries a sentence headline now and needs the wider
       share of what the channel leaves. */
    grid-template-columns: minmax(0, 1.12fr) minmax(0, 1fr);
    gap: 240px;
    /* Centred, like the block itself — see .hero above for why top alignment
       was tried and dropped. */
    align-items: center;
    text-align: left;
}

/* The headline is a sentence, not a two-word slogan, so it cannot be set at
   68px — a clause would wrap in the middle of the phrase. It holds the eye on
   mass and contrast instead: two full lines of it against a 34px FAQ heading.

   The size is bounded by whichever clause is longer, because each has a line
   to itself and neither may wrap. That is now the FIRST line: "Telegram —
   через прокси." at 24 characters, against 16 in the second. It used to be the
   second, when that line still named Госуслуги alongside the bank.

   2.6vw keeps the longer clause on one line at 1400px and the 2.7rem ceiling
   keeps it there on wider screens. Both were set against a 28-character line,
   so there is roughly four characters of slack in them today — the type could
   go up about a tenth before the first line starts to wrap. Left alone
   deliberately: check it against the real font before spending that slack, and
   remember the English line is longer still at 27 characters. */
.hero-title {
    /* 37.6px at 769 → 43px at 1440 and above.
       A plain vw term is what made this too small everywhere except the
       widest screens: 2.6vw of a 1024px laptop is 26px, and the h1 came out
       barely larger than the FAQ's h2 beside it. A rem base plus a gentler
       slope holds the size up across the whole middle of the range and still
       lands on the same 43px ceiling it always had.

       The floor is the size the ≤768 rules set, so the two meet without a step
       at the breakpoint. What lets it be this large is the hero stacking below
       1200: the headline has the full width there, and 24 characters at 40px
       need about 480 of it. */
    font-size: clamp(2.35rem, 1.575rem + 1.25vw, 2.7rem);
    font-weight: 700;
    line-height: 1.14;
    letter-spacing: -0.02em;
    /* If it wraps anyway — longer translation, narrower window — wrap it
       evenly rather than leaving one word alone on the last line. */
    text-wrap: balance;
    margin-bottom: 26px;
    animation: fadeInUp 0.8s ease 0.1s both;
}

.gradient-text {
    background: var(--gradient-text);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}

.hero-subtitle {
    font-size: clamp(1rem, 2.5vw, 1.2rem);
    color: var(--text-secondary);
    /* Held short so the headline keeps the visual mass in this column. */
    max-width: 30em;
    margin: 0 0 40px;
    line-height: 1.7;
    animation: fadeInUp 0.8s ease 0.2s both;
}

.hero-actions {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 16px;
    flex-wrap: wrap;
    animation: fadeInUp 0.8s ease 0.3s both;
}

/* ─── The FAQ in the hero's right half ─── */

.hero-faq {
    animation: fadeInUp 0.8s ease 0.4s both;
}

/* A real heading, not a label — but capped well under the h1 so the hierarchy
   between the two columns stays obvious at a glance. */
/* 28px under the heading against 14px between the questions: the heading has
   to belong to the list more than the questions belong to each other, and at
   the old 24 against 14 those two gaps were near enough equal that the title
   read as the first item in the column rather than as its name. */
.hero-faq-title {
    /* Same treatment as the headline above, and for the same reason. */
    font-size: clamp(1.75rem, 1.2rem + 0.9vw, 2.1rem);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.02em;
    color: var(--text-primary);
    margin-bottom: 44px;
}

/* Translucent, so the routing graphic carries on behind the column instead of
   being boxed out by five opaque cards.

   No backdrop-filter, and that is the accordion's stutter. A backdrop filter
   is recomputed whenever anything under it changes, and what is under these
   five cards is a graphic animating on every frame — so every one of them was
   re-blurring the moving lines continuously, and opening or closing one, which
   moves the other four, made the browser redo all five mid-gesture. At 82%
   opacity the blur was deciding under a fifth of each pixel. Raised to 0.86
   and dropped: the lines still show through, the cards no longer cost a
   readback apiece. */
.hero-faq .faq-item {
    background: rgba(31, 31, 37, 0.86);
}

.hero-faq .faq-item:hover {
    background: rgba(42, 42, 49, 0.85);
}

/* Roomier than the old full-width section, not tighter: the questions are half
   the offer on this screen, so they are sized to be read rather than skimmed. */
.hero-faq .faq-list {
    gap: 14px;
}

.hero-faq .faq-item > summary {
    padding: 21px 26px;
    font-size: 1.05rem;
}

/* The height is set in pixels by script.js for the length of the open or
   close and then released; this is the curve it travels on. Nothing else in
   the file transitions height, and for good reason — height is a layout
   property and animating it costs a reflow a frame. It is worth it exactly
   here: this list sits inside a hero that centres its content, so an instant
   open moves the headline and the buttons with it, and the jerk is what gets
   noticed instead of the answer. Five answers, one at a time, is a reflow the
   page can afford. */
/* padding-bottom is in the transition, and that is the whole of the flinch on
   close. Everything on this page is border-box, so an element cannot be
   shorter than its own padding: height animates down to 0 and the box still
   occupies 22px, which the browser then takes away in one frame when `open`
   goes. Animating the padding with the height means the box really does reach
   zero before anything is removed. */
.hero-faq .faq-answer {
    padding: 0 26px 22px;
    transition: height 0.34s cubic-bezier(0.22, 1, 0.36, 1),
                padding-bottom 0.34s cubic-bezier(0.22, 1, 0.36, 1);
}

/* The open card is a target for closing it — see the handler in script.js. */
.hero-faq .faq-item[open] {
    cursor: pointer;
}

.hero-faq .faq-item[open] a {
    cursor: auto;
}

.hero-faq-more {
    margin-top: 24px;
    font-size: 0.95rem;
}

/* ─── The closed questions light in turn ───
   A light travelling down the list, not five separate flashes. Each question
   is lit for two fifths of a pass while its neighbours are offset by one
   fifth, so two are always glowing — one rising, one falling — and the peak
   glides from card to card without the list ever going dark. Item five's fall
   overlaps item one's rise, so the wrap is a handover too.

   A pass is two beats. Locking each card to its own beat, as this first did,
   put a beat and a half of black between one card and the next: five discrete
   blinks sixteen seconds apart rather than anything flowing.

   A question that is open drops out: it is being read, and a pulse under the
   text would be noise.

   Carried on a pseudo-element rather than the card's own border, because the
   card already transitions border-color and background on hover and an
   animation on those properties would win every one of those exchanges. */
.hero-faq .faq-item {
    position: relative;
}

/* On every item, open or closed — and that is the point. Written as
   `:not([open])`, the whole pseudo-element came into existence the instant a
   card closed, and the animation it carries came with it at whatever phase the
   shared clock happened to be at: often mid-flare, which is a rim of light
   snapping on the moment the answer finishes collapsing. That flash is what
   reads as the card twitching after it closes.

   Existing always and being switched off by `animation: none` while open means
   the resting state is opacity 0 either way, and closing re-creates the
   animation from its first frame — which is also opacity 0. Nothing appears
   abruptly. The cost is that a card which has been open rejoins the wave on
   its own phase rather than the list's; over a 2.5s cycle nobody can see it. */
.hero-faq .faq-item::after {
    content: '';
    position: absolute;
    inset: -1px;
    border: 1px solid var(--stx-accent-plum-light);
    border-radius: inherit;
    box-shadow: 0 0 20px rgba(201, 166, 212, 0.16),
                inset 0 0 14px rgba(201, 166, 212, 0.07);
    opacity: 0;
    pointer-events: none;
    animation: faqShimmer calc(var(--hero-beat) * 2) linear infinite;
}

.hero-faq .faq-item[open]::after {
    animation: none;
}

/* One fifth of a pass apart — 1.28s — which is the speed the light appears to
   travel at. The pass is a whole number of beats, so the wave stays periodic
   with the graphic even though no single card is pinned to a beat. */
.hero-faq .faq-item:nth-child(2)::after { animation-delay: calc(var(--hero-beat) * 0.4); }
.hero-faq .faq-item:nth-child(3)::after { animation-delay: calc(var(--hero-beat) * 0.8); }
.hero-faq .faq-item:nth-child(4)::after { animation-delay: calc(var(--hero-beat) * 1.2); }
.hero-faq .faq-item:nth-child(5)::after { animation-delay: calc(var(--hero-beat) * 1.6); }

/* Up over 1.28s and down over 1.28s on a sine ease, so there is no corner at
   the peak and no straight edge on either ramp — a swell passing through,
   rather than something switching on and off. */
@keyframes faqShimmer {
    0%   { opacity: 0; animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1); }
    20%  { opacity: 1; animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1); }
    40%  { opacity: 0; }
    100% { opacity: 0; }
}

/* Still wider than the hub — below this the circle starts touching the panel
   again, which is the one thing the channel exists to prevent. Narrowing it
   here is what keeps the two columns alive down to 1200: at 240px the copy
   column is 507px on a 1280 screen and the headline's longest clause wants
   494 of it, which it does not get once the gutters are counted. */
@media (max-width: 1400px) {
    .hero-content {
        gap: 160px;
    }
}

/* ─── The hero is two columns only while both of them fit ───
   The channel between them has to stay wider than the hub, and the copy column
   gets a shade over half of what is left: at 1200 that is 528px, which holds
   the headline's longest clause on one line at the size the scale asks for
   there. At 1024 the same arithmetic leaves 414px against a clause that wants
   456, and the only way the old layout survived was by setting the headline
   small enough to disappear — 26px, barely larger than the h2 next to it.

   So the pair splits at 1200 instead, which is also where the navigation folds
   and the page stops being a desktop layout. Stacked, the headline has the
   whole width and can be the size it should be. */
@media (max-width: 1200px) {
    .hero-content {
        grid-template-columns: 1fr;
        gap: 48px;
    }

    /* Stacked, the questions have no column to sit in — hold them to a
       readable measure instead of letting them run the full width. */
    .hero-faq {
        max-width: 640px;
    }
}

/* ============================
   Section Headers
   ============================ */

.section-header {
    text-align: center;
    margin-bottom: 64px;
}

.section-title {
    font-size: clamp(1.75rem, 4.5vw, 2.75rem);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.02em;
    margin-bottom: 16px;
}

.section-subtitle {
    font-size: 1.1rem;
    color: var(--text-secondary);
    max-width: 550px;
    margin: 0 auto;
}

/* ============================
   Features Section
   ============================ */

/* The overhang numbers live on the section, not on the carousel, because the
   section header has to see them too — and it is the carousel's sibling, so
   anything declared down there is invisible to it. */
.features {
    padding: 120px 0;
    position: relative;

    /* How far a frame stands above its panel's top edge, and the panel's own
       padding. Everything about the overhang is derived from these two: the
       frame's negative top margin is their sum, the stage reserves the rise as
       clearance, and the header's margin is cut from it. Change one number,
       not four.

       The rise is a third of the frame's height, which is what fixes it: a
       264px frame is 555px tall at 9:19.5, and 555 / 3 is 185. Resize the
       frame and this has to be recut to match. */
    --shot-rise: 185px;
    --panel-pad: 48px;
    /* Stated as a variable because three rules need the same number: the frame
       itself, the rise cut from it, and the header's cap below. It used to be
       written into .feature-shot .device-phone at each breakpoint, which is
       why the header never knew how much room the frame was taking. */
    --shot-w: 264px;
}

/* ─── The header moves into the band the frame rises through ───
   The stage holds --shot-rise of clear air above its panel, and only the
   right-hand end of that band is ever occupied — the frame is one column wide.
   Left where it was, the header would have the frame rising straight through
   it, and the section would carry 185px of blank space it does not need. So
   the header is pulled down into the left of the band instead, and set flush
   left so it reads against that edge rather than floating over the middle.

   What the arithmetic holds constant is the gap to the panel: 64px, whatever
   the rise happens to be. When the rise is zero — stacked, below 700px — the
   expression collapses to a plain 64px margin, which is what every other
   section header on the site uses. */
/* Indented by the panel's own padding, so the heading and the subtitle start
   on the same vertical as the panel's title and copy directly below them
   rather than one padding to the left of it — the two blocks read as one
   column. The indent is the panel's padding variable, not a number, so it
   follows --panel-pad down at every breakpoint. The max-width carries the
   indent too, or the measure would grow by exactly what the indent took. */
/* Two caps, and the smaller wins. The first is the measure: 640px of text plus
   the indent it is pushed in by. The second is the room the frame leaves:
   whatever the section is wide, less the frame, less the padding it sits in,
   less a gutter. Without it the header keeps its 640px on a 820px screen and
   the subtitle runs under the phone — which is exactly what it did, because
   the header sits *inside* the band the frame rises through and nothing was
   telling it where the frame was. */
.features .section-header {
    margin-bottom: calc(64px - var(--shot-rise));
    padding-left: var(--panel-pad);
    max-width: min(calc(640px + var(--panel-pad)),
                   calc(100% - var(--shot-w) - var(--panel-pad) - 32px));
    position: relative;
    z-index: 2;
}

/* One place to say how long a feature holds the stage. script.js reads this
   value rather than keeping a number of its own, so the bar counting down under
   the selected card and the timer driving it cannot disagree. */
.features-carousel {
    --feature-interval: 7s;
}

/* Clearance above for the part of the frame that stands outside the panel.
   Only the top needs it — the frames rise, they do not also hang below. */
.feature-stage {
    display: grid;
    padding-top: var(--shot-rise);
    margin-bottom: 20px;
}

/* Every panel sits in the same grid cell, so the stage is as tall as the
   tallest of them and holds that height as they change. */
.feature-panel {
    grid-area: 1 / 1;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.6fr);
    gap: 48px;
    align-items: center;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    /* Always the variable, never a literal: the header is indented by this
       same value to line up with the panel's copy, and the frame's negative
       top margin cancels it. A hard-coded padding at one breakpoint puts all
       three out of step with each other. */
    padding: var(--panel-pad);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.42s ease, visibility 0.42s;
}

.feature-panel.is-active {
    opacity: 1;
    visibility: visible;
}

/* Nothing to photograph, so these two carry a drawing instead — and a drawing
   needs more room than a phone frame. The right column is wider than the 0.6fr
   a screenshot gets; a list of five lines or a two-column diagram squeezed into
   208px would be unreadable. */
.feature-panel-figure {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
}

.feature-figure {
    min-width: 0;
}

/* ─── "What is never collected" ───
   Every line is a thing that does not happen, so every line is struck through:
   the list reads as a set of crossings-out before a single word of it is read. */
.feature-panel .feature-nots-title {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--text-muted);
    margin-bottom: 14px;
}

.feature-nots {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* The left padding and the disc are one measurement: the disc is 34px across
   centred at x=25, so the text starts at 52 and clears it. Written as a
   background rather than a third pseudo-element because a list item has only
   two, and both are spoken for by the arms of the cross. */
.feature-nots li {
    position: relative;
    padding: 11px 16px 11px 52px;
    background-color: var(--bg-inset);
    background-image: radial-gradient(circle at 25px 20.5px,
                      rgba(201, 166, 212, 0.16) 0,
                      rgba(201, 166, 212, 0.16) 17px,
                      transparent 17.5px);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-sm);
    font-size: 0.94rem;
    line-height: 1.45;
    color: var(--text-secondary);
}

/* Drawn rather than set as a character so it lines up with the first line of
   text at any wrap, and so it cannot be read aloud as content. */
/* The mark now sits on a disc of its own rather than floating on the panel:
   22px of tinted plum with a rim, and the two arms struck across it at 20 by 3
   with a glow under them. Weight and a ground, not a change of hue — the
   near-white this was briefly set to put it in competition with the text
   beside it, which is the one thing a mark next to a sentence must not do.
   The list is a column of crossings-out and they should be the first thing
   the eye lands on. */
.feature-nots li::before,
.feature-nots li::after {
    content: '';
    position: absolute;
    left: 15px;
    top: 19px;
    width: 20px;
    height: 3px;
    border-radius: 2px;
    background: var(--stx-accent-plum-light);
    box-shadow: 0 0 12px rgba(201, 166, 212, 0.9);
}

.feature-nots li::before {
    transform: rotate(45deg);
}

.feature-nots li::after {
    transform: rotate(-45deg);
}

/* ─── The dual-engine diagram ───
   Sized in viewBox units and left to scale, so the only thing that changes
   between a 380px column and a 240px one is the size of the whole drawing. */
.engine-diagram {
    display: block;
    width: 100%;
    max-width: 340px;
    height: auto;
    margin-inline: auto;
    font-family: var(--font-family);
}

.engine-diagram text {
    font-size: 12px;
    fill: var(--text-secondary);
}

.engine-rail {
    stroke: var(--border-hover);
}

.engine-inlet rect,
.engine-router rect {
    fill: var(--bg-inset);
    stroke: var(--border-color);
    stroke-width: 1;
}

.engine-inlet text {
    font-family: var(--font-mono);
    font-size: 11.5px;
    fill: var(--text-muted);
}

/* The two engines are the subject of the drawing, so they are the only filled
   shapes in it — everything else is an outline. */
.engine-box rect {
    fill: var(--bg-card-hover);
    stroke: var(--stx-accent-plum-soft);
    stroke-width: 1.5;
}

/* Scoped through .engine-diagram to outrank the blanket `.engine-diagram text`
   above, which would otherwise win on specificity and flatten both of these. */
.engine-diagram .engine-name {
    font-size: 15px;
    font-weight: 700;
    fill: var(--text-primary);
}

.engine-diagram .engine-sub {
    font-size: 10.5px;
    font-family: var(--font-mono);
    fill: var(--text-muted);
}

.engine-chip rect {
    fill: none;
    stroke: var(--border-color);
    stroke-width: 1;
}

.engine-chip text {
    font-family: var(--font-mono);
    font-size: 10.5px;
    fill: var(--text-muted);
}

.feature-panel .feature-figure-note {
    margin-top: 16px;
    text-align: center;
    font-size: 0.85rem;
    color: var(--text-muted);
    line-height: 1.5;
}

/* A div, not an h3, and deliberately so. Every feature is written twice on this
   page — once on the card that selects it, once on the panel it opens — and two
   h3s per feature would put twelve headings in the outline for six things, five
   of them inside a hidden panel at any moment. The heading stays on the card,
   which is on screen the whole time; the panel is named for assistive tech by
   the aria-labelledby that already points at its tab, so nothing is lost by
   letting this one be styling alone. */
.feature-panel .feature-panel-title {
    font-size: clamp(1.4rem, 2.2vw, 2rem);
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
    margin-bottom: 14px;
    color: var(--text-primary);
}

/* A div for the same reason the title above is one: this sentence is written
   twice on the page, and the copy on the card is the canonical one. Panels are
   the display, cards are the content — every element in a panel that repeats a
   card is styling only, and the two `<p>`s left inside a panel are there
   precisely because they say something no card does. */
.feature-panel .feature-panel-desc {
    font-size: 1.1rem;
    color: var(--text-secondary);
    line-height: 1.7;
    max-width: 52ch;
}

/* Bigger than the same icon on the cards below. On a card it labels a line of
   summary; up on the stage it is the only object in a column of text half a
   metre wide, and at the card's 52px it read as a leftover from the row rather
   than the head of the panel. font-size drives the glyph — .icon is sized in
   em — so the plate and the mark grow together. */
.feature-panel .feature-icon {
    width: 76px;
    height: 76px;
    font-size: 38px;
    border-radius: var(--radius-lg);
    margin-bottom: 24px;
}

/* Smaller than the free-standing screenshot sections — here the frame supports
   the copy rather than the other way round. */
/* ─── The frame rises out of the top of its panel ───
   Pinned to the top of the panel's content box and then pulled up past it. The
   margin is the padding plus the rise, because the content box starts one
   padding in from the border: --panel-pad of it cancels the padding and gets
   the frame level with the panel's top edge, and --shot-rise carries it above.

   Nothing else needs saying about the height. The panel is free to be whatever
   its contents make it — the frame is measured from the top edge, not centred
   in the middle, so the rise is exactly --shot-rise whether the panel comes out
   at 400px or 500px. That is why there is no min-height here any more: an
   earlier version centred the frame and had to prop the panel up to a fixed
   height to control the overhang, which is backwards.

   The bottom is left alone. With the top pinned, the frame's lower end lands
   inside the panel on its own, and the row grows to hold it if it does not. */
/* Scoped through .feature-panel for one reason only: `.screenshot` sets
   `margin: 20px 0 0` and is declared further down this file. The figure carries
   both classes, the two selectors weigh the same, and source order decides —
   so a bare `.feature-shot` loses its margin to a rule 1400 lines away and the
   frame quietly sits 20px *below* where it was told to go. The same trap is
   sprung on .step-shot below. */
.feature-panel .feature-shot {
    margin: 0;
    align-self: start;
    margin-top: calc((var(--panel-pad) + var(--shot-rise)) * -1);
    position: relative;
    z-index: 1;
    /* Rises into place as the panel fades in. The hover tilt lives on .device
       inside it, so the two transforms never fight over the same element. */
    transform: translateY(28px);
    transition: transform 0.65s cubic-bezier(0.22, 1, 0.36, 1);
}

.feature-panel.is-active .feature-shot {
    transform: none;
}

.feature-shot .device-phone {
    max-width: var(--shot-w);
}

/* Lifted off the panel rather than lying on it: a frame this size reads as
   pasted on without a shadow under it.

   The inset highlight is repeated in both of these, not because it changes —
   it never does — but because box-shadow is one property. Naming only the drop
   shadow here would drop the lit top edge .device draws on itself, and the
   frame would lose its bevel the moment it entered this panel. */
.feature-shot .device {
    box-shadow: 0 26px 60px rgba(0, 0, 0, 0.45),
                inset 0 1px 0 rgba(230, 214, 234, 0.20);
    transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
                box-shadow 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

.feature-shot .device:hover,
.feature-shot .device:focus-visible {
    transform: rotate(-5deg) translateY(-14px) scale(1.05);
    box-shadow: 0 44px 96px rgba(0, 0, 0, 0.58),
                inset 0 1px 0 rgba(230, 214, 234, 0.20);
}

.features-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 20px;
}

/* Also the carousel's tabs, hence the pointer, the selected state and the bar. */
.feature-card {
    position: relative;
    overflow: hidden;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: 32px;
    cursor: pointer;
    transition: var(--transition);
}

.feature-card:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-hover);
    transform: translateY(-4px);
    box-shadow: var(--shadow-card);
}

.feature-card:focus-visible {
    outline: 2px solid var(--stx-accent-plum-light);
    outline-offset: 3px;
}

.feature-card.is-active {
    background: var(--bg-card-hover);
    border-color: var(--stx-accent-plum-soft);
}

/* The time this card has left on the stage. It exists only on the selected
   card, so selecting one restarts the count from zero for free. */
.feature-card.is-active::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background: var(--stx-accent-plum-light);
    transform: scaleX(0);
    transform-origin: left;
    animation: featureProgress var(--feature-interval) linear forwards;
}

.features-carousel.is-paused .feature-card.is-active::after {
    animation-play-state: paused;
}

/* Held for one forced reflow by script.js when the rotation restarts, which is
   what makes the bar begin again from zero instead of carrying on from where a
   hover left it — the bar and the timer are two clocks showing the same thing,
   and only the timer was being reset. Two classes and a pseudo-element, so it
   outweighs the rule above without needing !important. */
.features-carousel .feature-card.is-active.is-restarting::after {
    animation: none;
}

/* The reader has picked a card and it is theirs for a while. A bar counting
   down through that rest would be counting to nothing, so it goes away
   entirely. Taking it out of the layout is also what resets it: restoring
   display restarts the animation from zero, so when the rotation picks up
   again the bar and the timer start together without being told to. */
.features-carousel.is-stopped .feature-card.is-active::after {
    display: none;
}

@keyframes featureProgress {
    to { transform: scaleX(1); }
}

.feature-icon {
    margin-bottom: 16px;
    width: 52px;
    height: 52px;
    font-size: 26px; /* drives .icon sizing (1em) */
}

.feature-card h3 {
    font-size: 1.3rem;
    font-weight: 700;
    line-height: 1.25;
    margin-bottom: 10px;
}

.feature-card p {
    font-size: 1rem;
    color: var(--text-secondary);
    line-height: 1.65;
}

/* ============================
   How It Works
   ============================ */

/* ─── The tinted band, and how it ends ───
   The section is a shade darker than the page so the three steps read as one
   panel. A flat `background` did that with a hard edge at each end: two solid
   colours meeting on a line across the full width, which is a seam whichever
   side of it you are on.

   A gradient of the same tint instead, opaque through the middle and gone at
   both ends over 120px — which is exactly the section's padding, so the band
   is at full strength for as long as there is content in it and has faded out
   by the time the edge arrives. Nothing to see at the join, and the middle
   looks identical to what it was. */
.how-it-works {
    padding: 120px 0;
    /* The numeral's flare is a 280px layer centred on a numeral that is barely
       a third of that, so it reaches well past the card by design — it is
       decoration, pointer-events: none, and it is supposed to spill. On a
       phone the card is already the width of the screen, so the spill lands
       outside the viewport and the document grows sideways: 463px of scroll
       width at a 390px viewport, i.e. the whole page could be dragged left,
       with nothing to see there. Every section on this page was measured; this
       is the only one that leaks.

       `clip`, not `hidden`: hidden would make this a scroll container, which
       changes what scrollIntoView and the reveal engine are measuring against
       and would give position: sticky a new nearest scrollport. `clip` cuts
       the paint and nothing else. The y axis is left visible so the flare
       still bleeds up and down between sections, which is where it is meant
       to be seen. */
    overflow-x: clip;
    background: linear-gradient(to bottom,
                rgba(0, 0, 0, 0) 0,
                rgba(0, 0, 0, 0.15) 120px,
                rgba(0, 0, 0, 0.15) calc(100% - 120px),
                rgba(0, 0, 0, 0) 100%);
}

/* Wider than the usual 64px, not narrower. Two things climb out of the first
   card towards the heading — the frame and a numeral the height of a fist —
   and they close the gap from below: measured to the top of the numeral, the
   heading has about 30px of air under it, not 80. */
.how-it-works .section-header {
    margin-bottom: 84px;
}

/* ─── One step, three layers ───
   The old version of this section laid everything out side by side: numeral,
   copy, detail box, frame, each in its own column with air between them. Four
   separate objects in a row, and nothing to say they belonged to one step.

   Everything here overlaps something else instead:
     · the frame stands taller than the card and crosses its top and bottom
       edges, so the phone and the panel occupy the same band of the page;
     · the numeral straddles the card's outer top corner, half on the card and
       half on the section behind it;
     · the detail panel rides out of the copy column and onto the frame, lit
       from behind by what is on the screen under it.

   The geometry is derived from two numbers, so the breakpoints restate those
   and nothing else:
     --step-shot-w    the frame's width; the height follows from the 9:19.5
                      screen plus 22px of bezel, which is what --step-shot-h says
     --step-overhang  how far the frame stands above and below the card
   The card's min-height is the frame's height less the two overhangs, which is
   what keeps the overhang even top and bottom on a step whose copy runs short
   — step 03 has three lines and a button where step 01 has a four-row table. */
.steps {
    --step-shot-w: 216px;
    --step-shot-h: calc(var(--step-shot-w) * 2.1667 + 22px);
    --step-overhang: 34px;
    /* The frame's distance from the step's outer edge, and the clearance the
       copy keeps from the frame. */
    --step-shot-inset: 32px;
    --step-gutter: 28px;
    --step-pad: 52px;
    /* How far the detail panel rides onto the frame. */
    --step-overlap: 44px;

    display: grid;
    gap: 64px;
    max-width: 1000px;
    /* Room for the first frame's rise and the last one's drop, which stand
       outside their cards and so outside the grid's own box. */
    padding: var(--step-overhang) 0;
    margin: 0 auto;
}

.step {
    position: relative;
    display: flex;
}

/* Light thrown behind the frame, wider than the frame itself so it reads as
   the screen lighting the page rather than a shape with a rim round it. It is
   the first positioned thing in the step, so everything else paints over it. */
.step::before {
    content: '';
    position: absolute;
    top: 50%;
    right: calc(var(--step-shot-inset) - 60px);
    width: calc(var(--step-shot-w) + 120px);
    height: var(--step-shot-h);
    transform: translateY(-50%);
    background: radial-gradient(50% 44% at 50% 50%, var(--accent-glow), transparent 70%);
    pointer-events: none;
}

.step-card {
    /* Positioned but deliberately left at z-index auto. The three layers here
       are ordered by one stacking context — the step's — and a z-index on the
       card would trap the detail panel inside a context of its own, where no
       number could lift it over the frame. */
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    min-height: calc(var(--step-shot-h) - var(--step-overhang) * 2);
    /* The outer side is padded for the numeral; the frame's side is padded for
       the frame: its inset, its width, and the gutter the copy keeps clear. */
    padding: 92px calc(var(--step-shot-inset) + var(--step-shot-w) + var(--step-gutter)) 52px var(--step-pad);
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-card);
}

/* A lit top edge under the frame's shadow — the phone crosses the card here,
   and a plain hairline made the two read as stacked rather than joined. */
.step-card::before {
    content: '';
    position: absolute;
    top: -1px;
    left: 10%;
    right: 10%;
    height: 1px;
    background: linear-gradient(90deg, transparent, rgba(230, 214, 234, 0.3), transparent);
}

/* Scoped through .step for the same reason as .feature-shot above: `.screenshot`
   sets a top margin later in the file and would otherwise win.

   Over the card, not beside it: the frame sits in the padding the card keeps
   clear on that side and hangs past both its edges.

   Centred rather than pinned to a fixed rise. The card's height is whatever
   its copy makes it — a Russian paragraph runs a line longer than the English
   one — and a fixed top offset would spend that difference entirely on the
   bottom overhang, so a step could stand 34px proud at the top and 5px at the
   bottom. Centring splits the difference by construction, whatever language
   the page is in, and --step-overhang is then the *most* it can hang: the
   card's min-height is what holds the frame to it. */
.step .step-shot {
    position: absolute;
    z-index: 2;
    top: 50%;
    right: var(--step-shot-inset);
    width: var(--step-shot-w);
    margin: 0;
    transform: translateY(-50%);
}

.step-shot .device-phone {
    max-width: var(--step-shot-w);
}

/* Heavier than a frame standing on the page, because this one stands on the
   card and the shadow is what separates the two. */
.step-shot .device {
    box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55),
                inset 0 1px 0 rgba(230, 214, 234, 0.20);
    transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
                box-shadow 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* The frame opens a lightbox, so the pointer gets an answer. */
.step-shot .device:hover,
.step-shot .device:focus-visible {
    transform: translateY(-10px) scale(1.03);
    box-shadow: 0 38px 84px rgba(0, 0, 0, 0.6),
                inset 0 1px 0 rgba(230, 214, 234, 0.24);
}

/* Set at the size a step number is worth reading from across the room — the
   glyphs run about 140px tall at desktop width — and parked on the card's
   outer top corner so it breaks the edge. Painted over the card rather than
   behind it: behind an opaque card only the part above the border would
   survive, and a numeral cut in half by an edge is a bug, not a composition.

   The card's 80px top padding is what the heading uses to clear these glyphs;
   the two move together, and so does the numeral's own top offset. */
.step-number {
    position: absolute;
    z-index: 3;
    /* 104px at 950 and 160 at the top of the range. The old floor was 5rem and
       an 9vw slope, which meant every screen between a phone and a laptop got
       the floor — 80px, against a 3.5rem heading beside it — and the numeral
       stopped being the thing that numbers the step. The card's top padding is
       measured from the same figure. */
    top: -78px;
    left: -18px;
    font-size: clamp(6.5rem, 11vw, 10rem);
    /* 700, not 800: the four weights the page loads stop at 700, and asking for
       a weight that was never downloaded makes the browser draw a fake bold —
       it smears the 700 outline sideways. At 140px that is visible, and it
       costs a synthesis pass on every paint of the numeral. */
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.05em;
    background: var(--gradient-text);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    opacity: 0.85;
    pointer-events: none;
}

/* Capped so the paragraph keeps a readable measure — the column it sits in is
   wider than the copy wants, and the slack becomes air under the numeral. */
.step-content {
    max-width: 560px;
}

.step-content h3 {
    font-size: 1.45rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    margin-bottom: 10px;
}

.step-content p {
    color: var(--text-secondary);
    font-size: 1rem;
    line-height: 1.65;
    margin-bottom: 26px;
}

/* Rides out of the copy column and onto the frame: the negative margin is the
   gutter the card keeps clear plus the overlap proper. Translucent and blurred
   rather than solid, so the screen underneath shows through as light and the
   panel reads as floating over the phone instead of punched out of it.

   z-index 3 puts it over the frame (2). Both numbers are read in the step's
   stacking context — see the note on .step-card. */
.step-details {
    position: relative;
    z-index: 3;
    /* Anchored to the frame's side and narrower than the column, so it hangs
       off the copy's edge rather than sitting square under it: three blocks
       flush left would put the panel back in a stack, which is what this
       section was before. */
    align-self: flex-end;
    width: min(100%, 460px);
    /* On the floor of the card. The card is held to the frame's height, so a
       short step — 03 is a heading, two lines and a button — has slack to
       spend; spending it between the copy and the panel keeps the heading at
       the top where the numeral is, instead of floating the pair in the
       middle of a half-empty panel. */
    margin-top: auto;
    margin-right: calc((var(--step-gutter) + var(--step-overlap)) * -1);
    padding: 18px 22px;
    border: 1px solid var(--border-hover);
    border-radius: var(--radius-md);
    background: rgba(26, 26, 31, 0.86);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    box-shadow: 0 24px 50px rgba(0, 0, 0, 0.5);
    display: flex;
    flex-direction: column;
}

/* ─── Even steps are the same composition mirrored ───
   Three identical rows read as one row printed three times; alternating gives
   the section a rhythm and keeps the eye moving down it. Every offset above is
   measured from one side, so the mirror restates the four that have a side:
   the glow, the frame, the numeral, and the panel's overhang.

   The DOM order is untouched — numeral, heading, copy, panel, and the frame
   positioned out of flow — so the reading and tab order stay 01, 02, 03. */
.step:nth-child(even)::before {
    right: auto;
    left: calc(var(--step-shot-inset) - 60px);
}

.step:nth-child(even) .step-card {
    padding-right: var(--step-pad);
    padding-left: calc(var(--step-shot-inset) + var(--step-shot-w) + var(--step-gutter));
}

.step:nth-child(even) .step-shot {
    right: auto;
    left: var(--step-shot-inset);
}

.step:nth-child(even) .step-number {
    left: auto;
    right: -12px;
}

.step:nth-child(even) .step-details {
    align-self: flex-start;
    margin-right: 0;
    margin-left: calc((var(--step-gutter) + var(--step-overlap)) * -1);
}

.detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    padding: 7px 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
    font-size: 0.85rem;
}

.detail-row:last-child {
    border-bottom: none;
}

.detail-label {
    color: var(--text-muted);
    font-weight: 500;
}

.detail-value {
    color: var(--text-primary);
    font-weight: 600;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    text-align: right;
}

.detail-value.accent {
    color: var(--accent-primary);
}

/* Connect visual — step 03 in the same slot as the other two panels, but the
   panel itself is taken away: one button in a box the width of a four-row
   table is mostly empty box. The button is its own surface, so it can ride
   onto the frame on its own and the overlap survives. */
.connect-visual {
    width: auto;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;
}

/* ─── The rim carries the light, not the button ───
   Two backgrounds in one box: the fill is clipped to the padding box and the
   conic gradient to the border box, so the only place the gradient shows is
   the 1px ring — a lit edge travelling round the pill. Turning it is one
   custom property, which @property makes an angle rather than a string, and an
   angle is something the browser can interpolate. Registered properties
   animate off the main thread.

   Where @property is not supported the declaration is ignored, --rim-angle
   stays the plain string it was given, and the rim is simply a static gradient
   edge. Nothing is lost that the reader can name. */
@property --rim-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
}

.connect-btn-demo {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 10px 24px 10px 10px;
    border: 1px solid transparent;
    border-radius: 999px;
    background:
        linear-gradient(var(--stx-tertiary-container), var(--stx-tertiary-container)) padding-box,
        conic-gradient(from var(--rim-angle),
            var(--stx-accent-plum-soft) 0deg,
            var(--stx-accent-plum-light) 70deg,
            var(--stx-on-tertiary) 120deg,
            var(--stx-accent-plum-light) 170deg,
            var(--stx-accent-plum-soft) 240deg,
            var(--stx-accent-plum-soft) 360deg) border-box;
    box-shadow: 0 20px 44px rgba(0, 0, 0, 0.5);
    color: var(--stx-on-tertiary);
    font-weight: 600;
    font-size: 0.95rem;
    /* No animation at all until the button reaches the middle of the screen.
       Pausing it was not enough: a paused animation still applies its first
       keyframe, so the rim sat lit and the glow sat burning from the moment
       the page loaded, two screens above anywhere they could be seen. The
       animation is attached by the rule below instead, and until then this is
       a plain pill with a static gradient edge. */
    animation: none;
}

/* Slow on purpose: one turn every six seconds, so it is caught out of the
   corner of the eye rather than watched. `.in-centre` is the middle 40% of the
   viewport — see the second observer in script.js. */
.step.in-centre .connect-btn-demo {
    animation: rimSweep 6s linear infinite;
}

/* ─── The colour under the button ───
   A soft plum shadow the size of the button itself, drifting under it — the
   light the rim is catching, given a source. It is a blurred radial gradient
   rather than a box-shadow because a shadow cannot move on its own: this one
   is an element, so the drift is a transform and costs nothing per frame.

   -1 puts it behind the button's own background inside the button's stacking
   context, and the generous inset is what makes it read as a glow the button
   sits in rather than an outline stuck to it. */
.connect-btn-demo::after {
    content: '';
    position: absolute;
    z-index: -1;
    inset: -34px -44px -46px;
    border-radius: inherit;
    background: radial-gradient(58% 58% at 50% 55%,
                rgba(201, 166, 212, 0.85) 0%,
                rgba(155, 107, 168, 0.6) 40%,
                rgba(110, 53, 119, 0.32) 66%,
                transparent 82%);
    filter: blur(18px);
    /* Invisible and unanimated until the button is in the middle of the
       screen. Both halves are needed: opacity 0 for the resting state, and no
       animation at all, because a paused animation would paint its own first
       keyframe over this. */
    opacity: 0;
    animation: none;
    pointer-events: none;
}

.step.in-centre .connect-btn-demo::after {
    animation: glowDrift 9s ease-in-out infinite;
}

@keyframes rimSweep {
    to { --rim-angle: 360deg; }
}

/* Wanders rather than pulses — no two ends of the cycle in the same place, so
   it never reads as a blink. Transform and opacity only. */
@keyframes glowDrift {
    0%   { transform: translate(-8px, 3px) scale(0.96); opacity: 0.75; }
    30%  { transform: translate(7px, -4px) scale(1.06); opacity: 1; }
    62%  { transform: translate(5px, 5px) scale(1.02); opacity: 0.88; }
    100% { transform: translate(-8px, 3px) scale(0.96); opacity: 0.75; }
}

/* The glow is a pseudo-element that is scaled and faded, not a box-shadow that
   is re-drawn. An animated box-shadow repaints its whole spread area every
   frame on the main thread; a static shadow on a layer that only changes
   transform and opacity is handed to the compositor and costs nothing per
   frame. Same glow, and it can run while the rest of the page scrolls. */
.connect-circle {
    position: relative;
    isolation: isolate;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--gradient-main);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--stx-on-tertiary);
}

/* z-index -1 inside the circle's own stacking context: the shadow is an outer
   one, so it falls outside the border box and never touches the glyph. */
/* A plain lit disc. What moves is the rim of the pill around it — see
   .connect-btn-demo. This used to swell and shrink on its own clock, which
   read as something breathing inside the button rather than a control. */
.connect-circle::after {
    content: '';
    position: absolute;
    z-index: -1;
    inset: 0;
    border-radius: 50%;
    box-shadow: 0 0 26px rgba(110, 53, 119, 0.55);
}

.connect-circle svg {
    stroke: var(--stx-on-tertiary);
}

/* ============================
   Protocols
   ============================ */

.protocols {
    padding: 120px 0;
}

.protocols-table-wrapper {
    max-width: 780px;
    margin: 0 auto;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.protocols-table {
    width: 100%;
    border-collapse: collapse;
}

.protocols-table caption {
    padding: 20px 24px 0;
    text-align: left;
    font-size: 0.85rem;
    color: var(--text-secondary);
}

.protocols-table th {
    padding: 16px 24px;
    text-align: left;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    border-bottom: 1px solid var(--border-color);
    background: var(--bg-inset);
}

.protocols-table td {
    padding: 14px 24px;
    border-bottom: 1px solid var(--border-color);
    font-size: 0.9rem;
}

.protocols-table tr:last-child td {
    border-bottom: none;
}

.protocols-table tbody tr:hover td {
    background: var(--stx-surface-container-high);
}

.protocol-name {
    font-weight: 600;
    color: var(--text-primary);
}

.protocols-more {
    max-width: 780px;
    margin: 20px auto 0;
    text-align: right;
    font-size: 0.9rem;
}

/* ============================
   Home page overview blocks
   Each summarises a dedicated page and links through to it.
   ============================ */

/* Vertical rhythm is deliberately uneven. Eight sections at an identical 120px
   read as one repeated block; varying it lets the page group and breathe. */
.home-section {
    padding: 120px 0;
}

.home-section-tight {
    padding: 96px 0;
}

/* Same tint and the same reason for fading it as .how-it-works above: a solid
   band ends in a line across the page. The ramp is 96px here because that is
   this section's padding. */
.home-section-alt {
    background: linear-gradient(to bottom,
                rgba(0, 0, 0, 0) 0,
                rgba(0, 0, 0, 0.15) 96px,
                rgba(0, 0, 0, 0.15) calc(100% - 96px),
                rgba(0, 0, 0, 0) 100%);
}

/* Asymmetric layout: copy on one side, a device on the other.
   The device is the point of the section, so it gets the wider column. */
.split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.45fr);
    gap: 56px;
    align-items: center;
}

.split-media {
    display: flex;
    justify-content: center;
}

/* The figure is a flex item, so it sizes to its own content — the placeholder
   text — unless told to fill. Without this the frame never reaches its cap. */
.split-media .screenshot {
    width: 100%;
}

/* In a split the TV fills its column instead of the shared 520px cap. */
.split-media .device-tv {
    max-width: 760px;
}

/* The stand keeps its proportion to the wider body. */
.split-media .device-tv::after {
    width: 120px;
}

/* Left-aligned heading block, for split and band sections. */
.section-header-left {
    text-align: left;
    margin-bottom: 28px;
}

/* Centres a heading over the full width of its section. It has to sit outside
   the split for that: inside the left column it can only ever be centred over
   that column, which is not the middle of anything the reader sees. */
.section-title-center {
    text-align: center;
}

/* The heading stands alone above the split, so it carries the gap the header
   block would have given it — and a little more than that. Balanced against
   the 96px this section pads at the top: 40px underneath left the title
   sitting on the paragraph, reading as its first line rather than as the
   section's title. 64px is the site's own header gap; the extra 8 pay for the
   subtitle that used to sit between them. */
.tv-title {
    margin-bottom: 72px;
}

.section-header-left .section-subtitle {
    margin-left: 0;
    margin-right: 0;
    max-width: 52ch;
}

/* Icon + text rows — a lighter alternative to yet another grid of cards. */
.feature-list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 22px;
    margin: 0 0 28px;
}

.feature-list li {
    display: grid;
    grid-template-columns: 40px minmax(0, 1fr);
    gap: 16px;
    align-items: start;
}

.list-icon {
    width: 40px;
    height: 40px;
    font-size: 20px; /* drives .icon sizing (1em) */
}

.feature-list h3 {
    font-size: 1rem;
    font-weight: 600;
    margin-bottom: 4px;
}

.feature-list p {
    color: var(--text-secondary);
    font-size: 0.92rem;
    line-height: 1.6;
}

/* A contrasting slab, so one section stops reading as another plain block. */
.band {
    background: var(--stx-tertiary-container);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-xl);
    padding: 56px 48px;
}

@media (max-width: 900px) {
    .split {
        grid-template-columns: 1fr;
        gap: 36px;
    }

    /* Stacked, the frame no longer has a column to fill — keep it sane. */
    .split-media .device-tv {
        max-width: 620px;
    }
}

@media (max-width: 768px) {
    .home-section-tight {
        padding: 72px 0;
    }

    .band {
        padding: 32px 20px;
    }
}

.section-more {
    text-align: center;
    margin-top: 36px;
    font-size: 0.95rem;
}

/* A block may now offer two ways out — the walkthrough and the reference — and
   two links in one <p> would otherwise run together into a single line of text
   with no seam. Flex gives them the gap and lets them stack on a narrow screen
   instead of wrapping mid-phrase; `justify-content` inherits nothing from
   text-align, so it is restated here and again in the -left modifier. A block
   with one link is unaffected: a single flex item centres exactly as it did. */
.section-more,
.protocols-more {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 28px;
    justify-content: center;
}

.protocols-more {
    justify-content: flex-end;
}

.section-more-left,
.section-more.section-more-left {
    justify-content: flex-start;
}

/* After .section-more, not before it. The two weigh the same, so source order
   decides — declared earlier in the file, as this was, the modifier lost to
   the rule it exists to override and the link under the TV column sat centred
   with the class doing nothing. */
.section-more-left {
    text-align: left;
}

/* The arrow belongs to the presentation, not to the translated string — it used
   to be typed into seven dictionary entries in both languages. */
/* ─── The way out of a block is a control, not a coloured sentence ───
   These were plum text at 0.9rem with an arrow after it. The colour was never
   the problem — measured at 8.16:1, it is one of the strongest things on the
   page — but a line of tinted text at the foot of a section reads as the last
   line of the section, and eight sections all ending that way taught the eye
   to skip it. What was missing was an edge: something with a boundary reads as
   a thing to press, and nothing else on these pages carries one.

   So: a chip. Not a filled button — that is what the download CTAs are, and
   eight of these competing with them would flatten the page's one real call to
   action. A bordered chip sits a step below a button and a long step above
   body text, which is exactly the rank these have.

   ─── Amber, and asked for ───
   Two plum versions came before this one and both stayed quiet, because plum
   is the page: a plum chip on a plum-lit page is the same object as everything
   around it, only outlined. Amber is the one colour here that is not the page,
   so it separates on hue rather than on brightness — which is why the wash is
   visible at all now (1.47:1 against the section, against 1.18 for the plum
   version, and the plum one needed an 8:1 border to be seen).

   Built from the same parts as the icon plates so it belongs to them rather
   than being a second amber: the resin gradient, the top gloss, the halo.
   One departure — the border is solid --stx-amber instead of --stx-amber-edge.
   The edge token is 45% alpha, which composites to 2.06:1 against the page:
   right for a 58px decorative plate, below the 3:1 that WCAG 1.4.11 asks of a
   control's boundary. Solid reads 5.34:1.

   Measured 2026-08-20 across all ten links and both breakpoints: text 5.35:1
   at worst against the lightest stop of the chip, border 4.66:1 at worst. The
   last link sits on a lighter section, which is where both worst cases are.

   The padding is also the tap target: 13px against a 1.25 line box gives 48px,
   where the bare text was 23. */
.section-more a,
.protocols-more a,
.arrow-link {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 13px 22px;
    border: 1px solid var(--stx-amber);
    border-radius: var(--radius-md);
    background: var(--gradient-amber-chip);
    box-shadow: inset 0 1px 0 rgba(242, 168, 104, 0.34),
                0 4px 16px var(--stx-amber-halo);
    color: var(--stx-amber-glow);
    font-size: 1.02rem;
    font-weight: 600;
    line-height: 1.25;
    /* Long labels wrap inside the chip rather than pushing it past the
       container — "Как правила разрешаются: группы, домены, порядок
       сопоставления" is 62 characters and a phone is 360px wide. */
    text-align: left;
    transition: color var(--transition),
                border-color var(--transition),
                box-shadow var(--transition);
}

/* ─── The arrow is a plum disc, and the two-colour split is the point ───
   A bare arrow glyph in the label's own colour is part of the sentence: it
   sits on the baseline, it is the same weight, and at a glance the chip is one
   run of amber text in a box. Given its own disc it stops being punctuation
   and becomes the part you press — the widget reads as label plus button
   rather than as a line with a mark after it.

   Plum for the disc, and deliberately: plum is this site's interaction colour
   everywhere else — focus rings, hover states, the accordion's chevron — so
   the one element here that behaves like a control is the one wearing it.
   Amber says "this is a way onward", plum says "this is the handle". They also
   simply separate: two accents at opposite ends of the wheel on one object
   catch the eye far harder than one accent could at any brightness.

   `em`, not pixels, so the disc tracks the label if the type scale ever moves.
   Pushed 4px into the right padding, which is what keeps a 30px disc from
   making the chip look padded on one side. */
.section-more a::after,
.protocols-more a::after,
.arrow-link::after {
    content: '\2192';
    /* Never squeezed out by a label that wrapped to three lines. */
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.85em;
    height: 1.85em;
    margin-right: -4px;
    border-radius: 50%;
    border: 1px solid var(--stx-accent-plum-mid);
    background: rgba(58, 24, 64, 0.72);
    color: var(--stx-accent-plum-light);
    font-size: 0.94em;
    line-height: 1;
    transition: transform var(--transition),
                background var(--transition),
                border-color var(--transition),
                color var(--transition);
}

.section-more a:hover,
.protocols-more a:hover,
.arrow-link:hover {
    color: var(--stx-amber-glow);
    border-color: var(--stx-amber-glow);
    box-shadow: inset 0 1px 0 rgba(242, 168, 104, 0.55),
                0 6px 22px rgba(184, 90, 28, 0.38);
}

/* The handle lights rather than the whole chip: the disc fills, its rim goes
   to the light plum and the glyph to the page's brightest text. */
.section-more a:hover::after,
.protocols-more a:hover::after,
.arrow-link:hover::after {
    border-color: var(--stx-accent-plum-light);
    background: var(--stx-accent-plum-soft);
    color: var(--text-primary);
}

.section-more a:hover::after,
.protocols-more a:hover::after,
.arrow-link:hover::after {
    transform: translateX(3px);
}

/* ─── Подпись кнопки внутри прозы ───
   Чип стоит в тексте раздела, а значит попадает под общее правило `.prose
   a:not(.btn)`: сиреневый цвет ссылки и подчёркивание. Специфичность у него
   выше, чем у самого чипа, поэтому кнопка выходила трёхцветной — янтарная
   рамка, плам-диск и сиреневая подпись со строчным подчёркиванием, — и
   читалась как ссылка, случайно попавшая внутрь кнопки. Подпись набрана
   основным цветом текста: белое на янтаре не спорит ни с рамкой, ни с диском.
   Подчёркивание снято по той же причине: подчёркивают ссылку в строке, а не
   кнопку. */
.prose .arrow-link:not(.btn),
.prose .arrow-link:not(.btn):hover {
    color: var(--text-primary);
    text-decoration: none;
}

/* Чип в карточке статьи стоит сразу за описанием, а у описания в карточке
   нижнего отступа нет: рамка кнопки касалась последней строки текста и на
   глаз читалась как наезд. Отступ ставится связкой «абзац + чип», чтобы он
   не появился там, где чип идёт первым или единственным элементом. */
.info-card p + .arrow-link {
    margin-top: 14px;
}

@media (max-width: 768px) {
    .home-section {
        padding: 72px 0;
    }
}

.check {
    font-size: 1rem;
    color: var(--stx-accent-plum-light);
    font-weight: 700;
}

.cross {
    color: var(--text-muted);
}

.auth-type {
    color: var(--text-secondary);
    font-family: var(--font-mono);
    font-size: 0.8rem;
    padding: 2px 8px;
    background: var(--stx-tertiary-container);
    border-radius: 6px;
}

/* Engine badges */
.engine-badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 100px;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.03em;
}

.engine-badge.kotlin {
    background: var(--stx-tertiary-container);
    color: var(--stx-on-tertiary);
    border: 1px solid var(--border-hover);
}

.engine-badge.go {
    background: var(--stx-accent-plum);
    color: var(--stx-on-tertiary);
    border: 1px solid var(--stx-accent-plum-soft);
}

/* ============================
   Architecture
   ============================ */

/* ============================
   Download Section
   ============================ */

.download {
    padding: 120px 0;
}

/* Plain card. The amber here is the glow behind it, nothing more — the
   translucent slab treatment was tried and reverted. */
/* Capped and centred, like .cta-band on the subpages — without this the card
   is only as wide as .container (1400px minus its own padding), and at any
   desktop width the two buttons and three lines of copy end up as a small
   cluster stranded in the middle of a box hundreds of pixels too wide either
   side of it. 880px holds the widest line here — the two buttons side by
   side, ~613px — with enough air around it to still read as a considered
   composition rather than a stretched template. */
/* ─── Redesigned 2026-08-20 ───
   It was a grey slab with a blurred lamp behind it, and grey is what it read
   as: the last and most important block on the page looked like the info
   cards two sections above. Amber already carries "act on this" here — the
   icon plates and now the read-more widgets — so the terminal call to action
   is where it should be strongest. The card is lit rather than tinted: the
   warmth falls from the top edge, as if the light were above it, and the
   surface underneath is still the page.

   The rim is amber-edge rather than solid amber, unlike the widgets, and the
   difference is deliberate: this is a 880px panel, not a 56px control, and a
   solid ginger line around something that size is a box first and a card
   second. Non-text contrast does not apply to a decorative container edge. */
.download-card {
    position: relative;
    max-width: 880px;
    margin: 0 auto;
    background: var(--bg-card);
    border: 1px solid var(--stx-amber-edge);
    border-radius: var(--radius-xl);
    padding: 76px 48px;
    text-align: center;
    overflow: hidden;
    /* The top hairline is the lit edge; the drop is what lifts the panel off
       a section that has no background of its own. */
    box-shadow: inset 0 1px 0 rgba(242, 168, 104, 0.30),
                0 24px 64px rgba(0, 0, 0, 0.42);
}

/* The light, and it is a gradient rather than a lamp behind frosted glass.
   This used to be a 500x400 box carrying `filter: blur(150px)`: a 150px blur
   is one of the most expensive things a browser can be asked to paint, the
   surface it has to cover is the box plus the blur radius in every direction,
   and it was doing it on a decorative element that never changes. A radial
   gradient draws the same falloff in one pass with no filter, no layer and no
   re-raster — and the shape is actually better controlled, because the stops
   are where they are put rather than wherever a Gaussian lands. */
.download-glow {
    position: absolute;
    inset: 0 0 auto 0;
    height: 66%;
    background: radial-gradient(ellipse 62% 100% at 50% 0%,
                rgba(218, 115, 40, 0.34) 0%,
                rgba(184, 90, 28, 0.16) 42%,
                transparent 74%);
    pointer-events: none;
}

.download-content {
    position: relative;
    z-index: 1;
}

/* ─── The last block on the page, given a shape ───
   Heading, paragraph, three requirements and two buttons were four blocks
   16 to 40px apart in one centred column, all of them at similar weights: the
   whole thing read as one grey mass with a button at the bottom. It is four
   things doing four jobs, and the spacing now says so — the heading is the
   size of a section title, the paragraph is held to a measure and set apart,
   the requirements are a strip of small print with a rule above them, and the
   buttons stand clear of all of it. */
.download-content h2 {
    font-size: clamp(2rem, 4.4vw, 2.9rem);
    font-weight: 700;
    line-height: 1.14;
    margin-bottom: 20px;
    letter-spacing: -0.025em;
}

.download-content > p {
    color: var(--text-secondary);
    font-size: 1.12rem;
    max-width: 46ch;
    margin: 0 auto 40px;
    line-height: 1.62;
}

/* The buttons come before the small print, not after it. What the reader is
   being asked to do is download; the requirements are the answer to "will it
   run on mine", which is a question they ask *after* they have decided. Order
   reversed with flexbox rather than in the markup, so the heading, the pitch
   and the specification still read in that order to a screen reader and to
   anything that ignores CSS. */
.download-content {
    display: flex;
    flex-direction: column;
}

.download-actions {
    order: 1;
}

.download-requirements {
    order: 2;
}

/* A strip of small print under a rule: three facts about the file, set apart
   from the two things above them that are asking for a decision. Uppercase and
   tracked out because at this size it reads as a specification rather than as
   another sentence. */
.download-requirements {
    display: flex;
    justify-content: center;
    gap: 10px 12px;
    margin: 38px auto 0;
    max-width: 640px;
    flex-wrap: wrap;
}

/* Each fact its own pill. As a rule with three phrases under it the strip
   read as one more sentence in a column that already had two; as three
   bordered objects it reads as a specification, which is what it is, and the
   rule above them is no longer needed to say "different kind of thing". */
.req {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 7px 14px;
    border: 1px solid var(--border-color);
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.03);
    font-size: 0.82rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-secondary);
}

.req svg {
    color: var(--stx-amber-lit);
    flex-shrink: 0;
}

.download-actions {
    display: flex;
    justify-content: center;
    gap: 16px;
    flex-wrap: wrap;
}

/* ============================
   Footer
   ============================ */

.footer {
    padding: 48px 0 32px;
    border-top: 1px solid var(--border-color);
}

.footer-content {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 32px;
    margin-bottom: 32px;
}

/* Never shrinks and never wraps — see the note on .footer-links. */
.footer-brand {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-shrink: 0;
    white-space: nowrap;
    font-weight: 700;
    font-size: 1.35rem;
}

/* The mark is the last thing on the page and was the smallest thing in the
   footer — smaller than it is in the bar at the top, where it competes with a
   full row of links. Down here it has the row to itself. */
.footer-brand .logo-icon {
    width: 72px;
    height: 72px;
}

.footer-links {
    display: flex;
    gap: 24px;
}

.footer-links a {
    font-size: 0.9rem;
    color: var(--text-muted);
    transition: var(--transition);
}

.footer-links a:hover {
    color: var(--text-primary);
}

.footer-bottom {
    text-align: center;
    padding-top: 24px;
    border-top: 1px solid var(--border-color);
}

.footer-bottom p {
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* ============================
   Animations
   ============================ */

@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes pulse {
    0%, 100% {
        opacity: 1;
    }
    50% {
        opacity: 0.4;
    }
}

/* Scroll animations */
/* Applied by script.js, on phones, to a list of blocks — never written into
   the markup. See the note there: an element hidden by CSS that only script
   can reveal is an element that vanishes when the script does not run.

   translateY(18px), not 30: the arrival is read against the scroll, which is
   already moving the block up the screen, and a longer throw reads as the
   block lagging the finger rather than settling into place. */
.fade-in {
    opacity: 0;
    transform: translateY(18px);
    transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
    /* Both animated properties are compositor properties, and the hint keeps
       them off the main thread while the reader is dragging the page. */
    will-change: opacity, transform;
}

/* Dropped once it has arrived: a permanent will-change on every block in the
   page is a permanent layer for every block in the page. */
.fade-in.visible {
    will-change: auto;
}

/* ═══ What happens when a block arrives ═══
   These run off the `.visible` class the observer in script.js adds, not off a
   scroll timeline. That is deliberate and it is the second attempt: the first
   version put everything on `animation-timeline: view()`, which is elegant,
   free, and supported by exactly one browser engine. Chrome and Edge have it
   from 115; Firefox and Safari do not have it at all. Written that way, the
   effects were invisible to anyone not on Chromium — which is how they were
   reported.

   A class the observer sets works in every browser that has
   IntersectionObserver, which is all of them, and it costs one callback per
   element for the life of the page — the observer stops watching each element
   as it fires. The scroll-timeline block further down still runs where it is
   supported, but only for the continuous parallax, which is the one effect
   that genuinely needs to track the scroll position rather than fire once.

   Under prefers-reduced-motion the effects here are off and the arrival is
   not: the block still fades in, it just does not travel. The reduced-motion
   block near the end of this file is where that is decided, and it is the only
   place motion policy is written. Worth knowing that on Windows the preference
   comes from Accessibility → Visual effects → "Animation effects", which sets
   it for every browser on the machine at once. If nothing on the page moves,
   that switch is the first thing to check, not this stylesheet. */

/* Each frame greets the reader the way it does under the pointer — the same
   lift, every time it comes back past the line.

   `backwards`, never `both`. `both` holds the last keyframe on the element
   for good, and a transform held by an animation cannot be overridden by the
   hover transition underneath it: the frames stopped answering the pointer
   entirely, which is exactly what was reported. `backwards` fills only the
   delay and lets go at the end, so the moment the greeting finishes the
   element is plain again and hover owns the transform. */
/* The frames carry a large blurred shadow, and a transform on an element
   without its own layer repaints that shadow every frame — so the layer is
   worth asking for. Where it is asked for is the block below. */
/* ─── The greeting runs on arrival, once, and that is the fix for the blink ───
   It used to hang off `in-view`, which is not the moment the block arrives.
   `visible` reveals the block as soon as its top edge crosses the line;
   `in-view` fires later and, crucially, comes off and goes back on every time
   the reader crosses the block again. So the frame faded up, came to rest, and
   was then thrown back to the first keyframe of shotGreet — 54px low, scaled
   down and tilted — while in full view. The reset was the first thing the eye
   caught, and it arrived *before* the animation it belonged to.

   Hiding the reset behind an opacity ramp was the previous attempt and it was
   the wrong repair: it turned a jump into a blink, which is what was reported
   next, and the numerals blinked for the same reason — stepNumberGlow opens at
   opacity 0.06, so every replay dropped a settled numeral to near-black and
   brought it back.

   Taking the replay away fixed the blink and lost the effect: reading back up
   the steps, nothing moved at all, which was reported straight back. So the
   split is by what the animation resets to, not by which class carries it:

     · resets to nothing visible  → `in-view`, replays on every crossing.
       shotGreet moves a transform and touches no opacity; the flare and the
       spark are pseudo-elements that rest at opacity 0. Replaying any of the
       three starts from a state the reader cannot see, so there is nothing to
       blink.

     · resets to something visible → `visible`, plays once. stepNumberGlow
       opens at opacity 0.06 on a glyph that rests at 0.85, so every replay
       dropped a settled numeral to near-black and brought it back. That is
       the blink, and it is the only animation here that had one.

   The opacity ramp that used to hide shotGreet's reset is gone with it: with
   `visible` now landing on the same line the effect fires on, the first pass
   has nothing to hide, and on later passes the frame is simply moving.

   The wave down the transports table keeps `in-view` too, and for the same
   reason — a passing highlight has no resting state to snap back to. */
/* ─── The layer is asked for before the animation, not by it ───
   `will-change: transform` used to sit in the rule below, which is the rule
   that starts the animation. A hint delivered at the same instant as the
   movement asks the compositor to promote the element on the movement's first
   frame — the one frame that cannot afford the work — so the hint that exists
   to prevent a stutter was paying for one. Declared on the resting state, the
   layer is already standing when the greeting begins.

   Not released afterwards, and that is the trade: ten frames on the home page
   keep a compositor layer for the life of the visit. Releasing it would mean
   changing will-change while the transform is still settling, which is the
   same churn moved to the other end. Ten small layers is a price worth paying;
   the twenty-nine that `.fade-in` would have kept was not, which is why that
   one is still dropped on arrival. */
.step .step-shot .device,
.feature-stage .device,
.feature-card .device,
.split-media .device {
    will-change: transform;
}

.step.in-view .step-shot .device,
.feature-stage.in-view .device,
.feature-card.in-view .device,
.split-media.in-view .device {
    animation: shotGreet 1.05s cubic-bezier(0.22, 1, 0.36, 1) 0.08s backwards;
}

/* The numeral comes up out of the dark, flares, and settles at its resting
   weight. `both` is right here — the last keyframe *is* the resting state. */
.step.visible .step-number {
    animation: stepNumberGlow 1.25s cubic-bezier(0.33, 1, 0.68, 1) both;
}

/* ─── Two layers of flare, one behind the glyph and one in front ───
   A single layer, wherever it is put, reads flat: behind, the numeral is a
   silhouette on a light; in front, it is a decal over the numeral. Depth comes
   from the light being on both sides of the object at once, which is what it
   would be if it were real.

   The back layer is the source — wide, warm, four long spikes, and it keeps
   the soft haze around them, because rays without a bloom are a diagram of a
   star rather than a star. The front layer is what the light does on the way
   past the glyph: shorter, cooler, turned 45 degrees so its spikes fall
   between the other four rather than on top of them, and much fainter, since
   it is a highlight rather than the source.

   Both are pseudo-elements of the numeral itself, so they inherit its position
   and are thrown open on the same clock. */
.step-number::after,
.step-number::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 46%;
    opacity: 0;
    pointer-events: none;
    background-repeat: no-repeat;
    background-position: center;
}

/* Behind. -1 keeps it under the glyph but above the card, because the numeral
   makes a stacking context of its own with z-index 3. */
.step-number::after {
    z-index: -1;
    /* 280, not 360. The layer is scaled through its whole life, and the raster
       the compositor has to carry is the square of that number — dropping it
       by a fifth takes a third off the surface being pushed around, which is
       the difference between a flare that plays and one that hitches on a
       page already animating a hero graphic above it. */
    width: 280px;
    height: 280px;
    margin: -140px 0 0 -140px;
    /* Four background layers and no filter. The haze used to be a
       drop-shadow, and a filter does not survive being scaled for free:
       even a static one is re-applied every time the transform changes the
       size it has to cover, which is once a frame for the whole of the flare.
       Painted as a wide, soft radial gradient instead, it is part of the same
       raster as the rays — drawn once, then only moved and faded. The layer
       list, back to front: the haze, the core, the horizontal ray, the
       vertical one. */
    background-image:
        radial-gradient(closest-side, rgba(201, 166, 212, 0.42), rgba(201, 166, 212, 0.16) 45%, transparent 78%),
        radial-gradient(closest-side, rgba(230, 214, 234, 0.55), rgba(201, 166, 212, 0.28) 42%, transparent 72%),
        linear-gradient(to right, transparent 2%, rgba(230, 214, 234, 0.95) 50%, transparent 98%),
        linear-gradient(to bottom, transparent 8%, rgba(201, 166, 212, 0.8) 50%, transparent 92%);
    background-size: 100% 100%, 62% 62%, 100% 3px, 3px 78%;
}

/* On the resting state, for the same reason as the frames above: a layer asked
   for by the rule that starts the flare has to be built on the flare's first
   frame. These two are the numeral's light — 280px and 190px of rasterised
   gradient, scaled through their whole life — and building that raster
   mid-flare is what a blink at the start of the animation looks like. */
.step-number::after,
.step-number::before {
    will-change: transform, opacity;
}

/* In front, and cooler: the plum reads as reflected light rather than a second
   sun. Half the size, a quarter of the strength. */
.step-number::before {
    z-index: 2;
    width: 190px;
    height: 190px;
    margin: -95px 0 0 -95px;
    transform: rotate(45deg);
    background-image:
        radial-gradient(closest-side, rgba(230, 214, 234, 0.4), transparent 66%),
        linear-gradient(to right, transparent 10%, rgba(201, 166, 212, 0.55) 50%, transparent 90%),
        linear-gradient(to bottom, transparent 10%, rgba(230, 214, 234, 0.45) 50%, transparent 90%);
    background-size: 40% 40%, 100% 2px, 2px 100%;
}

.step.in-view .step-number::after {
    animation: stepNumberFlare 1.35s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.step.in-view .step-number::before {
    animation: stepNumberSpark 1.35s cubic-bezier(0.22, 1, 0.36, 1) 0.08s both;
}

/* The wave. One animation, eight rows, and the stagger is a delay per row —
   the geometry trick that produced it for free needs a scroll timeline, and
   this has to work without one. 90ms a row crosses the table in three
   quarters of a second, and it runs again on every pass. */
.protocols-table-wrapper.in-view tbody tr {
    animation: rowWave 0.9s ease-out both;
}

/* Met on the way down — the table is entering from the bottom of the screen —
   and the wave runs down it, first row first. */
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(1) { animation-delay: 0.05s; }
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(2) { animation-delay: 0.14s; }
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(3) { animation-delay: 0.23s; }
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(4) { animation-delay: 0.32s; }
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(5) { animation-delay: 0.41s; }
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(6) { animation-delay: 0.50s; }
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(7) { animation-delay: 0.59s; }
.protocols-table-wrapper.in-view.from-below tbody tr:nth-child(8) { animation-delay: 0.68s; }

/* Met on the way up, and the wave runs the other way — last row first. A wave
   that always travels downwards reads as a canned effect the moment the reader
   scrolls back; travelling with them, it reads as the light being theirs.
   Written out rather than computed, because eight rows of arithmetic in a
   custom property would be harder to read than eight lines. */
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(8) { animation-delay: 0.05s; }
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(7) { animation-delay: 0.14s; }
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(6) { animation-delay: 0.23s; }
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(5) { animation-delay: 0.32s; }
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(4) { animation-delay: 0.41s; }
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(3) { animation-delay: 0.50s; }
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(2) { animation-delay: 0.59s; }
.protocols-table-wrapper.in-view:not(.from-below) tbody tr:nth-child(1) { animation-delay: 0.68s; }

/* The pointer's own lift, borrowed and given room. Overshoots and settles
   rather than sliding into place, which is what makes it read as the frame
   arriving rather than the page finishing its layout.

   The travel was 26px over 0.85s and read as a twitch — at the size these
   frames are drawn, a movement has to be a tenth of the object to be seen as
   movement. 54px and a slower curve, with the tilt going with it: a frame that
   only slides is a frame on rails, and one that rights itself as it lands has
   weight. */
/* ─── One easing per segment, declared, not inherited ───
   A timing function on the `animation` shorthand is applied to every gap
   between keyframes separately, not once across the whole run. With
   cubic-bezier(0.22, 1, 0.36, 1) — which is nine tenths done in a quarter of
   its time — a three-keyframe animation is therefore three sprints with a
   stall at the end of each, and the stalls land at 58% and at the finish.
   That is the "it brakes in the middle" report, and it is the same fault in
   all three of the animations here.

   Written out per keyframe instead: the rise decelerates into the overshoot,
   the settle leaves the peak at rest and eases down. One curve through the
   whole movement rather than two glued together. */
@keyframes shotGreet {
    0%   { transform: translateY(54px) scale(0.93) rotate(-1.6deg);
           animation-timing-function: cubic-bezier(0.16, 0.72, 0.36, 1); }
    58%  { transform: translateY(-14px) scale(1.03) rotate(0.5deg);
           animation-timing-function: cubic-bezier(0.42, 0, 0.28, 1); }
    100% { transform: translateY(0) scale(1) rotate(0deg); }
}

/* 0.85 is the numeral's resting opacity — the animation has to land exactly on
   it or the glyph steps in brightness at the end. The haze travels with it:
   the glyph is lit from behind at the peak and cools to nothing, which is the
   soft half of the effect the rays are the hard half of. */
/* Opacity only. A drop-shadow that changes radius has to be re-rasterised on
   every frame — on a 156px glyph painted through background-clip: text, that is
   a full repaint of the numeral and its blur sixty times a second, and it is
   what the stutter was. The haze is still there: it is a static shadow on the
   flare layer behind the glyph, which only ever moves and fades, and both of
   those the compositor does for free. */
@keyframes stepNumberGlow {
    0%   { opacity: 0.06; }
    38%  { opacity: 1; }
    70%  { opacity: 0.95; }
    100% { opacity: 0.85; }
}

/* Opens fast and dies slowly, like the real thing. The rays are widest a third
   of the way in and gone by the end — a flare that stayed would be a decal. */
/* Four keyframes and the shorthand's ease-out on each of the three gaps meant
   three sprints and three stalls — the flare snapped open, stopped dead, crept,
   stopped again. Only the opening is supposed to be fast; the decay is supposed
   to be even, and even means linear here, because the shape of the fall is
   already carried by the opacity steps. See the note on shotGreet. */
@keyframes stepNumberFlare {
    0%   { opacity: 0; transform: scale(0.3) rotate(-14deg);
           animation-timing-function: cubic-bezier(0.15, 0.85, 0.35, 1); }
    28%  { opacity: 1; transform: scale(1.08) rotate(0deg);
           animation-timing-function: linear; }
    62%  { opacity: 0.5; transform: scale(1.18) rotate(5deg);
           animation-timing-function: linear; }
    100% { opacity: 0; transform: scale(1.26) rotate(8deg); }
}

/* The front layer keeps its 45 degrees the whole way, so its spikes stay
   between the back layer's rather than sweeping across them. */
@keyframes stepNumberSpark {
    0%   { opacity: 0; transform: rotate(45deg) scale(0.4);
           animation-timing-function: cubic-bezier(0.15, 0.85, 0.35, 1); }
    32%  { opacity: 0.9; transform: rotate(45deg) scale(1.04);
           animation-timing-function: linear; }
    100% { opacity: 0; transform: rotate(45deg) scale(1.3); }
}

/* ═══ Movement that the scroll position drives ═══
   What is left here is the parallax, which is the one thing that has to follow
   the scroll continuously rather than fire once. Where the timeline is not
   supported the frames simply hold still, and everything above still runs. */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        /* NEVER write these with the `animation` shorthand. The shorthand sets
           every sub-property it does not mention, and the one it sets to zero
           is the duration — which for a scroll-driven animation means the whole
           thing is over before the first frame, frozen on its `from` values by
           `fill-mode: both`. That is not a subtle failure: the numerals sat at
           35% opacity forever and every frame kept a transform it had never
           been scrolled into, which then fought the hover lift.
           `animation-duration: auto` is the value that means "as long as the
           range", and only the longhand can say it. */

        /* ─── The screenshot drifts inside its frame ───
           The frame does not move — the picture inside it does, which is the
           parallax and also the reason this is on the image and not on .device:
           .device carries the hover lift, and an animation on a property beats
           a transition on the same property every time.

           The image is over-tall and anchored to the top, so there is a strip
           of it below the frame to travel into. .device-screen already clips.

           `view()` measures the element against the scrollport; the range is
           cut at both ends so the travel is spent while the frame is on screen
           rather than mostly above and below it. */
        .step-shot .device-screen img,
        .feature-shot .device-screen img,
        .split-media .device-screen img {
            height: 116%;
            animation-name: shotDrift;
            animation-duration: auto;
            animation-timing-function: linear;
            animation-fill-mode: both;
            animation-timeline: view();
            animation-range: cover 5% cover 95%;
        }

    }
}

/* Percentages of the image's own height, so the travel scales with the frame
   instead of being a pixel count tuned for one breakpoint. 16% over-height,
   14% of travel: the picture never runs out from under the frame. */
@keyframes shotDrift {
    from { transform: translateY(0); }
    to   { transform: translateY(-14%); }
}

/* Bright enough to be seen crossing the table and gone by the end: the wave is
   a highlight passing through, not a state the row is left in. The lift on the
   text goes with it, so the row reads as catching the light rather than being
   painted. */
@keyframes rowWave {
    0%   { background-color: transparent; }
    40%  { background-color: rgba(201, 166, 212, 0.13); }
    100% { background-color: transparent; }
}

.fade-in.visible {
    opacity: 1;
    transform: translateY(0);
}

/* ============================
   Responsive
   ============================ */

@media (max-width: 1024px) {
    .features-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    /* Only the frame and the card's padding move; the composition is the same,
       and everything derived from --step-shot-w re-derives itself. */
    .steps {
        --step-shot-w: 196px;
        --step-shot-inset: 24px;
        --step-gutter: 24px;
        --step-pad: 40px;
        gap: 56px;
    }

    /* Only the padding changes here. The frame is still 264px, so the rise is
       still a third of 555px — and because the negative margin is computed from
       both, it re-derives itself. */
    .features {
        --panel-pad: 36px;
    }

    .feature-panel {
        gap: 32px;
    }
}

@media (max-width: 900px) {
    .steps {
        --step-shot-w: 176px;
        --step-shot-inset: 18px;
        --step-gutter: 20px;
        --step-pad: 32px;
        --step-overlap: 34px;
        --step-overhang: 26px;
        /* Not tightened with the rest: the numeral hangs 64px over the card's
           top edge and the row gap is what it hangs into, so a smaller gap
           here would put 02 on the back of 01's card. */
        gap: 56px;
    }

    /* Trimmed with the frame: the overhang is the difference between the two
       heights, so a card that keeps its desktop padding while the frame comes
       down swallows the frame whole. The top stays generous because the
       numeral hanging into it does not shrink at the same rate. */
    .step-card {
        padding-top: 78px;
        padding-bottom: 40px;
    }

    .step-content h3 {
        font-size: 1.3rem;
    }

}

/* ─── Below the desktop, the panel is one column and has to read as one ───
   The side-by-side panel only works while both halves have room. It used to
   hold on down to 700px, and everything between there and about 900 was the
   worst of both: a copy column too narrow for the paragraph, a frame rising
   145px into a header that had to dodge it, and a field of empty card under
   the copy where the frame's column ran on. The screenshot of it at 750px is
   the argument — a phone hanging over the section heading with a third of the
   panel left blank beneath it.

   So it splits where the rest of the page does, at 1024: below that the page
   is already in tablet dress, with the navigation folded into the burger and
   the hero stacked. Side by side above, stacked below, and nothing in between
   trying to be both.

   Stacked, the arrangement is stated fresh rather than inherited: one measure
   for the vertical rhythm, the frame centred and given the width the panel can
   spare, and an icon cut to the size the column is rather than the size the
   stage was. */
@media (max-width: 1024px) {
    .feature-panel {
        grid-template-columns: minmax(0, 1fr);
        justify-items: stretch;
        gap: 24px;
    }

    /* Centred and as wide as the column allows, so the frame is the panel's
       lower half rather than something parked in the corner of it. */
    .feature-panel .feature-shot {
        justify-self: center;
        width: 100%;
    }

    .feature-shot .device-phone {
        margin-inline: auto;
    }

    /* The stage icon's 76px answers a column half a metre wide. In a phone's
       column it is a third of the width above a 20px heading. */
    .feature-panel .feature-icon {
        width: 58px;
        height: 58px;
        font-size: 29px;
        border-radius: var(--radius-md);
        margin-bottom: 18px;
    }

    /* Same rhythm as the gap between the blocks, so the whole panel steps
       down evenly instead of tightening where the grid stops and the text
       starts. */
    .feature-panel .feature-panel-title {
        margin-bottom: 12px;
    }

    .feature-panel .feature-panel-desc {
        font-size: 1rem;
        line-height: 1.6;
    }

    /* Stacked, the frame sits under the copy, and a frame that rose out of the
       top would rise through the paragraph above it. Zeroing the rise takes the
       negative margin and the stage's clearance with it.

       240px between here and 768: a tablet panel is 700px wide and a 200px
       frame centred in it is a stamp. The phone rules below bring it back down
       to 200, where the panel is only 440 across. */
    .features {
        --shot-w: 240px;
        --shot-rise: 0px;
    }

    .feature-panel .feature-shot {
        margin-top: 0;
    }

    /* No frame beside the header any more, so the header stops budgeting for
       one and goes back to being capped by its measure alone. */
    .features .section-header {
        max-width: calc(640px + var(--panel-pad));
    }
}

/* ─── The bar gives up at 1200, not 1024 ───
   Raised 2026-08-24 with the same argument that set it at 1024 before.

   The bar then held six links in Russian — Возможности, Как работает,
   Транспорты, Как пользоваться, Вопросы, Скачать — plus the wordmark and the
   language button, about 980px of bar. At 900 the first link was sitting on
   the "y" of Stealtify and two of them had wrapped onto a second line; at 1024
   it fitted with nothing to spare, which is not a margin, it is luck.

   Two destinations joined it — Своя подписка and Статьи — and one duplicate
   left: "Как пользоваться" and "Документация" both pointed at /docs, so the
   first went. Eight links now measure 965px of links alone, and with the
   wordmark and the language button the row needs about 1230. Measured over
   CDP: at 1025 and at 1100 the links wrapped onto a second row, at 1200 they
   did not. So the threshold follows the measurement rather than the other way
   round.

   The whole of the menu's mobile treatment moves up to this width together —
   the overlay, the burger, the cross it turns into, and the language button
   that has to stay above the overlay. */
@media (max-width: 1200px) {
    .nav-links {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        /* Fully opaque: the links must never sit over page content. */
        background: var(--stx-surface-dark);
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 40px;
        z-index: 99;
        overflow-y: auto;
        overscroll-behavior: contain;
        padding: 96px 24px 40px;
    }

    /* Mobile browsers report 100vh taller than the visible area; dvh accounts
       for the collapsing address bar. Declared second so it only applies where
       supported. */
    @supports (height: 100dvh) {
        .nav-links {
            height: 100dvh;
        }
    }

    .nav-links.active {
        display: flex;
    }

    /* ─── The menu comes down, it does not appear ───
       display:none to display:flex is one frame, and a full-screen opaque
       panel replacing the page in one frame is indistinguishable from a
       navigation. 200ms of fade and a 10px drop is enough for the eye to
       register that a layer arrived over the page rather than that the page
       changed.

       An animation rather than a transition, for the same reason as the
       lightbox: there is no transitioning out of display:none without
       @starting-style. Opening only — closing stays instant, because the
       reader has already chosen and an exit they have to wait through is an
       exit in the way.

       The links follow the panel rather than arriving with it: six of them
       rising a beat behind the background is what makes it read as a menu
       opening instead of a screen being swapped. 34ms apart is under a
       quarter of a second for the whole set. */
    .nav-links.active {
        animation: navOpen 0.2s cubic-bezier(0.22, 1, 0.36, 1);
    }

    .nav-links.active > * {
        animation: navItemIn 0.26s cubic-bezier(0.22, 1, 0.36, 1) backwards;
    }

    .nav-links.active > *:nth-child(1) { animation-delay: 0.05s; }
    .nav-links.active > *:nth-child(2) { animation-delay: 0.084s; }
    .nav-links.active > *:nth-child(3) { animation-delay: 0.118s; }
    .nav-links.active > *:nth-child(4) { animation-delay: 0.152s; }
    .nav-links.active > *:nth-child(5) { animation-delay: 0.186s; }
    .nav-links.active > *:nth-child(6) { animation-delay: 0.22s; }

    @keyframes navOpen {
        from { opacity: 0; transform: translateY(-10px); }
        to   { opacity: 1; transform: translateY(0); }
    }

    @keyframes navItemIn {
        from { opacity: 0; transform: translateY(12px); }
        to   { opacity: 1; transform: translateY(0); }
    }

    .nav-links a {
        font-size: 1.5rem;
        font-weight: 600;
    }

    /* Hide desktop lang switcher inside nav-links on mobile */
    .nav-links .lang-switcher {
        display: none;
    }

    /* Must stay above the overlay, otherwise the close button and the language
       switcher are buried under the open menu. */
    .nav-right-mobile {
        display: flex;
        position: relative;
        z-index: 101;
    }

    /* On a desktop this button sits in a row of six links and takes its size
       from them. Here it is one of two controls in the whole bar, next to a
       44px burger, and at 0.8rem in 6px of padding it was a chip — under the
       44px that a finger needs to hit reliably. */
    .nav-right-mobile .lang-switcher {
        min-height: 44px;
        padding: 10px 18px;
        font-size: 0.95rem;
    }

    .nav-toggle {
        display: flex;
        z-index: 101;
    }

    /* Stop the page behind the overlay from scrolling. */
    body.nav-open {
        overflow: hidden;
    }

    /* The burger becoming a cross. This lived in the short-viewport block
       below, next to the rest of the menu's mobile styling, where the extra
       `and (max-height: 560px)` meant it only ran on a landscape phone — on
       every portrait phone the menu opened and the three bars stayed three
       bars. Nothing about a cross depends on the viewport being short. */
    .nav-toggle.active span:nth-child(1) {
        transform: rotate(45deg) translate(5px, 5px);
    }

    .nav-toggle.active span:nth-child(2) {
        opacity: 0;
    }

    .nav-toggle.active span:nth-child(3) {
        transform: rotate(-45deg) translate(5px, -5px);
    }
}

/* Everything below is about the page being narrow, not about the menu, so it
   keeps the 768 it was written for. */
@media (max-width: 768px) {
    .features-grid {
        grid-template-columns: 1fr;
    }

    /* ─── The TV section, phones only ───
       Above this width nothing here applies and the section is exactly what it
       was: a centred heading over a two-column split.

       On a phone the split is one column and the order was heading, copy,
       list, link, and then finally the device the whole section is about —
       three screens after the sentence that introduced it. So the device comes
       first and the heading moves onto it, the way a caption sits on a still.

       Revised 2026-08-20. The heading used to be laid over the foot of the
       screen with a scrim behind it, the way a title sits on a still. It read
       badly and was reported twice: the TV frame is wide and short, the scrim
       took two thirds of it to carry a 2rem line, and what the reader got was
       a screenshot mostly hidden under a gradient with a caption competing for
       the same corner.

       So the heading comes off the picture and goes under it. Order on a
       phone: the device, then the heading, then the copy — the device still
       arrives first, which was the whole point of the rearrangement, and the
       heading now introduces the copy it belongs to instead of annotating an
       image. The scrim and the overlaid caption go with it.

       The mechanics: `display: contents` dissolves the split's own box so its
       two halves become items of the container's grid, which is what lets the
       heading be placed between them — it is the split's sibling, not its
       child, so without this it cannot be ordered against the split's halves
       at all. */
    .tv-section .container {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
    }

    .tv-section .split {
        display: contents;
    }

    .tv-section .split-media {
        grid-area: 1 / 1;
        position: relative;
    }

    /* Under the device, above the copy it introduces. Left aligned, like
       every other heading on a phone here — a centred line over a left-set
       paragraph is the seam that made the old overlay look pasted on. */
    .tv-section .tv-title {
        grid-area: 2 / 1;
        text-align: left;
        margin: 26px 0 0;
    }

    .tv-section .split-text {
        grid-area: 3 / 1;
        margin-top: 14px;
    }

    /* The scrim existed only to carry the heading that used to lie on the
       screen. With the heading under the device there is nothing to carry, and
       a gradient over two thirds of a screenshot is just a screenshot the
       reader cannot see. Removed with it; the stand comes back for the same
       reason.

       Left as an explicit `none` rather than deleted, because the base rule
       for .device-screen::after lives outside this breakpoint and this is
       where the phone opts out of it. */
    .tv-section .device-screen::after {
        display: none;
    }

    /* Through the variable, so the section header's indent and the frame's
       negative margin come down with the padding instead of being left at the
       1024px value. The frame comes down with the panel it is centred in. */
    .features {
        --panel-pad: 28px;
        --shot-w: 200px;
    }

    .steps {
        --step-shot-w: 152px;
    }

    .step-content p {
        font-size: 0.95rem;
    }

    /* ─── Headings, set for a phone rather than scaled down to one ───
       All four of these are fluid, and a viewport-width term is exactly the
       wrong instrument at this end of the range: 2.6vw of a 390px screen is
       10px, so every one of them was pinned to its floor, and the floors were
       set to whatever kept a desktop clause on one line. A phone headline is
       not competing with anything for the width — it is the first thing on the
       screen and it should look it. Stated flat here, so the clamps upstairs
       keep governing the sizes they were tuned for and nothing changes above
       768px. */
    .hero-title {
        font-size: 2.8rem;
    }

    .hero-faq-title {
        font-size: 1.9rem;
    }

    .section-title {
        font-size: 2.3rem;
    }

    .page-title {
        font-size: 2.4rem;
    }

    .step-content h3,
    .feature-card h3,
    .feature-panel-title {
        font-size: 1.35rem;
    }

    .download-card {
        padding: 48px 24px;
    }

    .download-requirements {
        flex-direction: column;
        align-items: center;
        gap: 12px;
    }

    .download-actions {
        flex-direction: column;
        align-items: center;
    }

    .footer-content {
        flex-direction: column;
        gap: 24px;
        text-align: center;
    }

    /* Mark over name, not beside it. Stacked, the footer is a centred column,
       and a horizontal lockup in the middle of it reads as a row that failed
       to wrap. 14px, not the 6 it had: a 72px mark needs a gap you can see or
       the wordmark reads as part of the drawing. */
    .footer-brand {
        flex-direction: column;
        gap: 14px;
    }

    /* Stacked but still left-aligned. Centred, the two buttons floated under a
       left-aligned headline and paragraph with nothing to line up with —
       stretch puts their left edge on the same vertical as every other line in
       the column, and the pair reads as part of the copy rather than as a
       separate centred block. */
    .hero-actions {
        flex-direction: column;
        align-items: stretch;
    }

    /* ─── The hero's own rhythm on a phone ───
       Stacked, the two halves of the hero are one column, and the 48px grid gap
       that separated them side by side is doing a different job now: it is the
       space between "here is the product" and "here is what people ask about
       it". Those are two sections of reading, not two columns.

       The FAQ heading loses some of the 28px it needs beside a list on a wide
       screen — on a phone the questions are full width and the heading sits
       directly over the first one, so the relationship is already obvious. */
    .hero-content {
        gap: 64px;
    }

    .hero-faq-title {
        margin-bottom: 32px;
    }

    /* The link under the questions is the end of the hero, and what follows it
       is 120px of section padding. Two full screens of air on a 390px phone,
       for a gap that reads correctly at 24px because the FAQ ends in a line of
       text rather than a block. */
    .hero-faq-more {
        margin-top: 20px;
    }

    .hero {
        padding-bottom: 32px;
    }
}

/* Short viewports (landscape phones, split screen): six links at 1.5rem with
   40px gaps do not fit, so tighten the menu instead of relying on scrolling.
   What is left here is only what a short viewport actually needs — the menu,
   and a hero that would otherwise reserve a whole 100vh for itself on a
   screen 500px tall. */
@media (max-width: 768px) and (max-height: 560px) {
    .nav-links {
        gap: 22px;
        padding-top: 76px;
    }

    .nav-links a {
        font-size: 1.2rem;
    }

    .hero {
        padding: 100px 0 60px;
        min-height: auto;
    }

}

/* ─── Steps, stacked ───
   Below about 620px the copy column and the frame can no longer stand side by
   side: reserving 190px of the card for the frame leaves the heading about
   forty characters of line. So the frame comes out from beside the card and
   goes over it — still overlapping, only now it stands above the card and the
   card is drawn up over its foot, instead of standing across its side. The
   rest of the composition is unchanged. */
@media (max-width: 620px) {
    /* 185px, not the 148 it was. The numeral above it is 93px tall and the
       card is the full width of the screen; at 148 the frame was the smallest
       thing in a composition built out of large ones, and it read as an
       afterthought rather than as the screen the step produces. 185 is the
       most that leaves the frame clear of the card's rounded corner on a
       360px screen. */
    /* Declared on the section, not on .steps, so the heading can budget for
       the frame too — it is a sibling of .steps and would not see a property
       set inside it. One number, two users. */
    .how-it-works {
        --step-shot-col: 185px;
        --step-shot-margin: 22px;
    }

    .steps {
        --step-shot-w: var(--step-shot-col);
        --step-pad: 22px;
        gap: 44px;
        padding: 0;
    }

    /* ─── The heading moves into the corner the first frame leaves empty ───
       Stacked, the first step is a frame pinned to the right edge with a
       column of nothing beside it — measured at 121px wide and 328px tall on a
       360px screen — while the heading sat above the whole thing, centred and
       full width, spending 159px of height to say four words. The two are put
       in the same grid cell instead: one row, both children in it, the heading
       held to the top-left corner and capped at the width the frame leaves.

       A grid cell rather than a negative margin, because the pull would have
       to equal the heading's own height and that is not a constant — it is two
       lines in Russian, and the width and the language both move it. Stacked,
       the row is as tall as .steps and the heading simply sits inside it.

       Only the odd steps put their frame on the right (see the mirror above),
       and the first step is odd — which is the one this is measured against. */
    .how-it-works .container {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
    }

    .how-it-works .section-header,
    .how-it-works .steps {
        grid-area: 1 / 1;
    }

    .how-it-works .section-header {
        align-self: start;
        justify-self: start;
        text-align: left;
        /* It no longer pushes anything: the steps are in the same cell. */
        margin-bottom: 0;
        max-width: calc(100% - var(--step-shot-col) - var(--step-shot-margin));
        /* Above the frame, not under it — the cap keeps them apart, and this
           is the guard for the case where a longer translation defeats it. */
        position: relative;
        z-index: 2;
    }

    /* Long words in a column this narrow have nowhere to go, and a heading
       that overflows its cap would run under the frame. */
    .how-it-works .section-title {
        overflow-wrap: break-word;
    }

    .step {
        flex-direction: column;
        align-items: flex-end;
    }

    /* The desktop alternates sides and the stack did not — every frame sat at
       the right edge, which turns three steps into a column with a stripe down
       one side of it. The even steps put theirs at the left, and with them the
       numeral goes to the other corner, so each step is a mirror of the one
       above rather than a copy. */
    .step:nth-child(even) {
        align-items: flex-start;
    }

    .step:nth-child(even) .step-shot {
        margin-left: 22px;
        margin-right: 0;
    }

    .step:nth-child(even) .step-number {
        left: auto;
        right: -6px;
        top: -50px;
        font-size: 5.8rem;
    }

    .step-card,
    .step:nth-child(even) .step-card {
        min-height: 0;
        padding: 70px var(--step-pad) 28px;
    }

    /* The frame goes *above* the card and the card is pulled up over its foot,
       rather than the frame hanging below the card. Same overlap, opposite
       order — and the difference is that the card is opaque, so it cuts the
       frame off cleanly at its own top edge. Underneath, nothing cut it: the
       frame ended in mid-air a couple of hundred pixels below the card it
       belonged to, which is what read as the screenshots having slid out of
       their steps.

       z-index 0 against the card's 1, so the card really does paint over it —
       a frame with no z-index at all would sit above the card's background and
       the crop would not happen. */
    .step .step-shot {
        position: relative;
        z-index: 0;
        order: 0;
        top: auto;
        right: auto;
        left: auto;
        align-self: flex-end;
        margin: 0 22px -56px 0;
        transform: none;
    }

    /* Mirrored, not re-declared: only the side changes. */
    .step:nth-child(even) .step-shot {
        align-self: flex-start;
        margin: 0 0 -56px 22px;
    }

    .step-card,
    .step:nth-child(even) .step-card {
        position: relative;
        z-index: 1;
        order: 1;
    }

    .step-details,
    .step:nth-child(even) .step-details {
        margin-left: 0;
        margin-right: 0;
    }

    /* The numeral does not scale with the layout — it is the section's one
       piece of display type, and a phone is where it has the least competition
       for attention, not the most. 5.8rem is about 93px, near enough the
       desktop size; the card's top padding is cut from the same measurement so
       the heading still clears the glyph. */
    .step-number {
        top: -50px;
        left: -6px;
        right: auto;
        font-size: 5.8rem;
    }

    /* The glow follows the frame to the head of the step. */
    .step::before,
    .step:nth-child(even)::before {
        top: -30px;
        bottom: auto;
        left: auto;
        right: -30px;
        width: 260px;
        height: 260px;
        transform: none;
    }
}

@media (max-width: 480px) {
    .container {
        padding: 0 16px;
    }

    /* One step down from the 768px sizes, not down to the clamp floors — see
       the note there. Still well above the 1.5rem the scale asks for at this
       width, and deliberately so: on a phone the headline is the first thing
       on the screen and has nothing to share the width with. */
    .hero-title {
        font-size: 2.5rem;
    }

    .hero-faq-title {
        font-size: 1.75rem;
    }

    .section-title {
        font-size: 2.05rem;
    }

    .page-title {
        font-size: 2.15rem;
    }

    /* The old note, kept because the trap is worth remembering: a flat size at
       480px overrides the clamp *upwards* if it is larger than the floor. That
       is now on purpose. It was not before — 2rem sat here against a scale
       asking for 24px, with nothing saying why, which is what put
       the headline over three lines on a phone. */

    .protocols-table th,
    .protocols-table td {
        padding: 12px 12px;
        font-size: 0.78rem;
    }

    /* The final CTA's own buttons carry the longest strings on the page
       ("Скачать последнюю версию") at .btn-large's desktop size, in a card
       whose padding and the container's own leave only ~280px to put them
       in below 400px wide — `white-space: nowrap` (site-wide, so a button
       never wraps into a two-line pill) was clipping the primary button's
       label past the card's edge, invisibly, because the card also clips
       overflow. Sized down and stacked full-width instead of shrunk to fit
       an intrinsic content width: at this width the two actions are already
       the only thing on their own row each, so filling it reads as one
       deliberate choice, not a leftover default. */
    .download-actions .btn-large {
        width: 100%;
        justify-content: center;
        padding: 14px 20px;
        font-size: 0.92rem;
    }

    .download-actions .btn-large svg {
        width: 18px;
        height: 18px;
    }
}

/* ============================
   Subpage primitives
   Shared by faq / docs / inside / eula / privacy.
   Same tokens as the landing page — nothing new is introduced here.
   ============================ */

.container-narrow {
    max-width: 860px;
    margin: 0 auto;
    padding: 0 40px;
}

/* ─── Spacing helpers ───
   Deliberately three, and deliberately named after what they do. They replace
   nine style="margin…" attributes on the subpages, which local preview served
   without a CSP and production would have dropped: the policy in
   nginx.conf.example has no 'unsafe-inline' in style-src, and a style
   attribute is inline style. The pages would have looked right on the laptop
   and lost every one of those margins on the live site.

   Reach for the component's own rule first. These are for the case the
   attributes were doing: one block that needs air above it in one place. */
.gap-above-sm { margin-top: 20px; }
.gap-above    { margin-top: 24px; }
.gap-around   { margin: 18px 0; }

/* Skip link — first tab stop on every page */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 200;
    padding: 12px 20px;
    background: var(--stx-surface-container-high);
    color: var(--text-primary);
    border-radius: 0 0 var(--radius-md) 0;
    font-weight: 600;
}

.skip-link:focus {
    left: 0;
}

/* The skip link and the in-page anchors move focus to their target, so that the
   next Tab continues from where the reader was sent rather than from the link
   they just left. That target is a landmark or a section, not a control, and a
   focus ring drawn round a whole screen of content reads as an error rather
   than as a position — so the ring is dropped while the outline on everything
   a reader can actually operate stays untouched.

   :focus, not :focus-visible: the move is programmatic, and Chrome treats a
   scripted focus on a tabindex="-1" element as visible when it followed a key
   press — which is exactly the case here, and exactly when the box appears. */
main:focus,
[tabindex="-1"]:focus {
    outline: none;
}

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

/* Visible keyboard focus everywhere */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
    outline: 2px solid var(--stx-accent-plum-light);
    outline-offset: 3px;
    border-radius: 4px;
}

/* ─── Page hero (compact, under the fixed navbar) ─── */

.page-hero {
    padding: 160px 0 64px;
    position: relative;
    overflow: hidden;
}

.page-hero::after {
    content: '';
    position: absolute;
    top: -320px;
    left: 50%;
    transform: translateX(-50%);
    width: 900px;
    height: 520px;
    background: var(--stx-accent-plum);
    filter: blur(150px);
    opacity: 0.35;
    pointer-events: none;
    z-index: 0;
}

.page-hero > .container,
.page-hero > .container-narrow {
    position: relative;
    z-index: 1;
}

.page-title {
    font-size: clamp(2rem, 5vw, 3rem);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.02em;
    margin-bottom: 16px;
}

.page-lead {
    font-size: 1.05rem;
    color: var(--text-secondary);
    line-height: 1.7;
    max-width: 680px;
}

.breadcrumbs {
    font-size: 0.85rem;
    color: var(--text-muted);
    margin-bottom: 20px;
}

.breadcrumbs a {
    color: var(--text-secondary);
    transition: color var(--transition);
}

.breadcrumbs a:hover {
    color: var(--text-primary);
}

.breadcrumbs .sep {
    margin: 0 8px;
}

/* ─── Content sections ─── */

.page-section {
    padding: 56px 0;
}

.page-section + .page-section {
    border-top: 1px solid var(--border-color);
}

.page-section h2 {
    font-size: clamp(1.4rem, 3vw, 1.9rem);
    font-weight: 700;
    letter-spacing: -0.01em;
    margin-bottom: 20px;
}

.page-section h3 {
    font-size: 1.1rem;
    font-weight: 600;
    margin: 28px 0 10px;
}

.prose p,
.prose li {
    color: var(--text-secondary);
    font-size: 0.98rem;
    line-height: 1.75;
}

.prose p + p {
    margin-top: 14px;
}

.prose ul,
.prose ol {
    margin: 14px 0 14px 22px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.prose strong {
    color: var(--text-primary);
    font-weight: 600;
}

.prose a:not(.btn) {
    color: var(--accent-primary);
    text-decoration: underline;
    text-underline-offset: 3px;
    transition: color var(--transition);
}

.prose a:not(.btn):hover {
    color: var(--stx-on-tertiary);
}

code,
.code {
    font-family: var(--font-mono);
    font-size: 0.85em;
    padding: 2px 7px;
    border-radius: 6px;
    background: var(--stx-surface-variant-dark);
    color: var(--stx-on-tertiary);
    word-break: break-word;
}

/* ─── Cards ─── */

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

/* Deliberately a different hover from .feature-card: an accent edge that draws
   in, rather than the same lift repeated on every component. */
/* These are anchors now, not articles — `display: block` and the inherited
   colour are what stop the browser drawing them as links, since everything
   else about them was already a card. The rest of the file's `a { color:
   inherit }` covers the colour; `display` has to be said here. */
.info-card {
    display: block;
    position: relative;
    overflow: hidden;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: 24px;
    transition: background var(--transition), border-color var(--transition), transform var(--transition);
}

/* A card that leads somewhere says so under the pointer: the top edge fills in
   (that was already here) and the whole card lifts a little, which is the
   difference between "this changes colour" and "this is a button". */
.info-card:hover {
    transform: translateY(-3px);
}

.info-card:focus-visible {
    outline: 2px solid var(--stx-accent-plum-light);
    outline-offset: 3px;
}

.info-card::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: linear-gradient(90deg, var(--stx-accent-plum-soft), var(--stx-accent-plum-light));
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--transition);
}

.info-card:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-hover);
}

.info-card:hover::after {
    transform: scaleX(1);
}

.info-card h3 {
    margin: 0 0 8px;
    font-size: 1.05rem;
    font-weight: 600;
}

.info-card p {
    color: var(--text-secondary);
    font-size: 0.92rem;
    line-height: 1.65;
}

.card-icon {
    width: 44px;
    height: 44px;
    margin-bottom: 14px;
    font-size: 22px; /* drives .icon sizing (1em) */
}

/* ═══ «Что дальше» — виджеты вместо строчки ссылок ═══
   Ссылка на соседний материал была набрана тем же чипом, что и «читать
   дальше» внутри раздела: строка текста в рамке. Три таких строки подряд
   читаются как список сносок, а не как «вот куда стоит пойти» — глазу не за
   что зацепиться, и на пролистывании блок пропускают целиком.

   Здесь он собран как полка рекомендаций: карточка, надпись-рубрика, название
   и знак, который выходит за её границу. Выход за границу — не украшение, а
   единственное, что отличает полку от очередной сетки карточек: плитка со
   знаком стоит на 22px выше верхнего края и на 6 градусов повёрнута, поэтому
   ряд читается как приклеенные ярлыки, а не как таблица. Рамке карточки при
   этом нельзя обрезать содержимое — overflow остаётся видимым, а сверху у
   сетки зарезервирован воздух ровно на вынос.

   Ни одного изображения: знак берётся из того же набора масок, что и все
   иконки сайта. */
.next-cards {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: 18px;
    /* Воздух под вынос плитки: 22px выноса плюс тень. */
    margin: 34px 0 0;
    padding: 30px 0 0;
}

/* Внутри документации и прозы у списков и ссылок свои правила, и все они
   специфичнее одного класса: `.prose ul` разворачивает список в колонку
   флексом, `.docs-section ul` добавляет отступ под маркеры, а `.prose
   a:not(.btn)` красит ссылку в сиреневый и подчёркивает её. Полке
   рекомендаций не нужно ни одно из трёх, поэтому сетка, отступ и вид ссылки
   повторены здесь — селекторами от секции, чтобы спорить на равных.
   Три класса у ссылки — ровно чтобы перебить `.prose a:not(.btn)`. */
.docs-section .next-cards,
.prose .next-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    padding-left: 0;
    gap: 18px;
}

.docs-section .next-cards .next-card-link,
.prose .next-cards .next-card-link {
    color: inherit;
    text-decoration: none;
}

.docs-section .next-cards .next-card,
.prose .next-cards .next-card,
.next-cards .next-card {
    max-width: none;
    margin: 0;
}

.next-card {
    position: relative;
}

.next-card-link {
    display: block;
    position: relative;
    height: 100%;
    padding: 40px 20px 20px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
    color: inherit;
    text-decoration: none;
    transition: background var(--transition), border-color var(--transition),
                transform var(--transition), box-shadow var(--transition);
}

.next-card-link:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-hover);
    transform: translateY(-3px);
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
}

.next-card-link:focus-visible {
    outline: 2px solid var(--stx-accent-plum-light);
    outline-offset: 3px;
}

/* Тот самый вынос. Плитка стоит выше карточки и левее её padding-бокса, чуть
   повёрнута — и распрямляется под курсором. */
.next-art {
    position: absolute;
    top: -22px;
    left: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border: 1px solid var(--stx-accent-plum-soft);
    border-radius: var(--radius-md);
    background: linear-gradient(150deg, var(--stx-accent-plum-mid) 0%, var(--stx-surface-variant-dark) 100%);
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.38);
    font-size: 26px;
    color: var(--text-primary);
    transform: rotate(-6deg);
    transition: transform var(--transition), border-color var(--transition);
}

.next-card-link:hover .next-art {
    transform: rotate(0deg) translateY(-2px);
    border-color: var(--stx-accent-plum-light);
}

/* Инструкция — единственная карточка, которую не читают, а выполняют, и на
   сайте для этого уже есть янтарь. */
.next-card--do .next-art {
    border-color: var(--stx-amber-edge);
    background: linear-gradient(150deg, var(--stx-amber-core) 0%, var(--stx-surface-variant-dark) 100%);
}

.next-card--do .next-card-link:hover .next-art {
    border-color: var(--stx-amber);
}

.next-kicker {
    display: block;
    margin-bottom: 6px;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.next-card--do .next-kicker {
    color: var(--stx-amber);
}

.next-title {
    display: block;
    font-size: 0.98rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--text-primary);
}

.next-desc {
    display: block;
    margin-top: 8px;
    font-size: 0.86rem;
    line-height: 1.6;
    color: var(--text-secondary);
}

/* Стрелка появляется у нижнего края и уезжает вправо под курсором: тот же
   жест, что у чипов, только без рамки — карточка сама уже кнопка. */
.next-go {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 14px;
    font-size: 0.84rem;
    font-weight: 600;
    color: var(--stx-accent-plum-light);
}

.next-card--do .next-go {
    color: var(--stx-amber-glow);
}

.next-go::after {
    content: '\2192';
    transition: transform var(--transition);
}

.next-card-link:hover .next-go::after {
    transform: translateX(3px);
}

@media (max-width: 620px) {
    /* Одна колонка — вынос плитки уменьшается: на узкой карточке ярлык в
       56px съедает строку заголовка. */
    .next-cards {
        gap: 26px;
        padding-top: 26px;
    }

    .next-art {
        width: 48px;
        height: 48px;
        top: -18px;
        font-size: 22px;
    }

    .next-card-link {
        padding: 34px 16px 18px;
    }
}

/* ═══ Список статей: первая — витриной, остальные — полкой ═══
   Пять одинаковых карточек в сетке не говорят читателю ничего о том, с чего
   начинать: все пять весят одинаково, и выбор превращается в чтение пяти
   заголовков подряд. Здесь первая статья вынесена витриной — крупный
   заголовок, описание, знак во всю высоту блока, выходящий за его правый
   край, — а остальные лежат полкой карточек того же языка, что и «что
   дальше». Разница в весе и есть навигация. */
.article-list {
    display: flex;
    flex-direction: column;
    gap: 34px;
}

.article-lead {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 148px;
    gap: 24px;
    align-items: center;
    padding: 30px 32px;
    border: 1px solid var(--stx-accent-plum-soft);
    border-radius: var(--radius-lg);
    background:
        linear-gradient(103deg, rgba(58, 24, 64, 0.42) 0%, transparent 58%),
        var(--bg-card);
    color: inherit;
    text-decoration: none;
    transition: border-color var(--transition), background var(--transition),
                transform var(--transition), box-shadow var(--transition);
}

.article-lead:hover {
    border-color: var(--stx-accent-plum-light);
    transform: translateY(-3px);
    box-shadow: 0 16px 38px rgba(0, 0, 0, 0.34);
}

.article-lead:focus-visible {
    outline: 2px solid var(--stx-accent-plum-light);
    outline-offset: 3px;
}

/* Знак витрины выходит за правый край карточки — на 26px и с наклоном.
   Он же единственная «иллюстрация» на странице: маска из общего набора,
   растянутая до размера, на котором читается как рисунок, а не как значок. */
.article-lead-art {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 148px;
    height: 148px;
    justify-self: end;
    margin-right: -54px;
    border: 1px solid var(--stx-accent-plum-soft);
    border-radius: var(--radius-lg);
    background: linear-gradient(150deg, var(--stx-accent-plum-mid) 0%, var(--stx-surface-variant-dark) 100%);
    box-shadow: 0 18px 40px rgba(0, 0, 0, 0.42);
    font-size: 66px;
    color: var(--text-primary);
    transform: rotate(-5deg);
    transition: transform var(--transition), border-color var(--transition);
}

.article-lead:hover .article-lead-art {
    transform: rotate(0deg) scale(1.03);
    border-color: var(--stx-accent-plum-light);
}

.article-kicker {
    display: block;
    margin-bottom: 10px;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--stx-accent-plum-light);
}

.article-lead h2 {
    margin: 0 0 10px;
    font-size: 1.42rem;
    font-weight: 700;
    line-height: 1.25;
    color: var(--text-primary);
}

.article-lead p {
    margin: 0;
    font-size: 0.95rem;
    line-height: 1.65;
    color: var(--text-secondary);
}

@media (max-width: 700px) {
    /* Знак уходит наверх и становится ярлыком: витрина в одну колонку. */
    .article-lead {
        grid-template-columns: minmax(0, 1fr);
        gap: 18px;
        padding: 34px 20px 24px;
    }

    .article-lead-art {
        width: 72px;
        height: 72px;
        justify-self: start;
        margin: 0;
        font-size: 34px;
        order: -1;
    }
}

/* ═══ Страница статьи ═══ */

/* Содержание. Статья длиной в семь разделов читается кусками, и человеку,
   пришедшему с конкретным вопросом, нужен вход в нужный кусок, а не первая
   строка. Машине, которая ищет готовый ответ, — тоже: ссылки с якорями дают
   ей карту статьи до чтения. */
.art-toc {
    margin: 30px 0 0;
    padding: 20px 24px;
    border: 1px solid var(--border-color);
    border-left: 2px solid var(--stx-accent-plum-mid);
    border-radius: var(--radius-md);
    background: var(--bg-card);
}

.art-toc-title {
    margin: 0 0 12px;
    font-size: 0.74rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.prose .art-toc ol,
.art-toc ol {
    list-style: none;
    counter-reset: art-toc;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 6px 22px;
    margin: 0;
    padding: 0;
}

.prose .art-toc li,
.art-toc li {
    max-width: none;
    font-size: 0.9rem;
    line-height: 1.5;
}

/* От секции — иначе `.prose a:not(.btn)` подчёркивает каждый пункт
   содержания: семь подчёркнутых строк подряд читаются как сноски. */
.prose .art-toc a,
.art-toc a {
    display: block;
    padding: 5px 0 5px 30px;
    position: relative;
    color: var(--text-secondary);
    text-decoration: none;
    transition: color var(--transition);
}

.art-toc a::before {
    counter-increment: art-toc;
    content: counter(art-toc);
    position: absolute;
    left: 0;
    top: 5px;
    width: 20px;
    font-family: var(--font-mono);
    font-size: 0.82rem;
    font-weight: 700;
    color: var(--stx-accent-plum-light);
}

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

/* Заголовок раздела статьи получает волосяную линию сверху: семь разделов
   подряд без неё сливаются в одну ленту текста. Якорь — цель ссылки из
   содержания, поэтому ему нужен отступ под липкой шапкой. */
.article-body h2[id] {
    scroll-margin-top: 96px;
}

.article-body > h2 {
    margin-top: 40px;
    padding-top: 22px;
    border-top: 1px solid var(--border-color);
}

.article-body > h2:first-of-type {
    border-top: none;
    padding-top: 0;
}

/* ─── Callout / note ─── */

.note {
    display: flex;
    gap: 14px;
    padding: 18px 20px;
    background: var(--stx-tertiary-container);
    border: 1px solid var(--border-hover);
    border-radius: var(--radius-md);
    margin: 20px 0;
}

.note p {
    color: var(--stx-on-tertiary);
    font-size: 0.92rem;
    line-height: 1.65;
}

.note-icon {
    flex-shrink: 0;
    font-size: 19px; /* drives .icon sizing (1em) */
    line-height: 1.5;
    color: var(--stx-amber);
    display: flex;
    align-items: center;
    height: 1.6em;
}

/* ─── Numbered guide steps ─── */

.guide-steps {
    display: flex;
    flex-direction: column;
    gap: 20px;
    counter-reset: guide;
    list-style: none;
    margin: 0;
}

/* Янтарь здесь, и только здесь из всего текста — по роли, а не для украшения.
   Плам на этом сайте означает структуру: заголовки, номера разделов,
   навигацию, ссылки. Шаг инструкции — не структура, а действие, которое
   выполняют руками, и до сих пор он выглядел ровно как всё остальное: серая
   карточка на сером фоне, бледно-сиреневый заголовок и номер кеглем в
   семнадцать пикселей. При быстром пролистывании страницы в десять
   разделов такой блок не виден вообще — а он-то и есть то, ради чего сюда
   приходят.

   Три приёма, и все три работают на периферийном зрении, то есть до чтения:
   тёплая полоса у левого края (единственное пятно другого цвета на всей
   полосе прокрутки), крупный номер и тёплая подсветка от него внутрь
   карточки. Ни один из них не кричит вблизи: полоса в три пикселя, подсветка
   в семь процентов, номер — цифра, а не плашка. */
.guide-step {
    position: relative;
    background:
        linear-gradient(103deg, var(--stx-amber-halo) 0%, transparent 46%),
        var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: 26px 26px 26px 96px;
    overflow: hidden;
    transition: background var(--transition), border-color var(--transition),
                box-shadow var(--transition);
}

/* Полоса у левого края — то, за что цепляется глаз при пролистывании. Она же
   держит номер: цифра стоит на ней, а не висит в поле. */
.guide-step::after {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--gradient-amber-glyph);
}

.guide-step:hover {
    background:
        linear-gradient(103deg, var(--stx-amber-face) 0%, transparent 52%),
        var(--bg-card-hover);
    border-color: var(--stx-amber-edge);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.32);
}

/* Номер набран градиентом по тексту, а не заливкой: сплошной оранжевый в таком
   кегле спорит с заголовком за внимание, а градиент от светлого к ядру
   оставляет цифру яркой сверху и уводит её в тень снизу — она читается как
   метка, а не как второй заголовок. */
.guide-step::before {
    counter-increment: guide;
    content: counter(guide, decimal-leading-zero);
    position: absolute;
    left: 24px;
    top: 18px;
    font-family: var(--font-mono);
    font-size: 2.5rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.05em;
    background: var(--gradient-amber-glyph);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.guide-step h3,
.guide-step h4 {
    margin: 0 0 8px;
    font-size: 1.08rem;
    font-weight: 600;
    /* Заголовок шага — первичный текст. Сиреневый оставлен структуре
       страницы; внутри карточки он делал название шага похожим на ссылку. */
    color: var(--text-primary);
}

.guide-step p,
.guide-step li {
    color: var(--text-secondary);
    font-size: 0.94rem;
    line-height: 1.7;
}

.guide-step ul,
.guide-step ol {
    margin: 12px 0 0 20px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* ─── Screenshots in device frames ─── */
/*
   Markup:
     <figure class="screenshot" data-slot="rules-screen">
       <div class="device device-phone">          (or .device-tv)
         <div class="device-screen">
           …placeholder, or <img> once the asset exists…
         </div>
       </div>
       <figcaption>…</figcaption>
     </figure>

   The frame fixes the aspect ratio (9:19.5 phone, 16:9 TV), so a row of
   screenshots stays aligned whatever device they were captured on, and the
   section does not reflow when placeholders are swapped for real images.
   To fill a slot, replace the .screenshot-placeholder div with an <img>.
*/

.screenshot {
    margin: 20px 0 0;
}

.device {
    position: relative;
    margin-inline: auto;
    border-radius: 30px;
    padding: 9px 9px 13px;
    background: linear-gradient(158deg,
                var(--stx-accent-plum-soft) 0%,
                var(--stx-surface-container-high) 38%,
                var(--stx-surface-dark) 100%);
    box-shadow: 0 18px 46px rgba(0, 0, 0, 0.45),
                inset 0 1px 0 rgba(230, 214, 234, 0.20);
}

/* Earpiece slot */
.device-phone::before {
    content: '';
    position: absolute;
    top: 17px;
    left: 50%;
    transform: translateX(-50%);
    width: 46px;
    height: 4px;
    border-radius: 3px;
    background: rgba(0, 0, 0, 0.6);
    z-index: 2;
}

/* Capped so a portrait frame cannot grow absurdly tall in a wide column. */
.device-phone {
    max-width: 240px;
}

.device-screen {
    position: relative;
    overflow: hidden;
    border-radius: 22px;
    background: var(--stx-surface-variant-dark);
}

/* Соотношение самих скриншотов, а не «типового» телефона. Рамка стояла на
   9/19.5 (0.399), а снимки в screenshot/ — 573x1280 (0.448): object-fit: cover
   срезал 10.8% высоты, и при object-position: top center это молча съедало
   нижнюю панель приложения. Кадр обязан совпадать с тем, что в него кладут. */
.device-phone .device-screen {
    aspect-ratio: 573 / 1280;
}

/* TV: shallower bezel, 16:9, and a stand drawn below the body. */
.device-tv {
    max-width: 520px;
    border-radius: 14px;
    padding: 8px;
    margin-bottom: 8px;
}

.device-tv .device-screen {
    aspect-ratio: 16 / 9;
    border-radius: 8px;
}

.device-tv::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: -8px;
    transform: translateX(-50%);
    width: 84px;
    height: 8px;
    border-radius: 0 0 8px 8px;
    background: var(--stx-surface-container-high);
    border: 1px solid var(--border-color);
    border-top: 0;
}

/* ─── Zoomable frames and the lightbox ───
   The class is added by script.js only to frames that hold a real image, so a
   placeholder never advertises a zoom that would open nothing. */
.device.is-zoomable {
    cursor: zoom-in;
}

.device.is-zoomable:focus-visible {
    outline: 2px solid var(--stx-accent-plum-light);
    outline-offset: 5px;
}

/* Centred explicitly rather than by the browser's own dialog rules. A modal
   dialog is centred by a UA `margin: auto` against `inset: 0`, and the reset at
   the top of this file sets `margin: 0` on everything — which outranks the UA
   sheet and drops the dialog in the top-left corner. Restating position, inset
   and margin here makes it independent of that. */
/* ─── Opening is a movement, not a cut ───
   The dialog went from nothing to a full-screen blurred backdrop with a
   picture on it in a single frame, and at that size a cut reads as a glitch —
   the eye has no idea whether the page navigated. 180ms of scale and fade is
   enough to say "this came out of the page" and short enough that nobody
   waits for it.

   On the animation, not on a transition: <dialog> is display:none until
   showModal(), and a transition out of display:none needs @starting-style and
   allow-discrete, which are Chrome 117+ and Safari 17.4+. An animation on
   [open] starts the moment the element is rendered and works everywhere the
   dialog itself does.

   Closing is left as a cut on purpose. It is dismissal — the reader has
   already decided — and 180ms of exit is 180ms of the thing they asked to be
   rid of. The reduced-motion block takes the name away and the dialog simply
   appears, which is correct for the preference.

   The backdrop is animated separately because it is a separate box: sharing
   one animation would scale the blur with the picture. */
.lightbox[open] {
    animation: lightboxIn 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}

.lightbox[open]::backdrop {
    animation: lightboxBackdropIn 0.18s ease-out;
}

@keyframes lightboxIn {
    from { opacity: 0; transform: scale(0.94); }
    to   { opacity: 1; transform: scale(1); }
}

@keyframes lightboxBackdropIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

.lightbox {
    position: fixed;
    inset: 0;
    margin: auto;
    width: fit-content;
    height: fit-content;
    max-width: min(92vw, 460px);
    max-height: 94vh;
    border: 0;
    padding: 0;
    background: transparent;
    overflow: visible;
    color: var(--text-primary);
}

.lightbox::backdrop {
    background: rgba(10, 10, 13, 0.86);
    -webkit-backdrop-filter: blur(6px);
            backdrop-filter: blur(6px);
}

/* Bounded by height and left to find its own width. These are 9:20 portraits,
   so height is always the binding constraint: fixing the width instead would
   letterbox a 330px-wide screenshot inside a 460px box and draw the border
   around the empty space rather than around the picture. The dialog is
   fit-content, so it shrinks onto whatever width the image settles at. */
.lightbox img {
    display: block;
    width: auto;
    height: auto;
    max-width: 92vw;
    max-height: 82vh;
    margin-inline: auto;
    border-radius: var(--radius-lg);
    border: 1px solid var(--border-color);
    background: var(--stx-surface-variant-dark);
}

.lightbox figcaption {
    margin-top: 14px;
    text-align: center;
    font-size: 0.9rem;
    color: var(--text-secondary);
}

.lightbox-close {
    position: absolute;
    top: -14px;
    right: -14px;
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: 1px solid var(--border-color);
    background: var(--stx-surface-container-high);
    color: var(--text-primary);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    transition: var(--transition);
}

.lightbox-close:hover {
    background: var(--stx-tertiary-container);
    border-color: var(--border-hover);
}

/* The dialog is modal, but the page behind it still scrolls under a wheel. */
body.lightbox-open {
    overflow: hidden;
}

/* Real screenshots fill the screen area edge to edge. */
.device-screen img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: top center;
    display: block;
}

.screenshot-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 20px;
    text-align: center;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.78rem;
    line-height: 1.5;
}

.screenshot-glyph {
    font-size: 26px; /* drives .icon sizing (1em) */
    opacity: 0.45;
}

/* Names the asset that belongs here, so the slot is self-documenting in the
   browser and not only in the README table. Disappears with the placeholder. */
.screenshot-slot {
    padding: 3px 9px;
    border-radius: 6px;
    background: var(--stx-tertiary-container);
    color: var(--stx-on-tertiary);
    font-size: 0.72rem;
    word-break: break-all;
}

/* Screenshot galleries (several shots side by side) */
.screenshot-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 24px;
}

.screenshot-grid .screenshot {
    margin: 0;
}

.screenshot figcaption {
    margin-top: 10px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* ─── FAQ accordion (native details/summary) ─── */

.faq-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.faq-item {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    transition: background var(--transition), border-color var(--transition);
}

.faq-item:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-hover);
}

.faq-item > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 20px 24px;
    cursor: pointer;
    font-size: 1rem;
    font-weight: 600;
    color: var(--text-primary);
    list-style: none;
}

.faq-item > summary::-webkit-details-marker {
    display: none;
}

.faq-item > summary::after {
    content: '+';
    flex-shrink: 0;
    font-size: 1.4rem;
    font-weight: 400;
    line-height: 1;
    color: var(--stx-accent-plum-light);
    transition: transform var(--transition);
}

.faq-item[open] > summary::after {
    transform: rotate(45deg);
}

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

.faq-answer p,
.faq-answer li {
    color: var(--text-secondary);
    font-size: 0.94rem;
    line-height: 1.7;
}

.faq-answer ul {
    margin: 10px 0 0 20px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* ─── Opening and closing ───
   <details> has nothing to animate on its own: the content goes from no box to
   its full box in a single frame. ::details-content is the box the browser
   generates around that content, and interpolate-size is what allows `auto` to
   be one end of a transition — the two together give a real collapse instead
   of a jump. content-visibility has to travel with it, discretely, or the
   content vanishes before the height has finished closing.

   Both properties are recent. Where they are missing the panel snaps open the
   way it always did; nothing else on the page depends on them. */
.faq-item {
    interpolate-size: allow-keywords;
}

.faq-item::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size 0.32s cubic-bezier(0.32, 0.72, 0.3, 1),
                content-visibility 0.32s allow-discrete;
}

.faq-item[open]::details-content {
    block-size: auto;
}

/* The answer arrives a fraction behind its box, so the text reads as landing
   rather than as being uncovered by a wipe. */
.faq-answer {
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity 0.26s ease 0.05s, transform 0.26s ease 0.05s;
}

.faq-item[open] .faq-answer {
    opacity: 1;
    transform: none;
}

/* ─── One accordion per accordion ───
   The rule above this is a complete CSS accordion: ::details-content goes from
   block-size 0 to auto over 0.32s. The hero's list has a second one, written
   by hand in script.js, which animates the answer's own height and padding
   over 0.34s on a different curve. Both were running on the same five cards.

   Two transitions of nearly the same length on nested boxes do not average
   out — the outer one clips while the inner one grows, they finish two
   hundredths apart, and the card settles in two stages with a step at the end.
   That step is the flinch that survived every attempt to fix the hand-written
   half, because the hand-written half was never the one making it. It also
   defeated the height reservation: `reserve()` opens each card and measures,
   and what it measured was a box still clipped to zero by a transition that
   had not started yet, so the room set aside was short by an answer.

   The hand-written one stays, because it is the one that works everywhere —
   ::details-content and interpolate-size are Chrome 129 and later, and on
   /faq, where nothing is hand-animated, they are the whole animation and are
   left alone. Inside the hero the element goes back to behaving like a plain
   <details> and script.js owns the motion. */
.hero-faq .faq-item::details-content {
    block-size: auto;
    overflow: visible;
    transition: none;
}

/* ─── Generic data table ─── */

.data-table-wrapper {
    /* Обязательно, а не для красоты. В ячейках таблицы лежат подписи
       `.visually-hidden` — «поддерживается» / «не поддерживается» для галочек,
       — и они `position: absolute`. Пока у обёртки нет своего позиционирования,
       содержащим блоком для них становится не она, а окно: подписи выпадают
       из прокручиваемой области и не обрезаются ею. Сами по себе они размером
       в пиксель, но лежат по месту своей ячейки — то есть на 430-й точке
       таблицы шириной 625, — и на телефоне растягивают вбок весь документ.
       Видно это было только по тому, что scrollWidth у <html> больше, чем у
       <body>: в потоке страницы не переполнялось ничего.

       `position: relative` возвращает их внутрь обёртки, где overflow их и
       обрезает. Скринридер по-прежнему читает их: clip-path прячет от глаз,
       а не от дерева доступности. */
    position: relative;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    overflow-x: auto;
    background: var(--bg-card);

    /* Полоса прокрутки — часть таблицы, а не системная деталь поверх неё. По
       умолчанию движок рисует в тёмной карточке светлый ползунок во всю свою
       толщину: на телефоне это широкая серая планка поперёк низа карточки,
       перечёркивающая её скруглённый угол. Тонкая полоса с прозрачной дорожкой
       занимает вчетверо меньше и остаётся в цветах страницы.

       Две записи об одном и том же: `scrollbar-*` — стандартные свойства,
       ::-webkit-scrollbar — тот же вид там, где стандартные ещё не работают.
       Полоса именно оставлена, а не спрятана: она единственное, что сообщает,
       что у таблицы есть продолжение справа.

       Цвет — акцент страницы, а не её фон. Ползунок здесь орган управления:
       его берут пальцем и тянут, значит он обязан отличаться от поверхности,
       по которой ездит, — серым по тёмно-серому он читался как шов карточки, а
       не как то, за что можно взяться. */
    scrollbar-width: thin;
    scrollbar-color: var(--stx-accent-plum-mid) var(--bg-inset);
}

.data-table-wrapper::-webkit-scrollbar {
    height: 8px;
}

.data-table-wrapper::-webkit-scrollbar-track {
    background: var(--bg-inset);
}

.data-table-wrapper::-webkit-scrollbar-thumb {
    border-radius: 99px;
    background: var(--stx-accent-plum-mid);
    /* Прозрачная рамка на фоне под сам ползунок: она отодвигает его от края
       карточки, и он не упирается в её границу и скругление. */
    border: 2px solid transparent;
    background-clip: padding-box;
}

.data-table-wrapper:hover::-webkit-scrollbar-thumb,
.data-table-wrapper:focus-within::-webkit-scrollbar-thumb {
    background: var(--stx-accent-plum-light);
    background-clip: padding-box;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    min-width: 520px;
}

.data-table caption {
    padding: 18px 24px 0;
    text-align: left;
    font-size: 0.85rem;
    color: var(--text-secondary);
}

.data-table th,
.data-table td {
    padding: 14px 20px;
    text-align: left;
    font-size: 0.9rem;
    border-bottom: 1px solid var(--border-color);
}

.data-table thead th {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    background: var(--bg-inset);
}

.data-table tbody th {
    font-weight: 600;
    color: var(--text-primary);
}

.data-table td {
    color: var(--text-secondary);
}

.data-table tbody tr:last-child th,
.data-table tbody tr:last-child td {
    border-bottom: none;
}

.data-table tbody tr:hover th,
.data-table tbody tr:hover td {
    background: var(--stx-surface-container-high);
}

/* ─── Pills ─── */

.pill-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    list-style: none;
    margin: 0;
}

.pill {
    padding: 6px 14px;
    border-radius: 100px;
    background: var(--stx-tertiary-container);
    border: 1px solid var(--border-color);
    color: var(--stx-on-tertiary);
    font-size: 0.85rem;
    font-weight: 500;
    font-family: var(--font-mono);
}

/* ─── Page CTA band ─── */

.cta-band {
    margin: 0 auto;
    max-width: 860px;
    background: var(--gradient-main);
    border: 1px solid var(--stx-accent-plum-soft);
    border-radius: var(--radius-xl);
    padding: 40px 32px;
    text-align: center;
    box-shadow: var(--shadow-card);
}

.cta-band h2 {
    font-size: clamp(1.3rem, 3vw, 1.7rem);
    font-weight: 700;
    margin-bottom: 12px;
    color: var(--stx-on-tertiary);
}

.cta-band p {
    color: var(--stx-on-tertiary);
    opacity: 0.85;
    font-size: 0.95rem;
    margin-bottom: 24px;
}

.cta-band .btn-primary {
    background: var(--stx-on-tertiary);
    color: var(--stx-accent-plum-deep);
    border-color: transparent;
}

.cta-band .btn-secondary {
    background: transparent;
    border-color: var(--stx-on-tertiary);
    color: var(--stx-on-tertiary);
}

.cta-actions {
    display: flex;
    justify-content: center;
    gap: 14px;
    flex-wrap: wrap;
}

/* ─── Contact form ─── */

.contact-form {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 18px;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: 28px;
}

.form-field {
    display: flex;
    flex-direction: column;
    gap: 7px;
}

.form-field-wide {
    grid-column: 1 / -1;
}

.form-field label {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-primary);
}

.form-field .hint {
    font-size: 0.78rem;
    color: var(--text-muted);
}

.form-field input,
.form-field select,
.form-field textarea {
    font-family: var(--font-family);
    font-size: 0.92rem;
    color: var(--text-primary);
    background: var(--stx-surface-variant-dark);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-sm);
    padding: 11px 14px;
    transition: border-color var(--transition), background var(--transition);
    width: 100%;
}

.form-field input::placeholder,
.form-field textarea::placeholder {
    color: var(--text-muted);
}

.form-field input:hover,
.form-field select:hover,
.form-field textarea:hover {
    border-color: var(--border-hover);
}

.form-field input[aria-invalid="true"],
.form-field select[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
    border-color: var(--stx-accent-plum-light);
}

.field-error {
    font-size: 0.78rem;
    color: var(--stx-accent-plum-light);
    min-height: 1em;
}

.form-status {
    grid-column: 1 / -1;
    font-size: 0.88rem;
    color: var(--text-secondary);
}

/* ─── The footer carries thirteen links now ───
   Ragged right is what a wrapping flex row does when it is told to end at the
   right edge: the first line fills, whatever is left drops to a second line
   and hangs off the same right edge, so eleven items sit on one line and two
   sit alone under them. Centring the rows makes the block read as a block —
   each line is balanced against the one above it whatever the wrap does.

   flex: 1 lets the links take the room the brand does not need, and the brand
   is told not to give any up: at space-between with a full line of links, the
   flex algorithm was shrinking the wordmark until "Stealtify" broke in two
   beside its own mark. */
.footer-links {
    flex: 1;
    flex-wrap: wrap;
    justify-content: center;
    column-gap: 26px;
    row-gap: 14px;
    /* Capped, and the cap is what balances the two rows. Left to the full width
       the wrap put ten links on the first line and three on the second; held to
       this, it breaks nearer the middle and the block reads as a block rather
       than a line with a remainder under it. */
    max-width: 820px;
    margin-inline: auto;
}

@media (max-width: 768px) {
    .container-narrow {
        padding: 0 20px;
    }

    .page-hero {
        padding: 120px 0 48px;
    }

    .page-section {
        padding: 40px 0;
    }

    /* Номер уходит на строку выше текста: полосы в 96 пикселей под него на
       телефоне нет, а мельче его делать нельзя — он затем и крупный, чтобы
       карточка ловилась взглядом на прокрутке. */
    .guide-step {
        padding: 68px 18px 22px;
    }

    .guide-step::before {
        left: 18px;
        top: 16px;
        font-size: 2.2rem;
    }

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

    .footer-links {
        justify-content: center;
    }
}

@media (max-width: 480px) {
    .container-narrow {
        padding: 0 16px;
    }

    .faq-item > summary {
        padding: 16px 18px;
        font-size: 0.95rem;
    }

    .faq-answer {
        padding: 0 18px 18px;
    }
}

/* ═══ Reduced motion: the movement goes, the page does not go dead ═══
   What was here was the blanket everybody copies — `animation-duration:
   0.001ms` and `transition-duration: 0.001ms` on `*`, `*::before` and
   `*::after`. It is heavier than the preference asks for. "Reduce motion" is a
   request not to be moved; it is not a request for a page where nothing ever
   changes, and the two are not the same thing. Under that rule this site lost
   its arrivals, its hovers, its carousel and the hero's supergraphic all at
   once, and the result is indistinguishable from a stylesheet that failed to
   load — which is exactly how it got reported.

   So the one decision is split into three.

   1. Keyframe animations are off. Every one on this site is decoration —
      a drift, a flare, a wave, a sweep, a shimmer — and none of them carries
      anything a reader would otherwise lose.

   2. Except inside the hero's supergraphic, which keeps running always, by
      request. It is scenery: it is behind the content rather than under the
      pointer, it never moves anything the reader is trying to read, and it is
      slow — one turn of --hero-beat is 3.2s. `.hero.is-idle` still parks it
      when the hero is off screen; that is a different rule and it stays.

   3. Transitions are left alone, and the motion is taken out of them at the
      source instead: where a transition moved something, both ends are given
      the same transform, so there is no distance left to cover. Colour and
      opacity keep their transitions, because a fade is not motion — and that
      is the whole difference between a page that honours the setting and a
      page that looks broken under it.

   On Windows this media query is driven by Accessibility → Visual effects →
   "Animation effects". Turning it off sets the preference for every browser on
   the machine at once, which is worth knowing before hunting for a bug here. */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    /* `animation-name: none`, not `animation-duration: 0.001ms`. The duration
       trick is the one every snippet uses and it does not remove an animation
       — it runs the whole thing inside a single frame. The keyframes are still
       applied, so the element is still put at its `from` values for that frame
       and then snapped to its last: shotGreet starts 54px low, tilted and
       scaled down, so every frame that restarts it is a visible flinch. With
       `in-view` going on and off as the reader scrolls, that is a screenshot
       twitching all the way down the page — under the setting whose entire
       purpose is that nothing twitches. Reported exactly that way.

       Taking the name away means no keyframe is ever applied and the element
       simply keeps its own style. That works here because every animated
       element on this page already rests at its base values — the numeral's
       0.85 opacity is written on `.step-number` as well as in the last frame
       of stepNumberGlow, the flare layers are `opacity: 0` at rest, shotGreet
       fills `backwards` only, and the hero's fadeInUp elements are opaque
       without it. Nothing depends on a fill to look finished.

       The exception is written as an exclusion so it stays a single rule: the
       hero's animations have six different durations between them, and
       re-declaring them here to undo an `!important` would be the same numbers
       copied to a second place to drift apart.

       `:not()` taking a selector list is Level 4 — Chrome/Edge 88, Firefox 84,
       Safari 15.4. Older engines drop the rule entirely, which leaves the
       animations running rather than the layout broken: the wrong outcome for
       the preference, but a safe one for the page. */
    *:not(.hero-graphic, .hero-graphic *),
    *:not(.hero-graphic, .hero-graphic *)::before,
    *:not(.hero-graphic, .hero-graphic *)::after {
        animation-name: none !important;
    }

    /* The arrival becomes what it should have been under this setting from the
       start: the block fades up in place. Only the travel is taken away, and
       from the starting end — `.visible` already resolves to no offset, so
       there is nothing left for the transition to animate and the opacity
       carries it alone. */
    .fade-in,
    .faq-answer {
        transform: none;
    }

    /* Lifts under the pointer. These are transitions, so shortening their
       duration never removed them — it only made them instant, which is more
       jarring than the movement was. Removing the destination removes them. */
    .btn-primary:hover,
    .btn-secondary:hover,
    .feature-card:hover,
    .info-card:hover,
    .section-more a:hover::after,
    .protocols-more a:hover::after,
    .arrow-link:hover::after {
        transform: none;
    }

    /* The frames still break out of their panel — that is layout, not motion —
       but they no longer rise into place or tilt away under the pointer. */
    .feature-panel .feature-shot,
    .feature-shot .device:hover,
    .feature-shot .device:focus-visible {
        transform: none;
    }

    /* Same distinction in the steps: the lean on .step-shot stays, because
       that is how the frame is drawn, but the lift under the pointer goes. */
    .step-shot .device:hover,
    .step-shot .device:focus-visible {
        transform: none;
    }

    /* The features carousel does not advance under this setting — script.js
       checks the same query and never starts the timer. So the bar under the
       selected card has nothing to count towards, and the rule above would
       freeze it full the instant it appears: a finished countdown to a change
       that is never coming. Take it away instead. */
    .feature-card.is-active::after {
        display: none;
    }
}

/* ─── Legal pages ─── */

.legal-meta {
    margin-top: 14px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.legal-placeholder {
    margin: 10px 0 8px;
    padding: 14px 18px;
    border: 1px dashed var(--border-hover);
    border-radius: var(--radius-md);
    background: var(--stx-surface-variant-dark);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.85rem;
}

.prose .legal-placeholder + h2 {
    margin-top: 32px;
}
/* ============================
   Icon set
   24 stroke icons on one 24px grid, one weight. Drawn as CSS masks, so they
   inherit currentColor and need no <svg> in the markup and no JS:

       <i class="icon icon-rules"></i>

   This replaced the emoji that used to stand in for icons. Emoji render
   differently per platform, are full colour so they fight the palette, and
   cannot be recoloured — none of which is true of a mask.
   Regenerate with tools/gen-icons.ps1 after editing a path.
   ============================ */

.icon {
    display: inline-block;
    width: 1em;
    height: 1em;
    background-color: currentColor;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: center;
            mask-position: center;
    -webkit-mask-size: contain;
            mask-size: contain;
    flex-shrink: 0;
}

.icon-rules {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='4' width='17' height='16' rx='2.5'/%3E%3Cpath d='M7 9h6M7 13h6M7 17h4'/%3E%3Ccircle cx='17' cy='9' r='1.1'/%3E%3Ccircle cx='17' cy='13' r='1.1'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='4' width='17' height='16' rx='2.5'/%3E%3Cpath d='M7 9h6M7 13h6M7 17h4'/%3E%3Ccircle cx='17' cy='9' r='1.1'/%3E%3Ccircle cx='17' cy='13' r='1.1'/%3E%3C/svg%3E");
}

.icon-shield {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l7.5 3v5.6c0 4.3-3 8.1-7.5 9.7C7.5 19.7 4.5 15.9 4.5 11.6V6z'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l7.5 3v5.6c0 4.3-3 8.1-7.5 9.7C7.5 19.7 4.5 15.9 4.5 11.6V6z'/%3E%3C/svg%3E");
}

.icon-activity {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12h4l3-7 4 14 3-7h4'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12h4l3-7 4 14 3-7h4'/%3E%3C/svg%3E");
}

.icon-scan {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8.5V5.5A1.5 1.5 0 0 1 5.5 4h3M15.5 4h3A1.5 1.5 0 0 1 20 5.5v3M20 15.5v3a1.5 1.5 0 0 1-1.5 1.5h-3M8.5 20h-3A1.5 1.5 0 0 1 4 18.5v-3'/%3E%3Crect x='8.5' y='8.5' width='7' height='7' rx='1'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8.5V5.5A1.5 1.5 0 0 1 5.5 4h3M15.5 4h3A1.5 1.5 0 0 1 20 5.5v3M20 15.5v3a1.5 1.5 0 0 1-1.5 1.5h-3M8.5 20h-3A1.5 1.5 0 0 1 4 18.5v-3'/%3E%3Crect x='8.5' y='8.5' width='7' height='7' rx='1'/%3E%3C/svg%3E");
}

.icon-eye-off {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12s3.6-6 9-6c1.6 0 3.1.5 4.3 1.3M21 12s-3.6 6-9 6c-1.7 0-3.2-.5-4.4-1.3'/%3E%3Ccircle cx='12' cy='12' r='2.6'/%3E%3Cpath d='M4 4l16 16'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12s3.6-6 9-6c1.6 0 3.1.5 4.3 1.3M21 12s-3.6 6-9 6c-1.7 0-3.2-.5-4.4-1.3'/%3E%3Ccircle cx='12' cy='12' r='2.6'/%3E%3Cpath d='M4 4l16 16'/%3E%3C/svg%3E");
}

.icon-layers {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l9 5-9 5-9-5z'/%3E%3Cpath d='M3 13l9 5 9-5'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l9 5-9 5-9-5z'/%3E%3Cpath d='M3 13l9 5 9-5'/%3E%3C/svg%3E");
}

.icon-tv {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='12' rx='2'/%3E%3Cpath d='M8.5 21h7M12 17v4'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='12' rx='2'/%3E%3Cpath d='M8.5 21h7M12 17v4'/%3E%3C/svg%3E");
}

.icon-dpad {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='7.5' y='2.5' width='9' height='19' rx='3.5'/%3E%3Ccircle cx='12' cy='9' r='2.2'/%3E%3Cpath d='M12 14.5h.01M12 17.5h.01'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='7.5' y='2.5' width='9' height='19' rx='3.5'/%3E%3Ccircle cx='12' cy='9' r='2.2'/%3E%3Cpath d='M12 14.5h.01M12 17.5h.01'/%3E%3C/svg%3E");
}

.icon-home {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 11l8-6.5 8 6.5'/%3E%3Cpath d='M6.5 10.2V19h11v-8.8'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 11l8-6.5 8 6.5'/%3E%3Cpath d='M6.5 10.2V19h11v-8.8'/%3E%3C/svg%3E");
}

.icon-smartphone {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='6.5' y='2.5' width='11' height='19' rx='2.5'/%3E%3Cpath d='M10.5 18.5h3'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='6.5' y='2.5' width='11' height='19' rx='2.5'/%3E%3Cpath d='M10.5 18.5h3'/%3E%3C/svg%3E");
}

.icon-link {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.6 13.4a4 4 0 0 0 5.7 0l2.1-2.1a4 4 0 0 0-5.7-5.7l-1.2 1.2'/%3E%3Cpath d='M13.4 10.6a4 4 0 0 0-5.7 0l-2.1 2.1a4 4 0 0 0 5.7 5.7l1.2-1.2'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.6 13.4a4 4 0 0 0 5.7 0l2.1-2.1a4 4 0 0 0-5.7-5.7l-1.2 1.2'/%3E%3Cpath d='M13.4 10.6a4 4 0 0 0-5.7 0l-2.1 2.1a4 4 0 0 0 5.7 5.7l1.2-1.2'/%3E%3C/svg%3E");
}

.icon-lifebuoy {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Ccircle cx='12' cy='12' r='3.4'/%3E%3Cpath d='M9.6 9.6L6 6M14.4 9.6L18 6M14.4 14.4L18 18M9.6 14.4L6 18'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Ccircle cx='12' cy='12' r='3.4'/%3E%3Cpath d='M9.6 9.6L6 6M14.4 9.6L18 6M14.4 14.4L18 18M9.6 14.4L6 18'/%3E%3C/svg%3E");
}

.icon-compass {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Cpath d='M15.4 8.6l-2 4.8-4.8 2 2-4.8z'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Cpath d='M15.4 8.6l-2 4.8-4.8 2 2-4.8z'/%3E%3C/svg%3E");
}

.icon-package {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l8 4.2v9.6L12 21l-8-4.2V7.2z'/%3E%3Cpath d='M4 7.2l8 4.3 8-4.3M12 11.5V21'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l8 4.2v9.6L12 21l-8-4.2V7.2z'/%3E%3Cpath d='M4 7.2l8 4.3 8-4.3M12 11.5V21'/%3E%3C/svg%3E");
}

.icon-zap {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.5 3L6.5 13H11l-.5 8 7-10H13z'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.5 3L6.5 13H11l-.5 8 7-10H13z'/%3E%3C/svg%3E");
}

.icon-puzzle {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.2 4.6a1.8 1.8 0 1 1 3.6 0V6H17a1 1 0 0 1 1 1v3.2h1.4a1.8 1.8 0 1 1 0 3.6H18V18a1 1 0 0 1-1 1h-3.2v-1.4a1.8 1.8 0 1 0-3.6 0V19H7a1 1 0 0 1-1-1v-4.2h1.4a1.8 1.8 0 1 0 0-3.6H6V7a1 1 0 0 1 1-1h3.2z'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.2 4.6a1.8 1.8 0 1 1 3.6 0V6H17a1 1 0 0 1 1 1v3.2h1.4a1.8 1.8 0 1 1 0 3.6H18V18a1 1 0 0 1-1 1h-3.2v-1.4a1.8 1.8 0 1 0-3.6 0V19H7a1 1 0 0 1-1-1v-4.2h1.4a1.8 1.8 0 1 0 0-3.6H6V7a1 1 0 0 1 1-1h3.2z'/%3E%3C/svg%3E");
}

.icon-key {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='12' r='3.6'/%3E%3Cpath d='M11.6 12H21M18 12v3M14.8 12v2.4'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='12' r='3.6'/%3E%3Cpath d='M11.6 12H21M18 12v3M14.8 12v2.4'/%3E%3C/svg%3E");
}

.icon-braces {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 4H8.5a2 2 0 0 0-2 2v3.3L4.5 12l2 2.7V18a2 2 0 0 0 2 2h1M14.5 4h1a2 2 0 0 1 2 2v3.3l2 2.7-2 2.7V18a2 2 0 0 1-2 2h-1'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 4H8.5a2 2 0 0 0-2 2v3.3L4.5 12l2 2.7V18a2 2 0 0 0 2 2h1M14.5 4h1a2 2 0 0 1 2 2v3.3l2 2.7-2 2.7V18a2 2 0 0 1-2 2h-1'/%3E%3C/svg%3E");
}

.icon-plug {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3v5M15 3v5'/%3E%3Cpath d='M6.5 8h11v2.8a5.5 5.5 0 0 1-11 0z'/%3E%3Cpath d='M12 16.3V21'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3v5M15 3v5'/%3E%3Cpath d='M6.5 8h11v2.8a5.5 5.5 0 0 1-11 0z'/%3E%3Cpath d='M12 16.3V21'/%3E%3C/svg%3E");
}

.icon-help {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Cpath d='M9.7 9.6a2.4 2.4 0 0 1 4.6.8c0 1.6-2.3 2-2.3 3.4'/%3E%3Cpath d='M12 17.3h.01'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Cpath d='M9.7 9.6a2.4 2.4 0 0 1 4.6.8c0 1.6-2.3 2-2.3 3.4'/%3E%3Cpath d='M12 17.3h.01'/%3E%3C/svg%3E");
}

.icon-info {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Cpath d='M12 11.2v5.4M12 7.8h.01'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Cpath d='M12 11.2v5.4M12 7.8h.01'/%3E%3C/svg%3E");
}

.icon-lock {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5' y='10.5' width='14' height='10' rx='2'/%3E%3Cpath d='M8.5 10.5V7.6a3.5 3.5 0 0 1 7 0v2.9'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5' y='10.5' width='14' height='10' rx='2'/%3E%3Cpath d='M8.5 10.5V7.6a3.5 3.5 0 0 1 7 0v2.9'/%3E%3C/svg%3E");
}

.icon-file-text {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 3H7.5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V8z'/%3E%3Cpath d='M14 3v5h4.5M9 13.5h6M9 17h4'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 3H7.5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V8z'/%3E%3Cpath d='M14 3v5h4.5M9 13.5h6M9 17h4'/%3E%3C/svg%3E");
}

.icon-image {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='5' width='17' height='14' rx='2'/%3E%3Ccircle cx='9' cy='10' r='1.5'/%3E%3Cpath d='M4.5 16.8l4-3.6 3.5 3 3-2.4 4.5 3.8'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.5' y='5' width='17' height='14' rx='2'/%3E%3Ccircle cx='9' cy='10' r='1.5'/%3E%3Cpath d='M4.5 16.8l4-3.6 3.5 3 3-2.4 4.5 3.8'/%3E%3C/svg%3E");
}

/* ─── Amber icon plates ───
   Each icon is a chip of resin: translucent orange layers over whatever surface
   it sits on, a polished highlight along the top edge, depth pooling at the
   bottom, and a warm halo cast outwards. The glyph is filled with a gradient,
   not a flat colour — a CSS mask passes any background through, so the ramp
   shows through the icon outline.

   Amber appears here and on .download-card. Nowhere else. */

.card-icon,
.feature-icon,
.list-icon {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    border-radius: var(--radius-md);
    background: var(--gradient-amber-chip);
    border: 1px solid var(--stx-amber-edge);
    box-shadow: inset 0 1px 0 rgba(242, 168, 104, 0.50),
                inset 0 -10px 16px rgba(62, 27, 8, 0.45),
                0 4px 16px var(--stx-amber-halo);
    transition: box-shadow var(--transition);
}

/* Gloss on a polished piece. Above the chip, behind the glyph. */
.card-icon::after,
.feature-icon::after,
.list-icon::after {
    content: '';
    position: absolute;
    z-index: -1;
    inset: 0;
    background: radial-gradient(ellipse 70% 55% at 26% 16%, rgba(242, 168, 104, 0.38) 0%, transparent 60%);
    pointer-events: none;
}

.card-icon .icon,
.feature-icon .icon,
.list-icon .icon,
.note-icon .icon {
    background-image: var(--gradient-amber-glyph);
}

/* Hovering the card warms the piece it holds. */
.info-card:hover .card-icon,
.feature-card:hover .feature-icon {
    box-shadow: inset 0 1px 0 rgba(242, 168, 104, 0.62),
                inset 0 -10px 16px rgba(62, 27, 8, 0.36),
                0 6px 22px rgba(184, 90, 28, 0.38);
}
/* ─── Hero supergraphic ───
   A routing fabric drawn at page scale: one origin, three paths through proxy
   nodes, two running straight through as Direct. It is the product diagram
   blown up until it reads as pattern rather than as a chart.

   Decorative: aria-hidden in the markup, pointer-events off, and masked at the
   top and bottom so the lines dissolve instead of being cut by the section
   edge. Sits above the glows and below .hero-content (z-index 1). */

/* ─── Nothing moves while the hero is off screen ───
   The graphic is fourteen animations wide — seven of them stroke-dashoffset,
   which is a paint every frame, not a composite — and they ran for as long as
   the tab was open, whether or not the section was on it. On a page this long
   that is most of the visit: the reader is at the transports table and the
   machine is still drawing packets into a hub two screens above, on the same
   main thread that has to keep the scroll smooth.

   script.js adds .is-idle when the hero leaves the viewport (with a margin, so
   it is already running again before it comes back). Paused, not stopped:
   animation-play-state holds each one where it was, and the beat resumes in
   phase rather than snapping to zero.

   The selector reaches into the graphic with * on purpose — every animation in
   there is on a different element and several are on generated content. It
   sets one property, and only when the class is on. */
.hero.is-idle .hero-graphic *,
.hero.is-idle .faq-item::after {
    animation-play-state: paused;
}

.hero-graphic {
    /* The beat itself is --hero-beat, set on .hero. One turn of it carries one
       chain of cause and effect:

         0%      the clients light up and hand a payload over
         0–36%   the payloads run down the feeds, accelerating
         36–40%  they land — the app flares open in 0.13s, overshooting
         40–46%  the overshoot springs back
         46–56%  full, tensing
         56–61%  the power stroke: it contracts hard, and that is what
                 puts the packets on the transports
         61–67%  rebound
         67–100% the long passive relax, ready for the next one

       The speeds are deliberately lopsided — a bell, not a pulse. The two
       strokes take an eighth of a second each; the relax that follows takes
       eight times longer. The rim and the stubs run the same shape a fraction
       of a beat late, so the movement travels outwards through the hub
       instead of the whole thing scaling as one rigid piece. */
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    opacity: 0.6;
    /* Above the floor gradient in .hero-bg::after, which is a later sibling
       and would otherwise paint over the bottom of the routes. */
    z-index: 1;
    /* Opaque across almost the whole height now. The routes run above and below
       the two content blocks, and the old 20%/74% band cut exactly the strips
       they travel in. */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 10%, #000 90%, transparent 100%);
            mask-image: linear-gradient(to bottom, transparent 0%, #000 10%, #000 90%, transparent 100%);
}

/* The dot fabric used to be densest on the left, which is now where the
   headline is. Cleared off that column and carried across the channel and the
   FAQ instead, where the routes are. */
.hero-fabric {
    opacity: 0.5;
    -webkit-mask-image: linear-gradient(to right, transparent 0%, transparent 22%, rgba(0, 0, 0, 0.4) 46%, rgba(0, 0, 0, 0.45) 80%, transparent 100%);
            mask-image: linear-gradient(to right, transparent 0%, transparent 22%, rgba(0, 0, 0, 0.4) 46%, rgba(0, 0, 0, 0.45) 80%, transparent 100%);
}

/* Proxy nodes: a ring with a core, not a plain dot — they have to read as
   something the traffic passes through. */
.hero-node .node-ring {
    fill: var(--bg-primary);
    stroke: var(--stx-accent-plum-light);
    stroke-width: 1.5;
    opacity: 0.9;
}

.hero-node .node-core {
    fill: var(--stx-accent-plum-light);
}

/* The hub. Concentric rings, radiating stubs and the logo at the centre, so it
   reads as the app the routes meet at rather than a dot on the diagram.

   The halo is exempted from this rule rather than overridden after it:
   fill="url(#hubHalo)" in the markup is a presentation attribute, so a plain
   `.hero-hub circle` rule outranks it and the glow painted as fill:none —
   invisible. Restoring the fill from the stylesheet is not an option either,
   because url(#…) there resolves against the stylesheet, not the document.

   :where() and not :not(), because :not() would contribute the class it wraps
   and take this rule to (0,2,1) — enough to outrank `.hero-hub .hub-ring-outer`
   below on the type count and repaint the rim in the wrong accent. */
.hero-hub circle:not(:where(.hub-halo)) {
    fill: none;
    stroke: var(--stx-accent-plum-light);
}

/* The resting opacity has to be stated, not left to the keyframes: without it
   the base value is 1, and anyone on reduced motion — where the global rule at
   the top of the file kills the animation outright — gets the halo frozen at
   full flare. */
.hero-hub .hub-halo {
    stroke: none;
    opacity: 0.45;
    animation: heroHubGlow var(--hero-beat) ease-in-out infinite;
}

/* The app's own movement: a hit and a release, not a pulse. It answers the
   arriving payloads and then throws the packets onto the transports, so the
   speeds are lopsided on purpose — see the phase table on .hero-graphic.

   transform-box pins the pivot to the group's own centre — the default
   reference box is the viewBox, which would swing the hub across the page. */
.hub-breath {
    transform-box: fill-box;
    transform-origin: center;
    /* linear here on purpose — every segment sets its own easing below. */
    animation: heroBreath var(--hero-beat) linear infinite;
}

@keyframes heroBreath {
    /* Coasting from the last stroke, drawing very slightly in. */
    0%   { transform: scale(0.992); animation-timing-function: cubic-bezier(0.4, 0, 0.75, 0.25); }
    /* The payloads land. 0.13s to full, and it goes past it — nothing
       biological stops dead. */
    36%  { transform: scale(0.976); animation-timing-function: cubic-bezier(0, 0.92, 0.16, 1); }
    40%  { transform: scale(1.078); animation-timing-function: cubic-bezier(0.35, 0, 0.45, 1); }
    /* The overshoot springs back. */
    46%  { transform: scale(1.046); animation-timing-function: cubic-bezier(0.4, 0, 0.85, 0.3); }
    /* Full, tensing for the stroke. */
    56%  { transform: scale(1.056); animation-timing-function: cubic-bezier(0.04, 0.82, 0.22, 1); }
    /* The power stroke. The transports fire on this frame. */
    61%  { transform: scale(0.956); animation-timing-function: cubic-bezier(0.3, 0, 0.4, 1); }
    /* Rebound, then the long passive relax — eight times the stroke. */
    67%  { transform: scale(1.006); animation-timing-function: cubic-bezier(0.22, 0, 0.35, 1); }
    100% { transform: scale(0.992); }
}

@keyframes heroHubGlow {
    0%   { opacity: 0.45; }
    36%  { opacity: 0.5;  animation-timing-function: cubic-bezier(0, 0.92, 0.16, 1); }
    41%  { opacity: 1;    animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1); }
    61%  { opacity: 0.82; }
    100% { opacity: 0.45; }
}

/* The rim and the stubs run the same shape a fraction of a beat behind the
   core, so the stroke travels outwards through the hub. Their scale multiplies
   with the group's, hence the small amplitude. */
@keyframes heroTrail {
    0%   { transform: scale(1);     animation-timing-function: cubic-bezier(0.4, 0, 0.75, 0.25); }
    36%  { transform: scale(0.988); animation-timing-function: cubic-bezier(0, 0.92, 0.2, 1); }
    42%  { transform: scale(1.032); animation-timing-function: cubic-bezier(0.4, 0, 0.85, 0.3); }
    56%  { transform: scale(1.014); animation-timing-function: cubic-bezier(0.04, 0.82, 0.22, 1); }
    62%  { transform: scale(0.972); animation-timing-function: cubic-bezier(0.22, 0, 0.35, 1); }
    100% { transform: scale(1); }
}

.hero-hub .hub-ring-outer {
    stroke: var(--stx-accent-plum-soft);
    stroke-width: 1.2;
    stroke-dasharray: 3 7;
    opacity: 0.85;
    transform-box: fill-box;
    transform-origin: center;
    animation: heroTrail var(--hero-beat) linear infinite;
    animation-delay: calc(var(--hero-beat) * 0.035);
}

.hero-hub .hub-ring-mid {
    stroke-width: 1.3;
    opacity: 0.5;
}

/* The stubs trail furthest — last to be reached by the stroke, last to relax. */
.hero-hub .hub-ticks {
    transform-box: fill-box;
    transform-origin: center;
    animation: heroTrail var(--hero-beat) linear infinite;
    animation-delay: calc(var(--hero-beat) * 0.07);
}

/* The logo sits where the core used to. Its stroke is given in logo units and
   scaled down with the mark, so it keeps the proportion of the navbar version
   instead of matching the hairlines around it. */
.hero-hub .hub-logo {
    fill: none;
    stroke: var(--stx-accent-plum-light);
    stroke-width: 9;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.hero-hub .hub-ticks line {
    stroke: var(--stx-accent-plum-light);
    stroke-width: 1.6;
    stroke-linecap: round;
    opacity: 0.65;
}

/* The apps handing traffic to the hub. They brighten on the downbeat, as the
   payload leaves them — the first link in the chain, and the one that makes
   the feeds read as clients rather than decoration. */
.hero-apps rect {
    fill: none;
    stroke: var(--stx-accent-plum-mid);
    stroke-width: 1.4;
    opacity: 0.55;
    animation: heroAppSend var(--hero-beat) linear infinite;
}

@keyframes heroAppSend {
    0%   { opacity: 0.95; animation-timing-function: cubic-bezier(0.2, 0, 0.5, 1); }
    22%  { opacity: 0.55; }
    100% { opacity: 0.55; }
}

.hero-feed path {
    fill: none;
    stroke: var(--stx-accent-plum-mid);
    stroke-width: 1.2;
    stroke-linecap: round;
    opacity: 0.45;
}

/* Incoming payloads. The paths carry pathLength="100", so the dash pattern is
   read as percentages and the three feeds — which differ in real length — are
   timed as one. 7 + 193 is twice the path: the dash is on the path only while
   the offset runs 200 → 100, and off it for the rest, which is what lets the
   crossing occupy a chosen slice of the beat instead of all of it. */
.hero-feed-packet {
    fill: none;
    stroke: var(--stx-on-tertiary);
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-dasharray: 7 193;
    stroke-dashoffset: 200;
    animation: heroFeedPacket var(--hero-beat) linear infinite;
    opacity: 0.85;
}

@keyframes heroFeedPacket {
    /* Leaves the client unhurried and is pulled in hard over the last third,
       so the arrival is a hit the hub can visibly answer rather than a dash
       that merely stops. */
    0%   { stroke-dashoffset: 200; animation-timing-function: cubic-bezier(0.66, 0, 0.88, 0.36); }
    36%  { stroke-dashoffset: 100; }
    /* Off the path from here — this leg only carries the offset round towards
       the start again, so it runs out the rest of the beat unseen. It stops at
       10 rather than 0 because the dash re-enters at the client once the offset
       drops below 7, which would put the payload back at the client before the
       client has flashed to hand it over. The jump from 10 to 200 on the loop
       is the payload appearing, and it lands on the flash. */
    100% { stroke-dashoffset: 10; }
}

/* Packets. A short dash chased along a very long gap reads as one payload
   travelling the route. The four routes measure roughly 900–960 units, so the
   1414-unit pattern is longer than any of them and only ever puts one dash on
   a path at a time.

   Thrown, not driven: most of the route is covered in the first moments after
   the hub contracts, and the rest is a long coast out of frame. The packet
   carries the release with it, which is what ties the transports back to the
   app instead of leaving them running on their own.

   The curve is gentler than it was. Starting at the channel rather than the
   far left cut the routes by a fifth, and the old easing threw the packet off
   the edge in the first three-tenths of its cycle — four routes standing empty
   most of the time. This one keeps a payload in frame for about four-tenths,
   which is where the density was before. */
.hero-packet {
    stroke-dasharray: 14 1400;
    stroke-dashoffset: 1414;
    animation: heroPacket calc(var(--hero-beat) * 2) cubic-bezier(0.25, 0.5, 0.55, 1) infinite;
    opacity: 0.9;
}

/* Every delay ends in the 0.56 the release lands on, so a departure can only
   ever happen on the frame the app contracts.

   Which beat it lands on is the other half of the problem. Two routes run on
   every second beat and two on every third; left to line up naively their
   common cycle is six beats long and one of the six comes up empty — the app
   strokes with nothing leaving it. So the two-beat pair is split, route 1 on
   the even beats and route 3 on the odd: between them they cover every beat,
   whatever the threes are doing. The threes then vary the density on top —
   one, two, two, one, two, two, and round again.

   The delays are negative so all four are already in flight on first paint,
   instead of the graphic filling up over the first eleven seconds.

   All four are declared on their numbered class, none on the shared one. Put
   route 1's delay on `.hero-packet` and it also matches the other three, which
   then depend on staying below it in the file — move the line and the pattern
   quietly collapses onto the even beats again. */
.hero-packet-1 { animation-duration: calc(var(--hero-beat) * 2); animation-delay: calc(var(--hero-beat) * -1.44); }
.hero-packet-2 { animation-duration: calc(var(--hero-beat) * 3); animation-delay: calc(var(--hero-beat) * -1.44); }
.hero-packet-3 { animation-duration: calc(var(--hero-beat) * 2); animation-delay: calc(var(--hero-beat) * -0.44); }
.hero-packet-4 { animation-duration: calc(var(--hero-beat) * 3); animation-delay: calc(var(--hero-beat) * -0.44); }

/* Stops at 120, not 0. Below an offset of 14 the following dash starts poking
   out of the hub, and the easing above spends its last 15% of time inside that
   final 1% of the offset — so a packet would sit visibly growing at the app for
   a full second before the contraction that is supposed to release it. Any
   value between 14 and 220 keeps the pattern clear of both ends of every
   route; the dash appears at the hub on the loop, which is the departure. */
@keyframes heroPacket {
    to { stroke-dashoffset: 120; }
}

/* Cropping bites hard on narrow screens, and the lines start competing with the
   headline rather than sitting behind it. From 1200 down the two columns stack,
   which puts the copy over the middle of the frame — where the hub is — so the
   graphic steps back at the same width the layout changes at, not later. */
@media (max-width: 1200px) {
    .hero-graphic {
        opacity: 0.35;
    }
}

@media (max-width: 560px) {
    .hero-graphic {
        opacity: 0.22;
    }
}
/* ============================
   Docs page (/docs) — sidebar table of contents
   Nine reference sections behind one page. The nav is a plain anchor list —
   no JS, no IntersectionObserver-driven active state: the CSP here is
   script-src 'self' with no inline allowance, and script.js is
   site-motion's file, not this page's. A static list that always points
   at the right anchor beats a scroll-observer that has to be threaded
   through someone else's file.
   ============================ */

/* Только вертикальные поля обнуляются. Раньше здесь стоял сплошной `padding: 0`,
   и он снимал вместе с ними боковые поля контейнера — на телефоне текст ложился
   вплотную к краю экрана, без единого пикселя отступа. */
.docs-shell {
    padding-top: 0;
    padding-bottom: 0;
}

/* Обёртка подраздела своих боковых полей не добавляет: они уже есть у
   контейнера. С ними заголовок раздела, лежащий прямо в секции, стоял по
   одному краю, а абзацы внутри обёртки — по другому, на шестнадцать пикселей
   правее. Тот самый разнобой в выравнивании. */
.docs-section .container-narrow {
    max-width: none;
    padding-left: 0;
    padding-right: 0;
}

.docs-layout {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    /* Промежуток держит три вещи, а не одну: 40 воздуха после оглавления, 84
       под сам номер и 28 до текста. Раньше здесь стояло 116 при номере
       шириной 92 — то есть номер начинался ровно там, где кончалось
       оглавление, вплотную, и слева у него не было ничего. */
    gap: 152px;
    align-items: start;
    padding: 8px 0 96px;
}

/* Sticky under the fixed navbar — same offset the anchor scroll already
   reserves via scroll-padding-top, so the first item never sits behind
   the bar when the reader jumps here from the footer. */
.docs-toc {
    position: sticky;
    top: 112px;
    align-self: start;
}

.docs-toc ul {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* Рамка объявлена прозрачной сразу: активное состояние её только перекрашивает,
   иначе пункт дёргался бы на пиксель в момент подсветки. */
.docs-toc a {
    display: block;
    padding: 9px 13px;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 0.9rem;
    line-height: 1.4;
    transition: background var(--transition), color var(--transition),
                border-color var(--transition);
}

.docs-toc a:hover {
    background: var(--bg-card-hover);
    color: var(--text-primary);
}

/* Где читатель сейчас. На десяти разделах оглавление без этого — просто
   список ссылок: оно говорит, куда можно пойти, и молчит о том, где ты уже
   находишься.

   Оформлено ровно как выбранная карточка в «Возможностях» — та же заливка
   `--bg-card-hover`, та же рамка `--stx-accent-plum-soft`, та же полоска
   акцентом. На сайте уже есть язык «этот элемент выбран», и заводить для
   документации второй значило бы, что два одинаковых по смыслу состояния
   выглядят по-разному. */
.docs-toc a.is-current {
    color: var(--text-primary);
    background: var(--bg-card-hover);
    border-color: var(--stx-accent-plum-soft);
    box-shadow: inset 2px 0 0 var(--stx-accent-plum-light);
}

/* Колонка шире, чем строка текста, и это намеренно. Текст ограничен 68
   знаками правилом ниже, а оставшееся справа — не поля, а полка: туда уходят
   скриншоты. Раньше эта треть экрана просто пустовала, а снимки стояли в
   колонке текста и рвали чтение пополам. */
.docs-content {
    min-width: 0;
    max-width: 1028px;
}

/* ─── Один ритм на всю страницу ───
   Замер по разделу «Первый запуск» дал такие зазоры между соседними блоками:
   28, 38, 12, 24, 6, 62, 12, 14, 6, 84. Ни одной повторяющейся пары. Так
   получается всегда, когда каждый элемент носит собственные поля сверху и
   снизу: расстояние между двумя блоками оказывается суммой двух не связанных
   между собой решений, а иногда — результатом их схлопывания, и предсказать
   его нельзя, только измерить постфактум.

   Поэтому вертикальный ритм здесь задаётся в одном месте и только полем
   сверху: расстояние принадлежит паре «что идёт за чем», а не элементу.
   Значений всего три — обычный промежуток, промежуток перед подзаголовком и
   промежуток перед крупным объектом вроде таблицы или карточек. Поля снизу
   сняты у всего, что участвует в потоке, чтобы складывать было нечего. */
.docs-section {
    --flow: 18px;
    --flow-h3: 44px;
    --flow-h4: 30px;
    --flow-block: 30px;
}

.docs-section .container-narrow > *,
.docs-section .docs-body > *,
.docs-section > *:not(h2) {
    margin-bottom: 0;
}

.docs-section .container-narrow > * + *,
.docs-section .docs-body > * + *,
.docs-section > *:not(h2) + * {
    margin-top: var(--flow);
}

.docs-section .container-narrow > * + h3,
.docs-section .docs-body > * + h3,
.docs-section > * + h3 {
    margin-top: var(--flow-h3);
}

/* Подраздел завёрнут в собственный .container-narrow — это наследство трёх
   поглощённых страниц, где такая обёртка центрировала колонку. Поэтому его
   заголовок всегда первый ребёнок своей обёртки и предыдущего соседа не имеет:
   правило выше на него не попадало никогда, и все подразделы стояли на
   обычном промежутке в 18px, как абзац от абзаца. Расстояние принадлежит
   обёртке, раз заголовок начинает именно её. */
.docs-section > * + .container-narrow:has(> h3:first-child),
.docs-section > * + .container-narrow:has(> .docs-body > h3:first-child) {
    margin-top: var(--flow-h3);
}

.docs-section .container-narrow > * + h4,
.docs-section .docs-body > * + h4,
.docs-section > * + h4 {
    margin-top: var(--flow-h4);
}

.docs-section .container-narrow > * + :is(ul, ol, table, .card-grid, .note, .data-table-wrapper, .guide-steps),
.docs-section .docs-body > * + :is(ul, ol, table, .card-grid, .note, .data-table-wrapper, .guide-steps),
.docs-section > * + :is(ul, ol, table, .card-grid, .note, .data-table-wrapper, .guide-steps) {
    margin-top: var(--flow-block);
}

/* ─── Подраздел со снимками — две колонки, выровненные по низу ───
   Текст стоит блоком слева, снимки — справа, и нижние края у них общие. До
   этого снимки шли в потоке, то есть столбиком под текстом: страница
   растягивалась вниз, справа оставалась пустая треть экрана, а связь между
   абзацем и его иллюстрацией держалась только на порядке следования.

   Это не удалось сделать одними стилями. Абзацы подраздела — отдельные
   соседи, и чтобы противопоставить их снимкам как единый блок, их пришлось
   обернуть в разметке: .docs-body для текста, .docs-media для снимков.
   Ни сетка, ни обтекание такой пары не дают — сетка расставляет каждого
   соседа в свою строку, обтекание оставляет дыру во весь остаток высоты
   снимка.

   flex-end — это буквально то, что просили: у колонок разная высота, и
   совмещать их по верху бессмысленно (снимок вдвое выше абзаца), а по низу
   они образуют общую линию, на которой и держится композиция раздела. */
.docs-section .container-narrow:has(> .docs-media) {
    display: flex;
    align-items: stretch;
    gap: 40px;
}

/* Колонка по тексту, а не по остатку строки. `flex: 1 1 0` растягивал её на
   всю свободную ширину, тогда как сам текст внутри ограничен строкой набора:
   колонка была под семьсот пикселей при тексте в пятьсот восемьдесят, и снимок
   отъезжал от текста на две сотни пикселей пустоты вместо заявленных сорока.
   Теперь колонка занимает ровно ширину строки, а остаток строки уходит за
   снимок, а не между ними. */
.docs-section .docs-body {
    flex: 0 1 60ch;
    min-width: 0;
}

/* Выравнивание по низу живёт здесь, а не на строке, и это принципиально.
   Пока по низу равнялась вся строка, выше оказывался тот, кто короче: когда
   снимок был выше текста — а он выше почти всегда, — вниз уезжал текст, и над
   заголовком подраздела зияла пустота в пол-экрана. Ровно то, что было видно
   в 8.5.

   Колонка снимков растягивается на высоту строки, а снимки прижимаются к её
   низу. Текст при этом всегда начинается сверху: если он длиннее — снимок
   встаёт у его нижнего края, как и просили; если короче — строка равна высоте
   снимка, и пустоте взяться неоткуда. */
.docs-section .docs-media {
    flex: 0 0 auto;
    display: flex;
    align-items: flex-end;
    justify-content: flex-end;
    gap: 16px;
}

.docs-section .docs-media .screenshot {
    position: relative;
    width: 236px;
    margin: 0;
}

/* Подпись выведена из потока фигуры, и это ровно то, что просили: пока она
   считалась частью высоты, по нижнему краю равнялась она, а корпус телефона
   заканчивался на две строки выше последней карточки. Теперь высота фигуры —
   это высота корпуса, он и встаёт вровень, а подпись висит под ним.

   Место под неё зарезервировано отступом снизу у самой колонки: без него
   подпись легла бы на следующий подраздел. Три строки — с запасом на самую
   длинную из имеющихся и на английский перевод, который обычно длиннее. */
@media (min-width: 1160px) {
    .docs-section .docs-media .screenshot figcaption {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        margin-top: 10px;
    }
}

/* Место под подпись зарезервировано у строки, а не у колонки снимков. На
   колонке этот отступ входил в её высоту и поднимал корпус телефона на те же
   семьдесят пикселей вверх — то есть ровно возвращал ту рассинхронизацию, ради
   устранения которой подпись и выводилась из потока. На строке он просто
   освобождает полосу под ней. */
@media (min-width: 1160px) {
    .docs-section .container-narrow:has(> .docs-media) {
        padding-bottom: 4.4em;
    }
}

/* ─── Когда две колонки перестают быть двумя колонками ───
   Порог был один — 901, — и он оказался взят с потолка. Колонка документации
   равна ширине экрана минус поля, минус рельс оглавления: на 901 это 434
   пикселя, из которых 236 забирает снимок. Тексту оставалось 158 — колонка в
   двадцать знаков, набранная лесенкой по одному слову в строку. А подраздел с
   парой снимков требует 488 и не сжимается вовсе: он просто выезжал за правый
   край экрана, и вся страница получала горизонтальную прокрутку.

   Порогов теперь два, потому что случая тоже два. Одному снимку нужно 1160:
   там текст получает 417 пикселей — колонка в полсотни знаков, читаемая
   строка, а не лесенка. Паре нужно 1440: она забирает 488 и не сжимается
   вовсе, так что тексту достаётся ровно остаток — на 1330 это 335 пикселей,
   снова лесенка. Раньше 1440 пара встаёт под текстом, как на телефоне: два
   кадра рядом, во всю ширину колонки.

   Ширина колонки, а не ширина экрана, решает, сколько колонок в строке; экран
   здесь — лишь способ её измерить. */
@media (min-width: 1160px) and (max-width: 1439px) {
    .docs-section .container-narrow:has(> .docs-media > .screenshot + .screenshot) {
        display: block;
        padding-bottom: 0;
    }

    .docs-section .container-narrow:has(> .docs-media > .screenshot + .screenshot) > .docs-media {
        display: flex;
        align-items: flex-start;
        justify-content: center;
        gap: 16px;
        margin-top: var(--flow-block);
    }

    .docs-section .container-narrow:has(> .docs-media > .screenshot + .screenshot) .screenshot {
        flex: 0 1 calc((100% - 16px) / 2);
        min-width: 0;
        width: auto;
        max-width: 300px;
    }

    .docs-section .container-narrow:has(> .docs-media > .screenshot + .screenshot) .device-phone {
        max-width: none;
    }

    .docs-section .container-narrow:has(> .docs-media > .screenshot + .screenshot) figcaption {
        position: static;
    }
}

/* ─── Типографика документации ───
   Одиннадцать разделов, семьдесят пять абзацев и сорок три подзаголовка на
   одной странице. Пока всё это набрано одним кеглем и одним цветом, читать
   такую страницу нельзя — не потому, что текст плох, а потому, что глазу не за
   что зацепиться: он не видит, где раздел кончился, что здесь главное и куда
   можно не смотреть. Ниже — не украшение, а разметка смысла: у каждого уровня
   свой вес, свой цвет и свой воздух, и расстояние сверху всегда больше, чем
   снизу, чтобы заголовок принадлежал тому, что под ним.

   Отдельная причина завести это здесь: три поглощённые страницы оформлялись
   правилами `.page-section h3`, а при переезде обёртка снялась — их
   подзаголовки остались вовсе без стилей. */

.docs-content {
    counter-reset: docs-section;
}

/* Раздел — это привал, а не абзац. 96px сверху и 72 снизу: между разделами
   должно быть видно пустоту, иначе десять тем читаются как одна длинная.
   flow-root — чтобы плавающие скриншоты не вываливались в следующий раздел. */
.docs-section {
    padding: 96px 0 72px;
    border-top: 1px solid var(--border-color);
    counter-increment: docs-section;
    display: flow-root;
}

.docs-section:first-child {
    padding-top: 8px;
    border-top: none;
}

/* Номер раздела вынесен на поля и набран моноширинным: он нужен, чтобы
   понимать, где ты в десяти разделах, но не должен читаться как часть
   заголовка. На узком экране поля нет — номер встаёт над заголовком. */
.docs-section > h2 {
    position: relative;
    font-size: 1.62rem;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.01em;
    color: var(--text-primary);
    margin-bottom: 18px;
}

/* Крупно и в поле. Номер должен читаться раньше заголовка — он отвечает на
   «где я в десяти разделах», а на этот вопрос смотрят мельком, боковым
   зрением, не вчитываясь. Мелкая цифра такой службы не несёт: её нужно
   искать. Выключен из потока и прижат к правому краю поля, чтобы однозначные
   и двузначные номера стояли по одной линии с текстом. */
.docs-section > h2::before {
    content: counter(docs-section);
    position: absolute;
    left: -112px;
    width: 84px;
    text-align: right;
    top: -0.14em;
    font-size: 3.9rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.04em;
    /* В полную силу цвета. Приглушённый до четверти номер не работает
       ориентиром: чтобы его заметить, на него нужно посмотреть намеренно, а
       смотрят на него мельком — именно затем, чтобы не вчитываться. Плам здесь
       не украшение, а единственное, что отличает служебную цифру от текста;
       выцветая, она превращается в серое пятно на поле. */
    color: var(--stx-accent-plum-light);
    pointer-events: none;
}

/* Первый абзац раздела — врезка: он объясняет, о чём раздел, и должен
   отличаться от абзацев, которые объясняют подробности. */
.docs-section > h2 + p,
.docs-section > .docs-lead {
    font-size: 1.06rem;
    line-height: 1.7;
    color: var(--text-primary);
    max-width: 62ch;
    }

/* ─── Сквозная нумерация подразделов ───
   8.1, 8.2, 8.3 — как в документации, которую читают не подряд. Номер нужен,
   чтобы на него можно было сослаться словами: «смотри 4.2» короче и точнее,
   чем «в разделе про правила, там где про домены».

   Считает CSS, а не текст. Раньше номера были вписаны прямо в заголовки
   поглощённого гайда — «1. Установка APK», — и они врали, как только гайд
   перестал быть отдельной страницей и стал восьмым разделом: на экране стояла
   единица там, где по странице шло 8.1. Из словарей они убраны, и теперь
   номер не может разойтись с положением заголовка, потому что выводится из
   него. */
.docs-section {
    counter-reset: docs-sub;
}

.docs-section h3 {
    font-size: 1.14rem;
    font-weight: 650;
    line-height: 1.35;
    color: var(--text-primary);
        counter-increment: docs-sub;
}

/* Номер подраздела был набран кеглем самого заголовка — восемнадцать пикселей
   на цифру, которая должна отвечать «где я» раньше, чем заголовок ответит
   «о чём». Полтора кегля и полный вес: ниже раздельного номера в поле, выше
   строчного текста, то есть ровно на своей ступени. Единица высоты строки
   оставлена, чтобы крупная цифра не растягивала строку заголовка. */
.docs-section h3::before {
    content: counter(docs-section) "." counter(docs-sub);
    margin-right: 14px;
    font-size: 1.5em;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    color: var(--stx-accent-plum-light);
}

/* ─── Инструкции: тот же янтарь, что и на карточках шагов ───
   «Первый запуск» и установка на приставку — это не справочник, а
   последовательность действий, которую выполняют с телефоном в руках. Карточки
   шагов внутри них уже помечены янтарём; подразделы, набранные заголовком и
   текстом, до сих пор ничем не отличались от справочных, хотя роль у них та
   же. Номер подраздела здесь горит янтарём — на пролистывании видно, где
   инструкция, а где описание. */
#first-run h3::before,
#android-tv h3::before {
    background: var(--gradient-amber-glyph);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.docs-section h4 {
    /* Крупнее основного текста, пусть и немного: подзаголовок, набранный
       мельче того, что он озаглавливает, читается как подпись, а не как
       ступень. Различает его в первую очередь цвет и вес, но и кегль не
       должен спорить с ролью. */
    font-size: 1.02rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--stx-accent-plum-light);
    letter-spacing: 0.01em;
    }

/* Строка набора. 760px при 0.98rem — это около 95 знаков, заметно больше того,
   на чём глаз уверенно находит начало следующей строки. Ограничение стоит на
   тексте, а не на колонке: таблицы и блоки кода по-прежнему занимают всю
   ширину. */
/* Ограничение на текст, а не на блок. `.guide-step` — тоже <li>, но это
   карточка шага, и сжатая до строки набора она не оставила бы места полке со
   снимками; строку внутри неё держат её собственные абзацы. */
.docs-section p,
.docs-section li:not(.guide-step) {
    max-width: 68ch;
}

.docs-section ul,
.docs-section ol {
    margin-left: 0;
    padding-left: 22px;
    gap: 10px;
}

.docs-section li::marker {
    color: var(--stx-accent-plum-mid);
}

/* Этот счётчик заводился впрок и не использовался ни разу, а `counter-reset`
   на всех `ol` подряд перебивал родной `counter-reset: guide` у списка шагов.
   Без общего счётчика каждая карточка заводила свой собственный экземпляр и
   печатала «01» — все семь. Оставлен только тот сброс, который списку и нужен. */
.docs-section .guide-steps {
    counter-reset: guide;
}

/* ─── Восемь схем ссылок ───
   Это не проза, а образцы синтаксиса, и до сих пор они шли обычными пунктами
   списка вперемешку с объяснениями. Каждый пункт написан как «Название —
   схема», поэтому разделять их на два узла не нужно: моноширинный шрифт и
   собственная поверхность уже говорят, что это образец, который сверяют
   глазами, а не фраза, которую читают. */
/* Селектор задан от секции по той же причине, что у схемы 6.1: правило
   `.docs-section ul, .docs-section ol` специфичнее одного класса, и обе
   строчки ниже до сих пор не работали — список схем стоял с отступом в 22px
   под маркеры, которых у него нет, и с зазором в 10px вместо 6px. */
.docs-section .docs-scheme-list,
.docs-scheme-list {
    padding-left: 0;
    gap: 6px;
}

/* Ширина строки набора считается в `ch` — в ширине символа текущего шрифта. У
   моноширинного он заметно уже, поэтому те же 60ch давали схемам всего 438px,
   и половина адресов ломалась пополам на глазах у читателя, при том что рядом,
   в третьем разделе, таблица спокойно занимала тысячу. Ограничение снято: это
   не проза, её не читают строками — её сверяют посимвольно, и перенос здесь
   не помогает, а мешает. */
/* Коробка по содержимому, а не по колонке. Ограничение строки набора здесь
   вредило — оно резало адреса пополам, — но снять его и растянуть блок на всю
   ширину значило поменять одну крайность на другую: тысяча пикселей коробки
   под строку в тридцать символов. Схема занимает ровно столько, сколько в ней
   написано; переносится только то, что не влезает в колонку целиком. */
.docs-section .container-narrow > .docs-scheme-list,
.docs-section .docs-body > .docs-scheme-list,
.docs-section .docs-scheme-list {
    max-width: none;
}

.docs-section .docs-scheme-list li:not(.guide-step) {
    width: fit-content;
    max-width: 100%;
}

/* ─── Схема порядка разрешения ───
   Порядок, по которому клиент выбирает маршрут, был набран нумерованным
   списком из пяти строк. Список перечисляет — он не показывает, что здесь
   ветвление: на каждом шаге есть исход «нашлось» и исход «не нашлось», и
   именно вторым читатель падает вниз, к следующему шагу. В списке эта развилка
   спрятана внутри предложений, и понять из него, куда попадёт трафик, можно
   только вчитавшись.

   Рисунок здесь — разметка, а не картинка: те же пять шагов остаются
   нумерованным списком, доступным скринридеру дословно, а ветвление рисуют
   рамка, линия связи и подписи на ней. Ни одного изображения не добавлено —
   схема переживает перевод, смену кегля и любую ширину экрана.

   Проверено по исходнику приложения: docs/ru/USER_GUIDE.md:236–240 — тот же
   порядок приоритетов; строка 128 — у группы без прокси трафик идёт напрямую,
   это исход третьего шага. */
.flow {
    counter-reset: flow;
    /* Схема стоит в колонке текста, а не поперёк неё: 68ch — та же строка
       набора, по которой сверстаны абзацы раздела. Ограничение перенесено
       сюда с отдельного шага (см. ниже, `.flow-step`): пока его держал каждый
       <li>, ширина шага зависела от того, есть ли у него капсула исхода, и
       пять шагов кончались на трёх разных вертикалях. */
    max-width: 68ch;
}

/* Подпись полосы: два вопроса, на которые схема отвечает по очереди — сначала
   какая группа, потом через что она ходит. Без них пять шагов читаются как
   один длинный список, хотя это две разные развилки. */
.flow-lane {
    margin: 0 0 14px;
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}

.flow-steps + .flow-lane {
    margin-top: 38px;
}

/* Селектор задан от секции нарочно. Общее правило раздела —
   `.docs-section ul, .docs-section ol` — специфичнее одного класса и перебивало
   здесь и зазор, и отступ: 34px превращались в 10px, а слева появлялись 22px
   под маркеры, которых у схемы нет. Десяти пикселей мало по построению: линия
   связи под шагом — 26px плюс 7px стрелки, — так что линия, стрелка и подпись
   «правила нет» уезжали внутрь следующей карточки, а сама схема стояла на 22px
   правее подписей полос. 34px — это высота линии со стрелкой плюс воздух. */
.docs-section .flow-steps,
.flow-steps {
    list-style: none;
    margin: 0;
    padding-left: 0;
    display: flex;
    flex-direction: column;
    gap: 34px;
}

/* Ограничение строки набора (68ch у `.docs-section li`) на шаге схемы держать
   нечего: строку внутри карточки держит сама карточка, а на <li> это правило
   резало ряд «карточка + исход» по-разному в зависимости от того, что в ряду
   стоит. Ширину задаёт `.flow` — один раз, на всю схему. Три класса — ровно
   чтобы перебить `.docs-section li:not(.guide-step)`. */
.docs-section .flow-steps .flow-step {
    max-width: none;
}

.flow-step {
    position: relative;
    display: grid;
    /* Правая колонка — полка исходов, и она одинаковой ширины во всех рядах,
       даже там, где исхода нет: иначе карточки шагов 4 и 5 растягиваются на
       всю схему, а первые три кончаются на 200px раньше, и правый край ступенек
       гуляет. 232px — самая длинная капсула («прокси не задан → напрямую»)
       плюс запас; `max-content` оставляет колонке право вырасти, если перевод
       окажется длиннее. */
    grid-template-columns: minmax(0, 1fr) minmax(232px, max-content);
    gap: 14px;
    align-items: center;
}


.flow-node {
    position: relative;
    padding: 14px 18px 15px 58px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
}

/* Номер шага — тот же приём, что у карточек шагов и у номеров разделов:
   служебная цифра моноширинным, крупнее текста, но легче заголовка. */
.flow-node::before {
    counter-increment: flow;
    content: counter(flow);
    position: absolute;
    left: 18px;
    top: 13px;
    font-family: var(--font-mono);
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1;
    color: var(--stx-accent-plum-light);
}

/* Последний шаг — не проверка, а исход: пунктирная рамка отличает «трафик ушёл
   напрямую» от «клиент ещё выбирает». Поверхность у карточки при этом та же,
   что у остальных: прозрачный фон вычитал её из схемы — на тёмной полосе
   пунктир почти не виден, и пятая ступень читалась как пустое место там, где
   у схемы вывод. Пунктира достаточно, чтобы сказать «дальше некуда». */
.flow-node--end {
    border-style: dashed;
}


.flow-title {
    display: block;
    font-size: 1rem;
    font-weight: 600;
    color: var(--text-primary);
}

.flow-desc {
    display: block;
    margin-top: 4px;
    font-size: 0.88rem;
    line-height: 1.55;
    color: var(--text-secondary);
}

/* Исход вправо — то, что происходит, когда проверка удалась. Пунктирная
   капсула, а не сплошная плашка: это подпись к стрелке, а не кнопка. */
.flow-out {
    justify-self: end;
    padding: 7px 15px;
    border: 1px dashed var(--stx-accent-plum-soft);
    border-radius: 100px;
    font-size: 0.82rem;
    line-height: 1.3;
    color: var(--stx-accent-plum-light);
    background: rgba(58, 24, 64, 0.28);
}

/* У шага может быть два исхода сразу, и второй здесь важнее первого: если
   правила домена тоже нет, выбирать больше нечего — трафик уходит напрямую, а
   не во вторую полосу схемы. Пока этот исход был подписан на линии связи,
   линия вела к «прокси группы» и обещала ровно то, чего не произойдёт. */
.flow-outs {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 8px;
}

/* Исход, на котором маршрут кончается: без плам-заливки, серым по краю. Тот же
   язык, что у пунктирной карточки «Напрямую» внизу схемы. */
.flow-out--end {
    border-color: var(--border-color);
    background: transparent;
    color: var(--text-muted);
}

/* Линия связи вниз и подпись на ней — второй исход: проверка не удалась,
   читатель падает к следующему шагу. Линия начинается ровно под номером, так
   что взгляд идёт по цифрам, а не по краю карточки. */
.flow-step:not(:last-child)::after {
    content: '';
    position: absolute;
    left: 27px;
    top: 100%;
    width: 2px;
    height: 26px;
    background: var(--stx-accent-plum-soft);
}

.flow-step:not(:last-child)::before {
    content: '';
    position: absolute;
    left: 23px;
    top: calc(100% + 25px);
    width: 0;
    height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 7px solid var(--stx-accent-plum-soft);
}

.flow-fall {
    position: absolute;
    left: 46px;
    top: calc(100% + 8px);
    font-size: 0.74rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

@media (max-width: 620px) {
    /* Исход не помещается рядом с карточкой на телефоне: капсула в тридцать
       знаков и карточка в одной строке дают колонку в полтора слова. Он
       встаёт под карточку, у её левого края — там же, где идёт линия связи,
       то есть по той же оси чтения. */
    .flow-step {
        grid-template-columns: minmax(0, 1fr);
        gap: 10px;
    }

    .flow-out {
        justify-self: start;
    }

    .flow-outs {
        align-items: flex-start;
    }

    .flow-node {
        padding: 13px 15px 14px 52px;
    }

    .flow-node::before {
        left: 15px;
        font-size: 1.35rem;
    }
}

.docs-scheme-list li {
    list-style: none;
    max-width: none;
    font-family: var(--font-mono);
    font-size: 0.83rem;
    line-height: 1.6;
    color: var(--text-primary);
    background: var(--stx-surface-variant-dark);
    border: 1px solid var(--border-color);
    border-left: 2px solid var(--stx-accent-plum-mid);
    border-radius: var(--radius-sm);
    padding: 10px 14px;
    overflow-wrap: anywhere;
}

/* Инлайновый код внутри прозы: чип, а не просто другой шрифт. Иначе имя
   параметра теряется в строке ровно там, где его и ищут. */
.docs-section :not(.docs-scheme-list) > code,
.docs-section p code,
.docs-section li code {
    background: var(--stx-surface-variant-dark);
    border: 1px solid var(--border-color);
    border-radius: 5px;
    padding: 1px 6px;
    color: var(--stx-on-tertiary);
}

/* Заголовки тоже держат строку набора: заголовок во всю ширину колонки
   выглядел бы подписью к полке, а не к тексту под ним. */
.docs-section > h2,
.docs-section h3,
.docs-section h4 {
    max-width: 30ch;
}

.docs-section h3 {
    max-width: 40ch;
}

/* ─── Полка со скриншотами ───
   Тридцать пять снимков стояли в колонке текста: каждый разрывал чтение
   пополам, и абзац до него не имел отношения к абзацу после. Теперь они
   уходят вправо, в ту треть экрана, которая пустовала, — рядом с тем шагом, о
   котором рассказывают, а не поперёк него.

   float, а не вторая колонка сетки: снимки лежат внутри `.guide-step`, а не
   прямыми детьми раздела, и разложить их сеткой значило бы переписать
   разметку тридцати пяти блоков. Плавающий элемент, наоборот, сам встаёт
   рядом со своим текстом и сам уступает место следующему.

   Отрицательное правое поле выносит снимок за границу строки набора — иначе
   он отъедал бы у текста те самые 68 знаков, ради которых всё и делалось. */
/* Порог посчитан, а не выбран на глаз. Полке нужно 2×148 под снимки и 2×18 на
   отступы, строке набора — 580, оглавлению — 220, промежутку под номер
   раздела — 116, полям контейнера — 80. Сумма — 1328. Ниже этого снимки
   остаются в потоке: полка, в которую они не помещаются, — это не полка, а
   наложение поверх текста, и при 1080 их было двадцать. */
@media (min-width: 1330px) {
    /* Поглощённые страницы принесли собственный контейнер шириной 860px — он
       центрировал их текст, когда они были самостоятельными документами.
       Внутри колонки документации он лишний: это он, а не полка, решал бы,
       где кончается строка, и снимку было бы некуда встать. Ширину строки
       держит ограничение в 68 знаков, а не обёртка. */
    .docs-section .container-narrow {
        max-width: none;
        padding: 0;
    }

    /* Отрицательных полей нет намеренно: полка — это уже разница между
       колонкой в 1000px и строкой в 68 знаков. Плавающий вправо элемент
       встаёт ровно в неё сам, и вытолкнуть его наружу нечему. */
    /* Ширина полки — это в первую очередь вопрос высоты. Телефон снимают 9:19.5,
       поэтому каждый пиксель ширины стоит двух пикселей высоты, и на полке в
       268px восемь снимков раздела «Первый запуск» выстраивались в колонну на
       4700px при 3600px текста рядом: две с лишним тысячи пикселей снимков
       висели в пустоте, когда текст слева давно кончился.

       190px — та ширина, при которой восемь снимков укладываются в высоту
       своего раздела. Это миниатюра, и так и задумано: клик открывает снимок
       во весь экран, а на полке он нужен, чтобы понять, о каком экране речь, а
       не чтобы читать в нём подписи. */
    /* Полка в два снимка, а не в один, и без `clear`. Это и было причиной
       хвоста: снимки шли строго друг под другом, а в разделе «Первый запуск»
       их восемь и лежат они гнёздами — три подряд на короткий подраздел. Чем
       ниже колонна, тем дальше она уезжала за конец текста; при 268px хвост
       был 2216 пикселей, и уменьшение снимка его не лечило, потому что дело
       не в размере, а в том, что колонна одна. Пущенные по два в ряд, восемь
       снимков заканчиваются на 570 пикселей раньше текста. */
    /* ─── Снимки раскладываются по тому, чем они являются ───
       Восемь снимков раздела «Первый запуск» — не галерея и не украшение: это
       иллюстрации к шагам инструкции, и лежат они в разметке двумя разными
       способами. Четыре стоят поодиночке — каждый при своём шаге. Ещё две
       пары идут подряд, и подряд они не случайно: «Импорт подписки: три
       способа» показывает альтернативы, которые сравнивают между собой.

       Отсюда две раскладки вместо одной. Одиночный уходит на полку и обтекается
       текстом своего шага — там ему хватает и места, и высоты. Пара остаётся в
       колонке текста и встаёт полосой в два кадра: рядом друг с другом, потому
       что читать их надо вместе, а не один под другим.

       Так снимок перестал быть мелким. Сжимать его до 148px пришлось, пока все
       восемь стояли одной колонной и она уезжала на две тысячи пикселей ниже
       текста; разложенные по смыслу, они помещаются в свои разделы при
       нормальном размере. */
    /* Полки больше нет — и это отказ от идеи, а не её настройка.

       Обтекание тут не работает и работать не может: рамка телефона имеет
       пропорции 9:19.5, то есть при любой разумной ширине она вдвое выше
       абзаца, к которому приставлена. Плавающий элемент занимает место рядом с
       текстом, но текста рядом столько нет — и под ним остаётся дыра во весь
       остаток его высоты. Ни один размер снимка этого не лечит: я проходил
       268, 232, 190, 155, 148 и 140 пикселей, каждый раз мерил, и каждый раз
       дыра просто меняла величину. Считать её нужно было не размером снимка, а
       отношением его высоты к длине абзаца — а оно от размера не зависит.

       Снимки возвращены в поток колонки текста. В потоке дыр не бывает по
       построению: элемент занимает свою высоту целиком, следующий начинается
       сразу под ним. Идущие подряд встают рядом — там, где в тексте показаны
       альтернативы, они и должны читаться вместе. */
    .docs-section .screenshot {
        display: inline-block;
        vertical-align: top;
        width: 236px;
        margin: 0 16px 0 0;
    }

    .docs-section .container-narrow > * + .screenshot,
    .docs-section > * + .screenshot {
        margin-top: var(--flow-block);
    }

    /* Соседние снимки стоят в одной строке, поэтому отступ сверху у них
       обязан быть одинаковым: обнулив его у второго, я сдвинул первый на
       тридцать пикселей вниз относительно него — в строке отступ сдвигает
       сам элемент, а не строку. */
    .docs-section .screenshot + .screenshot {
        margin-top: var(--flow-block);
    }

    /* Снимки лежат в строке текста, а у строки есть собственная высота: под
       нижним краем рамки оставался просвет в треть кегля — тот самый, из-за
       которого зазор после снимка выбивался из шкалы. Строке, в которой стоят
       только снимки, высота не нужна: её задаёт сам снимок. */
    .docs-section .container-narrow > .screenshot {
        line-height: 0;
    }

    /* Телевизор снимают 16:9 — при той же ширине он вчетверо ниже телефона,
       и место ему можно дать щедрее. */
    .docs-section .screenshot:has(.device-tv) {
        width: 300px;
    }

    /* Полка принадлежит разделу, а не шагу. Пока карточка шага содержала свой
       снимок сама (`flow-root`), она растягивалась под него во всю его высоту:
       три строки текста, а под ними полкартины пустой карточки — и снимок,
       повисший в этой пустоте сбоку. Теперь снимок всплывает к правому краю
       раздела и встаёт рядом с карточкой, а не внутри неё.

       Чтобы он не оказался поверх содержимого, вся мебель раздела — карточки
       шагов, сетки карточек, врезки и таблицы — ограничена шириной колонки
       текста. Полка справа остаётся пустой по построению, и налезать друг на
       друга там нечему. */
    /* Вся мебель раздела и сам текст держат одну и ту же ширину — иначе полка
       получается рваной: снимок встаёт по правому краю колонки, а под ним
       список, который до этого края не доходит. Список и таблица тоже
       ограничены как блоки, а не только по содержимому: ограничить один <li>,
       оставив <ul> во всю ширину, значит оставить прозрачную коробку под
       полкой, на которую снимок и наложится. */
    .docs-section p,
    .docs-section li:not(.guide-step) {
        max-width: 60ch;
    }

    .docs-section .guide-steps,
    .docs-section .card-grid,
    .docs-section .note,
    .docs-section > ul,
    .docs-section > ol,
    .docs-section .container-narrow > ul,
    .docs-section .container-narrow > ol {
        max-width: 60ch;
    }

    /* Таблица — исключение, и его пришлось выучить дважды. Ограничить её
       строкой набора значит сжать четыре столбца до 606px там, где колонка
       даёт тысячу: подписи в ячейках налезают друг на друга, а справа стоит
       пустая полка. Ширина строки — правило для текста, который читают
       слева направо; таблицу читают по столбцам, и ей нужно ровно столько,
       сколько есть. */
    .docs-section .data-table-wrapper {
        max-width: none;
        clear: right;
    }

    /* Значения слева — технические токены: «Go (xray-core)», «user:pass»,
       «AmneziaWG». Браузер делит ширину между столбцами, не спрашивая, влезает
       ли содержимое, и «Движок» вставал в две строки при том, что места на
       странице было вдоволь. Перенос им запрещён — столбцы берут свою
       естественную ширину.

       Последний столбец — исключение, и это ровно та ошибка, которую здесь
       уже сделали один раз: в «Особенностях» лежат предложения, и запрет
       переноса растянул таблицу до 1206px при колонке в 1028, то есть чинил
       перенос переполнением. Прозе перенос нужен, ей задан минимум ширины,
       чтобы она не схлопнулась в столбик по одному слову. */
    .docs-section .data-table th,
    .docs-section .data-table td {
        white-space: nowrap;
    }

    .docs-section .data-table th:last-child,
    .docs-section .data-table td:last-child {
        white-space: normal;
        min-width: 30ch;
    }

    /* Снимков внутри карточек шагов быть не должно — им место на полке, рядом
       со списком, а не внутри его пункта. Единственный такой снимок вынесен из
       разметки наружу; если появится ещё один, он снова растянет карточку во
       всю свою высоту, и лечится это переносом, а не стилями. */
}

/* ─── Симптом отдельно от лечения ───
   Раздел «Решение проблем» — семнадцать пунктов вида «что случилось, что
   делать», и до сих пор каждый был одним сплошным предложением. Читают такой
   список не подряд: человек приходит с готовым симптомом и ищет глазами свой,
   а находил семнадцать одинаковых серых абзацев, которые надо прочесть
   целиком, чтобы понять, тот ли это. Симптом вынесен вперёд и набран светлым
   — теперь список сканируется, а не читается. */
.docs-section .fix-symptom {
    display: block;
    color: var(--text-primary);
    font-weight: 600;
    margin-bottom: 2px;
}

/* ─── Врезка ───
   Предупреждения и оговорки шли обычными абзацами и терялись ровно там, где
   их и надо заметить. */
.docs-section .note {
    border: 1px solid var(--border-color);
    border-left: 3px solid var(--stx-accent-plum-mid);
    border-radius: var(--radius-md);
    background: var(--stx-surface-variant-dark);
    padding: 16px 20px;
        max-width: 66ch;
}

.docs-section .note p {
    max-width: none;
}

.docs-section .note p:last-child {
    margin-bottom: 0;
}

/* Шаг гайда — уже готовая карточка с номером в кружке, фоном и рамкой; она
   приехала вместе с текстом и переоформления не требует. Ей нужно только
   больше воздуха между соседями, чем было на прежней странице, и полная
   ширина колонки: строку набора внутри держит правило ниже, а карточка,
   сжатая до строки, не оставила бы места полке со снимками. */
.docs-section .guide-steps {
    gap: 28px;
    }

.docs-section .guide-step {
    max-width: none;
}

.docs-section .guide-step > h4:first-child {
    margin-top: 0;
}

/* Подзаголовки документации сиреневые — это цвет структуры. Внутри карточки
   шага заголовок структурой не является: он название действия, и сиреневым
   читался как ссылка. Правило одной специфичности с типографикой раздела,
   поэтому оно и стоит ниже неё. */
.docs-section .guide-step h3,
.docs-section .guide-step h4 {
    color: var(--text-primary);
    font-size: 1.08rem;
}

/* Link-scheme examples mix Cyrillic explanation with a Latin URI pattern —
   forcing the whole line into the monospace stack reads worse than Inter
   does, so this only guards against a long scheme string forcing the page
   wider on a narrow phone. */
.docs-scheme-list li {
    overflow-wrap: anywhere;
}

/* Below the point where a 240px rail plus a 760px column stop fitting
   next to each other, the rail becomes a wrapped row of chips above the
   content instead of a column beside it — same nine links, same markup,
   no second nav to keep in sync. */
@media (max-width: 900px) {
    .docs-layout {
        grid-template-columns: 1fr;
        gap: 8px;
        padding: 0 0 48px;
    }

    /* Полей больше нет, поэтому номер возвращается в поток и встаёт над
       заголовком — та же роль, единственное доступное место. Кегль остаётся
       крупным: он и здесь должен читаться раньше заголовка.

       Заголовок набран тем же кеглем, что и на широком экране. Уменьшённый до
       1.46rem, он проигрывал цифре вдвое и читался как подпись под ней, хотя
       отвечает на главный вопрос — «о чём раздел». Между цифрой и строкой
       заголовка добавлен воздух: при line-height: 1 цифра садится на строку
       вплотную, и номер с названием слипались в один блок. */
    .docs-section > h2 {
        font-size: 1.62rem;
    }

    .docs-section > h2::before {
        position: static;
        display: block;
        width: auto;
        text-align: left;
        font-size: 3.4rem;
        margin-bottom: 10px;
    }


    .docs-section {
        padding: 64px 0 48px;
    }

    .docs-section h3 {
        margin-top: 34px;
    }

    /* Строка набора здесь и так задана шириной экрана — ограничения только
       мешали бы заголовку встать в две строки вместо трёх. */
    .docs-section > h2,
    .docs-section h3,
    .docs-section h4 {
        max-width: none;
    }

    /* Номер уходит на строку над текстом — здесь, а не только на телефоне.
       Полосу в 96px под цифру колонка на этой ширине уже не отдаёт, и отступ
       срезался до 18px, но сама цифра оставалась на месте: 40 пикселей номера
       ложились прямо на заголовок шага. Видно это было ровно между 769 и
       900px — ниже 769 номер и так поднимается общим правилом, выше 900
       возвращается поле. Значения те же, что в блоке @media (max-width: 768px),
       нарочно: это одна и та же раскладка, просто включается раньше. */
    .docs-section .guide-step {
        padding: 68px 18px 22px;
    }

    .docs-section .guide-step::before {
        left: 18px;
        top: 16px;
        font-size: 2.2rem;
    }

    .docs-toc {
        position: static;
        top: auto;
    }

    .docs-toc ul {
        flex-direction: row;
        flex-wrap: wrap;
        gap: 8px;
    }

    /* Подпись части — заголовок над рядом чипов, а не чип. В колонке она
       стояла отдельной строкой и вопроса не вызывала; в ряду с переносом
       она осталась таким же пунктом списка и встала посреди ссылок:
       «РАЗДЕЛ 1 — С ЧЕГО НАЧАТЬ» оказывался слева от «Коротко о главном», а
       «РАЗДЕЛ 2 — ПОДРОБНОСТИ» — справа от него, в одной строке со ссылкой,
       к которой не относится. Занимает всю ширину — и ряд снова читается
       как две группы, а не как одна каша. */
    .docs-toc-part {
        flex-basis: 100%;
        margin: 14px 0 2px;
    }

    .docs-toc-part:first-child {
        margin-top: 4px;
    }


    .docs-toc a {
        padding: 6px 12px;
        border-radius: 100px;
        background: var(--stx-tertiary-container);
        border: 1px solid var(--border-color);
        font-size: 0.82rem;
    }

    .docs-toc a:hover {
        background: var(--bg-card-hover);
    }

    /* Чипом полоска слева не читается — её место занимает рамка акцентом. */
    .docs-toc a.is-current {
        box-shadow: none;
        border-color: var(--stx-accent-plum-mid);
        background: var(--bg-card-hover);
    }
}

/* ─── Таблицы в узкой колонке ───
   Прокрутка вбок появлялась не потому, что данных много, а потому, что вокруг
   них стояли настольные поля: 20px слева и справа в каждой ячейке — это 40px
   на столбец, 200 из 625 у таблицы транспортов, треть её ширины на пустоту.
   Плюс `min-width: 520px`, назначенный ещё для широкой колонки.

   Условие — ширина самой обёртки, а не окна, и это здесь принципиально.
   Таблице всё равно, какой ширины экран: у неё своя колонка, и на 901 пикселе
   окна колонка документации даёт 434 — меньше, чем телефон на 768. Пороги по
   окну ловили бы то одно, то другое; порог по контейнеру ловит ровно тот
   случай, ради которого написан.

   Поля ужаты до 7px, кегль на ступень ниже, минимум снят, длинные токены
   («chacha20-ietf-poly1305», «Dynamic port forwarding») получают право
   переноса внутри слова. Данные остаются все: ни один столбец не спрятан. */
.data-table-wrapper {
    container-type: inline-size;
    container-name: table;
}

@container table (max-width: 560px) {
    .data-table {
        min-width: 0;
    }

    .data-table th,
    .data-table td {
        padding: 9px 7px;
        font-size: 0.76rem;
        line-height: 1.35;
        white-space: normal;
        overflow-wrap: anywhere;
        hyphens: auto;
    }

    .data-table thead th {
        font-size: 0.62rem;
        letter-spacing: 0.02em;
        padding: 8px 7px;
    }

    /* Заголовок раздела таблицы — ссылка на её же содержимое: он и так стоит
       над ней, и настольные поля в 24px здесь только съедали ширину. */
    .data-table caption {
        padding: 12px 10px 0;
        font-size: 0.76rem;
    }

    /* Имя транспорта рвать по буквам нельзя: «SOCKS5» в столбце шириной сорок
       пять пикселей превращается в «SOCKS» и «5». Перенос по буквам оставлен
       там, где он нужен, — в «Особенностях» с их «chacha20-ietf-poly1305», — а
       первому столбцу возвращён обычный: браузер считает его минимальную
       ширину по самому длинному слову и уже не сожмёт уже него. */
    .data-table tbody th {
        overflow-wrap: normal;
        hyphens: none;
    }

    /* Минимум в 30 знаков задавался для настольной колонки — на телефоне он
       и есть та ширина, из-за которой таблица не помещалась. */
    .docs-section .data-table th:last-child,
    .docs-section .data-table td:last-child {
        min-width: 0;
    }
}

/* ─── Композиция в одну колонку ───
   Порог здесь не 900, где рельс оглавления превращается в строку чипов, а 1160
   — там, где текстовой колонке рядом со снимком перестаёт хватать ширины. Это
   разные вопросы, и раньше они были склеены одним числом: на 901 навигация уже
   разворачивалась в чипы, а подраздел всё ещё пытался встать в две колонки и
   оставлял тексту 158 пикселей.

   Два снимка становятся в ряд и делят ширину поровну, а не встают друг под
   другом фиксированными 260px. Рамка держит пропорции сама, так что уменьшение
   по ширине уменьшает и высоту: два кадра рядом занимают столько же места,
   сколько раньше занимал один, и остаются сравнимыми между собой — а показаны
   они рядом именно затем, чтобы их сравнивали.

   Доля, а не размер: полоса под снимок — половина строки за вычетом
   промежутка. Одиночный снимок получает ровно ту же половину и встаёт по
   центру, а не растягивается на всю ширину: иначе одиночные кадры оказывались
   бы крупнее парных, и соседние подразделы шли бы вразнобой — то 240 пикселей
   у левого края, то 173 во всю строку. Один масштаб на всю страницу, одна
   ось. */
@media (max-width: 1159px) {
    .docs-section .container-narrow:has(> .docs-media) {
        display: block;
        padding-bottom: 0;
    }

    .docs-section .docs-media {
        display: flex;
        align-items: flex-start;
        justify-content: center;
        gap: 12px;
        margin-top: var(--flow-block);
    }

    .docs-section .docs-media .screenshot {
        flex: 0 1 calc((100% - 12px) / 2);
        min-width: 0;
        width: auto;
        max-width: 300px;
        margin: 0;
    }

    /* Собственный потолок рамки здесь только мешает: он оставлял внутри полосы
       незанятый остаток, и снимок сидел не по её ширине, а по своей. Полосу
       задаёт строка — рамка занимает её целиком. */
    .docs-section .docs-media .device-phone {
        max-width: none;
    }

    /* Телевизор — кадр горизонтальный, и половина строки ему не полоса: 16:9 в
       173 пикселя нечитаемы. Он берёт ширину целиком, оставаясь на той же
       центральной оси, что и вертикальные. */
    .docs-section .docs-media:has(.device-tv) .screenshot {
        flex-basis: 100%;
        max-width: none;
    }

    .docs-section .docs-media .device-tv {
        max-width: none;
    }
}

/* Средняя ступень: колонка документации на ноутбуке даёт около 611 пикселей, а
   таблице транспортов с настольными полями нужно 625 — четырнадцати не
   хватает, и появляется прокрутка на ровном месте. Поля здесь ужаты вдвое
   слабее, чем в узкой колонке: этого хватает, чтобы уложиться, и текст не
   становится тесным. */
@container table (min-width: 560px) and (max-width: 780px) {
    .data-table th,
    .data-table td {
        padding: 13px 13px;
    }
}

@media (max-width: 480px) {
    .docs-toc a {
        padding: 6px 10px;
        font-size: 0.78rem;
    }
}

/* ─── The FAQ's questions are headings ───
   They used to be a <span> inside <summary>, which reads correctly to a person
   and not at all to a machine: fifteen questions on /faq produced exactly one
   heading in the document outline, and an answer engine looking for
   question-shaped headings found none. `summary` may contain one heading
   element by spec, so the question became an <h3> and the outline now has the
   fifteen entries the page always looked like it had. The FAQPage JSON-LD is
   the other half of the same signal and was already there.

   Everything below is undoing what an <h3> brings with it. The summary is a
   flex row and the heading has to behave like the text it replaced — same
   size, same weight, no block margins, and no line of its own. */
.page-section .faq-item > summary .faq-question,
.faq-question {
    margin: 0;
    font-size: inherit;
    font-weight: inherit;
    line-height: inherit;
    color: inherit;
    letter-spacing: inherit;
}

/* ─── Одна строка под подзаголовком hero на простой версии главной ───
   Требование, которое нельзя спрятать: сервер читатель приносит свой. На
   основной главной это сказано внутри плотного текста и не теряется, потому
   что тот читатель ищет подробности. Здесь читатель их не ищет — он ищет
   кнопку, — поэтому оговорка вынесена в отдельную строку с рамкой слева и
   стоит между подзаголовком и кнопками, то есть ровно на пути к ним.

   Не .note: та карточка крупная и с иконкой, и в hero она перебила бы
   заголовок. Здесь нужна ровно одна заметная строка. */
.hero-note {
    margin: 0 0 30px;
    padding: 10px 0 10px 16px;
    border-left: 2px solid var(--stx-accent-plum-mid);
    color: var(--text-secondary);
    font-size: 0.98rem;
    line-height: 1.6;
    max-width: 52ch;
}

/* ─── /docs разделён на две части ───
   Раздел 1 адресован сегменту A, раздел 2 — всей прежней аудитории страницы.
   Заголовок части — не ещё один раздел, а разделитель: он должен читаться как
   смена регистра, а не как одиннадцатый пункт в ряду десяти. Отсюда верхний
   регистр, разрядка и линия под ним. */
.docs-part-title {
    margin: 0 0 32px;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--stx-accent-plum-soft);
    font-size: 0.86rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--stx-accent-plum-light);
}

/* Второй разделитель стоит вплотную к концу первого раздела, поэтому ему нужен
   воздух сверху, которого первому не нужно. */
.docs-section + .docs-part-title {
    margin-top: 72px;
}

/* Тот же приём в оглавлении: подпись части, а не ссылка. */
.docs-toc-part {
    margin: 20px 0 8px;
    font-size: 0.74rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.docs-toc-part:first-child {
    margin-top: 0;
}

/* ─── Словарь в разделе 1 ───
   Определения, а не список: термин и объяснение связаны разметкой, и это
   единственное место на сайте, где такая связь есть по существу. */
.docs-glossary {
    margin: 18px 0 0;
    padding: 22px 24px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
}

.docs-glossary dt {
    font-weight: 700;
    color: var(--text-primary);
}

.docs-glossary dt + dd {
    margin: 6px 0 0;
}

.docs-glossary dd + dt {
    margin-top: 16px;
    padding-top: 16px;
    border-top: 1px solid var(--border-color);
}

.docs-glossary dd {
    margin: 0;
    color: var(--text-secondary);
    line-height: 1.7;
}

/* ═══ Раздел 1 набран схемами, а не абзацами ═══
   Раздел адресован тому, кто открыл приложение впервые и слов «прокси» и
   «транспорт» ещё не знает. До сих пор он был шестью подряд идущими стенами
   текста: пять минут чтения обещал вводный абзац, а глазу не за что было
   зацепиться, чтобы понять, где он находится. Ни одного изображения при этом
   не добавлено — всё, что ниже, рисуют рамка, сетка и подпись, поэтому оно
   переживает перевод, смену кегля и любую ширину экрана.

   Механика у всех пяти блоков одна и та же, и она же — грабли, на которые
   уже наступала схема 6.1: правило раздела `.docs-section ul, ol`
   специфичнее одного класса и навязывает списку отступ под маркеры и зазор в
   10px, а `.docs-section li:not(.guide-step)` режет каждый пункт по строке
   набора в 68ch. Карточке ни то, ни другое не нужно, поэтому каждый селектор
   ниже задан от секции — чтобы спорить на равных, а не проигрывать по
   специфичности. */

/* ─── 1.1 Сравнение: весь телефон против маршрута по программам ───
   Разница между обычным VPN и этим клиентом — единственное, что читатель
   обязан унести из первого подраздела, и она формулируется одной картинкой:
   слева все четыре программы помечены «через сервер», справа две из них идут
   напрямую. Названия программ — тот же ряд, что уже назван в тексте выше
   (банк, такси) и на /start; это иллюстрация выбора, а не список
   «поддерживаемых». */
.compare {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 16px;
    /* Та же колонка, что у текста раздела, — как у схемы 6.1. */
    max-width: 68ch;
    margin: 22px 0 4px;
}

.compare-side {
    padding: 18px 18px 16px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
}

/* Правая половина — то, ради чего схема нарисована. Плам по кромке: тот же
   цвет структуры, что у номеров разделов, значит «смотри сюда». */
.compare-side--stx {
    border-color: var(--stx-accent-plum-soft);
}

.compare-title {
    margin: 0 0 12px;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.compare-side--stx .compare-title {
    color: var(--stx-accent-plum-light);
}

.docs-section .compare-apps,
.compare-apps {
    list-style: none;
    margin: 0;
    padding-left: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.docs-section .compare-apps .compare-app,
.compare-app {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    max-width: none;
    padding: 9px 12px;
    border-radius: var(--radius-sm);
    background: var(--stx-surface-variant-dark);
    font-size: 0.9rem;
    line-height: 1.3;
}

.compare-app-name {
    color: var(--text-primary);
    font-weight: 600;
}

/* Маршрут — капсула, как исходы в схеме 6.1: тот же язык на двух схемах одной
   страницы. Плам — «ушло в туннель», серый пунктир — «осталось как было». */
.compare-route {
    flex-shrink: 0;
    padding: 3px 10px;
    border-radius: 100px;
    font-size: 0.74rem;
    line-height: 1.4;
    white-space: nowrap;
}

.compare-app--via .compare-route {
    border: 1px solid var(--stx-accent-plum-soft);
    background: rgba(58, 24, 64, 0.28);
    color: var(--stx-accent-plum-light);
}

.compare-app--direct .compare-route {
    border: 1px dashed var(--border-hover);
    color: var(--text-muted);
}

.compare-foot {
    margin: 14px 0 0;
    font-size: 0.86rem;
    line-height: 1.55;
    color: var(--text-secondary);
}

.compare-caption {
    grid-column: 1 / -1;
    margin: 0;
    font-size: 0.84rem;
    line-height: 1.55;
    color: var(--text-muted);
}

/* ─── 1.2 Словарь: шесть карточек вместо шести абзацев подряд ───
   Определения остаются определениями — <dl>, термин связан с объяснением
   разметкой, — но лежат сеткой: слово ищут глазами, а не читают подряд.
   Обёртка <div> внутри <dl> разрешена спецификацией именно для такой пары. */
.docs-section .glossary-grid,
.glossary-grid {
    max-width: 68ch;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 14px;
    margin: 18px 0 0;
    padding-left: 0;
}

.glossary-item {
    padding: 16px 18px;
    border: 1px solid var(--border-color);
    border-left: 2px solid var(--stx-accent-plum-mid);
    border-radius: var(--radius-md);
    background: var(--bg-card);
}

.glossary-item dt {
    margin: 0 0 6px;
    font-weight: 700;
    color: var(--text-primary);
}

.glossary-item dd {
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.65;
    color: var(--text-secondary);
}

/* ─── 1.3 и 1.5 Чек-листы ───
   Оба подраздела отвечают на вопрос «что должно быть, чтобы получилось»: в
   1.3 — три условия, без которых приложение бесполезно, в 1.5 — восемь
   признаков, по которым выбирают поставщика. Это списки, по которым водят
   пальцем, поэтому у каждого пункта галка и своя карточка. */
.docs-section .check-list,
.check-list {
    max-width: 68ch;
    list-style: none;
    margin: 18px 0 0;
    padding-left: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 12px;
}

.docs-section .check-list li,
.check-list li {
    position: relative;
    max-width: none;
    padding: 14px 16px 14px 44px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-card);
    font-size: 0.92rem;
    line-height: 1.6;
}

.check-list li::before {
    content: "\2713";
    position: absolute;
    left: 16px;
    top: 13px;
    font-size: 0.94rem;
    font-weight: 700;
    line-height: 1.55;
    color: var(--stx-accent-plum-light);
}

.check-list strong {
    display: block;
    margin-bottom: 4px;
    color: var(--text-primary);
}

.check-list span {
    color: var(--text-secondary);
}

/* Восемь коротких строк — колонка уже и плотнее: пункт здесь одна фраза, а не
   название с объяснением. */
.docs-section .check-list--tight,
.check-list--tight {
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 10px;
}

.docs-section .check-list--tight li,
.check-list--tight li {
    padding: 12px 14px 12px 40px;
    font-size: 0.9rem;
}

.check-list--tight li::before {
    left: 14px;
    top: 11px;
}

/* ─── 1.4 Четыре отличия — плитками с иконками ───
   Иконка здесь не украшение: четыре пункта различаются ролями (правила,
   несколько серверов, форматы ссылок, отсутствие сбора данных), и на
   пролистывании плитка узнаётся по знаку раньше, чем по заголовку. */
.docs-section .trait-grid,
.trait-grid {
    max-width: 68ch;
    list-style: none;
    margin: 18px 0 0;
    padding-left: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 16px;
}

.docs-section .trait-grid .trait,
.trait {
    max-width: none;
    padding: 20px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
}

.trait-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-bottom: 12px;
    border: 1px solid var(--stx-accent-plum-soft);
    border-radius: var(--radius-md);
    background: rgba(58, 24, 64, 0.28);
    font-size: 20px;
    color: var(--stx-accent-plum-light);
}

.trait strong {
    display: block;
    margin-bottom: 6px;
    font-size: 1rem;
    line-height: 1.35;
    color: var(--text-primary);
}

.trait > span:last-child {
    display: block;
    font-size: 0.9rem;
    line-height: 1.65;
    color: var(--text-secondary);
}

/* ─── 1.6 Четыре предупреждения ───
   Янтарь — тот же приём, что у карточек шагов: он на этом сайте означает
   «делается руками» и «читать внимательно». Четыре вещи, которые выясняются
   поздно, — ровно тот случай. Иконки нет: полосы у края и веса заголовка
   достаточно, а четыре одинаковых значка превратились бы в шум. */
.docs-section .caution-grid,
.caution-grid {
    max-width: 68ch;
    list-style: none;
    margin: 18px 0 0;
    padding-left: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 16px;
}

.docs-section .caution-grid .caution,
.caution {
    position: relative;
    max-width: none;
    overflow: hidden;
    padding: 18px 20px 18px 22px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background:
        linear-gradient(103deg, var(--stx-amber-halo) 0%, transparent 46%),
        var(--bg-card);
}

.caution::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--gradient-amber-glyph);
}

.caution strong {
    display: block;
    margin-bottom: 6px;
    font-size: 1rem;
    line-height: 1.35;
    color: var(--text-primary);
}

.caution > span:last-child {
    display: block;
    font-size: 0.9rem;
    line-height: 1.65;
    color: var(--text-secondary);
}

/* Мостик в раздел 2 — приглушён, чтобы не спорить с содержанием над ним. */
.docs-next {
    margin-top: 28px;
    padding-top: 18px;
    border-top: 1px solid var(--border-color);
    color: var(--text-muted);
    font-size: 0.95rem;
}

/* ============================
   /update — канал обновлений
   ============================ */

/* Отдельный документ в корне update.stealtify.app: приложение проверяет там
   новую версию само, и та же страница открывается, если человек ввёл адрес
   руками. Отсюда её форма — один блок, ни разделов, ни прокрутки по
   содержанию: всё, зачем сюда приходят, обязано поместиться на первый экран.

   Карточка — та же .download-card, что завершает главную, и это не экономия
   разметки. Блок делает ту же работу: последний шаг, кнопка, характеристики
   файла. Второй элемент с тем же смыслом и другим оформлением развёл бы один
   язык на два ради страницы, которую видят реже всех прочих. */

/* Блок стоит по центру окна, а не под шапкой. На странице ровно одна вещь, и
   прижимать её к верхнему краю значило бы оставить под ней экран пустоты —
   composition, в которой единственный объект выглядит забытым, а не главным.

   min-height объявлен дважды: svh браузеры без поддержки просто пропустят и
   останутся на vh. На телефоне это разница ровно в высоту сворачивающейся
   адресной строки — с vh под карточкой висит её полоса. */
.update-shell {
    display: flex;
    align-items: center;
    min-height: 100vh;
    min-height: 100svh;
    /* Сверху — шапка (88px) плюс воздух, снизу меньше: под блоком сразу
       подвал, и лишнее поле здесь читалось бы как незагрузившийся кусок. */
    padding: 104px 0 64px;
}

/* ─── Карточка сжата против той, что стоит на главной ───
   Там она завершает страницу в три экрана: к ней приходят прокруткой, и её
   рост — часть того, что она последняя. Здесь она страница целиком, и
   измеренная (1440×900) она не помещалась в окно: 853px при 900px высоты,
   то есть сноска уезжала за сгиб на самой странице, где кроме неё ничего
   нет. Ужаты поля карточки и промежутки внутри неё — ни одна величина не
   тронута в общем правиле, чтобы главная осталась какой была. */
.update-card {
    padding: 52px 44px;
}

/* Номер версии — заголовок, и размер у него заголовочный: на главной ту же
   роль играет .download-content h2, здесь на её месте h1, потому что это
   единственный заголовок документа. Значения совпадают намеренно — два
   размера для одного и того же объекта на двух страницах читались бы как
   разные объекты. */
.update-card h1 {
    font-size: clamp(2rem, 4.4vw, 2.9rem);
    font-weight: 700;
    line-height: 1.14;
    letter-spacing: -0.025em;
    margin-bottom: 8px;
}

/* Надпись над номером: одно слово о том, куда человек попал. Янтарь тут тот
   же, что на кнопках и плашках карточки, — он в этом блоке уже означает
   «действие», и служебная строка над заголовком ничего нового не вводит. */
/* Селекторы всех четырёх абзацев ниже начинаются с .update-card, и это не
   стилистика. Общее правило .download-content > p задаёт им max-width 46ch,
   1.12rem и margin 0 auto 40px, и по специфичности (0,1,1) оно перебивает
   одиночный класс (0,1,0): написанные без префикса, эти правила молча теряли
   и меру, и отступы — блок мерился на 150px выше положенного. */
.update-card .update-eyebrow {
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--stx-amber-lit);
    margin-bottom: 12px;
}

/* Дата — под номером и тише его. Она отвечает на «свежая ли», то есть на
   вопрос, который задают после того, как прочитали номер, а не вместо. */
.update-card .update-released {
    color: var(--text-muted);
    font-size: 0.9rem;
    margin-bottom: 18px;
}

/* Мера шире, чем у абзаца на главной: там 46ch держат текст компактным
   столбиком под крупным заголовком, здесь те же 46ch ломали две фразы на
   три строки в карточке шириной 724px — и каждая лишняя строка тут стоит
   ровно того, ради чего страницу и делали в один экран. */
.update-card .update-lead {
    max-width: 56ch;
    margin-bottom: 30px;
    font-size: 1.06rem;
}

/* Строка характеристик стоит ближе к кнопкам, чем на главной: там между
   ними разворачивается вся ширина страницы, здесь — край карточки. */
.update-card .download-requirements {
    margin-top: 26px;
}

/* Сноска идёт последней и по смыслу, и по порядку. Порядок задан флексом
   выше по файлу (.download-actions — order 1, .download-requirements — 2),
   поэтому элементу без своего order место досталось бы перед кнопками. */
.update-card .update-foot {
    order: 3;
    max-width: 68ch;
    margin: 26px auto 0;
    padding-top: 18px;
    border-top: 1px solid var(--border-color);
    color: var(--text-muted);
    font-size: 0.88rem;
    line-height: 1.7;
}

/* На узком экране карточка уже сжата общим правилом (.download-card, 768px),
   но вертикальный центр на невысоком экране перестаёт быть центром: блок
   выше окна, и `align-items: center` начинает срезать его сверху вместе с
   заголовком. Здесь блок просто встаёт под шапку. */
@media (max-width: 768px) {
    .update-shell {
        align-items: flex-start;
        min-height: 0;
        padding: 104px 0 56px;
    }

    /* Общее правило сажает .download-card на 48px/24px, но .update-card стоит
       ниже по файлу с той же специфичностью и просто выигрывает порядком —
       44px боковых полей оставались бы и на 390px, где это шестая часть
       экрана. Пишется здесь, а не переносом карточки выше по файлу: выше по
       файлу её место заняло бы правило страницы среди правил главной. */
    .update-card {
        padding: 40px 24px;
    }

    .update-card .update-foot {
        margin-top: 28px;
        font-size: 0.84rem;
    }
}
