/*
 * Telegram theme binding — PROTOTYPE.
 *
 * Two jobs, deliberately kept apart:
 *
 *   1. Bind SurveyJS to Telegram's theme variables. A BINDING TABLE, not a
 *      stylesheet of overrides. Every value is `var(--tg-theme-*)`; a literal
 *      colour anywhere in this file is a bug.
 *   2. The small amount of chrome SurveyJS does not draw — header, list rows,
 *      bottom action bar.
 *
 * Nothing here invents a colour. Telegram supplies them; light/dark comes free.
 */

/* ── 0 · One small scale — type and space, chrome AND survey ──────────────
 *
 * Every screen draws from these and nothing else. Four sizes, two weights, one
 * line-height ratio per script, five spacings and one radius. Not a framework: if a
 * screen needs a value that is not here, the screen is what gets questioned first.
 *
 * Space follows one rule (ADR-042, contracts §18.5): the gap above a block comes
 * from its relationship to the block above it, never from what the block is. No
 * component carries an outer top margin; the screen sets it (section 6). The only
 * literal spacings and radii left are named exemptions — shapes (bars, dots, round
 * marks), the chat mock, preview-only chrome and optical offsets — and the guard
 * (layout.test.js) keeps that list exact.
 *
 * Latin keeps SurveyJS's own ratio (1.5) so, in English, nothing inside the
 * survey moves. Persian letterforms carry dots and marks above and below the
 * line, and at 1.5 consecutive lines touch; 1.8 is applied to the WHOLE screen
 * at once — chrome, labels, inputs, hints, errors, buttons — never element by
 * element, which is how the chrome and the survey drifted apart before (1.9 vs
 * 1.5, measured).
 *
 * One family stack for both scripts. It is the platform's own: Telegram draws
 * its chats in it, so the Mini App reads as part of Telegram in either language.
 * A bundled Persian face was compared and did not clearly win once line-height
 * was unified (FOUNDATION-GATE §12) — nothing is fetched, nothing is bundled.
 */
:root {
  --av-font: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Roboto,
    "Helvetica Neue", sans-serif;
  --av-lh: 1.5;

  --av-title: 20px;      /* a screen's own heading */
  --av-body: 16px;       /* everything read or typed */
  --av-secondary: 14px;  /* hints, row values, subtitles */
  --av-caption: 13px;    /* group headings, the header's context line */
  --av-regular: 400;
  --av-strong: 600;

  --av-tight: 4px;       /* a line and the line under it, inside one component */
  --av-near: 8px;        /* a component and its own part: a heading and its card, a continuation */
  --av-gap: 12px;        /* between related blocks */
  --av-gutter: 16px;     /* screen edge to content, inside every surface, the header to the first block */
  --av-space: 24px;      /* between unrelated groups */
  --av-row: 48px;        /* a tappable row */
  --av-target: 44px;     /* anything else tappable */
  --av-radius: 10px;     /* every surface: a card, a notice, a version, a reference */

  /* Text colours, derived from the user's theme so small text holds WCAG AA
     (4.5:1) in light and dark alike. Telegram's own hint and link colours,
     used directly as 13-16px text, measured 3.0-4.3:1 on its default
     palettes (review-capture audit). They stay for icons and rules, which
     need 3:1; text uses these mixes. Still the theme's own colours — mixed,
     never invented, so no literal colour enters this file. */
  --av-muted: color-mix(in srgb, var(--tg-theme-text-color) 58%, var(--tg-theme-secondary-bg-color));
  --av-muted-card: color-mix(in srgb, var(--tg-theme-text-color) 58%, var(--tg-theme-section-bg-color));
  --av-muted-header: color-mix(in srgb, var(--tg-theme-text-color) 58%, var(--tg-theme-header-bg-color));
  --av-link: color-mix(in srgb, var(--tg-theme-link-color) 78%, var(--tg-theme-text-color));
  /* The shell. A phone fills it; a wider window (Telegram Desktop, a tablet) gets
     ONE centred column — header, content and the bottom action — at a reading
     width, never a stretched phone layout or a desktop redesign. */
  --av-shell: 640px;
  --av-danger: color-mix(in srgb, var(--tg-theme-destructive-text-color) 82%, var(--tg-theme-text-color));
  /* A press, answered: the theme's own text colour drawn at the target's edge.
     Measured (۳۰-۳۷): a wash OVER the surface cannot work here — the muted and
     link mixes above clear AA by 0.06-0.83, so any wash dark enough to be seen
     (the largest compliant one is under 1 %, invisible) takes text below 4.5:1.
     Drawn at the edge, where no text sits, a press can be seen and every label
     keeps exactly the contrast it had at rest. */
  --av-press: color-mix(in srgb, var(--tg-theme-text-color) 42%, transparent);
}
html[dir="rtl"] { --av-lh: 1.8; }

/* ── 1 · SurveyJS → Telegram ──────────────────────────────────────────────
 *
 * survey-core 3.0.3 defines its tokens on `:where(.sd-theme-root)` — a CLASS on
 * the survey container, not on :root. Custom properties resolve from the nearest
 * defining ancestor, so a `:root` binding is simply never seen. The first cut of
 * this file made exactly that mistake and the survey rendered in SurveyJS green
 * on a mint background. Bind at the same element, where `:where()`'s zero
 * specificity means a plain class selector wins.
 *
 * Ten bindings. That is the whole theme.
 */
.sd-theme-root {
  /* accent — buttons, focus rings, selected radio and checkbox */
  --sjs2-color-project-brand-600: var(--tg-theme-button-color);
  /* body text; secondary and tertiary derive from it by opacity */
  --sjs2-color-fg-basic-primary: var(--tg-theme-text-color);
  /* label ON the accent */
  --sjs2-color-fg-neutral-on-primary: var(--tg-theme-button-text-color);
  /* the page the survey sits on */
  --sjs2-color-utility-surface-survey: var(--tg-theme-secondary-bg-color);
  /* panels */
  --sjs2-color-bg-basic-primary: var(--tg-theme-section-bg-color);
  /* the recessed control surface — text fields, unselected radio and checkbox.
     Left unbound, these stayed #F5F5F5 and dark mode showed white inputs on a
     dark card. Telegram's secondary background is the same idea in both themes. */
  --sjs2-palette-gray-100: var(--tg-theme-secondary-bg-color);
  /* errors and the required marker — Telegram has its own opinion here */
  --sjs2-palette-red-600: var(--av-danger);
  --sjs2-palette-red-700: var(--av-danger);
  /* text drawn in the accent ("Add another", a switch's label) */
  --sjs2-color-fg-brand-primary: var(--av-link);
  /* the ring round text fields, radios and checkboxes. SurveyJS's grey is a
     light-theme grey: on Telegram's dark background it read as a white frame
     round every input (captured). Telegram's hint colour, at part strength,
     is a quiet outline in both themes. */
  --sjs2-color-border-basic-secondary: color-mix(in srgb, var(--tg-theme-hint-color) 45%, transparent);
  /* Typography. survey-core 3.0.3 defines 90 typography tokens and every one
     resolves from these three roots — so these are bound, and the 90 are left
     alone. Body is 2 font units and 3 line units in SurveyJS's own arithmetic.
     (The previous `family: inherit` only worked by accident: `inherit` on a
     custom property inherits the PROPERTY, which is unset above the survey, so
     font-family fell back to the page. Say what is meant instead.) */
  --sjs2-typography-font-family-text: var(--av-font);
  --sjs2-base-unit-font-size: calc(var(--av-body) / 2);
  --sjs2-base-unit-line-height: calc(var(--av-body) * var(--av-lh) / 3);
}

/* Layout. Same principle as type: SurveyJS 3 lays itself out from a few
 * layout tokens, and on a phone its own defaults already give a 16px gutter and
 * 16px inside every question. Four are rebound. The survey's box opened with a
 * 40px gap above the first question and closed with 80px below the last: the
 * screen sets the space above a form like any other block's (section 6), and the
 * bottom action already reserves the space below. */
.sd-theme-root {
  --sjs2-layout-component-survey-box-gap-vertical: 0px;
  --sjs2-layout-component-survey-box-padding-bottom: 0px;
  /* Choices were spaced by padding OUTSIDE their tappable label; the space moves
     inside it (section 3), so the outer gap goes — and with it the padding above the
     first choice and below the last, bound together (ADR-042). A radio list started
     28px under its title and a checkbox list 16px; both now start 16px under it and
     end 16px above the question's edge, as a text field does. The mark stays centred
     in its 48px row, so it does not move inside the row. */
  --sjs2-layout-component-labeled-group-box-gap-vertical: 0px;
  --sjs2-layout-component-labeled-group-box-padding-vertical: var(--sjs2-layout-component-labeled-group-box-gap-vertical);
}

/* ── 2 · The chrome SurveyJS does not draw ─────────────────────────────── */

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background: var(--tg-theme-secondary-bg-color);
  color: var(--tg-theme-text-color);
  font-family: var(--av-font);
  font-size: var(--av-body);
  font-weight: var(--av-regular);
  line-height: var(--av-lh);
  -webkit-font-smoothing: antialiased;
}

#app {
  position: relative;
  width: 100%;
  max-width: var(--av-shell);
  margin-inline: auto;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  /* Real values from the platform (Bot API 8.0+), not guesses. */
  padding-top: var(--tg-content-safe-area-inset-top, 0px);
  padding-left: var(--tg-safe-area-inset-left, 0px);
  padding-right: var(--tg-safe-area-inset-right, 0px);
  /* a screen coming in shifts sideways for a moment (ADR-044): nothing may widen
     or scroll the page while it does; `clip`, unlike `hidden`, is no scroll
     container, so the root navigation stays sticky */
  overflow-x: clip;
}

/* Header — where you are. The project is the subject; ARTVOID's name is
   already in Telegram's own title bar above this, so it is not repeated. */
#hdr {
  display: flex;
  align-items: center;
  gap: var(--av-tight);
  background: var(--tg-theme-header-bg-color);
  padding: var(--av-gap) var(--av-gutter);
  border-bottom: 1px solid var(--tg-theme-section-separator-color);
}
#hdr .texts { min-width: 0; flex: 1; }
/* Title and context keep Latin line-height in both scripts: Persian needs its
   taller line only where lines stack. Measured: the header was 8px taller in
   Persian; now the same in both. A long name wraps rather than being cut: the
   header is where a project or a deliverable says which one it is, and an
   ellipsis there hid exactly that (the long-value states, L01–). */
#hdr .title, #hdr .sub {
  margin: 0;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
#hdr .title { font-weight: var(--av-strong); }
#hdr .sub { font-size: var(--av-caption); color: var(--av-muted-header); }

/* Icons. Directional ones (.icon-dir) are drawn pointing the LTR way and
   mirrored ONCE here for RTL: Back points right in Persian, a row's chevron
   points left. Never text arrows: the characters used before are
   bidi-mirrored, so flipping them as well pointed them the wrong way (review
   PDF p. 43, p. 55). Checkmarks, brand marks, images and PDFs never mirror. */
.icon { display: block; width: 24px; height: 24px; flex: none; }
.icon path { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.icon circle { fill: currentColor; }
html[dir="rtl"] .icon-dir { transform: scaleX(-1); }

/* Stands in for Telegram's BackButton outside Telegram (and for a client too
   old to have one); inside, the platform draws it in its own header. A 44px
   target at the start edge, the arrow optically on the gutter line (an optical
   offset, exempt from the scale). */
#backbtn {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--av-target);
  height: var(--av-target);
  margin-inline-start: -10px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 50%;
  color: var(--tg-theme-link-color);
  cursor: pointer;
}
#backbtn.hidden { display: none; }
/* Keyboard focus is always visible, on everything the app draws itself. */
#backbtn:focus-visible, .row:focus-visible, .next:focus-visible,
.ref:focus-visible, .linkbtn:focus-visible, #mainbtn:focus-visible,
.field select:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--tg-theme-link-color);
  outline-offset: -2px;
}
.ref-add:focus-within .ref-thumb, label.row:focus-within {
  outline: 2px solid var(--tg-theme-link-color);
  outline-offset: -2px;
}

/* One pressed state, for as long as the press lasts (ADR-042, contracts §18.7):
   a hairline at the target's own edge, and nothing else — nothing grows, moves,
   waits or is delayed, so a press never reads as a change of state. An inset
   shadow, so no background, background image or layout is touched, and it is the
   same in both directions. It follows a target's own corners — square on a row,
   which a card clips, rounded on a card or a button — and where a focus ring sits
   over it — 2 px of the accent colour, inset — the hairline's third pixel still
   shows, so a key press on a focused target shows both. (The first-step button
   puts its ring outside itself, so nothing overlaps there at all.) Only what the app itself draws and acts on:
   not a static row, not a disabled button, not SurveyJS's own controls or the
   reference tiles inside them, and not the preview-only bottom action. (Three of the nine
   carry no :not(:disabled): two are labels, which cannot be disabled — the row that
   replaces a file and the one that adds files — and the header's Back, which is
   never disabled anywhere, only shown or hidden.) Colour
   alone says nothing here — it echoes a touch the finger already knows about.
   The platform's own tap highlight stays until a device shows this in its
   place (ROADMAP ۳۰-۲۸). */
button.row:not(:disabled):active,
label.row:active,
.next:not(:disabled):active,
.linkbtn:not(:disabled):active,
.file-act:not(:disabled):active,
.file-add:active,
.primary-inline:not(:disabled):active,
#rootnav button:not(:disabled):active,
#backbtn:active {
  box-shadow: inset 0 0 0 3px var(--av-press);
}

/* Progress — a hairline under the header. Not a stepper, not a percentage. */
#progress {
  height: 2px;
  background: var(--tg-theme-section-separator-color);
}
#progress > i {
  display: block;
  height: 2px;
  background: var(--tg-theme-button-color);
  transition: width .25s ease;
}

#view {
  flex: 1;
  padding-bottom: 112px; /* room for the bottom action */
}

/* Grouped list. Inset and rounded, like the survey's own question groups, so a
   screen of ours and a screen of SurveyJS's are visibly the same app. Group
   headings align with the text inside the group, not with its edge. The space
   above a group is the screen's to set (section 6). */
.group > h2 {
  font-size: var(--av-caption);
  font-weight: var(--av-regular);
  color: var(--av-muted);
  margin: 0 0 var(--av-near);
  padding: 0 calc(var(--av-gutter) * 2);
}
.card {
  margin: 0 var(--av-gutter);
  background: var(--tg-theme-section-bg-color);
  border-radius: var(--av-radius);
  overflow: hidden;
}
.row {
  display: flex;
  align-items: center;
  gap: var(--av-gap);
  padding: var(--av-gap) var(--av-gutter);
  min-height: var(--av-row);
  color: inherit;
  background: none;
  border: 0;
  width: 100%;
  font: inherit;
  text-align: start;
  cursor: pointer;
}
.row + .row { border-top: 1px solid var(--tg-theme-section-separator-color); }
/* A row's value moves under its label, to the end edge, only when the two no
   longer fit side by side — with text at 200 %, never at the normal size (the
   label keeps at least 7em beside the value — enough for the longest status,
   "Gathering information", at 360 px). Nothing is cut either way. */
.row { flex-wrap: wrap; }
.row .label { flex: 1 1 7em; min-width: 0; }
.row .value { margin-inline-start: auto; }
/* a row's value and its chevron: one unit at the end edge, wrapping together */
.row .trail { display: flex; align-items: center; gap: var(--av-near); margin-inline-start: auto; }
.row .trail .value { margin-inline-start: 0; }
.row .label b { display: block; font-weight: var(--av-regular); }
.row .label small {
  display: block;
  font-size: var(--av-secondary);
  color: var(--av-muted-card);
}
.attn { color: var(--av-link); }
.row .value {
  font-size: var(--av-secondary);
  color: var(--av-muted-card);
  white-space: nowrap;
}
.row .chev { width: 20px; height: 20px; color: var(--tg-theme-hint-color); opacity: .6; }
/* A row with nowhere to go is not drawn as a button. */
.row.static { cursor: default; }
.row.danger .label b { color: var(--av-danger); }
.row.link .label b { color: var(--av-link); }

/* Review states: complete is quiet, missing is the one thing in colour,
   optional recedes. Colour never carries the meaning alone — the word does. */
.row .value.is-complete { color: var(--av-muted-card); }
.row .value.is-need { color: var(--av-danger); }
.row .value.is-optional { color: var(--av-muted-card); }
.row .value.is-attn { color: var(--av-link); }
/* An access capability's state is a label, not a control (ROADMAP ۳۰-۲۴, ۳۰-۳۹): an icon
   and the word, so it never rests on colour. Enabled - the check, the text's own colour, a
   very light neutral fill. Not enabled - a dash, muted text, a hairline outline. Both are
   mixed from the theme's text colour: no green, no red, nothing a theme did not supply. */
.row .value.status {
  display: inline-flex;
  align-items: center;
  gap: var(--av-tight);
  padding: var(--av-tight) var(--av-near);
  border-radius: var(--av-radius);
}
.row .value.status .icon { width: 16px; height: 16px; }
.row .value.status.is-on {
  color: var(--tg-theme-text-color);
  background: color-mix(in srgb, var(--tg-theme-text-color) 7%, transparent);
}
.row .value.status.is-off {
  color: var(--av-muted-card);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tg-theme-text-color) 22%, transparent);
}

/* A fact: its label above it, so a long value wraps instead of squeezing. */
.row.info { flex-direction: column; align-items: stretch; gap: var(--av-tight); }
.row.info .k { font-size: var(--av-caption); color: var(--av-muted-card); }
.row.info .v { overflow-wrap: anywhere; }

/* A row titled in the client's words (app.js titledRow) — a request. The title
   is the row: its own block, regular weight, the full width beside the chevron.
   It runs and aligns its own way (.said carries its dir), as client words do
   everywhere in the app. Three lines, then an ellipsis: the whole title is one
   tap away (data-full). Its own alignment is also what keeps that ellipsis
   honest: aligned to the row's edge instead, a Persian title in the English
   interface lost its last word with no ellipsis at all (measured, L05) — the
   ellipsis falls at the text's own end, which was then flush against the clip.
   Under it, secondary and in the interface's direction, the status and the
   reference — the reference whole, never broken inside. The chevron is centred
   on the whole row, never on one line of the title. */
.row.titled { flex-wrap: nowrap; }
.row.titled .label { flex: 1 1 auto; }
.row.titled .label .title > .said {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.row.titled .label .meta { margin-top: var(--av-tight); }
.row.titled .meta .machine { white-space: nowrap; }
.row.titled .trail { flex: none; }

/* The next action — the first thing on a project, and the only large thing. */
.next {
  display: block;
  width: calc(100% - var(--av-gutter) * 2);
  margin: 0 var(--av-gutter);
  padding: var(--av-gutter);
  background: var(--tg-theme-section-bg-color);
  border: 0;
  border-radius: var(--av-radius);
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}
.next > span { display: block; }
.next .next-label {
  font-size: var(--av-caption);
  color: var(--av-muted-card);
}
.next .next-title {
  font-size: var(--av-title);
  font-weight: var(--av-strong);
  margin-top: var(--av-tight);
}
.next .next-sub {
  font-size: var(--av-secondary);
  color: var(--av-muted-card);
  margin-top: var(--av-tight);
}
.next .next-note { display: block; margin-top: var(--av-tight); color: var(--tg-theme-text-color); }

/* Where things stand: the stage, one sentence, the latest change. */
.card.status { padding: var(--av-gap) var(--av-gutter); }
.status p { margin: 0; }
.status .status-stage { font-weight: var(--av-strong); }
.status .status-line { margin-top: var(--av-tight); }
.status .status-latest { margin-top: var(--av-near); font-size: var(--av-secondary); color: var(--av-muted-card); }

/* What a client wrote, as the subject of a screen. */
.said-title {
  padding: 0 calc(var(--av-gutter) * 2);
  font-size: var(--av-title);
  font-weight: var(--av-strong);
}
.said-title .said { display: block; }
/* Client-written text standing alone on its line takes its own direction's
   alignment: an English title in a Persian row wraps from the left. */
.said { unicode-bidi: isolate; }
.next-sub > .said:only-child, .row .label b > .said:only-child,
.row .label small > .said:only-child, .row.info .v > .said:only-child { display: block; }

/* An answer carried from an earlier brief says where it came from. */
.carried { margin: var(--av-tight) 0 0; font-size: var(--av-caption); color: var(--av-muted-card); }

/* A form's own Previous: quiet, under the form, a full 44 px target — Back leaves the form instead. */
.form-prev { padding: 0 var(--av-gutter); }
.form-prev .linkbtn { display: inline-flex; align-items: center; gap: var(--av-tight); padding-inline-start: var(--av-tight); }
.form-prev .icon { width: 20px; height: 20px; }

/* The draft's save state: quiet unless it failed. */
.saveline { margin: var(--av-near) 0 0; font-size: var(--av-caption); color: var(--av-muted); }
.saveline:empty { display: none; }
.saveline.is-failed { color: var(--av-danger); display: flex; align-items: center; flex-wrap: wrap; gap: var(--av-tight); }
.linkbtn {
  min-height: var(--av-target);
  padding: 0 var(--av-near);
  background: none;
  border: 0;
  border-radius: var(--av-radius);
  color: var(--av-link);
  font: inherit;
  font-weight: var(--av-strong);
  cursor: pointer;
}

/* Said once, for the whole project — never once per area. */
.calm { padding: 0 calc(var(--av-gutter) * 2); margin: 0; }
.calm p { margin: 0; }
.calm .calm-sub { color: var(--av-muted); font-size: var(--av-secondary); margin-top: var(--av-tight); }

/* A section's own heading, with where it sits in the brief — quietly. */
.section-head { padding: 0 calc(var(--av-gutter) * 2); }
.section-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--av-gap);
}
.section-desc { margin: var(--av-tight) 0 0; color: var(--av-muted); }
.section-head h1 {
  font-size: var(--av-title);
  font-weight: var(--av-strong);
  margin: 0;
}
.section-head .count {
  flex: none;
  font-size: var(--av-caption);
  color: var(--av-muted);
}

.lead {
  margin: 0;
  padding: 0 calc(var(--av-gutter) * 2);
  font-size: var(--av-secondary);
  color: var(--av-muted);
}

/* A message from the studio, or a fact about this screen: an ordinary neutral
   surface, the same radius and inset as the groups beside it. No coloured side
   bar. Only what asks the client to act is tinted — the whole surface, faintly,
   with a hairline outline — so a routine "Approved" never looks like a warning.
   The words carry the meaning; colour only supports them. */
.notice {
  margin: 0 var(--av-gutter);
  padding: var(--av-gap) var(--av-gutter);
  background: var(--tg-theme-section-bg-color);
  border-radius: var(--av-radius);
}
.notice p { margin: 0; }
.notice .notice-from {
  font-size: var(--av-caption);
  font-weight: var(--av-strong);
  color: var(--av-muted-card);
}
.notice .notice-from + .notice-body { margin-top: var(--av-tight); }
/* something failed: act on it */
.notice.notice-error {
  background: color-mix(in srgb, var(--tg-theme-destructive-text-color) 9%, var(--tg-theme-section-bg-color));
  border: 1px solid color-mix(in srgb, var(--tg-theme-destructive-text-color) 32%, transparent);
}
/* not known yet: check before anything else */
.notice.notice-warn,
#survey .asked-note {
  background: color-mix(in srgb, var(--tg-theme-link-color) 8%, var(--tg-theme-section-bg-color));
  border: 1px solid color-mix(in srgb, var(--tg-theme-link-color) 30%, transparent);
}

/* A version, shown whole: the page is the subject. */
.preview {
  display: flex;
  justify-content: center;
  margin: 0 var(--av-gutter);
  padding: var(--av-gutter);
  border-radius: var(--av-radius);
  background: var(--tg-theme-section-bg-color);
}
.preview img {
  display: block;
  max-width: 100%;
  max-height: 44vh;
  box-shadow: 0 1px 4px color-mix(in srgb, var(--tg-theme-text-color) 22%, transparent);
}
.preview.small img { max-height: 22vh; }

/* Bottom action — stands in for Telegram's MainButton outside Telegram. Preview-only
   chrome, drawn to Telegram's measure: its 10px and 50px are exempt from the scale. */
#bottom {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  max-width: var(--av-shell);
  margin-inline: auto;
  padding: 10px var(--av-gutter) calc(10px + var(--tg-content-safe-area-inset-bottom, 0px));
  background: var(--tg-theme-bottom-bar-bg-color);
  border-top: 1px solid var(--tg-theme-section-separator-color);
}
#bottom.hidden { display: none; }
/* The stand-in lies over the foot of the page; Telegram's own bar sits outside the
   webview. So what the browser brings into view — the control Tab focuses — stops
   above it, as in Telegram. Its footprint is this box: 1px border, 10px padding,
   the button (50px, or its label's line and the browser's 1px padding when that is
   taller — Persian text at 200%), 10px padding and the safe area. Never in Telegram. */
html:has(#bottom:not(.hidden)) {
  scroll-padding-bottom: calc(1px + 10px + max(50px, var(--av-body) * var(--av-lh) + 2px) + 10px + var(--tg-content-safe-area-inset-bottom, 0px));
}
#mainbtn {
  width: 100%;
  min-height: 50px;
  border: 0;
  border-radius: var(--av-radius);
  /* the theme's colours, held at AA by the app (MainButton.setParams → the shim) */
  background: var(--av-main-bg, var(--tg-theme-button-color));
  color: var(--av-main-text, var(--tg-theme-button-text-color));
  font: inherit;
  font-weight: var(--av-strong);
  cursor: pointer;
}
#mainbtn[disabled] { opacity: .4; }
/* sending: the spinner in place of the label, never on top of it — the label is
   still there for assistive tech (aria-busy says what is happening) */
#mainbtn.busy { position: relative; color: transparent; }
#mainbtn.busy::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 20px;
  height: 20px;
  border: 2px solid var(--av-main-text, var(--tg-theme-button-text-color));
  border-inline-end-color: transparent;
  border-radius: 50%;
  animation: av-spin 1s linear infinite;
}
@keyframes av-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { #mainbtn.busy::after { animation: none; } }

/* A message that is the whole screen — or a receipt's heading — starts where every
   screen starts (section 6), not lower. */
.done { padding: 0 calc(var(--av-gutter) * 2); text-align: center; }
.done h1 { font-size: var(--av-title); font-weight: var(--av-strong); margin: 0 0 var(--av-gap); }
.done p { color: var(--av-muted); margin: 0 0 var(--av-near); }
.done > :last-child { margin-bottom: 0; }

.emptystate {
  margin: 0;
  padding: 0 calc(var(--av-gutter) * 2);
  text-align: center;
  color: var(--av-muted);
}

/* The language choice: both languages at once, two equal options. */
.langpick { padding: 0 calc(var(--av-gutter) * 2); text-align: center; }
.langpick-title { margin: 0; font-size: var(--av-title); font-weight: var(--av-strong); }
.langpick-title + .langpick-title { margin-top: var(--av-tight); }
.row.langpick-row { justify-content: center; min-height: 56px; }
.row.langpick-row .label { flex: none; }
.row.langpick-row .label b { font-weight: var(--av-strong); }

/* PROTOTYPE ONLY — the ARTVOID chat, drawn so the first-run conversation can
   be reviewed next to the screens it opens. Telegram draws the real one. */
/* Telegram keeps the message list left-to-right in every language — sent on
   the right, received on the left — and lets each line of text run its own way. */
.chat { direction: ltr; display: flex; flex-direction: column; gap: 6px; padding: var(--av-gutter); }
.msg {
  max-width: 82%;
  padding: 8px 12px;
  border-radius: 16px;
  background: var(--tg-theme-bg-color);
}
.msg p { margin: 0; }
.msg.in { align-self: flex-start; }
.msg.out {
  align-self: flex-end;
  background: color-mix(in srgb, var(--tg-theme-button-color) 16%, var(--tg-theme-bg-color));
}
.kb { display: flex; gap: 6px; width: 82%; }
.kb-btn {
  flex: 1;
  padding: 10px 8px;
  border-radius: var(--av-radius);
  text-align: center;
  font-size: var(--av-secondary);
  font-weight: var(--av-strong);
  background: color-mix(in srgb, var(--tg-theme-bg-color) 72%, transparent);
  color: var(--av-link);
}

/* ── Image lists — compared side by side ─────────────────────────────
 *
 * Drawn through SurveyJS's own extension point (`renderAs`): the question's
 * frame, title, description, errors and value stay SurveyJS's; only the list
 * of tiles is drawn here.
 */
/* The paneldynamic frame leaves padding to its own panels and sets nowrap for
   its toolbar; a custom body takes the frame's padding from SurveyJS's own
   tokens, so it moves with every other question. */
.refgrid-body {
  white-space: normal;
  padding: 0 var(--sjs2-layout-component-panel-simple-content-area-padding-horizontal)
    var(--sjs2-layout-component-panel-simple-content-area-padding-bottom);
}
/* The line under the title separates a stack of panels; a grid has none. */
.av-image-list .sd-paneldynamic__header { border-bottom: 0; }
.refgrid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--av-gap);
}
.ref {
  display: flex;
  flex-direction: column;
  gap: var(--av-near);
  min-width: 0;
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}
.ref-thumb {
  display: block;
  aspect-ratio: 1;
  width: 100%;
  border-radius: var(--av-radius);
  overflow: hidden;
  background: var(--tg-theme-secondary-bg-color);
}
.ref-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.ref-cap {
  font-size: var(--av-caption);
  color: var(--av-muted-card);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ref-cap.add { color: var(--av-link); }
.ref-add .ref-thumb {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--av-tight);
  border: 1px solid var(--tg-theme-section-separator-color);
  background: none;
  color: var(--av-link);
  font-size: var(--av-secondary);
}
.ref-add .ref-plus { font-size: 28px; line-height: 1; }
/* A composed board (PDF) is a document tile: its kind, then its name. */
.ref-doc {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--av-tight);
  padding: var(--av-near);
  border: 1px solid var(--tg-theme-section-separator-color);
  background: var(--tg-theme-section-bg-color);
}
.ref-doc-kind { font-size: var(--av-caption); font-weight: var(--av-strong); }
.ref-doc-name {
  max-width: 100%;
  font-size: var(--av-caption);
  color: var(--av-muted-card);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ref-view.ref-doc { min-height: 160px; }
/* one reference, opened: its whole name — the tile may shorten it, this is where it is whole */
.ref-view .ref-doc-name { white-space: normal; overflow-wrap: anywhere; text-align: center; }
.ref-error {
  margin: var(--av-gap) 0 0;
  font-size: var(--av-secondary);
  color: var(--av-danger);
}
.ref-hint {
  margin: var(--av-gap) 0 0;
  font-size: var(--av-secondary);
  color: var(--av-muted-card);
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* One reference, full width. */
.ref-view {
  margin: 0 var(--av-gutter);
  border-radius: var(--av-radius);
  overflow: hidden;
  background: var(--tg-theme-section-bg-color);
}
.ref-view img { display: block; width: 100%; max-height: 40vh; object-fit: contain; }
/* Shaped like a SurveyJS question: title, then the recessed field. */
.field { padding: var(--av-gutter); }
.field-title { display: block; font-weight: var(--av-strong); margin-bottom: var(--av-gap); }
.field select {
  display: block;
  width: 100%;
  min-height: var(--av-row);
  padding: 0 var(--av-gap);
  border: 1px solid color-mix(in srgb, var(--tg-theme-hint-color) 45%, transparent);
  border-radius: var(--av-radius);
  background: var(--tg-theme-secondary-bg-color);
  color: inherit;
  font: inherit;
}
.field-error { margin: var(--av-near) 0 0; font-size: var(--av-secondary); color: var(--av-danger); }
.field textarea[aria-invalid="true"] { border-color: var(--tg-theme-destructive-text-color); }
.field textarea {
  display: block;
  width: 100%;
  min-height: 96px;
  padding: var(--av-gap);
  border: 1px solid var(--tg-theme-section-separator-color);
  border-radius: var(--av-radius);
  background: var(--tg-theme-secondary-bg-color);
  color: inherit;
  font: inherit;
  resize: vertical;
}

/* ── 3 · SurveyJS — the two rules left ──────────────────────────────────
 *
 * Title, page titles, progress and navigation are switched off with SurveyJS's
 * own options (app.js), not hidden here. What remains cannot be expressed as a
 * token or an option.
 */

/* SurveyJS draws each question's frame as a 1px shadow ring; Telegram's groups
   have no outline, and match the grouped lists above in radius. */
#survey .sd-element--with-frame {
  box-shadow: none;
  border-radius: var(--av-radius);
}

/* A read-only answer can still be reached and read by keyboard or screen reader;
   SurveyJS draws no focus for it. In a field, its frame takes SurveyJS's own focused
   look — the rounded border turning blue — never a square outline on the field itself. */
#survey .sd-formbox:has([readonly]:focus-visible) {
  box-shadow: var(--sjs2-border-effect-component-formbox-default-reset), var(--sjs2-border-effect-component-formbox-focused), var(--sjs2-border-effect-component-formbox-a11y);
}
#survey [readonly]:not(.sd-formbox *):focus-visible { outline: 2px solid var(--tg-theme-link-color); outline-offset: 1px; }

/* An accepted brief is a record, not a form (ROADMAP ۳۰-۲۲). SurveyJS draws the
   answer itself (app.js initLocales: a read-only text or comment is a div, never an
   input), but it still puts a text answer inside the field's own box. Read-only, that
   box gives up its ring, its white ground and its inset, so the answer reads as the
   document's text under a quieter, caption-like question: the client's line breaks
   kept, wrapping where it must, a long link or token never running past the card, and
   each answer running its own way (app.js freezeRecord sets its dir, as the client's
   words do everywhere). Never greyed: the words keep the page's own colour and size.
   Only read-only — an editable field keeps every bit of its chrome and its focus. */
#survey .sd-question--readonly .sd-formbox--readonly {
  box-shadow: none;
  background: none;
  border-radius: 0;
  padding: 0;
}
#survey .sd-question--readonly .sd-formbox--readonly > div:not([class]),
#survey .sd-question--readonly .sd-question__content > div:not([class]) {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  unicode-bidi: isolate;
}
/* A question of the record nobody answered says so, quietly (app.js freezeRecord). */
#survey .not-answered { color: var(--av-muted-card); }
#survey .sd-question--readonly .sd-question__title {
  font-size: var(--av-secondary);
  font-weight: var(--av-regular);
  color: var(--av-muted-card);
}

/* SurveyJS actions are 40px; everything the client taps should clear 44. */
#survey .sd-action,
#survey .sd-formbox__input { min-height: var(--av-target); }
/* a file's remove button measured 32-40px wide, its name 16px tall */
#survey .sd-action { min-width: var(--av-target); }
#survey .sd-file a { display: inline-flex; align-items: center; min-height: var(--av-target); }

/* A choice is tapped anywhere on its row. The tappable label measured 24px tall
   and only as wide as its words; the rows keep their 48px rhythm, which is now
   all target. */
#survey .sd-selectbase__label {
  display: flex;
  width: 100%;
  min-height: var(--av-row);
  align-items: center;
}

/* Theme/locale switcher — PROTOTYPE ONLY. Never ships; exempt from the scale. */
#devbar {
  position: fixed;
  bottom: 0; inset-inline-start: 0;
  z-index: 99;
  display: flex;
  gap: 2px;
  padding: 2px;
  font-size: 9px;
  opacity: .25;
}
#devbar button {
  font: inherit;
  border: 0;
  padding: 2px 4px;
  background: var(--tg-theme-hint-color);
  color: var(--tg-theme-bg-color);
  border-radius: 3px;
  cursor: pointer;
}

/* ── 4 · Correction pass: fewer containers, honest states ─────────────────
 *
 * One card per form section. SurveyJS's own panelless mode (app.js:
 * isCompact) removes the frame round every question; the section's content
 * becomes one card, questions divided by space and a hairline. Fewer
 * containers, the same touch targets, the same Persian line height.
 */
#survey .sd-root--compact .sd-page { padding: 0 var(--av-gutter); }
#survey .sd-root--compact .sd-page__content {
  box-sizing: border-box;
  /* SurveyJS's page is a column that aligns its children to the start, so the
     card shrank to its content (245 of 358px, G05): it stretches to the shell */
  align-self: stretch;
  min-inline-size: 0;
  width: auto;
  margin: 0;
  padding: 0 var(--av-gutter);
  background: var(--tg-theme-section-bg-color);
  border-radius: var(--av-radius);
  row-gap: 0; /* SurveyJS spaces rows 24px apart; here a hairline and the question's own padding divide them */
}
#survey .sd-root--compact .sd-page__row + .sd-page__row { border-top: 1px solid var(--tg-theme-section-separator-color); }
#survey .sd-root--compact .sd-page__row > .sd-element-wrapper > .sd-question { padding: var(--av-gutter) 0; }

/* ARTVOID's question, drawn inside the question it is about — read before the
   label, answered right under it (app.js: onAfterRenderQuestion) */
#survey .asked-note {
  white-space: normal; /* SurveyJS sets nowrap on the question it sits in */
  margin: 0 0 var(--av-gap);
  padding: var(--av-gap) var(--av-gutter);
  border-radius: var(--av-radius);
}
#survey .asked-note p { margin: 0; }
#survey .asked-note .asked-from { font-size: var(--av-caption); font-weight: var(--av-strong); color: var(--av-muted-card); }
#survey .asked-note .asked-text { margin-top: var(--av-tight); }

/* F09: a field's error is one short line, 8px under the field, in its own words —
   SurveyJS's role="alert" box, linked to the field by aria-describedby — never a
   second tinted card touching the field */
#survey .sd-element__erbox {
  margin: var(--av-tight) 0 0; /* + SurveyJS's own 4px between a field and its message: 8px clear */
  padding: 0;
  background: none;
  border-radius: 0;
  color: var(--av-danger);
  font-size: var(--av-secondary);
}
/* a group of choices has ONE message: the options themselves stay as they are */
#survey .sd-item--error.sd-checkbox:not(.sd-checkbox--checked) .sd-checkbox__decorator {
  background-color: var(--sjs2-color-component-checkbox-false-default-bg);
  box-shadow: var(--sjs2-border-effect-component-checkbox-false-default);
}
#survey .sd-item--error.sd-radio:not(.sd-radio--checked) .sd-radio__decorator {
  background-color: var(--sjs2-color-component-radio-false-default-bg);
  box-shadow: var(--sjs2-border-effect-component-radio-false-default);
}

/* A section's answers in the review: enough to know what was sent without opening
   the section — a short answer whole, a long one previewed (app.js summary) — with
   this clamp as the visual limit, five lines, and the whole of it one tap away in
   the section itself (data-full, ROADMAP ۳۰-۲۲). */
.row .label small.sum { display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; }
.row.latest small { color: var(--av-muted-card); }
.row.info.secondary .k, .row.info.secondary .v, .lead.secondary { font-size: var(--av-caption); color: var(--av-muted-card); }
.row.info.quiet .v, .muted { color: var(--av-muted-card); }

/* approval: what exactly is being approved, before anything else */
.approve-card {
  margin: 0 var(--av-gutter);
  padding: var(--av-gutter);
  border-radius: var(--av-radius);
  background: var(--tg-theme-section-bg-color);
}
.approve-card p { margin: 0; }
.approve-card .approve-what { font-size: var(--av-title); font-weight: var(--av-strong); }
.approve-card .approve-where { margin-top: var(--av-tight); }
.approve-card .approve-meta { margin-top: var(--av-near); font-size: var(--av-secondary); color: var(--av-muted-card); }
.approve-card .approve-who { margin-top: var(--av-gap); font-weight: var(--av-strong); }

/* the current language, marked — not only listed first */
.row.langpick-row.is-current .label small { color: var(--av-muted-card); font-size: var(--av-caption); }
.row.langpick-row .icon { color: var(--av-link); }

/* attached files: each named, its state said, every control labelled */
.filelist-body { white-space: normal; }
.file-list { list-style: none; margin: 0 0 var(--av-gap); padding: 0; border: 1px solid var(--tg-theme-section-separator-color); border-radius: var(--av-radius); }
/* The name decides: a name that fits on one line keeps its action beside it; a
   name that needs the room — a long one, or any at large text — takes the whole
   row, and the action moves under it. Never a narrow column of name squeezed
   beside "Remove" (L14, L15). */
.file-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--av-tight) var(--av-gap); min-height: 56px; padding: var(--av-near) var(--av-gap); }
.file-row + .file-row { border-top: 1px solid var(--tg-theme-section-separator-color); }
.file-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--av-tight); }
/* the state line and the progress bar fill the name's width but never widen it */
.file-main .file-state, .file-main .file-bar { width: 0; min-width: 100%; }
.file-row .file-act { margin-inline-start: auto; }
/* the whole name, extension included: wrapped, never cut — the extension is what the file is */
.file-name { overflow-wrap: anywhere; }
.file-row:focus-visible { outline: 2px solid var(--tg-theme-link-color); outline-offset: -2px; }
.file-state { font-size: var(--av-caption); color: var(--av-muted-card); }
.file-row.is-failed .file-state { color: var(--av-danger); }
.file-bar { display: block; height: 3px; border-radius: 2px; overflow: hidden; background: var(--tg-theme-section-separator-color); }
.file-bar i { display: block; height: 100%; background: var(--tg-theme-button-color); }
.file-act, .file-add {
  display: inline-flex;
  align-items: center;
  gap: var(--av-tight);
  min-height: var(--av-target);
  min-width: var(--av-target);
  padding: 0 var(--av-gap);
  border: 0;
  border-radius: var(--av-radius);
  background: var(--tg-theme-secondary-bg-color);
  color: var(--av-link);
  font: inherit;
  font-size: var(--av-secondary);
  font-weight: var(--av-strong);
  cursor: pointer;
}
.file-act .icon, .file-add .icon { width: 18px; height: 18px; }
.file-act:focus-visible, .file-add:focus-within { outline: 2px solid var(--tg-theme-link-color); outline-offset: -2px; }

/* a known choice, with a quiet way to change it */
.row .value.is-link { color: var(--av-link); }
/* files that will not go, said under the list before Send */
.lead.files-left-out { color: var(--av-danger); }
.ref-hint.files-none { color: var(--av-danger); }

/* A machine-like value — a file name, a URL, a reference, a version id — keeps
   its own left-to-right order inside a sentence in either language (direction.js:
   <bdi dir="ltr">, which isolates), and a long one wraps rather than overflowing. */
.machine { unicode-bidi: isolate; overflow-wrap: anywhere; }

/* ── 5 · The shell: roots, Home, a part's journey (PROTOTYPE, ADR-041) ───────
 *
 * The root navigation shows on every browse screen — the three roots, Home,
 * Projects and Account, and the places read from them — where it is the one thing
 * at the foot of the screen: a browse screen has no main button, and a pushed one
 * adds a Back in the header (app.js applyShell, ADR-041 amendment). A focus screen
 * has the main button instead, never both. It is the app's own, inside the webview, so
 * unlike the stand-in bottom action its footprint is reserved in Telegram too.
 * Its labels keep Latin line-height in both scripts, as the header does: the bar
 * is the same height in English and Persian. It sits in the page's own flow, held
 * at the foot of the screen, so the page always ends above it at whatever height
 * its labels need — at 200% text a Persian label takes two lines. */
#rootnav {
  position: sticky;
  bottom: 0;
  display: flex;
  padding: var(--av-tight) var(--av-tight) calc(var(--av-tight) + var(--tg-content-safe-area-inset-bottom, 0px));
  background: var(--tg-theme-bottom-bar-bg-color);
  border-top: 1px solid var(--tg-theme-section-separator-color);
}
#rootnav button {
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--av-target);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* from the top: a label that wraps grows its own item downwards, and the three icons stay on one line */
  justify-content: flex-start;
  gap: var(--av-tight);
  padding: var(--av-tight);
  background: none;
  border: 0;
  border-radius: var(--av-radius);
  color: var(--av-muted);
  font: inherit;
  font-size: var(--av-caption);
  line-height: 1.5;
  cursor: pointer;
}
/* the current root: the accent, and a weight — never colour alone */
#rootnav button[aria-current="page"] { color: var(--av-link); font-weight: var(--av-strong); }
#rootnav .rootnav-label { max-width: 100%; text-align: center; overflow-wrap: anywhere; }
#rootnav button:focus-visible { outline: 2px solid var(--tg-theme-link-color); outline-offset: -2px; }
/* What Tab brings into view stops above it: its footprint is this box — 1px border,
   the bar's padding, the button's, the 24px icon, the gap, two lines of label (a label
   that keeps to one only leaves a little more room), the button's and the bar's
   padding again, and the safe area. */
html:has(#rootnav:not([hidden])) {
  scroll-padding-bottom: calc(1px + var(--av-tight) * 5 + 24px + var(--av-caption) * 1.5 * 2 + var(--tg-content-safe-area-inset-bottom, 0px));
}
/* A browse screen has no main button, so none of its room is kept either; and a short
   one still has its bar at the foot of the screen — the shell is at least the webview's
   stable height (Telegram's own variable; the preview's window outside it). */
html:has(#rootnav:not([hidden])) #view { padding-bottom: var(--av-space); }
html:has(#rootnav:not([hidden])) #app { min-height: var(--tg-viewport-stable-height, 100dvh); }

/* Home: each question a group; the first need is the lead card, its button the
   root's one action, in the main button's own colours (held at AA by the app). The
   card that continues it in the same group sits near it. */
.group > .next + .card, .group > .notice + .card { margin-top: var(--av-near); }
/* A group's own note, under its card (Account's access footer): near it, aligned with
   the group's heading, one line per sentence. */
.group > .group-note {
  margin: var(--av-near) 0 0;
  padding: 0 calc(var(--av-gutter) * 2);
  font-size: var(--av-caption);
  color: var(--av-muted);
}
.group-note > span { display: block; }
.next .next-action {
  display: block;
  margin-top: var(--av-gap);
  padding: var(--av-gap) var(--av-gutter);
  border-radius: var(--av-radius);
  background: var(--av-action-bg, var(--tg-theme-button-color));
  color: var(--av-action-text, var(--tg-theme-button-text-color));
  font-weight: var(--av-strong);
  text-align: center;
}
/* No project yet: the one action in the page. */
.first-step { padding: 0 var(--av-gutter); }
.primary-inline {
  width: 100%;
  min-height: 50px;
  border: 0;
  border-radius: var(--av-radius);
  background: var(--av-action-bg, var(--tg-theme-button-color));
  color: var(--av-action-text, var(--tg-theme-button-text-color));
  font: inherit;
  font-weight: var(--av-strong);
  cursor: pointer;
}
.primary-inline:focus-visible { outline: 2px solid var(--tg-theme-link-color); outline-offset: 2px; }

/* A part's journey (proposal §4) — steps, not a bar that fills: done restrained,
   the current one in the accent, what is still to come quiet. Small, under the
   part's row; its words are the row's own. Its own line under the row's text,
   spanning the row's content column (ROADMAP ۳۰-۲۹, which replaced the bounded
   60 % of ۳۰-۲۱): it starts where the text starts and ends where the row's
   status and chevron end, so the row's own inset is its inset on both sides and
   the track never floats in the middle of the row. Mirrored by direction; the
   steps spread evenly along it and keep their size. */
.row .journey { flex-basis: 100%; display: block; margin-top: var(--av-near); }
.journey-track { display: flex; align-items: center; width: 100%; }
.journey-step { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--tg-theme-section-separator-color); }
.journey-step.is-done { background: var(--av-muted-card); }
.journey-step.is-current {
  width: 10px;
  height: 10px;
  background: var(--tg-theme-link-color);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--tg-theme-link-color) 22%, transparent);
}
/* the track's own geometry — a shape, exempt from the scale */
.journey-link { flex: 1 1 0; min-width: 6px; height: 2px; margin: 0 3px; background: var(--tg-theme-section-separator-color); }
.journey-link.is-done { background: var(--av-muted-card); }
/* In full, on the part's own page: each step named, the current one with its sentence. */
.journey-full { padding: var(--av-near) var(--av-gutter); }
.journey-full ol { list-style: none; margin: 0; padding: 0; }
.journey-full li { display: flex; align-items: flex-start; gap: var(--av-gap); padding: var(--av-near) 0; color: var(--av-muted-card); }
/* the mark sits optically on the step's first line: an optical offset, exempt */
.journey-full .journey-mark { flex: none; width: 20px; height: 20px; margin-top: 2px; display: flex; align-items: center; justify-content: center; }
.journey-full .journey-mark::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--tg-theme-section-separator-color); }
.journey-full li.is-done .journey-mark::before { content: none; }
.journey-full li.is-done .icon { width: 18px; height: 18px; color: var(--av-muted-card); }
.journey-full li.is-current { color: var(--tg-theme-text-color); }
.journey-full li.is-current .journey-mark::before { width: 10px; height: 10px; background: var(--tg-theme-link-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tg-theme-link-color) 22%, transparent); }
.journey-full .journey-text b { font-weight: var(--av-regular); }
.journey-full li.is-current .journey-text b { font-weight: var(--av-strong); }
.journey-full .journey-text small, .journey-now small { display: block; font-size: var(--av-secondary); color: var(--av-muted-card); }
.journey-now { margin: var(--av-near) 0; }

/* ── 6 · The screen sets the space between its blocks (ADR-042) ────────────
 *
 * A block on a screen — a group, a notice, a version, a form, a line of text —
 * carries no top margin of its own. Its gap comes from here, from what it is to the
 * block above it (contracts §18.5):
 *
 *   the first block    --av-gutter below the header, or the top of the screen
 *   unrelated          --av-space — the default
 *   related            --av-gap — a notice and what it explains, a title and its
 *                      status, a version or a reference and its facts, a line of
 *                      text and the card it introduces
 *   a heading's own    --av-near — a form section's heading and its form, the
 *                      language choice's title and its options, as a group's
 *                      heading and its card
 *
 * The audit (review-capture, `layout`) measures every screen against this. */
#view > * + * { margin-top: var(--av-space); }
#view > :first-child { margin-top: var(--av-gutter); }
/* the chat mock draws Telegram's own chat, edge to edge: exempt */
#view > .chat:first-child { margin-top: 0; }

#view > .said-title + .group,
#view > .notice + :is(.preview, .ref-view, .lead, #survey),
#view > .notice + .group:not(:has(> h2)),
#view > .calm + .notice,
#view > .approve-card + .lead,
#view > .lead + :is(.next, .preview, .lead),
#view > .lead + .group:not(:has(> h2)),
#view > :is(.preview, .ref-view) + .group:not(:has(> h2)),
#view > .preview + .lead,
#view > .done + :is(.group, .first-step),
#view > .done.receipt ~ .lead,
#view > .group:not(:has(> h2)) + #survey,
#view > #survey + :is(.form-prev, .lead),
#view > .form-prev + .lead { margin-top: var(--av-gap); }

#view > :is(.section-head, .langpick) + :is(#survey, .group) { margin-top: var(--av-near); }
