================================================================================
GATE 7 — RESPONSIVE AUDIT  (W16_gate_responsive_audit)
Ticket : #3248  oms-ui-ux-and-functional
Branch : feat/3248-oms-ui-ux-and-functional  @ e8fb724
Stack  : nextjs-tailwind-standalone (oms-web)
Result : FAIL  —  3 P1, 5 P2, 4 P3
================================================================================

--------------------------------------------------------------------------------
0. SKILL RESOLUTION  (gate file "Skill resolution", steps 1-4)
--------------------------------------------------------------------------------
All six audit skills were searched for through all three resolution paths:

  1. /nextjs-tailwind-<name>                      -> no slash-command layer present
  2. /<name>  (repo's own tuned copy)             -> no .claude/ directory in oms-web
  3. read_file(".claude/skills/nextjs-tailwind-<name>/SKILL.md")

  scanned, per skill, for:
    ./.claude/skills/nextjs-tailwind-responsive-<name>-audit/SKILL.md   NOT FOUND
    ./.claude/skills/responsive-<name>-audit/SKILL.md                   NOT FOUND
    /opt/drcarrot-deploy/.claude/skills/nextjs-tailwind-...             NOT FOUND

  Repo-wide: `find /opt/drcarrot-deploy -name SKILL.md -path "*responsive*"` -> 0 hits.
  The only skills/ trees on this host are /opt/drcarrot-deploy/.claude/skills,
  .../dr.carrot-v3-react/.claude/skills and .../emr_v1_l12/.claude/skills; none
  contains a responsive-audit skill.

  => All six skills UNREACHABLE. Per gate step 4 this is reported, not skipped
     silently. Each audit below was executed directly from the gate file's own
     one-line definition of that audit (gate lines 27-38), against the real
     files, with real measurements. No audit was dropped.

  screen_inventory input was <missing>. The [ux-consistency] audit therefore
  compares screens against each other within feature_files (the Add/Edit form
  family, the List family) rather than against an external inventory. Noted as a
  scope reduction, not a pass.

--------------------------------------------------------------------------------
1. METHOD — measured, not reasoned
--------------------------------------------------------------------------------
Live-server route was attempted first and abandoned honestly:
  - Next dev server up on 127.0.0.1:3947 (HTTP 200 /login).
  - Login with .env SUPER_ADMIN_* -> "Invalid username or password."
  - Cause: `prisma.user.findMany()` returns [] — the database is EMPTY.
    Seeding is a write; this node runs read-only, so the live route was dropped
    rather than worked around.

Substitute (equal or better fidelity), all of it real, none of it hand-mocked:
  - Stylesheet: rebuilt from source with the project's own Tailwind CLI —
      ./node_modules/.bin/tailwindcss -c tailwind.config.ts -i app/globals.css
    (26,890 bytes; matches the .next build output size class).
  - Markup: the REAL components imported and rendered through
    react-dom/server (esbuild bundle, next/link + next/navigation stubbed):
      DataTable, StatusBadge, PageHeader, ListToolbar, FilterBar, Pagination,
      RowActions, SearchInput, Sidebar, Modal, SupplierForm, ItemForm, OrderForm
    inside the verbatim app shell from app/layout.tsx:38-41.
    Column sets copied verbatim from app/orders/page.tsx:27-35 and
    app/admin/users/page.tsx:38-45. These are the shipped components, not replicas.
  - Geometry read with Playwright/Chromium via getBoundingClientRect() +
    getComputedStyle() at 8 viewports: 320, 375, 414, 640, 768, 1024, 1280, 1920.
  - 5 screens x 8 viewports = 40 measured rows -> /tmp/w16b/measurements.json

Breakpoints reviewed: sm(640) md(768) lg(1024) xl(1280), plus 320/375/414/1920.

--------------------------------------------------------------------------------
2. AGGREGATE FACT THAT DRIVES MOST OF THIS REPORT
--------------------------------------------------------------------------------
  $ grep -rnoE '\b(md|lg|xl|2xl):[a-z-]+' app components | wc -l
  0

  ZERO md:, lg:, xl: or 2xl: utilities exist anywhere in app/ or components/.
  The entire responsive vocabulary of this ticket is two `sm:` usages:
      app/globals.css:28              sm:text-sm     (in .field-input/.field-control)
      components/ui/fields/TextField  sm:w-[350px]   (via each form's fieldWidth)

  Everything else adapts only as an accident of flexbox, never by intent.

--------------------------------------------------------------------------------
3. MEASURED DATA (excerpt — full set in /tmp/w16b/measurements.json)
--------------------------------------------------------------------------------
screen              vw  aside    main   inner  docScrollW  tablePaneHiddenPx
orders-list        320    256  1288.1  1240.1        1544                  0
orders-list        375    256  1288.1  1240.1        1544                  0
orders-list        768    256  1288.1  1240.1        1544                  0
orders-list       1024    256  1288.1  1240.1        1544                  0
orders-list       1280    256  1288.1  1240.1        1544                  0
orders-list       1920    256    1664    1616        1920                  0
users-list         320    256  1051.8  1003.8        1308                  0
users-list        1280    256  1051.8  1003.8        1308                  0
supplier-new       320    256   281.1   233.1         537                  0
supplier-new      1024    256     768     720        1024                  0
order-new          320    256   551.5   503.5         808                  0
order-new         1024    256     768     720        1024                  0

  Read the docScrollW column against vw: the document is wider than the
  viewport at every breakpoint up to and including 1280 on both list screens.
  tablePaneHiddenPx is 0 everywhere — DataTable's own overflow-x-auto is
  carrying nothing. Both facts have the same single root cause (F1).

--------------------------------------------------------------------------------
4. FINDINGS
--------------------------------------------------------------------------------

### F1 — P1 — [components][mobile][tablet][desktop]
`<main>` is a flex child with no `min-w-0`, so DataTable's `overflow-x-auto`
is inert and the whole PAGE scrolls sideways instead of the table.

  Site      : app/layout.tsx:40  `<main className="flex-1 p-6">`
  Collateral: components/ui/DataTable.tsx:42 `<div className="card overflow-x-auto">`
              — present, correct, and never able to fire.

  A flex item defaults to min-width:auto, so `flex-1` cannot shrink below its
  content's intrinsic width. DataTable is `min-w-full` + `whitespace-nowrap`
  (DataTable.tsx:43,53,82), so <main> is forced to the table's natural width and
  the overflow escapes to the document.

  Measured, orders-list: main = 1288.1px at EVERY viewport 320 -> 1280.
  docScrollW pinned at 1544px. At 1280 (a normal laptop) the page still scrolls
  horizontally by 264px. This is not a mobile-only defect.

  Proven fix (applied in-browser, re-measured, not assumed) — adding `min-w-0`
  to <main>:
      vw= 375  docSW 1544 -> 782   main 1288.1 -> 119    panePinned 0 -> 1169
      vw= 768  docSW 1544 -> 778   main 1288.1 -> 512    panePinned 0 ->  776
      vw=1024  docSW 1544 -> 1024  main 1288.1 -> 768    panePinned 0 ->  520
      vw=1280  docSW 1544 -> 1280  main 1288.1 -> 1024   panePinned 0 ->  264
  At 1024 and 1280 docScrollW becomes exactly the viewport width — page-level
  horizontal scroll gone, table scrolls inside its own card as designed.
  Residual overflow at 375/768 is F2, not this.

### F2 — P1 — [mobile][components]
Sidebar has no mobile variant; it is 256px at 320px wide.

  Site: components/layout/Sidebar.tsx:71-74
        clsx("shrink-0 ...", collapsed ? "w-16" : "w-64")

  Measured asideW = 256.0 at all 8 viewports incl. 320. `collapsed` is React
  state seeded from localStorage (Sidebar.tsx:47-55) — a user preference, not a
  viewport response — and defaults to false on first paint on every device.

  At 320px the nav consumes 80% of the screen. Even after F1's min-w-0 fix
  <main> is left 119px at vw=375 (measured above). There is no off-canvas
  drawer, no `hidden md:flex`, no hamburger. Below `md` the app has no usable
  content column.

  Not auto-fixable: this is the layout redesign the gate's step 3 explicitly
  says to report rather than guess at.

### F3 — P1 — [mobile][tablet][components]
`grid grid-cols-2` / `grid-cols-4` with no breakpoint prefix — never reflows.

  components/forms/OrderForm.tsx:119            grid grid-cols-2 gap-4
  components/forms/ExpenseForm.tsx              grid grid-cols-2 gap-4
  components/forms/OrderFulfillmentForm.tsx     grid grid-cols-4 gap-x-6 gap-y-4

  Measured gridTemplateColumns on order-new:
      vw= 320   "243.766px 243.766px"   (docScrollW 808 — 2.5x the viewport)
      vw= 375   "243.766px 243.766px"
      vw= 768   "243.766px 243.766px"
      vw=1024   "352px 352px"
      vw=1920   "376px 376px"

  Two side-by-side inputs at 243.8px each on a 320px phone; the 4-column
  fulfillment grid is worse. Needs `grid-cols-1 md:grid-cols-2`
  (and `sm:grid-cols-2 lg:grid-cols-4` for fulfillment).

### F4 — P2 — [tablet][desktop]
The two-column field layout only engages at ~1078px — iPad landscape gets one column.

  Sites: SupplierForm.tsx:50,59 / ItemForm.tsx:56,64 / UomForm.tsx:41,49 /
         ProjectForm.tsx:40,48 / UserForm.tsx:56,63
         `flex flex-wrap gap-x-6 gap-y-4 max-w-[772px]` + `w-full sm:w-[350px]`

  Two fields need 350 + 24(gap-x-6) + 350 = 724px of container. Measured
  container width vs. fields-per-row on supplier-new:
      vw=1000 -> 646px -> 1 per row
      vw=1024 -> 670px -> 1 per row      <-- iPad landscape
      vw=1032 -> 678px -> 1 per row
      vw=1040 -> 686px -> 1 per row
      vw=1100 -> 746px -> 2 per row      <-- threshold ~1078px

  At 1024 the container is 720px — 4px short of the 724px needed. The design's
  two-column form silently degrades to a single 350px column with ~320px of
  dead space beside it on every tablet and on 1024-wide laptops.

### F5 — P2 — [ux-consistency]
Two different, contradictory responsive strategies inside one screen family.

  Add/Edit forms split cleanly in two:
    reflows      (flex flex-wrap + sm:w-[350px]) :
        SupplierForm, ItemForm, UomForm, ProjectForm, UserForm
    never reflows (grid grid-cols-2, no prefix)  :
        OrderForm, ExpenseForm

  Same family, same user task, opposite behavior at the same viewport: at 375px
  a Supplier field is one full-width column while an Order field is two 243.8px
  columns. Whichever is correct, both should not ship. This is exactly the
  inconsistency the [ux-consistency] audit is defined to catch (gate line 37).

### F6 — P2 — [mobile]
Interactive targets below touch minimums; some below the WCAG 2.5.8 AA floor.

  Measured on orders-list @375 (CSS px, live rects):
      28.0 x 28.0   View / Edit / Delete row actions   RowActions.tsx:41,47,55
      32.0 x 30.0   pagination page numbers            Pagination.tsx:104-113
      35.8 x 30.0   pagination page numbers (2-digit)  Pagination.tsx:104-113
      56.5 x 30.0   Prev  /  58.6 x 30.0  Next         Pagination.tsx:96,120
      40.0 x 40.0   Filters toggle                     FilterBar.tsx:54
      78.3 x 20.0   sortable column header link        DataTable.tsx:56-62
      48.5 x 20.0   sortable column header link        DataTable.tsx:56-62
     121.8 x 16.0   row link (order no)                app/orders/page.tsx:28

  - 44x44 (WCAG 2.5.5 AAA / iOS-Android guidance): everything above fails.
  - 24x24 (WCAG 2.5.8 AA, WCAG 2.2): the 20px-tall column-header links and the
    16px-tall row links FAIL. The 28x28 row actions pass AA but not AAA.
  The 20/16px heights are the ones worth treating as a real accessibility defect
  rather than a comfort issue.

### F7 — P2 — [mobile]
Modal spans edge-to-edge below 414px — no gutter.

  Site: components/ui/Modal.tsx:33-38
        wrapper `fixed inset-0 z-50 flex items-center justify-center` (no padding)
        dialog  `relative card shadow-lg w-full p-5` + max-w-sm/max-w-lg

  Measured (quick-create-modal, real Modal + real embedded ItemForm):
      vw=320  dialog w=320.0  left=0     <-- full bleed, border+radius clipped
      vw=375  dialog w=375.0  left=0     <-- full bleed
      vw=414  dialog w=414.0  left=0     <-- full bleed
      vw=640  dialog w=512.0  left=64
  `max-w-lg` (512px) never binds below 512px, so `w-full` wins and the card's
  rounded border sits flush against both screen edges. Wrapper needs `p-4`.

  DISPROVED, and therefore NOT reported as a finding: I hypothesised the same
  modal would clip vertically (no max-height, no overflow-y-auto, centred in a
  fixed inset-0 parent). Tested at 812x375, 740x360 and 375x667 landscape/short
  viewports — tallest real content (ItemForm) measures 345-349px and fits every
  time; clipsAbove=false, clipsBelow=false in all three. The risk is latent for
  future taller content but is not a defect today.

### F8 — P2 — [components]
FilterBar fields are a hard `w-[200px]`, not fluid.

  Site: components/ui/FilterBar.tsx:68  `<div className="w-[200px]">`

  Four filter fields on Orders (page.tsx:57-70) at a fixed 200px + gap-[25px]
  (FilterBar.tsx:66) = 875px of intrinsic width in a `flex-wrap` row. Wraps
  rather than shrinks, and at mobile widths contributes the residual overflow
  that survives F1's fix. Should be `w-full sm:w-[200px]` or a min-w-0 basis.

### F9 — P3 — [typography]
No responsive type scale exists.

  Measured h1 font-size, orders-list and supplier-new, all 8 viewports:
      320 -> 20px    375 -> 20px    414 -> 20px    640 -> 20px
      768 -> 20px   1024 -> 20px   1280 -> 20px   1920 -> 20px
  Distinct rendered sizes across the whole surface: 14px/20px, 16px/24px,
  20px/28px. Nothing scales. PageHeader.tsx:13 is a flat `text-xl`.

  Held at P3 deliberately: a dense admin/data UI legitimately uses a fixed
  scale, and a 20px h1 is not broken at 1920. Flagged because the [typography]
  audit is defined as "type scale that doesn't adapt across breakpoints"
  (gate line 35) and the answer here is a measured, unambiguous "it doesn't".

  Counter-note, in the implementation's favour: `.field-input`/.field-control
  carry `sm:text-sm` (globals.css:28,38), i.e. 16px below 640px and 14px above.
  That is the correct idiom — 16px inputs prevent iOS auto-zoom on focus.
  Measured 16px/24px on supplier-new at mobile widths. Working as intended.

### F10 — P3 — [tablet]
The `md` breakpoint is entirely unimplemented.

  0 occurrences of md: in app/ + components/ (section 2). Consequence, measured:
  every geometry on every screen at vw=768 is byte-identical to vw=320 —
  orders-list main 1288.1 / docSW 1544 at both; order-new grid
  "243.766px 243.766px" at both. Tablet portrait renders the mobile layout with
  no adaptation whatsoever. This is the aggregate cause behind F1/F2/F3 rather
  than a separate defect site, recorded because tablet is the breakpoint this
  gate calls out as most often left untested (gate lines 33-34).

### F11 — P3 — [desktop]
List screens have no max-width; content stretches unbounded.

  Measured orders-list at 1920: main 1664px, inner content 1616px, and the
  6 data columns stretch to fill it. Forms are bounded (max-w-[772px],
  OrderForm max-w-3xl) but list screens are not, so row scan-distance grows
  without limit on wide monitors.

### F12 — P3 — [desktop][mobile]
Collapsed sidebar rail depends on a hover tooltip for its labels.

  Site: components/layout/Sidebar.tsx:113  `title={collapsed ? item.label : undefined}`
  In the collapsed (w-16) rail the link's only visible content is an
  aria-hidden icon; the label exists solely as a `title` tooltip. Accessible
  name is fine (title is the accname fallback), but a touch user has no hover,
  so a collapsed rail on a touch device is unlabelled icons. Non-hover fallback
  is what the [desktop] audit asks for (gate line 30).

--------------------------------------------------------------------------------
5. TALLY
--------------------------------------------------------------------------------
  P1 : 3   (F1 F2 F3)
  P2 : 5   (F4 F5 F6 F7 F8)
  P3 : 4   (F9 F10 F11 F12)

  By audit tag (findings carry multiple tags; counted once each per gate step 2):
    [components]     F1 F2 F3 F8
    [desktop]        F1 F4 F11 F12
    [mobile]         F1 F2 F3 F6 F7 F12
    [tablet]         F1 F3 F4 F10
    [typography]     F9
    [ux-consistency] F5

  gate_status = FAIL — 3 unresolved P1 findings (gate line 85: PASS requires zero).

--------------------------------------------------------------------------------
6. AUTO-FIX (gate step 3) — NOT APPLIED
--------------------------------------------------------------------------------
The gate's step 3 asks for safe fixes to be applied directly. This node was
invoked with explicit read-only scope ("You have no write access, by design"),
which overrides that step for this run. Every finding is therefore reported with
fixed=false. No source file was modified; the only write performed is this
evidence file, which the returns contract requires.

Had writes been permitted, F1 (add `min-w-0` to <main>) was the one fix already
verified end-to-end by re-measurement (section 4, F1) and would have been safe to
apply. F3 and F8 are mechanical prefix additions but were not re-measured post-fix
and should not have been applied blind. F2 is a redesign — report only, per step 3.

--------------------------------------------------------------------------------
7. REPRODUCTION
--------------------------------------------------------------------------------
  ./node_modules/.bin/tailwindcss -c tailwind.config.ts -i app/globals.css \
      -o /tmp/w16b/app.css --minify
  node /tmp/w16b/entry.cjs        # render real components -> /tmp/w16b/out/*.html
  node /tmp/w16b/measure.cjs      # 5 screens x 8 viewports -> measurements.json
  node /tmp/w16b/verify.cjs       # F1 min-w-0 proof + F7 clipping disproof
  node /tmp/w16b/grid.cjs         # F3 grid track widths
  node /tmp/w16b/thresh.cjs       # F4 two-column threshold sweep

  Harness lives outside the checkout (/tmp/w16b) so the repo stays clean.
================================================================================
