/* ═══════════════════════════════════════════════════════════════════════════
   BrokerCNC — MOBILE & TOUCH LAYER
   ───────────────────────────────────────────────────────────────────────────
   Loaded LAST, after every other stylesheet. Two rules govern this file:

     1. Every LAYOUT rule lives inside a `max-width` media query, so the
        desktop cascade is provably untouched. Three deliberate exceptions,
        none of which change a desktop rendering:
          - the base styles for the three elements this layer introduces
            (.mobi-nav-toggle, .mobi-nav-backdrop, .mobi-tip), each hidden
            by default and only ever shown by mobile-shell.js;
          - the iOS focus-zoom guard in section 05 and the tooltip swap in
            section 10, both keyed on the pointer rather than a width. Neither
            problem is about how wide the screen is: a hover-only tooltip is
            broken on a touch device at any width, and an iPad Pro in landscape
            is 1366px wide and still zooms on focus. Both queries match the
            device's PRIMARY pointer, so a touchscreen laptop with a trackpad
            reports `pointer: fine` and keeps the desktop behaviour.
     2. Loading last means equal specificity already wins — `!important` is
        used only where an inline style or an `!important` rule upstream
        genuinely requires it, and every such case is commented.

   Breakpoints (do not invent others):
     1024px  tablet / small laptop — the navigation drawer breakpoint
      768px  phone landscape / small tablet portrait — the main one
      480px  phone portrait

   Pairs with public/js/modules/mobile-shell.js.

   CONTENTS
   Sections 01-12 are the foundation: primitives shared by every surface.
   Sections 13+ are per-surface, and exist only where a surface breaks in a way
   the primitives cannot reach.

     01  App shell & header
     02  Navigation drawer
     03  Content surface & cards
     04  Tables
     05  Forms & controls
     06  Buttons & tap targets
     07  Tab strips, toolbars & filter bars
     08  Grid helpers
     09  Modals, drawers & overlays
     10  Tooltips & hover-only affordances
     11  Charts & canvases
     12  Login page

     13  Shell — global surfaces
     14  Stress Tester & Menu Builder
     15  Control Panel, Alerts, Event Risk & Server Health
     16  Eagle View & My Dashboard
     17  Account Intelligence, Accounts & Search
     18  Investigation Center & Incident Evidence
     19  Predictive Detection
     20  Connection & Network Intelligence
     21  Risk & Exposure surfaces
     22  Market & chart surfaces
     23  Reports & Analytics
     24  Completeness pass — affordances the foundation cannot reach
   ═══════════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════════
   01  APP SHELL & HEADER
   ───────────────────────────────────────────────────────────────────────────
   #app is a two-column grid (206px sidebar | content) with the header spanning
   both via `grid-column-end: 3`. Collapsing to one column REQUIRES resetting
   that end line, otherwise the header creates an implicit second column and
   the page gains a phantom horizontal scroll.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  html {
    -webkit-text-size-adjust: 100%;
  }

  #app {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    height: 100vh;
    height: 100dvh;
  }

  .header {
    grid-column: 1 / -1;
    grid-row: 1;
    grid-template-columns: minmax(0, 1fr);
    min-height: 56px;
    padding-top: env(safe-area-inset-top);
  }

  /* The wordmark lives in the drawer on mobile; the header keeps the hamburger
     plus the page title. */
  .header-logo {
    display: none;
  }

  .header-top {
    grid-column: 1;
    grid-row: 1;
    height: 56px;
    padding: 0 calc(8px + env(safe-area-inset-right)) 0 calc(4px + env(safe-area-inset-left));
    gap: 6px;
  }

  .header-spacer {
    flex: 1 1 auto;
    min-width: 0;
  }

  .header-context {
    margin-top: 0;
    border-left: 0;
    padding-left: 4px;
  }

  .header-context-title {
    font-size: 16px;
  }

  .header-context-subtitle,
  .header-context-eyebrow {
    display: none;
  }

  .header-right {
    gap: 4px;
    flex: 0 0 auto;
  }

  .server-time,
  #tab-refresh-stamp {
    display: none;
  }

  .feed-indicator span {
    display: none;
  }

  /* `overflow-x: hidden` here is the safety net that stops the whole page from
     panning sideways. Wide content stays reachable because it is given its own
     scroll container instead — .mobi-tscroll for tables, and a per-surface
     overflow-x rule for the strips in sections 13-24.

     The left/right insets matter because the viewport meta now says
     viewport-fit=cover: with the previous `auto`, iOS inset the layout viewport
     away from the sensor housing for us. With `cover` it does not, and in
     landscape on a notched phone the housing covers ~44px of one edge. */
  .main-content {
    grid-column: 1;
    grid-row: 2;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
    padding-bottom: env(safe-area-inset-bottom);
  }

  .breadcrumb-bar {
    padding: 6px 10px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .breadcrumb-bar::-webkit-scrollbar {
    height: 0;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   02  NAVIGATION DRAWER
   ───────────────────────────────────────────────────────────────────────────
   The 206px sidebar becomes an off-canvas drawer. mobile-shell.js strips
   `body.menu-collapsed` while in the mobile range, so none of the icon-rail
   rules apply here and no specificity gymnastics are needed to undo them —
   only the plain `@media (max-width: 900px)` icon-rail block in app.css has
   to be reversed, which the same-specificity rules below do by load order.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Base state for the two elements this layer injects — hidden everywhere
   except inside the mobile media query below. */
.mobi-nav-toggle {
  display: none;
}

.mobi-nav-backdrop {
  display: none;
}

@media (max-width: 1024px) {

  .mobi-nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--border, rgba(255, 255, 255, 0.06));
    border-radius: 2px;
    background: transparent;
    color: var(--text2, #a9b4c7);
    cursor: pointer;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
  }

  .mobi-nav-toggle:active {
    background: rgba(25, 211, 255, 0.08);
    border-color: rgba(25, 211, 255, 0.22);
  }

  .mobi-burger {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 18px;
    height: 14px;
  }

  .mobi-burger i {
    display: block;
    height: 2px;
    width: 100%;
    border-radius: 1px;
    background: currentColor;
    transition: transform 0.22s ease, opacity 0.16s ease;
  }

  body.mobi-nav-open .mobi-nav-toggle {
    color: var(--cyan, #19d3ff);
    border-color: rgba(25, 211, 255, 0.3);
  }

  body.mobi-nav-open .mobi-burger i:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
  }

  body.mobi-nav-open .mobi-burger i:nth-child(2) {
    opacity: 0;
  }

  body.mobi-nav-open .mobi-burger i:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
  }

  .mobi-nav-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 3900;
    background: rgba(0, 6, 25, 0.62);
    opacity: 0;
    visibility: hidden;
    touch-action: none;
    transition: opacity 0.24s ease, visibility 0.24s;
  }

  body.mobi-nav-open .mobi-nav-backdrop {
    opacity: 1;
    visibility: visible;
  }

  .v-menu {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: 4000;
    width: min(86vw, 292px);
    padding-top: calc(12px + env(safe-area-inset-top));
    padding-bottom: calc(12px + env(safe-area-inset-bottom));
    padding-left: calc(7px + env(safe-area-inset-left));
    transform: translateX(-100%);
    /* `visibility` keeps the off-canvas menu out of the tab order and out of
       the accessibility tree while it is closed. */
    visibility: hidden;
    transition:
      transform 0.26s cubic-bezier(0.4, 0, 0.2, 1),
      visibility 0.26s;
    will-change: transform;
    box-shadow: none;
    overscroll-behavior: contain;
  }

  body.mobi-nav-open .v-menu {
    transform: translateX(0);
    visibility: visible;
    box-shadow: 12px 0 40px rgba(0, 0, 0, 0.55);
  }

  /* The desktop collapse strip has no meaning once the sidebar is a drawer. */
  .v-menu-collapse-edge {
    display: none;
  }

  /* Reverse the 900px icon-rail treatment in app.css — the drawer is wide
     enough for full labels. */
  .v-menu-logo img {
    max-width: 150px;
  }

  .v-menu-label {
    max-width: none;
    opacity: 1;
    overflow: hidden;
    padding: 7px 10px 4px;
  }

  .v-menu-sub-label {
    max-width: none;
    opacity: 0.76;
    overflow: hidden;
    padding: 8px 10px 3px 22px;
  }

  .v-menu-text {
    max-width: none;
    opacity: 1;
    overflow: hidden;
  }

  .v-menu-item {
    justify-content: flex-start;
    padding: 10px 12px;
    min-height: 44px;
  }

  .v-menu-icon {
    margin-right: 10px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   03  CONTENT SURFACE & CARDS
   ───────────────────────────────────────────────────────────────────────────
   .card-hdr uses negative margins that must track .card's padding exactly,
   so the two are always changed together.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  .tab-content {
    padding: 8px 8px 28px;
  }

  /* Two cards deliberately run at padding:0 because their own children supply
     the inset — .fa-card (financial-analytics.css:25) and .mydash-controls
     (app.css:4568). Both are (0,1,0) like this rule, so mobile.css's later
     source order would silently re-pad them and break their layouts. */
  .card:not(.fa-card):not(.mydash-controls) {
    padding: 14px;
  }

  .card-hdr {
    margin: -14px -14px 12px;
    padding: 8px 12px;
  }

  .card-value {
    font-size: 22px;
  }

  .hero-number {
    font-size: 26px;
  }

  .panel {
    margin-bottom: 12px;
  }

  .panel-header {
    padding: 10px 12px;
    flex-wrap: wrap;
    gap: 8px;
  }

  .panel-body {
    padding: 12px;
    padding-bottom: 20px;
  }

  .stat-card {
    padding: 12px;
  }

  .stat-val {
    font-size: 18px;
  }
}

@media (max-width: 480px) {

  .card:not(.fa-card):not(.mydash-controls) {
    padding: 12px;
  }

  .card-hdr {
    margin: -12px -12px 10px;
  }

  .card-value {
    font-size: 20px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   04  TABLES
   ───────────────────────────────────────────────────────────────────────────
   app.css declares `table { table-layout: fixed; width: 100% }` with
   `tbody td { max-width: 0; white-space: nowrap; text-overflow: ellipsis }`.
   That is a deliberate desktop choice — columns share the available width and
   truncate. On a 390px viewport it degrades to a column of ellipses.

   The mobile behaviour is the opposite: let content size the columns and let
   the row scroll. That needs BOTH halves — the table must be allowed to exceed
   its container, and the container must be able to scroll in x. mobile-shell.js
   supplies the container by tagging each table's parent `.mobi-tscroll`.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  .mobi-tscroll {
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }

  /* Per CSS Overflow L3, `overflow-y: visible` computes to `auto` as soon as the
     other axis is not visible — so tagging a wrapper makes it a scroll container
     in BOTH axes. That re-parents `thead th { position: sticky; top: 0 }`
     (app.css:1937) from .main-content to the wrapper, and a wrapper at auto
     height has no scroll range, so the header would stick to nothing and simply
     stop working. app.css documents this exact hazard at :1925.

     Rather than half-break it, sticky is switched off inside a mobile scroll
     container and the header scrolls with the table. The alternative — giving
     the wrapper a max-height so sticky has range — creates a nested vertical
     scroll region, and on touch that traps the page scroll whenever a swipe
     starts over the table. Wrappers that opted into their own max-height via an
     inline style keep their sticky header, because they already had the range. */
  .mobi-tscroll:not([style*="max-height"]) thead th {
    position: static;
  }

  table {
    table-layout: auto;
    width: auto;
    min-width: 100%;
  }

  /* Base is `max-width: 0`, the truncate-to-fit hack that only works under
     `table-layout: fixed`. Auto layout needs a real cap instead: wide enough
     that ordinary columns size to their content, tight enough that one long
     free-text cell cannot stretch the table to several screens. */
  tbody td {
    max-width: 68vw;
  }
}

@media (max-width: 768px) {

  table {
    font-size: 12px;
  }

  thead th {
    padding: 7px 9px;
  }

  tbody td {
    padding: 7px 9px;
    font-size: 12px;
  }

  /* Row hover is the only affordance marking the active row on desktop; on
     touch it never fires, so lean on the zebra striping instead. */
  tbody tr:nth-child(even) {
    background: rgba(255, 255, 255, 0.03);
  }
}

@media (max-width: 480px) {

  thead th {
    padding: 6px 8px;
    font-size: 9px;
  }

  tbody td {
    padding: 6px 8px;
    font-size: 11px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   05  FORMS & CONTROLS
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  .form-control,
  select,
  textarea {
    max-width: 100%;
  }

  .form-control,
  select,
  input[type="text"],
  input[type="number"],
  input[type="password"],
  input[type="search"],
  input[type="email"],
  input[type="date"],
  input[type="datetime-local"],
  input[type="time"] {
    min-height: 40px;
  }

  .form-check-input {
    width: 20px;
    height: 20px;
  }

  .form-switch .form-check-input {
    width: 40px;
    height: 22px;
  }

  .input-group {
    flex-wrap: wrap;
  }

  .input-group > * {
    min-width: 0;
  }
}

/* iOS zooms the whole viewport whenever a focused field renders below 16px,
   and with a sticky header the zoomed page then has to be pinched back out
   before the next field is reachable. Scoped to coarse pointers so mouse-driven
   small laptops keep their dense sizing.

   `!important` is deliberate and is the only blanket use in this file. A bare
   `input`/`select` selector is (0,0,1), which loses to essentially every
   surface's own field styling — `.ci-toolbar input`, `.pdt-filters select`,
   `.ic-cfg-field input`, `.em-select`, `.mi-set-row input`, `.ni-search input`
   and `.pdt-cfg-field input[type=number]` all declare 12-13px at (0,1,1) or
   higher. Chasing them one by one across nine stylesheets would leave the next
   new surface broken again. This is a viewport-level guarantee that no local
   rule should be able to defeat, so it is enforced rather than negotiated.

   Keyed on pointer alone, with NO width gate: an iPad Pro in landscape is
   1366px wide with a coarse pointer, and zooms on focus exactly like a phone.
   A width gate would have left every large tablet uncovered. */
@media (pointer: coarse) {

  .form-control,
  select,
  textarea,
  input[type="text"],
  input[type="number"],
  input[type="password"],
  input[type="search"],
  input[type="email"],
  input[type="tel"],
  input[type="url"],
  input[type="date"],
  input[type="datetime-local"],
  input[type="time"],
  input:not([type]) {
    font-size: 16px !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   06  BUTTONS & TAP TARGETS
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  .btn {
    min-height: 38px;
    padding: 9px 14px;
    touch-action: manipulation;
  }

  .btn-theme,
  .btn-settings,
  .btn-notif,
  .btn-logout,
  .profile-avatar-btn {
    min-width: 38px;
    min-height: 38px;
    touch-action: manipulation;
  }

  .profile-menu {
    width: min(260px, calc(100vw - 16px));
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   07  TAB STRIPS, TOOLBARS & FILTER BARS
   ───────────────────────────────────────────────────────────────────────────
   There is deliberately no shared .mobi-hscroll utility class. It existed at
   first, but every strip that needed scrolling turned out to need its own
   padding, gap and child-flex treatment as well, so sections 13-24 each style
   their strip directly and nothing ever opted into the generic class.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  .tabs {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    padding: 0 8px;
  }

  .tabs::-webkit-scrollbar {
    height: 0;
  }

  .tab {
    flex: 0 0 auto;
    padding: 11px 13px;
  }

  /* .section-header (app.css:1538) is a space-between flex row with no wrap and
     no gap, used on ~20 tabs. Its right-hand cluster — server select, book
     filter, timestamp, period strip — squeezes the title to ~100px or pushes
     past the edge. Declared once here rather than per-surface: three separate
     sections had each written their own copy of this rule and only the last
     one survived, silently discarding the other two. */
  .section-header {
    flex-wrap: wrap;
    gap: 10px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   08  GRID HELPERS
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  .grid-3,
  .grid-4,
  .grid-6 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 768px) {

  .grid-2,
  .grid-3,
  .grid-4,
  .grid-6 {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }

  .stat-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .row {
    gap: 12px;
  }

  .col-md-3,
  .col-md-6 {
    flex: 1 1 100%;
    min-width: 0;
  }
}

@media (max-width: 480px) {

  .stat-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   09  MODALS, DRAWERS & OVERLAYS
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  .modal-card,
  [class*="modal-card"],
  [class*="modal-box"],
  .session-warn-box {
    width: 100%;
    max-width: 100%;
    max-height: 88vh;
    max-height: 88dvh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding: 18px;
  }

  /* Anchored to the viewport rather than offset by a header height: section 01
     gives .header `env(safe-area-inset-top)` on top of its 56px, so any fixed
     offset is wrong by the inset on a notched phone — the drawer would overlap
     the header and hang the same distance past the bottom. The drawer carries
     its own ✕, so covering the app header costs nothing. */
  .notif-drawer {
    top: 0;
    width: 100%;
    max-width: 100%;
    height: 100vh;
    height: 100dvh;
    padding-bottom: env(safe-area-inset-bottom);
  }

  .notif-drawer-header {
    padding-top: calc(12px + env(safe-area-inset-top));
  }

  #toast-container {
    left: calc(10px + env(safe-area-inset-left));
    right: calc(10px + env(safe-area-inset-right));
    bottom: calc(10px + env(safe-area-inset-bottom));
  }

  .toast {
    min-width: 0;
    width: 100%;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   10  TOOLTIPS & HOVER-ONLY AFFORDANCES
   ───────────────────────────────────────────────────────────────────────────
   Three hover tooltip patterns exist (.info-dot, .metric-info, .info-tip).
   Two of them render inside the cell, so the horizontal scroll containers
   from section 04 would clip them. On coarse pointers all three are replaced
   by the single body-level .mobi-tip panel that mobile-shell.js positions.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The panel is styled UNCONDITIONALLY, outside any media query, and stays
   `display: none` until mobile-shell.js shows it. This has no desktop effect —
   nothing renders — but it matters on hybrid hardware: the long-press and
   chart-tap paths gate on `pointerType === 'touch'`, which is TRUE on a
   touchscreen laptop whose PRIMARY pointer is a trackpad. That machine reports
   `pointer: fine`, so a coarse-only style block would leave the panel unstyled
   and dump an unpositioned div at the end of <body>. The JS gate and the style
   have to agree, and the honest place to agree is "always styled, shown on
   demand". */
.mobi-tip {
  display: none;
  position: fixed;
  z-index: 100000;
  max-width: min(320px, calc(100vw - 16px));
  padding: 10px 13px;
  border: 1px solid rgba(25, 211, 255, 0.18);
  border-radius: 6px;
  background: var(--bg2, #0d1526);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  color: var(--text2, #a9b4c7);
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  white-space: normal;
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
}

@media (hover: none), (pointer: coarse) {

  /* Suppress the hover renderings so they cannot double up with .mobi-tip. */
  .info-tip:hover .tip-content {
    display: none;
  }

  .metric-info:hover::after,
  .metric-info:hover::before {
    opacity: 0;
    visibility: hidden;
  }

  .info-dot.mobi-tip-active,
  .metric-info.mobi-tip-active,
  .info-tip.mobi-tip-active {
    opacity: 1;
    border-color: var(--cyan, #19d3ff);
    color: var(--cyan, #19d3ff);
  }

  /* Mouse-only hints. */
  #chart-zoom-hint {
    display: none;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   11  CHARTS & CANVASES
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  canvas {
    max-width: 100%;
  }

  .uplot,
  .u-wrap {
    max-width: 100%;
  }

  /* uPlot legends are wide multi-column tables; let them wrap on a phone. */
  .u-legend {
    font-size: 11px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   12  LOGIN PAGE
   ───────────────────────────────────────────────────────────────────────────
   The logo carries an inline `height: 350px`. `max-height` constrains a
   replaced element's used height regardless of specificity, and the width is
   re-derived from the intrinsic ratio — so no !important is needed.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  #login-page {
    min-height: 100vh;
    min-height: 100dvh;
    padding: 24px 14px;
    align-items: flex-start;
  }

  .login-box {
    padding: 28px 20px;
    max-width: 100%;
  }

  .login-logo {
    margin-bottom: 18px;
  }

  .login-logo h1 img {
    max-height: 26vh;
    max-width: 100%;
  }

  .btn-login {
    min-height: 46px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   13  SHELL — GLOBAL SURFACES
   ───────────────────────────────────────────────────────────────────────────
   Control Panel master-detail, the four inline-styled shell modals, the
   spread-history modal and the breadcrumb trail. Everything the foundation
   already covers (login page, header, tables, .stat-grid, .notif-drawer,
   .metric-info tooltips) is deliberately absent.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  /* Breadcrumb trail — section 01 turns .breadcrumb-bar into an x-scroller, but
     its children keep the default flex-shrink:1, so a 3-level trail squeezes
     each label into a two-line stump instead of scrolling. */
  .bc-item,
  .bc-current,
  .bc-sep {
    flex: 0 0 auto;
    white-space: nowrap;
  }
}


@media (max-width: 768px) {

  /* The Control Panel master/detail stack used to be duplicated here. Section 15
     declares the same four selectors at the same breakpoint, later in the file,
     and wins every collision — including making `.settings-body { display:block }`,
     which rendered this block's `flex-direction: column` inert. Removed rather
     than left as dead weight; section 15 is the single owner. */
}


@media (max-width: 480px) {

  /* index.ejs declares these modal form grids in style attributes, so the
     .grid-* collapse in section 08 cannot see them. Each 1fr track floors at an
     <input>'s ~177px min-content width, so two tracks need ~364px inside a
     ~318px modal body and the right-hand field is cut off. !important is
     unavoidable — the column list lives in the style attribute. */
  #create-alert-modal [style*="grid-template-columns:1fr 1fr"],
  #server-modal [style*="grid-template-columns:1fr 1fr"],
  #recipient-profile-modal [style*="grid-template-columns:1fr 1fr"],
  #book-modal [style*="grid-template-columns:1fr 1fr"],
  #book-modal [style*="repeat(3,1fr)"],
  #book-modal [style*="1fr 160px 100px"] {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  /* 2FA backup codes — 4 inline columns give ~64px per cell for an 11-char
     XXXXX-XXXXX mono code, so every one-time code wraps at its hyphen. Shown
     exactly once, so a mis-transcribed code is unrecoverable. */
  #profile-backup-code-list {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }

  /* Spread-history modal is pinned to 82vw x 82vh by inline styles — a 320x692
     box on a 390px phone, which leaves the uPlot pane ~320x200. The card is
     unclassed so the sheet rule in section 09 does not reach it. Percentages of
     the fixed inset:0 overlay are used rather than 100vw/100dvh: they track the
     overlay exactly, so flex centring cannot offset the sheet. */
  #tick-stats-modal > div {
    width: 100% !important;
    max-width: 100% !important;
    height: 100% !important;
    max-height: 100% !important;
    border-radius: 0 !important;
  }

  /* Profile modal — the overlay carries an inline padding:24px on top of
     .profile-modal-card's own 18px: 42px of dead gutter each side. */
  #profile-modal {
    padding: 8px !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   14  STRESS TESTER & MENU BUILDER
   ───────────────────────────────────────────────────────────────────────────
   Two app.css surfaces whose rows are hard-coded wide: neither is a <table>,
   so the .mobi-tscroll wrapper from section 04 never reaches them and the
   overflow runs into .main-content's overflow-x: hidden instead — i.e. it is
   clipped, not scrollable.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* Stress Tester > Cascade Path. Four `1fr` tracks over steps that carry
     min-width: 0, so the tracks collapse to ~79px while each label stays
     ~112px of nowrap mono ("34 stop / 12 call"): the four labels overlap one
     another and the last one spills out of .cascade-box. Two tracks clear the
     label; normal wrapping covers longer stop/call counts. */
  .cascade-path-bars {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cascade-path-label {
    white-space: normal;
  }

  /* Control Panel > Menu Builder. .mb-item is a nowrap flex row of seven
     children — handle, icon preview, label input (min 80px), icon input
     (92px), nowrap slug, hide, delete — roughly 440px of irreducible width
     inside a ~330px card. The tail of the row lands past the viewport edge
     and .main-content clips it, so hide/delete cannot be tapped at all. */
  .mb-item,
  .mb-cat-head {
    flex-wrap: wrap;
  }

  /* The slug is a read-only label, not a control; its nowrap keeps it glued
     to the inputs' line, so give it a line of its own below them. */
  .mb-slug {
    flex-basis: 100%;
    order: 1;
    white-space: normal;
  }

  /* The inputs need that 26px nesting indent more than the drag cue does. */
  .mb-items {
    padding-left: 10px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   15  CONTROL PANEL, ALERTS, EVENT RISK & SERVER HEALTH
   ───────────────────────────────────────────────────────────────────────────
   Three tabs pin themselves to a 100vh-derived inline height and scroll
   internally. On mobile 100vh is the LARGE viewport, so the bottom of every
   such nested scroller — which is exactly where the Save buttons sit — lives
   under the browser chrome. All of them are released to .main-content's own
   native scroll here.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* Control Panel is one card at inline `height: calc(100vh - 80px)`
     (control-panel.ejs:4) wrapping .settings-body -> .settings-content.
     Dropping .settings-body to block also takes .settings-content's `flex: 1`
     out of play, so the whole chain is plain content-height block flow. */
  #tab-control-panel > .card {
    height: auto !important;
  }

  .settings-body {
    display: block;
  }

  /* The permanent 160px sub-nav rail (app.css:2066) leaves ~180px for the
     densest form surface in the app. Same scroll-strip treatment as .tabs. */
  .settings-nav {
    width: auto;
    flex-direction: row;
    border-right: 0;
    border-bottom: 1px solid var(--border);
    padding: 6px 8px;
    gap: 4px;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  .settings-nav::-webkit-scrollbar {
    height: 0;
  }

  .settings-nav-item {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    min-height: 38px;
    white-space: nowrap;
  }

  .settings-content {
    padding: 12px 10px;
  }

  /* Every fixed column list in the tab is written into a `style` attribute
     (worst: repeat(2, minmax(220px,1fr)) = a 452px minimum at :1821), so only
     !important reaches them. The :not() guards leave the seven
     repeat(auto-fit|auto-fill, minmax()) grids alone — those already resolve
     to a single column at phone width. */
  #tab-control-panel div[style*="grid-template-columns"]:not([style*="auto-fit"]):not([style*="auto-fill"]) {
    grid-template-columns: 1fr !important;
  }

  /* .section-header (app.css:1538) is space-between with no wrap, and its
     right-hand toolbars are inline flex rows with no wrap either — a 150px
     filter input plus two or three buttons against a ~340px column. */
  #tab-control-panel .section-header,
  #tab-event-risk .section-header {
    flex-wrap: wrap;
  }

  #tab-control-panel .section-header > div,
  #tab-event-risk .section-header > div {
    flex-wrap: wrap;
    min-width: 0;
  }

  /* CP > System: six sub-tab buttons wrap to three rows of 35px chrome above
     every settings block. One scrolling 40px row instead. */
  #sys-tab-bar {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  #sys-tab-bar::-webkit-scrollbar {
    height: 0;
  }

  #sys-tab-bar .systab-btn {
    flex: 0 0 auto;
    padding: 8px 12px;
    min-height: 40px;
  }

  /* Menu Builder rows are nowrap flex lines of ~440px minimum: handle + icon
     preview + label input + 92px icon input + a nowrap slug + two buttons.
     Wrapping plus a full-width label line removes the overflow; the slug is
     the other hard minimum ("connection-intelligence" never breaks). */
  .mb-cat-head,
  .mb-item {
    flex-wrap: wrap;
  }

  .mb-label {
    flex: 1 1 100%;
    min-width: 0;
  }

  .mb-slug {
    white-space: normal;
    overflow-wrap: break-word;
    overflow-wrap: anywhere;
  }

  .mb-items {
    padding-left: 12px;
  }

  /* Recipient profile rows: a 130px kind select + label input + "On" + remove
     on one nowrap line (recipient-profiles.js:57), and the Telegram chat-id /
     bot-token pair is an inline two-column grid (:73). */
  .rpe-row > div:first-child {
    flex-wrap: wrap;
  }

  .rpe-fields > div {
    grid-template-columns: 1fr !important;
  }

  /* The Control Panel CRUD dialogs live in index.ejs and are classless —
     id + inline styles only — so section 09's modal rule cannot reach them.
     Their shells already carry max-width:95/96vw + overflow-y:auto; it is the
     inline form grids that leave ~38px for the Book Name field.
     The auto-fit/auto-fill exclusions match the sibling Control Panel rule:
     those grids already collapse on their own, and force-flattening them would
     needlessly stack #book-servers-grid's checkboxes one per row. */
  #server-modal div[style*="grid-template-columns"]:not([style*="auto-fit"]):not([style*="auto-fill"]),
  #recipient-profile-modal div[style*="grid-template-columns"]:not([style*="auto-fit"]):not([style*="auto-fill"]),
  #book-modal div[style*="grid-template-columns"]:not([style*="auto-fit"]):not([style*="auto-fill"]) {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  /* Global column chooser: the gear is a real <button> pinned to 20x20px
     (column-chooser.css:7) and is the only route to column prefs; the popover
     rows are ~24px tall around a native checkbox (column-chooser.css:47). */
  .cc-gear {
    width: 32px;
    height: 32px;
    font-size: 14px;
  }

  .cc-item {
    min-height: 38px;
    padding: 8px;
  }

  /* Server Health: three uPlot charts share an inline `1fr 1fr 1fr` row, so
     each gets ~85px of drawing width against a 150px height. :has() names the
     row without relying on nth-child; the ResizeObserver in _uplot()
     (chart-utils.js:461) redraws them at the new width. */
  #tab-server-health > div:has(#sh-chart-ping) {
    grid-template-columns: 1fr !important;
  }

  /* Alerts: 12 chips + 2 dividers wrap to ~5 rows inside a fixed-height
     column, and every pixel of header growth is stolen from the feed. Give
     the chips their own full-width line and let them scroll in place. */
  #alerts-chips {
    flex: 1 0 100%;
    min-width: 0;
    flex-wrap: nowrap !important;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    padding-bottom: 4px;
  }

  #alerts-chips > * {
    flex: 0 0 auto;
  }

  /* ...and the Rule Triggers panel below it reserves a flat inline 190px. */
  #tab-alerts > div > .card:last-child {
    max-height: 150px !important;
  }

  /* Event Risk card is inline `height: calc(100vh - 100px)`. app.css:3536
     already stacks .event-risk-layout below 1200px, but the stack then lives
     in a nested scroller measured against the large viewport. */
  #tab-event-risk > .card {
    height: auto !important;
  }

  /* With the card unpinned the calendar needs its own cap, or the detail
     panel lands hundreds of rows below the fold. */
  #tab-event-risk .event-risk-layout > .stream-tbl-wrap {
    max-height: 60vh;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   16  EAGLE VIEW & MY DASHBOARD
   ───────────────────────────────────────────────────────────────────────────
   Eagle View is the default landing tab; my-dashboard is the only tab whose
   layout comes from a vendor grid engine (gridstack 11.5.1) rather than CSS.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* Eagle View — .evx-delta is nowrap and carries strings like
     "↑ 12.3% vs yesterday" (~104px). The KPI row is still 3-up down to 760px
     (eagle-view.css:9-10), which leaves ~108px of value track, so the chip
     overruns the card and .main-content's overflow-x clips it. Wrapping only
     kicks in where it has to — the wider .evx-stats cells stay on one line. */
  .evx-delta {
    white-space: normal;
    line-height: 1.35;
  }

  /* Eagle View — the Live Feed list is capped at 620px below 1100px, a figure
     picked for a desktop side column. On a phone that is one card owning ~79%
     of the viewport as a nested scroller, so any swipe over it never reaches
     the two cards below. Half the viewport instead, in dvh so that a landscape
     phone (768 wide, ~390 tall) is not capped taller than the screen. */
  .evx-feed-card .evx-feed-list {
    max-height: 50vh;
    max-height: 50dvh;
  }

  /* my-dashboard — GridStack keeps its 12-column layout at every width unless
     told otherwise, so a default 4-wide widget renders at 33% of a 390px
     screen. The real fix is in JS (my-dashboard.js sets `columnOpts` to
     collapse to one column below 768px, then `setStatic(true)`), which gives
     each widget its own correct height.

     What follows is the FALLBACK for when that JS did not run — an older
     gridstack build, a load failure, an init that predates columnOpts. It
     flattens the absolutely-positioned grid into ordinary block flow at a
     uniform height, which is cruder than the JS path but readable.

     It is gated on :not(.grid-stack-static): setStatic(true) adds that class
     (gridstack.js `_setStaticClass`), so whenever the JS path IS working this
     whole block stands down and gridstack's per-widget heights survive.

     `!important` is confined to the two heights gridstack writes itself — the
     inline height on .grid-stack, and the <style> it injects into
     .mydash-canvas at runtime (i.e. after mobile.css in document order).
     Everything else is won on specificity or load order. */
  .mydash-canvas .grid-stack:not(.grid-stack-static) {
    height: auto !important;
  }

  /* position:static is what neutralises the percentage left/top, so those two
     never need overriding on their own. */
  .mydash-canvas .grid-stack:not(.grid-stack-static) > .grid-stack-item {
    position: static;
    width: auto;
    height: auto !important;
    margin-bottom: 10px;
  }

  /* The content wrapper is absolutely inset by the same injected sheet; once
     static it needs a definite height, because every widget body is built with
     an inline `height: calc(100% - 28px)`. */
  .mydash-canvas .grid-stack:not(.grid-stack-static) > .grid-stack-item > .grid-stack-item-content {
    position: static;
    height: 340px;
  }

  /* The SE resize grip is position:absolute — with its item static it would
     float against .grid-stack instead of its own card. */
  .mydash-canvas .grid-stack-item > .ui-resizable-handle {
    display: none;
  }

  /* my-dashboard widget picker — an entirely inline-styled dialog: a
     min(820px,95vw) panel split into a fixed 230px list plus preview, which
     leaves ~84px of readable description at 390px. Every dimension touched
     below is an inline style attribute, so !important is the only way in. */
  #my-dashboard-picker > div {
    width: 100% !important;
    height: 100% !important;
    border-radius: 0 !important;
  }

  /* The two panes carry no class or id of their own — only their children do,
     hence :has(). Positional fallback: `#my-dashboard-picker > div >
     div:nth-child(2)` is the body row, `> div:first-child` its list column. */
  #my-dashboard-picker div:has(> #my-dashboard-picker-preview) {
    flex-direction: column;
  }

  #my-dashboard-picker div:has(> #my-dashboard-picker-list) {
    width: auto !important;
    max-height: 40vh;
    border-right: 0 !important;
    border-bottom: 1px solid var(--border);
  }

  #my-dashboard-picker-preview {
    padding: 16px !important;
  }

  /* my-dashboard — the widget remove button is an unclassed <button> in a 28px
     drag handle at ~18x18px, and it is the only way to remove a widget. The
     28px is a hard contract (widget bodies are `calc(100% - 28px)`), so widen
     rather than heighten; the negative margin reclaims the handle's padding. */
  .gs-drag-handle > button {
    min-width: 34px;
    height: 28px;
    margin-right: -6px;
  }
}


@media (max-width: 480px) {

  /* Eagle View — the KPI card pins a 96px sparkline in an `auto` second track.
     Two-up at 390px each card is ~181px, ~153px inside its padding, so the
     value/label track is left with ~47px for a 22px mono figure. Hiding the
     spark collapses the track; the trend is still carried numerically by the
     .evx-delta chip directly below. */
  .evx-kpi-spark {
    display: none;
  }

  /* Eagle View — Live Feed rows are `auto minmax(0,1fr) auto` with a nowrap
     badge, leaving ~209px that ellipsises BOTH the title and the message. The
     only recovery is a hover-only title="" attribute. Drop the third track and
     put the badge on row 2 of the text column: ~308px, and the message reads
     in full. Title stays ellipsised — it is the short label, not the detail. */
  .evx-feed-row {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 4px 10px;
  }

  /* Explicit column so auto-placement cannot drop it under the icon;
     justify-self stops the coloured pill stretching the full width. */
  .evx-feed-badge {
    grid-column: 2;
    justify-self: start;
  }

  /* overflow stays hidden so an unbreakable token cannot escape the row into
     .main-content's clip; text-overflow is inert once the text wraps. */
  .evx-feed-msg {
    white-space: normal;
    overflow-wrap: break-word;
    overflow-wrap: anywhere;
  }

  /* Eagle View — the Top-Patterns chips are 2-up at every width, leaving a
     ~111px label track for names like "Weekend Margin-Drain Gap Play". These
     chips are the only place the active pattern names appear on this tab and
     there is no scroll container to recover them. Capped at 6 by the renderer,
     so stacking costs ~90px of height. */
  .evx-chips {
    grid-template-columns: minmax(0, 1fr);
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   17  ACCOUNT INTELLIGENCE, ACCOUNTS & SEARCH
   ───────────────────────────────────────────────────────────────────────────
   Table ellipsis / horizontal scroll on these surfaces is already handled by
   section 04 + mobile-shell's .mobi-tscroll tagging (.ai-acct-wrap,
   .stream-tbl-wrap and .tbl-wrap are all the table's direct parent), so
   nothing here re-declares table geometry.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  /* AI dossier: these four grids already collapse to one column at 1100/1200px,
     but `1fr` is `minmax(auto, 1fr)` — the track still cannot shrink below its
     widest nowrap money value, so one cell silently widens the page. Only the
     min floor changes here. */
  .ai-grid-2,
  .ai-grid-31,
  .ai-grid-32,
  .ai-routing-top {
    grid-template-columns: minmax(0, 1fr);
  }

  /* All Accounts listing: `calc(100vh - 320px)` leaves a ~70px scroll window in
     phone landscape. A proportional cap survives both orientations. */
  .ai-acct-wrap {
    max-height: 65vh;
    max-height: 65dvh;
  }
}


@media (max-width: 768px) {

  /* .section-header is handled once, globally, in section 07 — three per-surface
     sections had each declared the bare class at this breakpoint, and only the
     last survived. */

  /* .search-bar has no wrap and its children carry inline minimums (select
     160px, #dpw-ticket 180px) — a ~450px floor. A full-width flex basis is what
     actually forces the break; the inline min-widths then fit on their own line. */
  .search-bar {
    flex-wrap: wrap;
  }

  .search-bar select,
  .search-bar input {
    flex: 1 1 100%;
  }

  .search-bar .btn-search {
    flex: 1 1 auto;
  }

  /* Search dossier panels are cloned from #sr-panel-tpl with every id rewritten
     (account-search.js:100), so the inline style attribute is the only stable
     hook — and being inline it is also what makes !important unavoidable.
     Tag add-form: minmax(150px,1fr) minmax(110px,1fr) 74px 90px = 448px floor. */
  #tab-search div[style*="minmax(150px,1fr)"] {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }

  /* Case workflow row: 130px 150px minmax(180px,1fr) = 480px floor. */
  #tab-search div[style*="130px 150px"] {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  /* Account Intelligence: 40px of wrapper padding on top of .tab-content's own. */
  #tab-account-intelligence .ai-wrap {
    padding: 12px 10px;
  }

  /* Financials / Execution tiles: four tracks of un-breakable money strings, so
     the min-content floor of `1fr` pushed the grid out of its card. */
  .ai-exec-summary {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* Routing card: a fixed 150px donut and a 26px nowrap volume figure share one
     nowrap flex row that cannot fit a phone. */
  .ai-route-left {
    flex-wrap: wrap;
  }

  .ai-route-volblock {
    flex: 1 1 100%;
    min-width: 0;
  }

  .ai-val-xl {
    font-size: 20px;
  }

  /* All Accounts filters: each multi-select panel is `left: 0` on an inline-block
     chip inside a wrapping bar, so any chip not at x=0 opens its 230px panel off
     the right edge. Full-width chips make left/right anchoring safe. */
  .ai-ms {
    display: block;
    width: 100%;
  }

  .ai-ms-btn {
    width: 100%;
    max-width: none;
    justify-content: space-between;
  }

  .ai-ms-panel {
    right: 0;
    min-width: 0;
    max-width: none;
  }
}


@media (max-width: 480px) {

  /* Universal search dropdown: `90px 1fr auto auto` leaves the account name a few
     characters wide. Two columns reflow it to login+name / group+match. */
  .ai-sr-row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 3px 8px;
    padding: 9px 10px;
  }

  .ai-sr-row > * {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Live-state KPI cards: a 21px value in a ~141px card ellipsises 7-figure
     balances mid-number. */
  .ai-kpi2 {
    padding: 10px 11px;
  }

  .ai-kpi2-val {
    font-size: 17px;
  }

  /* Per-book performance: 4 tracks of ~65px wrap the uppercase captions. */
  .ai-perf-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* Routing recommendation: two mode readouts squeezed beside a 62px gauge —
     move the gauge to its own centred row. */
  .ai-rec-modes {
    grid-template-columns: 1fr 1fr;
  }

  .ai-rec-conf {
    grid-column: 1 / -1;
    justify-self: center;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   18  INVESTIGATION CENTER & INCIDENT EVIDENCE
   ───────────────────────────────────────────────────────────────────────────
   investigation-center.css carries no media queries at all. incident-evidence.css
   has exactly one, at 820px, which already stacks .epi-2col / .epi-2col-conc /
   .la-cl-2col / .epi-cl-grid and takes .la-stats and .epi-stats to 2-up — only
   what that block misses is repeated here.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* The Investigation header's right-hand cluster (server select + book filter +
     3 action buttons, ~540px of min-content) is an inline `display:flex` with no
     wrap, so opening the tab pans the whole page. Only display/gap/align-items
     are inline, so flex-wrap needs no !important. */
  .ic-head > div:last-child {
    flex-wrap: wrap;
  }

  /* Section 09 already reaches these three modals via [class*="modal-box"] — the
     sheet width/height is right, but .ic-modal-box is pure chrome (header, body
     and footer each carry their own padding), so the 18px inset it injects wastes
     36px of width and detaches the header rule from both edges. */
  .ic-modal-box {
    padding: 0;
  }

  .ic-modal-body {
    padding: 12px;
  }

  /* Evidence titles run ~55 chars ("… — MT5-Live:1234567 · #4821") and wrap flush
     against the ✕, which is an 18px glyph with no padding and no min size. */
  .ic-modal-h {
    gap: 10px;
    align-items: flex-start;
    padding: 11px 12px;
  }

  .ic-modal-h button {
    flex: 0 0 auto;
    min-width: 38px;
    min-height: 38px;
  }

  /* Configure / Actions: a 300px fixed rail beside the pane leaves the pane ~60px
     on a phone. Stack it — both halves already scroll themselves, and the pane's
     own overflow-y:auto gives it a zero automatic minimum size in a column flex. */
  .ic-cfg-body {
    flex-direction: column;
  }

  .ic-cfg-rail {
    width: auto;
    max-height: 34vh;
    border-right: 0;
    border-bottom: 1px solid var(--border2, #2c3140);
  }

  .ic-cfg-pane {
    padding: 12px;
  }

  /* investigation-center.css:203 pins rule-builder rows to nowrap so a whole
     condition stays on one desktop line; in a phone-width pane the value input
     and the remove button run off the modal. */
  .ic-act-row {
    flex-wrap: wrap;
  }

  /* .section-header is flex/space-between with no gap and no wrap, so the
     Incidents title block and its 7-control filter bar fight for one row. */
  #ic-panel-incidents .section-header {
    flex-wrap: wrap;
    gap: 10px;
  }

  #ic-panel-incidents .ic-flag-controls {
    margin-top: 10px;
  }

  /* Once the 820px block stacks .epi-2col, the event timeline is a 300-520px
     scroll trap nested inside the modal's own scroll, with no visible boundary
     on touch. Let it flow so .ic-modal-body owns the gesture. */
  .epi-tl {
    max-height: none;
    overflow-y: visible;
    padding-right: 0;
  }
}

@media (max-width: 480px) {

  /* The 5 incident filters open 190-250px absolutely-positioned panels anchored
     left:0 to their button; a button mid-row throws its panel past the viewport,
     where .main-content's overflow-x:hidden clips it dead rather than scrolling.
     One filter per row makes left/right:0 span the card instead. */
  .ic-ims {
    flex: 1 1 100%;
  }

  .ic-ims .ic-ms-btn {
    width: 100%;
    min-height: 36px;
  }

  .ic-ims .ic-ms-panel {
    right: 0;
    min-width: 0;
  }

  /* The Type panel carries ~25 full detector names. */
  .ic-ims .ic-ms-opt {
    white-space: normal;
  }

  /* Fallback evidence view: a 160px monospace key column leaves ~170px for the
     value, which is word-break:break-word and shreds across 4-5 lines. */
  .ic-ev-row {
    flex-direction: column;
    gap: 2px;
  }

  /* Save status ("✓ Saved (3 thresholds, 2 switches, …)") shrinks to min-content
     and wraps to 5 lines wedged between Close and Save. Give it its own row. */
  .ic-cfg-foot {
    flex-wrap: wrap;
  }

  .ic-cfg-foot .ic-dim {
    flex: 1 1 100%;
  }

  /* 3 fixed badge columns leave ~88px per value; 15px/800 amounts plus a 14px
     inline svg spill the card and collide with the neighbour. */
  .epi-badges {
    grid-template-columns: repeat(2, 1fr);
  }

  /* The 820px block collapses .epi-cl-grid, but the column-fill variant is
     `display:block; columns:2` — grid-template-columns never reaches it. */
  .epi-cl-grid.epi-cl-cols {
    columns: 1;
  }

  /* Profit Distribution: a 128px name track + 42px pct track + a nowrap USD
     column leave ~50px for the bar the panel exists to show. Give the symbol
     name its own line and the bar the full row. */
  .epi-dist-row {
    grid-template-columns: 1fr auto auto;
    gap: 4px 8px;
  }

  .epi-dist-row .epi-dist-name {
    grid-column: 1 / -1;
  }

  .epi-dist-row .epi-dist-track {
    grid-column: 1;
  }

  /* Timeline rows spend 118px of chrome on the date rail before the event card
     starts, so the event name and its amount cannot share a line. */
  .epi-ev {
    grid-template-columns: 62px 18px 1fr;
    gap: 7px;
  }

  .epi-ev-date {
    text-align: left;
  }

  .epi-cap {
    padding-left: 0;
  }

  /* 4 flow nodes + 3 arrows on one nowrap row = ~62px per node, so captions like
     "MARGIN CALL TRIGGERED" wrap 5 characters wide. Two per row; the → arrows are
     dropped because they would point the wrong way once wrapped. */
  .epi-flow {
    flex-wrap: wrap;
    gap: 14px 6px;
  }

  .epi-fl-node {
    flex: 1 1 calc(50% - 6px);
  }

  .epi-fl-arrow {
    display: none;
  }

  /* The 820px block stops at 2-up; at 390px .la-stat-v is nowrap with no ellipsis
     and overflows its tile. */
  .la-stats {
    grid-template-columns: 1fr;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   19  PREDICTIVE DETECTION  (views/tabs/predictive-detection.ejs, tab 47)
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* Header action bar: nowrap flex row holding #pdt-server plus three .pdt-btn.
     Without wrap each button shrinks to its longest word ("Run / scan"). */
  .pdt-head-actions {
    flex-wrap: wrap;
  }

  .pdt-btn {
    white-space: nowrap;
  }

  /* Filter bar hangs off the right of the tab strip on a 200px search box and
     wraps into right-ragged lines. Give it its own full-width, left-aligned row. */
  .pdt-tabbar-filters {
    width: 100%;
    margin-bottom: 0;
  }

  .pdt-filters {
    width: 100%;
    justify-content: flex-start;
  }

  /* flex-basis replaces width in the main axis, so this beats the inline
     width:200px buildFilters() writes without needing !important. */
  .pdt-filters input {
    flex: 1 1 100%;
  }

  /* Incidents table: the Actions column is position:sticky right:0 at a fixed
     250px, so on a ~374px viewport two thirds of the scroll window is
     permanently occluded and the other 13 columns slide underneath it. */
  #pdt-incidents-table th:last-child,
  #pdt-incidents-table td:last-child {
    position: static;
    white-space: normal;
  }

  /* Evidence / Confirm / Reject wrap inside 160px instead of demanding 250px. */
  #pdt-incidents-table th:nth-child(14) {
    width: 160px;
  }

  /* Pre-News-Unhedge timeline is a 1000x320 viewBox with
     preserveAspectRatio="none": at ~310px it scales X to 0.31 and the 5-decimal
     price labels and 30-minute HH:MM ticks smear into each other. Floor the
     render width and scroll instead. Scoped to the predictive evidence host so
     the shared pnu-* Investigation-Center charts are untouched; .ie-graph is
     excluded because it is the one pnu-svg that scales proportionally. */
  #pdt-evidence-host .pnu-chart-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }

  #pdt-evidence-host .pnu-svg:not(.ie-graph) {
    min-width: 640px;
  }

  /* Configure modal is master/detail: a 300px flex-shrink:0 rail inside a
     min(960px, 94vw) box leaves the detail pane ~66px, so every Engine setting,
     recipient, action and the Custom Rules mount renders in a ~26px strip.
     Stack rail over pane; both keep their own overflow-y. */
  .pdt-cfg-body {
    flex-direction: column;
  }

  .pdt-cfg-rail {
    width: auto;
    max-height: 34vh;
    border-right: 0;
    border-bottom: 1px solid var(--border2, var(--border));
  }

  /* Rule editor: .pdt-field cannot wrap and every control carries an inline
     width (260 / 140 / 110 / 130px) written by predictive-rules.js. */
  .pdt-field {
    flex-wrap: wrap;
  }

  .pdt-field .pdt-input {
    flex: 1 1 100%;
  }

  /* justify-content here is an inline style attribute, so !important is the only
     way to left-align the signal checkboxes under their label. */
  #pdt-rule-signals {
    flex: 1 1 100%;
    justify-content: flex-start !important;
  }

  /* Nested AND/OR/NOT builder: the field (210px) and operator (120px) selects
     are inline-styled and so cannot shrink; three .pdt-grp levels of indent push
     them past the modal edge. !important is unavoidable — element.style widths. */
  .pdt-cond-field,
  .pdt-cond-op {
    width: 100% !important;
  }

  .pdt-cond-val {
    flex: 1 1 auto;
    min-width: 0;
  }

  /* flex-basis:0 also neutralises the inline 70/80/120/180px value widths, and
     keeps the two-input range operator side by side rather than stacked. */
  .pdt-cond-val .pdt-input {
    flex: 1 1 0;
    min-width: 0;
  }

  /* Trim the per-nesting-level indent from 23px to 17px. */
  .pdt-grp {
    padding: 8px 7px;
  }

  /* A NOT wrapper cannot wrap and its child keeps min-width:auto, so the
     stacking above never takes effect inside a negated condition. */
  .pdt-not {
    flex-wrap: wrap;
  }

  .pdt-not > .pdt-cond,
  .pdt-not > .pdt-grp {
    flex: 1 1 auto;
    min-width: 0;
  }

  /* Custom-rule list: names are user-supplied and neither flex child has
     min-width:0, so a long name pushes Edit and ✕ off the pane. */
  .pdt-rule-name,
  .pdt-rule-meta {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Per-pattern enable toggle is 30x17 and sits inside a row that is itself
     clickable, so a near-miss selects the pattern instead of toggling the
     detector. Extend the hit area with a transparent pseudo-element: padding
     would stretch .pdt-switch-track (inset: 0), and a wider switch would break
     its alignment with the 30px RAIL_SPACER used on the switch-less rows. */
  .pdt-switch::before {
    content: '';
    position: absolute;
    inset: -12px -7px;
  }
}

@media (max-width: 480px) {

  /* Sub-tab strip cannot wrap: two tabs plus the confirmed badge exceed a 374px
     row, so both tab labels compress to min-content and break mid-phrase. */
  .pdt-tabbar-tabs {
    flex-wrap: wrap;
  }

  .pdt-tab {
    white-space: nowrap;
    padding: 9px 10px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   20  CONNECTION & NETWORK INTELLIGENCE
   ───────────────────────────────────────────────────────────────────────────
   Connection Intelligence (.ci-*), IB Network Intelligence (.ni-*) and the
   Profit Drain Radar (.pd-*). Three toolbars, two fixed-track flex rows, one
   oversized chart box and one touch trap.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  /* Connection Intelligence > syndicate detail > Relationship Graph. The
     layered SVG is 640px tall (inline, connection-intelligence.js:867) and
     declares `touch-action: none`, so every touch-drag inside it is swallowed
     and a user who lands mid-graph cannot scroll past it. `pan-y` hands
     vertical panning back to the browser; the pointermove handler still
     receives horizontal movement, so drag-to-pan the graph keeps working. */
  .ci-layer-svg {
    touch-action: pan-y;
  }
}


@media (max-width: 768px) {

  /* All three page toolbars are nowrap flex rows. Their <select>s never shrink
     below their widest option and the shared BookFilter trigger carries an
     inline `min-width: 112px`, so ~460px of controls sit on a ~374px line and
     the trailing refresh button is clipped by .main-content's overflow guard. */
  .ci-topbar-right,
  .ni-topbar-right,
  .pd-topbar-right {
    flex-wrap: wrap;
  }

  /* Connection Intelligence page gutter — 44px of a 390px viewport, on top of
     .tab-content's own padding, for no benefit. */
  .ci-wrap {
    padding: 14px 10px 28px;
  }

  /* Profit Drain quadrant radar: 520px tall in a ~340px column is taller than
     the viewport and nothing like the square a four-quadrant scatter needs.
     min-height must be reset too — the 380px floor would clamp the height
     declaration straight back into a no-op. */
  .pd-chart-host {
    height: 340px;
    min-height: 0;
  }
}


@media (max-width: 480px) {

  /* Network Intelligence P&L / Booking & Exposure waterfalls: a `flex: 0 0 170px`
     label plus a `flex: 0 0 210px` value give the row a 400px floor before the
     bar gets a pixel. Label onto its own line, bar + value beneath it. */
  .ni-wf-row {
    flex-wrap: wrap;
  }

  .ni-wf-lbl {
    flex: 1 1 100%;
  }

  /* Real basis, not the source's `flex: 1` (= basis 0): with a zero basis the
     track absorbs none of the shrink and collapses to an invisible bar. */
  .ni-wf-track {
    flex: 1 1 90px;
  }

  /* Shrinkable, NOT `0 0 auto` — this cell also carries the
     "fl -$340K · offsets desk (…)" tail, whose max-content would re-overflow. */
  .ni-wf-val {
    flex: 0 1 auto;
  }

  /* Syndicate relationship cards. The 1000px auto-fit rule still lands on two
     ~152px columns on 412-430px phones, which ellipsises the IPs and device
     fingerprints the panel exists to show (.ci-rel-card .ci-item .k). Same
     specificity as connection-intelligence.css:226 — mobile.css loads last. */
  .ci-rel-cards {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Profit Drain range filters: an inline-flex group whose nowrap label makes
     the widest group ~300px min-content on a ~296px line. Letting the group
     wrap drops the label onto its own row and removes the floor. */
  .pd-range-group {
    flex-wrap: wrap;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   21  RISK & EXPOSURE SURFACES
   ───────────────────────────────────────────────────────────────────────────
   Exposure / Exposures 2 / Exposure Monitor / Margin Call / Firm VaR /
   Margin Cascade / Simulator / Global Symbol Exposure Matrix.

   Everything here is a header or grid that has no media query of its own.
   The shared table, card, form and grid-helper behaviour comes from
   sections 03-08; nothing below repeats it.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* Section 03 already lets .panel-header wrap, but the title is still a flex
     item competing with the control cluster on the same line — on Firm VaR the
     "Parametric (Variance–Covariance)" select alone reserves ~250px once
     section 05 bumps it to 16px, leaving the title one word per line.
     Scoped to the tab that motivates it: .panel-header is a global component
     used across ~20 tabs, and the bare selector would force the title onto its
     own row everywhere. */
  #tab-firm-var .panel-header > .panel-title {
    flex: 1 1 100%;
  }

  /* .section-header is handled once, globally, in section 07. */

  /* Exposures 2: the right-hand group is an inline `display:flex` with no wrap
     and ~414px of content, so it still runs off the right edge after
     .section-header itself has wrapped. */
  #tab-exposures .section-header > div {
    flex-wrap: wrap;
  }

  /* Simulator toolbar: inline `display:flex` holding 140px + 112px + 120px
     fixed-width controls plus a Run button — ~540px on one line, so the button
     lands off-screen. The inline style sets display/gap/align-items but not
     flex-wrap, so no !important is needed. */
  #tab-simulator .panel-header > div {
    flex-wrap: wrap;
  }

  /* Book filter: book-filter.js writes an inline 230px panel anchored
     `right: 0` to a 112px button, so its left edge sits ~118px off-screen and
     .main-content's overflow guard clips it away. Re-anchor to the viewport.
     !important is required — position/top/right/width are an inline style
     attribute. `display` is deliberately NOT touched: openPanel/closePanel
     toggle it inline, and overriding it would pin the panel open or shut. */
  .bf-panel {
    position: fixed !important;
    left: 12px !important;
    right: 12px !important;
    top: auto !important;
    bottom: 12px !important;
    width: auto !important;
  }

  /* Exposure Monitor — the heatmap title is `white-space: nowrap` and carries
     the live mode label, so its min-content is ~420px inside a ~340px card and
     it drags the whole page into horizontal scroll. */
  .em-heat-title {
    white-space: normal;
  }

  /* Card headers pack a label next to an inline control cluster with no wrap.
     The full-width rule is scoped to .em-row-between because on the other
     headers the first ELEMENT child is the trailing .em-dim / .em-badge
     suffix, not the label. */
  .em-card-h {
    flex-wrap: wrap;
  }

  .em-card-h.em-row-between > span:first-child {
    flex: 1 1 100%;
  }

  /* Two-track grids with no media query anywhere: the radar squeezes a 30px
     hero number into ~146px, and Exposure Drivers squeezes a 4-column nowrap
     contributors table into ~185px. Stacking is what makes the generic
     .mobi-tscroll wrapper useful here. minmax(0,1fr) so the nowrap table and
     the donut canvas cannot re-inflate the track. */
  .em-radar-main,
  .em-drivers {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Symbols pager: "Showing 1 to 10 of 245 entries" plus a prev/next/page
     strip of up to 10 buttons on one non-wrapping space-between row. Both the
     outer row and the inner strip can overflow on their own. */
  .em-pager {
    flex-wrap: wrap;
    gap: 8px;
  }

  .em-pg-btns {
    flex-wrap: wrap;
  }

  /* Margin cascade: 4 fixed tracks of ~71px each holding a `nowrap`
     "-5.0% / 12 stop / 3 call" label ~106px wide. Bar heights are written as
     inline absolute px, so halving the columns keeps the steps comparable
     across the two rows. */
  .cascade-path-bars {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cascade-path-label {
    white-space: normal;
  }
}

@media (max-width: 480px) {

  /* 18px side gutters on top of .tab-content's and .em-card's own padding cost
     36px of a 390px viewport before anything is drawn. The source selector is
     ID-qualified, so the override repeats the ID to match it. */
  #tab-exposure-monitor .em-wrap {
    padding: 10px 8px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   22  MARKET & CHART SURFACES
   ───────────────────────────────────────────────────────────────────────────
   Streaming, Liquidity Pulse, Arbitrage, LP Execution Quality, OFI Radar,
   Market Watch. Almost every layout here is an inline `style` attribute
   written into the EJS (or by JS on every tick), so `!important` appears more
   often than elsewhere in this file — each one is against an inline value and
   is noted as such.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  /* Streaming hero row — streaming-logs.js re-writes BOTH the template
     (`repeat(N, minmax(180px,1fr)) 220px`) and a per-card grid-column/grid-row
     on every stream tick, inline. Five currencies already demand ~1100px.
     Collapsing only the template would leave cards pinned at `grid-column: 5`,
     which creates implicit columns and the same overflow. */
  #stream-currencies-combined {
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)) !important;
  }

  #stream-currencies-combined > * {
    grid-column: auto !important;
    grid-row: auto !important;
  }
}


@media (max-width: 768px) {

  /* Wrapping itself is global (section 07). These two headers additionally need
     top alignment because their right-hand cluster is much taller than the
     title once it wraps — Streaming pairs the title with a 150px server select,
     Market Watch with a five-button period strip (~210px). Scoped to the two
     tabs: the bare class would re-align every section header in the app, which
     is what the unscoped version of this rule was silently doing. */
  #tab-streaming .section-header,
  #tab-market-watch .section-header {
    align-items: flex-start;
  }

  /* ── Liquidity Pulse ─────────────────────────────────────────────────── */

  /* Inline padding:20px on the tab wrapper, on top of .tab-content's own. */
  #tab-liquidity-pulse > div {
    padding: 10px !important;
  }

  /* Header pins the whole toolbar beside the H2 — let it drop to its own row. */
  #tab-liquidity-pulse > div > div:first-child {
    flex-wrap: wrap;
    gap: 10px;
  }

  /* 7-mode strip is ~650px of nowrap buttons. The inline `overflow:hidden`
     stays: once the strip wraps it clips nothing, and it is what gives the
     segmented pill its rounded ends. */
  #heatmap-mode-btns {
    flex-wrap: wrap;
  }

  #heatmap-mode-btns button {
    flex: 1 1 auto;
  }

  /* The Horizon legend is two 4-swatch bands plus captions (~400px). */
  #heatmap-legend {
    flex-wrap: wrap;
  }

  /* The symbol picker gates every canvas mode. Its panel is absolutely
     positioned `right: 0` inside this wrap; giving the wrap its own full-width
     row in the (already wrapping) toolbar keeps that 240px panel on-screen
     wherever the toolbar happens to break. */
  #hmap-sym-wrap {
    width: 100%;
  }

  /* Data Table view: inline padding:20px, plus a count/filter/export row that
     runs to ~420px without wrapping. */
  #heatmap-table-wrapper {
    padding: 10px !important;
  }

  #heatmap-table-wrapper > div:first-child {
    flex-wrap: wrap;
  }

  /* ── Arbitrage & Toxic Execution ─────────────────────────────────────── */

  /* Inline padding + gap on the tab wrapper. */
  #tab-arbitrage > div {
    padding: 10px !important;
    gap: 10px !important;
  }

  /* KPI strip is an inline `repeat(5, minmax(0,1fr))` — 62px per column. */
  #arb-status + div {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }

  /* Toxicity Radar row is `display:flex; height:560px` with 240px + 290px of
     flex-shrink:0 side columns — 530px consumed before the chart gets any
     width. The row div has no id/class, so :has() is the only handle. */
  #arb-tab-radar div:has(> #arb-radar-legend) {
    flex-direction: column;
    height: auto !important;
  }

  /* Gated on the same :has() as the row above. Dropping the side columns'
     widths only makes sense once the row has actually become a column — if the
     engine ignores :has() and the row stays horizontal, these would strip the
     widths from a still-horizontal layout and collapse both panels. */
  #arb-tab-radar div:has(> #arb-radar-legend) > #arb-radar-legend,
  #arb-tab-radar div:has(> #arb-radar-legend) > #arb-radar-detail {
    width: auto !important;
    max-height: 240px;
    border-left: 0 !important;
    border-right: 0 !important;
    border-top: 1px solid var(--border);
  }

  /* Stacked, the canvas column keeps its inline `flex: 1` — and in a column
     container flex-basis:0 beats height, so the Chart.js instance (responsive,
     maintainAspectRatio:false) would render at zero. Override the basis. */
  #arb-tab-radar div:has(> #arbitrageChart) {
    flex: 0 0 340px !important;
  }

  /* ── LP Execution Quality ────────────────────────────────────────────── */

  /* Inline padding:20px on the tab wrapper. */
  #tab-lp-slippage > div {
    padding: 10px !important;
  }

  /* The header row and its margin-left:auto control cluster (Server / mode
     switch / Show / Generate ≈ 550px) are both nowrap, so Generate ends up
     off-screen. */
  #tab-lp-slippage > div > div:first-child {
    flex-wrap: wrap;
  }

  #tab-lp-slippage div:has(> #lp-slippage-server) {
    flex-wrap: wrap;
    margin-left: 0 !important;
    width: 100%;
  }

  /* Inline `repeat(4, 1fr)` — invisible to the central .grid-N collapse. */
  #lp-stats {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }

  /* Scorecard KPI tiles: same inline 4-column template. */
  #tab-lp-slippage div:has(> div > #exec-score-overall) {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }

  /* Scorecard split grid: the table column has an inline minmax(460px,…)
     floor, so the card can never be narrower than 474px. */
  #tab-lp-slippage div:has(> #exec-score-chart) {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  /* ── OFI Radar ───────────────────────────────────────────────────────── */

  /* Inline width:220px beside an info dot and a status badge still overruns
     the panel header even after it wraps; auto sizes it to ~165px. */
  #ofi-source-select {
    width: auto !important;
  }

  /* Inline height:250px — the four summary titles wrap to 2-3 lines at phone
     width and the content spills over the table below. */
  #ofi-summary-panel {
    height: auto !important;
  }
}


@media (max-width: 480px) {

  /* One card per row keeps each currency's native / USD pair readable and the
     24px mono values unbroken. */
  #stream-currencies-combined {
    grid-template-columns: minmax(0, 1fr) !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   23  REPORTS & ANALYTICS
   ───────────────────────────────────────────────────────────────────────────
   Report Wizard, Reporting Hub, Trading Sessions, Smart Routing,
   Financial Analytics, Summary, Macro Impact.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 768px) {

  /* Report Wizard — .rw-shell is a flex ROW whose sidebar is a fixed 320px
     (250px below 900px), leaving the chat column ~140px: the composer and its
     Send button cannot both fit. Stack, and give the sidebar a DEFINITE slice —
     .rw-sec-threads/.rw-sec-saved are `flex: 1.5`/`flex: 1` (basis 0), so an
     auto-height sidebar would collapse both lists to nothing. */
  .rw-shell {
    flex-direction: column;
  }

  .rw-sidebar {
    width: auto;
    height: 38vh;
    height: 38dvh;
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }

  /* Trading Sessions — the session cards and the per-dimension mini charts are
     both `repeat(4, minmax(0,1fr))` written into the element's inline style
     attribute, which no stylesheet rule can beat without !important. Four
     ~80px columns reduce every nowrap value and every chart axis to fragments. */
  #tsx-cards {
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) !important;
  }

  #tsx-bd-grid {
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)) !important;
  }

  /* Reporting Hub — the builder toolbar is a nowrap flex row (Back + name input
     + four action buttons, >600px of content). .main-content clips rather than
     pans, so Save / Export CSV / Schedule end up off-screen and unreachable.
     flex-basis longhand overrides the `flex: 1` shorthand's basis only. */
  #tab-reporting-hub .rh-toolbar {
    flex-wrap: wrap;
  }

  #tab-reporting-hub .rh-name {
    flex-basis: 100%;
  }

  #tab-reporting-hub .rh-toolbar-actions {
    margin-left: 0;
    width: 100%;
    flex-wrap: wrap;
  }

  /* Smart Routing — the KPI strip's lowest breakpoint is 1400px (3 columns), so
     nothing collapses it on a phone: ~73px of content per card clips the 22px
     monospace values and wraps the labels to five lines. */
  #tab-smart-routing .sr-kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  #tab-smart-routing .sr-kpi {
    padding: 10px 12px;
  }

  .sr-kpi-val {
    font-size: 18px;
  }

  /* Smart Routing simulator — two unclassed divs carry inline grids with hard
     280px / 360px second columns, wider than the card that holds them. The
     inline style attribute is both the only handle and the reason for
     !important; the sibling minmax(420px) grid is already handled in app.css. */
  #tab-smart-routing [style*="minmax(280px"],
  #tab-smart-routing [style*="minmax(360px"] {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  /* Financial Analytics — "Margin Bands & Equity Concentration" splits into two
     panes that never collapse (~132px / ~184px), each its own tiny table
     scroller. minmax(0,1fr) rather than 1fr, or the tables' min-content width
     re-inflates the track. The pane divider is an inline border-right. */
  .fa-split2 {
    grid-template-columns: minmax(0, 1fr);
  }

  .fa-split2 > .table-responsive:first-child {
    border-right: 0 !important;
    border-bottom: 1px solid var(--border);
  }

  /* Financial Analytics Configure modal — each matcher row is a nowrap flex row
     of two fixed 130px selects + a value input + ✕, ~478px against a ~310px
     modal body, so the value and the delete button fall outside. Field and
     operator share line one; value and ✕ take line two. min-width:0 is what
     actually defeats a select/input's intrinsic minimum. */
  .fa-mrow {
    flex-wrap: wrap;
  }

  .fa-m-field,
  .fa-m-op {
    flex: 1 1 calc(50% - 4px);
    min-width: 0;
  }

  .fa-m-val {
    flex: 1 1 100px;
    min-width: 0;
  }

  /* Summary — all three .section-header rows are nowrap space-between. The top
     one's right group alone (server select + 150px book-filter button +
     "Last update") needs ~420px, so the timestamp is clipped off-screen. */
  #tab-summary .section-header {
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 10px;
  }

  /* Macro Impact — the realized-impact tiles are a hard repeat(3, 1fr) while
     every sibling grid in the same file is auto-fit; ~84px per tile wraps
     "Realized B-Book P&L" to three lines. */
  .mi-actual {
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  }
}


@media (max-width: 480px) {

  /* Smart Routing — the book-distribution row pairs a fixed 150px donut with
     the legend, leaving ~134px for rows like "B-Book (Internalized) 1,234.5
     lots (56.7%)". At 768 the card is still wide enough; only phones need it. */
  .sr-dist-wrap {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }

  .sr-dist-legend {
    width: 100%;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   24  COMPLETENESS PASS — affordances & grids the foundation cannot reach
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {

  /* .period-btn (216 uses — Export CSV / Refresh / Configure / period toggles
     on ~40 tabs) and .alert-chip (the 12 Alerts filter chips) are 4px-padded
     11px text, ~21px of hit height, and neither is a .btn, so section 06 skips
     them. min-height is the load-bearing half: three scoped rules and ~10 call
     sites override the padding, none of them set a height. */
  .period-btn,
  .alert-chip {
    min-height: 38px;
    padding: 8px 14px;
    font-size: 12px;
    touch-action: manipulation;
  }

  /* 41 <input> elements in views/ omit `type`, so every `input[type="…"]`
     selector in section 05 skips them — the whole .ca-input CRUD-modal family,
     the .stream-filter Control-Panel / Market-Watch fields and .ai-tag-in.
     `input:not([type])` is (0,1,1), so it outranks those classes too. */
  input:not([type]) {
    min-height: 40px;
  }
}

/* No anti-zoom companion block here. This section originally re-listed
   input:not([type]), #ai-search / #agent-input / #rw-input and the five
   inline-styled selects, but section 05's guard now carries !important and
   matches `input:not([type])`, `select` and `textarea` directly — so every one
   of those is already covered, including the inline-styled ones. */


@media (max-width: 768px) {

  /* .info-tip (227 uses) is 15px and .info-dot (91 infoDot() call sites) is
     13px — ~3.5mm targets. Section 10 makes them tappable but not hittable,
     and most sit inside .drill-card, where a near-miss fires drillToTab() and
     leaves the page. An invisible inset overlay lifts both past 24px without
     reflowing a single card title or <th>; growing the box would. `::after` is
     already the 'i' glyph on .info-dot, but `::before` is free on both. */
  .info-tip::before {
    content: '';
    position: absolute;
    inset: -6px;
    border-radius: 50%;
  }

  /* .info-dot is inline-block with no positioned ancestor of its own. */
  .info-dot {
    position: relative;
  }

  .info-dot::before {
    content: '';
    position: absolute;
    inset: -7px;
    border-radius: 50%;
  }

  /* No #markout-stats rule here: app.css:3548 already forces it to one column
     with !important at every width <= 1200px, so the inline 4-col grid is long
     dead before a phone sees it. The override that used to sit here made the
     phone grid TWO columns — denser than the tablet it inherits from. */

  /* No #tab-smart-routing [style*="minmax(280px"] rule here — section 22 already
     collapses both the 280px and the 360px inline grids at this breakpoint. */

  /* AI Analyst launcher is a right-edge tab at mid-height. index.ejs sets
     viewport-fit=cover, so in landscape on a notched phone the layout viewport
     runs under the sensor housing and a tab at right:0 would sit beneath it. The
     panel opens leftward from the tab and has to fit beside it. */
  #agent-btn {
    right: env(safe-area-inset-right);
  }

  #agent-panel {
    right: calc(42px + env(safe-area-inset-right));
    max-width: calc(100vw - 54px - env(safe-area-inset-right));
    max-height: calc(100vh - 80px);
    max-height: calc(100dvh - 80px);
  }
}
