/* ═══════════════════════════════════════════════════════════════════════════
   SPARTAN CAD — css/responsive.css                                    (A-668)
   THE LAST STYLESHEET IN <head>. Nothing may come after it. See §0.
   ═══════════════════════════════════════════════════════════════════════════

   Every page of SpartanCAD must be laid out correctly on a folded phone
   (344px), an unfolded foldable (673-884px), a tablet and a desktop — and must
   re-lay-out live when a foldable is opened or closed. Contract + rationale:
   CONTRACT-responsive-layout.md. Gates: `npm run test:a668-responsive` (pure
   Node, in the Vercel deploy chain) and `npm run test:a668-sweep` (Chromium,
   19 screens × 8 viewports).

   ─── §0 · WHY THIS FILE IS LAST, AND WHY IT USES !important ───────────────
   SpartanCAD has no other stylesheet. Every layout decision in the app is an
   inline `style={{…}}` object in a React module — roughly ten thousand of
   them — and an inline style beats every selector ever written. So the rules
   below that must win over one carry !important, and the <link> must sit after
   index.html's <style> block or it loses to that too. Two consequences:

     • ANY STYLESHEET YOU ADD GOES ABOVE THIS ONE in index.html.
     • BUMP THE ?v= ON EVERY EDIT. css/ is not covered by the per-module
       content-hash manifest (scripts/generate-module-manifest.cjs walks
       modules/ only), so an unversioned edit can be served stale.

   And the corollary that matters more: this file is deliberately SMALL. CAD
   already threads responsive flags (isPhone / isHandheld, and now
   vpTier / vpNarrow / vpXs / vpShort from SpartanVP) through its JSX, so the
   right place to change a layout is the renderer that owns it. What lives here
   is only what JSX cannot express: media and pointer queries, the foldable's
   viewport segments, scrollbar affordances, dvh, and print.

   ─── §0a · TIER TABLE ─────────────────────────────────────────────────────
   These five numbers are written down in THREE places and must agree:
   modules/_lib/viewport.js's TIERS array, the inline pre-paint stamp in
   index.html's <head>, and this table. A stylesheet cannot read a JS array and
   an inline boot snippet cannot wait for a module, so there is no way to have
   one copy. test/a668-responsive-chain.mjs parses all three and compares them.

     | tier | media                    | devices                              |
     |------|--------------------------|--------------------------------------|
     | `xs` | (max-width: 479px)       | Fold COVER 344x882 · phone 412x915   |
     | `sm` | (max-width: 639px)       | large phones · phone landscape       |
     | `md` | (max-width: 899px)       | Fold UNFOLDED 673x841 / 717x845 ·    |
     |      |                          | iPad portrait 820x1180 · 884         |
     | `lg` | (max-width: 1279px)      | tablet landscape 1024x768 · laptops  |
     | `xl` | everything wider         | desktop — THE CONTROL                |

   Plus  [data-vp-h="short"]   viewport under 560px TALL (cover in landscape
                               is 882x344, and a keyboard makes any phone short)
         [data-vp-fold="h"|"v"] a real hinge seam is present
         [data-vp-nav]          drawer | docked
         [data-vp-ptr]          coarse | fine
         [data-vp-orient]       portrait | landscape

   ─── §0b · DESKTOP IS THE CONTROL ────────────────────────────────────────
   Almost every rule below is fenced behind a tier, pointer or segment
   condition, and the sweep runs 1440x900 as its control for exactly that
   reason: a change to the desktop layout is a bug, not a trade-off.

   TWO DELIBERATE EXCEPTIONS, stated here rather than left to be discovered.
   Both fix a bug the sweep found AT 1440x900, so leaving the desktop alone
   would have meant leaving a control unreachable on it:

     1. `.sp-tabstrip` is NOT tier-fenced, and its scrollbar thumb is visible on
        a fine pointer. The Production tab bar's eleven tabs want 1451px, so on
        a 1440px window two of them and the xlsx export were off the right-hand
        edge with nothing to scroll. It is a scroller at every width now, and a
        scroller nobody can see is not an affordance — so the desktop gains a
        4px thumb on that one row. Nothing else in the app overflows at 1440.
     2. The Colours pane's LIVE PREVIEW chip row wraps whenever the PANEL is
        under 520px wide, which includes the 400px default every desktop user
        gets. Its "French" chip measured 8px past the panel edge at 1440x900,
        inside a box with overflow:hidden — clipped, not merely off-screen, and
        unreachable since the panel was built. The row is one line taller on the
        desktop now. That fix lives in the renderer (39d-settings-products.js),
        not here, and is noted here because it is the other place the control
        deliberately moved.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ─── 1 · Foundation ───────────────────────────────────────────────────────
   index.html's <style> already sets box-sizing, 100% text-size-adjust and
   `overflow:hidden` on html/body/#root. What is missing is the min-width:0
   that lets a flex/grid child actually shrink, and a guard against the
   horizontal rubber-band that makes a one-pixel overflow feel like a broken
   page on Android. */

html { overscroll-behavior-x: none; }

/* A flex item's default min-width is auto — its CONTENT's min-content size —
   so one long word or one nowrap label props a whole row open and the row
   overflows its parent instead of the label ellipsising. That single default
   is behind most of the header overflows A-668 found.

   Scoped, NOT blanket. `#root * { min-width: 0 }` looks like the same fix and
   is not: it also collapses the intentional column widths in CAD's cut lists
   and pricing grids (the CRM tried it and squeezed six-column grids to 40px).
   So it applies to the surfaces whose job is to hold a row of controls, and
   :where() keeps the specificity at zero so any component can still say
   otherwise. */
html:not([data-vp="xl"]) :where(.sp-surface-head, .sp-surface-head > *,
                                .sp-tabstrip, .sp-row-wrap, .sp-row-wrap > *) {
  min-width: 0;
}


/* ─── 2 · Full-screen surface headers ──────────────────────────────────────
   CAD's six full-screen surfaces (Settings, Production, Builds, Search, the
   3D editor, the CRM modes) each open with one `display:flex` header row at a
   FIXED height with a generous gap and no wrapping. On a desktop that row is
   comfortable; at 344px the Settings header wanted 776px and the Production
   header — eleven tabs — wanted 1451px. Both simply ran off the right edge,
   taking Save, Backups and every tab past "Glass" with them.

   The renderers now mark those rows `.sp-surface-head` and let them wrap. All
   this file adds is the part JSX cannot: the fixed inline `height` has to
   become a MINIMUM or a wrapped row is clipped to one line's worth of pixels,
   and that override needs !important because the height is inline. */
html:not([data-vp="xl"]) .sp-surface-head {
  height: auto !important;
  flex-wrap: wrap;
  align-items: center;
  row-gap: 6px;
}
/* Small phones get the gap back as space for the controls themselves. */
html[data-vp="xs"] .sp-surface-head { column-gap: 8px !important; }

/* Chrome that is decoration, not function: the wordmark, the build stamp, the
   logo. It is the first thing to go when the row has to wrap, and it is never
   the only place its information lives. */
html[data-vp="xs"] .sp-head-aux,
html[data-vp-h="short"] .sp-head-aux { display: none !important; }


/* ─── 3 · Horizontal strips ────────────────────────────────────────────────
   A row of chips or tabs that cannot wrap (the document-type strip, the
   Production tab bar, the Service inspection tabs) becomes a SCROLLER rather
   than an overflow. Two things make that honest rather than a hidden trap:
   the children must not shrink (a squeezed tab is unreadable AND unreachable),
   and the scrollbar must be visible enough to read as scrollable — on a coarse
   pointer there is no hover to discover it with. */
/* ⚠ TWO THINGS TO KNOW BEFORE YOU PUT THIS CLASS ON A ROW.
   Both of them apply at EVERY width and EVERY pointer type — this class is not
   tier-fenced, because a row that overflows at 1440 overflows at 1440 (the
   Production tab bar wanted 1451px and did exactly that).

   1. `> * { flex-shrink: 0 }` PINS EVERY CHILD AT ITS OWN WIDTH. That is the
      point for tabs and chips — a squeezed tab is unreadable AND unreachable —
      but it is wrong for a row that also holds something meant to fill the
      space, like a `flex: 1` range input: that input gets pinned at its
      min-content width (~129px measured) and the control gets worse everywhere
      in exchange for the scroll. The Colours pane's pose row is exactly that
      shape and WRAPS instead. Reach for wrapping when the row mixes chips with
      a stretchy control; reach for this when the row is chips all the way.
   2. It is a SCROLLER, so whatever it hides is hidden behind an affordance the
      user has to notice. Hence the visible thumb below — on both pointer types,
      not just touch. An overlay scrollbar that only appears on hover is no
      affordance at all for the two Production tabs past the right edge of a
      1440px window, and it is none whatsoever on a phone. */
.sp-tabstrip {
  overflow-x: auto;
  overflow-y: hidden;
  flex-wrap: nowrap !important;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
}
.sp-tabstrip > * { flex-shrink: 0; scroll-snap-align: start; }

/* Always-visible thumb: subtle on a mouse, chunkier on a thumb. */
.sp-tabstrip::-webkit-scrollbar { height: 4px; }
.sp-tabstrip::-webkit-scrollbar-track { background: transparent; }
.sp-tabstrip::-webkit-scrollbar-thumb {
  background: rgba(196, 18, 48, 0.38);
  border-radius: 3px;
}
.sp-tabstrip:hover::-webkit-scrollbar-thumb { background: rgba(196, 18, 48, 0.62); }
html[data-vp-ptr="coarse"] .sp-tabstrip::-webkit-scrollbar { height: 3px; }
html[data-vp-ptr="coarse"] .sp-tabstrip::-webkit-scrollbar-thumb {
  background: rgba(196, 18, 48, 0.55);
}

/* A pane wider than the phone scrolls INSIDE ITSELF rather than widening the
   document. Opt-in by class, deliberately.

   The CRM's twin of this file makes the parent of EVERY table a scroller
   automatically, with `:has(> table)`. That is right there and wrong here, and
   the difference is worth writing down because it looks like an omission:

     • In the CRM a table almost always sits in a card carrying an inline
       `overflow:hidden` for its rounded corners, so the card CLIPS the table
       and the content is genuinely unreachable — the blanket rule is the fix.
     • In CAD the 68 tables live inside the Settings and Production content
       panes, which already declare `overflowY:'auto'`. Per CSS, one axis
       `visible` and the other not is not representable, so those panes' overflow-x
       computes to `auto` too and every wide table is ALREADY reachable by a
       horizontal swipe. The A-668 sweep confirms it: across 152 screen ×
       viewport combinations it reports no unreachable table content anywhere.

   And the blanket rule is not free. `overflow-x: auto` forces `overflow-y` to
   `auto` by the same rule, which CLIPS absolutely-positioned descendants — and
   the panes it would apply to contain a floating 3D preview, tooltips and
   dropdowns. So a rule that fixes nothing measured here would have traded a
   real popover for a hypothetical table.

   Do NOT reach for `table{display:block}` + `thead,tbody{display:table}`
   either: that is two table formatting contexts, and it puts header cells
   hundreds of pixels from the columns they label. Make the PARENT the scroller.
   `!important` because the parent's overflow is usually an inline style. */
html:not([data-vp="xl"]) .sp-scroll-x {
  overflow-x: auto !important;
  -webkit-overflow-scrolling: touch;
}
html:not([data-vp="xl"]) .sp-scroll-x > table {
  width: auto;
  min-width: 100%;
}


/* ─── 4 · Touch targets and fields ─────────────────────────────────────────
   Fenced on the POINTER, not the width: a 1024px tablet needs a thumb-sized
   control and a 400px desktop window does not. `narrow ≠ coarse`. */

/* 44px is the platform guidance; 36 is the floor below which a control is a
   genuine miss on a moving van seat. :where() keeps the specificity at ZERO so
   a component that deliberately sizes a control smaller still wins — in the
   CRM a blanket `min-width:36px` on every button silently overrode a 128px
   segmented control and collapsed four dashboard segments onto each other. */
html[data-vp-ptr="coarse"] :where(#root button, #root [role="button"], #root label) {
  min-height: 36px;
}
html[data-vp-ptr="coarse"][data-vp="xs"] :where(#root button, #root [role="button"]) {
  min-width: 36px;
}

/* 16px is not a taste decision: iOS Safari ZOOMS THE PAGE when a field with a
   smaller font gains focus, and the app never zooms back out. CAD sets 16px
   inline on the fields its React modules own; this catches the ones built as
   injected HTML (the Service flag-part modal, the settings backup panes),
   which cannot take a React inline style. */
html[data-vp-ptr="coarse"] #root :is(input, select, textarea) {
  font-size: max(16px, 1em);
}
/* …except the ones that are genuinely a compact numeric cell in a grid, which
   opt out explicitly. */
html[data-vp-ptr="coarse"] #root :is(input, select, textarea).sp-compact-field {
  font-size: 13px;
}

/* Long identifiers (job numbers, profile SKUs, file names) must break rather
   than prop a row open. `anywhere` would be wrong: it makes min-content ONE
   CHARACTER, so every flex/grid child squeezes to a single glyph — the CRM
   painted a stage tracker one letter per line that way. */
html:not([data-vp="xl"]) #root { overflow-wrap: break-word; }


/* ─── 5 · Short viewports ──────────────────────────────────────────────────
   The Fold's cover screen in LANDSCAPE is 882x344 — 344 pixels TALL. Every
   mobile browser also takes 200-350px of height for the on-screen keyboard.
   A dialog whose primary action sits below the fold is a trap here, because a
   backdrop tap is not a Save.

   The sweep found exactly that twice at 882x344: the New Frame drawer's "Add
   Frame" at y=458 and Check Measure's "Save PDF" at y=401, both on a 344px-tall
   screen. So on a short viewport a sheet's BODY scrolls and its footer is
   pinned inside the sheet — the action is always the bottom row of the visible
   dialog, never a row below it. */
/* The WIDTH half of the sheet problem, and it is not short-viewport-specific.
   A dialog declared width:560 inside a 344px fixed flex overlay does not shrink
   to 344: as a flex item its automatic minimum size is min-width:auto, i.e. its
   own min-content, so it stops there and the wrapper's overflow:hidden CLIPS
   the difference — the excess is unreachable, not one swipe away. maxWidth on
   its own does NOT fix that, because max-width does not beat the automatic
   minimum. So the class carries both, and carries them at every non-xl tier
   rather than leaving each call site to remember: the Price panel's close ✕ was
   12px off a 344px screen for exactly this reason, and four more dialogs in
   modules/39-main-app-33 had the identical shape. */
html:not([data-vp="xl"]) .sp-sheet {
  /* min-width WITH !important, max-width WITHOUT — and the asymmetry is the
     whole point. Removing the automatic-minimum floor is the load-bearing half
     and no call site should be able to lose it to an inline style. The
     max-width is only a DEFAULT: three dialogs in modules/39-main-app-33
     deliberately keep a gutter with an inline maxWidth of 92-94vw, and an
     !important here would have overridden them to edge-to-edge at every narrow
     tier — a change nobody asked for, made by a rule meant to fix something
     else. Without !important the inline value wins, which is the correct
     precedence: the class supplies the floor, the call site keeps its intent. */
  max-width: 100%;
  min-width: 0 !important;
}

/* A clamped floating panel still has to be able to SCROLL. .sp-float-panel caps
   the height at calc(100dvh - 16px), and the panels it applies to carry an
   inline overflow:hidden for their rounded corners — so on the Fold cover screen
   in landscape (882x344) a capped panel silently CLIPS its own lower rows rather
   than scrolling to them, which trades one unreachable control for another. The
   Colours preview grew ~30px taller when its chip row started wrapping, which is
   exactly the amount that makes this bite. */
html:not([data-vp="xl"]) .sp-float-panel {
  overflow-y: auto !important;
  overflow-x: hidden !important;
}

html[data-vp-h="short"] .sp-sheet {
  max-height: 100dvh;
  display: flex;
  flex-direction: column;
}
html[data-vp-h="short"] .sp-sheet-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
html[data-vp-h="short"] .sp-sheet-foot {
  flex: 0 0 auto;
  position: sticky;
  bottom: 0;
  z-index: 2;
}
/* ⚠ WHERE YOU PUT THIS CLASS CHANGES WHAT IT DOES, and one of the two placements
   is imperfect on purpose.

   · As a SIBLING of .sp-sheet-body inside a flex-column card (the New Frame
     drawer, the spanBlock dialog, the Service modals): flex already pins it and
     `bottom:0` is a no-op. This is the clean shape.
   · INSIDE a single scroller that holds header, body and footer together (36
     project-info, 47 generate-POs, the Zip-or-COD prompt): `bottom:0` resolves
     against the SCROLLER'S PADDING BOX, not the card's border box — measured at
     882x344, the footer's bottom lands ~20px above the card's inner edge, so a
     ~20px band of form content shows beneath it. That is chosen over
     restructuring three dialogs into three panes: a 20px band is a cosmetic
     imperfection, and the alternative — the primary action only reachable by
     scrolling to the very bottom on a 344px-tall screen — is not.

   Prefer the sibling shape in new code. If you restructure one of those three,
   the class needs no change; just move it. */
/* Vertical padding is the cheapest height there is to give back. */
html[data-vp-h="short"] .sp-surface-head { row-gap: 4px; }


/* ─── 6 · The hinge ────────────────────────────────────────────────────────
   On a book-style foldable held open the viewport is two segments with a
   physical seam between them. `env(viewport-segment-*)` reports where. A
   full-bleed row that ignores it puts a control, or the middle of a 3D view,
   under the fold — and on the Fold that is a raised ridge, not just a line.

   Progressive: the whole block is inside a media query no non-foldable ever
   matches, so it cannot affect any other device. */
@media (horizontal-viewport-segments: 2) {
  /* Keep the seam clear of anything the user has to hit or read across. */
  html[data-vp-fold="h"] .sp-seam-safe {
    padding-left: max(8px, calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0)));
  }
  /* A two-pane surface splits ON the seam instead of near it, so neither pane
     is bisected. */
  html[data-vp-fold="h"] .sp-split-panes {
    display: grid !important;
    grid-template-columns: env(viewport-segment-width 0 0) 1fr;
  }
}
@media (vertical-viewport-segments: 2) {
  /* Flip-style fold: the seam is horizontal, so a pinned footer must sit in
     the lower segment rather than straddling the crease. */
  html[data-vp-fold="v"] .sp-sheet-foot {
    margin-bottom: max(0px, calc(env(viewport-segment-top 0 1) - env(viewport-segment-bottom 0 0)));
  }
}


/* ─── 7 · Floating panels ──────────────────────────────────────────────────
   Settings → Products → Hardware & milling floats a draggable LIVE PREVIEW
   window at a size chosen in JS. At 344px it was 640px past the right edge and
   its own "— Hide" was the only control on it, so there was no way back. The
   renderer now sizes it from SpartanVP; this clamps whatever it asks for, so a
   future panel cannot reintroduce the same trap. */
html:not([data-vp="xl"]) .sp-float-panel {
  max-width: calc(100vw - 16px) !important;
  max-height: calc(100dvh - 16px) !important;
  left: auto !important;
  right: 8px !important;
  /* NOTE: `top` is deliberately NOT clamped here. The panel's top comes from a
     persisted pos.y, and an !important CSS override would snap it back every
     time the user dragged it on a tablet — CSS cannot tell "parked off screen"
     from "just moved there on purpose". That clamp belongs where pos is
     computed, and it is in _useDragResize (39d-settings-products.js), which
     recomputes it every render so a fold re-clamps live and a drag still
     writes through. */
}
html[data-vp="xs"] .sp-float-panel {
  right: 4px !important;
  max-width: calc(100vw - 8px) !important;
}


/* ─── 8 · Opt-outs ─────────────────────────────────────────────────────────
   A renderer that knows better says so, out loud, in one place. */
.sp-keep-cols   { grid-template-columns: revert !important; }
.sp-keep-nowrap { flex-wrap: nowrap !important; white-space: nowrap !important; }
.sp-wide        { min-width: max-content; }
html:not([data-vp="xl"]) .sp-desktop-only { display: none !important; }
html[data-vp="xl"]       .sp-narrow-only  { display: none !important; }


/* ─── 9 · Print ────────────────────────────────────────────────────────────
   CAD's quotations, check-measure sheets and assembly lists print. None of the
   above may follow them onto paper — a printed page is never "narrow", however
   narrow the screen that asked for it was. */
@media print {
  .sp-tabstrip { overflow: visible !important; }
  .sp-surface-head { display: none !important; }
  .sp-float-panel { display: none !important; }
  .sp-scroll-x { overflow-x: visible !important; }
}
