/* Every colour comes from a Telegram theme variable with a light-mode fallback.
   Hardcoding colours is antipattern #3 in RESEARCH.md §5: the member can switch
   theme while the app is open and black-on-black is the result. */
:root {
  --bg: var(--tg-theme-bg-color, #ffffff);
  --secondary: var(--tg-theme-secondary-bg-color, #f2f2f7);
  --section: var(--tg-theme-section-bg-color, var(--bg));
  --ink: var(--tg-theme-text-color, #16181d);
  --hint: var(--tg-theme-hint-color, #8a8f98);
  --link: var(--tg-theme-link-color, #2f7de1);
  --accent: var(--tg-theme-button-color, #2f7de1);
  --accent-ink: var(--tg-theme-button-text-color, #ffffff);
  /* Secondary TEXT, unlike --hint which is the client's placeholder grey. 65%
     was the smallest sRGB mix holding 4.5:1 on card and body; mixed in oklab
     below, the same 65% clears the measured worst case at 5.71:1 instead of
     scraping the floor at 4.92:1 — spec 054. */
  --sub-ink: var(--tg-theme-subtitle-text-color, var(--hint));
  /* A row's press, today just --secondary — spec 059's own T005 measured that
     as low as 0/255 from the card on both synthetic palettes. The plain twin. */
  --press: var(--secondary);
  /* Telegram's own separator colour where the client sends one (Bot API 7.0);
     otherwise the hint grey at full strength — heavier than the 26% mix
     below, which is the point. A custom property cannot fall back through
     the cascade the way a plain property can, so the color-mix() upgrade
     has to live in its own @supports block rather than a second line here. */
  --line: var(--tg-theme-section-separator-color, var(--hint));

  --safe-top: var(--tg-content-safe-area-inset-top, 0px);
  --safe-bottom: max(var(--tg-safe-area-inset-bottom, 0px), env(safe-area-inset-bottom, 0px));
  /* 72px, not 56: raised on member feedback ("fill the entire surface as it
     does in instagram") so the indicator can be a generously padded pill
     around the icon and label together — matching Instagram's proportions. */
  --tabbar-h: 72px;
  /* The tab bar never sits flush against the window edge. A phone reports its
     inset (34px of home indicator on iPhone) and that wins, but a client that
     draws furniture over the bottom edge without reporting anything — Chrome for
     Android's bottom toolbar, a device-frame overlay — would otherwise eat the
     labels, since they are the last 8px of the bar. */
  --tabbar-pad: max(var(--safe-bottom), 8px);
  /* Matches main's own 12px horizontal padding, so the floating bar's edges
     line up with the cards above it — spec 060. */
  --tabbar-inset: 12px;
  /* The margin between the indicator and the bar's own edge on every side —
     the "small border" the member asked for, matching Instagram's selected
     segment leaving a strip of the tray visible around it. */
  --tab-inset: 4px;

  /* One easing curve for the whole app, unconditional: a cubic-bezier() needs no
     feature query. Decelerating and asymmetric — it leaves fast and settles slow,
     which is what makes a short transition read as a response rather than as a
     delay. Shared by spec 056's tab tint and spec 058's press states; declared
     once here so the two cannot drift into two slightly different curves. */
  --ease: cubic-bezier(.2, 0, 0, 1);

  /* The press curve: it overshoots to 1.086 and settles, which is what makes a
     scale read as a spring rather than as a resize. Deliberately NOT behind a
     feature query — that would be backwards. An engine that does not know
     linear() substitutes an invalid value here, the whole `transition` that uses
     it goes invalid at computed-value time and falls to its initial `all 0s`, so
     the press is instant instead of animated. That is exactly the wanted
     fallback, and a query would remove it (spec 058). */
  --spring: linear(0, 0.006, 0.025, 0.101 6.5%, 0.539 15.5%, 0.826 22%, 0.937, 1.02, 1.07, 1.086, 1.077, 1.055, 1.031, 1.012, 1.001, 0.998 60%, 1.003, 1.001);
}

/* Block 1: the color-mix() upgrades that need @supports because a custom
   property cannot fall back through the cascade the way a plain property
   can — an unsupported color-mix() here would compute to *inherited*, not
   the plain declaration above. Guarded rules always sit AFTER their plain
   twin: @supports adds no specificity, so the reverse order would lose even
   where color-mix() works. */
@supports (color: color-mix(in srgb, red, blue)) {
  :root {
    /* --line and --tint-accent mix toward `transparent`, which is an alpha scale
       rather than a colour interpolation, so their space cancels out and srgb is
       not a choice here. --sub-ink blends two opaque colours, where the space is
       the whole point. They are deliberately different; do not harmonise them. */
    --line: color-mix(in srgb, var(--hint) 26%, transparent);
    --sub-ink: color-mix(in oklab, var(--ink) 65%, var(--bg));
    /* 12%, from a window of 11–14%. Below 12% the step falls under 20/255 on the
       dark synthetic palette and stops being visible; at 15% --sub-ink on the
       pressed row measures 4.47:1, under WCAG's 4.5. Next value to try is 13%,
       not 15%. Mixed toward --ink rather than replaced with a fixed colour so
       the step is the product's number and not the difference between two
       client tokens — spec 059. */
    --press: color-mix(in oklab, var(--ink) 12%, var(--section));
    /* The container behind the active tab's icon. No plain twin, deliberately:
       there is no meaningful un-blended value for it, and the use site declares
       `var(--tint-accent, transparent)` so an engine without color-mix() paints
       nothing and gets today's bar — spec 056. */
    --tint-accent: color-mix(in srgb, var(--accent) 16%, transparent);
  }
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

body {
  margin: 0;
  background: var(--secondary);
  color: var(--ink);
  font: 16px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* Pin to the STABLE viewport height, not 100vh: viewportHeight updates during
     the pull-to-expand gesture and "refreshes too slowly to smoothly track the
     lower border" (Telegram docs), which makes anything bottom-anchored jitter. */
  min-height: var(--tg-viewport-stable-height, 100dvh);
  overscroll-behavior-y: none;
}

/* ── header ──────────────────────────────────────────────────────────────── */

header {
  position: sticky; top: 0; z-index: 20;
  padding: calc(8px + var(--safe-top)) 12px 8px;
  /* Opaque, not a translucency plus a blur: the blur needed an unprefixed
     filter that does nothing on the WKWebView Telegram iOS uses below 18,
     which left rows scrolling under this bar legible through it. Same look
     on every engine — no vendor-prefixed filter added either. */
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

/* The separator earns its place: it appears once content has actually scrolled
   under the bar. Driven by a scroll timeline rather than a class toggle so that
   it reads the scroller's real offset and cannot disagree with the restored
   scroll position at app/js/main.js:369 — see spec 057 FR-006. Must stay AFTER
   the rule above: @supports adds no specificity (spec 03), and shell.test.mjs
   slices 700 chars from the FIRST `header {` in the file (spec 04). */
@supports (animation-timeline: scroll()) {
  @keyframes rn-sep {
    from { border-bottom-color: transparent }
    to   { border-bottom-color: var(--line) }
  }
  header {
    animation: rn-sep linear both;
    animation-timeline: scroll(root);
    animation-range: 0 8px;
  }
}

.searchbar { position: relative; display: flex; align-items: center; }
.searchbar .mag {
  position: absolute; left: 10px; width: 19px; height: 19px; color: var(--sub-ink);
  pointer-events: none;
}
#q {
  width: 100%; font: inherit; font-size: 16px;  /* <16px makes iOS zoom on focus */
  padding: 10px 36px 10px 32px;
  /* 22px = half of the field's own rendered height (10+10 padding + 1+1 border
     + 22.4 content line-height from body's font: 16px/1.4) — a true pill,
     matching Telegram's own primary search-field shape cue (spec 051). */
  border: 1px solid var(--line); border-radius: 22px;
  background: var(--secondary); color: var(--ink);
  -webkit-appearance: none; appearance: none;
}
#q::-webkit-search-cancel-button { display: none; }
#q:focus { outline: none; border-color: var(--accent); }
/* MUST stay after the rule above: both selectors are specificity (0,2,0) — one
   id, one pseudo-class — so source order decides, and placed first this would
   lose its outline to `outline: none` with no error anywhere. The offset is what
   puts the ring on the bar behind the field (4.08:1) rather than on the field's
   own fill (3.66:1), and an outline has followed border-radius since Safari 16.4,
   so it traces the pill (spec 058). */
#q:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.qclear {
  position: absolute; right: 4px; width: 30px; height: 30px;
  border: none; background: none; color: var(--sub-ink); font-size: 15px;
  cursor: pointer;
}
.crumb { margin-top: 7px; font-size: 13px; color: var(--sub-ink); }

/* ── main ────────────────────────────────────────────────────────────────── */

main {
  padding: 12px 12px calc(var(--tabbar-h) + var(--tabbar-pad) + 16px);
  max-width: 720px; margin: 0 auto;
}

/* A group head is a phrase to read, not a label to scan past, so it is set at
   full --ink rather than a grey. No text-transform at all: forced uppercase costs
   more in Cyrillic than in Latin, because Cyrillic lowercase has few ascenders or
   descenders to begin with and capitalising throws away most of the word-shape cue
   that survives in Latin. `align-items: center`, not `baseline`, because the count
   beside it is deliberately no longer the same size — spec 055. */
.grouphead {
  display: flex; justify-content: space-between; align-items: center;
  font-size: 17px; font-weight: 700; letter-spacing: -0.01em;
  color: var(--ink);
  margin: 22px 4px 9px;
  text-wrap: balance;
}
.grouphead:first-child { margin-top: 6px; }
/* The count must restate its own size and colour. Inheriting would carry the
   head's 17px and full --ink down onto a number nobody came to read. */
.grouphead .n {
  font-size: 13px; font-weight: 500; color: var(--sub-ink); letter-spacing: 0;
}

.hint { color: var(--sub-ink); font-size: 13.5px; margin: 6px 4px 10px; }

/* a card is a rounded group of rows, the platform-native list idiom. `overflow:
   hidden` does three jobs: it clips the radius, it keeps the separator's
   `right: 0` edge inside the card, and it bounds the heart's 1.28× press. */
.card { background: var(--section); border-radius: 18px; overflow: hidden; }
/* Must stay above .grouphead's own 22px top margin: a head that follows a card is
   separating two groups rather than opening one, so it needs more air, not less.
   Raise this whenever that 22px rises. */
.card + .grouphead { margin-top: 26px; }

.row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 0 12px; border: none; background: none; color: inherit;
  font: inherit; text-align: left; cursor: pointer;
  /* The containing block for the separator below. Without it the offsets would
     resolve against the nearest positioned ancestor, which is the sticky
     `header` — so every hairline would collect at the top of the screen rather
     than simply not paint. A quiet failure, not a loud one. */
  position: relative;
  /* Only the colour, and only on the way out: `.row:active` pins the duration to
     0s, so the highlight arrives the instant the finger lands and fades over a
     quarter second when it leaves. A symmetric .25s would fade *in* too, and a
     tap can be under 100ms — the member's own touch would be answered by a
     flicker rather than by a highlight (spec 058). */
  transition: background-color .25s var(--ease);
}
/* The separator starts where the title starts, which is the platform list idiom:
   a hairline that stops short of the text column reads as a division inside one
   object rather than as a stack of separate bars.
   Two declarations here are load-bearing and neither looks it.
   `transparent` and not `none`: the border keeps contributing its 1px to the
   row's border-box, so the list is exactly as tall as it was. `none` would take
   1px off every adjacent pair — 50px on the 51-row «🍩Десерты» list, which is the
   arithmetic the heart's 58px floor is built on.
   `position: absolute`: `.row` is `display: flex`, so a static ::before becomes a
   flex item ahead of `.body` and shifts every child. Do not remove it as
   redundant.
   `top: -1px` because offsets resolve against the padding box, which puts the
   hairline exactly where the border it replaces used to paint. `left` must equal
   `.row`'s own horizontal padding above — asserted as an equality, since there is
   no token binding them (spec 058). */
.row + .row { border-top: 1px solid transparent; }
.row + .row::before, .noteline + .row::before {
  content: ""; position: absolute; left: 12px; right: 0; top: -1px; height: 1px;
  background: var(--line);
}
/* No `transform` here, deliberately: .fav is a <button> nested inside this one,
   so this rule matches when the heart is pressed and a row scale would move the
   whole row on a tap aimed at the heart. */
.row:active { background: var(--press); transition-duration: 0s; }
.row .body { flex: 1; min-width: 0; padding: 11px 0; }
/* `pretty` and not `balance`: it only reflows the last few lines, so it cannot
   leave one word alone on a line the way an unhinted break does, and it costs
   nothing on the 43 titles of 51 that fit one line. No @supports — an engine
   that does not know the keyword drops the declaration and breaks as it does
   today (spec 057). */
.row .t { display: block; text-wrap: pretty; }
.row .s { display: block; font-size: 12.5px; color: var(--sub-ink); margin-top: 2px; }
.row .badge {
  display: inline-block; margin-right: 6px; padding: 0 4px;
  border: 1px solid var(--line); border-radius: 4px;
  font-size: 11px; line-height: 1.5;
}
.row .n { color: var(--sub-ink); font-size: 14px; font-variant-numeric: tabular-nums; }
.row .chev { color: var(--sub-ink); font-size: 15px; }

/* The heart is a separate hit target inside the row. WCAG 2.5.8 (24×24) and
   2.5.5 (44×44) — not Baymard, which appears nowhere in research/. Fills the
   row's cross axis so the whole column belongs to the control: height:auto is
   what lets stretch beat a fixed height. 58px = 36 + 2 × 11 — the row's own
   vertical padding, now on .row .body, floors the heart at the same total a
   theory row's body reaches with no subtitle. */
.fav {
  flex: 0 0 auto; width: 36px; align-self: stretch; height: auto; min-height: 58px;
  border: none; background: none;
  font-size: 19px; line-height: 1; cursor: pointer; color: var(--sub-ink);
  /* The heart GROWS where a chip shrinks, and that asymmetry is the point: a chip
     is a surface being pushed down, a heart is a mark being made. A transform
     affects no box, so the 58px floor above is untouched. Reset for reduced
     motion in the chips section below, beside .chip's. */
  transition: transform .4s var(--spring);
}
.fav:active { transform: scale(1.28); }
.fav[aria-pressed="true"] { color: #e0245e; }
.cook { font-size: 15px; }
.cook[aria-pressed="true"] { color: var(--accent); }

/* ── the note line ───────────────────────────────────────────────────────────
   A full-width button under the row, inside the same card — not a third 36px
   control in the row itself, which would leave the title ~174px on a 360px phone
   (see views.js). It breaks the `.row + .row` adjacency, so the separator for the
   row that follows is restored explicitly. */

.noteline {
  display: flex; align-items: flex-start; gap: 8px; width: 100%;
  min-height: 36px; margin-top: -5px; padding: 0 12px 11px;
  border: none; background: none; color: inherit;
  font: inherit; text-align: left; cursor: pointer;
  transition: background-color .25s var(--ease);
}
.noteline:active { background: var(--press); transition-duration: 0s; }
/* Restored explicitly because a note line breaks the `.row + .row` adjacency;
   transparent for the same reason as above, and its ::before is declared with
   the other one so a card cannot end up with two kinds of separator. */
.noteline + .row { border-top: 1px solid transparent; }
.noteline .pen { font-size: 12px; line-height: 1.5; flex: 0 0 auto; opacity: .75; }
/* The member's own words, so they are text and not a hint — dimming them to
   --hint would make a note look like the metadata it sits next to. Two lines,
   because a note is a sentence and a card of them must still be scannable. */
.noteline .s {
  flex: 1; min-width: 0; font-size: 12.5px; line-height: 1.5; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.noteline.add .s { color: var(--link); }
mark { background: var(--accent); color: var(--accent-ink); border-radius: 3px; }
/* Background and colour must switch together, so this needs its own query
   rather than living in block 1 — a color-mix() background with the opaque
   fallback's colour on top would be unreadable on every modern engine. */
@supports (color: color-mix(in srgb, red, blue)) {
  mark { background: color-mix(in srgb, var(--accent) 28%, transparent); color: inherit; }
}

/* ── the note editor ─────────────────────────────────────────────────────────
   A layer over everything, above both the sticky header (20) and the tab bar
   (30). Height comes from --tg-viewport-height, NOT the stable height the body
   uses: this is the one screen where the keyboard is up, and the stable height
   would put the field's own bottom edge behind it. The jitter that makes
   stableHeight the right choice elsewhere applies to bottom-anchored furniture,
   and nothing in here is bottom-anchored — the column grows from the top and the
   keyboard covers empty space below it. */

.editor {
  position: fixed; top: 0; left: 0; right: 0; z-index: 60;
  height: var(--tg-viewport-height, 100dvh);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg);
}
/* display:flex would otherwise beat the UA's [hidden] rule and the layer would
   sit over the app permanently. */
.editor[hidden] { display: none; }

.editorbox {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  width: 100%; max-width: 720px; margin: 0 auto;
}

/* Sticky, so «Готово» stays reachable however far the content scrolls: on
   Telegram iOS there is no inputAccessoryView and Return inserts a newline, so
   this button is the only way out of the keyboard. */
.editorhead {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; gap: 12px;
  padding: calc(10px + var(--safe-top)) 14px 10px;
  background: var(--bg); border-bottom: 1px solid var(--line);
}
.editorhead .t {
  flex: 1; min-width: 0; font-size: 15px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.editorhead .done {
  flex: 0 0 auto; border: none; background: none; color: var(--link);
  font: inherit; font-weight: 600; cursor: pointer; padding: 6px 2px;
}

/* Same style as .grouphead, deliberately: the note editor's label is a list head
   in a different place, and the suite asserts the two font blocks equal to each
   other rather than to literals. No text-wrap here — this label is one short word
   and has nothing to balance. */
.notelabel {
  display: flex; justify-content: space-between; align-items: center;
  margin: 20px 16px 8px;
  font-size: 17px; font-weight: 700; letter-spacing: -0.01em;
  color: var(--ink);
}
.hintinline {
  font-size: 13px; font-weight: 400; letter-spacing: 0; color: var(--sub-ink);
}

.notefield {
  margin: 0 14px; width: calc(100% - 28px);
  min-height: 128px; max-height: 45vh; resize: none;
  font: inherit; font-size: 16px; line-height: 1.45;   /* <16px zooms iOS on focus */
  padding: 12px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--section); color: var(--ink);
  -webkit-appearance: none; appearance: none;
}
.notefield:focus { outline: none; border-color: var(--accent); }

.editorfoot {
  display: flex; justify-content: space-between; gap: 10px;
  margin: 8px 16px 0; min-height: 18px;
  font-size: 12.5px; color: var(--sub-ink);
}
.editorfoot .counter { font-variant-numeric: tabular-nums; }

.editorbox .actions { display: flex; gap: 16px; margin: 20px 12px 0; }
.linkish.danger { color: #e0245e; }
.editorbox .hint { margin: 18px 16px calc(24px + var(--safe-bottom)); }

/* ── chips (products) ────────────────────────────────────────────────────── */

.chips { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 2px; }
.chip {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 8px 11px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--section); color: var(--ink); font: inherit; font-size: 14.5px;
  cursor: pointer; min-height: 36px;
  /* var(--section) and the --line border both stay: --secondary is the token the
     page itself is painted with and no .chips container sits inside a .card, so
     filling a chip with it would leave a 1.00:1 boundary on every theme. */
  transition: transform .34s var(--spring);
}
.chip:active { transform: scale(.95); }
.chip .n { color: var(--sub-ink); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.chip.group { border-style: dashed; }

/* Only the transforms. The rows' colour fade stays outside on purpose: nothing
   about it translates, scales or moves, and WCAG 2.3.3 is about motion — putting
   it here would make reduced motion a setting that removes *feedback*. Both
   declarations are needed: killing the transition alone would leave the :active
   transform applying instantly, which is a jump, i.e. motion without the cushion.
   MUST stay after `.fav`'s own rule above — a landed test finds `.fav`'s body
   with an unanchored match, and this selector list would otherwise be the one it
   reads. */
@media (prefers-reduced-motion: reduce) {
  .chip, .fav { transition: none; }
  .chip:active, .fav:active { transform: none; }
  /* The indicator still fades in and out of view — that is not motion — but
     it must not travel (FR-014). */
  nav#tabs::before { transition: opacity .18s var(--ease); }
}

/* ── empty states ───────────────────────────────────────────────────────── */

.empty { text-align: center; padding: 40px 20px; color: var(--sub-ink); }
.empty .big { font-size: 34px; display: block; margin-bottom: 10px; }
.empty p { margin: 6px 0; font-size: 14.5px; }
.empty .actions { margin-top: 16px; }
.linkish {
  border: none; background: none; color: var(--link); font: inherit;
  cursor: pointer; padding: 4px;
}

/* ── tab bar ─────────────────────────────────────────────────────────────── */

nav#tabs {
  position: fixed; z-index: 30;
  left: var(--tabbar-inset); right: var(--tabbar-inset);
  bottom: var(--tabbar-pad);
  max-width: 720px; margin-inline: auto;
  display: flex; height: var(--tabbar-h);
  background: var(--secondary); /* matches #q, not --bg — see comment below */
  border: 1px solid var(--line);
  /* A true pill: 28px = half of --tabbar-h's own 56px, the same reasoning as
     #q's 22px (app.css, #q rule) — half of one's own rendered height, not a
     borrowed number. Revised from .card's 18px after seeing the floating bar
     on a device: matching the cards read as a second design language instead
     of the rounder, search-field-like shape the member asked for. */
  border-radius: calc(var(--tabbar-h) / 2);
  overflow: hidden;
}
/* var(--secondary) above, revised from var(--bg): the bar used --bg while it
   sat flush with the screen edge like the header. On at least one Telegram
   dark theme --bg renders noticeably darker than --secondary — the inverse of
   the usual light-theme relationship (spec 059 research.md Decision 10) —
   which read as a heavy black slab against the page once the bar floated.
   The bar now sits ON the page the way #q sits in the header, so it takes
   #q's own token. Kept short above: two landed tests slice 700 characters
   from the first `nav#tabs {` (specs 03 and 04) and a longer comment here
   would have pushed this declaration past that window — the same hazard
   already documented for `header {`. */
/* Reopened on member request, 2026-08-24, after the plain opaque fill above
   landed: "can we add just a bit of blurred matte glass transparency to the
   lower bar?" This is the fourth time backdrop-filter has come up in this
   repo (spec.md §8) and the third time it has been rejected on either the
   member's own "no liquid glass" instruction or spec 04's measurement that it
   did not actually blur on the WKWebView version Telegram iOS used at the
   time — the underlying WebView may have moved on since. Guarded, so an
   engine without support (or a future member instruction to remove it again)
   falls straight back to the plain rule above, which stays fully opaque on
   its own — MUST stay after it, same ordering rule as every other guarded
   twin in this file. `-webkit-backdrop-filter` is the Safari-prefixed form;
   `or` in the @supports query accepts either. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  nav#tabs {
    /* Confirmed working on a device, then tuned twice more on device feedback:
       less blur each time (20px → 14px → 8px) so more of what's underneath
       stays legible as shapes rather than smoothing into a colour wash, and
       more transparency each time (70% → 55% → 45%) so more of the page
       shows through. saturate() keeps what shows through from washing out to
       grey, the standard pairing for a "frosted glass" look (Apple's own
       material recipe) rather than blur alone. */
    background: color-mix(in srgb, var(--secondary) 45%, transparent);
    backdrop-filter: blur(8px) saturate(1.5);
    -webkit-backdrop-filter: blur(8px) saturate(1.5);
  }
}
nav#tabs button {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  /* 3px, not 2px: the icon is no longer a bare glyph, and a 30px box needs a
     little more separation from the label than a 20px glyph did — spec 056. */
  justify-content: center; gap: 3px;
  border: none; background: none; color: var(--sub-ink);
  font: inherit; font-size: 10.5px; cursor: pointer;
}
/* «Ингредиенты» is the longest label and the one that overflowed the pill on
   a device (the pill now covers the label, not just the icon). Tightened
   rather than the pill widened: widening would have eaten the visible border
   the member asked to keep. font-size and letter-spacing target headroom on
   a real device's font metrics, not just this repo's own measurement — the
   two rarely agree exactly for Cyrillic. */
nav#tabs button .ico + span {
  font-size: 9px; letter-spacing: -0.3px; white-space: nowrap;
}
/* Inset, because nav#tabs clips: an outline-offset outside the bar is invisible. */
nav#tabs button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* One indicator for the whole bar, replacing the four tinted per-tab boxes
   spec 056 shipped (spec.md §7). Revised again on member feedback: rather
   than a small capsule around just the icon, it now covers the ICON AND THE
   LABEL together — a smaller, concentric copy of the bar's own shape, inset
   by --tab-inset on every side, the way Instagram's selected segment leaves a
   thin strip of the bar showing around it. Painted behind the button's
   children (a ::before is the first thing in paint order), so the icon and
   label simply sit on top of it — no z-index needed.
   `top`/`bottom` rather than a fixed height: the pill's height is derived
   from --tabbar-h, so raising the bar's height does not require a second
   number to keep in sync. `border-radius: 999px` still clamps to a stadium
   regardless of the box's exact size (CSS clamps an oversized radius to half
   the smaller dimension), so it reads as "the bar's own rounding, scaled
   down" without computing a smaller radius by hand. */
nav#tabs::before {
  content: ""; position: absolute; pointer-events: none;
  top: var(--tab-inset); bottom: var(--tab-inset);
  /* Positioned at its own true size directly — no background-clip. WebKit
     (WKWebView on iPhone, and Telegram Desktop on macOS — both WebKit family)
     has a long-standing bug where background-clip: content-box ignores
     border-radius and paints the fill with square corners, even though the
     border-box itself would round fine; found on real devices, absent in
     Chromium. Sizing the box to its true footprint sidesteps the bug rather
     than working around it. */
  left: calc(var(--i, 0) * (100% / 4) + var(--tab-inset));
  width: calc(100% / 4 - var(--tab-inset) * 2);
  background: var(--tint-accent, transparent);
  border-radius: 999px;
  opacity: var(--pill-o, 1);
  transform: scaleX(var(--stretch, 1));
  /* Divisor is 3, the tab count minus one, so the origin sweeps from the
     pill's own left edge (index 0) to its own right edge (index 3) — the
     anchor the end-cell stretch needs to grow inward rather than out past
     the bar's rounded corner. Percentages in transform-origin resolve
     against the element's OWN box, which is now the pill itself. */
  transform-origin: calc(var(--i, 0) * 100% / 3) center;
  transition: left .42s var(--spring), transform .42s var(--spring), opacity .18s var(--ease);
}
nav#tabs button .ico {
  display: grid; place-items: center;
  width: 32px; height: 32px;
  font-size: 20px; line-height: 1;
}
nav#tabs button[aria-current="page"] { color: var(--accent); font-weight: 600; }

/* The on-screen keyboard covers a bottom bar anyway, and on iOS it also shrinks
   the visual viewport out from under a fixed element, so the bar yields to it.
   Keyed on the keyboard actually being up — NOT on the field having focus: on
   Telegram Desktop nothing covers the viewport, and hiding the bar there would
   remove the way back to browsing at exactly the moment NN/g says it matters. */
body.keyboard nav#tabs { display: none; }
body.keyboard main { padding-bottom: 24px; }

.spinner { text-align: center; color: var(--sub-ink); padding: 48px 0; }

/* Visually hidden but present in the a11y tree — NOT display:none or
   [hidden], which remove a node from it entirely. */
.sronly {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
