/* ═══════════════════════════════════════════════════════════════════════════
   EGPL Billing — MOBILE / RESPONSIVE LAYER
   ───────────────────────────────────────────────────────────────────────────
   Loaded LAST (after style.css, invoice-engine.css, calculator.css) so it wins
   the cascade without needing !important except where an earlier rule uses a
   higher-specificity selector (documented inline at each site).

   Scope rules for this file:
     • Purely presentational. No business logic, no backend, no DOM contracts.
     • Desktop (>1024px) must render byte-identically to before. Every rule here
       lives inside a max-width media query, a touch/coarse-pointer query, or
       targets a class that only mobile-ux.js adds.
     • Reuses the existing component classes (.table-stack-mobile, .bottom-tabs,
       .invoice-stepper, .drawer-*) rather than introducing parallel components.

   Breakpoints used across the app (kept consistent with style.css):
     360px  small phone      480px  phone
     640px  large phone      768px  tablet portrait / phone landscape
     1024px tablet landscape 1280px small laptop
   ═══════════════════════════════════════════════════════════════════════════ */


/* ═══ 1. FOUNDATIONS ═══════════════════════════════════════════════════════ */

:root {
  /* Minimum comfortable touch target (WCAG 2.5.5 AAA / Apple HIG is 44px). */
  --tap: 44px;
  /* Height of the fixed mobile bottom-tab bar, incl. the iOS home indicator. */
  --bottom-tabs-h: calc(64px + env(safe-area-inset-bottom, 0px));
}

/* iOS Safari: `100vh` includes the collapsing URL bar, so full-height panels
   get clipped at the bottom. `100dvh` tracks the *visible* viewport. Declared
   as a variable with a vh fallback so older browsers keep the old behaviour. */
:root { --vh100: 100vh; }
@supports (height: 100dvh) { :root { --vh100: 100dvh; } }

@media (max-width: 1024px) {
  /* Nothing on a phone should ever cause the *page* to scroll sideways. Wide
     content (invoice documents, wide tables) scrolls inside its own container
     instead — see §5 and §8. */
  html, body { max-width: 100%; overflow-x: hidden; }

  /* Long unbroken strings (GSTINs, invoice numbers, emails, URLs) are the most
     common cause of overflow in this app. Let them wrap rather than push. */
  body { overflow-wrap: anywhere; word-break: break-word; }

  /* …but never inside inputs, code-like/number cells, or the rendered invoice
     document. The preview must stay a faithful mirror of the generated PDF, and
     the PDF path does not inherit this rule — letting descriptions break
     mid-word on screen would make the preview lie about the output. */
  input, select, textarea, .badge, kbd, .topbar-kbd,
  [class*="mono"], [style*="tnum"],
  .invoice-doc, .invoice-doc *, .gst-invoice, .gst-invoice *,
  #print-area, #print-area * { overflow-wrap: normal; word-break: normal; }
}

/* iOS Safari zooms the whole viewport when a focused input has font-size < 16px,
   and does NOT zoom back out — the classic "the form is now off-screen" bug.
   Applied by width alone rather than `pointer: coarse`, because some Android
   devices with a stylus report a fine pointer and would miss the fix. Phones are
   the only place this width is reached, so no desktop density is lost. */
@media (max-width: 768px) {
  /* `!important` is deliberate and scoped to this single property. Dozens of
     component rules across style.css and the standalone pages set a smaller
     font at higher specificity (`.form-group input`, `.items-table input`,
     `.pos-search input`, …). Losing to any one of them re-introduces the zoom
     trap, and the failure is invisible until someone taps the field on a real
     iPhone — so this must not be a specificity race. */
  input, select, textarea,
  input[type="text"], input[type="email"], input[type="password"],
  input[type="number"], input[type="tel"], input[type="date"], input[type="search"] {
    font-size: 16px !important;
  }
}
/* Tablets keep the denser type unless they're actually touch-driven. */
@media (min-width: 769px) and (max-width: 1024px) and (pointer: coarse) {
  input, select, textarea { font-size: 16px !important; }
}

/* Safe-area padding for notched/rounded devices (iPhone X+, Android cutouts). */
@media (max-width: 768px) {
  .page { padding-left: max(16px, env(safe-area-inset-left));
          padding-right: max(16px, env(safe-area-inset-right)); }
  .global-topbar { padding-left: max(12px, env(safe-area-inset-left));
                   padding-right: max(12px, env(safe-area-inset-right)); }
}

/* Responsive type scale — fluid between 360px and 768px so headings shrink
   smoothly instead of stepping at a single breakpoint. */
@media (max-width: 768px) {
  .page-header h1 { font-size: clamp(18px, 4.2vw, 22px); line-height: 1.25; }
  .page-header h2 { font-size: clamp(16px, 3.8vw, 20px); line-height: 1.3; }
  .page-header p, .page-sub { font-size: 13px; line-height: 1.45; }
  .card h3, .card h4 { font-size: 15px; }
}


/* The injected hamburger is mobile-only; hidden everywhere else. */
.mx-topbar-menu { display: none; }


/* ═══ 2. TOUCH TARGETS ═════════════════════════════════════════════════════ */
/* style.css:3103 SHRINKS buttons on mobile (`.btn{padding:7px 14px;font-size:
   12.5px}` → ~31px tall). That is backwards: touch needs *larger* targets than
   mouse. These rules restore a 44px minimum on every interactive control.     */

@media (max-width: 768px) {
  .btn,
  .btn-sm,
  .btn-secondary,
  .btn-primary {
    min-height: var(--tap);
    padding: 10px 16px;
    font-size: 14px;
  }
  /* .btn-sm keeps its smaller *type* but not a smaller hit area. */
  .btn-sm { padding: 8px 12px; font-size: 13px; }

  /* Icon-only controls: square, centred, full tap size. */
  .btn-icon,
  .btn-action,
  .topbar-icon-btn,
  .ai-assistant-btn,
  .drawer-close,
  .sidebar-toggle,
  .profile-trigger,
  .bottom-tab,
  .modal-header .btn-icon,
  .nav-group-header,
  .voice-search-mic {
    min-width: var(--tap);
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* .btn-action is a fixed 30×30 box (style.css:4121) — override both axes. */
  .btn-action { width: var(--tap); height: var(--tap); }

  /* Row action clusters need breathing room so neighbouring icons aren't
     mis-tapped; 8px is the minimum spacing that testing showed reliable. */
  .party-actions,
  .actions,
  .expense-header-actions,
  .list-page-header-actions,
  .page-header-actions { gap: 8px; flex-wrap: wrap; }

  /* Native controls and links inside table cells.
     This was 36px in the first (code-only) pass. Driving the real app showed
     that is the value that actually reaches the invoice-number link and the
     client/product name link — the primary way a user opens a record from a
     list — because this selector (0,2,0) outranks any single-class rule. 36px
     is below the 44px floor, so it is raised here rather than fought with a
     more specific override elsewhere. */
  .table-stack-mobile a,
  .table-stack-mobile button { min-height: var(--tap); }

  /* Checkboxes/radios are 13px by default — far too small to hit reliably. */
  input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }

  /* Form controls themselves. */
  input, select, textarea { min-height: var(--tap); }
  textarea { min-height: 88px; }

  /* Kill the grey flash on tap; we provide our own :active states. */
  a, button, .btn, [role="button"] { -webkit-tap-highlight-color: transparent; }
}

/* A 10" tablet is 1024px wide but still finger-driven, so it needs the same
   target sizes. Gated on `pointer: coarse` so a 1024px *desktop* window keeps
   its denser, mouse-appropriate controls. */
@media (min-width: 769px) and (max-width: 1024px) and (pointer: coarse) {
  .btn, .btn-sm { min-height: var(--tap); padding: 10px 16px; }
  .btn-icon, .btn-action, .topbar-icon-btn, .profile-trigger, .sidebar-toggle {
    min-width: var(--tap); min-height: var(--tap);
    display: inline-flex; align-items: center; justify-content: center;
  }
  .btn-action { width: var(--tap); height: var(--tap); }
  input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
}


/* ═══ 3. PAGE LAYOUT & HEADERS ═════════════════════════════════════════════ */

@media (max-width: 768px) {
  /* Header actions become full-width stacked buttons — the single biggest
     cause of "the New Invoice button is off-screen" reports. */
  /* Only .page-header-actions was ever covered here. Modules that ship their
     OWN header row were not, and they overflow silently: measured at 426px,
     .expense-header-actions was 780px wide with flex-wrap:nowrap — six buttons
     (Reports, Import CSV, Export, Scan Expense, Bulk Add, Add Expense) running
     to x=796, so the last three sat off-screen. It reads as a page cut in half
     rather than a scrollable strip, and it does NOT show up as page overflow
     because main.content scrolls, which is how it survived the first sweep. */
  .page-header { align-items: stretch; }
  .page-header-actions,
  .expense-header-actions,
  .list-page-header-actions,
  .page-header .actions { width: 100%; flex-wrap: wrap; min-width: 0; }
  .page-header-actions .btn,
  .expense-header-actions > .btn,
  .list-page-header-actions > .btn,
  .page-header .actions > .btn { flex: 1 1 auto; justify-content: center; min-width: 0; }
  /* A six-button row still needs a sane floor, or buttons shrink to slivers. */
  .expense-header-actions > .btn,
  .list-page-header-actions > .btn { flex-basis: calc(50% - 4px); }

  /* Filter/search bars: stack, full width, no horizontal scroll. */
  .list-filter-bar {
    flex-direction: column;
    align-items: stretch !important;  /* inline style="align-items:center" on
                                         several render sites (pages.js:3927) */
    gap: 10px;
  }
  .list-filter-bar > * { width: 100%; min-width: 0; }
  /* style.css bleeds this sticky bar edge-to-edge with margin: 0 -32px, sized
     for the DESKTOP .page gutter. The mobile gutter is 16px, so -32px overshot
     by 16px on each side: measured left:-16 / right:442 in a 426px viewport,
     which made main.content scroll sideways by 16px on every list page. Match
     the gutter instead of removing the bleed — the full-width sticky bar is the
     intended look. */
  .list-filter-bar {
    margin-left: -16px; margin-right: -16px;
    padding-left: 16px; padding-right: 16px;
  }
  .list-filter-bar select,
  .list-filter-bar input { width: 100%; }

  /* KPI/stat strips: two-up on phones, one-up when very narrow (see §9). */
  .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 360px) {
  .page { padding: 12px; }
  .stats-grid { grid-template-columns: 1fr; }
  /* .page drops to a 12px gutter here (see above), so the bleed must too. */
  .list-filter-bar {
    margin-left: -12px; margin-right: -12px;
    padding-left: 12px; padding-right: 12px;
  }
  .page-header-actions,
  .expense-header-actions,
  .list-page-header-actions { flex-direction: column; }
  .page-header-actions .btn,
  .expense-header-actions > .btn,
  .list-page-header-actions > .btn { width: 100%; flex-basis: auto; }
}

/* Reserve room for the fixed bottom-tab bar so the last row of any list, and
   any sticky page-level action bar, clears it. style.css:8012 already pads
   `.content`; this adds the safe-area inset and covers scroll containers. */
@media (max-width: 768px) {
  .content, main.content { padding-bottom: var(--bottom-tabs-h); }
  /* Anchored elements must sit above the bar, not behind it. */
  .toast-stack { bottom: calc(var(--bottom-tabs-h) + 8px); }
}


/* ═══ 4. FORMS — SINGLE COLUMN ═════════════════════════════════════════════ */
/* Every multi-column form grid in the app collapses to one column. Listed
   explicitly (rather than a blanket `[class*=grid]`) so purpose-built layouts
   like the dashboard KPI strip keep their own responsive rules.              */

@media (max-width: 768px) {
  .form-grid,
  .tm-form-grid,
  .branch-form-grid,
  .pay-form-grid-2,
  .pay-form-grid-3,
  .pos-product-form,
  .pos-quick-grid,
  .pos-split,
  .dm-map-grid,
  .exp-rep-grid-2,
  .lg-form,
  .lg-dl-grid,
  .trk-grid {
    grid-template-columns: 1fr !important;  /* several of these are set with
                                               repeat(n,...) at equal or higher
                                               specificity in style.css */
    gap: 14px;
  }

  /* A field flagged `.full` spans the (now single) column — harmless, but keep
     it explicit so nothing collapses to zero width. */
  .form-group.full { grid-column: 1 / -1; }

  /* Label above field, never beside it. */
  .form-group { width: 100%; min-width: 0; }
  .form-group label { margin-bottom: 2px; }

  /* Input + button pairs (GSTIN verify, branch assign, transfer create) stack.
     style.css:4590 already does this for .input-action; extend to the branch
     and logistics variants which use the same pattern. */
  .input-action { flex-direction: column; align-items: stretch; gap: 8px; }
  .input-action > input,
  .input-action > select,
  .input-action > button { width: 100%; margin: 0; }

  /* Inline field rows that were laid out with flex. */
  .field-row, .form-row { flex-direction: column; align-items: stretch; gap: 12px; }
}


/* ═══ 5. TABLES — CARD STACK ═══════════════════════════════════════════════ */
/* style.css:8031 already defines the `.table-stack-mobile` card layout, but it
   was effectively dead:
     (a) only ~20 of ~100 tables opted in, and
     (b) `.table-wrap table{min-width:720px}` (style.css:3094) has HIGHER
         specificity (0,1,1) than `.table-stack-mobile` (0,1,0), so every
         stacked table still forced a 720px min-width → horizontal scroll, and
     (c) almost no `<td>` carried `data-label`, so cards rendered as bare
         values with no column names.
   mobile-ux.js now applies the class and back-fills data-label on every table
   from its own <thead>. These rules make that layout actually take effect.  */

@media (max-width: 768px) {
  /* (b) — defeat every competing min-width. The `!important` is deliberate:
     six separate rules across style.css set min-width 720–760px on these
     elements, several at higher specificity. */
  .table-wrap table.table-stack-mobile,
  table.table-stack-mobile,
  .party-table.table-stack-mobile,
  .tm-table.table-stack-mobile,
  .expense-table.table-stack-mobile,
  .gstr-table.table-stack-mobile,
  .exp-rep-table.table-stack-mobile,
  .lg-table.table-stack-mobile,
  .po-table.table-stack-mobile,
  .pos-history-table.table-stack-mobile,
  .items-table.table-stack-mobile,
  .data-table.table-stack-mobile { min-width: 0 !important; }

  /* The wrapper must stop being a horizontal scroller once its table stacks,
     otherwise it keeps a phantom scrollbar. `:has()` covers modern browsers;
     mobile-ux.js also sets `.is-stacked` on the wrapper for older ones. */
  .table-wrap:has(> table.table-stack-mobile),
  [class*="table-wrap"].is-stacked,
  .table-scroll.is-stacked { overflow: visible !important; }

  /* Promote the stacked layout from 640px (style.css) up to 768px so tablets
     in portrait get cards too — a 10-column invoice table is unusable at 768. */
  .table-stack-mobile { display: block; }
  .table-stack-mobile thead { position: absolute; left: -9999px; top: -9999px; }
  .table-stack-mobile tbody,
  .table-stack-mobile tr,
  .table-stack-mobile td { display: block; width: auto; }
  /* ── Card layout ──────────────────────────────────────────────────────────
     A one-row-per-column stack made an invoice card ~9 rows / 400px tall, so
     barely two records fit on a 390px screen and the list read as a wall.

     The card is a two-column grid instead: the identifier and the amount share
     the header row, secondary fields pair up two-across with their label above
     the value, and the actions get the full width at the bottom. Same DOM, same
     data — roughly 45% shorter, and with an actual visual hierarchy.          */
  .table-stack-mobile tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: 12px;
    align-items: start;
    border: 1px solid var(--border, #e2e8f0);
    border-radius: 12px;
    padding: 12px 14px;
    margin-bottom: 8px;
    background: #fff;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
  }
  .table-stack-mobile td {
    padding: 4px 0 !important;
    border: none !important;
    /* Explicit line-heights: the page inherits 1.5–1.6, which on a two-line
       label/value pair costs ~10px per field — over a 9-column invoice card
       that alone is most of a screen. */
    line-height: 1.3;
    /* Label above value: a label:value row squeezes long values (client names,
       GSTINs) into a sliver once the label has taken its share of a half-width
       column. Stacking gives the value the full column. */
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    text-align: left;
    gap: 1px;
    font-size: 13.5px;
    min-width: 0;
    /* Card reading order, independent of column order in the <table>:
         1 bulk-select · 2 headline · 3 headline amount · 4 data · 5 actions
       Using `order` rather than explicit `grid-row` matters because the select
       column is optional — pinning the amount to "row 1" would collide with the
       checkbox strip on lists that have one and sit correctly on lists that
       don't. Auto-placement gets it right either way. */
    order: 4;
  }
  .table-stack-mobile td[data-label]::before {
    content: attr(data-label);
    font-weight: 500;
    color: var(--muted, #64748B);
    font-size: 10.5px;
    line-height: 1.25;
    letter-spacing: .02em;
    text-transform: uppercase;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Secondary detail the list views hang under a value (GSTIN, "3d overdue")
     inherits the page's loose leading otherwise. */
  /* 11.5px, not 11px: this rule is (0,2,1) and so outranks the readability
     floor in §16f, which would otherwise have left the secondary line of every
     stacked list row (GSTIN, due date, invoice ref) below the legibility bar
     that the rest of the file holds to. */
  .table-stack-mobile td .cell-hint { display: block; font-size: 11.5px; line-height: 1.3; }

  /* The headline cell — invoice number, client name, product name. It is NOT
     always the first cell: every list view opens with a bulk-select checkbox
     column, so mobile-ux.js tags the first real data cell instead of relying on
     source position. */
  .table-stack-mobile td.mx-cell-title {
    order: 2;
    grid-column: 1 / -1;
    font-weight: 700;
    font-size: 15px;
    line-height: 1.25;
    color: var(--text, #0F172A);
    padding-bottom: 7px !important;
    margin-bottom: 5px;
    border-bottom: 1px solid var(--border, #e2e8f0) !important;
  }
  .table-stack-mobile td.mx-cell-title::before { content: none; }
  /* Sub-detail the list views hang off the identifier (client GSTIN, "3d
     overdue") — keep it readable but clearly secondary. */
  .table-stack-mobile td.mx-cell-title .cell-hint {
    display: block;
    margin: 2px 0 0;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--muted, #64748b);
  }

  /* Bulk-select checkbox: a slim strip above the headline with a real touch
     target, rather than a mystery cell dressed up as the card title. */
  .table-stack-mobile td.mx-cell-select {
    order: 1;
    grid-column: 1 / -1;
    flex-direction: row;
    align-items: center;
    padding: 0 0 6px !important;
    min-height: 0;
  }
  /* The box itself stays 20px — a 44px checkbox just looks broken — while the
     padding around it supplies the touch target, and the matching negative
     margin keeps that padding from pushing the card's layout around. */
  .table-stack-mobile td.mx-cell-select input[type="checkbox"] {
    width: 20px;
    height: 20px;
    padding: 12px;
    margin: -12px;
    box-sizing: content-box;
  }

  /* The money column is what the user scans for — right-aligned on the title
     row so the eye can run down a single column of figures. */
  .table-stack-mobile td.mx-cell-amount {
    order: 3;
    grid-column: 2;
    align-items: flex-end;
    text-align: right;
    padding-bottom: 7px !important;
    margin-bottom: 5px;
    border-bottom: 1px solid var(--border, #e2e8f0) !important;
    font-weight: 700;
    font-size: 15px;
    line-height: 1.25;
    color: var(--text, #0F172A);
    font-variant-numeric: tabular-nums;
  }
  /* …which means the title must give up half the header row to it. The marker
     class comes from mobile-ux.js rather than `:has()` so the layout is not
     conditional on browser support — see classifyColumn(). */
  .table-stack-mobile tr.mx-has-amount td.mx-cell-title { grid-column: 1; }
  /* Any further money columns (tax, balance) stay in the normal grid. */
  .table-stack-mobile td.mx-cell-amount-alt {
    order: 4; grid-column: auto;
    align-items: flex-end; text-align: right;
    font-variant-numeric: tabular-nums; font-weight: 600;
  }

  .table-stack-mobile td.mx-cell-status { align-items: flex-start; }
  .table-stack-mobile td.mx-cell-status .badge,
  .table-stack-mobile td.mx-cell-status .status-badge { margin: 0; }
  .table-stack-mobile td.mx-cell-date { font-variant-numeric: tabular-nums; }

  /* Cells the JS marks as label-less (checkbox column, action column, empty
     header) render without a dangling label. */
  .table-stack-mobile td[data-mx-nolabel]::before { content: none; }
  .table-stack-mobile td[data-mx-nolabel] { align-items: flex-start; }

  /* Action buttons get their own full-width row at the bottom of the card,
     each at the full 44px touch height. */
  .table-stack-mobile td.mx-actions-cell {
    order: 5;
    grid-column: 1 / -1;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding-top: 10px !important;
    margin-top: 6px;
    border-top: 1px solid var(--border, #e2e8f0) !important;
  }
  .table-stack-mobile td.mx-actions-cell .party-actions,
  .table-stack-mobile td.mx-actions-cell .actions {
    width: 100%;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .table-stack-mobile td.mx-actions-cell .btn,
  .table-stack-mobile td.mx-actions-cell .btn-action {
    min-height: 40px;
    min-width: 40px;
    flex: 0 1 auto;
  }

  /* A whole card that is already clickable should feel like a button. */
  .table-stack-mobile tr.is-clickable,
  .table-stack-mobile tr[onclick] {
    -webkit-tap-highlight-color: transparent;
    transition: background 120ms ease, transform 120ms ease;
  }
  .table-stack-mobile tr.is-clickable:active,
  .table-stack-mobile tr[onclick]:active { background: #f8fafc; transform: scale(.995); }

  /* An empty-state row ("No branches yet") must not be dressed as a card. */
  .table-stack-mobile tr.mx-empty-row {
    display: block; border: none; box-shadow: none;
    background: transparent; padding: 24px 0;
  }
  .table-stack-mobile tr.mx-empty-row td {
    align-items: center; text-align: center;
    border: none !important; font-size: 14px;
    grid-column: 1 / -1;
  }
  .table-stack-mobile tr.mx-empty-row td::before { content: none; }

  /* A totals/footer row is a summary, not a record — keep it flat and full
     width so it never reads as one more card in the list. */
  .table-stack-mobile tfoot tr { display: block; border: none; box-shadow: none; background: transparent; }
  .table-stack-mobile tfoot td { flex-direction: row; justify-content: space-between; align-items: center; font-weight: 600; }

  /* Preserve row-level status accents (overdue / void) in card form. */
  .table-stack-mobile tr.is-overdue { border-left: 3px solid var(--c-danger-500, #dc2626); }
  .table-stack-mobile tr.row-void { opacity: .6; }
  .table-stack-mobile tr.is-overdue td:first-child { box-shadow: none; }
  .table-stack-mobile tr.is-overdue td,
  .table-stack-mobile tr.is-overdue:hover td { background: transparent; }
}

/* 320–360px: two columns still fit (≈140px each) but the gutters have to come
   down or long values start truncating mid-word. */
@media (max-width: 360px) {
  .table-stack-mobile tr { padding: 11px 12px; column-gap: 8px; }
  .table-stack-mobile td { font-size: 13px; }
  .table-stack-mobile td:first-child,
  .table-stack-mobile td.mx-cell-amount { font-size: 14.5px; }
  .table-stack-mobile td.mx-actions-cell .btn { flex: 1 1 calc(50% - 4px); justify-content: center; }
}

/* Tables that genuinely need a grid (the invoice *document* preview, the data
   migration column mapper, the POS product grid) are excluded by mobile-ux.js
   and keep a scroll container — but the scroll is contained, never the page. */
@media (max-width: 768px) {
  .table-wrap:not(.is-stacked),
  .table-scroll:not(.is-stacked),
  .dm-preview-table,
  .dm-allsheet-table {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
}


/* ═══ 6. MODALS — THE SAVE BUTTON MUST ALWAYS BE REACHABLE ═════════════════ */
/* showModal() injects the footer *inside* #modal-body, which scrolls. On a
   phone that puts Save/Update/Submit below the fold on every long form — the
   single most-reported blocker. Making #modal-body the scroll container and
   pinning .modal-footer to its bottom keeps the primary action permanently
   visible, with no DOM change (so every existing onclick keeps working).    */

@media (max-width: 1024px) {
  #modal-overlay { padding: 0; align-items: flex-end; }

  #modal, .modal {
    max-width: 100%;
    width: 100%;
    max-height: var(--vh100);
    height: var(--vh100);
    margin: 0;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;             /* the body scrolls, not the shell */
  }

  .modal-header {
    flex: 0 0 auto;
    padding: 14px 16px;
    padding-top: max(14px, env(safe-area-inset-top));
  }
  .modal-header h3 { font-size: 16px; }
  /* The ✕ is ~25px from style.css and is the control every user reaches for
     first. §2's 44px floor is gated on max-width:768px, which a phone in
     landscape (844px wide) never matches — so size it here, in the block that
     governs the full-screen modal presentation at every width up to 1024. */
  .modal-header .btn-icon {
    width: var(--tap);
    height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    flex: 0 0 auto;
  }

  #modal-body {
    /* The footer below bleeds to the modal's edges with a negative margin. That
       margin has to match whatever padding THIS body has — and it varies: the
       preview modal zeroes it (§8c) so the document can sit flush. Deriving
       both from one variable keeps them in step; hard-coding -16px pushed the
       preview's Print/Download bar 16px off each side of the screen, where
       `#modal{overflow:hidden}` then clipped it. */
    --mx-modal-pad: 16px;
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;             /* style.css:4629 caps this — flex handles it */
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding: var(--mx-modal-pad);
    /* Room for the pinned footer so the last field is never hidden under it. */
    padding-bottom: 8px;
  }

  /* ── The pinned action bar ── */
  #modal-body .modal-footer {
    position: sticky;
    bottom: 0;
    z-index: 6;
    margin: 12px calc(-1 * var(--mx-modal-pad)) 0;
    padding: 12px 16px;
    padding-bottom: max(12px, env(safe-area-inset-bottom));
    background: var(--surface, #fff);
    border-top: 1px solid var(--border, #e2e8f0);
    box-shadow: 0 -8px 20px -12px rgba(15, 23, 42, .25);
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: stretch;
    /* Without this, `align-items` defaults to stretch and every button grows to
       the tallest line of a wrapped footer — see the ::before fix below, which
       produced 273px-tall buttons on a 768px tablet. */
    align-items: center;
  }

  /* style.css:4803 injects "Tip: Ctrl+Enter to save · ESC to cancel" as a
     `flex:1` ::before inside the footer. It is hidden below 600px but NOT on
     tablets — where the wrapped footer hands it its own line, the text stacks
     one word per line, and the action bar grows to ~300px (a third of the
     screen) with the buttons stretched to match. Every viewport in this range
     gets the full-screen touch modal, where a Ctrl/ESC hint means nothing. */
  #modal-body .modal-footer::before { display: none; }
  /* Primary action gets the most width and always sits last (thumb-reachable). */
  #modal-body .modal-footer .btn { flex: 1 1 auto; min-width: 110px; justify-content: center; }
  #modal-body .modal-footer .btn-primary { flex: 2 1 60%; order: 3; }
  #modal-body .modal-footer .btn-secondary { order: 1; }
}

/* Very narrow phones: one action per row, primary on top. */
@media (max-width: 380px) {
  #modal-body .modal-footer { flex-direction: column-reverse; }
  #modal-body .modal-footer .btn { width: 100%; flex: 1 1 auto; }
}

/* The bottom-tab bar must not sit on top of a full-screen modal. */
@media (max-width: 768px) {
  body.mx-modal-open .bottom-tabs { display: none; }
  /* style.css:8014 lifts the overlay above the tab bar; with the tab bar hidden
     the modal can use the full viewport instead. */
  #modal-overlay { padding-bottom: 0; }
}

/* Drawer (Phase 4c) — full-screen sheet on phones, side panel on tablet up. */
@media (max-width: 768px) {
  .drawer-panel {
    width: 100%;
    top: 0; right: 0; bottom: 0; left: 0;
    transform: translateY(100%);
    border-radius: 16px 16px 0 0;
  }
  .drawer-root.is-open .drawer-panel { transform: translateY(0); }
  .drawer-header { padding-top: max(14px, env(safe-area-inset-top)); }
  .drawer-body { -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
  .drawer-footer {
    padding-bottom: max(14px, env(safe-area-inset-bottom));
    flex-wrap: wrap;
  }
  .drawer-footer .btn { flex: 1 1 auto; justify-content: center; }
}


/* ═══ 7. NAVIGATION ════════════════════════════════════════════════════════ */
/* Desktop  (>1024px) : persistent sidebar          — unchanged
   Tablet   (769–1024): collapsible icon rail       — new here
   Mobile   (≤768px)  : hamburger drawer + bottom tabs — existing, fixed up   */

/* ── Tablet: auto-collapse the sidebar to an icon rail ── */
@media (min-width: 769px) and (max-width: 1024px) {
  .sidebar:not(.is-expanded) { width: 72px; padding-left: 0; padding-right: 0; }
  .sidebar:not(.is-expanded) .nav-label,
  .sidebar:not(.is-expanded) .nav-section,
  .sidebar:not(.is-expanded) .gs-badge,
  .sidebar:not(.is-expanded) .sidebar-user-info,
  .sidebar:not(.is-expanded) .nav-chevron,
  .sidebar:not(.is-expanded) #sidebar-brand-name { display: none; }
  .sidebar:not(.is-expanded) nav a,
  .sidebar:not(.is-expanded) .nav-group-header { justify-content: center; padding: 12px 0; margin: 1px 8px; }
  .sidebar:not(.is-expanded) .logo { justify-content: center; }
  .sidebar:not(.is-expanded) .sidebar-user { justify-content: center; }
  /* Collapsed rail hides group bodies; the header icon still navigates. */
  .sidebar:not(.is-expanded) .nav-group-body { display: none; }
  /* Expanded state overlays the content instead of squeezing it. */
  .sidebar.is-expanded {
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 50;
    width: 260px; box-shadow: 0 10px 40px rgba(0,0,0,.18);
  }
}

/* ── Mobile drawer ── */
@media (max-width: 768px) {
  .sidebar {
    width: min(84vw, 300px);
    padding-top: max(16px, env(safe-area-inset-top));
    padding-bottom: max(20px, env(safe-area-inset-bottom));
  }
  /* Drawer items need full-size tap targets. */
  .sidebar nav a,
  .sidebar .nav-group-header { min-height: var(--tap); padding: 11px 14px; }

  /* Hamburger entry point. `.topbar-left` is hidden below 600px (style.css:9073)
     and the sidebar's own toggle lives *inside* the drawer, so with the drawer
     shut there was no visible menu affordance. mobile-ux.js injects this button
     as a direct child of .global-topbar (not of .topbar-left) so it survives
     that display:none. */
  .global-topbar { gap: 8px; }
  .mx-topbar-menu { display: inline-flex; flex: 0 0 auto; order: -1; }

  /* ── Topbar: two rows on a phone ──────────────────────────────────────────
     The previous rule here intended search to "collapse to an icon-width field
     so the action icons always fit", but it only cleared max-width, which makes
     the field WIDER. Measured at 375px, the single row asked for ~530px:
     hamburger 44 + search 133 + actions 337 + gaps. The consequences were all
     visible on screen at once:

       • .topbar-center was crushed to width 0 while its 133px search child
         overflowed it — rendering at left:-2, top:-5, height 62 inside a 52px
         bar, i.e. ON TOP of the hamburger and spilling off the top edge.
       • .topbar-right ran to x=409 on a 375px screen, so the profile avatar and
         its chevron were simply off-screen.
       • The bar overflowed horizontally by 34px.

     That overlap is also the reported "blur": .global-topbar and
     .topbar-search-wrap BOTH carry backdrop-filter, so two translucent blurred
     layers stacked on top of each other (and over the hamburger) composite into
     a smear that reads as a ghosted, doubled UI.

     One row genuinely cannot hold this: even with search removed the actions
     alone need ~388px. So search gets its own full-width row. Nothing is
     removed, every control keeps its 44px target, and the bar grows only when
     it must. The nested backdrop-filter is dropped — blurring inside an
     already-blurred sticky bar buys nothing and is what smears. */
  .global-topbar {
    flex-wrap: wrap;
    height: auto;
    min-height: 52px;
    row-gap: 6px;
    padding-top: 6px;
    padding-bottom: 6px;
    align-items: center;
  }

  /* Row 2, full width. `flex-basis:100%` is what forces the wrap. */
  .topbar-center {
    order: 10;
    flex: 1 0 100%;
    width: 100%;
    min-width: 0;
  }
  .topbar-search-wrap {
    width: 100%;
    max-width: none;
    min-width: 0;
    height: 40px;
    padding: 0 12px;
    gap: 8px;
    /* No nested blur — see above. */
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  /* Without min-width:0 the input refuses to shrink and re-overflows the row. */
  #global-search { flex: 1 1 auto; min-width: 0; width: auto; }

  /* Row 1 actions: allowed to shrink, but never below a 44px target. */
  .topbar-right { flex: 1 1 auto; min-width: 0; justify-content: flex-end; gap: 4px; }
  .topbar-right > * { flex: 0 0 auto; }
  .topbar-upgrade-btn { max-width: 132px; overflow: hidden; white-space: nowrap; }
  .topbar-divider { display: none; }   /* pure decoration; it only costs width */
  /* The chevron duplicates what tapping the avatar already does. */
  .profile-trigger > svg { display: none; }

  .topbar-kbd { display: none; }

  /* Row 1 must hold the hamburger AND all five actions, or .topbar-right wraps
     to a THIRD row and the bar grows to 153px. Measured budget at 375px:
     375 − 24 padding − 44 hamburger − 8 gap = 299px for the actions, which need
     44+44+44 (bell/calculator/AI) + 47 (avatar) + gaps ≈ 195 — leaving ~104 for
     the upgrade pill. Below 600px the pill therefore keeps its icon and drops
     its label; the trial state is still spelled out on the dashboard and the
     Billing page, and the button itself still works and stays 44px. */
  @media (max-width: 599px) {
    #topbar-upgrade-label { display: none; }
    .topbar-upgrade-btn { max-width: none; padding-left: 12px; padding-right: 12px; }
  }

  /* Body scroll lock while the drawer is open (style.css sets overflow:hidden;
     position:fixed also stops iOS rubber-band scroll behind the drawer). */
  body.drawer-open { position: fixed; width: 100%; }
}

/* ── Bottom tab bar ── */
@media (max-width: 768px) {
  .bottom-tabs { display: flex; }
  .bottom-tab { min-height: 48px; }
  /* Hide behind full-screen surfaces so it never overlaps their actions. */
  body.mx-drawer-open .bottom-tabs,
  body.drawer-open .bottom-tabs { display: none; }
}
/* Never show the mobile tab bar on tablet/desktop, even if a stale class
   lingers from a resize. */
@media (min-width: 769px) { .bottom-tabs { display: none !important; } }


/* ═══ 8. INVOICE / QUOTATION / PURCHASE-ORDER MODULE ═══════════════════════ */

/* ── 8a. Line-items grid → editable cards ──
   `.items-table` is a 960px fixed-layout grid of inputs (style.css:636). On a
   phone it forced a 960px horizontal scroll *inside* a modal that was itself
   scrolling — effectively unusable. mobile-ux.js stacks it like any other
   table; these rules make the inputs usable in card form. */
@media (max-width: 768px) {
  .items-table.table-stack-mobile tr {
    padding: 14px;
    background: #fff;
    position: relative;
    column-gap: 10px;
  }
  /* Every editable cell is already label-above-value from §5; here it just has
     to fill its grid column so a number input isn't squeezed to ~40px. */
  .items-table.table-stack-mobile td { text-align: left; align-items: stretch; }
  .items-table.table-stack-mobile td::before { align-self: flex-start; }
  /* Description and HSN need the full width; the numeric fields pair up
     two-across, which is how every mobile billing app lays this out. */
  .items-table.table-stack-mobile td:nth-child(2),
  .items-table.table-stack-mobile td[data-label*="escription" i],
  .items-table.table-stack-mobile td[data-label*="item" i],
  .items-table.table-stack-mobile td[data-label*="product" i] { grid-column: 1 / -1; }

  .items-table.table-stack-mobile input,
  .items-table.table-stack-mobile select {
    width: 100%;
    border: 1px solid var(--border, #e2e8f0) !important;  /* style.css:639 sets
                                                             border:none for the
                                                             dense grid look */
    background: #fff !important;
    border-radius: 8px;
    padding: 10px 12px !important;
  }
  .items-table.table-stack-mobile .discount-field { grid-template-columns: minmax(0, 1fr) 64px; }
  /* Serial number becomes a corner badge rather than a full row. */
  .items-table.table-stack-mobile td.line-serial,
  .items-table.table-stack-mobile td:first-child {
    font-size: 12px;
    font-weight: 700;
    color: var(--muted, #64748b);
    border-bottom: 1px solid var(--border, #e2e8f0) !important;
  }
  /* Remove-line button: full-width destructive row at the card bottom. */
  .items-table.table-stack-mobile tr > td:last-child {
    grid-column: 1 / -1;
    padding-top: 10px !important;
    margin-top: 4px;
    border-top: 1px solid var(--border, #e2e8f0) !important;
    flex-direction: row;
    justify-content: flex-end;
    align-items: center;
  }
  .items-table.table-stack-mobile tr > td:last-child .btn,
  .items-table.table-stack-mobile tr > td:last-child button { min-height: 40px; }
  .items-actions { display: flex; flex-direction: column; gap: 8px; }
  .items-actions .btn { width: 100%; justify-content: center; }
  /* Totals box spans full width instead of floating right. */
  .totals-box { justify-content: stretch; }
  .totals-inner { min-width: 0; width: 100%; }
}

/* ── 8b. Sticky action bar for the invoice form ──
   The stepper (style.css:4700) already pins Back/Next on ≤768px. This covers
   the non-stepper paths (quotations, purchase orders, edit-from-list) so the
   primary action is pinned there too. The generic modal footer rule in §6 does
   the heavy lifting; this just guarantees ordering and prominence. */
@media (max-width: 768px) {
  #modal-body .modal-footer #invoice-save-btn,
  #modal-body .modal-footer [id$="-save-btn"],
  #modal-body .modal-footer .btn-primary { order: 3; flex: 2 1 60%; }
  #modal-body .modal-footer #invoice-draft-btn { order: 2; flex: 1 1 40%; }
}

/* ── 8c. Invoice preview → full-screen responsive sheet ──
   The preview renders a fixed ~794px A4 document. style.css:7913 already zooms
   it to fit; these rules give it a proper full-screen presentation with its own
   pinned Print/Download/Save bar, and let the user opt into 100% zoom with a
   contained horizontal scroll when they need to inspect detail. */
@media (max-width: 768px) {
  .modal.modal-has-gst-invoice,
  .modal:has(.gst-invoice) {
    max-width: 100%;
    width: 100%;
    height: var(--vh100);
    border-radius: 0;
  }
  /* Zeroing the padding also zeroes the footer's compensating negative margin
     (see --mx-modal-pad in §6) — the two must never drift apart. */
  .modal-has-gst-invoice #modal-body,
  .modal:has(.gst-invoice) #modal-body {
    --mx-modal-pad: 0px;
    padding: 0;
    background: #f1f5f9;
  }

  .invoice-preview-frame {
    padding: 10px;
    overflow-x: hidden;
    /* Contain the scroll so swiping the document never scrolls the page. */
    overscroll-behavior: contain;
    /* The frame handles pinch itself (mobile-ux.js). Telling the browser we
       own multi-touch here stops Android Chrome from also page-zooming, which
       would fight the gesture and leave the whole app scaled. */
    touch-action: pan-x pan-y;
  }
  /* mobile-ux.js sets the real value as an inline `zoom`, measured from the
     document's own content width. These declarations are only the pre-script
     fallback, so a slow first paint still shows something sensibly sized
     rather than a document overflowing sideways. */
  .invoice-preview-frame .invoice-doc { margin: 0 auto; }
  .invoice-preview-frame:not([data-mx-zoom]) .invoice-doc { zoom: .62; }

  /* Only once the user has zoomed past the fit scale does a horizontal scroll
     axis appear — and it stays inside the frame, never on the page. */
  .invoice-preview-frame.mx-zoomed-in {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }

  /* Thermal / receipt templates are ~420px wide by design. Left to the generic
     A4 rules they were scaled down as if they were 794px and collapsed into an
     unreadable ribbon. Give them a receipt presentation instead: natural width,
     centred, on the frame's grey backing so it reads as paper. */
  .invoice-preview-frame .invoice-doc.tpl-thermal {
    width: min(100%, 400px);
    max-width: 400px;
    box-shadow: 0 6px 24px rgba(15, 23, 42, .16);
  }
  .invoice-preview-frame:not([data-mx-zoom]) .invoice-doc.tpl-thermal { zoom: 1; }

  /* ── Zoom toolbar ── */
  .mx-preview-zoom {
    position: sticky;
    top: 0;
    z-index: 4;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    padding: 8px 10px;
    padding-right: max(10px, env(safe-area-inset-right));
    background: rgba(241, 245, 249, .94);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-bottom: 1px solid rgba(148, 163, 184, .28);
  }
  .mx-zoom-btn {
    min-width: var(--tap);
    min-height: var(--tap);
    padding: 0 12px;
    border: 1px solid var(--border, #e2e8f0);
    border-radius: 10px;
    background: #fff;
    color: #334155;
    font: 600 15px/1 inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  /* Bleed to the frame edges — but only inside the modal preview, which is the
     only place with the 10px frame padding to cancel out. The shared public
     page has none, so it keeps the bar inside its own box. */
  .invoice-preview-frame .mx-preview-zoom { margin: -10px -10px 8px; }
  .mx-zoom-btn:active { background: #f1f5f9; }
  .mx-zoom-fit { font-size: 13px; font-weight: 600; }
  .mx-zoom-pct {
    min-width: 46px;
    text-align: center;
    font-size: 12.5px;
    font-weight: 600;
    color: #475569;
    font-variant-numeric: tabular-nums;
  }
}

/* ── 8d. Shared public invoice/quotation page (no app shell) ──
   This is the page a *customer* opens from a WhatsApp link, so it is the most
   phone-heavy surface in the product. It gets the same measured auto-fit and
   pinch zoom as the in-app preview (mobile-ux.js treats it as a preview frame).
   style.css:1383 forces the doc to a 760px minimum here; that has to go, or
   "fit" would still mean a sideways swipe. */
@media (max-width: 768px) {
  .public-share-shell { overflow-x: hidden; touch-action: pan-x pan-y; }
  .public-share-shell .invoice-doc.gst-invoice { min-width: 0; }
  /* Pre-script fallback only — JS replaces this with the measured scale. */
  .public-share-shell:not([data-mx-zoom]) .invoice-doc.gst-invoice { zoom: .6; }
  .public-share-shell.mx-zoomed-in { overflow-x: auto; -webkit-overflow-scrolling: touch; }
}
@media (max-width: 400px) {
  .public-share-shell:not([data-mx-zoom]) .invoice-doc.gst-invoice { zoom: .45; }
}


/* ═══ 9. DASHBOARD & REPORTS ═══════════════════════════════════════════════ */

@media (max-width: 768px) {
  /* KPI cards: 2-up on phones (1-up under 360px, §3) — a single column wastes
     vertical space and pushes the charts far below the fold. */
  .dash-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .dash-kpi-card { padding: 14px; }
  .dash-kpi-card .dash-kpi-value,
  .stat-card .value { font-size: clamp(17px, 5vw, 22px); }

  /* Charts: full width, capped height, and never wider than their container.
     Chart.js needs an explicitly-sized parent to size its canvas correctly. */
  .dash-chart-grid,
  .dash-table-grid,
  .dash-predictions-grid,
  .dash-actions-grid { grid-template-columns: 1fr; gap: 14px; }
  .dash-chart-card { padding: 14px; min-width: 0; }
  .dash-chart-card canvas,
  .chart-container canvas,
  canvas[id*="chart"], canvas[id*="Chart"] {
    max-width: 100% !important;
    height: auto !important;
    max-height: 260px;
  }
  .chart-container { position: relative; width: 100%; min-width: 0; height: 260px; }

  /* Chart legends wrap instead of clipping. */
  .dash-chart-legend { flex-wrap: wrap; gap: 8px; font-size: 11px; }

  /* Dashboard filter/date-range controls. */
  .dash-filters, .dash-toolbar, .exp-rep-filters {
    flex-direction: column; align-items: stretch; gap: 10px; width: 100%;
  }
  .dash-filters select, .dash-filters input,
  .exp-rep-filters select, .exp-rep-filters input { width: 100%; }

  /* Health / hero blocks. */
  .dash-hero-section { flex-direction: column; align-items: stretch; gap: 14px; }
  .dash-health-card { flex-direction: column; align-items: flex-start; gap: 14px; padding: 18px; }
  /* NOT `position: static`. This is a hover popover: the base rule takes it out
     of flow and reveals it on :hover/:focus-within. Making it static put a
     `visibility: hidden` 236x180 box back into normal flow, so it permanently
     reserved space it never painted — the large empty green slab under the
     greeting, with the "Healthy · 80/100" pill stranded inside it. It was also
     what forced the hero row to ~375px and pushed the period filter off the
     left edge (see §16d-2).

     Hidden outright rather than restored as a popover: hover does not exist on
     touch, the chip's own tap handler navigates to Reports (so it can never
     stay open long enough to read), and on a live account it renders empty. The
     health detail remains reachable by tapping the chip. */
  .dash-health-pop { display: none; }

  /* Reports: GSTR + expense report tables are wide and numeric — they stack via
     mobile-ux.js like everything else, but their summary strips need collapsing. */
  .gstr-summary, .exp-rep-summary, .branch-summary-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }
  .gstr-tabs, .exp-rep-tabs {
    display: flex; overflow-x: auto; gap: 6px;
    -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .gstr-tabs::-webkit-scrollbar, .exp-rep-tabs::-webkit-scrollbar { display: none; }
  .gstr-tabs button, .exp-rep-tabs button { white-space: nowrap; flex: 0 0 auto; min-height: 40px; }
}

@media (max-width: 480px) {
  .dash-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .gstr-summary, .exp-rep-summary, .branch-summary-grid { grid-template-columns: 1fr; }
  .chart-container, .dash-chart-card canvas { height: 220px; max-height: 220px; }
}


/* ═══ 10. POINT OF SALE ════════════════════════════════════════════════════ */
/* The POS is a two-pane desktop layout (products | cart) with an 820px-wide
   product grid. On mobile it becomes a single column with the cart pinned as a
   bottom sheet — the standard phone POS pattern. */

@media (max-width: 900px) {
  .pos-grid { grid-template-columns: 1fr; }
  .pos-topbar { flex-direction: column; align-items: stretch; gap: 10px; }
  .pos-customer { grid-template-columns: 1fr; max-width: none; width: 100%; }
  .pos-actions { width: 100%; }
  .pos-actions .btn { flex: 1 1 auto; justify-content: center; }
  .pos-products-panel { min-height: 0; }
  /* The 820px product row grid becomes a two-line card. */
  .pos-product-row { grid-template-columns: minmax(0, 1fr) auto; gap: 8px; min-width: 0; }
  .pos-product-head { display: none; }
  .pos-product-row > span:not(.pos-product-main):not(.pos-product-meta) { display: none; }
  .pos-product-row .pos-product-meta { display: block; }
  .pos-cart-panel { position: static; }
  .pos-cart-item { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
  .pos-reports { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pos-pay-modes, .pos-print-buttons { grid-template-columns: 1fr; }
  .pos-heatmap { grid-template-columns: repeat(4, 1fr); }
}


/* ═══ 11. MISC MODULES ═════════════════════════════════════════════════════ */

@media (max-width: 768px) {
  /* Data migration wizard */
  .dm-kpis, .dm-stats, .dm-source-grid, .dm-analysis-head,
  .dm-insights, .dm-ws-grid { grid-template-columns: 1fr !important; }
  .dm-preview-table table { min-width: 640px; }   /* genuinely needs a grid */

  /* Team management: stop hiding data columns (style.css:3134/3148 hid phone,
     dept and last-active). In card form there is room for every field, so the
     mobile user stops seeing less data than the desktop user. */
  .tm-table.table-stack-mobile thead th,
  .tm-table.table-stack-mobile tbody td { display: block; }
  .tm-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tm-modal { max-width: 100%; height: var(--vh100); border-radius: 0; }
  .tm-modal-footer {
    position: sticky; bottom: 0;
    padding-bottom: max(14px, env(safe-area-inset-bottom));
    background: #fff; border-top: 1px solid var(--border, #e2e8f0);
  }
  .tm-modal-footer .btn { flex: 1 1 auto; justify-content: center; }

  /* Pricing / plan cards */
  .pricing-grid, .plan-grid { grid-template-columns: 1fr !important; }

  /* Onboarding / getting-started checklist */
  .gs-grid, .onboarding-grid { grid-template-columns: 1fr !important; }

  /* Command palette */
  .cmdp-panel { width: 94vw; max-width: 94vw; top: 8vh; }

  /* AI panel */
  .ai-panel { position: fixed; left: 8px; right: 8px; width: auto; max-height: 70vh; }
}


/* ═══ 13. INVOICE WIZARD — THE NEXT BUTTON IS NON-NEGOTIABLE ═══════════════ */
/* InvoiceStepper.dockNav() moves `.invoice-stepper-nav` out of the scrolling
   #modal-body and makes it a direct flex child of #modal. Sticky positioning
   inside the scroller was not enough: on iOS the scroller's bottom edge sits
   under the collapsing URL bar, and once a field is focused it sits under the
   keyboard — so Back/Next disappeared at exactly the moment they were needed.
   As a flex footer it is laid out outside the scroll area and is structurally
   unable to scroll away.                                                     */

#modal > .invoice-stepper-nav.is-modal-docked {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  margin: 0;
  padding: 12px 16px;
  padding-bottom: max(12px, env(safe-area-inset-bottom));
  padding-left: max(16px, env(safe-area-inset-left));
  padding-right: max(16px, env(safe-area-inset-right));
  background: var(--surface, #fff);
  border-top: 1px solid var(--border, #e2e8f0);
  box-shadow: 0 -10px 24px -14px rgba(15, 23, 42, .3);
  /* Above the sticky step pills and any field dropdown that renders inside the
     form body — the bar must never be painted over. */
  position: relative;
  z-index: 12;
}
#modal > .invoice-stepper-nav .btn {
  flex: 1 1 auto;
  justify-content: center;
  gap: 6px;
  min-height: var(--tap);
  font-size: 14.5px;
  font-weight: 600;
}
/* Next/Preview is the action being reached for — give it the larger share. */
#modal > .invoice-stepper-nav #stepper-next-btn { flex: 1.6 1 auto; }
#modal > .invoice-stepper-nav .invoice-stepper-progress {
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--muted, #64748b);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  white-space: nowrap;
}
/* Back is disabled on step 1 — keep the slot so the bar doesn't reflow. */
#modal > .invoice-stepper-nav .btn[disabled] { opacity: .4; pointer-events: none; }

/* Under 340px the three-up bar gets cramped; the step counter is the least
   important element, so it is the one that goes. */
@media (max-width: 340px) {
  #modal > .invoice-stepper-nav .invoice-stepper-progress { display: none; }
}

@media (max-width: 768px) {
  /* style.css:4790 (`body.invoice-stepper-active #modal-body`) sets
     `max-height:100vh; padding-bottom:0` at a higher specificity than §6's
     `#modal-body`. With the nav docked, 100vh is now actively wrong — it is
     larger than the flex track on iOS and re-introduces the clipped bottom.
     Matched specificity here so the later file wins. */
  body.invoice-stepper-active #modal-body {
    max-height: none;
    padding-bottom: 12px;
  }
  /* The wizard drives the flow from its own docked bar, so the original
     Cancel / Save Draft / Preview & Save footer must stay hidden — showing both
     gives the user two competing sets of primary actions.

     style.css:4787 already says `body.invoice-stepper-active .modal-footer
     {display:none}` (0,2,0), but §6's `#modal-body .modal-footer` (1,1,0) wins
     on the ID and put it back. The footer has to stay in the DOM either way:
     step 4's "Preview & Save" and the "Save as draft instead" link both work by
     forwarding a click to the buttons inside it. */
  body.invoice-stepper-active #modal-body .modal-footer { display: none; }
  /* The step pills stay sticky at the top of the scroller — that part works.
     But they are tap targets (jump to step N), and style.css:4644 gives them
     6px of padding for a ~36px box. Anything the user is expected to hit needs
     the full 44px. */
  .party-form-wrap.is-stepper-mode .invoice-stepper {
    padding-top: 8px;
    background: var(--surface, #fff);
  }
  .party-form-wrap.is-stepper-mode .invoice-step-pill {
    min-height: var(--tap);
    padding: 8px 4px;
  }
  /* The in-flow copy of the nav must not also render while a docked one exists
     (it can't, since the node is moved — but a re-render of the form HTML can
     briefly produce a second one before applyMode runs). */
  .party-form-wrap.is-stepper-mode .invoice-stepper-nav:not(.is-modal-docked) { display: none; }
}


/* ── 13b. Landscape phones ──
   style.css:4622 wraps the whole stepper in `@media (max-width:768px)`, so a
   phone in landscape (844x390) falls out of wizard mode and gets the desktop
   single-page form — in the viewport with the LEAST vertical room of any. The
   JS gate (InvoiceStepper.MOBILE_QUERY) now covers this shape, so the display
   rules have to as well.

   Only the rules that switch the wizard on are restated; everything cosmetic
   still comes from style.css, and the docked action bar in §13 was already
   width-independent. Every selector here is gated on a class that only
   InvoiceStepper.applyMode() sets, so no desktop viewport can pick them up. */
@media (max-height: 500px) and (pointer: coarse) {
  .party-form-wrap.is-stepper-mode .invoice-stepper {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 8px 4px 10px;
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--surface, #fff);
    border-bottom: 1px solid var(--border, #e2e8f0);
  }
  .party-form-wrap.is-stepper-mode .invoice-step-pill { min-height: var(--tap); }
  .party-form-wrap.is-stepper-mode .form-step { display: none; }
  .party-form-wrap.is-stepper-mode .form-step.is-active-step { display: block; }
  .party-form-wrap.is-stepper-mode .invoice-stepper-review-totals { display: block; }
  .party-form-wrap.is-stepper-mode .invoice-stepper-nav:not(.is-modal-docked) { display: none; }
  body.invoice-stepper-active #modal-body .modal-footer { display: none; }
  body.invoice-stepper-active #modal-body { max-height: none; padding-bottom: 12px; }
}


/* ═══ 14. ON-SCREEN KEYBOARD ═══════════════════════════════════════════════ */
/* iOS Safari does not shrink the layout viewport when the keyboard opens: the
   modal keeps its full `100dvh` height and its footer — Save, or the wizard's
   Next — ends up behind the keyboard. mobile-ux.js measures window.visualViewport
   and publishes the real height as --mx-vvh, which these rules consume. Android
   Chrome resizes the layout viewport instead, so there the variable simply
   equals the layout height and nothing changes. */
@media (max-width: 1024px) {
  body.mx-keyboard-open #modal,
  body.mx-keyboard-open .modal {
    height: var(--mx-vvh, var(--vh100));
    max-height: var(--mx-vvh, var(--vh100));
  }
  /* Don't animate the resize — the keyboard opens fast and a transition here
     reads as the dialog lagging behind the user's tap. */
  body.mx-keyboard-open #modal { transition: none; }
  /* The bottom-tab bar would otherwise float in the middle of the screen. */
  body.mx-keyboard-open .bottom-tabs { display: none !important; }
}


/* ═══ 15. SEARCHABLE SELECT PICKER ═════════════════════════════════════════ */
/* A native <select> carrying a few hundred clients or products is an
   un-filterable flick-list on a phone. mobile-ux.js intercepts those (and only
   those — short selects keep the native control, which is faster) and opens
   this sheet instead. The <select> itself is untouched and stays the source of
   truth; the sheet only writes back its value.                               */

.mx-picker { display: none; }
.mx-picker.is-open { display: block; }

@media (max-width: 768px) {
  .mx-picker.is-open {
    position: fixed;
    inset: 0;
    /* Above #modal (style.css) and the wizard's docked action bar, since a
       picker is always opened from a form that sits inside one. */
    z-index: 12000;
  }
  .mx-picker-backdrop { position: absolute; inset: 0; background: rgba(15, 23, 42, .45); }
  .mx-picker-sheet {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    display: flex;
    flex-direction: column;
    /* Tall enough to show a useful number of rows, short enough that the form
       behind stays visible as context. */
    height: min(88vh, 88dvh);
    background: var(--surface, #fff);
    border-radius: 18px 18px 0 0;
    box-shadow: 0 -12px 40px rgba(15, 23, 42, .3);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    overflow: hidden;
  }
  .mx-picker-head {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    padding: 14px 16px 10px;
    border-bottom: 1px solid var(--border, #e2e8f0);
  }
  .mx-picker-title { margin: 0; font-size: 16px; font-weight: 700; color: var(--text, #0f172a); }
  .mx-picker-close {
    flex: 0 0 auto;
    width: var(--tap); height: var(--tap);
    border: 0; border-radius: 12px;
    background: #f1f5f9; color: #475569;
    font-size: 24px; line-height: 1; cursor: pointer;
  }
  .mx-picker-searchwrap { flex: 0 0 auto; padding: 10px 16px; }
  .mx-picker-search {
    width: 100%;
    min-height: var(--tap);
    padding: 10px 14px;
    border: 1px solid var(--border, #e2e8f0);
    border-radius: 12px;
    background: #f8fafc;
  }
  /* The list is the only scroller, so the sheet cannot outgrow its own height. */
  .mx-picker-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding: 4px 8px 12px;
  }
  .mx-picker-opt {
    display: block;
    width: 100%;
    min-height: 50px;
    padding: 13px 12px;
    border: 0;
    border-radius: 10px;
    background: transparent;
    color: var(--text, #0f172a);
    font: inherit;
    font-size: 15px;
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .mx-picker-opt:active { background: #f1f5f9; }
  .mx-picker-opt.is-selected {
    background: var(--primary-light, #eff6ff);
    color: var(--primary, #2563eb);
    font-weight: 600;
  }
  .mx-picker-empty { padding: 32px 12px; text-align: center; color: var(--muted, #64748b); font-size: 14px; }

  /* Freeze the page behind the sheet so a flick in the list can't scroll it. */
  body.mx-picker-open { overflow: hidden; }
}


/* ═══ 16. FORM CONTROLS ════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  /* 44px minimum on every field, and enough vertical rhythm that adjacent
     fields aren't mis-tapped. (§1 already pins font-size to 16px so iOS never
     zooms the viewport on focus.) */
  .form-group input,
  .form-group select,
  .form-group textarea,
  input[type="text"], input[type="email"], input[type="password"],
  input[type="number"], input[type="tel"], input[type="search"], input[type="url"] {
    min-height: var(--tap);
    padding: 11px 14px;
    border-radius: 10px;
  }
  .form-group textarea, textarea { min-height: 88px; padding: 11px 14px; }
  .form-group { margin-bottom: 14px; }
  .form-group label { font-size: 13px; margin-bottom: 6px; display: block; }

  /* Date/time: iOS renders these with their own intrinsic height and ignores
     line-height, so they end up shorter than their neighbours unless the box is
     sized explicitly. `-webkit-appearance:none` also stops Safari centring the
     text away from every other field in the form. */
  input[type="date"], input[type="month"], input[type="time"], input[type="datetime-local"] {
    -webkit-appearance: none;
    appearance: none;
    min-height: var(--tap);
    padding: 11px 14px;
    text-align: left;
  }
  /* Make the whole control open the picker, not just the tiny calendar glyph. */
  input[type="date"]::-webkit-calendar-picker-indicator {
    position: absolute;
    inset: 0;
    width: auto; height: auto;
    padding: 0; margin: 0;
    opacity: 0;
    cursor: pointer;
  }
  .form-group:has(input[type="date"]) { position: relative; }

  /* Native selects keep a visible affordance and a full-height tap target. */
  .form-group select, select {
    min-height: var(--tap);
    padding-right: 34px;
    background-position: right 12px center;
  }
}


/* ═══ 16b. STANDALONE AUTH PAGES ═══════════════════════════════════════════ */
/* auth.html / auth-otp.html / onboarding.html / accept-invite.html carry their
   own inline <style> and share none of the app's component classes, so §2's
   44px floor — which keys off .btn / .btn-icon — never reached them. This is
   the FIRST screen a phone user meets, and its controls measured 24–42px.
   Selectors are scoped to those pages' own class names.                      */
@media (max-width: 768px) {
  /* Login ⇄ Sign up, and the Password ⇄ Email OTP method switch. */
  .tabs .tab,
  .login-method-switch .lm-btn {
    min-height: var(--tap);
    padding-top: 10px;
    padding-bottom: 10px;
  }
  .tabs { padding: 4px; }

  /* Show/hide-password eye: 24px, and sitting inside the field's right edge.
     The box grows to a real target while the icon keeps its size, and the
     field's right padding grows to match so text never runs under it. */
  .password-toggle-btn {
    width: var(--tap);
    height: var(--tap);
    right: 4px;
  }
  .password-field-wrapper input { padding-right: 48px; }

  /* OTP digit boxes already flex to fit (`.otp-input-box{flex:1}`), so they
     only need a tap-sized height — and a tighter gap so six of them plus five
     gutters still leave each box wide enough to read at 320px. */
  .otp-inputs { gap: 6px; }
  .otp-inputs input,
  .otp-input { min-height: 52px; padding-left: 2px; padding-right: 2px; }

  /* Safe-area padding, matching what the app shell already gets in §1. */
  .auth-shell {
    padding-left: max(20px, env(safe-area-inset-left));
    padding-right: max(20px, env(safe-area-inset-right));
  }
}


/* ═══ 16c. TOUCH TARGETS — MEASURED, NOT ASSUMED ═══════════════════════════ */
/* §2 raised `.btn`, `.btn-sm`, `.btn-icon` and `.btn-action` to 44px. Driving
   the real application on a 375px viewport showed that only covers part of the
   product: most modules ship their OWN control classes that never inherit from
   `.btn`, so they stayed at 25–38px. Every selector below was measured in the
   running app, and the comment records what it was and where.

   Non-interactive status chips (.badge, .status-pill) are deliberately NOT
   listed — they are labels, not targets. They are handled for *readability*
   in §16d instead; inflating them to 44px would wreck the card layout.       */

@media (max-width: 768px) {

  /* ── Topbar / global chrome ── */
  .topbar-upgrade-btn,                 /* 95x32  — "Upgrade" */
  .ai-panel-close {                    /* 24x25  — worst offender in the app */
    min-height: var(--tap);
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* ── Dashboard ── */
  .dash-alert-action,                  /* 98x25  — "View invoices" */
  .dash-view-all-link,                 /* 166x25 — "More charts in Reports" */
  .gs-nudge-cta,                       /* 143x31 — "Continue setup" */
  .gs-nudge-dismiss,                   /* 30x30  — dismiss ✕ */
  .dash-period-tab {                   /* 30x30  — 3M / 6M chart range */
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .dash-period-tab,
  .gs-nudge-dismiss { min-width: 44px; }
  /* The period tabs sit in a tight row; let them breathe without wrapping. */
  .dash-period-tabs { display: flex; gap: 6px; flex-wrap: wrap; }

  /* ── List filters: status chips are the primary way to slice a list ── */
  .seg {                               /* 70x30 — All / Unpaid / Overdue / … */
    min-height: var(--tap);
    padding: 10px 14px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
  }
  .filter-tabs {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    /* A horizontally scrolling chip strip is the right pattern here (Vyapar,
       Zoho and myBillBook all use it) — but it must scroll INSIDE its own box
       and never widen the page. */
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .filter-tabs::-webkit-scrollbar { display: none; }
  .filter-tabs .seg { flex: 0 0 auto; }

  /* ── Row links: the primary way to open a record from a list card ── */
  .row-link-mono,                      /* 64x36  — invoice number */
  .party-name-link {                   /* 109x36 — client / product name */
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
  }

  /* ── Filter / utility buttons ── */
  .filter-clear-btn,                   /* 310x36 — "Clear Filters" */
  .btn-ghost {                         /* 306x38 — "Clear" */
    min-height: var(--tap);
  }

  /* ── Expenses date presets ── */
  .exp-date-preset {                   /* 59x27 — Today / This week / … */
    min-height: var(--tap);
    padding: 10px 14px;
    display: inline-flex;
    align-items: center;
  }
  .exp-date-preset-row { display: flex; flex-wrap: wrap; gap: 8px; }

  /* ── Settings ── */
  .numbering-mode-tab {                /* 149x34 — Simple / Advanced */
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* A bare inline <a> with no padding measured 96x13 ("Upgrade to unlock").
     Scoped to the settings/profile panes so ordinary prose links elsewhere keep
     their natural inline flow. */
  #page-settings a:not(.btn):not(.nav-link),
  #page-profile a:not(.btn):not(.nav-link) {
    display: inline-block;
    min-height: 32px;
    padding: 6px 2px;
    line-height: 1.5;
  }

  /* ── Profile / account management ── */
  .am-btn {                            /* 163x35, 95x35, 122x35 */
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .am-row { flex-wrap: wrap; gap: 8px; }
  .am-row .am-btn { flex: 1 1 auto; }

  /* ── AI assistant panel ──
     This panel had no mobile styling at all and it renders on every page, so it
     contributed the same 5 undersized controls everywhere: unclassed suggestion
     chips at 26px, a 33x33 send button, and a header ✕ that flex-shrank to 43px
     despite min-height:44 (a flex child compresses below its min-height when the
     container is shorter — hence the explicit height + flex-shrink:0 here). */
  .ai-panel-close {
    height: 44px; width: 44px;
    flex: 0 0 auto;
  }
  .ai-suggestions { display: flex; flex-wrap: wrap; gap: 8px; }
  .ai-suggestions button {
    min-height: var(--tap);
    padding: 10px 14px;
    display: inline-flex;
    align-items: center;
    flex: 0 1 auto;
  }
  .ai-panel-input { gap: 8px; align-items: stretch; }
  .ai-panel-input input {
    min-height: var(--tap);
    height: var(--tap);
    flex: 1 1 auto;
  }
  .ai-panel-input button {
    min-height: var(--tap);
    height: var(--tap);
    min-width: 44px;
    flex: 0 0 auto;
  }

  /* Profile: a bare icon button in a flex-between header row. */
  .section-flex-between > button { min-height: var(--tap); min-width: 44px; }

  /* ── Notification panel (ours) — 36px was still short of the bar.
     NOTE: the authoritative fix is in notifications.js, whose injected <style>
     lands in <head> AFTER this file and therefore wins on equal specificity.
     Kept here as a floor for any future non-injected render path. ── */
  .egn-markall, .egn-close { min-height: var(--tap); min-width: 44px; }
}


/* ═══ 16d. READABILITY ═════════════════════════════════════════════════════ */
/* Measured in the running app: status chips rendered at 9.5–11px and several
   metadata labels at 10–11px. Below ~11.5px, text on a phone stops being
   comfortably legible — and on a billing product these chips carry the state a
   user is scanning for (draft / unpaid / overdue / paid).                    */

@media (max-width: 768px) {
  .badge, .status-pill, .pill, .count-badge {
    font-size: 11.5px;
    line-height: 1.35;
    padding: 3px 8px;
    border-radius: 6px;
  }
  /* These carry status meaning, so give them a little more presence. */
  .badge-draft, .badge-unpaid, .badge-overdue, .badge-paid,
  .badge-partially_paid, .badge-void { font-weight: 600; letter-spacing: .01em; }

  .summary-label, .summary-sub, .stat-label, .stat-hint,
  .field-help, .biz-field-label, .exp-date-preset-label,
  .dash-section-period, .sidebar-user-role, .bottom-tab-label {
    font-size: 11.5px;
  }
  .exp-kbd, .topbar-kbd { font-size: 11px; }
}


/* ═══ 17. REMAINING OVERLAYS ═══════════════════════════════════════════════ */
/* Everything that floats above the page. Each one was checked at 320px for
   clipping, for a bottom edge under the tab bar or the home indicator, and for
   touch targets under 44px.                                                  */

@media (max-width: 768px) {

  /* ── Row action menu ──
     ActionMenu._position() (utils.js) already uses fixed positioning and clamps
     to the viewport, so it never clips. What it can't fix is size: a 12-item
     invoice menu is taller than a 320x568 screen, and its rows are 30px. */
  .action-menu {
    min-width: min(280px, calc(100vw - 24px));
    max-width: calc(100vw - 24px);
    /* Leave room for the bar the trigger sits near, either way it flips. */
    max-height: min(70vh, 70dvh);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  .action-menu-item { min-height: var(--tap); padding: 11px 12px; font-size: 14.5px; }
  .action-menu-item svg { width: 17px; height: 17px; }
  .action-menu-trigger { width: var(--tap); height: var(--tap); }

  /* ── Toasts ──
     Bottom-right at 24px puts them on top of the tab bar and, on a notched
     phone, under the home indicator. Full width just above the bar reads far
     better at this size anyway. */
  .toast-stack {
    left: 12px;
    right: 12px;
    max-width: none;
    bottom: calc(var(--bottom-tabs-h) + 8px);
    align-items: stretch;
  }
  /* No tab bar over a modal (§6) or with the keyboard up (§14) — so the toast
     can come back down to the bottom edge. */
  body.mx-modal-open .toast-stack,
  body.mx-keyboard-open .toast-stack {
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  }
  .toast-stack > * { width: 100%; }

  /* ── Profile dropdown ──
     260px anchored to the avatar overflows a 320px screen once the topbar's
     right padding is counted, and it is the least thumb-reachable corner of the
     display. A bottom sheet solves both. */
  .profile-dropdown {
    position: fixed;
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: auto;
    max-height: min(80vh, 80dvh);
    overflow-y: auto;
    border-radius: 18px 18px 0 0;
    transform: translateY(12px);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    z-index: 10600;
  }
  .profile-dropdown.open { transform: translateY(0); }
  .profile-dropdown-item { min-height: var(--tap); font-size: 14.5px; }

  /* ── Calculator ── clears the home indicator as well as the tab bar. */
  .egplcalc { bottom: max(8px, env(safe-area-inset-bottom)) !important; }
  .egplcalc-mini { bottom: calc(var(--bottom-tabs-h) + 12px); }

  /* ── Onboarding tour ── the card must never sit under the tab bar, and its
     buttons need real touch targets. */
  .obt-card { max-width: calc(100vw - 24px); }
  .obt-btn { min-height: var(--tap); padding: 11px 16px; }

  /* ── Command palette ── keep it clear of the keyboard and the notch. */
  .cmdp-panel {
    top: max(6vh, env(safe-area-inset-top));
    max-height: min(80vh, 80dvh);
  }
}


/* ═══ 16d-2. DASHBOARD HERO — GRID/MARKUP DRIFT ═══════════════════════════ */
/* style.css:5814 lays .dash-hero-right out as
      grid-template-columns: auto minmax(148px,1fr) auto auto
   i.e. FOUR columns, written when the hero had four children. It now renders
   two: .dash-health-pop-host (which wraps the health chip) and
   .dash-period-filter. The ≤480px rules that were meant to tame this still
   target `.dash-hero-chip` directly, but the chip is now nested inside the
   pop-host, so the element the grid actually lays out is never sized.

   Measured at 425px, that drift produced both dashboard defects at once:
     • the pop-host resolved to 375px inside a 364px container, so the grid
       overflowed and — because the base rule sets justify-content:flex-end —
       spilled to the LEFT: .dash-period-filter landed at x=-8 with its "PERIOD"
       label at x=-71, which is the sliver of text clipped off the left edge.
     • the grid row was then 180px tall and the chip, a flex child with default
       `align-items: normal`, STRETCHED to fill it — a 139x180 slab of green
       around one line of text ("Healthy · 80/100").

   A two-item row does not need a grid. Flex-wrap sizes to whatever the hero
   actually contains, so this cannot drift again when a child is added or
   removed. */

@media (max-width: 768px) {
  .dash-hero-right {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start;   /* never flex-end: overflow must not go left */
    gap: 8px;
    width: 100%;
  }

  /* The element the grid actually lays out. It must be allowed to shrink —
     `min-width: 0` is what stops it resolving to its 375px intrinsic width. */
  .dash-health-pop-host {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
  }

  /* `height: auto` + `align-self: center` stop the stretch. The chip is a
     status pill; it should hug its text and stay a legible tap target. */
  .dash-hero-chip {
    align-self: center;
    flex: 0 1 auto;
    width: auto;
    height: auto;
    min-height: var(--tap);
    max-width: 100%;
  }

  .dash-period-filter {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 100%;
    min-height: var(--tap);
  }
  .dash-period-filter select { flex: 1 1 auto; min-width: 0; }
}

@media (max-width: 480px) {
  /* style.css:5856 gives .dash-hero-chip width:100% here, but the chip is
     wrapped — so the wrapper is what has to go full width, or the chip's 100%
     resolves against a shrink-to-fit box and the row stays lopsided. */
  .dash-health-pop-host { width: 100%; }
  .dash-hero-chip { width: 100%; justify-content: center; }
  .dash-period-filter { width: 100%; }
}


/* ═══ 16e. TOUCH TARGETS — SECOND MEASURED PASS ════════════════════════════ */
/* §16c was written from a first walk of the app. This pass comes from driving
   all 20 pages plus the full invoice wizard, preview sheet and toasts while
   logged in. Every selector below was measured in the running app at 375px and
   the comment records the size it actually had. Nothing here is speculative. */

@media (max-width: 768px) {

  /* ── Invoice wizard ──
     "Save as draft instead" was a 122x15 text button on the Review step. It is
     one of only two ways to leave the wizard with the work kept, so it has to
     be a real target, not a caption. */
  .stepper-draft-link {
    min-height: var(--tap);
    padding: 12px 8px;
    display: inline-flex;
    align-items: center;
  }
  /* Remove-line button measured 44x40 — width was fine, height was not. */
  #modal-body .rm-btn { min-height: var(--tap); }
  /* Per-line tax-mode chip ("+ GST" / "GST incl.") measured 43px wide. */
  .pmode-chip { min-width: var(--tap); }

  /* ── Scanner ── "browse files" measured 15px tall and is the primary action
     in the Scan Expense / Scan Bill sheet on a phone, where drag-and-drop does
     not exist at all. */
  .exp-scan-link, .pbs-sc-upload {
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* ── Toast ── the dismiss control measured 22x22. A toast can cover the
     primary action beneath it, so closing it must not need a precise tap. */
  .toast-close {
    min-width: var(--tap);
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* ── Floating sidebar toggle ── measured 38x38 on every page. */
  .sidebar-toggle-floating { min-width: var(--tap); min-height: var(--tap); }

  /* ── Empty-state secondary action ── "Import from Excel →" measured 125x20
     and is the only other call to action on an empty list. */
  .empty-state-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
  }

  /* ── Module tab strips ──
     Reports (225x40, unclassed children of .gstr-tabs) and Inventory
     (.inv-tab, 36px) are the primary way to switch view in those modules.
     Matched via the parent because the children carry no class of their own. */
  .gstr-tabs > button,
  .inv-tab {
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* Both strips must scroll inside their own box, never widen the page. */
  .gstr-tabs, .inv-tabs {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .gstr-tabs::-webkit-scrollbar, .inv-tabs::-webkit-scrollbar { display: none; }
  .gstr-tabs > button, .inv-tabs > .inv-tab { flex: 0 0 auto; }

  /* The Reports module builds several of its controls as bare <button> with no
     class and no classed wrapper (measured: "Generate" at 91x32), so there is
     nothing to hook but the page itself. Scoped to this one page so it cannot
     reach any other module's buttons. */
  #page-gstr-filing button { min-height: var(--tap); }

  /* ── Reports download row ── 116x31 and 163x29. */
  .gstr-dl-row > button, .gstr-dl-btn { min-height: var(--tap); }
  .gstr-dl-row { display: flex; flex-wrap: wrap; gap: 8px; }

  /* ── Inventory pager ── Prev/Next measured 53x30. */
  .inv-pager > button {
    min-height: var(--tap);
    min-width: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .inv-pager { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

  /* ── Pricing period toggle ── 86x34 / 156x34. */
  .pricing-toggle-btn {
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}


/* ═══ 16f. READABILITY — SPECIFICITY-CORRECTED ═════════════════════════════ */
/* §16d set an 11.5px floor on a list of classes, but several of them never took
   effect: a component rule with a descendant selector outranks a bare class.
   Measured example — `.summary-label` still computed to 11px on the invoice,
   client, product, vendor and inventory lists because
   `.client-summary-card .summary-label` (0,2,0) in style.css beats
   `.summary-label` (0,1,0) here.

   The class is repeated (`.x.x`) to reach (0,2,0) and win on source order —
   mobile.css is loaded last. That is deliberate in preference to `!important`:
   it raises specificity by exactly one class, so a genuinely more specific
   component rule can still override it, and it keeps this file overridable.
   Only font-size is touched; nothing else about these components changes. */

@media (max-width: 768px) {
  /* List/summary cards — measured 11px */
  .summary-label.summary-label,
  .summary-sub.summary-sub,
  .cell-hint.cell-hint,
  .cell-badge.cell-badge,
  /* Dashboard — measured 10.5–11px */
  .dash-kpi-trend.dash-kpi-trend,
  .dash-chart-range-label.dash-chart-range-label,
  .dash-period-tab.dash-period-tab,
  .dash-table-count.dash-table-count,
  /* Settings → template gallery — measured 8–11px (.tpl-badge was 8px) */
  .template-card-name.template-card-name,
  .template-card-desc.template-card-desc,
  .tpl-cat-title.tpl-cat-title,
  .tpl-cat-count.tpl-cat-count,
  .tpl-badge.tpl-badge,
  .numbering-preview-label.numbering-preview-label,
  /* Team / branches / reports chips — measured 10–11px */
  .tm-branch-chip.tm-branch-chip,
  .tm-protected-note.tm-protected-note,
  .branch-locked-badge.branch-locked-badge,
  .gstr-plan-badge.gstr-plan-badge,
  /* Rewards — measured 11px */
  .rw-tile-hint.rw-tile-hint,
  .rw-fineprint.rw-fineprint,
  .rw-badge-progress.rw-badge-progress,
  /* Profile — measured 11px */
  .email-badge.email-badge,
  .badge-unverified.badge-unverified,
  /* Invoice wizard chrome — measured 10–11px */
  .form-section-sub.form-section-sub,
  .invoice-stepper-progress.invoice-stepper-progress,
  .line-notes-toggle.line-notes-toggle,
  .row-words.row-words,
  .ac-sub.ac-sub,
  .pmode-chip.pmode-chip,
  .ug-status-text.ug-status-text {
    font-size: 11.5px;
    line-height: 1.4;
  }

  /* Card-stacked list tables render their header text as the cell's own label,
     so 10.5px `th` type is read on every row of every list. */
  .table-stack-mobile th,
  .table-stack-mobile td::before { font-size: 11.5px; }
}


/* ═══ 12. PRINT — unchanged behaviour, but never print mobile chrome ═══════ */
@media print {
  .bottom-tabs, .sidebar-toggle-floating, .mx-topbar-menu,
  .mx-preview-zoom, .mx-preview-bar, .invoice-stepper-nav,
  .egn-panel, .egn-backdrop, .mx-picker, .sidebar-overlay { display: none !important; }
  /* Card-stacked tables must revert to real tables when printed. */
  .table-stack-mobile { display: table !important; }
  .table-stack-mobile thead { position: static !important; }
  .table-stack-mobile tbody { display: table-row-group !important; }
  .table-stack-mobile tr { display: table-row !important; border: 0 !important; }
  .table-stack-mobile td { display: table-cell !important; text-align: left !important; }
  .table-stack-mobile td::before { content: none !important; }
}
