/* Geist + Geist Mono — self-hosted, bundled inside the APK (www/fonts/).
   Раніше шрифти тягнулися з Google Fonts CDN. Для offline-first застосунку
   тривоги це означало системний fallback саме тоді, коли зв'язку немає, —
   тобто типографіка ламалася в найгірший момент. Тепер файли їдуть в APK.
   Це variable-шрифти (вісь wght 100–900), тому один файл на subset покриває
   всі потрібні насиченості: Geist 400/500/600, Geist Mono 400/500.
   Subset'и latin + latin-ext + cyrillic + cyrillic-ext; cyrillic обов'язковий —
   інтерфейс український (і, ї, є, ґ, №). Шрифти під ліцензією OFL (Vercel). */

/* Geist — cyrillic */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('../fonts/geist-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Geist — cyrillic-ext */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('../fonts/geist-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Geist — latin */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('../fonts/geist-latin.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;
}
/* Geist — latin-ext */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('../fonts/geist-latin-ext.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;
}

/* Geist Mono — cyrillic */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('../fonts/geist-mono-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Geist Mono — cyrillic-ext */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('../fonts/geist-mono-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* Geist Mono — latin */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('../fonts/geist-mono-latin.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;
}
/* Geist Mono — latin-ext */
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('../fonts/geist-mono-latin-ext.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;
}

/* ВАРТА ПОВІТРЯ — «Інженерний / B» design system.
   Чорне полотно, помаранчевий акцент, білі value-бокси, тонкі лінії,
   Geist + Geist Mono, квадратні кути. Reskin only — every id/class below
   is a hook the JS toggles; only look changes. See design_lab/eng/*. */
:root {
  /* exact token block from design_lab/eng/base.css */
  --bg: #000; --grid: #0e0e0e; --panel: #070707; --line: #1c1c1c; --line-2: #2a2a2a; --line-3: #3a3a3a;
  /* --dim and --info are lifted from #7d7d79 / #7d8592: raising the surfaces to
     #131313 / #1b1b1b dropped both under the 4.5:1 AA floor (4.17 and 4.13).
     Verified after the change: every text colour clears AA on all 3 surfaces. */
  --text: #e8e8e6; --text-2: #9b9b97; --dim: #858581;
  --accent: #ff6a2b; --accent-dim: #7a3417;
  --red: #ff4438; --orange: #ff8a3c; --yellow: #d8c24a; --green: #5cb176; --info: #7d8593;
  /* Chat-list redesign (concept A): the friend safety-state ring/dot and a
     lighter accent tint for small text (time, ticks, preview) on dark
     surfaces, where the saturated --accent reads muddy. Kept as their own
     tokens rather than aliased to --red/--green/--info, which independently
     drive alarm colouring elsewhere and must not shift with this feature. */
  --state-sheltering: #ff8a3c; --state-home: #5cb176; --state-work: #7aa2ff;
  --accent-text: #ff8a55;
  /* Map-only palette - hand-kept equal to app.js's MAP_COLORS (the map's own
     source of truth), NOT aliases of --red/--orange/--green: those still drive
     the rest of the UI (status pills, banners) and must not shift just because
     the map's colours were retuned. See MAP_COLORS for the JS side. */
  --map-active: #ff4438; --map-partial: #f5a524; --map-clear: #3f8a63; --map-unknown: #4b515c;
  --sans: "Geist", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  /* aliases so the rest of this file (and any inline overrides) can use
     descriptive names while staying pinned to the tokens above */
  --base: var(--bg);
  /* Surfaces are separated enough to survive an OLED screen at low brightness.
     #000 / #070707 / #0a0a0a spanned 3.9% luminance, so the whole depth system
     collapsed to "black with hairlines" in the dark — exactly the condition
     this app is used in. */
  --surface: #131313;
  --raised: #1b1b1b;
  --border: var(--line);
  --border-2: var(--line-2);
  --border-3: var(--line-3);
  --focus: var(--line-3);
  --muted: var(--dim);
  --amber: var(--accent);
  /* Softer geometry: the square-cornered terminal look read as a devops
     dashboard rather than a tool a frightened person reaches for. */
  --r-ctrl: 8px;
  --r-block: 12px;
  --r-surface: 12px;
  --tabbar-h: 64px;
  /* Ink on an accent or red fill. There used to be three values doing this one
     job (#0a0a0a, #0b0d10, #111) with no rule for which went where. */
  --on-accent: #0a0a0a;
  --avatar-size: 44px;
}

* { box-sizing: border-box; }
/* Author display rules (grid/flex on .tabbar, .card, .view) outrank the user
   agent's [hidden] { display: none }, so hidden elements would stay visible.
   Restore the attribute's meaning once, for everything. */
[hidden] { display: none !important; }
html, body { margin: 0; height: 100%; }

/* --- native touch behaviour ---
   None of this existed, which is a large part of why the app read as a webview:
   the default blue-grey tap flash, text selection on every label during a
   scroll-drag, and a rubber-band that revealed the page behind the app. */
* { -webkit-tap-highlight-color: transparent; }
body { overscroll-behavior-y: none; }
button, .as-button, [role="button"], .tab, .seg-btn, .pf-row, .feed-item, .chip,
.card-head, .status-banner, .sp-row, .chat-list-row, .area-item {
  -webkit-user-select: none; user-select: none; touch-action: manipulation; }
/* Text people may legitimately want to copy stays selectable. */
.feed-body, .fi-ctext, .chat-bubble, .invite-code, .diag-log, .ab-text, input, textarea {
  -webkit-user-select: text; user-select: text; }
/* A press needs a visible acknowledgement now that the platform flash is off. */
@media (hover: none) {
  button:active, .as-button:active, [role="button"]:active,
  .feed-item:active, .pf-row:active, .card-head:active { filter: brightness(1.35); }
  .tab:active .tab-ico { background: color-mix(in srgb, var(--accent) 12%, transparent); }
}
body {
  background: var(--base);
  color: var(--text);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--amber); color: #0a0a0a; }

#app {
  max-width: 480px; margin: 0 auto; min-height: 100%; display: flex; flex-direction: column;
  /* The decorative grid is gone: it carried no information and added visual
     noise to a screen people open while frightened. */
}
.view { flex: 1; display: flex; flex-direction: column; gap: 16px; padding: 20px; }
.view[hidden] { display: none; }
/* The feed is the one screen where vertical space is the scarce resource:
   every pixel of chrome is a pixel not showing the threat. */
#view-feed { gap: 10px; }
#view-feed .app-head { margin-bottom: 2px; }

/* pull-to-refresh indicator (Стрічка / Чати) */
.ptr-indicator { position: fixed; left: 50%; top: calc(52px + env(safe-area-inset-top));
  transform: translate(-50%, -100%); z-index: 35; width: 30px; height: 30px; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--border-2); display: grid; place-items: center;
  color: var(--accent); transition: transform .15s ease-out, opacity .15s ease-out; opacity: 0; pointer-events: none; }
.ptr-indicator svg { width: 16px; height: 16px; }
.ptr-indicator.pulling { transition: none; }
.ptr-indicator.spin svg { animation: ptr-spin .7s linear infinite; }
@keyframes ptr-spin { to { transform: rotate(360deg); } }

/* floating action button (Чати: new group) */
.fab { position: fixed; right: max(20px, calc((100vw - 480px) / 2 + 20px));
  bottom: calc(var(--tabbar-h) + 18px + env(safe-area-inset-bottom));
  width: 56px; height: 56px; border-radius: 18px; z-index: 30;
  background: var(--accent); color: var(--on-accent); border: 1px solid var(--accent);
  display: grid; place-items: center; cursor: pointer; box-shadow: 0 8px 20px -6px rgba(0,0,0,.6); }
.fab:hover { background: var(--amber); }
.fab svg { width: 24px; height: 24px; }

/* map area-detail bottom sheet */
.map-sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 91; max-width: 480px; margin: 0 auto;
  background: var(--surface); border: 1px solid var(--border-2); border-bottom: 0;
  border-radius: 16px 16px 0 0;
  padding: 8px 18px calc(20px + env(safe-area-inset-bottom)); transform: translateY(0);
  transition: transform .18s ease-out; }
.map-sheet[hidden] { display: none; }
.map-sheet-grip { width: 36px; height: 4px; border-radius: 2px; background: var(--border-3); margin: 0 auto 12px; }

@media (prefers-reduced-motion: no-preference) {
  /* directional enter when arriving via a tab swipe */
  @keyframes view-in-left { from { opacity: .35; transform: translateX(22px); } to { opacity: 1; transform: none; } }
  @keyframes view-in-right { from { opacity: .35; transform: translateX(-22px); } to { opacity: 1; transform: none; } }
  .view.swipe-l { animation: view-in-left .18s ease-out; }
  .view.swipe-r { animation: view-in-right .18s ease-out; }
  /* Tapping a tab used to be an instant cut while swiping to the same place
     animated, so one destination arrived two different ways. */
  .view.tab-in { animation: view-fade-in .16s ease-out; }
  @keyframes view-fade-in { from { opacity: .5; } to { opacity: 1; } }

  /* A full-screen push is a layer ABOVE, not a replacement. Without motion the
     back-button model has to be learned instead of felt. */
  .view-profile-screen:not([hidden]) { animation: screen-push .22s cubic-bezier(.2, 0, 0, 1); }
  @keyframes screen-push { from { opacity: 0; transform: translateX(26px); } to { opacity: 1; transform: none; } }

  /* The sheet declared a transform transition but was toggled via [hidden] ->
     display:none, so it never ran and the sheet teleported in. */
  .map-sheet:not([hidden]) { animation: sheet-up .24s cubic-bezier(.2, 0, 0, 1); }
  @keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }
}
.view-auth { padding: 24px 20px; gap: 24px; }
.view-app {
  padding-top: calc(52px + env(safe-area-inset-top));
  padding-bottom: calc(var(--tabbar-h) + 16px + env(safe-area-inset-bottom));
}

/* --- profile avatar bar + dropdown --- */
#profile-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 40; max-width: 480px; margin: 0 auto;
  display: flex; justify-content: flex-end; padding: calc(52px + env(safe-area-inset-top)) 20px 0; pointer-events: none; }
.avatar-btn { pointer-events: auto; width: var(--avatar-size); height: var(--avatar-size); border-radius: 50%; border: 1px solid var(--border-2);
  background: var(--surface); color: var(--amber); font-family: var(--mono); font-weight: 500; font-size: 13px;
  cursor: pointer; display: grid; place-items: center; overflow: hidden; position: relative;
  /* The avatar floats over whatever the feed puts at the top — during an alarm
     that is a filled red banner the same darkness as the button, which made it
     vanish ("картка тривоги затуляє"). A dark contrast ring + drop shadow keep it
     legible on any background without changing the calm-state look. */
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, .22), 0 2px 8px rgba(0, 0, 0, .6); }
.avatar-btn:hover { border-color: var(--amber); }
#profile-scrim { position: fixed; inset: 0; z-index: 45; background: rgba(0,0,0,.55); }
.profile-menu { position: fixed; z-index: 46; top: calc(50px + env(safe-area-inset-top)); right: 12px;
  width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 90px); overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-block);
  box-shadow: 0 20px 50px -20px rgba(0,0,0,.9); padding: 0; }
@media (min-width: 496px) { .profile-menu { right: calc(50% - 240px + 12px); } }
.pm-head { display: flex; align-items: center; gap: 10px; padding: 12px; border-bottom: 1px solid var(--border); }
.pm-avatar { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--border-2); background: var(--raised);
  color: var(--amber); font-family: var(--mono); font-weight: 500; display: grid; place-items: center; flex: none; overflow: hidden; }
.pm-id { flex: 1; min-width: 0; }
.pm-name { font-weight: 500; }
.pm-username { font-family: var(--mono); }
.pm-section { padding: 10px 12px; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
.pm-label { font-family: var(--sans); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
/* The profile menu holds Фільтри, Джерела and Вийти, in the hardest corner for a
   thumb; its rows should not also be undersized. */
.pm-row { display: flex; align-items: center; justify-content: space-between; width: 100%;
  background: none; border: 0; color: var(--text); font: inherit; font-size: 13px; padding: 10px 2px; min-height: 44px; cursor: pointer; }
.pm-row:hover { color: var(--amber); }
.pm-row.danger { color: var(--red); }
.pm-row.danger:hover { color: var(--red); }
.pm-row .fi-chev { color: var(--muted); transition: transform .15s; }
.pm-row.open .fi-chev { transform: rotate(180deg); }
#pw-form { display: flex; flex-direction: column; gap: 8px; }
#pw-form input { background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-ctrl);
  color: var(--text); padding: 12px; font-size: 14px; min-height: 42px; font-family: var(--sans); }
#pw-form input[hidden] { display: none; }

/* Full-screen profile */
.view-profile-screen { position: fixed; inset: 0; z-index: 60; max-width: 480px; margin: 0 auto;
  background: var(--base); overflow-y: auto; padding-bottom: calc(24px + env(safe-area-inset-bottom)); }
.pf-top { display: flex; align-items: center; gap: 8px; position: sticky; top: 0; z-index: 1;
  padding: calc(10px + env(safe-area-inset-top)) 12px 10px; background: rgba(0,0,0,.94); backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--border); }
.pf-title { margin: 0; font-size: 15px; letter-spacing: .02em; }
.pf-hero { display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 20px 12px 16px; }
.pf-photo { position: relative; }
.pm-avatar.big { width: 74px; height: 74px; font-size: 28px; color: var(--accent); border: 1px solid var(--border-2);
  background: var(--panel); }
.pf-photo-edit { position: absolute; right: -5px; bottom: -5px; width: 26px; height: 26px; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--border-2); color: var(--text-2); cursor: pointer; display: grid; place-items: center; }
.pf-photo-edit svg { width: 14px; height: 14px; }
.linklike { background: none; border: 0; color: var(--amber); font-family: var(--sans); font-size: 11px; letter-spacing: .05em;
  text-transform: uppercase; cursor: pointer; padding: 2px 0; }
.linklike:hover { text-decoration: underline; }
/* Rows are individual bordered engineering modules inside a padded column. */
.pf-list { display: flex; flex-direction: column; gap: 8px; padding: 4px 14px 0; }
.pf-row { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 12px 13px; border: 1px solid var(--line); background: var(--panel); color: var(--text); font: inherit; }
.pf-row.as-button { cursor: pointer; }
.pf-row.as-button:hover { border-color: var(--line-2); background: #0a0a0a; }
.pf-ic { width: 18px; height: 18px; flex: none; display: grid; place-items: center; color: var(--text-2); }
.pf-ic svg { width: 18px; height: 18px; }
/* avatar in a friends-screen row (search result, request, friend) */
.pf-ava { width: 40px; height: 40px; flex: none; display: grid; place-items: center; overflow: hidden;
  border: 1px solid var(--border-2); background: var(--raised); color: var(--accent);
  font-family: var(--sans); font-weight: 500; font-size: 15px; }
.pf-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.pf-cap { font-family: var(--sans); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
/* One-line explanation of what a settings row actually controls. */
.pf-desc { font-size: 12px; color: var(--muted); line-height: 1.25; }
.pf-val { font-size: 14px; color: var(--text); }
.pf-row .fi-chev { color: var(--muted); transition: transform .15s; display: grid; place-items: center; }
.pf-row .fi-chev svg { width: 14px; height: 14px; }
.pf-row.open .fi-chev { transform: rotate(180deg); }
/* Section label between row groups. */
.pf-group-label { font-family: var(--sans); font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--dim); padding: 14px 15px 2px; }
.pf-subform { display: flex; flex-direction: column; gap: 9px; margin: 0 14px; padding: 12px 13px 14px; border: 1px solid var(--line); background: var(--panel); }
.pf-subform input { background: var(--bg); border: 1px solid var(--border-2); border-radius: var(--r-ctrl);
  color: var(--text); padding: 11px; font-size: 14px; min-height: 42px; font-family: var(--sans); }
.pf-subform input[hidden] { display: none; }

/* Avatar picker (tint swatches) */
.avatar-picker { display: flex; gap: 8px; justify-content: center; padding: 4px 14px 8px; flex-wrap: wrap; }
.avatar-picker[hidden] { display: none; }
.avp-opt { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--border-2); background: var(--panel);
  padding: 0; cursor: pointer; overflow: hidden; font-family: var(--sans); color: var(--text-2); }
.avp-opt[aria-current="true"] { border-color: var(--amber); color: var(--amber); }
.avp-opt img { width: 100%; height: 100%; display: block; }
.avp-mono { font-family: var(--mono); font-weight: 500; font-size: 16px; color: var(--accent); }

/* Name edit form */
.pf-name-form { display: flex; flex-direction: column; gap: 8px; margin: 0 14px; padding: 11px 13px 13px; border: 1px solid var(--line); background: var(--panel); }
.pf-name-form[hidden] { display: none; }
.pf-name-form input { background: var(--bg); border: 1px solid var(--border-2); border-radius: var(--r-ctrl);
  color: var(--text); padding: 11px; font-size: 14px; font-family: var(--sans); }
.pf-form-actions { display: flex; gap: 8px; }
.pf-form-actions .btn-primary, .pf-form-actions .btn-ghost { flex: 1; }

/* Notification sound preferences + picker */
.toggle-row input[type=checkbox] { flex: none; }
.sp-hint { margin: 0 0 6px; }
.sp-hint-play { color: var(--amber); }
.sp-row { gap: 12px; padding: 12px; }
/* Round, clearly-tappable preview control. */
.sp-play { flex: none; width: 36px; height: 36px; border-radius: 50%; background: var(--raised);
  border: 1px solid var(--border-2); color: var(--amber); font-size: 13px; cursor: pointer;
  display: grid; place-items: center; padding-left: 2px; }
.sp-play:hover, .sp-play:active { border-color: var(--amber); background: rgba(255,106,43,0.14); }
/* Radio-style choice: every row shows an empty ring; the chosen one fills amber. */
.sp-check { flex: none; width: 24px; height: 24px; display: grid; place-items: center;
  border: 1.5px solid var(--line-2); border-radius: 50%; color: transparent;
  font-size: 14px; font-weight: 800; }
.sp-row[aria-current="true"] { border-color: var(--amber); border-left: 4px solid var(--amber);
  background: rgba(255,106,43,0.14); }
.sp-row[aria-current="true"] .pf-val { color: var(--amber); font-weight: 700; }
.sp-row[aria-current="true"] .sp-check { color: #0a0a0a; background: var(--amber); border-color: var(--amber); }
.sp-footer { position: sticky; bottom: 0; margin-top: 8px;
  padding: 10px 0 calc(8px + env(safe-area-inset-bottom));
  background: linear-gradient(to top, var(--base) 64%, transparent); }
.sp-footer .btn-primary { width: 100%; }
/* The footer is sticky bottom:0 inside the scrolling screen, so without room
   below the list the last sounds (Гудок, Чортик…) scroll UNDER the opaque
   "Готово" button and cannot be read or tapped. Clear the footer's height plus
   the gesture-nav inset so the final row always sits above it. */
#sound-picker-list { padding-bottom: calc(88px + env(safe-area-inset-bottom)); }

.app-toast { position: fixed; left: 50%; bottom: 88px; transform: translateX(-50%) translateY(8px);
  background: var(--surface); border: 1px solid var(--border-2); color: var(--text); font-family: var(--mono); font-size: 12px;
  padding: 9px 16px; border-radius: var(--r-ctrl); z-index: 80; opacity: 0; pointer-events: none; max-width: 88%;
  text-align: center; transition: opacity .2s, transform .2s; }
.app-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* --- icons --- */
.ic { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.ic-sm { width: 16px; height: 16px; }

/* --- brand / login --- */
.brand { text-align: center; margin-top: 8vh; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.brand-mark { width: 52px; height: 52px; border-radius: 50%; background: var(--surface); border: 1px solid var(--border-2);
  display: grid; place-items: center; color: var(--amber); font-size: 24px; }
.brand h1 { margin: 8px 0 0; font-size: 20px; letter-spacing: .08em; font-family: var(--sans); text-transform: uppercase; }
.brand-sub { margin: 0; color: var(--muted); font-size: 13px; max-width: 28ch; }

/* --- auth screen ---
   Full-bleed art behind the whole sign-in flow, with the brand's own dark
   scrim so form text keeps its contrast. The image is decoration: every word
   lives in the markup, and a failed load leaves the plain dark screen. */
.view-auth { position: relative; overflow: hidden; }
.auth-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: url("../img/auth-bg.webp") center / cover no-repeat; }
/* Just enough to hold text contrast, no more: stacked on top of dark art a
   heavy scrim reads as "the image failed to load". Lightest across the middle,
   where the sky carries the picture; firmer at the very top and bottom, which
   is where the headline and the legal line sit. */
.auth-bg::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,.28) 30%,
    rgba(0,0,0,.34) 62%, rgba(0,0,0,.72) 100%); }
.auth-in { position: relative; z-index: 1; flex: 1; display: flex;
  flex-direction: column; gap: 22px; }
.brand-mark .blogo-gem { width: 30px; height: 30px; }
.view-auth .brand h1 { font-weight: 700; letter-spacing: .16em; }
.view-auth .brand-sub { max-width: 32ch; line-height: 1.45; color: var(--text-2); }
.auth-stack { display: flex; flex-direction: column; gap: 10px; }
/* Scoped to .view-auth on purpose, and not only for intent: .btn-ghost and
   .btn-google are declared LATER in this file, so a bare .btn-onart loses the
   cascade to them at equal specificity and silently does nothing. That is
   exactly what happened - the auth buttons rendered as plain ghosts. */
.view-auth .btn-lg { min-height: 54px; font-size: 15px; }
/* Google leads, so it is the solid one. Its own dark-theme guidance is a dark
   surface with white text beside the full-colour mark, which is also what reads
   as "the fast way in" against the two outlined choices below it. */
.view-auth .btn-google { background: #1f2124; border-color: rgba(255,255,255,.16);
  color: #fff; font-weight: 500; box-shadow: 0 8px 22px -12px rgba(0,0,0,.9); }
/* Outlined buttons sit on photographic art, so they get a real backing: a bare
   1px border dissolves into the picture. */
.view-auth .btn-onart { background: rgba(12,13,15,.58); color: var(--text);
  border: 1px solid rgba(255,255,255,.26); min-height: 50px; font-size: 14px;
  font-weight: 500; text-transform: none; letter-spacing: 0;
  backdrop-filter: blur(10px); }
/* The returning-user path, told apart from "create an account" by warmth rather
   than by size - it must be findable without competing with the primary action. */
.view-auth .btn-signin { color: var(--amber);
  border-color: color-mix(in srgb, var(--accent) 46%, rgba(255,255,255,.16));
  background: color-mix(in srgb, var(--accent) 10%, rgba(12,13,15,.58)); }
.auth-card { background: color-mix(in srgb, var(--surface) 86%, transparent);
  backdrop-filter: blur(10px); }
.auth-alt { text-align: center; color: var(--text-2); font-size: 12px; margin: 8px 0 -4px; }

.auth-back { align-self: center; font-size: 12px; letter-spacing: 0; }
.auth-legal { margin-top: auto; text-align: center; color: var(--dim); font-size: 11px; }
/* Password strength: shape as well as colour, since the server's 12-character
   floor is strict enough that people need to see how far along they are. */
.pw-meter { height: 3px; border-radius: 2px; background: var(--raised); overflow: hidden;
  margin-top: -6px; }
.pw-meter i { display: block; height: 100%; width: 0; border-radius: 2px;
  background: var(--red); transition: width .18s ease-out, background-color .18s; }
.pw-meter i.ok { background: var(--green); }
.pw-meter i.mid { background: var(--yellow); }

.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-surface); padding: 18px; display: flex; flex-direction: column; gap: 14px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field span { font-family: var(--sans); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-2); }
.field input, select {
  background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-ctrl);
  color: var(--text); padding: 13px; font-size: 15px; min-height: 46px; font-family: var(--sans);
}
.field input:focus, select:focus { outline: none; border-color: var(--amber); }

.btn-primary { background: var(--amber); color: #0a0a0a; border: 1px solid var(--amber); border-radius: var(--r-ctrl);
  padding: 13px; font-family: var(--sans); font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
  font-weight: 500; min-height: 46px; cursor: pointer; }
.btn-primary:hover { background: #ff7c40; }
.btn-primary:disabled { opacity: .5; cursor: default; }
.btn-ghost { background: none; color: var(--text-2); border: 1px solid var(--border-2); border-radius: var(--r-ctrl);
  padding: 9px 14px; font-family: var(--sans); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; min-height: 40px; cursor: pointer; }
.btn-ghost:hover { border-color: var(--border-3); color: var(--text); }
.icon-btn { background: var(--surface); color: var(--text-2); border: 1px solid var(--border); border-radius: var(--r-ctrl);
  width: 44px; height: 44px; font-size: 15px; cursor: pointer; display: grid; place-items: center; }
.icon-btn:hover { border-color: var(--border-2); color: var(--text); }

.btn-google {
  width: 100%; display: flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--raised); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-ctrl); padding: 13px; font-size: 14px; min-height: 46px; cursor: pointer;
}
.btn-google:hover { border-color: var(--border-2); }
.btn-google:disabled { opacity: .5; }
.g-logo { flex: none; }
.divider { display: flex; align-items: center; gap: 12px; color: var(--muted); font-family: var(--sans); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; margin: 4px 0; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }

.error { color: var(--red); font-size: 13px; margin: 0; font-family: var(--mono); }
.muted { color: var(--muted); }
.small { font-size: 12px; }
.strong { font-weight: 600; }
.empty { color: var(--muted); text-align: center; padding: 32px 0; font-size: 13px; }
/* An empty state that names a problem should offer the way out, not describe a
   menu path the user has to memorise and follow themselves. */
.empty-acts { display: flex; flex-direction: column; gap: 8px; align-items: stretch;
  margin: 16px auto 0; max-width: 260px; }

/* --- illustrated empty states ---
   A calm, atmospheric backdrop turns a dead screen into a brand moment. The
   image is DECORATION, never information (DESIGN.md): every word stays in the
   text, so a failed image load costs nothing. Applied by adding .empty-hero and
   a .is-<state> class in JS; the plain .empty text still renders on top.
   Full-bleed, not a framed card, so it reads as the screen itself rather than a
   sticker. The scrim keeps white text legible over the brightest bands of the
   art. Deliberately NOT used for the feed's in-alarm case: a calm sky behind
   "there is an alarm in your regions" would contradict the state. */
.empty-hero { color: var(--text);
  min-height: 62vh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px;
  padding: 40px 26px; overflow: hidden;
  background-size: cover; background-position: center; background-repeat: no-repeat;
  /* Full-bleed regardless of the parent's padding (which differs by view: .view
     is 20px, the chat pane less). Breaks out to the viewport but never wider
     than the centred app column, so the phone gets edge-to-edge and a wide
     desktop preview still fills the 480px card rather than the whole window. */
  position: relative; left: 50%; transform: translateX(-50%);
  width: 100vw; max-width: 480px; }
/* The dark top-and-bottom scrim is baked to match the rendered art, which is
   itself darker at top and bottom for exactly this. */
.empty-hero::before { content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(0,0,0,.72) 0%, rgba(0,0,0,.16) 34%,
    rgba(0,0,0,.5) 74%, rgba(0,0,0,.92) 100%); }
.empty-hero > * { position: relative; z-index: 1; }
.empty-hero .eh-title { font-family: var(--sans); font-weight: 600; font-size: 17px;
  color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,.7); letter-spacing: -.01em; }
.empty-hero .eh-sub { font-size: 12.5px; color: #d5d7d9; max-width: 30ch; line-height: 1.45;
  text-shadow: 0 1px 9px rgba(0,0,0,.8); }
/* Actions sit on photographic art here, not on the flat app surface: the ghost
   button's 1px border and muted text dissolve into it, so it gets an opaque
   backing and full-strength text. Only inside a hero - the plain empty state
   keeps the quieter treatment. */
.empty-hero .empty-acts { width: 100%; max-width: 280px; }
.empty-hero .empty-acts .btn-ghost { background: rgba(10,10,10,.72); color: #fff;
  border-color: rgba(255,255,255,.28); backdrop-filter: blur(6px); }
.empty-hero.is-calm    { background-image: url("../img/empty/calm.webp"); }
/* The in-alarm empty case gets its own backdrop rather than the calm sky: the
   art must never say "quiet night" while the text says there is an alarm.
   A tense red horizon, still restrained - this is a state readout, not a poster. */
.empty-hero.is-alarm   { background-image: url("../img/empty/alarm.webp"); }
.empty-hero.is-chats   { background-image: url("../img/empty/chats.webp"); }
.empty-hero.is-friends { background-image: url("../img/empty/friends.webp"); }
.empty-hero.is-offline { background-image: url("../img/empty/offline.webp"); }
@media (prefers-reduced-motion: no-preference) {
  .empty-hero { animation: eh-fade .5s ease-out; }
  @keyframes eh-fade { from { opacity: 0; } to { opacity: 1; } }
}
.row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }

/* --- app header --- */
/* Clearance for the floating avatar, derived from its own size. */
.app-head { display: flex; align-items: center; justify-content: space-between;
  padding-right: calc(var(--avatar-size) + 8px); }
.app-head h2 { margin: 0; font-size: 15px; letter-spacing: .06em; text-transform: uppercase; font-family: var(--sans); color: var(--text-2); }

/* --- feed header: brand instead of a label that repeated the tab bar --- */
.feed-head { gap: 9px; }
.fh-mark { display: grid; place-items: center; color: var(--accent); flex: none; }
.fh-mark .ic { width: 21px; height: 21px; }
/* margin-right:auto on the wordmark rather than space-between on the row: it
   keeps mark+name locked to the left and the refresh button to the right,
   whatever else the row ends up holding. */
.fh-name { font-family: var(--sans); font-weight: 600; font-size: 15px; letter-spacing: .14em;
  margin-right: auto; }

/* --- P0: notifications-off warning (life-safety) --- */
.notif-warning { display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; cursor: pointer;
  color: var(--text); font: inherit; padding: 12px 14px; min-height: 52px;
  background: color-mix(in srgb, var(--red) 20%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--red) 55%, var(--border)); }
.notif-warning[hidden] { display: none; }
.nw-ic { flex: none; color: var(--red); display: grid; place-items: center; }
.nw-ic svg { width: 22px; height: 22px; }
.nw-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* --red on this tinted background measures 4.31:1 — just under AA. Lightened to
   clear it; the icon beside it still carries the pure --red. */
.nw-title { font-weight: 600; font-size: 14px; color: #ff8078; }
.nw-sub { font-size: 12px; color: var(--text-2); }

/* push_unreachable strip: unlike the .notif-warning cards above, a tap on
   this one does nothing - there is no in-app fix - so it is a div, not a
   button, and its cursor should not claim otherwise. Its own × (reusing
   .pb-close) is the only interactive part, pushed to the row's end. */
.notif-warning.push-unreachable { cursor: default; }
.push-unreachable-dismiss { margin-left: auto; }

/* offline / stale-cache bar: this screen is showing the last known good data.
   Amber, not red - the red notif-warning above means "you will miss alerts";
   this one only means "what you are looking at is not live". */
.offline-bar { display: flex; align-items: flex-start; gap: 8px; width: 100%;
  padding: 10px 11px;
  background: color-mix(in srgb, var(--amber) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--amber) 45%, var(--border)); }
.offline-bar[hidden] { display: none; }
.ob-ic { flex: none; color: var(--amber); display: grid; place-items: center; padding-top: 2px; }
.ob-ic svg { width: 18px; height: 18px; }
.ob-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.ob-title { font-weight: 600; font-size: 13px; color: var(--amber); overflow-wrap: anywhere; }
.ob-sub { font-family: var(--sans); font-size: 11px; letter-spacing: .02em;
  color: var(--text-2); overflow-wrap: anywhere; }
.ob-retry { flex: none; display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
  white-space: nowrap; font: inherit; font-size: 11px; padding: 6px 10px; min-height: 40px;
  background: transparent; color: var(--text); border: 1px solid var(--border-2); }
.ob-retry:hover { border-color: var(--amber); color: var(--amber); }
.ob-retry svg { width: 13px; height: 13px; flex: none; }
.ob-retry[disabled] { opacity: .5; cursor: default; }

/* one-line "your regions" verdict */
/* The personal verdict, inside the status banner. It is the line the user
   actually came for, so it outranks the source line beneath it. */
.status-regions { font-size: 13px; font-weight: 500; line-height: 1.35; margin-top: 2px; }
.status-regions[hidden] { display: none; }
.status-regions.safe { color: var(--green); }
.status-regions.alarm { color: var(--red); }
.status-regions.unknown { color: var(--muted); }

/* map colour legend */
.map-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; }
.mleg { display: flex; align-items: center; gap: 6px; font-family: var(--sans); font-size: 11px;
  letter-spacing: .04em; color: var(--text-2); }
/* Background is set inline from JS (see renderMapLegendColors), read straight
   from MAP_COLORS - not a sev-red/orange/green/grey class here, so the legend
   can never drift out of sync with the map's own palette again. */
.mleg-sw { width: 11px; height: 11px; flex: none; border: 1px solid var(--border-2); background: var(--line-3); }

/* --- MapLibre DOM markers (see mlSyncChips / mlSyncMyPlace in app.js) ---
   Markers rather than symbol layers: there are only a handful of them, they
   must be tappable, and a pill is a CSS job rather than a glyph. The two hexes
   are the map's own MAP_COLORS.active / MAP_COLORS.selected, hand-kept equal
   the same way the SVG fallback's --map-* vars are. */
.ml-chip { font-family: var(--mono); font-size: 10.5px; font-weight: 600; line-height: 1;
  color: #fff; background: #ff4438; border: 1.5px solid #07080a; border-radius: 9px;
  padding: 4px 7px; cursor: pointer; white-space: nowrap; }
.ml-me { position: relative; width: 14px; height: 14px; cursor: pointer; }
.ml-me-dot { position: absolute; inset: 2px; border-radius: 50%; background: #4aa8ff;
  border: 2px solid #07080a; }
.ml-me-ring { position: absolute; inset: 0; border-radius: 50%; border: 2px solid #4aa8ff;
  animation: mlMePulse 2s ease-out infinite; }
@keyframes mlMePulse {
  0%   { transform: scale(.6); opacity: .75; }
  100% { transform: scale(2.4); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .ml-me-ring { animation: none; opacity: .5; } }

/* keyboard/switch-access focus, visible on every control */
a:focus-visible, button:focus-visible, [role="button"]:focus-visible, [tabindex]:focus-visible,
input:focus-visible, select:focus-visible, .seg-btn:focus-visible, .tab:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; }

/* small icon controls: keep the glyph small but guarantee a >=40px hit area */
.ch-del, .sp-play, .kw-x, .pb-close { min-width: 40px; min-height: 40px; }

/* in-design confirmation sheet (replaces window.confirm) */
.confirm-scrim { position: fixed; inset: 0; z-index: 90; background: rgba(0,0,0,.6);
  display: flex; align-items: flex-end; justify-content: center; }
.confirm-scrim[hidden] { display: none; }
.confirm-sheet { width: 100%; max-width: 480px; background: var(--surface); border: 1px solid var(--border-2);
  border-bottom: 0; padding: 20px 16px calc(20px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; gap: 14px; }
.confirm-msg { font-size: 15px; color: var(--text); line-height: 1.4; }
.confirm-actions { display: flex; gap: 10px; }
.confirm-actions button { flex: 1; min-height: 46px; }

/* --- controls: seg / select-inline (shared toolbar building blocks) --- */
/* A track with a pill riding in it, not a divided box. The hairline dividers
   were doing the separating AND the amber block was doing it again, so the
   selected item read as a filled table cell with two square corners jammed
   against the frame. The pill separates on its own; the dividers are gone. */
.seg { display: flex; gap: 2px; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-ctrl); padding: 3px; }
/* 44px is the floor for anything a thumb aims at while walking; the old 32px
   toolbar was the most-used cluster in the app and 33% under spec. The floor is
   on the BUTTON, so the track's own padding is added on top of it rather than
   eaten out of it - a 3px ring of dead space around a control someone stabs at
   in the dark is how you miss. */
.seg-btn { flex: 1; background: none; border: 0; border-radius: calc(var(--r-ctrl) - 3px);
  color: var(--muted);
  font-family: var(--sans); font-size: 12px; letter-spacing: .04em; text-transform: uppercase;
  padding: 8px 8px; cursor: pointer; white-space: nowrap; min-height: 44px;
  /* The track now spends 8px of the row on its own padding and gap, and the
     feed row is nowrap at 58/42 with «УСІ ДЖЕРЕЛА» in it - already the tightest
     label in the app. Two guards: the horizontal padding gives that 8px back,
     and min-width:0 with an ellipsis means the worst case on a narrow phone is
     a clipped word rather than text spilling out of its own button. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
  transition: background-color .14s ease, color .14s ease; }
/* Pressing an unselected one shows it took the tap even before the feed reloads.
   Kept to the raised surface: an amber flash on touch would be indistinguishable
   from having actually selected it. */
.seg-btn:active { background: var(--raised); color: var(--text-2); }
/* The pill sits ON the track, so it carries the shadow. Small and tight - this
   is a control on a black screen at 3am, not a card. */
.seg-btn[aria-current="true"] { background: var(--amber); color: var(--on-accent); font-weight: 500;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .55); }
.seg-btn[aria-current="true"]:active { background: var(--amber); color: var(--on-accent); }
@media (prefers-reduced-motion: reduce) { .seg-btn { transition: none; } }

.usr-row { padding: 8px 0; border-bottom: 1px solid var(--border); }
.usr-row:last-child { border-bottom: 0; }
.usr-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.usr-name { color: var(--text); font-size: 13px; font-weight: 600; }
.usr-seen { flex: none; }
.usr-sub { margin-top: 2px; line-height: 1.4; }
/* seg-sm is now only used inside Фільтри, where the row is not thumb-critical
   while walking, so it may sit at the 40px secondary floor. */
.seg-sm { padding: 2px; }
.seg-sm .seg-btn { padding: 6px 8px; font-size: 11px; min-height: 40px;
  border-radius: calc(var(--r-ctrl) - 2px); }
.select-inline { flex: 1; min-width: 0; display: flex; }
/* 52px, not 44: it stands beside a .seg in the same row, and that track is now
   its 44px button plus 3px of padding top and bottom plus its border. Two
   controls in one row at two different heights is the kind of thing you stop
   seeing after a week and every new reader sees immediately. */
.select-inline select { flex: 1; min-width: 0; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-ctrl); color: var(--text-2); padding: 7px 9px;
  font-family: var(--sans); font-size: 12px; min-height: 52px; }
#feed-updated { text-align: right; font-family: var(--mono); letter-spacing: .03em; }
/* Polls stopped landing. Amber, not red: the data on screen is still real, it is
   just no longer being refreshed — same distinction the offline bar makes. */
#feed-updated.stalled { color: var(--amber); }

/* --- official status banner: the shield dome --- */
/* The banner is a shield: generated dome art at the bottom, a scrim that keeps
   the text readable over it, and a rim light in the state colour. Every fact is
   text — the art carries no information, so a failed image load costs nothing.
   --sc is the one state colour every layer below reads from. */
.status-clear   { --sc: var(--green); }
.status-active  { --sc: var(--red); }
.status-partial { --sc: var(--orange); }
.status-unknown { --sc: var(--info); }

.status-banner { position: relative; overflow: hidden; isolation: isolate;
  display: block; text-align: center; padding: 16px 14px 64px; border-radius: var(--r-block);
  border: 1px solid var(--border); background: var(--surface); cursor: pointer; }
/* This is the most important control on the screen; a 1px border-colour change
   is not a visible focus indicator, so it keeps the real ring. */
.status-banner:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* One scene per state, not one asset re-tinted: the radio telescope under red,
   amber and teal light differs in intensity and glow, not only in hue, and a
   hue-rotate of one picture could never do that. ~100 KB for all three. The dish
   stands in the right fifth of the frame. The banner keeps a strip below the
   chips for it (the tall bottom padding) and the picture is sized by WIDTH and
   pinned to the bottom: "cover" on a 150px banner blew it up by height and put
   the dish behind the title. Its top edge fades out through the mask so no
   seam shows under the eyebrow. Unknown reuses the calm scene in grey - no data
   is not a fourth mood. */
.sb-art { position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: url("../img/status-clear.webp") center bottom / 100% auto no-repeat;
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, transparent 22%, #000 62%);
  mask-image: linear-gradient(180deg, transparent 0%, transparent 22%, #000 62%); }
.status-active  .sb-art { background-image: url("../img/status-alarm.webp"); }
.status-partial .sb-art { background-image: url("../img/status-threat.webp"); }
.status-unknown .sb-art { filter: grayscale(1) brightness(.85); }
/* Black-based, not surface-based: during an alarm the surface underneath is a
   red field, and a --surface-coloured scrim would grey it out. */
.sb-scrim { position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(180deg, rgba(0,0,0,.88) 0%, rgba(0,0,0,.5) 38%,
      rgba(0,0,0,.14) 72%, transparent 100%),
    radial-gradient(90% 120% at 50% 118%, color-mix(in srgb, var(--sc) 30%, transparent) 0%, transparent 62%); }
.sb-rim { position: absolute; z-index: 1; left: -8%; right: -8%; bottom: -52px; height: 120px;
  border-radius: 50%; border-top: 1.5px solid var(--sc); pointer-events: none;
  box-shadow: 0 -12px 34px -12px color-mix(in srgb, var(--sc) 70%, transparent); }

.status-main { position: relative; z-index: 2; min-width: 0; }
.status-chev { position: absolute; right: 12px; top: 14px; z-index: 3; color: var(--muted);
  transition: transform .15s; font-family: var(--mono); }
.status-banner.open .status-chev { transform: rotate(180deg); }

/* Shape and words, not just hue: the eyebrow names the state in text and carries
   the state glyph, so colour is never the only carrier. */
.status-eyebrow { display: flex; align-items: center; justify-content: center; gap: 7px;
  font-family: var(--mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--sc); }
.status-ic { flex: none; display: grid; place-items: center; width: 14px; height: 14px; }
.status-ic .ic { width: 14px; height: 14px; stroke-width: 2; }

/* The answer must outrank the chrome. This used to be 13px under a 15px
   navigation label, so the biggest word on the screen was "СТРІЧКА". */
.status-title { font-weight: 700; font-family: var(--sans); font-size: 27px;
  letter-spacing: -.01em; line-height: 1.05; margin-top: 8px; color: var(--text); }

/* One row, never two: a wrapped second row changed the banner's height between
   states and made the whole feed jump on every transition. */
.status-chips { display: flex; flex-wrap: nowrap; overflow: hidden; justify-content: center;
  gap: 6px; margin-top: 12px; }
.status-chips[hidden] { display: none; }
/* Chips shrink and ellipsise rather than clip. One over-long value (a bad
   duration once read "496713 год") used to slice the chips on either side of it
   in half, which looked like a broken layout instead of a wrong number. */
.status-chip { font-family: var(--mono); font-size: 10.5px; letter-spacing: .02em; padding: 4px 8px;
  border-radius: 999px; border: 1px solid var(--border-2); color: var(--text-2);
  background: rgba(0,0,0,.45); white-space: nowrap;
  flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.status-chip.on { border-color: color-mix(in srgb, var(--sc) 55%, transparent); color: var(--sc); }
.status-chip b { color: var(--text); font-weight: 500; }
/* "+2": the row is capped at two regions and without this the cap is invisible,
   so two chips read as the whole list. It must not compete with a region's own
   chip, and must not shrink away before the chips it is counting for. */
.status-chip-more { flex: 0 0 auto; color: var(--text-2); opacity: .75; }
.status-since[hidden] { display: none; }
.status-sub { margin-top: 7px; }
.status-sub:empty { display: none; }

/* The negative margin fakes attachment to the banner across the view's 16px gap. */
.status-details { display: flex; flex-direction: column; gap: 8px; margin-top: -6px; padding: 12px; background: var(--surface);
  border: 1px solid var(--border); border-top: 0; border-radius: 0 0 var(--r-block) var(--r-block); }
.status-details[hidden] { display: none; }
/* When the banner goes full-bleed for an alarm, the panel below has to follow —
   otherwise the escalation state breaks its own connected-panel illusion, with
   the details inset 20px from a parent that runs edge to edge. */
.status-active + .status-details,
.status-partial + .status-details,
.status-clear.just-cleared + .status-details {
  margin-left: -20px; margin-right: -20px; margin-top: 0;
  border-left: 0; border-right: 0; border-radius: 0; padding: 14px 20px; }
.status-region { display: flex; flex-direction: column; gap: 3px; }
.status-drow { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; }
.status-dval { font-family: var(--mono); font-weight: 500; white-space: nowrap; }
.status-subline { line-height: 1.35; }
.status-threat { color: var(--amber); font-weight: 600; }
.status-threat[hidden] { display: none; }
.status-foot { padding-top: 6px; border-top: 1px dashed var(--border-2); }

/* ESCALATION. An alarm and an all-clear used to differ by a 9px dot's hue on an
   identical card. During an alarm the banner becomes a filled red field that
   breaks the page margin — it is the loudest thing on the screen, which is the
   only correct hierarchy while something is flying toward you. */
.status-active, .status-partial {
  margin: 0 -20px; border-radius: 0; border-left: 0; border-right: 0;
  padding: 20px 20px 72px; }
.status-active {
  background: linear-gradient(180deg, #5c0f0b, #3a0806);
  border-color: var(--red); border-top-width: 3px; }
.status-active .status-title { font-size: 34px; color: #ffd7d2; }
/* --red on the red field is barely a shade apart. The verdict line is the second
   thing read after the title, so on the filled fields it lightens instead. */
.status-active .status-sub,
.status-active .status-regions.alarm { color: #f6c9c4; }

.status-partial {
  background: linear-gradient(180deg, #4a2c05, #2e1a03);
  border-color: var(--orange); border-top-width: 3px; }
.status-partial .status-title { font-size: 28px; color: #ffe2c4; }
.status-partial .status-sub,
.status-partial .status-regions.alarm { color: #f7d6ac; }

/* Resting calm stays quiet on purpose — it is 99% of the app's life and a
   permanent green field would be noise. */
.status-clear .status-title { color: var(--text); }
.status-unknown .status-title { color: var(--text-2); }

/* THE END OF THE ALARM. Peak-end says the last moment is weighted as heavily as
   the peak: the app built a loud alarm and then resolved it with a colour swap.
   For a bounded window after an alarm actually ends, the all-clear gets the same
   structural weight the alarm had — a full-bleed field, a large title, and the
   duration the person just lived through, stated rather than deleted. */
.status-clear.just-cleared {
  margin: 0 -20px; border-radius: 0; border-left: 0; border-right: 0;
  padding: 20px 20px 72px;
  background: linear-gradient(180deg, #0f2e1c, #06150c);
  border-color: var(--green); border-top-width: 3px; }
.status-clear.just-cleared .status-title { font-size: 32px; color: #d6f5e0; }
.status-clear.just-cleared .status-sub,
.status-clear.just-cleared .status-regions.safe { color: #bfe6cb; }

/* RESTING STRIP. Calm is 99% of the app's life, and the full dome banner spent
   ~40% of the first screen saying "Відбій" three times before the feed began.
   At rest (calm past the just-cleared window, or no data yet) the same banner
   folds into one 64px strip: state glyph, title, chips, art on the right.
   Alarm, partial and the fresh all-clear keep the full banner untouched — the
   loud state is supposed to take the room. CSS only: every node renderStatus
   writes stays in the DOM, and the tap still opens #status-details. If a
   WebView cannot parse :is(), these rules drop and the full banner remains. */
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) {
  display: flex; align-items: center; min-height: 64px; text-align: left;
  padding: 10px 40px 10px 14px;
  border-color: color-mix(in srgb, var(--sc) 30%, var(--border)); }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-main {
  display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 11px;
  align-items: center; width: 100%; }
/* The eyebrow keeps only its glyph here: its word repeats the title. */
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-eyebrow {
  grid-column: 1; grid-row: 1 / span 3; }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) #status-eyebrow-text { display: none; }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-ic,
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-ic .ic { width: 22px; height: 22px; }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-title {
  grid-column: 2; font-size: 17px; margin-top: 0; line-height: 1.2; }
/* "Ваші регіони: спокійно" is what the title and the chips already say at rest. */
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-regions { display: none; }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-chips {
  grid-column: 2; justify-content: flex-start; margin-top: 3px; }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-chip { padding: 2px 7px; }
/* Stays visible: the offline/stale line is the one thing that must not fold. */
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-sub {
  grid-column: 2; margin-top: 3px; }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .status-chev {
  top: 50%; margin-top: -8px; }
/* Crop Б2 (approved 01.10): the whole dish over the horizon, darkened from the
   left so the text reads; the bottom dome glow and rim do not fit 64px. */
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .sb-art {
  background-size: 100% auto; background-position: center 58%;
  -webkit-mask-image: none; mask-image: none; }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .sb-scrim {
  background: linear-gradient(90deg, rgba(0,0,0,.82) 0%, rgba(0,0,0,.55) 38%, rgba(0,0,0,0) 62%); }
.status-banner:is(.status-clear:not(.just-cleared), .status-unknown) .sb-rim { display: none; }

/* The map already breathes on an active oblast; the banner, which is the thing
   people actually look at, did not. Motion is the third channel after shape and
   hue, and it is gated for anyone who asked for less of it. */
@media (prefers-reduced-motion: no-preference) {
  /* The alarm pulses; the all-clear settles once and stops. Relief is a single
     resolving movement, not a rhythm. */
  .status-clear.just-cleared { animation: clear-settle .9s cubic-bezier(.2, 0, 0, 1); }
  @keyframes clear-settle {
    from { opacity: .35; transform: scale(.985); }
    to { opacity: 1; transform: none; }
  }
  .status-active { animation: alarm-breathe 2.6s ease-in-out infinite; }
  @keyframes alarm-breathe {
    0%, 100% { border-top-color: var(--red); }
    50% { border-top-color: #ff9a90; }
  }
  /* The dome's rim is the calm counterpart of the alarm pulse: it breathes
     slowly in every state, so the banner reads as live rather than painted. */
  .sb-rim { animation: rim-breathe 3.6s ease-in-out infinite; }
  @keyframes rim-breathe { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
}

/* --- feed --- */
.feed-list { display: flex; flex-direction: column; gap: 10px; }
/* A standing warning block. Used on Джерела when nothing enabled covers the
   reader's own oblasts. */
.notice-warn { margin-bottom: 10px; padding: 11px 13px; border-radius: var(--r-block);
  border: 1px solid var(--red); background: color-mix(in srgb, var(--red) 12%, transparent);
  color: var(--text); font-size: 13px; line-height: 1.35; }
.notice-warn[hidden] { display: none; }

/* The admin's own switch on the feed. Quiet and unlike every user control on
   the screen ON PURPOSE: the owner finds bugs because he reads the same screen
   his readers do, and an admin view that looks like theirs is how he would stop
   noticing what they see. */
.feed-admin-toggle { align-self: flex-start; margin: 2px 0 0; padding: 7px 12px;
  border: 1px dashed var(--border-2); border-radius: var(--r-ctrl); background: none;
  color: var(--text-2); font-family: var(--mono); font-size: 10.5px;
  letter-spacing: .03em; cursor: pointer;
  transition: background-color .14s ease, border-color .14s ease; }
@media (prefers-reduced-motion: reduce) { .feed-admin-toggle { transition: none; } }
.feed-admin-toggle[hidden] { display: none; }
/* On, and still obviously not a reader control: the border stays DASHED. Going
   solid amber would have made it the twin of an active .seg-btn, which is the
   one thing this button must never look like. */
.feed-admin-toggle[aria-pressed="true"] { border-color: var(--accent);
  background: var(--accent-dim); color: var(--text); }

.feed-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-block); border-left-width: 2px;
  padding: 12px 13px; display: flex; flex-direction: column; gap: 6px; cursor: pointer; }
.feed-item .fi-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* One event retold by several channels: the newest is the card, the rest fold
   under this line. It sits tight under its card and indented, so it reads as
   belonging to the card above rather than as a feed entry of its own. */
.feed-group-more { display: block; margin: -2px 0 0 14px; padding: 8px 2px; min-height: 44px;
  background: none; border: 0; text-align: left; cursor: pointer;
  font-family: var(--mono); font-size: 11px; letter-spacing: .03em; color: var(--text-2); }
.feed-group-more:hover, .feed-group-more:focus-visible { color: var(--accent); }
.feed-group-extras { display: flex; flex-direction: column; gap: 8px; margin-left: 14px; }
.feed-group-extras[hidden] { display: none; }
/* Quieter than the head card - these are the same news in someone else's words. */
.feed-group-extras .feed-item { background: transparent; }
/* NOTE: feedItemNode() emits .fi-channel, .sev-chip, .feed-body and .feed-sub —
   it never emits .feed-title. Card-level emphasis therefore has to hang off
   .feed-body, the element that actually carries the message. */
.feed-time { color: var(--muted); font-family: var(--mono); font-size: 11px; white-space: nowrap; }
.feed-sub { color: var(--text-2); font-size: 13px; }
/* The severity chip is the most important token on a card; it was the smallest
   text in the app at 8px. */
.sev-chip { font-family: var(--sans); font-size: 11px; font-weight: 500; letter-spacing: .06em; padding: 3px 6px;
  border-radius: var(--r-ctrl); text-transform: uppercase; }
/* A RED card and an INFO note used to differ by a 2px border and a chip. RED now
   also carries a tinted surface and a red rule, so the escalation is legible
   while scrolling — not something you have to stop and read a chip to learn. */
.sev-RED { border-left-color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, var(--border));
  background: color-mix(in srgb, var(--red) 9%, var(--surface)); }
.sev-RED .sev-chip { background: var(--red); color: #0a0a0a; font-weight: 700; }
/* The message itself leads on a RED card, so the escalation is readable while
   scrolling a list rather than only after stopping to read an 11px chip. */
.sev-RED .feed-body { color: #ffd9d4; font-size: 15px; font-weight: 600; line-height: 1.35; }
.sev-ORANGE { border-left-color: var(--orange);
  background: color-mix(in srgb, var(--orange) 5%, var(--surface)); }
.sev-ORANGE .sev-chip { border: 1px solid var(--orange); color: var(--orange); }
.sev-ORANGE .feed-body { color: #ffe2c9; font-size: 14px; font-weight: 500; }
/* The ramp used to break at the third step: RED and ORANGE got a tinted card,
   YELLOW and GREEN only a border. */
.sev-YELLOW { border-left-color: var(--yellow);
  background: color-mix(in srgb, var(--yellow) 4%, var(--surface)); }
.sev-YELLOW .sev-chip { border: 1px solid var(--yellow); color: var(--yellow); }
.sev-GREEN { border-left-color: var(--green);
  background: color-mix(in srgb, var(--green) 4%, var(--surface)); }
.sev-GREEN .sev-chip { border: 1px solid var(--green); color: var(--green); }
.sev-INFO { border-left-color: var(--info); } .sev-INFO .sev-chip { border: 1px solid var(--border-3); color: var(--muted); }

/* --- feed controls (scope / level / auto-refresh) --- */
.feed-controls { display: flex; flex-direction: column; gap: 10px; }
/* One row: scope takes the space it needs to stay readable, level filter fills
   the rest. No wrap — a wrapped toolbar is what made this three rows tall. */
.feed-controls-row { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; }
.feed-controls-row #feed-scope { flex: 1 1 58%; min-width: 0; }
.feed-controls-row .select-inline { flex: 1 1 42%; min-width: 0; }
/* Sits under the row rather than in it, and only as wide as its two labels -
   a secondary choice inside "Усі джерела", not a peer of the tab switch. */
.feed-regions { display: inline-flex; width: auto; margin-top: 8px; }

/* --- Feed scope + level as underline tabs (design B) ---
   Scoped to the feed row: the pill .seg used on guest/map/filters is left as-is.
   This row sits directly under the red alarm banner, and a second filled-amber
   block competed with it for the eye - a flat, mixed-case, slightly larger tab
   strip with a thin amber underline reads as a filter, not as a second alarm.
   Behaviour is unchanged: two scope tabs and the level select, same as prod. */
#feed-scope.seg { background: none; border: 0; border-bottom: 1px solid var(--border-2);
  border-radius: 0; padding: 0; gap: 4px; }
#feed-scope .seg-btn { text-transform: none; letter-spacing: 0; font-size: 14px;
  font-weight: 500; color: var(--dim); border-radius: 0; min-height: 46px;
  padding: 11px 4px 12px; white-space: nowrap; position: relative; }
#feed-scope .seg-btn[aria-current="true"] { background: none; color: var(--text);
  font-weight: 600; box-shadow: none; }
/* The indicator is a shape, not only a hue: the amber underline is what a
   deuteranopic reader tracks when the colour alone reads as grey. */
#feed-scope .seg-btn[aria-current="true"]::after { content: ""; position: absolute;
  left: 12%; right: 12%; bottom: -1px; height: 2.5px; border-radius: 2px; background: var(--accent); }
#feed-scope .seg-btn[aria-current="true"]:active { background: none; color: var(--text); }
#feed-scope .seg-btn:active { background: none; color: var(--text-2); }

/* The level select drops its box and borrows the same underline, so the row is
   one family rather than a tab strip parked beside a dropdown. Native arrow is
   kept - it is the platform's own affordance for "this opens a list". */
.feed-controls-row .select-inline select { background: none; border: 0;
  border-bottom: 1px solid var(--border-2); border-radius: 0; color: var(--text-2);
  font-size: 13px; min-height: 46px; padding: 11px 18px 11px 2px; }

/* --- rich feed item --- */
.fi-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.fi-source { display: flex; align-items: center; gap: 8px; min-width: 0; }
.fi-more { flex: none; width: 26px; height: 26px; margin: -4px -6px -4px 0; border: 0; background: transparent;
  color: var(--muted); font-size: 18px; line-height: 1; border-radius: 8px; cursor: pointer;
  display: grid; place-items: center; transition: background .12s, color .12s; }
.fi-more:hover { color: var(--text-2); }
.fi-more:active { background: rgba(255,255,255,.08); }
.fi-avatar { position: relative; width: 24px; height: 24px; border-radius: 50%; border: 1px solid var(--border-2);
  flex: none; display: grid; place-items: center; font-size: 11px; font-weight: 500; color: var(--amber); background: var(--raised); overflow: hidden; }
.fi-avatar-mono { font-family: var(--mono); }
.fi-avatar-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity .15s; }
.fi-avatar-img.loaded { opacity: 1; }
.fi-channel { font-weight: 500; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.fi-meta { display: flex; align-items: center; gap: 8px; flex: none; }
.fi-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.fi-tags[hidden] { display: none; }
.tag-chip { display: inline-flex; align-items: center; gap: 5px; font-family: var(--sans); font-size: 11px;
  letter-spacing: .04em; font-weight: 500; padding: 2px 6px;
  border-radius: var(--r-ctrl); border: 1px solid var(--border-2); color: var(--text-2); }
.tag-chip .ic { width: 12px; height: 12px; }
.tag-chip.upd { color: var(--amber); border-color: color-mix(in srgb, var(--amber) 45%, var(--border)); }
.tag-chip.weapon { color: var(--info); border-color: color-mix(in srgb, var(--info) 40%, var(--border)); }
.tag-chip.filtered { color: var(--muted); }
/* "your region" match — subtle accent chip, never a big white block */
.tag-chip.matched { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
  background: color-mix(in srgb, var(--accent) 10%, transparent); }
.weapon-icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; flex: none; color: var(--info); }
.weapon-icon:empty { display: none; }
.weapon-img { width: 18px; height: 18px; object-fit: contain; display: block; }
.weapon-icon .weapon-icon-sprite { width: 16px; height: 16px; }
.feed-body { color: var(--text); font-size: 13px; line-height: 1.45; white-space: pre-line; }
.feed-body[hidden] { display: none; }
.fi-expand { display: flex; align-items: center; gap: 6px; color: var(--muted); font-family: var(--sans); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; margin-top: 2px; }
.fi-chev { transition: transform .15s; display: inline-block; }
.feed-item.open .fi-chev { transform: rotate(180deg); }
.fi-details { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; padding-top: 10px; border-top: 1px dashed var(--border-2); }
.fi-details[hidden] { display: none; }
.fi-drow { display: flex; gap: 10px; font-size: 12px; }
.fi-dlabel { color: var(--muted); flex: none; width: 84px; font-family: var(--sans); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
.fi-dval { color: var(--text-2); }
.fi-link { color: var(--info); text-decoration: none; }
.fi-link:hover { text-decoration: underline; }

/* --- map / area list --- */
.area-list { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); }
.area-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 13px; background: var(--surface); border: 0; border-radius: var(--r-block); }
.area-state { font-family: var(--mono); font-size: 11px; font-weight: 500; white-space: nowrap; }

/* --- interactive oblast map (kept neon glow, re-mapped palette) --- */
.map-wrap { display: flex; flex-direction: column; gap: 10px; }
.map-svg { background: #000;
  background-image: repeating-linear-gradient(0deg, transparent 0 27px, var(--grid) 27px 28px),
    repeating-linear-gradient(90deg, transparent 0 27px, var(--grid) 27px 28px);
  border: 1px solid var(--border); border-radius: var(--r-block); padding: 8px; overflow: hidden;
  touch-action: none; }   /* pinch/drag drive the map, not the page */
.ua-map { width: 100%; height: auto; display: block; transform-origin: 0 0; will-change: transform; }

/* Leaflet basemap (neptun-style): real map, zoom/pan, threat icons. */
.map-leaflet { height: 62vh; min-height: 320px; border: 1px solid var(--border); border-radius: var(--r-block); overflow: hidden; }
.map-leaflet[hidden] { display: none; }
/* MapLibre GL "Цілі" map (#map-maplibre reuses .map-leaflet sizing) */
.maplibregl-canvas { outline: none; }
.maplibregl-ctrl-attrib.maplibregl-compact { background: rgba(0,0,0,.45); border-radius: 8px; }
.maplibregl-ctrl-attrib a, .maplibregl-ctrl-attrib { color: var(--muted); font-size: 10px; }
.maplibregl-ctrl-attrib-button { filter: invert(0.85); }
/* Themed, minimal map controls (zoom / compass / satellite) to match the app. */
.maplibregl-ctrl-group { background: var(--surface); border: 1px solid var(--border-2);
  border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,.4); overflow: hidden; }
.maplibregl-ctrl-group button { width: 34px; height: 34px; background: transparent; }
.maplibregl-ctrl-group button + button { border-top: 1px solid var(--border); }
.maplibregl-ctrl-group button:hover { background: var(--raised); }
.maplibregl-ctrl-group button:focus { box-shadow: none; }
.maplibregl-ctrl button:disabled .maplibregl-ctrl-icon { opacity: .35; }
/* MapLibre's control glyphs are dark SVGs — invert them to read on a dark button. */
.maplibregl-ctrl-group button .maplibregl-ctrl-icon { filter: invert(0.85); }
#ml-basemap-btn { color: var(--text); line-height: 34px; padding: 0; }
.maplibregl-popup-content { background: var(--surface); color: var(--text); border: 1px solid var(--border-2);
  border-radius: 12px; padding: 10px 12px; font-family: var(--sans); box-shadow: 0 8px 26px rgba(0,0,0,.5); }
.maplibregl-popup-anchor-top .maplibregl-popup-tip { border-bottom-color: var(--surface); }
.maplibregl-popup-anchor-bottom .maplibregl-popup-tip { border-top-color: var(--surface); }
.maplibregl-popup-close-button { color: var(--muted); font-size: 18px; }
.np { min-width: 188px; max-width: 250px; }
.np-h { display: flex; align-items: center; gap: 10px; }
.np-htext { min-width: 0; }
.np-ic { width: 36px; height: 36px; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.np-ic svg { width: 21px; height: 21px; }
.np-t { font-size: 15px; font-weight: 700; }
.np-loc { font-size: 12px; color: var(--muted); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.np-expla { font-size: 12.5px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); color: var(--text); }
.np-warn { margin-top: 7px; font-size: 12px; color: #ffb27a; }
.np-conf { display: inline-block; margin-top: 8px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 600;
  background: rgba(125,133,147,.18); color: var(--muted); }
.np-conf.conf-high { background: rgba(92,177,118,.18); color: #8fd6a6; }
.np-conf.conf-medium { background: rgba(255,138,60,.18); color: #ffb27a; }
.np-meta { font-size: 12px; color: var(--muted); margin-top: 7px; }
.np-foot { font-size: 11.5px; color: var(--muted); margin-top: 7px; }
/* HTML target markers on the MapLibre map */
.ml-th { width: 28px; height: 28px; cursor: pointer; }
.ml-th img, .ml-th svg { display: block; width: 28px; height: 28px; filter: drop-shadow(0 1px 2px rgba(0,0,0,.75)); }
.leaflet-container { background: #05070a; font-family: var(--sans); touch-action: none; }   /* pinch/drag drive the map, not the page */
.leaflet-control-zoom a { background: var(--surface); color: var(--text); border-color: var(--border-2); }
.leaflet-control-zoom a:hover { background: var(--raised); }
.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--surface); color: var(--text); border: 1px solid var(--border-2); }
.leaflet-popup-content { margin: 8px 10px; font-size: 13px; font-family: var(--sans); }
.leaflet-popup-content b { color: color-mix(in srgb, var(--amber) 75%, var(--text)); }
.leaflet-bar a { border-radius: 6px; }
.threat-wrap { background: none; border: 0; }
/* --- neptun target markers (Leaflet divIcon) --- */
.threat-div { position: relative; width: 32px; height: 32px; }
.threat-div svg { position: relative; display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,.7)); }
.threat-div .thm-arrow { stroke: currentColor; stroke-width: 1.6; opacity: .9; }
.threat-div .thm-halo {
  position: absolute; left: 50%; top: 50%; width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border-radius: 50%; background: radial-gradient(circle, currentColor 0%, transparent 70%);
  opacity: 0; pointer-events: none;
}
.threat-div.confirmed .thm-halo { opacity: .45; animation: thpulse 1.8s ease-out infinite; }
.threat-div.uncertain svg { opacity: .68; }
.threat-div.uncertain path[fill="currentColor"] { stroke-dasharray: 1.6 1.4; }
/* advisory = observation (e.g. MiG-31K takeoff over a calm country): calm, dimmed,
   never pulsing — it must not read as a "take cover" signal. */
.threat-div.advisory { opacity: .78; }
.threat-div.advisory .thm-halo { display: none; }
.threat-div.area-only .thm-badge { display: none; }
.threat-div .thm-badge {
  position: absolute; top: -3px; right: -5px; min-width: 15px; height: 15px; padding: 0 3px;
  border-radius: 8px; background: #12151b; border: 1px solid currentColor; color: #fff;
  font: 700 9px/15px var(--sans); text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,.6);
}
@keyframes thpulse {
  0%   { transform: scale(.6); opacity: .5; }
  70%  { transform: scale(1.5); opacity: 0; }
  100% { transform: scale(1.5); opacity: 0; }
}
/* Ukrainian oblast labels drawn on the Leaflet map (permanent tooltips) */
.leaflet-tooltip.oblast-label {
  background: none; border: 0; box-shadow: none; padding: 0; color: #cdd3db;
  font: 600 10px/1.1 var(--sans); letter-spacing: .2px; white-space: nowrap;
  text-shadow: 0 0 3px #05070a, 0 0 3px #05070a, 0 1px 2px #000;
}
.leaflet-tooltip.oblast-label::before { display: none; }
.leaflet-tooltip.oblast-label.st-active  { color: #ff8f86; }
.leaflet-tooltip.oblast-label.st-partial { color: #ffbd8a; }
.leaflet-tooltip.oblast-label.st-clear   { color: #9fd9b1; }
/* Richer target popup */
.thp-t { display: block; font-size: 14px; font-weight: 700; }
.thp-place { margin-top: 2px; font-size: 12.5px; color: var(--text); }
.thp-meta { margin-top: 2px; font-size: 12px; color: var(--muted); }
.thp-meta.thp-dim { opacity: .8; }
.thp-note { margin-top: 5px; padding-top: 5px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text); }
/* Target-type legend swatches */
.threat-legend .tleg-img { width: 16px; height: 16px; object-fit: contain; margin-right: 5px; vertical-align: -3px; display: inline-block; }
.threat-legend .tleg-ic { display: inline-block; width: 12px; height: 12px; margin-right: 5px; vertical-align: -1px;
  -webkit-mask: center/contain no-repeat; mask: center/contain no-repeat; }
.threat-legend .tleg-ic.k-uav     { background: #f0820e; -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-10 L6.5,6.5 L0,3 L-6.5,6.5 Z"/></svg>'); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-10 L6.5,6.5 L0,3 L-6.5,6.5 Z"/></svg>'); }
.threat-legend .tleg-ic.k-mig31k  { background: #7c4dff; -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-11 L1.5,-1 L9,3 L9,4.6 L1.5,2.6 L1.1,7 L3,9 L3,10.4 L0,9.4 L-3,10.4 L-3,9 L-1.1,7 L-1.5,2.6 L-9,4.6 L-9,3 L-1.5,-1 Z"/></svg>'); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-11 L1.5,-1 L9,3 L9,4.6 L1.5,2.6 L1.1,7 L3,9 L3,10.4 L0,9.4 L-3,10.4 L-3,9 L-1.1,7 L-1.5,2.6 L-9,4.6 L-9,3 L-1.5,-1 Z"/></svg>'); }
.threat-legend .tleg-ic.k-missile { background: #e2384f; -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-11 L2,-5 L2,4 L3.6,8 L1,6.6 L0,8.2 L-1,6.6 L-3.6,8 L-2,4 L-2,-5 Z"/></svg>'); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-11 L2,-5 L2,4 L3.6,8 L1,6.6 L0,8.2 L-1,6.6 L-3.6,8 L-2,4 L-2,-5 Z"/></svg>'); }
.threat-legend .tleg-ic.k-ballist { background: #b21e6b; -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-11.5 L2.3,-3 L2.3,7 L1,5.4 L0,7.5 L-1,5.4 L-2.3,7 L-2.3,-3 Z"/></svg>'); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-11.5 L2.3,-3 L2.3,7 L1,5.4 L0,7.5 L-1,5.4 L-2.3,7 L-2.3,-3 Z"/></svg>'); }
.threat-legend .tleg-ic.k-kab     { background: #d9531e; -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-7 C5,-7 5,4.5 0,8.5 C-5,4.5 -5,-7 0,-7 Z"/></svg>'); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 32 32"><path d="M0,-7 C5,-7 5,4.5 0,8.5 C-5,4.5 -5,-7 0,-7 Z"/></svg>'); }
.threat-legend .tleg-pulse { display: inline-block; width: 10px; height: 10px; margin-right: 5px; vertical-align: -1px;
  border-radius: 50%; background: #ff6b3c; box-shadow: 0 0 0 3px rgba(255,107,60,.28); }
.basemap-toggle { width: 34px; height: 34px; line-height: 34px; text-align: center; font-size: 17px;
  background: var(--surface); color: var(--text); border-radius: 6px; text-decoration: none; cursor: pointer; }
.basemap-toggle:hover { background: var(--raised); }
/* On satellite the base is busy — thicken oblast borders and darken label halos. */
.leaflet-container.sat-mode .oblast-label { color: #fff; text-shadow: 0 0 3px #000, 0 0 4px #000, 0 1px 2px #000; }
.leaflet-container.sat-mode .oblast-label.st-active  { color: #ffd0cb; }
.leaflet-container.sat-mode .oblast-label.st-clear   { color: #cfefd8; }
.leaflet-control-attribution { font-size: 9px; background: rgba(0,0,0,.5); color: var(--muted); }
.leaflet-control-attribution a { color: var(--muted); }
.ua-map path { stroke-width: 1.1; stroke-linejoin: round; cursor: pointer; outline: none; transition: filter .3s; }
.ua-map path.clear { fill: color-mix(in srgb, var(--map-clear) 14%, transparent); stroke: color-mix(in srgb, var(--map-clear) 60%, var(--border)); }
.ua-map path.partial { fill: color-mix(in srgb, var(--map-partial) 22%, transparent); stroke: var(--map-partial); }
.ua-map path.active { fill: color-mix(in srgb, var(--map-active) 30%, transparent); stroke: var(--map-active); filter: url(#glowNeon); animation: pulseNeon 2.2s ease-in-out infinite; }
.ua-map path.unknown { fill: color-mix(in srgb, var(--map-unknown) 10%, transparent); stroke: var(--border-2); }
.ua-map path:focus-visible { stroke: var(--text); stroke-width: 2.5; }
.ua-map path.sel { stroke: var(--amber); stroke-width: 2.5; }
/* Territorial-community overlay (SVG fallback): only active+selected+raion
   громади are ever drawn (see commFeaturesToShow), so a "clear" one is simply
   never a path - no separate unstyled/clutter state is needed here. */
.ua-map path.comm-area { stroke-width: 1.3; }
.ua-map path.comm-active { fill: color-mix(in srgb, var(--map-active) 32%, transparent); stroke: var(--map-active); }
/* Declared-raion fill: same rule as comm-active, at --map-active 26% - a
   quieter "your whole raion is under alarm" read, never drawn over a genuine
   comm-active polygon (see renderMap's raionCommunitySet/activeCommunitySet check). */
.ua-map path.comm-raion { fill: color-mix(in srgb, var(--map-active) 26%, transparent); stroke: rgba(255,120,105,0.55); }
/* A partial oblast whose declared raions are drawn quiets down, so the
   raion shading is what the eye lands on - and never below the calm
   fill, or a partial oblast would read calmer than a calm one. */
.ua-map.has-raion path.partial { fill: color-mix(in srgb, var(--map-partial) 10%, transparent); }
.ua-map path.comm-selected { fill: none; stroke: var(--amber); stroke-width: 2; }
@keyframes pulseNeon { 0%, 100% { opacity: .82; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .ua-map path.active { animation: none; } }

/* Targets layer: the oblast outline is a dim backdrop, the enemy tracks sit on
   top as coloured dots with a small heading tick. */
.ua-map.targets path { fill: color-mix(in srgb, var(--info) 6%, transparent); stroke: var(--border-2);
  animation: none; filter: none; cursor: default; }
.threat { cursor: pointer; }
/* stroke-widths are in projection units and get multiplied by the marker's
   scale(4), so they read as ~1-2px on screen. */
.threat .thd { fill: var(--info); stroke: #0a0a0a; stroke-width: .5; }
.threat .thh { stroke: var(--info); stroke-width: 1.1; stroke-linecap: round; stroke-linejoin: round; opacity: .9; }
.threat.uav .thd { fill: var(--yellow); } .threat.uav .thh { stroke: var(--yellow); }
.threat.kab .thd, .threat.aviation_cab .thd { fill: var(--orange); }
.threat.kab .thh, .threat.aviation_cab .thh { stroke: var(--orange); }
.threat.cruise_missile .thd, .threat.missile .thd, .threat.ballistics .thd, .threat.aeroballistic .thd { fill: var(--red); }
.threat.cruise_missile .thh, .threat.missile .thh, .threat.ballistics .thh, .threat.aeroballistic .thh { stroke: var(--red); }
.threat.conf-high .thd { stroke: #fff; stroke-width: .9; }
.threat.sel .thd { stroke: var(--amber); stroke-width: 1.4; }
.map-modes { margin: 8px 0 4px; }

/* collapsible area list */
.list-toggle { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-block);
  color: var(--text-2); font: inherit; font-family: var(--mono); font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
  padding: 11px 13px; min-height: 40px; cursor: pointer; }
.list-toggle .fi-chev { transition: transform .15s; }
.list-toggle.open .fi-chev { transform: rotate(180deg); }
.map-caption { min-height: 20px; color: var(--text-2); margin: 0; display: flex; flex-direction: column; gap: 3px; font-family: var(--mono); font-size: 11px; }
.map-caption .cap-title { color: var(--text); font-family: var(--sans); font-weight: 600; }
.map-caption .cap-sub { line-height: 1.35; }
.map-caption .cap-cause, .map-caption .cap-threat { color: var(--amber); font-weight: 600; }
.area-row { width: 100%; text-align: left; cursor: pointer; color: var(--text); font: inherit; }
.area-name { font-weight: 500; }
@media (prefers-reduced-motion: reduce) {
  .ua-map path { transition: none; }
}

/* --- collapsible filter cards --- */
.card.collapse { padding: 0; gap: 0; }
.card-head { display: flex; align-items: center; justify-content: space-between; width: 100%;
  background: none; border: 0; color: var(--text); font: inherit; font-weight: 500; font-size: 13px;
  padding: 13px 16px; min-height: 46px; cursor: pointer; }
.card-head .fi-chev { color: var(--muted); transition: transform .15s; }
.card-head.open .fi-chev { transform: rotate(180deg); }
.card-body { display: flex; flex-direction: column; gap: 12px; padding: 0 16px 16px; }
.card-body[hidden] { display: none; }

/* --- compact channel list --- */
.ch-list { display: flex; flex-direction: column; }
/* Group headers when the picker has a channel_catalog to sort by (own-oblast
   sources, then recommended countrywide summaries, then everything else).
   Same look as .city-group-label, but with breathing room above each group
   since these headers sit between rows, not above a fresh chip grid. */
.ch-group-title { font-family: var(--mono); font-size: 11px; letter-spacing: .04em;
  color: var(--text-2); margin: 14px 0 6px; }
.ch-group-title:first-child { margin-top: 0; }
.ch-row { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 9px 0; border-top: 1px solid var(--border); }
.ch-row:first-child { border-top: 0; }
.ch-name { font-family: var(--sans); font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 14px, not 8px: "приглушити для мене" sits next to "видалити для всіх", and a
   thumb-width of separation between a personal toggle and a destructive shared
   action is not enough. */
.ch-actions { display: flex; align-items: center; gap: 14px; flex: none; }
.ch-mute { background: none; border: 1px solid var(--border-2); border-radius: var(--r-ctrl);
  color: var(--muted); font-family: var(--mono); font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  padding: 4px 10px; min-height: 40px; cursor: pointer; }
.ch-mute:hover { border-color: var(--border-3); color: var(--text-2); }
.ch-mute[aria-pressed="true"] { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, var(--border)); }
.ch-del { background: none; border: 1px solid var(--border); color: var(--muted); border-radius: var(--r-ctrl);
  font-size: 12px; width: 26px; height: 26px; cursor: pointer; }
.ch-del:hover { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, var(--border)); }

/* --- admin threat editor --- */
#threat-reset { align-self: flex-start; margin-bottom: 4px; }
.threat-block { border: 1px solid var(--border); border-radius: var(--r-block); margin-top: 8px; overflow: hidden; }
.threat-head { display: flex; align-items: center; gap: 8px; width: 100%; background: var(--surface);
  border: 0; color: var(--text); font: inherit; padding: 11px 13px; min-height: 42px; cursor: pointer; }
.threat-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.threat-name { font-weight: 500; font-size: 13px; }
.threat-key { margin-left: 2px; font-family: var(--mono); }
.threat-head .fi-chev { margin-left: auto; color: var(--muted); transition: transform .15s; }
.threat-head.open .fi-chev { transform: rotate(180deg); }
.threat-body { display: flex; flex-direction: column; gap: 10px; padding: 12px 13px; border-top: 1px solid var(--border); }
.threat-body[hidden] { display: none; }
.kw-chip { display: inline-flex; align-items: center; gap: 6px; padding-right: 4px; }
.kw-text { font-family: var(--mono); }
.kw-x { background: none; border: 0; color: var(--muted); font-size: 14px; line-height: 1; cursor: pointer; padding: 0 2px; }
.kw-x:hover { color: var(--red); }
.kw-add { display: flex; gap: 8px; }
.kw-add input { flex: 1; background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-ctrl);
  color: var(--text); padding: 10px 11px; font-size: 13px; min-height: 38px; font-family: var(--sans); }

/* --- cabinet controls --- */
/* The whole row is the label, so the whole row should be the target — the switch
   itself is 38×21, and one of these rows is the master notifications toggle. */
.toggle-row, .select-row { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; min-height: 44px; }
.select-row select { min-width: 150px; }
/* square engineering toggle: bordered track, square knob, ON = orange */
.toggle-row input[type=checkbox] {
  width: 38px; height: 21px; -webkit-appearance: none; appearance: none;
  background: var(--raised); border: 1px solid var(--border-2); border-radius: var(--r-ctrl); position: relative; cursor: pointer;
}
.toggle-row input[type=checkbox]::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
  background: var(--muted); transition: transform .15s, background .15s;
}
.toggle-row input[type=checkbox]:checked { border-color: var(--amber); }
.toggle-row input[type=checkbox]:checked::after { transform: translateX(17px); background: var(--amber); }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip { background: none; border: 1px solid var(--border-2); border-radius: var(--r-ctrl); padding: 3px 7px;
  font-family: var(--sans); font-size: 11px; letter-spacing: .03em; color: var(--text-2); }

/* selectable pickers (regions / cities / muted channels) */
.chip-select { min-height: 40px; padding: 5px 9px; font-family: var(--sans); font-size: 11px; font: inherit; cursor: pointer; border-color: var(--border-2); }
.chip-select[aria-pressed="true"] { border-color: color-mix(in srgb, var(--amber) 55%, var(--border)); color: var(--amber); background: none; }
.chip-select:not([aria-pressed="true"]) { color: var(--muted); }
.city-groups { display: flex; flex-direction: column; gap: 14px; }
.city-group-label { font-family: var(--mono); font-size: 11px; letter-spacing: .04em; color: var(--text-2); margin-bottom: 8px; }
.select-row input[type="time"] { background: var(--raised); border: 1px solid var(--border);
  border-radius: var(--r-ctrl); color: var(--text); padding: 10px 12px; font-family: var(--mono); font-size: 14px; min-height: 42px; }
.select-row input[type="time"]:focus { border-color: var(--amber); }
.select-row input[type="time"]:focus:not(:focus-visible) { outline: none; }

/* --- circles --- */
.circle-row { width: 100%; text-align: left; cursor: pointer; color: var(--text); font: inherit; }
.circle-row-main { display: flex; flex-direction: column; gap: 2px; }
.circle-row-name { font-weight: 500; font-size: 13px; }
.circle-row-count { font-family: var(--mono); }

/* --- chat list (friends + groups, Telegram-style) --- */
.head-actions { display: flex; gap: 8px; }
/* A little breathing room between the "ЧАТИ" header and the first row. */
#circles-list-pane .app-head { margin-bottom: 6px; }
/* Local-only search box, reusing the Джерела search's own look (.src-search)
   so the two filter fields in the app read as one pattern. */
.chat-search { margin-bottom: 10px; }
.chat-filters { display: flex; gap: 8px; margin: 0 0 8px; overflow-x: auto; scrollbar-width: none; }
.chat-filters::-webkit-scrollbar { display: none; }
.chat-filter-chip { flex: none; padding: 6px 12px; min-height: 36px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--border-2); background: transparent; color: var(--text-2);
  font-family: var(--sans); font-size: 13px; font-weight: 500; display: inline-flex; align-items: center; gap: 6px; }
.chat-filter-chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent);
  color: var(--on-accent); font-weight: 600; }
.chat-filter-count { font-family: var(--mono); font-size: 11px; opacity: .85; }
/* break out of the view's 20px side padding so rows run edge-to-edge */
.chat-list, .chat-new-panel { margin-left: -20px; margin-right: -20px; }
.chat-list { display: flex; flex-direction: column; }
.chat-list-row { position: relative; display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; cursor: pointer;
  color: var(--text); font: inherit; background: var(--surface); border: 0;
  /* 20px matches the "ЧАТИ" header's inset, so the avatar column lines up with
     it instead of sitting 4px to its left. */
  padding: 10px 20px; min-height: 60px; }
/* Divider is inset past the avatar and off both edges (Telegram-style), so the
   rows read as a rhythm rather than one heavy edge-to-edge block. */
.chat-list-row:not(:last-child)::after { content: ""; position: absolute; left: 80px; right: 20px;
  bottom: 0; height: 1px; background: var(--border); }
.chat-list-row:hover, .chat-list-row:active { background: var(--raised); }
.cl-avatar { width: 48px; height: 48px; flex: none; border-radius: 50%; display: grid; place-items: center; border: 1px solid var(--border-2);
  background: var(--raised); color: var(--accent); font-family: var(--mono); font-weight: 500; font-size: 18px; overflow: hidden; }
/* a chosen avatar tile fills its square container in every size */
.av-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cl-avatar.group { color: var(--text-2); }
.cl-avatar svg { width: 22px; height: 22px; }
/* A group emblem fills the whole circle; the glyph fallback keeps its padding. */
.cl-avatar.has-img { background: #12141a; }
.cl-avatar.has-img img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* --- group emblem picker (owner only, on the group detail screen) --- */
.gav-picker { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.gav-opt { width: 100%; aspect-ratio: 1; border-radius: 50%; overflow: hidden; padding: 0;
  background: #12141a; border: 2px solid var(--border-2); cursor: pointer;
  display: grid; place-items: center; color: var(--text-2); }
.gav-opt img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gav-opt svg { width: 22px; height: 22px; }
.gav-opt[aria-pressed="true"] { border-color: var(--accent); }

.cl-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.cl-name { font-weight: 500; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cl-sub { font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-list-row .fi-chev { color: var(--muted); flex: none; font-size: 18px; }
/* pinned "Спільнота" hero row - accented so it reads as a place, not a group */
.chat-list-row.hero { background: rgba(255,106,43,0.10); border-left: 2px solid var(--accent); }
.chat-list-row.hero:hover, .chat-list-row.hero:active { background: rgba(255,106,43,0.16); }
.chat-list-row.hero .cl-name { font-weight: 600; }
.chat-list-row.hero .hero-ava { border-radius: 10px; background: rgba(255,106,43,0.14);
  border: 1px solid rgba(255,106,43,0.35); color: var(--accent); }
/* friend status line in a chat-list row */
.cl-state { display: flex; align-items: center; gap: 6px; font-family: var(--sans); }
.cl-state-dot { width: 7px; height: 7px; flex: none; border-radius: 50%; background: currentColor; }
.cl-state.state-sheltering { color: var(--red); }
.cl-state.state-home { color: var(--green); }
.cl-state.state-work { color: var(--info); }

/* pending friend-requests strip */
.chat-requests-strip { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; cursor: pointer;
  color: var(--text); font: inherit; padding: 12px 14px; min-height: 58px; margin: 4px 0 10px;
  background: color-mix(in srgb, var(--accent) 9%, var(--surface)); border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border)); }
.chat-requests-strip .cl-sub { font-family: var(--sans); }
.chat-requests-strip:hover { background: color-mix(in srgb, var(--accent) 14%, var(--surface)); }
.chat-req-badge { flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
  background: var(--accent); color: var(--on-accent); font-family: var(--mono); font-weight: 700; font-size: 15px; }
.chat-requests-strip .cl-name { color: var(--accent); }
.chat-requests-strip .fi-chev { color: var(--accent); flex: none; font-size: 18px; }

/* collapsible new-group / join panel */
.chat-new-panel { display: flex; flex-direction: column; gap: 10px; padding: 16px;
  background: var(--surface); border-bottom: 1px solid var(--border); }
.member-list { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); }
.member-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 12px; background: var(--surface); }
.member-main { display: flex; flex-direction: column; gap: 2px; margin-right: auto; }
.member-remove { flex: none; min-height: auto; padding: 4px 9px; font-size: 13px; line-height: 1; }
#circle-add-member { margin-top: 8px; width: 100%; }
.member-name { font-weight: 500; font-size: 13px; }
.member-role { font-family: var(--mono); }
.state-tag { font-family: var(--sans); font-size: 11px; letter-spacing: .04em; font-weight: 500; padding: 2px 7px;
  border-radius: var(--r-block); white-space: nowrap; border: 1px solid var(--border-2); color: var(--text-2); background: none; }
.state-tag.state-sheltering { color: var(--red); border-color: color-mix(in srgb, var(--red) 50%, var(--border)); }
.state-tag.state-home { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
.state-tag.state-work { color: var(--info); border-color: color-mix(in srgb, var(--info) 45%, var(--border)); }

/* invite code + destructive actions */
.invite-code { margin-top: 12px; font-family: var(--mono);
  font-size: 15px; letter-spacing: .06em; text-align: center; word-break: break-all; background: var(--raised); color: var(--amber); border: 1px solid var(--border-2);
  border: 0; border-radius: var(--r-block); padding: 10px 12px; }
#circle-invite-out .btn-ghost { margin-top: 8px; }
.btn-ghost.danger { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, var(--border)); width: 100%; }

/* --- circle detail: one consistent stack, full-width actions --- */
#circle-detail-pane { display: flex; flex-direction: column; gap: 12px; }
#circle-detail-pane > .card, #circle-detail-pane > .app-head { margin: 0; }
/* The primary "Чат кола" action reads as the main thing to do here. */
#circle-open-chat { width: 100%; display: flex; align-items: center; justify-content: center;
  gap: 8px; padding: 10px 16px; font-size: 13px; min-height: 42px; border-radius: 10px; }
/* Trim the detail screen's action buttons a notch (scoped; globals untouched). */
#circle-detail-pane .btn-ghost, #circle-detail-pane .btn-ghost.danger { min-height: 38px; padding: 8px 12px; }
/* The member-row remove ✕ is a compact icon, NOT a full-width danger button:
   beat .btn-ghost.danger{width:100%} and the pane min-height/padding above. */
#circle-detail-pane .btn-ghost.danger.member-remove { width: auto; min-height: auto; padding: 4px 9px; line-height: 1; }
/* Section label separating personal DMs from the general/group chats in the list. */
.chat-section { padding: 14px 12px 6px; font-family: var(--mono); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }

/* --- chat-list row, concept A: one merged list (friends + groups), a preview
   line instead of a state/member subtitle, built by chatListRowNode. --- */
.chat-list-row.cl2-row { min-height: 78px; padding: 12px 20px; gap: 14px; }
.cl2-ava-friend, .cl2-ava-group { width: 54px; height: 54px; position: relative; font-size: 19px; }
/* The presence dot sits ON the avatar's edge, so the friend avatar cannot clip:
   the round crop moves onto the image itself. */
.cl2-ava-friend { overflow: visible; }
.cl2-ava-friend .av-img { border-radius: 50%; }
.cl2-ava-group { border-radius: 16px; }
/* Safety-state ring: an inset ring in the row's own background colour keeps the
   coloured ring from touching the avatar image, the way a badge outline does. */
.cl2-ava-friend.state-sheltering { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--state-sheltering); }
.cl2-ava-friend.state-home { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--state-home); }
.cl2-ava-friend.state-work { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--state-work); }
.cl2-presence { position: absolute; right: -3px; bottom: -3px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--state-home); border: 3px solid var(--surface); }
.cl2-main { gap: 4px; }
.cl2-row1 { display: flex; align-items: baseline; gap: 6px; }
.cl2-name { flex: 1; min-width: 0; font-size: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cl2-group-ic { flex: none; display: inline-flex; color: var(--text-2); }
.cl2-group-ic .ic { width: 13px; height: 13px; }
.cl2-time { flex: none; font-family: var(--mono); font-size: 12px; color: var(--muted); }
.cl2-time.unread { color: var(--accent-text); }
.cl2-row2 { display: flex; align-items: center; gap: 8px; }
.cl2-preview { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: 14px; color: var(--text-2); }
.cl2-preview.unread { color: var(--text); }
.cl2-prefix { color: var(--text); font-weight: 500; }
.cl2-tick { display: inline-flex; flex: none; color: var(--muted); vertical-align: middle; margin-right: 3px; }
.cl2-tick svg { display: block; width: 13px; height: 9px; }
.cl2-tick.read { color: var(--accent-text); }
.cl2-badge { flex: none; }
/* Owner rename + its save button share the card's full width. */
#circle-owner-tools { display: flex; flex-direction: column; gap: 10px; }
#circle-owner-tools #circle-rename-btn { width: 100%; justify-content: center; }
#circle-invite-card #circle-invite-btn { white-space: nowrap; }
#circle-invite-card .row { align-items: center; }

/* --- personal state picker --- */
.state-picker { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.state-btn { background: var(--raised); border: 1px solid var(--border-2); border-radius: var(--r-ctrl);
  color: var(--text-2); font-family: var(--sans); font-size: 11px; letter-spacing: .03em; padding: 10px; min-height: 40px; cursor: pointer; }
.state-btn:hover { border-color: var(--border-3); color: var(--text); }
.state-btn[aria-current="true"] { border-color: var(--amber); color: var(--amber);
  background: color-mix(in srgb, var(--amber) 12%, var(--raised)); }
.state-btn.state-clear { grid-column: 1 / -1; }

/* --- circle chat --- */
.chat-pane { position: relative; display: flex; flex-direction: column; flex: 1; min-height: 0; gap: 10px; }

/* --- chat wallpaper ---
   A per-device choice (localStorage), never sent to the server: it is a taste
   setting, not account data. The image is DECORATION - it carries nothing, so a
   missing file just leaves the plain background.
   Attached to the pane's own ::before rather than to `background` so the bubbles,
   header and composer keep painting above it, and so the art does not scroll
   with the message log. Every wallpaper is near-black and low-detail on purpose:
   this sits under running text, and contrast here costs readability. */
.chat-pane[data-bg]::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-size: cover; background-position: center; background-repeat: no-repeat;
}
.chat-pane[data-bg] > * { position: relative; z-index: 1; }
/* 2K finals (27.09). A saved id from the old set (facets, ember, ...) no longer
   matches CHAT_BGS, so chatBg() falls back to no wallpaper - never a dead rule.
   The photographic scenes keep their subject at the edges and the bottom; the
   two drawn ones are dithered, which is why сигнал ships at q97 and орбіта
   lossless - ordinary lossy webp flattens the dither back into bands. */
.chat-pane[data-bg="antena"]::before { background-image: url("../img/chatbg/antena.webp"); background-position: center bottom; }
.chat-pane[data-bg="mayaky"]::before { background-image: url("../img/chatbg/mayaky.webp"); background-position: center bottom; }
.chat-pane[data-bg="promin"]::before { background-image: url("../img/chatbg/promin.webp"); background-position: left bottom; }
.chat-pane[data-bg="misto"]::before  { background-image: url("../img/chatbg/misto.webp"); background-position: center bottom; }
.chat-pane[data-bg="syhnal"]::before { background-image: url("../img/chatbg/syhnal.webp"); }
.chat-pane[data-bg="orbita"]::before { background-image: url("../img/chatbg/orbita.webp"); }

/* Wallpaper picker (Профіль → Оформлення чату) */
.bg-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 0 16px 16px; }
.bg-opt { padding: 0; border: 2px solid var(--border-2); border-radius: 10px; overflow: hidden;
  background: var(--raised); cursor: pointer; aspect-ratio: 9 / 16; position: relative;
  display: grid; place-items: end center; }
.bg-opt img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.bg-opt span { position: relative; z-index: 1; width: 100%; padding: 5px 4px;
  font-family: var(--sans); font-size: 10px; color: var(--text);
  background: linear-gradient(transparent, rgba(0,0,0,.85)); }
.bg-opt[aria-pressed="true"] { border-color: var(--accent); }
.bg-opt.none { background: var(--base); }
/* The chat is a full-screen overlay, not a pane inside the scrolling tab view.
   The app's flex chain has no definite height (#app is min-height:100%), so a
   `flex:1; overflow:auto` log inside .view never became the scroller — the body
   scrolled instead and the log opened stuck at the top. As a fixed overlay the
   pane gets the viewport's height, so the log is the real scroller and pins to
   the newest message. It also reads as its own screen, Telegram-style. */
#circle-chat-pane.chat-pane {
  position: fixed; inset: 0; z-index: 50;
  max-width: 480px; margin: 0 auto;
  background: var(--base);
  padding: calc(8px + env(safe-area-inset-top)) 16px calc(8px + env(safe-area-inset-bottom));
}
.chat-head { gap: 6px; }
/* Telegram-style header identity: avatar + name over a subtitle, the whole block
   tappable to open the chat's details. */
.chat-head-id { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; background: transparent;
  border: 0; padding: 2px 4px; cursor: pointer; text-align: left; color: inherit; }
.chat-head-ava { flex: none; width: 40px; height: 40px; border-radius: 50%; background: var(--raised);
  border: 1px solid var(--border-2); color: var(--text-2); display: grid; place-items: center;
  font-family: var(--mono); font-size: 14px; font-weight: 500; overflow: hidden; }
.chat-head-ava .ic { width: 18px; height: 18px; }
/* Same safety-state ring the chat-list row draws (see .cl2-ava-friend), a
   notch thinner for the smaller header avatar. */
.chat-head-ava.state-sheltering { box-shadow: 0 0 0 1.5px var(--base), 0 0 0 3.5px var(--state-sheltering); }
.chat-head-ava.state-home { box-shadow: 0 0 0 1.5px var(--base), 0 0 0 3.5px var(--state-home); }
.chat-head-ava.state-work { box-shadow: 0 0 0 1.5px var(--base), 0 0 0 3.5px var(--state-work); }
.chat-head-text { min-width: 0; display: flex; flex-direction: column; line-height: 1.15; }
.chat-title { font-family: var(--sans); font-size: 15px; font-weight: 600; letter-spacing: 0; text-transform: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-subtitle { display: flex; align-items: center; gap: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-subtitle:empty { display: none; }
.chat-head-state-dot.state-sheltering { color: var(--state-sheltering); }
.chat-head-state-dot.state-home { color: var(--state-home); }
.chat-head-state-dot.state-work { color: var(--state-work); }
/* The transient connection line takes over the subtitle's own slot rather than
   adding a second row, so the header never changes height mid-raid. */
.chat-subtitle.is-conn { color: var(--amber); }
@media (prefers-reduced-motion: no-preference) {
  .chat-subtitle.is-conn { animation: chatConnPulse 1.6s ease-in-out infinite; }
}
@keyframes chatConnPulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

/* Forward/back slide between the chat-tab panes (list ↔ detail ↔ chat). */
.pane-in { animation: paneInRight .22s cubic-bezier(.22, 1, .36, 1); }
.pane-out { animation: paneOutRight .2s ease forwards; }
@keyframes paneInRight { from { transform: translateX(14%); opacity: .4; } to { transform: none; opacity: 1; } }
@keyframes paneOutRight { from { transform: none; opacity: 1; } to { transform: translateX(16%); opacity: 0; } }

/* Compose ("new chat") sheet: friends to DM, plus group / join actions. */
.compose-friends { display: flex; flex-direction: column; max-height: 46vh; overflow-y: auto; margin: 4px -4px 8px; }
.compose-empty { padding: 10px 6px; color: var(--muted); font-size: 13px; }
.compose-row { display: flex; align-items: center; gap: 12px; padding: 9px 8px; border-radius: 10px; cursor: pointer;
  background: transparent; border: 0; text-align: left; color: inherit; width: 100%; }
.compose-row:hover, .compose-row:active { background: var(--raised); }
.compose-row .cr-ava { flex: none; width: 40px; height: 40px; border-radius: 50%; background: var(--raised);
  border: 1px solid var(--border-2); color: var(--text-2); display: grid; place-items: center;
  font-family: var(--mono); font-size: 15px; overflow: hidden; }
.compose-row .cr-name { flex: 1; min-width: 0; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.compose-sheet-title { font-family: var(--sans); font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); padding: 2px 6px 6px; }
/* History strip above the log. It says one of three things - loading, the
   beginning of the conversation, or a failed page with its retry - and nothing
   the rest of the time.
   height:0 is load-bearing, not tidiness: as a normal flex row it would appear
   and disappear on every page load, resize the log by its own height plus the
   pane's gap, and jog the exact line the reader is on - which is what all the
   scroll-restore arithmetic in loadChatOlder exists to prevent. So it floats
   over the log's top edge instead, like the date pills inside it. */
.chat-older { text-align: center; height: 0; overflow: visible; position: relative;
  margin-bottom: -10px; }   /* cancels the pane's own gap, so nothing moved at all */
/* Beats `.chat-pane[data-bg] > * { z-index: 1 }`, which is more specific than a
   bare .chat-older and would let the log paint over the strip - invisibly, and
   only for readers who picked a wallpaper. */
#chat-load-older.chat-older { z-index: 3; }
.chat-older > * { position: relative; top: 3px; }
.chat-older .btn-ghost { width: auto; padding: 3px 12px; font-size: 12px; }
.chat-older-status { display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 12px; border-radius: 12px;
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  border: 1px solid var(--border); }
.chat-older-status:empty { display: none; }
/* At the very start the floating "Початок розмови" chip would sit on top of the
   first day separator; push that separator below it. */
.chat-log.at-start > .chat-daysep:first-child { margin-top: 36px; }
.chat-older.is-loading .chat-older-status::before {
  content: ""; width: 12px; height: 12px; border-radius: 50%; flex: none;
  border: 2px solid color-mix(in srgb, var(--amber) 30%, transparent);
  border-top-color: var(--amber);
}
@media (prefers-reduced-motion: no-preference) {
  .chat-older.is-loading .chat-older-status::before { animation: chatOlderSpin .8s linear infinite; }
}
@keyframes chatOlderSpin { to { transform: rotate(360deg); } }
.chat-log { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; padding: 4px 2px 8px; }

/* Telegram-style message rows: grouped runs, bubble carries the metadata. */
.chat-msg { display: flex; gap: 8px; max-width: 100%; align-items: flex-end; }
.chat-msg:not(.grouped) { margin-top: 10px; }
.chat-msg.theirs { justify-content: flex-start; }
.chat-msg.mine { justify-content: flex-end; }
.chat-ava { width: 28px; height: 28px; flex: none; border-radius: 50%; background: var(--raised); border: 1px solid var(--border-2);
  color: var(--text-2); display: grid; place-items: center; font-family: var(--mono); font-size: 12px; font-weight: 500;
  align-self: flex-end; overflow: hidden; }
/* A grouped message keeps the avatar column's width but hides the glyph, so the
   run stays left-aligned without repeating the face on every line. */
.chat-msg.grouped .chat-ava { visibility: hidden; }
.chat-col { display: flex; flex-direction: column; gap: 2px; max-width: 80%; min-width: 0; }
.chat-msg.mine .chat-col { align-items: flex-end; }
.chat-author { padding: 0 4px; font-family: var(--sans); font-size: 12px; font-weight: 600; letter-spacing: .01em;
  color: color-mix(in srgb, var(--amber) 70%, var(--text)); }

.chat-bubble { position: relative; padding: 7px 10px 6px; border-radius: 16px; font-size: 14px; line-height: 1.35;
  overflow-wrap: anywhere; white-space: pre-wrap; background: var(--raised); color: var(--text); cursor: pointer;
  border: 1px solid transparent; }
.chat-msg.theirs .chat-bubble { border-bottom-left-radius: 5px; }
.chat-msg.mine .chat-bubble { border-bottom-right-radius: 5px;
  background: color-mix(in srgb, var(--amber) 22%, #150d07); }
/* Inside a run the near corner stays tight so the run reads as one column. */
.chat-msg.grouped.theirs .chat-bubble { border-top-left-radius: 6px; }
.chat-msg.grouped.mine .chat-bubble { border-top-right-radius: 6px; }
.chat-bubble.tombstone { background: transparent; border: 1px dashed var(--border-2); color: var(--muted); font-style: italic; cursor: default; }
.chat-bubble.lp { filter: brightness(1.25); }
.chat-bubble.flash { animation: chatFlash 1s ease; }

/* Swipe to reply. pan-y hands the vertical axis to the browser and keeps the
   horizontal one for the gesture, so the log still scrolls natively. */
.chat-bubble { touch-action: pan-y; }
.chat-bubble.swipe-back { transition: transform .18s cubic-bezier(.22, 1, .36, 1); }
/* The arrow waits in the space the bubble is about to vacate: on the avatar
   side for someone else's message, on the screen edge for your own. */
.chat-swipe-arrow { position: absolute; top: 50%; margin-top: -13px; width: 26px; height: 26px;
  border-radius: 50%; display: grid; place-items: center; background: var(--raised);
  border: 1px solid var(--border); color: var(--muted); opacity: 0; pointer-events: none; }
/* 28px out + 26px wide: your own bubble sits against the right edge of the log,
   so any further out and the arrow is still half clipped at the moment the
   threshold is reached - which is exactly when it has to be readable. */
.chat-msg.theirs .chat-swipe-arrow { left: -28px; transform: scaleX(-1); }
.chat-msg.mine .chat-swipe-arrow { right: -28px; }
.chat-swipe-arrow.armed { color: var(--amber); border-color: color-mix(in srgb, var(--amber) 60%, var(--border)); }
/* A dragged bubble sticks out past the log's edge; without this the row grows a
   horizontal scroll of its own and the whole log can be slid sideways. */
.chat-log { overflow-x: hidden; }
@media (prefers-reduced-motion: reduce) { .chat-bubble.swipe-back { transition: none; } }
@keyframes chatFlash { 0%, 100% { box-shadow: 0 0 0 0 transparent; } 25% { box-shadow: 0 0 0 2px var(--amber); } }

/* Time + read ticks, tucked at the bubble's bottom-right, Telegram-style. */
.chat-metrow { display: inline-flex; align-items: center; gap: 4px; float: right; margin: 6px 0 -2px 10px;
  font-size: 10px; line-height: 1; color: var(--muted); user-select: none; }
.chat-msg.mine .chat-metrow { color: color-mix(in srgb, var(--amber) 40%, var(--muted)); }
.chat-edited { font-style: italic; }
.chat-ticks { display: inline-flex; align-items: center; }
.chat-ticks svg.tick-svg { display: block; width: 14px; height: 10px; }
.chat-ticks svg.tick-svg-double { width: 18px; }
.chat-ticks.read { color: var(--accent-text); }

/* Optimistic send. The bubble is drawn before the server has answered, so its
   delivery state has to be readable without leaving the message. */
.chat-msg.pending .chat-bubble { opacity: .68; }
.chat-msg.pending.failed .chat-bubble { opacity: 1; border-color: color-mix(in srgb, var(--red) 55%, transparent); }
/* Telegram's clock, drawn rather than typed: the webview font is not guaranteed
   to carry a clock glyph, and a missing one renders as a tofu box in exactly the
   slot where the delivery state has to be legible. */
.chat-ticks.pending { position: relative; width: 9px; height: 9px; }
.chat-ticks.pending::before { content: ""; position: absolute; inset: 0; border-radius: 50%;
  border: 1px solid currentColor; }
.chat-ticks.pending::after { content: ""; position: absolute; left: 50%; top: 2px; width: 1px; height: 3px;
  background: currentColor; }
.chat-ticks.failed { color: var(--red); font-weight: 700; letter-spacing: 0; }
/* Retry / discard, under a bubble that never left the phone. */
.chat-failrow { display: flex; justify-content: flex-end; gap: 6px; padding: 4px 2px 0; }
.chat-failbtn { padding: 2px 9px; border-radius: 11px; background: var(--raised); border: 1px solid var(--border);
  color: var(--text-2); font-family: var(--sans); font-size: 11px; line-height: 1.5; cursor: pointer; }
.chat-failbtn.danger { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, var(--border)); }

/* Quoted reply preview inside a bubble. */
.chat-reply { display: block; margin: 0 0 5px; padding: 3px 8px; border-left: 2px solid var(--amber);
  border-radius: 4px; background: color-mix(in srgb, var(--amber) 8%, transparent); cursor: pointer; }
.chat-reply .cr-name { display: block; font-size: 11px; font-weight: 600; color: color-mix(in srgb, var(--amber) 75%, var(--text)); }
.chat-reply .cr-text { display: block; font-size: 12px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Reaction chips under a bubble. */
.chat-reactions { display: flex; flex-wrap: wrap; gap: 4px; padding: 3px 2px 0; }
.chat-msg.mine .chat-reactions { justify-content: flex-end; }
.rx { display: inline-flex; align-items: center; gap: 3px; padding: 2px 7px; border-radius: 11px; cursor: pointer;
  background: var(--raised); border: 1px solid var(--border); font-size: 12px; line-height: 1.2; }
.rx.mine { background: color-mix(in srgb, var(--amber) 20%, var(--surface)); border-color: color-mix(in srgb, var(--amber) 55%, var(--border)); }
.rx .rx-n { font-family: var(--mono); font-size: 11px; color: var(--text-2); }
.rx.mine .rx-n { color: var(--amber); }

/* Date separators and the unread divider woven into the log. */
.chat-daysep { align-self: center; margin: 8px 0 2px; padding: 3px 12px; border-radius: 12px;
  background: color-mix(in srgb, var(--surface) 80%, transparent); border: 1px solid var(--border);
  font-family: var(--sans); font-size: 11px; letter-spacing: .04em; color: var(--text-2);
  position: sticky; top: 2px; z-index: 2; }
.chat-unreadsep { align-self: stretch; margin: 8px 0 2px; text-align: center; font-family: var(--sans); font-size: 11px;
  letter-spacing: .06em; text-transform: uppercase; color: color-mix(in srgb, var(--amber) 70%, var(--text-2));
  border-top: 1px solid color-mix(in srgb, var(--amber) 40%, var(--border)); padding-top: 4px; }

.chat-msg.enter { animation: chatIn .24s cubic-bezier(.22, 1, .36, 1); }
@keyframes chatIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* Jump-to-latest, floating just above the composer. */
.chat-jump { position: absolute; right: 6px; bottom: 66px; width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--border-2); background: var(--surface); color: var(--text); display: grid; place-items: center;
  cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,.45); z-index: 4; }
.chat-jump .ic { width: 20px; height: 20px; }
.chat-jump-badge { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 9px; background: var(--amber); color: #0a0a0a; font-family: var(--mono); font-size: 11px; font-weight: 600;
  display: grid; place-items: center; }

/* Reply/edit context strip above the composer. */
.chat-context { display: flex; align-items: center; gap: 8px; padding: 6px 4px 0; }
.chat-context .cc-bar { flex: none; width: 3px; align-self: stretch; min-height: 28px; border-radius: 2px; background: var(--amber); }
.chat-context .cc-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.chat-context .cc-label { font-weight: 600; color: color-mix(in srgb, var(--amber) 75%, var(--text)); }
.chat-context .cc-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-context .cc-cancel { flex: none; }

/* Quick-reaction bar in the message menu. */
.msg-menu-rx { display: flex; justify-content: space-around; gap: 4px; padding: 6px 4px 10px; }
.msg-menu-rx button { flex: 1; font-size: 22px; line-height: 1; background: transparent; border: 0; padding: 6px 0; cursor: pointer; border-radius: 8px; }
.msg-menu-rx button:hover, .msg-menu-rx button.on { background: color-mix(in srgb, var(--amber) 18%, var(--surface)); }

.chat-input { display: flex; gap: 8px; align-items: flex-end; }
.chat-input textarea { flex: 1; background: var(--raised); border: 1px solid var(--border); border-radius: 20px;
  color: var(--text); padding: 11px 14px; font-size: 15px; min-height: 44px; max-height: 132px; resize: none;
  font-family: var(--sans); line-height: 1.35; overflow-y: auto; }
.chat-input textarea:focus { border-color: var(--amber); }
.chat-input textarea:focus:not(:focus-visible) { outline: none; }
.chat-send { flex: none; width: 44px; height: 44px; border: 0; border-radius: 50%; background: var(--amber); color: #0a0a0a; font-size: 16px; cursor: pointer; align-self: flex-end; }
.chat-send:hover { background: #ff7c40; }
.chat-send:disabled { opacity: .5; }

/* --- foreground alert banner --- */
.push-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 50;
  max-width: 480px; margin: 0 auto;
  display: flex; align-items: flex-start; gap: 12px;
  padding: calc(12px + env(safe-area-inset-top)) 16px 12px;
  background: #050505;
  border-bottom: 1px solid var(--border);
  box-shadow: 0 12px 32px -8px rgba(0, 0, 0, .8);
  transform: translateY(-110%); transition: transform .28s cubic-bezier(.22, 1, .36, 1);
}
.push-banner.show { transform: translateY(0); }
.pb-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; margin-top: 4px; background: var(--info); box-shadow: 0 0 8px currentColor; }
.push-banner.sev-RED .pb-dot { background: var(--red); color: var(--red); }
.push-banner.sev-ORANGE .pb-dot { background: var(--orange); color: var(--orange); }
.push-banner.sev-YELLOW .pb-dot { background: var(--yellow); color: var(--yellow); }
.push-banner.sev-INFO .pb-dot { background: var(--info); color: var(--info); box-shadow: none; }
.push-banner.sev-GREEN .pb-dot { background: var(--green); color: var(--green); }
/* A ballistic warning used to arrive on #050505 — one hex value away from the
   app background — with a 9px dot as the only signal. In the foreground it now
   reads as an alarm, and it does not dismiss itself. */
.push-banner.sev-RED { background: linear-gradient(180deg, #5c0f0b, #3a0806);
  border-bottom: 3px solid var(--red); }
.push-banner.sev-RED .pb-title { font-size: 18px; font-weight: 700; color: #ffd9d4; }
.push-banner.sev-RED .pb-body { color: #f0c4bf; font-size: 13px; -webkit-line-clamp: 3; }
.push-banner.sev-RED .pb-dot { width: 12px; height: 12px; margin-top: 6px; }
.push-banner.sev-RED .pb-close { color: #ffd9d4; }
.push-banner.sev-ORANGE { border-bottom-color: color-mix(in srgb, var(--orange) 55%, var(--border)); }
.pb-text { flex: 1; min-width: 0; cursor: pointer; }
.pb-title { font-weight: 600; font-size: 14px; }
.pb-body { color: var(--text-2); font-size: 13px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.pb-close { background: none; border: 0; color: var(--muted); font-size: 15px; padding: 2px 6px; cursor: pointer; flex: none; }
.pb-close:hover { color: var(--text); }

/* --- bottom nav --- */
/* Guest region choice (feed view). The chip row IS the label - a "показувати"
   eyebrow above it said nothing the chips do not. */
#guest-region { display: flex; flex-direction: column; gap: 10px; }
#guest-region[hidden] { display: none; }
.guest-cta { display: flex; flex-direction: column; gap: 3px; text-align: left; width: 100%;
  padding: 13px; cursor: pointer; background: var(--surface);
  border: 1px dashed var(--border-3); border-radius: var(--r-block); color: var(--text); }
.guest-cta .gc-t { font-size: 15px; font-weight: 600; }
.guest-cta .gc-s { font-size: 12px; color: var(--text-2); line-height: 1.35; }
.area-more { width: 100%; min-height: 44px; padding: 11px; cursor: pointer; font: inherit; font-size: 13px;
  color: var(--text-2); background: none; border: 1px dashed var(--border-3); border-radius: var(--r-block); }
/* Grouped oblast list under the map: a status heading over a row of tappable
   chips. Chip border/text colour is set inline per status token. */
.area-group-head { font-size: 12px; font-weight: 600; margin: 10px 2px 4px; }
.area-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 2px 4px; }
.area-chip { font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 14px;
  border: 1px solid; background: transparent; cursor: pointer; white-space: nowrap; }
/* Inline note appended inside the oblast card (e.g. the sign-in hint). */
.ac-note { font-size: 12px; margin-top: 8px; }
.guest-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.guest-chips[hidden] { display: none; }
.gr-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 40px;
  padding: 5px 10px; cursor: pointer; font: inherit; font-size: 12px; font-weight: 600;
  color: var(--accent); background: none; border-radius: var(--r-ctrl);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, var(--border)); }
.gr-chip .ic { width: 13px; height: 13px; opacity: .8; }
.gr-add { min-height: 40px; padding: 5px 10px; cursor: pointer; font: inherit; font-size: 12px;
  color: var(--text-2); background: none; border: 1px dashed var(--border-3); border-radius: var(--r-ctrl); }
/* The answer card: one oblast, its official state, how long, and what the
   monitoring says is behind it. Severity colours the 1px rule, never a fill. */
.guest-answer { padding: 13px; background: var(--surface); border: 1px solid var(--border);
  border-left-width: 2px; border-radius: var(--r-block); display: flex; flex-direction: column; gap: 3px; }
.guest-answer[hidden] { display: none; }
.ga-region { font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
.ga-state { font-size: 13px; font-weight: 600; }
.ga-since { font-family: var(--mono); font-size: 12px; color: var(--text-2); }
.ga-cause { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.ga-more { align-self: flex-start; margin-top: 4px; }
.ga-active { border-left-color: var(--red); border-color: color-mix(in srgb, var(--red) 34%, var(--border)); }
.ga-active .ga-state { color: var(--red); }
.ga-partial { border-left-color: var(--orange); border-color: color-mix(in srgb, var(--orange) 32%, var(--border)); }
.ga-partial .ga-state { color: var(--orange); }
.ga-clear { border-left-color: var(--green); }
.ga-clear .ga-state { color: var(--green); }
.ga-unknown { border-left-color: var(--border-3); }
.ga-unknown .ga-state { color: var(--muted); }

/* --- guest mode ---------------------------------------------------------
   The guest shell drops the tabbar, so this strip takes its place at the
   bottom: it says what guest mode cannot do (push) and is the only way back
   to the sign-in screen. Taller than the tabbar, hence the padding override
   on .view-app below. */
/* Equal weight with "Створити акаунт" / "Увійти": a guest path hidden in small
   print is a guest path nobody takes. */
.btn-guest { margin-top: 10px; }
.guest-bar {
  position: fixed; left: 0; right: 0; z-index: 10;
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  max-width: 480px; margin: 0 auto;
  padding: 9px 14px;
  display: flex; flex-direction: column; gap: 8px;
  background: #040404; border-top: 1px solid var(--border);
}
.guest-bar[hidden] { display: none; }
.gb-text { font-size: 12px; line-height: 1.35; color: var(--text-2); }
.gb-acts { display: flex; align-items: center; gap: 10px; }
.gb-btn { flex: 1 1 auto; min-height: 40px; padding: 9px 14px; cursor: pointer;
  font: inherit; font-weight: 600; color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, var(--border)); border-radius: var(--r-ctrl); }
/* Secondary reads as secondary: with the primary no longer a filled block, two
   accent-coloured outlines side by side had no hierarchy left. */
.gb-link { flex: none; min-height: 40px; padding: 9px 12px; cursor: pointer;
  font-family: var(--sans); font-size: 11px; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-2); background: none; border: 1px solid var(--border-2); border-radius: var(--r-ctrl); }
/* The strip sits ON TOP of the tabbar, so both have to be cleared or the map
   legend and the last feed card hide behind them. */
body.guest .view-app { padding-bottom: calc(var(--tabbar-h) + 116px + env(safe-area-inset-bottom)); }
/* The active segment was a filled amber block sitting directly under a card
   whose ORANGE rule means "часткова тривога" - two different meanings in one
   colour, 8px apart. In the guest shell the segment states itself with weight
   and surface instead. */
body.guest .seg-btn[aria-current="true"] { background: var(--raised); color: var(--accent); font-weight: 600; }

/* Account-only furniture in the feed: the official-state banner is personal
   (it answers "am I safe" for YOUR regions), and the scope/level controls
   filter a personal feed a guest does not have. The guest feed is one
   unfiltered stream, so both would be levers with nothing behind them. */
body.guest #status-banner,
body.guest #notif-warning,
body.guest #geo-warning,
body.guest .feed-controls-row { display: none !important; }

/* The update banner stacks ABOVE the tabbar, never over it. The forced variant
   carries no dismiss button, so at bottom:0 with a z-index over the bar it
   covered the tabs for as long as the app stayed open - no Мапа, no Чати, no
   Меню, including during an alert. Same idiom as .guest-bar above. */
.update-banner {
  position: fixed; left: 0; right: 0; z-index: 20;
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  max-width: 480px; margin: 0 auto;
  padding: 12px 14px;
  display: flex; gap: 10px; align-items: center;
  font-size: 14px; line-height: 1.3;
  background: var(--raised); color: var(--text);
  border-top: 1px solid var(--border-2);
  box-shadow: 0 -2px 8px rgba(0, 0, 0, .4);
}
/* The guest shell already puts its conversion strip on top of the tabbar, so
   the banner has to clear both. */
body.guest .update-banner {
  bottom: calc(var(--tabbar-h) + 116px + env(safe-area-inset-bottom));
}
.update-banner-text { flex: 1; }
.update-banner-go {
  flex: none; background: var(--accent); color: #111; border: 0;
  border-radius: 6px; padding: 8px 14px; font-weight: 700; min-height: 44px;
}
.update-banner-x {
  flex: none; background: transparent; color: var(--text); border: 0;
  font-size: 16px; padding: 4px 10px; min-height: 44px;
}

.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
  max-width: 480px; margin: 0 auto;
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  display: flex;
  background: #040404; border-top: 1px solid var(--border);
}
.tab { flex: 1 1 0; min-width: 0; }
.tab[hidden] { display: none; }
/* 8.5px was 30% under the Material minimum and the dividers made the bar read
   as a spreadsheet header. Bigger label, no dividers. */
.tab { background: none; border: 0; color: var(--muted);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  font-family: var(--sans); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; cursor: pointer; }
/* Material's active-indicator pill sits BEHIND the icon, so it can never
   collide with the label the way an underline bar did. */
.tab-ico { font-size: 17px; line-height: 1; position: relative;
  display: grid; place-items: center; width: 52px; height: 30px;
  border-radius: 999px; transition: background-color .15s ease-out; }
/* Active state carries a shape, not only a hue: colour alone fails for the
   ~8% of men with deuteranopia and for anyone glancing at the bar. */
.tab[aria-current="true"] { color: var(--amber); }
.tab[aria-current="true"] .tab-ico { background: color-mix(in srgb, var(--accent) 20%, transparent); }
/* Anchored to the icon pill (52×30), not to a bare glyph, so it rides the
   badge on the icon's corner instead of floating off the pill. */
.tab-badge { position: absolute; top: -2px; right: 6px; min-width: 16px; height: 16px; padding: 0 4px;
  background: var(--accent); color: var(--on-accent); font-family: var(--mono); font-size: 11px; font-weight: 700;
  line-height: 16px; text-align: center; border-radius: 999px; }
.tab-badge[hidden] { display: none; }

/* unread chat-list row */
.chat-list-row.unread .cl-name { font-weight: 700; color: var(--text); }
.chat-list-row.unread .cl-sub.muted { color: var(--text-2); }
.cl-unread-dot { width: 9px; height: 9px; flex: none; border-radius: 50%; background: var(--accent); }
.cl-unread-badge { flex: none; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 11px; background: var(--accent);
  color: var(--on-accent); font-family: var(--mono); font-size: 12px; font-weight: 600; display: grid; place-items: center; }

/* --- review queue --- */
.review-item { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.review-item:last-child { border-bottom: 0; }
.review-head { font-family: var(--sans); font-size: 12px; letter-spacing: .01em; }
.review-sub { font-family: var(--mono); font-size: 11px; color: var(--muted); letter-spacing: .02em; }
.review-text { color: var(--text-2); font-size: 12px; line-height: 1.5; border-left: 2px solid var(--border-2); padding-left: 8px; }
.review-region { background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-ctrl); color: var(--text); padding: 9px 11px; font-size: 13px; min-height: 38px; font-family: var(--sans); }
.review-acts { display: flex; flex-wrap: wrap; gap: 6px; }
.review-acts .btn-primary, .review-acts .btn-ghost { padding: 7px 10px; min-height: 40px; flex: 1; }

/* --- skeleton loading states ------------------------------------------------
   Shown ONLY while the first fetch of a screen is in flight (JS: _skFirst);
   auto-refresh polls never flash them. Shapes roughly mirror the real cards so
   nothing jumps when the content lands. */
.sk-wrap[hidden] { display: none; }
.sk { display: block; background: var(--line); position: relative; overflow: hidden; }
.sk-line { height: 9px; }
.sk-line.sk-lg { height: 12px; }
.w14 { width: 14%; } .w20 { width: 20%; } .w30 { width: 30%; } .w35 { width: 35%; }
.w40 { width: 40%; } .w45 { width: 45%; } .w50 { width: 50%; } .w55 { width: 55%; }
.w60 { width: 60%; } .w65 { width: 65%; } .w70 { width: 70%; } .w75 { width: 75%; }
.w80 { width: 80%; } .w90 { width: 90%; }
.sk-right { margin-left: auto; }
@media (prefers-reduced-motion: no-preference) {
  .sk::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgba(232,232,230,.07), transparent);
    animation: sk-shimmer 1.5s ease-in-out infinite; }
  @keyframes sk-shimmer { to { transform: translateX(100%); } }
}
/* feed card placeholder — mirrors .feed-item (2px accent-less left border) */
.sk-card { background: var(--surface); border: 1px solid var(--border); border-left-width: 2px;
  border-left-color: var(--border-2); padding: 12px 13px; display: flex; flex-direction: column; gap: 8px; }
.sk-card-top { display: flex; align-items: center; gap: 8px; }
.sk-ava { width: 24px; height: 24px; flex: none; }
/* chat-list placeholder — mirrors .chat-list-row inside the negative-margin list */
.sk-chat-row { display: flex; align-items: center; gap: 12px; padding: 12px 20px;
  border-bottom: 1px solid var(--border); min-height: 64px; }
.sk-ava-lg { width: 48px; height: 48px; flex: none; border-radius: 50%; }
.sk-chat-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
/* map placeholder — mirrors .map-svg box + legend row */
.sk-map-wrap { display: flex; flex-direction: column; gap: 10px; }
.sk-map { width: 100%; aspect-ratio: 3 / 2; border: 1px solid var(--border); background: var(--panel); }
.sk-map-legend { display: flex; flex-wrap: wrap; gap: 12px; }

/* --- Про застосунок --------------------------------------------------------- */
.ab-hero { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 24px 14px 12px; }
.ab-mark { width: 56px; height: 56px; }
.ab-name { font-family: var(--sans); font-size: 15px; letter-spacing: .06em; text-transform: uppercase; }
.ab-ver { font-family: var(--mono); font-size: 11px; letter-spacing: .06em; color: var(--dim); }
.ab-note { border: 1px solid var(--line); background: var(--panel); padding: 13px;
  color: var(--text-2); font-size: 13px; line-height: 1.55; }
.ab-src { align-items: flex-start; }
.ab-src .pf-ic { margin-top: 2px; }
.ab-warn { display: flex; gap: 11px; align-items: flex-start; color: var(--text);
  border-color: color-mix(in srgb, var(--red) 45%, var(--border));
  background: color-mix(in srgb, var(--red) 12%, var(--panel)); }
.ab-warn-ic { flex: none; color: var(--red); display: grid; place-items: center; }
#view-about .pf-list:last-of-type { padding-bottom: 24px; }

/* --- first-run onboarding ---------------------------------------------------- */
/* Each step has a full-bleed background (set by the sN class in renderOnboarding)
   and a scrim over it; the header, cards and footer sit above with translucent
   backings so the art shows without eating text contrast. Art is decoration -
   every word is in the markup, so a failed image load just leaves the dark
   screen. */
/* position: fixed, NOT relative: this rule used to say relative, which silently beat
   .view-profile-screen's fixed (same specificity, later in the file) and left the intro
   as a short block at the top of the page with the whole feed rendered under it. The
   steps were short enough to hide that; a taller step pushes the buttons off-screen.
   The footer already carries the safe-area padding, so the screen's own is dropped. */
.ob-screen { z-index: 70; display: flex; flex-direction: column; position: fixed; overflow: hidden; padding-bottom: 0; }
.ob-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-size: cover; background-position: center; background-repeat: no-repeat; }
.ob-screen.s1 .ob-bg { background-image: url("../img/onboarding/welcome.webp"); }
.ob-screen.s2 .ob-bg { background-image: url("../img/onboarding/feed.webp"); }
.ob-screen.s3 .ob-bg { background-image: url("../img/onboarding/regions.webp"); }
.ob-screen.s4 .ob-bg { background-image: url("../img/onboarding/more.webp"); }
.ob-screen.s5 .ob-bg, .ob-screen.s6 .ob-bg { background-image: url("../img/onboarding/notify.webp"); }
.ob-scrim { position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(0,0,0,.30) 0%, rgba(0,0,0,.12) 26%,
    rgba(0,0,0,.5) 50%, rgba(0,0,0,.86) 70%, rgba(0,0,0,.95) 100%); }
.ob-top, .ob-body, .ob-foot { position: relative; z-index: 1; }
.ob-spacer { flex: 1; }
.ob-body { flex: 1; padding: 4px 18px 20px; overflow-y: auto; display: flex; flex-direction: column; }
.ob-step[hidden] { display: none; }
.ob-step { display: flex; flex-direction: column; gap: 12px; margin-top: auto; }
@media (prefers-reduced-motion: no-preference) {
  .ob-step:not([hidden]) { animation: ob-in .28s ease-out; }
  @keyframes ob-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; } }
}
.ob-mark { width: 50px; height: 50px; margin: 18px 0 4px; display: grid; place-items: center;
  border-radius: 14px; background: rgba(255,106,43,.14);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent); color: var(--accent); }
.ob-mark svg { width: 24px; height: 24px; }
.ob-h { margin: 0; font-family: var(--display, var(--sans)); font-weight: 800; font-size: 23px;
  letter-spacing: -.01em; text-transform: none; color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,.7); }
.ob-p { margin: 0; color: #d5d7d9; font-size: 13.5px; line-height: 1.55;
  text-shadow: 0 1px 8px rgba(0,0,0,.7); }
.ob-facts { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
.ob-fact { display: flex; align-items: flex-start; gap: 11px; padding: 12px 13px;
  border-radius: 12px; border: 1px solid rgba(255,255,255,.09);
  background: rgba(19,20,23,.62); backdrop-filter: blur(10px); }
.ob-fact > span:last-child { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ob-fact-t { font-size: 14px; color: #fff; }
.ob-src-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; margin-top: 5px; }
/* Threat-colour ramp on step 2: shape and label, never colour alone. */
.ob-ramp { display: flex; gap: 5px; }
.ob-ramp span { flex: 1; text-align: center; font-family: var(--mono); font-size: 9px;
  padding: 6px 2px; border-radius: 6px; color: #fff; letter-spacing: .02em; }
.ob-regions { margin-top: 4px; }
.ob-regions .chip-select { min-height: 40px; }
.ob-allow { width: 100%; margin-top: 4px; }
.ob-state { margin: 0; min-height: 18px; }
.ob-state.ok { color: var(--green); }
.ob-state.bad { color: var(--red); }
.ob-foot { position: sticky; bottom: 0; display: flex; flex-direction: column; gap: 14px;
  padding: 14px 18px calc(18px + env(safe-area-inset-bottom));
  background: linear-gradient(rgba(4,4,4,.4), rgba(4,4,4,.92) 40%);
  backdrop-filter: blur(8px); border-top: 1px solid rgba(255,255,255,.06); }
.ob-dots { display: flex; justify-content: center; gap: 7px; }
.ob-dot { width: 20px; height: 3px; background: var(--line-2); }
.ob-dot[aria-current="true"] { background: var(--accent); }
.ob-actions { display: flex; gap: 10px; }
.ob-actions button { flex: 1; min-height: 46px; }

/* --- Надійність сповіщень (checklist screen, onboarding step 6, feed strip) --- */
.pf-cap.rel-ok { color: var(--green); }
.pf-cap.rel-warn { color: var(--amber); }
.rel-intro { margin: 0; padding: 14px 15px 4px; line-height: 1.5; }
.rel-list { display: flex; flex-direction: column; gap: 10px; padding: 8px 14px 0; }
.rel-item { display: flex; flex-direction: column; gap: 10px; padding: 13px;
  border: 1px solid var(--line); background: var(--panel); }
.rel-item[hidden] { display: none; }
.rel-item[data-state="ok"] { border-color: color-mix(in srgb, var(--green) 35%, var(--line)); }
.rel-item[data-state="bad"] { border-color: color-mix(in srgb, var(--amber) 45%, var(--line)); }
.rel-head { display: flex; align-items: flex-start; gap: 11px; }
.rel-mark { flex: none; width: 22px; height: 22px; margin-top: 1px; display: grid; place-items: center; color: var(--text-2); }
.rel-mark svg { width: 22px; height: 22px; }
.rel-item[data-state="ok"] .rel-mark { color: var(--green); }
.rel-item[data-state="bad"] .rel-mark { color: var(--amber); }
.rel-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rel-title { font-size: 14px; font-weight: 600; color: var(--text); }
.rel-state { font-size: 12px; color: var(--text-2); }
.rel-item[data-state="ok"] .rel-state { color: var(--green); }
.rel-item[data-state="bad"] .rel-state { color: var(--amber); }
.rel-more { display: flex; flex-direction: column; gap: 8px; }
.rel-text { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text-2); }
.rel-hint { margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--text);
  padding: 8px 10px; background: var(--raised); border-left: 2px solid var(--amber); }
.rel-hint:empty { display: none; }
.rel-btn { width: 100%; min-height: 44px; }
.rel-check { min-height: 40px; }
.rel-msg { margin: 0; line-height: 1.45; }
.rel-msg.ok { color: var(--green); }
.rel-msg.bad { color: var(--red); }
.rel-msg[hidden] { display: none; }
.ob-fact .ob-rel-state { display: block; }
.ob-fact .ob-rel-state.ok { color: var(--green); }
.ob-fact .ob-rel-state.warn { color: var(--amber); }
#ob-step-6 .ob-fact > span:last-child { flex: 1; }
.ob-fact > span:last-child .ob-rel-btn { align-self: stretch; margin-top: 8px; min-height: 40px; }
.ob-fact .rel-check { margin-top: 2px; }

/* feed strip: amber (the phone MAY mute alarms), two tap targets side by side */
.reliability-strip { cursor: default; padding: 0; align-items: stretch;
  background: color-mix(in srgb, var(--amber) 12%, var(--surface));
  border-color: color-mix(in srgb, var(--amber) 45%, var(--border)); }
.reliability-strip .nw-ic { color: var(--amber); }
.reliability-strip .nw-title { color: var(--amber); }
.rs-open { flex: 1; min-width: 0; display: flex; align-items: center; gap: 11px; padding: 12px 6px 12px 14px;
  min-height: 52px; background: none; border: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.reliability-strip .nw-title { font-size: 13px; }
.rs-later { flex: none; align-self: center; margin: 0 10px 0 0; min-height: 36px; padding: 6px 9px; font-size: 10.5px; }

/* --- diagnostics screen --- */
.diag-actions { display: flex; gap: 10px; padding: 12px 14px 0; }
.diag-actions button { flex: 1; min-height: 44px; }
.diag-log { margin: 12px 14px calc(24px + env(safe-area-inset-bottom)); padding: 12px;
  background: var(--panel); border: 1px solid var(--line); color: var(--text-2);
  font-family: var(--mono); font-size: 11px; line-height: 1.6;
  white-space: pre-wrap; word-break: break-word; overflow-x: hidden; }

/* --- monitored sources (admin) --- */
.src-add-row { display: flex; gap: 8px; align-items: center; }
.src-add-row input { flex: 1; min-height: 44px; }

/* Filters the source list by name as the reader types - purely client-side,
   it never reads or writes a channel's on/off state. */
.src-search { display: flex; align-items: center; gap: 8px;
  background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-ctrl);
  padding: 0 11px; min-height: 44px; }
.src-search-ic { flex: none; width: 16px; height: 16px; color: var(--text-2); }
.src-search input { flex: 1; min-width: 0; background: none; border: 0; color: var(--text);
  font-size: 14px; font-family: var(--sans); padding: 10px 0; min-height: 44px; }
.src-search input:focus { outline: none; }
.src-search input::placeholder { color: var(--muted); }
/* WebKit/Blink paint a native magnifier + clear icon on type=search; the row
   already has both, so the native ones would just double up. */
.src-search input::-webkit-search-decoration,
.src-search input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.src-search-clear { flex: none; width: 28px; height: 28px; border-radius: 50%;
  border: 0; background: none; color: var(--text-2); font-size: 13px; line-height: 1; cursor: pointer; }
.src-search-clear:hover { color: var(--text); background: var(--panel); }
/* The group header carries the whole-group switch, so it becomes a row. Scoped
   to .src-section: .chat-section is also the chat's plain label. */
.src-section { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
/* Quiet by default - it acts on a dozen sources at once and must not read as
   the primary action of the screen. Full 44px target despite the small text. */
.src-bulk { flex: none; background: none; border: 0; padding: 6px 2px; min-height: 44px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-2); cursor: pointer; }
.src-bulk:hover, .src-bulk:focus-visible { color: var(--accent); }

.src-row { display: flex; align-items: center;
  gap: 10px; padding: 9px 4px; border-bottom: 1px solid var(--line); }
.src-row:last-child { border-bottom: 0; }
.src-avatar { flex: none; position: relative; width: 34px; height: 34px;
  border-radius: 50%; overflow: hidden; background: var(--panel);
  display: flex; align-items: center; justify-content: center; }
.src-avatar-mono { color: var(--text-2); font-size: 14px; font-weight: 600; }
.src-avatar-img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; transition: opacity .15s; }
/* Two lines with one job each: the handle, then whatever is true about the
   source. They used to be one inline run - badges flowed inside the name with
   their own left margins, so on a narrow phone they broke apart one per line,
   every row came out a different height, and the avatar (centred against a tall
   ragged block) drifted away from the name it belongs to. */
.src-name { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
/* anywhere, not break-all: break-all splits a short handle that would have fit,
   this only breaks one that genuinely cannot. */
.src-handle { color: var(--text); font-size: 14px; line-height: 1.25;
  overflow-wrap: anywhere; }
/* One row, wrapping as a row. The gap does the spacing, so no tag carries a
   margin of its own and two tags never sit at two different offsets. */
.src-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.src-tags:empty { display: none; }
.src-mute { flex: none; width: 40px; height: 40px; border-radius: var(--r-ctrl);
  border: 1px solid var(--line); background: transparent; font-size: 16px; line-height: 1; }
.src-mute.muted { border-color: var(--accent); }
.src-mute:active { background: var(--panel); }
/* ON/OFF toggle switch (per-source subscription). */
/* One size and one shape for every tag, set once here. The variants below may
   change COLOUR and nothing else - three chips at three sizes on one row was
   most of why this list looked unsettled. */
.src-tag { display: inline-block; padding: 2px 7px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--dim); font-size: 10px; line-height: 1.4;
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }
/* A feed-only source is deliberately quiet, not broken - mark it apart from the
   plain "додатковий" opt-in tag. */
.src-tag-feed { border-color: var(--accent); color: var(--accent); }
/* "рекомендований": a source that proved itself in a real raid. Green, because
   the accent orange already means "feed only" one tag away and the two would be
   read as the same thing. Filled rather than outlined so it wins against the
   other two chips without being louder than the alert colours elsewhere. */
.src-tag-rec { border-color: color-mix(in srgb, var(--green) 55%, transparent);
  color: var(--green); background: color-mix(in srgb, var(--green) 10%, transparent); }
/* The row itself carries a quiet green rail. Enough to find the recommended
   sources while scrolling 66 of them, not enough to look like a warning. */
/* The rail runs the full height of the row, so it has to be the row's own edge
   rather than a shadow that stops where the padding does. */
.src-row-rec { box-shadow: inset 2px 0 0 color-mix(in srgb, var(--green) 45%, transparent);
  padding-left: 10px; }
/* The avatar tracks the FIRST line of text, not the middle of the block. On a
   two-line row the centred version sat between the handle and its tags,
   belonging to neither. */
.src-row .src-avatar { align-self: flex-start; margin-top: 1px; }
/* ...and the controls stay centred against the whole row, where a thumb aims. */
.src-row .src-toggle, .src-row .src-mute, .src-row .src-del { align-self: center; }
.src-toggle { flex: none; width: 46px; height: 28px; border-radius: 999px; padding: 0;
  border: 1px solid var(--line); background: var(--panel); position: relative; cursor: pointer;
  transition: background .16s, border-color .16s; }
.src-toggle::after { content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--text-2); transition: transform .16s, background .16s; }
.src-toggle.on { background: color-mix(in srgb, var(--accent) 30%, transparent); border-color: var(--accent); }
.src-toggle.on::after { transform: translateX(18px); background: var(--accent); }
.src-del { flex: none; width: 40px; height: 40px; border-radius: var(--r-ctrl);
  border: 1px solid var(--line); background: transparent; color: var(--text-2);
  font-size: 15px; line-height: 1; }
.src-del:active { background: var(--panel); }

/* --- changelog (Про застосунок) --- */
.ab-changelog { padding-top: 6px; }
.cl-ver { font-size: 12px; font-weight: 700; color: var(--accent); margin: 10px 0 4px; }
.cl-ver:first-child { margin-top: 0; }
.cl-item { font-size: 13px; color: var(--text-2); line-height: 1.45; padding-left: 4px; }

/* --- admin analytics (Розбір) --- */
/* Was a fixed 4 columns: eight metrics at ~112px per cell in a 480px column,
   which wrapped badly the moment the system font scale went up. auto-fit lets
   the row pick its own column count instead. */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 8px; margin-top: 10px; }
.stat { background: var(--panel); border-radius: var(--r-ctrl); padding: 10px 6px; text-align: center; }
.stat-val { font-size: 18px; font-weight: 700; color: var(--text); line-height: 1.2; }
.stat-lbl { font-size: 11px; color: var(--text-2); margin-top: 3px; }
.stat.warn .stat-val { color: var(--accent); }
.stats-versions { margin-top: 10px; }

/* --- feed: compact mode, freeze pill, flag --- */
.fi-compact { display: none; }
.compact-btn.on { background: var(--accent); color: var(--on-accent); }
.feed-compact .feed-item { margin-bottom: 4px; }
/* Compact + collapsed: show the one-liner, hide the verbose parts. When a card
   is expanded (.open) none of these apply, so it shows its full header + details. */
.feed-compact .feed-item:not(.open) .fi-compact {
  display: block; padding: 8px 12px; }
.fi-cmeta { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
.fi-cdot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--info); }
.fi-cmetatext { font-size: 11px; color: var(--text-2); }
.fi-ctext { font-size: 13px; line-height: 1.35; color: var(--text);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.feed-compact .feed-item:not(.open) .fi-top,
.feed-compact .feed-item:not(.open) .fi-tags,
.feed-compact .feed-item:not(.open) .feed-body,
.feed-compact .feed-item:not(.open) .feed-sub,
.feed-compact .feed-item:not(.open) .fi-expand { display: none; }


.fi-flag {
  display: block; width: 100%; margin-top: 10px; padding: 9px 12px;
  border: 1px solid var(--line); border-radius: 8px; background: transparent;
  color: var(--text-2); font-size: 13px; text-align: center; }
.fi-flag:active { background: var(--panel); }
.fi-flag.done { color: var(--accent); border-color: var(--accent); }

/* --- reduced motion, as a floor ---
   Individual effects already opt in via `no-preference`, but the chat message
   enter, the push-banner slide and the pull-to-refresh spinner did not. A user
   who asked the system for less motion should not have to trust that every
   future rule remembers to check. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Visually hidden but still announced. A live region set to display:none is
   never read, so it cannot use [hidden]. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* --- Telegram-style floating message menu --- */
/* A centred overlay: quick reactions, a preview of the tapped bubble, then a
   stacked action panel. Tap the backdrop to dismiss. */
.msg-overlay { position: fixed; inset: 0; z-index: 95; background: rgba(0,0,0,.6);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 24px; }
.msg-overlay-inner { display: flex; flex-direction: column; align-items: stretch;
  gap: 10px; width: 100%; max-width: 320px; animation: msgOverlayIn .18s cubic-bezier(.22, 1, .36, 1); }
@keyframes msgOverlayIn { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }

.react-bar { display: flex; justify-content: space-around; gap: 2px; align-self: center;
  padding: 6px 8px; border-radius: 999px; background: var(--raised);
  border: 1px solid var(--border-2); box-shadow: 0 8px 24px rgba(0,0,0,.45); }
.react-bar button { width: 34px; height: 34px; flex: none; font-size: 21px; line-height: 1;
  background: transparent; border: 0; border-radius: 50%; cursor: pointer; padding: 0;
  display: grid; place-items: center; transition: background .12s ease, transform .12s ease; }
.react-bar button:hover { background: rgba(255,106,43,.14); }
.react-bar button.on { background: rgba(255,106,43,.24); transform: scale(1.08); }

/* Read-only preview of the tapped message. */
.msg-preview { display: flex; flex-direction: column; gap: 2px; max-width: 100%; }
.msg-preview.mine { align-items: flex-end; }
.msg-preview.theirs { align-items: flex-start; }
.msg-preview .chat-bubble { max-width: 100%; cursor: default; }
.msg-preview.mine .chat-bubble { background: rgba(255,106,43,.22); }

/* Stacked action panel. */
.msg-actions { display: flex; flex-direction: column; gap: 2px; padding: 6px;
  border-radius: var(--r-surface); background: var(--surface); border: 1px solid var(--border-2);
  box-shadow: 0 8px 24px rgba(0,0,0,.45); }
.msg-actions button { width: 100%; text-align: left; border: 0; background: transparent;
  border-radius: var(--r-ctrl); padding: 11px 14px; min-height: 44px; }
.msg-actions button:hover { background: var(--raised); }
.msg-actions button.danger { color: var(--red); }
.msg-actions .msg-cancel { text-align: center; color: var(--text-2); margin-top: 2px;
  border-top: 1px solid var(--border); border-radius: 0; }

/* --- Telegram-style tap menu: bar + compact menu anchored to the message --- */
.msg-pop-scrim { position: fixed; inset: 0; z-index: 95; background: rgba(0,0,0,.45); }
.msg-pop-rx { position: fixed; z-index: 97; display: flex; gap: 2px;
  background: var(--raised); border: 1px solid #2f2f2f; border-radius: 22px;
  padding: 5px 7px; box-shadow: 0 10px 26px rgba(0,0,0,.5); }
.msg-pop-rx button { width: 32px; height: 32px; flex: none; font-size: 20px; line-height: 1;
  display: grid; place-items: center; border: 0; background: transparent;
  border-radius: 50%; cursor: pointer; }
.msg-pop-rx button:hover { background: rgba(255,106,43,.14); }
.msg-pop-rx button.on { background: rgba(255,106,43,.24); }
.msg-pop-menu { position: fixed; z-index: 97; min-width: 180px; max-width: 240px;
  background: var(--surface); border: 1px solid #2a2a2a; border-radius: 12px;
  padding: 5px; box-shadow: 0 16px 40px rgba(0,0,0,.5); }
.msg-pop-item { display: block; width: 100%; text-align: left; border: 0;
  background: transparent; color: var(--text); font-size: 14px; padding: 9px 12px;
  border-radius: 7px; cursor: pointer; }
.msg-pop-item:hover, .msg-pop-item:active { background: var(--raised); }
.msg-pop-item.danger { color: var(--red); }

/* --- user profile card --- */
.profile-scrim { align-items: center; }
.profile-card { width: 100%; max-width: 300px; margin: 0 16px;
  background: var(--surface); border: 1px solid var(--border-2); border-radius: var(--r-surface);
  padding: 22px 20px 18px; display: flex; flex-direction: column; align-items: center; gap: 8px;
  box-shadow: 0 12px 36px rgba(0,0,0,.5);
  background-image: radial-gradient(120% 90% at 50% 0%, rgba(255,106,43,.12), transparent 60%); }
.pf-avatar { width: 76px; height: 76px; border-radius: 50%; overflow: hidden; flex: none;
  display: grid; place-items: center; background: var(--raised); border: 1px solid var(--border-2);
  color: var(--accent); font-family: var(--mono); font-size: 30px; font-weight: 500; }
.pf-avatar img.av-img { width: 100%; height: 100%; object-fit: cover; }
.pf-name { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.pf-name-text { font-family: var(--sans); font-size: 17px; font-weight: 600; color: var(--text); }
.pf-badge { flex: none; font-size: 10px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px; color: var(--accent);
  background: rgba(255,106,43,.14); border: 1px solid rgba(255,106,43,.45); }
.pf-meta { font-size: 13px; color: var(--text-2); }
.pf-online { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--green); }
.pf-online .pf-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); }
.pf-actions { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 10px; width: 100%; }
.pf-btn { border: 0; cursor: pointer; font-family: var(--sans); font-size: 14px; font-weight: 600;
  border-radius: 999px; padding: 8px 18px; min-height: 40px; }
.pf-btn.primary { background: var(--accent); color: var(--on-accent); }
.pf-btn.primary:disabled { background: var(--raised); color: var(--text-2); cursor: default; }
.pf-btn.ghost { background: transparent; color: var(--text-2); border: 1px solid var(--border-2); }
.pf-btn.ghost:hover { background: var(--raised); }
.pf-label { font-size: 13px; color: var(--text-2); text-align: center; padding: 8px 4px; }

/* --- Зворотний зв'язок (user screen + admin queue) --- */
body.guest #pf-feedback { display: none !important; }
.pf-badge { flex: none; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent); font-family: var(--mono); font-size: 11px; font-weight: 700;
  line-height: 18px; text-align: center; }
.pf-badge[hidden] { display: none; }
.fb-form { display: flex; flex-direction: column; gap: 10px; padding: 14px 14px 4px; }
.fb-text { width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px; background: var(--raised);
  border: 1px solid var(--border); border-radius: var(--r-ctrl); color: var(--text); padding: 10px 12px;
  font-family: var(--sans); font-size: 14px; line-height: 1.45; }
.fb-text:focus { border-color: var(--amber); }
.fb-text:focus:not(:focus-visible) { outline: none; }
.fb-counter { text-align: right; margin-top: -4px; }
.fb-form .ok { color: var(--green); font-size: 13px; margin: 0; }
.fb-form .btn-primary { width: 100%; min-height: 44px; }
.fb-diag { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.fb-diag[hidden] { display: none; }
.fb-diag-check { display: flex; align-items: center; gap: 10px; min-height: 44px; font-size: 14px; color: var(--text); cursor: pointer; }
.fb-diag-check input { width: 20px; height: 20px; flex: none; accent-color: var(--amber); }
.fb-pre { margin: 0; width: 100%; box-sizing: border-box; max-height: 240px; overflow: auto; padding: 10px;
  background: var(--panel); border: 1px solid var(--line); color: var(--text-2);
  font-family: var(--mono); font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.fb-pre[hidden] { display: none; }
.fb-list { gap: 10px; padding-bottom: 8px; }
.fb-item { display: flex; flex-direction: column; gap: 8px; padding: 12px 13px; border: 1px solid var(--line); background: var(--panel); }
.fb-item-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.fb-item-kind { font-size: 12px; color: var(--text-2); }
.fb-item-text { font-size: 14px; line-height: 1.45; color: var(--text); white-space: pre-wrap; word-break: break-word; }
.fb-chip { flex: none; border: 1px solid var(--border-2); border-radius: var(--r-ctrl); padding: 2px 8px;
  font-family: var(--sans); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.fb-st-in_progress { color: var(--amber); border-color: var(--amber); }
.fb-st-done { color: var(--green); border-color: var(--green); }
.fb-st-rejected { color: var(--red); border-color: var(--red); }
.fb-reply { border-left: 2px solid var(--amber); background: var(--raised); padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; }
.fb-reply-new { border-left-width: 3px; background: color-mix(in srgb, var(--amber) 12%, var(--raised)); }
.fb-reply-label { font-family: var(--sans); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--amber); }
.review-item .fb-text.ufb-reply { min-height: 64px; font-size: 13px; }
.review-item .fb-item-text { font-size: 12px; }
#ufb-filter { margin: 4px 0 8px; }

/* --- Безпека в чатах: скарги, блокування, правила --- */
.rules-body { padding: 6px 14px 28px; display: flex; flex-direction: column; gap: 8px; }
.rules-intro { margin: 8px 0 0; font-size: 14px; line-height: 1.5; color: var(--text); }
.rules-h { margin: 14px 0 2px; font-family: var(--sans); font-size: 12px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--amber); font-weight: 600; }
.rules-list { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 8px;
  font-size: 13px; line-height: 1.5; color: var(--text-2); }
.rules-ver { margin: 14px 0 0; text-align: center; color: var(--dim); font-size: 11px; }
.rules-scroll { max-height: 70vh; overflow-y: auto; -webkit-overflow-scrolling: touch; padding-right: 4px; }
.rules-scroll .rules-body { padding: 0 0 6px; }
.rules-sheet-title { font-size: 16px; font-weight: 600; color: var(--text); }
.confirm-sheet.rules-sheet .confirm-actions .btn-primary,
.confirm-sheet.rules-sheet .confirm-actions .btn-ghost { flex: 1; }

.report-reasons { display: flex; flex-direction: column; gap: 6px; }
.report-reason { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; min-height: 44px;
  padding: 8px 12px; background: var(--raised); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-ctrl); font-family: var(--sans); font-size: 14px; cursor: pointer; }
.report-reason::before { content: ""; flex: none; width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--border-2); box-sizing: border-box; }
.report-reason[aria-pressed="true"] { border-color: var(--amber); }
.report-reason[aria-pressed="true"]::before { border-color: var(--amber); background: var(--amber);
  box-shadow: inset 0 0 0 3px var(--raised); }
.report-err { margin: 0; color: var(--red); font-size: 13px; }
.report-err[hidden] { display: none; }
.confirm-sheet.report-sheet { max-height: 92vh; overflow-y: auto; }
.confirm-sheet.report-sheet .fb-text { min-height: 72px; }

.bl-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid var(--line); background: var(--panel); }
.bl-ava { flex: none; width: 40px; height: 40px; border-radius: 50%; background: var(--raised);
  border: 1px solid var(--border-2); color: var(--text-2); display: grid; place-items: center;
  font-family: var(--mono); font-size: 15px; overflow: hidden; }
.bl-ava img { width: 100%; height: 100%; object-fit: cover; }
.bl-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.bl-name { font-size: 14px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.blocks-empty { padding: 28px 14px; text-align: center; color: var(--muted); font-size: 14px; }

.cr-quote { white-space: pre-wrap; word-break: break-word; color: var(--text-2); font-size: 12px; line-height: 1.5;
  border-left: 2px solid var(--border-2); padding-left: 8px; }
.cr-deleted { color: var(--dim); font-size: 11px; }
#cr-filter { margin: 4px 0 8px; }

/* Front-line hint under the region chips (see frontlineHintRegions). Amber and
   quiet, like .offline-bar: nothing is broken, the reader just has a better
   option than the whole oblast. Never red - that colour means "you will miss
   an alarm". */
.fl-hint { display: flex; align-items: flex-start; gap: 10px; margin-top: 10px; padding: 11px 12px;
  background: color-mix(in srgb, var(--amber) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--amber) 45%, var(--border)); }
.fl-hint { scroll-margin-bottom: 90px; }
.fl-hint-ob { scroll-margin-bottom: 200px; }
.fl-hint[hidden] { display: none; }
.fl-ic { flex: none; color: var(--amber); display: grid; place-items: center; padding-top: 1px; }
.fl-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 auto; }
.fl-title { font-weight: 600; font-size: 13px; color: var(--amber); overflow-wrap: anywhere; }
.fl-body { margin: 0; font-size: 13px; line-height: 1.4; color: var(--text-2); }
.fl-btn { align-self: flex-start; margin-top: 6px; color: var(--text); border-color: var(--amber); }
.fl-btn:disabled { opacity: .7; cursor: default; color: var(--text-2); border-color: var(--border-2); }
/* On the onboarding's photo background the card gets the frosted treatment the
   .ob-fact rows already use, so the text stays readable. */
.fl-hint-ob { border-radius: 12px; background: rgba(19,20,23,.78); backdrop-filter: blur(10px);
  border-color: color-mix(in srgb, var(--amber) 55%, transparent); }
.fl-hint-ob .fl-body { color: #d8dce2; }
