/* FrigatNET kit v1+6e7303e232ca — built from code-reuse canon reference/ files by kit/build.mjs. Do not edit. */

/* ═══ ui-blocks/frigatnet-app-shell/reference/tokens.css ═══ */
/* FrigatNET — design tokens (canon, from the Fast Tasks design package).
   Theme — [data-theme] on the root <html>. Copy IN FULL; do NOT add module colors here —
   if adding your own module tokens, use a separate file, without breaking the base --ft-* set. */

:root, [data-theme="light"] {
  --ft-bg:       #F5F3EE;
  --ft-surface:  #FEFDFB;
  --ft-field:    #F3F0EA;
  --ft-line:     #E6E0D5;
  --ft-line2:    #EEE9DF;
  --ft-fg:       #221F1A;
  --ft-fg2:      #6A6255;
  --ft-fg3:      #908879;  /* non-text ≥3:1 on surface/field/bg (3.45/3.09/3.16); was #9C9484 = 2.96/2.64/2.71 */

  --ft-navy:     #123E6E;
  --ft-navy-h:   #0E3358;
  --ft-int:      #1B57A0;
  --ft-tint:     #EBEFF5;

  --ft-clay:     #9C5431;  /* tag text 10/600 on clay-t: 4.61 (AA); was #A15733 = 4.37 */
  --ft-clay-t:   #F3E6DC;

  --ft-ok:       #3B7048;  --ft-ok-t:   #E8F0E7;
  --ft-warn:     #8A5E0F;  --ft-warn-t: #F5EBD6;
  --ft-dgr:      #A5382A;  --ft-dgr-t:  #F6E7E2;

  --ft-r:        12px;
  --ft-rs:       9px;
  --ft-rx:       20px;

  --ft-sh:       0 8px 26px rgba(60,50,30,.10);
  --ft-sh-sm:    0 1px 2px rgba(60,50,30,.09);
  --ft-ring:     0 0 0 3px rgba(18,62,110,.18);
}

[data-theme="dark"] {
  --ft-bg:       #1B1916;
  --ft-surface:  #24211D;
  --ft-field:    #201D19;
  --ft-line:     #39342D;
  --ft-line2:    #302B25;
  --ft-fg:       #F0EBE2;
  --ft-fg2:      #B4AB9C;
  --ft-fg3:      #847C6F;

  --ft-navy:     #2C6BB0;
  --ft-navy-h:   #3B7AC0;
  --ft-int:      #7CB0E5;
  --ft-tint:     #20303F;

  --ft-clay:     #CD855B;  --ft-clay-t: #3A2A20;  /* tag text on clay-t: 4.64 (AA); was #CB8156 = 4.45 */
  --ft-ok:       #4BBD6E;  --ft-ok-t:   #20302A;  /* raised from #66AB7B @2026-08-14 (owner's decision): dark-theme semantics were paler than before; contrast 6.72/5.80 = previous brightness, AA-pass */
  --ft-warn:     #D9A441;  --ft-warn-t: #33291A;  /* raised from #C79A3E @2026-08-14 (same decision); contrast 7.13/6.34, AA-pass */
  --ft-dgr:      #DC6E60;  --ft-dgr-t:  #382220;

  --ft-sh:       0 10px 30px rgba(0,0,0,.45);
  --ft-sh-sm:    0 1px 2px rgba(0,0,0,.35);
  --ft-ring:     0 0 0 3px rgba(124,176,229,.30);

  color-scheme: dark;
}

:root {
  --ft-font-ui:    'Hanken Grotesk', system-ui, -apple-system, sans-serif;
  --ft-font-serif: 'PT Serif', Georgia, serif;
  --ft-font-mono:  'JetBrains Mono', ui-monospace, monospace;
}

/* ═══ ui-blocks/frigatnet-app-shell/reference/fonts.css ═══ */
/* FrigatNET canon fonts — SELF-HOSTED. Included instead of a <link> to
   fonts.googleapis.com: a portal without outside access still gets the same faces.
   Assembled from Google Fonts css2 responses; subsets latin, latin-ext, cyrillic,
   cyrillic-ext (greek and vietnamese are skipped — not our locales).

   COVERAGE BY TOKEN — check against unicode-range below, NOT the family name:
     --ft-font-ui    'Hanken Grotesk' — latin and latin-ext from Hanken itself; Cyrillic
                     (cyrillic AND cyrillic-ext, including the hryvnia sign) is carried by Inter,
                     included UNDER THE SAME NAME. Covered.
     --ft-font-mono  'JetBrains Mono' — latin, latin-ext, cyrillic, cyrillic-ext. Covered.
     --ft-font-serif 'PT Serif' — latin, latin-ext, cyrillic (one face for every locale, so a
                     mixed heading stays in one serif). Static faces 400, 700, italic 400:
                     the 600 headings render with the 700 file, the italic 500 brand with italic 400. Covered.
   ⚠ Coverage applies to the MODULE THAT COPIED THIS FILE together with assets/fonts/.
   Didn't copy it — no Cyrillic regardless of the table above.
   ⚠ The note below, "Hanken's cyrillic-ext subset is deliberately NOT included," is about
   Hanken's OWN files, not about the resulting coverage of the name.

   ⚠ CYRILLIC IS RENDERED BY INTER, NOT HANKEN GROTESK.
   Hanken Grotesk has no `cyrillic` subset (U+0400-045F) — the Russian and
   Ukrainian alphabet letters aren't in the family at all, not here and not via CDN.
   Until 2026-09-07 they silently fell back to the system fallback, different on every OS.
   Inter is included UNDER THE NAME 'Hanken Grotesk' with a unicode-range on Cyrillic —
   so nothing needs to change in the tokens or the module code.
   size-adjust: 91.43% — measured IN THE RENDERER, not by eye: Cyrillic x-height
   for Inter is 546.875 vs. Hanken's Latin 500 at 1000em (canvas TextMetrics,
   headless Chrome; you must measure with a letter FROM THE SAME SUBSET — measuring a
   Latin x against the Cyrillic file measures the fallback, not the font).
   Cap-height diverges by about 4% here — an accepted cost: in mixed text, matching
   x-height matters more.
   REVERSIBLE: delete the bottom Inter block and Cyrillic reverts to the system fallback.
   ⚙ HOW TO RE-VERIFY THE FIX (reproducible, we don't keep a separate file for this).
   Drop a temporary html file next to this one and open it through a local server (file:// won't
   work — the fonts won't load), then delete it:

     <script>
     const m = (fam, ch) => { const c = document.createElement('canvas').getContext('2d');
       c.font = `1000px "${fam}"`; return c.measureText(ch).actualBoundingBoxAscent; };
     document.fonts.ready.then(async () => {
       await document.fonts.load('1000px "Hanken Grotesk"', 'xх');
       // ⛔ THE LETTER MUST COME FROM THE SAME SUBSET AS THE FILE: measuring the Latin `x` against the
       // Cyrillic Inter file measures NOT Inter but the system fallback — this already cost a wasted cycle.
       console.log('lat x', m('Hanken Grotesk','x'), '· cyr х', m('Hanken Grotesk','х'));
     });
     </script>

   Expected: both numbers are equal (100% x-height match). If they diverge, the fix above is wrong.
   Hanken's `cyrillic-ext` subset (archaic characters, the hryvnia sign ₴) is deliberately NOT included:
   mixing ₴ from Hanken with letters from Inter would look uneven. */

/* Hanken Grotesk — latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin-ext.768af292.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Hanken Grotesk — latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin.e9201edd.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Hanken Grotesk — latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin-ext.768af292.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Hanken Grotesk — latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin.e9201edd.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Hanken Grotesk — latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin-ext.768af292.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Hanken Grotesk — latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin.e9201edd.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Hanken Grotesk — latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin-ext.768af292.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Hanken Grotesk — latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin.e9201edd.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Hanken Grotesk — latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin-ext.768af292.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Hanken Grotesk — latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(./fonts/hanken-grotesk-400-normal-latin.e9201edd.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* JetBrains Mono — cyrillic-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-cyrillic-ext.62213be8.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* JetBrains Mono — cyrillic */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-cyrillic.e17cfd15.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* JetBrains Mono — latin-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-latin-ext.db5ff4db.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* JetBrains Mono — latin */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-latin.83c005d4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* JetBrains Mono — cyrillic-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-cyrillic-ext.62213be8.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* JetBrains Mono — cyrillic */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-cyrillic.e17cfd15.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* JetBrains Mono — latin-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-latin-ext.db5ff4db.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* JetBrains Mono — latin */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-latin.83c005d4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* JetBrains Mono — cyrillic-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-cyrillic-ext.62213be8.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* JetBrains Mono — cyrillic */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-cyrillic.e17cfd15.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* JetBrains Mono — latin-ext */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-latin-ext.db5ff4db.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* JetBrains Mono — latin */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(./fonts/jetbrains-mono-400-normal-latin.83c005d4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* PT Serif — normal 400 — latin-ext */
@font-face {
  font-family: 'PT Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/pt-serif-400-normal-latin-ext.2d7cdb9a.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* PT Serif — normal 400 — latin */
@font-face {
  font-family: 'PT Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/pt-serif-400-normal-latin.4271064a.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* PT Serif — normal 400 — cyrillic */
@font-face {
  font-family: 'PT Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/pt-serif-400-normal-cyrillic.050aee25.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* PT Serif — normal 700 — latin-ext */
@font-face {
  font-family: 'PT Serif';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(./fonts/pt-serif-700-normal-latin-ext.94cdabe3.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* PT Serif — normal 700 — latin */
@font-face {
  font-family: 'PT Serif';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(./fonts/pt-serif-700-normal-latin.bf23a7a4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* PT Serif — normal 700 — cyrillic */
@font-face {
  font-family: 'PT Serif';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(./fonts/pt-serif-700-normal-cyrillic.bec4e724.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* PT Serif — italic 400 — latin-ext */
@font-face {
  font-family: 'PT Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/pt-serif-400-italic-latin-ext.e03d9090.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* PT Serif — italic 400 — latin */
@font-face {
  font-family: 'PT Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/pt-serif-400-italic-latin.cb373bde.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* PT Serif — italic 400 — cyrillic */
@font-face {
  font-family: 'PT Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(./fonts/pt-serif-400-italic-cyrillic.9d1c80c9.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* ── Cyrillic: Inter under the name Hanken Grotesk ─────────────────────────── */
/* Inter — cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic-ext.ca157063.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter — cyrillic */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic.71d5ee93.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter — cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic-ext.ca157063.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter — cyrillic */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic.71d5ee93.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter — cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic-ext.ca157063.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter — cyrillic */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic.71d5ee93.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter — cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic-ext.ca157063.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter — cyrillic */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic.71d5ee93.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Inter — cyrillic-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic-ext.ca157063.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Inter — cyrillic */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  size-adjust: 91.43%;
  src: url(./fonts/inter-400-normal-cyrillic.71d5ee93.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* ═══ ui-blocks/frigatnet-app-shell/reference/shell.css ═══ */
/* App shell: main block container + header. Canon from FrigatNET Fast Tasks.
   Requires tokens.css (the --ft-* variables). Copy IN FULL; the values are part of the canon. */

* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background: var(--ft-bg);
  color: var(--ft-fg);
  font-family: var(--ft-font-ui);
  font-size: 14px;
  line-height: 1.45;
}

/* ─── Main block MEASURE: 980px ───
   max-width (not width!) — shrinks without a horizontal scroll on narrow portals.
   980 is the measure for READABLE content, NOT a "product-line format": content that's
   compared (report columns) does NOT inherit it. Scope — see code-reuse skill,
   references/app-shell.md, section "5. Критические инварианты (легко пропустить — молча
   ломает вид)", п.1(б). The value is a design-package choice from Fast Tasks, confirmed in
   prod; NOT subject to recalculation by character count.
   Centering below is the default of the READING FORMAT, not an invariant. The bench format
   (see code-reuse skill, references/app-shell.md, section "5a. Форматы раскладки: чтение
   (канон) и верстак (СИД @2026-07-16)") removes it with a `.ft-bench` wrapper in the
   CONSUMING module's own css; do NOT edit THIS FILE for that.
   Bottom padding is enlarged for the floating action bar; without one, revert it to the
   side value (26px 24px). */
.wrap { max-width: 980px; margin: 0 auto; padding: 26px 24px 90px; }
/* The 90px bottom padding is the reserve UNDER the floating action bar (§13). A module without the bar
   returns the side value in its own css — `.wrap { padding-bottom: 26px; }` (BP Tools) — never by
   editing its copy of this file: the copy must match the block byte for byte. */

/* ─── HEADER BAR: chrome separated from the body ───
   The header lives in a full-bleed `.appbar` bar (surface + bottom border) ABOVE the body, NOT inside `.wrap`.
   Page markup order: <header class="appbar"><div class="appbar-inner">…header (+ optional module menu)…
   </div></header> THEN <div class="wrap">…body…</div>. The bar spans edge to edge (visually separating
   the chrome), the content is centered by `.appbar-inner` at the same 980 measure as `.wrap`.
   Bench format (§5a): a bench-format module overrides the `.appbar-inner` width in its own css just like `.wrap`
   (e.g. 1160) — do NOT edit THIS FILE for that. The optional module menu (tabs) is a recommendation, lives in
   the module's own css. */
.appbar { background: var(--ft-surface); border-bottom: 1px solid var(--ft-line); }
.appbar-inner { max-width: 980px; margin: 0 auto; padding: 0 24px; }

/* ─── Header ─── */
.hdr { display: flex; align-items: center; gap: 12px; padding: 12px 0; }
.brand { display: flex; align-items: center; gap: 10px; font-family: var(--ft-font-serif); }
.brand b { font-weight: 600; }
.brand i { font-style: italic; font-weight: 500; color: var(--ft-fg2); }
/* Logo as an image: ONE asset for both themes (bright brand colors read on both light and dark).
   Height is fixed, width auto — do NOT dim/recolor it (brand invariant). */
.brand-logo { height: 24px; width: auto; display: block; }
.brand-div { color: var(--ft-fg2); opacity: .5; }
.hdr .spacer { flex: 1; }

/* Language segmented control (active — aria-pressed="true", NOT a class) */
.seg { display: inline-flex; gap: 2px; background: var(--ft-field); border: 1px solid var(--ft-line); border-radius: var(--ft-rx); padding: 2px; }
.seg button { border: 0; background: transparent; color: var(--ft-fg2); font: 600 12px/1 var(--ft-font-ui); padding: 6px 11px; border-radius: 16px; cursor: pointer; }
.seg button[aria-pressed="true"] { background: var(--ft-navy); color: #fff; }

/* Feedback button: a TEXT button, and it stands FIRST, before the languages — languages and icons stay one group at the right end
   of the row (canon layout). Height follows the chrome row (32), not the 40 of a standalone .btn-secondary.
   Below 700px the caption shrinks to the short one — the narrow Bitrix24 frame fits the row in one line. */
.hdr .fbbtn { height: 32px; padding: 0 12px; }
.hdr .fbbtn-short { display: none; }
@media (max-width: 700px) {
  .hdr .fbbtn-long { display: none; }
  .hdr .fbbtn-short { display: inline; }
}

/* Icon group: every icon button of the chrome lives here, at the right end of the row */
.hdr .icons { display: inline-flex; align-items: center; gap: 8px; }

/* Theme button (icon — inline SVG Lucide on currentColor; we center it and set the SVG size) */
.iconbtn { width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--ft-line); background: var(--ft-field); color: var(--ft-fg2); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
.iconbtn svg { width: 18px; height: 18px; display: block; }

/* Phone menu «⋯»: the row (brand + feedback + languages + icons) is wider than a phone (~470px against 390), so on a
   phone the languages and the feedback button move into this menu, and the theme stays in sight. On a wider screen the
   menu is hidden and the row is the canon one. Rows of the menu are divided by a line. */
.hdr-more { position: relative; display: none; }
.hdr-more > summary { list-style: none; }
.hdr-more > summary::-webkit-details-marker { display: none; }
.hdr-more[open] > summary { border-color: var(--ft-navy); color: var(--ft-navy); }
.hdr-menu { position: absolute; right: 0; top: calc(100% + 6px); z-index: 50; min-width: 250px; background: var(--ft-surface); border: 1px solid var(--ft-line); border-radius: var(--ft-r); box-shadow: var(--ft-sh); padding: 6px; }
.hdr-menu-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 10px; font: 400 14px var(--ft-font-ui); color: var(--ft-fg); }
.hdr-menu-item { display: flex; align-items: center; gap: 12px; width: 100%; padding: 11px 10px; border: 0; border-top: 1px solid var(--ft-line2); background: transparent; font: 400 14px var(--ft-font-ui); color: var(--ft-fg); text-align: left; cursor: pointer; }
.hdr-menu-item svg { width: 18px; height: 18px; flex: none; color: var(--ft-fg2); }
@media (max-width: 560px) {
  .hdr { gap: 8px; }
  .hdr > .fbbtn, .hdr > .seg { display: none; }
  .hdr-more { display: block; }
  .hdr .brand { min-width: 0; }
  .hdr .brand i { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* ═══ ui-blocks/frigatnet-app-shell/reference/components.css ═══ */
/* FrigatNET canon components — priority 1 (nine states and controls), 2 (structures), 3 (base controls),
   4 (coverage: what the lineup solved beyond the canon — §21–29).
   Copied as a file next to tokens.css · fonts.css · shell.css; load order — after shell.css.

   ⚙ WHERE THE VALUES COME FROM. Every rule is taken from a LIVE module, not invented. Numbers were not
   rounded and not fitted to a 4/8 grid. Where modules disagreed, the canon picks by this rule:
     · the value held by the majority of carriers that reached the client;
     · on a tie — from the module that shipped to the client EARLIER.
   Ship order: Timeline 2026-07-24 · Field Widgets 2026-07-27 · Currency 2026-07-28 ·
   Mass BP 2026-07-31 · BP Tools 2026-08-02 · Fast Tasks 2026-09-04.
   Staff Handover and Sales Plan do NOT count — they have no live prod.

   ⚙ STATUSES. `prod-verified` — ≥2 carriers with a live prod · `seed` — one carrier or none
   live · `design canon` — present in the canon, no live carrier.

   ⛔ WHAT'S NOT HERE AND WHY. Tooltip — no component of its own in the modules, the native `title`
   attribute plays the hint's role. Breadcrumbs (§15) — design canon, no carrier. Pagination — was absent;
   since the coverage pass §26 carries Mass BP's pager (seed).
   Full inventory of carriers, values and discrepancies — `components-inventory.md` next to this file. */


/* ═══ 1. SELECT ══════════════════════════════════ prod-verified (6 live carriers)
   Native <select> with the arrow suppressed; the caret is an inline SVG in the markup.
   Markup: <div class="select-wrap"><select class="select">…</select><svg class="select-caret"/></div>
   Discrepancy NOT adopted into the canon: at runtime Field Widgets draws the caret via
   background-image with a literal color #808a96 — it isn't recolored by the theme. */
.select {
  appearance: none; -webkit-appearance: none;
  width: 100%; height: 42px; padding: 0 34px 0 12px;
  background: var(--ft-field); color: var(--ft-fg);
  border: 1px solid var(--ft-line); border-radius: var(--ft-rs);
  font: 400 14px var(--ft-font-ui);
  cursor: pointer;
  text-overflow: ellipsis;
}
.select:focus-visible { outline: 0; border-color: var(--ft-navy); box-shadow: var(--ft-ring); }
.select:disabled { color: var(--ft-fg3); cursor: not-allowed; opacity: .55; }
.select-wrap { position: relative; }
.select-caret {
  position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--ft-fg2); pointer-events: none;
}
/* Compact variant — seed (one carrier: Mass BP Launcher). */
.select--sm { height: 34px; border-radius: 7px; padding: 0 26px 0 9px; font: 500 12.5px var(--ft-font-ui); }
.select-wrap--sm > .select-caret { right: 8px; width: 13px; height: 13px; color: var(--ft-fg3); }


/* ═══ 2. RADIO ═══════════════════════════════════ prod-verified (ring+dot: 2 live carriers)
   Canon form — RING WITH A DOT. Dimensions taken from Field Widgets (shipped earlier);
   Currency carries 18×18 with an 8×8 dot — discrepancy named, not adopted into the canon.
   The native input is visually hidden but alive for keyboard and screen reader: three of five
   module implementations have no accessibility at all, and the canon does not inherit that hole.
   Markup: <label class="rdo"><input type="radio"><span class="rc"><i></i></span><span>…</span></label> */
.rdo {
  display: inline-flex; align-items: center; gap: 9px;
  font: 400 13.5px var(--ft-font-ui); color: var(--ft-fg);
  cursor: pointer; user-select: none;
}
.rdo > input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.rdo .rc {
  width: 16px; height: 16px; border-radius: 50%;
  border: 1px solid var(--ft-line); background: var(--ft-field);
  display: grid; place-items: center; flex: 0 0 auto;
}
.rdo .rc i { width: 8px; height: 8px; border-radius: 50%; background: transparent; }
.rdo:hover .rc { border-color: var(--ft-fg3); }
.rdo > input:checked + .rc { border-color: var(--ft-navy); }
.rdo > input:checked + .rc i { background: var(--ft-navy); }
.rdo > input:focus-visible + .rc { outline: 0; border-color: var(--ft-navy); box-shadow: var(--ft-ring); }
.rdo:has(> input:disabled) { color: var(--ft-fg3); cursor: not-allowed; opacity: .55; }
/* Vertical group. */
.rdo-col { display: flex; flex-direction: column; gap: 11px; }


/* ═══ 3. SWITCH ══════════════════════════════════ prod-verified (2 live carriers)
   Dimensions from Field Widgets (shipped earlier than Currency). Discrepancy: Currency's 44×26 with a 20×20 knob.
   ⚠ BP Tools carries a full `.switch` CSS rule (38×22, 16×16 knob) WITHOUT a single markup carrier —
   dead code, not counted.
   Markup: <button class="switch" role="switch" aria-checked="false"></button>
   ARIA: three incompatible contracts across the modules (aria-checked · aria-pressed · none).
   Canon — `role="switch"` + `aria-checked`: a switch is a state, not a press. */
.switch {
  width: 40px; height: 22px; border-radius: 11px; padding: 0;
  border: 1px solid var(--ft-line); background: var(--ft-field);
  position: relative; flex: 0 0 auto; cursor: pointer;
}
.switch::after {
  content: ""; position: absolute; top: 1px; left: 1px;
  width: 18px; height: 18px; border-radius: 50%;
  background: #fff; box-shadow: var(--ft-sh-sm);
  transition: left .15s ease;
}
.switch[aria-checked="true"] { border-color: var(--ft-navy); background: var(--ft-navy); }
.switch[aria-checked="true"]::after { left: 19px; }
.switch:focus-visible { outline: none; box-shadow: var(--ft-ring); }
.switch:disabled { opacity: .5; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) { .switch::after { transition: none; } }


/* ═══ 4. TEXTAREA ════════════════════════════════ prod-verified (3 live carriers)
   Differs from `.inp` in exactly three properties: min-height, resize, and the fixed height removed.
   NO autosize in any module — the canon does not introduce it.
   Discrepancies: Mass BP's 72px and 13.5px font; BP Tools sets no height at all (rows="6"). */
.textarea {
  width: 100%; box-sizing: border-box;
  min-height: 84px; height: auto; padding: 11px 12px;
  background: var(--ft-field); color: var(--ft-fg);
  border: 1px solid var(--ft-line); border-radius: var(--ft-rs);
  font: 400 14px var(--ft-font-ui); line-height: 1.4;
  resize: vertical;
}
.textarea::placeholder { color: var(--ft-fg3); }
.textarea:focus-visible { outline: 0; border-color: var(--ft-navy); box-shadow: var(--ft-ring); }


/* ═══ 5. DIALOG ══════════════════════════════════ prod-verified (4 live carriers)
       …but ACCESSIBILITY is design canon, no live carrier (see below).
   Backdrop layout — grid (Field Widgets, shipped earlier); .42 scrim — the majority.
   No blur in any module, the canon does not introduce it.
   Markup: <div class="dlg-back"><div class="dlg" role="alertdialog" aria-modal="true">…</div></div> */
.dlg-back {
  position: fixed; inset: 0; z-index: 60; padding: 20px;
  background: rgba(20, 17, 10, .42);
  display: grid; place-items: center;
}
.dlg {
  width: 100%; max-width: 460px; padding: 24px;
  background: var(--ft-surface); color: var(--ft-fg);
  border: 1px solid var(--ft-line); border-radius: var(--ft-r);
  box-shadow: var(--ft-sh);
}
.dlg-title { font: 600 19px/1.2 var(--ft-font-serif); margin: 0; text-wrap: pretty; }
.dlg-body { margin: 10px 0 0; color: var(--ft-fg2); font-size: 13.5px; }
.dlg-actions { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 10px; margin: 20px 0 0; }

/* ⚠ CLOSING — TWO DIFFERENT RULES, AND THIS IS NOT AN OVERSIGHT.
   · Esc — ALWAYS MANDATORY. No live carrier: the only implementation is Staff Handover
     (`src/web/ui/confirm.ts`), and it never reached the client ⇒ status `design canon`. It's also
     the only one in the lineup with a focus trap, `role="alertdialog"`, `aria-modal`, and focus return —
     take it from there wholesale, without them `aria-modal` lies.
   · Backdrop click — ONLY for a dialog WITHOUT input. A dialog with a field FORBIDS it: owner's decision
     @2026-08-01 in Field Widgets — an accidental click erased the typed name. */


/* ═══ 6. TOAST ═══════════════════════════════════ seed (ONE carrier: Field Widgets)
   Floating notification at the bottom center, inverse fill, lives 2600ms, gets replaced (not stacked).
   No icon, no ok/warn/danger variants — a failure is shown by the same toast with a text prefix.
   ⚠ The second "toast" in the lineup — `.mbp-toast-inline` in Mass BP — is NOT a toast: it's an inline
     banner with no timer. The canon does not absorb it; its defect is covered in components-inventory.md.
   ⚠ Field Widgets' z-index 60 puts the toast ON TOP of the modal (whose backdrop is 50). In the canon the
     dialog's backdrop is also 60 ⇒ the order between them is undefined. The fork is unresolved: until it's
     resolved, don't show a toast and a modal at the same time.
   ♿ `role="status"` on the toast element — bot-operator does this (plus manual close and per-kind
     durations); the seed carrier does not. Without it a screen reader never hears the toast. */
.toast {
  position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); z-index: 60;
  background: var(--ft-fg); color: var(--ft-bg);
  border-radius: var(--ft-rs); padding: 11px 18px;
  font: 600 13px var(--ft-font-ui); box-shadow: var(--ft-sh);
  animation: ft-toast-in .18s ease;
}
@keyframes ft-toast-in {
  from { opacity: 0; transform: translateX(-50%) translateY(6px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}


/* ═══ 7. EMPTY STATE ═══════════════════════════ prod-verified (3 live carriers)
   Padding from Field Widgets (shipped earlier). 19px serif heading — the majority.
   No illustrations in any module — the spec's ban confirmed by fact, not taste.
   Action button — OPTIONAL: present in Currency and Field Widgets, absent in Mass BP.
   ⛔ A "Retry" button here is FORBIDDEN: "no data under this filter" isn't an error, retrying gives
      the same result (the rule is recorded in Sales Plan's `views/states.ts`, the canon adopts it).
   Discrepancies: Currency draws a 1px dashed border and a 16px heading; Mass BP uses 48px padding. */
.empty {
  padding: 40px 20px; text-align: center;
}
.empty-title { font: 600 19px/1.2 var(--ft-font-serif); color: var(--ft-fg); margin: 0 0 6px; }
.empty-desc { color: var(--ft-fg2); font-size: 13.5px; margin: 0 auto; max-width: 46ch; }
.empty-actions { margin: 18px 0 0; display: flex; justify-content: center; gap: 10px; }
/* ⚠ "Nothing here" and "nothing MATCHES" are two states, not one: an empty filter result keeps the
   summary and offers to clear the filter; an empty fleet offers to add (bot-operator). One text for
   both sends the user hunting for data that exists. */
/* Empty INSIDE a list or dropdown — not a screen, its own small form. */
.list-empty { padding: 14px; text-align: center; color: var(--ft-fg2); font-size: 12.5px; }


/* ═══ 8. LOADING ═════════════════════════════════ skeleton — seed · indeterminate bar — prod-verified
   ⚠ Three mechanics in the lineup: placeholder blocks (BP Tools ONLY), an indeterminate bar (Currency,
     Field Widgets, Sales Plan), bare text (Mass BP, Staff Handover). The canon takes the first two.
   Skeleton — seed: BP Tools is the only carrier. Its load-bearing rule: CARDS WITHOUT NUMBERS —
   a flashed zero reads as a fact.
   Indeterminate bar — geometry from Field Widgets; Currency carries 60×4 with a 40% runner (discrepancy). */
.skeleton-line {
  width: 140px; height: 12px; border-radius: 6px;
  background: var(--ft-line); margin-bottom: 16px;
  animation: ft-pulse 1.2s ease-in-out infinite;
}
.skeleton-card { min-height: 168px; animation: ft-pulse 1.2s ease-in-out infinite; }
@keyframes ft-pulse { 50% { opacity: .55; } }

.indet { position: relative; height: 10px; border-radius: 5px; background: var(--ft-field); overflow: hidden; }
.indet::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 38%;
  background: var(--ft-navy); border-radius: 5px;
  animation: ft-indet 1.15s ease-in-out infinite;
}
@keyframes ft-indet { 0% { left: -40%; } 100% { left: 100%; } }
/* The response to prefers-reduced-motion DIFFERS across modules (freeze vs. slow down) —
   the canon freezes: a motionless bar still communicates "work in progress" via the runner's position. */
@media (prefers-reduced-motion: reduce) { .indet::after { animation: none; left: 0; } }
/* Skeleton pulse and transitions must obey the same setting — seed (BP Tools): one global rule instead
   of chasing every keyframe. Copied verbatim; scope it to the module root if a third party owns the page. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
/* An indeterminate bar must announce itself: `role="progressbar" aria-busy="true" aria-label="…"` —
   of all the carriers only Sales Plan does this, but a mute indicator doesn't exist for a screen reader. */


/* ═══ 9. SCREEN-LEVEL ERROR ═══════════════════ container — seed (Currency)
       separating "no permission" ≠ "portal didn't respond" — design canon, no live carrier
   ⚠ The tone diverges sharply across the lineup: Staff Handover paints a failure YELLOW (warn),
     Sales Plan and Mass BP — RED (dgr), Currency — neutral. The canon takes Currency's neutral
     container and a red badge only where it's genuinely a failure.
   ⛔ Three states are three different views, and they must not be confused:
     · empty       → `.empty` above, an info icon, NO badge and NO "Retry";
     · failure     → dgr badge + "Retry";
     · no rights   → a lock icon + explanation, NO badge and NO "Retry"; NOT a blank page —
                     emptiness sends people looking for a fault where there is none. */
.state-banner {
  max-width: 520px; margin: 40px auto; padding: 28px 26px;
  background: var(--ft-surface); border: 1px solid var(--ft-line);
  border-radius: var(--ft-r); box-shadow: var(--ft-sh-sm);
  text-align: center;
}
.state-banner-title { font: 600 20px var(--ft-font-serif); margin: 0 0 10px; }
.state-banner-text { color: var(--ft-fg2); font-size: 13.5px; margin: 0 0 6px; }
.state-banner-actions { margin: 18px 0 0; display: flex; justify-content: center; gap: 10px; }
/* State icon (info for empty, lock for "no rights"). */
.ic-state { width: 26px; height: 26px; opacity: .5; color: var(--ft-fg2); }


/* ══════════════════════════════════════════════════════════════════════════════════
   PRIORITY 2 — structures. Same method: the value held by the majority of live carriers,
   on a tie — from the module that shipped to the client earlier.
   ⚠ RULE REFINEMENT derived here: if the earliest carrier's value is INSEPARABLE from a
   named defect, the tiebreak goes to the next one by date. The argument is in
   components-inventory.md, "Tabs"; without this refinement the canon would have inherited a dangling margin.
   ══════════════════════════════════════════════════════════════════════════════════ */


/* ═══ 10. TABLE ══════════════════════════════════ prod-verified (2 live table carriers)
   ⚠ SCOPE — a semantic table. Currency and Fast Tasks build a "table" on CSS grid: that's
     a different implementation, the canon neither covers nor overrides it (see inventory).
   ⛔ NO zebra striping in any carrier — the canon does not introduce it. Rows are separated only by a bottom line.
   ⛔ NO row hover in any carrier. A clickable row is marked by the cursor only. */
.tbl { width: 100%; border-collapse: collapse; }
.tbl th {
  text-align: left; padding: 9px 12px;
  font: 700 10px/1.2 var(--ft-font-ui); letter-spacing: .05em; text-transform: uppercase;
  color: var(--ft-fg2); border-bottom: 1px solid var(--ft-line2);
  text-wrap: pretty;
}
.tbl td { padding: 9px 12px; border-bottom: 1px solid var(--ft-line2); font-size: 12.5px; }
.tbl--compact th, .tbl--compact td { padding: 7px 10px; }
/* Sticky header — yes; a background is mandatory, otherwise rows show through underneath it while scrolling. */
.tbl thead tr { position: sticky; top: 0; z-index: 1; background: var(--ft-surface); }
/* A wide table scrolls WITHIN its own area — the page never scrolls horizontally.
   The table's min-width is set by the consumer based on column count: there's no canon value for it. */
.tbl-scroll { overflow: auto; }
/* A numeric cell is MONOSPACE, not tabular-nums: that's how it's decided across the live carriers.
   (tabular-nums is applied in the lineup to tiles and captions, but not to table cells.) */
.tbl-num { font: 500 11.5px var(--ft-font-mono); color: var(--ft-fg2); white-space: nowrap; }

/* Total row — DESIGN CANON, no live carrier: both implementations (Staff Handover, Sales Plan)
   are in modules without a live prod. Their values agree, so they're taken as is. */
.tbl-total th, .tbl-total td { border-top: 2px solid var(--ft-line); border-bottom: 0; font-weight: 700; }

/* Sorting — SEED: the only carrier (Fast Tasks), and even that's on a grid table.
   The arrow is a text glyph in the column label, cycle direction: asc → desc → RESET to the original
   order. The third step is mandatory: without it the original order is unreachable. */
.tbl-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.tbl-sort:hover { color: var(--ft-fg); }
.tbl-sort.is-on { color: var(--ft-navy); }


/* ═══ 11. FILTER BAR ═════════════════════════════ row — prod-verified (2 live carriers)
   Two live carriers agree almost word for word — a rare case in this collection:
   flex + wrap + gap 12 + a counter pinned right + 14 bottom margin.
   ⛔ NO filter reset in any carrier — the canon does not introduce one.
   ⛔ NO count of active filters; the counter above the panel shows the volume of DATA, not filters. */
.filterbar {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 14px;
}
/* Filter field — sized to its content, NOT full-width: otherwise there's no room left in the row for a button. */
.filter-field { flex: 0 1 220px; min-width: 180px; }

/* ⚠ THE APPLY MODEL DIVERGES, and the canon does NOT mandate one — it's a screen-level decision:
   · instant (the majority of live carriers);
   · draft → button (the only complete implementation — Sales Plan, no live prod ⇒ design canon):
     the button is active only when the draft differs from what's applied AND is valid.
   No live carrier persists the filter between visits.

   ⚠ A COLLAPSIBLE FILTER RAIL — SEED (one live carrier). Not canonized: the two
   implementations diverge on everything — width (220 vs. 360), collapse threshold, animation, persistence. */


/* ═══ 12. MODULE TABS ═════════════════════════════ prod-verified (4 live carriers)
   Form — an UNDERLINE on the active tab (4 carriers vs. 1 with a filled pill).
   Geometry — per the rule refinement (see the section header above): the earliest carrier carries a
   dangling margin-right on the last tab, the canon takes the next reproducible value by date.
   Markup: nav.tabs[role=tablist] > button[role=tab][aria-selected] */
/* ⚠ WRAPPING IS MANDATORY — seed, but without it the canon BREAKS THE PAGE. A stand measurement at 375px:
   a bar of four tabs gives the page a scrollWidth of 495 against a clientWidth of 375, and the culprit is
   the last tab. `white-space: nowrap` below only guarantees the overflow.
   Wrapping is the only solution among LIVE carriers (one, hence the seed status);
   the second known one, horizontal bar scroll, lives outside the eight modules of the lineup. */
.tabs { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.tabs button {
  position: relative; border: 0; background: transparent; color: var(--ft-fg2);
  font: 600 13px/1 var(--ft-font-ui); padding: 14px 14px 12px; cursor: pointer;
  border-bottom: 2px solid transparent;
  display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;
}
.tabs button:hover { color: var(--ft-fg); }
.tabs button[aria-selected="true"] { color: var(--ft-fg); border-bottom-color: var(--ft-navy); }
.tabs button:focus-visible { outline: 0; box-shadow: var(--ft-ring); border-radius: 6px; }
/* Tab counter — seed (one carrier). */
.tabs .tab-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
  background: var(--ft-navy); color: #fff; font: 700 10.5px/1 var(--ft-font-ui);
}
.tabs .tab-count[hidden] { display: none; }  /* our own display rule overrides the browser's [hidden] */

/* ⚠ TAB ACCESSIBILITY — DESIGN CANON, no live carrier.
   Live carriers split 3:2 on aria-pressed vs. aria-selected, and none supplies roles.
   The full set — role=tablist + role=tab + aria-selected + aria-controls + a paired
   role=tabpanel with aria-labelledby and tabindex=0 — exists in exactly one module of the lineup
   (Staff Handover, src/web/app.ts), and it never reached the client. Take it from there wholesale:
   role=tab without aria-controls and without a paired panel promises a switcher that isn't there.
   ⚠ A bar with a SINGLE tab isn't drawn at all — it's noise.

   ⚠ WIDTH OVERFLOW is UNRESOLVED in four of five live carriers — the canon closes it with
   wrapping, see the callout by `.tabs` above. */


/* ═══ 13. FLOATING ACTION BAR ═════════════════════ prod-verified (2 live carriers)
   ⛔ IT IS sticky, NOT fixed — both carriers keep it in the page flow, under the content.
   ⚠ The shell canon (shell.css) keeps .wrap padding-bottom at 90px "for the floating bar."
     There's no carrier backing this promise: both modules with a bar have it sticky-in-flow and it
     needs no reserved space, while the canon's donor module has no bar at all. No bar — the canon itself
     mandates reverting to the side value (26px), and doing so by OVERRIDING it in your own css, not by
     editing the copy. */
.actionbar {
  position: sticky; bottom: 0; margin-top: 20px;
  background: var(--ft-surface); border: 1px solid var(--ft-line);
  border-radius: var(--ft-r); box-shadow: var(--ft-sh);
  padding: 15px 18px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
}
.actionbar-info { flex: 1; min-width: 180px; display: flex; flex-direction: column; gap: 3px; }
.actionbar-hint { font: 400 12px/1.4 var(--ft-font-ui); color: var(--ft-fg2); }


/* ═══ 14. AVATAR ══════════════════════════════════ seed (ONE live carrier)
   ⛔ INITIALS ONLY, photos never happen (owner's decision @2026-09-07). Confirmed by fact:
     there's no photo fallback anywhere in the lineup — no img, no background-image, no user photo field.
   Initials — the first letters of the first two words of the name, uppercase.
   ⚠ The guide's former §5 recipe prescribed tint/int — a light background and blue text. The only
     live carrier does the OPPOSITE. The recipe was corrected to match the fact, not the other way around. */
.avatar {
  width: 23px; height: 23px; border-radius: 50%;
  background: var(--ft-navy); color: #fff;
  font: 700 10px var(--ft-font-ui);
  display: inline-flex; align-items: center; justify-content: center; flex: none;
}


/* ═══ 15. BREADCRUMBS ═════════════════════════════ NOT IN ANY MODULE
   None of the eight modules in the lineup have breadcrumbs. Navigation deeper than tabs exists in exactly one
   (a left section rail), and even that one gives no breadcrumbs.
   We don't create an empty component: people would start referencing it. The screen heading fills the
   breadcrumbs' place — its size diverges across the lineup (24 · 26 · 28), no common value, see the inventory. */


/* ══════════════════════════════════════════════════════════════════════════════════
   PRIORITY 3 — base controls. Until now they were NOT canonized by any block: every module
   ported `styles/controls.css` from its design starter, and the copies drifted silently.
   Same method: the value held by the majority of live carriers; on a tie — the module that
   shipped to the client earlier (see components-inventory.md, "Base controls").
   ══════════════════════════════════════════════════════════════════════════════════ */


/* ═══ 16. BUTTONS ════════════════════════════════ prod-verified (2 live carriers: Currency, Mass BP)
   Primary — the ONE navy action on a screen; secondary — everything else.
   ⚠ Carriers disagree on gap/padding (8/20 vs 9/22): tie resolved by ship date → Currency's 8/20,
     which the design package holds as well. Text colour is `#fff` on purpose: there is no
     `--ft-on-navy` token, and the dark theme keeps navy as navy.
   `white-space: nowrap` — a translated label never wraps inside a fixed-height button. */
.btn-primary {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--ft-navy); color: #fff; border: 0; border-radius: var(--ft-rs);
  height: 44px; padding: 0 20px; font: 700 14px var(--ft-font-ui);
  cursor: pointer; white-space: nowrap;
}
.btn-primary:hover { background: var(--ft-navy-h); }
.btn-primary:focus-visible { outline: 0; box-shadow: var(--ft-ring); }
/* Disabled = the safety catch of a destructive "Run": greyed, never hidden.
   ⚠ A disabled primary EXPLAINS itself: the carriers that shipped a reason next to the button
     (Fast Tasks `.reason`, Mass BP, Field Widgets) replaced "why is it grey?" with the missing
     condition. Render the reason as text beside the button, not as a `title`.
   ♿ Icon-only button: a text `aria-label` on the button, `aria-hidden="true"` on the decorative
     `<svg>` (BP Tools) — otherwise the icon is a mute control. */
.btn-primary:disabled { background: var(--ft-line); color: var(--ft-fg3); cursor: default; }
.btn-secondary {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--ft-surface); color: var(--ft-fg); border: 1px solid var(--ft-line); border-radius: var(--ft-rs);
  height: 40px; padding: 0 16px; font: 600 13.5px var(--ft-font-ui);
  cursor: pointer; white-space: nowrap;
}
.btn-secondary:hover { border-color: var(--ft-fg3); }
.btn-secondary:focus-visible { outline: 0; border-color: var(--ft-navy); box-shadow: var(--ft-ring); }
.btn-secondary:disabled { background: var(--ft-field); color: var(--ft-fg3); border-color: var(--ft-line2); cursor: default; }
/* Compact size — seed (one carrier: Staff Handover `web/app.css`). Modifier on top of .btn-primary / .btn-secondary;
   the About screen's «Подключить» button uses it (about.ts). Stands AFTER both so its height wins at equal weight.
   Discrepancy NOT adopted: Work Report carries `min-height: 30px; padding: 4px 11px; font-size: 12.5px`. */
.btn--sm { height: 34px; padding: 0 14px; font-size: 13px; }


/* ═══ 17. INPUT + LABEL ══════════════════════════ prod-verified (input: 1 live carrier holds the
   full rule, the same values recur in every module's own field; label: identical everywhere)
   A fixed height of 42 — the same as `.select` (§1): field and select sit on one line and align.
   The design starters give `padding: 10px 12px` with no height instead; the live carrier fixes the
   height, and the canon follows the live one. `.textarea` (§4) is derived from this rule. */
.inp {
  width: 100%; background: var(--ft-field); color: var(--ft-fg);
  border: 1px solid var(--ft-line); border-radius: var(--ft-rs);
  padding: 0 12px; height: 42px; font: 400 14px var(--ft-font-ui);
}
.inp::placeholder { color: var(--ft-fg3); }
.inp:focus-visible { outline: 0; border-color: var(--ft-navy); box-shadow: var(--ft-ring); }
.inp:disabled { color: var(--ft-fg3); cursor: not-allowed; opacity: .55; }
/* Variants — seed (one carrier: Mass BP Launcher). Numeric input is MONOSPACE, like `.tbl-num`. */
.inp--num { font: 500 14px var(--ft-font-mono); }
.inp--sm { height: 34px; font-size: 12.5px; }
/* Field label = secondary-text rung (fg2, 400): fg3 is non-text only — it fails AA as text (3.45:1). */
.lbl { display: block; font: 400 12.5px var(--ft-font-ui); color: var(--ft-fg2); margin: 0 0 5px; }


/* ═══ 18. STATUS BADGE + TAG ═════════════════════ badge: prod-verified (2 live carriers, identical:
   Mass BP, Fast Tasks) · tag: prod-verified (1 live carrier + design package, same values)
   Badge = a semantic pair (`ok-t/ok`, `warn-t/warn`, `dgr-t/dgr`, `tint/int`); tag = a neutral
   category in clay. Modifiers are BEM `--`; the design starters use `.badge.ok` — the live
   carriers do not, so the canon does not. */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  border-radius: var(--ft-rx); padding: 4px 10px;
  font: 700 10px/1.4 var(--ft-font-ui); text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.badge--ok    { background: var(--ft-ok-t);   color: var(--ft-ok); }
.badge--warn  { background: var(--ft-warn-t); color: var(--ft-warn); }
.badge--dgr   { background: var(--ft-dgr-t);  color: var(--ft-dgr); }
.badge--run   { background: var(--ft-tint);   color: var(--ft-int); }
.badge--muted { background: var(--ft-field);  color: var(--ft-fg2); }
/* In a table cell the badge is one step smaller — seed (Mass BP). */
.badge--sm { padding: 3px 9px; font-size: 9.5px; }
.tag {
  display: inline-flex; align-items: center;
  background: var(--ft-clay-t); color: var(--ft-clay); border-radius: var(--ft-rx);
  padding: 4px 11px; font: 600 10px/1.4 var(--ft-font-ui); letter-spacing: .02em; white-space: nowrap;
}


/* ═══ 19. SCROLLBAR ══════════════════════════════ seed (one live carrier: Mass BP — the WebKit
   pseudo-elements; the standard properties come from Staff Handover, which has no live prod)
   Both halves together: Firefox reads the standard ones, Chromium/Safari read the pseudo-elements.
   Applied to a scrolling AREA (`.tbl-scroll`, a list, a rail), never to `html`. */
.ft-scroll { scrollbar-width: thin; scrollbar-color: var(--ft-line) transparent; }
.ft-scroll::-webkit-scrollbar { width: 10px; height: 10px; }
.ft-scroll::-webkit-scrollbar-thumb { background: var(--ft-line); border-radius: 8px; border: 2px solid var(--ft-surface); }


/* ═══ 20. ICON (mask) ════════════════════════════ seed (one live carrier: Mass BP; the same
   mask-on-currentColor idea drives `.select-caret` in §1, which is prod-verified)
   The icon is a MASK filled with `currentColor`: it recolours with the theme and with the
   semantic colour of its parent (a `dgr` badge gets a red icon for free). An inline `<svg>`
   with `fill="currentColor"` is the equivalent — Currency does that; the two coexist.
   ⚠ The named icons (`.ic-search`, `.ic-trash`, …) are NOT canon: each names an asset path of
     its module. Declare them next to your assets:
     `.ic-search { -webkit-mask-image: url(/assets/icons/search.svg); mask-image: url(/assets/icons/search.svg); }` */
.ic {
  display: inline-block; width: 15px; height: 15px; flex: 0 0 auto;
  background: currentColor;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}


/* ══════════════════════════════════════════════════════════════════════════════════════════════
   PRIORITY 4 — COVERAGE (added 2026-09-18 from the eight-module sweep: what the lineup had already
   solved beyond the canon). Same sourcing rule as above: every rule is copied from a LIVE module,
   module prefixes (`mbp-`) stripped, nothing re-fitted. Carriers and discrepancies —
   components-inventory.md. Behaviours that are code, not CSS (validation next to the field,
   debounce + stale-response drop, "why is this button disabled") — the block guide, §9.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */


/* ═══ 21. PROGRESS (determinate) ═══════════════════ prod-verified (3 live carriers: Currency, Mass BP,
   Fast Tasks). Height 10 and radius `--ft-rx` — the majority; the fill transition diverges (.12s linear ·
   .5s ease · none) — tie resolved by ship date → Currency. `--sm` — seed (Mass BP).
   The indeterminate sibling is §8 `.indet`. Announce it: `role="progressbar" aria-valuenow="…"
   aria-valuemin="0" aria-valuemax="100"`. */
.progress { height: 10px; border-radius: var(--ft-rx); background: var(--ft-field); overflow: hidden; }
.progress-bar { height: 100%; border-radius: var(--ft-rx); background: var(--ft-navy); transition: width .12s linear; }
.progress--sm { height: 6px; }


/* ═══ 22. CHIPS (selected entities, removable) ═════ prod-verified (3 live carriers: Field Widgets,
   Mass BP, Fast Tasks). Values — Mass BP (the earliest carrier whose css was compared); Fast Tasks
   diverges (radius 20px, `--ft-int` text); Field Widgets renders its chips from TS and was not compared.
   `.multibox` — the field that holds chips plus an "add" affordance (seed: Fast Tasks). */
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip { display: inline-flex; align-items: center; gap: 6px; background: var(--ft-tint); color: var(--ft-navy); border-radius: var(--ft-rx); padding: 4px 6px 4px 10px; font: 600 12px var(--ft-font-ui); }
.chip-del { border: 0; background: transparent; color: var(--ft-navy); cursor: pointer; font: 700 14px/1 var(--ft-font-ui); padding: 0 4px; }
.chip-del:hover { color: var(--ft-dgr); }
.multibox { min-height: 42px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; background: var(--ft-field); border: 1px solid var(--ft-line); border-radius: var(--ft-rs); padding: 7px 9px; }
.addbtn { border: 0; background: transparent; color: var(--ft-int); font: 600 12.5px var(--ft-font-ui); padding: 4px 7px; border-radius: 6px; cursor: pointer; }
.addbtn:hover { background: var(--ft-field); }


/* ═══ 23. RADIO PILL (segmented choice) ═══════════ prod-verified (2 live carriers: Mass BP, Timeline
   comments; Sales Plan too, no live). Values — Mass BP (copied); Timeline's segment was not compared.
   Mutually exclusive options side by side; the checked pill takes the tint. ARIA as in the carrier:
   `role="radio" aria-checked` on each pill inside `role="radiogroup"`. */
.radio-pill { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 14px; border-radius: var(--ft-rs); font: 600 13px var(--ft-font-ui); cursor: pointer; border: 1px solid var(--ft-line); background: var(--ft-field); color: var(--ft-fg2); }
.radio-pill[aria-checked="true"] { background: var(--ft-tint); border-color: var(--ft-navy); color: var(--ft-navy); }
.radio-pill .dot { width: 15px; height: 15px; border-radius: 50%; border: 2px solid currentColor; display: inline-flex; align-items: center; justify-content: center; }
.radio-pill .dot > i { width: 7px; height: 7px; border-radius: 50%; background: transparent; display: block; }
.radio-pill[aria-checked="true"] .dot > i { background: var(--ft-navy); }


/* ═══ 24. INLINE STATUS BANNER ═════════════════════ prod-verified (2 live carriers: BP Tools, Fast Tasks)
   A message that lives NEXT TO its context (a strip under the form, above the action), unlike the
   screen-level `.state-banner` (§9). Values — BP Tools (shipped earlier); Fast Tasks diverges: its
   `.banner` uses the `--ft-*-t` tint tokens and adds `ok`/`err` variants and 11px/13px padding.
   The two colour recipes are not merged: BP Tools mixes in srgb, Fast Tasks uses tokens.
   `.spin` — an inline spinner for "checking…" states — seed (Fast Tasks). */
.status { padding: 9px 12px; border-radius: var(--ft-rs); font-size: 12.5px; margin-bottom: 14px; }
.status.warn { background: color-mix(in srgb, var(--ft-warn) 12%, transparent); border: 1px solid color-mix(in srgb, var(--ft-warn) 40%, transparent); }
.status.info { background: color-mix(in srgb, var(--ft-navy) 8%, transparent); border: 1px solid color-mix(in srgb, var(--ft-navy) 28%, transparent); color: var(--ft-fg); }
.status.err { background: var(--ft-dgr-t); border: 1px solid var(--ft-dgr); color: var(--ft-dgr); }
.status.ok { background: var(--ft-ok-t); border: 1px solid var(--ft-ok); color: var(--ft-ok); }
.spin { display: inline-block; width: 13px; height: 13px; border: 2px solid var(--ft-line); border-top-color: var(--ft-navy); border-radius: 50%; animation: ft-spin .7s linear infinite; vertical-align: -2px; margin-right: 7px; }
@keyframes ft-spin { to { transform: rotate(360deg); } }


/* ═══ 25. POPOVER SELECT WITH SEARCH ═══════════════ prod-verified (3 live carriers: Fast Tasks, Mass BP,
   Field Widgets). A button-that-looks-like-a-select (§1 look) opening a popover with a sticky search
   field, a check mark on the chosen option and an explicit "nothing found" row. Values — Fast Tasks
   (the most complete markup); Mass BP's picker (`fielddrop`) diverges in paddings and was not merged.
   ⛔ IFRAME INVARIANT (Mass BP, acceptance 2026-07-29): an `absolute` popover is CLIPPED by any scrolling
     container above it. Inside a scrolling area switch it to `.pop.is-float` while open and pin it to its
     button by CSS anchor positioning — `floatPin.ts` of the `entity-search-picker` block. A list moved by a
     script on scroll (top/left or transform alike) lags its button by a frame; transform is only the
     fallback for a browser without anchors. */
.selwrap { position: relative; }
.selbtn { width: 100%; height: 42px; background: var(--ft-field); border: 1px solid var(--ft-line); border-radius: var(--ft-rs); padding: 0 12px; display: flex; align-items: center; gap: 9px; font: 400 13.5px var(--ft-font-ui); color: var(--ft-fg); text-align: left; cursor: pointer; }
.selbtn:focus { outline: none; border-color: var(--ft-navy); box-shadow: var(--ft-ring); }
.selname { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ph { flex: 1; color: var(--ft-fg3); }
.chev { color: var(--ft-fg3); font-size: 11px; flex: none; }
.pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 40; background: var(--ft-surface); border: 1px solid var(--ft-line); border-radius: var(--ft-rs); box-shadow: var(--ft-sh); padding: 5px; max-height: 260px; overflow: auto; min-width: 220px; }
.pop.is-float { position: fixed; top: 0; left: 0; right: auto; will-change: transform; }
.opt { width: 100%; display: flex; align-items: center; gap: 9px; padding: 9px 10px; border: 0; background: transparent; border-radius: 8px; font: 400 13.5px var(--ft-font-ui); color: var(--ft-fg); text-align: left; cursor: pointer; }
.opt:hover { background: var(--ft-field); }
.opt[hidden] { display: none; }  /* own display above overrides the browser's [hidden]: a search inside the popover filters by opt.hidden */
.opt-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.opt-check { color: var(--ft-ok); flex: none; }
.popsearch { position: sticky; top: -5px; z-index: 1; background: var(--ft-surface); padding: 2px 0 7px; }
.popsearch-ic { position: absolute; left: 12px; top: 21px; transform: translateY(-50%); color: var(--ft-fg3); pointer-events: none; }
.popsearch-in { width: 100%; height: 38px; padding: 0 12px 0 34px; font: 400 13.5px var(--ft-font-ui); color: var(--ft-fg); background: var(--ft-field); border: 1px solid var(--ft-line); border-radius: 10px; }
.popsearch-in:focus { outline: none; border-color: var(--ft-navy); box-shadow: var(--ft-ring); background: var(--ft-surface); }
.popsearch-in::placeholder { color: var(--ft-fg3); }
.noopt { padding: 14px 10px; text-align: center; color: var(--ft-fg2); font: 400 13px var(--ft-font-ui); }


/* ═══ 26. PAGER ═════════════════════════════════════ seed (one live carrier with css: Mass BP; bot-operator
   pages its journal too, rendered from TS — not compared). Closes the "no UI pagination" gap named in the
   header. Prev/next only, a text summary between them; the out-of-range page is an EMPTY STATE that
   leads to the last existing page (bot-operator), not a blank list. */
.pager { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 18px; border-top: 1px solid var(--ft-line2); }
.pager-btns { display: flex; gap: 6px; }
.pagerbtn { background: var(--ft-field); color: var(--ft-fg); border: 1px solid var(--ft-line); border-radius: var(--ft-rs); height: 32px; padding: 0 13px; font: 600 12px var(--ft-font-ui); cursor: pointer; }
.pagerbtn:disabled { background: var(--ft-bg); color: var(--ft-fg3); cursor: default; }


/* ═══ 27. COLLAPSIBLE FILTER RAIL ═══════════════════ seed (one live carrier: Mass BP; Sales Plan's rail has
   no live). §11 declined to canonize this while the two implementations disagreed on everything; the
   coverage pass takes the live one as a seed. Rail and main column scroll INDEPENDENTLY, the page does not;
   collapsed, the rail shrinks to a 40px column holding only the toggle.
   ⚠ Children of a scrolling flex column DO NOT SHRINK (`flex: 0 0 auto`) — otherwise cards flatten to
     stripes while the data is all there (Mass BP, 2026-07-30). Sales Plan adds two things worth taking
     when it ships: the collapsed rail is `inert`, and the collapse threshold is a media query, not JS. */
.rail { flex: 0 0 360px; min-width: 320px; max-width: 440px; display: flex; flex-direction: column; gap: 16px; height: 100%; overflow-y: auto; overscroll-behavior: contain; min-height: 0; padding-right: 4px; }
.rail--col { flex: 0 0 40px; width: 40px; position: sticky; top: 16px; }
.main { flex: 1; min-width: 0; max-width: 660px; display: flex; flex-direction: column; gap: 18px; height: 100%; overflow-y: auto; overscroll-behavior: contain; min-height: 0; padding-right: 4px; padding-bottom: 24px; }
.main > *, .rail > * { flex: 0 0 auto; }
.railhead { display: flex; align-items: center; justify-content: space-between; }
.railhead > span { font: 700 10.5px/1.2 var(--ft-font-ui); letter-spacing: .08em; text-transform: uppercase; color: var(--ft-fg2); }
.icobtn { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--ft-line); background: var(--ft-field); color: var(--ft-fg2); cursor: pointer; font-size: 15px; line-height: 1; }
.railtoggle { width: 40px; height: 40px; border: 1px solid var(--ft-line); background: var(--ft-surface); color: var(--ft-fg2); border-radius: var(--ft-rs); cursor: pointer; box-shadow: var(--ft-sh-sm); display: flex; align-items: center; justify-content: center; font-size: 16px; line-height: 1; }


/* ═══ 28. CHECKBOX ═══════════════════════════════════ seed (one live carrier: Currency). A drawn box next
   to a label; `.on` fills it navy, `.locked` removes the pointer (a mandatory option cannot be unchecked —
   the `.pill-req` tag says why). Keep the native `<input type=checkbox>` in the DOM for a11y and forms;
   the box is visual only. */
.check-opt { display: inline-flex; align-items: center; gap: 9px; font: 400 13.5px var(--ft-font-ui); color: var(--ft-fg); cursor: pointer; }
.check-opt.locked { cursor: default; }
.check-box { width: 18px; height: 18px; border-radius: 5px; border: 1px solid var(--ft-line); background: var(--ft-field); display: grid; place-items: center; flex: 0 0 auto; color: #fff; }
.check-box.on { border-color: var(--ft-navy); background: var(--ft-navy); }
.pill-req { font: 600 9px var(--ft-font-ui); text-transform: uppercase; letter-spacing: .05em; color: var(--ft-fg2); border: 1px solid var(--ft-line); border-radius: var(--ft-rx); padding: 2px 7px; }


/* ═══ 29. STEPPER ════════════════════════════════════ seed (one live carrier: Currency). Stages of a long
   operation as uppercase pills: pending · active · done · error, arrows between them. Wraps on narrow hosts. */
.stepper { display: flex; align-items: center; gap: 6px; margin: 0 0 16px; flex-wrap: wrap; }
.step { font: 700 10px var(--ft-font-ui); letter-spacing: .04em; text-transform: uppercase; border-radius: var(--ft-rx); padding: 5px 11px; }
.step-pending { color: var(--ft-fg2); background: transparent; }
.step-active { color: var(--ft-navy); background: var(--ft-tint); }
.step-done { color: var(--ft-ok); background: var(--ft-ok-t); }
.step-error { color: var(--ft-dgr); background: var(--ft-dgr-t); }
.step-arrow { color: var(--ft-fg3); }

/* ═══ ui-blocks/frigatnet-about-screen/reference/about.css ═══ */
/* "About the module" screen — "showcase" look. Canon example: FrigatNET Mass BP Launcher (patch-007).
   Colors ONLY via --ft-* tokens (from tokens.css of the frigatnet-app-shell block) — the dark theme
   recolors everything at once. Hero on the CANVAS with no card frame: a frame around the whole screen
   made the introduction look like a service notice. Cards carry only the tiles and the steps block. */
.ft-about-hero { max-width: 700px; padding: 4px 0 0; }
.ft-about-eyebrow { font: 700 10.5px/1.2 var(--ft-font-ui); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ft-fg2); margin: 0 0 10px; }
/* Title — serif over grotesque body: a recognizable pairing from the product line's visual language (style-guide §3). */
.ft-about-title { font: 600 30px/1.12 var(--ft-font-serif); letter-spacing: -.01em; margin: 0; color: var(--ft-fg); text-wrap: pretty; }
.ft-about-lead { margin: 12px 0 0; color: var(--ft-fg2); font-size: 15.5px; max-width: 62ch; }
/* Icons are linear, on currentColor — they take color from context (accent in tiles, warn in the warning line). */
.ft-about-ico { flex: 0 0 auto; width: 20px; height: 20px; color: var(--ft-navy); }
.ft-about-ico svg { width: 20px; height: 20px; display: block; }

/* Feature tiles: 4 in a row, 2×2 on a narrow host (the portal iframe width isn't constant). */
.ft-about-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin: 24px 0 0; }
.ft-about-tile { background: var(--ft-surface); border: 1px solid var(--ft-line); border-radius: var(--ft-r);
  box-shadow: var(--ft-sh-sm); padding: 16px 16px 18px; }
.ft-about-tile .ft-about-ico { margin: 0 0 11px; }
.ft-about-tile-t { font: 600 13.5px/1.3 var(--ft-font-ui); margin: 0 0 5px; color: var(--ft-fg); }
.ft-about-tile-d { font-size: 12.5px; color: var(--ft-fg2); margin: 0; }

/* «Как это работает» — three steps separated by a thin line (borders instead of shadows). */
.ft-about-how { margin: 22px 0 0; background: var(--ft-surface); border: 1px solid var(--ft-line);
  border-radius: var(--ft-r); box-shadow: var(--ft-sh-sm); padding: 18px 20px; }
.ft-about-how-h { font: 700 10.5px/1.2 var(--ft-font-ui); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ft-fg2); margin: 0 0 14px; }
.ft-about-steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.ft-about-step { padding: 0 20px 0 0; }
.ft-about-step + .ft-about-step { padding-left: 20px; border-left: 1px solid var(--ft-line2); }
.ft-about-step-n { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
  border-radius: 50%; background: var(--ft-tint); color: var(--ft-navy); font: 700 11px/1 var(--ft-font-ui); margin: 0 0 8px; }
.ft-about-step-t { font: 600 13px/1.3 var(--ft-font-ui); margin: 0 0 3px; color: var(--ft-fg); }
.ft-about-step-d { font-size: 12.5px; color: var(--ft-fg2); margin: 0; }

.ft-about-foot { display: flex; align-items: center; gap: 9px; margin: 16px 0 0; }
.ft-about-foot .ft-about-ico { width: 15px; height: 15px; color: var(--ft-fg3); }
.ft-about-foot .ft-about-ico svg { width: 15px; height: 15px; }
.ft-about-hint { margin: 0; color: var(--ft-fg2); font-size: 12.5px; }

/* Warning line: no access, or the module isn't connected. The warning color is a state, not an error. */
.ft-about-note { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 18px 0 0; padding: 11px 14px;
  max-width: 700px; border-radius: var(--ft-rs); background: var(--ft-warn-t); color: var(--ft-warn); font-size: 13px; }
.ft-about-note .ft-about-ico { color: var(--ft-warn); }
.ft-about-note-txt { flex: 1; min-width: 220px; }

/* Narrow portal host: tiles in two rows, steps stacked (the separator line loses its meaning).
   Also tightening the vertical: rearranged into two rows, the screen grows taller, and there should be
   no scrolling on the introduction. It fully goes away only on a wide host — on a very narrow one it
   remains; this is a known LIMIT. */
@media (max-width: 1000px) {
  .ft-about-title { font-size: 26px; }
  .ft-about-lead { font-size: 15px; margin-top: 10px; }
  .ft-about-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
  .ft-about-tile { padding: 14px 14px 15px; }
  .ft-about-how { margin-top: 16px; padding: 16px 18px; }
  .ft-about-steps { grid-template-columns: 1fr; gap: 12px; }
  .ft-about-step { padding-right: 0; }
  .ft-about-step + .ft-about-step { padding-left: 0; border-left: 0; }
}

/* ═══ ui-blocks/entity-search-picker/reference/picker.css ═══ */
/* Entity picker for an unknown-size set — the look of a dropdown list with search.
   Canon code of block `ui-blocks/entity-search-picker`; source — FrigatNET Mass BP Launcher,
   `src/web/styles/app.css` (components "search list", "field picker", "record binding").
   ⚠ There are six differences from the source — see code-reuse skill, references/entity-search-picker.md,
   section "6. Отличия канона блока от первоисточника (шесть, все осознанные)", not just the `mbp-` → `ft-`
   prefix. What matters for this file: `.ft-checks`/`.ft-chk` is an ASSEMBLY of rules (never ran in this
   form in prod), and `.ft-pick` does NOT carry the host's `flex: 1` (`.mbp-cond-field`) — stretching in a
   flex row is now the module's job.

   ⚠ DEPENDS on two foreign files and looks broken without them:
     · `tokens.css` of block `frigatnet-app-shell` — every color comes from `--ft-*`;
     · the base controls `.select` and `.ft-scroll` — canon in `components.css` of block
       `frigatnet-app-shell` (§1, §19; see code-reuse skill, references/entity-search-picker.md, section
       "5. Зависимости — без них блок выглядит сломанным"). The button's caret is this file's own rule below.
   Copy IN FULL, don't tweak the values. */

/* ── Button-as-select: selected label or placeholder ── */
.ft-pick { position: relative; min-width: 0; }
.ft-pickbtn { text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ft-pickbtn.is-ph { color: var(--ft-fg3); }
/* The button's caret — `<span class="caret">` from dropShell.ts. Placed like `.select-caret` (components.css §1):
   a chevron on `--ft-fg2`, recolored by the theme. The shape is an inline data: SVG mask — no asset to ship.
   `:where()` keeps it at ONE class of weight: a module that draws `.select-wrap > .caret` itself (Mass BP:
   an SVG-asset mask with the same property set) still wins whatever the stylesheet order. */
:where(.ft-pick) > .caret { position: absolute; right: 11px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; pointer-events: none; background: var(--ft-fg2); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center/contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") center/contain no-repeat; }

/* ── Popup block ── */
.ft-drop { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 30; background: var(--ft-surface); border: 1px solid var(--ft-line); border-radius: var(--ft-rs); box-shadow: var(--ft-sh); padding: 8px; }
/* ⭐ List OVER the page. The host block scrolls, and an absolutely-positioned dropdown gets CLIPPED by
   that container — the list runs off the edge of the screen (a production acceptance defect). We keep the
   host's scroll ⇒ while open, the dropdown switches to fixed, and the controller sets coordinates from its
   own button.
   ⚠ Positioning — `floatPin.ts`: CSS anchor positioning pins the list to its field, so the browser moves both
   in the same frame. A list moved by a script in a scroll listener — by `top`/`left` OR by `transform` — is one
   frame late on the screen and twitches under its field while the wheel turns: the compositor paints the scroll
   before the listener runs. `transform` from the origin stays only as the fallback for a browser without
   anchors (it spares the layout, not the lag); `will-change` reserves its layer in advance. */
.ft-drop.is-float { position: fixed; top: 0; left: 0; right: auto; will-change: transform; }
/* Wide list: rows are long (record name + number), the button width is too small for them. */
.ft-drop.is-wide { max-width: 92vw; }

/* ── Search input inside the dropdown ── */
.ft-search { width: 100%; background: var(--ft-field); color: var(--ft-fg); border: 1px solid var(--ft-line); border-radius: var(--ft-rs); padding: 0 12px; height: 36px; font: 400 13px var(--ft-font-ui); margin: 0 0 8px; }
.ft-search:focus-visible { outline: 0; border-color: var(--ft-navy); box-shadow: var(--ft-ring); }
.ft-dropsearch { margin: 0 0 8px; }

/* ── List body ──
   Block layout (NOT flex-column): flex-shrink squashes the options into stripes in height when there are many rows. */
.ft-opts { max-height: 220px; overflow-y: auto; }
.ft-opt { display: block; width: 100%; box-sizing: border-box; text-align: left; background: transparent; border: 0; padding: 8px 9px; border-radius: 6px; cursor: pointer; font: 400 13px var(--ft-font-ui); color: var(--ft-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ft-opt:hover { background: var(--ft-tint); }
.ft-opt.is-on { background: var(--ft-tint); color: var(--ft-navy); font-weight: 600; }
/* Secondary annotation in a result row — the record number (`#8097`). */
.ft-row-cat { flex: 0 0 auto; font: 600 10px/1.3 var(--ft-font-ui); text-transform: uppercase; letter-spacing: .03em; color: var(--ft-fg2); white-space: nowrap; }
/* Empty result — TEXT, not an empty block: an empty block reads as a broken list. */
.ft-list-empty { padding: 14px; text-align: center; color: var(--ft-fg2); font-size: 12.5px; }

/* ── Multi-select: checkboxes inside the dropdown ──
   Inside the dropdown checkboxes are part of the menu, not a form field, so their own border and background ("panel inside a panel") are dropped. */
.ft-checks { max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; }
.ft-chk { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px 9px; border-radius: 6px; cursor: pointer; font: 400 13px var(--ft-font-ui); color: var(--ft-fg); }
.ft-chk:hover { background: var(--ft-tint); }
.ft-chk > input { flex: 0 0 auto; width: 15px; height: 15px; accent-color: var(--ft-navy); cursor: pointer; }
.ft-chk > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Dropdown header (entity switcher) and confirmation footer ── */
.ft-droplbl { font-size: 11px; font-weight: 600; color: var(--ft-fg2); text-transform: uppercase; letter-spacing: .02em; margin: 0 0 4px; }
.ft-picktype { margin: 0 0 8px; }
.ft-droprule { height: 1px; background: var(--ft-line); margin: 8px -8px; }
.ft-dropfoot { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; }
.ft-dropfoot > button { height: 32px; padding: 0 12px; font: 600 12.5px var(--ft-font-ui); }

/* ── Selected: chips above the button (the value stores only the identifier, the label comes from the cache) ── */
.ft-pickfield { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.ft-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.ft-chip { display: inline-flex; align-items: center; gap: 6px; background: var(--ft-tint); color: var(--ft-navy); border-radius: var(--ft-rx); padding: 4px 6px 4px 11px; font: 600 12px var(--ft-font-ui); }
.ft-chip-del { border: 0; background: transparent; color: var(--ft-navy); cursor: pointer; font: 700 14px/1 var(--ft-font-ui); padding: 0 4px; }
.ft-chip-del:hover { color: var(--ft-dgr); }
