/* natalka/app/static/app.css — shell layout: loader (frame L) + safe-area page frame.
   Screen content lives in partials; this file only sets the frame every screen shares. */

* { box-sizing: border-box; }

/* Task 4.4 fix: a `hidden` element whose own class sets `display` (e.g. `.chip`,
   `friend-date-lock-invite`) otherwise stays visible — a class selector outranks the UA
   `[hidden]` rule's specificity. One global override, ahead of every other rule in this file. */
[hidden] { display: none !important; }

/* A3: one visible focus ring for every control (inputs use the border instead, see below) */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.g-row:focus-visible { outline-offset: -2px; border-radius: var(--r-chip); }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font: var(--t-body);
  -webkit-font-smoothing: antialiased;
}

body {
  padding-top: var(--tg-content-safe-area-inset-top, 0px);
  padding-bottom: var(--tg-content-safe-area-inset-bottom, 0px);
  padding-left: var(--tg-content-safe-area-inset-left, 0px);
  padding-right: var(--tg-content-safe-area-inset-right, 0px);
  min-height: 100vh;
}

#screen {
  padding: 0 var(--pad-x);
  opacity: 0;
  transition: opacity var(--unblur-ms) var(--ease-out);
}

body.ready #screen { opacity: 1; }

/* Frame L — full-screen loader, visible until body.ready */
.loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--loader-bg);
  transition: opacity 200ms var(--ease-out), visibility 200ms var(--ease-out);
}

.loader-monogram {
  width: 96px;
  height: 96px;
  transform: translateY(-12vh); /* monogram centre sits at ~44% of screen height */
}

.loader-monogram svg { width: 100%; height: 100%; display: block; }

body.ready .loader {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* Voice field (product) — DIRECTION.md "System beyond chat": ONE flat sunflower field per
   screen carrying Natalka's current line; no speaker row outside chat — the field itself
   carries a 24px monogram top-right (inset 14px), text right-padded to clear it. */
.voice-field {
  position: relative;
  margin-top: var(--gap-l);
  background: var(--voice);
  color: var(--voice-ink);
  border-radius: var(--r-field);
  padding: 16px 48px 16px 16px;
}

.voice-field .voice-line {
  margin: 0;
  font: var(--t-voice);
  font-variation-settings: var(--flar-voice);
}

.voice-field .monogram-badge {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 32px;   /* her face (canon №326); stays inside the 48px right gutter */
  height: 32px;
}

.voice-field .monogram-badge svg { width: 100%; height: 100%; display: block; }

/* Onboarding screens (Task 2.6, style C, DIRECTION frames A/B) */

.progress-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: var(--gap-m);
}

.progress-label { font: var(--t-meta); color: var(--muted); }
.progress-row[data-role="fm-progress"] { justify-content: flex-end; }

.progress-track { display: flex; gap: var(--gap-xs); }

.progress-seg { width: 20px; height: 4px; border-radius: var(--r-hair); background: var(--line-strong); }
.progress-seg.on { background: var(--accent); }

.card {
  margin-top: var(--gap-m);
  background: var(--surface);
  border-radius: var(--r-card);
  padding: var(--gap-l);
}

.card-title { font: var(--t-title); color: var(--text); }

.rows { margin-top: var(--gap-m); display: flex; flex-direction: column; gap: 10px; }

.row-label { font: var(--t-body); font-weight: 600; color: var(--text); display: inline-block; }

.row-label.marked {
  background: linear-gradient(transparent 55%, var(--marker) 55%);
  background-size: 100% 100%;
  padding: 0 3px;
}

.row-text {
  margin-top: var(--gap-xs);
  font: var(--t-body);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-text.open { white-space: normal; }

.row-text.blurred {
  filter: blur(var(--blur));
  opacity: var(--blur-opacity);
  user-select: none;
  height: 1em;
  background: var(--line);
  border-radius: var(--r-small);
}

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

.row.reveal .row-label.marked { background-size: 0 100%; animation: mark-sweep var(--marker-ms) var(--ease-out) var(--marker-delay) forwards; }
.row.reveal .row-text.open { filter: blur(var(--blur)); opacity: var(--blur-opacity); animation: unblur var(--unblur-ms) var(--ease-out) forwards; }

@keyframes mark-sweep { to { background-size: 100% 100%; } }
@keyframes unblur { to { filter: blur(0); opacity: 1; } }
@keyframes calc-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .row.reveal .row-label.marked { animation: none; background-size: 100% 100%; }
  .row.reveal .row-text.open { animation: none; filter: blur(0); opacity: 1; }
}

.speaker-row { display: flex; align-items: center; gap: var(--gap-s); margin-top: var(--gap-l); }
.speaker-row .avatar { width: var(--avatar); height: var(--avatar); flex: none; display: block; }
.speaker-row .avatar svg { width: 100%; height: 100%; display: block; }
.speaker-photo { width: 100%; height: 100%; display: block; border-radius: 50%; object-fit: cover; }
.speaker-name { font: var(--t-meta); color: var(--muted); }

.voice-line.plain { margin-top: var(--gap-s); font: var(--t-voice); font-variation-settings: var(--flar-voice); color: var(--text); }

.reply {
  margin-top: var(--gap-m);
  margin-left: auto;
  width: fit-content;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-reply);
  padding: 0 20px;
  height: var(--btn-h);
  display: flex;
  align-items: center;
  font: var(--t-reply);
  color: var(--text);
}

.voice-field.tail { border-radius: var(--r-voice); margin-top: var(--gap-m); }
.vf-meta { font: var(--t-meta); color: var(--voice-ink); opacity: 0.7; }
.vf-text { margin-top: var(--gap-xs); font: var(--t-voice); font-variation-settings: var(--flar-voice); color: var(--voice-ink); }
.vf-text.no-meta { margin-top: 0; }
.vf-link { display: inline-block; margin: calc(10px - var(--tap-pad)) 0 calc(0px - var(--tap-pad)); padding: var(--tap-pad) 0; color: var(--voice-ink); }   /* --tap-min hit area, layout as before (margin-top 10); font + underline: see .vf-link, .pair-quiet below */

.date-field { margin-top: var(--gap-l); }
.field-label { display: block; font: var(--t-label); color: var(--text); margin-bottom: var(--gap-xs); }
.field-hint { font: var(--t-body); color: var(--muted); margin-top: var(--gap-s); }
.field-error { font: var(--t-meta); color: var(--text); margin-top: var(--gap-xs); }

input[data-role], .name-input, .country-input {
  width: 100%;
  height: var(--btn-h);
  padding: 0 16px;
  border-radius: var(--r-chip);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--text);
  font: var(--t-body);
}
/* Polish: a placeholder is lighter than typed text, so an empty field never reads as filled, and still 4.5:1 on the field (its own token) */
input::placeholder, textarea::placeholder { color: var(--placeholder); opacity: 1; }
/* A3/A9: no UA blue ring on a field; focus = the ink border, an error = the 1.5px ink border (aria-invalid is set
   by a11y.js while a .field-error is shown) */
input[data-role]:focus-visible, .name-input:focus-visible, .country-input:focus-visible { outline: 0; border-color: var(--accent); }
input[aria-invalid="true"] { border-width: var(--bw-state); border-color: var(--text); }
.screen-name .name-input { margin-top: var(--gap-m); }   /* canon №137: step 1, the field alone under her line */

/* canon №137 follow-up (builder's call №111, 2026-09-25): visible "work" on the calculating
   screen — a thin bar filling toward completion (JS sets its width/duration, app.js token-only
   colours), and a checklist that accumulates one line at a time, the current one spinning,
   finished ones checked off and dimmed. */
/* the waiting progress IS the onboarding segment track (templates/partials/blocks.html wait_progress); chart.js lights it */
.progress-row[data-role="calc-progress"] { justify-content: flex-end; }
.progress-seg { transition: background-color var(--state-ms) var(--ease-out); }

.calc-card { margin-top: var(--gap-m); }
.calc-card .calc-checklist { margin-top: 0; }
.calc-checklist { margin-top: var(--gap-m); list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--gap-s); }
.calc-item { display: flex; align-items: flex-start; gap: var(--gap-s); }   /* the mark sits on the first line when a step wraps */
.calc-item .calc-text { min-width: 0; font: var(--t-voice); font-variation-settings: var(--flar-voice); color: var(--text); text-wrap: pretty; overflow-wrap: anywhere; }   /* a long step wraps, never an ellipsis */
.calc-item.done .calc-text { color: var(--muted); }

.calc-mark { width: 16px; height: 16px; margin-top: 6px; /* (28px voice line - 16px) / 2 */ flex: none; border-radius: 50%; box-sizing: border-box; }
.calc-item.active .calc-mark { border: 2px solid var(--progress-off); border-top-color: var(--accent); animation: calc-spin 800ms linear infinite; }
.calc-item.done .calc-mark { background: var(--accent); position: relative; }
.calc-item.done .calc-mark::after {
  content: ""; position: absolute; left: 4px; top: 2px; width: 5px; height: 8px;
  border: solid var(--accent-ink); border-width: 0 2px 2px 0; transform: rotate(45deg);
}

/* the retry state (showChartError) reuses .calc-item for its two lines + the retry chip, stacked
   instead of the usual mark+text row. */
.calc-item.calc-error { display: block; }
.calc-item.calc-error .calc-text { display: block; }
.calc-item.calc-error .chips { margin-top: var(--gap-s); }

.chips { margin-top: var(--gap-m); display: flex; flex-wrap: wrap; gap: var(--gap-s); }

.chip {
  height: var(--tap-min);
  padding: 0 16px;
  border-radius: var(--r-chip);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--text);
  font: var(--t-chip);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.chip .pencil { width: 14px; height: 14px; flex: none; display: block; }

/* Task 4.4: a native `disabled` button keeps this class's own colours otherwise — dim it the
   same way .guess-chips already dims a tap in flight, everywhere a `.chip` can be disabled
   (chart.slots's manual-friend submit until the form is valid). */
.chip:disabled { opacity: 0.5; pointer-events: none; }

/* canon №126 (D) + polish B1: ONE style per kind of in-page action. Secondary = the outlined .chip above
   (the chat preview's pill is the same look); primary = the full-width 48px twin of the
   native BottomButton (same colours, same radius), so one action looks the same at a root and on a depth screen;
   quiet = an underlined text link with a 44px hit area. */
button.chip, .btn-primary { appearance: none; -webkit-appearance: none; cursor: pointer; }
.btn-primary { display: flex; align-items: center; justify-content: center; width: 100%; height: var(--btn-h); padding: 0 var(--gap-l);
  border: 0; border-radius: var(--r-chip); background: var(--main-btn); color: var(--main-btn-text); font: var(--t-chip); }
.btn-primary:disabled { background: var(--progress-off); color: var(--muted); cursor: default; }
.vf-link, .pair-quiet { font: var(--t-chip); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 2px; }

.chip.selected, .chip[aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: transparent;
}

.subfield, .place-field, .born-ukraine { margin-top: var(--gap-m); }

.relatives-button { appearance: none; -webkit-appearance: none; cursor: pointer; margin-top: var(--gap-s); }
.relatives-hint { margin-top: var(--gap-s); }

/* polish B2: the suggestions are one grouped list card (the row spec lives in reading.css) */
/* №318 row 27 / P14c: the chosen city stays in the field with a check; a long name is shown in full under it */
.place-field.place-picked { position: relative; }
.place-field.place-picked input { padding-right: 44px; text-overflow: ellipsis; }
.place-field.place-picked::after { content: "\2713"; position: absolute; top: 0; right: 16px; height: var(--btn-h); display: flex; align-items: center; font: var(--t-row); color: var(--text); pointer-events: none; }
.place-chosen-full { margin: var(--gap-s) 0 0; font: var(--t-row-sub); color: var(--muted); overflow-wrap: anywhere; }
.place-results { list-style: none; margin: var(--gap-s) 0 0; padding: 0 var(--gap-l); background: var(--surface); border-radius: var(--r-card); }
.place-results:empty { display: none; }
.place-results li { display: flex; flex-direction: column; gap: 0; }
.place-results li + li { border-top: 1px solid var(--line); }
/* Owner (2026-09-26): the oblast (plus country outside Ukraine) tells apart same-named places —
   same smaller/muted meta pair used elsewhere on this screen (.progress-label, .speaker-name). */

/* Dark theme: --nm-tile and --voice are both #F7D743 there, so the badge's tile blends into the
   field it sits on and disappears. Reuse the MainButton pair instead of inventing new colours: since №318 row 30
   the dark --main-btn is light (#F4F3F0) and --main-btn-text is dark (#141317), so the badge is a dark tile with
   light letters. */
[data-theme="dark"] .voice-field .monogram-badge {
  --nm-tile: var(--main-btn-text);
  --nm-ink: var(--main-btn);
}

/* Chart screen (Task 3.4, DIRECTION frame C): the finished short-about-you card sits on the
   sunflower field, r20 padding 20 — the ONLY yellow on this screen; everything below is quiet
   ink/grey (`.chart-section`, DIRECTION "System beyond chat" §heading/§sub tokens). */
.voice-card {
  margin-top: var(--gap-l);
  background: var(--voice);
  color: var(--voice-ink);
  border-radius: var(--r-field);
  padding: 20px;
}

.voice-card .cof-body { margin-top: var(--gap-m); }
.voice-card .cof-body p { margin: var(--gap-m) 0 0; font: var(--t-body); color: var(--voice-ink); }
.voice-card .cof-body p:first-child { margin-top: 0; }
.voice-card .cof-body strong { font-weight: 600; }
/* №125: a label is its own paragraph, never smaller than the text under it */
.voice-card .cof-body .run-label { font: var(--t-body); font-weight: 600; margin-bottom: calc(var(--gap-xs) - var(--gap-m)); }

.chart-section { margin-top: var(--gap-section); }
.sign-block { margin-top: var(--gap-m); background: var(--surface); border-radius: var(--r-card); padding: 14px var(--gap-l); font: var(--t-body); color: var(--text); }
.sign-block p { margin: 0; }
.sign-block .run-label { font: var(--t-label); margin-bottom: var(--gap-xs); }
.sign-block.task { background: transparent; border: 1.5px dashed var(--line); }
.chart-section .sign-extra { margin-top: var(--gap-m); color: var(--muted); }
.chart-section .sign-extra blockquote { margin: var(--gap-m) 0 0; padding: 0 0 0 var(--gap-m); border-left: 2px solid var(--line); }
.chart-section .sign-extra blockquote:first-child { margin-top: 0; }
.chart-section .sign-extra blockquote p { margin: 0; }
/* The farewell ends the chart (canon №127): Natalka's own line, nothing sold in it. */
.farewell { margin-top: var(--gap-xl); font: var(--t-voice); font-variation-settings: var(--flar-voice); color: var(--text); }
.farewell p { margin: 0; }
.chart-section .vf-link { color: var(--accent); }         /* not on a voice field here — override */
.chart-section .section-body { margin-top: var(--gap-m); font: var(--t-body); color: var(--text); }
.chart-section .section-body p { margin: var(--gap-m) 0 0; }
.chart-section .section-body p:first-child { margin-top: 0; }
.chart-section .section-body strong { font-weight: 700; }
.chart-section .section-body .run-label { font: var(--t-body); font-weight: 600; margin-bottom: calc(var(--gap-xs) - var(--gap-m)); }
.chart-section .section-body ol, .chart-section .section-body ul { margin: var(--gap-m) 0 0; padding-left: 20px; }
.chart-section .section-body li { margin-top: var(--gap-xs); }

/* Task 5.3: offer.transition, right after the upsell section — Natalka's own voice (line 1),
   then the price-free promise in the bolder --t-voice-strong weight (line 2, №98). No card, no
   yellow field: same quiet ink-on-bg surface as the sections around it. */
.offer-field { margin-top: var(--gap-xl); }
.offer-field .offer-line { margin-top: var(--gap-m); font: var(--t-voice); color: var(--text); }
.offer-field .offer-line:first-child { margin-top: 0; }
.offer-field .offer-line-strong { font: var(--t-voice-strong); }
.offer-field > .chart-section:first-child { margin-top: 0; }
.offer-more { margin-top: var(--gap-m); }
.offer-more > summary { list-style: none; color: var(--accent); }
.offer-more > summary::-webkit-details-marker { display: none; }
.offer-more .chart-section { margin-top: var(--gap-m); }

/* Chart-to-chat prompt (Task 3.5, save.ask): a quiet service card, not a line from Natalka
   herself — same surface as the onboarding `.card`, not the yellow `.voice-card`, and sits after
   the last chart section with the same rhythm as one (`--gap-xl`). */
.save-ask {
  margin-top: var(--gap-xl);
  background: var(--surface);
  border-radius: var(--r-card);
  padding: var(--gap-l);
}
.save-ask-text { margin: 0; font: var(--t-body); color: var(--text); }
.save-ask .chips { margin-top: var(--gap-m); }
.tail-steps > .save-ask:first-child { margin-top: var(--gap-xl); }

/* Task 4.3: the g_ guess screen — same .chip as elsewhere, just tappable and dimmed while the
   answer is in flight. */
.guess-chips .chip { cursor: pointer; }
.guess-chips .chip.disabled { opacity: 0.5; pointer-events: none; }

/* Task 4.4: chart.slots — the friend-places block reuses .save-ask's surface; only the parts
   .save-ask does not already cover (the accepted-friends list, the manual form's checkbox row)
   get their own rule here. */
.slots-count { color: var(--muted); }
.slots-list { list-style: none; margin: var(--gap-m) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--gap-xs); }
.link-button {
  appearance: none; -webkit-appearance: none; border: none; background: none; cursor: pointer;
  padding: 0; font: var(--t-body); color: var(--accent); text-decoration: underline;
  text-underline-offset: 3px; text-align: left;
}
.chip-check { display: flex; align-items: center; gap: 8px; margin-top: var(--gap-m); font: var(--t-body); color: var(--text); }
.chip-check input { width: 18px; height: 18px; flex: none; }

/* Task 9: full-chart contents/chapter screens — style C, no new colours or fonts. The chapter
   title/body and the what-to-do block reuse .chart-section/.section-heading/.section-body
   verbatim (same look as the short chart's own body sections); the next / to-contents pair reuses
   .chips (same as save-ask's yes/no row); the mid-chapter Natalka quote reuses .voice-field, just
   without its reserved monogram-badge gutter (`.plain`) since no badge sits in it here. Only the
   contents list itself (row/locked/unread) has no existing analogue, so it gets its own rules. */
/* the right gutter is reserved only for the monogram badge (the plain .voice-field with one): the tail and plain fields have none and use the full width */
.voice-field.tail, .voice-field.plain { padding-right: var(--pad-x); }
.voice-field .voice-line p { margin: 0; font: inherit; }

/* Review fix (Group A quality review, 2026-09-26): the pre-A6 contents markup (`<ol class=
   "contents">`, `.title`/`.note`/`.mark` spans) is gone — full_contents.html now renders the
   shared `.grouped`/`.g-row` list (reading.css). These rules' higher selector specificity was
   beating the new grouped-list styles (card padding lost, a doubled divider from both this
   `.contents-row` border-bottom AND `.g-row + .g-row`'s border-top, and `.title`'s font winning
   over `.g-label`'s) — removed rather than fixed in place, since nothing renders them any more. */

/* "why?" explanation opened under its link (app.js delegated handler) */
.vf-link { cursor: pointer; }
.vf-why { margin: var(--gap-s) 0 0; font: var(--t-note); color: inherit; }
[data-role="chart-end"] { height: 1px; }

/* canon №133: the chart-ending deck — one full-height card at a time (static/ending.js) */
.ending { margin-top: var(--gap-xl); }
/* №139: a card switch is one short fade in place (CSS restarts it whenever a card is unhidden) —
   no timed build, no smooth scroll; reduced motion turns it off (tokens.css) */
.ending-card { display: flex; flex-direction: column; gap: var(--gap-m); animation: card-in 180ms var(--ease-out); }
@keyframes card-in { from { opacity: 0; } to { opacity: 1; } }
.ending-card[hidden], .fm-step[hidden] { display: none; }
/* №138: the offer lines sat glued to the last build line; an ending card has no avatar row, so no top gap */
[data-role="offer-lines"] { margin-top: var(--gap-m); }
.ending-card .voice-card > .cof-body:first-child { margin-top: 0; }
.build-lines { list-style: none; margin: var(--gap-m) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--gap-s); }
.cut-mark { display: block; margin: var(--gap-s) 0; font: var(--t-meta); color: var(--muted); }
/* №139: the read mark is a quiet status; only the begun chapter keeps the marker highlight */
.full-contents .contents-row .mark-read, .full-portraits .contents-row .mark-read { background: none; padding: 0; color: var(--muted); }
.just-revealed { animation: just-revealed 1.2s var(--ease-out) 1; }
@keyframes just-revealed { from { background: var(--marker); } to { background: transparent; } }

/* E1 (plan 2026-09-29-empty-screens, canon 148): the deck cards that ask or offer show what the action gives.
   Tokens only - both themes follow the Telegram theme variables through tokens.css. */
.ending-card .speaker-row { margin-top: 0; }
/* G / g_buy: the friend places as round tiles - empty = dashed ring with a plus, used = Natalka's monogram */
.slot-row { display: flex; flex-direction: column; align-items: center; gap: var(--gap-l); padding: var(--gap-2xl) 0 var(--gap-xl); }
.slot-tiles { display: flex; gap: var(--gap-xl); justify-content: center; }
.slot-tile { width: 88px; height: 88px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  border: 1.5px dashed var(--line); background: var(--surface); color: var(--muted); }
.slot-tile .icon { width: 28px; height: 28px; }
.slot-tile.filled { border-style: solid; border-color: transparent; }
.slot-tile .avatar { width: var(--avatar-l); height: var(--avatar-l); display: block; }
.slot-tile .avatar svg { width: 100%; height: 100%; display: block; }
.slot-row .slots-count { margin: 0; text-align: center; }
/* B4: what arrives in the chat - a Telegram-like message and the service line with its button label */
.chat-preview { display: flex; flex-direction: column; gap: var(--gap-s); padding: var(--gap-s) 0 var(--gap-l); }
.chat-bubble { background: var(--surface); border-radius: var(--r-voice); padding: var(--gap-m) var(--gap-l); }
.chat-body { margin: 0; font: var(--t-body); color: var(--text); white-space: pre-line; text-wrap: pretty; }
/* both bubbles show only the opening of their message, faded out (E4: the saved line stops before the sentence
   the question card already says); the first one is also height-capped */
.chat-first .chat-body { max-height: 9.9em; overflow: hidden; }
.chat-fade .chat-body {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.6em), transparent); mask-image: linear-gradient(to bottom, #000 calc(100% - 2.6em), transparent); }
.chat-pill { margin-top: var(--gap-m); display: flex; align-items: center; justify-content: center; width: 100%; height: var(--tap-min);
  border: 1px solid var(--line-strong); border-radius: var(--r-chip); background: var(--surface); font: var(--t-chip); color: var(--text); }

/* ===== E2 (canon №148): friend form + locked chapter on the onboarding pattern — separate block ===== */
.screen-friend-manual .fm-step > input { margin-top: var(--gap-m); }
.screen-friend-manual .fm-step .chip[data-role="friend-date-lock-invite"] { margin-top: var(--gap-s); }
.locked-chapter { margin-top: var(--gap-m); padding: var(--gap-m) var(--gap-l); background: var(--surface); border-radius: var(--r-card); }
.locked-chapter .lc-title { margin: 0; font: var(--t-label); color: var(--text); }
.locked-chapter .lc-promise { margin: var(--gap-xs) 0 0; font: var(--t-body); color: var(--muted); }

/* == motion (style D1, canon №231): the voice enters with a short rise, its lines settle one by one, a pushed screen slides 24px in.
   Every duration and curve is a token; nothing moves under prefers-reduced-motion (the one rule at the end of tokens.css). == */
@keyframes voice-enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes voice-line { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.field, .voice-field, .voice-card { animation: voice-enter var(--enter-ms) var(--ease-voice) both; }
.field > :nth-child(-n+3), .voice-field > :nth-child(-n+3), .voice-card > :nth-child(-n+3) { animation: voice-line var(--enter-ms) var(--ease-voice) both; }
.field > :nth-child(2), .voice-field > :nth-child(2), .voice-card > :nth-child(2) { animation-delay: var(--stagger-ms); }
.field > :nth-child(3), .voice-field > :nth-child(3), .voice-card > :nth-child(3) { animation-delay: calc(2 * var(--stagger-ms)); }

@keyframes push-in { from { opacity: 0; transform: translateX(var(--push-x)); } to { opacity: 1; transform: none; } }
@keyframes push-back { from { opacity: 0; transform: translateX(calc(-1 * var(--push-x))); } to { opacity: 1; transform: none; } }
/* a forward push shifts the new screen in from the right: for the --nav-clear-ms of the animation #screen holds the overshoot (no horizontal scroll, no zoom-out);
   the attribute is removed right after, so nothing stays clipped. Back enters from the left, which never overflows. */
#screen[data-motion="push"] { overflow-x: clip; }
#screen[data-motion="push"] > :first-child { animation: push-in var(--push-in-ms) var(--ease-voice) both; }
#screen[data-motion="back"] > :first-child { animation: push-back var(--push-out-ms) var(--ease-voice) both; }
