================================================================================
GATE 7 — RESPONSIVE AUDIT (Next.js/Tailwind standalone)   Ticket #3250
Branch: feat/3250-oms-ui-bug-fixes-chips-s
Run date: 2026-09-01
Result: FAIL (4 unresolved P1)
================================================================================

SKILL RESOLUTION
--------------------------------------------------------------------------------
No slash-command layer was available in this run. All six skills were resolved
via resolution path 3 (read the SKILL.md file directly) from:

    cross-repo-orchestration/skills/nextjs-tailwind-responsive-<name>/SKILL.md

  [components]      path 3 — file read OK
  [desktop]         path 3 — file read OK
  [mobile]          path 3 — file read OK
  [tablet]          path 3 — file read OK
  [typography]      path 3 — file read OK
  [ux-consistency]  path 3 — file read OK

No skill was unreachable. Note that the gate doc points at
`.claude/skills/...` inside the product repo; oms-web has no `.claude/skills`
directory, so the shared copies under cross-repo-orchestration/skills/ were
used. Same text, different location.

INPUT GAP — screen_inventory was <missing>
--------------------------------------------------------------------------------
The [ux-consistency] audit is specified to compare this ticket's screens
against the wider screen inventory. With no inventory supplied it ran in a
reduced mode: the 10 feature screens were compared against EACH OTHER and
against the shared components they all render through. Cross-screen findings
involving screens outside feature_files may therefore be missed. This is a
coverage reduction, recorded here rather than passed off silently.

METHOD — measured, not reasoned
--------------------------------------------------------------------------------
Source review alone cannot settle whether `overflow-x-auto` actually engages,
so layout claims here were measured. The repo's own Tailwind config and
globals.css were compiled (`npx tailwindcss -c ./tailwind.config.ts`) against a
static HTML replica whose markup is copied from the real components
(app/(app)/layout.tsx, Sidebar, Header, ListToolbar, FilterBar, SearchInput,
DataTable, Pagination, RowActions, RepeaterTable, ExpenseForm,
ResetPasswordForm). The replica was measured in headless Chromium
(Playwright, /home/devops/.cache/ms-playwright/chromium-1234) at 16 viewport
widths: 320 360 375 390 430 600 640 768 820 900 1024 1280 1366 1440 1920 2560.

The DB was not seeded, so real routes were not rendered — every authenticated
route redirects to /login against an empty DB. The replica approach measures
layout/CSS behaviour, which is what this gate reviews. It does NOT replace
ui-verify's screenshot-vs-design check.

Two candidate fixes initially measured as no-ops because the candidate classes
were absent from the compiled CSS (Tailwind only emits classes it finds in
`content`). Those runs were discarded and re-measured with the classes
safelisted and their presence in out.css verified first. Only the corrected
numbers appear below.

BREAKPOINT COVERAGE CENSUS (the headline number)
--------------------------------------------------------------------------------
Responsive utilities (sm:/md:/lg:/xl:/2xl:) per file:

  app/(app)/admin/items/page.tsx                          0
  app/(app)/admin/projects/page.tsx                       0
  app/(app)/admin/suppliers/page.tsx                      0
  app/(app)/admin/uom/page.tsx                            0
  app/(app)/admin/users/page.tsx                          0
  app/(app)/expense-summary/[project]/page.tsx            0
  app/(app)/expense-summary/page.tsx                      0
  app/(app)/expense-summary/ExpenseSummaryRowActions.tsx  0
  app/(app)/expenses/page.tsx                             0
  app/(app)/orders/page.tsx                               0
  components/forms/ExpenseForm.tsx                        0
  components/forms/ResetPasswordForm.tsx                  1  lg:grid-cols-[1fr_280px]

Shell + shared components these pages render through:

  app/(app)/layout.tsx        0     components/ui/Pagination.tsx     0
  components/layout/Sidebar   0     components/ui/SearchInput.tsx    0
  components/layout/Header    0     components/ui/RepeaterTable.tsx  0
  components/ui/DataTable     0     components/ui/RowActions.tsx     0
  components/ui/ListToolbar   0     components/ui/AttachmentChip     0
  components/ui/FilterBar     0     app/globals.css                  1  sm:text-sm

ONE responsive utility exists across the entire audited surface. Breakpoints
reviewed (sm 640 / md 768 / lg 1024 / xl 1280) are effectively unimplemented
rather than implemented-and-wrong.

MEASUREMENT 1 — list screen (app shell + ListToolbar + DataTable + Pagination)
--------------------------------------------------------------------------------
  vw     doc.scrollWidth   overflow   main.clientW   DataTable scroll/client
  320    1458              1138       1202           1152/1152  NOT scrolling
  375    1458              1083       1202           1152/1152  NOT scrolling
  430    1458              1028       1202           1152/1152  NOT scrolling
  640    1458               818       1202           1152/1152  NOT scrolling
  768    1458               690       1202           1152/1152  NOT scrolling
  820    1458               638       1202           1152/1152  NOT scrolling
  1024   1458               434       1202           1152/1152  NOT scrolling
  1280   1458               178       1202           1152/1152  NOT scrolling
  1366   1458                92       1202           1152/1152  NOT scrolling
  1440   1458                18       1202           1152/1152  NOT scrolling
  1920   1920                 0       1664           1614/1614
  2560   2560                 0       2304           2254/2254

The document is a CONSTANT 1458px wide from 320px to 1440px. The layout has a
hard floor and does not respond at all below it. DataTable's scrollWidth always
equals its clientWidth — its `overflow-x-auto` never engages at any width.

MEASUREMENT 2 — root-cause counterfactual (adding min-w-0 to layout.tsx:33)
--------------------------------------------------------------------------------
  vw     BEFORE                          AFTER  +min-w-0
  320    doc 1458  table 1152/1152        doc  657  table 1152/14    scrolls
  375    doc 1458  table 1152/1152        doc  657  table 1152/69    scrolls
  768    doc 1458  table 1152/1152        doc  768  table 1152/462   scrolls
  1024   doc 1458  table 1152/1152        doc 1024  table 1152/718   scrolls
  1440   doc 1458  table 1152/1152        doc 1440  table 1152/1134  scrolls

Confirms the root cause. It also shows a SECOND, independent cause: at 320/375
the document still floors at 657px after the fix — that residual is the fixed
256px sidebar plus padding. Both are P1 and neither alone is sufficient.

MEASUREMENT 3 — form screen (ExpenseForm + ResetPasswordForm)
--------------------------------------------------------------------------------
  vw     doc.scrollWidth  overflow   RepeaterTable width   pwgrid columns
  320     975              655        669                  670.656px      (1 col)
  375     975              600        669                  670.656px      (1 col)
  430     975              545        669                  670.656px      (1 col)
  768     975              207        669                  670.656px      (1 col)
  900     975               75        669                  670.656px      (1 col)
  1024   1024                0        718                  400px 280px    (2 col)
  1440   1440                0        766                  760px 280px    (2 col)

ResetPasswordForm's grid switches correctly at lg (1024). ExpenseForm's
RepeaterTable holds a hard ~669px floor with no scroll affordance.

MEASUREMENT 4 — touch targets @375px
--------------------------------------------------------------------------------
  RowActions view icon      28 x 28 px   below minimum
  RowActions edit icon      28 x 28 px   below minimum
  RowActions delete icon    28 x 28 px   below minimum
  Pagination page link      32 x 30 px   below minimum
  FilterBar toggle          40 x 40 px   ok

  Candidate `inline-flex h-11 w-11 items-center justify-center` -> 44 x 44 px
  (measured). Cost: Actions cell content width 100px for three icons.

MEASUREMENT 5 — fix validation, ExpenseForm grid (shell corrected first)
--------------------------------------------------------------------------------
  vw     grid-cols-2 (current)      grid-cols-1 sm:grid-cols-2 (candidate)
  375    field  27.5px              field  71px
  430    field  55px                field 126px
  640    field 160px                field 160px    identical
  768    field 224px                field 224px    identical
  1024   field 352px                field 352px    identical

The candidate is free above 640px and materially better below it.

MEASUREMENT 6 — RepeaterTable scroll wrapper: fixes overflow, breaks dropdowns
--------------------------------------------------------------------------------
  vw     doc before -> after adding overflow-x-auto     computed overflow-y
  320     950 -> 392                                    auto
  375     950 -> 412                                    auto
  768     768 -> 768                                    auto
  1024   1024 -> 1024                                   auto

It does fix the overflow, but computed overflow-y becomes `auto` in every case
— which is precisely the Listbox/DatePicker dropdown-clipping regression that
RepeaterTable.tsx:41-54 documents as the reason the wrapper was removed. So
this is NOT a safe mechanical fix; it needs a real decision. Reported, not
applied.

MEASUREMENT 7 — variable-length table content @1440px
--------------------------------------------------------------------------------
  Suppliers Address cell (whitespace-nowrap, no truncate):  518px of a 1152px table
  With truncate + max-w-[16rem]:                            326px cell, 1134px table

================================================================================
MERGED FINDINGS
================================================================================

--- P1 -------------------------------------------------------------------------

[P1-1] tags: [mobile] [tablet] [desktop] [components] [ux-consistency]
File:    app/(app)/layout.tsx:33 (root cause)
Affects: all 10 list-page feature files
Problem: `<div className="flex flex-1 flex-col">` has no `min-w-0`. A flex item
         defaults to min-width:auto, so this column refuses to shrink below its
         content's intrinsic width. DataTable's `overflow-x-auto` is therefore
         never able to engage, and the whole document scrolls horizontally
         instead of just the table.
Evidence: doc.scrollWidth pinned at 1458px for every viewport 320-1440px;
         DataTable scroll/client 1152/1152 (never scrolls). Adding min-w-0
         returns doc to viewport width at 768/1024/1440 and the table starts
         scrolling internally (1152/462 at 768px). See Measurements 1 and 2.
Why it matters: this is not a mobile-only problem — a 1366px laptop, the single
         most common desktop width, overflows by 92px. Every list screen in the
         ticket is affected. The table-scroll pattern the code already chose is
         silently inert.
Fix:     add `min-w-0` to the flex column wrapper.
Risk:    low, and measured — the layout is unchanged at 1920/2560 where no
         overflow existed.
Fixed:   NO (read-only run)

[P1-2] tags: [mobile] [components]
File:    components/layout/Sidebar.tsx:99-104 (root cause)
Affects: all 12 feature files
Problem: the sidebar is `shrink-0` at a fixed `w-64` (256px) with zero
         responsive variants. There is no hamburger, drawer, or off-canvas
         pattern; it is always rendered inline and never shrinks.
Evidence: 0 responsive utilities in Sidebar.tsx. After P1-1 is fixed, the
         document still floors at 657px at 320/375px — the residual is this
         sidebar. 256px is 80% of a 320px viewport and 68% of a 375px viewport.
Why it matters: the app is unusable on any phone. This is the largest single
         contributor to mobile breakage after P1-1.
Fix:     genuine responsive work, not a utility swap — hide below md and add a
         drawer/overlay with a toggle in Header. The manual collapse toggle that
         exists is a user preference, not a responsive behaviour, and defaults
         to expanded.
Risk:    medium — new interactive surface; needs its own ui-verify pass.
Fixed:   NO (report-only by design — genuine layout work, not a safe auto-fix)

[P1-3] tags: [mobile] [tablet] [components]
File:    components/ui/RepeaterTable.tsx:55 / components/forms/ExpenseForm.tsx:245
Problem: the Expense Items repeater has a hard ~669px minimum width (Type w-56
         224px + Amount w-32 128px + Attachment + remove w-10, all cells
         `whitespace-nowrap`) and deliberately carries no scroll wrapper.
Evidence: form-page doc.scrollWidth constant at 975px from 320px through 900px
         — 600px of overflow at 375px, still 207px at 768px (iPad portrait) and
         75px at 900px. See Measurement 3.
         The comment at RepeaterTable.tsx:52-54 asserts "This table's column set
         is narrow/bounded enough that horizontal overflow isn't a real risk in
         practice." That is measurably false at every width below 1024px.
Why it matters: the Add/Edit Expense form cannot be completed on a phone or an
         iPad in portrait; content is pushed off-viewport with no affordance.
Fix:     NOT the obvious one. Measurement 6 shows `overflow-x-auto` resolves the
         overflow but forces computed overflow-y:auto, reintroducing the
         documented dropdown-clipping regression. Needs either a mobile card
         transformation of the repeater, or a portalled dropdown for
         Listbox/DatePicker so the scroll wrapper becomes safe.
Risk:    high if done naively — the naive fix regresses a previously-fixed bug.
Fixed:   NO (genuine redesign — explicitly out of auto-fix scope)

[P1-4] tags: [mobile] [ux-consistency]
File:    components/forms/ExpenseForm.tsx:207
Problem: `<div className="grid grid-cols-2 gap-4">` for the Date / Project pair
         never stacks. Two columns at every width including 320px.
Evidence: with the shell corrected so the grid itself is what's measured, fields
         are 27.5px wide at 375px and 55px at 430px. See Measurement 5.
         The mobile skill names this case explicitly ("multi-field forms that
         don't stack vertically on mobile").
Consistency angle: ResetPasswordForm.tsx:127 — the other form in this same
         changeset — does this correctly (`grid-cols-1 ... lg:grid-cols-[1fr_280px]`,
         verified switching at 1024px in Measurement 3). Two forms, same ticket,
         opposite treatment. This is the classic "only one viewport was actually
         designed for" pattern the ux-consistency skill exists to catch.
Fix:     `grid grid-cols-1 sm:grid-cols-2 gap-4` — measured identical at 640px
         and above, so there is no tablet/desktop cost.
Risk:    very low; measured no-op above 640px.
Fixed:   NO (read-only run)

--- P2 -------------------------------------------------------------------------

[P2-1] tags: [mobile]
File:    app/(app)/expense-summary/ExpenseSummaryRowActions.tsx:9
         app/(app)/orders/OrderRowActions.tsx:12
         app/(app)/expenses/ExpenseRowActions.tsx:13
         components/ui/RowActions.tsx:33,37,45
         components/ui/RepeaterTable.tsx:99
Problem: every row-action icon control is `p-1.5 -m-1.5` around a `h-4 w-4`
         glyph, giving a 28x28px tappable area — below the ~40px practical
         floor and the 44x44 WCAG 2.5.5 target. RowActions packs three of them
         at gap-3 (12px), so mis-taps are likely.
Evidence: measured 28x28px at 375px for all three RowActions icons
         (Measurement 4). Candidate `inline-flex h-11 w-11 items-center
         justify-center` measures exactly 44x44px.
Note:    ExpenseSummaryRowActions.tsx is NEW in this ticket (#3250 R5) and
         copies the pattern, so the ticket adds a new instance rather than
         inheriting only existing ones.
Risk:    low, but not zero — the Actions cell content grows to 100px for three
         icons, which is a visible table-layout change and should go through
         ui-verify rather than being applied blind.
Fixed:   NO (read-only run)

[P2-2] tags: [mobile]
File:    components/ui/Pagination.tsx:97-110
Problem: numbered page links are `min-w-[2rem] px-2 py-1`, measuring 32x30px —
         below the touch minimum, and rendered as a row of up to 7 adjacent
         small targets.
Evidence: measured 32x30px at 375px (Measurement 4).
Why it matters: introduced by #3247's numbered-pagination work; every list page
         in this ticket renders it.
Fix:     raise to a 40-44px min target at mobile widths.
Fixed:   NO (read-only run)

[P2-3] tags: [tablet]
File:    components/ui/SearchInput.tsx:109-113 with components/ui/ListToolbar.tsx:24
Problem: the expanded search box is a hard `w-80` (320px) and `shrink-0`, with
         no responsive variant. With the 256px sidebar and 48px of main padding,
         the toolbar row needs 624px before the New button and filter toggle are
         accounted for, so on `flex-wrap justify-end` the New button drops to
         its own line through the intermediate range.
Evidence: w-80 + shrink-0 in source; sidebar 256px and p-6 confirmed in
         Measurement 1 (main.clientW). NOTE: this specific wrap point was NOT
         directly measured — the shell bug (P1-1) masks it until P1-1 is fixed.
         Stated as arithmetic, flagged as such rather than presented as a
         measurement.
Fix:     let the expanded width be fluid with a max (e.g. `w-full max-w-xs`)
         below lg.
Fixed:   NO (read-only run)

--- P3 -------------------------------------------------------------------------

[P3-1] tags: [typography] [mobile] [tablet]
File:    components/ui/DataTable.tsx:89 (via components/ui/table.tsx:62)
Problem: every table cell is `whitespace-nowrap` with no `truncate` or
         max-width. Columns holding free-text real-world data — Suppliers
         `addressLine1`, Items/Projects `name`, Orders supplier/project names —
         can grow without bound and drive the table's width.
Evidence: the Suppliers Address cell measured 518px of a 1152px table at 1440px;
         with `truncate max-w-[16rem]` it drops to 326px (Measurement 7).
Fix:     cap and truncate the free-text columns; keep nowrap for dates, amounts,
         status and actions where it is correct.
Fixed:   NO (read-only run)

[P3-2] tags: [typography]
File:    components/ui/AttachmentChip.tsx:44
Problem: the anchor carries `min-w-0` but neither `truncate` nor
         `overflow-hidden`, so `min-w-0` has no effect and a long filename never
         truncates. Sits inside a `whitespace-nowrap` cell in
         expense-summary/[project] and in every ExpenseForm repeater row.
Fix:     add `truncate` (or `overflow-hidden text-ellipsis`) alongside min-w-0,
         with the full name in a title attribute.
Fixed:   NO (read-only run)

[P3-3] tags: [desktop]
File:    app/(app)/layout.tsx:35
Problem: `<main className="flex-1 p-6">` has no max-width. Content stretches to
         the full viewport at any width.
Evidence: main.clientWidth measured 2304px at a 2560px viewport (Measurement 1).
         Expense Summary renders only three columns (Project Name, Total,
         Actions) across that width.
Judgement: acceptable for wide data tables, genuinely poor for the 3-column
         Expense Summary list. Low priority; noted rather than pressed.
Fixed:   NO (read-only run)

[P3-4] tags: [desktop] [tablet]
File:    components/layout/Sidebar.tsx:99-104
Problem: the 256px sidebar is 25% of a 1024px laptop viewport — the
         desktop skill's named "sidebar eats a disproportionate share of a
         smaller laptop" case. Partially mitigated by the manual rail toggle
         (persisted to localStorage), but that defaults to expanded and is a
         user preference rather than a responsive rule.
Fix:     default to the rail variant below xl.
Fixed:   NO (read-only run)

[P3-5] tags: [ux-consistency]
File:    app/(app)/expense-summary/page.tsx
Problem: it is the only list screen of the nine with no <Pagination>. Every
         other list page renders one.
         (items/projects/suppliers/uom/users/expenses/orders/[project] = yes.)
Responsive consequence: an unbounded row count on the narrowest viewports,
         where each row already costs the most vertical space.
Caveat:  this is as much a data-layer decision (expenseSummaryByProject returns
         no `total`) as a responsive one, and is at the edge of this gate's
         scope. Recorded for the record, deliberately kept at P3.
Fixed:   NO (read-only run)

[P3-6] tags: [ux-consistency] [components]
File:    components/ui/FilterBar.tsx:139
Problem: filter fields are fixed `w-[200px]` inside a `flex-wrap` row. They wrap
         rather than overflow, so this degrades gracefully — but with three
         filters (Users, Orders, Expenses) the row is 650px, forcing a
         three-row stack on narrow viewports with no reflow to full-width
         fields, which is the more usual mobile filter treatment.
Judgement: wraps rather than breaks. Genuinely minor.
Fixed:   NO (read-only run)

================================================================================
POSITIVE BASELINE (checked, not defects)
================================================================================
- [desktop] No hover-only interactions. RowActions icons carry resting colours
  (text-text / text-status-info / text-status-danger) by explicit design
  decision in #3248, so nothing is discoverable only on hover. Row `hover:` on
  the table is decorative only. This check passes cleanly.
- [typography] No oversized fixed headings. The largest type on the audited
  surface is HeaderTitle's `text-xl` and the Expense Summary total's `text-2xl`;
  neither overflows or overwhelms a 320px viewport. Per the typography skill's
  own guidance, no responsive type scale is warranted here and none is
  recommended — adding one would be noise.
- [typography] No unbroken-string overflow risk in headings or body copy; the
  variable-length risk is confined to table cells and the attachment chip,
  reported as P3-1/P3-2.
- [mobile/performance] No heavy animation or unoptimised imagery. The
  SearchInput and FilterBar transitions are CSS width/opacity/grid-rows only;
  all iconography is inline SVG via lucide-react.
- [components] ResetPasswordForm.tsx is the one genuinely responsive file in the
  changeset and is correct: measured 1-column below 1024px, `1fr 280px` at and
  above it, with the policy card stacking beneath the fields. It is the model
  the rest of the changeset should follow.
- [components] DataTable chose the right strategy in principle — horizontal
  scroll, not column hiding, so no information is silently dropped on small
  screens. The strategy is sound; it is simply defeated by P1-1.

================================================================================
STEP 3 — AUTO-FIX: NOT PERFORMED
================================================================================
The gate's Step 3 allows fixing clearly-safe findings in place. This node was
invoked with explicit read-only repo access ("You have no write access, by
design"), so no source file was modified. Every finding above is marked
Fixed: NO. This evidence file is the gate's own output artifact, not a source
change.

Two findings would have qualified as safe auto-fixes under a writable run, and
both have measured fix validation ready: P1-1 (`min-w-0`, Measurement 2) and
P1-4 (`grid-cols-1 sm:grid-cols-2`, Measurement 5). P1-2 and P1-3 would NOT
have been auto-fixed even with write access — both are genuine layout redesigns,
and for P1-3 the obvious fix is measurably a regression (Measurement 6).

================================================================================
STEP 5 — REDMINE NOTE: NOT POSTED
================================================================================
REDMINE_URL and TASK_ID are both unset in this environment (an API key is
present, but a key alone cannot address a request). The PUT in the gate spec
could not be constructed. No note was posted and none was faked. Posting is
also an outward-facing action that this read-only node should not take
unprompted. The gate result is carried by this file and the returned JSON.

================================================================================
SUMMARY
================================================================================
Breakpoints reviewed: sm(640) md(768) lg(1024) xl(1280), plus 320/360/375/390/
430 mobile, 600/820/900 intermediate, 1366/1440/1920/2560 desktop.

P1: 4    P2: 3    P3: 6    Total: 13

gate_status: FAIL — 4 unresolved P1 findings.

Bottom line: this is not a set of scattered responsive nits. Exactly one
responsive utility exists across the whole audited surface, and the document
measures a constant 1458px wide from 320px to 1440px. The screens in this
ticket were built at one viewport. Two of the four P1s (P1-1, P1-4) are small,
measured, low-risk changes; the other two (P1-2 sidebar, P1-3 repeater) are real
design work and should be scoped as such rather than patched.
