/* Bot AI component primitives -- the pieces the redesign repeats on nearly every
   screen, lifted out of its inline styles so each screen does not re-derive them
   by hand (and drift).

   Requires tokens.css to be loaded first; every colour here is a var(--token),
   so a palette or mode change repaints all of it with no rule changes.

   The `b-` prefix exists because cabinet.css, landing.css and cortex-tokens.css are
   still live during the migration and own generic names like .btn, .card, .field
   and .avatar. Nothing in this file may collide with those.

   Conventions:
     - Cards are 20px radius, pills/buttons are 100px. Those two numbers carry the
       whole look; do not introduce a third.
     - Body copy is 'Noto Sans Georgian', headings are var(--hf), and anything
       numeric (KPIs, prices, counts) is 'Bricolage Grotesque' -- Georgian has no
       lining figures worth the trade, so digits get their own face.
     - State is expressed with .is-* classes. The design's own data-attribute
       states (data-a / data-n / data-t / data-s / data-m / data-day) are also
       honoured inside .b-app, so markup can be pasted from the design file and
       work unchanged. */

/* ==========================================================================
   0. TRANSFERRING FROM THE DESIGN DELIVERY -- the two lookup tables

   `design_handoff_all/redesign/BotAI-Redesign-Final.html` is 100% inline styles
   with literal values. Two of those literals fail a test in this repo the moment
   they are pasted: a colour (designTokens.test.ts) and a font size
   (typeScale.test.ts). So a blind transfer does not merely look wrong -- IT DOES
   NOT MERGE, and the fix-agent finds that out at the end rather than the start.

   These are the two mappings that turn a pasted inline style into something that
   does merge. They live here, in the file every transferred screen already has
   open, because that is the only place they get read.

   --- COLOUR: design literal -> our token -----------------------------------

   The delivery's own `var(--x, LITERAL)` fallbacks name OUR tokens -- the
   designer worked from tokens.css -- so the mapping is 1:1. Drop the literal,
   keep the token.

     #1F6B42, #3B8A5B ............ var(--acc)      both mean "accent"
     #0F1A13 .................... var(--fg), or var(--inv) as a card background
     #F6F8F4 .................... var(--bg), or var(--invFg) on an inverted card
     #E9EFE6 .................... var(--panel2)
     #fff (295 uses) ............ var(--panel), or var(--accFg) on an accent fill
     rgba(20,50,30,.11) (280) ... var(--line)
     rgba(15,26,19,.66/.58/.5) .. var(--fg2)
     #2E7D5B .................... var(--pos)
     the design's danger red .... var(--neg) / var(--negFg). It is named in
                                  designTokens.test.ts, which fails on it in
                                  every file but tokens.css -- including this
                                  one, which is why it is not written out here.
                                  It misses AA on our panels; --neg is the same
                                  hue moved to a lightness that clears it.
     #FCEFD9 / #8A5A12 .......... var(--warn) on a var(--warn) tint. There is no
     (the `limit` banner)         --warnBg: the tint is
                                  color-mix(in srgb, var(--warn) 12%, var(--panel)),
                                  so it follows the palette instead of being a
                                  ninth colour to keep AA-rating.
     #C9A227 (the "unknown" dot)  var(--fg2). NOT a colour of its own -- see the
                                  second half of tokens.css's caution block. The
                                  class is .b-dot-unknown.
     #D98324, #6b5f52, #F1EBE1,   AMBER-PALETTE LEAKAGE, not a fifth palette.
     #1B1611, #FBF8F3, #7C6A55,   The fallbacks were written while the amber
     #C97615                      palette was active and never re-rendered on
                                  moss. Drop the literal, keep the token.

   --- TYPE: design px -> our rung -------------------------------------------

   Nine rungs (tokens.css): --type-micro 10.5 · --type-meta 11.5 · --type-body 13
   · --type-emphasis 14.5 · --type-lead 16 · --type-title 17.5 · --type-heading
   20 · --type-section 28 · --type-hero 34.

   The delivery uses at least twenty sizes and only seven land on a rung. Its
   most-used size, 12.5px (180 occurrences), is not one of them.

   THE RULE IS: NEAREST RUNG, TIES GO UP. Ours is the larger size in every row
   below, which is the whole reason ties go up -- a sweep that silently shrinks
   Georgian ink is the one outcome nobody wants.

     11px .................. var(--type-meta)      (11.5)
     11.5px, 13px .......... already on the scale
     12px, 12.5px .......... var(--type-body)      (13)
     13.5px, 14px .......... var(--type-emphasis)  (14.5)
     15px .................. var(--type-lead)      (16)
     23px .................. var(--type-heading)   (20) -- nearer than 28
     26/30/32/34/36/38/44/   Bricolage numerals. Use .b-num / .b-num-sm/md/lg/xl
     52/64 ................. / .b-num-boundless, which already carry the
                             type-scale-exempt(numerals) tag. Never write the
                             literal -- a raw px here fails typeScale.test.ts
                             like any other.

   AND THE CONSEQUENCE THAT IS A REAL FINDING WHEN IT HAPPENS: ours is the larger
   size in every row, so any fixed-width or fixed-height container copied from the
   delivery has LESS room than the designer assumed. A box sized for 12.5px text
   overflowing at 13px is answered by letting the box grow. It is never answered
   by writing the smaller size back in.
   ========================================================================== */

/* ==========================================================================
   1. Keyframes -- ported wholesale from the design's <style> block.
   ========================================================================== */

@keyframes rise      { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: none; } }
@keyframes riseIn    { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
@keyframes pop       { from { opacity: 0; transform: scale(.95); }       to { opacity: 1; transform: none; } }
@keyframes floaty    { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(40px, -30px) scale(1.12); } }
@keyframes floaty2   { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-36px, 26px) scale(.92); } }
@keyframes marquee   { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes blink     { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
@keyframes grow      { from { transform: scaleY(.04); } to { transform: scaleY(1); } }
@keyframes dash      { from { stroke-dashoffset: 300; } to { stroke-dashoffset: 0; } }
@keyframes spinSlow  { to { transform: rotate(360deg); } }
@keyframes pulseRing { 0% { transform: scale(.8); opacity: .7; } 100% { transform: scale(2.2); opacity: 0; } }
@keyframes sheen     { from { background-position: 0% 50%; } to { background-position: 200% 50%; } }
@keyframes barIn     { from { opacity: 0; transform: scaleX(0); } to { opacity: 1; transform: scaleX(1); } }

/* Entrance animations are decoration; the content is already in the DOM. Anyone
   who has asked the OS to stop moving things gets the end state immediately.

   THE GUARD MUST NOT BE ESCAPABLE BY MARKUP THAT FORGOT OUR CLASS NAMES.
   It used to be `[class^="b-"], [class*=" b-"]` and nothing else, which reaches
   exactly the elements wearing our prefix. The design file does not write its
   motion that way: it writes `animation:` as an INLINE STYLE on elements with
   no `b-` class at all -- 24 of them run `infinite`, one of them a
   `marquee 26s linear infinite` -- so every one of those walked straight past
   this block, and would have kept running for somebody who had asked the whole
   operating system to stop moving things. A guard a paste can slip out of is
   not a guard.

   `.b-app *` closes it. `.b-app` is on the <body> of all six pages that link
   this file, so the reach is „everything on our pages" and it stops exactly
   there -- it does not leak into cabinet.css's or landing.css's markup on any
   page that has not opted in, which is the same containment rule section 17
   follows. The two prefix selectors stay for the rare `b-` element rendered
   outside a `.b-app` body.

   `!important` is not tidiness either: an inline `animation:` beats any
   stylesheet declaration without it, and inline is the only place the design's
   animations live.

   THE THIRTEEN KEYFRAMES ARE NEUTRALISED BY REACH, NOT BY NAME, and that is
   deliberate. A block that listed `rise, riseIn, pop, floaty, floaty2, marquee,
   blink, grow, dash, spinSlow, pulseRing, sheen, barIn` would be a hand-written
   list of exactly the thing being checked -- the fault sharedAssetVersions and
   typeScale both record paying for -- and a fourteenth keyframe would walk past
   it silently. Duration, iteration count and delay are properties of the
   ELEMENT, so every animation on every element inside our pages is covered,
   including one nobody has written yet.

   The descendant and pseudo-element forms of the prefix rules close the last
   gap. `[class^="b-"]` reaches a `b-` element rendered outside a `.b-app` body
   but neither its unclassed children nor its generated boxes -- and both of
   those are real: `dash` runs on an inline SVG's `<path>` (.b-kpi-spark below),
   and `floaty` runs on .b-empty-art::before. Today all six pages that link this
   file carry .b-app on <body>, so the first four selectors already cover
   everything; the list does not depend on that staying true.

   `animation-delay: 0s` is not decoration either. Duration alone leaves the
   delay intact, so `.b-rise-5`'s `.30s both` holds the element at opacity 0 for
   three tenths of a second and then snaps it in -- for somebody who asked the
   operating system to stop moving things, that is a blank card followed by a
   flash, which is worse than the animation. */
@media (prefers-reduced-motion: reduce) {
  .b-app,
  .b-app *,
  .b-app *::before,
  .b-app *::after,
  [class^="b-"], [class^="b-"]::before, [class^="b-"]::after,
  [class*=" b-"], [class*=" b-"]::before, [class*=" b-"]::after,
  [class^="b-"] *, [class^="b-"] *::before, [class^="b-"] *::after,
  [class*=" b-"] *, [class*=" b-"] *::before, [class*=" b-"] *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
    transition-duration: .001ms !important;
    /* Duration alone is not enough for the drawer: it delays its visibility
       flip by the length of the slide so the panel is not blanked halfway out
       (section 18). With the slide gone the delay is just a quarter-second in
       which an off-screen drawer still counts as visible. */
    transition-delay: 0s !important;
  }
}

/* Staggered entrances. The design writes `animation:rise .45s .12s both` inline
   dozens of times with only the delay changing; these cover the ladder. */
.b-rise   { animation: rise .45s both; }
.b-rise-1 { animation: rise .45s .06s both; }
.b-rise-2 { animation: rise .45s .12s both; }
.b-rise-3 { animation: rise .45s .18s both; }
.b-rise-4 { animation: rise .45s .24s both; }
.b-rise-5 { animation: rise .45s .30s both; }
.b-rise-in { animation: riseIn .35s both; }
.b-pop     { animation: pop .2s both; }

/* ==========================================================================
   2. Page scaffolding
   ========================================================================== */

.b-app {
  min-height: 100vh;
  background: var(--bg);
  color: var(--fg);
  font-family: 'Noto Sans Georgian', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}
.b-app *,
.b-app *::before,
.b-app *::after { box-sizing: border-box; }

/* THE TWENTY-NINTH SIZE, WHICH IS IN NO FILE AND SO IN NO SEARCH.
   A <button>, <input>, <select> or <textarea> that nobody gives a font-size to
   does NOT inherit one: form controls are the exception to inheritance, and
   Chrome hands them its own default of 13.333px in Arial. That number appears
   in no stylesheet, so grepping for it finds nothing and typeScale.test.ts --
   which reads files -- cannot see it either. It is a size on no scale, arrived
   at by omission, and it was live on nine controls across the pages that could
   be loaded without signing in: the landing page's nav toggle and scroll-top,
   the cabinet's sidebar toggle, its bell and its workspace switcher, and the
   sliders and checkboxes on both.
   Every `.b-*` control below sets `font-family: inherit` and most set a size,
   which is why this was invisible: the ones that forgot looked identical to
   the ones that did not.
   `var(--type-body)` and NOT `inherit`, deliberately. `inherit` would have
   given them whatever the ancestor happened to be -- 16px from the root on
   both pages measured, which is a rung nobody chose for a nav toggle -- and it
   would leave the size once again in no file. This resolves to a named rung.
   Specificity is (0,0,1), so every class in this file and in cabinet.css,
   landing.css and admin.css still outranks it, including the 16px
   phone-width rules tapZoom.test.ts owns. It is a floor, not a policy. */
button,
input,
select,
textarea { font-size: var(--type-body); }

/* `:not(.b-btn)` is load-bearing, not tidiness. `.b-app a` is (0,1,1) and every
   button variant below is (0,1,0), so a link styled as a button lost its own
   colour to this line: `<a class="b-btn b-btn-fill">` rendered accent-on-accent
   and the word inside it was simply invisible -- and only on a device with no
   hover, because `.b-btn-fill:hover:not(:disabled)` is specific enough to win
   and painted the text back in. The outline and ghost anchors were wrong the
   same way, just legibly so. Excluding buttons here lets each variant own its
   own colour, which is what the rest of this file already assumes. */
/* `.btn` is excluded for the same reason as `.b-btn`, and it is not a tidiness
   point: this selector is (0,2,1) and `.btn-gradient` is (0,1,0), so an anchor
   carrying the CABINET's older button classes lost its own colour to this rule
   and was painted var(--acc) on a var(--acc) fill. „Facebook გვერდის მიერთება"
   on the channels screen rendered as a green rectangle with an invisible label
   -- present in the DOM, readable to a screen reader, and gone from the page.
   Nothing failed: a contrast test reads the declared colour, and this one is
   declared correctly two rules away. The cabinet still holds both button
   vocabularies, so both have to be named here until it holds one. */
.b-app a:not(.b-btn):not(.btn) { color: var(--acc); text-decoration: none; }
.b-app a:not(.b-btn):not(.btn):hover { color: var(--fg); }
.b-app ::selection { background: var(--acc); color: var(--accFg); }

/* Keyboard focus. The design hides outlines on most controls; putting one ring
   back in one place is cheaper than 200 screens each forgetting to.

   `!important` IS THE RULE, NOT NOISE ON IT -- do not tidy it away. The design
   file carries `outline:none` as an INLINE STYLE on about thirty of its inputs.
   An inline declaration beats any stylesheet declaration that is not
   !important, so without this keyword, pasting that markup in deletes the focus
   ring for every keyboard user, on every screen it was pasted onto, silently:
   a missing ring looks exactly like a page nobody has tabbed into yet, so
   nothing on screen says it happened and no test catches it by looking at a
   colour. The ring is the accessibility floor, so it wins over the markup
   rather than negotiating with it. If a control genuinely must not show one,
   delete the inline `outline:none` and give that control its own rule -- do not
   weaken this one. */
.b-app :focus-visible {
  outline: 2px solid var(--acc) !important;
  outline-offset: 2px !important;
}

/* Sticky translucent header. panelBlur is the only token that already carries the
   right alpha for this, which is what it exists for. */
.b-topbar {
  position: sticky;
  top: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 16px;
  padding: 10px 22px;
  background: var(--panelBlur);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border-bottom: 1px solid var(--line);
}
.b-topbar-spacer { flex: 1; }

.b-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: 'Bricolage Grotesque', sans-serif;
  font-weight: 800;
  font-size: var(--type-title);
  letter-spacing: -.02em;
  color: var(--fg);
}
.b-brand-mark {
  width: 26px;
  height: 26px;
  border-radius: 9px;
  background: var(--acc);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.b-brand-mark::after {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accFg);
}
/* „BotAI.ge" IS ONE WORD, AND UNTIL NOW ONLY THE MARKUP SAID SO.
   This wrapper is used on all six pages and no stylesheet defined it -- the
   same shape as the `b-delta-down` that was never a class. It was not doing
   nothing: .b-brand is a flex container with `gap: 10px`, so without the span
   „.ge" becomes its own flex item and the gap pushes it 10px off „BotAI". The
   span holds the brand together; this rule is that job written down.

   COHESION ONLY. No colour, no size, no spacing: „.ge" stays lighter and
   thinner (.b-brand-tld below), the 10px gap still separates the MARK from the
   word, and the brand reads exactly as it reads today.

   Measured before it was written, at 320 / 375 / 768 / 1440 against the real
   stylesheets, on the cabinet topbar, the platform topbar and the landing
   header: the word is 75x24 at every width and „.ge" never leaves its line.
   „BotAI.ge" carries no break opportunity, so nothing splits it today. What
   this insures against is one inherited property: with `overflow-wrap:
   anywhere` or `word-break: break-all` reaching it -- and the first of those is
   already used in this codebase, on the staff modal's address line -- the brand
   shatters into SEVEN lines, 168px tall, with „.ge" 92px below the „B".
   `white-space: nowrap` beat both in the same measurement. */
.b-brand-word { white-space: nowrap; }
.b-brand-tld { opacity: .4; font-weight: 500; }

/* Console shell: fixed-width rail + fluid main, and ONE scroll -- the page's.
   The rail used to be `position: sticky; height: calc(100vh - 53px);
   overflow: auto`, which gave it a scrollbar of its own beside the document's:
   two scrollers side by side, and the owner's report was exactly that --
   „მენიუს ცალკე სქროლი აქვს, ერთი სქროლი ყოფნის". It is in the flow now and
   scrolls with the page on /cabinet and /admin alike.

   Why not sticky-without-overflow: a sticky box taller than the viewport hides
   its own bottom until the whole page has scrolled past, and the cabinet's rail
   (picker, eleven nav rows, the usage card, the foot) is taller than a laptop
   screen. A rail whose last rows you cannot reach is worse than one that moves.

   NOT `align-items: stretch`, although that would draw the column for free:
   the cabinet's rail pushes its usage card and account foot down with
   `margin-top: auto`, and a rail stretched to a 4000px page puts both at the
   very end of it. The rail is instead at least one screen tall (min-height --
   a floor, never a scroller), so the foot lands where it always did on load,
   and the column below it is painted by the SHELL's background: the rail's
   own panel colour for 257px and its 1px border line after. A background and
   not an absolutely placed ::before, because a pseudo-element would need
   `isolation` to sit behind the rail, and a new stacking context around the
   shell would put every fixed modal rendered inside main under the topbar.
   --b-header-h is the topbar both consoles measure and publish; 53px is the
   one-row fallback. */
.b-shell {
  display: flex;
  align-items: flex-start;
  background: linear-gradient(to right, var(--panel) 257px, var(--line) 257px, var(--line) 258px, transparent 258px) no-repeat;
}
.b-shell:has(> .b-sidebar-muted) {
  background-image: linear-gradient(to right, var(--panel2) 257px, var(--line) 257px, var(--line) 258px, transparent 258px);
}
.b-sidebar {
  width: 258px;
  flex: none;
  border-right: 1px solid var(--line);
  padding: 20px 14px;
  min-height: calc(100vh - var(--b-header-h, 53px));
  background: var(--panel);
}
.b-sidebar-muted { background: var(--panel2); color: var(--fg); }
/* THE MEASURE, AND WHY IT IS ON BOTH OF THESE.
   Measured on /app at a 1920px viewport before this line existed: .b-main was
   1647px wide and .b-container inside it was 1180 -- so the console already had
   a measure, but only for markup that remembered to wrap itself in
   .b-container. A screen that renders straight into .b-main got the full 1647,
   which is a KPI tile 387px wide: a stat tile the width of a phone, and a line
   of prose nobody can track back to the left edge of.

   1232 is 1180 plus .b-main's own 26px of padding on each side, so the two caps
   describe the SAME measure from the inside and the outside and cannot drift
   into disagreeing. With .b-container present nothing moves at any width --
   verified at 1920: content box 1180px at x=492 before and after. Without it,
   the page is now capped anyway instead of sprawling. Below 1232 both are
   inert, so every width the app is actually read at is untouched. */
.b-main { flex: 1; min-width: 0; max-width: 1232px; margin-inline: auto; padding: 24px 26px 60px; }
.b-container { width: 100%; max-width: 1180px; margin: 0 auto; }

/* ONE CONSOLE BREAKPOINT, 860px, AND IT IS THE SAME ONE AS SECTION 18's.
   This block used to be 900px while the drawer, the hamburger and the tab bar
   below all engage at 860, which made 861-900px A THIRD LAYOUT forty pixels
   wide: .b-shell was already a stacked block, so the 258px rail had turned into
   a full-width horizontal band across the top of the page, while .b-navdrawer
   had not engaged, so nothing had turned it into a drawer and there was no
   hamburger to open one. Measured at 880px before this was folded: .b-sidebar
   865px wide, static, sitting above the content; .b-hamburger display:none.
   Nobody designed that; it was the gap between two numbers that were meant to
   be one.

   Folding it down rather than up on purpose. 861px is a small laptop or a
   landscape tablet, and the design's own single breakpoint is 860 -- at 861 the
   rail plus the content leaves 551px of content box, which is wider than the
   785px-wide tablet reads at once the rail is a drawer. Raising 860 to 900
   instead would have taken the drawer away from a size that wants it. */
@media (max-width: 860px) {
  .b-shell { display: block; background: none; }
  .b-sidebar {
    width: auto;
    min-height: 0;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  .b-main { padding: 16px 14px 48px; }
}

/* ==========================================================================
   3. Typography
   ========================================================================== */

.b-h1, .b-h2, .b-h3 {
  font-family: var(--hf), 'Noto Sans Georgian', sans-serif;
  font-weight: 800;
  letter-spacing: -.02em;
  color: var(--fg);
  margin: 0;
}
.b-h1 { font-size: var(--type-hero); letter-spacing: -.025em; line-height: var(--type-hero-lh); }
.b-h2 { font-size: var(--type-section); }
.b-h3 { font-size: var(--type-heading); }

/* Hero headline: fluid so the Georgian ascenders never wrap mid-word on mobile.

   1.15 AND NOT THE 1.02 THIS CARRIED, WHICH IS A LATIN DISPLAY VALUE.

   THE VALUE IS RIGHT; THE MECHANISM THIS PARAGRAPH USED TO DESCRIBE WAS NOT,
   and the difference decides where the next person is allowed to set a leading.
   Noto Sans Georgian has TWO heights and they are 0.34em apart. Read off the
   font at 100px with measureText:

     - metric box  = ascent 1.07em + descent 0.29em = 1.36em
       This is the box the browser reserves. Any line-height below 1.36 makes
       consecutive metric boxes OVERLAP. That is where the „11.3px at 34px" this
       comment used to cite comes from -- 1.36 x 34 = 46.24px of box against a
       1.02 x 34 = 34.68px line, a shortfall of 11.56px -- and it is a fact
       about reserved space, not about anything visible.

     - ink        = ascent 0.77em + descent 0.25em = 1.02em
       This is where the glyphs actually are. INK COLLISION -- a descender
       landing on the ascender under it, which is the thing a reader sees --
       begins below 1.02, not below 1.36.

   So 1.02 was not „slightly tight". It is exactly the ink height, i.e. the
   value at which the deepest descender of one line touches the tallest
   ascender of the next with zero clearance, and „თქვენი ბიზნესი / პასუხობს"
   showed it first only because that pair happens to put a descender directly
   over an ascender. Which letters collide first is data; that the leading had
   no clearance at all was the bug.

   1.15 buys 0.13em of clearance over the ink (4.4px at 34px) while staying
   inside the metric box, which is what „tight display leading" means on this
   alphabet.

   THE RULE, so this does not have to be rediscovered: ANY LINE-HEIGHT UNDER
   1.36 ON THIS ALPHABET IS A DELIBERATE MEASURED DECISION AND MUST SAY WHAT IT
   WAS MEASURED AGAINST. 1.36 is where the boxes start overlapping and 1.02 is
   where the ink does; between them is a judgement about clearance, and below
   1.02 there is no judgement left to make. A value carried over from Latin
   display type -- where the same two numbers are roughly 1.2 and 0.9 -- lands
   inside Georgian's ink and is never a style choice, whatever it looked like
   in the file it came from. */
.b-display {
  font-family: var(--hf), 'Noto Sans Georgian', sans-serif;
  font-weight: 800;
  /* type-scale-exempt(fluid): a range, not a rung -- the ceiling answers the
     viewport. The floor is the hero step, so the two cannot drift apart. */
  font-size: clamp(var(--type-hero), 5vw, 72px);
  line-height: 1.15;
  letter-spacing: -.03em;
  margin: 0;
  color: var(--fg);
  text-wrap: balance;
}

.b-lead { font-size: var(--type-lead); line-height: var(--type-lead-lh); color: var(--fg2); margin: 8px 0 0; }
.b-muted { color: var(--fg2); }
.b-sub { font-size: var(--type-body); color: var(--fg2); }

.b-eyebrow {
  font-size: var(--type-meta);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg2);
}

/* Numerals. Bricolage has the tabular figures the KPI grid needs; Georgian text
   never uses this face.

   Which is also why every size below carries `type-scale-exempt(numerals)`: the
   type scale in tokens.css is derived from Noto Sans Georgian's metrics -- ink
   at 0.77em because Mkhedruli is unicase -- and none of that arithmetic governs
   a Latin-only digit face. This ramp was never the drift either: five sizes,
   one declaration each, no half-pixel neighbours. */
.b-num {
  font-family: 'Bricolage Grotesque', sans-serif;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
  color: var(--fg);
}
.b-num-sm { font-size: 17px; /* type-scale-exempt(numerals): Bricolage Grotesque */ }
.b-num-md { font-size: 26px; /* type-scale-exempt(numerals): Bricolage Grotesque */ }
.b-num-lg { font-size: 32px; /* type-scale-exempt(numerals): Bricolage Grotesque */ }
.b-num-xl { font-size: 44px; letter-spacing: -.03em; /* type-scale-exempt(numerals): Bricolage Grotesque */ }

/* „∞" where a price would be -- the volume above the ceiling we quote to.
   Sized on its own rather than borrowed from a step of the numeric scale: the
   glyph has no ascender and no descender, so at 32px it reads as a small mark
   floating in a slot built for a four-digit figure. The line-height comes down
   with it so the row does not grow at exactly the moment the boundary is
   crossed, which is what made the first attempt at this jump 22px on the public
   page. `tabular-nums` above is harmless here and is left alone.

   Placed beside the scale rather than in a screen's stylesheet because both
   calculators use it -- the cabinet's and the public page's -- and one of them
   cannot see the other's CSS. */
.b-num-boundless { font-size: 56px; line-height: .8; letter-spacing: 0; /* type-scale-exempt(numerals): Bricolage Grotesque */ }

.b-delta { font-size: var(--type-body); font-weight: 700; color: var(--pos); }
.b-delta-neg { color: var(--neg); }

.b-divider { height: 1px; background: var(--line); border: 0; margin: 0; }

/* ==========================================================================
   4. Buttons
   ========================================================================== */

.b-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  border: 0;
  border-radius: 100px;
  padding: 11px 20px;
  font-family: inherit;
  font-size: var(--type-body);
  font-weight: 700;
  line-height: 1.2;
  cursor: pointer;
  text-decoration: none;
  /* ---------------------------------------------------------------------
     A BUTTON WHOSE LABEL IS A SENTENCE MUST WRAP, NOT MOVE THE PAGE.

     This carried `white-space: nowrap`, and the cost of that was measured on
     the LIVE cabinet at a 375px viewport: the account-gate banner's
     „ვერიფიკაციის წერილის ხელახლა გაგზავნა" rendered 348.7px wide inside a
     299px `.gate-banner-actions` row, ran to x=387, and gave the whole page a
     horizontal scrollbar -- the failure that outranks everything else here.
     Georgian is why it bites us and not the English screen this was copied
     from: the words are longer, they do not abbreviate, and „Resend" is one
     word where „ხელახლა გაგზავნა" is two long ones.

     Dropping nowrap costs nothing in the common case, because of how the two
     sizing rules meet. A flex item's base size is its MAX-content size, so a
     button in a row still takes one line wherever the row has space -- which
     is every button in the modal feet, the topbar and the card heads. Only
     when the container is genuinely too narrow does it fall back to its
     MIN-content size and take two lines, and two lines inside the button is
     the correct answer there. Single-word Georgian labels („შენახვა",
     „წაშლა") cannot wrap at all: `normal` breaks at spaces, and they have
     none. So the guarantee nowrap was protecting is still held by the words.

     `max-width: 100%` and `overflow-wrap` are the floor under that: a button
     can never be wider than what holds it, and a single word longer than the
     button breaks rather than painting outside it -- „text showing outside its
     control" is the other half of the same report and it is closed here too,
     not traded for.

     `text-align: center` because .b-btn centres its content as a flex box but
     the lines INSIDE a wrapped label align by text-align, which inherits --
     so without it a two-line button is centred as a block and ragged inside. */
  max-width: 100%;
  white-space: normal;
  overflow-wrap: break-word;
  text-wrap: balance;
  text-align: center;
  background: transparent;
  color: var(--fg);
  transition: background .2s, color .2s, border-color .2s, transform .2s, opacity .2s;
}
.b-btn:disabled,
.b-btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

.b-btn-fill { background: var(--acc); color: var(--accFg); }
.b-btn-fill:hover:not(:disabled) { opacity: .88; color: var(--accFg); }

.b-btn-outline { border: 1px solid var(--line); color: var(--fg); font-weight: 600; }
.b-btn-outline:hover:not(:disabled) { border-color: var(--acc); color: var(--fg); }

.b-btn-ghost { color: var(--fg2); font-weight: 600; }
.b-btn-ghost:hover:not(:disabled) { background: var(--panel2); color: var(--fg); }

/* Sits on top of an inverted (dark ink) surface -- the footer and the CTA band. */
.b-btn-inv { background: var(--invSoft); border: 1px solid var(--invLine); color: var(--invFg); font-weight: 600; }
.b-btn-inv:hover:not(:disabled) { background: var(--invLine); color: var(--invFg); }

.b-btn-danger { background: var(--neg); color: var(--negFg); }
.b-btn-danger-quiet { color: var(--neg); font-weight: 600; }
.b-btn-danger-quiet:hover:not(:disabled) { background: var(--panel2); color: var(--neg); }

.b-btn-sm { padding: 7px 14px; font-size: var(--type-meta); }
.b-btn-lg { padding: 14px 26px; font-size: var(--type-emphasis); }
.b-btn-block { width: 100%; }

/* Round icon button (bell, close, kebab). */
.b-btn-icon {
  width: 34px;
  height: 34px;
  padding: 0;
  flex: none;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: transparent;
  color: var(--fg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-family: inherit;
  transition: border-color .2s, background .2s;
}
.b-btn-icon:hover { border-color: var(--acc); }
.b-btn-icon-sm { width: 28px; height: 28px; color: var(--fg2); font-size: 14px; /* type-scale-exempt(glyph-box): an icon inside a 28px square */ }

/* Squared icon button, used in toolbars where round would crowd. */
.b-btn-square {
  width: 32px;
  height: 32px;
  padding: 0;
  flex: none;
  border: 1px solid var(--line);
  border-radius: 11px;
  background: transparent;
  color: var(--fg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-family: inherit;
  transition: border-color .2s;
}
.b-btn-square:hover { border-color: var(--acc); }

/* ==========================================================================
   5. Segmented control / tabs / chips
   ========================================================================== */

/* --------------------------------------------------------------------------
   A SEGMENTED CONTROL WRAPS, AND HAS TO LOOK LIKE IT MEANT TO.

   The report was „ბევრგან მენიუ ან ტაბები ფეიჯიდან გვერდზე გადადის, ასევე
   ტაბებიდან ტექსტები გარეთ ჩანს რადგან ტაბები დავიწროებულია ზოგგან". Measured
   on the cabinet's settings tabs AS THEY WERE THEN -- five of them, ძირითადი /
   მუშაობა / ბიზნესის ინფო / შეტყობინებები / სხვა -- and on the panel's
   four-item `.b-seg-fill`. There are FOUR now (მუშაობა / ბიზნესი /
   შეტყობინებები / სხვა): `a40f090` and `4b303f7` dissolved „ძირითადი" onto the
   dashboard, billing and knowledge screens and shortened „ბიზნესის ინფო" to
   „ბიზნესი". The numbers below are the five-tab measurement and are left as
   they were taken -- what they establish is that the strip wraps and what it
   looks like when it does, which is why this rule exists at all. What the
   four-tab strip costs today is in the last paragraph.

     320px: the settings strip wrapped to THREE rows and stood 106px tall.
     375px: TWO rows, 72px. On the `.b-seg-fill` variant, three of five
       segments were two lines high and two were one -- so the buttons inside a
       single control were different heights -- and the one item left alone on
       the last row was stretched to 341px to fill it, a 74px word in a 341px
       button.

   The wrapping is not the bug. THE PILL IS. A 100px border-radius over a 72px
   or 106px box is a stretched lozenge, and a lozenge with three rows of
   buttons in it reads as a control that has come apart, not one that adapted.
   Two rows of a 20px-radius track reads as a control with two rows.

   `border-radius: 20px` is the fix and it introduces NO third number: the file
   allows 20 for containers and 100 for pills, and border-radius is clamped to
   half the box, so on a one-row 38px strip 100px was already rendering as 19px.
   At one row the change is invisible (19px vs 20px); at two it is the whole
   difference. The segments keep their own 100px, because a segment IS a pill.

   WRAPPING IS THE DEFAULT BECAUSE OF THE ALPHABET, NOT BECAUSE THE STRIP
   FAILED. Re-measured 2026-08-30 at this rule's own typography (13px / 600):
   „პარამეტრები" is 93.3px of text in a 123.3px pill where the English
   „Settings" is 47.8px -- the Georgian label is very nearly TWICE the width of
   the English word for the same idea. „წვდომა აკრძალულია" is 150.4px;
   „რეგისტრირებული" is 127.5px. Georgian has no case change and no abbreviation
   to fall back on, so a tab row sized against English words overflows here as a
   matter of course, at every width, for every screen. A layout that has to
   survive that is a layout that wraps. (The pair this paragraph used to cite --
   „ასისტენტის პარამეტრები", 179.6px of text -- made the point more loudly and
   no longer exists; the label was shortened in `4b303f7`. The ratio is what
   carries the argument, and the ratio did not move.)

   HORIZONTAL SCROLLING IS REFUSED, and there is a measurement behind it as well
   as the owner's „ვფიქრობ ჰორიზონტალური სქროლინგი ცუდი გადაწყვეტაა". The
   settings strip was made scrollable and re-measured: the SELECTED tab then sat
   at left 459 in a 375px viewport -- off screen. Somebody opening that page
   sees three tabs, none of them highlighted, and nothing at all telling them
   the row continues.

   THAT 459 WAS MEASURED BEFORE THE RENAME, on „ასისტენტის პარამეტრები", and is
   left here rather than quietly re-attributed to the shorter label. What can be
   said about today without re-measuring the cabinet: the assistant's sub-tab
   strip went 699.2 -> 612.9px with the rename, so it is still 238px wider than
   a 375px viewport and the last tab still begins past the right edge. The
   finding survives the label; only its exact offset is stale.

   Making scrolling honest would take `scrollIntoView` on mount AND on every
   selection, plus an edge fade to signal there is more; wrapping needs none of
   it, because every tab is on screen and the active one is simply visible.
   Ellipsis is not on the table either: a person cannot choose a tab they cannot
   read, and a truncated „შეტყობინებები" is „შეტყო…" and nothing else.

   WHAT WRAPPING COSTS, so the trade is visible rather than assumed: one extra
   row is +34px of page. That is cheap against the requirement it serves.

   THE FIX THIS PARAGRAPH ASKED FOR HAS LANDED, and the numbers are worth
   keeping because they are what said it was needed. With FIVE tabs the strip
   was 103 + 88 + 135 + 137 + 61 = 524px of segment against 284px of usable
   track at 320px, which is THREE rows (106px tall), and neither dropping the
   padding to the file's smallest step nor the type to its smallest size got it
   under two (312px and 294px of row-one candidates against 284px). The
   conclusion was that five Georgian tabs are too many for one row at 320px at
   ANY typography this file owns, and that the fix was four tabs in cabinet.js's
   tab list rather than anything here.

   `a40f090` did that. The four are 88 + 85 + 137 + 61 = 371px of segment
   against the same 284px track -- still over, so still TWO rows at 320px, but
   no longer three. Two rows is the shape this rule's 20px radius was designed
   for, so the control now looks like what it is. Getting to one row would need
   a track ~87px wider than a 320px phone has, which is not a typography
   problem and is not solvable here. */
.b-seg {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  /* Not 100px -- see above. Identical at one row, deliberate at two. */
  border-radius: 20px;
  background: var(--panel2);
}
.b-seg-item {
  border: 0;
  background: transparent;
  color: var(--fg2);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--type-body);
  font-weight: 600;
  padding: 7px 15px;
  border-radius: 100px;
  /* Two jobs, and the second is the one that stops text leaving its control.
     It keeps a two-word label on one line rather than broken across two inside a
     single segment -- which is what made three segments of one strip two lines
     high while the other two stayed one.

     THE LABEL THAT MOTIVATED IT IS GONE: that was „ბიზნესის ინფო", shortened to
     the single word „ბიზნესი" in `4b303f7`, and a single word cannot break
     across two lines. The first job is not dead, because the strips still carry
     multi-word labels („წვდომა აკრძალულია", 150.4px) and nothing stops the next
     one being written. But the SECOND job is now the load-bearing half, and it
     never depended on word count at all. And because a flex item's automatic
     minimum size is its MIN-CONTENT size, `nowrap` is also what stops
     `.b-seg-fill`'s `flex: 1` squeezing a segment narrower than its own label:
     with it, the segment cannot go under label + 30px of padding, so the strip
     wraps instead of the label spilling.

     The overflow this could cause instead is a SINGLE label wider than the whole
     row, which would push the page sideways. Measured against the widest label
     either console has -- „რეგისტრირებული", 128px at this size, 158px with the
     padding -- against a 284px track at 320px: 1.8x of headroom. A future label
     over ~280px would need this reconsidered, and that is a label nobody should
     be writing on a tab. */
  white-space: nowrap;
  transition: background .25s, color .25s, opacity .25s;
}
.b-seg-item:hover { opacity: .75; }
.b-seg-item.is-active { background: var(--acc); color: var(--accFg); }
.b-seg-fill .b-seg-item { flex: 1; text-align: center; }

/* EQUAL WIDTHS ARE A ONE-ROW IDEA. Once a `.b-seg-fill` strip has wrapped,
   `flex: 1` is measured per ROW, so the item left alone on the last row is
   stretched to the full width of the control: measured at 375px, the panel's
   „უპატრონო" -- a 74px word -- was rendered 341px wide under a row of three
   items 81, 98 and 158px wide. That is the „ძაან ჩამოიწელოს" in the
   requirement, occurring inside a single widget.

   Below 640 the fill is therefore dropped and the segments go back to their
   content widths: the same two rows, the same 72px, but the orphan is 104px
   instead of 341 and the strip reads as a wrapped list of options, which is
   what it is. Above 640 the strip is one row and `flex: 1` is doing exactly
   what it was written for, so it is left alone. */
@media (max-width: 640px) {
  .b-seg-fill .b-seg-item { flex: 0 0 auto; }
}

/* Standalone filter chip -- same states, but bordered instead of tracked. */
.b-chip {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--fg2);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--type-meta);
  font-weight: 600;
  padding: 7px 14px;
  border-radius: 100px;
  /* Same reason as .b-seg-item and as .b-pill above it: a filter chip broken
     across two lines inside a 100px-radius outline is a control that looks
     damaged. Georgian filter labels run long -- „ვადაგადაცილებული" is 137px at
     this size -- and the row they sit in wraps, so the chip never has to. */
  white-space: nowrap;
  transition: background .2s, color .2s, border-color .2s;
}
.b-chip:hover { border-color: var(--acc); color: var(--fg); }
.b-chip.is-active { background: var(--acc); color: var(--accFg); border-color: var(--acc); }
/* A CHIP THAT IS OFF LOOKS OFF. `.b-btn` has carried this pair since it was
   written and `.b-chip` never did, so a `disabled` chip was pixel-identical to
   a live one and still lit up on hover -- which is the „live-looking control
   that does nothing" failure with the disabling already in place. It matters
   now because the carry-over draws the delivery's not-yet-built controls inert
   rather than refusing them. The hover rule has to be repeated: `.b-chip:hover`
   above is (0,2,0) and would otherwise paint the accent border back on. */
.b-chip:disabled,
.b-chip[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.b-chip:disabled:hover,
.b-chip[aria-disabled="true"]:hover { border-color: var(--line); color: var(--fg2); }

/* THE STRIP CONTAINER, WHICH IS WHAT `.b-table-wrap` WAS AND CHIPS NEVER HAD.
   A wide table has one shared answer to „what if this does not fit"; a row of
   chips had none, so every screen holding one writes `flex-wrap: wrap` inline
   or in its own stylesheet and any screen that forgets pushes the page
   sideways. Reproduced: five billing-filter chips in a plain
   `display:flex; gap:6px` row at a 375px viewport ran from x=333 to x=612 --
   237px outside the viewport, with the document 612px wide against a 375px
   layout box, i.e. a horizontal scrollbar on the whole page. (Both containers
   that hold chips today do remember; that is luck, not a design.)

   Chips WRAP and segments wrap too, but the reasoning differs and is worth
   keeping apart: a segment lives inside a shared track, so wrapping changes the
   shape of one control and the track has to be drawn to survive it. A chip is
   its own bordered button with its own background, so a second row of chips
   looks like exactly what it is and needs nothing else. */
.b-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--b-strip-gap, 8px);
}

/* ==========================================================================
   6. Cards and panels
   ========================================================================== */

.b-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 22px;
}
.b-card-tight { padding: 16px; }
.b-card-roomy { padding: 30px; }
.b-card-flat { background: var(--panel2); border-color: transparent; }
.b-card-accent { border-color: var(--acc); }
.b-card-flush { padding: 0; overflow: hidden; }

/* Empty states and "add new" tiles. */
.b-card-dashed {
  background: var(--panel);
  border: 1.5px dashed var(--acc);
  border-radius: 20px;
  padding: 34px;
  text-align: center;
  transition: background .25s;
}
.b-card-dashed:hover { background: var(--panel2); }

/* THE EMPTY STATE -- one component, eight screens.
   (dashboard · conversations · bookings · analytics · bots · billing · settings
   · support -- the delivery's own EMPTIES map, which is eight copies of one
   card differing only in three strings.)

   MARKUP:
     <div class="b-card b-empty">
       <div class="b-empty-art" aria-hidden="true">
         <div class="b-empty-frame"><span class="b-empty-dot"></span></div>
       </div>
       <div class="b-empty-title">…</div>
       <p class="b-empty-sub">…</p>
       <div class="b-empty-actions">
         <button class="b-btn b-btn-fill">…</button>
         <button class="b-btn b-btn-outline">…</button>   <- optional
       </div>
     </div>

   THE THREE STRINGS ARE THE CALLER'S AND NOT THIS FILE'S. Every screen already
   has an empty sentence written for it, and those are the ones that ship: the
   delivery's are drafts, and one of them („ტრიალის დასრულებამდე 11 დღეა") is a
   number typed into a template, which is the fault the two calculators exist to
   demonstrate. A shared renderer is a shared LAYOUT; the copy map stays per
   screen.

   .b-empty-actions WRAPS, and section 4 is why: a Georgian CTA that does not fit
   next to a second button wraps to a second line rather than running off the
   page. The buttons keep .b-btn's `white-space: normal`. Do not add nowrap here.

   The art is decoration and carries aria-hidden: a dashed square with a blinking
   dot in it says nothing a screen reader can use, and the title underneath says
   all of it. Both of its animations (floaty 11s, blink 1.8s) are infinite and
   both are neutralised by section 1's reduced-motion block. */
.b-empty {
  padding: 64px 40px;
  text-align: center;
}
.b-empty-art {
  position: relative;
  width: 96px;
  height: 96px;
  margin: 0 auto 24px;
}
/* The blurred bloom behind the frame. Absolute and first in tree order, so the
   positioned frame paints over it without either needing a z-index. blur(30px)
   and not .b-blob's 70: this is a 96px lamp, not a hero-sized wash. */
.b-empty-art::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--acc);
  filter: blur(30px);
  opacity: .28;
  animation: floaty 11s ease-in-out infinite;
}
.b-empty-frame {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 32px;
  border: 1.5px dashed var(--acc);
  display: flex;
  align-items: center;
  justify-content: center;
}
.b-empty-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--acc);
  animation: blink 1.8s infinite;
}
/* 23px in the delivery -> --type-heading (20). 20 is 3px away and 28 is 5, and
   nothing on this card is a section head. */
.b-empty-title {
  font-family: var(--hf), 'Noto Sans Georgian', sans-serif;
  font-weight: 800;
  font-size: var(--type-heading);
  letter-spacing: -.015em;
  color: var(--fg);
}
/* 400px of measure, which at --type-emphasis is roughly 45 Georgian characters
   -- two lines of the sentences these screens actually carry. */
.b-empty-sub {
  font-size: var(--type-emphasis);
  color: var(--fg2);
  line-height: 1.6;
  margin: 10px auto 0;
  max-width: 400px;
}
.b-empty-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  margin-top: 26px;
}

/* A card that is itself the click target. */
.b-card-click {
  width: 100%;
  text-align: inherit;
  font-family: inherit;
  color: inherit;
  cursor: pointer;
  transition: transform .25s, border-color .25s;
}
.b-card-click:hover { transform: translateY(-2px); border-color: var(--acc); }

.b-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.b-card-title { font-size: var(--type-emphasis); font-weight: 700; color: var(--fg); }
.b-card-sub { font-size: var(--type-body); color: var(--fg2); margin-top: 3px; }

/* Inverted surface: the dark ink band the design uses for footers and CTAs. It
   flips with the palette, so it stays dark ink in light mode and a raised panel
   in dark mode -- which is what inv/invFg are for. */
.b-inv {
  background: var(--inv);
  color: var(--invFg);
  border-radius: 20px;
}
/* THE THREE QUIET TEXT CLASSES, AND `.b-hint` IS THE ONE THAT WAS MISSING.
   All three say exactly the same thing about colour -- `color: var(--fg2)` --
   and differ only in size and margin (`.b-hint` is --type-meta with a 6px top,
   `.b-sub` is --type-body, `.b-muted` is neither). Leaving `.b-hint` out meant
   an inverted card could hold a line of dark ink on a dark ground, which is
   what kept the billing screen's plan card off this surface for a stage. The
   swap-the-class fix was the other candidate and is worse: it would move that
   line up a rung of the type scale to buy a colour, and the type scale is
   settled work. */
.b-inv .b-muted,
.b-inv .b-sub,
.b-inv .b-hint { color: var(--invFg); opacity: .62; }
.b-inv-soft { background: var(--invSoft); border: 1px solid var(--invLine); }

/* --------------------------------------------------------------------------
   THE RESPONSIVE GRID, WHICH IS `auto-fit` AND A FLOOR MEASURED OFF THE
   CONTENT -- and is now the rule here rather than one exception hiding in the
   KPI row.

   What it replaces. The design file this product is being ported from has ONE
   effective breakpoint and 57 of its 65 grids carry exactly one mobile rule:
   become one column. A four-up row goes 4 -> 1 with no two-up step, and
   `auto-fit` / `auto-fill` appear in it zero times, so no grid ever gains a
   column however wide the screen. Measured on its own portal, narrowing from
   880px to 800px -- a tablet, not a phone -- added 2 123px of page, +57%. That
   is the „ძაან ჩამოიწელოს" in the requirement, and stacking short tiles is what
   causes it.

   How to use it. `.b-fit` is the mechanism; `--b-fit-min` is the only number,
   and it belongs to the CONTENT, not to the screen. Set it per screen inline
   where the presets do not fit -- the floor is per-screen data in exactly the
   way .b-grid-row's track list is:

     <div class="b-fit" style="--b-fit-min:180px">

   `min(100%, …)` around the floor is not decoration. Bare
   `minmax(390px, 1fr)` inside a 347px content box (a 375px phone, less
   .b-main's 14px of padding each side) produces a 390px track and 43px of
   PAGE-WIDE HORIZONTAL SCROLL -- the one failure that outranks every other
   consideration on this file. `min(100%, …)` clamps the floor to the box, so a
   grid too narrow for even one track degrades to one full-width column instead
   of pushing the document sideways. Every floor in this file is written that
   way, including the two KPI rows below.

   THE FLOORS ARE MEASURED, NOT ROUNDED. Each was taken with measureText
   against this product's own Georgian strings, in the face and size the class
   actually renders at, plus the padding of the box the content sits in.

     .b-fit (235px) -- a compact card: a title, one line under it, maybe a pill.
       Widest title in the cabinet, „შეტყობინებების არხები", is 190.5px at
       .b-card-title's 14.5px/700; .b-card adds 22px of padding on each side.
       190.5 + 44 = 234.5. Gives 1 column at 375, 3 at 785, 4 at 1115 -- and 1
       on a phone is CORRECT here and not the fault being fixed: a 190px title
       has nowhere to go in a 167px track.

     .b-fit-tight (126px) -- checkbox rows, weekday toggles, badges, icon+label
       pairs. Widest permission label, „შეტყობინებები", is 104.9px at 13px;
       add a 15px .b-check and its 6px gap. 104.9 + 21 = 125.9. Gives 2 columns
       at 375 and 5 at 785. This is the case the design gets wrong 57 times.

     .b-fit-wide (390px) -- a card carrying a paragraph. Georgian at .b-lead's
       15.5px averages 9.83px per character (a 40-character sample measures
       393.3px); 35 characters is the shortest line this alphabet's compounds
       survive without breaking badly. 35 x 9.83 = 344, + .b-card's 44 = 388.
       Gives 1 column at 375 AND at 785, 2 from ~865. Deliberately one column on
       a tablet: two-up prose is the same mistake in the mirror.

   WHY THERE IS STILL ONLY ONE BREAKPOINT, checked at 768px because the design's
   single 860 gives its tablet a 678px single column and two columns would have
   taken 1 023px (-17.5%) off that screen. Ours does not have that problem to
   solve, because ours is auto-fit and the design's is not. Measured columns at
   each container width the consoles actually produce:

                       347px   725px   757px   837px   1115px
                       (375)   (768)   (800)   (880)   (1440)
     .b-kpis             2       4       4       3       4
     .b-fit              1       2       3       3       4
     .b-fit-tight        2       5       5       6       7
     .b-fit-wide         1       1       1       2       2
     .b-cols-1           1       1       1     inline  inline
     .b-cols-1 + knob    2       5       5     inline  inline

   Every row but one already gains columns on a tablet with no breakpoint to
   help it. The exception is `.b-cols-1`, and a second breakpoint would be the
   wrong tool for it: the reason it is one column at 768 is not the width, it is
   that the CALLER has not said what its content needs -- which the floor says
   in one attribute, per grid, at every width at once. A `600-860` breakpoint
   would guess that on every caller's behalf, including the ones that must stay
   one column. So: one breakpoint, and the tablet is answered by the floor.
   -------------------------------------------------------------------------- */
.b-fit {
  display: grid;
  gap: var(--b-fit-gap, 14px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--b-fit-min, 235px)), 1fr));
}
.b-fit-tight { --b-fit-min: 126px; --b-fit-gap: 8px; }
.b-fit-wide  { --b-fit-min: 390px; }

/* WHAT WAS DELIBERATELY LEFT AT ONE COLUMN, because „two-up everything" is the
   same mistake in the mirror and this list is what stops it being redone:

     .b-grid-head / .b-grid-row -- a table without a <table>. Its columns are
       ALIGNED ACROSS ROWS; that is the entire reason it exists. auto-fit
       re-flows each row independently, so it would destroy the alignment while
       looking like it had improved something.
     .b-thread / .b-bubble -- a conversation is chronological and one column.
       Two columns of messages is not a denser transcript, it is an unreadable
       one.
     .b-list-row, .b-table -- rows.
     .b-field and everything in section 7 -- a form field beside a form field
       halves the room for the value and doubles the eye's travel between the
       label and the thing it names. The signup form in particular is settled
       and is not being rearranged.
     .b-modal / .b-modal-body -- the signup surfaces are a decision already
       made; nothing here changes how they are laid out.
     .b-nav -- a vertical rail. Two columns of navigation is a menu you have to
       read twice.
     .b-seg -- see section 5: it wraps rather than gridding, because its items
       are a track of alternatives and not a set of tiles. */

/* KPI grid -- the four-up stat row that opens most screens, and the instance of
   the rule above that existed first.

   200px: the widest KPI caption in either console, „დღიური შემოსავალი", is
   137.2px at .b-kpi-label's 12.5px, and .b-card adds 22px each side --
   137.2 + 44 = 181.2, so a 200px track holds the longest caption on one line
   with room for the value under it. */
.b-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: 14px;
}
/* Five tiles, which 200px tracks cannot fit on one row at any width the app is
   read at -- the fifth wrapped onto a row of its own and stretched the full
   width, so the row that is meant to be scanned in one look read as "four
   things, and then this other thing".
   Below 860 the .b-kpis rule in section 18 is declared later at the same
   specificity and takes this one's place with a 150px floor, which is smaller
   and therefore only ever gives the five tiles MORE columns; that is why this
   line does not need a narrow-screen counterpart of its own.

   "ONLY THE PLATFORM OVERVIEW HAS FIVE" IS WHAT THIS USED TO SAY, AND IT WAS
   ALREADY UNTRUE WHEN IT WAS WRITTEN. The cabinet's dashboard has had five
   since დაზოგილი დრო was added and its analytics screen has five since
   კმაყოფილება, and both were on the plain 200px floor -- so the fault this
   modifier exists to prevent was live on the two screens a tenant opens first.
   Measured at a 960px content width: 200px gives two rows, 165px gives one.

   AND THE 165 IS NO LONGER DERIVED FROM THE WIDEST CAPTION. It was 137.2px
   („დღიური შემოსავალი") plus .b-card's 44px, which fits. The cabinet's
   „ოპერატორზე გადართვა" is 157px, so at this floor it takes two lines -- which
   it already did at a 1100px width under the 200px floor, and which is the
   cheaper of the two failures: a caption on two lines is still one row to
   scan, a tile on its own row is not. The wording is not the thing to change. */
.b-kpis-5 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 165px), 1fr)); }

.b-kpi-label { font-size: var(--type-body); color: var(--fg2); }
.b-kpi-value { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 30px; letter-spacing: -.02em; margin-top: 6px; color: var(--fg); /* type-scale-exempt(numerals): Bricolage Grotesque */ }

/* KPI trend: the sparkline beside the number, and the delta line under it.
   Shared by the cabinet dashboard and the platform overview, which is the whole
   reason it is here rather than in either console -- the delivery draws it eight
   times and it is one component, not eight.

   MARKUP:
     <div class="b-card">
       <div class="b-kpi-label">…</div>
       <div class="b-kpi-sparkrow">
         <div class="b-kpi-value">128</div>
         <svg class="b-kpi-spark" viewBox="0 0 70 30" aria-hidden="true">
           <path d="M0 24 L12 18 L24 21 L36 10 L48 14 L60 5 L70 8"/>
         </svg>
       </div>
       <div class="b-kpi-delta b-kpi-delta-up">+12% კვირაში</div>
     </div>

   THE PATH CARRIES NO ATTRIBUTES AND THAT IS THE FEATURE. The delivery writes
   `stroke="var(--acc,#D98324)" stroke-width="2" stroke-dasharray="300"` on the
   element -- and #D98324 is amber-palette leakage (section 0). Presentation
   attributes lose to any CSS rule, so the rule below wins over a pasted one
   whether or not anybody remembered to strip it. Paste the path, keep the `d`,
   ignore the rest.

   `viewBox` and no width/height on the svg: the 70x30 is set here, so a caller
   cannot ship a fourth sparkline size, and the coordinate space stays 0 0 70 30
   so a `d` copied from the delivery lands where it was drawn.

   stroke-dasharray:300 is the reveal, and 300 is a ceiling rather than a
   measurement -- it is longer than any 70x30 polyline, so `dash` starts fully
   offset. A path longer than 300 units would start partly drawn; at this size
   nothing is. */
/* THE FIGURE AND ITS TREND SLOT SHARE ONE ROW UNTIL THEY CANNOT, on both
   consoles. The slot is 70px and `flex: none`; a row that may not wrap makes
   the figure's longest word plus 10px of gap plus 70px the tile's floor,
   whatever track the grid gave it. Measured on the platform overview at 375px
   (two 168.5px tracks, 136.5px inside a phone card): „არანაკლებ 12 480 ₾" and
   „არანაკლებ 99.95%" ran their rows 58px past the card and the tile 42px past
   the viewport, and the same two overflowed by 20px at 1440 -- so none of this
   is inside a media query. The cabinet dashboard builds the same row
   (`kpiValueNode` in js/dashboard.js) and was spared only by short figures.
   A flex line breaks on the figure's max-content width, so a short figure keeps
   its slot beside it at every width and a long one sends the slot under it;
   alone on its line, the figure may then shrink and wrap -- BETWEEN WORDS.
   min-width:0 on the tile is for the grid: an item otherwise holds its
   content's width against its track. This was admin.css's fix (2de57f9) and
   moved here whole, because the component is shared.

   A WORD IS NEVER CUT, AND `overflow-wrap: anywhere` WAS HOW IT WAS. That
   property let the figure shrink below its longest word, and the longest word
   is „არანაკლებ": 140.7px in the numeral size against 125px inside a 169px
   desktop track (22px padding each side), so production showed „არანაკლ" over
   „ებ". Taking the property away alone trades that for 17px past the card. So
   the qualifier is not set in the numeral size at all: `.b-kpi-floor` below
   prints it at body size on its own line, and the figure keeps only numbers,
   whose groups are three digits and always fit. `normal` is written out, not
   left to inheritance, so no ancestor can hand the break back. */
.b-kpis > * { min-width: 0; }
.b-kpi-sparkrow {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 10px;
  margin-top: 10px;
}
.b-kpi-sparkrow .b-kpi-value { margin-top: 0; min-width: 0; }
.b-kpi-value { overflow-wrap: normal; word-break: normal; }

/* „არანაკლებ" ABOVE A FIGURE: the claim that it is a floor, read before the
   number and never cut. Markup: <div class="b-kpi-value"><span
   class="b-kpi-floor">არანაკლებ</span>99.95%</div> -- the text of the tile is
   still „არანაკლებ 99.95%" to a screen reader, and `kpi()` in admin.js builds
   it from the same string the view returns. The caption's size and colour, so
   it reads as a qualifier on the number rather than as a second number. */
.b-kpi-floor {
  display: block;
  font-family: 'Noto Sans Georgian', system-ui, sans-serif;
  font-size: var(--type-body);
  font-weight: 700;
  letter-spacing: 0;
  color: var(--fg2);
  white-space: nowrap;
  margin-bottom: 2px;
}

.b-kpi-spark {
  width: 70px;
  height: 30px;
  flex: none;
  overflow: visible;
}
.b-kpi-spark path,
.b-kpi-spark-line {
  fill: none;
  stroke: var(--acc);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 300;
  animation: dash 1.4s .2s ease-out both;
}

/* THE DELTA'S DEFAULT IS THE MUTED COLOUR, NOT A DIRECTION, and this is the
   indicator rule again rather than a styling default. A delta we could not
   compute -- no prior period, a read that failed, a tenant one day old -- is
   neither good news nor bad, and painting it --pos because green is the pleasant
   option tells an owner their traffic rose when what happened is that we have
   nothing to compare against. Reach for a modifier only when the sign is known. */
.b-kpi-delta { font-size: var(--type-body); font-weight: 700; color: var(--fg2); margin-top: 8px; }
.b-kpi-delta-up { color: var(--pos); }
.b-kpi-delta-down { color: var(--neg); }

/* No grid-template-columns here: section 18 forces every layout grid to one
   column from 860px down, which is wider than this, so a two-up rule would
   never be reached. Only the sizes below still belong to this breakpoint. */
@media (max-width: 640px) {
  .b-kpis { gap: 10px; }
  .b-card { padding: 16px; border-radius: 16px; }
  .b-kpi-value { font-size: 24px; /* type-scale-exempt(numerals): Bricolage Grotesque */ }

  /* THIS LINE IS LOAD-BEARING AND LOOKS LIKE A TWEAK. `.b-empty` is worn
     alongside `.b-card`, both are (0,1,0), and the `.b-card` rule above is
     declared LATER in the file -- so without this, an empty state on a phone
     silently takes 16px of padding from a rule that was never thinking about
     it, and the 64px/40px above only ever applies on a desktop. Restated here
     so the phone value is a decision rather than a source-order accident.
     40px/18px: the desktop 40px each side leaves 267px of a 347px content box
     at 375px, which is not a measure, it is a column. */
  .b-empty { padding: 40px 18px; }
}

/* ==========================================================================
   7. Form fields
   ========================================================================== */

.b-label {
  display: block;
  font-size: var(--type-body);
  font-weight: 600;
  color: var(--fg2);
  margin-bottom: 7px;
}

.b-field {
  width: 100%;
  padding: 12px 15px;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--panel2);
  color: var(--fg);
  font-family: inherit;
  font-size: var(--type-body);
  outline: none;
  transition: border-color .2s;
}
.b-field::placeholder { color: var(--fg2); opacity: .8; }
.b-field:focus { border-color: var(--acc); }
.b-field:disabled { opacity: .55; cursor: not-allowed; }
.b-field-lg { padding: 13px 16px; border-radius: 13px; font-size: var(--type-emphasis); }
.b-field-pill { border-radius: 100px; }

/* ---------------------------------------------------------------------------
   16px IS NOT A TYPOGRAPHIC CHOICE HERE, IT IS THE ZOOM THRESHOLD.

   iOS Safari zooms the whole page in when a text field is focused whose
   computed font-size is under 16px -- and it does not zoom back out when the
   field is left. That is the „the site zooms on my phone" report, and the
   cure people reach for first is `user-scalable=no` in the viewport meta.
   That cure does not work: iOS has deliberately ignored `user-scalable` and
   `maximum-scale` since iOS 10, precisely so a page cannot take pinch-zoom
   away from somebody who needs it. On Android it does work, which is worse --
   it means shipping a page a partially sighted person cannot enlarge, to fix a
   symptom the platform that causes it will not honour.

   So the field grows instead. Every control a finger can put a caret into goes
   to 16px at this width: the auto-zoom never fires, pinch-zoom is untouched,
   and the type is more legible on the screen it is hardest to read on anyway.
   Checkboxes and radios are excluded -- they take no caret and their size is
   their box.

   `.b-search` IS ON THIS LIST NOW AND WAS NOT, and it is the exact shape of
   hole this comment predicts. It is a whole separate control with its own
   `font-size: var(--type-body)` rather than a modifier of `.b-field`, so it
   inherited none of this, and it is the search box on კლიენტები, on
   შეტყობინებები and in the platform panel's lists -- one of the most-tapped
   fields in the product, and every tap of it on an iPhone zoomed the console
   and left it zoomed. The list is the CONTROLS, not the base class: a typeable
   thing that does not wear `.b-field` still has to be named here.

   `.b-select-trigger` is deliberately NOT here and is not an omission: it is a
   <button> and takes no caret, so iOS never zooms for it -- and it wears
   `.b-field` anyway, which is where its size comes from.
   --------------------------------------------------------------------------- */
@media (max-width: 640px) {
  /* THE FLOOR FIRST, at (0,0,1), for the same reason the base rule above
     section 3 sets one: a control nobody gave a class to still takes a caret,
     and Chrome's 13.333px default is under the threshold. It cannot outrank
     anything -- every class below and in the three other stylesheets beats it
     -- so it is a safety net and never a policy. `button` is deliberately not
     on it: iOS zooms for a field, not for a press target, and a blanket 16px
     on buttons would be a type decision made inside a media query. */
  input,
  textarea,
  select { font-size: 16px; /* type-scale-exempt(ios-zoom): iOS Safari's focus-zoom threshold, not our step -- see tapZoom.test.ts */ }

  .b-field,
  .b-field-lg,
  textarea.b-field,
  select.b-field,
  .b-search { font-size: 16px; /* type-scale-exempt(ios-zoom): iOS Safari's focus-zoom threshold, not our step -- see tapZoom.test.ts */ }
}


.b-field-onpanel { background: var(--panel); }
.b-field-invalid { border-color: var(--neg); }

textarea.b-field { resize: vertical; line-height: 1.5; min-height: 92px; }

/* Native select arrows are drawn by the OS and ignore the palette entirely, so
   the caret is painted with gradients from --fg2 instead. */
select.b-field {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 38px;
  cursor: pointer;
  background-image:
    linear-gradient(135deg, transparent 50%, var(--fg2) 50%),
    linear-gradient(45deg, var(--fg2) 50%, transparent 50%);
  background-position: calc(100% - 18px) calc(50% - 2px), calc(100% - 13px) calc(50% - 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

/* --------------------------------------------------------------------------
   b-select: the product's own dropdown, built around the select above.

   The rules for `select.b-field` stay exactly as they are. They are what a
   browser with no JavaScript, or a page whose script failed to load, still gets
   -- and they are still what the CLOSED control looks like, because the trigger
   below wears the same .b-field class the select did. What this adds is the
   open list, which is the half the OS used to draw in its own colours at its own
   width: on a dark palette, a white list falling out of a dark panel.

   Everything here is a token. There is no hardcoded colour in this block, which
   is the whole point of moving the list into the page.

   src/shared/public/select.js is the behaviour. Nothing below applies until that
   script has actually built a trigger and set data-enhanced, so the native
   control is never hidden before there is something to use instead.
   -------------------------------------------------------------------------- */

.b-select { position: relative; }
/* For the filters that sit in a row of controls and are sized by their content
   rather than by the column they are in. */
.b-select-inline { display: inline-block; width: auto; min-width: 150px; }
.b-select[data-enhanced] > select { display: none; }

.b-select-trigger {
  /* .b-field carries the padding, radius, border, background and type. Only the
     parts a <button> does not already agree with the <select> about are here. */
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
  cursor: pointer;
  /* The caret is an element now, so the two background gradients select.b-field
     paints have nothing to draw and would sit under the text. */
  background-image: none;
  padding-right: 15px;
}
.b-select-inline .b-select-trigger { width: 100%; }
.b-select-trigger:hover:not(:disabled) { border-color: var(--acc); }
.b-select[data-enhanced].is-open .b-select-trigger { border-color: var(--acc); }
.b-select-trigger:disabled { opacity: .55; cursor: not-allowed; }

.b-select-value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Nothing chosen yet. Same weight as .b-field::placeholder, for the same reason:
   a prompt should not read as an answer. */
.b-select-value.is-placeholder { color: var(--fg2); }

/* The same chevron the client picker draws, so the two read as one family. */
.b-select-caret {
  flex: none;
  width: 7px;
  height: 7px;
  margin-bottom: 3px;
  border-bottom: 1.5px solid var(--fg2);
  border-right: 1.5px solid var(--fg2);
  transform: rotate(45deg);
  transition: transform .2s;
}
.b-select[data-enhanced].is-open .b-select-caret { transform: rotate(-135deg); margin-bottom: -3px; }

.b-select-list {
  display: none;
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  /* Exactly the trigger's width, because the trigger is the full width of this
     wrapper too. Deliberately not a px value: at 320px it is 320 minus the
     page's own padding, and no label, however long, can widen it. */
  width: 100%;
  z-index: 60;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 30px 60px -30px var(--inv);
  /* max-height is set by select.js from the room actually available, capped;
     this is the floor it works against before measuring. */
  max-height: 320px;
  overflow-y: auto;
  /* A phone flick that reaches the end of this list must not carry on into the
     page behind it. */
  overscroll-behavior: contain;
  animation: pop .18s both;
}
.b-select[data-enhanced].is-open .b-select-list { display: block; }
/* Opened upwards when there is no room below -- see place() in select.js. */
.b-select[data-enhanced].is-above .b-select-list { top: auto; bottom: calc(100% + 6px); }

/* This list's scrollbar is now the product's scrollbar -- the rule moved to
   `.b-scroll` in section 16b and this class is named in it. */

.b-select-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 11px;
  cursor: pointer;
  color: var(--fg);
  font-size: var(--type-body);
  font-weight: 600;
  transition: background .15s;
}
.b-select-option-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-select-option.is-placeholder { color: var(--fg2); font-weight: 500; }
/* Pointer hover and keyboard cursor are the same state, so they look the same. */
.b-select-option.is-active { background: var(--panel2); }
.b-select-option[aria-selected="true"] { color: var(--acc); }
.b-select-option[aria-selected="true"] .b-select-check { opacity: 1; }
.b-select-option[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
/* The prompt row stays muted and uncheckable even while it is technically the
   current value: "აირჩიეთ სფერო" ticked as an answer is a control claiming the
   question has been answered. Declared after the selected rules so it wins. */
.b-select-option.is-placeholder[aria-selected="true"] { color: var(--fg2); }
.b-select-option.is-placeholder .b-select-check { display: none; }
/* Present but invisible at rest, so the row does not reflow when it is chosen. */
.b-select-check { flex: none; opacity: 0; color: var(--acc); font-size: var(--type-meta); font-weight: 700; }

/* Spinners on number inputs render in the browser's own colours. */
input.b-field[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input.b-field[type="number"]::-webkit-outer-spin-button,
input.b-field[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.b-check { width: 15px; height: 15px; accent-color: var(--acc); flex: none; }
.b-hint { font-size: var(--type-meta); color: var(--fg2); margin-top: 6px; }
.b-error { font-size: var(--type-meta); color: var(--neg); margin-top: 6px; }

.b-search {
  width: 100%;
  padding: 10px 16px;
  border-radius: 100px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--fg);
  font-family: inherit;
  font-size: var(--type-body);
  outline: none;
  transition: border-color .2s;
}
.b-search:focus { border-color: var(--acc); }

/* Selectable option card (plan picker, industry picker). */
.b-option {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 12px;
  border-radius: 16px;
  border: 1px solid var(--line);
  background: var(--panel2);
  color: var(--fg);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  transition: border-color .2s, box-shadow .2s;
}
.b-option:hover { border-color: var(--acc); }
.b-option.is-selected { border-color: var(--acc); box-shadow: inset 0 0 0 1.5px var(--acc); }

/* ==========================================================================
   8. Pills / badges / status
   ========================================================================== */

.b-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-meta);
  font-weight: 700;
  padding: 5px 11px;
  border-radius: 100px;
  background: var(--panel2);
  color: var(--fg);
  white-space: nowrap;
}
.b-pill-ok    { color: var(--pos); }
.b-pill-trial { color: var(--acc); }
.b-pill-busy  { color: var(--acc); }
.b-pill-warn  { color: var(--warn); }
.b-pill-risk  { color: var(--neg); }
.b-pill-acc   { background: var(--acc); color: var(--accFg); }
.b-pill-quiet { color: var(--fg2); }
.b-pill-inv   { background: var(--invSoft); color: var(--invFg); }
.b-pill-outline { background: transparent; border: 1px solid var(--line); color: var(--fg2); }

.b-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--acc); }
.b-dot-ok { background: var(--pos); }
.b-dot-warn { background: var(--warn); }
.b-dot-risk { background: var(--neg); }
/* THE THIRD STATE, AND ITS COLOUR IS THE POINT. „ვერ შემოწმდა" is not a fault --
   it is the read failing, i.e. we do not know. --fg2, the muted body colour, is
   what "no information" looks like; a yellow of its own would put it on the
   severity ladder between --pos and --neg and it would start reporting itself as
   a problem. The full reasoning is in tokens.css, which deliberately defines no
   token for this. Never let this fall back to .b-dot's --acc: an unstyled dot on
   an unknown state is the green dot the house rule forbids. */
.b-dot-unknown { background: var(--fg2); }

/* Three-dot "bot is typing" indicator. */
.b-typing { display: inline-flex; align-items: center; gap: 4px; }
.b-typing > span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  animation: blink 1.2s infinite;
}
.b-typing > span:nth-child(2) { animation-delay: .2s; }
.b-typing > span:nth-child(3) { animation-delay: .4s; }

/* Live indicator: a solid dot with a ring expanding out of it. */
.b-live { position: relative; display: inline-flex; }
.b-live::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--pos);
  animation: pulseRing 1.8s ease-out infinite;
}

/* Payment marks: Visa and Mastercard, each on its own small white card, then
   Apple Pay and Google Pay in their own outlines, then Flitt as the processor.
   The marks are src/shared/public/payment-marks.js, drawn by the public footer
   (server-side) and by the cabinet's card step — the same file both times.

   NO COLOUR IS NAMED HERE, AND THAT IS THE WHOLE ARRANGEMENT. A scheme's
   colours are part of its trademark and may not follow a palette, so they live
   as `fill` attributes inside the two SVGs, together with the white card both
   schemes ask to be shown on. This rule only frames that card — a hairline in
   the palette's own line colour, a radius and a size — so in a dark mode the
   card reads as a card rather than as a hole, and the stylesheet stays
   token-only like the rest of this file. */
.b-paymarks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.b-paymark {
  display: inline-flex;
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--line);
  line-height: 0;
}
.b-paymark > svg { display: block; width: auto; height: 30px; }
.b-paymarks-sm .b-paymark { border-radius: 5px; }
.b-paymarks-sm .b-paymark > svg { height: 24px; }
/* ONE ROW, EVERYWHERE (owner, 2026-10-05): the four marks never wrap; in a
   narrow column they get smaller instead. The list is its own size container,
   and the height is the most the four can have and still fit its width: their
   widths at a height of 30px sum to 199px, plus three 8px gaps, so
   h = (width - 24px) x 30 / 199. Capped at the normal 30px / 24px. A browser
   without container units keeps the old wrapping row, which is still legible. */
@supports (container-type: inline-size) {
  .b-paymarks { container-type: inline-size; width: 100%; flex-wrap: nowrap; }
  .b-paymark > svg { height: min(30px, (100cqi - 24px) * 0.1507); }
  .b-paymarks-sm .b-paymark > svg { height: min(24px, (100cqi - 24px) * 0.1507); }
}
/* Apple Pay and Google Pay bring their own outline (`data-frame="own"` in
   payment-marks.js): a hairline round them would be a second border, which both
   guidelines refuse, and clipping Apple's corners would cut the mark. Same
   height as the cards, no frame. The 8px gap above is also their clear space —
   Apple asks a quarter of the mark's height (7.5px at 30), Google half the G's
   (about 6.5px). */
.b-paymark-own { border-radius: 0; overflow: visible; box-shadow: none; }

/* ==========================================================================
   9. Toggle switch (46x27 track, 21px knob, 19px travel)
   ========================================================================== */

.b-toggle {
  width: 46px;
  height: 27px;
  flex: none;
  border-radius: 100px;
  border: 0;
  cursor: pointer;
  padding: 3px;
  display: flex;
  /* Fallback first: color-mix gives a neutral track in every palette, but an
     older engine must still get something that is visibly not the "on" colour. */
  background: var(--panel2);
  background: color-mix(in srgb, var(--fg) 30%, transparent);
  transition: background .25s;
}
.b-toggle-knob {
  width: 21px;
  height: 21px;
  border-radius: 50%;
  /* invFg is the light ink in BOTH modes, which is exactly what a knob needs. */
  background: var(--invFg);
  transition: transform .25s cubic-bezier(.4, 1.6, .5, 1);
}
.b-toggle.is-on { background: var(--acc); }
.b-toggle.is-on .b-toggle-knob { transform: translateX(19px); }

/* ==========================================================================
   10. Sidebar navigation
   ========================================================================== */

.b-nav { display: flex; flex-direction: column; gap: 3px; }
.b-nav-item {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 11px 12px;
  border: 0;
  border-radius: 13px;
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-family: inherit;
  font-size: var(--type-emphasis);
  font-weight: 600;
  text-align: left;
  opacity: .62;
  transition: background .2s, color .2s, opacity .2s;
}
.b-nav-item:hover { opacity: 1; background: var(--panel2); }
.b-nav-item.is-active { background: var(--acc); color: var(--accFg); opacity: 1; }
.b-nav-item svg { flex: none; }
.b-nav-item-locked { opacity: .35; cursor: not-allowed; }

.b-nav-group {
  font-size: var(--type-meta);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg2);
  padding: 16px 12px 6px;
}

/* ==========================================================================
   11. Avatars
   ========================================================================== */

.b-avatar {
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 12px;
  background: var(--panel2);
  color: var(--fg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 13px; /* type-scale-exempt(glyph-box): initials inside a 34px square */
  overflow: hidden;
}
.b-avatar-acc { background: var(--acc); color: var(--accFg); }
.b-avatar-round { border-radius: 50%; }
.b-avatar-xs { width: 26px; height: 26px; border-radius: 9px; font-size: 11px; /* type-scale-exempt(glyph-box): initials inside a 26px square */ }
.b-avatar-sm { width: 30px; height: 30px; border-radius: 10px; font-size: 12px; /* type-scale-exempt(glyph-box): initials inside a 30px square */ }
.b-avatar-lg { width: 44px; height: 44px; border-radius: 14px; font-size: 15px; /* type-scale-exempt(glyph-box): initials inside a 44px square */ }
.b-avatar-xl { width: 74px; height: 74px; border-radius: 26px; font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 26px; /* type-scale-exempt(glyph-box): initials inside a 74px square */ }

/* Overlapping face pile. The ring must match the surface behind it. */
.b-avatar-stack { display: inline-flex; }
.b-avatar-stack > .b-avatar { border: 2px solid var(--panel); border-radius: 50%; }
.b-avatar-stack > .b-avatar + .b-avatar { margin-left: -8px; }

/* ==========================================================================
   12. Tables and list rows
   ========================================================================== */

/* Wide tables scroll themselves rather than the page. */
.b-table-wrap { overflow-x: auto; }
.b-table { width: 100%; border-collapse: collapse; }
.b-table th {
  text-align: left;
  padding: 0 6px 10px;
  font-size: var(--type-meta);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg2);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.b-table td {
  padding: 14px 6px;
  font-size: var(--type-body);
  color: var(--fg);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.b-table tr:last-child td { border-bottom: 0; }
.b-table tbody tr { transition: background .2s; }
.b-table tbody tr.is-clickable { cursor: pointer; }
.b-table tbody tr.is-clickable:hover { background: var(--panel2); }

/* Grid-based row for the places the design uses aligned columns without a
   <table>. The caller supplies grid-template-columns inline -- the column widths
   are per-screen data, everything else is not. */
.b-grid-head,
.b-grid-row { display: grid; gap: 12px; }
.b-grid-head {
  padding: 0 6px 10px;
  font-size: var(--type-meta);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg2);
  border-bottom: 1px solid var(--line);
}
.b-grid-row {
  align-items: center;
  padding: 14px 6px;
  border-bottom: 1px solid var(--line);
  transition: background .2s;
}
.b-grid-row:hover { background: var(--panel2); }

.b-list-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line);
  transition: background .2s;
}
.b-list-row:last-child { border-bottom: 0; }
.b-list-row.is-clickable { cursor: pointer; width: 100%; text-align: left; font-family: inherit; border-left: 0; border-right: 0; border-top: 0; background: transparent; color: inherit; }
.b-list-row.is-clickable:hover { background: var(--panel2); }
.b-list-row.is-read { opacity: .6; }

/* ==========================================================================
   13. Meters and bar charts
   ========================================================================== */

.b-meter {
  height: 7px;
  border-radius: 100px;
  background: var(--panel2);
  overflow: hidden;
}
.b-meter-fill {
  height: 100%;
  border-radius: 100px;
  background: var(--acc);
  transform-origin: left center;
  animation: barIn .6s .1s both;
}
.b-meter-fill-risk { background: var(--neg); }

/* Vertical column in the usage chart -- grows up from its baseline. */
.b-bar {
  border-radius: 6px 6px 3px 3px;
  background: var(--acc);
  transform-origin: bottom center;
  animation: grow .8s both;
}
.b-bar-muted { background: var(--panel2); }

/* ==========================================================================
   14. Overlays: modal, drawer, dropdown menu
   ========================================================================== */

.b-backdrop {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  /* inv is the dark ink in every palette and mode, so it scrims correctly in
     both without a separate hardcoded black. */
  background: var(--inv);
  background: color-mix(in srgb, var(--inv) 58%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: pop .2s both;
}

.b-modal {
  position: relative;
  width: 100%;
  max-width: 560px;
  max-height: calc(100vh - 56px);
  overflow: auto;
  border-radius: 28px;
  background: var(--bg);
  color: var(--fg);
  box-shadow: 0 50px 100px -40px var(--inv);
  animation: riseIn .3s both;
}
.b-modal-wide { max-width: 940px; }
.b-modal-body { padding: 28px; }

/* NO MODAL MAY OVERLAP ITS OWN CLOSE BUTTON.
   ------------------------------------------------------------------------
   .b-modal-close below is absolutely positioned OVER the body, and the body
   reserved nothing for it. The button's left edge sits `inset + size` = 50px
   in from the modal's right edge; the body's content box ends `padding` =
   28px in from it. The last 22px of every line level with the button is
   therefore painted under an opaque circle -- in every modal on all six
   pages, at every width, since all three numbers are constants.

   Measured at 375px on the platform staff dialog: the modal is 319px wide,
   and „nino.khutsishvili@botai.ge" as a .b-h3 title -- one token, and .b-h3
   is `overflow-wrap: normal`, so it has no break opportunity at all -- ran
   35px past the button's left edge. The person the dialog was about was the
   one thing on it that could not be read. „ანგარიშის სამუდამო წაშლა", the
   account-deletion title, cleared the button by ONE pixel: not a margin, a
   coincidence. A long Georgian title is the general case here and that email
   address was only the instance somebody happened to look at.

   A FLOAT AND NOT PADDING, because the button occupies one CORNER and not a
   column. Right padding on the body would indent every line of every modal
   in the product to clear something 34px tall -- a visible change to six
   pages to fix the top 22px of them. A float is intruded on by the line
   boxes level with it and by nothing below it, so a modal whose first line
   was not already running into the × does not move at all; and a word that
   cannot fit beside it is carried BELOW it rather than under the button,
   which is the unbreakable-token case that started this.

   `+` and not `~`: openModal() in the platform panel draws TWO .b-modal-body
   blocks, a header and the content under it, and only the first is level with
   the button. The 22px is the reservation and the 10px is the gap between the
   text and the circle; without the second the title ends flush against it.
   modalCloseGutter.test.ts recomputes the 22 from the two rules below, so
   moving the button is a change to this rule as well. */
.b-modal-close + .b-modal-body::before {
  content: "";
  float: right;
  width: 22px;   /* 16 inset + 34 button - 28 padding */
  height: 22px;
  margin-left: 10px;
}
/* `wrap` is a safety net, not an arrangement -- it changes nothing at the
   content these feet carry today and only fires where the row would otherwise
   have run out of the box. At a 375px viewport the modal is 319px wide and its
   foot has 263px for buttons; the widest pair in the product,
   „გაუქმება" + „დადასტურება", measures 253px with the gap, so it fits with 10px
   to spare. A third button, or one longer word, and without this line a
   `white-space: nowrap` .b-btn would simply leave the modal. */
.b-modal-foot { display: flex; flex-wrap: wrap; gap: 10px; padding: 0 28px 28px; }
.b-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 2;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--panel);
  color: var(--fg);
  font-family: inherit;
  font-size: 16px; /* type-scale-exempt(glyph-box): the close glyph inside a 34px circle */
  line-height: 1;
  cursor: pointer;
}

.b-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 420px;
  max-width: 100vw;
  z-index: 70;
  background: var(--panel);
  color: var(--fg);
  border-left: 1px solid var(--line);
  box-shadow: -30px 0 70px -40px var(--inv);
  display: flex;
  flex-direction: column;
  animation: pop .25s both;
}
.b-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--line);
}
.b-drawer-body { flex: 1; overflow: auto; padding: 20px; }
/* Same safety net as .b-modal-foot, and this one is closer to the edge: the
   drawer is 420px but `max-width: 100vw`, so at 320px its foot has 280px. */
.b-drawer-foot { padding: 16px 20px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 10px; }

.b-menu {
  position: absolute;
  top: 44px;
  right: 0;
  z-index: 80;
  width: 240px;
  padding: 6px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 18px;
  box-shadow: 0 30px 60px -30px var(--inv);
  overflow: hidden;
  animation: pop .2s both;
}
.b-menu-wide { width: 330px; padding: 0; }
.b-menu-head { padding: 12px 12px 10px; border-bottom: 1px solid var(--line); margin-bottom: 6px; }
.b-menu-item {
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border: 0;
  border-radius: 11px;
  background: transparent;
  color: var(--fg);
  font-family: inherit;
  font-size: var(--type-body);
  font-weight: 600;
  cursor: pointer;
  transition: background .2s;
}
.b-menu-item:hover { background: var(--panel2); }
.b-menu-item-danger { color: var(--neg); }

/* ==========================================================================
   15. Chat bubbles
   ========================================================================== */

.b-thread { display: flex; flex-direction: column; gap: 10px; }
.b-bubble {
  max-width: 76%;
  padding: 11px 15px;
  font-size: var(--type-body);
  line-height: 1.5;
}
.b-bubble-in {
  align-self: flex-start;
  background: var(--panel2);
  color: var(--fg);
  border-radius: 18px 18px 18px 4px;
}
.b-bubble-out {
  align-self: flex-end;
  background: var(--acc);
  color: var(--accFg);
  border-radius: 18px 18px 4px 18px;
}

/* ==========================================================================
   16. Decorative
   ========================================================================== */

/* ==========================================================================
   16b. THE SCROLLBAR, WHEREVER WE OWN THE BOX THAT SCROLLS
   ==========================================================================

   One styled scrollbar existed in this file -- the enhanced select's list --
   and every other scrolling box in both consoles got the operating system's,
   which is a grey bar drawn in nobody's palette and, on Windows, a 17px one
   that lands beside a 10px one two cards away. The treatment below IS that
   block, moved out and given a name so it can be worn rather than copied; the
   widget keeps its own because it is a guest on a shop's page.

   BOTH MECHANISMS, because neither covers every browser on its own:
   `scrollbar-width` / `scrollbar-color` are the standard properties (Firefox,
   and Chrome since 121), the `::-webkit-scrollbar` trio is what older WebKit
   and Blink read. Writing only one leaves half the browsers on the default.

   `height` as well as `width`: a horizontal bar is sized by `height`, and
   `.b-table-wrap` is the class this file has specifically for horizontal
   scroll, so a width-only rule would have styled every bar except the one the
   wide-table wrapper exists to produce.

   THE NAMED CONTAINERS ARE HERE RATHER THAN WEARING `.b-scroll` THEMSELVES
   because they are this file's own boxes -- a caller that writes `.b-drawer`
   should not also have to remember a second class to stop its scrollbar
   looking foreign. `.b-scroll` is for a scroll container a SCREEN makes: the
   platform panel and the cabinet both have several.

   `textarea.b-field` IS ON THE LIST, and it is the one entry that is not a
   container at all. It was the last box in the product wearing the operating
   system's bar, and the widest: it is the shared multi-line field, so it is the
   landing page's request form, register.html, the auth modal and every
   paragraph field in the cabinet's settings at once. `rows="3"` with
   `overflow-y: auto` means it scrolls the moment somebody types a fourth line.
   Measured at 375px before this rule: `scrollbar-width: auto` AND
   `scrollbar-color: auto` -- neither inherited, so the whole bar was foreign,
   thumb included. That is the opposite half of the calculator's case, which
   inherited the token colour and kept the OS width. It is named here rather
   than made to wear `.b-scroll` for the reason the paragraph above gives:
   nobody writing a form should have to remember a second class on a textarea.

   AND IT KEEPS THE SAME 10px, which is a decision rather than an oversight.
   10px was chosen for a ~320px-tall dropdown list; a three-row field is 98px,
   so the fair question is whether a bar sized for the taller box eats a narrow
   one. Measured: the field is 307px wide at a 375px viewport and 254px at
   320px, so the bar is 3.3% and 3.9% of it. A second, narrower width would be a
   second number to keep in step, and it would shrink a pointer target that is
   already only 6px of visible bar inside 10px of hit area.

   Do not add a container we do not own. */
.b-scroll,
.b-select-list,
.b-table-wrap,
.b-sidebar,
.b-navdrawer,
.b-modal,
.b-drawer-body,
.b-menu,
textarea.b-field { scrollbar-width: thin; scrollbar-color: var(--fg2) transparent; }

.b-scroll::-webkit-scrollbar,
.b-select-list::-webkit-scrollbar,
.b-table-wrap::-webkit-scrollbar,
.b-sidebar::-webkit-scrollbar,
.b-navdrawer::-webkit-scrollbar,
.b-modal::-webkit-scrollbar,
.b-drawer-body::-webkit-scrollbar,
.b-menu::-webkit-scrollbar,
textarea.b-field::-webkit-scrollbar { width: 10px; height: 10px; }

.b-scroll::-webkit-scrollbar-track,
.b-select-list::-webkit-scrollbar-track,
.b-table-wrap::-webkit-scrollbar-track,
.b-sidebar::-webkit-scrollbar-track,
.b-navdrawer::-webkit-scrollbar-track,
.b-modal::-webkit-scrollbar-track,
.b-drawer-body::-webkit-scrollbar-track,
.b-menu::-webkit-scrollbar-track,
textarea.b-field::-webkit-scrollbar-track { background: transparent; }

.b-scroll::-webkit-scrollbar-thumb,
.b-select-list::-webkit-scrollbar-thumb,
.b-table-wrap::-webkit-scrollbar-thumb,
.b-sidebar::-webkit-scrollbar-thumb,
.b-navdrawer::-webkit-scrollbar-thumb,
.b-modal::-webkit-scrollbar-thumb,
.b-drawer-body::-webkit-scrollbar-thumb,
.b-menu::-webkit-scrollbar-thumb,
textarea.b-field::-webkit-scrollbar-thumb {
  background-color: var(--fg2);
  border-radius: 100px;
  /* Transparent border with content-box clipping: a 6px bar in a 10px target.
     The target stays a finger-and-pointer-sized 10px while the bar reads as
     the thin one the rest of the product draws. */
  border: 2px solid transparent;
  background-clip: content-box;
}

.b-scroll::-webkit-scrollbar-thumb:hover,
.b-select-list::-webkit-scrollbar-thumb:hover,
.b-table-wrap::-webkit-scrollbar-thumb:hover,
.b-sidebar::-webkit-scrollbar-thumb:hover,
.b-navdrawer::-webkit-scrollbar-thumb:hover,
.b-modal::-webkit-scrollbar-thumb:hover,
.b-drawer-body::-webkit-scrollbar-thumb:hover,
.b-menu::-webkit-scrollbar-thumb:hover,
textarea.b-field::-webkit-scrollbar-thumb:hover { background-color: var(--acc); }

/* Soft blurred accent blobs behind hero sections. */
.b-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  opacity: .28;
  background: var(--acc);
  pointer-events: none;
  animation: floaty 16s ease-in-out infinite;
}
.b-blob-2 { background: var(--acc2); animation: floaty2 19s ease-in-out infinite; }

/* Infinite logo/keyword strip. Duplicate the children so -50% loops seamlessly. */
.b-marquee { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, var(--fg) 12%, var(--fg) 88%, transparent); mask-image: linear-gradient(90deg, transparent, var(--fg) 12%, var(--fg) 88%, transparent); }
.b-marquee-track { display: flex; width: max-content; animation: marquee 32s linear infinite; }

/* Shimmering gradient sweep used on loading text and the hero wordmark. */
.b-sheen {
  background: linear-gradient(100deg, var(--fg2), var(--acc), var(--fg2));
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: sheen 3.5s linear infinite;
}

.b-spinner {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid var(--line);
  border-top-color: var(--acc);
  animation: spinSlow 1s linear infinite;
}

/* SVG line charts draw themselves in. Pair with stroke-dasharray:300 on the path. */
.b-draw { animation: dash 1.4s ease-out both; }

/* ==========================================================================
   17. Design-file state attributes
   Scoped to .b-app so markup can be pasted straight out of redesign-final.html
   without rewriting every data-* into a class. Global, unscoped [data-a] rules
   would reach into cabinet.css's markup, which is why these are not top-level.
   ========================================================================== */

.b-app [data-a="true"],
.b-app [data-n="true"] { background: var(--acc); color: var(--accFg); }
.b-app [data-a="false"] { background: transparent; color: var(--fg2); }
.b-app [data-n="false"] { background: transparent; color: inherit; opacity: .62; }
.b-app [data-n="false"]:hover { opacity: 1; background: var(--panel2); }

.b-app [data-t="true"] { background: var(--acc); }
.b-app [data-t="false"] { background: color-mix(in srgb, var(--fg) 30%, transparent); }
.b-app [data-k="true"] { transform: translateX(19px); }

.b-app [data-s] { background: var(--panel2); }
.b-app [data-s="ok"] { color: var(--pos); }
.b-app [data-s="trial"],
.b-app [data-s="busy"] { color: var(--acc); }
.b-app [data-s="risk"] { color: var(--neg); }

.b-app [data-m="in"] { align-self: flex-start; background: var(--panel2); border-radius: 18px 18px 18px 4px; }
.b-app [data-m="out"] { align-self: flex-end; background: var(--acc); color: var(--accFg); border-radius: 18px 18px 4px 18px; }

.b-app [data-day="today"] { background: var(--acc); color: var(--accFg); }
.b-app [data-day="on"] { background: var(--panel2); }
.b-app [data-day="off"] { opacity: .28; }

.b-app [data-sel="true"] { border-color: var(--acc); box-shadow: inset 0 0 0 1.5px var(--acc); }
.b-app [data-check="true"] { background: var(--acc); border-color: var(--acc); color: var(--accFg); }
.b-app [data-check="false"] { background: transparent; color: transparent; }
.b-app [data-step="done"],
.b-app [data-step="now"] { color: var(--acc); }
.b-app [data-step="next"] { color: var(--fg2); }
.b-app [data-dot="false"] { visibility: hidden; }
.b-app [data-unread="false"] { opacity: .6; }

/* ==========================================================================
   18. Small-screen console shell -- drawer, scrim, bottom tab bar

   One breakpoint, 860px, shared by /admin and /app. Above it none of this
   exists: the classes below are inert at every other width, so a console opts
   in purely by putting them in its markup and nothing about the desktop layout
   moves. Both consoles use the same names on purpose -- a phone-sized left nav
   is the same problem twice, and two copies of it would drift.

   What a console has to supply:
     - .b-navdrawer on its .b-sidebar, opened with .is-open
     - .b-navscrim as a sibling, shown with .is-open
     - .b-hamburger, a real <button> with aria-expanded, in the page header
     - .b-tabbar / .b-tabbar-item for the bottom bar
     - .b-main-tabbed on .b-main, so the bar does not sit on top of content
     - --b-header-h, the measured height of the sticky topbar (see below)

   The drawer hangs below the topbar rather than over it, so it needs the
   topbar's height. That is not a constant: .b-topbar wraps, and on a 375px
   screen it is two rows tall. So the console measures it and writes it to
   --b-header-h; 53px is the one-row fallback if it never does.
   ========================================================================== */

/* Off by default at every width. Only the media query below turns them on, so
   there is no state where a desktop layout has to undo any of this. */
.b-hamburger,
.b-navscrim,
.b-tabbar { display: none; }

.b-hamburger {
  width: 38px;
  height: 38px;
  flex: none;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  color: var(--fg);
  font-family: inherit;
  font-size: 16px; /* type-scale-exempt(glyph-box): the bars glyph inside a 38px square */
  line-height: 1;
  cursor: pointer;
  transition: border-color .2s;
}
.b-hamburger:hover { border-color: var(--acc); }

/* Starts below the topbar so the brand and the way out stay reachable while
   the drawer is open -- the drawer is navigation, not a modal. */
.b-navscrim {
  position: fixed;
  top: var(--b-header-h, 53px);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 74;
  /* inv is the dark ink in every palette and mode, which is what a scrim wants;
     a literal rgba() here could not follow the palette. */
  background: var(--inv);
  background: color-mix(in srgb, var(--inv) 35%, transparent);
  animation: pop .2s both;
}

.b-tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 70;
  align-items: stretch;
  justify-content: space-around;
  gap: 2px;
  padding: 6px 4px;
  padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  background: var(--panel);
  border-top: 1px solid var(--line);
}
.b-tabbar-item {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  border: 0;
  border-radius: 12px;
  background: transparent;
  color: var(--fg2);
  font-family: inherit;
  font-size: var(--type-micro);
  font-weight: 700;
  padding: 7px 2px;
  cursor: pointer;
  transition: color .2s;
}
.b-tabbar-item.is-active { color: var(--acc); }
.b-tabbar-item svg { flex: none; }
/* Four Georgian labels across 375px leaves ~86px of inner box each and the
   longest word either console puts here -- „ასისტენტები", 74.3px at 10.5px/700
   -- fits with room. The ellipsis is the floor under that, not the plan: a
   fifth tab or a longer word must shorten a label rather than push the bar
   wider than the screen. */
.b-tabbar-label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 320px IS WHERE THE FOUR LABELS STOP FITTING, BY 1.8 PIXELS.
   The bar's constraint is the label, not the count -- the four are fixed. At
   320px the default geometry (4px of bar padding a side, 2px between items, 2px
   inside each) leaves 72.5px of inner box, and „ასისტენტები" needs 74.3px, so
   the longest tab in both consoles rendered „ასისტენტებ…" -- truncated by less
   than two pixels, which is a bad trade for nothing.

   Buying it back costs 4.5px of gutter that nobody can see: the tabs are told
   apart by their icon, their label and the accent colour, never by the space
   between them, so the gap can go to nothing at the one width where the space
   is worth more as text. That lands 77px of inner box and every label whole.
   Scoped to 360 rather than applied everywhere because at 375 the labels
   already fit with 12px to spare and there is nothing to buy. */
@media (max-width: 360px) {
  .b-tabbar { gap: 0; padding-left: 2px; padding-right: 2px; }
  .b-tabbar-item { padding-left: 1px; padding-right: 1px; }
}

@media (max-width: 860px) {
  .b-hamburger { display: inline-flex; }
  .b-tabbar { display: flex; }
  .b-navscrim.is-open { display: block; }

  /* The rail leaves the flow and becomes the drawer. Everything reset here is
     something the 900px block above sets for the stacked-block layout. */
  .b-navdrawer {
    position: fixed;
    top: var(--b-header-h, 53px);
    left: 0;
    bottom: 0;
    width: 250px;
    height: auto;
    z-index: 76;
    overflow: auto;
    border-right: 1px solid var(--line);
    border-bottom: 0;
    transform: translateX(-100%);
    /* Closed is genuinely closed. translateX alone leaves the drawer in the
       focus order and in the accessibility tree, so a keyboard user tabs into
       an invisible menu; visibility takes it out of both.

       The two transitions are asymmetric on purpose. Opening flips visibility
       at 0s, because the slide has to be watchable and because focus cannot be
       moved into an element that is still visibility:hidden. Closing delays it
       by the length of the slide, so the drawer is not blanked out halfway
       through leaving. A single `visibility .25s` does neither: discrete
       properties flip at the midpoint, which is the one moment both directions
       look wrong. */
    visibility: hidden;
    transition: transform .25s ease, visibility 0s linear .25s;
  }
  .b-navdrawer.is-open {
    transform: translateX(0);
    visibility: visible;
    transition: transform .25s ease, visibility 0s linear 0s;
  }

  /* Room for the bar. 84px clears its ~62px plus the last card's shadow. */
  .b-main-tabbed { padding-bottom: 84px; }

  /* Secondary affordances that do not survive a one-handed layout. */
  .b-hide-narrow { display: none !important; }

  /* Every multi-column layout grid becomes one column. !important because the
     column tracks are frequently written inline -- they are per-screen data --
     and a stylesheet cannot otherwise reach them.

     `--b-cols-min` IS THE WAY OUT, AND IT IS OPT-IN BECAUSE THE DEFAULT HAS TO
     BE THE SAFE ONE. Unset, `min(100%, 100%)` is 100%, one track fits per line,
     and this is byte-for-byte the layout `minmax(0, 1fr)` produced -- every one
     of the thirteen call sites that already wears this class is unchanged, at
     every width, and none of them had to be opened to make that true.

     It matters because the collapse is currently costing real layout on
     TABLETS, not phones. Measured at an 800px viewport (785px of layout, 757px
     of content box): the template picker's own
     `repeat(auto-fill, minmax(280px,1fr))` wants two 370px columns and this
     line holds it at one 757px column; the team-permission grid's `1fr 1fr` --
     seven checkboxes whose longest Georgian label is 105px wide -- is held at
     one 757px column too, which is the „one thing per row, stretched" the
     owner's requirement names.

     So a caller that knows its content is short says so, in one attribute,
     beside the tracks it already writes:

       <div class="b-cols-1" style="--b-cols-min:126px; …">

     and gets 2 columns at 375 and 5 at 785 instead of 1 everywhere. Which
     callers should: the two team-permission grids and the billing extras strip.
     Which must NOT, and this is the same care in the other direction: the
     notification-preference grid (its labels run to „ასისტენტი გამორთული იყო
     და მომხმარებელი უპასუხოდ დარჩა"), the platform panel's split rail (a data
     grid beside a 280-380px side rail -- two of those on a tablet is a table in
     377px), and anything holding a paragraph, a form field or a table row.
     Those genuinely want the full row, and forcing two columns on them is the
     same mistake mirrored.

     The floor is clamped by `min(100%, …)` for the reason spelled out in
     section 6: a floor wider than the box is horizontal page scroll, and no
     amount of column count is worth that. */
  .b-cols-1 {
    grid-template-columns:
      repeat(auto-fit, minmax(min(100%, var(--b-cols-min, 100%)), 1fr)) !important;
  }

  /* KPI tiles are the exception to the one-column rule above. A tile is a short
     label and a number -- it does not need a full row, and giving it one turned
     the platform overview into five stacked cards you had to scroll past to
     reach anything. They stay two-up instead, and fall to one column on their
     own below ~340px of content width, where two 150px tracks genuinely stop
     fitting. auto-fit rather than a fixed `1fr 1fr` so a tablet at 800px gets
     four across rather than being held at two.

     150 AND NOT THE 200 THE WIDE RULE USES, and the two numbers are in tension
     on purpose. A 375px phone leaves 347px of content box after .b-main's
     padding; with the 10px gap this block sets, two tracks fit only up to
     168.5px each. The longest caption „დღიური შემოსავალი" needs 169.2px
     (137.2px of text plus .b-card's phone padding of 16px each side) to stay on
     one line. The two cannot both be had, so the caption wraps to two lines and
     the ROW stays two-up: a wrapped label costs one line of tile height, a
     collapsed row costs the whole screen. */
  .b-kpis { grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); }

  /* The topbar menus stop hanging off their button and become a sheet.

     .b-menu is absolute, right-aligned to whichever .topbar-pop opened it, and
     a fixed width -- 240px for the profile menu, 330px for the notifications
     panel. That is a right edge and a width, with nothing saying where the left
     edge lands, and on a phone it lands off the screen: the bell is not the
     last control in the topbar, so 330px measured left from it runs past x=0
     and the panel is clipped away. The notifications were unreachable on a
     phone, not merely awkward.

     Fixed rather than absolute because the anchor is the problem: pinned to the
     viewport with a margin on both sides, the panel is on screen at every width
     by construction, and no width can put it anywhere else. It sits under the
     topbar for the same reason .b-navscrim does -- the control that opened it
     stays visible, so the way to close it is where the user left it.

     max-height keeps a long list inside the viewport on a short phone; the
     notification list's own inner max-height is the smaller of the two on a
     tall one, so this only takes over when it has to. */
  .b-menu {
    position: fixed;
    top: calc(var(--b-header-h, 53px) + 8px);
    left: 10px;
    right: 10px;
    width: auto;
    /* dvh second so it wins where it is supported: on a phone, 100vh is the
       viewport with the browser chrome retracted, which is taller than what is
       actually on screen while the address bar is showing. */
    max-height: calc(100vh - var(--b-header-h, 53px) - 24px);
    max-height: calc(100dvh - var(--b-header-h, 53px) - 24px);
    overflow: auto;
  }
  /* .b-menu-wide's 330px is the same specificity as the rule above and is
     declared earlier in the file, so `width: auto` already beats it. Restated
     because that is true by source order rather than by intent, and the next
     person to move a block should not have to work it out. */
  .b-menu-wide { width: auto; }
}
