/*
 * mobile-native.css — the mobile layer.
 *
 * WHY THIS FILE EXISTS
 * The app was authored desktop-first (see CLAUDE.md's design baseline) and its
 * mobile behaviour is spread across ~19 stylesheets plus styles injected at
 * runtime by JS. Editing all of those in place would (a) collide with the other
 * agents currently working in them and (b) leave the mobile rules impossible to
 * review or revert as one unit. So every mobile correction lives HERE, loaded
 * LAST, and wins on cascade order rather than on !important.
 *
 * ⚠ LOAD ORDER IS THE MECHANISM — AND IT HAS TWO KNOWN LEAKS. This must stay
 * the final stylesheet in index.html, but "final <link>" is not "final rule":
 *   (1) index.html carries an inline <style> block (the dept-switcher critical
 *       CSS, ~line 264) that sits AFTER this <link>. It wins every tie.
 *   (2) src/departments/cultivation/tabs/overview/overview-fixed.js injects <style id="ovf-style"> into <head> at
 *       runtime, which also lands after this file.
 * Where a rule here has to beat one of those, that is stated at the rule and
 * the override is explicit. Everywhere else, order alone is enough.
 *
 * PRINCIPLES (from the brief: "feel natively built for mobile", "not
 * overcrowded or overstimulating", "PC can have the detail")
 *   1. Nothing is REMOVED on mobile. Detail is deferred, not deleted — the
 *      desktop layout is untouched above 768px.
 *   2. One column, one job per screen. Multi-column dashboards stack.
 *   3. 44px minimum touch target (Apple HIG); 48dp where Material applies.
 *   4. 16px minimum for any input — below that iOS auto-zooms the page on
 *      focus, which is the single most "not-native" thing a web app can do.
 *   5. Never a horizontal scrollbar on the page. Wide data (tables) becomes
 *      cards, or scrolls inside its OWN container with a visible affordance.
 *   6. Respect safe areas (notch, home indicator) via env().
 *
 * BREAKPOINTS — mobile rules are scoped to max-width:768px so the desktop
 * baseline at 1440px is provably unchanged. Tablet (769–1024) gets its own
 * lighter pass further down.
 */

/* ══════════════════════════════════════════════════════════════════════════
   1. FOUNDATION — type scale and box sizing
   ══════════════════════════════════════════════════════════════════════════
   The app ships a 13px root on every device. On a phone that pushes every
   derived em size under the 12px legibility floor (the audit found 318 such
   nodes on Overview alone) and makes inputs trigger iOS auto-zoom. 16px is the
   mobile base; the desktop scale is left alone. */
@media (max-width: 768px) {
  html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
  body { font-size: 16px; line-height: 1.5; }

  /* Nothing may introduce a sideways scroll on the document itself.
     ⚠ SIDE EFFECT, DOCUMENTED: overflow-x:hidden forces the other axis to
     compute to `auto`, so this is also what makes <body> the app's real
     vertical scroll container (base.css already sets html,body{height:100%}).
     Section 4 depends on that fact — measured, not assumed. */
  html, body { max-width: 100%; overflow-x: hidden; }

  /* THE LEGIBILITY FLOOR — 12px, read at arm's length under grow-room lights.
     The audit found 9px labels, 10px values and an 8px caption inside the
     donuts.

     Two traps this avoids:
     (a) A blanket `font-size: max(1em,12px) !important` would resurrect text
         that components deliberately hide with `font-size: 0` (an icon-only
         idiom this app uses, including the logo mark above). So the floor is
         never applied globally with !important.
     (b) `:where(...)` has ZERO specificity, so a first attempt at this lost to
         every component rule and changed nothing — measured, not assumed.

     These selectors are class-level (0,1,0), which TIES with the component
     rules that set the small sizes; because this file loads last, the tie goes
     here. Substring matching covers the app's naming conventions without
     needing an entry per component. */
  [class*="-label"], [class*="-eyebrow"], [class*="-meta"], [class*="-hint"],
  [class*="-sub"], [class*="-cap"], [class*="-tip"], [class*="-note"],
  [class*="-val"], [class*="-unit"], [class*="-foot"], [class*="-badge"],
  [class*="-chip"], [class*="-pct"], [class*="-n2"], [class*="-crumbs"],
  [class*="-empty"], small {
    font-size: 12px;
    line-height: 1.4;
  }

  /* (c) A THIRD TRAP, found by the 2026-08-24 sweep: the tie above only holds
     against class-level rules. A component that scopes its small type behind an
     ID — `#tab-clones .cln-kpi-l` is (1,1,0) — outranks the floor outright, and
     those labels stayed at 10.5-11px on the phone while the floor appeared to be
     working everywhere else. Matching the same weight is the only way to reach
     them; keep this list to what a sweep has actually measured rather than
     guessing at IDs. */
  #tab-clones .cln-kpi-l, #tab-clones .cln-kpi-sub, #tab-clones .cln-gf,
  #tab-clones .cln-kpi-v, #tab-clones .cln-mini-l,
  #tab-clones .cln-lg, #tab-clones .cln-lg span,
  #tab-clones .cln-mo-strain, #tab-clones .cln-mo-role,
  #tab-batches .ba-sync, .ba-sync {
    font-size: 12px;
    line-height: 1.4;
  }

  /* Explicit opt-out for anything that genuinely must stay tiny or hidden.
     `.torus-mark-badge` is the round Torus disc: the wordmark is rendered as
     TEXT inside the circle, and src/styles/2-generic/animations.css documents the micro size as
     deliberate ("at small sizes a simple round logo reads better than
     illegible micro-type"). It is a logo, not copy — the floor must not touch
     it, and an earlier attempt to blank it turned the mark into a black dot. */
  [class*="-label"][data-tiny], .visually-hidden, [aria-hidden="true"][class*="-cap"],
  .torus-mark-badge {
    font-size: inherit;
  }

  /* iOS zooms the whole page when a focused field is under 16px. This is the
     rule that most makes a web app feel like a web app. */
  input, select, textarea { font-size: max(1em, 16px); }
}

/* ══════════════════════════════════════════════════════════════════════════
   2. TOUCH TARGETS
   ══════════════════════════════════════════════════════════════════════════
   44x44 CSS px minimum. Where a control must stay visually small (an icon
   button in a dense toolbar), the HIT AREA is extended with a pseudo-element
   instead of inflating the visual box — the design stays tight, the thumb
   still lands. */
@media (pointer: coarse) {
  /* ⚠ !important IS REQUIRED HERE, and only here, for a reason worth stating.
     Cascade ORDER only breaks ties at equal specificity. Component rules set
     these on classes (0,1,0) — e.g. .ovf-tg{min-height:42px} — which outranks
     an element selector (0,0,1) no matter how late this file loads. Measured:
     every toolbar button rendered at exactly 42px, 2px under the HIG floor.
     This is an accessibility minimum that components must not be able to
     undercut, so it is enforced rather than merely proposed. Everything else
     in this file still wins on order alone. */
  button, a[href], [role="button"], [role="tab"], select,
  input[type="checkbox"], input[type="radio"], input[type="submit"] {
    min-height: 44px !important;
    min-width: 44px !important;
  }
  /* Inline links in prose are text, not targets — inflating them would wreck
     paragraph layout. Excluded deliberately. */
  p a[href], li a[href], td a[href], .prose a[href] {
    min-height: 0 !important;
    min-width: 0 !important;
  }

  /* Small icon buttons: keep the visual size, grow the hit box.
     NOTE this technique only works on non-replaced elements — <input> cannot
     render ::after, which is why checkboxes/radios above take the blunt
     min-size instead. */
  .ipm-iconbtn, .nav-icon-btn, .gx-icon-btn, .tk-icon-btn,
  .modal-close, .close-btn, [data-act="close"], .gx-step {
    position: relative;
  }
  .ipm-iconbtn::after, .nav-icon-btn::after, .gx-icon-btn::after,
  .tk-icon-btn::after, .modal-close::after, .close-btn::after,
  [data-act="close"]::after, .gx-step::after {
    content: '';
    position: absolute;
    top: 50%; left: 50%;
    width: max(100%, 48px); height: max(100%, 48px);
    transform: translate(-50%, -50%);
  }

  /* 8px minimum between adjacent targets so a thumb cannot hit two. */
  .nav-tab + .nav-tab,
  .ipm-topbar-actions > * + * { margin-left: max(8px, var(--gap, 0px)); }
}

/* ══════════════════════════════════════════════════════════════════════════
   3. GLOBAL CHROME — the header, on every screen
   ══════════════════════════════════════════════════════════════════════════
   The audit found the same defects on all 18 tabs because they all share this
   bar: a 26x26 logo with 5px text, a 114x25 department badge, and 9px dropdown
   labels. Fixing it here fixes every screen at once. */
@media (max-width: 768px) {
  /* Enlarged for touch, but the mark is NOT blanked. An earlier attempt set
     font-size:0 here to drop the 5px wordmark; the badge renders that wordmark
     as text, so it turned the logo into a solid black circle. Scale the type
     instead of removing it, and let the SVG fill the larger box. */
  .nav-logo-icon, .torus-mark-badge {
    width: 38px; height: 38px;
    font-size: 7px;
    display: grid; place-items: center;
  }
  .nav-logo-icon svg, .torus-mark-badge svg { width: 100%; height: 100%; }

  #nav-dept-badge, .nav-dept-badge {
    min-height: 40px;
    padding: 8px 12px;
    font-size: 13px;
  }
  .nav-dept-name { font-size: 13px; letter-spacing: 0.02em; }
}

/* ── 3b. DEPARTMENT SWITCHER → BOTTOM SHEET ────────────────────────────────
   MEASURED FIRST, then written. At 375px the previous attempt here produced a
   menu 70px WIDER than the screen (rect left=70, width=375 → right edge 445);
   body's overflow-x:hidden hid the damage instead of fixing it, so the last
   department in the list was simply unreachable. Two reasons it failed:

     1. src/app/boot/app.js PORTALS the dropdown to <body> on open and writes INLINE
        styles for position/top/left/right/zIndex/transform (see DD_PROPS in
        _wireDeptSwitcherDelegation). Inline styles beat every stylesheet, so
        `top:auto; left:0` from a .css file never applied — hence !important on
        exactly those properties and nothing else.
     2. index.html's inline <style> (~line 264) also declares
        `.nav-dept-dropdown{position:absolute;top:calc(100% + 8px);left:0}` and
        sits AFTER this file, so it won every tie at equal specificity.

   Because the element is portaled to <body> while open, `.nav-dept-wrap.open
   .nav-dept-dropdown` does not match the open state. The badge, which stays
   put, carries aria-expanded — so :has() on it is the reliable open signal. */
@media (max-width: 768px) {
  .nav-dept-dropdown {
    position: fixed !important;
    top: auto !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    width: 100% !important;
    min-width: 0 !important;
    max-width: 100% !important;
    /* The JS entrance animates transform (translateY(-8px) scale(.97)); on a
       full-bleed sheet that scale visibly narrows and insets it. Pin transform
       and animate `translate` instead — a separate property the JS never
       touches, so the two cannot fight. */
    transform: none !important;
    transform-origin: center bottom !important;

    max-height: min(70vh, 70dvh);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    border-radius: 20px 20px 0 0;
    padding: 4px 10px calc(12px + env(safe-area-inset-bottom, 0px));
    /* OPAQUE ON PURPOSE. src/styles/3-objects/departments.css gives this rgba(10,13,22,0.92)
       plus backdrop-filter, which is right for a 240px dropdown floating over
       a nav bar. Stretched to a full-width sheet over the dashboard it was
       measurably see-through in the 375px screenshot — the donut tiles and the
       tab-bar labels read straight through the department list, because the
       scrim below is a box-shadow that paints OUTSIDE the sheet and the blur
       is stripped on phones by src/shared/platform/perf/perf.css's lite tier. src/styles/5-utilities/mobile-v2.css
       already sets `.nav-dept-dropdown{backdrop-filter:none}` on phones for
       the same GPU reason; this finishes that job by supplying the solid fill
       it left implied. A sheet is a surface, not a tint. */
    background: #0b0f1e;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    /* Second shadow is the scrim: a 100vmax spread paints the whole viewport
       without needing a scrim ELEMENT (there is nowhere to add one — the sheet
       is portaled by JS). It cannot swallow taps, and the app already closes
       the menu on any outside click, so dismissal still works. */
    box-shadow:
      0 -18px 50px -14px rgba(0, 0, 0, 0.72),
      0 0 0 100vmax rgba(3, 5, 14, 0.55);
  }

  /* Slide-up entrance, keyed off the badge's aria-expanded. Default state is
     off-screen ONLY inside @supports, so a browser without :has() can never
     leave the sheet stranded below the fold. */
  @supports selector(:has(*)) {
    .nav-dept-dropdown {
      translate: 0 100%;
      transition: translate 280ms cubic-bezier(0.22, 1, 0.36, 1);
    }
    body:has(#nav-dept-badge[aria-expanded="true"]) .nav-dept-dropdown { translate: 0 0; }
  }

  /* A grab handle reads as "this dismisses downward". Sticky so it survives
     the sheet's own scroll. */
  .nav-dept-dropdown::before {
    content: '';
    position: sticky; top: 0;
    display: block;
    width: 38px; height: 4px;
    margin: 6px auto 8px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.24);
  }
  html[data-theme="light"] .nav-dept-dropdown::before { background: rgba(15, 23, 42, 0.22); }
  html[data-theme="light"] .nav-dept-dropdown { background: #f4f7ff; }

  .nav-dept-dd-label { font-size: 12px; padding: 4px 6px 8px; }
  /* !important because §2's `button{min-height:44px !important}` (pointer:coarse)
     would otherwise cap these rows at 44px — measured. A sheet row is the
     primary target on the screen and gets the roomier 56px. */
  .nav-dept-item, .nav-dept-item.current {
    min-height: 56px !important;
    border-radius: 12px;
  }
}
@media (max-width: 768px) and (prefers-reduced-motion: reduce) {
  .nav-dept-dropdown { transition: none !important; translate: 0 0 !important; }
}

/* ── 3c. THE DEAD "⋯ MORE" MENU ────────────────────────────────────────────
   MEASURED, not assumed. At 375px the top bar renders TWO adjacent more-menus:

     #nav-more-btn  (title="More", 44x44, x=223)  → opens .nav-more-menu
     #mtop-more     (the ⋮ button, 44x44, x=313)  → opens #mtop-sheet

   The first one opens NOTHING. src/shared/platform/mobile.js's top-bar takeover (its TARGETS
   list) MOVES the six utility controls — patch notes, review queue, Help,
   Bugs, Easy View, theme, account, log out — out of .nav-more-menu and into
   #mtop-sheet, leaving an HTML COMMENT placeholder behind for each so it can
   put them back. Probed live: .nav-more-menu has 0 element children and 6
   comment nodes, and opening it paints a 216x14 empty sliver just under the
   nav — which reads as a rendering glitch, not a menu.

   So this is a live control with an empty payload sitting next to the control
   that actually works. Two "More" buttons is also exactly the crowding the
   brief calls out.

   NOTHING IS REMOVED. Every one of those buttons is the SAME DOM ELEMENT,
   still wired, now rendered as a labelled row in the ⋮ sheet.

   `body.mtop-on` is the precise signal: src/shared/platform/mobile.js adds it in sync() only
   while it owns the bar, and restore() drops it above 768px, which also puts
   the buttons back into .nav-more-menu — so the wrap returns by itself. Using
   the body class rather than :has(> .nav-more-menu:empty) means this also
   holds on a browser without :has(), and it stays correct during the ~500ms
   boot poll before mobile.js has adopted anything (no class yet → menu still
   full → wrap still shown). */
@media (max-width: 768px) {
  body.mtop-on .nav-more-wrap { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   4. BOTTOM NAV CLEARANCE
   ══════════════════════════════════════════════════════════════════════════
   #mnav is fixed at the bottom (60px + safe area). Two separate defects were
   measured here, and the first version of this section fixed neither:

   (a) WRONG CONTAINER. The rule padded .tab-panel, #dashboard-v2-mount,
       .tab-content and .prod-main. On Overview all three of the first ones
       nest inside each other, so the padding STACKED — measured 240px of dead
       black space above the bar (mount ended at 4613, #dashboard at 4853) —
       while the real scroll container got nothing. The real scroller is
       <body> itself: base.css sets html,body{height:100%} and §1's
       overflow-x:hidden forces overflow-y to compute to `auto`, so body is
       667px tall with a 4853px scrollHeight. Its full-height child
       .dept-dashboard is the ONE element that needs the clearance, and
       src/styles/5-utilities/mobile-v2.css already pads exactly that element. Padding the same
       element from here cannot stack, so this is now a single 60+12 layer.

   (b) TRANSLUCENCY. The reported "pH-crash chip renders visibly through the
       bar" is not a padding bug at all — at 92% alpha with blur(16px), the
       PH CRASH? / VPD WATCH tiles were plainly readable through #mnav as they
       scrolled under it, which reads as broken rather than as a native
       translucent tab bar. The bar is made near-opaque below with a stronger
       blur, so passing content becomes a soft wash instead of legible text. */
@media (max-width: 768px) {
  :root {
    --mnav-h: 60px;
    --mnav-clear: calc(var(--mnav-h) + env(safe-area-inset-bottom, 0px) + 12px);
  }

  /* ONE clearance layer, on the dept root — the full-height child of the real
     (body) scroller. Do NOT add .tab-panel / .tab-content / #dashboard-v2-mount
     back: they nest, and the padding stacks. */
  body.mob-nav-on .dept-dashboard { padding-bottom: var(--mnav-clear); }

  /* Keyboard focus / scrollIntoView must never park a field under the bar.
     Costs no layout — it only shifts where programmatic scrolls land. */
  body.mob-nav-on,
  body.mob-nav-on .dept-dashboard { scroll-padding-bottom: var(--mnav-clear); }

  /* Content passing under a fixed bar is normal and native — an iOS tab bar
     does exactly that. What was NOT native here is that the tile row was
     bisected with no visible boundary, so it read as clipped rather than as
     "scrolled beneath".

     ⚠ THE OPACITY HAS TO DO THE WORK, NOT THE BLUR. src/styles/5-utilities/mobile-v2.css gives
     the bar blur(16px) at 0.92 alpha, but measured on a phone the computed
     backdrop-filter is `none`: src/shared/platform/perf/perf.css strips it from EVERY element with
     `html[data-perf="lite"] * { backdrop-filter: none !important }`, and the
     lite tier is exactly where phones land. So the bar was relying on a blur
     that never runs, leaving 8% of the dashboard legible through it. 0.97 plus
     an upward shadow makes it a distinct surface without any GPU cost. The
     blur is still declared so a non-lite phone gets the glass; it is a no-op
     under lite, which is the honest reason it is not load-bearing here. */
  #mnav {
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: rgba(6, 9, 21, 0.97);
    -webkit-backdrop-filter: blur(16px) saturate(150%);
    backdrop-filter: blur(16px) saturate(150%);
    box-shadow: 0 -12px 28px -14px rgba(0, 0, 0, 0.75);
  }
  /* src/styles/5-utilities/mobile-v2.css sets the light bar at (1,1,1) — match it or lose. */
  html[data-theme="light"] #mnav {
    background: rgba(246, 249, 255, 0.97);
    box-shadow: 0 -12px 28px -16px rgba(10, 16, 32, 0.30);
  }

  /* The bar's own labels were the last sub-12px text on every screen. 12px
     still fits: 5 buttons x 73px at 375px, icon 21 + gap + label ≈ 40px of a
     48px content box. Class-level so it does not disturb §1's floor. */
  .mnav-btn span, .mnav-sheet-btn span {
    font-size: 12px;
    letter-spacing: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Anything floating above the bar must clear it too. */
  .toast, .toast-stack, .gx-toast, .ipm-toast { bottom: var(--mnav-clear) !important; }

  /* ── The "Working…" pill was landing ON the page heading ────────────────
     SEEN, then measured. On the Bugs screen at 375px the busy pill rendered
     as a three-line amber box directly over "FEEDBACK TRACKER" and the
     "Bugs & Features" title — the page had no readable heading for as long as
     the fetch ran. It is global chrome (every tab shows it while Sheets is
     slow), so it belongs in this section rather than in any one tab.

     ⚠ TWO OWNERS, AND NEITHER IS A <link>. This is cascade trap (2) from the
     file header in practice:
       • src/shared/platform/loading-indicator.js injects <style id="tbusy-style"> at runtime
         with `.tbusy{position:fixed;top:16px;left:50%;
         transform:translateX(-50%) translateY(-14px)}`. Injected styles land
         after EVERY stylesheet, so load order cannot reach it.
       • src/styles/5-utilities/mobile-v2.css already fights that with `top:54px !important` to
         get the pill off the nav icons — which is what parks it on the
         heading instead, 54px being just under the 47px bar.
     So !important is required here too, on exactly the two properties being
     changed and nothing else. It is not a preference; an ordinary declaration
     is unreachable.

     BOTTOM IS THE NATIVE PLACE FOR TRANSIENT STATUS — it is where both mobile
     platforms put a snackbar, it cannot collide with a title, and this app's
     other transients (.toast, .gx-toast, .ipm-toast, one line up) are already
     sent there. --mnav-clear is reused so the pill rides 12px above the tab
     bar and above the home indicator, and moves with the bar in landscape
     (§11 shrinks --mnav-h to 46px and this follows automatically).

     `transform` is deliberately NOT touched. The injected rule animates the
     entrance through it (translateY(-14px) → 0 on `.on`), so overriding it
     would either freeze the pill visible or kill the fade. Only the anchor
     moves; the animation keeps working exactly as written.

     THE THREE-LINE WRAP IS A SEPARATE BUG, AND max-width WAS NOT THE CAUSE.
     Measured: the pill rendered 188px wide while mobile-v2.css's cap
     (min(72vw,330px) = 270px here) and this file's were both far larger, so
     raising a cap did nothing. The pill is `position:fixed; left:50%` with no
     width, so it is shrink-to-fit — and the space available to a fixed box
     starting at the 50% line is only the 187.5px to the right viewport edge.
     That, not any max-width, is what it sized to; the translateX(-50%) then
     pulls the already-too-narrow box back into the centre. 187.5 ≈ the 188
     measured, which is how the cause was confirmed rather than guessed.

     `width: max-content` opts out of that available-space calculation
     entirely, and max-width clamps the result, so the pill takes the width
     its text actually wants up to a sane cap — two comfortable lines instead
     of three cramped ones. Neither property needs !important: the injected
     stylesheet sets no width at all, and this file outranks mobile-v2.css's
     max-width on order alone. */
  .tbusy {
    top: auto !important;
    bottom: var(--mnav-clear) !important;
    width: max-content;
    max-width: min(calc(100vw - 32px), 340px);
  }
}
/* src/styles/5-utilities/mobile-v2.css drops these to 9px below 400px — that is under the floor. */
@media (max-width: 400px) {
  .mnav-btn span { font-size: 12px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   5. LAYOUT — one column is for PAGE grids, not for metric strips
   ══════════════════════════════════════════════════════════════════════════
   A page-level grid (content column + side rail) has to collapse on a phone.
   `min-width:0` goes with it because a grid item defaults to min-width:auto,
   which prevents the track from shrinking and produced exactly the
   497px-inside-341px clipping this pass found on Overview.

   ⚠ 2026-08-02 — THE LIST USED TO SAY "ANY multi-column grid" AND THAT WAS
   WRONG. It was blanket-applied to eight more class names. Audited one by one
   against what each grid's own stylesheet authors for a phone:

   REMOVED — the collapse was FIGHTING the authored phone layout:
     • .prod-strip      four ~76px WIP metric tiles. Stacked = 340px of
                        scrolling to say four short things; 2-up = 166px.
                        src/departments/production/production.css had to win the
                        fight back with `#dashboard-production` + !important.
     • .gx-receipt-grid its BASE rule is `repeat(2, 1fr)` and 4-up is the
                        ≥560 DESKTOP upgrade — 2-up IS the phone layout. Same
                        counter-!important in
                        src/departments/production/pages/genetics/components/allocation-editor/allocation-editor.css.
                        Both counter-rules are now deleted; the plain authored
                        declarations do the job unaided.
     • .ovf-pcm         the pH-crash modal's four metric tiles (BIGGEST DROP /
                        LOW PH / LATEST PH / WHEN). src/departments/cultivation/tabs/overview/overview-fixed.js:1617
                        authors `@media (max-width:560px){repeat(2,1fr)}`; that
                        declaration is not !important, so this rule beat it and
                        printed four 68px full-width bands. Measured on Overview
                        at 375px: 309px stacked → the authored 2-up instead.
     • .ovf-donuts      DEAD CODE, and wrong twice over. overview-fixed.js:1306
                        injects `.ovf-donuts{grid-template-columns:1fr 1fr
                        !important}` at ≤768 into <head> at runtime, which lands
                        after every <link> and wins the !important tie on order.
                        The two ring cards have rendered 2-up all along (166+166
                        measured); this line only ever looked like it did
                        something. Same trap as §6's .ovf-vitals note.

   REMOVED — inert, matched nothing (the §8 lesson, applied to §5):
     • .dash-grid    only `.v2-dash-grid` (class) and `#dash-grid` (id) exist.
     • .gx-stages / .cards-grid / .metrics-grid   zero occurrences repo-wide.
     • .stat-grid    only `.ns-stat-grid` exists, which is a different element.
   Verified by grep over every .css/.js/.html outside .git/.retired, and by
   probing synthesized nodes in the live page — all six computed `1fr` purely
   because the (empty) grid had one implicit column, not because of a match.
   ⚠ §10 (769–1024px) still lists the same four inert names. They are equally
   dead there, but that band is outside this pass's ≤768 scope, so they were
   left in place rather than edited blind. Clear them with the tablet pass.

   KEPT — .ovf-grid, and only .ovf-grid. It is the genuine page grid:
   `1fr minmax(340px,400px)` (content + right rail), authored to collapse at
   ≤1040. Redundant with that, but same computed value, and the `min-width:0`
   below is the load-bearing half. */
@media (max-width: 768px) {
  .ovf-grid { grid-template-columns: 1fr !important; }

  /* min-width:0 is right for a 2-up strip too — it is what stops a long value
     from pushing a track past its share. Applied to the grids that stay
     multi-column, NOT only to the ones that collapse. */
  .ovf-grid > *, .prod-strip > *, .ovf-pcm > *, .gx-receipt-grid > * { min-width: 0; }

  /* Side rails and sticky asides have nowhere to go on a phone. */
  .side-rail, .ovf-right, .drilldown-rail { position: static; width: auto; max-height: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   6. DENSITY — the "overstimulating" fix
   ══════════════════════════════════════════════════════════════════════════
   The brief: mobile should not be crowded with buttons and data; the PC is
   where the detail lives. These rules DEFER detail, they do not delete it —
   every hidden element remains in the DOM, reachable on a larger screen, and
   nothing here runs above 768px. */
@media (max-width: 768px) {
  /* The Overview donuts were 300px tall each to render one percentage — 600px
     of a 667px screen for two numbers. Halved, and laid out as a row so the
     figure and its label read together. */
  .ovf-dcard-body { padding: 12px 14px; gap: 12px; }
  .ovf-donut, .ovf-dcard svg { width: 108px !important; height: 108px !important; }

  /* OPT-IN HOOKS ONLY. Verified by grep: nothing in the repo currently carries
     data-detail="desktop" / .desktop-only / .ovf-card-sub-detail, so this hides
     no existing feature. It exists so a component author can mark genuinely
     redundant chrome as deferred — the node stays in the DOM and returns above
     768px. Do not add a selector here for content that has no other route. */
  [data-detail="desktop"], .desktop-only, .ovf-card-sub-detail { display: none !important; }

  /* Long metric strips scroll horizontally in their own rail with a visible
     edge, rather than wrapping into a wall of tiles.
     ⚠ .ovf-vitals is deliberately NOT in this list. src/departments/cultivation/tabs/overview/overview-fixed.js
     injects `@media(max-width:560px){.ovf-vitals{grid-template-columns:repeat(2,1fr)}}`
     into <head> at runtime, which lands after this file and wins the tie — the
     rail rule was dead code here. The 2-up grid it produces is the better
     layout anyway (measured on Overview at 375px), so it is left to own it. */
  .prod-strip.is-rail, .tk-stats {
    display: flex; overflow-x: auto; gap: 10px;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .tk-stats::-webkit-scrollbar { display: none; }
  .prod-strip.is-rail > *, .tk-stats > * { scroll-snap-align: start; flex: 0 0 auto; min-width: 132px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   7. TABLES → CARDS
   ══════════════════════════════════════════════════════════════════════════
   A data table cannot be made to work at 375px by shrinking it. Tables that
   opt in with .mob-cards restructure into stacked records using each cell's
   data-label as its heading. Tables that do NOT opt in get a scroll container
   with a shadow affordance, so at minimum nothing is silently clipped. */
@media (max-width: 768px) {
  .table-wrap, .tbl-wrap, .data-table-wrap {
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    background:
      linear-gradient(to right, var(--col-navy, #0b1020) 30%, transparent),
      linear-gradient(to left, var(--col-navy, #0b1020) 30%, transparent) 100% 0;
    background-size: 28px 100%, 28px 100%;
    background-repeat: no-repeat;
    background-attachment: local, local;
  }

  table.mob-cards, table.mob-cards thead, table.mob-cards tbody,
  table.mob-cards tr, table.mob-cards td, table.mob-cards th { display: block; width: 100%; }
  table.mob-cards thead { position: absolute; left: -9999px; }   /* headings move into the rows */
  table.mob-cards tr {
    margin-bottom: 10px; border-radius: 12px; padding: 6px 2px;
    background: var(--glass-1, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
  }
  table.mob-cards td {
    display: grid; grid-template-columns: 40% 1fr; gap: 10px;
    padding: 9px 12px; border: 0; text-align: left;
  }
  table.mob-cards td::before {
    content: attr(data-label);
    font-weight: 600; color: var(--text-muted, #94a3b8); font-size: 12px;
  }
  table.mob-cards td:empty { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   8. MODALS → SHEETS
   ══════════════════════════════════════════════════════════════════════════
   ⚠ THE PREVIOUS VERSION OF THIS SECTION TARGETED CLASS NAMES THAT DO NOT
   EXIST. It styled `.modal, .modal-card, .smd-modal, .tkd-modal, .sched-modal,
   .modal-body, .modal-actions, .gx-modal-body` — a scan of the live CSSOM
   found ZERO elements or rules for a bare `.modal`/`.modal-card`, and the
   *-body/*-actions names appear once or twice in dev-only markup. The whole
   block was inert.

   What the app ACTUALLY does (scanned from the CSSOM: 29 matches):
   modals are a fixed, full-viewport OVERLAY that CENTRES its dialog child with
   place-items/align-items:center. Almost none use translate(-50%,-50%). So the
   native conversion is to flip the overlay's alignment from centre to end and
   let the dialog fill the width — the same technique src/departments/cultivation/tabs/overview/overview-fixed.js
   already uses for `.ovf-im-ov{place-items:end center}`.

   SCOPE IS DELIBERATE, not a wildcard:
   • Included: centring overlays whose DIALOG has no ≤768px rule of its own.
     ⚠ Keying that test on the overlay's name was the first mistake here: the
     Data Stream modal has no rule on `.ns-modal-overlay` but a complete one on
     `.ns-modal` (mobile-v2.css makes it a deliberate 100vw/100dvh full-screen
     modal — a legitimate native shape for a dense data panel). Verified in the
     browser: my rule lost to it at (1,1,0), so it was inert. Dropped, along
     with .pn-modal-backdrop / .ticket-modal-backdrop / .gcal-modal-backdrop,
     whose dialogs are likewise already tuned, and .v2c-backdrop, whose child
     is a DRAWER and has its own idiom.
   • Excluded because another agent already tuned them at ≤768px and this file
     would silently outrank that work: .ftest-overlay, .thm-overlay,
     .calc-xpl-overlay, .ev-overlay, .ovf-im-ov, .addback-*,
     .perc-sub-modal-overlay, .ns-modal-overlay.
   • Excluded because they are not dialogs: #auth-screen, .boot-loader,
     #wip-banner, #torus-intro, .tbusy, .tk-toast, .sidenav-edge-toggle,
     .mchat-mute-menu (an anchored context menu), .mmv-overlay (full-screen
     media viewer) and .bug-shot-lightbox (image lightbox) — a bottom sheet
     would be the wrong shape for all of them.
   • Excluded on purpose: confirms and alerts stay CENTRED. That is the native
     idiom too — iOS uses a centred alert for a decision and a bottom sheet for
     a list of actions. They get width/target treatment further down instead. */
@media (max-width: 768px) {
  .rh-overlay, .rh-cm-overlay, .tmail-overlay,
  .msrch-overlay, .mup-overlay, .mpres-shift-overlay, .memo-overlay,
  .wkr-overlay, .modal-overlay, .pa-modal-ov, .rcp-ov,
  .v2-inbox-modal-backdrop {
    /* Only the padding is reset here. The alignment is NOT flipped on the
       overlay, deliberately: these 17 overlays are a mix of grid and flex, and
       in a COLUMN flex container `align-items:flex-end` right-aligns instead
       of bottom-aligning — it would have silently pushed half of them to the
       wrong edge. The dialog uses auto margins instead (below), which resolve
       to bottom-centre in grid, row-flex and column-flex alike, so no
       per-overlay axis knowledge is needed. */
    padding: 0 0 env(safe-area-inset-bottom, 0px);
  }

  /* The dialog itself: full-bleed, rounded at the top only, capped so the
     sheet never exceeds the screen, and scrolling inside itself. dvh first so
     iOS toolbars cannot push the actions out of reach.
     `margin: auto auto 0` is the axis-agnostic bottom-centre described above.
     Its own padding is left untouched — the safe-area inset is applied to the
     overlay instead, so a dialog's internal spacing is never zeroed out. */
  .rh-overlay > *, .rh-cm-overlay > *, .tmail-overlay > *,
  .msrch-overlay > *, .mup-overlay > *, .mpres-shift-overlay > *, .memo-overlay > *,
  .wkr-overlay > *, .modal-overlay > *, .pa-modal-ov > *, .rcp-ov > *,
  .v2-inbox-modal-backdrop > * {
    width: 100%;
    max-width: 100%;
    margin: auto auto 0;
    max-height: 92vh;
    max-height: 92dvh;
    border-radius: 20px 20px 0 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }

  /* .v2-inbox-modal is the one dialog that positions ITSELF (fixed + top/left
     50% + translate(-50%,-50%)), so the overlay flip above cannot move it. */
  .v2-inbox-modal, .v2-inbox-modal.is-open {
    top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
    transform: none !important;
    width: 100% !important; max-width: 100% !important;
    max-height: 92dvh;
    border-radius: 20px 20px 0 0 !important;
  }

  /* Bodies scroll; headers and action rows pin. Names verified against the
     app's per-component prefixes rather than a guessed shared convention —
     and only for the dialogs this file actually sheets. .ns-modal-body,
     .thm-body, .calc-xpl-body and .perc-body are handled by their own files at
     ≤768px (and win on specificity), so listing them here was dead weight. */
  .rh-cm-body, .msrch-body, .mpres-shift-body {
    flex: 1 1 auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }

  /* Confirms + alerts stay centred (see the note above) — they just get room
     to breathe and a thumb-sized pair of buttons. */
  .rooms-confirm-overlay, .ftest-confirm-overlay, .alert-modal-overlay { padding: 16px; }
  .rooms-confirm-modal, .ftest-confirm-modal, .alert-modal {
    width: 100%; max-width: 420px;
  }
  .rooms-confirm-modal button, .ftest-confirm-modal button, .alert-modal button {
    min-height: 48px;
  }

  /* Any sheet's action row sits above the home indicator.
     ⚠ `.msrch-foot` USED TO BE LISTED HERE AND DOES NOT EXIST. Re-audited by
     grepping the repo for every name in this section: all the others resolve
     to their own component (a CSS file + the JS that builds the markup), but
     `.msrch-foot` matched nothing outside this file. The message-search modal
     has no footer element at all — its parts are .msrch-overlay > .msrch-card
     > .msrch-bar / .msrch-body / .msrch-res / .msrch-x, and the search input
     lives in the BAR at the top. Two dead rules, removed. This is the same
     guessed-shared-convention mistake this section's header already warns
     about, so: verify a name against the component that renders it, never
     against the naming pattern of its neighbours. */
  .rh-cm-foot {
    position: sticky; bottom: 0;
    padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    background: var(--col-navy, #0b1020);
    border-top: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
    display: flex; gap: 10px;
  }
  .rh-cm-foot > button { flex: 1 1 auto; min-height: 48px; }

  /* ── The Data Stream modal is SEE-THROUGH ──────────────────────────────
     This modal is deliberately NOT converted to a sheet (see the header: at
     ≤768px src/styles/5-utilities/mobile-v2.css makes it a full-bleed 100vw/100dvh panel, which
     is a legitimate native shape for a dense data readout). But being the
     right SHAPE is not the same as being a surface, and measured at 375px it
     was transparent enough to read the dashboard through it: the screenshot
     shows "WELCOME BACK, DEV TEST", "PH CRASH? YES" and "Room 4 FLOWER 48"
     legible straight through the stat tiles.

     ROOT CAUSE — IDENTICAL TO §3b, SECOND ELEMENT. src/styles/5-utilities/mobile-v2.css strips
     the blur on phones for GPU reasons (`.nav-switch-dropdown,
     .nav-dept-dropdown, .ns-modal, .ns-tooltip { backdrop-filter: none }`)
     but supplies a replacement fill only for #main-nav. .ns-modal was left on
     its glass value of rgba(8,13,30,0.94) with nothing behind it — measured
     computed backdrop-filter `none`, because src/shared/platform/perf/perf.css's lite tier
     (confirmed active: html[data-perf="lite"]) drops it from every element
     anyway. 6% of the dashboard was showing through a full-screen modal.
     §3b fixed exactly this for the department sheet; this finishes the job.

     ⚠ SPECIFICITY: mobile-v2.css scopes its rules `#ns-modal-overlay
     .ns-modal` (1,1,0). A bare `.ns-modal` here would be outranked no matter
     how late this file loads, so the ID form is matched and cascade order
     settles it — the mechanism this whole file runs on. The bare selector is
     kept alongside as the fallback for the (currently unused) case where the
     modal renders outside that overlay id. Light theme has to clear
     `html[data-theme="light"] .ns-modal` in src/app/shell/nav/nav.css (0,2,1), hence the
     ID-scoped form there too. */
  .ns-modal,
  #ns-modal-overlay .ns-modal {
    background: #070b18;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  html[data-theme="light"] .ns-modal,
  html[data-theme="light"] #ns-modal-overlay .ns-modal { background: #f2f5ff; }
}

/* ══════════════════════════════════════════════════════════════════════════
   9. FORMS
   ══════════════════════════════════════════════════════════════════════════
   ⚠ The previous version applied `input,select,textarea{width:100%;
   min-height:48px}` to EVERY input. On a checkbox or radio that is a
   full-width, 48px-tall control — the exclusions below are load-bearing, not
   tidiness. Likewise `label{display:block}` broke labels that legitimately sit
   inline beside their checkbox, so it is now scoped to form rows.

   The Submit-Data form (.ftest-*) already has a full ≤768px pass in
   src/departments/cultivation/lib/form/form.css (2-up field grid, 44px controls, sticky
   safe-area footer). Nothing here targets .ftest-* — this file loads last and
   would silently outrank that work. */
@media (max-width: 768px) {
  .form-row, .form-grid, .field-row { display: grid; grid-template-columns: 1fr !important; gap: 12px; }
  .form-row > label, .form-grid > label, .field-row > label { display: block; margin-bottom: 4px; }

  /* ⚠ padding-BLOCK only. An earlier version set `padding: 12px 14px`, which
     also rewrote the HORIZONTAL padding — and several fields in this app carry a
     larger left padding on purpose, to clear an absolutely-positioned icon
     (.auth-input-ic on the login screen sits at left:16px). Collapsing that to
     14px put the person and lock glyphs directly on top of the "U" in USERNAME
     and the "A" in ACCESS CODE, on the first screen every user sees.
     Vertical rhythm is ours to set; horizontal padding belongs to whichever
     component decided what sits inside the field. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="submit"]):not([type="button"]):not([type="image"]):not([type="file"]),
  select,
  textarea {
    width: 100%;
    min-height: 48px;
    padding-block: 12px;
    border-radius: 10px;
  }
  textarea { min-height: 96px; }

  /* LOGIN FIELDS — icon/placeholder collision.
     .input-wrapper positions .auth-input-ic absolutely at left:16px, but the
     input's own padding-left is also 16px, so the glyph renders ON TOP of the
     first character: the person icon over the "U" of USERNAME, the padlock over
     the "A" of ACCESS CODE. Measured at 375px: the icon occupies x 65-81 while
     text begins at x 65.
     This is PRE-EXISTING, not caused by the mobile pass — verified by removing
     this file's padding override entirely and re-measuring, which still gave
     16px. Corrected here because the login screen is the first thing every user
     sees on a phone and nothing else owns it. The value clears the icon's right
     edge plus a 12px gap; padding-inline-start (not padding-left) so it also
     behaves in RTL.

     The real owner is src/styles/5-utilities/mobile.css, which sets
     `#username-input,#password-input{padding:12px 16px}` at max-width:640px —
     found by asking the browser which rules actually matched, after two guesses
     at the cascade were wrong. That is an ID selector (1,0,0), so the class form
     of this rule was inert; matching ID specificity lets cascade ORDER settle it,
     which is this file's whole mechanism, with no !important needed. */
  .input-wrapper > input,
  #username-input, #password-input { padding-inline-start: 46px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   10. TABLET — 769 to 1024
   ══════════════════════════════════════════════════════════════════════════
   Not a big phone and not a laptop. The brief asks for two columns; the
   previous version collapsed .ovf-grid to ONE, which gave a 1024px tablet a
   narrower layout than a 768px phone gets. .ovf-grid is `1fr minmax(340px,
   400px)` (main + inbox rail) and src/departments/cultivation/tabs/overview/overview-fixed.js already single-columns
   it below 1040px — so it is left alone here and the generic card grids take
   the two-column treatment instead. Detail stays; only the columns change. */
@media (min-width: 769px) and (max-width: 1024px) {
  .dash-grid, .cards-grid, .stat-grid, .metrics-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .dash-grid > *, .cards-grid > *, .stat-grid > *, .metrics-grid > * { min-width: 0; }

  .ovf-right { position: static; max-height: none; }

  /* MEASURED: the only over-width element on an 810px tablet is the decorative
     Three.js backdrop, #overview-canvas — box [64 … 874] against an 810px
     viewport. Its own CSS is `position:fixed; inset:0; width:100%`, which
     should be harmless; the reason it is not is that GSAP leaves an IDENTITY
     transform (matrix(1,0,0,1,0,0)) on #tab-overview, and a transformed
     ancestor becomes the containing block for fixed descendants. The canvas is
     therefore laid out against #tab-overview's box, which starts 64px in
     (side-nav offset), and 64px of it hangs off the right edge.

     Retargeting the canvas itself does NOT work — verified by setting left/
     right/width live in the page: every variant still resolved against the same
     offset containing block. Clipping at that containing block does. `clip`
     rather than `hidden` on purpose: clip does not create a scroll container,
     so sticky positioning inside the tab keeps working, and naming only the X
     axis leaves vertical overflow (dropdowns, tooltips) free to escape.
     Nothing is lost — it is a background canvas. Never runs above 1024px. */
  #tab-overview { overflow-x: clip; }
}
/* Touch targets on a tablet, but only where the pointer is actually coarse —
   a 1024px desktop window with a mouse keeps the tighter desktop sizing.
   src/styles/5-utilities/mobile.css already runs a defensive 641–1194 band; this narrows to the
   controls that band misses and keeps prose links out of it. */
@media (min-width: 769px) and (max-width: 1024px) and (pointer: coarse) {
  button, [role="button"], [role="tab"], select { min-height: 48px; }
  p a[href], li a[href], td a[href], .prose a[href] { min-height: 0; }
}

/* ══════════════════════════════════════════════════════════════════════════
   11. LANDSCAPE PHONE
   ══════════════════════════════════════════════════════════════════════════
   A 375x667 phone turned sideways is 667x375 — barely 300px of usable height
   once the bars are drawn. Trim the vertical chrome so content still fits.
   (pointer:coarse keeps short desktop windows out of this band.) */
@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) and (pointer: coarse) {
  /* The bar shrinks — and --mnav-h has to shrink with it or every clearance
     derived from it over-reserves. src/styles/5-utilities/mobile-v2.css hard-codes the height, so
     it is restated here rather than left to the custom property alone. */
  :root { --mnav-h: 46px; }
  #mnav { height: calc(46px + env(safe-area-inset-bottom, 0px)); }

  /* LABELS SURVIVE LANDSCAPE — they just lie down.
     Hiding them left five unlabelled icons, and an icon-only bar is a
     discoverability problem: the whole point of a labelled tab bar is that you
     do not have to already know what the pictograms mean. Height is the scarce
     axis in landscape, not width — there is 667px of it — so stacking the icon
     ABOVE the label was the expensive choice. Side by side costs nothing
     vertically and keeps the words.
     Two safeguards: the label truncates rather than wrapping (a wrapped label
     would re-grow the bar it was meant to shrink), and it disappears below
     560px of width, where five icon+label pairs genuinely stop fitting. */
  .mnav-btn {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 4px 6px;
    min-width: 0;
  }
  .mnav-btn .nav-tab-icon { width: 20px; height: 20px; flex: none; }
  .mnav-btn span {
    display: block;
    font-size: 11px;
    line-height: 1;
    max-width: 8ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Genuinely out of room — icons only, as before. */
  @media (max-width: 560px) {
    .mnav-btn { flex-direction: column; gap: 0; }
    .mnav-btn span { display: none; }
    .mnav-btn .nav-tab-icon { width: 22px; height: 22px; }
  }

  /* Decorative vertical chrome is the first thing to go when height is the
     scarce axis — none of it carries information. */
  .overview-bg-text, .wip-bg-text, .page-eyebrow { display: none; }
  .dashboard-header { padding-top: 8px; padding-bottom: 8px; }
  .page-title { font-size: 18px; line-height: 1.15; }

  .ovf-donut, .ovf-dcard svg { width: 76px !important; height: 76px !important; }
  .ovf-dcard-body { padding: 8px 12px; }

  /* Sheets keep their actions reachable when there are only ~330 usable px. */
  .nav-dept-dropdown { max-height: 88dvh; }
  .rh-overlay > *, .msrch-overlay > *, .mup-overlay > *,
  .modal-overlay > *, .pa-modal-ov > *, .rcp-ov > * { max-height: 96dvh; }
}

/* ══════════════════════════════════════════════════════════════════════════
   12. MOTION
   ══════════════════════════════════════════════════════════════════════════
   Phones are where reduced-motion is most commonly enabled and where GPU
   effects cost the most battery. */
@media (max-width: 768px) and (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   13. TRIAGE + DENSITY — the mobile philosophy, applied app-wide
   ══════════════════════════════════════════════════════════════════════════
   Proven on the Overview dashboard and generalised here so every tab inherits
   the same behaviour instead of each one being hand-tuned.

   THE RULE: on a monitor, tiles can be equal — the eye sweeps the whole grid at
   once. On a 375px phone you see roughly four at a time, so equal weight means
   scrolling past "pH CRASH? YES" to reach "WATER TEMP 69" with both looking
   identical on the way. The phone therefore sorts by URGENCY; the desktop keeps
   source order.

   NOTHING IS HIDDEN. This re-ranks and resizes; it never removes. Every tile is
   still present, still reachable, still the same content.

   SCOPE IS DELIBERATE. `order` only affects flex/grid children, and applying it
   to every status-classed element in the app would reorder lists where sequence
   carries meaning (a log, a timeline, a route). So it is limited to containers
   whose NAME says they are an unordered collection of tiles — *-grid, *-cards,
   *-tiles, *-stats, *-metrics, *-vitals — and only to their DIRECT children. A
   container that means something by its order is not called a grid.

   ── HOW MUCH OF THE APP DOES THIS ACTUALLY REACH? MEASURED, AND THE ANSWER IS
   "ALMOST NONE — CORRECTLY." ──────────────────────────────────────────────────
   A measurement pass reported "triage ok=0" on most tabs and the obvious
   conclusion was that the container match is too narrow. It is not. Every
   flex/grid container in the app with TWO OR MORE state-classed direct children
   was enumerated, across both departments and all of their tabs at 375px. The
   entire census is four containers:

     .tk-grid.tk-grid-4     grid  3/4 stated   Overdue · Unassigned · Done today
     .ovf-vitals            grid  4/6 stated   pH crash? · Equipment · pH alerts…
     .ovf-summary           flex  2/7 stated   "7 rooms · 16 systems · 3 need…"
     .eq-ov-segbar          flex  4/4 stated   four zero-text bar segments

   • .tk-grid ALREADY WORKS — measured child orders 0,-2,-1,2: "Overdue" (alert)
     floats up, "Unassigned" (warn) next, "Done today" (good) sinks. Exactly the
     intended behaviour, no change needed.
   • .ovf-vitals is the canonical case from the brief and is ALREADY TRIAGED —
     but by src/departments/cultivation/tabs/overview/overview-fixed.js, which writes order 1..4 INLINE, so it wins
     over any stylesheet and the rules below are inert there. `-vitals` is
     added to the container list anyway, as a safety net: it is the one
     container in the census that is a genuine unordered tile grid but whose
     name this section did not cover. If that inline ordering is ever removed,
     the tiles keep triaging instead of silently reverting to source order.
     Verified there is no other `-vitals` container in the app.
   • THE OTHER TWO MUST NEVER BE REORDERED, which answers the question this
     section exists to be careful about. .ovf-summary is a SENTENCE ("7 rooms ·
     16 systems · 3 need attention · 647 unread") whose separator dots are
     siblings — re-ranking "3 need attention" would move it away from its
     dots and scramble the line. .eq-ov-segbar is a proportional SEGMENTED BAR
     whose four segments encode a distribution by position; sorting it would
     misreport the data. Neither matches the selector below, and neither may be
     added to it.

   So the narrow scope is not a bug to be widened — it is the reason nothing in
   this app has been scrambled. The behaviour generalises to future tile grids;
   it should not be forced onto today's rows, strips and bars.

   The state VOCABULARY was re-checked the same way rather than extended on
   spec. The names that actually appear on the two real tile grids — crash,
   warn, good, is-alert, is-warn, is-good — are all already covered below. The
   app's other state tokens (tone-crit, sev-good) were traced to their parents
   and sit inside a `display:block` list and inside a <button>'s own internals,
   where `order` does nothing at all. Adding them would have been dead code. */
@media (max-width: 768px) {
  /* Worst first. Two ranks of bad news, then neutral, then healthy. */
  :is([class*="-grid"], [class*="-cards"], [class*="-tiles"], [class*="-stats"], [class*="-metrics"], [class*="-vitals"])
    > :is(.crash, .critical, .is-critical, .is-crit, .crit, .bad, .is-bad, .is-danger, .error, .is-error) {
    order: -3;
    grid-column: 1 / -1;
  }
  :is([class*="-grid"], [class*="-cards"], [class*="-tiles"], [class*="-stats"], [class*="-metrics"], [class*="-vitals"])
    > :is(.alert, .is-alert, .overdue, .is-overdue, .urgent) {
    order: -2;
    grid-column: 1 / -1;
  }
  :is([class*="-grid"], [class*="-cards"], [class*="-tiles"], [class*="-stats"], [class*="-metrics"], [class*="-vitals"])
    > :is(.warn, .is-warn) { order: -1; }
  /* Healthy states sink: confirming "fine" is the least urgent thing on screen. */
  :is([class*="-grid"], [class*="-cards"], [class*="-tiles"], [class*="-stats"], [class*="-metrics"], [class*="-vitals"])
    > :is(.good, .is-good, .ok, .is-ok, .healthy, .is-healthy) { order: 2; }

  /* A problem that spans the row has earned the space to state itself. */
  :is(.crash, .critical, .is-critical, .bad, .is-bad, .alert, .is-alert) :is(b, strong, [class*="-v"], [class*="-val"]) {
    font-size: max(1em, 22px);
  }

  /* DENSITY. Desktop gaps of 14-20px read as dead space at 375px, where every
     wasted 10px is ~1.5% of the screen. Tightened globally, not per-tab. */
  :is([class*="-grid"], [class*="-cards"], [class*="-tiles"], [class*="-stats"], [class*="-metrics"], [class*="-vitals"]) {
    gap: 8px;
  }
  :is([class*="-card"], [class*="-tile"], [class*="-panel"]) { border-radius: 14px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   14. FIELD-USE RULES — researched, not guessed
   ══════════════════════════════════════════════════════════════════════════
   These come from published guidance on industrial / agricultural field apps,
   not from generic mobile-web habit. The use scene is specific: a grower
   holding a phone one-handed, in gloves, under high-intensity grow lights.

   ── (a) 48dp targets, not 44px ────────────────────────────────────────────
   Apple's HIG floor is 44pt; Material's is 48dp. Field-app guidance is explicit
   that gloved operation needs the LARGER of the two, because a gloved fingertip
   has a bigger and less precise contact patch than a bare one. §2 and §8 were
   built to 44 and are now 48. This is the one place the app deliberately
   chooses Material over Apple, and the reason is the gloves.

   ── (b) The thumb zone is the bottom third ────────────────────────────────
   One-handed use means the top of a 667px screen is a stretch and the top
   corners are effectively out of reach. Primary and destructive actions belong
   in the lower third; the app already docks its nav there. Page-level primary
   actions that are authored at the top of a form or panel are pinned to the
   bottom on phones so the thumb never travels.

   ── (c) Contrast, because a grow room is not an office ────────────────────
   Dark UI is the app's identity and stays. But the research is consistent that
   dark themes lose legibility as ambient light rises, and a flowering room runs
   far brighter than the desk this palette was designed at. So on phones the
   glass surfaces get less transparent and borders get firmer — the look is
   preserved, the contrast is raised. Users who want the full answer have the
   light theme, which is why (d) exists.

   ── (d) The theme switch must be reachable ────────────────────────────────
   It currently lives inside the "⋯ More" sheet, several taps deep. Walking from
   a dim corridor into a lit flower room is exactly when someone needs it, and
   that is the worst moment to hunt through a menu. Raised in the sheet's order;
   a dedicated control is a JS change and is noted for the owner of nav-more.js.
*/
@media (max-width: 768px) {
  /* (b) THUMB ZONE — a primary action authored at the top of a panel docks to
     the bottom on phones. Scoped to explicit primary/submit classes so it can
     never catch an ordinary button. */
  .form-actions, .modal-actions, [class*="-actions"]:has(> [class*="primary"]),
  [class*="-footer"]:has(> [class*="primary"]) {
    position: sticky;
    bottom: calc(60px + env(safe-area-inset-bottom, 0px));
    z-index: 5;
    padding-block: 10px;
    background: linear-gradient(to top,
      var(--col-void, #080d1e) 62%,
      color-mix(in srgb, var(--col-void, #080d1e) 0%, transparent));
  }

  /* (c) CONTRAST LIFT for high ambient light. Same palette, firmer surfaces.
     Only raises opacity — it does not change a single hue. */
  .ovf-card, .ovf-room, [class*="-card"], [class*="-panel"], [class*="-tile"] {
    background: color-mix(in srgb, var(--col-indigo, #0f1535) 82%, transparent);
    border-color: color-mix(in srgb, var(--text-secondary, #cbd5e1) 22%, transparent);
  }
  /* Secondary text is the first thing to disappear under glare. */
  [class*="-sub"], [class*="-meta"], [class*="-hint"], [class*="-cap"] {
    color: var(--text-secondary, #cbd5e1);
  }
}
