/*
 * EasyVisit — Mobile Responsive Styles
 * Targets: 320px – 768px
 * Rules: NO logic changes, CSS/layout only
 */

/* `clip`, not `hidden`. Both stop horizontal scrolling, but `overflow: hidden`
   makes the element a *scroll container* — and a sticky descendant resolves
   against its nearest scroll container, not the viewport. With `hidden` here,
   BODY became that container; body.scrollTop never moves (the page scrolls
   documentElement), so every `position: sticky` in the app silently did
   nothing: the /search toolbar and filter rail, the /business booking rail,
   the /calendar header. `clip` clips without creating a scrollport, so sticky
   resolves against the viewport again. Do not "simplify" this back to hidden.
   Guard: tests/unit/pages/sticky-scroll-container.test.js */
html,
body {
  overflow-x: clip;
}

:root {
  --tg-theme-bg-color-fallback: var(--page-bg);
  --tg-theme-text-color-fallback: var(--text-primary);
}

/* ============================================================
   0. BASE — touch-targets & input safety
   ============================================================ */
@media (max-width: 768px) {
  /* Custom mobile-visibility helpers; not defined elsewhere. Source order
     resolves vs any inline Tailwind utility on the same element. */
  .desktop-only-ui,
  .header-desktop {
    display: none;
  }

  #tma-page-island-root[data-page="calendar"] {
    display: block;
    min-height: calc(100vh - 7rem);
  }

  #calendar-mobile-skeleton {
    display: block;
  }

  .tma-react-active.tma-page-calendar #calendar-mobile-skeleton {
    display: none;
  }

  /* Ensure tap targets are at least 44x44px */
  button,
  a,
  input,
  select,
  textarea,
  [role="button"],
  .btn,
  .btn-primary,
  .btn-cta,
  .btn-secondary,
  .btn-outline,
  .btn-danger,
  .btn-ghost,
  .sidebar-link {
    min-height: 44px;
  }

  /* Full-width CTAs on mobile. `.btn-cta` replaces `.btn-cta-mobile`, which was
     declared here but had zero call sites — so every apricot CTA silently sat
     outside the mobile button vocabulary (no full width, no tap-target floor).
     That only stayed invisible while `cta` was rare and lived on marketing
     pages inside flex-col rows, where align-items:stretch filled it in. */
  .btn-primary,
  .btn-cta {
    width: 100%;
    justify-content: center;
  }

  /* Inputs — prevent iOS focus zoom by forcing 16px floor.
     `:root` (pseudo-class, 0,1,0) + element (0,0,1) = 0,1,1, beats
     class selectors like `.input-warm` (0,1,0) without !important. */
  :root input,
  :root select,
  :root textarea {
    font-size: 16px;
  }

  /* Prevent keyboard from hiding active input */
  .form-group,
  .input-group {
    padding-bottom: env(keyboard-inset-height, 0px);
  }
}

/* ============================================================
   1. LAYOUT — consumer header & main
   ============================================================ */
@media (max-width: 768px) {
  /* Fixed so header never scrolls off-screen on mobile/TMA.
     Raise z-index above any page content stacking contexts.
     Single class (0,1,0). mobile.css loads after app.css, so at equal
     specificity mobile.css wins on source order — no !important needed.
     tailwind.css's .glass-header at @layer components only sets background
     and backdrop-filter; no conflict on position/inset/z-index. */
  .glass-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    z-index: 200;
    min-height: 56px;
    height: auto;
  }

  .glass-header .max-w-7xl {
    padding-left: 15px;
    padding-right: 15px;
  }

  .glass-header .max-w-7xl > .flex {
    display: flex;
    justify-content: space-between;
    align-items: center;
    min-height: 56px;
    height: auto;
  }

  /* Header notif icon: full 44×44 tap target */
  #consumer-notif-btn {
    width: 44px;
    height: 44px;
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    position: relative;
    z-index: 10001;
    pointer-events: auto;
  }

  /* Safe-area bottom padding for footer */
  footer {
    padding-bottom: env(safe-area-inset-bottom);
  }

  /* Main content top padding is intentionally NOT zeroed: the layout spacer
     matches the fixed header exactly (no breathing room), so each layout's
     <main> provides its own top padding via Tailwind utilities (`p-4` on
     dashboard / staff-portal, `py-12` on auth). Consumer's <main> has no
     padding class, so consumer pages own their top padding via per-page
     wrappers (`pt-comfy` etc.). */

  /* Default mobile gutter for max-width containers. 0.75rem = the `cozy`
     spacing token (tailwind.config.js). Wrapped in :where() so specificity
     is 0 — any page-level Tailwind utility (e.g. px-2, px-[5vw], px-0)
     wins without needing !important. */
  :where(.max-w-7xl, .max-w-6xl, .max-w-5xl, .max-w-4xl) {
    padding-left: 0.75rem;
    padding-right: 0.75rem;
  }
}

/* ============================================================
   2. DASHBOARD LAYOUT — sidebar & content
   ============================================================ */
@media (max-width: 1279px) {
  /* Dashboard wrapper becomes single column */
  .dashboard-layout,
  .flex.min-h-screen {
    flex-direction: column;
  }

  /* Dashboard/staff sidebars become full overlays — JS toggles `.open` /
     `.translate-x-0`. Scoped by ID to avoid matching unrelated `aside.lg\:w-64`
     uses (the search-page filter aside used to get caught here and parked off-
     screen — keep this scoped, not by class). */
  #dashboard-sidebar,
  #staff-sidebar,
  aside.sidebar {
    position: fixed;
    top: 0;
    left: 0;
    height: 100%;
    z-index: var(--z-sidebar, 40);
    transform: translateX(-100%);
    transition: transform 0.3s ease;
    width: 280px;
  }

  #dashboard-sidebar.open,
  #dashboard-sidebar.translate-x-0,
  #staff-sidebar.open,
  #staff-sidebar.translate-x-0,
  aside.sidebar.open,
  aside.sidebar.translate-x-0 {
    transform: translateX(0);
  }

  #dashboard-sidebar,
  #staff-sidebar {
    z-index: 9999;
  }

  #sidebar-backdrop {
    z-index: 9998;
  }

  #notification-btn,
  #dashboard-profile-trigger,
  #staff-profile-trigger {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    z-index: 1;
  }

  #notification-dropdown,
  #dashboard-profile-menu {
    right: 0;
    left: auto;
    max-width: calc(100vw - 1rem);
  }

  /* Main content occupies full width. `:root main` (0,1,1) beats utility
     classes; `.main-content` (0,1,0) ties and wins on source order
     (mobile.css is loaded last). */
  :root main,
  .main-content {
    margin-left: 0;
    width: 100%;
  }

  /* Dashboard page padding. `.p-6.lg\:p-8` is 0,2,0; `.px-8`/`.px-10` are
     0,1,0 but mobile.css loads after Tailwind so source-order wins. */
  .p-6.lg\:p-8,
  .px-8,
  .px-10 {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}

/* Stat cards — compact on mobile. .stat-card in tailwind.css @layer components
   only sets border-left; no padding conflict, source order resolves cleanly. */
@media (max-width: 768px) {
  .stat-card {
    padding: 0.875rem 1rem;
  }
}

/* ============================================================
   4. SEARCH PAGE — sticky toolbar + bottom-sheet filter/sort
   ============================================================
   Mobile-first redesign:
   - Sticky toolbar (search input + filter/sort/view pill row) replaces the
     dual stacked cards above the results.
   - Filter & sort each open a bottom sheet (.bottom-sheet) instead of a
     legacy drawer + inline custom-select.
   - Active filter chips render under the toolbar so the user always knows
     what's narrowing the result set without re-opening the sheet.
   Desktop (lg+) keeps the sidebar + inline sort layout — toolbar still
   sticks but stays just the search input + submit button.
*/

/* At rest the toolbar has no background: the colorful body blobs read
   straight through, so the input + pills feel like discrete controls
   floating on the page surface rather than a contained card. That rest
   state is what the user sees first and it stays.

   Once the toolbar pins, it earns a frosted band. The gaps between the
   controls are NOT small — a 16:9 result photo scrolls through them
   sharp and unblurred, and the toolbar stops reading as chrome. So
   `.is-stuck` applies the sanctioned glass-header treatment (DESIGN.md §5
   Navigation: frosted-pane-faint + backdrop-blur + hairline bottom border).
   The class is toggled by an IntersectionObserver sentinel in
   consumer-search.js — no scroll handler, nothing per-frame on a budget
   Android. Motion here conveys state: "I am floating above your results."

   This band was dead code until 2026-08-07: `html,body{overflow-x:hidden}`
   made BODY a scroll container, so sticky never engaged anywhere. See the
   note at the top of this file. */
.search-toolbar {
  position: sticky;
  top: 64px;
  z-index: 30;
  transition: background-color 0.2s cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
.search-toolbar.is-stuck {
  background-color: var(--surface-glass-faint);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  box-shadow: inset 0 -1px 0 var(--border-light);
}
@media (prefers-reduced-motion: reduce) {
  .search-toolbar { transition: none; }
}

/* Ghost-style pills — transparent fill, subtle outline, muted text. Sits
   quietly under the search input so the input remains the primary control.
   Smaller text (13px) + secondary text color keeps them as affordances,
   not buttons that compete for attention. */
.search-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  padding: 0.5rem 0.75rem;
  border-radius: 999px;
  background: transparent;
  border: 1px solid rgba(15, 23, 42, 0.1);
  color: var(--text-secondary, #475569);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  min-height: 40px;
  transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.05s;
}
.search-pill:hover {
  background: var(--surface-glass-faint);
  border-color: rgba(15, 23, 42, 0.2);
  color: var(--text-primary, #0f172a);
}
.search-pill:active { transform: scale(0.97); }

/* Active state — used when a filter sheet has selections or sort != default */
.search-pill.is-active {
  background: var(--surface-900, #0f172a);
  color: #ffffff;
  border-color: var(--surface-900, #0f172a);
}

.search-pill-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* `.hidden` wins by source order (mobile.css after Tailwind), so the badge's
   own `display: inline-flex` would override it — re-assert `display: none`
   when the badge is hidden via the utility. */
.search-pill-badge.hidden { display: none; }

.search-pill-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--primary-500, #0ea5e9);
  color: #ffffff;
  font-size: 11px;
  font-weight: 700;
  margin-left: 0.125rem;
}
.search-pill.is-active .search-pill-badge {
  background: var(--surface-solid);
  color: var(--surface-900, #0f172a);
}

/* iOS-style segmented control for view toggle */
.search-pill-segmented {
  display: inline-flex;
  padding: 3px;
  background: rgba(15, 23, 42, 0.06);
  border-radius: 999px;
  gap: 2px;
  flex-shrink: 0;
}
.search-pill-segmented button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 36px;
  height: 36px;
  /* Section-0 base rule sets `button { min-height: 44px }` on mobile — that
     constraint always wins over `height` (used value is max(min-height, height)),
     so a 36×36 square would stretch to 36×44 ovals. Override here. */
  min-height: 36px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary, #475569);
  transition: background 0.15s, color 0.15s;
}
.search-pill-segmented button[aria-pressed="true"] {
  background: var(--surface-solid);
  color: var(--text-primary, #0f172a);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.10);
}

/* Text variant — the desktop /search view toggle. Same track, same white
   pressed thumb, same aria-pressed contract as the icon-only mobile control
   above; only the button box changes to hold a label. Two sizes of ONE
   control beats two different controls for the same choice, and the shared
   track visually binds the pair so it reads as a single unit sitting next to
   the sort control. Labels are Cyrillic («Списком» / «На карте»), so the
   buttons size to content — no fixed width. */
.search-pill-segmented-text button {
  width: auto;
  height: 34px;
  min-height: 34px;
  gap: 0.375rem;
  padding: 0 0.875rem;
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
}
.search-pill-segmented-text button[aria-pressed="false"]:hover {
  color: var(--text-primary, #0f172a);
}

/* Active filter chips (under the toolbar) — removable */
.chip-removable {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.3125rem 0.5rem 0.3125rem 0.625rem;
  background: var(--primary-50, #f0f9ff);
  color: var(--primary-700, #0369a1);
  border: 1px solid var(--primary-100, #e0f2fe);
  font-size: 0.75rem;
  font-weight: 600;
  border-radius: 999px;
  line-height: 1;
  transition: background 0.15s;
}
.chip-removable svg { width: 12px; height: 12px; flex-shrink: 0; }
.chip-removable:hover { background: var(--primary-100, #e0f2fe); }

/* ============================================================
   4b. BOTTOM SHEET — generic filter/sort dialog primitive
   ============================================================ */
.bottom-sheet {
  position: fixed;
  inset: 0;
  /* Must beat the consumer tabbar (z-index 130). Otherwise the tabbar paints
     over the bottom of the sheet, hiding lower options — that's why
     "Рядом со мной" appeared missing from the sort sheet. */
  z-index: 140;
  pointer-events: none;
  visibility: hidden;
}
.bottom-sheet.open { pointer-events: auto; visibility: visible; }

.bottom-sheet-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(15, 23, 42, 0.45);
  opacity: 0;
  transition: opacity 0.25s ease;
}
.bottom-sheet.open .bottom-sheet-backdrop { opacity: 1; }

.bottom-sheet-panel {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--surface-glass-strong);
  backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
  -webkit-backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
  border-radius: 1.25rem 1.25rem 0 0;
  box-shadow: 0 -20px 60px -20px rgba(15, 23, 42, 0.35);
  display: flex;
  flex-direction: column;
  max-height: 85vh;
  transform: translateY(100%);
  transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
  padding-bottom: env(safe-area-inset-bottom);
}
.bottom-sheet.open .bottom-sheet-panel { transform: translateY(0); }

.bottom-sheet-handle {
  width: 40px;
  height: 4px;
  border-radius: 999px;
  background: rgba(15, 23, 42, 0.18);
  margin: 0.625rem auto 0;
  flex-shrink: 0;
}

.bottom-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.75rem 1.25rem 0.5rem;
  flex-shrink: 0;
}
.bottom-sheet-header h2 {
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--text-primary, #0f172a);
  letter-spacing: -0.01em;
}

.bottom-sheet-close {
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--text-muted, #94a3b8);
  background: rgba(15, 23, 42, 0.05);
  transition: background 0.15s, color 0.15s;
}
.bottom-sheet-close:hover {
  background: rgba(15, 23, 42, 0.1);
  color: var(--text-primary, #0f172a);
}

.bottom-sheet-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0.25rem 1.25rem 1rem;
  -webkit-overflow-scrolling: touch;
}

.bottom-sheet-section-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted, #94a3b8);
  font-weight: 700;
  margin: 1.125rem 0 0.625rem;
}
.bottom-sheet-body section:first-child .bottom-sheet-section-label {
  margin-top: 0.25rem;
}

.bottom-sheet-footer {
  display: flex;
  gap: 0.625rem;
  padding: 0.75rem 1.25rem 1rem;
  border-top: 1px solid rgba(15, 23, 42, 0.06);
  background: var(--surface-glass-strong);
  flex-shrink: 0;
}
.bottom-sheet-footer button {
  flex: 1 1 0;
  min-height: 48px;
  border-radius: 0.875rem;
  font-weight: 600;
  font-size: 0.9375rem;
  transition: background 0.15s, color 0.15s;
}
.bottom-sheet-footer .sheet-secondary {
  background: rgba(15, 23, 42, 0.06);
  color: var(--text-primary, #0f172a);
  flex: 0 0 38%;
}
.bottom-sheet-footer .sheet-secondary:hover { background: rgba(15, 23, 42, 0.1); }
.bottom-sheet-footer .sheet-primary {
  background: var(--surface-900, #0f172a);
  color: #ffffff;
}
.bottom-sheet-footer .sheet-primary:hover { background: #1e293b; }

/* Chip-group inside a sheet — category multi-select as toggleable pills */
.chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.chip-group label {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.5625rem 0.875rem;
  border-radius: 999px;
  border: 1.5px solid rgba(15, 23, 42, 0.1);
  background: var(--surface-solid);
  color: var(--text-primary, #0f172a);
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  min-height: 44px;
  user-select: none;
  position: relative;
}
.chip-group label:hover { border-color: rgba(15, 23, 42, 0.22); }
.chip-group input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 1px;
  height: 1px;
}
.chip-group label:has(input:checked) {
  background: var(--surface-900, #0f172a);
  border-color: var(--surface-900, #0f172a);
  color: #ffffff;
}
.chip-group .chip-check {
  display: none;
  width: 14px;
  height: 14px;
}
.chip-group label:has(input:checked) .chip-check { display: inline-flex; }

/* Sort sheet — large radio rows with a check-mark on the right */
.sort-radio-list {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding-top: 0.25rem;
}
.sort-radio-list label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.875rem 0.875rem;
  border-radius: 0.875rem;
  cursor: pointer;
  background: transparent;
  transition: background 0.15s, color 0.15s;
  min-height: 56px;
  color: var(--text-primary, #0f172a);
  font-size: 0.9375rem;
  font-weight: 500;
  position: relative;
}
.sort-radio-list label:hover { background: rgba(15, 23, 42, 0.04); }
.sort-radio-list label:has(input:checked) {
  background: var(--primary-50, #f0f9ff);
  color: var(--primary-700, #0369a1);
  font-weight: 600;
}
.sort-radio-list input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 1px;
  height: 1px;
}
.sort-radio-list .check-mark {
  display: none;
  width: 20px;
  height: 20px;
  color: var(--primary-500, #0ea5e9);
  flex-shrink: 0;
}
.sort-radio-list label:has(input:checked) .check-mark { display: inline-flex; }

/* Body scroll-lock when a sheet is open */
body.sheet-open { overflow: hidden; }

/* ── Mobile-only adjustments ────────────────────────────────── */
@media (max-width: 1023px) {
  .search-toolbar { top: 56px; }

  /* Make the search input the prominent primary control: opaque white,
     soft shadow for elevation off the page, slightly more vertical room
     so it feels like a proper mobile search bar instead of a translucent
     form field. The pills below intentionally remain ghost-styled so the
     input wins the hierarchy. */
  .search-toolbar .input-warm {
    background: var(--surface-solid);
    border-color: rgba(15, 23, 42, 0.08);
    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
    padding-top: 0.75rem;
    padding-bottom: 0.75rem;
  }
}

/* ── Desktop: search-toolbar reduces to just the search bar ── */
@media (min-width: 1024px) {
  .search-toolbar { top: 64px; }
}

/* ============================================================
   5. NOTIFICATIONS / FILTER CHIPS
   ============================================================ */
@media (max-width: 768px) {
  .mobile-page-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
  }

  .mobile-page-header-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
  }

  .mobile-page-header-actions > * {
    flex: 1 1 auto;
    min-width: 0;
  }

  /* Custom class — source order resolves. */
  .mobile-page-header-link {
    min-height: 40px;
    padding-left: 0.875rem;
    padding-right: 0.875rem;
    justify-content: center;
  }

  /* --- Consumer notifications: 2 chips → equal-width, no scroll ---
     Custom class — source order resolves. */
  .notif-filter-chips {
    display: flex;
    gap: 6px;
    overflow-x: visible;
    white-space: normal;
  }

  /* Class chain (0,2,0) — already beats Tailwind utilities without !important. */
  .notif-filter-chips .notif-filter-btn {
    flex: 1;
    min-height: 38px;
    padding-top: 0.375rem;
    padding-bottom: 0.375rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* --- Business notifications: 4 chips → horizontally scrollable ---
     Custom class — source order resolves. */
  .tab-filter-chips {
    display: flex;
    gap: 6px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding-bottom: 4px; /* prevents clipping of box-shadow on active tab */
  }

  .tab-filter-chips::-webkit-scrollbar {
    display: none;
  }

  /* Class chain (0,2,0) — !important not needed. */
  .tab-filter-chips .tab-btn {
    flex: 0 0 auto;
    min-height: 38px;
    min-width: 72px;
    padding-top: 0.375rem;
    padding-bottom: 0.375rem;
    white-space: nowrap;
  }

  /* Exactly two tabs → a segmented control, not a scroll strip.
     `:first-child:nth-last-child(2)` matches a first child that is also the
     second-from-last, i.e. the track holds precisely two buttons; the sibling
     combinator then picks up the other one. Three or more chips keep the
     scrolling behaviour above, which is what that rule was written for.
     Without this, two tabs shrink to their text width and leave a dead gap —
     the halves should split the track like the Mini App's segmented control. */
  .tab-filter-chips .tab-btn:first-child:nth-last-child(2),
  .tab-filter-chips .tab-btn:first-child:nth-last-child(2) ~ .tab-btn {
    flex: 1 1 0;
    min-width: 0;
  }

  /* A two-tab track has nothing to scroll to, so don't make it a scroll
     container — that also kills the stray horizontal drag on the panel. */
  .tab-filter-chips:has(.tab-btn:first-child:nth-last-child(2)) {
    overflow-x: visible;
  }

  /* Exempt filter chips from the global 44px min-height rule.
     Class chain (0,2,0). */
  .notif-filter-chips button,
  .tab-filter-chips button {
    min-height: 38px;
  }

  /* Empty state cards. IDs (1,0,0) — !important not needed. */
  #consumer-notifications-empty,
  #emptyState {
    width: 92%;
    max-width: 92%;
    margin: 0 auto;
    padding: 20px;
    border-radius: 12px;
  }

  .notification-card,
  .warm-card,
  .warm-card-static {
    overflow-wrap: anywhere;
  }

  .notification-card .flex.items-center.justify-between,
  .notification-card .flex.justify-between,
  #notificationsList .flex.items-center.justify-between,
  #notificationsList .flex.justify-between {
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: flex-start;
  }

  #consumer-notifications-content > * + *,
  #notificationsList > * + * {
    margin-top: 0.75rem;
  }

  /* Booking cards rendered on client */
  #upcoming-list > div,
  #past-list > div {
    border-radius: 1rem;
  }

  #upcoming-list .flex.items-start.justify-between,
  #past-list .flex.items-start.justify-between {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
  }

  #upcoming-list .text-right,
  #past-list .text-right {
    text-align: left;
    width: 100%;
  }

  /* ID + class chain (1,4,0) — !important not needed. */
  #upcoming-list .flex.items-center.gap-4.mt-4,
  #past-list .flex.items-center.gap-4.mt-4,
  #upcoming-list .flex.items-center.gap-4.mt-3,
  #past-list .flex.items-center.gap-4.mt-3 {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }

  #upcoming-list .flex.gap-3.mt-4,
  #past-list .flex.gap-3.mt-4,
  #cancel-modal .flex.gap-3,
  #rate-review-modal .flex.gap-3,
  #reschedule-modal .flex.gap-3,
  #delete-account-modal .flex.justify-end.gap-3 {
    flex-direction: column;
  }

  #upcoming-list .flex.gap-3.mt-4 > *,
  #past-list .flex.gap-3.mt-4 > *,
  #cancel-modal .flex.gap-3 > *,
  #rate-review-modal .flex.gap-3 > *,
  #reschedule-modal .flex.gap-3 > *,
  #delete-account-modal .flex.justify-end.gap-3 > * {
    width: 100%;
  }

  /* Dashboard and detail page section headers */
  .dashboard-section-header,
  .business-section-header,
  .consumer-card-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
  }

  .dashboard-section-header > :last-child,
  .business-section-header > :last-child,
  .consumer-card-header > :last-child {
    flex-shrink: 0;
  }

  /* Custom class — source order resolves. */
  .business-service-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
  }

  .business-service-row > :first-child {
    width: 100%;
  }

  /* Class + element chain (0,1,1) — beats utilities, no !important needed. */
  .business-service-row > a {
    opacity: 1;
    margin-left: 0;
    width: 100%;
    text-align: center;
  }

  /* Settings and favorites top actions */
  #settings-page .flex.justify-end,
  .favorites-page-header {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
  }

  #settings-page .flex.justify-end > *,
  .favorites-page-header > * {
    width: 100%;
  }

  /* Appointment list items become cards. Note: `.service-row` removed from
     this selector — that class is now owned by views/partials/service-row.ejs
     which intentionally lays out as an inline row, not a stacked card. */
  .appointment-row,
  tr.appointment-row {
    display: flex;
    flex-direction: column;
    background: var(--surface-glass);
    border-radius: 0.75rem;
    padding: 1rem;
    margin-bottom: 0.75rem;
    gap: 0.5rem;
    border: 1px solid var(--surface-border);
  }

  /* Tables that should scroll horizontally. Custom class — source order resolves. */
  .table-responsive {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  .table-responsive table {
    min-width: 600px;
  }
}

/* ============================================================
   6. CALENDAR — bottom sheet / modal on mobile

   Note: mobile.css is loaded LAST in dashboard/staff-portal layouts (after
   flatpickr.min.css and flatpickr-theme.css). At equal specificity, source
   order resolves in our favour. No !important needed here.
   ============================================================ */
@media (max-width: 768px) {
  /* Flatpickr inline calendar in modal — full width */
  .flatpickr-calendar {
    width: 100%;
    max-width: 100%;
    font-size: 1rem;
    border-radius: 1rem;
    box-shadow: none;
  }

  /* Calendar grid — 7 equal columns.

     flatpickr hard-codes `width/min-width/max-width: 307.875px` on both
     .flatpickr-days and .dayContainer, so the body grid stayed 307.875px wide
     while .flatpickr-weekdays (width: 100%) stretched to the full calendar —
     two different column pitches. Releasing the min-width floor is what lets
     the box actually shrink; overriding max-width alone does nothing.

     The explicit 7-column grid is load-bearing, not cosmetic: flatpickr emits
     42 sibling day cells with no row markup, so weeks exist only as flex-wrap
     line breaks. Any overflow — even a sub-pixel one — re-shards the month into
     6-day rows and slides every date under the wrong weekday, while the header
     (span.flatpickr-weekday is `flex: 1`, no min-width) cannot wrap to match.
     Grid tracks cannot wrap, so the columns stay aligned at any width.
     Guard: tests/unit/pages/flatpickr-week-grid.test.js */
  .flatpickr-days,
  .flatpickr-days .dayContainer {
    width: 100%;
    min-width: 0;
    max-width: 100%;
  }

  .flatpickr-days .dayContainer {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
  }

  /* .flatpickr-rContainer is an inline-block flex item that shrink-to-fits its
     children; without an explicit width its percentage-width children have
     nothing stable to resolve against. */
  .flatpickr-innerContainer,
  .flatpickr-rContainer {
    width: 100%;
  }

  /* Day cells — 44px min tap target.
     Height only: the cell's WIDTH must come from the grid track. A fixed px
     width here overrides flatpickr's `max-width: 39px` cap (min-width is
     clamped last, per CSS sizing) and is what broke the grid — 7 x 44px = 308px
     against a 307.875px container. */
  .flatpickr-day {
    width: auto;
    min-width: 0;
    max-width: none;
    height: 44px;
    min-height: 44px;
    line-height: 44px;
    font-size: 0.875rem;
  }

  /* Calendar container shown as bottom-sheet on mobile.
     Custom class — source order resolves. */
  .calendar-bottom-sheet {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: var(--z-modal, 60);
    background: var(--surface-glass-strong);
    backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
    -webkit-backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
    border-radius: 1.5rem 1.5rem 0 0;
    padding: 1.5rem 1rem 2rem;
    max-height: 90vh;
    overflow-y: auto;
    transform: translateY(100%);
    transition: transform 0.3s ease;
  }

  .calendar-bottom-sheet.open {
    transform: translateY(0);
  }

  .calendar-bottom-sheet-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(4px);
    z-index: calc(var(--z-modal, 60) - 1);
    display: none;
  }

  .calendar-bottom-sheet-backdrop.open {
    display: block;
  }

  /* The popup z-index for .flatpickr-calendar.open is owned by
     flatpickr-theme.css (z-index: 250) — that value is correct against
     the actual modal z-indices used in start.ejs. No override here. */

  /* Weekly calendar grid cells on mobile — scroll horizontally.
     Custom class — source order resolves. */
  .calendar-week-grid {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Business time-slot grid. Custom classes — source order resolves. */
  .time-slot-grid,
  .slots-container {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
}

/* ============================================================
   7. CHARTS — horizontal scroll wrapper
   ============================================================ */
@media (max-width: 768px) {
  /* Wrap chart canvases in scrollable container.
     Custom class — source order resolves. */
  .chart-scroll-wrapper {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Class + element (0,1,1) — !important not needed. */
  .chart-scroll-wrapper canvas {
    min-width: 480px;
  }

  /* Move chart legend below the chart. Custom class — source order resolves. */
  .chart-with-legend {
    flex-direction: column;
  }

  /* Class chain (0,2,0) — !important not needed. */
  .chart-with-legend .chart-legend {
    margin-top: 0.75rem;
    margin-left: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;
  }
}

/* ============================================================
   8. MODALS — full screen on mobile
   ============================================================ */
@media (max-width: 640px) {
  /* .modal-content in tailwind.css @layer components sets background/backdrop-
     filter/border-radius/border/box-shadow/animation/padding. Conflicting props
     here (border-radius, animation) resolve to mobile.css via source order. */
  .modal-content {
    border-radius: 1.5rem 1.5rem 0 0;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    width: 100%;
    max-width: 100%;
    max-height: 92vh;
    overflow-y: auto;
    animation: slideUpMobile 0.3s ease-out;
    margin: 0;
    padding-bottom: env(safe-area-inset-bottom, 1rem);
  }

  /* Time-off modal: center positioning instead of bottom sheet.
     ID + class (1,1,0) — already beats utilities, no !important needed. */
  #time-off-modal .modal-content {
    position: fixed;
    bottom: auto;
    top: 50%;
    left: 50%;
    right: auto;
    transform: translate(-50%, -50%);
    width: 90%;
    max-width: 90%;
    max-height: 80vh;
    border-radius: 1rem;
    animation: modalCenterFade 0.3s ease-out;
    margin: 0;
  }

  /* ID + class (1,1,0) — !important not needed. */
  #time-off-modal .modal-overlay {
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  @keyframes modalCenterFade {
    from {
      opacity: 0;
      transform: translate(-50%, -45%);
    }
    to {
      opacity: 1;
      transform: translate(-50%, -50%);
    }
  }

  /* Custom class — source order resolves. */
  .modal-backdrop {
    align-items: flex-end;
  }

  /* ID (1,0,0) — !important not needed. */
  #time-off-modal {
    z-index: 1001;
  }

  @keyframes slideUpMobile {
    from {
      transform: translateY(100%);
      opacity: 0;
    }
    to {
      transform: translateY(0);
      opacity: 1;
    }
  }
}

/* ============================================================
   9. PROFILE DROPDOWN — full-width on small screens
   ============================================================ */
@media (max-width: 480px) {
  /* Profile / notification dropdowns. IDs (1,0,0) and one custom class —
     all win without !important. */
  #profile-dropdown,
  #notif-dropdown,
  .dropdown-panel {
    right: 0;
    left: 0;
    width: auto;
    margin: 0 0.5rem;
  }

  /* Consumer notification dropdown — fixed to viewport so it isn't
     clipped by its tiny bell-button containing block.
     ID (1,0,0) — !important not needed. */
  #consumer-notif-dropdown {
    position: fixed;
    top: 4rem;
    left: 1rem;
    right: 1rem;
    width: auto;
    margin: 0;
  }
}

/* ============================================================
   10. BOOKING PAGE — full width steps
   ============================================================ */
@media (max-width: 768px) {
  /* All custom classes / IDs below — source order resolves. */
  .booking-step-panel {
    padding: 1rem;
  }

  .services-grid {
    grid-template-columns: 1fr;
  }

  .staff-cards-row {
    flex-wrap: wrap;
  }

  .staff-card {
    width: 100%;
  }

  /* "Book" CTA always full width. ID + custom classes + attribute selector. */
  #book-btn,
  .book-action-btn,
  [data-action="book"] {
    width: 100%;
    min-height: 52px;
    font-size: 1rem;
  }

  .booking-summary-sticky {
    position: sticky;
    bottom: 0;
    background: var(--surface-glass-strong);
    backdrop-filter: blur(8px);
    border-top: 1px solid rgba(203, 213, 225, 0.5);
    padding: 0.75rem 1rem;
    z-index: 20;
  }
}

/* ============================================================
   11. SAFE AREA — iPhone notch/home indicator
   ============================================================ */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
  .modal-content,
  .calendar-bottom-sheet,
  .filter-panel,
  .booking-summary-sticky {
    padding-bottom: max(1rem, env(safe-area-inset-bottom));
  }
}

/* ============================================================
   12. BUSINESS PUBLIC PAGE — mobile hero & info  
   ============================================================ */
@media (max-width: 768px) {
  /* All custom classes — source order resolves. */
  .business-hero {
    height: 200px;
  }

  .tabs-row {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    scrollbar-width: none;
  }

  .tabs-row::-webkit-scrollbar {
    display: none;
  }

  .services-list-grid {
    grid-template-columns: 1fr;
  }
}

/* ============================================================
   13. MISC HELPERS
   ============================================================ */
@media (max-width: 768px) {
  .clients-page-filters .custom-select,
  .clients-page-filters .search-bar {
    width: 100%;
  }

  /* All chains here are 0,2,0 or 1,1,0 — !important not needed. */
  .calendar-toolbar-mobile .view-btn,
  .calendar-toolbar-mobile .cal-filter-btn,
  .calendar-toolbar-mobile .btn-create-promo,
  .calendar-toolbar-mobile #go-today-btn,
  .calendar-toolbar-mobile #reset-filters-btn {
    min-height: 44px;
  }

  .calendar-chip-ribbon {
    flex-wrap: wrap;
    overflow: visible;
  }

  .clients-list .client-card {
    border-radius: 14px;
  }

  /* ID (1,0,0) — !important not needed. */
  #pagination {
    padding-left: 16px;
    padding-right: 16px;
  }

  #pagination .flex.items-center.gap-2 {
    width: 100%;
  }

  #prev-btn,
  #next-btn {
    flex: 1;
    min-height: 44px;
  }

  /* Calendar is a viewport-fit, internal-scroll page — opt its <main>
     out of the 100px tabbar-reserve padding (doc pattern at line 1370).
     Scoped via `:has(> #calendar-page)` so no layout-file edit is needed
     and only the calendar route is affected. */
  main.app-mobile-main:has(> #calendar-page) {
    padding-bottom: 0;
  }

  /* All ID-rooted selectors below — !important not needed.
     Bound #calendar-page to the actual viewport so only #calendar-scroll
     scrolls. Subtractions:
       - 68px : real mobile glass-header height (matches layouts/dashboard.ejs
                spacer `h-[68px]` — 4px taller than the desktop xl:h-16).
                Was 4rem (64px), undercut by 4px → the calendar's bottom edge
                slipped under the tabbar.
       - var(--space-4) : <main> top padding (p-4 = 1rem = `comfy` token)
       - 70px : real mobile-tabbar height (56px min-height + 6+8px padding,
                see mobile-tabbar-dashboard.ejs)
       - var(--space-3) : `cozy` spacing token (0.75rem / 12px) — the
                breathing-room gap above the tabbar
       - env(safe-area-inset-bottom) : home-indicator clearance
     `dvh` so iOS Safari URL-bar collapse doesn't shrink/grow the
     calendar; `vh` fallback for older WebKit. */
  #calendar-page {
    height: calc(100vh - 68px - var(--space-4) - 70px - var(--space-3) - env(safe-area-inset-bottom, 0px));
    height: calc(100dvh - 68px - var(--space-4) - 70px - var(--space-3) - env(safe-area-inset-bottom, 0px));
    min-height: 0;
  }

  /* #day-headers is a sibling of #calendar-scroll inside the grid card —
     structurally sticky (lives outside the scroll axis, can't move).
     Tighten vertical padding to 4px on mobile — intentional sub-token
     exception. The `tight` spacing token (10px, the floor of the
     semantic scale) is correct for desktop header-bar density, but on
     mobile the row needs to be denser to maximize the calendar grid
     area. Same exception pattern as `py-2.5` on input internals
     (CLAUDE.md "Intentional exceptions"). */
  #day-headers > div {
    padding-top: 4px;
    padding-bottom: 4px;
  }

  #calendar-scroll {
    overflow-y: auto;
  }

  /* Week view → horizontal scroll on mobile.
     At flex:1 the 7 day-columns collapse to ~37px on a phone — too narrow
     to render an appointment chip. In week view we instead give each day a
     usable min-width (6rem) and let the grid scroll sideways, showing ~3
     days at a time with the time gutter pinned. Gated on
     [data-cal-view="week"] (set by renderCalendar) so day/month are untouched.
       - width:max-content on #calendar-grid is load-bearing: the sticky
         gutter's containing block must span the FULL scroll width, or it
         un-sticks once you scroll past ~1 viewport. (#day-headers is itself
         the scroll container, so its gutter sticks without this.)
       - scrollbar-width:none keeps #day-headers and #calendar-scroll the same
         client width — a desktop vertical scrollbar offsets only one of them
         and drifts the header/column alignment at the far edge. Mobile uses
         overlay scrollbars, so nothing visible is lost.
     The two scrollers are kept in horizontal lockstep by ensureWeekScrollSync()
     in business-calendar.js. */
  #calendar-page[data-cal-view="week"] #day-headers,
  #calendar-page[data-cal-view="week"] #calendar-scroll {
    overflow-x: auto;
    scrollbar-width: none;
  }
  #calendar-page[data-cal-view="week"] #day-headers::-webkit-scrollbar,
  #calendar-page[data-cal-view="week"] #calendar-scroll::-webkit-scrollbar {
    display: none;
  }
  #calendar-page[data-cal-view="week"] #calendar-grid {
    width: max-content;
  }
  #calendar-page[data-cal-view="week"] #day-headers .day-column,
  #calendar-page[data-cal-view="week"] #calendar-grid .day-column {
    flex: 0 0 6rem;
    min-width: 6rem;
  }
  #calendar-page[data-cal-view="week"] #day-headers .time-column,
  #calendar-page[data-cal-view="week"] #calendar-grid .time-column {
    position: sticky;
    left: 0;
    z-index: 8;
    background: #eef4f8;
  }

  .calendar-mobile-agenda .agenda-item {
    min-height: 64px;
  }

  /* Remove hover transforms on touch devices to prevent stutter.
     Class + pseudo (0,1,1) — source order resolves. */
  .warm-card:hover {
    transform: none;
  }

  /* All custom classes below — source order resolves. */
  .flex-nowrap-desktop {
    flex-wrap: wrap;
  }

  /* Element + class (0,1,1) — !important not needed. */
  select.w-auto {
    width: 100%;
  }

  .hero-section {
    padding-top: 3rem;
    padding-bottom: 3rem;
  }

  /* `.page-title` (0,1,0) resolves via source order; `h1.text-3xl`/
     `h1.text-4xl` (0,1,1) already beat utility text-* classes. */
  .page-title,
  h1.text-3xl,
  h1.text-4xl {
    font-size: 1.5rem;
    line-height: 1.3;
  }

  .section-gap {
    gap: 1rem;
  }
}

@media (min-width: 1280px) {
  /* ID (1,0,0) — !important not needed. */
  #mobile-new-appointment-fab {
    display: none;
  }
}

@media (max-width: 1279px) {
  body {
    background: var(--tg-theme-bg-color, var(--tg-theme-bg-color-fallback));
    color: var(--tg-theme-text-color, var(--tg-theme-text-color-fallback));
  }

  /* Mobile horizontal gutter is owned by page-level wrappers (px-cozy on
     consumer/auth pages, p-4 on dashboard/staff <main>) — not by .app-mobile-main
     itself. Stacking both caused 28px effective gutter on consumer layouts. */

  /* Bottom reserve for the fixed .mobile-tabbar. Load-bearing — must beat
     Tailwind padding utilities so pages aren't obscured. Class-selector
     specificity (0,1,0) ties with .p-4 / .pb-* and mobile.css loads AFTER
     app.css, so source order wins. To opt out (e.g. internal-scroll pages),
     drop the .app-mobile-main class from <main>. */
  .app-mobile-main {
    padding-bottom: calc(100px + env(safe-area-inset-bottom, 0px));
  }

  /* Stack-spacing between visible siblings inside <main>. Skip <style> and
     <script> on either side — they're non-rendered nodes (display:none) but
     under the previous unguarded `* + *` rule, a page that opened with an
     inline <style> would trip the cozy-token (12px) margin onto its first
     visible div. Result: calendar/staff-list/etc. sat 12px lower than the
     dashboard's "Обзор" benchmark on mobile. Filtering both sides aligns
     every page's first-content top to <main>'s p-4 alone. */
  .app-mobile-main > *:not(style):not(script) + *:not(style):not(script) {
    margin-top: 12px;
  }

  .mobile-tabbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 120;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 4px;
    padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px));
    background: var(--surface-glass-strong);
    border-top: 1px solid var(--surface-border);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }

  /* Five-tab variant (owner dashboard: Обзор · Календарь · Клиенты ·
     Сотрудники · Ещё). The staff portal still runs four tabs on the base rule,
     so the column count lives on a modifier rather than in `.mobile-tabbar`.
     Horizontal padding and gap tighten because «Сотрудники» is the longest
     label in the set: at a 320px viewport a fifth column of the default
     10px/4px geometry leaves it only 1.5px of slack (measured), which is one
     font-fallback away from wrapping the bar to two lines. 6px/2px restores
     ~4.7px. Vertical padding is untouched — the 70px bar height is
     load-bearing for the scroll reserve and --toast-tabbar-clear. */
  .mobile-tabbar--5 {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 2px;
    padding-left: 6px;
    padding-right: 6px;
  }

  .mobile-more-modal {
    position: fixed;
    inset: 0;
    z-index: 130;
    background: rgba(15, 23, 42, 0.46);
    backdrop-filter: blur(4px);
  }

  .mobile-more-sheet {
    background: var(--surface-glass-strong);
    backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
    -webkit-backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
    height: 100%;
    width: 100%;
    display: flex;
    flex-direction: column;
    padding: 18px 16px calc(20px + env(safe-area-inset-bottom, 0px));
  }

  .mobile-more-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 14px;
  }

  .mobile-more-head h3 {
    font-size: 20px;
    font-weight: 700;
  }

  .mobile-more-head button {
    width: 44px;
    height: 44px;
    border-radius: 12px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: #f8fafc;
  }

  .mobile-more-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .mobile-more-item {
    min-height: 52px;
    border-radius: 14px;
    border: 1px solid var(--surface-border);
    background: var(--surface-glass);
    padding: 0 14px;
    display: flex;
    align-items: center;
    font-weight: 600;
    color: var(--text-primary);
  }

  .calendar-toolbar-clean {
    background: var(--surface-glass-strong);
    backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
    -webkit-backdrop-filter: blur(var(--glass-strong-blur)) saturate(var(--glass-strong-saturate));
    border: 1px solid var(--surface-border);
  }

  .calendar-toolbar-clean .calendar-chip-ribbon {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 10px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .calendar-toolbar-clean .calendar-chip-ribbon::-webkit-scrollbar {
    display: none;
  }

  /* All chains (0,2,0 / 1,1,0) — !important not needed. */
  .calendar-toolbar-clean .cal-filter-btn,
  .calendar-toolbar-clean #reset-filters-btn,
  .calendar-toolbar-clean #status-dropdown-btn,
  .calendar-toolbar-clean #staff-dropdown-btn {
    flex: 0 0 auto;
    min-height: 44px;
  }

  .calendar-toolbar-clean .cal-dropdown-menu {
    right: auto;
    left: 0;
  }

  /* Class + ID chains (1,1,0) — !important not needed. */
  .calendar-toolbar-clean #go-today-btn,
  .calendar-toolbar-clean #date-display,
  .calendar-toolbar-clean #nav-prev-btn,
  .calendar-toolbar-clean #nav-next-btn {
    display: none;
  }

  .mobile-tabbar-item {
    min-height: 56px;
    border-radius: 14px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    font-size: 11px;
    font-weight: 600;
    color: var(--text-muted);
  }

  .mobile-tabbar-item.active {
    background: var(--primary-ring);
    color: var(--primary-dark);
  }

  .mobile-tabbar-item svg {
    width: 20px;
    height: 20px;
  }

  /* Tab labels are deliberately below every type-ramp role: a label that
     outgrows its column clips rather than wraps, and a two-line tab grows the
     whole bar, desynchronising the bottom scroll reserve. (0,2,1) beats any
     utility a call site might attach, so source order is irrelevant. No-op for
     the icon-only consumer bar, whose items have no <span>.

     This used to live in `partials/mobile-more-drawer/start.ejs` on the premise
     that every labelled bar also renders an «Ещё» trigger. The agent/partner bar
     is five plain links with no drawer, so it never loaded that shell and its
     labels fell back to the 16px element reset — moved here 2026-08-04 so the
     rule follows the tabbar, not the drawer. The reset it guarded against is
     gone (see MOBILE TYPE RAMP above); this rule stays because 9–11px is the
     authored size for a tab label, not a workaround. */
  .mobile-tabbar .mobile-tabbar-item > span {
    font-size: clamp(9px, 2.6vw, 11px);
    line-height: 1.15;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* IDs (1,0,0) — !important not needed. */
  #dashboard-sidebar,
  #staff-sidebar,
  #sidebar-close {
    display: none;
  }

  /* `:root h1` (0,1,1) beats utility text-* (0,1,0); `.page-title` ties
     and wins on source order. */
  :root h1, .page-title {
    font-size: 20px;
    line-height: 1.3;
    font-weight: 700;
  }

  :root h2, .section-title {
    font-size: 18px;
    line-height: 1.35;
    font-weight: 700;
  }

  /* Calendar toolbar date-display is an h2 but shares the row with the "+"
     CTA and prev/next chevrons — at the global 18px floor it truncates
     "среда, 20 мая 2026 г." to "среда, 20 …". Drop to the toolbar chip
     family (14px = text-sm token) on mobile so the full date fits. The
     :root h2#date-display selector (0,2,1) beats :root h2 (0,1,1) without
     !important; the EJS template already declares `text-sm sm:text-lg` on
     this element, so desktop is handled at the Tailwind layer. */
  :root h2#date-display {
    font-size: 0.875rem;
    line-height: 1.4;
  }

  /* ── MOBILE TYPE RAMP ─────────────────────────────────────────────
     Scales the six DESIGN.md §3 type roles for arm's-length reading on a
     budget Android, keeping the RATIO between roles intact. Must stay in
     lockstep with tailwind.config.js → theme.extend.fontSize.

                  desktop → mobile
       label        12px  →  13px
       body         14px  →  16px
       body-lg      16px  →  17px
       title        18px  →  19px
       headline     30px  →  24px   (down — 30px is a desktop page-title size)
       display            clamp() — already fluid, deliberately untouched

     These six are ROLES, not a geometric ramp, so adjacent values are not all
     a fixed ratio apart — body (16) and body-lg (17) are near-neighbours by
     design, the same way they are on desktop (14 / 16). The contrast that
     carries hierarchy lives at the role boundaries that matter: body 16 →
     title 19 → headline 24. Don't "fix" the tight pairs into a geometric
     series; that would push body-lg prose to 20px on a 390px screen.

     Only font-size is restated; each token's line-height, letter-spacing and
     weight still come from the utility, and line-heights are unitless so they
     scale with the new size. Verified in-browser at 390px:
     13 / 16 / 17 / 19 / 24, weights and tracking intact.

     `:root .text-*` is (0,1,1): beats plain utilities (0,1,0) outright, and
     beats the `:root h1` / `:root h2` legacy rules above on source order
     (equal specificity, declared later). So a migrated
     `<h1 class="text-headline">` renders 24px while an unmigrated `<h1>`
     keeps the legacy 20px — the transitional state is intended.

     REPLACES the element-level reset that stood here until 2026-08-05:

         p, span, label, input, select, textarea, button { font-size: 16px }

     That was a reset, not a floor. At (0,0,1) it targeted text nodes
     DIRECTLY, so a bare <span> inside a 12px chip detached from its parent
     and rendered 16px — the parent's declared size stopped applying at all.
     Six bugs traced back to it (filter-drawer chips, four leaks on the
     page-status card, the dashboard tile's auto-confirm label, the partner
     tabbar labels), each patched with a bespoke `… > span { font-size:
     inherit }`. Scaling the roles fixes the class instead of the instances:
     inheritance works again, so a size declared on a container reaches the
     text inside it.

     Its iOS focus-zoom half was already redundant — `:root input, :root
     select, :root textarea { font-size: 16px }` near the top of this file
     is (0,1,1), covers form controls at higher specificity, and is
     untouched. Guard: tests/unit/pages/mobile-type-ramp.test.js */
  :root .text-label    { font-size: 0.8125rem; }
  :root .text-body     { font-size: 1rem; }
  :root .text-body-lg  { font-size: 1.0625rem; }
  :root .text-title    { font-size: 1.1875rem; }
  :root .text-headline { font-size: 1.5rem; }

  /* All custom classes — source order resolves.
     `.service-card` was removed 2026-07-28: it is used only by the consumer
     booking page, where the `border` shorthand here reset `border-color` and
     the `box-shadow` overwrote Tailwind's `ring-*` (also box-shadow). Because
     mobile.css loads after app.css at equal (0,1,0) specificity, that silently
     erased the *selected service* state below 1280px — the chip kept its tint
     but lost its blue border and ring. book.ejs now owns that chip's surface. */
  .warm-card-static,
  .warm-card,
  .client-card,
  .appointment-item,
  .notification-card {
    border-radius: 16px;
    border: 1px solid var(--surface-border);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
  }

  /* Mix of custom classes and IDs — source order / ID specificity resolve. */
  .clients-list,
  #upcoming-list,
  #past-list,
  #notificationsList,
  #appointment-list,
  #services-list,
  .staff-list,
  .reviews-list {
    gap: 12px;
  }

  .clients-list > *,
  #upcoming-list > *,
  #past-list > *,
  #notificationsList > * {
    margin-bottom: 12px;
  }

  /* `:root button` (0,1,1) beats utility h-*/min-h-* (0,1,0);
     `.btn*` and `[type=…]` (0,1,0) tie and win on source order. */
  :root button,
  .btn,
  .btn-primary,
  .btn-cta,
  .btn-secondary,
  .btn-outline,
  .btn-danger,
  [type="button"],
  [type="submit"] {
    min-height: 48px;
  }

  .btn-primary,
  .btn-cta,
  .btn-secondary,
  .btn-outline,
  .btn-danger,
  .toolbar .custom-select,
  .toolbar .custom-select-trigger,
  .toolbar input,
  .toolbar select {
    width: 100%;
  }

  /* Buttons that opt into `shrink-0` (via wrapperClass on the button partial)
     explicitly signal "I belong in an inline row, don't grow to fill it".
     Specificity 0,2,0 beats the global `.btn-*` (0,1,0) rule above. This
     supersedes the prior calendar-specific override at .calendar-toolbar-wrap
     and also fixes the Сброс button on the clients page, which was being
     stretched to the full width and squeezing the filter dropdowns to 0. */
  .btn-primary.shrink-0,
  .btn-cta.shrink-0,
  .btn-secondary.shrink-0,
  .btn-outline.shrink-0,
  .btn-danger.shrink-0 { width: auto; }

  /* Chains (0,2,0) + custom single classes — source order resolves. */
  .clients-page-filters > .flex,
  .calendar-toolbar-mobile > .flex,
  .mobile-page-header,
  .mobile-page-header-actions {
    gap: 12px;
  }

  .calendar-mobile-day-ribbon {
    display: flex;
    overflow-x: auto;
    gap: 8px;
    padding: 10px 12px 6px;
    border-bottom: 1px solid var(--surface-border);
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .calendar-mobile-day-ribbon::-webkit-scrollbar {
    display: none;
  }

  .calendar-day-chip {
    flex: 0 0 auto;
    min-width: 68px;
    min-height: 48px;
    border-radius: 14px;
    border: 1px solid var(--surface-border);
    background: var(--surface-glass-strong);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 6px 10px;
    font-size: 12px;
    color: var(--text-secondary);
  }

  .calendar-day-chip.active {
    background: var(--primary-ring);
    border-color: rgba(14, 165, 233, 0.35);
    color: var(--primary-dark);
  }

  .calendar-mobile-agenda-empty {
    text-align: center;
    padding: 32px 16px;
    color: var(--text-muted);
  }

  .calendar-mobile-agenda-empty .empty-icon {
    width: 52px;
    height: 52px;
    margin: 0 auto 12px;
    border-radius: 50%;
    border: 1px solid var(--surface-border);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-faint);
  }

  /* ID (1,0,0) — !important not needed. */
  #mobile-new-appointment-fab {
    right: 16px;
    bottom: calc(84px + env(safe-area-inset-bottom, 0px));
    width: 56px;
    height: 56px;
    padding: 0;
  }
}

/* ============================================================
   14. MOBILE COMPONENT TOOLKIT
   Generic iOS-list / settings-style components used across all
   mobile surfaces (business, consumer, staff). Single source of
   truth: edit here and every page that uses these classes updates.
   Historical note: these classes were formerly prefixed `tma-`
   (when they were thought to be Telegram-Mini-App-specific) before
   the 2026-05-16 cleanup. The TMA-only rules live in tma-premium.css.
   ============================================================ */
:root {
  --mobile-card: #ffffff;
  --mobile-card-bg: #f8fafc;
  --mobile-card-text: #111827;
  --mobile-card-hint: #6b7280;
  --mobile-sep: rgba(0, 0, 0, 0.08);
  --mobile-link: #2ea6ff;
  --mobile-link-ring: rgba(46, 166, 255, 0.5);
  --mobile-link-tint: rgba(46, 166, 255, 0.12);
}

.mobile-section {
  background: var(--mobile-card);
  border: 1px solid var(--mobile-sep);
  border-radius: 14px;
  overflow: hidden;
}

.mobile-section-title {
  font-size: 20px;
  font-weight: 700;
  line-height: 1.2;
  margin: 0 0 10px;
}

.mobile-cell {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: 12px;
  border-bottom: 1px solid var(--mobile-sep);
  background: var(--mobile-card);
}

.mobile-cell:last-child {
  border-bottom: 0;
}

.mobile-cell-avatar {
  width: 38px;
  height: 38px;
  border-radius: 999px;
  background: #e5e7eb;
  color: #374151;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  flex-shrink: 0;
}

.mobile-cell-content {
  min-width: 0;
  flex: 1;
}

.mobile-cell-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--mobile-card-text);
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mobile-cell-subtitle {
  font-size: 13px;
  color: var(--mobile-card-hint);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mobile-cell-action {
  color: var(--mobile-card-hint);
  flex-shrink: 0;
}

.mobile-icon-btn {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  border: 1px solid var(--mobile-sep);
  background: var(--mobile-card);
  color: var(--mobile-card-text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Header buttons get a guaranteed 44x44 tap area so finger taps land cleanly. */
.header-tap-btn {
  min-width: 44px;
  min-height: 44px;
}

.mobile-filter-strip {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: 8px 0 0;
}

.mobile-filter-strip::-webkit-scrollbar {
  display: none;
}

.mobile-chip {
  min-height: 40px;
  border: 1px solid var(--mobile-sep);
  border-radius: 999px;
  padding: 0 12px;
  background: var(--mobile-card);
  color: var(--mobile-card-text);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}

.mobile-chip-active {
  color: var(--mobile-link);
  border-color: var(--mobile-link-ring);
  background: var(--mobile-link-tint);
}

.mobile-modal-page {
  position: fixed;
  inset: 0;
  z-index: 140;
  background: var(--mobile-card);
  padding: 14px 12px calc(16px + env(safe-area-inset-bottom, 0px));
  overflow-y: auto;
}

.mobile-week-strip {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: 8px 0;
}

.mobile-week-strip::-webkit-scrollbar {
  display: none;
}

.mobile-day-pill {
  min-width: 64px;
  min-height: 48px;
  border-radius: 12px;
  border: 1px solid var(--mobile-sep);
  background: var(--mobile-card);
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: var(--mobile-card-hint);
}

.mobile-day-pill.active {
  color: var(--mobile-link);
  border-color: var(--mobile-link-ring);
  background: var(--mobile-link-tint);
}

.mobile-fab {
  position: fixed;
  right: 16px;
  bottom: calc(88px + env(safe-area-inset-bottom, 0px));
  width: 56px;
  height: 56px;
  border-radius: 999px;
  border: 0;
  background: var(--mobile-link);
  color: #ffffff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 110;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
}

.mobile-placeholder {
  text-align: center;
  padding: 28px 14px;
  color: var(--mobile-card-hint);
}

.mobile-placeholder-icon {
  width: 48px;
  height: 48px;
  border-radius: 999px;
  border: 1px solid var(--mobile-sep);
  margin: 0 auto 10px;
  display: flex;
  align-items: center;
  justify-content: center;
}

@media (min-width: 1280px) {
  /* Custom classes, defined only above — source order resolves. */
  .mobile-fab,
  .mobile-week-strip,
  .mobile-modal-page {
    display: none;
  }
}
