/* ORBOX · LANDING — page layer on top of tokens/orbox-tokens.css.
   Only tokens; the single literal below is the one ТЗ §2.1 mandates. */
:root{--section-pad:128px;--web-glass:#FFFFFF;--on-brand:#FFFFFF}
/* Two literals now, where there was one doing two jobs. --on-brand is the page's
   standing override of the SSOT (#12161C): слово Роба 10.08, БЕЛОЕ слово на
   бронзовой кнопке — 3.35:1 on --brand, an accepted client decision, and the
   chevron follows it because .btn::after paints in currentColor.
   --web-glass is the half that was never a label at all: the floating topbar
   pane and the mobile drawer had borrowed --on-brand as a plain "white" for
   their ground. They name their own token now, so the CTA's word and the bar's
   glass can no longer move together by accident — which is what happened the
   one time the override was lifted. */
/* Type system swap 13.08 (landing-only, SSOT tokens.css untouched): body/labels
   move to IBM Plex Sans for broader multilingual coverage, no monospace anywhere —
   --font-mono repointed at the body stack so existing kicker/label call sites
   inherit the change without edits. Arabic still falls back to IBM Plex Sans
   Arabic for both. Shapiro/Inter remain the SSOT default for app/console.
   English headings 14.08: Inter Tight (via @fontsource), replacing the Instrument
   Sans trial. Both it and General Sans are kept installed but unreferenced, so a
   revert is a one-line change to this stack plus the import in layout.tsx.
   Weights stay capped at 400 and 500, and both are real cuts — Inter Tight ships
   100-900, so nothing here is synthesized. */
:root{--font-text:"IBM Plex Sans","IBM Plex Sans Arabic",system-ui,sans-serif;--font-display: "Inter Tight", "IBM Plex Sans", "IBM Plex Sans Arabic", system-ui, sans-serif;--font-mono:"IBM Plex Sans","IBM Plex Sans Arabic",system-ui,sans-serif}
@font-face{font-family:"General Sans";src:url("assets/fonts/GeneralSans-Light.woff2") format("woff2");font-weight:300;font-style:normal;font-display:swap}
@font-face{font-family:"General Sans";src:url("assets/fonts/GeneralSans-Medium.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}
/* Weight cap (spec 019): the display face renders at regular and nothing else.
   The SSOT asks for 300 on the hero and 500 below it, both drawn against
   Shapiro; neither survives the move to Inter Tight — 300 does not exist in the
   family and was silently resolving to 400, and 500 reads heavier at display
   sizes than the hierarchy needs. Every display role is 400 now, which is a
   real cut, so nothing is synthesized. The three other 300 call sites are
   corrected in place further down, because they redeclare the weight after this
   point and would otherwise win on source order. */
.t-hero{font-weight:400}
/* Display tracking (spec 019). Every heading and title sits at zero. The
   negative tracking these roles carried was drawn for Shapiro, whose wide,
   loosely-fitted display cuts needed pulling together; Inter Tight is already
   drawn tight, so the same values close it past the point where counters stay
   open — most visible on the hero, where -0.03em at 72px reads as a squeeze
   rather than as fit. The small-width corrections above went with it: they
   existed only to soften the negative tracking as displays shrank.
   The five roles below live in the SSOT, so they are overridden here rather
   than edited there; the page-layer roles are set to zero in place. */
.t-hero,.t-section,.t-card-title,.t-sub-title,.t-big-number{letter-spacing:0}
/* Headings sit at regular. The SSOT asks for medium on everything below the
   hero, which was drawn against Shapiro; Inter Tight's medium reads heavier at
   display sizes than the hierarchy needs, and the size step already separates
   a section title from body copy without leaning on weight too. Regular is now
   the only display weight on the page. */
.t-section,.t-card-title,.t-sub-title,.t-big-number{font-weight:400}
/* Fluid body scale (spec 019 task-9). The four display steps in the SSOT have
   always been clamped; these four were hard pixels at every width from 320 to
   1640. That is what left numeric labels at 11px and running prose at 12.5px
   unchanged on a 1440px canvas — sized for a phone and never allowed to grow.

   Each ramp is solved to hit its minimum at 390px and its maximum at 1440px:
   slope = (max - min) / 1050 * 100vw, intercept = min - slope% * 390. The
   kicker floor moves 11px -> 12px, which is the readability fix; the rest gain
   roughly a step and a half across the range. The order of the scale holds at
   both ends — kicker <= caption <= body <= lead — so nothing overtakes the
   step above it mid-ramp. Page layer, so app/console keep the SSOT values. */
:root{
  --fs-body-lead: clamp(16px, 15.26px + 0.19vw, 18px);
  --fs-body:      clamp(14.5px, 13.94px + 0.143vw, 16px);
  --fs-caption:   clamp(12.5px, 11.94px + 0.143vw, 14px);
  --fs-kicker:    clamp(12px, 11.63px + 0.095vw, 13px);
}
/* Типографический аудит 10.08 (adaptive-typography): fluid-шкала обязана везти
   трекинг за собой — дисплеи, сжавшиеся к 32–40px, не носят −0.03em десктопа. */
@media (max-width:599px){
}
html.night{--bg:var(--night);--bg-soft:var(--night-raised);--bg-deep:var(--night-field);--panel:var(--night-raised);--panel-2:var(--night-field);--card:var(--night-raised);--card-2:var(--night-field);--web-ink:var(--on-dark);--web-ink-90:var(--on-dark);--web-ink-70:var(--on-dark-70);--web-ink-60:var(--on-dark-60);--web-ink-40:var(--on-dark-40);--web-ink-20:var(--web-line-on-dark);--web-ink-10:var(--web-line-on-dark);--web-ink-06:var(--fill-on-dark);--web-line:var(--web-line-on-dark);--web-line-2:var(--web-line-on-dark);--web-line-control:var(--web-line-control-on-dark);--focus-ring:var(--brand-soft);--placeholder:var(--on-dark-60);--shadow-1:none;--shadow-2:none;--shadow-3:none;--shadow-4:none}
html.night .st-accent{color:var(--brand-soft)}
html{scroll-behavior:smooth}
/* No global overflow-x mask (spec 019 task-12). It hid real breakage from
   users and from any scrollWidth assertion equally, so nothing could be
   proven about the page while it stood. Sections that genuinely need to
   bleed carry their own overflow:clip — .ob-hero, #role, .ob-market,
   .ob-vision, #cta, .ob-foot — which clips the decoration without
   silencing the document. */
/* A link is foreground text with an underline, never a brand-coloured word.
   The default here was --brand-deep, which every link on the page then had to
   override with a class — .ob-navlink, .ob-foot-link, .btn and the logo all
   restate a colour purely to escape it. Inheriting means a bare <a> dropped
   into body copy is correct without being told, and the two night rules below
   it stop being needed: --web-ink is already remapped on every dark ground, so
   `currentColor` and an --web-ink hover follow the band on their own. */
:where(a){color:currentColor;text-decoration-thickness:1px;text-underline-offset:3px}
:where(a:hover){color:var(--web-ink)}
/* Слово Роба 10.08: CTA = цвет НАШЕГО символа. Канон SSOT и держит это сам:
   .btn-primary = --brand #A5876B + --on-brand #12161C (5.42:1, тёмное слово на
   бронзе). Страничные отходы (--on-brand:#FFF, фон brand-deep) сняты. */
.ob-navlink{color:var(--web-ink-70)}
.ob-navlink:hover{color:var(--web-ink)}
/* Button height. The shared control resolved to 43.6px, and both halves of
   that figure were doing the same job: 13.5px type on the body's 1.6 leading
   (21.6px) plus 11px of padding either side. A 1.6 line-height is a
   running-prose value — a single-line pill has no second line to lead into,
   so it was buying blank space twice. Leading drops to 1.2 and the padding to
   9px: 34.2px. Horizontal padding, type size, radius and every colour are
   untouched, so only the height moves.
   One height, every pointer. A `(pointer:coarse)` floor of 44px was tried and
   removed by decision: it meant the control the whole page is built around was
   the one control that never got more compact, which is the opposite of the
   ask. The drawer CTA keeps its own 48px — it is a full-width panel row next
   to 52px drawer links, so it belongs to that context rather than to this
   rule, and e2e/web/src/responsive.spec.ts asserts it at 44. */
.btn{padding-block:9px;line-height:1.2}
img,svg{display:block}
h1,h2,h3,p,ul,ol{margin:0}
ul,ol{list-style:none;padding:0}

/* Vertical rhythm (spec 019 task-14). Measured across the seven sections, the
   seam between them — one section's bottom plus the next one's top — ranged
   from 128 to 352 with no pattern, because three sections derived their air
   from ad-hoc multipliers of the rail (x1.35 on market, x1.4 on vision) and a
   viewport clamp on the footer, while the rest used the rail itself. Every
   content section now takes --section-pad unmodified, so the seam is a uniform
   256 at desktop and 160 on a phone, and the rail is the only number to change
   if the rhythm should change.

   Two asymmetries are deliberate and stay. #cta keeps 0.5x on top against a
   clamp below: that lower figure is clearance for the orb rising into the
   section, not section air, and it was tuned against the orb geometry. The
   hero keeps a lighter step through the tablet band, where it has already lost
   its second column. */
/* ── rails ── */
/* The landing rail is 1216, not the 1640 app shell. DESIGN.md §3 splits the
   two: landing content sits on --content-max, and only app surfaces (topbar,
   dashboard, marketplace) take the wider shell. This file had the shell width
   on every section and a --text modifier holding the right number that nothing
   ever applied, so at 1440 the content ran 1376 wide — a section title starting
   at x=45 with its own body copy at x=1040, which is the canyon down the middle
   of the page. Corrected on .ob-rail itself rather than by adding a class to
   ten call sites: this stylesheet is landing-only, so .ob-rail has no other
   consumer to protect, and one owner is the point. The --text modifier goes
   with it — it now says nothing the base rule does not. */
.ob-rail{width:100%;max-width:var(--content-max);margin-inline:auto;padding-inline:var(--web-page-pad)}
.ob-sec{padding-block:var(--section-pad);position:relative}
/* Anchor targets clear the floating bar. .ob-top is fixed at 12px carrying a
   64px pane, so an in-page jump parked the section edge at y=0 and left the
   bar sitting on whatever the section opened with. Desktop had 128px of
   section padding to hide behind; a phone has 80px, and #platform is a pin
   whose stage pads 40px, so both landed their title under the pane.
   12 + 64 + 20 of air, stated once for every target the nav and the footer
   can reach. Anchor jumps only — normal scrolling is untouched. */
:where(#hero,#problem,#platform,#role,#market,#vision,#cta){scroll-margin-block-start:96px}
.ob-head{display:grid;gap:20px;max-width:640px}
.ob-head .t-section{max-width:15em}
.ob-head .t-body-lead{max-width:34em}

/* ── topbar ── */
/* A floating bar rather than a full-bleed edge-to-edge strip. The material
   moves from .ob-top to .ob-top-in: the sticky element stays transparent and
   only positions, while the bar itself carries the ground, the blur, the
   hairline and the radius. That split is what lets it detach from the top edge
   — a background on the sticky element would paint the full viewport width and
   there would be nothing to float.
   The blur is always on now, not only once stuck. The bar sits over the hero
   from the first frame, so it needs its ground from the first frame too. */
/* Fixed, not sticky: the bar floats over the page rather than occupying a
   band above it. Sticky still reserves its own height in flow, which pushed
   the hero down by the height of the bar and left a strip of page ground
   above a section that is meant to start at the top of the viewport. */
/* How far down the page the floating bar reaches: its 12px offset plus the
   64px min-height of .ob-top-in. Declared here, next to the two numbers it is
   made of, because a pinned section has to reserve it — content centred in a
   full-height sticky stage would otherwise sit under the bar. */
:root{--top-h:76px}
/* The bar's two geometry figures, named because the drawer is measured off
   them. --top-off is the drop from the top of the viewport; --top-inset is the
   distance from the page edge to the first thing inside the pane, which is the
   bar's own padding plus .ob-top-in's. The full-screen drawer starts its
   contents on exactly that line, so the menu items sit under the wordmark
   rather than on an indent of their own — and it now does so at two different
   bar geometries (tablet and phone) without either being written twice. */
.ob-top{position:fixed;inset-block-start:var(--top-off);inset-inline:0;z-index:30;padding-inline:var(--web-page-pad);pointer-events:none;--top-off:12px;--top-inset:calc(var(--web-page-pad) + 20px)}
/* The bar takes pointer events back; the transparent gutter either side of
   it must not swallow clicks on the content underneath. */
.ob-top-in,.ob-drawer{pointer-events:auto}
/* White glass, not a tint of the page. The pane was mixed from --bg, which is
   the warm page ground, so over the dark hero it read as a grey wash and the
   bar dissolved into the section behind it — the one place a floating bar has
   to hold its own edge. Mixing from white instead keeps it a lit pane on both
   grounds: barely there over cream, clearly a pane over ink.
   No shadow at all: a drop shadow states an edge and a
   distance, which is the opposite of what glass does — it should read as a
   pane the page passes through, not a panel resting above it. The depth
   comes from the optics instead: a heavier blur so the content behind
   dissolves rather than merely softens, saturation and brightness lifted so
   the pane looks lit rather than tinted, a low-alpha ground, and a hairline
   that is brighter along the top edge than the bottom, which is how a real
   pane catches light. */
.ob-top-in{display:flex;align-items:center;gap:32px;min-height:64px;padding-inline:20px;border:1px solid color-mix(in srgb, var(--web-ink) 10%, transparent);border-top-color:color-mix(in srgb, var(--web-glass) 55%, transparent);border-radius:var(--web-r-lg);background:color-mix(in srgb, var(--web-glass) 60%, transparent);-webkit-backdrop-filter:blur(28px) saturate(180%) brightness(1.06);backdrop-filter:blur(28px) saturate(180%) brightness(1.06);transition:background var(--web-dur-base) var(--ease-ui),border-color var(--web-dur-base) var(--ease-ui)}
/* Once the page has moved under it the pane thickens slightly — enough to keep
   the nav legible over whatever scrolls beneath, without becoming a solid
   bar and losing the effect. */
.ob-top.is-stuck .ob-top-in{background:color-mix(in srgb, var(--web-glass) 74%, transparent)}
/* Over a dark band the pane inverts. The white glass above is correct on the
   cream page and wrong on the four dark sections: 60% white over --web-surface-dark
   resolves to a flat light grey, and a flat grey rectangle is a slab, not a
   pane — the one thing a floating bar must not do is stop reading as glass.
   Lowering the alpha instead is not available: the nav copy is --web-ink-70, a
   dark word, and it only survives because the pane stays light. So the pane
   inverts and the copy comes with it, which is the system's own answer
   (surfaces.dark — wrap it and the contents flip themselves) rather than a
   second set of colours written at each call site.
   The ground is --night and not --web-surface-night: #12161C carries a blue cast
   that belongs to no other surface on this page, and behind a saturating blur
   it turned the bar into a cold pane on a warm site. --night is the charcoal
   the footer and the night bands already stand on.
   Which bands are dark is measured at runtime from the section's own computed
   background, not listed here; see installStickyHeader in document-runtime.ts. */
.ob-top--dark{
  --web-ink:var(--on-dark);--web-ink-90:var(--on-dark);--web-ink-70:var(--on-dark-70);
  --web-ink-60:var(--on-dark-60);--web-ink-40:var(--on-dark-40);--web-ink-20:var(--web-line-on-dark);
  --web-ink-10:var(--web-line-on-dark);--web-ink-06:var(--fill-on-dark);
  --web-line:var(--web-line-on-dark);--web-line-2:var(--web-line-on-dark);--focus-ring:var(--brand-soft);
  /* --card comes with the ink ladder, and leaving it out was a bug: the
     language popup is a --card panel with --web-ink-70 entries, so flipping only
     the type painted cream words on a cream panel over the dark bands — the
     menu was there and unreadable. Anything the bar opens is on this ladder,
     not just the bar itself.
     Shadows go with it. The system does not put shadows on dark ground, and
     --shadow-3 under a popup on a night band is a grey smear, not a lift. */
  --card:var(--night-raised);--card-2:var(--night-field);
  --shadow-1:none;--shadow-2:none;--shadow-3:none;--shadow-4:none;
}
.ob-top--dark .ob-top-in,
.ob-top--dark .ob-drawer{
  border-color:color-mix(in srgb, var(--on-dark) 14%, transparent);
  border-top-color:color-mix(in srgb, var(--on-dark) 32%, transparent);
}
.ob-top--dark .ob-top-in{background:color-mix(in srgb, var(--night) 46%, transparent)}
.ob-top--dark.is-stuck .ob-top-in{background:color-mix(in srgb, var(--night) 64%, transparent)}
.ob-top--dark .ob-drawer{background:color-mix(in srgb, var(--night) 82%, transparent)}
/* The wordmark is a two-file swap, not a currentColor mark, so it has to be
   told as well — the day cut is a dark wordmark and would vanish here. */
.ob-top--dark .ob-logo .ob-logo--day{display:none}
.ob-top--dark .ob-logo .ob-logo--night{display:block}
/* The three honest fallbacks the light pane already carries, restated for the
   dark one: no blur support, reduced transparency and raised contrast all get
   an opaque pane rather than a translucent one. Specificity, not source order,
   decides these — .ob-top--dark .ob-top-in is (0,2,0) against the light rule's
   (0,1,0), so the guards below have to name the dark variant themselves. */
@supports not (backdrop-filter:blur(1px)){
  .ob-top--dark .ob-top-in,.ob-top--dark.is-stuck .ob-top-in,.ob-top--dark .ob-drawer{background:var(--night)}
}
@media (prefers-reduced-transparency:reduce){
  .ob-top--dark .ob-top-in,.ob-top--dark.is-stuck .ob-top-in,.ob-top--dark .ob-drawer{background:var(--night);-webkit-backdrop-filter:none;backdrop-filter:none}
}
@media (prefers-contrast:more){
  .ob-top--dark .ob-top-in,.ob-top--dark.is-stuck .ob-top-in,.ob-top--dark .ob-drawer{background:var(--night);-webkit-backdrop-filter:none;backdrop-filter:none;border-color:var(--web-line-on-dark)}
}
/* Apple §12/§14: материал-шапка с честными фолбэками — без поддержки блюра или при
   reduced-transparency плашка становится глухой, читаемость не торгуется */
@supports not (backdrop-filter:blur(1px)){.ob-top-in{background:var(--bg)}}
@media (prefers-reduced-transparency:reduce){.ob-top-in,.ob-top.is-stuck .ob-top-in{background:var(--bg);-webkit-backdrop-filter:none;backdrop-filter:none}}
@media (prefers-contrast:more){.ob-top-in,.ob-top.is-stuck .ob-top-in{background:var(--bg);-webkit-backdrop-filter:none;backdrop-filter:none;border-color:var(--web-line-2)}}
/* The header row is the one place that genuinely did not fit once the global
   overflow mask came off (spec 019 task-12). At 320px: 20 pad + 123 logo + 32
   gap + 155 actions + 20 pad = 350 in a 320 viewport, 10px over. The desktop
   gap is the slack; nothing else on the page overflowed at any width.
   The correction itself now lives in the 899 block at the foot of the drawer
   section, where the rest of the mobile header geometry is stated, rather than
   being declared twice at two widths. It starts one step earlier than it used
   to, which only widens the margin on the overflow this paragraph is about. */
.ob-logo{display:inline-flex;align-items:center;height:26px;flex:none}
.ob-logo img{height:26px;width:auto}
.ob-logo .ob-logo--night,html.night .ob-logo .ob-logo--day{display:none}
html.night .ob-logo .ob-logo--night{display:block}
.ob-nav{display:flex;align-items:center;gap:28px;margin-inline-start:auto}
/* Nav labels take a step down the scale, --fs-body -> --fs-caption. The bar
   is a wayfinding row, not copy: at --fs-body its six labels read at the
   same weight as the hero's own sentences and competed with them. One step
   down also puts them level with .ob-lang-trigger, which was already on
   --fs-caption and sat visibly smaller than the links beside it. */
.ob-navlink{font-family:var(--font-text);font-size:var(--fs-caption);font-weight:400;color:var(--web-ink-70);text-decoration:none}
.ob-navlink:hover{color:var(--web-ink)}
.ob-top-act{display:flex;align-items:center;gap:12px}
.ob-top .btn{white-space:nowrap}
/* ── language menu (spec 019) ────────────────────────────────────────────────
   A dropdown rather than a row of pills: the row could only ever hold the
   languages that already exist, and adding a third made that plain. The
   trigger shows the current choice; the menu carries the full list including
   the one that is not ready. */
.ob-lang{position:relative;display:inline-flex}
.ob-lang-trigger{display:inline-flex;align-items:center;gap:6px;appearance:none;border:1px solid var(--web-line-2);background:transparent;border-radius:var(--web-r-pill);min-height:36px;padding:0 10px 0 14px;font-family:var(--font-text);font-size:var(--fs-caption);font-weight:400;color:var(--web-ink-70);cursor:pointer;transition:background var(--web-dur-fast) var(--ease-ui),color var(--web-dur-fast) var(--ease-ui)}
.ob-lang-chevron{width:12px;height:12px;flex:none;transition:transform var(--web-dur-fast) var(--ease-ui)}
.ob-lang-trigger[aria-expanded=true]{color:var(--web-ink)}
.ob-lang-trigger[aria-expanded=true] .ob-lang-chevron{transform:rotate(180deg)}
@media (hover:hover) and (pointer:fine){.ob-lang-trigger:hover{color:var(--web-ink);background:var(--web-ink-06)}}
.ob-lang-menu{position:absolute;top:calc(100% + 6px);inset-inline-end:0;z-index:40;min-width:180px;display:grid;margin:0;padding:6px;list-style:none;border:1px solid var(--web-line-2);border-radius:var(--web-r-md);background:var(--card);box-shadow:var(--shadow-3)}
.ob-lang-menu[hidden]{display:none}
.ob-lang-btn{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;appearance:none;border:0;background:transparent;border-radius:var(--web-r-sm);min-height:40px;padding:0 12px;font-family:var(--font-text);font-size:var(--fs-body);font-weight:400;color:var(--web-ink-70);text-align:start;cursor:pointer;transition:background var(--web-dur-fast) var(--ease-ui),color var(--web-dur-fast) var(--ease-ui)}
.ob-lang-btn[aria-checked=true]{background:var(--web-ink);color:var(--card-2)}
@media (hover:hover) and (pointer:fine){.ob-lang-btn:not([disabled]):hover{color:var(--web-ink);background:var(--web-ink-06)}}
@media (hover:hover) and (pointer:fine){.ob-lang-btn[aria-checked=true]:hover{background:var(--web-ink);color:var(--card-2)}}
/* Not-yet-ready languages stay legible rather than being dimmed into
   illegibility — the point is to say "coming", not to hide the entry. */
.ob-lang-btn[disabled]{cursor:default;color:var(--web-ink-40)}
.ob-lang-soon{font-size:var(--fs-caption);color:var(--web-ink-40);border:1px solid var(--web-line);border-radius:var(--web-r-pill);padding:1px 8px}

/* ── 01 hero ── ink-грунт (слово Роба: геро на тёмном). Фрагмент рисует грунт сам;
   секция несёт тот же тон под ним — фолбэк без сцены обязан читаться текстом.
   Лестница — зеркало .ob-bookend, грунт --web-surface-dark (= ink, «standard dark section»). */
/* 86svh, not 100. A hero that measures the whole viewport ends exactly where
   the fold does, so nothing below it is ever on screen and there is no reason
   in the composition itself to scroll. Giving the section 14% back puts the
   top of the next one in view — roughly 135px at a 960 canvas, 150px at 1080
   — which is enough to read as "the page continues" without taking a bite out
   of the hero. The px floor drops with it, or a short laptop would sit on the
   floor and lose the peek the ratio is here to buy. */
.ob-hero{position:relative;min-height:max(620px,86svh);display:grid;align-items:center;padding-block:var(--section-pad);overflow:clip;--web-ink:var(--on-dark);--web-ink-90:var(--on-dark);--web-ink-70:var(--on-dark-70);--web-ink-60:var(--on-dark-60);--web-ink-40:var(--on-dark-40);--web-ink-20:var(--web-line-on-dark);--web-ink-10:var(--web-line-on-dark);--web-ink-06:var(--fill-on-dark);--web-line:var(--web-line-on-dark);--web-line-2:var(--web-line-on-dark);--focus-ring:var(--brand-soft);--shadow-1:none;--shadow-2:none;--shadow-3:none;--shadow-4:none;background:var(--web-surface-dark);color:var(--on-dark-70)}
html.night .ob-hero{background:var(--night)}
/* декоративные фоны без маунта: проявление после загрузки — грунт фрагмента (sand до
   первого рендера) не должен вспыхивать на тёмной секции */
.ob-hero-bg,.ob-vision-bg{opacity:0;transition:opacity var(--web-dur-slow) var(--ease-ui)}
.ob-hero-bg.is-up,.ob-vision-bg.is-up{opacity:1}
.ob-hero-bg{position:absolute;inset:0;z-index:0;border:0;width:100%;height:100%;pointer-events:none}
.ob-hero-in{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,520px) minmax(0,1fr);gap:56px;align-items:center}
.ob-hero-copy{display:grid;gap:28px;justify-items:start}
.ob-hero-copy h1 span{display:block}
/* слово Роба 10.08: геро-тайтл крупнее, сабтайтл акцентнее — страничный шаг поверх шкалы */
.ob-hero .t-hero{font-size:clamp(44px,2.2rem + 2.6vw,78px)}
.ob-hero-sub{font-size:var(--fs-sub-title);font-weight:400}
.ob-hero-sub{font-family:var(--font-text);font-size:var(--fs-body-lead);letter-spacing:.005em;color:var(--web-ink-70);max-width:34ch}
.ob-hero-reserve{aspect-ratio:1/1;width:100%;max-width:520px;justify-self:center}

/* Ask Orbox composer (spec 019): the hero swaps into this state when any
   "Ask Orbox" control fires — header, drawer or the hero's own CTA — via
   ask-orbox-runtime.ts. It renders inside .ob-hero-copy, so it inherits the
   dark-band --web-ink custom properties .ob-hero already sets above. The
   input itself is @orbox/ui's AskComposer (light mode, its own styling) —
   this file only positions its wrapper and the surrounding hero content. */
.ob-hero-tags{display:flex;flex-wrap:wrap;gap:8px;font-family:var(--font-mono);font-size:var(--fs-kicker);color:var(--web-ink-60)}
.ob-hero-soft{color:var(--web-ink-60);font-weight:400}
.ob-hero-ask{width:100%;max-width:620px;display:flex;flex-direction:column;gap:12px}

/* The "Ask Orbox" input — geometry copied 1:1 from @orbox/ui's AskComposer
   variant="chat" (the AI/AskComposer → PromptBar story), which is the shape
   the product's own composer has. Every number below is that component's:
   22px card radius (--r-lg), the 52→240px textarea, 16/14/6 text padding,
   14px body type at 1.5 (--text-body-lg, a fixed step there, so fixed here —
   the page's fluid --fs-body would make this the one composer on the estate
   that changes size with the viewport), a 10px-inset toolbar, and two 32×32
   buttons.

   The palette is the part that is ours. That component reaches its dark
   surface through a `dark:` class this page never sets, so its tokens resolve
   light here; these rules restate the same roles on the landing's own on-dark
   ladder — the one measured for contrast against this section's ground in
   tokens/orbox-tokens.css — over the ui card tone --default-50 (#1b1a18). */
/* Light card on the dark hero — the ui component's own light mode, value for
   value: --surface #fff, --ink-10 hairline, --ink type, --ink-45 placeholder,
   --default-400 on hover/focus, an --ink send disc with a --canvas-cream
   glyph, --midtone when it has nothing to send. Values are literal because
   @orbox/ui's tokens are not on this page and the landing's own ladder is
   built for type on dark grounds, which is the opposite problem. */
.ob-ask{display:flex;flex-direction:column;overflow:hidden;border-radius:22px;border:1px solid rgba(61,61,59,0.1);background:#ffffff;transition:border-color var(--web-dur-fast) var(--ease-ui)}
.ob-ask:hover,.ob-ask:focus-within{border-color:#a8a6a1}
.ob-ask-field{min-height:52px;max-height:240px;resize:none;overflow-y:auto;border:0;background:transparent;padding:14px 16px 6px;font-family:var(--font-text);font-size:14px;line-height:1.5;color:#3d3d3b}
/* The page rings every :focus-visible in --focus-ring, which is --brand-soft
   on this section — a gold outline around the input the moment anyone types
   in it, which the design system's composer does not have. The card's own
   border darkening to --default-400 is the focus signal, exactly as in the
   component. Stated at (0,3,0) so it outranks the token sheet's bare
   :focus-visible whatever order the sheets end up in — app/globals.css is
   module-injected and its position against the two <link>s is not fixed. */
.ob-ask .ob-ask-field:focus,
.ob-ask .ob-ask-field:focus-visible{outline:none;box-shadow:none}
/* -webkit-text-fill-color is stated because it, not `color`, is what paints
   the glyphs in a WebKit field once the UA or an autofill style has set it. */
.ob-ask-field,.ob-ask-field::placeholder{-webkit-text-fill-color:currentColor}
.ob-ask-field::placeholder{color:rgba(64,61,56,0.45);opacity:1}
.ob-ask-bar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 10px 10px}
.ob-ask-attach,.ob-ask-send{display:flex;flex:none;align-items:center;justify-content:center;width:32px;height:32px;border:0;padding:0}
.ob-ask-attach{border-radius:16px;background:transparent;color:rgba(64,61,56,0.45);opacity:.6;cursor:not-allowed}
.ob-ask-send{border-radius:var(--web-r-pill);background:#3d3d3b;color:#f8f7f4;cursor:pointer;transition:background var(--web-dur-fast) var(--ease-ui)}
.ob-ask-send:disabled{background:#a2a19f;cursor:not-allowed}
.ob-hero-roles-row{display:flex;flex-wrap:wrap;gap:10px}
.ob-hero-rcard{display:flex;flex-direction:row;align-items:center;gap:7px;border:1px solid var(--web-ink-20);border-radius:var(--web-r-pill);background:transparent;padding:8px 14px;text-align:start;cursor:pointer;transition:background var(--web-dur-fast) var(--ease-ui),border-color var(--web-dur-fast) var(--ease-ui)}
.ob-hero-rcard:hover{background:color-mix(in srgb, var(--on-dark) 6%, transparent)}
.ob-hero-rcard[data-active="true"]{border-color:var(--brand-soft);background:color-mix(in srgb, var(--brand) 14%, transparent)}
.ob-hero-rcard-icon svg{display:block;width:14px;height:14px;color:var(--web-ink-60)}
.ob-hero-rcard[data-active="true"] .ob-hero-rcard-icon svg{color:var(--brand-soft)}
.ob-hero-rcard-label{display:block;font-family:var(--font-text);font-size:var(--fs-caption);font-weight:500;color:var(--web-ink)}
.ob-hero-rcard-desc{display:block;font-family:var(--font-text);font-size:var(--fs-caption);color:var(--web-ink-60)}
/* One line, always. Wrapped, each role's prompts took whatever number of rows
   they happened to need — one for four of the five roles and two for Service
   Provider — so picking that role changed the block's height and lifted the
   title and the input 20px up the screen. Nothing above a prompt row should
   move because of what is in it.
   align-self, not margin:auto — this is the cross axis of .ob-hero-ask's
   column, and a flex item centres there even when it is wider than the track,
   which is what lets the row run past the 620px composer and use the copy
   column's full width before it has to scroll. */

.ob-hero-prompts{display:flex;flex-wrap:nowrap;gap:8px;justify-content:flex-start;align-self:center;width:max-content;max-width:920px;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:none;-ms-overflow-style:none}
.ob-hero-prompts::-webkit-scrollbar{display:none}
.ob-hero-prompt{flex:none}
/* Inverted (spec 019 follow-up). These were light chips — a cream fill on a
   warm hairline, both hardcoded from the ui package's light-mode suggestion
   chips — which on this ground lit up as a row of tiles competing with the
   input above them. A ghost fill on the on-dark hairline puts them back
   behind it, and --fs-kicker takes them a step under the role switch, so the
   composer state reads input first, roles second, prompts last. */
.ob-hero-prompt{cursor:pointer;border:1px solid var(--web-line-on-dark);border-radius:var(--web-r-pill);background:var(--fill-on-dark);padding:6px 12px;font-family:var(--font-text);font-size:var(--fs-kicker);line-height:1.35;color:var(--on-dark-60);transition:background var(--web-dur-fast) var(--ease-ui),border-color var(--web-dur-fast) var(--ease-ui),color var(--web-dur-fast) var(--ease-ui)}
.ob-hero-prompt:hover{background:color-mix(in srgb, var(--on-dark) 14%, transparent);border-color:var(--on-dark-40);color:var(--on-dark)}
/* The phone step, not a 640 of its own: the page layer runs on one breakpoint
   ladder — 600/900/1200, written as max-width 599/899/1199 plus the min-width
   1200 counterpart — and e2e/web responsive.spec holds it to exactly that set.
   Neither rule loses anything by moving. Releasing the 620px cap is a no-op at
   either step, because the rail is already narrower than the cap by 640 (600)
   and by 599 (559); the row gap gives up 2px between 600 and 640. */
@media (max-width:599px){
  .ob-hero-ask{max-width:none}
  .ob-hero-roles-row{gap:8px}
}

/* Composer layout (spec 019): the prototype's actual composition is a single
   centered column — orb, then tags, headline, subcopy, input, roles — not
   the hero's default two-column grid. Scoped to [data-hero-mode="composer"]
   so the default (pre-click) hero is untouched. The text column runs wider
   than the default hero's copy column so the (longer) composer headline
   holds to two lines instead of three or four. */
.ob-hero[data-hero-mode="composer"] .ob-hero-in{grid-template-columns:minmax(0,1fr);justify-items:center}
.ob-hero[data-hero-mode="composer"] .ob-hero-reserve{display:none}
/* width:100% matters: .ob-hero-in centres its items, which sizes this column
   to its own max-content — the widest child, the prompt row. Without it the
   composer's max-width is never reached and the input silently inherits the
   chips' width instead of its own. */
.ob-hero[data-hero-mode="composer"] .ob-hero-copy{justify-items:center;text-align:center;width:100%;max-width:920px;margin-inline:auto}
.ob-hero[data-hero-mode="composer"] .ob-hero-tags{justify-content:center}
.ob-hero[data-hero-mode="composer"] .ob-hero-sub{max-width:64ch;margin-inline:auto}
.ob-hero[data-hero-mode="composer"] .ob-hero-ask{margin-inline:auto}
.ob-hero[data-hero-mode="composer"] .ob-hero-roles-row{justify-content:center}
/* The scene is NOT hidden here any more. Composer mode keeps the same Dubai
   plate the default hero has and asks the fragment to let go of the glass
   ball instead — postMessage {type:'es:sphere'}, sent from showcase-hero.tsx.
   Hiding the iframe would have cost the photograph too, and re-sourcing it
   with a query flag would have cost a scene reload mid-transition. */
/* One line at every width is the requirement, so the title is both stepped
   down from the default hero's clamp and held on a single line outright. The
   copy (HERO_COMPOSER_GUEST.title*) is short enough that nowrap never
   overruns the rail: ~19 characters at a 42px ceiling. */
.ob-hero[data-hero-mode="composer"] .t-hero{font-size:clamp(30px,1.6rem + 1.4vw,42px);white-space:nowrap}
/* The gentle half of the switch. The incoming state arrives on the page's own
   `ob-rise` entrance — React builds fresh elements for it, so that animation
   replays without being asked — and this is the matching exit for the state
   being replaced. Timing pairs with SWITCH_FADE_MS in showcase-hero.tsx. */
.ob-hero[data-hero-leaving="true"] .ob-hero-copy{opacity:0;transform:translateY(-6px);transition:opacity 200ms var(--ease-ui),transform 200ms var(--ease-ui)}
/* The composer column is centred in the section at every width. The default
   hero has its own reasons to sit low on a phone (see the 899 step); this
   state has none, so it says so here rather than inheriting them. */
.ob-hero[data-hero-mode="composer"]{align-items:center}
/* The poster is a raster of the scene WITH the glass ball in it, so it cannot
   answer the es:sphere message the live scene answers — it would put the ball
   back under a composer that exists because the ball moved out of the way.
   Dropping it leaves the poster tier (phones pre-first-frame, and reduced
   motion at any width) on the section's own ink, which is exactly what this
   state showed before it kept the plate at all. Live scenes are unaffected:
   the fragment's document is opaque and paints over this background anyway.
   The real fix is a ball-less poster from scripts/capture-fragment-posters.ts
   — a new asset, so it is not smuggled in here. */
.ob-hero[data-hero-mode="composer"] .ob-hero-bg{background-image:none}

/* ── composer on mobile ─────────────────────────────────────────────────────
   The phone hero splits into two bands — scene on top, copy below — so three
   lines of cream type never land on the one bright object on the screen. That
   reasoning is about the sphere, and composer mode has no sphere: the band
   would leave a cropped photograph above a half-section of flat ink. So the
   plate takes the whole section back and the column centres in it, which is
   the desktop composition at phone measurements.
   Both selectors are (0,2,0) on purpose — the rules they override live in the
   899 and 599 blocks far below and would otherwise win on source order. */
@media (max-width:899px){
  .ob-hero[data-hero-mode="composer"]{padding-block:var(--section-pad)}
  .ob-hero[data-hero-mode="composer"] .ob-hero-bg{inset:0;height:100%;-webkit-mask-image:none;mask-image:none}
}
@media (max-width:599px){
  /* Neither of these rows fits a phone on one line, and each answers it
     differently. Sideways lost for both: a horizontal scroller inside a
     vertical page is a gesture the reader has to discover, and half of what it
     holds is off-screen while they decide whether to.
     The roles wrap. They are five short pills — two or three rows of them fit
     inside the height one prompt row takes, every role is on screen at once,
     and each is its own target. This used to be a native <select> instead, a
     second control for the same state that put the five roles behind a tap
     and a sheet; the pills say it in place.
     The prompts stack, one per line, hugging their own text. They were
     stacked full-width rows, which turned three short suggestions into three
     banners the width of the screen — heavier than the field they feed, and
     reading as primary actions rather than as things to try. Hugged and left
     aligned, they read as a list of things to try rather than a row of
     buttons, and the block stops shouting. */
  .ob-hero[data-hero-mode="composer"] .ob-hero-roles-row{
    flex-wrap:wrap;justify-content:center;row-gap:7px;column-gap:7px;
  }
  /* Compact, because on a phone these five are the widest thing in the
     composer. They used to be inflated here instead — 10px/14px with the label
     at body size — to clear the 44px tap target the platform asks for, which
     made two rows of pills the heaviest object on the screen and pushed the
     field they are supposed to feed below the fold of attention. Back to the
     caption scale the pills carry at every other width, padding and icon
     trimmed to match. They read as a quiet row of options above the field
     rather than as the composer's main event.
     The trade is deliberate and it is real: at this size a pill is about 33px
     tall, under the platform's 44px. Each one is still its own target with 7px
     between them, they set state rather than commit anything, and the field
     below them — which is the control that matters — is full width. */
  .ob-hero[data-hero-mode="composer"] .ob-hero-rcard{padding:6px 11px;gap:6px}
  .ob-hero[data-hero-mode="composer"] .ob-hero-rcard-label{font-size:var(--fs-caption)}
  .ob-hero[data-hero-mode="composer"] .ob-hero-rcard-icon svg{width:13px;height:13px}
  /* A column, not the desktop's sideways scroller: a horizontal scroller inside
     a vertical page is a gesture the reader has to discover, and half of what
     it holds is off-screen while they decide whether to. width:auto undoes the
     max-content the scroller needs, so the stack can take the column's width
     instead of running past the edge.
     One per line, and left aligned. Centred, three pills of three different
     widths made a ragged stack with no edge to read down; on one left edge —
     the field's own — they become a list. `align-self:stretch` gives the
     container the column's full width while `align-items:flex-start` keeps
     each pill hugging its own sentence, which is what separates this from the
     full-width banners these used to be. */
  .ob-hero[data-hero-mode="composer"] .ob-hero-prompts{
    flex-direction:column;flex-wrap:nowrap;
    align-items:flex-start;justify-content:flex-start;align-self:stretch;
    width:auto;max-width:none;overflow:visible;row-gap:8px;column-gap:0;
  }
  /* Hugged: each pill is as wide as its own sentence, and its text starts at
     the same edge the pill does. */
  .ob-hero[data-hero-mode="composer"] .ob-hero-prompt{padding:8px 13px;text-align:start}
  /* iOS Safari zooms the page whenever a focused field is under 16px, and it
     does not zoom back out. 14px is the design system's step and it holds
     everywhere else; below the phone cutover the field takes 16px so tapping
     it does not throw the composition off the screen. */
  .ob-ask-field{font-size:16px}
}

/* ── shared: split section ── */
.ob-split{display:grid;gap:64px;align-items:start;margin-top:72px}
.ob-split--stack{grid-template-columns:minmax(0,1fr) auto;align-items:stretch}
.ob-sticky{position:sticky;top:104px}
/* Inside the pin the stage is already sticky, so a second sticky does nothing.
   The column stretches to the height of the list and the scene sits in its
   centre: equal margin left and right by construction, and top and bottom too. */
.ob-pin .ob-sticky{position:static;display:grid;align-content:center;justify-items:center;min-height:0;padding-block:var(--stack-inset,clamp(28px,3.2vw,72px))}

/* ── fragment mounts ── */
.ob-mount{position:relative;width:100%;background:var(--bg)}
.ob-mount>iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}
/* 03 — the video stack. The scene is square and its edge mask is a unit square
   stretched to the canvas, so a non-square box distorts the feather and crops
   the fit. The width has to be a definite length: in an `auto` grid track a
   percentage resolves against an unresolved track and collapses it to nothing. */
/* The square is allowed past the fold now. Held inside it, the scene could only
   ever be the viewport height minus the stage's chrome, and that ceiling is what
   kept reading as small — the object inside the frame occupies about seventy per
   cent of it, so a square that fits exactly draws a board much smaller than the
   screen. At 106svh the box overruns by three per cent top and bottom, the stage
   clips that, and every part of the render that carries anything is still on
   screen. The ratio stays 1:1: the edge mask is a unit square stretched to the
   canvas and warps the moment the box leaves the ratio, so only the size moves. */
.ob-mount--stack{aspect-ratio:1/1;width:var(--stack-cap,clamp(360px,106svh,76vw));margin:0;min-height:0}
/* Short viewports: 63svh plus the inset and the head does not fit a 720px-tall
   window, and the pin has to fit the viewport whole. */
/* Short viewports get the same treatment, against their own height. 64svh was
   set when the scene had to fit inside the stage whole and share the rail with a
   list; it does neither now, and on a 720-tall window it was drawing the scene
   at 461px on a 1280 screen. */
@media (max-height:820px){.ob-mount--stack{--stack-cap:clamp(280px,98svh,68vw)}}
#role{overflow-x:clip} /* выпуск сцены за рейл не даёт документу горизонтальной ширины */
/* до краёв (слово Роба): frame сцены зажат кодом на 1.0 — добираем монтажом.
   Асимметричный выпуск: рой (0.144W) на линию тайтла, конец стрелки (0.96W)
   в правый край рейла, ядро выходит на 53.7% — визуальный центр. */
/* веер fan (слово Роба 11.08) симметричен — выпуск за рейл снят, композиция по
   рейлу. Геометрия сцены живёт в блоке 04 ниже: с 26.08 она во всех ширинах
   рисует крону, и пропорция у неё одна. */
.ob-fb{position:absolute;inset:0;display:none;align-content:center;justify-items:center;gap:12px;padding:32px;text-align:center;background:var(--web-ink-06)}
.ob-mount[data-state=loading] .ob-fb,.ob-mount[data-state=error] .ob-fb{display:grid}
.ob-mount[data-state=loading] .ob-fb-msg,.ob-mount[data-state=error] .ob-fb-skel{display:none}
.ob-fb-skel{display:grid;gap:12px;width:min(280px,60%)}
.ob-fb-skel i{display:block;height:8px;border-radius:var(--web-r-pill);background:var(--web-ink-10);animation:ob-pulse 1.6s var(--ease-ui) infinite}
.ob-fb-skel i:nth-child(2){width:72%;animation-delay:var(--stagger)}
.ob-fb-skel i:nth-child(3){width:48%;animation-delay:calc(var(--stagger)*2)}
.ob-fb-msg{font-family:var(--font-mono);font-size:var(--fs-kicker);color:var(--web-ink-70)}
@keyframes ob-pulse{0%,100%{opacity:.5}50%{opacity:1}}

/* ── 02 матрица проблемы — 4 карточки вместо ганта (слово Роба: диаграмму
   снять). Гант держал весь довод одной картинкой, которую сперва надо было
   расшифровать; матрица разбирает его на четыре отдельных утверждения, и под
   каждым лежит тот кусок продукта, который на нём и ломается — TaskRow,
   ToolCallChips, ApprovalQueue, AskComposer, пересобранные на токенах пака,
   без импорта @orbox/ui (лендинг не тащит бандл приложения).
   Градиент — семейство .lens из SSOT: точки трёх радиалов вынесены в
   переменные и разведены по карточкам так, что бронза всходит к центру
   решётки — 2×2 читается одним полем, а не четырьмя плитками.
   Копия обеих локалей лежит в DOM целиком, рантайм двигает только data-*:
   поэтому переключение языка не может рассинхронизировать ридаут. */
.ob-pm{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin-top:72px}
.ob-pm-card{display:flex;flex-direction:column;background:var(--card);border:1px solid var(--web-line);border-radius:var(--web-r-lg);overflow:hidden;opacity:0;transform:translateY(10px);transition:opacity var(--web-dur-slow) var(--ease-ui),transform var(--web-dur-slow) var(--ease-ui)}
.ob-pm[data-in=true] .ob-pm-card{opacity:1;transform:none}
.ob-pm-card[data-card="1"]{transition-delay:var(--stagger)}
.ob-pm-card[data-card="2"]{transition-delay:calc(var(--stagger)*2)}
.ob-pm-card[data-card="3"]{transition-delay:calc(var(--stagger)*3)}
/* Сцена фиксирована по высоте, а не по контенту: линейка под иллюстрацией —
   единственная горизонталь, которую глаз читает поперёк всей решётки, и
   плавающая высота ставила бы её на четырёх разных уровнях. Разницу в длине
   заголовков забирает .ob-pm-copy (flex:1), а не сцена. */
/* Фон сцены — одна размытая призматическая подложка (prism-wash), нарезанная
   решёткой: background-size 200% 200%, и каждая карточка держит свой угол, так
   что 2×2 читается окнами в одну картинку, а не четырьмя плитками. Растяжение
   по вертикали ~24% (сцена 2.11, исходник 1.60) на снимке с extreme gaussian
   blur не проявляется — искажать в нём нечего, ради чего его и брали.
   Кадр подан как есть: ни грейда в ассете, ни вуали поверх. Вуаль тут стояла
   ради контраста подписей, но подписи с тех пор переехали на --card (ниже:
   .ob-pm-read, .ob-pm-checks, .ob-pm-answer), и держать её больше незачем —
   на самой подложке остались одни нетекстовые метки.
   Стык с копией — без линейки (слово Роба): кадр и так кончается собственным
   краем, а хейрлайн поверх него читался швом. Фиксированная высота сцены при
   этом остаётся: она держит четыре иллюстрации на одной базовой линии.
   Две декларации на элемент — как у постеров выше: plain url() ловит браузеры
   без image-set(), остальные берут AVIF. */
/* The figure's own type scale. Four steps, fixed pixels, declared here beside
   the height they are composed against: the stage is a 268px box holding four
   hand-drawn figures, so the type in them is part of the drawing rather than
   running text, and it must not move when the viewport does. Everything
   outside the stage — .ob-pm-title and .ob-pm-body, in .ob-pm-copy — stays on
   the page's fluid scale, because it is running text.
   These four values are what the figures rendered at 390px before the scale
   was named, which is the width they were drawn at; the two converted tokens
   each take their own floor (--fs-caption 12.5, --fs-kicker 12).
   Frozen at the phone value and not the desktop one on purpose: below 900 the
   stage relaxes to min-height 236px, so it is the tighter of the two boxes and
   labels must not grow into it.
   Before this, two of the four tiers were hardcoded px and two were on the
   fluid ramp, so they slid past each other as the canvas grew — measured at
   1277px, .ob-pm-chip-val (12.84) had overtaken the .ob-pm-chip-unit (12.5) it
   annotates, and .ob-pm-read-line (13.77) had overtaken .ob-pm-step-name
   (13.5). At 1440 --fs-kicker reaches exactly 13px, level with the names it is
   meant to sit under. responsive.spec holds the ladder ordered and identical
   at 390/1024/1440. */
.ob-pm-stage{--pm-pos:0% 0%;
  --pm-lead:13.5px;  /* sentences and primary names */
  --pm-name:13px;    /* secondary names */
  --pm-note:12.5px;  /* the thesis chip and the figure's caption line */
  --pm-meta:12px;    /* values, tags, badges, stamps */
  position:relative;isolation:isolate;height:268px;padding:22px;display:flex;flex-direction:column;justify-content:center;
  background-color:var(--panel);
  background-image:url('assets/textures/prism-wash.webp');
  background-image:image-set(
      url('assets/textures/prism-wash.avif') type('image/avif'),
      url('assets/textures/prism-wash.webp') type('image/webp')
    );
  background-size:200% 200%;
  background-position:var(--pm-pos);
  background-repeat:no-repeat}
.ob-pm-card[data-card="1"] .ob-pm-stage{--pm-pos:100% 0%}
.ob-pm-card[data-card="2"] .ob-pm-stage{--pm-pos:0% 100%}
.ob-pm-card[data-card="3"] .ob-pm-stage{--pm-pos:100% 100%;justify-content:flex-end}
/* Ночь — единственное место, где кадр всё же под вуалью: снимок светлый, и на
   тёмной странице поданный как есть он был бы не картинкой, а дырой. Рецепт
   тот же, которым .on-dark .lens переигрывает светлый .lens в SSOT. */
html.night .ob-pm-stage{background-color:var(--night-raised);
  background-image:
    linear-gradient(200deg,
      color-mix(in srgb, var(--night-raised) 88%, transparent) 0%,
      color-mix(in srgb, var(--night-raised) 74%, transparent) 46%,
      color-mix(in srgb, var(--brand-deep) 46%, transparent) 100%),
    url('assets/textures/prism-wash.webp');
  background-image:
    linear-gradient(200deg,
      color-mix(in srgb, var(--night-raised) 88%, transparent) 0%,
      color-mix(in srgb, var(--night-raised) 74%, transparent) 46%,
      color-mix(in srgb, var(--brand-deep) 46%, transparent) 100%),
    image-set(
      url('assets/textures/prism-wash.avif') type('image/avif'),
      url('assets/textures/prism-wash.webp') type('image/webp')
    );
  background-size:auto, 200% 200%;
  background-position:center, var(--pm-pos)}
.ob-pm-fig{width:100%}
.ob-pm-copy{flex:1;display:flex;flex-direction:column;gap:6px;padding:20px 22px 22px}
/* 400 and zero tracking, like every other display element on the page. This
   rule was written before the Inter Tight swap and kept Shapiro's 500/-0.02em
   — the blanket reset at the top of the file only names the five .t-* roles,
   and this one does not go through them. */
.ob-pm-title{margin:0;font-family:var(--font-display);font-weight:400;font-size:var(--fs-sub-title);letter-spacing:0;line-height:1.2;color:var(--web-ink);text-wrap:balance}
.ob-pm-body{margin:0;font-family:var(--font-text);font-size:var(--fs-body);line-height:1.5;color:var(--web-ink-70);text-wrap:pretty}

/* ── общий знак состояния: пусто / крутится / готово ── */
.ob-pm-mark{position:relative;box-sizing:border-box;flex:none;width:18px;height:18px;border-radius:50%;border:1.5px solid var(--web-ink-20);background:transparent;transition:background var(--web-dur-base) var(--ease-ui),border-color var(--web-dur-base) var(--ease-ui)}
.ob-pm-mark[data-mark=done],
.ob-pm-check[data-state=done] .ob-pm-mark,
.ob-pm-target[data-state=done] .ob-pm-mark{background:var(--pos);border-color:var(--pos)}
.ob-pm-mark[data-mark=done]::after,
.ob-pm-check[data-state=done] .ob-pm-mark::after,
.ob-pm-target[data-state=done] .ob-pm-mark::after{content:"";position:absolute;left:50%;top:46%;width:4px;height:7.5px;border:solid var(--card);border-width:0 1.5px 1.5px 0;transform:translate(-50%,-50%) rotate(45deg)}
.ob-pm-mark[data-mark=running],.ob-pm-check[data-state=running] .ob-pm-mark{border-top-color:var(--brand);animation:ob-pm-spin 900ms linear infinite}
@keyframes ob-pm-spin{to{transform:rotate(360deg)}}

/* ── карточка 01: шаг едет, контекст стоит ──
   Чип — единственный элемент, которого рантайм не касается: рельс заливается,
   строка загорается, тег стороны меняется, а он остаётся. Это и есть тезис
   карточки, поэтому он стоит первым и держит собственную поверхность. */
.ob-pm-chip{display:flex;align-items:stretch;width:fit-content;max-width:100%;margin:0 0 6px;border:1px solid var(--web-line);border-radius:var(--web-r-pill);background:var(--card);overflow:hidden}
.ob-pm-chip-unit{padding:4px 11px;font-family:var(--font-text);font-size:var(--pm-note);line-height:1.4;color:var(--web-ink)}
.ob-pm-chip-val{padding:4px 11px;border-inline-start:1px solid var(--web-line);font-family:var(--font-mono);font-size:var(--pm-meta);line-height:1.55;letter-spacing:0;color:var(--web-ink-60)}
.ob-pm-rail{list-style:none;margin:0;padding:0}
.ob-pm-rail li{position:relative}
.ob-pm-rail li + li{margin-block-start:3px}
/* Связка рисуется в самом зазоре между шагами: кнопки стоят на --card, и
   сквозная линия под ними была бы не видна. Шесть пикселей непрерывности —
   ровно то, что отличает один маршрут от четырёх отдельных плиток. */
.ob-pm-rail li + li::before{content:"";position:absolute;top:-3px;inset-inline-start:16px;width:1.5px;height:3px;background:var(--brand)}
.ob-pm-step{appearance:none;width:100%;display:flex;align-items:center;gap:10px;padding:3px 12px;text-align:start;border:1px solid var(--web-line);border-radius:var(--web-r-sm);background:var(--card);color:inherit;cursor:pointer;transition:border-color var(--web-dur-base) var(--ease-ui),box-shadow var(--web-dur-base) var(--ease-ui)}
.ob-pm-step[data-on=true]{border-color:var(--brand);box-shadow:0 0 0 1px var(--brand)}
.ob-pm-node{flex:none;box-sizing:border-box;width:9px;height:9px;margin-inline-start:1.5px;border-radius:50%;border:1.5px solid var(--brand);background:var(--card);transition:background var(--web-dur-base) var(--ease-ui),box-shadow var(--web-dur-base) var(--ease-ui)}
.ob-pm-step[data-on=true] .ob-pm-node{background:var(--brand);box-shadow:0 0 0 3px var(--brand-glow)}
html.night .ob-pm-step[data-on=true] .ob-pm-node{box-shadow:0 0 0 3px var(--fill-on-dark)}
.ob-pm-step-name{flex:none;font-family:var(--font-text);font-size:var(--pm-lead);color:var(--web-ink)}
.ob-pm-tag{flex:1 1 auto;text-align:end;font-family:var(--font-mono);font-size:var(--pm-meta);line-height:1.3;letter-spacing:0;color:var(--web-ink-60)}
/* Ридаут — поле на --card, а не слова на подложке. Кремовая вуаль поднимает
   тёмные участки кадра максимум до 4.00 для ink-60: подложка в этой системе —
   грунт МЕЖДУ панелями, ровно как .lens в SSOT, а текст живёт на карточке. */
.ob-pm-read{margin:9px 0 0;min-height:26px;padding:5px 12px;border:1px solid var(--web-line);border-radius:var(--web-r-sm);background:var(--card)}
.ob-pm-read-line{display:none;font-family:var(--font-mono);font-size:var(--pm-note);letter-spacing:0;line-height:1.45;color:var(--web-ink-60)}
.ob-pm-read-line[data-on=true]{display:block}
.ob-pm-read-line[data-on=true]:not([data-index="-1"]){color:var(--brand-deep)}
html.night .ob-pm-read-line[data-on=true]:not([data-index="-1"]){color:var(--brand-soft)}

/* ── карточка 02: введено раз, верно везде ──
   Обе локали и все три факта лежат в DOM целиком; рантайм двигает data-fact,
   показ выбирает CSS. Поэтому смена языка и смена факта не могут разойтись. */
.ob-pm-fact{display:none}
.ob-pm-fig--sync[data-fact="0"] .ob-pm-fact[data-index="0"],
.ob-pm-fig--sync[data-fact="1"] .ob-pm-fact[data-index="1"],
.ob-pm-fig--sync[data-fact="2"] .ob-pm-fact[data-index="2"]{display:inline}
.ob-pm-source{appearance:none;width:100%;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px 10px;padding:10px 12px;text-align:start;border:1px solid var(--brand);border-radius:var(--web-r-sm);background:var(--card);color:inherit;cursor:pointer}
.ob-pm-source-key{font-family:var(--font-text);font-size:var(--pm-lead);color:var(--web-ink)}
.ob-pm-source-val{grid-row:2;font-family:var(--font-mono);font-size:var(--pm-meta);letter-spacing:0;color:var(--web-ink)}
.ob-pm-source-badge{grid-column:2;grid-row:1/3;align-self:center;font-family:var(--font-mono);font-size:var(--pm-meta);letter-spacing:0;color:var(--pos-ink)}
html.night .ob-pm-source-badge{color:var(--pos-on-dark)}
.ob-pm-fan{display:flex;justify-content:center;align-items:flex-start;gap:22px;height:18px;padding-block:2px}
/* Три штриха расходятся из источника — одно значение уходит в три системы.
   Наклон и делает их веером: параллельные палочки читались тремя случайными
   чёрточками, а не связью. */
.ob-pm-fan i{width:1.5px;height:14px;background:var(--web-ink-20);transform-origin:top center;transition:background var(--web-dur-base) var(--ease-ui)}
.ob-pm-fan i:first-child{transform:rotate(-18deg)}
.ob-pm-fan i:last-child{transform:rotate(18deg)}
.ob-pm-fig--sync:is([data-state=running],[data-state=done]) .ob-pm-fan i{background:var(--brand)}
.ob-pm-fig--sync[data-state=running] .ob-pm-fan i{animation:ob-pm-fall 620ms var(--ease-ui) both}
.ob-pm-fig--sync[data-state=running] .ob-pm-fan i:nth-child(2){animation-delay:.1s}
.ob-pm-fig--sync[data-state=running] .ob-pm-fan i:nth-child(3){animation-delay:.2s}
@keyframes ob-pm-fall{from{transform:translateY(-6px);opacity:0}to{transform:none;opacity:1}}
.ob-pm-targets{list-style:none;margin:0;padding:5px;border:1px solid var(--web-line);border-radius:var(--web-r-sm);background:var(--card)}
.ob-pm-target{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:var(--web-r-sm)}
.ob-pm-target .ob-pm-mark{width:15px;height:15px}
.ob-pm-target-name{flex:none;font-family:var(--font-text);font-size:var(--pm-name);color:var(--web-ink-70);transition:color var(--web-dur-base) var(--ease-ui)}
.ob-pm-target[data-state=done] .ob-pm-target-name{color:var(--web-ink)}
.ob-pm-target-val{flex:1 1 auto;text-align:end;font-family:var(--font-mono);font-size:var(--pm-meta);letter-spacing:0;color:var(--web-ink-40);transition:color var(--web-dur-base) var(--ease-ui)}
.ob-pm-target[data-state=done] .ob-pm-target-val{color:var(--web-ink)}

/* ── карточка 03: проверки идут, пока едет сделка ──
   Четыре дорожки заливаются одновременно — очередь и параллель отличаются
   именно этим, и разложить их лесенкой значило бы нарисовать прежний тезис. */
.ob-pm-checks{list-style:none;margin:0;padding:5px;border:1px solid var(--web-line);border-radius:var(--web-r-sm);background:var(--card)}
.ob-pm-check{display:flex;align-items:center;gap:10px;padding:4px 10px;border-radius:var(--web-r-sm);background:transparent;transition:background var(--web-dur-base) var(--ease-ui)}
.ob-pm-check[data-state=running]{background:var(--web-ink-06)}
.ob-pm-check .ob-pm-mark{width:15px;height:15px}
.ob-pm-check-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-text);font-size:var(--pm-name);color:var(--web-ink-70);transition:color var(--web-dur-base) var(--ease-ui)}
.ob-pm-check[data-state=done] .ob-pm-check-name{color:var(--web-ink)}
.ob-pm-track{flex:0 0 30%;height:3px;border-radius:2px;background:var(--web-ink-06);overflow:hidden}
.ob-pm-track i{display:block;width:0;height:100%;border-radius:2px;background:var(--brand)}
.ob-pm-check[data-state=running] .ob-pm-track i{animation:ob-pm-fill 700ms var(--ease-ui) forwards}
.ob-pm-check[data-state=done] .ob-pm-track i{width:100%;background:var(--pos)}
@keyframes ob-pm-fill{from{width:0}to{width:100%}}
.ob-pm-check-wait{flex:none;min-width:6ch;text-align:end;font-family:var(--font-mono);font-size:var(--pm-meta);letter-spacing:0;color:var(--web-ink-60);opacity:0;transition:opacity var(--web-dur-base) var(--ease-ui)}
.ob-pm-check:is([data-state=running],[data-state=done]) .ob-pm-check-wait{opacity:1}
/* Вторая половина фразы: строка, которая идёт всё время прогона. Без неё
   «пока едет сделка» не нарисовано ничем. Пунктир — она вне очереди. */
.ob-pm-meanwhile{display:flex;align-items:center;gap:10px;margin:6px 0 0;padding:5px 11px;border:1px dashed var(--web-line-2,var(--web-line));border-radius:var(--web-r-sm);background:var(--card)}
.ob-pm-meanwhile .ob-pm-mark{width:15px;height:15px}
.ob-pm-meanwhile-name{flex:none;font-family:var(--font-text);font-size:var(--pm-name);color:var(--web-ink)}
.ob-pm-meanwhile-note{flex:1 1 auto;text-align:end;font-family:var(--font-mono);font-size:var(--pm-meta);letter-spacing:0;color:var(--web-ink-60)}
/* Кнопка — последнее, что стояло прямо на подложке: с ходом вуали бронзовое
   слово легло на тёмный участок кадра и перестало читаться. Поверхность у неё
   теперь своя, как у всего остального внутри иллюстраций. */
.ob-pm-run{appearance:none;align-self:flex-start;margin-block-start:9px;padding:7px 15px;border:1px solid var(--brand);border-radius:var(--web-r-pill);background:var(--brand);font-family:var(--font-text);font-size:var(--pm-lead);color:var(--on-brand);cursor:pointer;transition:background var(--web-dur-base) var(--ease-ui),border-color var(--web-dur-base) var(--ease-ui)}
/* Ховер уходит в --brand-deep, слово остаётся белым — ровно рецепт из шапки
   файла (3.35:1 в покое — решение заказчика, 5.81 на ховере). */
@media (hover:hover) and (pointer:fine){.ob-pm-run:hover{background:var(--brand-deep);border-color:var(--brand-deep)}}
.ob-pm-run-label{display:none}
.ob-pm-fig--parallel[data-state=idle] .ob-pm-run-label[data-when=idle],
.ob-pm-fig--parallel[data-state=running] .ob-pm-run-label[data-when=running],
.ob-pm-fig--parallel[data-state=done] .ob-pm-run-label[data-when=done]{display:inline}

/* ── карточка 04: спроси Orbox, а не переписку ──
   Ответ показан в покое: карточка обязана заявить тезис до касания, а не
   предлагать его. Нажатие переигрывает сборку строк. */
.ob-pm-bubble{max-width:88%;margin:0;padding:6px 13px;border:1px solid var(--web-line);border-radius:var(--web-r-md);border-start-start-radius:6px;background:var(--card);font-family:var(--font-text);font-size:var(--pm-lead);line-height:1.45;color:var(--web-ink-60)}
.ob-pm-answer{display:none;margin-block-start:6px;padding:4px;border:1px solid var(--web-line);border-radius:var(--web-r-sm);background:var(--card)}
.ob-pm-fig--ask[data-state=answered] .ob-pm-answer{display:block}
.ob-pm-status{list-style:none;margin:0;padding:0}
.ob-pm-status-row{display:flex;align-items:center;gap:9px;padding:2px 8px;border-radius:var(--web-r-sm)}
.ob-pm-status-row .ob-pm-mark{width:15px;height:15px}
.ob-pm-status-name{flex:none;font-family:var(--font-text);font-size:var(--pm-name);color:var(--web-ink)}
.ob-pm-status-state{flex:1 1 auto;text-align:end;font-family:var(--font-mono);font-size:var(--pm-meta);letter-spacing:0;color:var(--web-ink-60)}
.ob-pm-stamp{margin:2px 0 0;padding:0 8px 1px;font-family:var(--font-mono);font-size:var(--pm-meta);letter-spacing:0;color:var(--pos-ink)}
html.night .ob-pm-stamp{color:var(--pos-on-dark)}
.ob-pm-fig--ask[data-state=answered] .ob-pm-status-row{animation:ob-pm-pop var(--web-dur-base) var(--ease-ui) both}
.ob-pm-fig--ask[data-state=answered] .ob-pm-status-row[data-index="1"]{animation-delay:70ms}
.ob-pm-fig--ask[data-state=answered] .ob-pm-status-row[data-index="2"]{animation-delay:140ms}
.ob-pm-fig--ask[data-state=answered] .ob-pm-status-row[data-index="3"]{animation-delay:210ms}
@keyframes ob-pm-pop{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.ob-pm-ask{appearance:none;width:100%;display:flex;align-items:center;gap:10px;margin-block-start:6px;padding:5px 5px;padding-inline-start:14px;border:1px solid var(--web-line-control);border-radius:var(--web-r-pill);background:var(--card);font-family:var(--font-text);font-size:var(--pm-lead);text-align:start;color:var(--web-ink);cursor:pointer;transition:color var(--web-dur-base) var(--ease-ui),border-color var(--web-dur-base) var(--ease-ui)}
.ob-pm-ask-text{flex:1 1 auto}
.ob-pm-ask-send{position:relative;flex:none;width:24px;height:24px;border-radius:50%;background:var(--brand)}
.ob-pm-ask-send::after{content:"";position:absolute;left:50%;top:50%;width:5px;height:5px;border:solid var(--on-brand);border-width:1.5px 1.5px 0 0;transform:translate(-62%,-50%) rotate(45deg)}
html[dir="rtl"] .ob-pm-ask-send::after{transform:translate(-38%,-50%) rotate(-135deg)}

/* ── 03 stack ── */
/* Chips, not rows. The hairline form gave all seven layers the full width of
   the column whether or not one was being read, so the list read as a table of
   contents and the open layer differed from the other six by a 2px tick. A chip
   is the shape of the thing it names: closed it is only as wide as its own
   label, open it grows to the column and becomes the panel the copy sits in.
   Which layer is live is then the loudest fact in the column instead of the
   quietest, which is the whole job of a list that drives a scene.
   Width animates by the same mechanism the body's height already used —
   `interpolate-size:allow-keywords` below is what lets fit-content and auto
   interpolate at all — and the @supports fallback further down covers the
   engines without it: there the chip snaps and only the copy fades. */
/* Flex column, not a grid. As a grid each chip sat in its own auto row, and an
   auto row sizes itself from the item's max-content block size — which for
   these came out 22px taller than the chip actually renders, so every pill was
   stretched into a rounded rectangle with the gaps to match. A flex column
   sizes each item to its own content in both axes and has no row to disagree
   with. align-items is the inline-start edge under both directions, so RTL
   needs nothing said about it. */
/* Both keyword sizes this section animates — the plate's fit-content width and
   the sleeve's auto height — interpolate only because of this line. It is a
   page-level switch, declared here next to the two rules that need it, and the
   @supports below is its fallback. */
/* Two beats, and --copy-out is the hinge between them. Opening, the box travels
   first and the copy fades in after it has arrived (delay = the box's own
   duration). Closing, the copy goes first and the box waits --copy-out before it
   starts folding, so the reader never watches text reflow inside a moving box.
   It is declared as a token because four rules have to agree on it. */
:root{interpolate-size:allow-keywords;--copy-out:150ms}
.ob-stack{list-style:none;padding:0;display:flex;flex-direction:column;align-items:stretch;gap:6px;container-type:inline-size}
/* The <li> is a full-width lane that paints nothing, and stays the click
   target: a click anywhere on a layer's line picks that layer, which is the
   contract the row form had and fragment-gating.spec.ts pins. It reaches over
   the scene behind it, and that costs nothing — the frame is decorative and
   aria-hidden, with nothing under there to click. */
.ob-stack-item{width:100%}
/* ONE shape. The pill and the panel are the same box at two sizes — one
   background, one radius — so there is no seam to see and nothing to keep in
   sync. The previous pass split the paint across the head and the copy, and
   that is what read as a card cut into two sections: mid-travel the head had
   squared its bottom corners while the copy under it was still fading its own
   background up from zero, so for a third of a second the chip was a clipped
   plate sitting on a paler one.
   fit-content is measurable here only because the copy is sleeved (below) and
   contributes no width — the plate measures its title, and travels from it. */
.ob-stack-plate{width:fit-content;max-width:100%;overflow:hidden;border-radius:var(--web-r-pill);background:color-mix(in srgb,var(--night) 72%,var(--on-dark) 7%);-webkit-backdrop-filter:blur(14px) saturate(150%);backdrop-filter:blur(14px) saturate(150%);padding-block:8px;padding-inline:16px 8px;transition:width var(--web-dur-slow) var(--ease-ui) var(--copy-out),border-radius var(--web-dur-slow) var(--ease-ui) var(--copy-out),background var(--web-dur-base) var(--ease-ui),padding var(--web-dur-slow) var(--ease-ui) var(--copy-out)}
.ob-stack-item[data-active=true] .ob-stack-plate{width:100%;border-radius:var(--web-r-lg);background:color-mix(in srgb,var(--night) 82%,var(--on-dark) 10%);padding-block:12px 14px;padding-inline:16px 12px;transition-delay:0s}
.ob-stack-head{display:flex;align-items:center;gap:10px}
/* The title is the one thing that does NOT move between states. It was shrunk
   and greyed on open, following the reference, and that turned out to be the
   loudest part of the switch: the name of the layer you had just arrived at was
   the element that jumped. One size, one colour, and the plate growing under it
   carries the state on its own. */
.ob-stack-title{flex:1 1 auto;font-family:var(--font-display);font-size:var(--fs-body);font-weight:400;letter-spacing:0;line-height:1.25;color:var(--web-ink)}
/* + closed, x open: one circle rotated 45 degrees rather than two glyphs
   swapped. A swap pops; the rotation is the affordance animating into its own
   dismissal. Centred with physical left/translate on purpose — the two bars are
   symmetric, so there is nothing here for RTL to mirror, and an inset-inline
   start would fight the physical translate under dir=rtl. */
.ob-stack-plus{position:relative;flex:none;width:20px;height:20px;border-radius:50%;background:var(--web-ink-10);transition:transform var(--web-dur-base) var(--ease-ui),background var(--web-dur-base) var(--ease-ui)}
.ob-stack-plus::before,.ob-stack-plus::after{content:"";position:absolute;left:50%;top:50%;width:8px;height:1.5px;border-radius:1px;background:var(--web-ink);transform:translate(-50%,-50%)}
.ob-stack-plus::after{transform:translate(-50%,-50%) rotate(90deg)}
.ob-stack-item[data-active=true] .ob-stack-plus{transform:rotate(45deg);background:var(--web-ink-20)}
/* The sleeve. width:0 so it hands the plate no intrinsic width at all — that
   was the whole difficulty: a paragraph with height:0 still offers its parent
   the full max-content width of its text, so while the copy was a direct child
   the plate's fit-content WAS the full width and the pill jumped to its final
   size on frame one. Zero-width, the sleeve is invisible to that measurement,
   and only its height moves. It does not clip: the plate does, and the sleeve
   clipping too would take the paragraph's own width to nothing with it. */
.ob-stack-copy{width:0;height:0;transition:height var(--web-dur-slow) var(--ease-ui) var(--copy-out),margin-top var(--web-dur-slow) var(--ease-ui) var(--copy-out)}
.ob-stack-item[data-active=true] .ob-stack-copy{height:auto;margin-top:8px;transition-delay:0s}
/* One measure, fixed, taken from the list rather than from the plate — so the
   paragraph is laid out once and never re-wraps while the plate travels. 38px
   is the open plate's own inline padding, 22 + 16. */
.ob-stack-body{width:calc(100cqi - 28px);font-size:var(--fs-caption);line-height:1.5;color:var(--web-ink-70);text-wrap:pretty;opacity:0;transition:opacity var(--copy-out) var(--ease-ui)}
.ob-stack-item[data-active=true] .ob-stack-body{opacity:1;transition:opacity var(--web-dur-base) var(--ease-ui) var(--web-dur-slow)}
@keyframes ob-reveal{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@supports not (interpolate-size:allow-keywords){
  .ob-stack-body{transition:opacity var(--web-dur-base) var(--ease-ui)}
  .ob-stack-item[data-active=true] .ob-stack-body{animation:ob-reveal var(--web-dur-slow) var(--ease-ui) both}
  /* fit-content -> 100% cannot interpolate here either, and a width that jumps
     while the radius eases reads as two separate events. Both snap together. */
  .ob-stack-plate{transition:background var(--web-dur-base) var(--ease-ui)}
}
.ob-stack-plate{cursor:default}
@media (prefers-reduced-motion:reduce){.ob-stack-plate,.ob-stack-copy,.ob-stack-body{transition:none}.ob-stack-item[data-active=true] .ob-stack-body{animation:none}}
@media print{.ob-stack-plate{width:100%}.ob-stack-copy{height:auto}.ob-stack-body{opacity:1;transition:none}}

/* ── 03 инверсия (слово Роба: «визуальный перепад») ── ночная полоса --night,
   фрагмент подан ВСЕГДА с &theme=night (srcFor его не дублирует), механика
   связки не тронута. Стыки со светлыми соседями — хейрлайны, теней нет. */
.ob-nightband{--web-ink:var(--on-dark);--web-ink-90:var(--on-dark);--web-ink-70:var(--on-dark-70);--web-ink-60:var(--on-dark-60);--web-ink-40:var(--on-dark-40);--web-ink-20:var(--web-line-on-dark);--web-ink-10:var(--web-line-on-dark);--web-ink-06:var(--fill-on-dark);--web-line:var(--web-line-on-dark);--web-line-2:var(--web-line-on-dark);--focus-ring:var(--brand-soft);--shadow-1:none;--shadow-2:none;--shadow-3:none;--shadow-4:none;background:var(--night);color:var(--on-dark-70);border-block:1px solid var(--web-line-on-dark)}
.ob-nightband--ink{background:var(--web-surface-dark)}
.ob-nightband .ob-mount{background:transparent}
/* The scene brings its own ground and the band has to swallow it. The clip was
   encoded against #1F1C17 — the value --night carries — but the encode fell
   short by 3 to 8 levels per layer, so the frame lands a little above the band
   and reads as a rounded-square patch resting on it. `gfix` subtracts that
   residual by hand and has to be re-tuned per clip; `lighten` makes the
   subtraction unnecessary, because every pixel at or below the band's own value
   is clamped to the band and stops existing — patch, vignette and all.
   `lighten`, not `multiply`: multiply is the half of this trick that belongs on
   a LIGHT ground, where the plate to drop is white. Here the plate is black, so
   the lightening half is the one that drops it. `screen` was measured first and
   is wrong for a different reason — it never darkens, so the residual came back
   as a patch BRIGHTER than the band rather than gone.
   The ground sits on the mount rather than on a section: a blended element
   composites against the nearest stacking context it is inside, and which
   ancestor that is changes with the layout — .ob-pin-stage is sticky (and so a
   stacking context) above 1200, .ob-sticky is below it. The mount is the
   frame's own parent in both, so putting the band's ground there is the one
   place that is correct at every width. */
.ob-nightband .ob-mount--stack{isolation:isolate;background:none}
.ob-nightband .ob-mount--stack::before{content:"";position:absolute;inset:0;background:var(--night)}
.ob-mount--stack > iframe{mix-blend-mode:lighten}
/* isolation, and a ground of its own inside it. A blended element composites
   against the nearest stacking context that contains it, and until now that was
   whichever ancestor happened to make one — .ob-pin-stage above 1200, .ob-sticky
   below it, something else again if a parent gains a transform. That is a lot of
   page for one blend to depend on, and it is the kind of dependency that renders
   one way here and another way on a GPU that composites the frame differently.
   isolation:isolate makes the mount the group, full stop, and the ::before is
   the band's own ground painted INSIDE that group so there is always something
   to blend with. Nothing above the mount can change the result now. */
/* пин-скролл 03 (слово Роба: «застряли на чёрном и скроллим, пока не проскроллим»):
   секция = длинный пробег, состав пришпилен на весь проход, слои гонит скролл */
/* 520vh, not 260: 420vh of travel over six transitions is ~70vh per layer, and
   below that a flick of the trackpad jumps clean over a state. */
.ob-pin{padding-block:0;height:520vh}
/* The head's own clearance under the fixed topbar, which used to come from
   .ob-pin-stage's padding back when the head lived inside it. Desktop only:
   below 1200 the section's own --section-pad is already doing this, and the
   stage is not pinned there at all. */
@media (min-width:1200px){
  /* The head sat on the topbar's clearance alone, which is the space the bar
     needs and not spacing anyone reads — the title landed flush against the top
     edge of the band. The clearance plus a real interval. */
  .ob-pin-head{padding-block:calc(var(--top-h,73px) + 72px) 0}
  /* The split is the only thing in the stage now, and the stage centres it.
     The 36px below separates head from split in the static layout, where they
     are still siblings in flow; here it would just push the centred block
     off-centre. */
  .ob-pin .ob-split{margin-top:0}
}
.ob-pin-stage{position:sticky;top:0;min-height:100svh;display:grid;align-items:center;overflow:clip;padding-block:calc(var(--top-h,73px) * 0.5) 8px}
/* пин обязан влезать в вьюпорт целиком: состав плотнее только внутри пина */
.ob-pin .ob-head{gap:12px}
.ob-pin .ob-split{margin-top:36px}
/* One cell, not two columns. Side by side, the scene could only ever be as wide
   as the half of the rail the list did not want, and it is the thing this
   section is about — so the two now share a single grid area and the scene
   takes the width it deserves, right-aligned, with the chips riding over its
   left edge. The overlap is the point and not a collision: a chip is a small
   opaque plate and the render behind it is a field, so the two read as depth.
   Only inside the pin — below 1200 the split is a flex column and the scene
   sits above the list rather than behind it. */
/* Gated to the desktop pin. Below 1200 the split is a flex column and none of
   the grid placement means anything — but `width:min(560px,48%)` does, and at
   (0,2,0) it was quietly setting every phone card to 48% of the rail while the
   rail's own rules could not outrank it. The overlap is a desktop composition;
   it says so here. */
@media (min-width:1200px){
  /* Column density. These live behind the query with the overlap for the same
     reason: at (0,2,0) they outrank anything the phone rail can say about its
     own gap, and a 6px gap between cards is 6px the track does not travel — the
     rail drifted one gap further off-screen with every step. */
  .ob-pin .ob-stack{gap:6px}
  .ob-pin .ob-stack-plate{padding-block:7px}
  .ob-pin .ob-stack-item[data-active=true] .ob-stack-plate{padding-block:10px 12px}
  /* The gap is left as it is on purpose. One column with both children in the
     same area has no track edge to space, so the declared gutter is inert here
     — and e2e/web/src/responsive.spec.ts reads it as the desktop reference the
     collapsed bands must come in under. Zeroing it made that reference 0. */
  .ob-pin .ob-split--stack{grid-template-columns:minmax(0,1fr)}
  .ob-pin .ob-split--stack > *{grid-area:1/1}
  /* Capped so the open panel stays a column of text rather than a band across
     the whole rail, and so there is always scene to the right of it. */
  .ob-pin .ob-stack{position:relative;z-index:2;justify-self:start;align-self:center;width:min(560px,48%)}
  /* Off the rail's trailing edge, and as big as the stage's height budget
     allows. It is what the section is about, so it takes the width it deserves
     rather than the half the list did not want, and the chips float over it as
     an overlay.
     `end` rather than `center`: the list runs down the leading edge and the
     scene sat squarely behind it, so the composition had the two objects
     stacked on one centre line with the section's only open space split into
     two thin margins. Ending on the rail gathers that space into one side and
     lets the scene lean away from the column. It is a small move by
     construction: the frame is already most of the rail, so all `end` can give
     it is the leading margin it used to keep. Measured at 1440x960 the frame is
     1018 wide inside 1152 of rail content, which makes this a 67px shift — from
     centred to flush, no more, which is why it stops at the rail rather than at
     a number of its own.
     The size is untouched: --stack-cap still sets the box, and `end` moves the
     same frame rather than trading width for the room. The overlap the chips
     need survives it — the list ends at 697 and the frame's leading edge lands
     at 278, so about 420px of the scene still sits behind the column.
     Both insets go: the sticky's padding held the scene off a list that was
     beside it, and the split's top margin separated a head that is no longer in
     this box. Together they were 128px of the stage's height, which at 86svh is
     the difference between the scene fitting the fold and being cut by it. */
  .ob-pin .ob-sticky{z-index:1;justify-self:end;justify-items:end;padding-block:0}
  .ob-pin .ob-split{margin-top:0}
}

/* A pin has to fit the viewport whole, and on a short desktop window it does
   not: at 1280x720 the open item ran 39px past the fold, so the layer being
   read was the one you could not finish reading. The scene is capped by its own
   --stack-cap rule; this takes the rest out of the list, which is the taller
   half. Height-only, so the width ladder stays 599/899/1199/1200. */
@media (min-width:1200px) and (max-height:760px){
  .ob-pin .ob-split{margin-top:22px}
  .ob-pin .ob-stack{gap:4px}
  .ob-pin .ob-stack-plate{padding-block:6px}
  .ob-pin .ob-stack-item[data-active=true] .ob-stack-plate{padding-block:9px 11px}
}
@media (max-width:1199px){
  .ob-pin{height:auto;padding-block:var(--section-pad)}
  /* overflow:clip has to go with the pin: it keeps the pinned stage from
     spilling, but a clipping ancestor is also the scrollport a sticky
     descendant is measured against, and that one never scrolls — leaving it
     here stops the scene sticking without showing why. */
  .ob-pin-stage{position:static;min-height:0;padding-block:0;overflow:visible}
}

/* ── 04 role ── целевой вид (скрин Роба 10.08): свежий converge-orbox verbatim,
   ?embed=1&preset=hero — сцена сама несёт 13 источников, ядро, стрелку и tagline
   «One seamless, managed deal». Страничного текста-дубля нет. */
/* Copy in the first column, the scene in the second, on two equal halves of
   the rail (word from the design engineer, 26-27.08).

   Which diagram the scene draws is a matter of frame shape, not breakpoint:
   portrait gets the crown of marks fanning up out of the core, landscape gets
   the wide fan converging into it. Half a rail is landscape, so the desktop
   column carries the fan; the collapsed band below 1200 gives the scene a 1:2
   box and gets the crown. */
.ob-role-split{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(40px,5vw,72px);align-items:center}
.ob-role-copy{display:grid;gap:22px;align-content:center}
.ob-role-copy .t-section{max-width:15em}
.ob-role-copy .t-body-lead{max-width:34em}
/* слоган — страничной типографикой под шаром (скрин Роба 20:10); в сцене он
   погашен её же параметром &tagline=0. Both diagrams leave a clear wedge under
   the core, between the two arms that reach lowest, and that is where this
   sits.

   Anchored from the top, not the bottom. The core lands at a fixed fraction of
   the frame in either diagram — its foot at 0.72 of the height for the fan and
   0.87 for the crown, both scale-free — so a percentage from the top keeps the
   same gap under it whatever size the frame is. `bottom` did not: the words are
   a page size and the frame is not, so the further the frame grew the further
   they drifted. Measured at 944x531, the collapsed band's frame at 1024, the
   caption sat 96px below the core; it is 22px now, against 12px at 540x304. */
.ob-role-sum{position:absolute;left:50%;transform:translateX(-50%);top:76%;bottom:auto;z-index:1;pointer-events:none;margin:0;text-align:center}
.ob-role-sum span{font-family:var(--font-display);font-size:var(--fs-card-title);font-weight:400;letter-spacing:0;line-height:1.14;color:var(--web-ink)}

/* Landscape is the default, so the fan is what every width above the phone
   draws. The collapsed band used to take the crown with it, and a crown is
   350px wide by contract — in a 960px column that read as a postage stamp
   stranded in the middle of it, and cost the section 700px of height to say
   it. The band gets the fan at the full rail instead: 960x540 at 1024, big
   and short, and the same diagram the desktop shows. */
.ob-mount--lines{aspect-ratio:16/9;width:100%;height:auto;min-height:0;max-height:none;margin-inline:auto}

/* Landscape, for the two-column desktop. The fan's own proportion is 41/20 —
   its ink measures 444x234 at 540 wide and stops growing there, because the
   fit is bound by width once the box is this narrow, so a taller box is air
   rather than a bigger drawing (fill went 88% of the height at 265px to 51% at
   420px for the same 444px of ink). 16/9 buys back exactly one band of that
   air, under the core, for the caption to sit in: at 41/20 the words cleared
   the core's foot by 7px. The height follows the column, so nothing here has
   to be kept in step with the viewport. */
@media (min-width:1200px){
  /* 40/60 (word from the design engineer, 27.08). The scene is the thing worth
     looking at and it is width-bound — its ink is a fixed fraction of the
     column, so 60% of the rail is the only way to make it bigger. 648x365 at
     1440 against 540x304 before, which is 533px of drawing where there were
     444. The copy takes the 40 and its lead steps down one place on the scale
     to hold a readable measure in it: 14.5px over 432px is ~62 characters. */
  .ob-role-split{grid-template-columns:minmax(0,40fr) minmax(0,60fr)}
  .ob-role-copy .t-body-lead{font-size:var(--fs-body)}
  /* And then past the rail, because 60% of it is not the ceiling the scene
     runs into — its own width is. The drawing is a fixed 82% of whatever box
     it gets and it is already there, with 56px of margin inside a 648px frame,
     so the only way to make it bigger is to make the frame wider. Height does
     nothing: measured at 648, the ink stayed 529px wide whether the box was
     365 or 560 tall.

     The bleed is exactly the room between the rail's content edge and the page
     edge — (100vw - content-max)/2 plus the page padding — so the scene lands
     on the page edge rather than at some invented offset, and capped so a wide
     monitor does not run it away from the copy. 792px wide at 1440 against
     648, which is 646px of drawing where there were 529. #role already clips
     its own overflow, and the inline-end margin mirrors in RTL with the
     tracks. */
  .ob-mount--lines{--role-bleed:min(calc(max(0px,(100vw - var(--content-max))/2) + var(--web-page-pad)),160px);width:calc(100% + var(--role-bleed));margin-inline:0 calc(-1 * var(--role-bleed))}
  /* The fan's size is set by the column, not by the viewport, so the caption
     under it stops tracking svh here — on a 1920 window the drawing stayed
     540x263 while the words grew, and the clear wedge under the core is only
     63px deep. On the scale rather than a number of its own: measured
     clearance from the core's foot to the top of the words is 28px. */
  .ob-role-sum span{font-size:var(--fs-sub-title)}
}

/* The phone is the one width where the fan has nowhere to go: a 350px rail
   puts its marks at 27px and its wordmarks under 8px. That is what the crown
   is for, and the scene picks it up on its own from the portrait box. The
   crown carries its core lower — 0.87 of the frame against the fan's 0.72 —
   so the caption follows it down. */
@media (max-width:599px){
  .ob-mount--lines{aspect-ratio:1/2;width:min(350px,100%)}
  /* The caption is back on the phone. It was hidden here because it landed on
     the drawing, and the reason is one number: the base rule anchors it at 76%
     of the frame, which is 4% under the fan's foot at 0.72 and 11% ABOVE the
     crown's at 0.87. The crown is what every phone draws, so it gets the
     crown's fraction — same scale-free anchor as the desktop, measured from
     the right diagram. 92% leaves 35px of clear frame under the core's foot at
     the 350x700 box, against the 22px the fan is given up there, which is the
     extra room this needed to stop reading as a collision.
     Kept absolute rather than put in flow: the crown leaves 13% of the frame
     empty below its foot, so a caption stacked under the whole box would sit
     114px off the drawing with nothing in between. */
  .ob-role-sum{display:block;top:92%}
  /* Smaller and quieter than the desktop's. Up there it is the one line of
     type in its half of the split and carries card-title weight; here it
     follows a paragraph that has already made the point in full, so it reads
     as the picture's label rather than as a second claim. */
  .ob-role-sum span{font-size:var(--fs-body-lead);color:var(--web-ink-60)}
}

/* ── 05 market ── */
.ob-segments{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:56px}
.ob-seg{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:24px;background:var(--card);border:1px solid var(--web-line);border-radius:var(--web-r-md)}
.ob-seg-name{font-family:var(--font-display);font-size:var(--fs-sub-title);font-weight:400;letter-spacing:0;color:var(--web-ink)}
.ob-seg--phase{border-color:var(--brand);background:var(--brand-glow)}
html.night .ob-seg--phase{border-color:var(--brand-soft);background:var(--fill-on-dark)}
/* ── 05 market ── сухо и сжато (слово Роба 20:14): заголовок слева, знак Orbox
   малым графическим элементом напротив, ростер плотный на весь рейл. */
.ob-market{position:relative;overflow:clip;padding-bottom:var(--section-pad)}
/* ── 05: иерархия пересобрана (слово Роба 22:32) — заголовок главный слева,
   справа визуальная игра (arcs, прозрачный bare), сегменты крупными тегами-
   пилюлями, лид Brokerage бронзой с Phase 1 ── */
.ob-market-split{display:grid;grid-template-columns:minmax(0,6fr) minmax(0,6fr);gap:64px;align-items:center}
.ob-market-copy{display:grid;gap:28px;justify-items:start}
.ob-market .t-section{font-size:clamp(38px,2rem + 1.9vw,58px)} /* слово Роба: заголовок крупнее, сабтайтл прежний */
/* 560, not 500. The orbit's radii are multiples of the sphere's radius and the
   sphere is sized to this box, so the box is what sets the whole cloud's
   spread — the reference build composes against 560 and at 500 the pills flew
   a visibly tighter ring than the design draws. */
.ob-market-play{position:relative;min-height:560px}
.ob-market-play-frame{position:absolute;inset:0;width:100%;height:100%;border:0;pointer-events:none;opacity:0;z-index:1;transition:opacity var(--web-dur-slow) var(--ease-ui)}
.ob-market-play-frame.is-up{opacity:1}

/* ── 05: the orbit, taken from the reference build ──────────────────────────
   design/ORBOX_ORBIT-PILLS_2026-08-25 drives this section. `orbit.js` is
   vendored BYTE FOR BYTE into public/showcase/orbit.js — same md5 as the file
   in the design folder — and the rules below are its section-05.css, copied
   across unchanged. Four hand-written ports of that engine each read wrong on
   the page in a different way, so the port is gone and the reference runs the
   section itself. Do not "tidy" either file: the only way this stays right is
   if it stays identical, and `bun run verify:orbit` is what says so.

   The engine reads its palette from the token names the reference uses, which
   are the unprefixed ones. This page carries the same values under --web-*,
   so the aliases below are what let orbit.js's paint() find them; they are a
   translation layer, not new colour. Everything orbit.js computes lands in
   --pill-*, which is why there is not a raw hex in any rule here. */
:root{
  --surface-dark:var(--web-surface-dark);
  --ink:var(--web-ink);
  --ink-06:var(--web-ink-06);
  --ink-10:var(--web-ink-10);
  --dur-slow:var(--web-dur-slow);
  --dur-fast:var(--web-dur-fast);
  --r-pill:var(--web-r-pill);
}

/* Хвост лида под выбранной ролью. Высоту под самую длинную роль резервирует
   ОБЁРТКА, а не сам абзац: иначе риска тянулась во весь резерв и висела ниже
   текста (замер 25.08). */
.ob-lead-slot{min-height:calc(var(--fs-body-lead)*1.6*3);display:flex;align-items:flex-start}
.ob-head .ob-lead-slot{max-width:34em}
.ob-lead-slot .t-body-lead{position:relative;transition:opacity 220ms var(--ease-ui),transform 220ms var(--ease-ui)}
.ob-lead-slot .t-body-lead.is-swapping{opacity:0;transform:translateY(4px)}
.ob-lead-slot .t-body-lead::before{content:"";position:absolute;inset-block:2px;inset-inline-start:-18px;width:2px;border-radius:1px;background:var(--brand-soft);transform:scaleY(0);transform-origin:top;opacity:0;transition:transform 320ms var(--ease-ui),opacity 220ms var(--ease-ui)}
.ob-market-copy.has-role .t-body-lead::before{transform:scaleY(1);opacity:1}

/* ── пилюли ──────────────────────────────────────────────────────────
   Форма и грамматика чипа те же, что в лендинге. Цвет и кегль пришли на ручки
   твик-панели: значения считает orbit.js из SSOT-токенов и кладёт в --pill-*,
   поэтому в разметке по-прежнему ноль сырых хексов. Фолбэки — исходный вид. */
.ob-seg-tags{display:flex;flex-wrap:wrap;gap:calc(10px*var(--pill-scale,1));margin:4px 0 0;padding:0;list-style:none;max-width:640px}
.ob-seg-tag{position:relative;display:inline-flex;align-items:center;gap:10px;
  font-family:var(--font-display);font-weight:400;
  /* The reference's rule is calc(var(--fs-body-lead)*var(--pill-scale)) and
     its token file pins --fs-body-lead at a flat 16px, so its pill renders at
     17.92px at every width. This page's --fs-body-lead is a fluid clamp, so
     the same rule would grow the pill to 20.16px on a wide window. The
     constant is what reproduces the reference's own rendering. */
  font-size:calc(var(--pill-base,16px)*var(--pill-scale,1));
  letter-spacing:0;line-height:1.2;
  padding:calc(9px*var(--pill-scale,1)) calc(18px*var(--pill-scale,1));
  color:var(--pill-ink,var(--ink));background:var(--pill-fill,var(--ink-06));
  border:var(--pill-bw,0) solid var(--pill-line,transparent);
  border-radius:var(--r-pill);cursor:pointer;white-space:nowrap;
  transition:background var(--dur-fast) var(--ease-ui),color var(--dur-fast) var(--ease-ui),transform var(--dur-fast) var(--ease-ui)}
@media (hover:hover) and (pointer:fine){.ob-seg-tag:hover{background:var(--pill-hover,var(--ink-10))}}
.ob-seg-tag:active{transform:scale(.97)}
.ob-seg-tag[aria-pressed="true"]{background:var(--pill-sel-fill,var(--brand));color:var(--pill-sel-ink,var(--on-brand));border-color:transparent}
.ob-seg-tag:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
/* hit-target ≥44px прозрачным паддингом, видимая геометрия не растёт */
.ob-seg-tag::after{content:"";position:absolute;inset:50% 0 auto;transform:translateY(-50%);height:44px;left:0;right:0}

/* ── глубина ────────────────────────────────────────────────────────
   Пилюли НЕ переезжают между слоями-обёртками: appendChild тасует DOM, а с ним
   и порядок Tab (замер 25.08). Список остаётся на своём месте в разметке, а
   глубину решает z-index внутри одного контекста наложения .ob-market-split:
   0 = под iframe, 2 = над. Канвас шара идёт с bg=none и прозрачен везде, кроме
   тела шара, поэтому задняя пилюля срезается кромкой, а не прямоугольником. */
.ob-market-split{position:relative}
/* протяжка облака: тянуть можно за пилюлю и за пустое место сцены */
.is-orbit .ob-market-play,.is-orbit .ob-seg-tag{cursor:grab;touch-action:none}
.is-orbit .ob-market-split.is-dragging,
.is-orbit .is-dragging .ob-market-play,
.is-orbit .is-dragging .ob-seg-tag{cursor:grabbing}
.is-orbit .is-dragging{user-select:none}
.is-orbit .ob-seg-tags{position:absolute;margin:0;padding:0;max-width:none;display:block;pointer-events:none}
.is-orbit .ob-seg-tags li:not([hidden]){display:block}
.is-orbit .ob-seg-tag{position:absolute;top:0;left:0;z-index:2;pointer-events:auto;will-change:transform;opacity:0;transition:opacity 520ms var(--ease-ui),background var(--dur-fast) var(--ease-ui),color var(--dur-fast) var(--ease-ui)}
.is-orbit .ob-seg-tag.is-in{opacity:1}
/* задняя дуга: под шаром, курсор не ловит, гасится ЦВЕТОМ к своей же заливке */
.is-orbit .ob-seg-tag.is-behind{z-index:0;pointer-events:none;background:var(--pill-back-fill);color:var(--pill-back-ink);border-color:transparent;filter:blur(var(--pill-blur,.6px))}
@media (hover:hover) and (pointer:fine){.is-orbit .ob-seg-tag.is-behind:hover{background:var(--pill-back-fill)}}
/* выбранная пилюля всегда носит бренд-вид: правило стоит ПОСЛЕ гашения задней
   дуги, иначе равная специфичность отдала бы победу приглушению */
.is-orbit .ob-seg-tag[aria-pressed="true"]{background:var(--pill-sel-fill,var(--brand));color:var(--pill-sel-ink,var(--on-brand));filter:none;z-index:2}

@media (max-width:1199px){.ob-market-split{grid-template-columns:minmax(0,1fr)}.ob-market-play{min-height:360px}}
@media (prefers-reduced-motion:reduce){.ob-market-play-frame{transition:none}}
 /* воздух до каркасного глобуса Vision (слово Роба) */


/* ── 06 vision ── композиция донора 00d на нашем 06-mesh-field: сцена с запасом
   за края секции (перескан −26%/−18%, 136×152%).
   The copy used to sit on an opaque --bg plate cut out of the scene, which read
   as a slab pasted over the mesh. It is gone: heading and cards now sit
   straight on the field, and the separation is bought where it belongs — the
   scene itself was muted a stop (see the fragment's ramp and FADE) and the
   cards keep their own --card ground. */
.ob-vision{position:relative;overflow:clip;min-height:min(108vh,1080px);display:grid;align-items:center;padding-block:var(--section-pad)}
.ob-vision-bg{position:absolute;top:-26%;left:-18%;width:136%;height:152%;z-index:0;border:0;pointer-events:none}
/* The copy column, not the rail. The scene is still the section's full-bleed
   background — nothing about .ob-vision-bg moved — but the words now hold the
   leading half of the rail and leave the other half open, so the mesh has a
   side of its own to turn in instead of spinning entirely behind text. 600 of
   the rail's 1216: just under half, which keeps the open side reading as the
   larger of the two. Below the collapse step it goes back to the full rail. */
.ob-vision-in{position:relative;z-index:1;display:block;max-width:min(100%,600px)}

.ob-vision-rail{position:relative;z-index:1}
.ob-vision-lead{font-family:var(--font-display);font-size:var(--fs-section);font-weight:400;letter-spacing:0;line-height:1.05;color:var(--web-ink);text-wrap:balance;max-width:16em;margin-bottom:clamp(28px,3.4vw,40px)}

/* Four paragraphs, one column. They were four cards once and then one block of
   four sentences; the block was right while the copy had the whole rail, and
   wrong the moment it lost half of it — at this measure four sentences run
   long enough to read as a wall. Split, each beat of the argument gets its own
   line and the build stays legible. Still not cards: no faces, no ground of
   their own, one gap between them and nothing else. The scene is muted enough
   to be read over (see the fragment's ramp and FADE), so they sit on it
   directly, the way the heading above them does.
   The measure is the column now, so the old 34em cap has nothing left to do. */
.ob-vision-copy{display:grid;gap:clamp(14px,1.4vw,20px)}
.ob-vision-para{margin:0;font-family:var(--font-text);font-size:var(--fs-sub-title);line-height:1.55;color:var(--web-ink-70);text-wrap:pretty}

/* ── 07+08 тёмный bookend: закрывающий бит страницы ── */
.ob-bookend{--web-ink:var(--on-dark);--web-ink-90:var(--on-dark);--web-ink-70:var(--on-dark-70);--web-ink-60:var(--on-dark-60);--web-ink-40:var(--on-dark-40);--web-ink-20:var(--web-line-on-dark);--web-ink-10:var(--web-line-on-dark);--web-ink-06:var(--fill-on-dark);--web-line:var(--web-line-on-dark);--web-line-2:var(--web-line-on-dark);--focus-ring:var(--brand-soft);--shadow-1:none;--shadow-2:none;--shadow-3:none;--shadow-4:none;background:var(--night);color:var(--on-dark-70)}
html.night #cta{border-top:1px solid var(--web-line)}
.ob-bookend .ob-sec{padding-block:var(--section-pad)}

/* ── 07 CTA ── полоса ink (слово Роба: перепад «сероватым» от ночного футера),
   генеративка снята по слову Роба 10.08 — состав по референс-скрину:
   центр, две кнопки, email, хейрлайн. */
.ob-bookend--ink{background:var(--web-surface-dark)}
#cta{position:relative;overflow:clip;display:grid;align-items:center;min-height:min(70vh,700px);padding-block:calc(var(--section-pad)*0.5) clamp(180px,24vh,240px)} /* нижняя цифра — клиренс под орб, пересчитан вместе с ним ниже */
/* «планета» (слово Роба): Thinking Sphere оверскейлом, верхушка выглядывает
   из-под футера; грунт сцены = ink полосы (?ground=ink), стыка нет.
   The offset is the whole trick and it was set too shallow. #cta clips at its
   own bottom edge, which is also the footer's top edge and the point where the
   ground steps from --web-surface-dark to --night, so whatever the sphere is doing
   at that line is stated twice — once as a silhouette and once as a colour
   change. At -746 of a 1200 box, 454px showed: the cut landed close to the
   equator, where the chord is at its widest and the profile is still vertical,
   and a sphere cut across its widest point does not read as a sphere behind
   something. It reads as a sliced ball.
   At -820 the cut sits well past the equator: the silhouette's ends are
   visibly turning back in before they reach it, which is the horizon geometry
   the "planet" note was after, and the dome keeps the mass the note wanted.
   Measured against the scene rather than the box — the frame carries padding
   around the sphere, so past about -940 the visible strip is empty scene and
   the planet disappears altogether. The section's lower padding is clearance
   for exactly this and comes down with it. */
.ob-cta-orb{position:absolute;left:50%;bottom:-820px;transform:translateX(-50%);width:1200px;height:1200px;border:0;pointer-events:none;z-index:0;opacity:0;transition:opacity var(--web-dur-slow) var(--ease-ui)}
.ob-cta-orb.is-up{opacity:1}
.ob-cta-in{position:relative;z-index:1;display:grid;justify-items:center;text-align:center}
.ob-cta-copy{display:grid;justify-items:center;gap:22px}
.ob-cta-copy .t-hero{max-width:17em;font-weight:400}
.ob-cta-sub{font-family:var(--font-text);font-size:var(--fs-sub-title);font-weight:400;letter-spacing:.005em;line-height:1.5;color:var(--web-ink-70);max-width:40ch}
.ob-cta-act{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:center;margin-top:14px}
.ob-bookend--ink .btn-dark{background:var(--night);color:var(--on-dark)}
@media (hover:hover) and (pointer:fine){.ob-bookend--ink .btn-dark:hover{background:var(--night-hover)}} /* ховер обязан быть видим (Apple §1): ink-pressed на ink-полосе неотличим */
.ob-bookend .ob-mount{background:transparent}


/* ── 08 footer ── донор 00d на SSOT: три колонки (слоган+CTA / Read / Elsewhere),
   легал-строка без хейрлайна, БОЛЬШОЙ вордмарк понизу. Грунт — не общий
   .ob-bookend, а --night: футер на тон ниже всего остального.
   Верхнего бордера нет — шов несёт сам тон. */
/* The horizon the sphere rises over. 07 and 08 are one band by design but not
   one tone, and the seam between them used to be graded away over the footer's
   first 140px — a ramp from the CTA's ground into the footer's. It read as a
   smear rather than as a horizon: a soft vertical wash sitting behind the
   sphere with no edge for the eye to resolve.

   The step is stated rather than blended, and it is a step DOWN IN THE SAME
   FAMILY: --night is the sand palette with the light taken out, so the footer
   is one tone below the CTA's --web-surface-dark and still the same brown.
   That is the correction here. The ground used to be --web-surface-night, and
   the token pack is plain about what that is — #12161C, "COOL blue-black" —
   which put the one cold plane on the site at the bottom of every page. This
   file had already refused it twice for that exact reason (see the dark topbar
   pane and its phone fallback, both of which reach for --night and say why);
   the footer is the third and last place that was still holding it.

   The horizon survives the change: the CTA above stays --web-surface-dark, so
   there is still a stated step at the seam for the sphere to rise over — one
   the eye reads as depth rather than as a different screen. Still no top
   border; the tone carries the seam on its own. */
.ob-foot{padding-block:var(--section-pad) 0;overflow:clip;background:var(--night)}
.ob-foot-grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(0,1fr) minmax(0,1fr);gap:clamp(36px,4vw,72px);align-items:start}

.ob-foot-slogan{font-family:var(--font-display);font-weight:400;font-size:var(--fs-card-title);line-height:1.2;letter-spacing:0;color:var(--web-ink);max-width:20ch}
.ob-foot-brand .btn{margin-top:34px}
/* ── the address, and the copy button beside it ──────────────────────────────
   A mailto only pays off for a reader whose mail client is wired up in this
   browser. The button carries the same address to the clipboard for everyone
   else (copy-mail-runtime.ts). It is in the flow at rest, not mounted on
   hover: a control that appears would reflow the row under the pointer and
   would be unreachable by keyboard. Only opacity and a small slide move, and
   :focus-within brings it up for the keyboard on the same terms. */
.ob-foot-mailrow{display:flex;align-items:center;gap:4px;width:fit-content;margin-top:10px}
.ob-foot-mail{display:flex;align-items:center;min-height:40px;font-family:var(--font-mono);font-size:var(--fs-body-lead);letter-spacing:0;color:var(--web-ink-60);text-decoration:none;transition:color var(--web-dur-fast) var(--ease-ui)}
.ob-foot-mailrow:hover .ob-foot-mail,.ob-foot-mail:hover{color:var(--web-ink)}
.ob-foot-copy{display:grid;place-items:center;width:32px;height:32px;flex:none;appearance:none;padding:0;border:1px solid transparent;border-radius:var(--web-r-sm);background:transparent;color:var(--web-ink-60);cursor:pointer;opacity:0;transform:translateX(-4px);transition:opacity var(--web-dur-base) var(--ease-ui),transform var(--web-dur-base) var(--ease-ui),color var(--web-dur-fast) var(--ease-ui),border-color var(--web-dur-fast) var(--ease-ui),background var(--web-dur-fast) var(--ease-ui)}
.ob-foot-mailrow:hover .ob-foot-copy,.ob-foot-mailrow:focus-within .ob-foot-copy{opacity:1;transform:none}
.ob-foot-copy:hover{color:var(--web-ink);border-color:var(--web-line);background:var(--fill-on-dark)}
/* Both marks share the one cell, so the swap is a cross-fade in place rather
   than the button changing size under the cursor mid-press. */
.ob-foot-copy-icon,.ob-foot-copy-tick{grid-area:1/1;width:15px;height:15px;transition:opacity var(--web-dur-fast) var(--ease-ui),transform var(--web-dur-fast) var(--ease-ui)}
.ob-foot-copy-tick{opacity:0;transform:scale(0.6);color:var(--brand-soft)}
.ob-foot-copy[data-state="done"]{color:var(--brand-soft);opacity:1;transform:none}
.ob-foot-copy[data-state="done"] .ob-foot-copy-icon{opacity:0;transform:scale(0.6)}
.ob-foot-copy[data-state="done"] .ob-foot-copy-tick{opacity:1;transform:none}
/* A refused write says so by staying put and dimming, not by flashing a tick
   it did not earn. The address is still on screen to select by hand. */
.ob-foot-copy[data-state="failed"]{opacity:1;transform:none;color:var(--web-ink-40)}

/* Sections in the footer, and their links.
   The heads step down from --web-ink-60 to --web-ink-40: "Read" and "Elsewhere"
   name the columns, they are not part of the reading, and at the links' own
   tone they competed with the four anchors underneath them for the same eye.
   The rows tighten with them — the links carried a 44px min-height, which is a
   thumb target rather than a line, and it set the rhythm of a list that is four
   short labels. 34px keeps them comfortably apart and past the 24px WCAG 2.2
   minimum, and takes ~40px out of the column. One height at every pointer, the
   way .btn is stated above. */
.ob-foot-col{display:flex;flex-direction:column;align-items:flex-start;margin-top:-6px}
.ob-foot-head{margin-bottom:10px;font-family:var(--font-mono);font-size:var(--fs-caption);letter-spacing:0;color:var(--web-ink-40)}
/* --fs-caption, the same rung .ob-navlink sits on: these are the same links as
   the header's, and at --fs-body they were reading as body copy — the loudest
   thing in the footer after the slogan. min-height sets the rhythm here, not
   the type size; see the note on .ob-foot-col above for why it came down. */
.ob-foot-link{position:relative;display:inline-flex;align-items:center;min-height:34px;min-width:44px;font-family:var(--font-text);font-size:var(--fs-caption);font-weight:400;letter-spacing:0;line-height:1.4;color:var(--web-ink-70);text-decoration:none;transition:color var(--web-dur-fast) var(--ease-ui),padding-left var(--web-dur-base) var(--ease-ui)}
.ob-foot-link::before{content:"";position:absolute;left:0;top:50%;width:0;height:1px;background:var(--brand);transition:width var(--web-dur-base) var(--ease-ui)}
.ob-foot-link:hover{color:var(--web-ink);padding-left:20px}
.ob-foot-link:hover::before{width:13px}
/* No hairline over the legal line. The rule the donor drew there was fencing
   two pieces of the same quiet grey off from each other, and the margin above
   already says everything the border was saying. */
.ob-foot-base{margin-top:clamp(56px,7vw,108px);padding-top:26px;display:flex;flex-wrap:wrap;gap:10px 28px;align-items:center;justify-content:space-between}
.ob-foot-legal{font-family:var(--font-mono);font-size:var(--fs-caption);letter-spacing:0;color:var(--web-ink-60)}
/* The wordmark is a watermark, not a graphic: it should read as the ground
   remembering its own name, one step up from the footer rather than a bronze
   band laid across it. The asset is flat #A5876B, so opacity is the only lever
   without re-cutting the SVG as a mask — and .13 of it over --night lands at
   about #302A22, which is --night-field's step (+8%) to within a point per
   channel. So the mark sits exactly one rung above the ground it is drawn on,
   on the same ladder the rest of the dark surfaces use.
   --rise-to carries that value into the entrance; see @keyframes ob-rise. */
.ob-foot-mark{display:block;width:100%;height:auto;max-width:none;margin-top:clamp(34px,4.5vw,72px);--rise-to:.13;opacity:var(--rise-to)}

/* ── mobile section drawer (spec 019 task-10) ───────────────────────────────
   Below the mobile cutover the header hid .ob-nav and its CTA with nothing in
   their place, so the four section anchors were reachable only by scrolling the
   whole page. The trigger and panel are rendered at every width and hidden here
   above 1200px, so the runtime needs no width branch — if the trigger cannot be
   clicked, none of its behaviour runs. */
.ob-burger {
  display: none;
  appearance: none;
  border: 1px solid var(--web-line-2);
  background: transparent;
  border-radius: var(--web-r-pill);
  /* 44px square: the floor for a tap target, and the reason this is sized in
     absolutes rather than padding around a glyph. */
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--web-ink);
  transition: background var(--web-dur-fast) var(--ease-ui);
}
.ob-burger-bars {
  display: grid;
  gap: 5px;
  width: 18px;
}
.ob-burger-bars i {
  display: block;
  height: 1.5px;
  border-radius: 2px;
  background: currentColor;
}
@media (hover: hover) and (pointer: fine) {
  .ob-burger:hover {
    background: var(--web-ink-06);
  }
}
.ob-drawer {
  display: grid;
  gap: 8px;
  align-content: start;
  /* Inset to match the bar it hangs from, and rounded on the same radius, so
     the two read as one floating object rather than a panel behind a pill. */
  margin: 8px var(--web-page-pad) 0;
  padding: 12px 20px 20px;
  border: 1px solid color-mix(in srgb, var(--web-ink) 10%, transparent);
  border-top-color: color-mix(in srgb, var(--web-glass) 55%, transparent);
  border-radius: var(--web-r-lg);
  background: color-mix(in srgb, var(--web-glass) 82%, transparent);
  -webkit-backdrop-filter: blur(28px) saturate(180%) brightness(1.06);
  backdrop-filter: blur(28px) saturate(180%) brightness(1.06);
}
.ob-drawer[hidden] {
  display: none;
}
.ob-drawer-nav {
  display: grid;
}
.ob-drawer-link {
  display: flex;
  align-items: center;
  min-height: 52px;
  font-family: var(--font-text);
  font-size: var(--fs-body-lead);
  color: var(--web-ink);
  text-decoration: none;
  border-bottom: 1px solid var(--web-line);
}
/* Mono, quiet, and its own tap row: the address is reference material next to
   the CTA above it, not a second thing to press. Same face and tone the footer
   gives it, so the two read as one address rather than two links. */
.ob-drawer-mail {
  display: flex;
  align-items: center;
  min-height: 44px;
  margin-top: 12px;
  font-family: var(--font-mono);
  font-size: var(--fs-body);
  letter-spacing: 0;
  color: var(--web-ink-60);
  text-decoration: none;
}
.ob-drawer-mail:hover {
  color: var(--web-ink);
}
.ob-drawer-cta {
  margin-top: 16px;
  justify-content: center;
  /* The shared .btn resolves to 43.6px here, which misses the 44px tap floor by
     a rounding error. Stated rather than inherited so it cannot drift back. */
  min-height: 48px;
}
/* The cutover is the tablet step, not the mobile one. .ob-nav is a no-wrap
   flex row sharing .ob-top-in with the wordmark, the language control and the
   CTA; from 1199 down the four anchors ran out of room and, being flex items
   that shrink before the row overflows, broke their own labels onto second
   lines — a nav that read as two rows of text rather than as a layout that had
   given up. The whole 900-1199 band takes the trigger and the drawer instead,
   which is the same set of anchors on a screen with room for them. */
@media (max-width: 1199px) {
  .ob-burger {
    display: inline-flex;
    /* No resting outline. At this width the trigger is the only control in the
       row, so nothing needs a border to separate it from a neighbour, and a
       44px pill drawn around a 18px glyph was the loudest thing in the bar.
       :focus-visible still rings it — that outline is not this one. */
    border: 0;
  }
  .ob-nav {
    display: none;
  }
  /* Same reasoning for the language control: the topbar copy goes, the drawer
     copy takes over, and the band is spared two globes on screen at once.
     Scoped to the row so the one inside the drawer survives. */
  .ob-top-act > .ob-lang {
    display: none;
  }
  /* Hard right. .ob-nav carried the margin that pushed this row over and it is
     display:none here, so the actions sat against the wordmark instead of the
     far edge. Stated on the row that survives the cutover. */
  .ob-top-act {
    margin-inline-start: auto;
  }

  /* ── the drawer is a full screen, not a panel ────────────────────────────
     A hanging panel has to be shorter than the page and wider than its
     contents at the same time, which is what left the menu reading as a card
     dropped over the hero. Fixed to the viewport it is simply the screen, and
     `position:fixed` resolves against the viewport rather than .ob-top, so the
     bar's drop and the gutter above do not inset it.
     The bar stays on top of it: both are children of .ob-top and the drawer is
     the later sibling, so it would otherwise paint over the logo and over the
     trigger that closes it. Body scroll is already locked while it is open —
     see setOpen in drawer-runtime.ts. */
  .ob-top-in {
    position: relative;
    z-index: 1;
  }
  .ob-drawer {
    position: fixed;
    inset: 0;
    z-index: 0;
    margin: 0;
    border: 0;
    border-radius: 0;
    /* Clears the bar — its drop plus its 64px min-height — with 24 of air, and
       starts its contents on the bar's own inline inset so the menu items sit
       under the wordmark rather than on an indent of their own. Both figures
       are read off .ob-top rather than written out, because the bar has two
       geometries below the cutover now: the tablet keeps the page rail, the
       phone step tightens it. */
    padding: calc(var(--top-off) + 88px) var(--top-inset) 40px;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Links, then the CTA, then whatever is left — which is what puts the
       language control on the floor of the screen rather than under the button.
       `start` and not `left`: in Arabic the whole drawer mirrors and the
       control belongs on the other edge without a second rule. */
    grid-template-rows: auto auto 1fr;
    align-content: stretch;
    gap: 0;
    /* The menu is always a dark screen, whatever band it happens to open over.
       Following the band was right for the bar, which is a thin pane the page
       shows through, and wrong for this, which replaces the page: the same
       control flipped between two themes depending on scroll position, and the
       language popup inside it was still being drawn light on dark.
       --card comes along because .ob-lang-menu is built on it. */
    --web-ink: var(--on-dark);
    --web-ink-90: var(--on-dark);
    --web-ink-70: var(--on-dark-70);
    --web-ink-60: var(--on-dark-60);
    --web-ink-40: var(--on-dark-40);
    --web-ink-20: var(--web-line-on-dark);
    --web-ink-10: var(--web-line-on-dark);
    --web-ink-06: var(--fill-on-dark);
    --web-line: var(--web-line-on-dark);
    --web-line-2: var(--web-line-on-dark);
    --card: var(--night-raised);
    --card-2: var(--night-field);
    --focus-ring: var(--brand-soft);
    /* --night, not --web-surface-night. The latter is #12161C, which carries a
       blue cast that has nothing to do with this palette — against the warm
       sand page and the bronze mark it read as a cold screen dropped on a warm
       site. --night is the same charcoal the footer and the night bands
       already stand on. */
    background: color-mix(in srgb, var(--night) 95%, transparent);
  }
  /* Restated at (0,2,0) so it beats the band-following rule further up, which
     is the same selector shape and would otherwise win on source order and
     hold the screen at the pane's 82%. */
  .ob-top--dark .ob-drawer {
    background: color-mix(in srgb, var(--night) 95%, transparent);
  }
  @supports not (backdrop-filter: blur(1px)) {
    .ob-drawer,
    .ob-top--dark .ob-drawer {
      background: var(--night);
    }
  }
  /* Menu items carry the screen now that the screen is the whole viewport —
     at body-lead they were the size of a caption on a page with nothing else
     on it. */
  .ob-drawer-link {
    min-height: 64px;
    font-size: var(--fs-card-title);
    color: var(--on-dark);
  }
  .ob-drawer-cta {
    font-size: var(--fs-body-lead);
    min-height: 56px;
    margin-top: 28px;
  }

  /* ── language: a globe and a word, not a field ───────────────────────────
     The bordered pill read as an input — something to type into — which is
     exactly what a two-item switch is not. Inside the drawer it loses the box
     and keeps the globe, the current language and the chevron. */
  .ob-drawer .ob-lang {
    align-self: end;
    justify-self: start;
    display: inline-flex;
    margin-top: 40px;
  }
  .ob-drawer .ob-lang-trigger {
    width: auto;
    justify-content: flex-start;
    border: 0;
    background: transparent;
    padding: 0;
    gap: 10px;
    min-height: 44px;
    font-size: var(--fs-body-lead);
    color: var(--on-dark);
  }
  /* Opens upward: the trigger sits on the floor of the screen, so a menu
     hanging below it would open off the bottom edge. */
  .ob-drawer .ob-lang-menu {
    position: absolute;
    inset-block: auto calc(100% + 10px);
    inset-inline: 0 auto;
    min-width: 200px;
    margin: 0;
    box-shadow: none;
  }
}
/* The globe is the affordance once the box is gone, so it is markup, not a
   background image — it has to sit in the button's flow next to the label. */
.ob-lang-globe {
  width: 18px;
  height: 18px;
  flex: none;
}

/* ── poster tier (spec 019 task-5, phone half reversed) ─────────────────────
   Reduced-motion visitors never fetch a fragment document: an ambient
   animation is precisely what the motion preference is about. Phones used to
   be in here too, on the payload argument, which cost a phone the page's whole
   ambient register — six frozen pictures of scenes that move everywhere else.
   They mount live now (device-tier.ts, readDeviceTier), so this block is keyed
   on the preference alone and the width step below only picks which cut of
   each still a reduced-motion phone gets. The stills stand in: 159 KB of AVIF
   for all five scenes.

   The backgrounds are painted on the iframe elements themselves rather than on
   their containers. An iframe with no src is an empty replaced element, so its
   CSS background renders, and it already carries the exact geometry the scene
   would occupy — no second set of positioning rules to keep in step, and
   nothing to unwind when a tier does mount, because the rule simply does not
   apply there. Two declarations per element: the plain url() is the fallback
   for browsers without image-set(), which then take the AVIF.
   Regenerate with apps/web/scripts/capture-fragment-posters.ts. */
@media (prefers-reduced-motion: reduce) {
  /* Every decorative frame starts at opacity 0 and is revealed by .is-up when
     its document loads — which never happens here. All four need opting in, not
     just the two full-bleed backgrounds: the CTA orb and the market frame carry
     the same reveal, and missing them leaves those sections looking empty. */
  .ob-hero-bg,
  .ob-vision-bg,
  .ob-market-play-frame,
  .ob-cta-orb {
    opacity: 1;
  }
  .ob-hero-bg {
    background-image: url('posters/01-hero-sphere.webp');
    background-image: image-set(
      url('posters/01-hero-sphere.avif') type('image/avif'),
      url('posters/01-hero-sphere.webp') type('image/webp')
    );
    background-size: cover;
    background-position: center;
  }
  .ob-vision-bg {
    background-image: url('posters/06-mesh-field.webp');
    background-image: image-set(
      url('posters/06-mesh-field.avif') type('image/avif'),
      url('posters/06-mesh-field.webp') type('image/webp')
    );
    background-size: cover;
    background-position: center;
  }
  .ob-mount--stack > iframe {
    background-image: url('posters/03-video-stack.webp');
    background-image: image-set(
      url('posters/03-video-stack.avif') type('image/avif'),
      url('posters/03-video-stack.webp') type('image/webp')
    );
  }
  .ob-mount--lines > iframe {
    background-image: url('posters/04-lines-core.webp');
    background-image: image-set(
      url('posters/04-lines-core.avif') type('image/avif'),
      url('posters/04-lines-core.webp') type('image/webp')
    );
  }

  /* Portrait variants, captured at each mount's real dimensions on a 390px
     viewport. The wide captures above still serve reduced-motion visitors on a
     desktop, where they are the correct shape. */
  @media (max-width: 599px) {
    .ob-hero-bg {
      background-image: url('posters/01-hero-sphere.phone.webp');
      background-image: image-set(
        url('posters/01-hero-sphere.phone.avif') type('image/avif'),
        url('posters/01-hero-sphere.phone.webp') type('image/webp')
      );
    }
    .ob-vision-bg {
      background-image: url('posters/06-mesh-field.phone.webp');
      background-image: image-set(
        url('posters/06-mesh-field.phone.avif') type('image/avif'),
        url('posters/06-mesh-field.phone.webp') type('image/webp')
      );
    }
    /* 03 needs no portrait variant: the stack mount is square at every width,
       so the one capture is already the right shape and `contain` is a no-op —
       the same reason 07-orb-peek ships a single still. */
  }
  /* 04's portrait variant is the phone's alone. That is where its mount turns
     to 1:2 and the scene answers with the crown; every width above it — the
     collapsed band included — draws the fan and takes the wide still. */
  @media (max-width: 599px) {
    .ob-mount--lines > iframe {
      background-image: url('posters/04-lines-core.phone.webp');
      background-image: image-set(
        url('posters/04-lines-core.phone.avif') type('image/avif'),
        url('posters/04-lines-core.phone.webp') type('image/webp')
      );
    }
  }
  .ob-market-play-frame,
  .ob-cta-orb {
    background-image: url('posters/07-orb-peek.webp');
    background-image: image-set(
      url('posters/07-orb-peek.avif') type('image/avif'),
      url('posters/07-orb-peek.webp') type('image/webp')
    );
  }
  .ob-mount--stack > iframe,
  .ob-mount--lines > iframe,
  .ob-market-play-frame,
  .ob-cta-orb {
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
  }
}

/* ── hero: the sphere and the copy get separate bands below 900 ─────────────
   Centred copy over a centred sphere put three lines of cream type across the
   one bright object on the page — measured on a 390 canvas the sphere spanned
   y 236-515 and the copy block 300-555. The headline lost its ground on the
   first screen of the site.
   The frame cannot be panned the way a background image can, so it is given a
   band rather than the whole section: sized to the element, the scene re-lays
   out and centres its sphere inside that band, and the copy sits underneath it
   on the section's own ink. One composition for the entire single-column band,
   whether the frame is carrying a live scene or a still — which is why the
   phone no longer has a framing of its own here.
   Stated as max-width 899 rather than a min-width 600 pair: the ladder is
   max-width only by contract, and responsive.spec.ts enumerates the exact
   breakpoints this file is allowed to declare. */
@media (max-width: 899px) {
  .ob-hero {
    align-items: end;
    /* The top padding this composition needs — 0, so the 56% band below can sit
       against the padding box edge — is declared with the rest of the 899 step
       further down the file. The 1199 block restates padding-block for the
       tablet, both blocks match a phone, and both selectors are (0,1,0); source
       order is the only tiebreak and this one loses it from up here. */
  }
  .ob-hero-bg {
    /* Lifted, not shrunk. The band is what sizes the sphere — the scene centres
       it and scales it to the box — so buying clearance by making the band
       shorter costs the object the hero is built around. The height is left
       alone and the whole band is pulled up instead: same sphere, 6% of the
       hero higher, and 6% of the hero more air between its foot and the
       headline.
       The copy is bottom-aligned in a section that is only max(520px, 86svh)
       tall, so on a short phone the two were arriving at the same band of
       pixels: measured against a 690px hero the sphere's foot landed at 327
       and its glow carried past 360, with the headline starting around 373.
       6% is what the top edge can give — at that floor the sphere's crown sits
       about 14px inside the frame, so anything more clips it. The mask travels
       with the band, so the dissolve still ends where the scene does.
       Not on the composer: `.ob-hero[data-hero-mode="composer"] .ob-hero-bg`
       is (0,2,0) and restates inset:0 with height:100%, so it outranks this
       wherever it applies. */
    inset: -6% 0 auto 0;
    height: 56%;
    /* The scene paints its own ground, which is lighter than the section's
       ink, so ending the band at a hard edge drew a horizontal seam across the
       hero. Dissolving the last quarter of the frame lets the scene settle
       into the section instead of stopping against it. */
    -webkit-mask-image: linear-gradient(to bottom, #000 0 74%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0 74%, transparent 100%);
  }
}

/* ── breakpoint ladder (spec 019 task-3) ────────────────────────────────────
   Three steps, and only three: 600 / 900 / 1200.
     <600   phone      — tracking corrections (top of file)
     <900   mobile     — spacing rail steps down, nav and chart swap out
     <1200  tablet     — multi-column layouts collapse to one column
     >=1200 desktop    — full composition
   Not in this ladder, by measurement rather than assumption:
   - 860px lives in the SSOT tokens file and stays there for app/console. It
     targets .topbar / .nav / .footer / .section-head-row, none of which exist
     on this page, so only its :root line can reach the landing — and the 900
     block below shadows that. Effective here: nothing.
   - 1099px is deleted. It released the CTA min-height 100px before the layout
     collapse, for no reason anyone recorded; the rule now rides the 1200 step
     with the rest of the collapse. */
@media (max-width:1199px){
  /* Collapsed: copy, then the scene under it at the full rail. The scene's
     box is not restated here — the base rule already gives it the rail and
     16/9, and the phone step below 600 is the only place it turns. The gap is
     vertical rhythm now, not a column gutter, so it steps down with the band. */
  .ob-role-split{grid-template-columns:minmax(0,1fr);gap:clamp(32px,4vw,48px)}
  /* Tablet and phone: the square sticks below the bar and the list reads under
     it. Flex, not a one-column grid: a grid item's containing block is its own
     row, so a sticky one has nowhere to travel and silently does not stick. As
     a flex item it is positioned against the whole split, which is as tall as
     the list. Capped at 62svh so a piece of the list stays visible below it. */
  .ob-split--stack{display:flex;flex-direction:column}
  /* Scoped to .ob-pin to outrank the `.ob-pin .ob-sticky{position:static}` that
     centres the scene inside the desktop pin — a bare .ob-sticky loses to it. */
  .ob-pin .ob-sticky{position:sticky;top:calc(var(--top-h) + 8px);order:-1;z-index:2;align-content:start;padding-block:12px;background:var(--bg)}
  /* The panel has to be opaque, or the list slides visibly through the scene
     that is pinned over it. `background:inherit` cannot do it: .ob-split has no
     background of its own, so it inherits transparency. It is the band's own
     ground, so the scene's feathered edges still meet it invisibly. */
  .ob-nightband .ob-sticky{background:var(--night)}
  .ob-mount--stack{width:min(100%,62svh);max-width:840px;margin-inline:auto}
  .ob-hero-in{grid-template-columns:minmax(0,1fr)}
  .ob-hero-reserve{display:none}
  /* 06 gives its column back. The half-rail split above 1200 buys the
     mesh an open side; at this width there is no second side to give it,
     and a 600px column inside a 900px rail is just a short measure with
     dead space beside it. */
  .ob-vision-in{max-width:min(100%,1080px)}
  .ob-segments{grid-template-columns:repeat(2,minmax(0,1fr))}
  #cta{min-height:0;padding-block:var(--section-pad)}
  /* Restored below the tablet band, taller than the content needs. The orb is
     anchored to this section's bottom edge, so height added here is air above
     the copy rather than more sphere — which is the point: at content height
     the closing beat of the page was a 371px strip, shorter than any section
     above it and too tight for the planet to read as a horizon. */
  /* Tablet band (spec 019 task-13). Every rule above turns a two-column grid
     into one column, which silently repurposes each `gap` from a column gutter
     into a vertical rhythm — and 64px of gutter is not 64px of rhythm. Measured
     across 390-1440 these two never moved at all, so the band inherited a
     desktop gutter as its stacking gap. The hero's padding was fixed at every
     width for the same reason: it was authored against a two-column hero that
     stops existing here. */
  .ob-split--stack{gap:40px}
  .ob-market-split{gap:40px}
  .ob-hero{padding-block:96px}
  /* The orb is sized to peek in from below the copy, and that only works if it
     is sized against the section it sits in. #cta above drops from a 700px
     min-height to content height here, but the orb kept its desktop 1200px box
     and -746px offset, so its apex landed above the buttons: measured at 1000px
     wide, apex ~176px against a copy block ending at ~318px — the CTA buttons
     and sub-line rendered on the lit face of the sphere. The mobile box already
     solves this for a collapsed section; the tablet band takes the same one.
     Measured after: apex ~360px, clear of the copy at every width in the band. */
  .ob-cta-orb{width:760px;height:760px;bottom:-560px}
}
/* The header CTA is now in the bar at every width, phones included. It used to
   stop at the phone step: the button at desktop size left 320 about 34px short
   of holding the wordmark, the button and the trigger together, and 320 is in
   the overflow contract, so the phone sent readers to the drawer instead.
   Shrinking the button rather than dropping it is what buys those pixels back —
   the conversion route should not depend on opening a drawer first. */
@media (max-width:599px){
  /* The bar carries the CTA at every width now, at a smaller size than the
     desktop one — which is what makes it fit where it did not before. Padding
     and label each drop a step; the row is wordmark + button + trigger, and
     320 is the width that has to hold them.
     Still scoped to the topbar row: .ob-drawer lives inside .ob-top, and its
     own CTA keeps the full size it has room for. */
  .ob-top-act{gap:8px}
  .ob-top-act > .btn{padding:7px 11px;font-size:12.5px}
  /* The chevron is an affordance for a row that has space for one. At 320 the
     bar needs 283px of content in 268px, and the glyph plus its gap is most of
     that difference — the label alone still reads as the button it is. */
  .ob-top-act > .btn::after{content:none}
}
@media (max-width:899px){
  :root{--section-pad:80px;--web-page-pad:20px}
  /* Tighter to the edges: the bar is a phone-width object here, and 20 + 20 of
     gutter plus 12 of drop was reading as a card floating in a margin rather
     than as a header. The drawer above is measured off these two, so it
     follows without a second rule. */
  .ob-top{--top-off:8px;--top-inset:26px;padding-inline:12px}
  .ob-top-in{gap:12px;padding-inline:14px}
  /* The three rules that used to sit here — full-width trigger, static menu in
     flow — described the hanging panel, which was only ever as tall as its own
     contents. The drawer is the whole screen now and the control lives on its
     floor, so it is stated with the rest of the full-screen drawer above. Left
     here it would still win: this block is (0,2,0) like that one and comes
     later in the file, so at any phone width it would put the field back. */
  #cta{min-height:min(72svh,620px)}
  /* Матрица 02 распрямляется в один столбец, и высота сцены отпускается:
     фикс в 268px держал линейку на одном уровне поперёк решётки, а в одну
     колонку держать её не поперёк чего. */
  .ob-pm{grid-template-columns:minmax(0,1fr);gap:16px;margin-top:48px}
  .ob-pm-stage{height:auto;min-height:236px;padding:20px;background-size:cover}
  html.night .ob-pm-stage{background-size:auto, cover}
  /* Тег стороны не встаёт рядом с именем шага на 390: уводим его на свою
     строку под именем, с отступом по узлу рельса. */
  .ob-pm-step{flex-wrap:wrap}
  .ob-pm-step .ob-pm-tag{flex:1 0 100%;text-align:start;padding-inline-start:19px}
  /* Ответ Orbox — не реплика в переписке, ему незачем держать её отступ. */
  .ob-pm-bubble{max-width:100%}
  .ob-pm-status-row{flex-wrap:wrap}
  .ob-pm-status-state{flex:1 0 100%;text-align:start;padding-inline-start:24px}
  /* Same ratio on a phone, against the phone's own floor. */
  .ob-hero{min-height:max(520px,86svh)}
  /* Carries the separate-bands composition stated at the 899 step above. It has
     to be restated here, after the 1199 block, because that block sets
     padding-block:96px for the tablet and reaches every phone width too. */
  .ob-hero{padding-block:0 var(--section-pad)}
  /* The stack scene keeps its square at every width — its edge mask is a unit
     square stretched to the canvas, so letting the box off the ratio warps the
     feather. Only the cap moves; the 1199 block above sets it. */
  .ob-segments{grid-template-columns:minmax(0,1fr)}
  .ob-foot-grid{grid-template-columns:minmax(0,1fr)}
  .ob-vision{min-height:560px}
}

/* ── 03 on a phone: a rail of cards, not a column ──────────────────────────
   The vertical list is a desktop idea. It needs a tall column beside a scene,
   and a phone has neither the width to put them side by side nor the height to
   read seven chips under a square. So below the tablet step the section holds
   still and the layers travel: the stage pins, the scene keeps its place and
   changes layer under its own scrubber as before, and the copy becomes a single
   card that slides in from the trailing edge each time the layer turns.
   Every card is open here. The pill state exists to say "there is more under
   this one", and on a rail showing exactly one card at a time there is no more
   to promise — so the +/x goes with it.

   The travel is one line of arithmetic: the runtime already writes data-active
   on exactly one item, so :has() reads which one that is into --i and the whole
   track shifts by that many card widths. No JavaScript is added and nothing in
   progress-runtime.ts changes — mountPosition() takes its reading from the .ob-pin
   section's own scroll span, which exists again here because the pin is back. */
@media (max-width:899px){
  /* The pin returns. It was collapsed for every width under 1200 because the
     collapsed layout had nothing to pin; this layout is the opposite — it is
     the standing-still that makes the rail readable. Shorter runway than the
     desktop's 520vh: a phone scrolls in bigger gulps and seven layers over five
     screens is a long hold on a small device. */
  .ob-pin{height:400svh;padding-block:0}
  /* The head's own air. Above the tablet step this comes from .ob-pin-head's
     own rule and below it from the section's padding — and the phone had
     neither once the pin came back and took that padding to zero, so the title
     sat flush on the band's top edge. */
  .ob-pin-head{padding-block:calc(var(--top-h,73px) + 40px) 0}
  .ob-pin-stage{position:sticky;top:0;min-height:100svh;overflow:clip;padding-block:calc(var(--top-h,73px) + 12px) 24px}
  /* The stage is the sticky one now, so the scene inside it goes back to being
     a plain block — a second sticky inside a pinned stage does nothing, and the
     opaque plate it needed to hide a list sliding under it is not wanted when
     nothing slides under it any more. */
  .ob-pin .ob-sticky{position:static;order:-1;z-index:auto;padding-block:0;background:none;justify-items:center;min-width:0}
  .ob-nightband .ob-sticky{background:none}
  /* minmax(0,1fr), not the default auto track. An auto track sizes to the
     max-content of what is in it, and what is in it is a square scene measured
     in svh — on a tall phone that square is wider than the phone, so the track
     grew to fit it and took the rail, the head and the card rail out to 423px
     with it. Every card then stepped by a width the viewport did not have and
     the rail sat permanently off to one side. The track is capped to the stage
     now and the scene is capped to the smaller of its own height budget and the
     rail, so one card is exactly one screen and translateX(-100%) is exactly
     one step. */
  .ob-pin-stage{grid-template-columns:minmax(0,1fr)}
  .ob-mount--stack{width:min(100%,46svh);max-width:none}
  /* No clip here. The rail is 20px of gutter either side, and clipping the cards
     to it meant a card arrived already at its resting place — the reader never
     saw it travel, and never saw the two either side of it. The cards run the
     full width of the screen instead and the pinned stage does the clipping, so
     the one being read is centred with its neighbours showing at both edges and
     the rail is something the cards pass through rather than live in. */
  .ob-split--stack{overflow:visible;min-width:0;gap:clamp(16px,4svh,32px);justify-content:center}
  /* Out of the rail and across the screen. --card is what one card occupies,
     --step is card plus gutter, and the leading offset centres the active one —
     which puts exactly (100 - --card)/2 minus the gutter of each neighbour on
     screen at either side. width:100vw with a negative half-difference margin is
     the break-out; align-items:start on .ob-split would otherwise leave the
     track sized by its own content, so the width is stated. */
  .ob-stack{--card:82vw;--step:86vw;width:100vw;margin-inline:calc(50% - 50vw);flex-direction:row;flex-wrap:nowrap;align-items:start;gap:4vw;min-width:0;transform:translateX(calc(9vw - var(--i,0) * var(--step)));transition:transform var(--web-dur-slow) var(--ease-ui)}
  .ob-stack-item{flex:0 0 var(--card);width:var(--card)}
  /* The cards to either side are context, not competition. */
  .ob-stack-item:not([data-active=true]) .ob-stack-plate{opacity:.45;transition:opacity var(--web-dur-slow) var(--ease-ui)}
  /* Open, always: head and copy are one plate with one radius, and the head
     stops being a control. */
  .ob-stack .ob-stack-plate{width:100%;padding-block:16px 18px;padding-inline:20px;border-radius:var(--web-r-lg);background:color-mix(in srgb,var(--night) 82%,var(--on-dark) 10%);transition:none}
  .ob-stack .ob-stack-copy{height:auto;margin-top:10px;transition:none}
  .ob-stack .ob-stack-body{width:calc(var(--card) - 40px);opacity:1;transition:none}
  .ob-stack-plus{display:none}
  /* Physical translate, so the sign is stated rather than inherited: under RTL
     the first card sits at the right and the rail runs the other way. */
  html[dir="rtl"] .ob-stack{transform:translateX(calc(var(--i,0) * var(--step) - 9vw))}
  .ob-stack:has(> :nth-child(1)[data-active="true"]){--i:0}
  .ob-stack:has(> :nth-child(2)[data-active="true"]){--i:1}
  .ob-stack:has(> :nth-child(3)[data-active="true"]){--i:2}
  .ob-stack:has(> :nth-child(4)[data-active="true"]){--i:3}
  .ob-stack:has(> :nth-child(5)[data-active="true"]){--i:4}
  .ob-stack:has(> :nth-child(6)[data-active="true"]){--i:5}
  .ob-stack:has(> :nth-child(7)[data-active="true"]){--i:6}
}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .ob-fb-skel i,.ob-step,.ob-stack,.ob-stack-item,.ob-stack-plate,.ob-stack-copy,.ob-stack-body,.ob-top,.ob-hero-bg,.ob-vision-bg{animation:none;transition:none}
  .ob-pm-card,.ob-pm-fan i,.ob-pm-track i,.ob-pm-status-row{animation:none;transition:none}
   .ob-pm-check[data-state=done] .ob-pm-track i{width:100%}
  .ob-pm-mark{animation:none}
  /* No scene to scrub, so the pin has nothing to spend its runway on. Left
     pinned, 03 is 520vh of scrolling past one frozen still. Collapsing it here
     is safe for the progress runtime: with no pin span it falls through to
     reading position, which is the right measure for a static section. */
  .ob-pin{height:auto;padding-block:var(--section-pad)}
  /* overflow:clip has to go with the pin: it keeps the pinned stage from
     spilling, but a clipping ancestor is also the scrollport a sticky
     descendant is measured against, and that one never scrolls — leaving it
     here stops the scene sticking without showing why. */
  .ob-pin-stage{position:static;min-height:0;padding-block:0;overflow:visible}
}

/* ── арабское превью (слово Роба 10.08: страница целиком; строки — ryba до
   утверждения клиентом). Связное письмо не терпит разрядки: любой letter-spacing
   рвёт соединения — трекинг в ноль всей странице. Дисплеи получают запас
   интерлиньяжа под выносы и диакритики. RTL-деталь: активная риска пина 03
   переезжает на правую кромку пункта вместе с чтением справа налево. ── */
/* Whole Arabic page runs on IBM Plex Sans Arabic itself, not on a fallback:
   Inter Tight and IBM Plex Sans carry no Arabic glyphs, so the default stacks
   only reached Plex Arabic per-glyph and left Latin fragments — Orbox, digits,
   % — in the Latin cut mid-heading. Arabic family first across display/text/
   mono keeps one family everywhere; weights stay at the imported 400/500. */
html[lang="ar"]{--font-text:"IBM Plex Sans Arabic","IBM Plex Sans",system-ui,sans-serif;--font-display:"IBM Plex Sans Arabic","IBM Plex Sans",system-ui,sans-serif;--font-mono:"IBM Plex Sans Arabic","IBM Plex Sans",system-ui,sans-serif}
html[lang="ar"] *{letter-spacing:0}
html[lang="ar"] .t-hero{line-height:1.22}
html[lang="ar"] :is(.t-section,.ob-vision-lead,.ob-foot-slogan,.ob-stack-title){line-height:1.3}

/* Русский герой набирается по своей шкале. Заголовок сложен из трёх
   <span>-строк и обязан читаться как три, а кириллика этой копии длиннее и
   латиницы, и вязи: "Единая система" занимает 7.23px ширины на каждый
   пиксель кегля, вдвое больше "Every deal". Ломается это с двух концов:
   от 1200px .ob-hero-in встаёт в две колонки и текстовая жёстко упирается
   в 520px, куда влезает кегль не крупнее 71.9px — а базовый clamp тянется
   к 78px; на 320px колонка одна и узкая (280px, предел 38.7px), а нижняя
   граница базового clamp стоит на 44px. Между этими краями места вдвое
   больше нужного, поэтому min() трогает только края: потолок 67px снимает
   верхний, вторая ветвь — нижний, и обе отпускают базовую шкалу там, где
   строка и так помещается. Запас всюду около 7%.
   Правится кеглем, а не колонкой: справа от копии стоит квадратная сцена
   520px, и раздать ширину тексту можно только отняв её у сцены.
   До правки копии эта же строка ломалась от 1600px на "Каждая сделка" —
   лечится тем же правилом. */
html[lang="ru"] .ob-hero .t-hero{font-size:min(clamp(44px,2.2rem + 2.6vw,67px),calc(12.8vw - 5px))}
/* Матрица 02 набрана логическими свойствами и зеркалится сама. Физических
   осталось две, и обе — глифы, а не раскладка: бронзовое всхождение градиента
   считается в процентах от бокса, поэтому по x его надо развернуть вручную,
   а стрелка отправки — направленный знак. */
html[dir="rtl"] .ob-pm-card[data-card="0"] .ob-pm-stage{--pm-pos:100% 0%}
html[dir="rtl"] .ob-pm-card[data-card="1"] .ob-pm-stage{--pm-pos:0% 0%}
html[dir="rtl"] .ob-pm-card[data-card="2"] .ob-pm-stage{--pm-pos:100% 100%}
html[dir="rtl"] .ob-pm-card[data-card="3"] .ob-pm-stage{--pm-pos:0% 100%}

/* React keeps inactive locale spans hidden from both layout and accessibility. */
.ob-showcase [data-locale="en"]{display:contents}
.ob-showcase :is([data-locale="ar"],[data-locale="ru"]){display:none}
html[lang="ar"] .ob-showcase :is([data-locale="en"],[data-locale="ru"]){display:none}
html[lang="ar"] .ob-showcase [data-locale="ar"]{display:contents}
html[lang="ru"] .ob-showcase :is([data-locale="en"],[data-locale="ar"]){display:none}
html[lang="ru"] .ob-showcase [data-locale="ru"]{display:contents}

/* Presentation-only accessibility helpers for localized inline and landmark text. */
.ob-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
.ob-showcase [data-locale="en"].ob-locale-inline{display:inline}
.ob-showcase :is([data-locale="ar"],[data-locale="ru"]).ob-locale-inline{display:none}
html[lang="ar"] .ob-showcase :is([data-locale="en"],[data-locale="ru"]).ob-locale-inline{display:none}
html[lang="ar"] .ob-showcase [data-locale="ar"].ob-locale-inline{display:inline}
html[lang="ru"] .ob-showcase :is([data-locale="en"],[data-locale="ar"]).ob-locale-inline{display:none}
html[lang="ru"] .ob-showcase [data-locale="ru"].ob-locale-inline{display:inline}

/* ── text entrances ─────────────────────────────────────────────────────────
   A short rise into place, on the pack's own terms: 420ms on --ease-ui, from
   hidden, no bounce and nothing decorative.

   Two mechanisms, because the hero is a different problem from the rest. The
   hero is above the fold, so a reveal tied to scroll position would already be
   past the end of its range on the first frame and would never play; it
   animates on load instead, staggered on --stagger. Everything below the fold
   waits for an IntersectionObserver to say it has been reached, which is what
   keeps it from firing for content the reader has not got to yet.

   The block is wrapped in a no-preference query, the hidden state is keyed on
   an attribute only the runtime writes, and the resting state is the visible
   one — so a visitor who asked for less motion, a browser with no observer, or
   a JS bundle that never arrives all simply get the text. Nothing here can
   strand content hidden, which is the failure mode the [data-reveal] rules in
   globals.css are one JS bundle away from for the route that uses them. */
/* --rise-to, not a hard 1: the animation runs with `both`, so its end state
   outranks whatever opacity the element declares for itself. The footer
   wordmark is the case that caught it — a watermark drawn at .3 that the
   entrance finished at full bronze, turning a quiet mark into a band across
   the bottom of the page. Anything that rests below full opacity sets
   --rise-to to the same value; everything else inherits the 1 it had. */
@keyframes ob-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: var(--rise-to, 1); transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  .ob-hero-copy > * {
    animation: ob-rise var(--web-dur-slow) var(--ease-ui) both;
  }
  .ob-hero-copy > *:nth-child(2) { animation-delay: var(--stagger); }
  .ob-hero-copy > *:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }

  /* Everything below the fold. The runtime marks a group with data-rise and
     flips data-in when it scrolls into view; the ladder here is animation
     delays, which every engine runs. This was `animation-timeline: view()`
     behind an @supports guard, which meant Chromium got the entrance and
     Safari and Firefox got nothing — the flat page being the one most phones
     see. See installPageReveal for what is in the two lists and what is
     deliberately out of them.

     The hidden state hangs off data-rise, and only the runtime writes that, so
     no path through this file can leave copy hidden. `data-rise` rather than
     `data-reveal` because globals.css already spends that name on the other
     route's <Reveal>, which opens on data-shown — see installPageReveal. */
  /* An animation, not a transition, for two reasons that both bit.
     - A transition on the reveal is declared on the element itself, so it
       REPLACES whatever transition that element already had. `.btn` in the
       footer lost its hover sheen and colour travel to it, permanently, since
       data-in stays true. An animation sits alongside.
     - The ladder's delay has to belong to the open state alone. Left on the
       base rule it is a transition-delay against the default
       transition-property of `all`, so marking a group deferred its own hiding
       by up to three stagger steps and the copy sat there visible before
       snapping out.
     ob-rise is the same keyframe the hero opens on; `both` holds the end
     state, and holds the start state through the delay. */
  :is([data-rise='ladder'] > *, [data-rise='solo']) {
    opacity: 0;
  }
  :is(
    [data-rise='ladder'][data-in='true'] > *,
    [data-rise='solo'][data-in='true']
  ) {
    animation: ob-rise var(--web-dur-slow) var(--ease-ui) both;
  }
  [data-rise='ladder'][data-in='true'] > *:nth-child(2) {
    animation-delay: var(--stagger);
  }
  [data-rise='ladder'][data-in='true'] > *:nth-child(3) {
    animation-delay: calc(var(--stagger) * 2);
  }
  /* Flat after the third, rather than one step per line: a fourth and fifth
     step is a queue the reader waits out, not a stagger they read. */
  [data-rise='ladder'][data-in='true'] > *:nth-child(n + 4) {
    animation-delay: calc(var(--stagger) * 3);
  }
}

/* ── button hover: a sweep, not a jump ──────────────────────────────────────
   The SSOT hover is translateY(--lift-hover), and on a page whose controls sit
   in rows — the topbar pair, the footer CTA — the hovered one stepped out of
   the row and read as a jump rather than as a response. The lift goes, and the
   hover becomes a soft highlight travelling left to right across the face:
   because it is a background position moving through a gradient, it is a
   continuous change rather than a flash, which is the one thing the motion
   rules forbid outright.
   The lift is dropped outside the reduced-motion gate on purpose — it has to go
   for everyone, including the readers who never see the sweep, who get the
   fill's own step up instead. :active keeps its press scale: a press is a
   press. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { transform: none; }
  /* The word stays white across both states, which the SSOT rule did not do: it
     hovers the fill to --brand-deep and flips the label to --card-2 cream to
     keep AA, so the one control the page is built around changed colour under
     the pointer. The fill still travels to --brand-deep — white on it is 5.81,
     up from 3.35 at rest, so the hover is the state that reads best — and the
     label is restated as --on-brand so it does not flip with it. Same under
     .on-dark, where brand keeps the bronze and would otherwise inherit the
     cream. .btn-primary is left out on a dark band: there it has already
     inverted to a cream plane with a dark word, and that pairing is correct. */
  .btn-primary:hover,
  .btn-brand:hover,
  .on-dark .btn-brand:hover { background: var(--brand-deep); color: var(--on-brand); }
}

/* Stated on top of the fill, so every variant keeps its own colour and its own
   contrast; this only adds the travel. background-position is named in the
   transition explicitly rather than left to the `background` shorthand. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .btn, .ob-seg-tag {
    background-image: linear-gradient(
      100deg,
      /* 15% was tried here first and measured invisible on the hovered bronze —
         about seven levels of separation, which on a moving highlight is
         nothing. The gentleness is the wide band (28-72 rather than 34-66) and
         the 420ms travel, not a peak too faint to see. */
      transparent 0 28%,
      color-mix(in srgb, var(--on-dark) 22%, transparent) 50%,
      transparent 72% 100%
    );
    background-size: 300% 100%;
    background-position: 130% 0;
    background-repeat: no-repeat;
    transition:
      transform var(--web-dur-fast) var(--ease-ui),
      background-color var(--web-dur-base) var(--ease-ui),
      background-position var(--web-dur-slow) var(--ease-ui);
  }
  .btn:hover, .ob-seg-tag:hover {
    background-position: -30% 0;
  }
}
