/* The Mini App's baseline. Mobile-first, Telegram-themed, safe-area aware.
 *
 * Colours come from Telegram's theme variables with a fallback, so the app is
 * readable in a plain browser and correct inside the client. Applying the
 * theme parameters from the SDK is a sibling Story; using the variables here
 * costs nothing and means it has somewhere to write them.
 */

:root {
  --bg: var(--tg-theme-bg-color, #ffffff);
  --fg: var(--tg-theme-text-color, #10161f);
  --muted: var(--tg-theme-hint-color, #646e7a);
  --section-bg: var(--tg-theme-secondary-bg-color, #f2f4f7);
  --accent: var(--tg-theme-link-color, #1f6ac4);
  --button-fg: var(--tg-theme-button-text-color, #ffffff);
  /* The modal's backdrop. Named here rather than written where it is used,
     because **every colour in this file lives in this block or in the two
     scheme blocks below** — `TestNoScreenHardcodesAColour` fails on one
     anywhere else, and a scrim is exactly the kind of value that gets typed
     inline during a rushed fix and then stops following the theme. */
  --scrim: rgba(0, 0, 0, 0.4);
  --gap: 12px;

  /* The device's edges, read once and named once.
   *
   * Every rule that has to keep clear of a notch, a home indicator or one of
   * Telegram's own bars uses these — nothing else calls env() (TDE-189 AC-6).
   * A screen that remembered its own inset would be a screen that forgets one.
   *
   * **A fixed control must add --app-inset-bottom to its own bottom offset.**
   * There is no fixed control yet; the reading screen's «Пройдено» button is
   * the first and this is the rule it inherits. AGENTS.md: "Do not let fixed
   * controls overlap Telegram system areas."
   */
  --app-inset-top: env(safe-area-inset-top, 0px);
  --app-inset-right: env(safe-area-inset-right, 0px);
  --app-inset-bottom: env(safe-area-inset-bottom, 0px);
  --app-inset-left: env(safe-area-inset-left, 0px);
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* §21: readable under zoom — nothing here fixes a font size in px that the
     user cannot enlarge, and no viewport meta disables scaling. */
  -webkit-text-size-adjust: 100%;
}

body {
  /* The height Telegram says we have, not the browser's idea of it.
   *
   * Inside a WebView the two disagree and 100vh is the wrong one: it counts
   * space the client's chrome is occupying, which is what puts a bottom control
   * under the home indicator. viewport.js sets --app-height from the client and
   * updates it on every resize; the fallback is for a plain browser, where
   * there is no client to ask and 100dvh is right.
   */
  min-height: var(--app-height, 100dvh);

  /* **The column the height is now divided in**, rather than given whole to
     `#app`.
   *
   * `backcontrol.js` puts a Back control above the app, outside the container
   * so that no screen can replace it. Something has to give it room: with
   * `#app` claiming the full height itself, the control would push the app's
   * last 44 pixels below the fold. So the height is stated once here and the
   * two share it — the control takes what it needs and `#app` takes the rest,
   * which is also what makes it disappear cleanly on Home.
   *
   * `height` as well as `min-height`: a flex child cannot take "the rest" of a
   * container that has no definite height, and `#app` giving `.rows` a definite
   * height is what keeps a list scrolling inside the screen instead of growing
   * the page — see the note on `.rows`. */
  display: flex;
  flex-direction: column;
  height: var(--app-height, 100dvh);
}

/* The shell's own chrome: one Back control, above the screen and outside it.
 *
 * See backcontrol.js for why it is not rendered by the screens, and why it is
 * hidden rather than disabled on Home. */
.app-nav {
  display: flex;
  padding:
    calc(var(--app-inset-top) + 12px)
    calc(var(--app-inset-right) + 16px)
    0
    calc(var(--app-inset-left) + 16px);
  max-width: 640px;
  width: 100%;
  margin: 0 auto;
  /* Never shrinks. It is the way out, so it is the last thing that should give
     up space when a screen is tall. */
  flex: 0 0 auto;
}

/* `display: flex` above beats the `[hidden]` the user agent applies, so the
   rule has to be restated. A control that is meant to be gone and is merely
   invisible is still in the tab order. */
.app-nav[hidden] {
  display: none;
}

#app {
  /* §21: respect the safe areas. The Telegram header and the home indicator
     both live inside these insets, which are named once on :root. */
  padding:
    calc(var(--app-inset-top) + 16px)
    calc(var(--app-inset-right) + 16px)
    calc(var(--app-inset-bottom) + 16px)
    calc(var(--app-inset-left) + 16px);
  max-width: 640px;
  /* **`width` as well as `max-width`, now that the body is a flex column.** An
     auto margin on the cross axis stops a flex item stretching to its
     container, so `margin: 0 auto` alone centres the app at its *content*
     width — which made a full-width list row as narrow as its title.
     `TestTheTextRow` caught it at 135px. */
  width: 100%;
  margin: 0 auto;
  /* A column, so a list can take the height that is left instead of a number
     somebody guessed.
     *
     * `height` and not `min-height`: a container that grows with its content
     * never makes its flex children shrink, so the list would extend the page
     * instead of scrolling inside it — which silently turns off restoration,
     * because there is nothing to restore a scroll within. */
  display: flex;
  flex-direction: column;
  /* Takes the height the column has left, rather than claiming all of it: the
     Back control above is the other share. `min-height: 0` is what lets it
     actually shrink, and without it `.rows` grows the page instead of
     scrolling. */
  flex: 1 1 auto;
  min-height: 0;
}

/* A list scrolls inside the screen, rather than the screen scrolling.
 *
 * **Placeholder layout**, and the Learning Flow Screens Epic may well move the
 * scroll back to the page. `keepScroll` takes whatever scrolls and does not
 * care which it is; that is the point of it taking an element.
 */
.rows {
  flex: 1 1 auto;
  /* Without this a flex child refuses to shrink below its content, and the
     list grows the page instead of scrolling. */
  min-height: 0;
  overflow-y: auto;
  /* A scroll that reaches the end of the list must not then scroll the client
     behind it. */
  overscroll-behavior: contain;
}

h1 {
  font-size: 1.35rem;
  margin: 0 0 4px;
}

.subtitle {
  color: var(--muted);
  margin: 0 0 20px;
}

.sections {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* **The tap-target minimum, in one place.**
 *
 * §21 asks for at least 24×24 CSS px and AGENTS.md asks for more on primary
 * actions. Every interactive primitive in controls.js carries `.control`, so
 * the minimum is a property of the primitive rather than something each screen
 * remembers.
 *
 * `control-word` is the one exemption, and it is decided and explained in
 * controls.js: an inline word in running prose meets the height and cannot
 * meet the width without breaking the typography it lives in. */
.control {
  min-height: 24px;
  min-width: 24px;
  color: inherit;
  cursor: pointer;
  border: none;
  background: none;
  font: inherit;
  text-align: left;
}

/* A list row, and the app's main navigation target. Comfortably above the
   minimum, because a row is what a thumb aims at while walking. */
.control-row {
  display: block;
  min-height: 56px;
  padding: 14px 16px;
  border-radius: 12px;
  background: var(--section-bg);
  text-decoration: none;
  width: 100%;
}

/* The Back control. A row would span the screen and read as a destination; this
   is chrome, so it is sized to its label and sits at the leading edge where a
   back control has sat since before any of this. Comfortably past the 24px
   minimum because it is reached in a hurry. */
.control-back {
  min-height: 44px;
  padding: 10px 14px 10px 0;
  font-weight: 600;
  color: var(--tg-theme-link-color, var(--accent));
}

/* A main action — AGENTS.md: "prefer larger targets for primary actions". */
.control-primary {
  min-height: 48px;
  padding: 12px 20px;
  border-radius: 12px;
  background: var(--tg-theme-button-color, var(--accent));
  color: var(--button-fg);
  font-weight: 600;
}

/* A tappable word inside running text. See controls.js for the exemption and
   why the height is met this way: the vertical padding overlaps the leading the
   line already had, so the line box reaches 24px without spreading the text.
 *
 * **A dotted underline, and nothing else** (TDE-219).
 *
 * Almost every word in a text is tappable — measured, 636 of 636 committed
 * tokens resolve to a dictionary word — so whatever marks one marks nearly the
 * whole paragraph. The filled background this rule used to carry turned a
 * reading screen into a block of grey with white slots where the spaces were:
 * a treatment that is fine on one word and unreadable on sixty.
 *
 * A dotted underline is a **shape**, so it survives being seen without colour,
 * and it reads as a hint rather than as a highlight. Nothing else changes: same
 * font, same size, same weight, same background as the prose around it — so the
 * text is still text.
 *
 * A colour change alone would not have been acceptable here; §21 and AGENTS.md
 * both refuse colour as the only carrier of meaning. */
.reading {
  line-height: 24px;
}

.control-word {
  display: inline;
  padding: 2px 1px;
  text-decoration: underline dotted;
  text-decoration-color: var(--muted);
  text-underline-offset: 3px;
}

/* §11's Random 5: the draw's actions, under the words. */
.draw-actions {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  margin-top: 20px;
}

/* §10's saved-words list: the row, and the control that removes it.
 *
 * The two sit side by side — tapping the word opens it, tapping «Убрать» takes
 * it out of the list — so the row must not swallow the whole width. */
.saved-item {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

.saved-row {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 10px;
  flex: 1 1 auto;
}

.row-remove {
  flex: 0 0 auto;
  width: auto;
  color: var(--muted);
}

/* §9's lexical card. The frame is modal.js's; this is what is inside it.
 *
 * The lemma leads, because it is the word a learner just tapped and the reason
 * the card is open. Everything else is quieter than it. */
.card-lemma {
  margin: 0 0 4px;
  font-size: 1.25rem;
  font-weight: 600;
}

.card-translation {
  margin: 0 0 16px;
}

.card-label {
  margin: 0 0 2px;
  color: var(--muted);
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.card-example {
  margin: 0 0 2px;
}

.card-example-ru {
  margin: 0 0 16px;
  color: var(--muted);
}

.card-saved {
  margin: 0;
  color: var(--muted);
}

/* «Пройдено», under the text rather than fixed over it.
 *
 * **Not a fixed control**, which is what AGENTS.md's rule about Telegram's
 * system areas is mostly about: a button pinned to the bottom of a reading
 * screen covers the last line of the text, and on a device with a home
 * indicator it needs --app-inset-bottom to avoid sitting under it. A text is
 * about 350 characters, so the end of it is a short scroll away and the button
 * is simply there when a learner arrives. Nothing to overlap, nothing to inset.
 */
.reading-actions {
  margin-top: 20px;
}

.reading-done {
  margin: 0;
  color: var(--muted);
}

/* §21: a visible label, never a placeholder standing in for one. */
.field {
  display: block;
  margin: 0 0 var(--gap);
}

.field-label {
  display: block;
  margin-bottom: 4px;
  color: var(--muted);
  font-size: 0.9rem;
}

.field-input {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid var(--section-bg);
  background: var(--section-bg);
  color: var(--fg);
  cursor: text;
}

/* The modal. Focus handling is modal.js's; this is only what it looks like. */
.modal-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  /* Above everything, and inside the safe areas so a card is never under the
     notch or the home indicator. */
  padding-top: calc(var(--app-inset-top) + 16px);
  padding-bottom: calc(var(--app-inset-bottom) + 16px);
  background: var(--scrim);
  z-index: 10;
}

.modal {
  width: 100%;
  max-width: 480px;
  max-height: 100%;
  overflow-y: auto;
  padding: 20px;
  border-radius: 16px;
  background: var(--bg);
  color: var(--fg);
}

.modal-title {
  margin: 0 0 8px;
  font-size: 1.2rem;
}

.modal-body {
  margin-bottom: 16px;
}

/* §8's text list.
 *
 * **The status indicator carries three signals and colour is the least of
 * them** — see textlist.js for why the eye became a circle. These two values
 * are the only semantic colours in the product: they mean something rather
 * than decorating something, so they cannot be inherited from the client's
 * theme the way everything else is (docs/decisions/0010). They are named per
 * scheme here, and measured against both palettes by TDE-210.
 *
 * Grey comes from --muted, which is the client's own hint colour and is
 * already legible on whatever background it sent. */
:root {
  --mark-started: #8a6100;
  --mark-done: #1f7a3d;
}

:root[data-colour-scheme="dark"] {
  --mark-started: #f0c419;
  --mark-done: #4cc46e;
}

.text-row {
  /* The indicator and the number stay at their natural width, the title takes
     the rest, and the actions area is whatever it needs. A long title wraps
     inside its column instead of pushing the indicator off the row. */
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: center;
  gap: 10px;
  text-align: left;
}

.row-mark {
  font-size: 1.1rem;
  line-height: 1;
  color: var(--muted);
}

.row-mark[data-status="in_progress"] {
  color: var(--mark-started);
}

.row-mark[data-status="completed"] {
  color: var(--mark-done);
}

.row-number {
  color: var(--muted);
  /* So the numbers line up down the list rather than shuffling with their
     digits. */
  font-variant-numeric: tabular-nums;
}

.row-title {
  /* A German compound with no spaces in it is one word and would otherwise
     widen the row until the layout gives up. */
  overflow-wrap: anywhere;
}

/* Reserved by §8, empty in v1, and taking no space while it is empty. A
   fixed-width gap would be a hole a learner can see and cannot explain. */
.row-actions:empty {
  display: none;
}

.section-title {
  display: block;
  font-weight: 600;
}

.section-hint {
  display: block;
  color: var(--muted);
  font-size: 0.9rem;
}

/* **§21's focus rule, applied to everything focusable rather than to a class.**
 *
 * A rule scoped to one component is a rule the next component does not have.
 * This is deliberately `:focus-visible` on every element: the browser decides
 * when a focus ring is warranted — keyboard yes, a tap no — and we decide only
 * that it is visible when it is.
 *
 * **Nothing anywhere sets `outline: none`.** TestTheAccessibilityBaseline reads
 * the computed style of every focusable element and fails if a focus ring is
 * missing, so removing this for looks is a failing build rather than a quiet
 * loss for everyone navigating by keyboard. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.control[aria-disabled="true"],
.control:disabled {
  opacity: 0.6;
}

/* The two screens a launch can end on instead of Home: opened outside Telegram,
   and a sign-in that did not happen. Both are text and, at most, one button. */
.notice {
  margin: 0 0 12px;
  color: var(--muted);
}


/* The three shared states.
 *
 * **They must be visually distinct, and the empty one must not read as a
 * failure.** A learner meets an empty list on their first day — no saved words
 * yet, a filter matching nothing — and rendering that as an error teaches them
 * the app is broken when it is working exactly as intended.
 *
 * So: loading and empty are quiet, in the hint colour. Only the failed state
 * carries emphasis, and it earns it.
 */
.state {
  padding: 24px 0;
  text-align: center;
}

.state-loading,
.state-empty {
  color: var(--muted);
}

.state-failed {
  /* The one state with weight. A left rule rather than a background: it reads
     as emphasis at any zoom and does not depend on colour alone, which
     §21 asks for. */
  text-align: left;
  border-left: 3px solid var(--accent);
  padding: 12px 0 12px 12px;
}

.state-note {
  color: var(--muted);
  font-size: 0.9rem;
  /* A request id is read out loud or copied; both are easier when it is not
     wrapped mid-token. */
  user-select: all;
  margin: 4px 0 12px;
}
