/* --bubble-x is a bubble's own horizontal padding, hoisted to a token
   because it is also the page's speech edge: anything the bot says
   outside a bubble has to line up with the text inside one. */
/* --marker is the column an emoji occupies at the head of a bubble line:
   its own width plus the space after it, measured at chat size. The
   chain's dots sit in that same column, so a bot line that opens with
   an emoji and a step that opens with a dot share one left edge. */
:root {
  --thread: 44rem; --gutter: 2.9rem; --ava: 2.375rem;
  /* measured off the client: ~11px side padding at every screen size —
     it does not widen on desktop — and ~7px between the last word and
     the stamp. */
  --bubble-x: 0.7rem; --stamp-gap: 7px; --marker: 1.625rem;
  /* An emoji's ink sits higher above the baseline than a letter's, and a
     line box's centre is higher still — so an emoji or a dot centred on
     the box reads as riding above the words next to it. Measured at 17px:
     emoji ink centre 6.5px above the baseline, line box centre 6.2px,
     text ink centre 4.6px. This drops both onto the text's own centre. */
  --optical-drop: 0.1em;
  /* ONE gap between message blocks. Bubbles, quotes and quote/answer
     pairs all use it — nothing in the thread invents its own spacing. */
  --msg-gap: 1.5rem;
}

/* ============================================================
   steward.str.systems
   Telegram's interface language, carrying Steward's own brand.
   Structure borrowed; identity is brass, never the link blue.
   Dark only — the use scene is a phone at night, already inside
   a dark messaging client.
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

html {
  color-scheme: dark;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  padding-bottom: 10.5rem;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  overflow-x: hidden;
}
/* The two washes used to live here as a `background-attachment: fixed`
   background-image on the body. Measured 2026-08-17: that is the single most
   expensive thing on this page on a phone — a fixed background cannot be
   scrolled by the compositor, so the browser repaints the viewport on every
   scroll frame, and the page felt heavy the whole way down.
   They now ride body::before, which is already `position: fixed` and therefore
   stays put for free. Identical picture, no per-frame repaint. */

/* An almost-invisible dark stroke behind every glyph. This page's type
   sits on a live wallpaper rather than a flat panel, and a sub-pixel
   stroke painted *behind* the fill separates letters from whatever is
   under them without thickening or muddying them. paint-order is what
   keeps it behind — without it the stroke is drawn centred on the
   outline and eats into the fill, so the whole thing is gated on it. */
@supports (paint-order: stroke) {
  body {
    paint-order: stroke fill;
    -webkit-text-stroke: 0.45px rgba(0, 0, 0, 0.55);
  }
  /* white on mid-blue is the page's thinnest contrast — the visitor's
     own messages carry the heaviest stroke of anything here */
  .msg--ask, .msg--out { -webkit-text-stroke: 0.7px rgba(0, 0, 0, 0.6); }
  /* dark ink on brass needs no help, and a stroke there only fattens it */
  .btn, .kbd__key--primary, .composer__send { -webkit-text-stroke-width: 0; }
}

/* The wallpaper: the icon tile is a MASK over one page-wide gradient that
   runs STR.systems blue into STR.systems orange. Masking rather than tiling
   a coloured SVG is what lets the colour flow across the whole surface
   instead of resetting at every tile edge. */
body::before,
body::after {
  content: "";
  position: fixed;
  inset: -10%;
  z-index: -1;
  pointer-events: none;
  /* Promoted to their own layer. A fixed, masked, full-viewport element that
     is NOT promoted is re-rasterised every scroll frame; promoted, the
     compositor simply leaves it where it is and costs nothing per frame. */
  will-change: transform;
  transform: translateZ(0);
  backface-visibility: hidden;
}
/* the colour wallpaper: one page-wide run from STR.systems blue to orange */
body::before {
  background:
    radial-gradient(1100px 620px at 78% -8%, rgba(43, 82, 120, 0.62), transparent 62%),
    radial-gradient(820px 520px at 6% 4%, rgba(224, 169, 74, 0.17), transparent 60%),
    linear-gradient(128deg, #3c77ef 0%, #6a63d8 34%, #d1683f 68%, #f0553d 100%);
  opacity: 0.13;
}
/* the icons, cut into that colour as a darker relief — the way Telegram
   overlays its pattern on a gradient rather than colouring the strokes */
body::after {
  background: #cfe0f2;
  opacity: 0.055;
  -webkit-mask-image: url("img/pattern.svg");
  mask-image: url("img/pattern.svg");
  -webkit-mask-size: 220px 220px;
  mask-size: 220px 220px;
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
}

/* a leading emoji, dropped onto the optical centre of the words it opens */
.em { position: relative; top: var(--optical-drop); }

h1, h2, h3 { font-family: var(--font-display); font-weight: 600; }
p { margin: 0 0 var(--s-3); }
p:last-child { margin-bottom: 0; }
a { color: var(--link); text-underline-offset: 0.18em; }
b, strong { font-weight: 600; color: var(--ink); }
code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: rgba(255, 255, 255, 0.07);
  border-radius: 5px;
  padding: 0.1em 0.36em;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Hidden by CLIPPING, not by being parked at left: -9999px.
   The old off-screen idiom only works in one writing direction. Mirrored for
   the Arabic page it became right: -9999px and stretched the document to
   10404px wide, which shoved every bubble 30px sideways; left un-mirrored it
   does the same thing in the other direction, because in RTL it is the LEFT
   overflow that becomes scrollable. Clipping has no side at all, so both
   directions get an invisible link that still takes focus and still reads. */
.skip {
  position: absolute;
  top: var(--s-2);
  inset-inline-start: var(--s-3);
  z-index: 60;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  background: var(--accent);
  color: var(--on-accent);
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-btn);
  font-weight: 600;
  text-decoration: none;
}
.skip:focus {
  width: auto;
  height: auto;
  overflow: visible;
  clip-path: none;
}

/* ---- the bar: iOS translucent chrome ---------------------- */
.bar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--veil);
  border-bottom: 1px solid var(--veil-line);
  padding-top: env(safe-area-inset-top);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .bar {
    -webkit-backdrop-filter: blur(22px) saturate(180%);
    backdrop-filter: blur(22px) saturate(180%);
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .bar { background: #101b27; }
}
/* ---- the mobile cost ceiling ------------------------------
   Measured 2026-08-17 after the page was reported as laggy on a phone. Three
   things were being paid for on every scroll frame, and a phone cannot afford
   them together. Two of them are switched off below the tablet breakpoint —
   the effects they replace are, on a 375px screen, essentially invisible
   anyway, while the cost of them is not.

   1. backdrop-filter on a STICKY element is the expensive case: the bar stays
      put while content moves under it, so the blurred region has to be
      recomputed continuously. The solid fill is the same one the no-support
      fallback above already uses, so this is a path the design already had.
   2. A sub-pixel stroke behind every glyph means every glyph is painted twice,
      down 7,000px of page. It exists to separate type from the live wallpaper;
      at phone size that wallpaper is a faint wash and the type holds without
      help. The heavier stroke on the visitor's own messages — the page's
      thinnest contrast — is kept.
   ---------------------------------------------------------- */
@media (max-width: 48rem) {
  .bar {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: #101b27;
  }
  body { -webkit-text-stroke-width: 0; }
  .msg--ask, .msg--out { -webkit-text-stroke: 0.6px rgba(0, 0, 0, 0.6); }
}

.bar__in {
  max-width: 780px;
  margin: 0 auto;
  padding: var(--s-2) var(--s-4);
}
.bar__row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.avatar {
  flex: none;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  object-fit: cover;
  background: #0c131b;
  filter: brightness(0.82) contrast(1.14);
}

.bar__id { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.bar__id b { font-size: var(--t-body); font-weight: 600; }
.bar__id small {
  font-size: var(--t-micro);
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Room under the baseline for the STR.systems underline. `overflow: hidden`
     is here for the ellipsis, and it was silently clipping that underline
     1.8px outside this box — the rule existed and simply never painted. */
  padding-bottom: 2px;
}
/* The company link under the bot's name. Deliberately NOT the link blue:
   this page's own rule is that brass carries every brand moment and the link
   blue never does, and a blue word inside the identity block reads as a stray
   UI link rather than as the maker's name. Muted like the line it sits in,
   brass on hover so it is discoverable without shouting. */
.bar__id small a {
  color: inherit;
  /* A real underline, not a border-bottom. The border sat on the link's own
     box, which `padding-bottom` pushed past the clipping edge above; an
     underline is painted with the text, so it travels with the baseline. */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.bar__id small a:hover { color: var(--accent); }


.iconbtn {
  margin-left: auto;
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  border-radius: 50%;
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out),
              background var(--dur-1) var(--ease-out);
}
.iconbtn svg { width: 20px; height: 20px; }
.iconbtn:hover { color: var(--ink); background: rgba(255, 255, 255, 0.07); }

/* ---- buttons ---------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  font-size: var(--t-sm);
  text-decoration: none;
  padding: 0.55rem 0.95rem;
  border-radius: var(--r-btn);
  border: 0;
  white-space: nowrap;
  box-shadow: var(--shadow-1);
  transition: filter var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.btn:hover { filter: brightness(1.07); }
.btn:active { transform: translateY(1px); }
.btn--sm { font-size: var(--t-xs); padding: 0.44rem 0.7rem; min-width: 0; }
.bar .btn { margin-left: auto; }
/* The bar keeps its action on every width — the page is long and the
   next CTA can be five sections away. Only the label shortens. */
.lbl-sm { display: none; }
@media (max-width: 30rem) {
  .lbl-lg { display: none; }
  .lbl-sm { display: inline; }
  .bar__in { gap: var(--s-2); }
}

/* ---- chat column ------------------------------------------ */
.chat {
  max-width: 780px;
  margin: 0 auto;
  /* the extra left padding is the avatar gutter: every bubble, quote,
     group and keyboard shares one left edge, the way a real thread does */
  padding: var(--s-6) var(--s-4) var(--s-8);
  padding-left: calc(var(--s-4) + var(--gutter));
}

.sec { margin-bottom: var(--s-9); }
/* The deck section ends in a full-bleed dark panel, not in a bubble. After a
   panel the standard 6rem section rhythm stops reading as rhythm and starts
   reading as a hole — measured at 82px of empty ground between the panel's
   edge and the next question. Halved here only. */
.sec--deck { margin-bottom: var(--s-7); }
.sec:last-child { margin-bottom: var(--s-7); }
/* the day divider is its own separator: equal air above and below,
   so the thread does not jump before it and snap after it */
.sec--intro { margin-bottom: var(--msg-gap); }

/* ---- message bubbles -------------------------------------- */
/* Telegram bubbles are compact. The client never pads like a web card. */
.msg {
  position: relative;
  width: fit-content;
  max-width: var(--thread);
  padding: 0.4rem var(--bubble-x) 0.42rem;
  border-radius: var(--r-bubble);
  margin-bottom: var(--msg-gap);
  line-height: 1.42;
}
/* Telegram's bubbles sit ON the wallpaper, not over a solid sheet.
   Translucent + backdrop blur is what gives the client its depth. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .msg.msg--in    { background: rgba(30, 42, 56, 0.95); }
  .msg.msg--out,
  .msg.msg--ask   { background: rgba(58, 110, 168, 0.90); }
  .msg.msg--title { background: rgba(43, 58, 75, 0.90); }
  .group          { background: rgba(23, 33, 43, 0.90); }
  /* .quote is deliberately absent: it has no surface, only its bar */
  .msg, .group {
    -webkit-backdrop-filter: blur(20px) saturate(165%);
    backdrop-filter: blur(20px) saturate(165%);
    box-shadow:
      0 2px 4px rgba(0, 0, 0, 0.5),
      0 14px 30px -12px rgba(0, 0, 0, 0.62),
      inset 0 0 0 1px rgba(255, 255, 255, 0.085);
  }
  .msg.msg--title, .msg.msg--ask {
    box-shadow: var(--shadow-lift), inset 0 1px 0 rgba(255, 255, 255, 0.075);
  }
}

/* The time is what makes a block read as a message rather than a panel. */
.meta {
  position: absolute;
  /* tracks the bubble's own padding instead of a hardcoded copy of it,
     so the stamp keeps the same inset when the padding widens */
  right: var(--bubble-x);
  bottom: 0.42rem;
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  user-select: none;
  pointer-events: none;
}
/* The room the stamp sits in, plus air between it and the last word.
   .meta is always the bubble's last child, so ":has(+ .meta)" is the
   last block of text whatever tag it is — the p rule alone missed the
   visitor's bubbles, whose only child is a heading, and their stamp
   was landing under the text instead of beside it. */
.msg > p:last-of-type::after,
.msg > *:has(+ .meta)::after {
  content: "";
  display: inline-block;
  /* the stamp's measured width plus the client's small gap before it.
     The fallback only applies for the instant before JS measures. */
  width: calc(var(--stamp, 2.6rem) + var(--stamp-gap));
}
.msg--out .meta { color: var(--msg-out-meta); }

/* A section opener is the same bubble, brought nearer the viewer:
   lighter surface, more air, and a real cast shadow behind it.
   Depth carries the hierarchy so the type never has to shout. */
.msg--title {
  background: var(--surface-3);
  padding: var(--s-5) var(--s-5) var(--s-4);
  margin-bottom: var(--msg-gap);
  box-shadow: var(--shadow-lift);
  z-index: 1;
}
.msg--title::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  pointer-events: none;
}

/* The bot's own face beside each group — Telegram's sender cue, and the
   single strongest signal that this is a real thread and not a web panel. */
.msg--title::before,
.msg--greet::before,
.swipe__lead::before,
.msg--ask + .msg--in::before,
/* folding the story put a <details> between the question and its answer, so
   the summary needs naming too or the avatar falls off that group */
.msg--ask + .story > .msg--sum::before {
  content: "";
  position: absolute;
  left: calc(var(--gutter) * -1);
  bottom: 0;
  width: var(--ava);
  height: var(--ava);
  border-radius: 50%;
  background: #0c131b url("img/steward-avatar.webp") center / cover;
  filter: brightness(0.82) contrast(1.14);
}

/* "Who built this?" is the one group where a person is talking, not the bot,
   so it carries Alex's face instead.

   Two layers on purpose: if img/alex.webp is missing, that layer is simply
   transparent and the bot avatar underneath shows through — so the page never
   renders an empty circle, and the swap happens the moment the file lands.
   Drop a square photo at steward/img/alex.webp; nothing else to change. */
.sec--who .msg--ask + .msg--in::before,
.sec--who .msg--ask + .story > .msg--sum::before {
  background-image: url("img/alex.webp?v=1786725418"), url("img/steward-avatar.webp");
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
  background-color: #0c131b;
  filter: none;
}

.msg--in {
  background: var(--msg-in);
  color: var(--msg-in-ink);
  border-bottom-left-radius: var(--r-tail);
  margin-right: auto;
}
.msg--out {
  background: var(--msg-out);
  color: var(--msg-out-ink);
  border-bottom-right-radius: var(--r-tail);
  margin-left: auto;
  max-width: 26rem;
  font-weight: 500;
}
.msg--wide { padding: var(--s-5); }

.msg p { margin-bottom: 0.4rem; }
.msg h2 {
  font-size: var(--t-h2);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-h2);
  margin: 0 0 var(--s-3);
}
/* Everything the bot says runs at chat size — the same size the
   visitor's own message is set in. Telegram never enlarges type
   inside a bubble; a heading there is bold and brass, not bigger. */
.msg h3 {
  font-size: var(--t-body);
  line-height: var(--lh-snug);
  margin: 0 0 var(--s-2);
  color: var(--accent);
}
.msg p + h3 { margin-top: var(--s-4); }
.muted { color: var(--ink); font-size: var(--t-body); }

/* A short message is short. Uniform full-width bubbles are the panel tell. */
.msg--short { display: table; max-width: min(100%, 34rem); }

/* The bot's own message shape: a bold title line, then the body. */
.jobtitle {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-body);
  color: var(--accent);
  margin: 0 0 var(--s-3);
}

/* Telegram's day divider — a centred translucent pill. */
.daydiv {
  display: flex;
  justify-content: center;
  margin: 0 0 var(--msg-gap);
}
.daydiv span {
  padding: 0.28rem 0.8rem;
  border-radius: var(--r-pill);
  background: rgba(10, 17, 25, 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  font-size: var(--t-xs);
  font-weight: 500;
  color: var(--ink-2);
}

/* ---- the composer -------------------------------------------
   Telegram's most recognisable furniture, and the most native
   possible home for the action: always there, never a banner. */
.composer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  padding: var(--s-3) var(--s-4) calc(var(--s-3) + env(safe-area-inset-bottom));
  background: var(--veil);
  border-top: 1px solid var(--veil-line);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .composer {
    -webkit-backdrop-filter: blur(22px) saturate(180%);
    backdrop-filter: blur(22px) saturate(180%);
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .composer { background: #101b27; }
}
.composer__in { max-width: 780px; margin: 0 auto; }
.composer__field {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 0.5rem 0.5rem 0.5rem var(--s-4);
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: var(--t-sm);
  text-decoration: none;
  transition: background var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.composer__field:hover,
.composer__field:focus-within { background: var(--surface-3); color: var(--ink); }
/* The field is a real text input now, so it must look like the placeholder text
   it replaced — no browser chrome, no serif fallback, no zoom-on-focus. iOS
   zooms any input under 16px, which on this page moved the whole layout. */
.composer__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  padding: 0;
  color: var(--ink);
  font: inherit;
  font-size: max(16px, var(--t-sm));
  line-height: 1.4;
}
.composer__input::placeholder { color: var(--ink-2); opacity: 1; }
.composer__input:focus { outline: none; }
.composer__send {
  flex: none;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 0;
  padding: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background: var(--accent);
  color: var(--on-accent);
}
.composer__send svg { width: 19px; height: 19px; }
.composer__hint {
  margin: var(--s-2) 0 0;
  text-align: center;
  font-size: var(--t-xs);
  color: var(--ink-2);
  opacity: 0.75;
}

/* ---- background music ------------------------------------- */
.music {
  position: fixed;
  left: max(var(--s-4), env(safe-area-inset-left));
  bottom: calc(5.4rem + env(safe-area-inset-bottom));
  z-index: 70;
  width: min(18rem, calc(100vw - (var(--s-4) * 2)));
  padding: 0.72rem;
  border: 1px solid var(--veil-line);
  border-radius: var(--r-group);
  background: rgba(14, 22, 33, 0.86);
  color: var(--ink);
  box-shadow: var(--shadow-2);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .music {
    -webkit-backdrop-filter: blur(18px) saturate(170%);
    backdrop-filter: blur(18px) saturate(170%);
  }
}
.music__label {
  margin: 0 0 0.34rem;
  font-size: var(--t-micro);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.09em;
}
.music__tracks {
  display: grid;
  gap: 0.1rem;
  margin: 0 0 var(--s-2);
}
.music__tracks p {
  margin: 0;
  overflow: hidden;
  color: var(--ink-2);
  font-size: var(--t-xs);
  line-height: 1.25;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.music__tracks p.is-playing {
  color: var(--accent);
  font-weight: 600;
}
.music__controls {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.music__btn {
  flex: none;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.music__btn:hover { background: var(--surface-3); border-color: var(--accent-line); }
.music__btn:active { transform: translateY(1px); }
.music__btn svg {
  width: 17px;
  height: 17px;
  fill: currentColor;
}
.music__icon-pause { display: none; }
.music[data-state="playing"] .music__icon-play { display: none; }
.music[data-state="playing"] .music__icon-pause { display: block; }
.music__volume {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  color: var(--ink-3);
  font-size: var(--t-micro);
}
.music__volume span { flex: none; }
.music__volume input {
  width: 100%;
  min-width: 4.5rem;
  accent-color: var(--accent);
}
.music[data-state="blocked"] .music__tracks::after {
  content: "Tap play to start";
  color: var(--ink-3);
  font-size: var(--t-micro);
}

/* ---- the language shelf -------------------------------------
   Sits above the field, scrolls sideways, and holds every language in
   its OWN name. The typed matcher is for people who can read the
   placeholder; this is for everyone else, and it is the reason the
   picker does not need a server-side detector the way the bot does. */
.langs {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding: 0 0 var(--s-3);
  margin: 0 0 calc(var(--s-2) * -1);
}
.langs::-webkit-scrollbar { display: none; }
.langs[hidden] { display: none; }
.langs__chip {
  flex: none;
  padding: 0.34rem 0.72rem;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: var(--t-xs);
  line-height: 1.5;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.langs__chip:hover { background: var(--surface-3); color: var(--ink); }
.langs__chip.is-current {
  background: var(--accent);
  color: var(--on-accent);
}
.langs__chip[hidden] { display: none; }

/* ---- hero -------------------------------------------------- */
.display {
  font-size: var(--t-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
  font-weight: 700;
  margin: 0 0 var(--s-3);
  text-wrap: pretty;
}
/* The English hero was hand-fitted to its own three short lines, and
   --t-display's clamp floor (2.5rem) is what holds it there on a phone.
   Translated headlines do not get that luxury: measured at 375px, seven of the
   twenty overflowed the 330px column — and "короткострокову" is a single
   unbreakable word exactly as wide as the whole column, so it bled off the
   screen rather than wrapping.
   Lowering the floor lets the 9vw term actually take effect on small screens.
   Scoped off English so the tuned original is left exactly as it was. */
html:not([lang="en"]) .display { font-size: clamp(1.9rem, 9vw, 4rem); }

.display--sm { font-size: var(--t-h2); letter-spacing: var(--tr-h2); }
.lede {
  color: var(--accent);
  font-weight: 600;
  font-size: var(--t-h3);
  margin: 0 0 var(--s-4);
}

/* the chain — the one authored motion on the page.
   Each step is set like a bubble line that opens with an emoji: the
   dot takes the marker column, the words start where words start. */
.chain {
  list-style: none;
  margin: 0 0 var(--s-4);
  padding: 0 0 0 var(--marker);
  position: relative;
}
.chain::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 0.6em;
  bottom: 0.6em;
  width: 2px;
  background: var(--line);
  border-radius: 2px;
}
.chain::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 0.6em;
  bottom: 0.6em;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg, #3c77ef 0%, #6a63d8 35%, #d1683f 68%, #f0553d 100%);
  box-shadow: 0 0 14px rgba(224, 169, 74, 0.28);
  transform: scaleY(var(--music-progress, 0));
  transform-origin: top;
  transition: transform 180ms linear;
}
.chain li {
  position: relative;
  padding: 0.34rem 0;
  font-size: var(--t-body);
  color: var(--ink);
}
.chain li::before {
  content: "";
  position: absolute;
  left: calc(var(--marker) * -1);
  top: calc(50% + var(--optical-drop));
  width: 12px;
  height: 12px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--accent);
  box-shadow: 0 0 0 4px var(--accent-wash);
}
@keyframes node-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.chain.is-live li::before { animation: node-in 520ms var(--ease-out) both; }
.chain.is-live li:nth-child(1)::before { animation-delay: 0ms; }
.chain.is-live li:nth-child(2)::before { animation-delay: 130ms; }
.chain.is-live li:nth-child(3)::before { animation-delay: 260ms; }
.chain.is-live li:nth-child(4)::before { animation-delay: 390ms; }
.chain.is-live li:nth-child(5)::before { animation-delay: 520ms; }
.chain.is-live li:last-child { font-weight: 600; }

/* Stacked while narrow. Separators only appear once the row can
   hold them, and they trail their own item, so a wrap can never
   orphan a dot at the start of a line. */
.facts {
  list-style: none;
  margin: 0 0 var(--s-3);
  padding: var(--s-3) 0 0;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  font-size: var(--t-sm);
  color: var(--ink-2);
}
@media (min-width: 34rem) {
  .facts {
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--s-1) var(--s-3);
  }
  .facts li:not(:last-child)::after {
    content: "·";
    margin-left: var(--s-3);
    color: var(--ink-3);
  }
}
.whatis { color: var(--ink-2); font-size: var(--t-sm); margin: 0; }
/* What this actually is, second line — not last and smallest. */
.whatis--top {
  color: var(--ink);
  font-size: var(--t-h3);
  font-weight: 400;
  line-height: var(--lh-snug);
  max-width: 34rem;
  margin: 0 0 var(--s-4);
}

/* ---- inline keyboard --------------------------------------- */
.kbd {
  max-width: var(--thread);
  display: grid;
  gap: 3px;
  margin-bottom: var(--msg-gap);
}
/* A Telegram inline keyboard is welded to the bottom of its bubble,
   at the bubble's width, squared on top and carrying the bubble's own
   bottom corners. Detached with its own radius and shadow, it is just
   a generic CTA — the thing the THESIS says this page refuses. */
.msg--flush {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  margin-bottom: 2px;
}
.kbd--welded { max-width: var(--thread); }
.kbd--welded .kbd__key { border-radius: 0; box-shadow: none; }
.kbd--welded .kbd__key:last-child {
  border-radius: 0 0 var(--r-bubble) var(--r-tail);
}
.kbd__key {
  display: block;
  text-align: center;
  text-decoration: none;
  font-weight: 600;
  font-size: var(--t-body);
  padding: var(--s-4);
  border-radius: var(--r-btn);
  background: rgba(255, 255, 255, 0.08);
  color: var(--ink);
  transition: background var(--dur-1) var(--ease-out),
              filter var(--dur-1) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.kbd__key:hover { background: rgba(255, 255, 255, 0.13); }
.kbd__key--primary {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-2);
}
.kbd__key--primary:hover { background: var(--accent); filter: brightness(1.07); }
.kbd__key--primary:active { transform: translateY(1px); }

.undernote {
  max-width: var(--thread);
  font-size: var(--t-xs);
  color: var(--ink-2);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
  margin: 0;
  text-align: center;
}

/* ---- quoted messages --------------------------------------- */
.pair { margin-bottom: var(--msg-gap); }
/* Telegram's quoted message: a rounded bar inset from the edge,
   not a border stuck to it. */
/* No box: the bar alone carries it. Its bar sits in the same marker
   column the emoji and the chain dots use, and the words it quotes
   start where words after a marker start. */
.quote {
  position: relative;
  max-width: var(--thread);
  margin: 0 0 var(--msg-gap) var(--bubble-x);
  padding: 0 0 0 var(--marker);
  background: none;
  border-radius: 0;
}
.quote::before {
  content: "";
  position: absolute;
  left: 0;
  top: 3px;
  bottom: 3px;
  width: 3px;
  border-radius: 3px;
  background: var(--accent);
}
.quote p {
  margin: 0 0 var(--s-2);
  font-size: var(--t-body);
  color: var(--ink);
}
.quote cite {
  display: block;
  font-style: normal;
  font-size: var(--t-sm);
  color: var(--ink-3);
  letter-spacing: 0.01em;
}
/* The modelled quote must never be mistaken for a sourced one. With the
   box gone, the dashed border went with it — so the mark moved into the
   bar itself: broken instead of solid, grey instead of brass. */
.quote--model::before {
  background: repeating-linear-gradient(
    var(--ink-3) 0 5px, transparent 5px 9px);
  opacity: 0.75;
}
.quote--model p { color: var(--ink-2); font-style: italic; }
.quote--model cite { color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em; }

/* ---- settings-style groups (scannable content, never bubbles) */
.group {
  max-width: var(--thread);
  background: var(--surface);
  border-radius: var(--r-group);
  overflow: hidden;
  margin-bottom: var(--s-3);
  box-shadow: var(--shadow-1);
}
.row {
  display: grid;
  /* Each .row is its OWN grid, so `auto auto` sized the last two columns to
     that row's own content — $29 and $179 are different widths, so no two
     rows agreed on where the price column began, and the header sat over
     neither. Fixed widths plus right alignment make the money read as one
     column. Wide enough for the PRICE / PER DAY headers at --t-micro. */
  /* A fourth column for the per-tier Pay key. The plan name keeps minmax(0,1fr)
     so it is the column that gives way, not the money — and it wraps rather
     than overflowing, which is why the price and per-day columns stay fixed. */
  /* The Pay column is FIXED, not auto, for the same reason the two money
     columns are: measured with `auto`, the header's empty cell collapsed to 0
     and sat 74px off the data rows, and "Pay $179" being wider than "Pay $29"
     pushed its own row 6px out of line with the other three. Wide enough for
     the longest translated label ("Оплатити $179"). */
  grid-template-columns: minmax(0, 1fr) 7.5rem 4rem 7rem;
  gap: var(--s-4);
  align-items: baseline;
  padding: var(--s-4);
  border-bottom: 1px solid var(--line-soft);
  font-size: var(--t-sm);
}
/* Grid children default to min-width:auto, so a long Cyrillic plan
   label would push the table past its container instead of wrapping.
   The Russian edition is planned; this has to hold at +30%. */
.row > * { min-width: 0; overflow-wrap: break-word; }
.row > *:nth-child(2), .row > *:nth-child(3) { text-align: right; }
.row:last-child { border-bottom: 0; }
.row--head {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-3);
  padding-top: var(--s-3);
  padding-bottom: var(--s-3);
}
.row--best { background: var(--accent-wash); }
/* The two brass numbers on this page, declared together so they stay the same
   brass: the winning plan's price, and the per-property figure the whole
   pricing claim rests on. Only these two — the other bolds in .row--wide
   ("unlimited, free") stay ordinary emphasis, or the section has no focal
   point left. */
.row--best b,
.row__hi { color: var(--accent); }
.row--wide { grid-template-columns: 1fr; color: var(--ink-2); }
.row--wide > * { text-align: left; }
.row .num { font-family: var(--font-mono); font-size: var(--t-xs); color: var(--ink-3); }

/* Air above a bubble that sums a section up, rather than continuing it. */
.msg--spaced { margin-top: var(--s-7); }

.pointdown { text-align: center; letter-spacing: 0.18em; }

/* ---- the founder story, folded ------------------------------ */
/* Five bubbles long. <details> keeps it in the DOM and in the page's own
   text — so it stays readable and indexable — without spending five screens
   of every visitor's scroll before the final CTA. */
.story { max-width: var(--thread); }
.msg--sum { cursor: pointer; list-style: none; }
.msg--sum::-webkit-details-marker { display: none; }
.story__cta { color: var(--accent); white-space: nowrap; }
.story[open] .story__cta { opacity: 0.55; }


/* ---- footer ------------------------------------------------ */
.foot {
  border-top: 1px solid var(--line-soft);
  /* clears the fixed composer */
  padding: var(--s-5) var(--s-4) calc(var(--s-8) + env(safe-area-inset-bottom));
}
.foot p {
  max-width: 780px;
  margin: 0 auto;
  font-size: var(--t-xs);
  color: var(--ink-3);
  text-align: center;
}
/* Legal links. Quiet by design — they must be present and findable, not
   compete with the composer's action sitting right above them. */
.foot__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-2) var(--s-4);
  margin: 0 auto var(--s-4);
  max-width: 780px;
}
.foot__legal a {
  color: var(--ink-3);
  font-size: var(--t-xs);
  text-decoration: none;
}
.foot__legal a:hover { color: var(--ink); }

/* The STR.systems credit in the footer line. Same treatment as the one under
   the logo, and for the same reason stated up there: this page's rule is that
   brass carries every brand moment and the link blue never carries the brand.
   This one had no rule at all, so it fell through to the global link colour
   and rendered blue while its twin in the header was grey. */
.foot p a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.foot p a:hover { color: var(--accent); }

/* ---- the desktop rail --------------------------------------
   Telegram on a large screen is a chat list beside a thread.
   Below 64rem there is no list, exactly as on the phone. */
.shell { max-width: 1180px; margin: 0 auto; }
.rail { display: none; }

[id] { scroll-margin-top: 5rem; }

/* ---- wider viewports --------------------------------------- */
@media (min-width: 48rem) {
  /* a bubble's padding does not change with the screen — the client
     keeps the same ~11px inset on a phone and on a desktop window */
  .chat { padding: var(--s-5) var(--s-5) var(--s-9);
           padding-left: calc(var(--s-5) + var(--gutter)); }
  .msg--wide { padding: var(--s-5) var(--s-6); }
}

@media (min-width: 64rem) {
  .shell {
    display: grid;
    grid-template-columns: 17rem minmax(0, 1fr);
    gap: var(--s-6);
    align-items: start;
    padding: 0 var(--s-5);
  }
  /* the gutter must survive here or the avatars fall out of the column */
  .chat {
    margin: 0;
    max-width: none;
    padding-left: var(--gutter);
    padding-right: 0;
  }

  .rail { display: none; }
  .rail__cap {
    margin: 0 0 var(--s-2);
    padding-left: var(--s-3);
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: 0.11em;
    color: var(--ink-3);
  }
  .rail ul { list-style: none; margin: 0; padding: 0; }
  .rail a {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-2) var(--s-3);
    border-radius: var(--r-btn);
    color: var(--ink-2);
    text-decoration: none;
    font-size: var(--t-sm);
    transition: background var(--dur-1) var(--ease-out),
                color var(--dur-1) var(--ease-out);
  }
  .rail a:hover { background: var(--surface); color: var(--ink); }
  .ava {
    flex: none;
    width: 30px;
    height: 30px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--surface-3);
    color: var(--ink-3);
    font-size: var(--t-xs);
    font-weight: 600;
    transition: background var(--dur-1) var(--ease-out),
                color var(--dur-1) var(--ease-out);
  }
  .rail a:hover .ava { background: var(--accent-wash); color: var(--accent); }

  /* Telegram's composer belongs to the thread, not to the chat list. */
  .composer { padding-left: 0; padding-right: 0; }
  /* bar, thread and composer all resolve to the same left edge */
  .composer__in,
  .bar__in {
    display: grid;
    grid-template-columns: 17rem minmax(0, 1fr);
    gap: var(--s-6);
    max-width: 1180px;
    padding-left: var(--s-5);
    padding-right: var(--s-5);
  }
  .composer__in { margin: 0 auto; }
  .bar__in { padding-top: var(--s-2); padding-bottom: var(--s-2); }
  .composer__field,
  .bar__row {
    grid-column: 2;
    margin-left: var(--gutter);
    max-width: var(--thread);
  }
  .foot p {
    max-width: 1180px;
    padding-left: calc(var(--s-5) + 17rem + var(--s-6) + var(--gutter));
    padding-right: calc(1180px - var(--s-5) - 17rem - var(--s-6) - var(--gutter) - var(--thread));
    text-align: left;
  }
}

@media (min-width: 64rem) {
  .music {
    left: var(--s-5);
    bottom: calc(5.4rem + env(safe-area-inset-bottom));
  }
}

@media (min-width: 88rem) {
  .music {
    bottom: max(var(--s-4), env(safe-area-inset-bottom));
  }
}

@media (max-width: 30rem) {
  body { padding-bottom: 12rem; }
  .music {
    right: max(var(--s-4), env(safe-area-inset-right));
    width: auto;
  }
}


/* ---- the visitor's turn -------------------------------------
   A section no longer opens with a heading; it opens with the
   question a reader is already asking, in their own bubble, with
   Telegram's letter avatar for an account with no photo. The bot
   answers underneath. The page stops narrating and starts talking. */
.msg--ask {
  background: var(--msg-out);
  color: var(--msg-out-ink);
  border-bottom-right-radius: var(--r-tail);
  border-bottom-left-radius: var(--r-bubble);
  margin-left: auto;
  margin-right: var(--gutter);
  max-width: 29rem;
  /* no padding of its own: the visitor's bubble is a bubble like any
     other, and the client gives both sides the same inset */
  box-shadow: var(--shadow-lift);
  z-index: 1;
}
.msg--ask h2 {
  margin: 0;
  font-size: var(--t-h3);
  line-height: var(--lh-snug);
  letter-spacing: -0.01em;
  color: var(--msg-out-ink);
}
.msg--ask p { margin: 0; font-weight: 500; }
.msg--ask .meta { color: var(--msg-out-meta); }
.msg--ask::before {
  content: "U";
  position: absolute;
  right: calc(var(--gutter) * -1);
  bottom: 0;
  width: var(--ava);
  height: var(--ava);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: linear-gradient(160deg, #74b8f0, #2a72c9);
  font-family: var(--font-ui);
  font-size: 0.72rem;
  font-weight: 700;
  color: #fff;
}
@media (min-width: 48rem) {
}

/* ---- headings step out of the thread -------------------------
   H1 and the section H2s lose their bubble: they are the page's
   own voice, not a message. The bubbles stay for what is actually
   being said, which makes the thread read as content rather than
   as chrome wrapped around a heading. */
.msg.msg.msg--title.msg--short {
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0;
  margin: 0 0 var(--msg-gap);
  max-width: var(--thread);
}
.msg.msg--title.msg--short::before,
.msg.msg--title.msg--short::after { display: none; }
.msg.msg--title.msg--short .meta { display: none; }


/* ---- the blue full stop -------------------------------------
   STR.systems blue, used nowhere else — the parent brand signing
   the sentence without taking the page over. */
.dot { color: #3c77ef; }

/* ---- an attached photo --------------------------------------
   Telegram insets media by a few pixels inside the bubble, so the
   image reads as a thing that was sent rather than as typography. */
.msg--photo { padding: 3px; }
.photo {
  padding: var(--s-4);
  border-radius: calc(var(--r-bubble) - 2px);
  border-bottom-left-radius: 2px;
  background: linear-gradient(155deg, #1e2d3c 0%, #151f2a 100%);
  border: 1px solid rgba(255, 255, 255, 0.055);
}
.photo .chain { margin-bottom: 0; }

/* ---- the swipe deck ------------------------------------------
   Five problems, one at a time. Two scrollers rather than one so the
   bot's line can sit still between the problem and its answer; a
   scroll listener keeps them on the same index. Native overflow
   scrolling means touch, trackpad and arrow keys work with no drag
   handling of our own, and it degrades to a plain scroll strip if
   the script never runs. */
.swipe {
  /* full-bleed, like the screen it can contain: it steps out of the
     avatar gutter so a slide gets the whole width of the display */
  margin-left: calc(var(--gutter) * -1);
  /* BUTTON-DRIVEN, not swiped (owner's call, 2026-08-17). `hidden` rather
     than `auto`: the deck is still scrollable programmatically — that is what
     `overflow: hidden` means, as opposed to `visible` — so the arrows keep
     working, but a finger cannot drag it.
     This deletes the whole class of bugs the deck kept producing. Two
     scrollers mirroring each other through scroll events had to infer where a
     touch gesture started and ended, and a fling is drag → pause → momentum:
     any pause read as the end of one gesture and the start of another, which
     is how a single swipe moved two slides. With no gesture to infer, there is
     nothing left to get wrong. Snap goes with it — there is no free scrolling
     left for it to correct. */
  overflow: hidden;
  scrollbar-width: none;
}
.swipe::-webkit-scrollbar { display: none; }
.swipe:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.swipe__track {
  display: flex;
  align-items: flex-start;
  list-style: none;
  margin: 0;
  padding: 0;
}
.swipe__slide {
  flex: 0 0 100%;
  /* puts the slide's contents back on the thread's own left edge, so a
     quote and a bubble inside a slide align exactly as they would outside */
  padding-left: var(--gutter);
}
/* inside a slide the screen is already full width */
.swipe__slide .demo { margin-left: calc(var(--gutter) * -1); }
.swipe__slide .quote,
.swipe__slide .msg { margin-bottom: 0; }

/* the bot's line holds still while the deck moves under it */
.swipe__lead { margin: var(--msg-gap) 0; }

.swipe__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  /* it sits above the deck now, so the gap goes under it */
  margin: 0 0 var(--msg-gap);
}
.swipe__arrow {
  /* Matched to .avatar (38px). They are the only way through the deck now, so
     they are sized as a primary control rather than as a hint — and 38px clears
     the 24px minimum a touch target wants. */
  width: 38px;
  height: 38px;
  flex: none;
  display: grid;
  place-items: center;
  padding: 0 0 2px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink-2);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out);
}
.swipe__arrow:hover { color: var(--ink); border-color: var(--surface-3); }
.swipe__arrow[disabled] { opacity: 0.35; cursor: default; }
.swipe__dots {
  display: flex;
  gap: var(--s-2);
  list-style: none;
  margin: 0;
  padding: 0;
}
.swipe__dots li {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--surface-3);
  transition: background var(--dur-2) var(--ease-out);
}
.swipe__dots li.is-at { background: var(--accent); }

/* ---- the embedded example ------------------------------------
   A chat inside a chat needs a frame or the reader cannot tell whose
   conversation they are looking at. So this is set as a screen lifted
   out of the product: its own inset ground, a hairline, a label, and
   type one step down from the thread around it. Nothing in here is
   written for the page — it is the bot's own copy. */
.demo {
  /* It is a screen, not a message, so it takes the whole content width
     and steps out of the avatar gutter the bubbles sit inside. On a
     phone that gutter is 12% of the display — room this needs. */
  margin: 0 0 var(--msg-gap) calc(var(--gutter) * -1);
  padding: var(--s-4) var(--s-3) var(--s-3);
  max-width: none;
  background: rgba(8, 14, 21, 0.55);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-group);
}
.demo__cap {
  margin: 0 0 var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--line);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}

/* Two named views with one message per row, in the order they actually
   happen. Nothing sits beside anything else, so reading top to bottom is
   reading the sequence, and the side a message lands on says whose screen
   it is — the client's own left/right rule, used for two people instead
   of two directions. The staircase that falls out of it IS the timeline. */
.demo__flow {
  position: relative;
  display: flow-root;
}
.demo__who {
  margin: 0 0 var(--s-3);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-2);
}
.demo__who--m { max-width: 78%; }
.demo__who--t {
  max-width: 78%;
  margin-left: auto;
  text-align: right;
  margin-top: calc(var(--t-micro) * -1 - var(--s-3));
}
/* No centre rule any more: which edge a message hugs is what says whose
   screen it is, exactly as a chat does it, and dropping the rule gives
   every message back the width the two half-columns were costing. */
.demo__flow .dm { max-width: 78%; }
/* the SOP docs that follow the job, each in its own message */
.dm__t {
  display: block;
  text-align: right;
  margin-top: 1px;
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  user-select: none;
}
/* the proof grid the manager reviews — placeholders, not real photos */
.dm__photos {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2px;
  margin: 6px 0 2px;
}
.dm__photos span {
  aspect-ratio: 1;
  border-radius: 3px;
  background:
    linear-gradient(135deg, rgba(255,255,255,.07), rgba(255,255,255,.02)),
    var(--surface-3);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
}
.dm--quiet { opacity: 0.72; }
.dm--quiet i { color: var(--ink-3); font-style: italic; }
/* a real gap in time, said out loud rather than drawn */
.dm__gap {
  margin: var(--s-3) 0;
  text-align: center;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-3);
}
.dm--t { margin-left: auto; }
/* the client's own service line: centred, quiet, from neither side */
.demo__svc {
  margin: var(--s-2) 0;
  text-align: center;
  font-size: var(--t-xs);
  color: var(--ink-3);
  text-wrap: balance;
}
.demo__svc b { color: var(--ink-2); font-weight: 600; }

.dm {
  width: fit-content;
  max-width: 85%;
  padding: 0.34rem 0.6rem 0.36rem;
  border-radius: 13px;
  margin-bottom: 5px;
  /* two columns inside one phone width: the type steps down so a line of
     the bot's own copy still fits without breaking mid-word */
  font-size: clamp(0.8125rem, 2.7vw, 0.9375rem);
  line-height: 1.42;
}
.dm p { margin: 0; }
.dm b { font-weight: 600; }
.dm--bot {
  background: var(--surface-2);
  color: var(--ink-2);
  border-bottom-left-radius: var(--r-tail);
}
.dm--bot b { color: var(--ink); }
.dm--staff {
  margin-left: auto;
  background: var(--msg-out);
  color: #e8f1fb;
  border-bottom-right-radius: var(--r-tail);
}
.dm--staff b { color: #fff; }
/* the refusal is the point of the whole sequence — it reads as the
   client's own warning toast, not as another message */
.dm--toast {
  background: var(--alert-wash);
  border: 1px solid rgba(229, 105, 95, 0.4);
  color: var(--ink);
}

/* an inline keyboard, welded under the message it belongs to */
.dm__keys {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3px;
  margin: 6px -0.6rem -0.36rem;
}
.dm__keys span {
  padding: 0.34rem;
  text-align: center;
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--link);
  background: rgba(255, 255, 255, 0.06);
}
.dm__keys--one { grid-template-columns: 1fr; }
.dm__keys span:first-child { border-bottom-left-radius: 13px; }
.dm__keys span:last-child { border-bottom-right-radius: 13px; }

/* ---- the capability strip ------------------------------------
   Three things the bot does, on one line, at footnote size — the
   same register as "14 days free". Deliberately not a bulleted
   list: it reads as a caption under the job, not as an argument. */
.strip {
  list-style: none;
  margin: 0 0 var(--s-4);
  padding: 0 0 0 var(--bubble-x);   /* the same speech edge */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.6rem;
  max-width: var(--thread);
  /* holds one line on a 375 screen, then settles at the footnote
     size it shares with "14 days free" */
  font-size: clamp(0.6875rem, 3.05vw, 0.8125rem);
  color: var(--ink-2);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}
.strip li { display: flex; align-items: baseline; gap: 0.6rem; }
.strip li + li::before {
  content: "·";
  color: var(--ink-3);
}

/* a photo message carries its time on the image, on a scrim */
.msg--photo { position: relative; }
.msg--photo .meta {
  position: absolute;
  right: 11px;
  bottom: 11px;
  margin: 0;
  padding: 0.1rem 0.4rem;
  border-radius: var(--r-pill);
  background: rgba(8, 14, 20, 0.6);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: #dfe8f0;
}
.photo { padding-bottom: calc(var(--s-4) + 0.6rem); }


/* ---- hero line break ----------------------------------------
   Two deliberate lines once there is room for them; below that the
   title wraps on its own rather than breaking somewhere worse. */
.ln { display: block; }

/* ---- 3 · the greeting is an ordinary message ------------------
   It reads as something the bot said, not as a subtitle. */
.msg .whatis--top {
  font-size: var(--t-body);
  font-weight: 400;
  color: var(--msg-in-ink);
  line-height: var(--lh-body);
  max-width: none;
  margin: 0 0 var(--s-3);
}
.msg .lede {
  font-size: var(--t-body);
  font-weight: 400;
  color: var(--msg-in-ink);
  margin: 0;
}

/* ---- 2 · the thread is centred on the viewport ----------------
   The chat column is the page's axis; the chat list hangs off it,
   the way a Telegram window centres its thread rather than its
   whole chrome. */
@media (min-width: 64rem) {
  .shell { display: block; max-width: none; padding: 0; }
  .chat {
    max-width: var(--thread);
    margin: 0 auto;
    padding-left: 0;
    padding-right: 0;
  }
  /* the gutter is empty margin once the thread is centred, so the screen
     stops at the bubbles' own edge rather than hanging off into it */
  .demo,
  .swipe__slide .demo { margin-left: 0; }
  .composer__in,
  .bar__in {
    display: block;
    max-width: none;
    padding-left: 0;
    padding-right: 0;
  }
  .bar__in { padding-top: var(--s-2); padding-bottom: var(--s-2); }
  .composer__field { max-width: var(--thread); margin: 0 auto; }
  /* the bar's contents sit on the thread's own edges, not on a wider
     frame of their own — the action lines up with the bubbles */
  .bar__row { max-width: var(--thread); margin: 0 auto; }
  .foot p {
    max-width: var(--thread);
    padding: 0;
    text-align: left;
  }
}

/* The chat list only appears once the centred thread leaves room beside it;
   below this it would compute a negative offset and fall off-screen. */
@media (min-width: 88rem) {
  .rail {
    display: block;
    position: fixed;
    top: 5.2rem;
    left: calc(50% - var(--thread) / 2 - var(--gutter) - var(--s-6) - 17rem);
    width: 17rem;
    padding-top: 0;
  }
  /* It is chrome, not content. It stays out of the opening view and
     arrives once the reader has committed to scrolling. */
  .js .rail {
    opacity: 0;
    transform: translateX(-10px);
    pointer-events: none;
    transition: opacity 420ms var(--ease-out), transform 420ms var(--ease-out);
  }
  .js.scrolled .rail {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
  @media (prefers-reduced-motion: reduce) {
    .js .rail { transform: none; transition: opacity 420ms linear; }
  }
}

.msg.msg--title.msg--short > *::after { content: none; }
.msg.msg--title.msg--short { width: fit-content; }

/* the inline keyboard welds to whatever it actually follows */
.strip:has(+ .kbd--welded) { margin-bottom: var(--s-4); }


/* ---- the other side of the conversation ----------------------
   A section question is a message the visitor sent: right-aligned,
   brighter blue, and set at ordinary chat size — a person typing a
   question does not type it in heading type. */
.msg.msg--ask {
  margin-left: auto;
  margin-right: var(--gutter);
  max-width: 29rem;
  border-bottom-right-radius: var(--r-tail);
  border-bottom-left-radius: var(--r-bubble);
  box-shadow: var(--shadow-lift);
}
.msg.msg--ask h2 {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--t-body);
  font-weight: 500;
  line-height: 1.42;
  letter-spacing: 0;
  color: var(--msg-out-ink);
}
.msg.msg--ask p {
  margin: 0;
  font-weight: 500;
  color: var(--msg-out-ink);
}
.msg.msg--ask .meta { display: block; color: var(--msg-out-meta); }
.msg.msg--ask::before {
  content: "U";
  position: absolute;
  right: calc(var(--gutter) * -1);
  bottom: 0;
  width: var(--ava);
  height: var(--ava);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: linear-gradient(160deg, #74b8f0, #2a72c9);
  font-family: var(--font-ui);
  font-size: 0.72rem;
  font-weight: 700;
  color: #fff;
}
.msg.msg--ask::before { font-size: 0.85rem; }


/* ---- bare passages -------------------------------------------
   After the bot's opening message the hero drops out of bubbles
   and runs as plain page content, the way the H1 does. Hierarchy
   is carried by type and the accent, not by a container. */
.msg--bare {
  background: none !important;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: none;
  border-radius: 0;
  /* it lost the bubble, but it keeps the bubble's text edge */
  padding: 0 0 0 var(--bubble-x);
  width: auto;
  max-width: var(--thread);
  margin: var(--msg-gap) 0;
}
.msg--bare .meta,
.msg--bare::before,
.msg--bare::after { display: none; }
/* it has no stamp, so it reserves no room for one. Specificity has to
   clear the :has(+ .meta) reserve, which is why this is doubled up. */
.msg.msg--bare > *::after { content: none; }
.msg--bare .photo {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}
.msg--bare .jobtitle {
  font-size: var(--t-body);
  margin-bottom: var(--s-4);
}

/* the strip sits directly under the bare job block */
.strip { margin-bottom: var(--s-5); }
.strip:has(+ .kbd--welded) { margin-bottom: var(--s-5); }


/* ---- messages arrive once ------------------------------------
   Each element reveals as it enters, then is unobserved, so
   scrolling back up and down again leaves everything in place.
   Gated on .js so a no-script visitor gets the finished state. */
.js .rv { opacity: 0; transform: translateY(14px); }
.js .rv.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 400ms var(--ease-out), transform 400ms var(--ease-out);
}
/* the title arrives from the left, the way a message enters a thread */
.js .rv.rv--left { transform: translateX(-28px); }
.js .rv.rv--left.is-in {
  transform: none;
  transition: opacity 560ms var(--ease-out), transform 560ms var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .js .rv,
  .js .rv.rv--left { opacity: 1; transform: none; }
}

/* the job message fades in place — no travel */
.js .rv.rv--fade { transform: none; }
.js .rv.rv--fade.is-in { transition: opacity 620ms var(--ease-out); }

/* The per-tier payment key (2026-08-17). Sized and coloured as a control, not a
   link: Monobank's brandbook wants a payment button that reads as one, with no
   payment-system logos anywhere near it. */
.row__pay {
  justify-self: stretch;
  text-align: center;
  align-self: center;
  text-decoration: none;
  white-space: nowrap;
  /* Owner: the same size and weight as the price it sits beside. `.row` is
     --t-sm and $29 is a <b>, so this is --t-sm / 700 — not a smaller "link"
     size, which made the control read as secondary to its own row. */
  /* 600, not 700: <b> on this page is globally 600, so the price computes to
     600 and a literal "bold" here would have been one step heavier than the
     number it must match. Measured, not assumed. */
  font-weight: 600;
  font-size: var(--t-sm);
  padding: 0.4rem 0.7rem;
  border-radius: var(--r-btn);
  background: rgba(255, 255, 255, 0.08);
  color: var(--ink);
  transition: background var(--dur-1) var(--ease-out);
}
/* Measured: with the row on `align-items: baseline`, $29 / 95¢ / Pay sat at
   three different heights — 95¢ 1px low (smaller font, same baseline) and the
   Pay key 6.4px low (a box centred on itself, not on a baseline). Only the
   tier rows switch to centre alignment; the rest of the table keeps baseline,
   which is right for text-only rows. */
.row:has(.row__pay) { align-items: center; }
.row__pay:hover { background: rgba(255, 255, 255, 0.14); }
.row--best .row__pay { background: var(--accent); color: var(--on-accent); }

/* Measured, not guessed: at 375px the four fixed columns (price 72, per-day 64,
   Pay 74, plus three gaps) left the plan name minmax(0,1fr) about 7px, so
   "Monthly" wrapped to one letter per line and the row grew to 196px tall.
   Below 560px the Pay key therefore takes its own full-width line under its
   tier — which is also the bigger tap target — and the money columns above it
   keep the alignment the table is built on. */
@media (max-width: 560px) {
  /* The header must lose its fourth column with the data rows, or the two
     disagree on where the money columns start — measured 175px vs 191px, which
     is exactly the misalignment the fixed widths above exist to prevent. */
  /* Narrower than desktop: the struck price needs 7.5rem there, but at 375px
     that left the plan name 49px and wrapped "3 months" onto three lines.
     Measured, then tightened until every name fits on one. */
  .row:has(.row__pay), .row--head { grid-template-columns: minmax(0, 1fr) 6.25rem 3.25rem; }
  .row__was { font-size: 0.72rem; margin-right: 0.25rem; }
  .row--head > :nth-child(4) { display: none; }
  .row__pay {
    grid-column: 1 / -1;
    justify-self: stretch;
    text-align: center;
    margin-top: var(--s-3);
    font-size: var(--t-sm);
    padding: 0.6rem;
  }
}

/* The payment explainer, under the tier table rather than under the buttons:
   it describes what the in-row Pay keys do, so it belongs with them and not
   at the foot of the section. Quiet by default — it is reassurance for
   someone already looking at prices, not a claim competing with them. */
.paynote {
  max-width: var(--thread);
  margin: calc(var(--s-3) * -1) 0 var(--s-5);
  padding: var(--s-4) var(--s-4) var(--s-3);
  font-size: var(--t-xs);
  line-height: 1.55;
  color: var(--ink-3);
}
.paynote p { margin: 0 0 var(--s-3); }
.paynote p:last-child { margin-bottom: 0; }

/* The undiscounted price — what the same months would cost at the monthly
   rate. Struck and quiet: it is context for the real number beside it, not a
   second price competing with it. Monthly has none, because a saving against
   itself would be invented. */
.row__was {
  /* --ink-2, not --ink-3: at 13px the old grey read as "almost invisible"
     against the surface, and the strike has to be legible for the discount to
     land at all. */
  color: var(--ink-2);
  font-size: var(--t-xs);
  font-weight: 400;
  margin-right: 0.4rem;
  /* NOT text-decoration. Tried it with explicit thickness and skip-ink off and
     it still rendered faint and broken at 13px — the browser positions and
     anti-aliases that line itself and there is no way to make it obey. This
     draws the rule as a real box, so colour, thickness and position are exact
     and identical everywhere. */
  text-decoration: none;
  position: relative;
}
.row__was::after {
  content: "";
  position: absolute;
  left: -0.06em;
  right: -0.06em;
  top: 50%;
  height: 2px;
  transform: translateY(-50%);
  background: currentColor;
  /* Slightly under the digits it crosses, so the rule reads as a mark ON the
     old price rather than a second element competing with it. */
  opacity: 0.8;
  border-radius: 1px;
}
