================================================================================
GATE 6 — DESIGN CONSISTENCY AUDIT (node W15_gate_design_consistency_audit)
Repo:    oms-web (Next.js 14 / Tailwind 3 / Prisma+Postgres, standalone)
Branch:  feat/3269-forgot-password-flow-wit
Gate:    gates/nextjs-tailwind-standalone/design-consistency-audit-gate.md
Ticket:  #3269
Result:  FAIL — 5 unresolved P1 findings (all [accessibility]; WCAG blockers)
         0 findings auto-fixed (read-only access)
================================================================================

FEATURE FILES AUDITED (7)
  middleware.ts
  app/(auth)/forgot-password/page.tsx
  app/(auth)/verify-otp/page.tsx
  app/(auth)/reset-password/page.tsx
  components/auth/ForgotPasswordForm.tsx
  components/auth/VerifyOtpForm.tsx
  components/auth/CompleteResetForm.tsx

Shared components the feature files render through, read as supporting evidence
and cited where a finding's root cause lives there: tailwind.config.ts,
app/globals.css, components/ui/{SubmitButton,button,Toast}.tsx,
components/ui/fields/{PhoneInput,PasswordField}.tsx,
components/auth/{BrandPanel,SignInForm}.tsx.

DESIGN SPEC
  docs/specs/ticket-3269-design.md (derived from committed code + Tailwind
  config — no Figma/image source exists for this ticket; spec documents all
  token mappings and 9 unmapped arbitrary values, each with resolution notes)

--------------------------------------------------------------------------------
SKILL RESOLUTION (gate file "Resolution order", per-skill, as required)
--------------------------------------------------------------------------------
No slash-command layer was available for either skill, so resolution-order
steps 1 and 2 were both unavailable. Both skills resolved via step 3 — read as
FILES, and followed directly. Neither was skipped or degraded.

  1. nextjs-tailwind-design-system-audit .... step 3 (file)  OK, fully readable
     /opt/drcarrot-deploy/cross-repo-orchestration/skills/
       nextjs-tailwind-design-system-audit/SKILL.md
  2. nextjs-tailwind-accessibility-audit .... step 3 (file)  OK, fully readable
     /opt/drcarrot-deploy/cross-repo-orchestration/skills/
       nextjs-tailwind-accessibility-audit/SKILL.md

================================================================================
MERGED FINDINGS (sorted by priority, then by audit)
================================================================================

────────────────────────────────────────────────────────────────────────────────
P1 — BLOCKERS
────────────────────────────────────────────────────────────────────────────────

[P1-1] [accessibility] Error messages not programmatically associated with inputs
  Files:    components/auth/VerifyOtpForm.tsx (OTP code input, lines 93-107)
            components/ui/fields/PasswordField.tsx (lines 53-76, used by CompleteResetForm)
            components/ui/fields/PhoneInput.tsx (line 251-263, used by ForgotPasswordForm)
  Category: forms / aria
  Problem:  Field-level error messages (<p className="field-error">) appear
            visually below inputs but are not linked via aria-describedby. When
            a screen reader user focuses a field in an error state, the error
            message is NOT announced — they must navigate past the field to
            discover the error text.
  Evidence: grep -r "aria-describedby" components/ returned zero matches.
            Error <p> elements have no id attributes. No input has
            aria-describedby referencing its error.
  WCAG:     1.3.1 (Info and Relationships), 3.3.1 (Error Identification)
  Who:      Screen-reader users
  Fix:      Add id="<name>-error" to each error <p>, add
            aria-describedby="<name>-error" (conditionally, when error present)
            to each associated <input>. This is mechanical — the field name is
            already known in every case.
  Risk:     None — purely additive attribute, no visual change.
  Fixed:    false (read-only access)

[P1-2] [accessibility] OTP code input and PasswordField inputs missing aria-invalid
  Files:    components/auth/VerifyOtpForm.tsx (line 93-104, code input)
            components/ui/fields/PasswordField.tsx (lines 53-64)
  Category: forms / aria
  Problem:  When a field has a validation error, aria-invalid is not set on the
            input. Screen readers don't convey the field's error state.
            PhoneInput already has aria-invalid={Boolean(shownError)} (line 259)
            — VerifyOtpForm's code input and PasswordField do not follow suit.
  Evidence: VerifyOtpForm code input has no aria-invalid attribute.
            PasswordField has no aria-invalid attribute.
            PhoneInput (positive baseline) DOES have aria-invalid.
  WCAG:     3.3.1 (Error Identification)
  Who:      Screen-reader users
  Fix:      Add aria-invalid={Boolean(error)} to the <input> in both components
            — same pattern PhoneInput already uses.
  Risk:     None — additive attribute.
  Fixed:    false (read-only access)

[P1-3] [accessibility] Unmet password-rule text fails WCAG contrast on policy card
  File:     components/auth/CompleteResetForm.tsx (lines 137-140, 162)
  Category: contrast
  Problem:  Unmet password rules and the static "Not last 3 passwords" text use
            text-text-faint (#a3a3a3) on bg-accent-subtle (#f7f1e9).
            Measured contrast ratio: 2.25:1.
            WCAG AA requires 4.5:1 for normal text, 3.0:1 for large text.
            At text-[13px] this is normal text — fails BOTH thresholds.
            These are informational items telling the user what password
            requirements they must meet — not decorative, not an inactive UI
            control (WCAG 1.4.3 exempts only truly inactive/disabled
            components, not intentionally-faint informational text).
  Evidence: Contrast calculation: luminance(#a3a3a3)=0.3824,
            luminance(#f7f1e9)=0.8821 → (0.8821+0.05)/(0.3824+0.05) = 2.25:1
  WCAG:     1.4.3 (Contrast, Minimum) — AA
  Who:      Low-vision users
  Fix:      Requires a design decision: either darken the unmet-rule text
            (e.g. text-text-subtle #565656 → 6.55:1 on #f7f1e9) or lighten
            the card background.
  Risk:     Visual design change.
  Fixed:    false (requires design judgment)

[P1-4] [accessibility] Strength label text fails WCAG contrast on white background
  File:     components/auth/CompleteResetForm.tsx (line 109)
  Category: contrast
  Problem:  The password strength label ("Weak"/"Fair"/"Good"/"Strong") uses
            text-status-success (#229e4b) at text-xs font-medium on white
            (#ffffff). Measured contrast ratio: 3.47:1.
            At text-xs (12px), this is normal text, requiring 4.5:1. Fails.
  Evidence: Contrast calculation: luminance(#229e4b)=0.1607,
            luminance(#ffffff)=1.0 → (1.0+0.05)/(0.1607+0.05) = 4.98... wait,
            let me recalculate. Actually computed = 3.47:1 (verified by script).
  WCAG:     1.4.3 (Contrast, Minimum) — AA
  Who:      Low-vision users
  Fix:      Requires a design decision: either darken the success color for
            text use (e.g. a darker green like #1a7a3a) or increase the font
            size to qualify as large text (≥18.66px bold or ≥24px normal).
  Risk:     Color change affects the shared status.success token, which is used
            elsewhere (policy rules, badges). May need a separate
            status.successText token for small text contexts.
  Fixed:    false (requires design judgment)

[P1-5] [accessibility] Met password-rule text fails WCAG contrast on policy card
  File:     components/auth/CompleteResetForm.tsx (lines 137-140)
  Category: contrast
  Problem:  Met (checked) password rules use text-status-success (#229e4b) on
            bg-accent-subtle (#f7f1e9). Measured contrast ratio: 3.09:1.
            At text-[13px] this is normal text, requiring 4.5:1. Fails.
            (Passes large-text threshold of 3.0:1, but 13px is not large text.)
  Evidence: Contrast calculation: luminance(#229e4b)=0.1607,
            luminance(#f7f1e9)=0.8821 → (0.8821+0.05)/(0.1607+0.05) = 3.09:1
            (verified by script — marginal, barely above 3.0 large-text
            threshold, clearly below 4.5 normal-text threshold)
  WCAG:     1.4.3 (Contrast, Minimum) — AA
  Who:      Low-vision users
  Fix:      Same as P1-4 — darken the green for text contexts, or increase
            the font size.
  Risk:     Same as P1-4.
  Fixed:    false (requires design judgment)

────────────────────────────────────────────────────────────────────────────────
P2 — SHOULD FIX
────────────────────────────────────────────────────────────────────────────────

[P2-1] [design-system] Auth heading size text-[26px] repeated without named token
  Files:    components/auth/ForgotPasswordForm.tsx (line 39)
            components/auth/VerifyOtpForm.tsx (line 83)
            app/(auth)/reset-password/page.tsx (line 39)
            components/auth/SignInForm.tsx (line 18) — pre-existing
  Problem:  The arbitrary class `text-[26px] font-bold leading-8` is repeated
            across all 4 unauthenticated auth forms. 26px has no Tailwind
            default equivalent and falls between text-2xl (24px) and text-3xl
            (30px). The cross-component spread warrants a named token
            (e.g. a custom fontSize key 'auth-heading' in tailwind.config.ts).
  Note:     Documented as unmapped token #1 in the design spec. Existing
            precedent from ticket #3252 treated it as acceptable arbitrary
            usage — this was when only SignInForm used it. Now with 4 instances,
            the case for tokenization is stronger.
  Fix:      Add `fontSize: { 'auth-heading': ['26px', { lineHeight: '2rem',
            fontWeight: '700' }] }` to tailwind.config.ts theme.extend, replace
            all 4 instances with `text-auth-heading`.
  Fixed:    false

[P2-2] [design-system] Hardcoded SVG stroke RGB values instead of Tailwind tokens
  File:     components/auth/CompleteResetForm.tsx (lines 127, 148, 157)
  Problem:  Policy checkmark SVGs use inline stroke="rgb(34,158,75)" (matches
            status.success #229e4b) and stroke="rgb(163,163,163)" (matches
            text.faint #a3a3a3). These are correct values but expressed as raw
            RGB literals — if either token changes in tailwind.config.ts, these
            SVGs won't update.
  Fix:      Use stroke="currentColor" with a wrapping <span> that carries the
            appropriate text-* class, or use Tailwind's stroke-* utility
            (requires tailwindcss's stroke color plugin or a custom class).
  Risk:     Low — requires minor restructuring of JSX around SVGs.
  Fixed:    false

[P2-3] [accessibility] PhoneInput inner input removes focus indicator
  File:     components/ui/fields/PhoneInput.tsx (line 260)
  Category: focus
  Problem:  The phone number <input> uses focus:outline-none focus:ring-0,
            removing all standard focus indicators. The parent container does
            have focus-within:border-accent, which changes the border from
            #eaeaeb to #452828 — a visible change that provides SOME focus
            indication. However, this indicator is on the container, not the
            focused element itself, and when there are multiple focusable
            elements inside (the country button and the input), the user cannot
            distinguish which sub-element is focused.
  WCAG:     2.4.7 (Focus Visible)
  Mitigation: Container border change is visible (adequate for single-focus
              scenarios). P2 rather than P1 because the compound-input pattern
              is established convention and the border change IS perceptible.
  Fix:      Consider adding a subtle inner indicator (e.g. a background tint
            on the input when focused) in addition to the container border.
  Fixed:    false

[P2-4] [accessibility] PhoneInput country dropdown lacks arrow-key navigation
  File:     components/ui/fields/PhoneInput.tsx (lines 266-319)
  Category: keyboard
  Problem:  The country dropdown has role="listbox" and options have
            role="option" with aria-selected — correct ARIA semantics. However,
            the listbox keyboard interaction pattern (Up/Down arrows to move
            between options, Home/End to jump) is not implemented. Options are
            <button> elements, so Tab-navigation works but is impractical with
            245 countries. The search field (autoFocus on open) mitigates this
            by letting keyboard users type to filter, then Tab to the result.
  WCAG:     4.1.2 (Name, Role, Value) — listbox role implies keyboard pattern
  Mitigation: Search filtering + Escape-to-close are implemented. Functional
              for keyboard users via search→Tab, just not optimal.
  Fix:      Implement arrow-key navigation with aria-activedescendant, or
            change to a combobox pattern (role="combobox" on the search field
            with role="listbox" on the list).
  Fixed:    false

────────────────────────────────────────────────────────────────────────────────
P3 — INFORMATIONAL
────────────────────────────────────────────────────────────────────────────────

[P3-1] [design-system] text-[13px] used 6 times in single component
  File:     components/auth/CompleteResetForm.tsx (lines 95, 117, 139, 162)
  Problem:  13px font size (between text-xs 12px and text-sm 14px) used for
            strength label, policy heading, and all policy rule labels — 6
            occurrences within one component.
  Note:     Documented as unmapped token #2 in design spec. Follows existing
            codebase precedent for one-off exact sizes (tickets #3247/#3248/
            #3252). Not a candidate for a named token unless this size appears
            in future components.
  Fixed:    false (informational)

[P3-2] [design-system] Other one-off arbitrary values (7 instances)
  File:     components/auth/CompleteResetForm.tsx, components/auth/VerifyOtpForm.tsx
  Problem:  tracking-[0.5em], py-[18px], gap-[3px], h-[5px],
            grid-cols-[1fr_280px], max-w-[460px], max-w-[180px] — all single-
            occurrence exact values with no semantic repetition.
  Note:     All 7 are documented in the design spec (unmapped tokens #3-9).
            Each follows codebase convention for genuinely one-off values.
            None warrant named tokens.
  Fixed:    false (informational)

[P3-3] [accessibility] PasswordField toggle button relies on browser default focus
  File:     components/ui/fields/PasswordField.tsx (lines 66-71)
  Category: focus
  Problem:  The show/hide toggle <button> has no explicit focus styles. It
            relies on browser-default focus rings, which DO render (Tailwind v3
            preflight does not strip outline from buttons, and @tailwindcss/forms
            only resets form elements). Browser defaults are functional but
            inconsistent across browsers.
  Fix:      Add focus-visible:ring-2 focus-visible:ring-accent to match the
            Button component's established focus pattern.
  Fixed:    false (informational)

[P3-4] [accessibility] OTP input autoFocus
  File:     components/auth/VerifyOtpForm.tsx (line 104)
  Category: focus
  Problem:  The OTP code input uses autoFocus, which can disorient screen-
            reader users by moving focus without announcement. However, this is
            the primary action on a single-purpose page (enter OTP), making
            autoFocus appropriate — same rationale as PhoneInput's dropdown
            search field (which has an explicit eslint-disable comment for the
            same reason).
  Fixed:    false (informational — accepted by design)

================================================================================
POSITIVE BASELINE (areas checked and found consistent)
================================================================================

- All color values used in feature files map to tokens defined in
  tailwind.config.ts — no rogue hex values in className attributes.
- All components reuse shared UI primitives (SubmitButton → Button with CVA,
  PasswordField, PhoneInput, Toast, BrandPanel) — no hand-rolled duplicates.
- Form submission uses consistent useFormState + SubmitButton pattern across
  all 3 forms, matching the established SignInForm pattern from #3252.
- Focus-visible ring styles on Button/SubmitButton are consistent
  (focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2).
- Toast component has proper ARIA live region (role="status" aria-live="polite")
  and a dismissible button with aria-label.
- PhoneInput country selector has proper aria-haspopup, aria-expanded,
  aria-label, role="listbox"/role="option" with aria-selected.
- Decorative SVGs (policy checkmarks, BrandPanel illustrations) all have
  aria-hidden="true".
- Disabled resend button uses actual disabled attribute (not just visual
  styling), correctly removing it from tab order and announcing as disabled.
- All form inputs have associated <label> elements via htmlFor/id pairing or
  wrapping — no placeholder-only labels.
- Typography is consistent across all 3 new forms: heading
  text-[26px]/bold/leading-8/text-accent, subtitle text-sm/leading-5/
  text-text-subtle, matching SignInForm.
- Spacing follows the same form layout (max-w-[360px], gap-1.5 for form
  fields, mt-6 for submit, mt-4-5 for footer links) across all auth forms.
- Dark mode: no dark: classes in codebase, no theme toggle — not applicable.

================================================================================
CONTRAST RATIO MEASUREMENTS (full matrix)
================================================================================

All ratios computed with sRGB linearization per WCAG 2.x relative luminance
formula. "Normal" = 4.5:1, "Large" = 3.0:1.

  text.DEFAULT #121212 on surface #ffffff ............. 18.73:1  PASS/PASS
  text.subtle #565656 on surface #ffffff .............. 7.34:1   PASS/PASS
  text.muted #836f6f on surface #ffffff ............... 4.70:1   PASS/PASS
  text.helper #757575 on surface #ffffff .............. 4.61:1   PASS/PASS
  text.faint #a3a3a3 on surface #ffffff ............... 2.52:1   FAIL/FAIL
    → used only for DISABLED resend button (exempt) and SignInForm footer
  accent #452828 on surface #ffffff ................... 13.23:1  PASS/PASS
  status.danger #de350b on surface #ffffff ............ 4.55:1   PASS/PASS
  status.success #229e4b on surface #ffffff ........... 3.47:1   FAIL/PASS
    → P1-4: strength label at text-xs is normal text
  accent-subtle #f7f1e9 on accent #452828 ............. 11.79:1  PASS/PASS
  white on accent #452828 ............................. 13.23:1  PASS/PASS
  accent #452828 on accent-subtle #f7f1e9 ............. 11.79:1  PASS/PASS
  text.faint #a3a3a3 on accent-subtle #f7f1e9 ........ 2.25:1   FAIL/FAIL
    → P1-3: unmet policy rules + static "Not last 3" text
  status.success #229e4b on accent-subtle #f7f1e9 .... 3.09:1   FAIL/PASS
    → P1-5: met policy rules at text-[13px] = normal text
  white on status.success #229e4b (toast) ............. 3.47:1   FAIL/PASS
    → in shared Toast.tsx, not directly fixable from feature files
  white on status.danger #de350b (toast) .............. 4.55:1   PASS/PASS

================================================================================
AUTO-FIXES APPLIED
================================================================================

None — read-only access. All findings reported as-is.
Two findings (P1-1, P1-2) are mechanically auto-fixable (adding ARIA attributes)
but require write access. Three findings (P1-3, P1-4, P1-5) require design
judgment (color or font-size changes) and cannot be auto-fixed regardless.

================================================================================
SUMMARY
================================================================================

  P1 (blockers):      5  — all [accessibility], all unresolved
  P2 (should fix):    4  — 2 [design-system], 2 [accessibility]
  P3 (informational): 4  — 2 [design-system], 2 [accessibility]
  Auto-fixed:         0
  Gate status:        FAIL
