/* Reusable component primitives — buttons, cards, badges, inputs, modals, toasts.
   Built out starting Phase 1. Everything references tokens.css semantic vars. */

/* Defensive: the [hidden] attribute must always win over a class's own display
   rule (e.g. .modal-row sets display:grid, which otherwise beats the UA
   [hidden] default since our stylesheet loads after it). */
[hidden] { display: none !important; }

/* Minimal button so early auth screens (Phase 1) have a baseline.
   2026-07-13: height trimmed 44px→40px and radius bumped to --radius-lg
   (12px) per polish feedback ("нижче, ширше, radius 12, менше вертикального
   паддінгу") — 40px still clears the WCAG 2.2 24px touch-target minimum by
   a wide margin, so this isn't a shrink to worry about. Wider side padding
   is the "ширше" half of the ask. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: 40px; padding: 0 var(--space-5);
  border: 1px solid transparent; border-radius: var(--radius-lg);
  font-size: var(--text-sm); font-weight: var(--font-semibold);
  background: var(--color-surface); color: var(--color-fg);
  transition: background var(--dur-fast) var(--ease-in-out),
              border-color var(--dur-fast) var(--ease-in-out),
              transform var(--dur-fast) var(--ease-out);
}
.btn:active { transform: scale(0.98); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Generic inline loading spinner for a button's own loading-state text
   (e.g. "Sending invitation…") — .btn:disabled already handles the dimmed/
   not-allowed look, this is just the spinning glyph itself. */
.btn-spinner { display: inline-block; width: 14px; height: 14px; margin-right: var(--space-2); vertical-align: -2px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; opacity: 0.7; animation: btn-spin 0.6s linear infinite; }
@keyframes btn-spin { to { transform: rotate(360deg); } }

.btn--primary { background: var(--color-primary); color: var(--color-on-primary); box-shadow: var(--shadow-xs); }
.btn--primary:hover:not(:disabled) { background: var(--color-primary-hover); box-shadow: var(--shadow-sm); }

.btn--ghost { background: transparent; border-color: var(--color-border); }
.btn--ghost:hover:not(:disabled) { background: var(--color-surface-hover); }

.btn--orange { background: var(--color-orange); color: #fff; }
.btn--orange:hover:not(:disabled) { background: var(--color-orange-hover); }

.btn--danger { color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 46%, var(--color-border)); }
.btn--danger:hover:not(:disabled) { background: var(--color-danger-soft); border-color: var(--color-danger); }

/* Card baseline. */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

/* ── Modal ─────────────────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: grid; place-items: center; padding: var(--space-4);
  background: var(--color-overlay);
  opacity: 0; transition: opacity var(--dur-base) var(--ease-in-out);
}
.modal-overlay--in { opacity: 1; }
.modal {
  position: relative;
  width: 100%; max-width: 460px; max-height: 90vh; max-height: 90dvh; overflow-y: auto;
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-xl); box-shadow: var(--shadow-xl);
  transform: translateY(12px) scale(0.98); transition: transform var(--dur-base) var(--ease-out);
}
.modal--wide { max-width: 680px; }
/* 2026-07-13 — Leave Approval Detail redesign wants a genuinely large
   modal (900-1000px), not just "wide". */
.modal--xl { max-width: 960px; }
.modal-overlay--in .modal { transform: none; }
.modal__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--color-border); }
.modal__head-text { min-width: 0; }
/* 2026-07-13 — optional icon + subtitle row (Invite Employee redesign) —
   only rendered when a caller passes them, so every other modal in the app
   is visually unchanged. */
.modal__title-row { display: flex; align-items: center; gap: var(--space-3); }
.modal__icon { display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--radius-md); flex-shrink: 0; background: var(--color-primary-soft); color: var(--color-primary); }
.modal__icon .icon { width: 19px; height: 19px; }
.modal__title { font-size: var(--text-lg); }
.modal__subtitle { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--color-fg-muted); }
.modal__close { display: grid; place-items: center; width: 32px; height: 32px; border: 0; border-radius: var(--radius-md); background: var(--color-surface-2); color: var(--color-fg-muted); flex-shrink: 0; }
.modal__close:hover { background: var(--color-surface-hover); }
.modal__body { padding: var(--space-6); }

.modal-form { display: flex; flex-direction: column; gap: var(--space-4); }
.modal-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.modal-label { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-on-surface); }
.modal-input {
  /* width:100% + min-width:0 so a field with long intrinsic content (e.g. a
     <select> with long option text, like the Company settings timezone
     picker) shrinks to fit its grid/flex column instead of forcing the
     column — and the whole .modal-row — wider than its container. */
  width: 100%; min-width: 0; box-sizing: border-box;
  height: 44px; padding: 0 var(--space-3); font: inherit; font-size: var(--text-sm);
  color: var(--color-fg); background: var(--color-surface-2);
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
}
.modal-input:focus { outline: none; border-color: var(--color-ring); box-shadow: 0 0 0 3px var(--color-primary-soft); }
/* The dropdown popup itself always shows full option text regardless of the
   closed box's width — only the closed, selected-value display needs to
   truncate gracefully instead of being hard-clipped by the browser. */
select.modal-input {
  cursor: pointer; overflow: hidden; padding-block: 0;
  line-height: normal; text-overflow: ellipsis; white-space: nowrap;
}
select.modal-input[multiple],
select.modal-input[size]:not([size="1"]) {
  height: auto;
  min-height: 112px;
  padding-block: var(--space-2);
  line-height: 1.4;
  white-space: normal;
}
select.modal-input[multiple] option,
select.modal-input[size]:not([size="1"]) option {
  padding: 6px var(--space-2);
}
/* Native file controls use a different baseline for the browser-provided
   button and filename (especially in Safari). This keeps any legacy/fallback
   file input aligned; new upload surfaces use .file-picker below. */
input[type="file"].modal-input {
  padding: 5px 6px;
  line-height: 32px;
  color: var(--color-fg-muted);
}
input[type="file"].modal-input::file-selector-button {
  height: 32px;
  margin: 0 var(--space-3) 0 0;
  padding: 0 var(--space-3);
  vertical-align: middle;
  border: 0;
  border-radius: 8px;
  background: var(--color-primary-soft);
  color: var(--color-primary);
  font: inherit;
  font-weight: var(--font-semibold);
  line-height: 32px;
  cursor: pointer;
}

/* Cross-browser file picker. The native input remains available to assistive
   technology and handles the actual file selection, while button and filename
   share one predictable flex baseline. */
.file-picker {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  min-width: 0;
  height: 44px;
  box-sizing: border-box;
  gap: var(--space-3);
  padding: 5px 7px;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: 11px;
  background: var(--color-surface);
  box-shadow: 0 1px 2px rgba(15,23,42,.025);
  cursor: pointer;
}
.file-picker:hover { border-color: var(--color-border-strong); }
.file-picker:focus-within {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary-soft) 76%, transparent);
}
.file-picker input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}
.file-picker__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  flex: 0 0 auto;
  padding: 0 var(--space-3);
  border-radius: 8px;
  background: var(--color-primary-soft);
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: var(--font-semibold);
  line-height: 1;
}
.file-picker__name {
  min-width: 0;
  flex: 1;
  overflow: hidden;
  color: var(--color-fg-muted);
  font-size: var(--text-sm);
  font-weight: var(--font-normal);
  line-height: 1.25;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-picker.has-file .file-picker__name { color: var(--color-fg); font-weight: var(--font-medium); }
.file-picker--compact { height: 42px; }
.file-picker--compact .file-picker__button { height: 30px; padding-inline: 10px; font-size: var(--text-xs); }
.file-picker--compact .file-picker__name { font-size: var(--text-xs); }
/* Multi-line fields are content-sized by textarea-autogrow.js. Unlike the
   generic input primitive they must not inherit a fixed 42/44px height. */
textarea.modal-input {
  height: auto;
  min-height: 74px;
  padding-block: var(--space-3);
  line-height: 1.5;
  box-sizing: border-box;
  resize: vertical;
}
textarea.modal-input[data-autogrow-ready] {
  transition: height 120ms var(--ease-out);
}
.modal-error { min-height: 1em; font-size: var(--text-sm); color: var(--color-danger); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: var(--space-2); }

/* Consistent action hierarchy for every dialog. Destructive/secondary
   controls may move left, but primary actions always stay grouped and every
   label is allowed to wrap instead of overflowing the viewport. */
.modal-actions > * { min-width: 0; }
.modal-actions .btn { max-width: 100%; white-space: normal; text-align: center; }
.modal-actions--start { justify-content: flex-start; }
.modal-actions__leading { margin-right: auto; }
.section-action-row { margin-bottom: var(--space-4); }
.section-action-row--top { margin-top: var(--space-4); }

/* Shared workflow editor used by goals, reviews, development, assets,
   documents, tasks and requests.  The feature modules keep their own data
   and submit handlers; this class only gives their forms one predictable
   visual hierarchy and mobile behaviour. */
.workflow-modal {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-color: color-mix(in srgb, var(--color-primary) 14%, var(--color-border));
  border-radius: 24px;
  box-shadow: 0 30px 88px color-mix(in srgb, var(--color-fg) 22%, transparent);
}
.workflow-modal.modal--wide { max-width: 840px; }
.workflow-modal .modal__head {
  flex: 0 0 auto;
  padding: 28px 32px 24px;
  background: radial-gradient(circle at 90% -30%, color-mix(in srgb, var(--color-primary) 17%, transparent), transparent 44%), var(--color-surface);
}
.workflow-modal .modal__icon { width: 44px; height: 44px; border-radius: 14px; }
.workflow-modal .modal__title { font-size: var(--text-xl); line-height: var(--leading-snug); letter-spacing: -.025em; overflow-wrap: anywhere; }
.workflow-modal .modal__subtitle { max-width: 550px; margin: var(--space-2) 0 0 56px; line-height: var(--leading-relaxed); overflow-wrap: anywhere; }
.workflow-modal .modal__close { width: 38px; height: 38px; border: 1px solid var(--color-border); background: var(--color-surface); }
.workflow-modal .modal__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--space-5) var(--space-6) 0; }
.workflow-modal .modal-form { gap: var(--space-4); }
.workflow-modal .modal-label { gap: 7px; color: var(--color-fg-muted); font-size: var(--text-xs); font-weight: var(--font-semibold); }
.workflow-modal .modal-input { min-height: 46px; border-radius: var(--radius-lg); background: var(--color-surface); color: var(--color-fg); font-weight: var(--font-normal); }
.workflow-modal textarea.modal-input { min-height: 100px; line-height: var(--leading-relaxed); }
.workflow-form-section {
  display: grid;
  gap: var(--space-4);
  padding: 0 0 var(--space-5);
  border: 0;
  border-bottom: 1px solid var(--color-border);
  border-radius: 0;
  background: transparent;
}
.workflow-form-section:last-of-type { padding-bottom: 0; border-bottom: 0; }
.workflow-form-section__head { display: flex; align-items: flex-start; gap: var(--space-3); }
.workflow-form-section__head > span { display: grid; place-items: center; flex: 0 0 34px; width: 34px; height: 34px; border-radius: 11px; background: var(--color-primary-soft); color: var(--color-primary); font-size: var(--text-xs); font-weight: var(--font-bold); }
.workflow-form-section__head h3 { margin: 0; color: var(--color-fg); font-size: var(--text-sm); }
.workflow-form-section__head p { margin: 3px 0 0; color: var(--color-fg-subtle); font-size: var(--text-xs); font-weight: var(--font-normal); line-height: 1.45; }
.workflow-modal .modal-actions {
  position: sticky;
  bottom: 0;
  z-index: 3;
  flex-wrap: wrap;
  margin: var(--space-2) calc(-1 * var(--space-6)) 0;
  padding: var(--space-4) var(--space-6);
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
  box-shadow: 0 -12px 22px color-mix(in srgb, var(--color-surface) 88%, transparent);
}
.survey-create-modal .modal__body { min-height: min(560px, calc(100vh - 190px)); min-height: min(560px, calc(100dvh - 190px)); }
.request-create-modal .modal__body { min-height: min(520px, calc(100vh - 190px)); min-height: min(520px, calc(100dvh - 190px)); }
.profile-action-modal .workflow-form-section,
.referral-modal .workflow-form-section { padding-top: var(--space-1); }
.lms-lesson-row { display: grid; gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface-2); }
.content-language-tabs {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  padding: 4px;
  border-radius: var(--radius-lg);
  background: var(--color-surface-2);
}
.content-language-tabs button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  min-width: 0;
  min-height: 40px;
  gap: 7px;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-fg-muted);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: var(--font-semibold);
  cursor: pointer;
}
.content-language-tabs button.is-active { background: var(--color-surface); color: var(--color-fg); box-shadow: var(--shadow-xs); }
.content-language-tabs button > span { display: grid; place-items: center; width: 24px; height: 20px; border-radius: 6px; background: var(--color-primary-soft); color: var(--color-primary); font-size: 8px; font-weight: var(--font-extrabold); }
.content-language-tabs button > i { padding: 2px 6px; border-radius: var(--radius-full); background: var(--color-warning-soft); color: var(--color-warning); font-size: 8px; font-style: normal; }
.content-language-tabs button > i.is-optional { background: var(--color-surface-hover); color: var(--color-fg-muted); }
.content-language-hint { margin: calc(-1 * var(--space-2)) 0 0; color: var(--color-fg-subtle); font-size: var(--text-xs); line-height: 1.45; }
.content-language-panel { display: grid; gap: var(--space-4); }
.content-language-panel[hidden] { display: none; }
@media (max-width: 600px) {
  /* padding-top reserves the status bar's own height (--safe-area-top —
     0 on the web, the real inset on Android) so a modal tall enough to
     nearly fill the screen still starts below it instead of its rounded
     top corner rendering underneath/behind the status bar (Android only —
     the WebView draws edge-to-edge there; Chrome always has its own
     chrome above the page and never had this problem). */
  .modal-overlay { align-items: end; padding: var(--space-2); padding-top: max(var(--space-2), var(--safe-area-top)); }
  .workflow-modal { max-height: calc(100vh - var(--space-4) - var(--safe-area-top)); max-height: calc(100dvh - var(--space-4) - var(--safe-area-top)); border-radius: 22px; }
  .workflow-modal .modal__head, .workflow-modal .modal__body { padding-inline: var(--space-5); }
  .workflow-modal .modal__head { padding-block: var(--space-5) var(--space-4); }
  .workflow-modal .modal__body { padding-bottom: var(--space-5); }
  .workflow-modal .modal__subtitle { margin-left: 0; }
  .workflow-modal .modal-row { grid-template-columns: 1fr; }
  .workflow-form-section { padding: 0 0 var(--space-4); }
  /* Was a forced two-column row (flex:1 1 140px each) — a longer primary
     label ("Надіслати запрошення →") had no room to stay on one line at
     mobile widths and wrapped to two, reading as a bulky, mis-sized button.
     Multiple attempts at fixing this via plain specificity (even a
     deliberately bumped .workflow-modal.workflow-modal selector) kept
     silently losing to the generic .modal-actions:not(.modal-actions
     --inline) rule further down this file (or to flex-wrap:wrap inherited
     from the un-scoped .workflow-modal .modal-actions rule above, which
     wraps onto a second line rather than shrinking even with flex-shrink
     enabled). !important on every property that rule (or that inherited
     flex-wrap) could otherwise contest removes the ambiguity outright —
     one row, no wrap, "Скасувати" sized to its own short label, the
     primary button given the rest of the row's width for its longer
     label. */
  .workflow-modal .modal-actions { position: static !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: stretch; margin: 0; padding: var(--space-4) 0 0; border-top: 1px solid var(--color-border); background: transparent; backdrop-filter: none; }
  .workflow-modal .modal-actions .btn { max-width: 100%; white-space: normal; }
  .workflow-modal .modal-actions .btn--ghost { flex: 0 0 auto !important; width: auto !important; }
  .workflow-modal .modal-actions .btn--primary { flex: 1 1 auto !important; width: auto !important; min-width: 0; }
  .content-language-tabs button { padding-inline: var(--space-2); }
  .content-language-tabs button > i { display: none; }
}
/* 2026-07-13 — sticky footer for long forms (Edit Employee redesign) — bleeds
   to the edges of .modal__body's own padding so it reads as a full-width bar
   pinned to the bottom of the modal's scroll area, not just another row of
   content. Opt-in modifier — other modals' footers are unaffected. */
.modal-actions--sticky {
  /* bottom must be 0, not negative — a negative offset lets the sticky
     calculation let the bar overshoot past the modal's own visible bottom
     edge, so it never sits fully in view until you scroll to the true end
     of the content (2026-08-02 fix). */
  position: sticky; bottom: 0; z-index: 2;
  margin: var(--space-2) calc(-1 * var(--space-6)) calc(-1 * var(--space-6));
  padding: var(--space-4) var(--space-6); background: var(--color-surface); border-top: 1px solid var(--color-border);
}

/* Calendar icon inside a date input — same "icon inside the field" device
   .people-search/.hdr-search already use, just for a date field instead of
   a search field. Purely decorative: the underlying native date input and
   its behavior are unchanged. */
.modal-date-wrap { position: relative; }
.modal-date-wrap input[type="date"] { padding-right: 40px; }
.modal-date-wrap__icon { position: absolute; right: var(--space-3); top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--color-fg-subtle); pointer-events: none; }

/* "Discard unsaved changes?" guard (Edit Employee redesign) — an overlay
   inside the modal itself (not a second openModal()) so "Continue editing"
   simply removes this panel and the underlying form's typed values are
   still exactly as the user left them. */
.discard-confirm { position: absolute; inset: 0; z-index: 10; display: grid; place-items: center; padding: var(--space-6); background: var(--color-overlay); border-radius: var(--radius-xl); animation: card-in var(--dur-fast) var(--ease-out) both; }
.discard-confirm__box { width: 100%; max-width: 320px; padding: var(--space-6); background: var(--color-surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-xl); text-align: center; }
.discard-confirm__box h3 { font-size: var(--text-base); font-weight: var(--font-bold); margin: 0 0 var(--space-2); }
.discard-confirm__box p { margin: 0 0 var(--space-5); font-size: var(--text-sm); color: var(--color-fg-muted); }
.discard-confirm__actions { display: flex; gap: var(--space-3); justify-content: center; }

/* Was 480px — too narrow to actually cover most phones (many modern devices
   report 481-560px CSS width), so a 2-up row with long-content fields (e.g.
   Company settings' timezone/country selects) stayed squeezed side-by-side
   right where it needed to stack. 560px matches this app's other established
   "small phone" breakpoint (people.css/dashboard.css). */
@media (max-width: 560px) { .modal-row { grid-template-columns: 1fr; } }

/* Shared operational surfaces. A number of modules previously supplied
   slightly different table/empty-state implementations, which made the same
   state look unrelated from page to page. These primitives deliberately use
   low-specificity selectors so a module can still add domain details. */
:where(.table-wrap) {
  width: 100%;
  max-width: 100%;
  overflow: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  box-shadow: var(--shadow-xs);
  scrollbar-gutter: stable;
}

:where(.data-table) {
  width: 100%;
  min-width: 640px;
  border-collapse: separate;
  border-spacing: 0;
  color: var(--color-fg);
  font-size: var(--text-sm);
}

:where(.data-table) th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 12px 16px;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-fg-muted);
  font-size: var(--text-xs);
  font-weight: var(--font-semibold);
  letter-spacing: .035em;
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}

:where(.data-table) td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}

:where(.data-table) tbody tr:last-child td { border-bottom: 0; }
:where(.data-table) tbody tr { transition: background var(--dur-fast) var(--ease-out); }
:where(.data-table) tbody tr:hover { background: var(--color-surface-hover); }

:where(.empty-state) {
  display: grid;
  min-height: 190px;
  place-items: center;
  align-content: center;
  gap: var(--space-3);
  padding: var(--space-8) var(--surface-padding);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-xl);
  background: color-mix(in srgb, var(--color-surface-2) 72%, var(--color-surface));
  color: var(--color-fg-muted);
  text-align: center;
}
/* A generic "empty tray" glyph automatically prefixed onto every .empty-state
   via ::before — every call site across the app just writes a <p>, no icon
   markup to keep in sync, so newly added empty states inherit this for free. */
:where(.empty-state)::before {
  content: '';
  order: -1;
  width: 40px;
  height: 40px;
  background-color: currentColor;
  opacity: 0.4;
  -webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0zIDEyaDRsMiAzaDZsMi0zaDQiLz48cGF0aCBkPSJNNS41IDVoMTNMMjEgMTJ2NmExLjUgMS41IDAgMCAxLTEuNSAxLjVoLTE1QTEuNSAxLjUgMCAwIDEgMyAxOHYtNkw1LjUgNVoiLz48L3N2Zz4=") center / contain no-repeat;
  mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0zIDEyaDRsMiAzaDZsMi0zaDQiLz48cGF0aCBkPSJNNS41IDVoMTNMMjEgMTJ2NmExLjUgMS41IDAgMCAxLTEuNSAxLjVoLTE1QTEuNSAxLjUgMCAwIDEgMyAxOHYtNkw1LjUgNVoiLz48L3N2Zz4=") center / contain no-repeat;
}
:where(.empty-state--sm)::before { width: 28px; height: 28px; }

:where(.empty-state--sm) { min-height: 112px; padding-block: var(--space-5); }

/* Route-level loading keeps the same spatial rhythm as a real workspace.
   Lazy modules used to flash a single centred text line before mounting a
   full hero + tab rail, which made the People tabs visibly jump on Android.
   This stable shell reserves those blocks while the module is downloaded. */
.route-loading {
  --route-shimmer-a: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface-2));
  --route-shimmer-b: color-mix(in srgb, var(--color-primary) 17%, var(--color-surface));
  display: grid;
  gap: var(--space-6);
  min-height: min(680px, calc(100dvh - var(--header-height, 64px)));
  padding-bottom: var(--space-8);
}
.route-loading__hero {
  display: grid;
  align-content: end;
  min-height: 196px;
  padding: var(--space-6) var(--space-6) 0;
  border-bottom: 1px solid color-mix(in srgb, var(--color-primary) 14%, var(--color-border));
  background: color-mix(in srgb, var(--color-surface) 92%, var(--color-primary-soft));
}
.route-loading__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: max-content;
  margin-bottom: 14px;
  color: var(--color-fg-muted);
  font-size: 11px;
  font-weight: var(--font-bold);
  letter-spacing: .08em;
  text-transform: uppercase;
}
.route-loading__pulse {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-primary) 12%, transparent);
  animation: route-pulse 1.4s var(--ease-in-out) infinite;
}
.route-loading__line,
.route-loading__summary > span,
.route-loading__tabs > span {
  display: block;
  overflow: hidden;
  border-radius: var(--radius-full);
  background: var(--route-shimmer-a);
  background-image: linear-gradient(100deg, transparent 15%, var(--route-shimmer-b) 46%, transparent 78%);
  background-position: 140% 0;
  background-size: 220% 100%;
  animation: route-shimmer 1.35s linear infinite;
}
.route-loading__line--title { width: min(420px, 68vw); height: 34px; }
.route-loading__line--copy { width: min(560px, 76vw); height: 13px; margin-top: 12px; }
.route-loading__tabs { display: flex; gap: 28px; min-height: 55px; margin-top: 20px; align-items: center; }
.route-loading__tabs > span { width: 88px; height: 12px; }
.route-loading__tabs > span:nth-child(2) { width: 118px; }
.route-loading__tabs > span:nth-child(3) { width: 74px; }
.route-loading__body { display: grid; gap: var(--space-5); padding-inline: var(--space-6); }
.route-loading__summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-4); }
.route-loading__summary > span { height: 92px; border-radius: var(--radius-lg); }
.route-loading__content {
  display: grid;
  gap: 18px;
  padding: var(--space-6);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
}
.route-loading__line--row { width: 100%; height: 14px; }
.route-loading__line--short { width: 62%; }
.route-loading__hint { margin: -8px 0 0; color: var(--color-fg-subtle); font-size: var(--text-sm); text-align: center; }

.route-error {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-4);
  max-width: 760px;
  margin: clamp(40px, 10vh, 112px) auto;
  padding: var(--space-5);
  border: 1px solid color-mix(in srgb, var(--color-danger) 24%, var(--color-border));
  border-radius: var(--radius-xl);
  background: color-mix(in srgb, var(--color-danger-soft) 42%, var(--color-surface));
  box-shadow: var(--shadow-sm);
}
.route-error__icon { display: grid; width: 44px; height: 44px; place-items: center; border-radius: var(--radius-md); background: var(--color-danger-soft); color: var(--color-danger); }
.route-error__icon .icon { width: 21px; height: 21px; }
.route-error__copy h2 { margin: 0; color: var(--color-fg); font-family: 'Sora', var(--font-sans); font-size: var(--text-lg); }
.route-error__copy p { margin: 5px 0 0; color: var(--color-fg-muted); font-size: var(--text-sm); }

@keyframes route-shimmer { to { background-position: -120% 0; } }
@keyframes route-pulse { 50% { opacity: .45; transform: scale(.8); } }

@media (max-width: 720px) {
  .route-loading { gap: var(--space-4); min-height: calc(100dvh - var(--header-height, 56px) - 64px); }
  .route-loading__hero { min-height: 176px; padding-inline: var(--space-4); }
  .route-loading__tabs { gap: 22px; overflow: hidden; }
  .route-loading__body { padding-inline: var(--space-4); }
  .route-loading__summary { grid-template-columns: 1fr 1fr; }
  .route-loading__summary > span:last-child { display: none; }
  .route-loading__content { padding: var(--space-5); }
  .route-error { grid-template-columns: auto 1fr; margin: 48px var(--space-4); }
  .route-error .btn { grid-column: 1 / -1; width: 100%; }
}

/* Full-screen "no connection" state — Android-only (app.js's
   checkAndroidConnectivity()), replaces #app entirely rather than sitting
   inside it like .empty-state, since there's no shell/sidebar to show yet
   on a boot that never reached a session. */
.app-offline {
  display: grid;
  place-items: center;
  gap: var(--space-4);
  min-height: 100vh; min-height: 100dvh;
  padding: var(--space-8) var(--space-5);
  text-align: center;
}
.app-offline__icon { width: 56px; height: 56px; border-radius: var(--radius-lg); opacity: .85; }
.app-offline__title { font-size: var(--text-xl); font-weight: var(--font-semibold); color: var(--color-fg); margin: 0; }
.app-offline__text { max-width: 320px; color: var(--color-fg-muted); margin: 0; }
.app-offline__retry { margin-top: var(--space-2); }

@media (max-width: 560px) {
  .modal-actions:not(.modal-actions--inline) { flex-direction: column-reverse; }
  .modal-actions:not(.modal-actions--inline) .btn { width: 100%; }
  .modal-actions__leading { margin-right: 0; }
  :where(.table-wrap) { border-radius: var(--radius-lg); }
  :where(.empty-state) { min-height: 150px; padding-block: var(--space-6); }
}

/* ── Status chips ──────────────────────────────────────────────────────────
   2026-07-13 — one dot treatment for every colored status/priority pill in
   the app (people.css's .status-badge, leaves.css's .status-badge.lv-*,
   documents.css's .prio-badge/.doc-exp, perform.css's .goal-conf/
   .dev-action__status) — each family already sets its own text color per
   state, so `background: currentColor` on the dot picks that up for free;
   this rule only needs to exist once, here, rather than duplicated per
   family. `.doc-exp--ok` is deliberately excluded — it's a plain expiry
   date, not a colored status, so a dot there would be a false signal. */
.status-badge, .prio-badge, .doc-exp, .goal-conf, .dev-action__status {
  display: inline-flex; align-items: center; gap: 5px;
}
.status-badge::before, .prio-badge::before, .doc-exp:not(.doc-exp--ok)::before,
.goal-conf::before, .dev-action__status::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0;
}

/* ── Toast ─────────────────────────────────────────────────────────────── */
.toast-stack { position: fixed; bottom: var(--space-6); right: var(--space-6); z-index: var(--z-toast); display: flex; flex-direction: column; gap: var(--space-2); }
.toast {
  min-width: 240px; max-width: 360px; padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md); background: var(--color-surface); color: var(--color-fg);
  border: 1px solid var(--color-border); border-left: 3px solid var(--color-info);
  box-shadow: var(--shadow-xl); font-size: var(--text-sm); cursor: pointer;
  display: flex; align-items: center; gap: var(--space-2);
  opacity: 0; transform: translateX(16px) scale(0.96); transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.toast--in { opacity: 1; transform: none; }
.toast--success { border-left-color: var(--color-success); }
.toast--error { border-left-color: var(--color-danger); }
.toast--warning { border-left-color: var(--color-warning); }
.toast--info { border-left-color: var(--color-info); }
.toast__icon {
  flex-shrink: 0; width: 18px; height: 18px; color: var(--color-success);
  transform: scale(0.5); opacity: 0;
  transition: transform 0.3s var(--ease-out) 0.1s, opacity 0.3s var(--ease-out) 0.1s;
}
.toast--in .toast__icon { transform: scale(1); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .toast__icon { transition: none; }
}
.toast__undo-btn {
  flex-shrink: 0; margin-left: auto; padding: var(--space-1) var(--space-2);
  border: none; background: none; color: var(--color-primary);
  font: inherit; font-size: var(--text-sm); font-weight: var(--font-semibold); cursor: pointer;
}
.toast__undo-btn:hover { text-decoration: underline; }

/* ── Android first-login tour (public/js/components/swipe-tutorial.js) ────
   Android-app-only (window.HRlumeNative gate in the JS itself), shown once
   after first login: three short steps (drawer gesture, back gesture,
   updates), user-paced rather than auto-dismissing. */
.app-tour {
  position: fixed; inset: 0; z-index: var(--z-toast);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-6);
  background: color-mix(in srgb, var(--color-bg) 78%, transparent);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  opacity: 0; transition: opacity var(--dur-base) var(--ease-out);
}
.app-tour.is-in { opacity: 1; }
.app-tour__card {
  width: min(90vw, 340px);
  display: flex; flex-direction: column; align-items: center; gap: var(--space-4);
  padding: var(--space-6) var(--space-5);
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-xl); box-shadow: var(--shadow-xl);
  transform: scale(.96) translateY(6px); opacity: 0;
  animation: app-tour-card-in var(--dur-slow, 300ms) var(--ease-out) forwards;
}
@keyframes app-tour-card-in { to { transform: none; opacity: 1; } }

.app-tour__stage {
  position: relative; width: 100%; height: 96px;
  border-radius: var(--radius-lg); background: var(--color-surface-2);
  overflow: hidden; display: flex; align-items: center; justify-content: center;
}
.app-tour__phone-edge {
  position: absolute; left: 28px; top: 12px; bottom: 12px; width: 2px;
  background: var(--color-border-strong); border-radius: var(--radius-full);
}
.app-tour__phone-edge--full { left: 0; }
.app-tour__track { position: relative; width: 100%; height: 44px; }
.app-tour__chevron {
  position: absolute; left: var(--space-5); top: 50%; transform: translateY(-50%);
  color: var(--color-fg-subtle); opacity: .5;
}
.app-tour__chevron svg { width: 20px; height: 20px; }
.app-tour__chevron--back { left: auto; right: var(--space-5); }
.app-tour__dot {
  position: absolute; top: 50%; left: 20px; width: 40px; height: 40px;
  border-radius: 50%; background: var(--color-primary);
  box-shadow: 0 0 16px 2px color-mix(in srgb, var(--color-primary) 55%, transparent);
  animation: app-tour-slide 1.8s ease-in-out infinite;
}
.app-tour__track--back .app-tour__dot { left: 0; animation-name: app-tour-slide-back; }
@keyframes app-tour-slide {
  0%, 15% { transform: translate(0, -50%); opacity: 1; }
  55% { transform: translate(170px, -50%); opacity: 1; }
  70%, 100% { transform: translate(170px, -50%); opacity: 0; }
}
@keyframes app-tour-slide-back {
  0%, 15% { transform: translate(220px, -50%); opacity: 1; }
  55% { transform: translate(0, -50%); opacity: 1; }
  70%, 100% { transform: translate(0, -50%); opacity: 0; }
}
.app-tour__update-ring {
  position: absolute; width: 56px; height: 56px; border-radius: 50%;
  border: 2px solid var(--color-primary); opacity: 0;
  animation: app-tour-ring-pulse 1.8s ease-out infinite;
}
.app-tour__update-icon {
  position: relative; width: 32px; height: 32px; color: var(--color-primary);
  animation: app-tour-spin 1.8s ease-in-out infinite;
}
@keyframes app-tour-ring-pulse {
  0% { transform: scale(.7); opacity: .6; }
  100% { transform: scale(1.15); opacity: 0; }
}
@keyframes app-tour-spin {
  0%, 40% { transform: rotate(0deg); }
  70% { transform: rotate(320deg); }
  100% { transform: rotate(360deg); }
}
.app-tour__bell-icon {
  position: relative; width: 34px; height: 34px; color: var(--color-primary);
  transform-origin: 50% 12%;
  animation: app-tour-bell-ring 1.8s ease-in-out infinite;
}
.app-tour__bell-dot {
  position: absolute; top: 22px; right: 34px; width: 10px; height: 10px;
  border-radius: 50%; background: var(--color-danger);
  box-shadow: 0 0 0 2px var(--color-surface-2);
}
@keyframes app-tour-bell-ring {
  0%, 55%, 100% { transform: rotate(0deg); }
  60% { transform: rotate(-14deg); }
  65% { transform: rotate(11deg); }
  70% { transform: rotate(-8deg); }
  75% { transform: rotate(5deg); }
  80% { transform: rotate(-2deg); }
}

.app-tour__dots { display: flex; gap: var(--space-2); }
.app-tour__dotnav { width: 6px; height: 6px; border-radius: 50%; background: var(--color-border-strong); transition: background var(--dur-fast) var(--ease-out), width var(--dur-fast) var(--ease-out); }
.app-tour__dotnav.is-active { width: 18px; border-radius: var(--radius-full); background: var(--color-primary); }

.app-tour__title { margin: 0; font-size: var(--text-lg); font-weight: var(--font-semibold); color: var(--color-fg); text-align: center; }
.app-tour__text { margin: 0; max-width: 280px; text-align: center; font-size: var(--text-sm); color: var(--color-fg-muted); }
.app-tour__actions { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: var(--space-3); margin-top: var(--space-1); }
.app-tour__actions:has(.app-tour__next:only-child) { justify-content: center; }
.app-tour__skip { background: none; border: none; padding: var(--space-2); font-size: var(--text-sm); color: var(--color-fg-muted); cursor: pointer; }
.app-tour__skip:hover { color: var(--color-fg); }
.app-tour__next { min-width: 120px; }

@media (prefers-reduced-motion: reduce) {
  .app-tour__dot, .app-tour__update-ring, .app-tour__update-icon { animation: none; }
  .app-tour__card { animation-duration: 1ms; }
}

/* Pull-to-refresh indicator (Android app only — see app.js's
   setupPullToRefresh()). Fixed at the top, hidden by default (opacity 0 +
   scaled down); --pull-progress (0-1, set live during the drag) drives the
   spinner's rotation before release, .is-refreshing takes over with a
   continuous spin once the pull committed. */
.pull-refresh {
  position: fixed; top: var(--safe-area-top, 0px); left: 50%;
  width: 36px; height: 36px; margin-top: 6px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-full); background: var(--color-surface-2);
  box-shadow: var(--shadow-md);
  opacity: 0; transform: translate(-50%, -40px) scale(0.7);
  z-index: var(--z-toast); pointer-events: none;
}
.pull-refresh.is-visible { opacity: 1; }
.pull-refresh__spinner {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--color-border-strong);
  border-top-color: var(--color-primary);
  transform: rotate(calc(var(--pull-progress, 0) * 360deg));
}
.pull-refresh.is-refreshing .pull-refresh__spinner {
  animation: pull-refresh-spin 0.7s linear infinite;
}
@keyframes pull-refresh-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .pull-refresh.is-refreshing .pull-refresh__spinner { animation: none; }
}

/* Edge-swipe reminder (Android app only — see app.js's setupEdgeHint()). A
   permanent, low-key tab at the left edge, roughly where the drawer-open
   swipe zone actually starts (MainActivity's EDGE_ZONE_DP). Went through
   three rounds of feedback: a thin gradient line was too subtle; a solid-
   fill+heavy-glow+3x-sharp-pulse version was the opposite ("вирвиглядно" —
   jarring); a softer one-time version (2 gentle pulses, then gone for good
   after a fixed show-count) kept reading as "the animation got removed"
   once that count was used up during testing — a quota is fundamentally
   the wrong shape for an ongoing discoverability aid. This version never
   goes away (hidden only while the drawer is actually open) and breathes
   continuously, but slowly/subtly — a persistent presence, not a repeated
   attention-grab. */
.edge-swipe-hint {
  position: fixed; left: 0; top: 50%; transform: translate(-100%, -50%);
  display: flex; align-items: center; justify-content: center;
  width: 14px; height: 76px; border-radius: 0 var(--radius-full) var(--radius-full) 0;
  background: color-mix(in srgb, var(--color-primary) 62%, var(--color-surface) 38%);
  color: var(--color-on-primary);
  box-shadow: 0 1px 8px color-mix(in srgb, var(--color-fg) 14%, transparent);
  opacity: 0; pointer-events: none; z-index: var(--z-toast);
  transition: transform 0.6s var(--ease-out), opacity 0.6s var(--ease-out);
}
.edge-swipe-hint .icon { width: 11px; height: 11px; }
.edge-swipe-hint.is-visible {
  transform: translate(0, -50%);
  opacity: 0.88;
  animation: edge-swipe-hint-breathe 2.6s ease-in-out infinite;
}
@keyframes edge-swipe-hint-breathe {
  0%, 100% { transform: translate(0, -50%); opacity: 0.72; }
  50% { transform: translate(4px, -50%); opacity: 0.95; }
}
@media (prefers-reduced-motion: reduce) {
  .edge-swipe-hint.is-visible { animation: none; opacity: 0.8; }
}

/* Full-screen search overlay, opened from the mobile bottom-nav's "Пошук"
   button (app.js's openSearchOverlay()) — the bottom nav replaced its old
   Notifications slot with this, since notifications already have the header
   bell. Reuses wireGlobalSearch()/the same .search-results-menu classes the
   desktop header and dashboard's own mobile search already render into. */
.search-overlay {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: var(--color-surface);
  display: flex; flex-direction: column;
  padding-top: var(--safe-area-top);
  transform: translateY(-100%); opacity: 0;
  transition: transform 0.25s var(--ease-out), opacity 0.25s var(--ease-out);
  pointer-events: none;
}
.search-overlay.is-open { transform: translateY(0); opacity: 1; pointer-events: auto; }
.search-overlay__bar {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  flex-shrink: 0;
}
/* Same rounded-pill input treatment as the dashboard's own mobile search
   (.dash-search, modules/dashboard.css) rather than a bare borderless bar —
   keeps the two visually consistent instead of inventing a second look. */
.search-overlay__input-wrap { position: relative; flex: 1; min-width: 0; display: flex; align-items: center; }
.search-overlay__input-wrap .icon { position: absolute; left: var(--space-4); width: 18px; height: 18px; color: var(--color-fg-subtle); pointer-events: none; }
.search-overlay__bar input {
  width: 100%; height: 48px; padding: 0 var(--space-4) 0 var(--space-12);
  font: inherit; font-size: var(--text-base); color: var(--color-fg);
  background: var(--color-surface-hover); border: 1px solid var(--color-border); border-radius: var(--radius-full);
}
.search-overlay__bar input:focus {
  outline: none; border-color: var(--color-ring);
  box-shadow: 0 0 0 3px var(--color-primary-soft); background: var(--color-surface);
}
.search-overlay .search-results-menu {
  position: static; flex: 1; overflow-y: auto;
  max-height: none; border: none; box-shadow: none; border-radius: 0; padding: var(--space-2) 0;
}
.search-overlay .search-results-menu[hidden] { display: none; }
.search-overlay .search-result-summary,
.search-overlay .search-result-help,
.search-overlay .search-index,
.search-overlay .search-state,
.search-overlay .search-result-group { width: min(720px, 100%); margin-left: auto; margin-right: auto; box-sizing: border-box; }
@media (max-width: 480px) {
  .search-quick-grid { grid-template-columns: 1fr; }
  .search-result-help { justify-content: space-between; gap: var(--space-2); }
}

/* Live network-status banner (app.js's showNetBanner()) — top-anchored,
   above the header, in front of everything else while a connection drop is
   actually in progress. */
.net-banner {
  position: fixed; left: 0; right: 0; top: 0; z-index: var(--z-toast);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-2) var(--space-4);
  padding-top: calc(var(--space-2) + var(--safe-area-top));
  font-size: var(--text-sm); font-weight: var(--font-medium); color: #fff; text-align: center;
  transform: translateY(-100%); transition: transform 0.3s var(--ease-out);
  pointer-events: none;
}
.net-banner.is-visible { transform: translateY(0); }
.net-banner--offline { background: var(--color-danger); }
.net-banner--online { background: var(--color-success); }

/* QR-login modal (auth-page.js) — a quiet pulsing "waiting…" line under the
   code, so the modal doesn't read as a static image that might not be
   doing anything; the poll cycle itself has no other visible indicator. */
.qr-waiting-pulse {
  margin-top: var(--space-2); text-align: center; font-size: var(--text-sm); color: var(--color-fg-muted);
  animation: qr-waiting-pulse 1.8s ease-in-out infinite;
}
@keyframes qr-waiting-pulse {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .qr-waiting-pulse { animation: none; opacity: 0.75; }
}
