!! Baroclinic open-boundary kernels for the ocean dyn-core.
module rdb_ocean_obc_baroclinic
   !! Open-boundary baroclinic schemes for the ocean dyn-core.
   !!
   !!   * `ocean_obc_apply_baroclinic` — per-layer normal velocity at open
   !!     faces.  Two radiation schemes via `bc%radiation_scheme`:
   !!       0 (default, "anomaly"): Flather mean + zero-gradient baroclinic
   !!         anomaly (bit-identical to prior behaviour).
   !!       1 ("orlanski"): per-layer implicit-upwind radiation (Orlanski 1976)
   !!         with running-mean phase speed `rx` + optional inflow/outflow
   !!         nudging toward `clamped_u/v` (Marchesiello et al. 2001).
   !!     The continuity renorm at the wall still forces per-layer mass fluxes
   !!     to sum to the BT transport, so mass/eta consistency holds regardless
   !!     of scheme.  RESTART CAVEAT: `rx`/`u_prev` are NOT restart-registered;
   !!     a restart cold-starts the Orlanski scheme with rx = 0 / u_prev = 0.
   !!
   !!   * `ocean_obc_fill_ghosts` — fills h_layer and tracer hTr open-edge
   !!     ghosts (zero-gradient h, upwind-aware hTr), called before
   !!     `continuity_tracer_step_split`.
   !!
   !!   * `ocean_obc_update_reservoirs` — evolves per-edge reservoir
   !!     concentrations `tres` toward interior/external values via implicit
   !!     backward-Euler (Marchesiello et al. 2001).  Called after
   !!     `continuity_tracer_step_split` while mass_flux_* still hold the
   !!     stage's wall-face fluxes.  No-op unless `res_lscale_out/in` > 0;
   !!     when active, `ocean_obc_fill_ghosts` uses the reservoir value.
   !!
   !! GPU rules: explicit-shape dummies; do concurrent j-outer/i-inner;
   !! depth-mean via sequential k-loop in the DC body (nz small); no per-step
   !! allocation; per-tracer loops OUTSIDE the DC kernels (outer-shim); never
   !! deref bc%... inside a DC body (pass rx/u_prev/tres arrays as dummies).
   use rdb_constants, only: wp, H_VANISHED
   use rdb_grid, only: hgrid_t
   use rdb_multilayer_state, only: multilayer_state_t
   use rdb_barotropic_workstate, only: barotropic_workstate_t
   use rdb_ocean_boundary_types, only: ocean_bc_state_t, &
                                       OBC_WALL, OBC_OPEN, OBC_TIDAL, OBC_CHAPMAN, &
                                       OBC_NESTED, OBC_CLAMPED, OBC_PERIODIC
   implicit none
   private

   public :: ocean_obc_apply_baroclinic
   public :: ocean_obc_fill_ghosts
   public :: ocean_obc_refill_ghost_ssh
   public :: ocean_obc_update_reservoirs
   public :: ocean_obc_any_open_edge

   ! Minimum depth for outward-normal velocity computation in reservoir update.
   ! Matches the continuity h_min floor (1e-6 m).
   real(wp), parameter :: RES_H_MIN = 1.0e-6_wp

   ! Integer tags for the radiation scheme (mirrors bc%radiation_scheme).
   integer, parameter :: RAD_ANOMALY = 0  !! BT-mean + zero-gradient anomaly
   integer, parameter :: RAD_ORLANSKI = 1  !! Orlanski 1976 per-layer radiation

contains

   subroutine ocean_obc_apply_baroclinic(grid, bc, bt_work, ms, dt)
      !! Set per-layer normal velocity at open-ish faces after
      !! `apply_bt_correction`.  Scheme via `bc%radiation_scheme`:
      !!   RAD_ANOMALY (0, default): Flather mean + zero-gradient anomaly.
      !!   RAD_ORLANSKI (1): per-layer implicit-upwind radiation (Orlanski 1976),
      !!     running-mean phase speed `rx`; `u_prev` snapshot refreshed at END
      !!     so cold-start (u_prev=0) sees rx=0 for a quiescent IC.
      !! Optional nudging (Marchesiello et al. 2001) composes AFTER either
      !! scheme when the selected tau > 0, toward the edge `clamped_u/v`:
      !! tau_in when incoming (dhdt·dhdx ≤ 0 Orlanski; outward vel ≤ 0 anomaly),
      !! else tau_out.  CLAMPED: u_layer(wall) = clamped_u, all layers.
      !! No-op when no edge is open-ish.
      type(hgrid_t), intent(in) :: grid
      type(ocean_bc_state_t), intent(inout) :: bc
      type(barotropic_workstate_t), intent(in) :: bt_work
      type(multilayer_state_t), intent(inout) :: ms
      real(wp), intent(in) :: dt
         !! Outer baroclinic timestep (seconds).  Needed for nudging and Orlanski rx.

      integer :: bc_w, bc_e, bc_s, bc_n
      logical :: any_open
      integer :: i_w, i_e, j_s, j_n
      integer :: nz, nx, ny, nxt, nyt
      integer :: rad_scheme

      bc_w = bc%west%bc_type
      bc_e = bc%east%bc_type
      bc_s = bc%south%bc_type
      bc_n = bc%north%bc_type
      ! MPI-seam neutralisation (O0): a seam edge carries no physical BC.
      ! WALL is a no-op throughout this routine (verified: all dispatch is
      ! guarded by is_open_ish/is_radiating, neither of which includes
      ! OBC_WALL), so remapping the cached tag makes every per-edge block
      ! skip the seam.
      if (.not. bc%has_west) bc_w = OBC_WALL
      if (.not. bc%has_east) bc_e = OBC_WALL
      if (.not. bc%has_south) bc_s = OBC_WALL
      if (.not. bc%has_north) bc_n = OBC_WALL

      any_open = is_open_ish(bc_w) .or. is_open_ish(bc_e) .or. &
                 is_open_ish(bc_s) .or. is_open_ish(bc_n)
      if (.not. any_open) return

      nz = ms%nz_ml
      nxt = grid%nx_total
      nyt = grid%ny_total
      nx = grid%nx_phys
      ny = grid%ny_phys
      rad_scheme = bc%radiation_scheme

      ! Physical wall-face indices (u_face_x is nx_total+1 wide).
      i_w = grid%nghost + 1
      i_e = grid%nghost + nx + 1
      j_s = grid%nghost + 1
      j_n = grid%nghost + ny + 1

      if (rad_scheme == RAD_ORLANSKI) then
         ! ---- Orlanski per-layer radiation ----
         ! Zonal edges
         if (is_radiating(bc_w) .and. allocated(bc%rx_west)) then
            call apply_orlanski_west(ms%u_face_x_layer, &
                                     bc%rx_west, bc%u_prev_west, &
                                     nxt, nyt, nz, i_w, j_s, j_n, &
                                     bc%orlanski_rx_max, bc%orlanski_gamma, &
                                     bc%nudge_tau_in, bc%nudge_tau_out, &
                                     dt, bc%west%clamped_u)
         else if (bc_w == OBC_CLAMPED) then
            call apply_clamped_zonal_west(ms%u_face_x_layer, nxt, nyt, nz, i_w, j_s, j_n, &
                                          bc%west%clamped_u)
         end if
         if (is_radiating(bc_e) .and. allocated(bc%rx_east)) then
            call apply_orlanski_east(ms%u_face_x_layer, &
                                     bc%rx_east, bc%u_prev_east, &
                                     nxt, nyt, nz, i_e, j_s, j_n, &
                                     bc%orlanski_rx_max, bc%orlanski_gamma, &
                                     bc%nudge_tau_in, bc%nudge_tau_out, &
                                     dt, bc%east%clamped_u)
         else if (bc_e == OBC_CLAMPED) then
            call apply_clamped_zonal_east(ms%u_face_x_layer, nxt, nyt, nz, i_e, j_s, j_n, &
                                          bc%east%clamped_u)
         end if
         ! Meridional edges
         if (is_radiating(bc_s) .and. allocated(bc%rx_south)) then
            call apply_orlanski_south(ms%v_face_y_layer, &
                                      bc%rx_south, bc%u_prev_south, &
                                      nxt, nyt, nz, j_s, i_w, i_e - 1, &
                                      bc%orlanski_rx_max, bc%orlanski_gamma, &
                                      bc%nudge_tau_in, bc%nudge_tau_out, &
                                      dt, bc%south%clamped_v)
         else if (bc_s == OBC_CLAMPED) then
            call apply_clamped_meridional_south(ms%v_face_y_layer, nxt, nyt, nz, j_s, i_w, i_e - 1, &
                                                bc%south%clamped_v)
         end if
         if (is_radiating(bc_n) .and. allocated(bc%rx_north)) then
            call apply_orlanski_north(ms%v_face_y_layer, &
                                      bc%rx_north, bc%u_prev_north, &
                                      nxt, nyt, nz, j_n, i_w, i_e - 1, &
                                      bc%orlanski_rx_max, bc%orlanski_gamma, &
                                      bc%nudge_tau_in, bc%nudge_tau_out, &
                                      dt, bc%north%clamped_v)
         else if (bc_n == OBC_CLAMPED) then
            call apply_clamped_meridional_north(ms%v_face_y_layer, nxt, nyt, nz, j_n, i_w, i_e - 1, &
                                                bc%north%clamped_v)
         end if

         ! Refresh u_prev snapshots (first-interior-face normal velocity).
         ! Done AFTER all four edge updates so the current call's u_new is
         ! consistent with what the next call will see.
         if (allocated(bc%u_prev_west)) then
            call snapshot_u_prev_west(ms%u_face_x_layer, bc%u_prev_west, &
                                      nxt, nyt, nz, i_w, j_s, j_n)
         end if
         if (allocated(bc%u_prev_east)) then
            call snapshot_u_prev_east(ms%u_face_x_layer, bc%u_prev_east, &
                                      nxt, nyt, nz, i_e, j_s, j_n)
         end if
         if (allocated(bc%u_prev_south)) then
            call snapshot_v_prev_south(ms%v_face_y_layer, bc%u_prev_south, &
                                       nxt, nyt, nz, j_s, i_w, i_e - 1)
         end if
         if (allocated(bc%u_prev_north)) then
            call snapshot_v_prev_north(ms%v_face_y_layer, bc%u_prev_north, &
                                       nxt, nyt, nz, j_n, i_w, i_e - 1)
         end if

      else
         ! ---- Anomaly scheme (default) ----
         ! ---- Zonal open faces (west / east) ----
         if (is_open_ish(bc_w) .or. is_open_ish(bc_e)) then
            call apply_zonal_baroclinic(ms%u_face_x_layer, &
                                        ms%h_layer, &
                                        bt_work%bt_ubt_end, &
                                        nxt, nyt, nz, &
                                        i_w, i_e, j_s, j_n, &
                                        bc_w, bc_e, &
                                        bc%west%clamped_u, bc%east%clamped_u)
         end if

         ! ---- Meridional open faces (south / north) ----
         if (is_open_ish(bc_s) .or. is_open_ish(bc_n)) then
            call apply_meridional_baroclinic(ms%v_face_y_layer, &
                                             ms%h_layer, &
                                             bt_work%bt_vbt_end, &
                                             nxt, nyt, nz, &
                                             i_w, i_e - 1, j_s, j_n, &
                                             bc_s, bc_n, &
                                             bc%south%clamped_v, bc%north%clamped_v)
         end if

         ! ---- Anomaly-scheme nudging (Marchesiello et al. 2001) ----
         ! Applied when either tau > 0.  Sign convention for "incoming":
         ! anomaly scheme uses the outward velocity sign:
         !   west  inflow: u_wall(i_w) > 0  → incoming
         !   east  inflow: u_wall(i_e) < 0  → incoming
         !   south inflow: v_wall(j_s) > 0  → incoming
         !   north inflow: v_wall(j_n) < 0  → incoming
         if (bc%nudge_tau_in > 0.0_wp .or. bc%nudge_tau_out > 0.0_wp) then
            if (is_radiating(bc_w)) then
               call apply_nudge_zonal_west(ms%u_face_x_layer, &
                                           nxt, nyt, nz, i_w, j_s, j_n, &
                                           bc%nudge_tau_in, bc%nudge_tau_out, &
                                           dt, bc%west%clamped_u)
            end if
            if (is_radiating(bc_e)) then
               call apply_nudge_zonal_east(ms%u_face_x_layer, &
                                           nxt, nyt, nz, i_e, j_s, j_n, &
                                           bc%nudge_tau_in, bc%nudge_tau_out, &
                                           dt, bc%east%clamped_u)
            end if
            if (is_radiating(bc_s)) then
               call apply_nudge_meridional_south(ms%v_face_y_layer, &
                                                 nxt, nyt, nz, j_s, i_w, i_e - 1, &
                                                 bc%nudge_tau_in, bc%nudge_tau_out, &
                                                 dt, bc%south%clamped_v)
            end if
            if (is_radiating(bc_n)) then
               call apply_nudge_meridional_north(ms%v_face_y_layer, &
                                                 nxt, nyt, nz, j_n, i_w, i_e - 1, &
                                                 bc%nudge_tau_in, bc%nudge_tau_out, &
                                                 dt, bc%north%clamped_v)
            end if
         end if
      end if

      ! Zero-gradient fill of the per-layer VELOCITY ghosts incl. ghost×ghost
      ! corners: the corner-adjacent C-grid Coriolis/KE/vorticity stencil reads
      ! the diagonal corner ghost, so unfilled corner garbage blows up (open-edge
      ! NaN).  x-then-y; cross-axis range clipped to physical span unless the
      ! adjacent edge is also open, keeping WALL/PERIODIC ghosts untouched.
      call fill_uv_layer_ghosts(ms%u_face_x_layer, ms%v_face_y_layer, &
                                nxt, nyt, nz, grid%nghost, nx, ny, &
                                bc_w, bc_e, bc_s, bc_n)
   end subroutine ocean_obc_apply_baroclinic

   pure subroutine fill_uv_layer_ghosts(u_layer, v_layer, &
                                        nx_total, ny_total, nz, nghost, &
                                        nx_phys, ny_phys, bc_w, bc_e, bc_s, bc_n)
      !! Zero-gradient fill of per-layer face velocities into the OPEN-edge
      !! ghost region, corners included.  u_layer (nx_total+1, ny_total, nz);
      !! v_layer (nx_total, ny_total+1, nz).  x-pass over the full cross extent;
      !! y-pass clipped to the physical span unless the adjacent x-edge is open.
      !! Gated on open-ish tags ⇒ WALL/PERIODIC ⇒ no DC ⇒ bit-identical.
      integer, intent(in) :: nx_total, ny_total, nz, nghost, nx_phys, ny_phys
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
      integer, intent(in) :: bc_w, bc_e, bc_s, bc_n
      integer :: i, j, k, g, ng
      integer :: iu_lo, iu_hi, iv_lo, iv_hi
      logical :: fw, fe, fs, fn

      ng = nghost
      ! Fill set = SAME as the barotropic-substep η-ghost fill (OPEN/TIDAL/
      ! CHAPMAN/CLAMPED), deliberately NOT `is_open_ish` (excludes NESTED, which
      ! the C-grid barotropic treats as a wall) so velocity and η fills stay
      ! consistent and don't re-trip the corner NaN.
      fw = open_ghost_fill_edge(bc_w)
      fe = open_ghost_fill_edge(bc_e)
      fs = open_ghost_fill_edge(bc_s)
      fn = open_ghost_fill_edge(bc_n)

      ! ---- x-pass: west / east, over the full j extent (covers corners) ----
      if (fw) then
         do concurrent(k=1:nz, j=1:ny_total, g=1:ng)
            u_layer(g, j, k) = u_layer(ng + 1, j, k)             ! u faces 1..ng <- wall face ng+1
         end do
         do concurrent(k=1:nz, j=1:ny_total + 1, g=1:ng)
            v_layer(g, j, k) = v_layer(ng + 1, j, k)             ! v cols 1..ng <- first phys col
         end do
      end if
      if (fe) then
         do concurrent(k=1:nz, j=1:ny_total, g=1:ng)
            u_layer(ng + nx_phys + 1 + g, j, k) = u_layer(ng + nx_phys + 1, j, k)  ! east ghost faces
         end do
         do concurrent(k=1:nz, j=1:ny_total + 1, g=1:ng)
            v_layer(ng + nx_phys + g, j, k) = v_layer(ng + nx_phys, j, k)          ! east ghost cols
         end do
      end if

      ! ---- y-pass: south / north, over the corner-safe i extent ----
      ! Extend the y-pass into an x-ghost column for any NON-PERIODIC x-edge so a
      ! south/north-open edge fills its ghost CORNER even against a WALL side.  A
      ! PERIODIC x-edge is excluded — it owns its ghost via the wrap (preserves
      ! periodic+open mixed configs, e.g. Eady).
      iu_lo = ng + 1                 ! u face i-range (physical span by default)
      iu_hi = ng + nx_phys + 1
      iv_lo = ng + 1                 ! v cell i-range (physical span by default)
      iv_hi = ng + nx_phys
      if (bc_w /= OBC_PERIODIC) then
         iu_lo = 1
         iv_lo = 1
      end if
      if (bc_e /= OBC_PERIODIC) then
         iu_hi = nx_total + 1
         iv_hi = nx_total
      end if
      if (fs) then
         do concurrent(k=1:nz, g=1:ng, i=iu_lo:iu_hi)
            u_layer(i, g, k) = u_layer(i, ng + 1, k)            ! u south ghost rows <- first phys row
         end do
         do concurrent(k=1:nz, g=1:ng, i=iv_lo:iv_hi)
            v_layer(i, g, k) = v_layer(i, ng + 1, k)            ! v south ghost faces <- wall face
         end do
      end if
      if (fn) then
         do concurrent(k=1:nz, g=1:ng, i=iu_lo:iu_hi)
            u_layer(i, ng + ny_phys + g, k) = u_layer(i, ng + ny_phys, k)          ! u north ghost rows
         end do
         do concurrent(k=1:nz, g=1:ng, i=iv_lo:iv_hi)
            v_layer(i, ng + ny_phys + 1 + g, k) = v_layer(i, ng + ny_phys + 1, k)  ! v north ghost faces
         end do
      end if
   end subroutine fill_uv_layer_ghosts

   pure logical function open_ghost_fill_edge(bc_type) result(res)
      !! Edge types that receive the open zero-gradient ghost fill (velocity
      !! here, η in the barotropic substep — kept identical on purpose).
      !! OPEN / TIDAL / CHAPMAN / CLAMPED; NOT NESTED (barotropic-wall path).
      integer, intent(in) :: bc_type
      res = (bc_type == OBC_OPEN .or. bc_type == OBC_TIDAL .or. &
             bc_type == OBC_CHAPMAN .or. bc_type == OBC_CLAMPED)
   end function open_ghost_fill_edge

   pure logical function ocean_obc_any_open_edge(bc) result(res)
      !! True when ANY edge TAG of the domain needs the baroclinic open-edge
      !! treatment.  Reads the global tags only (never `has_*`), so every
      !! rank gets the same answer: callers use it to gate a COLLECTIVE seam
      !! exchange after `ocean_obc_apply_baroclinic`.
      type(ocean_bc_state_t), intent(in) :: bc
      res = is_open_ish(bc%west%bc_type) .or. is_open_ish(bc%east%bc_type) .or. &
            is_open_ish(bc%south%bc_type) .or. is_open_ish(bc%north%bc_type)
   end function ocean_obc_any_open_edge

   pure logical function is_open_ish(bc_type) result(res)
      !! Returns .true. for edge types that need baroclinic velocity treatment.
      integer, intent(in) :: bc_type
      res = (bc_type == OBC_OPEN .or. bc_type == OBC_TIDAL .or. &
             bc_type == OBC_CHAPMAN .or. bc_type == OBC_NESTED .or. &
             bc_type == OBC_CLAMPED)
   end function is_open_ish

   pure logical function is_radiating(bc_type) result(res)
      !! Returns .true. for Flather-class edges (not CLAMPED).
      integer, intent(in) :: bc_type
      res = (bc_type == OBC_OPEN .or. bc_type == OBC_TIDAL .or. &
             bc_type == OBC_CHAPMAN .or. bc_type == OBC_NESTED)
   end function is_radiating

   ! -----------------------------------------------------------------
   ! Orlanski per-layer radiation kernels (Orlanski 1976).
   ! Per edge: 1st/2nd interior faces are stepped inward from the wall;
   !   dhdt = u_prev - u_new (1st int), dhdx = u(1st int) - u(2nd int) (toward
   !   the boundary).  rx_raw = min(dhdt/dhdx, rx_max) when dhdt·dhdx > 0
   !   (outgoing), else 0 (incoming, no radiation).
   ! Nudging (Marchesiello et al. 2001): tau = tau_in when dhdt·dhdx <= 0
   !   (incoming) else tau_out; g2 = dt/(tau+dt) (skip tau<=0);
   !   u_b = (1-g2)*u_b + g2*u_data.
   ! -----------------------------------------------------------------

   pure subroutine apply_orlanski_west(u_layer, &
                                       rx, u_prev, &
                                       nx_total, ny_total, nz, &
                                       i_w, j0, j1, &
                                       rx_max, gamma_u, &
                                       tau_in, tau_out, dt, u_data)
      !! Orlanski radiation for the west open edge (Orlanski 1976).
      !! Interior face index I = i_w+1 (1st), I-1 = i_w+2 (2nd).
      !! Outward normal = -x.  dhdx = u(i_w+1) - u(i_w+2) (westward gradient).
      !! rx updated in-place; u_prev NOT updated here (done by the caller
      !! after all edges are set).
      integer, intent(in) :: nx_total, ny_total, nz
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(inout) :: rx(ny_total, nz)
      real(wp), intent(in)    :: u_prev(ny_total, nz)
      integer, intent(in) :: i_w, j0, j1
      real(wp), intent(in) :: rx_max, gamma_u, tau_in, tau_out, dt, u_data

      integer  :: j, k
      real(wp) :: u_int_1
      real(wp) :: dhdt_jk, dhdx_jk, rx_raw, rx_new, u_b
      real(wp) :: tau, g2

      do concurrent(j=j0:j1) &
         local(k, u_int_1, dhdt_jk, dhdx_jk, rx_raw, rx_new, u_b, tau, g2)

         do k = 1, nz
            u_int_1 = u_layer(i_w + 1, j, k)  ! first interior face velocity

            ! West edge: outward = -x; dhdx = u(i_w+1) - u(i_w+2).
            dhdt_jk = u_prev(j, k) - u_int_1
            dhdx_jk = u_int_1 - u_layer(i_w + 2, j, k)

            if (dhdt_jk*dhdx_jk > 0.0_wp) then
               rx_raw = min(dhdt_jk/dhdx_jk, rx_max)
            else
               rx_raw = 0.0_wp
            end if

            ! Running-mean update (gamma_u = 1 = instant, no memory).
            rx_new = (1.0_wp - gamma_u)*rx(j, k) + gamma_u*rx_raw
            rx(j, k) = rx_new

            ! Semi-implicit boundary update: u_b = (u_b_old + rx*u_int_1)/(1+rx).
            ! Anchors on the wall face's OWN per-layer value, so rx=0 leaves the
            ! baroclinic structure untouched (anchoring on BT velocity would
            ! collapse the boundary shear every quiet phase).
            u_b = (u_layer(i_w, j, k) + rx_new*u_int_1)/(1.0_wp + rx_new)

            ! Optional nudging: "incoming" = dhdt*dhdx <= 0 ⟹ tau_in.
            if (dhdt_jk*dhdx_jk <= 0.0_wp) then
               tau = tau_in
            else
               tau = tau_out
            end if
            if (tau > 0.0_wp) then
               g2 = dt/(tau + dt)
               u_b = (1.0_wp - g2)*u_b + g2*u_data
            end if

            u_layer(i_w, j, k) = u_b
         end do
      end do
   end subroutine apply_orlanski_west

   pure subroutine apply_orlanski_east(u_layer, &
                                       rx, u_prev, &
                                       nx_total, ny_total, nz, &
                                       i_e, j0, j1, &
                                       rx_max, gamma_u, &
                                       tau_in, tau_out, dt, u_data)
      !! Orlanski radiation for the east open edge (Orlanski 1976).
      !! Interior face index I = i_e-1 (1st), I-1 = i_e-2 (2nd).
      !! Outward normal = +x.  dhdx = u(i_e-1) - u(i_e-2) (eastward gradient).
      integer, intent(in) :: nx_total, ny_total, nz
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(inout) :: rx(ny_total, nz)
      real(wp), intent(in)    :: u_prev(ny_total, nz)
      integer, intent(in) :: i_e, j0, j1
      real(wp), intent(in) :: rx_max, gamma_u, tau_in, tau_out, dt, u_data

      integer  :: j, k
      real(wp) :: u_int_1
      real(wp) :: dhdt_jk, dhdx_jk, rx_raw, rx_new, u_b
      real(wp) :: tau, g2

      do concurrent(j=j0:j1) &
         local(k, u_int_1, dhdt_jk, dhdx_jk, rx_raw, rx_new, u_b, tau, g2)

         do k = 1, nz
            u_int_1 = u_layer(i_e - 1, j, k)

            ! East edge: outward = +x; dhdx = u(i_e-1) - u(i_e-2).
            dhdt_jk = u_prev(j, k) - u_int_1
            dhdx_jk = u_int_1 - u_layer(i_e - 2, j, k)

            if (dhdt_jk*dhdx_jk > 0.0_wp) then
               rx_raw = min(dhdt_jk/dhdx_jk, rx_max)
            else
               rx_raw = 0.0_wp
            end if

            rx_new = (1.0_wp - gamma_u)*rx(j, k) + gamma_u*rx_raw
            rx(j, k) = rx_new

            ! Per-layer anchor — see the west kernel's comment.
            u_b = (u_layer(i_e, j, k) + rx_new*u_int_1)/(1.0_wp + rx_new)

            if (dhdt_jk*dhdx_jk <= 0.0_wp) then
               tau = tau_in
            else
               tau = tau_out
            end if
            if (tau > 0.0_wp) then
               g2 = dt/(tau + dt)
               u_b = (1.0_wp - g2)*u_b + g2*u_data
            end if

            u_layer(i_e, j, k) = u_b
         end do
      end do
   end subroutine apply_orlanski_east

   pure subroutine apply_orlanski_south(v_layer, &
                                        rx, v_prev, &
                                        nx_total, ny_total, nz, &
                                        j_s, i0, i1, &
                                        rx_max, gamma_u, &
                                        tau_in, tau_out, dt, v_data)
      !! Orlanski radiation for the south open edge (Orlanski 1976).
      !! Interior face J = j_s+1 (1st), J-1 = j_s+2 (2nd).
      !! Outward normal = -y.  dhdx = v(j_s+1) - v(j_s+2) (southward gradient).
      integer, intent(in) :: nx_total, ny_total, nz
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(inout) :: rx(nx_total, nz)
      real(wp), intent(in)    :: v_prev(nx_total, nz)
      integer, intent(in) :: j_s, i0, i1
      real(wp), intent(in) :: rx_max, gamma_u, tau_in, tau_out, dt, v_data

      integer  :: i, k
      real(wp) :: v_int_1
      real(wp) :: dhdt_ik, dhdx_ik, rx_raw, rx_new, v_b
      real(wp) :: tau, g2

      do concurrent(i=i0:i1) &
         local(k, v_int_1, dhdt_ik, dhdx_ik, rx_raw, rx_new, v_b, tau, g2)

         do k = 1, nz
            v_int_1 = v_layer(i, j_s + 1, k)

            ! South edge: outward = -y; dhdx = v(j_s+1) - v(j_s+2).
            dhdt_ik = v_prev(i, k) - v_int_1
            dhdx_ik = v_int_1 - v_layer(i, j_s + 2, k)

            if (dhdt_ik*dhdx_ik > 0.0_wp) then
               rx_raw = min(dhdt_ik/dhdx_ik, rx_max)
            else
               rx_raw = 0.0_wp
            end if

            rx_new = (1.0_wp - gamma_u)*rx(i, k) + gamma_u*rx_raw
            rx(i, k) = rx_new

            ! Per-layer anchor — see the west kernel's comment.
            v_b = (v_layer(i, j_s, k) + rx_new*v_int_1)/(1.0_wp + rx_new)

            if (dhdt_ik*dhdx_ik <= 0.0_wp) then
               tau = tau_in
            else
               tau = tau_out
            end if
            if (tau > 0.0_wp) then
               g2 = dt/(tau + dt)
               v_b = (1.0_wp - g2)*v_b + g2*v_data
            end if

            v_layer(i, j_s, k) = v_b
         end do
      end do
   end subroutine apply_orlanski_south

   pure subroutine apply_orlanski_north(v_layer, &
                                        rx, v_prev, &
                                        nx_total, ny_total, nz, &
                                        j_n, i0, i1, &
                                        rx_max, gamma_u, &
                                        tau_in, tau_out, dt, v_data)
      !! Orlanski radiation for the north open edge (Orlanski 1976).
      !! Interior face J = j_n-1 (1st), J-1 = j_n-2 (2nd).
      !! Outward normal = +y.  dhdx = v(j_n-1) - v(j_n-2) (northward gradient).
      integer, intent(in) :: nx_total, ny_total, nz
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(inout) :: rx(nx_total, nz)
      real(wp), intent(in)    :: v_prev(nx_total, nz)
      integer, intent(in) :: j_n, i0, i1
      real(wp), intent(in) :: rx_max, gamma_u, tau_in, tau_out, dt, v_data

      integer  :: i, k
      real(wp) :: v_int_1
      real(wp) :: dhdt_ik, dhdx_ik, rx_raw, rx_new, v_b
      real(wp) :: tau, g2

      do concurrent(i=i0:i1) &
         local(k, v_int_1, dhdt_ik, dhdx_ik, rx_raw, rx_new, v_b, tau, g2)

         do k = 1, nz
            v_int_1 = v_layer(i, j_n - 1, k)

            ! North edge: outward = +y; dhdx = v(j_n-1) - v(j_n-2).
            dhdt_ik = v_prev(i, k) - v_int_1
            dhdx_ik = v_int_1 - v_layer(i, j_n - 2, k)

            if (dhdt_ik*dhdx_ik > 0.0_wp) then
               rx_raw = min(dhdt_ik/dhdx_ik, rx_max)
            else
               rx_raw = 0.0_wp
            end if

            rx_new = (1.0_wp - gamma_u)*rx(i, k) + gamma_u*rx_raw
            rx(i, k) = rx_new

            ! Per-layer anchor — see the west kernel's comment.
            v_b = (v_layer(i, j_n, k) + rx_new*v_int_1)/(1.0_wp + rx_new)

            if (dhdt_ik*dhdx_ik <= 0.0_wp) then
               tau = tau_in
            else
               tau = tau_out
            end if
            if (tau > 0.0_wp) then
               g2 = dt/(tau + dt)
               v_b = (1.0_wp - g2)*v_b + g2*v_data
            end if

            v_layer(i, j_n, k) = v_b
         end do
      end do
   end subroutine apply_orlanski_north

   ! -----------------------------------------------------------------
   ! CLAMPED helpers for the Orlanski dispatch path (simple wrappers)
   ! -----------------------------------------------------------------

   pure subroutine apply_clamped_zonal_west(u_layer, nx_total, ny_total, nz, &
                                            i_w, j0, j1, clamp_u)
      integer, intent(in) :: nx_total, ny_total, nz, i_w, j0, j1
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(in) :: clamp_u
      integer :: j, k
      do concurrent(k=1:nz, j=j0:j1)
         u_layer(i_w, j, k) = clamp_u
      end do
   end subroutine apply_clamped_zonal_west

   pure subroutine apply_clamped_zonal_east(u_layer, nx_total, ny_total, nz, &
                                            i_e, j0, j1, clamp_u)
      integer, intent(in) :: nx_total, ny_total, nz, i_e, j0, j1
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(in) :: clamp_u
      integer :: j, k
      do concurrent(k=1:nz, j=j0:j1)
         u_layer(i_e, j, k) = clamp_u
      end do
   end subroutine apply_clamped_zonal_east

   pure subroutine apply_clamped_meridional_south(v_layer, nx_total, ny_total, nz, &
                                                  j_s, i0, i1, clamp_v)
      integer, intent(in) :: nx_total, ny_total, nz, j_s, i0, i1
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(in) :: clamp_v
      integer :: i, k
      do concurrent(k=1:nz, i=i0:i1)
         v_layer(i, j_s, k) = clamp_v
      end do
   end subroutine apply_clamped_meridional_south

   pure subroutine apply_clamped_meridional_north(v_layer, nx_total, ny_total, nz, &
                                                  j_n, i0, i1, clamp_v)
      integer, intent(in) :: nx_total, ny_total, nz, j_n, i0, i1
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(in) :: clamp_v
      integer :: i, k
      do concurrent(k=1:nz, i=i0:i1)
         v_layer(i, j_n, k) = clamp_v
      end do
   end subroutine apply_clamped_meridional_north

   ! -----------------------------------------------------------------
   ! u_prev snapshot helpers (copy first-interior-face velocity)
   ! -----------------------------------------------------------------

   pure subroutine snapshot_u_prev_west(u_layer, u_prev, nx_total, ny_total, nz, &
                                        i_w, j0, j1)
      !! Snapshot u at the first interior face (i_w+1) into u_prev_west.
      integer, intent(in) :: nx_total, ny_total, nz, i_w, j0, j1
      real(wp), intent(in)    :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(inout) :: u_prev(ny_total, nz)
      integer :: j, k
      do concurrent(k=1:nz, j=j0:j1)
         u_prev(j, k) = u_layer(i_w + 1, j, k)
      end do
   end subroutine snapshot_u_prev_west

   pure subroutine snapshot_u_prev_east(u_layer, u_prev, nx_total, ny_total, nz, &
                                        i_e, j0, j1)
      !! Snapshot u at the first interior face (i_e-1) into u_prev_east.
      integer, intent(in) :: nx_total, ny_total, nz, i_e, j0, j1
      real(wp), intent(in)    :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(inout) :: u_prev(ny_total, nz)
      integer :: j, k
      do concurrent(k=1:nz, j=j0:j1)
         u_prev(j, k) = u_layer(i_e - 1, j, k)
      end do
   end subroutine snapshot_u_prev_east

   pure subroutine snapshot_v_prev_south(v_layer, v_prev, nx_total, ny_total, nz, &
                                         j_s, i0, i1)
      !! Snapshot v at the first interior face (j_s+1) into u_prev_south.
      integer, intent(in) :: nx_total, ny_total, nz, j_s, i0, i1
      real(wp), intent(in)    :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(inout) :: v_prev(nx_total, nz)
      integer :: i, k
      do concurrent(k=1:nz, i=i0:i1)
         v_prev(i, k) = v_layer(i, j_s + 1, k)
      end do
   end subroutine snapshot_v_prev_south

   pure subroutine snapshot_v_prev_north(v_layer, v_prev, nx_total, ny_total, nz, &
                                         j_n, i0, i1)
      !! Snapshot v at the first interior face (j_n-1) into u_prev_north.
      integer, intent(in) :: nx_total, ny_total, nz, j_n, i0, i1
      real(wp), intent(in)    :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(inout) :: v_prev(nx_total, nz)
      integer :: i, k
      do concurrent(k=1:nz, i=i0:i1)
         v_prev(i, k) = v_layer(i, j_n - 1, k)
      end do
   end subroutine snapshot_v_prev_north

   ! -----------------------------------------------------------------
   ! Nudging-only helpers for the anomaly-scheme path
   ! -----------------------------------------------------------------

   pure subroutine apply_nudge_zonal_west(u_layer, nx_total, ny_total, nz, &
                                          i_w, j0, j1, tau_in, tau_out, dt, u_data)
      !! Post-anomaly nudging for the west edge.
      !! "Incoming" at west = u_wall > 0 (eastward, into domain) ⟹ tau_in.
      integer, intent(in) :: nx_total, ny_total, nz, i_w, j0, j1
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(in) :: tau_in, tau_out, dt, u_data
      integer :: j, k
      real(wp) :: tau, g2, u_b
      do concurrent(j=j0:j1, k=1:nz) local(tau, g2, u_b)
         if (u_layer(i_w, j, k) > 0.0_wp) then
            tau = tau_in
         else
            tau = tau_out
         end if
         if (tau > 0.0_wp) then
            g2 = dt/(tau + dt)
            u_b = (1.0_wp - g2)*u_layer(i_w, j, k) + g2*u_data
            u_layer(i_w, j, k) = u_b
         end if
      end do
   end subroutine apply_nudge_zonal_west

   pure subroutine apply_nudge_zonal_east(u_layer, nx_total, ny_total, nz, &
                                          i_e, j0, j1, tau_in, tau_out, dt, u_data)
      !! Post-anomaly nudging for the east edge.
      !! "Incoming" at east = u_wall < 0 (westward, into domain) ⟹ tau_in.
      integer, intent(in) :: nx_total, ny_total, nz, i_e, j0, j1
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
      real(wp), intent(in) :: tau_in, tau_out, dt, u_data
      integer :: j, k
      real(wp) :: tau, g2, u_b
      do concurrent(j=j0:j1, k=1:nz) local(tau, g2, u_b)
         if (u_layer(i_e, j, k) < 0.0_wp) then
            tau = tau_in
         else
            tau = tau_out
         end if
         if (tau > 0.0_wp) then
            g2 = dt/(tau + dt)
            u_b = (1.0_wp - g2)*u_layer(i_e, j, k) + g2*u_data
            u_layer(i_e, j, k) = u_b
         end if
      end do
   end subroutine apply_nudge_zonal_east

   pure subroutine apply_nudge_meridional_south(v_layer, nx_total, ny_total, nz, &
                                                j_s, i0, i1, tau_in, tau_out, dt, v_data)
      !! Post-anomaly nudging for the south edge.
      !! "Incoming" at south = v_wall > 0 ⟹ tau_in.
      integer, intent(in) :: nx_total, ny_total, nz, j_s, i0, i1
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(in) :: tau_in, tau_out, dt, v_data
      integer :: i, k
      real(wp) :: tau, g2, v_b
      do concurrent(i=i0:i1, k=1:nz) local(tau, g2, v_b)
         if (v_layer(i, j_s, k) > 0.0_wp) then
            tau = tau_in
         else
            tau = tau_out
         end if
         if (tau > 0.0_wp) then
            g2 = dt/(tau + dt)
            v_b = (1.0_wp - g2)*v_layer(i, j_s, k) + g2*v_data
            v_layer(i, j_s, k) = v_b
         end if
      end do
   end subroutine apply_nudge_meridional_south

   pure subroutine apply_nudge_meridional_north(v_layer, nx_total, ny_total, nz, &
                                                j_n, i0, i1, tau_in, tau_out, dt, v_data)
      !! Post-anomaly nudging for the north edge.
      !! "Incoming" at north = v_wall < 0 ⟹ tau_in.
      integer, intent(in) :: nx_total, ny_total, nz, j_n, i0, i1
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
      real(wp), intent(in) :: tau_in, tau_out, dt, v_data
      integer :: i, k
      real(wp) :: tau, g2, v_b
      do concurrent(i=i0:i1, k=1:nz) local(tau, g2, v_b)
         if (v_layer(i, j_n, k) < 0.0_wp) then
            tau = tau_in
         else
            tau = tau_out
         end if
         if (tau > 0.0_wp) then
            g2 = dt/(tau + dt)
            v_b = (1.0_wp - g2)*v_layer(i, j_n, k) + g2*v_data
            v_layer(i, j_n, k) = v_b
         end if
      end do
   end subroutine apply_nudge_meridional_north

   pure subroutine apply_zonal_baroclinic(u_layer, h_layer, ubt_end, &
                                          nx_total, ny_total, nz, &
                                          i_w, i_e, j0, j1, &
                                          bc_w, bc_e, clamp_u_w, clamp_u_e)
      !! Set west and east wall-face per-layer u to
      !! Flather mean + zero-gradient baroclinic anomaly (radiating edges)
      !! or uniform clamped_u (CLAMPED edge).
      !! Explicit-shape dummies; one do concurrent per edge (j outer, k inner).
      integer, intent(in) :: nx_total, ny_total, nz
      real(wp), intent(inout) :: u_layer(nx_total + 1, ny_total, nz)
         !! u_face_x_layer, shape (nx_total+1, ny_total, nz).
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: ubt_end(nx_total + 1, ny_total)
         !! bt_ubt_end, shape (nx_total+1, ny_total).
      integer, intent(in) :: i_w, i_e   !! west / east physical wall-face indices
      integer, intent(in) :: j0, j1     !! first / last physical j-cell
      integer, intent(in) :: bc_w, bc_e
      real(wp), intent(in) :: clamp_u_w, clamp_u_e

      integer  :: j, k
      real(wp) :: h_tot, u_bar, u_int
      integer  :: i_int_w, i_int_e  ! first interior face index (one step inside)

      ! Interior face = one face step inward from the wall face.
      ! For west wall at i_w: interior face is i_w+1 (reads h_layer cell i_w).
      ! For east wall at i_e: interior face is i_e-1 (reads h_layer cell i_e-1).
      i_int_w = i_w + 1
      i_int_e = i_e - 1

      ! ---- West wall ----
      if (bc_w == OBC_CLAMPED) then
         do concurrent(k=1:nz, j=j0:j1)
            u_layer(i_w, j, k) = clamp_u_w
         end do
      else if (is_radiating(bc_w)) then
         do concurrent(j=j0:j1) local(k, h_tot, u_bar, u_int)
            ! Sequential k-loop for depth mean (nz is small, no reduction needed).
            h_tot = 0.0_wp
            u_bar = 0.0_wp
            do k = 1, nz
               ! Face i_int_w is between cell i_w and cell i_w+1.
               ! Use the upstream cell (i_w) thickness for the wall side.
               h_tot = h_tot + h_layer(i_w, j, k)
               u_bar = u_bar + u_layer(i_int_w, j, k)*h_layer(i_w, j, k)
            end do
            if (h_tot > 0.0_wp) then
               u_bar = u_bar/h_tot
            else
               u_bar = 0.0_wp
            end if
            do k = 1, nz
               u_int = u_layer(i_int_w, j, k)
               u_layer(i_w, j, k) = ubt_end(i_w, j) + (u_int - u_bar)
            end do
         end do
      end if

      ! ---- East wall ----
      if (bc_e == OBC_CLAMPED) then
         do concurrent(k=1:nz, j=j0:j1)
            u_layer(i_e, j, k) = clamp_u_e
         end do
      else if (is_radiating(bc_e)) then
         do concurrent(j=j0:j1) local(k, h_tot, u_bar, u_int)
            h_tot = 0.0_wp
            u_bar = 0.0_wp
            do k = 1, nz
               ! Use cell (i_e-1) thickness — the last interior cell.
               h_tot = h_tot + h_layer(i_e - 1, j, k)
               u_bar = u_bar + u_layer(i_int_e, j, k)*h_layer(i_e - 1, j, k)
            end do
            if (h_tot > 0.0_wp) then
               u_bar = u_bar/h_tot
            else
               u_bar = 0.0_wp
            end if
            do k = 1, nz
               u_int = u_layer(i_int_e, j, k)
               u_layer(i_e, j, k) = ubt_end(i_e, j) + (u_int - u_bar)
            end do
         end do
      end if
   end subroutine apply_zonal_baroclinic

   pure subroutine apply_meridional_baroclinic(v_layer, h_layer, vbt_end, &
                                               nx_total, ny_total, nz, &
                                               i0, i1, j_s, j_n, &
                                               bc_s, bc_n, clamp_v_s, clamp_v_n)
      !! Set south and north wall-face per-layer v.  Mirror of
      !! `apply_zonal_baroclinic` for the y-direction.
      integer, intent(in) :: nx_total, ny_total, nz
      real(wp), intent(inout) :: v_layer(nx_total, ny_total + 1, nz)
         !! v_face_y_layer, shape (nx_total, ny_total+1, nz).
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: vbt_end(nx_total, ny_total + 1)
      integer, intent(in) :: i0, i1     !! first / last physical i-cell
      integer, intent(in) :: j_s, j_n   !! south / north physical wall-face indices
      integer, intent(in) :: bc_s, bc_n
      real(wp), intent(in) :: clamp_v_s, clamp_v_n

      integer  :: i, k
      real(wp) :: h_tot, v_bar, v_int
      integer  :: j_int_s, j_int_n

      j_int_s = j_s + 1
      j_int_n = j_n - 1

      ! ---- South wall ----
      if (bc_s == OBC_CLAMPED) then
         do concurrent(k=1:nz, i=i0:i1)
            v_layer(i, j_s, k) = clamp_v_s
         end do
      else if (is_radiating(bc_s)) then
         do concurrent(i=i0:i1) local(k, h_tot, v_bar, v_int)
            h_tot = 0.0_wp
            v_bar = 0.0_wp
            do k = 1, nz
               h_tot = h_tot + h_layer(i, j_s, k)
               v_bar = v_bar + v_layer(i, j_int_s, k)*h_layer(i, j_s, k)
            end do
            if (h_tot > 0.0_wp) then
               v_bar = v_bar/h_tot
            else
               v_bar = 0.0_wp
            end if
            do k = 1, nz
               v_int = v_layer(i, j_int_s, k)
               v_layer(i, j_s, k) = vbt_end(i, j_s) + (v_int - v_bar)
            end do
         end do
      end if

      ! ---- North wall ----
      if (bc_n == OBC_CLAMPED) then
         do concurrent(k=1:nz, i=i0:i1)
            v_layer(i, j_n, k) = clamp_v_n
         end do
      else if (is_radiating(bc_n)) then
         do concurrent(i=i0:i1) local(k, h_tot, v_bar, v_int)
            h_tot = 0.0_wp
            v_bar = 0.0_wp
            do k = 1, nz
               h_tot = h_tot + h_layer(i, j_n - 1, k)
               v_bar = v_bar + v_layer(i, j_int_n, k)*h_layer(i, j_n - 1, k)
            end do
            if (h_tot > 0.0_wp) then
               v_bar = v_bar/h_tot
            else
               v_bar = 0.0_wp
            end if
            do k = 1, nz
               v_int = v_layer(i, j_int_n, k)
               v_layer(i, j_n, k) = vbt_end(i, j_n) + (v_int - v_bar)
            end do
         end do
      end if
   end subroutine apply_meridional_baroclinic

   ! Ghost fills at open edges (h_layer + tracer hTr)

   subroutine ocean_obc_fill_ghosts(grid, bc, ms)
      !! Fill h_layer and tracer hTr ghosts at open-ish edges.
      !! h_layer: zero-gradient (copy adjacent interior column).
      !! Tracer hTr per (j,k): outflow ⇒ ghost := interior (zero-gradient);
      !! inflow ⇒ ghost hTr := clamped_tracer(it) * h_ghost.  Inflow criterion
      !! uses the per-layer wall-face velocity sign (outward-normal convention):
      !! west inflow u_wall>0, east u_wall<0, south v_wall>0, north v_wall<0.
      !! No-op when no edge is open-ish.  Per-tracer loop outside the DCs.
      type(hgrid_t), intent(in) :: grid
      type(ocean_bc_state_t), intent(in) :: bc
      type(multilayer_state_t), intent(inout) :: ms

      integer :: bc_w, bc_e, bc_s, bc_n
      logical :: any_open
      integer :: it, nz, nxt, nyt, nx, ny
      integer :: i_w, i_e, j_s, j_n

      bc_w = bc%west%bc_type
      bc_e = bc%east%bc_type
      bc_s = bc%south%bc_type
      bc_n = bc%north%bc_type
      ! MPI-seam neutralisation (O0): a seam edge carries no physical BC.
      ! WALL is a no-op throughout this routine (verified: all dispatch is
      ! guarded by is_open_ish, which excludes OBC_WALL), so remapping the
      ! cached tag makes every per-edge block skip the seam.
      if (.not. bc%has_west) bc_w = OBC_WALL
      if (.not. bc%has_east) bc_e = OBC_WALL
      if (.not. bc%has_south) bc_s = OBC_WALL
      if (.not. bc%has_north) bc_n = OBC_WALL

      any_open = is_open_ish(bc_w) .or. is_open_ish(bc_e) .or. &
                 is_open_ish(bc_s) .or. is_open_ish(bc_n)
      if (.not. any_open) return

      nz = ms%nz_ml
      nxt = grid%nx_total
      nyt = grid%ny_total
      nx = grid%nx_phys
      ny = grid%ny_phys
      i_w = grid%nghost + 1          ! west physical wall-face / first interior cell
      i_e = grid%nghost + nx         ! last interior cell (east)
      j_s = grid%nghost + 1          ! south physical wall-face / first interior cell
      j_n = grid%nghost + ny         ! last interior cell (north)

      ! ---- h_layer ghosts: zero-gradient ----
      call fill_h_layer_ghosts(ms%h_layer, nxt, nyt, nz, &
                               i_w, i_e, j_s, j_n, grid%nghost, &
                               bc_w, bc_e, bc_s, bc_n)

      ! ---- Tracer hTr ghosts: upwind-aware or reservoir-based ----
      ! Reservoir active (tres_* allocated): unconditional hTr_ghost = tres*h_ghost
      ! (Marchesiello et al. 2001).  Absent (default): sign-switch path,
      ! bit-identical to prior behaviour.  Dispatch is per-edge-per-direction.
      if (allocated(ms%tracers)) then
         do it = 1, size(ms%tracers)
            if (.not. allocated(ms%tracers(it)%hTr)) cycle

            ! Zero-gradient CONCENTRATION pre-fill over the FULL open-edge ghost
            ! region (corners included): the per-edge upwind fills below only
            ! touch the physical cross-extent, so unfilled ghost×ghost corners
            ! would accumulate garbage under inflow and blow up T/S.  Upwind
            ! fills then overwrite the physical edges, leaving corners zero-grad.
            call fill_tracer_ghosts_zerograd(ms%tracers(it)%hTr, ms%h_layer, &
                                             nxt, nyt, nz, i_w, i_e, j_s, j_n, &
                                             grid%nghost, bc_w, bc_e, bc_s, bc_n)

            ! ---- West ghost fill ----
            if (is_open_ish(bc_w)) then
               if (allocated(bc%tres_west)) then
                  call fill_ghost_west_res( &
                     ms%tracers(it)%hTr, ms%h_layer, bc%tres_west, &
                     nxt, nyt, nz, i_w, j_s, j_n, grid%nghost, it, size(bc%tres_west, 3))
               else
                  call fill_ghost_west_sign(ms%tracers(it)%hTr, ms%h_layer, &
                                            ms%u_face_x_layer, &
                                            nxt, nyt, nz, i_w, j_s, j_n, grid%nghost, &
                                            get_clamped_tracer(bc%west%clamped_tracer, it))
               end if
            end if

            ! ---- East ghost fill ----
            if (is_open_ish(bc_e)) then
               if (allocated(bc%tres_east)) then
                  call fill_ghost_east_res( &
                     ms%tracers(it)%hTr, ms%h_layer, bc%tres_east, &
                     nxt, nyt, nz, i_e, j_s, j_n, grid%nghost, it, size(bc%tres_east, 3))
               else
                  call fill_ghost_east_sign(ms%tracers(it)%hTr, ms%h_layer, &
                                            ms%u_face_x_layer, &
                                            nxt, nyt, nz, i_e, j_s, j_n, grid%nghost, &
                                            get_clamped_tracer(bc%east%clamped_tracer, it))
               end if
            end if

            ! ---- South ghost fill ----
            if (is_open_ish(bc_s)) then
               if (allocated(bc%tres_south)) then
                  call fill_ghost_south_res( &
                     ms%tracers(it)%hTr, ms%h_layer, bc%tres_south, &
                     nxt, nyt, nz, j_s, i_w, i_e, grid%nghost, it, size(bc%tres_south, 3))
               else
                  call fill_ghost_south_sign(ms%tracers(it)%hTr, ms%h_layer, &
                                             ms%v_face_y_layer, &
                                             nxt, nyt, nz, j_s, i_w, i_e, grid%nghost, &
                                             get_clamped_tracer(bc%south%clamped_tracer, it))
               end if
            end if

            ! ---- North ghost fill ----
            if (is_open_ish(bc_n)) then
               if (allocated(bc%tres_north)) then
                  call fill_ghost_north_res( &
                     ms%tracers(it)%hTr, ms%h_layer, bc%tres_north, &
                     nxt, nyt, nz, j_n, i_w, i_e, grid%nghost, it, size(bc%tres_north, 3))
               else
                  call fill_ghost_north_sign(ms%tracers(it)%hTr, ms%h_layer, &
                                             ms%v_face_y_layer, &
                                             nxt, nyt, nz, j_n, i_w, i_e, grid%nghost, &
                                             get_clamped_tracer(bc%north%clamped_tracer, it))
               end if
            end if

         end do
      end if
   end subroutine ocean_obc_fill_ghosts

   subroutine ocean_obc_refill_ghost_ssh(grid, bc, ms, bt_H_ref)
      !! Re-establish a zero-gradient free surface in the open-edge GHOST
      !! columns, called at the END of the outer step (after the ALE remap)
      !! so the diagnostic manager sees a consistent halo.
      !!
      !! Why this is needed: the ghost cells are updated by the slow
      !! continuity over the full array (`i = 1..nx_total`) using the
      !! zeroed array-edge fluxes, and the conservative ALE remap
      !! (`remap_h_ref = Σh − bt_eta`) PRESERVES that spurious ghost
      !! transport divergence.  Nothing re-imposes the open-boundary
      !! zero-gradient invariant between the remap and the diagnostic
      !! read, so `SSH = Σ_k h_layer − b` in the ghosts drifts to tens of
      !! metres (worst where the boundary bathymetry is steep and at the
      !! corners where two open edges meet), while the physical interior
      !! stays healthy.
      !!
      !! Fix: overwrite each open-edge ghost column with the nearest
      !! interior column SCALED so the ghost column total equals
      !! `bt_H_ref(ghost) + η_interior`, i.e. the free-surface anomaly is
      !! flat across the open boundary.  Since `bt_H_ref == b`, this makes
      !! `SSH_ghost = η_interior` for ANY bathymetry — including formula
      !! topographies whose ghost `b` differs from the first interior cell.
      !! For file bathymetry (ghost `b` constant-extrapolated) the scale
      !! collapses to 1, i.e. a plain zero-gradient re-copy that simply
      !! discards the accumulated ghost drift.
      !!
      !! Physics-neutral: the next outer step's stage-1 `ocean_obc_fill_ghosts`
      !! re-fills the ghost h_layer (thickness copy) BEFORE any physics
      !! kernel reads it, so this pass only affects what the diagnostics
      !! (and other halo consumers) observe — the prognostic trajectory is
      !! bit-identical.  Gated on open-ish edges ⇒ WALL/PERIODIC ⇒ no-op.
      type(hgrid_t), intent(in) :: grid
      type(ocean_bc_state_t), intent(in) :: bc
      type(multilayer_state_t), intent(inout) :: ms
      real(wp), intent(in) :: bt_H_ref(grid%nx_total, grid%ny_total)
         !! Mode-split reference column depth (== seeded bathymetry `b`).

      integer :: bc_w, bc_e, bc_s, bc_n
      logical :: any_open
      integer :: it, nz, nxt, nyt, nx, ny
      integer :: i_w, i_e, j_s, j_n

      bc_w = bc%west%bc_type
      bc_e = bc%east%bc_type
      bc_s = bc%south%bc_type
      bc_n = bc%north%bc_type
      ! An MPI seam is not an open edge: its ghost columns are a neighbour's
      ! interior, filled by the halo exchange.  Overwriting them with the
      ! zero-gradient copy made every decomposed OBC run diverge from the
      ! serial one (same gate as `ocean_obc_fill_ghosts`).
      if (.not. bc%has_west) bc_w = OBC_WALL
      if (.not. bc%has_east) bc_e = OBC_WALL
      if (.not. bc%has_south) bc_s = OBC_WALL
      if (.not. bc%has_north) bc_n = OBC_WALL

      any_open = is_open_ish(bc_w) .or. is_open_ish(bc_e) .or. &
                 is_open_ish(bc_s) .or. is_open_ish(bc_n)
      if (.not. any_open) return
      if (.not. allocated(ms%h_layer)) return

      nz = ms%nz_ml
      nxt = grid%nx_total
      nyt = grid%ny_total
      nx = grid%nx_phys
      ny = grid%ny_phys
      i_w = grid%nghost + 1          ! west physical wall-face / first interior cell
      i_e = grid%nghost + nx         ! last interior cell (east)
      j_s = grid%nghost + 1          ! south physical wall-face / first interior cell
      j_n = grid%nghost + ny         ! last interior cell (north)

      ! ---- h_layer ghosts: zero-gradient free surface (scaled to ghost b) ----
      call refill_h_ghost_scaled(ms%h_layer, bt_H_ref, nxt, nyt, nz, &
                                 i_w, i_e, j_s, j_n, grid%nghost, &
                                 bc_w, bc_e, bc_s, bc_n)

      ! ---- Tracer hTr ghosts: zero-gradient concentration onto the new h ----
      if (allocated(ms%tracers)) then
         do it = 1, size(ms%tracers)
            if (.not. allocated(ms%tracers(it)%hTr)) cycle
            call fill_tracer_ghosts_zerograd(ms%tracers(it)%hTr, ms%h_layer, &
                                             nxt, nyt, nz, i_w, i_e, j_s, j_n, &
                                             grid%nghost, bc_w, bc_e, bc_s, bc_n)
         end do
      end if
   end subroutine ocean_obc_refill_ghost_ssh

   pure subroutine refill_h_ghost_scaled(h_layer, bt_H_ref, nx_total, ny_total, nz, &
                                         i_w, i_e, j_s, j_n, nghost, &
                                         bc_w, bc_e, bc_s, bc_n)
      !! Overwrite open-edge h_layer ghost columns with the nearest interior
      !! column scaled so `Σ_k h_ghost = bt_H_ref(ghost) + η_interior`
      !! (η_interior = `Σ_k h_int − bt_H_ref(int)`).  x-pass (west/east) over
      !! the full j extent (covers corner rows); y-pass (south/north) over the
      !! full i extent, reading the already x-filled corner columns — same
      !! corner-coverage order as `fill_h_layer_ghosts`.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: bt_H_ref(nx_total, ny_total)
      integer, intent(in) :: i_w, i_e, j_s, j_n
      integer, intent(in) :: bc_w, bc_e, bc_s, bc_n

      integer :: i, j, k, g
      real(wp) :: sh_int, eta_ref, sc

      ! West ghosts
      if (is_open_ish(bc_w)) then
         do concurrent(j=1:ny_total, g=1:nghost) local(k, sh_int, eta_ref, sc)
            sh_int = 0.0_wp
            do k = 1, nz
               sh_int = sh_int + h_layer(i_w, j, k)
            end do
            eta_ref = sh_int - bt_H_ref(i_w, j)
            sc = (bt_H_ref(g, j) + eta_ref)/max(sh_int, H_VANISHED)
            do k = 1, nz
               h_layer(g, j, k) = h_layer(i_w, j, k)*sc
            end do
         end do
      end if
      ! East ghosts
      if (is_open_ish(bc_e)) then
         do concurrent(j=1:ny_total, g=1:nghost) local(k, sh_int, eta_ref, sc)
            sh_int = 0.0_wp
            do k = 1, nz
               sh_int = sh_int + h_layer(i_e, j, k)
            end do
            eta_ref = sh_int - bt_H_ref(i_e, j)
            sc = (bt_H_ref(nx_total - g + 1, j) + eta_ref)/max(sh_int, H_VANISHED)
            do k = 1, nz
               h_layer(nx_total - g + 1, j, k) = h_layer(i_e, j, k)*sc
            end do
         end do
      end if
      ! South ghosts (read the x-filled corner columns)
      if (is_open_ish(bc_s)) then
         do concurrent(i=1:nx_total, g=1:nghost) local(k, sh_int, eta_ref, sc)
            sh_int = 0.0_wp
            do k = 1, nz
               sh_int = sh_int + h_layer(i, j_s, k)
            end do
            eta_ref = sh_int - bt_H_ref(i, j_s)
            sc = (bt_H_ref(i, g) + eta_ref)/max(sh_int, H_VANISHED)
            do k = 1, nz
               h_layer(i, g, k) = h_layer(i, j_s, k)*sc
            end do
         end do
      end if
      ! North ghosts (read the x-filled corner columns)
      if (is_open_ish(bc_n)) then
         do concurrent(i=1:nx_total, g=1:nghost) local(k, sh_int, eta_ref, sc)
            sh_int = 0.0_wp
            do k = 1, nz
               sh_int = sh_int + h_layer(i, j_n, k)
            end do
            eta_ref = sh_int - bt_H_ref(i, j_n)
            sc = (bt_H_ref(i, ny_total - g + 1) + eta_ref)/max(sh_int, H_VANISHED)
            do k = 1, nz
               h_layer(i, ny_total - g + 1, k) = h_layer(i, j_n, k)*sc
            end do
         end do
      end if
   end subroutine refill_h_ghost_scaled

   pure function get_clamped_tracer(arr, it) result(val)
      !! Safe accessor: return clamped_tracer(it) or 0 if unallocated/out of range.
      real(wp), allocatable, intent(in) :: arr(:)
      integer, intent(in) :: it
      real(wp) :: val
      val = 0.0_wp
      if (allocated(arr)) then
         if (it <= size(arr)) val = arr(it)
      end if
   end function get_clamped_tracer

   pure subroutine fill_tracer_ghosts_zerograd(hTr, h_layer, nx_total, ny_total, nz, &
                                               i_w, i_e, j_s, j_n, nghost, &
                                               bc_w, bc_e, bc_s, bc_n)
      !! Zero-gradient CONCENTRATION fill of a tracer's hTr ghosts at open-ish
      !! edges over the FULL cross-extent (ghost×ghost corners included):
      !! hTr_ghost = (hTr_int / h_int) * h_ghost.  Runs BEFORE the upwind-aware
      !! per-edge fill, so corners keep this zero-gradient value (no corner T/S
      !! blow-up under inflow).  Same gating/corner coverage as fill_h_layer_ghosts.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      integer, intent(in) :: i_w, i_e, j_s, j_n
      integer, intent(in) :: bc_w, bc_e, bc_s, bc_n
      integer :: i, j, k, g

      ! West / East over the full j extent (covers corner rows).
      if (is_open_ish(bc_w)) then
         do concurrent(k=1:nz, j=1:ny_total, g=1:nghost)
            hTr(g, j, k) = (hTr(i_w, j, k)/max(h_layer(i_w, j, k), H_VANISHED))*h_layer(g, j, k)
         end do
      end if
      if (is_open_ish(bc_e)) then
         do concurrent(k=1:nz, j=1:ny_total, g=1:nghost)
            hTr(nx_total - g + 1, j, k) = &
               (hTr(i_e, j, k)/max(h_layer(i_e, j, k), H_VANISHED))*h_layer(nx_total - g + 1, j, k)
         end do
      end if
      ! South / North over the full i extent (reads the x-filled corner cols).
      if (is_open_ish(bc_s)) then
         do concurrent(k=1:nz, j=1:nghost, i=1:nx_total)
            hTr(i, j, k) = (hTr(i, j_s, k)/max(h_layer(i, j_s, k), H_VANISHED))*h_layer(i, j, k)
         end do
      end if
      if (is_open_ish(bc_n)) then
         do concurrent(k=1:nz, g=1:nghost, i=1:nx_total)
            hTr(i, ny_total - g + 1, k) = &
               (hTr(i, j_n, k)/max(h_layer(i, j_n, k), H_VANISHED))*h_layer(i, ny_total - g + 1, k)
         end do
      end if
   end subroutine fill_tracer_ghosts_zerograd

   pure subroutine fill_h_layer_ghosts(h_layer, nx_total, ny_total, nz, &
                                       i_w, i_e, j_s, j_n, nghost, &
                                       bc_w, bc_e, bc_s, bc_n)
      !! Zero-gradient fill of h_layer ghosts at open-ish edges.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: h_layer(nx_total, ny_total, nz)
      integer, intent(in) :: i_w, i_e, j_s, j_n
      integer, intent(in) :: bc_w, bc_e, bc_s, bc_n

      integer :: i, j, k, g

      ! West ghosts: copy first interior column (i_w) into ghost columns
      if (is_open_ish(bc_w)) then
         do concurrent(k=1:nz, j=1:ny_total, g=1:nghost)
            h_layer(g, j, k) = h_layer(i_w, j, k)
         end do
      end if
      ! East ghosts
      if (is_open_ish(bc_e)) then
         do concurrent(k=1:nz, j=1:ny_total, g=1:nghost)
            h_layer(nx_total - g + 1, j, k) = h_layer(i_e, j, k)
         end do
      end if
      ! South ghosts
      if (is_open_ish(bc_s)) then
         do concurrent(k=1:nz, j=1:nghost, i=1:nx_total)
            h_layer(i, j, k) = h_layer(i, j_s, k)
         end do
      end if
      ! North ghosts
      if (is_open_ish(bc_n)) then
         do concurrent(k=1:nz, g=1:nghost, i=1:nx_total)
            h_layer(i, ny_total - g + 1, k) = h_layer(i, j_n, k)
         end do
      end if
   end subroutine fill_h_layer_ghosts

   pure subroutine fill_tracer_ghosts_zonal(hTr, h_layer, u_layer, &
                                            nx_total, ny_total, nz, &
                                            i_w, i_e, j_s, j_n, nghost, &
                                            bc_w, bc_e, &
                                            clamped_tr_w, clamped_tr_e)
      !! Upwind-aware tracer ghost fill for west and east open edges.
      !! West inflow  : u(i_w,j,k) > 0  → ghost = clamped_tr * h_ghost
      !! West outflow : u(i_w,j,k) <= 0 → ghost = interior (zero-gradient)
      !! East inflow  : u(i_e,j,k) < 0
      !! East outflow : u(i_e,j,k) >= 0
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: u_layer(nx_total + 1, ny_total, nz)
      integer, intent(in) :: i_w, i_e, j_s, j_n
      integer, intent(in) :: bc_w, bc_e
      real(wp), intent(in) :: clamped_tr_w, clamped_tr_e

      integer :: j, k, g
      real(wp) :: hTr_bc, hTr_int

      ! West ghosts
      if (is_open_ish(bc_w)) then
         do concurrent(k=1:nz, j=j_s:j_n, g=1:nghost) local(hTr_bc, hTr_int)
            ! Use wall-face velocity at u_layer(i_w, j, k).
            ! West inflow = u > 0 (eastward into domain).
            if (u_layer(i_w, j, k) > 0.0_wp) then
               ! Inflow: set ghost hTr = boundary concentration * ghost h
               hTr_bc = clamped_tr_w*h_layer(g, j, k)
               hTr(g, j, k) = hTr_bc
            else
               ! Outflow: zero-gradient — copy interior
               hTr_int = hTr(i_w, j, k)
               hTr(g, j, k) = hTr_int
            end if
         end do
      end if

      ! East ghosts
      if (is_open_ish(bc_e)) then
         do concurrent(k=1:nz, j=j_s:j_n, g=1:nghost) local(hTr_bc, hTr_int)
            ! East inflow = u < 0 (westward into domain from east).
            if (u_layer(i_e + 1, j, k) < 0.0_wp) then
               hTr_bc = clamped_tr_e*h_layer(nx_total - g + 1, j, k)
               hTr(nx_total - g + 1, j, k) = hTr_bc
            else
               hTr_int = hTr(i_e, j, k)
               hTr(nx_total - g + 1, j, k) = hTr_int
            end if
         end do
      end if
   end subroutine fill_tracer_ghosts_zonal

   pure subroutine fill_tracer_ghosts_meridional(hTr, h_layer, v_layer, &
                                                 nx_total, ny_total, nz, &
                                                 i_w, i_e, j_s, j_n, nghost, &
                                                 bc_s, bc_n, &
                                                 clamped_tr_s, clamped_tr_n)
      !! Upwind-aware tracer ghost fill for south and north open edges.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: v_layer(nx_total, ny_total + 1, nz)
      integer, intent(in) :: i_w, i_e, j_s, j_n
      integer, intent(in) :: bc_s, bc_n
      real(wp), intent(in) :: clamped_tr_s, clamped_tr_n

      integer :: i, k, g
      real(wp) :: hTr_bc, hTr_int

      ! South ghosts
      if (is_open_ish(bc_s)) then
         do concurrent(k=1:nz, i=i_w:i_e, g=1:nghost) local(hTr_bc, hTr_int)
            ! South inflow = v > 0 (northward into domain).
            if (v_layer(i, j_s, k) > 0.0_wp) then
               hTr_bc = clamped_tr_s*h_layer(i, g, k)
               hTr(i, g, k) = hTr_bc
            else
               hTr_int = hTr(i, j_s, k)
               hTr(i, g, k) = hTr_int
            end if
         end do
      end if

      ! North ghosts
      if (is_open_ish(bc_n)) then
         do concurrent(k=1:nz, i=i_w:i_e, g=1:nghost) local(hTr_bc, hTr_int)
            ! North inflow = v < 0 (southward into domain from north).
            if (v_layer(i, j_n + 1, k) < 0.0_wp) then
               hTr_bc = clamped_tr_n*h_layer(i, ny_total - g + 1, k)
               hTr(i, ny_total - g + 1, k) = hTr_bc
            else
               hTr_int = hTr(i, j_n, k)
               hTr(i, ny_total - g + 1, k) = hTr_int
            end if
         end do
      end if
   end subroutine fill_tracer_ghosts_meridional

   ! -----------------------------------------------------------------
   ! Per-edge helpers: reservoir path (unconditional concentration)
   ! -----------------------------------------------------------------

   pure subroutine fill_ghost_west_res(hTr, h_layer, tres_w, &
                                       nx_total, ny_total, nz, &
                                       i_w, j0, j1, nghost, it, n_tr)
      !! Reservoir-based ghost fill, west edge.
      !! hTr_ghost = tres_w(j, k, it) * h_ghost — unconditional.
      integer, intent(in) :: nx_total, ny_total, nz, nghost, it, n_tr
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: tres_w(ny_total, nz, n_tr)
      integer, intent(in) :: i_w, j0, j1
      integer :: j, k, g
      real(wp) :: hTr_bc
      do concurrent(k=1:nz, j=j0:j1, g=1:nghost) local(hTr_bc)
         hTr_bc = tres_w(j, k, it)*h_layer(g, j, k)
         hTr(g, j, k) = hTr_bc
      end do
   end subroutine fill_ghost_west_res

   pure subroutine fill_ghost_east_res(hTr, h_layer, tres_e, &
                                       nx_total, ny_total, nz, &
                                       i_e, j0, j1, nghost, it, n_tr)
      !! Reservoir-based ghost fill, east edge.
      integer, intent(in) :: nx_total, ny_total, nz, nghost, it, n_tr
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: tres_e(ny_total, nz, n_tr)
      integer, intent(in) :: i_e, j0, j1
      integer :: j, k, g
      real(wp) :: hTr_bc
      do concurrent(k=1:nz, j=j0:j1, g=1:nghost) local(hTr_bc)
         hTr_bc = tres_e(j, k, it)*h_layer(nx_total - g + 1, j, k)
         hTr(nx_total - g + 1, j, k) = hTr_bc
      end do
   end subroutine fill_ghost_east_res

   pure subroutine fill_ghost_south_res(hTr, h_layer, tres_s, &
                                        nx_total, ny_total, nz, &
                                        j_s, i0, i1, nghost, it, n_tr)
      !! Reservoir-based ghost fill, south edge.
      integer, intent(in) :: nx_total, ny_total, nz, nghost, it, n_tr
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: tres_s(nx_total, nz, n_tr)
      integer, intent(in) :: j_s, i0, i1
      integer :: i, k, g
      real(wp) :: hTr_bc
      do concurrent(k=1:nz, i=i0:i1, g=1:nghost) local(hTr_bc)
         hTr_bc = tres_s(i, k, it)*h_layer(i, g, k)
         hTr(i, g, k) = hTr_bc
      end do
   end subroutine fill_ghost_south_res

   pure subroutine fill_ghost_north_res(hTr, h_layer, tres_n, &
                                        nx_total, ny_total, nz, &
                                        j_n, i0, i1, nghost, it, n_tr)
      !! Reservoir-based ghost fill, north edge.
      integer, intent(in) :: nx_total, ny_total, nz, nghost, it, n_tr
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: tres_n(nx_total, nz, n_tr)
      integer, intent(in) :: j_n, i0, i1
      integer :: i, k, g
      real(wp) :: hTr_bc
      do concurrent(k=1:nz, i=i0:i1, g=1:nghost) local(hTr_bc)
         hTr_bc = tres_n(i, k, it)*h_layer(i, ny_total - g + 1, k)
         hTr(i, ny_total - g + 1, k) = hTr_bc
      end do
   end subroutine fill_ghost_north_res

   ! Per-edge helpers: sign-switch path (default fallback)

   pure subroutine fill_ghost_west_sign(hTr, h_layer, u_layer, &
                                        nx_total, ny_total, nz, &
                                        i_w, j0, j1, nghost, clamped_tr_w)
      !! Sign-switch ghost fill for the west edge.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: u_layer(nx_total + 1, ny_total, nz)
      integer, intent(in) :: i_w, j0, j1
      real(wp), intent(in) :: clamped_tr_w
      integer :: j, k, g
      real(wp) :: hTr_bc, hTr_int
      do concurrent(k=1:nz, j=j0:j1, g=1:nghost) local(hTr_bc, hTr_int)
         if (u_layer(i_w, j, k) > 0.0_wp) then
            hTr_bc = clamped_tr_w*h_layer(g, j, k)
            hTr(g, j, k) = hTr_bc
         else
            hTr_int = hTr(i_w, j, k)
            hTr(g, j, k) = hTr_int
         end if
      end do
   end subroutine fill_ghost_west_sign

   pure subroutine fill_ghost_east_sign(hTr, h_layer, u_layer, &
                                        nx_total, ny_total, nz, &
                                        i_e, j0, j1, nghost, clamped_tr_e)
      !! Sign-switch ghost fill for the east edge.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: u_layer(nx_total + 1, ny_total, nz)
      integer, intent(in) :: i_e, j0, j1
      real(wp), intent(in) :: clamped_tr_e
      integer :: j, k, g
      real(wp) :: hTr_bc, hTr_int
      do concurrent(k=1:nz, j=j0:j1, g=1:nghost) local(hTr_bc, hTr_int)
         if (u_layer(i_e + 1, j, k) < 0.0_wp) then
            hTr_bc = clamped_tr_e*h_layer(nx_total - g + 1, j, k)
            hTr(nx_total - g + 1, j, k) = hTr_bc
         else
            hTr_int = hTr(i_e, j, k)
            hTr(nx_total - g + 1, j, k) = hTr_int
         end if
      end do
   end subroutine fill_ghost_east_sign

   pure subroutine fill_ghost_south_sign(hTr, h_layer, v_layer, &
                                         nx_total, ny_total, nz, &
                                         j_s, i0, i1, nghost, clamped_tr_s)
      !! Sign-switch ghost fill for the south edge.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: v_layer(nx_total, ny_total + 1, nz)
      integer, intent(in) :: j_s, i0, i1
      real(wp), intent(in) :: clamped_tr_s
      integer :: i, k, g
      real(wp) :: hTr_bc, hTr_int
      do concurrent(k=1:nz, i=i0:i1, g=1:nghost) local(hTr_bc, hTr_int)
         if (v_layer(i, j_s, k) > 0.0_wp) then
            hTr_bc = clamped_tr_s*h_layer(i, g, k)
            hTr(i, g, k) = hTr_bc
         else
            hTr_int = hTr(i, j_s, k)
            hTr(i, g, k) = hTr_int
         end if
      end do
   end subroutine fill_ghost_south_sign

   pure subroutine fill_ghost_north_sign(hTr, h_layer, v_layer, &
                                         nx_total, ny_total, nz, &
                                         j_n, i0, i1, nghost, clamped_tr_n)
      !! Sign-switch ghost fill for the north edge.
      integer, intent(in) :: nx_total, ny_total, nz, nghost
      real(wp), intent(inout) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in)    :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in)    :: v_layer(nx_total, ny_total + 1, nz)
      integer, intent(in) :: j_n, i0, i1
      real(wp), intent(in) :: clamped_tr_n
      integer :: i, k, g
      real(wp) :: hTr_bc, hTr_int
      do concurrent(k=1:nz, i=i0:i1, g=1:nghost) local(hTr_bc, hTr_int)
         if (v_layer(i, j_n + 1, k) < 0.0_wp) then
            hTr_bc = clamped_tr_n*h_layer(i, ny_total - g + 1, k)
            hTr(i, ny_total - g + 1, k) = hTr_bc
         else
            hTr_int = hTr(i, j_n, k)
            hTr(i, ny_total - g + 1, k) = hTr_int
         end if
      end do
   end subroutine fill_ghost_north_sign

   ! Open-edge tracer reservoir update

   subroutine ocean_obc_update_reservoirs(grid, bc, ms, dt)
      !! Evolve per-edge reservoir concentrations `tres` one timestep.
      !! Called after `continuity_tracer_step_split` while
      !! `ms%mass_flux_{x,y}_layer` still hold the stage's wall-face fluxes.
      !! Per open-ish edge (with allocated `tres_*`), per (j|i, k, tracer):
      !!   u_n = sign_edge * mass_flux(wall) / max(h_int, h_min)  (outward normal)
      !! Implicit backward-Euler (Marchesiello et al. 2001):
      !!   c_out = max(0,u_n)*dt/L_out, c_in = max(0,-u_n)*dt/L_in  (0 if L==0)
      !!   tres = (tres + c_out*T_int + c_in*T_data)/(1 + c_out + c_in)
      !! Degenerate L_out==0 & u_n>0 ⇒ tres = T_int (L_in==0 & u_n<0 ⇒ T_data).
      !! T_int = hTr(int)/max(h(int),h_min); T_data = clamped_tracer(it).
      !! No-op when both length scales are zero (default path).
      type(hgrid_t), intent(in) :: grid
      type(ocean_bc_state_t), intent(inout) :: bc
      type(multilayer_state_t), intent(in) :: ms
      real(wp), intent(in) :: dt

      logical :: do_res
      integer :: nxt, nyt, nz, nx, ny
      integer :: i_w, i_e, j_s, j_n
      integer :: it, n_tr

      do_res = (bc%res_lscale_out > 0.0_wp .or. bc%res_lscale_in > 0.0_wp)
      if (.not. do_res) return

      nxt = grid%nx_total
      nyt = grid%ny_total
      nz = ms%nz_ml
      nx = grid%nx_phys
      ny = grid%ny_phys
      i_w = grid%nghost + 1
      i_e = grid%nghost + nx
      j_s = grid%nghost + 1
      j_n = grid%nghost + ny

      n_tr = bc%n_tracers
      if (n_tr <= 0) return
      if (.not. allocated(ms%tracers)) return

      ! Per-tracer outer shim: pass full explicit-shape arrays to kernels.
      do it = 1, n_tr
         if (.not. allocated(ms%tracers(it)%hTr)) cycle

         ! ---- West ----
         ! MPI-seam neutralisation (O0): dispatch here is on allocated(tres_*),
         ! not bc_type, so the remap trick cannot be used — explicit has_* guard.
         if (allocated(bc%tres_west) .and. bc%has_west) then
            call update_reservoir_zonal_west( &
               ms%tracers(it)%hTr, ms%h_layer, ms%mass_flux_x_layer, &
               bc%tres_west, &
               get_clamped_tracer(bc%west%clamped_tracer, it), &
               nxt, nyt, nz, i_w, j_s, j_n, &
               bc%res_lscale_out, bc%res_lscale_in, dt, it, n_tr)
         end if

         ! ---- East ----
         if (allocated(bc%tres_east) .and. bc%has_east) then
            call update_reservoir_zonal_east( &
               ms%tracers(it)%hTr, ms%h_layer, ms%mass_flux_x_layer, &
               bc%tres_east, &
               get_clamped_tracer(bc%east%clamped_tracer, it), &
               nxt, nyt, nz, i_e, j_s, j_n, &
               bc%res_lscale_out, bc%res_lscale_in, dt, it, n_tr)
         end if

         ! ---- South ----
         if (allocated(bc%tres_south) .and. bc%has_south) then
            call update_reservoir_meridional_south( &
               ms%tracers(it)%hTr, ms%h_layer, ms%mass_flux_y_layer, &
               bc%tres_south, &
               get_clamped_tracer(bc%south%clamped_tracer, it), &
               nxt, nyt, nz, j_s, i_w, i_e, &
               bc%res_lscale_out, bc%res_lscale_in, dt, it, n_tr)
         end if

         ! ---- North ----
         if (allocated(bc%tres_north) .and. bc%has_north) then
            call update_reservoir_meridional_north( &
               ms%tracers(it)%hTr, ms%h_layer, ms%mass_flux_y_layer, &
               bc%tres_north, &
               get_clamped_tracer(bc%north%clamped_tracer, it), &
               nxt, nyt, nz, j_n, i_w, i_e, &
               bc%res_lscale_out, bc%res_lscale_in, dt, it, n_tr)
         end if

      end do
   end subroutine ocean_obc_update_reservoirs

   pure subroutine update_reservoir_zonal_west(hTr, h_layer, mass_flux_x, &
                                               tres_w, T_data, &
                                               nx_total, ny_total, nz, &
                                               i_w, j0, j1, &
                                               L_out, L_in, dt, it, n_tr)
      !! Update reservoir for the west open edge.  Wall-face/interior cell = i_w.
      !! Outward normal = -x: u_n = -mass_flux_x(i_w,j,k)/max(h,hmin).
      integer, intent(in) :: nx_total, ny_total, nz, n_tr, it
      integer, intent(in) :: i_w, j0, j1
      real(wp), intent(in) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in) :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in) :: mass_flux_x(nx_total + 1, ny_total, nz)
      real(wp), intent(inout) :: tres_w(ny_total, nz, n_tr)
      real(wp), intent(in) :: T_data
      real(wp), intent(in) :: L_out, L_in, dt

      integer :: j, k
      real(wp) :: h_int, T_int, u_n, c_out, c_in, tres_old, denom

      do concurrent(j=j0:j1, k=1:nz) local(h_int, T_int, u_n, c_out, c_in, tres_old, denom)
         h_int = max(h_layer(i_w, j, k), RES_H_MIN)
         T_int = hTr(i_w, j, k)/h_int
         ! mass_flux_x(i_w,...) > 0 = eastward into domain (inflow at west).
         u_n = -mass_flux_x(i_w, j, k)/h_int
         tres_old = tres_w(j, k, it)
         ! Degenerate limits: L_out==0 & u_n>0 ⇒ T_int; L_in==0 & u_n<0 ⇒ T_data
         ! instantly.  Both L zero ⇒ implicit form is a no-op (tres unchanged).
         if (L_out == 0.0_wp .and. u_n > 0.0_wp) then
            tres_w(j, k, it) = T_int
         else if (L_in == 0.0_wp .and. u_n < 0.0_wp) then
            tres_w(j, k, it) = T_data
         else
            c_out = merge(max(0.0_wp, u_n)*dt/L_out, 0.0_wp, L_out > 0.0_wp)
            c_in = merge(max(0.0_wp, -u_n)*dt/L_in, 0.0_wp, L_in > 0.0_wp)
            denom = 1.0_wp + c_out + c_in
            tres_w(j, k, it) = (tres_old + c_out*T_int + c_in*T_data)/denom
         end if
      end do
   end subroutine update_reservoir_zonal_west

   pure subroutine update_reservoir_zonal_east(hTr, h_layer, mass_flux_x, &
                                               tres_e, T_data, &
                                               nx_total, ny_total, nz, &
                                               i_e, j0, j1, &
                                               L_out, L_in, dt, it, n_tr)
      !! Update reservoir for the east open edge.
      !! Wall-face index for mass flux = i_e+1  (east face of the last physical cell i_e).
      !! Interior cell = i_e.
      !! Outward normal at east = +x: u_n = +mass_flux_x(i_e+1,j,k)/max(h,hmin).
      integer, intent(in) :: nx_total, ny_total, nz, n_tr, it
      integer, intent(in) :: i_e, j0, j1
      real(wp), intent(in) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in) :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in) :: mass_flux_x(nx_total + 1, ny_total, nz)
      real(wp), intent(inout) :: tres_e(ny_total, nz, n_tr)
      real(wp), intent(in) :: T_data
      real(wp), intent(in) :: L_out, L_in, dt

      integer :: j, k
      real(wp) :: h_int, T_int, u_n, c_out, c_in, tres_old, denom

      do concurrent(j=j0:j1, k=1:nz) local(h_int, T_int, u_n, c_out, c_in, tres_old, denom)
         h_int = max(h_layer(i_e, j, k), RES_H_MIN)
         T_int = hTr(i_e, j, k)/h_int
         ! Outward normal at east = +x.
         u_n = mass_flux_x(i_e + 1, j, k)/h_int
         tres_old = tres_e(j, k, it)
         if (L_out == 0.0_wp .and. u_n > 0.0_wp) then
            tres_e(j, k, it) = T_int
         else if (L_in == 0.0_wp .and. u_n < 0.0_wp) then
            tres_e(j, k, it) = T_data
         else
            c_out = merge(max(0.0_wp, u_n)*dt/L_out, 0.0_wp, L_out > 0.0_wp)
            c_in = merge(max(0.0_wp, -u_n)*dt/L_in, 0.0_wp, L_in > 0.0_wp)
            denom = 1.0_wp + c_out + c_in
            tres_e(j, k, it) = (tres_old + c_out*T_int + c_in*T_data)/denom
         end if
      end do
   end subroutine update_reservoir_zonal_east

   pure subroutine update_reservoir_meridional_south(hTr, h_layer, mass_flux_y, &
                                                     tres_s, T_data, &
                                                     nx_total, ny_total, nz, &
                                                     j_s, i0, i1, &
                                                     L_out, L_in, dt, it, n_tr)
      !! Update reservoir for the south open edge.
      !! Wall-face index = j_s; interior cell = j_s.
      !! Outward normal = -y: u_n = -mass_flux_y(i,j_s,k)/max(h,hmin).
      integer, intent(in) :: nx_total, ny_total, nz, n_tr, it
      integer, intent(in) :: j_s, i0, i1
      real(wp), intent(in) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in) :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in) :: mass_flux_y(nx_total, ny_total + 1, nz)
      real(wp), intent(inout) :: tres_s(nx_total, nz, n_tr)
      real(wp), intent(in) :: T_data
      real(wp), intent(in) :: L_out, L_in, dt

      integer :: i, k
      real(wp) :: h_int, T_int, u_n, c_out, c_in, tres_old, denom

      do concurrent(i=i0:i1, k=1:nz) local(h_int, T_int, u_n, c_out, c_in, tres_old, denom)
         h_int = max(h_layer(i, j_s, k), RES_H_MIN)
         T_int = hTr(i, j_s, k)/h_int
         u_n = -mass_flux_y(i, j_s, k)/h_int
         tres_old = tres_s(i, k, it)
         if (L_out == 0.0_wp .and. u_n > 0.0_wp) then
            tres_s(i, k, it) = T_int
         else if (L_in == 0.0_wp .and. u_n < 0.0_wp) then
            tres_s(i, k, it) = T_data
         else
            c_out = merge(max(0.0_wp, u_n)*dt/L_out, 0.0_wp, L_out > 0.0_wp)
            c_in = merge(max(0.0_wp, -u_n)*dt/L_in, 0.0_wp, L_in > 0.0_wp)
            denom = 1.0_wp + c_out + c_in
            tres_s(i, k, it) = (tres_old + c_out*T_int + c_in*T_data)/denom
         end if
      end do
   end subroutine update_reservoir_meridional_south

   pure subroutine update_reservoir_meridional_north(hTr, h_layer, mass_flux_y, &
                                                     tres_n, T_data, &
                                                     nx_total, ny_total, nz, &
                                                     j_n, i0, i1, &
                                                     L_out, L_in, dt, it, n_tr)
      !! Update reservoir for the north open edge.
      !! Wall-face index = j_n+1; interior cell = j_n.
      !! Outward normal = +y: u_n = +mass_flux_y(i,j_n+1,k)/max(h,hmin).
      integer, intent(in) :: nx_total, ny_total, nz, n_tr, it
      integer, intent(in) :: j_n, i0, i1
      real(wp), intent(in) :: hTr(nx_total, ny_total, nz)
      real(wp), intent(in) :: h_layer(nx_total, ny_total, nz)
      real(wp), intent(in) :: mass_flux_y(nx_total, ny_total + 1, nz)
      real(wp), intent(inout) :: tres_n(nx_total, nz, n_tr)
      real(wp), intent(in) :: T_data
      real(wp), intent(in) :: L_out, L_in, dt

      integer :: i, k
      real(wp) :: h_int, T_int, u_n, c_out, c_in, tres_old, denom

      do concurrent(i=i0:i1, k=1:nz) local(h_int, T_int, u_n, c_out, c_in, tres_old, denom)
         h_int = max(h_layer(i, j_n, k), RES_H_MIN)
         T_int = hTr(i, j_n, k)/h_int
         u_n = mass_flux_y(i, j_n + 1, k)/h_int
         tres_old = tres_n(i, k, it)
         if (L_out == 0.0_wp .and. u_n > 0.0_wp) then
            tres_n(i, k, it) = T_int
         else if (L_in == 0.0_wp .and. u_n < 0.0_wp) then
            tres_n(i, k, it) = T_data
         else
            c_out = merge(max(0.0_wp, u_n)*dt/L_out, 0.0_wp, L_out > 0.0_wp)
            c_in = merge(max(0.0_wp, -u_n)*dt/L_in, 0.0_wp, L_in > 0.0_wp)
            denom = 1.0_wp + c_out + c_in
            tres_n(i, k, it) = (tres_old + c_out*T_int + c_in*T_data)/denom
         end if
      end do
   end subroutine update_reservoir_meridional_north

end module rdb_ocean_obc_baroclinic
